diff --git a/bun.lockb b/bun.lockb index 0b44cbc..c4db671 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/client/auto-imports.d.ts b/packages/client/auto-imports.d.ts index 5af6981..ff4db06 100644 --- a/packages/client/auto-imports.d.ts +++ b/packages/client/auto-imports.d.ts @@ -91,6 +91,7 @@ declare global { const useModel: typeof import('vue')['useModel'] const useRoute: typeof import('vue-router')['useRoute'] const useRouter: typeof import('vue-router')['useRouter'] + const useScroll: typeof import('./src/composables/useScroll/index')['useScroll'] const useSeoMeta: typeof import('@unhead/vue')['useSeoMeta'] const useServerHead: typeof import('@unhead/vue')['useServerHead'] const useServerHeadSafe: typeof import('@unhead/vue')['useServerHeadSafe'] @@ -206,6 +207,7 @@ declare module 'vue' { readonly useModel: UnwrapRef readonly useRoute: UnwrapRef readonly useRouter: UnwrapRef + readonly useScroll: UnwrapRef readonly useSeoMeta: UnwrapRef readonly useServerHead: UnwrapRef readonly useServerHeadSafe: UnwrapRef diff --git a/packages/client/components.d.ts b/packages/client/components.d.ts index 613e14f..34cb5de 100644 --- a/packages/client/components.d.ts +++ b/packages/client/components.d.ts @@ -9,10 +9,13 @@ export {} declare module 'vue' { export interface GlobalComponents { AiDemo: typeof import('./src/components/AiDemo/index.vue')['default'] + ChatBox: typeof import('./src/components/ChatBox/index.vue')['default'] ClientOnly: typeof import('./../../internal/x/components/ClientOnly.vue')['default'] CookieDemo: typeof import('./src/components/CookieDemo.vue')['default'] DataFetch: typeof import('./src/components/DataFetch.vue')['default'] HelloWorld: typeof import('./src/components/HelloWorld.vue')['default'] + Msg: typeof import('./src/components/ChatBox/_/Msg.vue')['default'] + Node: typeof import('./src/components/ChatBox/_/Node.vue')['default'] QuillEditor: typeof import('./src/components/QuillEditor/index.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] diff --git a/packages/client/index.html b/packages/client/index.html index 789957a..16d0a38 100644 --- a/packages/client/index.html +++ b/packages/client/index.html @@ -4,6 +4,63 @@ +
+
+ + +
diff --git a/packages/client/package.json b/packages/client/package.json index aae169d..d98baf8 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -8,6 +8,7 @@ "check": "vue-tsc" }, "devDependencies": { + "@types/jsdom": "^27.0.0", "sass-embedded": "^1.93.2", "unplugin-vue-components": "^29.1.0", "vue-tsc": "^3.1.0" @@ -20,6 +21,7 @@ "ant-design-x-vue": "^1.3.2", "dompurify": "^3.2.7", "htmlparser2": "^10.0.0", + "jsdom": "^27.0.0", "marked": "^16.3.0", "maz-ui": "^4.1.6", "ofetch": "^1.4.1", diff --git a/packages/client/src/assets/styles/scss/_global.scss b/packages/client/src/assets/styles/scss/_global.scss index 5b4dac0..fe31c38 100644 --- a/packages/client/src/assets/styles/scss/_global.scss +++ b/packages/client/src/assets/styles/scss/_global.scss @@ -1,26 +1,11 @@ -/* - theme-helpers.scss - 为主题开发提供的 SCSS 帮助函数 / mixin / 示例 - 说明(中文注释): - - 提供把 SCSS map 转换为 CSS 自定义属性(variables)的 mixin - - 提供生成主题(light/dark)选择器的 mixin 示例 - - 提供读取 CSS 变量的辅助函数 `css-var()` - - 提供颜色可读性判断函数 `readable-text()`(基于简单亮度公式) - - 提供颜色微调辅助 `tone()` - - 使用方式:在你的主样式中导入此文件并调用 mixin/map。示例在文件底部有注释。 -*/ - -// ----------------------------- -// CSS 变量相关帮助 -// ----------------------------- +@use "sass:string"; // 返回一个 var(...) 字符串,方便在 SCSS 中使用 CSS 变量 @function css-var($name, $fallback: null) { @if $fallback == null { - @return unquote("var(--#{$name})"); + @return string.unquote("var(--#{$name})"); } @else { - @return unquote("var(--#{$name}, #{$fallback})"); + @return string.unquote("var(--#{$name}, #{$fallback})"); } } @@ -41,166 +26,3 @@ @include declare-theme-variables($map); } } - - -// ----------------------------- -// 颜色工具函数 -// ----------------------------- - -// 计算近似亮度(0-255)用于对比判定(基于 ITU BT.601 近似) -@function _luma($color) { - // 期望 $color 为 color 类型 - $r: red($color); - $g: green($color); - $b: blue($color); - @return ($r * 0.299) + ($g * 0.587) + ($b * 0.114); -} - -// 根据背景色返回可读的文字颜色(#000 或 #fff) -// 示例: color: readable-text(#0d1117); -@function readable-text($bg, $light: #ffffff, $dark: #000000) { - // 如果传入的不是 color 类型,尝试转换(如果是变量字符串则无法计算) - @if type-of($bg) != 'color' { - // 无法在构建时计算 CSS 变量的对比,默认返回白色以便在暗色环境下可读 - @return $light; - } - @if _luma($bg) > 186 { - @return $dark; - } - @return $light; -} - -// 基于 lighten/darken 的简单色调微调函数(正值变亮,负值变暗) -@function tone($color, $percent) { - @if type-of($color) != 'color' { - @warn "tone(): first argument is not a color; returned value will be unchanged."; - @return $color; - } - @if $percent == 0 { - @return $color; - } - @if $percent > 0 { - @return lighten($color, $percent); - } @else { - @return darken($color, abs($percent)); - } -} - -// 使颜色变浅的辅助函数 -// 用法: -// lighten-by(#0d1117, 20) -> 以 20% 变亮 -// lighten-by(#0d1117, 20%) -> 以 20% 变亮 -// lighten-by(#0d1117, 0.2) -> 以 20% 变亮(小数形式) -@function lighten-by($color, $amount) { - @if type-of($color) != 'color' { - @warn "lighten-by(): first argument is not a color; returned value will be unchanged."; - @return $color; - } - @if type-of($amount) != 'number' { - @warn "lighten-by(): amount must be a number (e.g. 20, 20% or 0.2). Returning original color."; - @return $color; - } - - // 规范化为百分比单位(Sass 的 percent 类型) - $pct: $amount; - @if unit($amount) != '%' { - // 无单位数字:如果在 (0,1] 范围内,视为小数比例;否则当作百分比数值 - @if $amount > 0 and $amount <= 1 { - $pct: $amount * 100%; - } @else { - $pct: $amount * 1%; - } - } - - @return lighten($color, $pct); -} - -// ----------------------------- -// 常用组件/场景 mixin -// ----------------------------- - -// 简单的背景/文字组合,接收背景颜色或变量名 -// 用法:@include bg-fg('color-canvas-default'); // 传入变量名 -// @include bg-fg(#0d1117); // 传入 color 类型 -@mixin bg-fg($bg, $fg: null) { - @if type-of($bg) == 'string' { - // 假定传入的是变量名,使用 css-var - background: css-var($bg); - @if $fg == null { - // 无法静态计算对比,留空或用户自行指定 - color: inherit; - } else { - color: css-var($fg); - } - } @else if type-of($bg) == 'color' { - background: $bg; - @if $fg == null { - color: readable-text($bg); - } @else if type-of($fg) == 'color' { - color: $fg; - } @else { - color: css-var($fg); - } - } @else { - @warn "bg-fg(): unsupported bg type"; - } -} - -// 一个可重用的按钮样式 mixin,支持传入变量名或颜色 -// 用法: -// .btn { @include theme-button('color-accent-emphasis'); } -@mixin theme-button($bg, $fg: null, $radius: 6px, $pad-y: 8px, $pad-x: 12px) { - display: inline-flex; - align-items: center; - justify-content: center; - padding: $pad-y $pad-x; - border-radius: $radius; - border: none; - cursor: pointer; - @if type-of($bg) == 'string' { - background: css-var($bg); - @if $fg == null { color: css-var('color-fg-default'); } @else { color: css-var($fg); } - } @else if type-of($bg) == 'color' { - background: $bg; - @if $fg == null { color: readable-text($bg); } @else if type-of($fg) == 'color' { color: $fg; } @else { color: css-var($fg); } - } - // 微交互 - &:hover { filter: brightness(0.95); } - &:active { transform: translateY(1px); } -} - - -// ----------------------------- -// 示例(注释掉,直接拷贝到你的样式里使用) -// ----------------------------- - -// 示例主题 maps:键名与全局 CSS 变量中的命名保持一致(但不包含前缀 --) -// $theme-light: ( -// 'color-fg-default': #24292f, -// 'color-fg-muted': #57606a, -// 'color-canvas-default': #ffffff, -// 'color-border-default': #d0d7de, -// 'color-accent-fg': #0969da, -// ); -// -// $theme-dark: ( -// 'color-fg-default': #c9d1d9, -// 'color-fg-muted': #8b949e, -// 'color-canvas-default': #0d1117, -// 'color-border-default': #30363d, -// 'color-accent-fg': #58a6ff, -// ); -// -// 生成到 :root 和手动切换器: -// @include generate-theme(':root', $theme-light); -// @include generate-theme(':root[data-theme="dark"]', $theme-dark); -// -// 使用 CSS 变量: -// .markdown-body { color: css-var('color-fg-default'); background: css-var('color-canvas-default'); } -// -// 使用 mixin 快速为按钮应用主题颜色: -// .btn { @include theme-button('color-accent-emphasis'); } - -// ----------------------------- -// 结束 -// ----------------------------- diff --git a/packages/client/src/assets/styles/scss/_theme-helpers.scss b/packages/client/src/assets/styles/scss/_theme-helpers.scss deleted file mode 100644 index 19e3dcf..0000000 --- a/packages/client/src/assets/styles/scss/_theme-helpers.scss +++ /dev/null @@ -1,190 +0,0 @@ - -// 返回一个 var(...) 字符串,方便在 SCSS 中使用 CSS 变量 -@function css-var($name, $fallback: null) { - @if $fallback == null { - @return unquote("var(--#{$name})"); - } @else { - @return unquote("var(--#{$name}, #{$fallback})"); - } -} - -// 将一个 map 转换为 CSS 变量声明,需在选择器块内使用 -// 用法: -// :root { @include declare-theme-variables($my-theme-map); } -@mixin declare-theme-variables($map) { - @each $token, $val in $map { - // 允许传入颜色、字符串或数字 - --#{$token}: #{$val}; - } -} - -// 生成主题选择器(selector 可以是 ":root"、":root[data-theme=\"dark\"]" 或 ".theme-dark") -// 用法:@include generate-theme(':root', $theme-light); -@mixin generate-theme($selector, $map) { - #{$selector} { - @include declare-theme-variables($map); - } -} - - -// ----------------------------- -// 颜色工具函数 -// ----------------------------- - -// 计算近似亮度(0-255)用于对比判定(基于 ITU BT.601 近似) -@function _luma($color) { - // 期望 $color 为 color 类型 - $r: red($color); - $g: green($color); - $b: blue($color); - @return ($r * 0.299) + ($g * 0.587) + ($b * 0.114); -} - -// 根据背景色返回可读的文字颜色(#000 或 #fff) -// 示例: color: readable-text(#0d1117); -@function readable-text($bg, $light: #ffffff, $dark: #000000) { - // 如果传入的不是 color 类型,尝试转换(如果是变量字符串则无法计算) - @if type-of($bg) != 'color' { - // 无法在构建时计算 CSS 变量的对比,默认返回白色以便在暗色环境下可读 - @return $light; - } - @if _luma($bg) > 186 { - @return $dark; - } - @return $light; -} - -// 基于 lighten/darken 的简单色调微调函数(正值变亮,负值变暗) -@function tone($color, $percent) { - @if type-of($color) != 'color' { - @warn "tone(): first argument is not a color; returned value will be unchanged."; - @return $color; - } - @if $percent == 0 { - @return $color; - } - @if $percent > 0 { - @return lighten($color, $percent); - } @else { - @return darken($color, abs($percent)); - } -} - -// 使颜色变浅的辅助函数 -// 用法: -// lighten-by(#0d1117, 20) -> 以 20% 变亮 -// lighten-by(#0d1117, 20%) -> 以 20% 变亮 -// lighten-by(#0d1117, 0.2) -> 以 20% 变亮(小数形式) -@function lighten-by($color, $amount) { - @if type-of($color) != 'color' { - @warn "lighten-by(): first argument is not a color; returned value will be unchanged."; - @return $color; - } - @if type-of($amount) != 'number' { - @warn "lighten-by(): amount must be a number (e.g. 20, 20% or 0.2). Returning original color."; - @return $color; - } - - // 规范化为百分比单位(Sass 的 percent 类型) - $pct: $amount; - @if unit($amount) != '%' { - // 无单位数字:如果在 (0,1] 范围内,视为小数比例;否则当作百分比数值 - @if $amount > 0 and $amount <= 1 { - $pct: $amount * 100%; - } @else { - $pct: $amount * 1%; - } - } - - @return lighten($color, $pct); -} - -// ----------------------------- -// 常用组件/场景 mixin -// ----------------------------- - -// 简单的背景/文字组合,接收背景颜色或变量名 -// 用法:@include bg-fg('color-canvas-default'); // 传入变量名 -// @include bg-fg(#0d1117); // 传入 color 类型 -@mixin bg-fg($bg, $fg: null) { - @if type-of($bg) == 'string' { - // 假定传入的是变量名,使用 css-var - background: css-var($bg); - @if $fg == null { - // 无法静态计算对比,留空或用户自行指定 - color: inherit; - } else { - color: css-var($fg); - } - } @else if type-of($bg) == 'color' { - background: $bg; - @if $fg == null { - color: readable-text($bg); - } @else if type-of($fg) == 'color' { - color: $fg; - } @else { - color: css-var($fg); - } - } @else { - @warn "bg-fg(): unsupported bg type"; - } -} - -// 一个可重用的按钮样式 mixin,支持传入变量名或颜色 -// 用法: -// .btn { @include theme-button('color-accent-emphasis'); } -@mixin theme-button($bg, $fg: null, $radius: 6px, $pad-y: 8px, $pad-x: 12px) { - display: inline-flex; - align-items: center; - justify-content: center; - padding: $pad-y $pad-x; - border-radius: $radius; - border: none; - cursor: pointer; - @if type-of($bg) == 'string' { - background: css-var($bg); - @if $fg == null { color: css-var('color-fg-default'); } @else { color: css-var($fg); } - } @else if type-of($bg) == 'color' { - background: $bg; - @if $fg == null { color: readable-text($bg); } @else if type-of($fg) == 'color' { color: $fg; } @else { color: css-var($fg); } - } - // 微交互 - &:hover { filter: brightness(0.95); } - &:active { transform: translateY(1px); } -} - - -// ----------------------------- -// 示例(注释掉,直接拷贝到你的样式里使用) -// ----------------------------- - -// 示例主题 maps:键名与全局 CSS 变量中的命名保持一致(但不包含前缀 --) -// $theme-light: ( -// 'color-fg-default': #24292f, -// 'color-fg-muted': #57606a, -// 'color-canvas-default': #ffffff, -// 'color-border-default': #d0d7de, -// 'color-accent-fg': #0969da, -// ); -// -// $theme-dark: ( -// 'color-fg-default': #c9d1d9, -// 'color-fg-muted': #8b949e, -// 'color-canvas-default': #0d1117, -// 'color-border-default': #30363d, -// 'color-accent-fg': #58a6ff, -// ); -// -// 生成到 :root 和手动切换器: -// @include generate-theme(':root', $theme-light); -// @include generate-theme(':root[data-theme="dark"]', $theme-dark); -// -// 使用 CSS 变量: -// .markdown-body { color: css-var('color-fg-default'); background: css-var('color-canvas-default'); } -// -// 使用 mixin 快速为按钮应用主题颜色: -// .btn { @include theme-button('color-accent-emphasis'); } - -// ----------------------------- -// 结束 -// ----------------------------- diff --git a/packages/client/src/assets/styles/scss/common.scss b/packages/client/src/assets/styles/scss/common.scss index c09dff8..8241350 100644 --- a/packages/client/src/assets/styles/scss/common.scss +++ b/packages/client/src/assets/styles/scss/common.scss @@ -50,6 +50,7 @@ $theme-dark: ( // 手动主题切换支持:data-theme 或 class @include generate-theme(':root[data-theme="dark"]', $theme-dark); +@include generate-theme(".theme-light", $theme-light); @include generate-theme(".theme-dark", $theme-dark); #app { diff --git a/packages/client/src/components/AiDemo/_/sseData.ts b/packages/client/src/components/AiDemo/_/sseData.ts index 5ec3ce3..e61a4c2 100644 --- a/packages/client/src/components/AiDemo/_/sseData.ts +++ b/packages/client/src/components/AiDemo/_/sseData.ts @@ -1,31 +1,102 @@ export default [ - { event: "message", answer: "## asdas\n" }, + { + event: "message", + answer: + "## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n", + }, { event: "message", answer: "**asasa**\n" }, - { event: "message", answer: "![啊啊啊](https://ts1.tc.mm.bing.net/th/id/R-C.823270fc68b9c58f0d9b3feb92b7b172?rik=aubbEBMSC86e%2bw&riu=http%3a%2f%2fimg95.699pic.com%2fphoto%2f50038%2f1181.jpg_wh860.jpg&ehk=iQboj4JMLLfDitOL7VJtSktED0AE%2f7Fyxfik0GTJkyQ%3d&risl=&pid=ImgRaw&r=0)asd\n" }, + { + event: "message", + answer: + "![啊啊啊](https://ts1.tc.mm.bing.net/th/id/R-C.823270fc68b9c58f0d9b3feb92b7b172?rik=aubbEBMSC86e%2bw&riu=http%3a%2f%2fimg95.699pic.com%2fphoto%2f50038%2f1181.jpg_wh860.jpg&ehk=iQboj4JMLLfDitOL7VJtSktED0AE%2f7Fyxfik0GTJkyQ%3d&risl=&pid=ImgRaw&r=0)asd\n", + }, { event: "message", answer: "```\nasdaaaasasaasaas\n" }, { event: "message", answer: "asdsaa\n" }, { event: "message", answer: "console.log(as)\n" }, { event: "message", answer: "asa\n```\n\n" }, - { event: "message", answer: "\n\n" }, - { event: "message", answer: "## asdas" }, - { event: "message", answer: "## asdas" }, - { event: "message", answer: "## asdas" }, - { event: "message", answer: "## asdas" }, - { event: "message", answer: "## asdas" }, - { event: "message", answer: "## asdas" }, - { event: "message", answer: "## asdas" }, - { event: "message", answer: "## asdas" }, - { event: "message", answer: "## asdas" }, - { event: "message", answer: "## asdas" }, - { event: "message", answer: "## asdas" }, - { event: "message", answer: "## asdas" }, - { event: "message", answer: "## asdas" }, - { event: "message", answer: "## asdas" }, - { event: "message", answer: "## asdas" }, - { event: "message", answer: "## asdas" }, - { event: "message", answer: "## asdas" }, - { event: "message", answer: "## asdas" }, - { event: "message", answer: "## asdas" }, - { event: "message", answer: "## asdas" }, + { event: "message", answer: '\n\n' }, + { event: "message", answer: "qweqen" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, { event: "message_end", answer: "## asdas" }, -] \ No newline at end of file +]; diff --git a/packages/client/src/components/ChatBox/_/Msg.vue b/packages/client/src/components/ChatBox/_/Msg.vue new file mode 100644 index 0000000..89d2d6d --- /dev/null +++ b/packages/client/src/components/ChatBox/_/Msg.vue @@ -0,0 +1,64 @@ + + + + + diff --git a/packages/client/src/components/ChatBox/_/Node.vue b/packages/client/src/components/ChatBox/_/Node.vue new file mode 100644 index 0000000..dbca24f --- /dev/null +++ b/packages/client/src/components/ChatBox/_/Node.vue @@ -0,0 +1,61 @@ + + + + + \ No newline at end of file diff --git a/packages/client/src/components/ChatBox/_/sseData.ts b/packages/client/src/components/ChatBox/_/sseData.ts new file mode 100644 index 0000000..e61a4c2 --- /dev/null +++ b/packages/client/src/components/ChatBox/_/sseData.ts @@ -0,0 +1,102 @@ +export default [ + { + event: "message", + answer: + "## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n## asdas\n", + }, + { event: "message", answer: "**asasa**\n" }, + { + event: "message", + answer: + "![啊啊啊](https://ts1.tc.mm.bing.net/th/id/R-C.823270fc68b9c58f0d9b3feb92b7b172?rik=aubbEBMSC86e%2bw&riu=http%3a%2f%2fimg95.699pic.com%2fphoto%2f50038%2f1181.jpg_wh860.jpg&ehk=iQboj4JMLLfDitOL7VJtSktED0AE%2f7Fyxfik0GTJkyQ%3d&risl=&pid=ImgRaw&r=0)asd\n", + }, + { event: "message", answer: "```\nasdaaaasasaasaas\n" }, + { event: "message", answer: "asdsaa\n" }, + { event: "message", answer: "console.log(as)\n" }, + { event: "message", answer: "asa\n```\n\n" }, + { event: "message", answer: '\n\n' }, + { event: "message", answer: "qweqen" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "qweqeq" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "## asdas\n" }, + { event: "message_end", answer: "## asdas" }, +]; diff --git a/packages/client/src/components/ChatBox/index.vue b/packages/client/src/components/ChatBox/index.vue new file mode 100644 index 0000000..43b75c6 --- /dev/null +++ b/packages/client/src/components/ChatBox/index.vue @@ -0,0 +1,153 @@ + + + + + diff --git a/packages/client/src/composables/useScroll/index.ts b/packages/client/src/composables/useScroll/index.ts new file mode 100644 index 0000000..ffbea6f --- /dev/null +++ b/packages/client/src/composables/useScroll/index.ts @@ -0,0 +1,90 @@ +interface IOption { + containerEl: Readonly>; + contentEl: Readonly>; + // 是否首次滚动到底部 + firstToBottom?: boolean; +} + +const defaultOption: Partial = { + firstToBottom: true, +}; + +export function useScroll(option: IOption) { + if (import.meta.env.SSR) { + return; + } + const { containerEl, contentEl, firstToBottom } = { + ...defaultOption, + ...option, + }; + const isAtBottom = ref(false); + const isAtTop = ref(false); + let _firstToBottom = firstToBottom; + let resizeObserver: ResizeObserver | null = null; + nextTick(() => { + const targetElement = containerEl.value!; + const talkContent = contentEl.value!; + function handleScroll() { + if (targetElement.scrollHeight === targetElement.clientHeight) { + isAtBottom.value = false; + isAtTop.value = false; + return; + } + if ( + Math.abs( + targetElement.scrollHeight - + targetElement.clientHeight - + targetElement.scrollTop + ) <= 50 + ) { + isAtBottom.value = true; + } else { + isAtBottom.value = false; + } + if (targetElement.scrollTop <= 50) { + isAtTop.value = true; + } else { + isAtTop.value = false; + } + } + targetElement.onscroll = handleScroll; + resizeObserver = new ResizeObserver(() => { + // 当内容高度发生变化时,如果当前不在底部,则滚动到底部 + if (targetElement.scrollHeight === targetElement.clientHeight) { + return; + } + if (isAtBottom.value || _firstToBottom) { + _firstToBottom = false; + scrollToBottom(); + } + }); + resizeObserver.observe(talkContent); + + handleScroll(); + }); + onScopeDispose(clear); + function scrollToBottom() { + const container = containerEl.value!; + const scrollTop = container.scrollHeight - container.clientHeight; + container.scrollTop = scrollTop; + // container.scrollTo({ top: scrollTop, behavior: "smooth" }); + } + function scrollToTop() { + containerEl.value!.scrollTop = 0; + } + function clear() { + if (containerEl.value) { + containerEl.value.onscroll = null; + } + if (resizeObserver) { + contentEl.value && resizeObserver.unobserve(contentEl.value); + resizeObserver.disconnect(); + } + } + return { + isAtBottom, + scrollToTop, + scrollToBottom, + clear, + }; +} diff --git a/packages/client/src/pages/index.vue b/packages/client/src/pages/index.vue index f7ce484..f8a38e7 100644 --- a/packages/client/src/pages/index.vue +++ b/packages/client/src/pages/index.vue @@ -9,41 +9,23 @@ definePage({ defineOptions({ name: "home", }); - -// import { useModal } from "vue-final-modal"; -// import ModalConfirmPlainCss from "./_M.vue"; - -// const { open, close } = useModal({ -// component: ModalConfirmPlainCss, -// attrs: { -// title: "Hello World!", -// onConfirm() { -// close(); -// }, -// }, -// slots: { -// default: "

The content of the modal

", -// }, -// });