feat:部分功能优化
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 2m1s

This commit is contained in:
xupan 2026-07-11 15:39:48 +08:00
parent bfde0250e3
commit e3f81d4f7b
8 changed files with 1036 additions and 957 deletions

519
package-lock.json generated
View File

@ -11,6 +11,7 @@
"@bddh/starling-realtime-client": "^2.0.9",
"crypto-js": "^4.2.0",
"element-plus": "^2.9.7",
"opencc-js": "^1.4.0",
"uuid": "^9.0.1",
"vue": "^3.5.13"
},
@ -2010,24 +2011,6 @@
"node": ">=12"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
"integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
@ -2045,24 +2028,6 @@
"node": ">=12"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
"integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
@ -2080,24 +2045,6 @@
"node": ">=12"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
"integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
@ -4546,6 +4493,12 @@
"node": ">=12.20.0"
}
},
"node_modules/opencc-js": {
"version": "1.4.0",
"resolved": "https://registry.npmmirror.com/opencc-js/-/opencc-js-1.4.0.tgz",
"integrity": "sha512-SEb4huI5+lT4CFPA4sgs+NAOOgpcmdHZ+vYxhRLR1bQWIt6VRwpFPjWgTJAWAg1MviDcXTBe5zVoUOVxVxNK8w==",
"license": "MIT AND Apache-2.0"
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz",
@ -5258,420 +5211,6 @@
}
}
},
"node_modules/vitest/node_modules/@esbuild/aix-ppc64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
"integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/android-arm": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
"integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/android-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
"integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/android-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
"integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/darwin-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
"integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/darwin-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
"integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/freebsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
"integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/freebsd-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
"integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/linux-arm": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
"integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/linux-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
"integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/linux-ia32": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
"integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/linux-loong64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
"integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/linux-mips64el": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
"integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/linux-ppc64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
"integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/linux-riscv64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
"integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/linux-s390x": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
"integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/linux-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
"integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/netbsd-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
"integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/openbsd-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
"integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/sunos-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
"integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/win32-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
"integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/win32-ia32": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
"integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/win32-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
"integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@vitest/mocker": {
"version": "4.1.8",
"resolved": "https://registry.npmmirror.com/@vitest/mocker/-/mocker-4.1.8.tgz",
@ -5699,50 +5238,6 @@
}
}
},
"node_modules/vitest/node_modules/esbuild": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.28.0.tgz",
"integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.28.0",
"@esbuild/android-arm": "0.28.0",
"@esbuild/android-arm64": "0.28.0",
"@esbuild/android-x64": "0.28.0",
"@esbuild/darwin-arm64": "0.28.0",
"@esbuild/darwin-x64": "0.28.0",
"@esbuild/freebsd-arm64": "0.28.0",
"@esbuild/freebsd-x64": "0.28.0",
"@esbuild/linux-arm": "0.28.0",
"@esbuild/linux-arm64": "0.28.0",
"@esbuild/linux-ia32": "0.28.0",
"@esbuild/linux-loong64": "0.28.0",
"@esbuild/linux-mips64el": "0.28.0",
"@esbuild/linux-ppc64": "0.28.0",
"@esbuild/linux-riscv64": "0.28.0",
"@esbuild/linux-s390x": "0.28.0",
"@esbuild/linux-x64": "0.28.0",
"@esbuild/netbsd-arm64": "0.28.0",
"@esbuild/netbsd-x64": "0.28.0",
"@esbuild/openbsd-arm64": "0.28.0",
"@esbuild/openbsd-x64": "0.28.0",
"@esbuild/openharmony-arm64": "0.28.0",
"@esbuild/sunos-x64": "0.28.0",
"@esbuild/win32-arm64": "0.28.0",
"@esbuild/win32-ia32": "0.28.0",
"@esbuild/win32-x64": "0.28.0"
}
},
"node_modules/vitest/node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz",

View File

@ -14,6 +14,7 @@
"@bddh/starling-realtime-client": "^2.0.9",
"crypto-js": "^4.2.0",
"element-plus": "^2.9.7",
"opencc-js": "^1.4.0",
"uuid": "^9.0.1",
"vue": "^3.5.13"
},

File diff suppressed because it is too large Load Diff

BIN
src/assets/avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

View File

@ -1,10 +1,8 @@
<template>
<div class="subtitle-panel">
<div
class="subtitle-content ai-only-mode"
ref="scrollContainerRef"
>
<div v-if="aiSubtitle" class="ai-subtitle-bar" ref="aiSubtitleBarRef" @scroll="handleScroll">
<!-- AI字幕 -->
<div class="subtitle-content ai-only-mode" ref="scrollContainerRef">
<div v-if="aiSubtitle" class="ai-subtitle-bar" ref="aiSubtitleBarRef">
<div
class="ai-subtitle-text"
ref="aiSubtitleTextRef"
@ -12,6 +10,24 @@
>
{{ aiSubtitle }}
</div>
<!-- 滚动指示器 -->
<div
v-if="aiTextOverflow"
class="scroll-hint"
@click="scrollAIToBottom"
>
<svg
viewBox="0 0 24 24"
width="14"
height="14"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
>
<polyline points="6 9 12 15 18 9" />
</svg>
</div>
</div>
<div v-else class="subtitle-placeholder">
<svg
@ -27,54 +43,138 @@
d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"
/>
</svg>
等待AI回...
等待AI回...
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {ref, watch, nextTick, onMounted} from "vue";
import { ref, watch, nextTick, onMounted, onBeforeUnmount } from "vue";
const props = defineProps<{
aiSubtitle: string;
}>();
// ref .subtitle-content
const scrollContainerRef = ref<HTMLElement | null>(null);
const aiSubtitleBarRef = ref<HTMLElement | null>(null);
const aiSubtitleTextRef = ref<HTMLElement | null>(null);
// ===== AI =====
const aiFontSize = ref(18);
const isUserScrolling = ref(false);
const aiTextOverflow = ref(false);
// 1440×2560
const isTargetResolution = ref(false);
/**
* 检测当前视口是否为 1440×2560 分辨率
* 使用 window.innerWidth innerHeight 精确匹配
*/
const checkResolution = () => {
const TARGET_WIDTH = 1440;
const TARGET_HEIGHT = 2560;
console.log("------------", window.innerWidth, window.innerHeight);
isTargetResolution.value =
(window.innerWidth === TARGET_WIDTH &&
window.innerHeight === TARGET_HEIGHT) ||
(window.innerWidth > 1000 && window.innerHeight > 1680);
};
/**
* 获取目标分辨率下的字号基准值
* 1440×2560 下使用更大的字号以确保清晰可读
*/
const getTargetFontSizeBase = (textLength: number): number => {
if (textLength <= 50) return 36;
if (textLength <= 100) return 32;
if (textLength <= 200) return 28;
if (textLength <= 400) return 24;
return 22;
};
/**
* 获取普通分辨率下的字号基准值
*/
const getNormalFontSizeBase = (textLength: number): number => {
if (textLength <= 50) return 18;
if (textLength <= 100) return 16;
if (textLength <= 200) return 14;
if (textLength <= 400) return 13;
return 12;
};
// AI
const computeAIFontSize = () => {
const text = props.aiSubtitle;
console.log("------------", text);
if (!text) {
aiFontSize.value = 18;
aiFontSize.value = isTargetResolution.value ? 36 : 18;
return;
}
const len = text.length;
if (len <= 50) aiFontSize.value = 18;
else if (len <= 100) aiFontSize.value = 16;
else if (len <= 200) aiFontSize.value = 14;
else if (len <= 400) aiFontSize.value = 13;
else aiFontSize.value = 12;
aiFontSize.value = isTargetResolution.value
? getTargetFontSizeBase(len)
: getNormalFontSizeBase(len);
};
const handleScroll = () => {
if (!aiSubtitleBarRef.value) return;
const { scrollTop, scrollHeight, clientHeight } = aiSubtitleBarRef.value;
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 10;
isUserScrolling.value = !isAtBottom;
// AI
const checkAIOverflow = () => {
nextTick(() => {
const bar = aiSubtitleBarRef.value;
const textEl = aiSubtitleTextRef.value;
if (bar && textEl) {
aiTextOverflow.value = textEl.scrollHeight > bar.clientHeight + 4;
}
});
};
const scrollAIToBottom = () => {
if (aiSubtitleBarRef.value) {
aiSubtitleBarRef.value.scrollTo({
top: aiSubtitleBarRef.value.scrollHeight,
behavior: "smooth",
});
}
};
/**
* 处理视口尺寸变化重新检测分辨率并更新字号
*/
const handleResize = () => {
const wasTarget = isTargetResolution.value;
checkResolution();
//
if (wasTarget !== isTargetResolution.value) {
computeAIFontSize();
}
};
onMounted(() => {
//
checkResolution();
computeAIFontSize();
//
window.addEventListener("resize", handleResize);
});
onBeforeUnmount(() => {
window.removeEventListener("resize", handleResize);
});
// ===== =====
// AI + +
watch(
() => props.aiSubtitle,
() => {
computeAIFontSize();
checkAIOverflow();
// AI
nextTick(() => {
if (aiSubtitleBarRef.value && !isUserScrolling.value) {
if (aiSubtitleBarRef.value) {
aiSubtitleBarRef.value.scrollTo({
top: aiSubtitleBarRef.value.scrollHeight,
behavior: "smooth",
@ -92,6 +192,7 @@ watch(
height: 100%;
}
/* ===== 字幕内容 ===== */
.subtitle-content {
flex: 1;
min-height: 0;
@ -129,16 +230,16 @@ watch(
font-size: 14px;
}
/* ===== AI字幕 ===== */
.ai-only-mode {
display: flex;
flex-direction: column;
align-items: flex-end;
padding-bottom: 4px;
}
.ai-subtitle-bar {
width: 100%;
flex: 1;
min-height: 0;
max-height: 100%;
overflow-y: auto;
background: linear-gradient(
135deg,
@ -171,4 +272,64 @@ watch(
transition: font-size 0.3s ease;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
}
.scroll-hint {
position: sticky;
bottom: 0;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 50%;
background: rgba(108, 140, 255, 0.3);
color: rgba(255, 255, 255, 0.8);
cursor: pointer;
margin: 8px auto 0;
transition: all 0.2s;
&:hover {
background: rgba(108, 140, 255, 0.5);
transform: translateX(-50%) scale(1.1);
}
}
/* ===== 1440×2560 分辨率专用样式 ===== */
@media (min-width: 1000px) and (min-height: 1680px) {
.subtitle-placeholder {
font-size: 24px;
line-height: 1.75;
letter-spacing: 0.5px;
font-weight: 400;
}
/* AI字幕字号由 JavaScript 动态控制,此处仅设置辅助样式 */
.ai-subtitle-text {
line-height: 1.75;
letter-spacing: 0.6px;
font-weight: 400;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.7);
}
/* 字幕容器:背景与边框适配大字号 */
.ai-subtitle-bar {
padding: 18px 24px;
border-radius: 12px;
background: linear-gradient(
135deg,
rgba(108, 140, 255, 0.12) 0%,
rgba(79, 110, 247, 0.06) 100%
);
border: 1px solid rgba(108, 140, 255, 0.15);
}
/* 滚动指示器:尺寸适配 */
.scroll-hint {
width: 40px;
height: 40px;
margin-top: 12px;
}
}
</style>

View File

@ -4,10 +4,9 @@ import RealTimeHuman from "@bddh/starling-realtime-client";
import { checkPlayUnMute } from "@/utils/audioUtils";
import { SilenceDetector } from "@/utils/SilenceDetector";
import { TokenManager } from "@/utils/tokenManager";
import type { ChatMessage, SubtitleMode } from "@/types";
import type { ChatMessage } from "@/types";
const DEFAULT_TEXT =
"你好!我是皮特教授,欢迎向我提问任何有关电池的问题。";
const DEFAULT_TEXT = "你好!我是皮特教授,歡迎向我提問任何有關電池的問題。";
const OPENING_TTS_DELAY_MS = 200;
const SILENCE_TIMEOUT_MS = 5000;
// 最短有效语音时长(秒),低于此时长视为无效输入
@ -17,7 +16,15 @@ const BROADCAST_TIMEOUT_MS = 60000;
const APPID = "i-sfyrkdr0ubepa";
const APP_KEY = "72vpdansbpg8nxjbxzhp";
// 打断词列表:识别到这些词时自动打断播报
const INTERRUPT_KEYWORDS = ["稍等一下", "停一下", "抱歉打断一下", "不好意思", "对不起", "停", "闭嘴"];
const INTERRUPT_KEYWORDS = [
"稍等一下",
"停一下",
"抱歉打断一下",
"不好意思",
"对不起",
"停",
"闭嘴",
];
/**
*
@ -127,7 +134,6 @@ export function useDigitalHuman(options?: {
const isBroadcasting = ref(false);
// 字幕相关
const subtitleMode = ref<SubtitleMode>("ai-only");
const currentAISubtitle = ref("");
const currentUserSubtitle = ref("");
const chatMessages = ref<ChatMessage[]>([]);
@ -329,10 +335,14 @@ export function useDigitalHuman(options?: {
if (pickPhase.value === "broadcasting") {
const hasInterrupt = containsInterruptKeyword(subtitleContent);
if (hasInterrupt) {
console.info(`[打断词检测] 播报中识别到打断词: "${subtitleContent}"自动打断仅中断播报不调用LLM`);
console.info(
`[打断词检测] 播报中识别到打断词: "${subtitleContent}"自动打断仅中断播报不调用LLM`,
);
interrupt();
} else {
console.info(`[打断词检测] 播报中收到ASR结果: "${subtitleContent}",非打断词,忽略`);
console.info(
`[打断词检测] 播报中收到ASR结果: "${subtitleContent}",非打断词,忽略`,
);
}
break;
}
@ -348,9 +358,7 @@ export function useDigitalHuman(options?: {
if (pickPhase.value === "idle") {
// 忽略打断后3秒内到达的延迟ASR结果避免打断词被当作新输入
if (Date.now() - lastInterruptTime < 3000) {
console.info(
"[状态机] 打断后延迟到达的ASR结果忽略",
);
console.info("[状态机] 打断后延迟到达的ASR结果忽略");
break;
}
const hasSubstance = hasSubstantialContent(subtitleContent);
@ -846,7 +854,6 @@ export function useDigitalHuman(options?: {
// 播报状态
isBroadcasting,
// 字幕
subtitleMode,
currentAISubtitle,
currentUserSubtitle,
chatMessages,

View File

@ -1,4 +1,5 @@
import { ref } from "vue";
import OpenCC from "opencc-js";
// 火山方舟 Chat Completions API 配置
const LLM_API_URL =
@ -51,7 +52,7 @@ export function useLLM() {
const SYSTEM_PROMPT: LLMMessage = {
role: "system",
content: "你是一个友好的数字人助手,请用简洁自然的语言回答用户的问题。",
content: "你是一個友善的數位人助手,請用簡潔自然的語言回答使用者的問題。",
};
const sendToLLM = async (userMessage: string): Promise<string> => {
@ -102,7 +103,7 @@ export function useLLM() {
return aiReply;
} catch (err: any) {
const errorMsg = err.message || "请求失败";
const errorMsg = err.message || "請求失敗";
error.value = errorMsg;
console.error("LLM请求错误:", err);
throw err;
@ -143,7 +144,9 @@ export function useLLM() {
}
const data = await response.json();
const aiReply = data.text || "";
// 将简体中文回复转换为繁体中文
const rawText = data.text || "";
const aiReply = OpenCC.Converter({ from: "cn", to: "tw" })(rawText);
conversationHistory.value.push({
role: "user",
@ -157,7 +160,7 @@ export function useLLM() {
return aiReply;
} catch (err: any) {
const errorMsg = err.message || "请求失败";
const errorMsg = err.message || "請求失敗";
error.value = errorMsg;
console.error("ChatBase请求错误:", err);
throw err;

View File

@ -5,11 +5,3 @@ export interface ChatMessage {
timestamp: number;
completed: boolean;
}
export type SubtitleMode = 'ai-only' | 'bubble' | 'merged';
export const SUBTITLE_MODE_LABELS: Record<SubtitleMode, string> = {
'ai-only': 'AI字幕',
'bubble': '对话气泡',
'merged': '合并气泡',
};