feat(i18n): add i18n for remaining pages and fix content disorder

fix unable to automatically download AI-generated content
add github action to enter the corresponding key into the generation environment
This commit is contained in:
Dawn
2025-11-29 07:06:40 +08:00
parent d582052da4
commit b7ec9b21d4
16 changed files with 621 additions and 276 deletions

View File

@@ -183,6 +183,26 @@ jobs:
}
EOF
- name: Prepare production config
shell: bash
env:
YOUDAO_APP_KEY: ${{ secrets.YOUDAO_APP_KEY }}
YOUDAO_APP_SECRET: ${{ secrets.YOUDAO_APP_SECRET }}
TENCENT_API_KEY: ${{ secrets.TENCENT_API_KEY }}
TENCENT_SECRET_ID: ${{ secrets.TENCENT_SECRET_ID }}
TENCENT_MAP_KEY: ${{ secrets.TENCENT_MAP_KEY }}
run: |
mkdir -p src-tauri/configuration
cat > src-tauri/configuration/production.yaml <<'EOF'
youdao:
app_key: "${YOUDAO_APP_KEY}"
app_secret: "${YOUDAO_APP_SECRET}"
tencent:
api_key: "${TENCENT_API_KEY}"
secret_id: "${TENCENT_SECRET_ID}"
map_key: "${TENCENT_MAP_KEY}"
EOF
# 安装 Rust
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable # Set this to dtolnay/rust-toolchain@nightly

26
docs/release-config.md Normal file
View File

@@ -0,0 +1,26 @@
# 发布流程中的生产配置注入说明
在 GitHub Actions 的 release 流程(`.github/workflows/release.yml`)里,打包前会自动生成 `src-tauri/configuration/production.yaml`,将密钥写入其中。需要在仓库的 **Settings → Secrets and variables → Actions** 中配置以下 Secrets
- `YOUDAO_APP_KEY`
- `YOUDAO_APP_SECRET`
- `TENCENT_API_KEY`
- `TENCENT_SECRET_ID`
- `TENCENT_MAP_KEY`
工作流会在 `publish-tauri` 任务中执行:
```bash
mkdir -p src-tauri/configuration
cat > src-tauri/configuration/production.yaml <<'EOF'
youdao:
app_key: "${YOUDAO_APP_KEY}"
app_secret: "${YOUDAO_APP_SECRET}"
tencent:
api_key: "${TENCENT_API_KEY}"
secret_id: "${TENCENT_SECRET_ID}"
map_key: "${TENCENT_MAP_KEY}"
EOF
```
配置完成后,推送符合规则的标签(如 `v1.2.3`)触发发布时,以上内容会被写入生产配置文件参与打包,无需将密钥提交到仓库。***

16
locales/en/home.json vendored
View File

@@ -230,6 +230,22 @@
"group_name_update_failed": "Failed to rename group"
}
},
"manage_group_member": {
"title": "Manage group members",
"search_placeholder_mobile": "Search members~",
"search_placeholder_pc": "Search group members",
"selected_count": "Selected {count} members",
"remove_button": "Remove from group",
"dialog_negative": "Cancel",
"dialog_positive": "Confirm",
"remove_confirm": "Remove {count} members?",
"channel_not_allowed": "Channels cannot remove members",
"select_member_warning": "Select members to remove",
"remove_success": "Removed {count} members",
"remove_failed": "Failed to remove members, please retry",
"confirm_remove_title": "Confirm removal",
"channel_manage_unsupported": "Channels do not support member management"
},
"profile_card": {
"online_status": "Online status",
"status": {

View File

@@ -187,6 +187,49 @@
"open_main_panel": "Open main panel",
"exit": "Exit"
},
"location": {
"modal": {
"title": {
"map_error": "Map error",
"location_error": "Location failed",
"default": "Pick a location"
},
"result": {
"map_error_title": "Map failed to load",
"location_error_title": "Failed to get location"
},
"buttons": {
"cancel": "Cancel",
"retry": "Retry",
"send": "Send location"
},
"loading": {
"locating": "Fetching location...",
"map": "Loading map..."
},
"info": {
"current": "Current location",
"fetching_address": "Getting address...",
"coordinate": "Coord: {lat}, {lng}",
"unknown_address": "Unknown address"
},
"errors": {
"missing_api_key": "Tencent Map API key not configured",
"geocode_failed": "Failed to fetch address"
}
},
"map": {
"marker_current": "Current location"
},
"hook": {
"permission_check_failed": "Failed to check geolocation permission",
"unsupported": "Geolocation is not supported in this browser",
"error_generic": "Failed to obtain location",
"permission_denied": "Location permission denied",
"position_unavailable": "Location unavailable",
"timeout": "Location request timed out"
}
},
"update_window": {
"updating": "Updating",
"fetch_commit_failed": "Failed to load release notes for v{version}",

View File

@@ -229,6 +229,22 @@
"group_name_update_failed": "群名称更新失败"
}
},
"manage_group_member": {
"title": "管理群成员",
"search_placeholder_mobile": "搜索成员~",
"search_placeholder_pc": "搜索群成员",
"selected_count": "已选择 {count} 人",
"remove_button": "踢出群聊",
"dialog_negative": "取消",
"dialog_positive": "确定",
"remove_confirm": "确定要踢出 {count} 位成员吗?",
"channel_not_allowed": "频道不允许踢出成员",
"select_member_warning": "请选择要踢出的成员",
"remove_success": "成功踢出 {count} 位成员",
"remove_failed": "踢出失败,请重试",
"confirm_remove_title": "确认踢出",
"channel_manage_unsupported": "频道不支持管理成员"
},
"profile_card": {
"online_status": "在线状态",
"status": {

View File

@@ -187,6 +187,49 @@
"open_main_panel": "打开主面板",
"exit": "退出"
},
"location": {
"modal": {
"title": {
"map_error": "地图错误",
"location_error": "位置获取失败",
"default": "选择位置"
},
"result": {
"map_error_title": "地图加载失败",
"location_error_title": "位置获取失败"
},
"buttons": {
"cancel": "取消",
"retry": "重试",
"send": "发送位置"
},
"loading": {
"locating": "正在获取位置...",
"map": "地图加载中..."
},
"info": {
"current": "当前位置",
"fetching_address": "获取地址中...",
"coordinate": "坐标: {lat}, {lng}",
"unknown_address": "未知地址"
},
"errors": {
"missing_api_key": "腾讯地图API密钥未配置",
"geocode_failed": "获取地址失败"
}
},
"map": {
"marker_current": "当前位置"
},
"hook": {
"permission_check_failed": "检查地理位置权限失败",
"unsupported": "浏览器不支持地理位置功能",
"error_generic": "获取位置失败",
"permission_denied": "位置权限被拒绝",
"position_unavailable": "位置信息不可用",
"timeout": "获取位置超时"
}
},
"update_window": {
"updating": "更新中",
"fetch_commit_failed": "v{version} 版本更新内容获取失败",

9
package.json vendored
View File

@@ -108,20 +108,19 @@
"driver.js": "^1.3.6",
"es-toolkit": "^1.41.0",
"file-type": "^21.0.0",
"github-markdown-css": "^5.8.1",
"grapheme-splitter": "^1.0.4",
"hula-emojis": "^1.2.31",
"internal-ip": "^8.0.1",
"mermaid": "^11.12.1",
"mitt": "^3.0.1",
"naive-ui": "^2.43.1",
"naive-ui": "^2.43.2",
"p-limit": "^7.2.0",
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"pinia-shared-state": "^1.0.1",
"seemly": "^0.3.10",
"stream-markdown": "^0.0.10",
"stream-monaco": "^0.0.6",
"stream-monaco": "^0.0.7",
"tauri-plugin-mic-recorder-api": "^2.0.0",
"tauri-plugin-safe-area-insets": "^0.1.0",
"three": "^0.181.0",
@@ -167,14 +166,14 @@
"postcss-pxtorem": "^6.1.0",
"prettier": "^3.6.2",
"release-it": "^17.11.0",
"sass": "1.93.2",
"sass": "1.94.2",
"typescript": "^5.9.3",
"unplugin-auto-import": "^20.2.0",
"unplugin-vue-components": "^30.0.0",
"vite": "7.2.4",
"vite-plugin-vue-setup-extend": "^0.4.0",
"vitest": "^4.0.8",
"vue-tsc": "^3.1.3",
"vue-tsc": "^3.1.5",
"web-vitals": "^5.1.0"
},
"config": {

309
pnpm-lock.yaml generated vendored
View File

@@ -107,9 +107,6 @@ importers:
file-type:
specifier: ^21.0.0
version: 21.0.0
github-markdown-css:
specifier: ^5.8.1
version: 5.8.1
grapheme-splitter:
specifier: ^1.0.4
version: 1.0.4
@@ -126,29 +123,29 @@ importers:
specifier: ^3.0.1
version: 3.0.1
naive-ui:
specifier: ^2.43.1
version: 2.43.1(vue@3.5.25(typescript@5.9.3))
specifier: ^2.43.2
version: 2.43.2(vue@3.5.25(typescript@5.9.3))
p-limit:
specifier: ^7.2.0
version: 7.2.0
pinia:
specifier: ^3.0.3
version: 3.0.3(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
specifier: ^3.0.4
version: 3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
pinia-plugin-persistedstate:
specifier: ^4.7.1
version: 4.7.1(pinia@3.0.3(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3)))
version: 4.7.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3)))
pinia-shared-state:
specifier: ^1.0.1
version: 1.0.1(pinia@3.0.3(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3)))
version: 1.0.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3)))
seemly:
specifier: ^0.3.10
version: 0.3.10
stream-markdown:
specifier: ^0.0.10
version: 0.0.10(shiki@3.15.0)
version: 0.0.10(shiki@3.17.0)
stream-monaco:
specifier: ^0.0.6
version: 0.0.6(monaco-editor@0.52.2)
specifier: ^0.0.7
version: 0.0.7(monaco-editor@0.52.2)
tauri-plugin-mic-recorder-api:
specifier: ^2.0.0
version: 2.0.0
@@ -178,7 +175,7 @@ importers:
version: 11.1.12(vue@3.5.25(typescript@5.9.3))
vue-renderer-markdown:
specifier: 0.0.62-beta.1
version: 0.0.62-beta.1(katex@0.16.25)(mermaid@11.12.1)(shiki@3.15.0)(stream-markdown@0.0.10(shiki@3.15.0))(stream-monaco@0.0.6(monaco-editor@0.52.2))(vue-i18n@11.1.12(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3))
version: 0.0.62-beta.1(katex@0.16.25)(mermaid@11.12.1)(shiki@3.17.0)(stream-markdown@0.0.10(shiki@3.17.0))(stream-monaco@0.0.7(monaco-editor@0.52.2))(vue-i18n@11.1.12(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3))
vue-router:
specifier: ^4.6.3
version: 4.6.3(vue@3.5.25(typescript@5.9.3))
@@ -233,13 +230,13 @@ importers:
version: 66.5.4
'@unocss/vite':
specifier: ^66.5.4
version: 66.5.4(vite@7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1))
version: 66.5.4(vite@7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1))
'@vitejs/plugin-vue':
specifier: ^6.0.1
version: 6.0.1(vite@7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.3))
version: 6.0.1(vite@7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.3))
'@vitejs/plugin-vue-jsx':
specifier: ^5.1.1
version: 5.1.1(vite@7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.3))
version: 5.1.1(vite@7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.3))
'@vitest/coverage-v8':
specifier: ^4.0.8
version: 4.0.8(vitest@4.0.8)
@@ -280,8 +277,8 @@ importers:
specifier: ^17.11.0
version: 17.11.0(typescript@5.9.3)
sass:
specifier: 1.93.2
version: 1.93.2
specifier: 1.94.2
version: 1.94.2
typescript:
specifier: ^5.9.3
version: 5.9.3
@@ -293,16 +290,16 @@ importers:
version: 30.0.0(@babel/parser@7.28.5)(vue@3.5.25(typescript@5.9.3))
vite:
specifier: 7.2.4
version: 7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)
version: 7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)
vite-plugin-vue-setup-extend:
specifier: ^0.4.0
version: 0.4.0(vite@7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1))
version: 0.4.0(vite@7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1))
vitest:
specifier: ^4.0.8
version: 4.0.8(@types/node@24.10.0)(@vitest/ui@4.0.8)(happy-dom@20.0.2)(jiti@2.6.1)(sass@1.93.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)
version: 4.0.8(@types/node@24.10.0)(@vitest/ui@4.0.8)(happy-dom@20.0.2)(jiti@2.6.1)(sass@1.94.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)
vue-tsc:
specifier: ^3.1.3
version: 3.1.3(typescript@5.9.3)
specifier: ^3.1.5
version: 3.1.5(typescript@5.9.3)
web-vitals:
specifier: ^5.1.0
version: 5.1.0
@@ -1668,26 +1665,26 @@ packages:
cpu: [x64]
os: [win32]
'@shikijs/core@3.15.0':
resolution: {integrity: sha512-8TOG6yG557q+fMsSVa8nkEDOZNTSxjbbR8l6lF2gyr6Np+jrPlslqDxQkN6rMXCECQ3isNPZAGszAfYoJOPGlg==}
'@shikijs/core@3.17.0':
resolution: {integrity: sha512-/HjeOnbc62C+n33QFNFrAhUlIADKwfuoS50Ht0pxujxP4QjZAlFp5Q+OkDo531SCTzivx5T18khwyBdKoPdkuw==}
'@shikijs/engine-javascript@3.15.0':
resolution: {integrity: sha512-ZedbOFpopibdLmvTz2sJPJgns8Xvyabe2QbmqMTz07kt1pTzfEvKZc5IqPVO/XFiEbbNyaOpjPBkkr1vlwS+qg==}
'@shikijs/engine-javascript@3.17.0':
resolution: {integrity: sha512-WwF99xdP8KfuDrIbT4wxyypfhoIxMeeOCp1AiuvzzZ6JT5B3vIuoclL8xOuuydA6LBeeNXUF/XV5zlwwex1jlA==}
'@shikijs/engine-oniguruma@3.15.0':
resolution: {integrity: sha512-HnqFsV11skAHvOArMZdLBZZApRSYS4LSztk2K3016Y9VCyZISnlYUYsL2hzlS7tPqKHvNqmI5JSUJZprXloMvA==}
'@shikijs/engine-oniguruma@3.17.0':
resolution: {integrity: sha512-flSbHZAiOZDNTrEbULY8DLWavu/TyVu/E7RChpLB4WvKX4iHMfj80C6Hi3TjIWaQtHOW0KC6kzMcuB5TO1hZ8Q==}
'@shikijs/langs@3.15.0':
resolution: {integrity: sha512-WpRvEFvkVvO65uKYW4Rzxs+IG0gToyM8SARQMtGGsH4GDMNZrr60qdggXrFOsdfOVssG/QQGEl3FnJ3EZ+8w8A==}
'@shikijs/langs@3.17.0':
resolution: {integrity: sha512-icmur2n5Ojb+HAiQu6NEcIIJ8oWDFGGEpiqSCe43539Sabpx7Y829WR3QuUW2zjTM4l6V8Sazgb3rrHO2orEAw==}
'@shikijs/monaco@3.15.0':
resolution: {integrity: sha512-MKURaG5ehas44DBOfw0szEp89fA3WTERl7B6MjWcRQMIvHd++OQmPLyk1wcWbqZ3Jcvj0QBCJw+5tR/IBW016g==}
'@shikijs/monaco@3.17.0':
resolution: {integrity: sha512-M/1lh+VKALKBNE4esjPXt5qCmkZ1KOIviDcTbfq7eP9EX/K1WPQR/kcHc5MTGlB+4iqSWiRghUHdCReRghx/wg==}
'@shikijs/themes@3.15.0':
resolution: {integrity: sha512-8ow2zWb1IDvCKjYb0KiLNrK4offFdkfNVPXb1OZykpLCzRU6j+efkY+Y7VQjNlNFXonSw+4AOdGYtmqykDbRiQ==}
'@shikijs/themes@3.17.0':
resolution: {integrity: sha512-/xEizMHLBmMHwtx4JuOkRf3zwhWD2bmG5BRr0IPjpcWpaq4C3mYEuTk/USAEglN0qPrTwEHwKVpSu/y2jhferA==}
'@shikijs/types@3.15.0':
resolution: {integrity: sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw==}
'@shikijs/types@3.17.0':
resolution: {integrity: sha512-wjLVfutYWVUnxAjsWEob98xgyaGv0dTEnMZDruU5mRjVN7szcGOfgO+997W2yR6odp+1PtSBNeSITRRTfUzK/g==}
'@shikijs/vscode-textmate@10.0.2':
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
@@ -1956,8 +1953,8 @@ packages:
'@types/lodash-es@4.17.12':
resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
'@types/lodash@4.17.20':
resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==}
'@types/lodash@4.17.21':
resolution: {integrity: sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==}
'@types/mdast@4.0.4':
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
@@ -2174,9 +2171,6 @@ packages:
'@vue/compiler-core@3.5.13':
resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==}
'@vue/compiler-core@3.5.22':
resolution: {integrity: sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==}
'@vue/compiler-core@3.5.24':
resolution: {integrity: sha512-eDl5H57AOpNakGNAkFDH+y7kTqrQpJkZFXhWZQGyx/5Wh7B1uQYvcWkvZi11BDhscPgj8N7XV3oRwiPnx1Vrig==}
@@ -2186,9 +2180,6 @@ packages:
'@vue/compiler-dom@3.5.13':
resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==}
'@vue/compiler-dom@3.5.22':
resolution: {integrity: sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==}
'@vue/compiler-dom@3.5.24':
resolution: {integrity: sha512-1QHGAvs53gXkWdd3ZMGYuvQFXHW4ksKWPG8HP8/2BscrbZ0brw183q2oNWjMrSWImYLHxHrx1ItBQr50I/q2zw==}
@@ -2225,8 +2216,8 @@ packages:
'@vue/devtools-shared@7.7.7':
resolution: {integrity: sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==}
'@vue/language-core@3.1.3':
resolution: {integrity: sha512-KpR1F/eGAG9D1RZ0/T6zWJs6dh/pRLfY5WupecyYKJ1fjVmDMgTPw9wXmKv2rBjo4zCJiOSiyB8BDP1OUwpMEA==}
'@vue/language-core@3.1.5':
resolution: {integrity: sha512-FMcqyzWN+sYBeqRMWPGT2QY0mUasZMVIuHvmb5NT3eeqPrbHBYtCP8JWEUCDCgM+Zr62uuWY/qoeBrPrzfa78w==}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
@@ -2312,8 +2303,8 @@ packages:
alien-signals@2.0.8:
resolution: {integrity: sha512-844G1VLkk0Pe2SJjY0J8vp8ADI73IM4KliNu2OGlYzWpO28NexEUvjHTcFjFX3VXoiUtwTbHxLNI9ImkcoBqzA==}
alien-signals@3.0.5:
resolution: {integrity: sha512-+2bRQFO1f9GLeIabDQWJlluL1NspZlLjpjaSSwwpl+9Tz5tS/3KrceHdwjNvIMEbYWSpoqtOPuXLTSoPgvIEWw==}
alien-signals@3.1.1:
resolution: {integrity: sha512-ogkIWbVrLwKtHY6oOAXaYkAxP+cTH7V5FZ5+Tm4NZFd8VDZ6uNMDrfzqctTZ42eTMCSR3ne3otpcxmqSnFfPYA==}
ansi-align@3.0.1:
resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
@@ -2774,6 +2765,9 @@ packages:
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
cwise-compiler@1.1.3:
resolution: {integrity: sha512-WXlK/m+Di8DMMcCjcWr4i+XzcQra9eCdXIJrgh4TUgh0pIS/yJduLxS9JgefsHJ/YVLdgPtXm9r62W92MvanEQ==}
@@ -2954,8 +2948,8 @@ packages:
peerDependencies:
date-fns: ^3.0.0 || ^4.0.0
date-fns@3.6.0:
resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==}
date-fns@4.1.0:
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
dayjs@1.11.19:
resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==}
@@ -3324,10 +3318,6 @@ packages:
git-url-parse@14.0.0:
resolution: {integrity: sha512-NnLweV+2A4nCvn4U/m2AoYu0pPKlsmhK9cknG7IMwsjFY1S2jxM+mAhsDxyxfCIGfGaD+dozsyX4b6vkYc83yQ==}
github-markdown-css@5.8.1:
resolution: {integrity: sha512-8G+PFvqigBQSWLQjyzgpa2ThD9bo7+kDsriUIidGcRhXgmcaAWUIpCZf8DavJgc+xifjbCG+GvMyWr0XMXmc7g==}
engines: {node: '>=10'}
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
@@ -3471,8 +3461,8 @@ packages:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
immutable@5.1.3:
resolution: {integrity: sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==}
immutable@5.1.4:
resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==}
import-fresh@3.3.1:
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
@@ -3933,8 +3923,8 @@ packages:
engines: {node: '>= 20'}
hasBin: true
mdast-util-to-hast@13.2.0:
resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==}
mdast-util-to-hast@13.2.1:
resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==}
mdn-data@2.12.2:
resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==}
@@ -4059,8 +4049,8 @@ packages:
resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==}
engines: {node: ^18.17.0 || >=20.5.0}
naive-ui@2.43.1:
resolution: {integrity: sha512-w52W0mOhdOGt4uucFSZmP0DI44PCsFyuxeLSs9aoUThfIuxms90MYjv46Qrr7xprjyJRw5RU6vYpCx4o9ind3A==}
naive-ui@2.43.2:
resolution: {integrity: sha512-YlLMnGrwGTOc+zMj90sG3ubaH5/7czsgLgGcjTLA981IUaz8r6t4WIujNt8r9PNr+dqv6XNEr0vxkARgPPjfBQ==}
peerDependencies:
vue: ^3.0.0
@@ -4142,8 +4132,8 @@ packages:
oniguruma-parser@0.12.1:
resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==}
oniguruma-to-es@4.3.3:
resolution: {integrity: sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==}
oniguruma-to-es@4.3.4:
resolution: {integrity: sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==}
open@10.1.0:
resolution: {integrity: sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==}
@@ -4315,11 +4305,11 @@ packages:
peerDependencies:
pinia: ^3.0.0
pinia@3.0.3:
resolution: {integrity: sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==}
pinia@3.0.4:
resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==}
peerDependencies:
typescript: '>=4.4.4'
vue: ^2.7.0 || ^3.5.11
typescript: '>=4.5.0'
vue: ^3.5.11
peerDependenciesMeta:
typescript:
optional: true
@@ -4529,8 +4519,8 @@ packages:
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
sass@1.93.2:
resolution: {integrity: sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==}
sass@1.94.2:
resolution: {integrity: sha512-N+7WK20/wOr7CzA2snJcUSSNTCzeCGUTFY3OgeQP3mZ1aj9NMQ0mSTXwlrnd89j33zzQJGqIN52GIOmYrfq46A==}
engines: {node: '>=14.0.0'}
hasBin: true
@@ -4581,8 +4571,8 @@ packages:
engines: {node: '>=4'}
hasBin: true
shiki@3.15.0:
resolution: {integrity: sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw==}
shiki@3.17.0:
resolution: {integrity: sha512-lUZfWsyW7czITYTdo/Tb6ZM4VfyXlzmKYBQBjTz+pBzPPkP08RgIt00Ls1Z50Cl3SfwJsue6WbJeF3UgqLVI9Q==}
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
@@ -4680,8 +4670,8 @@ packages:
peerDependencies:
shiki: '>=3.13.0'
stream-monaco@0.0.6:
resolution: {integrity: sha512-ZyhDI9rIhcWEw7ld8az8Qu2Iug1pL4lwR+V/yzxuOok72mBPvSR3LVbOUxq7Rcb6dF1it2BWDmpiORJcw5c7kw==}
stream-monaco@0.0.7:
resolution: {integrity: sha512-VqqEpuOptFw1K/hDxCFZ2sA8dgTFdON0SdKovfddeJMK7vaS3jddkDl41D2avq4mpQ6x2fnYXSEXsmc5oswdlA==}
hasBin: true
peerDependencies:
monaco-editor: ^0.52.2
@@ -5215,8 +5205,8 @@ packages:
peerDependencies:
vue: ^3.5.0
vue-tsc@3.1.3:
resolution: {integrity: sha512-StMNfZHwPIXQgY3KxPKM0Jsoc8b46mDV3Fn2UlHCBIwRJApjqrSwqeMYgWf0zpN+g857y74pv7GWuBm+UqQe1w==}
vue-tsc@3.1.5:
resolution: {integrity: sha512-L/G9IUjOWhBU0yun89rv8fKqmKC+T0HfhrFjlIml71WpfBv9eb4E9Bev8FMbyueBIU9vxQqbd+oOsVcDa5amGw==}
hasBin: true
peerDependencies:
typescript: '>=5.0.0'
@@ -6561,39 +6551,39 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.53.1':
optional: true
'@shikijs/core@3.15.0':
'@shikijs/core@3.17.0':
dependencies:
'@shikijs/types': 3.15.0
'@shikijs/types': 3.17.0
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.4
hast-util-to-html: 9.0.5
'@shikijs/engine-javascript@3.15.0':
'@shikijs/engine-javascript@3.17.0':
dependencies:
'@shikijs/types': 3.15.0
'@shikijs/types': 3.17.0
'@shikijs/vscode-textmate': 10.0.2
oniguruma-to-es: 4.3.3
oniguruma-to-es: 4.3.4
'@shikijs/engine-oniguruma@3.15.0':
'@shikijs/engine-oniguruma@3.17.0':
dependencies:
'@shikijs/types': 3.15.0
'@shikijs/types': 3.17.0
'@shikijs/vscode-textmate': 10.0.2
'@shikijs/langs@3.15.0':
'@shikijs/langs@3.17.0':
dependencies:
'@shikijs/types': 3.15.0
'@shikijs/types': 3.17.0
'@shikijs/monaco@3.15.0':
'@shikijs/monaco@3.17.0':
dependencies:
'@shikijs/core': 3.15.0
'@shikijs/types': 3.15.0
'@shikijs/core': 3.17.0
'@shikijs/types': 3.17.0
'@shikijs/vscode-textmate': 10.0.2
'@shikijs/themes@3.15.0':
'@shikijs/themes@3.17.0':
dependencies:
'@shikijs/types': 3.15.0
'@shikijs/types': 3.17.0
'@shikijs/types@3.15.0':
'@shikijs/types@3.17.0':
dependencies:
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.4
@@ -6869,9 +6859,9 @@ snapshots:
'@types/lodash-es@4.17.12':
dependencies:
'@types/lodash': 4.17.20
'@types/lodash': 4.17.21
'@types/lodash@4.17.20': {}
'@types/lodash@4.17.21': {}
'@types/mdast@4.0.4':
dependencies:
@@ -6965,7 +6955,7 @@ snapshots:
dependencies:
'@unocss/core': 66.5.4
'@unocss/vite@66.5.4(vite@7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1))':
'@unocss/vite@66.5.4(vite@7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1))':
dependencies:
'@jridgewell/remapping': 2.3.5
'@unocss/config': 66.5.4
@@ -6976,7 +6966,7 @@ snapshots:
pathe: 2.0.3
tinyglobby: 0.2.15
unplugin-utils: 0.3.1
vite: 7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)
vite: 7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)
'@vant/area-data@2.1.0': {}
@@ -6986,22 +6976,22 @@ snapshots:
dependencies:
vue: 3.5.25(typescript@5.9.3)
'@vitejs/plugin-vue-jsx@5.1.1(vite@7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.3))':
'@vitejs/plugin-vue-jsx@5.1.1(vite@7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.3))':
dependencies:
'@babel/core': 7.28.4
'@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.4)
'@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.4)
'@rolldown/pluginutils': 1.0.0-beta.41
'@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.28.4)
vite: 7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)
vite: 7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)
vue: 3.5.25(typescript@5.9.3)
transitivePeerDependencies:
- supports-color
'@vitejs/plugin-vue@6.0.1(vite@7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.3))':
'@vitejs/plugin-vue@6.0.1(vite@7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.3))':
dependencies:
'@rolldown/pluginutils': 1.0.0-beta.29
vite: 7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)
vite: 7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)
vue: 3.5.25(typescript@5.9.3)
'@vitest/coverage-v8@4.0.8(vitest@4.0.8)':
@@ -7017,7 +7007,7 @@ snapshots:
magicast: 0.5.1
std-env: 3.10.0
tinyrainbow: 3.0.3
vitest: 4.0.8(@types/node@24.10.0)(@vitest/ui@4.0.8)(happy-dom@20.0.2)(jiti@2.6.1)(sass@1.93.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)
vitest: 4.0.8(@types/node@24.10.0)(@vitest/ui@4.0.8)(happy-dom@20.0.2)(jiti@2.6.1)(sass@1.94.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)
transitivePeerDependencies:
- supports-color
@@ -7030,13 +7020,13 @@ snapshots:
chai: 6.2.0
tinyrainbow: 3.0.3
'@vitest/mocker@4.0.8(vite@7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1))':
'@vitest/mocker@4.0.8(vite@7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1))':
dependencies:
'@vitest/spy': 4.0.8
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)
vite: 7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)
'@vitest/pretty-format@4.0.8':
dependencies:
@@ -7064,7 +7054,7 @@ snapshots:
sirv: 3.0.2
tinyglobby: 0.2.15
tinyrainbow: 3.0.3
vitest: 4.0.8(@types/node@24.10.0)(@vitest/ui@4.0.8)(happy-dom@20.0.2)(jiti@2.6.1)(sass@1.93.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)
vitest: 4.0.8(@types/node@24.10.0)(@vitest/ui@4.0.8)(happy-dom@20.0.2)(jiti@2.6.1)(sass@1.94.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)
'@vitest/utils@4.0.8':
dependencies:
@@ -7140,14 +7130,6 @@ snapshots:
estree-walker: 2.0.2
source-map-js: 1.2.1
'@vue/compiler-core@3.5.22':
dependencies:
'@babel/parser': 7.28.5
'@vue/shared': 3.5.22
entities: 4.5.0
estree-walker: 2.0.2
source-map-js: 1.2.1
'@vue/compiler-core@3.5.24':
dependencies:
'@babel/parser': 7.28.5
@@ -7169,11 +7151,6 @@ snapshots:
'@vue/compiler-core': 3.5.13
'@vue/shared': 3.5.13
'@vue/compiler-dom@3.5.22':
dependencies:
'@vue/compiler-core': 3.5.22
'@vue/shared': 3.5.22
'@vue/compiler-dom@3.5.24':
dependencies:
'@vue/compiler-core': 3.5.24
@@ -7255,12 +7232,12 @@ snapshots:
dependencies:
rfdc: 1.4.1
'@vue/language-core@3.1.3(typescript@5.9.3)':
'@vue/language-core@3.1.5(typescript@5.9.3)':
dependencies:
'@volar/language-core': 2.4.23
'@vue/compiler-dom': 3.5.22
'@vue/shared': 3.5.22
alien-signals: 3.0.5
'@vue/compiler-dom': 3.5.25
'@vue/shared': 3.5.25
alien-signals: 3.1.1
muggle-string: 0.4.1
path-browserify: 1.0.1
picomatch: 4.0.3
@@ -7347,7 +7324,7 @@ snapshots:
alien-signals@2.0.8: {}
alien-signals@3.0.5: {}
alien-signals@3.1.1: {}
ansi-align@3.0.1:
dependencies:
@@ -7832,6 +7809,8 @@ snapshots:
csstype@3.1.3: {}
csstype@3.2.3: {}
cwise-compiler@1.1.3:
dependencies:
uniq: 1.0.1
@@ -8040,11 +8019,11 @@ snapshots:
data-uri-to-buffer@6.0.2: {}
date-fns-tz@3.2.0(date-fns@3.6.0):
date-fns-tz@3.2.0(date-fns@4.1.0):
dependencies:
date-fns: 3.6.0
date-fns: 4.1.0
date-fns@3.6.0: {}
date-fns@4.1.0: {}
dayjs@1.11.19: {}
@@ -8446,8 +8425,6 @@ snapshots:
dependencies:
git-up: 7.0.0
github-markdown-css@5.8.1: {}
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
@@ -8542,7 +8519,7 @@ snapshots:
comma-separated-tokens: 2.0.3
hast-util-whitespace: 3.0.0
html-void-elements: 3.0.0
mdast-util-to-hast: 13.2.0
mdast-util-to-hast: 13.2.1
property-information: 7.1.0
space-separated-tokens: 2.0.2
stringify-entities: 4.0.4
@@ -8608,7 +8585,7 @@ snapshots:
ignore@5.3.2: {}
immutable@5.1.3: {}
immutable@5.1.4: {}
import-fresh@3.3.1:
dependencies:
@@ -9031,7 +9008,7 @@ snapshots:
marked@16.4.1: {}
mdast-util-to-hast@13.2.0:
mdast-util-to-hast@13.2.1:
dependencies:
'@types/hast': 3.0.4
'@types/mdast': 4.0.4
@@ -9161,18 +9138,18 @@ snapshots:
mute-stream@2.0.0: {}
naive-ui@2.43.1(vue@3.5.25(typescript@5.9.3)):
naive-ui@2.43.2(vue@3.5.25(typescript@5.9.3)):
dependencies:
'@css-render/plugin-bem': 0.15.14(css-render@0.15.14)
'@css-render/vue3-ssr': 0.15.14(vue@3.5.25(typescript@5.9.3))
'@types/katex': 0.16.7
'@types/lodash': 4.17.20
'@types/lodash': 4.17.21
'@types/lodash-es': 4.17.12
async-validator: 4.2.5
css-render: 0.15.14
csstype: 3.1.3
date-fns: 3.6.0
date-fns-tz: 3.2.0(date-fns@3.6.0)
csstype: 3.2.3
date-fns: 4.1.0
date-fns-tz: 3.2.0(date-fns@4.1.0)
evtd: 0.2.4
highlight.js: 11.11.1
lodash: 4.17.21
@@ -9257,7 +9234,7 @@ snapshots:
oniguruma-parser@0.12.1: {}
oniguruma-to-es@4.3.3:
oniguruma-to-es@4.3.4:
dependencies:
oniguruma-parser: 0.12.1
regex: 6.0.1
@@ -9423,18 +9400,18 @@ snapshots:
pidtree@0.6.0: {}
pinia-plugin-persistedstate@4.7.1(pinia@3.0.3(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))):
pinia-plugin-persistedstate@4.7.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))):
dependencies:
defu: 6.1.4
optionalDependencies:
pinia: 3.0.3(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
pinia: 3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
pinia-shared-state@1.0.1(pinia@3.0.3(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))):
pinia-shared-state@1.0.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))):
dependencies:
broadcast-channel: 7.0.0
pinia: 3.0.3(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
pinia: 3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
pinia@3.0.3(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3)):
pinia@3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3)):
dependencies:
'@vue/devtools-api': 7.7.7
vue: 3.5.25(typescript@5.9.3)
@@ -9698,10 +9675,10 @@ snapshots:
safer-buffer@2.1.2: {}
sass@1.93.2:
sass@1.94.2:
dependencies:
chokidar: 4.0.3
immutable: 5.1.3
immutable: 5.1.4
source-map-js: 1.2.1
optionalDependencies:
'@parcel/watcher': 2.5.1
@@ -9758,14 +9735,14 @@ snapshots:
interpret: 1.4.0
rechoir: 0.6.2
shiki@3.15.0:
shiki@3.17.0:
dependencies:
'@shikijs/core': 3.15.0
'@shikijs/engine-javascript': 3.15.0
'@shikijs/engine-oniguruma': 3.15.0
'@shikijs/langs': 3.15.0
'@shikijs/themes': 3.15.0
'@shikijs/types': 3.15.0
'@shikijs/core': 3.17.0
'@shikijs/engine-javascript': 3.17.0
'@shikijs/engine-oniguruma': 3.17.0
'@shikijs/langs': 3.17.0
'@shikijs/themes': 3.17.0
'@shikijs/types': 3.17.0
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.4
@@ -9859,16 +9836,16 @@ snapshots:
markdown-it-task-checkbox: 1.0.6
markdown-it-ts: 0.0.2-beta.3
stream-markdown@0.0.10(shiki@3.15.0):
stream-markdown@0.0.10(shiki@3.17.0):
dependencies:
shiki: 3.15.0
shiki: 3.17.0
stream-monaco@0.0.6(monaco-editor@0.52.2):
stream-monaco@0.0.7(monaco-editor@0.52.2):
dependencies:
'@shikijs/monaco': 3.15.0
'@shikijs/monaco': 3.17.0
alien-signals: 2.0.8
monaco-editor: 0.52.2
shiki: 3.15.0
shiki: 3.17.0
string-argv@0.3.2: {}
@@ -10230,13 +10207,13 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
vite-plugin-vue-setup-extend@0.4.0(vite@7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)):
vite-plugin-vue-setup-extend@0.4.0(vite@7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)):
dependencies:
'@vue/compiler-sfc': 3.5.13
magic-string: 0.25.9
vite: 7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)
vite: 7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)
vite@7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1):
vite@7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1):
dependencies:
esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.3)
@@ -10248,15 +10225,15 @@ snapshots:
'@types/node': 24.10.0
fsevents: 2.3.3
jiti: 2.6.1
sass: 1.93.2
sass: 1.94.2
terser: 5.37.0
tsx: 4.19.2
yaml: 2.8.1
vitest@4.0.8(@types/node@24.10.0)(@vitest/ui@4.0.8)(happy-dom@20.0.2)(jiti@2.6.1)(sass@1.93.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1):
vitest@4.0.8(@types/node@24.10.0)(@vitest/ui@4.0.8)(happy-dom@20.0.2)(jiti@2.6.1)(sass@1.94.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1):
dependencies:
'@vitest/expect': 4.0.8
'@vitest/mocker': 4.0.8(vite@7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1))
'@vitest/mocker': 4.0.8(vite@7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1))
'@vitest/pretty-format': 4.0.8
'@vitest/runner': 4.0.8
'@vitest/snapshot': 4.0.8
@@ -10273,7 +10250,7 @@ snapshots:
tinyexec: 0.3.2
tinyglobby: 0.2.15
tinyrainbow: 3.0.3
vite: 7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)
vite: 7.2.4(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 24.10.0
@@ -10338,7 +10315,7 @@ snapshots:
dependencies:
vue: 3.5.25(typescript@5.9.3)
vue-renderer-markdown@0.0.62-beta.1(katex@0.16.25)(mermaid@11.12.1)(shiki@3.15.0)(stream-markdown@0.0.10(shiki@3.15.0))(stream-monaco@0.0.6(monaco-editor@0.52.2))(vue-i18n@11.1.12(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3)):
vue-renderer-markdown@0.0.62-beta.1(katex@0.16.25)(mermaid@11.12.1)(shiki@3.17.0)(stream-markdown@0.0.10(shiki@3.17.0))(stream-monaco@0.0.7(monaco-editor@0.52.2))(vue-i18n@11.1.12(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3)):
dependencies:
'@floating-ui/dom': 1.7.4
stream-markdown-parser: 0.0.19
@@ -10346,9 +10323,9 @@ snapshots:
optionalDependencies:
katex: 0.16.25
mermaid: 11.12.1
shiki: 3.15.0
stream-markdown: 0.0.10(shiki@3.15.0)
stream-monaco: 0.0.6(monaco-editor@0.52.2)
shiki: 3.17.0
stream-markdown: 0.0.10(shiki@3.17.0)
stream-monaco: 0.0.7(monaco-editor@0.52.2)
vue-i18n: 11.1.12(vue@3.5.25(typescript@5.9.3))
vue-resize@2.0.0-alpha.1(vue@3.5.25(typescript@5.9.3)):
@@ -10360,10 +10337,10 @@ snapshots:
'@vue/devtools-api': 6.6.4
vue: 3.5.25(typescript@5.9.3)
vue-tsc@3.1.3(typescript@5.9.3):
vue-tsc@3.1.5(typescript@5.9.3):
dependencies:
'@volar/typescript': 2.4.23
'@vue/language-core': 3.1.3(typescript@5.9.3)
'@vue/language-core': 3.1.5(typescript@5.9.3)
typescript: 5.9.3
vue-virtual-scroller@2.0.0-beta.8(vue@3.5.25(typescript@5.9.3)):

View File

@@ -109,7 +109,6 @@ import { isDesktop } from '@/utils/PlatformConstants'
import { useBotStore } from '@/stores/bot'
import { useAssistantModelPresets, type AssistantModelPreset } from '@/hooks/useAssistantModelPresets'
import HuLaAssistant from './HuLaAssistant.vue'
import 'github-markdown-css/github-markdown.css'
// 当前语言
const currentLang = ref<'zh' | 'en'>('zh')

View File

@@ -23,6 +23,7 @@
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
type LocationData = {
latitude: number
longitude: number
@@ -51,6 +52,7 @@ const props = withDefaults(defineProps<LocationMapProps>(), {
})
const emit = defineEmits<LocationMapEmits>()
const { t } = useI18n()
// 地图实例和状态
const mapRef = ref()
@@ -91,7 +93,7 @@ const markerGeometries = computed(() => [
id: 'current',
styleId: 'current-location',
position: mapCenter.value,
properties: { title: '当前位置' },
properties: { title: t('message.location.map.marker_current') },
draggable: props.draggable
}
])

View File

@@ -25,11 +25,15 @@
<!-- 地图加载错误 -->
<div v-if="mapError" class="h-340px flex-center">
<n-result status="error" title="地图加载失败" :description="mapError">
<n-result status="error" :title="t('message.location.modal.result.map_error_title')" :description="mapError">
<template #footer>
<n-flex justify="center" :size="12">
<n-button secondary @click="modalVisible = false">取消</n-button>
<n-button type="primary" secondary @click="retryMapLoad">重试</n-button>
<n-button secondary @click="modalVisible = false">
{{ t('message.location.modal.buttons.cancel') }}
</n-button>
<n-button type="primary" secondary @click="retryMapLoad">
{{ t('message.location.modal.buttons.retry') }}
</n-button>
</n-flex>
</template>
</n-result>
@@ -37,11 +41,18 @@
<!-- 位置获取失败 -->
<div v-else-if="locationState.error && !selectedLocation" class="h-340px flex-center">
<n-result status="warning" title="位置获取失败" :description="locationState.error">
<n-result
status="warning"
:title="t('message.location.modal.result.location_error_title')"
:description="locationState.error">
<template #footer>
<n-flex justify="center" :size="12">
<n-button secondary @click="modalVisible = false">取消</n-button>
<n-button type="primary" secondary @click="relocate">重试</n-button>
<n-button secondary @click="modalVisible = false">
{{ t('message.location.modal.buttons.cancel') }}
</n-button>
<n-button type="primary" secondary @click="relocate">
{{ t('message.location.modal.buttons.retry') }}
</n-button>
</n-flex>
</template>
</n-result>
@@ -54,7 +65,13 @@
<!-- 地图加载中 -->
<div v-if="locationState.loading || mapLoading" class="flex-col-center gap-42px">
<n-spin :size="42" />
<p class="text-(14px [--text-cplor])">{{ locationState.loading ? '正在获取位置...' : '地图加载中...' }}</p>
<p class="text-(14px [--text-cplor])">
{{
locationState.loading
? t('message.location.modal.loading.locating')
: t('message.location.modal.loading.map')
}}
</p>
</div>
<!-- 地图组件 -->
@@ -72,12 +89,17 @@
<!-- 位置信息显示 -->
<div v-if="selectedLocation" class="rounded-6px bg-#fefefe dark:bg-#303030 p-12px">
<n-flex vertical :size="8">
<span class="text-14px font-medium">当前位置</span>
<span class="text-14px font-medium">{{ t('message.location.modal.info.current') }}</span>
<div class="text-12px text-gray-500">
{{ selectedLocation.address || '获取地址中...' }}
{{ selectedLocation.address || t('message.location.modal.info.fetching_address') }}
</div>
<div class="text-11px text-gray-400">
坐标: {{ selectedLocation.latitude.toFixed(6) }}, {{ selectedLocation.longitude.toFixed(6) }}
{{
t('message.location.modal.info.coordinate', {
lat: selectedLocation.latitude.toFixed(6),
lng: selectedLocation.longitude.toFixed(6)
})
}}
</div>
</n-flex>
</div>
@@ -85,7 +107,9 @@
<!-- 操作按钮 -->
<n-flex v-if="showActionButtons" align="center" :size="24" class="py-8px">
<n-button type="primary" secondary :loading="sendingLocation" @click="handleConfirm">发送位置</n-button>
<n-button type="primary" secondary :loading="sendingLocation" @click="handleConfirm">
{{ t('message.location.modal.buttons.send') }}
</n-button>
</n-flex>
</div>
</n-modal>
@@ -97,6 +121,7 @@ import { reverseGeocode } from '@/services/mapApi'
import { getSettings } from '@/services/tauriCommand'
import { isMac, isWindows } from '@/utils/PlatformConstants'
import LocationMap from './LocationMap.vue'
import { useI18n } from 'vue-i18n'
type LocationData = {
latitude: number
@@ -136,11 +161,13 @@ const mapError = ref<string | null>(null)
const sendingLocation = ref(false)
const apiKey = ref('')
const { t } = useI18n()
// 计算属性
const modalTitle = computed(() => {
if (mapError.value) return '地图错误'
if (locationState.error) return '位置获取失败'
return '选择位置'
if (mapError.value) return t('message.location.modal.title.map_error')
if (locationState.error) return t('message.location.modal.title.location_error')
return t('message.location.modal.title.default')
})
const showActionButtons = computed(() => {
@@ -163,15 +190,20 @@ const getLocation = async () => {
// 设置 API key
apiKey.value = settings.tencent?.map_key || ''
if (!apiKey.value) {
throw new Error('腾讯地图API密钥未配置')
const missingKeyMsg = t('message.location.modal.errors.missing_api_key')
mapError.value = missingKeyMsg
throw new Error(missingKeyMsg)
}
// 获取地址信息
const geocodeResult = await reverseGeocode(result.transformed.lat, result.transformed.lng).catch((error) => {
console.warn('获取地址失败:', error)
console.warn(t('message.location.modal.errors.geocode_failed'), error)
return null
})
const address = geocodeResult?.formatted_addresses?.recommend || geocodeResult?.address || '未知地址'
const address =
geocodeResult?.formatted_addresses?.recommend ||
geocodeResult?.address ||
t('message.location.modal.info.unknown_address')
selectedLocation.value = {
latitude: result.transformed.lat,
@@ -216,7 +248,7 @@ const handleLocationChange = async (newLocation: { lat: number; lng: number }) =
// 获取新位置的地址
const geocodeResult = await reverseGeocode(newLocation.lat, newLocation.lng).catch((error) => {
console.warn('获取地址失败:', error)
console.warn(t('message.location.modal.errors.geocode_failed'), error)
return null
})
const address =

View File

@@ -455,7 +455,7 @@ export const useChatMain = (isHistoryMode = false, options: UseChatMainOptions =
},
{
label: () => (isMac() ? t('menu.show_in_finder') : t('menu.open_folder')),
label: () => (isMac() ? t('menu.show_in_finder') : t('menu.show_in_folder')),
icon: 'file2',
click: async (item: RightMouseMessageItem) => {
console.log('打开文件夹的item项', item)
@@ -655,7 +655,7 @@ export const useChatMain = (isHistoryMode = false, options: UseChatMainOptions =
},
{
label: () => (isMac() ? t('menu.show_in_finder') : t('menu.open_folder')),
label: () => (isMac() ? t('menu.show_in_finder') : t('menu.show_in_folder')),
icon: 'file2',
click: async (item: RightMouseMessageItem) => {
console.log('打开文件夹的item项', item)
@@ -750,7 +750,7 @@ export const useChatMain = (isHistoryMode = false, options: UseChatMainOptions =
}
},
{
label: () => (isMac() ? t('menu.show_in_finder') : t('menu.open_folder')),
label: () => (isMac() ? t('menu.show_in_finder') : t('menu.show_in_folder')),
icon: 'file2',
click: async (item: MessageType) => {
const fileUrl = item.message.body.url || item.message.body.content

View File

@@ -1,4 +1,5 @@
import { transformCoordinates } from '@/services/mapApi'
import { useI18n } from 'vue-i18n'
type GeolocationState = {
loading: boolean
@@ -15,6 +16,7 @@ type GeolocationOptions = {
}
export const useGeolocation = () => {
const { t } = useI18n()
const state = ref<GeolocationState>({
loading: false,
error: null,
@@ -37,7 +39,7 @@ export const useGeolocation = () => {
state.value.permission = permission.state
return permission.state
} catch (error) {
console.warn('检查地理位置权限失败:', error)
console.warn(t('message.location.hook.permission_check_failed'), error)
}
}
return 'prompt'
@@ -47,7 +49,8 @@ export const useGeolocation = () => {
const getCurrentPosition = async (options?: GeolocationOptions): Promise<GeolocationPosition> => {
return new Promise((resolve, reject) => {
if (!navigator.geolocation) {
reject(new Error('浏览器不支持地理位置功能'))
const unsupportedError = t('message.location.hook.unsupported')
reject(new Error(unsupportedError))
return
}
@@ -69,17 +72,17 @@ export const useGeolocation = () => {
},
(error) => {
state.value.loading = false
let errorMessage = '获取位置失败'
let errorMessage = t('message.location.hook.error_generic')
switch (error.code) {
case error.PERMISSION_DENIED:
errorMessage = '位置权限被拒绝'
errorMessage = t('message.location.hook.permission_denied')
break
case error.POSITION_UNAVAILABLE:
errorMessage = '位置信息不可用'
errorMessage = t('message.location.hook.position_unavailable')
break
case error.TIMEOUT:
errorMessage = '获取位置超时'
errorMessage = t('message.location.hook.timeout')
break
}

View File

@@ -861,6 +861,7 @@
</template>
<script setup lang="ts">
import { convertFileSrc } from '@tauri-apps/api/core'
import { fetch as nativeFetch } from '@tauri-apps/plugin-http'
import { type InputInst, UploadFileInfo } from 'naive-ui'
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
import { Icon } from '@iconify/vue'
@@ -1026,70 +1027,123 @@ const conversationTokens = computed(() => {
})
const serverTokenUsage = ref<number | null>(null)
type ImageWorkerResponse = {
success: boolean
url: string
buffer?: ArrayBuffer
error?: string
}
const aiMediaDownloadTasks = new Map<string, Promise<ArrayBuffer>>()
type ImageWorkerWaiter = {
resolve: (buffer: ArrayBuffer) => void
reject: (reason?: unknown) => void
}
// 将各种可能的二进制返回形式统一转成 ArrayBuffer兼容 plugin-http/浏览器 fetch 等场景
const convertHttpDataToArrayBuffer = (rawData: unknown): ArrayBuffer => {
if (rawData === null || rawData === undefined) {
throw new Error('图片数据为空')
}
const aiImageWorkerRequests = new Map<string, ImageWorkerWaiter[]>()
let aiImageDownloadWorker: Worker | null = null
const aiImageWorkerUrl = new URL('../../../workers/imageDownloader.ts', import.meta.url)
if (rawData instanceof ArrayBuffer) {
return rawData
}
const ensureAiImageWorker = () => {
if (aiImageDownloadWorker || typeof window === 'undefined') return
aiImageDownloadWorker = new Worker(aiImageWorkerUrl, { type: 'module' })
aiImageDownloadWorker.onmessage = (event: MessageEvent<ImageWorkerResponse>) => {
const { url, success, buffer, error } = event.data
const waiters = aiImageWorkerRequests.get(url)
if (!waiters?.length) return
aiImageWorkerRequests.delete(url)
if (!success || !buffer) {
waiters.forEach(({ reject }) => reject(new Error(error || '下载失败')))
return
if (rawData instanceof Uint8Array) {
return rawData.slice().buffer
}
if (ArrayBuffer.isView(rawData)) {
// 复制一份,避免 SharedArrayBuffer 类型不兼容
const view = rawData as ArrayBufferView
const copy = new Uint8Array(view.byteLength)
copy.set(new Uint8Array(view.buffer, view.byteOffset, view.byteLength))
return copy.buffer
}
if (Array.isArray(rawData)) {
return Uint8Array.from(rawData).buffer
}
if (typeof rawData === 'object') {
const maybeData = (rawData as { data?: number[] }).data
if (Array.isArray(maybeData)) {
return Uint8Array.from(maybeData).buffer
}
waiters.forEach(({ resolve }) => resolve(buffer))
}
if (typeof rawData === 'string') {
const binaryString = atob(rawData)
const len = binaryString.length
const bytes = new Uint8Array(len)
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i)
}
return bytes.buffer
}
throw new Error('无法解析图片数据')
}
const requestAiImageBuffer = (url: string) => {
ensureAiImageWorker()
if (!aiImageDownloadWorker) {
return Promise.reject(new Error('Web Worker 不可用'))
// 下载 AI 媒体的二进制数据:同 URL 复用 Promise避免重复下载按 plugin-http 的多种返回形态兜底
const requestAiMediaBuffer = (url: string) => {
if (!url) {
return Promise.reject(new Error('图片地址无效'))
}
const waiters = aiImageWorkerRequests.get(url)
if (waiters && waiters.length > 0) {
return new Promise<ArrayBuffer>((resolve, reject) => {
waiters.push({ resolve, reject })
const existingTask = aiMediaDownloadTasks.get(url)
if (existingTask) {
return existingTask
}
const downloadTask = (async () => {
const response = await nativeFetch(url, {
method: 'GET'
})
}
return new Promise<ArrayBuffer>((resolve, reject) => {
aiImageWorkerRequests.set(url, [{ resolve, reject }])
aiImageDownloadWorker!.postMessage({ url })
const anyResponse = response as any
const status = typeof anyResponse.status === 'number' ? anyResponse.status : 200
const statusText = typeof anyResponse.statusText === 'string' ? anyResponse.statusText : ''
const ok = 'ok' in anyResponse ? Boolean(anyResponse.ok) : status >= 200 && status < 400
if (!ok) {
throw new Error(`下载失败: ${status} ${statusText}`.trim())
}
// 优先使用标准的 arrayBuffer
if (typeof anyResponse.arrayBuffer === 'function') {
const buffer = await anyResponse.arrayBuffer()
if (buffer instanceof ArrayBuffer) {
return buffer
}
}
// plugin-http 额外提供的 bytes 方法
if (typeof anyResponse.bytes === 'function') {
const bytes = await anyResponse.bytes()
return convertHttpDataToArrayBuffer(bytes)
}
// 最后回退 data 字段
if ('data' in anyResponse) {
return convertHttpDataToArrayBuffer(anyResponse.data)
}
throw new Error('无法解析图片数据')
})().finally(() => {
aiMediaDownloadTasks.delete(url)
})
aiMediaDownloadTasks.set(url, downloadTask)
return downloadTask
}
const getAiImageExtension = (url: string) => {
// 解析媒体扩展名,默认回退为指定后缀
const getAiMediaExtension = (url: string, fallback = 'png') => {
const cleanUrl = url.split(/[?#]/)[0] || ''
const ext = cleanUrl.split('.').pop() || ''
if (!ext || ext.length > 5 || ext.includes('/')) return 'png'
if (!ext || ext.length > 5 || ext.includes('/')) return fallback
return ext
}
const buildAiImageFileName = async (url: string) => {
const ext = getAiImageExtension(url)
const buildAiMediaFileName = async (url: string, fallbackExt: string, prefix: string) => {
const ext = getAiMediaExtension(url, fallbackExt)
try {
const hash = await md5FromString(url)
return `${hash}.${ext}`
return `${prefix}-${hash}.${ext}`
} catch (error) {
console.error('生成 AI 图片文件名失败:', error)
return `ai-image-${Date.now()}.${ext}`
console.error('生成 AI 媒体文件名失败:', error)
return `${prefix}-${Date.now()}.${ext}`
}
}
@@ -1102,7 +1156,7 @@ const ensureLocalAiImage = async (remoteUrl: string, messageIndex: number) => {
: targetMessage.content === remoteUrl
if (!isSameImage) return
try {
const fileName = await buildAiImageFileName(remoteUrl)
const fileName = await buildAiMediaFileName(remoteUrl, 'png', 'ai-image')
const existsResult = await resolveAiImagePath({
userUid: userStore.userInfo.uid,
conversationId: currentChat.value.id,
@@ -1110,7 +1164,7 @@ const ensureLocalAiImage = async (remoteUrl: string, messageIndex: number) => {
})
let absolutePath = existsResult.absolutePath
if (!existsResult.exists) {
const buffer = await requestAiImageBuffer(remoteUrl)
const buffer = await requestAiMediaBuffer(remoteUrl)
const data = new Uint8Array(buffer)
const saved = await persistAiImageFile({
userUid: userStore.userInfo.uid,
@@ -1130,6 +1184,82 @@ const ensureLocalAiImage = async (remoteUrl: string, messageIndex: number) => {
}
}
// 视频本地化:与图片同目录策略,下载后替换展示 URL
const ensureLocalAiVideo = async (remoteUrl: string, messageIndex: number) => {
if (!remoteUrl || !userStore.userInfo?.uid || !currentChat.value.id) return
const targetMessage = messageList.value[messageIndex]
if (!targetMessage || targetMessage.type !== 'assistant') return
const isSameVideo = targetMessage.videoUrl
? targetMessage.videoUrl === remoteUrl
: targetMessage.content === remoteUrl
if (!isSameVideo) return
try {
const fileName = await buildAiMediaFileName(remoteUrl, 'mp4', 'ai-video')
const existsResult = await resolveAiImagePath({
userUid: userStore.userInfo.uid,
conversationId: currentChat.value.id,
fileName
})
let absolutePath = existsResult.absolutePath
if (!existsResult.exists) {
const buffer = await requestAiMediaBuffer(remoteUrl)
const data = new Uint8Array(buffer)
const saved = await persistAiImageFile({
userUid: userStore.userInfo.uid,
conversationId: currentChat.value.id,
fileName,
data
})
absolutePath = saved.absolutePath
}
if (messageList.value[messageIndex]) {
const displayUrl = convertFileSrc(absolutePath)
messageList.value[messageIndex].content = displayUrl
messageList.value[messageIndex].videoUrl = remoteUrl
}
} catch (error) {
console.error('AI 视频本地化失败:', error)
}
}
// 音频本地化:与图片同目录策略,下载后替换展示 URL
const ensureLocalAiAudio = async (remoteUrl: string, messageIndex: number) => {
if (!remoteUrl || !userStore.userInfo?.uid || !currentChat.value.id) return
const targetMessage = messageList.value[messageIndex]
if (!targetMessage || targetMessage.type !== 'assistant') return
const isSameAudio = targetMessage.audioUrl
? targetMessage.audioUrl === remoteUrl
: targetMessage.content === remoteUrl
if (!isSameAudio) return
try {
const fileName = await buildAiMediaFileName(remoteUrl, 'mp3', 'ai-audio')
const existsResult = await resolveAiImagePath({
userUid: userStore.userInfo.uid,
conversationId: currentChat.value.id,
fileName
})
let absolutePath = existsResult.absolutePath
if (!existsResult.exists) {
const buffer = await requestAiMediaBuffer(remoteUrl)
const data = new Uint8Array(buffer)
const saved = await persistAiImageFile({
userUid: userStore.userInfo.uid,
conversationId: currentChat.value.id,
fileName,
data
})
absolutePath = saved.absolutePath
}
if (messageList.value[messageIndex]) {
const displayUrl = convertFileSrc(absolutePath)
messageList.value[messageIndex].content = displayUrl
messageList.value[messageIndex].audioUrl = remoteUrl
}
} catch (error) {
console.error('AI 音频本地化失败:', error)
}
}
const getMessageBubbleClass = (message: Message) => {
if (message.type === 'assistant' && isRenderableAiImage(message)) {
return []
@@ -1946,6 +2076,8 @@ const pollVideoStatus = async (
}
}
void ensureLocalAiVideo(video.videoUrl, messageIndex)
window.$message.success('视频生成成功')
scrollToBottom()
@@ -2076,6 +2208,8 @@ const pollAudioStatus = async (audioId: number, messageIndex: number, prompt: st
}
}
void ensureLocalAiAudio(audio.audioUrl, messageIndex)
window.$message.success('音频生成成功')
scrollToBottom()
@@ -2361,9 +2495,20 @@ const loadMessages = async (conversationId: string) => {
if (userStore.userInfo?.uid && currentChat.value.id) {
void Promise.all(
messageList.value.map((msg, index) => {
if (msg.type !== 'assistant' || msg.msgType !== AiMsgContentTypeEnum.IMAGE) return Promise.resolve()
const remoteUrl = msg.imageUrl || msg.content
return ensureLocalAiImage(remoteUrl, index)
if (msg.type !== 'assistant') return Promise.resolve()
if (msg.msgType === AiMsgContentTypeEnum.IMAGE) {
const remoteUrl = msg.imageUrl || msg.content
return ensureLocalAiImage(remoteUrl, index)
}
if (msg.msgType === AiMsgContentTypeEnum.VIDEO) {
const remoteUrl = msg.videoUrl || msg.content
return ensureLocalAiVideo(remoteUrl, index)
}
if (msg.msgType === AiMsgContentTypeEnum.AUDIO) {
const remoteUrl = msg.audioUrl || msg.content
return ensureLocalAiAudio(remoteUrl, index)
}
return Promise.resolve()
})
)
}
@@ -2678,9 +2823,6 @@ onUnmounted(() => {
useMitt.off('refresh-model-list', handleRefreshModelList)
useMitt.off('open-generation-history', handleOpenHistory)
useMitt.off('left-chat-title', handleLeftChatTitle)
aiImageDownloadWorker?.terminate()
aiImageDownloadWorker = null
aiImageWorkerRequests.clear()
})
// 监听会话切换,停止旧会话的轮询任务

View File

@@ -8,7 +8,7 @@
:hidden-right="true"
:enable-default-background="false"
:enable-shadow="false"
room-name="管理群成员" />
:room-name="t('home.manage_group_member.title')" />
<!-- 顶部搜索框 -->
<div class="px-16px mt-10px flex gap-3">
@@ -16,7 +16,7 @@
<n-input
v-model:value="keyword"
class="rounded-10px w-full bg-gray-100 relative text-14px"
placeholder="搜索成员~"
:placeholder="t('home.manage_group_member.search_placeholder_mobile')"
clearable
spellCheck="false"
autoComplete="off"
@@ -61,7 +61,11 @@
</span>
<div class="text-12px text-gray-500 flex items-center gap-4px truncate">
<n-badge :color="item.activeStatus === OnlineEnum.ONLINE ? '#1ab292' : '#909090'" dot />
{{ item.activeStatus === OnlineEnum.ONLINE ? '在线' : '离线' }}
{{
item.activeStatus === OnlineEnum.ONLINE
? t('home.friends_list.status.online')
: t('home.friends_list.status.offline')
}}
</div>
</div>
</div>
@@ -74,9 +78,11 @@
<!-- 底部操作栏 -->
<div class="px-16px py-10px bg-white border-t border-gray-200 flex justify-between items-center">
<span class="text-14px">已选择 {{ selectedList.length }} </span>
<span class="text-14px">
{{ t('home.manage_group_member.selected_count', { count: selectedList.length }) }}
</span>
<n-button type="error" :disabled="selectedList.length === 0" :loading="isLoading" @click="handleMobileRemove">
踢出群聊
{{ t('home.manage_group_member.remove_button') }}
</n-button>
</div>
</div>
@@ -87,7 +93,7 @@
<!-- PC端头部 -->
<div class="flex-shrink-0">
<div class="flex items-center justify-between px-20px py-16px border-b border-[--line-color]">
<h2 class="text-16px font-medium text-[--text-color]">管理群成员</h2>
<h2 class="text-16px font-medium text-[--text-color]">{{ t('home.manage_group_member.title') }}</h2>
</div>
</div>
@@ -101,7 +107,7 @@
autoCorrect="off"
autoCapitalize="off"
class="border-(solid 1px [--line-color]) rounded-8px"
placeholder="搜索群成员">
:placeholder="t('home.manage_group_member.search_placeholder_pc')">
<template #prefix>
<svg class="w-12px h-12px"><use href="#search"></use></svg>
</template>
@@ -141,7 +147,11 @@
</span>
<div class="text-11px text-[--chat-text-color] flex items-center gap-4px truncate">
<n-badge :color="item.activeStatus === OnlineEnum.ONLINE ? '#1ab292' : '#909090'" dot />
{{ item.activeStatus === OnlineEnum.ONLINE ? '在线' : '离线' }}
{{
item.activeStatus === OnlineEnum.ONLINE
? t('home.friends_list.status.online')
: t('home.friends_list.status.offline')
}}
</div>
</div>
</div>
@@ -154,14 +164,20 @@
<!-- 底部操作栏 -->
<div class="px-20px py-12px bg-[--bg-popover] border-t border-[--line-color] flex justify-between items-center">
<span class="text-13px text-[--text-color]">已选择 {{ selectedList.length }} </span>
<span class="text-13px text-[--text-color]">
{{ t('home.manage_group_member.selected_count', { count: selectedList.length }) }}
</span>
<n-popconfirm v-model:show="showDeleteConfirm">
<template #icon>
<svg class="size-22px"><use href="#explosion"></use></svg>
</template>
<template #action>
<n-button size="small" tertiary @click.stop="showDeleteConfirm = false">取消</n-button>
<n-button size="small" type="error" @click.stop="handleRemove">确定</n-button>
<n-button size="small" tertiary @click.stop="showDeleteConfirm = false">
{{ t('home.manage_group_member.dialog_negative') }}
</n-button>
<n-button size="small" type="error" @click.stop="handleRemove">
{{ t('home.manage_group_member.dialog_positive') }}
</n-button>
</template>
<template #trigger>
<n-button
@@ -170,10 +186,10 @@
type="error"
:disabled="selectedList.length === 0"
:loading="isLoading">
踢出群聊
{{ t('home.manage_group_member.remove_button') }}
</n-button>
</template>
确定要踢出 {{ selectedList.length }} 位成员吗
{{ t('home.manage_group_member.remove_confirm', { count: selectedList.length }) }}
</n-popconfirm>
</div>
</div>
@@ -188,6 +204,7 @@ import { useGroupStore } from '@/stores/group'
import { useGlobalStore } from '@/stores/global'
import { AvatarUtils } from '@/utils/AvatarUtils'
import router from '@/router'
import { useI18n } from 'vue-i18n'
defineOptions({
name: 'ManageGroupMember'
@@ -200,6 +217,7 @@ const emit = defineEmits<{
const groupStore = useGroupStore()
const globalStore = useGlobalStore()
const dialog = useDialog()
const { t } = useI18n()
const keyword = ref('')
const selectedList = ref<string[]>([])
@@ -248,12 +266,12 @@ const handleClose = () => {
const validateRemoval = () => {
if (globalStore.currentSessionRoomId === '1') {
window.$message.warning('频道不允许踢出成员')
window.$message.warning(t('home.manage_group_member.channel_not_allowed'))
return false
}
if (selectedList.value.length === 0) {
window.$message.warning('请选择要踢出的成员')
window.$message.warning(t('home.manage_group_member.select_member_warning'))
return false
}
@@ -275,13 +293,13 @@ const handleRemove = async () => {
try {
await groupStore.removeGroupMembers(members, globalStore.currentSessionRoomId)
window.$message.success(`成功踢出 ${count} 位成员`)
window.$message.success(t('home.manage_group_member.remove_success', { count }))
selectedList.value = []
handleClose()
return true
} catch (error) {
console.error('踢出失败:', error)
window.$message.error('踢出失败,请重试')
window.$message.error(t('home.manage_group_member.remove_failed'))
return false
} finally {
isLoading.value = false
@@ -293,10 +311,10 @@ const handleMobileRemove = () => {
if (!validateRemoval()) return
dialog.warning({
title: '确认踢出',
content: `确定要踢出 ${selectedList.value.length} 位成员吗?`,
positiveText: '确定',
negativeText: '取消',
title: t('home.manage_group_member.confirm_remove_title'),
content: t('home.manage_group_member.remove_confirm', { count: selectedList.value.length }),
positiveText: t('home.manage_group_member.dialog_positive'),
negativeText: t('home.manage_group_member.dialog_negative'),
onPositiveClick: handleRemove
})
}
@@ -313,7 +331,7 @@ const calculateScrollHeight = () => {
onMounted(async () => {
// 如果是频道roomId === '1'),直接返回上一页
if (globalStore.currentSessionRoomId === '1') {
window.$message.warning('频道不支持管理成员')
window.$message.warning(t('home.manage_group_member.channel_manage_unsupported'))
handleClose()
return
}

View File

@@ -10,7 +10,9 @@
@click="toggleStatus(item)"
class="p-6px rounded-4px hover:bg-[--tray-hover]">
<img class="size-14px" :src="item.url" alt="" />
<span>{{ item.title }}</span>
<span class="flex-1 overflow-hidden text-ellipsis whitespace-nowrap">
{{ translateStateTitle(item.title) }}
</span>
</n-flex>
<n-flex
@click="createWebviewWindow(t('message.tray.online_status_window_title'), 'onlineStatus', 320, 480)"
@@ -21,7 +23,7 @@
<svg class="size-14px">
<use href="#more"></use>
</svg>
<span>{{ t('message.tray.more_status') }}</span>
<span class="flex-1 overflow-hidden text-ellipsis whitespace-nowrap">{{ t('message.tray.more_status') }}</span>
</n-flex>
<component :is="division" />
@@ -106,6 +108,13 @@ const division = () => {
return <div class={'h-1px bg-[--line-color] w-full'}></div>
}
const translateStateTitle = (title?: string) => {
if (!title) return ''
const key = `auth.onlineStatus.states.${title}`
const translated = t(key)
return translated === key ? title : translated
}
const handleExit = () => {
/** 退出时关闭锁屏 */
lockScreen.value.enable = false