Commit 92a4f93e authored by tyyin lan's avatar tyyin lan
parents ad2150ea ace0b833
...@@ -16,6 +16,8 @@ pipeline { ...@@ -16,6 +16,8 @@ pipeline {
pnpm install pnpm install
pnpm run build:sit pnpm run build:sit
cp -r ./dist ./build cp -r ./dist ./build
cd build
ls
''' '''
} }
} }
......
...@@ -4,6 +4,8 @@ FROM nexus3.gsstcloud.com:8092/ddl/ddl-fe-base:latest ...@@ -4,6 +4,8 @@ FROM nexus3.gsstcloud.com:8092/ddl/ddl-fe-base:latest
## Remove default nginx website ## Remove default nginx website
RUN rm -rf /usr/share/nginx/html/* RUN rm -rf /usr/share/nginx/html/*
RUn ls
## From 'builder' stage copy over the artifacts in dist folder to default nginx public folder ## From 'builder' stage copy over the artifacts in dist folder to default nginx public folder
COPY dist/ /usr/share/nginx/html COPY dist/ /usr/share/nginx/html
......
...@@ -4,7 +4,7 @@ import checker from 'vite-plugin-checker' ...@@ -4,7 +4,7 @@ import checker from 'vite-plugin-checker'
import { visualizer } from 'rollup-plugin-visualizer' import { visualizer } from 'rollup-plugin-visualizer'
import AutoImport from 'unplugin-auto-import/vite' import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite' import Components from 'unplugin-vue-components/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers' import { NaiveUiResolver, VantResolver } from 'unplugin-vue-components/resolvers'
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite' import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
import UnoCSS from 'unocss/vite' import UnoCSS from 'unocss/vite'
import VueJsx from '@vitejs/plugin-vue-jsx' import VueJsx from '@vitejs/plugin-vue-jsx'
...@@ -20,7 +20,7 @@ export function setupPlugins(isBuild: boolean, envConf: ViteEnv, pathResolve: (d ...@@ -20,7 +20,7 @@ export function setupPlugins(isBuild: boolean, envConf: ViteEnv, pathResolve: (d
], ],
}), }),
Components({ Components({
resolvers: [NaiveUiResolver()], resolvers: [NaiveUiResolver(), VantResolver()],
}), }),
VueI18nPlugin({ VueI18nPlugin({
include: [pathResolve('./src/locales/langs/**')], include: [pathResolve('./src/locales/langs/**')],
......
...@@ -44,6 +44,7 @@ ...@@ -44,6 +44,7 @@
"spark-md5": "^3.0.2", "spark-md5": "^3.0.2",
"type-fest": "^4.26.1", "type-fest": "^4.26.1",
"validator": "^13.12.0", "validator": "^13.12.0",
"vant": "^4.9.18",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-i18n": "^9.14.0", "vue-i18n": "^9.14.0",
"vue-router": "^4.4.5" "vue-router": "^4.4.5"
...@@ -63,6 +64,8 @@ ...@@ -63,6 +64,8 @@
"@types/validator": "^13.12.2", "@types/validator": "^13.12.2",
"@typescript-eslint/parser": "^7.18.0", "@typescript-eslint/parser": "^7.18.0",
"@unocss/eslint-config": "^0.61.9", "@unocss/eslint-config": "^0.61.9",
"@unocss/postcss": "66.1.0-beta.10",
"@unocss/transformer-directives": "66.1.0-beta.10",
"@vitejs/plugin-vue": "^4.6.2", "@vitejs/plugin-vue": "^4.6.2",
"@vitejs/plugin-vue-jsx": "^4.0.1", "@vitejs/plugin-vue-jsx": "^4.0.1",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
...@@ -77,6 +80,7 @@ ...@@ -77,6 +80,7 @@
"naive-ui": "^2.39.0", "naive-ui": "^2.39.0",
"postcss": "^8.4.47", "postcss": "^8.4.47",
"postcss-html": "^1.7.0", "postcss-html": "^1.7.0",
"postcss-mobile-forever": "^5.0.0",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.6", "prettier-plugin-tailwindcss": "^0.6.6",
"rollup-plugin-visualizer": "^5.12.0", "rollup-plugin-visualizer": "^5.12.0",
......
...@@ -92,6 +92,9 @@ importers: ...@@ -92,6 +92,9 @@ importers:
validator: validator:
specifier: ^13.12.0 specifier: ^13.12.0
version: 13.12.0 version: 13.12.0
vant:
specifier: ^4.9.18
version: 4.9.18(vue@3.5.13(typescript@5.6.2))
vue: vue:
specifier: ^3.5.13 specifier: ^3.5.13
version: 3.5.13(typescript@5.6.2) version: 3.5.13(typescript@5.6.2)
...@@ -144,6 +147,12 @@ importers: ...@@ -144,6 +147,12 @@ importers:
'@unocss/eslint-config': '@unocss/eslint-config':
specifier: ^0.61.9 specifier: ^0.61.9
version: 0.61.9(eslint@9.10.0(jiti@2.0.0-beta.3))(typescript@5.6.2) version: 0.61.9(eslint@9.10.0(jiti@2.0.0-beta.3))(typescript@5.6.2)
'@unocss/postcss':
specifier: 66.1.0-beta.10
version: 66.1.0-beta.10(postcss@8.4.47)
'@unocss/transformer-directives':
specifier: 66.1.0-beta.10
version: 66.1.0-beta.10
'@vitejs/plugin-vue': '@vitejs/plugin-vue':
specifier: ^4.6.2 specifier: ^4.6.2
version: 4.6.2(vite@5.4.6(@types/node@20.16.5)(sass@1.79.1)(terser@5.39.0))(vue@3.5.13(typescript@5.6.2)) version: 4.6.2(vite@5.4.6(@types/node@20.16.5)(sass@1.79.1)(terser@5.39.0))(vue@3.5.13(typescript@5.6.2))
...@@ -186,6 +195,9 @@ importers: ...@@ -186,6 +195,9 @@ importers:
postcss-html: postcss-html:
specifier: ^1.7.0 specifier: ^1.7.0
version: 1.7.0 version: 1.7.0
postcss-mobile-forever:
specifier: ^5.0.0
version: 5.0.0(postcss@8.4.47)
prettier: prettier:
specifier: ^3.3.3 specifier: ^3.3.3
version: 3.3.3 version: 3.3.3
...@@ -959,6 +971,10 @@ packages: ...@@ -959,6 +971,10 @@ packages:
'@polka/url@1.0.0-next.25': '@polka/url@1.0.0-next.25':
resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==} resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==}
'@quansync/fs@0.1.2':
resolution: {integrity: sha512-ezIadUb1aFhwJLd++WVqVpi9rnlX8vnd4ju7saPhwLHJN1mJgOv0puePTGV+FbtSnWtwoHDT8lAm4kagDZmpCg==}
engines: {node: '>=20.0.0'}
'@rollup/pluginutils@5.1.0': '@rollup/pluginutils@5.1.0':
resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
...@@ -1172,9 +1188,16 @@ packages: ...@@ -1172,9 +1188,16 @@ packages:
resolution: {integrity: sha512-ATvZEFMQiW3/oUaaplVMBYuagEELtnLbHSYH4pUGbJ5MALAfV98mZRyk4FkKkYoMYqWLGdCylzpgMPFDOuFQlQ==} resolution: {integrity: sha512-ATvZEFMQiW3/oUaaplVMBYuagEELtnLbHSYH4pUGbJ5MALAfV98mZRyk4FkKkYoMYqWLGdCylzpgMPFDOuFQlQ==}
engines: {node: '>=14'} engines: {node: '>=14'}
'@unocss/config@66.1.0-beta.10':
resolution: {integrity: sha512-+qx4zlVA9f1cCifoVXYmiHTI1y1rIxHgpiUdb0pI8OOdFsK6j0adua/hSdlvroXAoj2MNdE8WVEEv3L6MlDOpg==}
engines: {node: '>=14'}
'@unocss/core@0.61.9': '@unocss/core@0.61.9':
resolution: {integrity: sha512-2W1YZQIWXcueGdbXU/ZCqn/8yQhWk8e8kAHFkVlbc9rictkd2UmPB9nIZ8Ii1tMwt6F0TT6vfHbLJEGCV08o2g==} resolution: {integrity: sha512-2W1YZQIWXcueGdbXU/ZCqn/8yQhWk8e8kAHFkVlbc9rictkd2UmPB9nIZ8Ii1tMwt6F0TT6vfHbLJEGCV08o2g==}
'@unocss/core@66.1.0-beta.10':
resolution: {integrity: sha512-zYn5kmrmmaEQ81+CkoRcNYaMgUePb85RCIr3oty3yc4tFtJHymHuv+4NKoWWZzfhdfVKeK2y7U9wpLcfcbIiew==}
'@unocss/eslint-config@0.61.9': '@unocss/eslint-config@0.61.9':
resolution: {integrity: sha512-TYjrfSLkRZSRBBcdKWKuZafB9XuFHqI/P/AVI5Vwqvbdq3uc/eQpRXgnYoX1jM0ds1jzOigWRWiYkJx7KhxSnw==} resolution: {integrity: sha512-TYjrfSLkRZSRBBcdKWKuZafB9XuFHqI/P/AVI5Vwqvbdq3uc/eQpRXgnYoX1jM0ds1jzOigWRWiYkJx7KhxSnw==}
engines: {node: '>=14'} engines: {node: '>=14'}
...@@ -1195,6 +1218,12 @@ packages: ...@@ -1195,6 +1218,12 @@ packages:
peerDependencies: peerDependencies:
postcss: ^8.4.21 postcss: ^8.4.21
'@unocss/postcss@66.1.0-beta.10':
resolution: {integrity: sha512-MYbdqNozge5MY/cKO9A7b1WWef7Xw9GiwPWu+RpmBs7218dfowtfIIYzVyLEKWNoFW3iy5JjVm0oYfYCBKj9wg==}
engines: {node: '>=14'}
peerDependencies:
postcss: ^8.4.21
'@unocss/preset-attributify@0.61.9': '@unocss/preset-attributify@0.61.9':
resolution: {integrity: sha512-AHlEF7PiIBz1jHZZ62+AZ1u5ITrPNL/mgN8XyKwocoAr9HH8aQ3xzUgIuEX6vfV4a8rTdawffY99BQ12msePWQ==} resolution: {integrity: sha512-AHlEF7PiIBz1jHZZ62+AZ1u5ITrPNL/mgN8XyKwocoAr9HH8aQ3xzUgIuEX6vfV4a8rTdawffY99BQ12msePWQ==}
...@@ -1226,6 +1255,10 @@ packages: ...@@ -1226,6 +1255,10 @@ packages:
resolution: {integrity: sha512-54Hw0nF+3ga70ETo3kes4He62wdsB4dHMgEiD/DEmJzyVY3ZuG/sIVAgkxjMQDo5w4SSYU/Ys1QaY+IQmeJHFQ==} resolution: {integrity: sha512-54Hw0nF+3ga70ETo3kes4He62wdsB4dHMgEiD/DEmJzyVY3ZuG/sIVAgkxjMQDo5w4SSYU/Ys1QaY+IQmeJHFQ==}
engines: {node: '>=14'} engines: {node: '>=14'}
'@unocss/rule-utils@66.1.0-beta.10':
resolution: {integrity: sha512-yGC8uQT6ErjLsWMU3Nx7mjvftYukLL+TELM+pmYOFmbhasrdnnJI1kT/04dyUHQTlm3XCs8I7LhbYaVz7MmIXA==}
engines: {node: '>=14'}
'@unocss/scope@0.61.9': '@unocss/scope@0.61.9':
resolution: {integrity: sha512-a9/vdg7YTFZEnJSaJBh/GqkLokYh3ZjEd3gHUxl/TZDSkGOz3WnkR2h+lgaLZm9MJ7RlSvJxYP8ySezH7jU1Pw==} resolution: {integrity: sha512-a9/vdg7YTFZEnJSaJBh/GqkLokYh3ZjEd3gHUxl/TZDSkGOz3WnkR2h+lgaLZm9MJ7RlSvJxYP8ySezH7jU1Pw==}
...@@ -1241,6 +1274,9 @@ packages: ...@@ -1241,6 +1274,9 @@ packages:
'@unocss/transformer-directives@0.61.9': '@unocss/transformer-directives@0.61.9':
resolution: {integrity: sha512-e4uIbHYdAYJSVpvxOv6kAsyI18X3gHkBsmBYWcUlPLVv+8tYo4eZtc0rn6ZvpiLzkFywG9e9cmpqVQwOR6pBVg==} resolution: {integrity: sha512-e4uIbHYdAYJSVpvxOv6kAsyI18X3gHkBsmBYWcUlPLVv+8tYo4eZtc0rn6ZvpiLzkFywG9e9cmpqVQwOR6pBVg==}
'@unocss/transformer-directives@66.1.0-beta.10':
resolution: {integrity: sha512-PVXVblXCVo6mjThQrTwHcoNuZvrxjkzW4qWzZQxrCiS+pQs31kod0UL9WFIUWc/xgyRgYUiLNlBBhdxDykNJjg==}
'@unocss/transformer-variant-group@0.61.9': '@unocss/transformer-variant-group@0.61.9':
resolution: {integrity: sha512-iewADYlY0LoeCb80E/4feHVSCKHl+QzGH4xUvW0zU85evMqNOa0/t0dCIoEG22wr/9piyEsg6OdHprZ2QliYqg==} resolution: {integrity: sha512-iewADYlY0LoeCb80E/4feHVSCKHl+QzGH4xUvW0zU85evMqNOa0/t0dCIoEG22wr/9piyEsg6OdHprZ2QliYqg==}
...@@ -1249,6 +1285,14 @@ packages: ...@@ -1249,6 +1285,14 @@ packages:
peerDependencies: peerDependencies:
vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0
'@vant/popperjs@1.3.0':
resolution: {integrity: sha512-hB+czUG+aHtjhaEmCJDuXOep0YTZjdlRR+4MSmIFnkCQIxJaXLQdSsR90XWvAI2yvKUI7TCGqR8pQg2RtvkMHw==}
'@vant/use@1.6.0':
resolution: {integrity: sha512-PHHxeAASgiOpSmMjceweIrv2AxDZIkWXyaczksMoWvKV2YAYEhoizRuk/xFnKF+emUIi46TsQ+rvlm/t2BBCfA==}
peerDependencies:
vue: ^3.0.0
'@vitejs/plugin-vue-jsx@4.0.1': '@vitejs/plugin-vue-jsx@4.0.1':
resolution: {integrity: sha512-7mg9HFGnFHMEwCdB6AY83cVK4A6sCqnrjFYF4WIlebYAQVVJ/sC/CiTruVdrRlhrFoeZ8rlMxY9wYpPTIRhhAg==} resolution: {integrity: sha512-7mg9HFGnFHMEwCdB6AY83cVK4A6sCqnrjFYF4WIlebYAQVVJ/sC/CiTruVdrRlhrFoeZ8rlMxY9wYpPTIRhhAg==}
engines: {node: ^18.0.0 || >=20.0.0} engines: {node: ^18.0.0 || >=20.0.0}
...@@ -1650,6 +1694,10 @@ packages: ...@@ -1650,6 +1694,10 @@ packages:
resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
css-tree@3.1.0:
resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
css-what@6.1.0: css-what@6.1.0:
resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
...@@ -1938,6 +1986,14 @@ packages: ...@@ -1938,6 +1986,14 @@ packages:
fastq@1.17.1: fastq@1.17.1:
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
fdir@6.4.3:
resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==}
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
file-entry-cache@8.0.0: file-entry-cache@8.0.0:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'} engines: {node: '>=16.0.0'}
...@@ -2269,6 +2325,10 @@ packages: ...@@ -2269,6 +2325,10 @@ packages:
resolution: {integrity: sha512-pmfRbVRs/7khFrSAYnSiJ8C0D5GvzkE4Ey2pAvUcJsw1ly/p+7ut27jbJrjY79BpAJQJ4gXYFtK6d1Aub+9baQ==} resolution: {integrity: sha512-pmfRbVRs/7khFrSAYnSiJ8C0D5GvzkE4Ey2pAvUcJsw1ly/p+7ut27jbJrjY79BpAJQJ4gXYFtK6d1Aub+9baQ==}
hasBin: true hasBin: true
jiti@2.4.2:
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
hasBin: true
js-tokens@4.0.0: js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
...@@ -2456,6 +2516,9 @@ packages: ...@@ -2456,6 +2516,9 @@ packages:
mdn-data@2.0.30: mdn-data@2.0.30:
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
mdn-data@2.12.2:
resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==}
meow@12.1.1: meow@12.1.1:
resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==}
engines: {node: '>=16.10'} engines: {node: '>=16.10'}
...@@ -2522,11 +2585,6 @@ packages: ...@@ -2522,11 +2585,6 @@ packages:
peerDependencies: peerDependencies:
vue: ^3.0.0 vue: ^3.0.0
nanoid@3.3.7:
resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
nanoid@3.3.8: nanoid@3.3.8:
resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
...@@ -2660,6 +2718,10 @@ packages: ...@@ -2660,6 +2718,10 @@ packages:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'} engines: {node: '>=8.6'}
picomatch@4.0.2:
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
engines: {node: '>=12'}
pidtree@0.6.0: pidtree@0.6.0:
resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==}
engines: {node: '>=0.10'} engines: {node: '>=0.10'}
...@@ -2687,6 +2749,11 @@ packages: ...@@ -2687,6 +2749,11 @@ packages:
postcss-media-query-parser@0.2.3: postcss-media-query-parser@0.2.3:
resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==} resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==}
postcss-mobile-forever@5.0.0:
resolution: {integrity: sha512-/kkhxMAd8kNZYeOFaTjhOx7hdfKE7AhJNU7SmlrxJV9fYqbLGrcGQMwXO4aWUvtxBseXwF2+qSMv3j/AZhitNA==}
peerDependencies:
postcss: ^8.0.0
postcss-resolve-nested-selector@0.1.6: postcss-resolve-nested-selector@0.1.6:
resolution: {integrity: sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==} resolution: {integrity: sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==}
...@@ -2810,6 +2877,9 @@ packages: ...@@ -2810,6 +2877,9 @@ packages:
resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
engines: {node: '>=0.6'} engines: {node: '>=0.6'}
quansync@0.2.10:
resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==}
queue-microtask@1.2.3: queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
...@@ -3133,6 +3203,10 @@ packages: ...@@ -3133,6 +3203,10 @@ packages:
tinyexec@0.3.0: tinyexec@0.3.0:
resolution: {integrity: sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==} resolution: {integrity: sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==}
tinyglobby@0.2.12:
resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==}
engines: {node: '>=12.0.0'}
tinymce@7.7.1: tinymce@7.7.1:
resolution: {integrity: sha512-rMetqSgZtYbj4YPOX+gYgmlhy/sIjVlI/qlrSOul/Mpn9e0aIIG/fR0qvQSVYvxFv6OzRTge++NQyTbzLJK1NA==} resolution: {integrity: sha512-rMetqSgZtYbj4YPOX+gYgmlhy/sIjVlI/qlrSOul/Mpn9e0aIIG/fR0qvQSVYvxFv6OzRTge++NQyTbzLJK1NA==}
...@@ -3208,6 +3282,9 @@ packages: ...@@ -3208,6 +3282,9 @@ packages:
unconfig@0.5.5: unconfig@0.5.5:
resolution: {integrity: sha512-VQZ5PT9HDX+qag0XdgQi8tJepPhXiR/yVOkn707gJDKo31lGjRilPREiQJ9Z6zd/Ugpv6ZvO5VxVIcatldYcNQ==} resolution: {integrity: sha512-VQZ5PT9HDX+qag0XdgQi8tJepPhXiR/yVOkn707gJDKo31lGjRilPREiQJ9Z6zd/Ugpv6ZvO5VxVIcatldYcNQ==}
unconfig@7.3.1:
resolution: {integrity: sha512-LH5WL+un92tGAzWS87k7LkAfwpMdm7V0IXG2FxEjZz/QxiIW5J5LkcrKQThj0aRz6+h/lFmKI9EUXmK/T0bcrw==}
undici-types@6.19.8: undici-types@6.19.8:
resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
...@@ -3284,6 +3361,11 @@ packages: ...@@ -3284,6 +3361,11 @@ packages:
resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==} resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==}
engines: {node: '>= 0.10'} engines: {node: '>= 0.10'}
vant@4.9.18:
resolution: {integrity: sha512-1bmWv/G0xz45btPqSasgv4TUdTqCneyFfnrQJ3xgerGvfTC5aP/rpO4wJb5FItCZjaSw5+9FNBj2Tz6n9TLXYA==}
peerDependencies:
vue: ^3.0.0
vdirs@0.1.8: vdirs@0.1.8:
resolution: {integrity: sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw==} resolution: {integrity: sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw==}
peerDependencies: peerDependencies:
...@@ -4181,6 +4263,10 @@ snapshots: ...@@ -4181,6 +4263,10 @@ snapshots:
'@polka/url@1.0.0-next.25': {} '@polka/url@1.0.0-next.25': {}
'@quansync/fs@0.1.2':
dependencies:
quansync: 0.2.10
'@rollup/pluginutils@5.1.0(rollup@4.21.3)': '@rollup/pluginutils@5.1.0(rollup@4.21.3)':
dependencies: dependencies:
'@types/estree': 1.0.5 '@types/estree': 1.0.5
...@@ -4387,8 +4473,15 @@ snapshots: ...@@ -4387,8 +4473,15 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@unocss/config@66.1.0-beta.10':
dependencies:
'@unocss/core': 66.1.0-beta.10
unconfig: 7.3.1
'@unocss/core@0.61.9': {} '@unocss/core@0.61.9': {}
'@unocss/core@66.1.0-beta.10': {}
'@unocss/eslint-config@0.61.9(eslint@9.10.0(jiti@2.0.0-beta.3))(typescript@5.6.2)': '@unocss/eslint-config@0.61.9(eslint@9.10.0(jiti@2.0.0-beta.3))(typescript@5.6.2)':
dependencies: dependencies:
'@unocss/eslint-plugin': 0.61.9(eslint@9.10.0(jiti@2.0.0-beta.3))(typescript@5.6.2) '@unocss/eslint-plugin': 0.61.9(eslint@9.10.0(jiti@2.0.0-beta.3))(typescript@5.6.2)
...@@ -4432,6 +4525,15 @@ snapshots: ...@@ -4432,6 +4525,15 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@unocss/postcss@66.1.0-beta.10(postcss@8.4.47)':
dependencies:
'@unocss/config': 66.1.0-beta.10
'@unocss/core': 66.1.0-beta.10
'@unocss/rule-utils': 66.1.0-beta.10
css-tree: 3.1.0
postcss: 8.4.47
tinyglobby: 0.2.12
'@unocss/preset-attributify@0.61.9': '@unocss/preset-attributify@0.61.9':
dependencies: dependencies:
'@unocss/core': 0.61.9 '@unocss/core': 0.61.9
...@@ -4484,6 +4586,11 @@ snapshots: ...@@ -4484,6 +4586,11 @@ snapshots:
'@unocss/core': 0.61.9 '@unocss/core': 0.61.9
magic-string: 0.30.17 magic-string: 0.30.17
'@unocss/rule-utils@66.1.0-beta.10':
dependencies:
'@unocss/core': 66.1.0-beta.10
magic-string: 0.30.17
'@unocss/scope@0.61.9': {} '@unocss/scope@0.61.9': {}
'@unocss/transformer-attributify-jsx-babel@0.61.9': '@unocss/transformer-attributify-jsx-babel@0.61.9':
...@@ -4509,6 +4616,12 @@ snapshots: ...@@ -4509,6 +4616,12 @@ snapshots:
'@unocss/rule-utils': 0.61.9 '@unocss/rule-utils': 0.61.9
css-tree: 2.3.1 css-tree: 2.3.1
'@unocss/transformer-directives@66.1.0-beta.10':
dependencies:
'@unocss/core': 66.1.0-beta.10
'@unocss/rule-utils': 66.1.0-beta.10
css-tree: 3.1.0
'@unocss/transformer-variant-group@0.61.9': '@unocss/transformer-variant-group@0.61.9':
dependencies: dependencies:
'@unocss/core': 0.61.9 '@unocss/core': 0.61.9
...@@ -4530,6 +4643,12 @@ snapshots: ...@@ -4530,6 +4643,12 @@ snapshots:
- rollup - rollup
- supports-color - supports-color
'@vant/popperjs@1.3.0': {}
'@vant/use@1.6.0(vue@3.5.13(typescript@5.6.2))':
dependencies:
vue: 3.5.13(typescript@5.6.2)
'@vitejs/plugin-vue-jsx@4.0.1(vite@5.4.6(@types/node@20.16.5)(sass@1.79.1)(terser@5.39.0))(vue@3.5.13(typescript@5.6.2))': '@vitejs/plugin-vue-jsx@4.0.1(vite@5.4.6(@types/node@20.16.5)(sass@1.79.1)(terser@5.39.0))(vue@3.5.13(typescript@5.6.2))':
dependencies: dependencies:
'@babel/core': 7.25.2 '@babel/core': 7.25.2
...@@ -4633,8 +4752,8 @@ snapshots: ...@@ -4633,8 +4752,8 @@ snapshots:
'@vue/compiler-ssr': 3.5.6 '@vue/compiler-ssr': 3.5.6
'@vue/shared': 3.5.6 '@vue/shared': 3.5.6
estree-walker: 2.0.2 estree-walker: 2.0.2
magic-string: 0.30.11 magic-string: 0.30.17
postcss: 8.4.47 postcss: 8.4.49
source-map-js: 1.2.1 source-map-js: 1.2.1
'@vue/compiler-ssr@3.5.13': '@vue/compiler-ssr@3.5.13':
...@@ -5010,6 +5129,11 @@ snapshots: ...@@ -5010,6 +5129,11 @@ snapshots:
mdn-data: 2.0.30 mdn-data: 2.0.30
source-map-js: 1.2.1 source-map-js: 1.2.1
css-tree@3.1.0:
dependencies:
mdn-data: 2.12.2
source-map-js: 1.2.1
css-what@6.1.0: {} css-what@6.1.0: {}
cssesc@3.0.0: {} cssesc@3.0.0: {}
...@@ -5338,6 +5462,10 @@ snapshots: ...@@ -5338,6 +5462,10 @@ snapshots:
dependencies: dependencies:
reusify: 1.0.4 reusify: 1.0.4
fdir@6.4.3(picomatch@4.0.2):
optionalDependencies:
picomatch: 4.0.2
file-entry-cache@8.0.0: file-entry-cache@8.0.0:
dependencies: dependencies:
flat-cache: 4.0.1 flat-cache: 4.0.1
...@@ -5621,6 +5749,8 @@ snapshots: ...@@ -5621,6 +5749,8 @@ snapshots:
jiti@2.0.0-beta.3: {} jiti@2.0.0-beta.3: {}
jiti@2.4.2: {}
js-tokens@4.0.0: {} js-tokens@4.0.0: {}
js-tokens@9.0.0: {} js-tokens@9.0.0: {}
...@@ -5796,6 +5926,8 @@ snapshots: ...@@ -5796,6 +5926,8 @@ snapshots:
mdn-data@2.0.30: {} mdn-data@2.0.30: {}
mdn-data@2.12.2: {}
meow@12.1.1: {} meow@12.1.1: {}
meow@13.2.0: {} meow@13.2.0: {}
...@@ -5867,8 +5999,6 @@ snapshots: ...@@ -5867,8 +5999,6 @@ snapshots:
vue: 3.5.13(typescript@5.6.2) vue: 3.5.13(typescript@5.6.2)
vueuc: 0.4.58(vue@3.5.13(typescript@5.6.2)) vueuc: 0.4.58(vue@3.5.13(typescript@5.6.2))
nanoid@3.3.7: {}
nanoid@3.3.8: {} nanoid@3.3.8: {}
nanoid@5.0.7: {} nanoid@5.0.7: {}
...@@ -5981,6 +6111,8 @@ snapshots: ...@@ -5981,6 +6111,8 @@ snapshots:
picomatch@2.3.1: {} picomatch@2.3.1: {}
picomatch@4.0.2: {}
pidtree@0.6.0: {} pidtree@0.6.0: {}
pinia@2.2.2(typescript@5.6.2)(vue@3.5.13(typescript@5.6.2)): pinia@2.2.2(typescript@5.6.2)(vue@3.5.13(typescript@5.6.2)):
...@@ -6001,20 +6133,24 @@ snapshots: ...@@ -6001,20 +6133,24 @@ snapshots:
dependencies: dependencies:
htmlparser2: 8.0.2 htmlparser2: 8.0.2
js-tokens: 9.0.0 js-tokens: 9.0.0
postcss: 8.4.47 postcss: 8.4.49
postcss-safe-parser: 6.0.0(postcss@8.4.47) postcss-safe-parser: 6.0.0(postcss@8.4.49)
postcss-media-query-parser@0.2.3: {} postcss-media-query-parser@0.2.3: {}
postcss-mobile-forever@5.0.0(postcss@8.4.47):
dependencies:
postcss: 8.4.47
postcss-resolve-nested-selector@0.1.6: {} postcss-resolve-nested-selector@0.1.6: {}
postcss-safe-parser@6.0.0(postcss@8.4.47): postcss-safe-parser@6.0.0(postcss@8.4.49):
dependencies: dependencies:
postcss: 8.4.47 postcss: 8.4.49
postcss-safe-parser@7.0.0(postcss@8.4.47): postcss-safe-parser@7.0.0(postcss@8.4.49):
dependencies: dependencies:
postcss: 8.4.47 postcss: 8.4.49
postcss-scss@4.0.9(postcss@8.4.47): postcss-scss@4.0.9(postcss@8.4.47):
dependencies: dependencies:
...@@ -6025,16 +6161,16 @@ snapshots: ...@@ -6025,16 +6161,16 @@ snapshots:
cssesc: 3.0.0 cssesc: 3.0.0
util-deprecate: 1.0.2 util-deprecate: 1.0.2
postcss-sorting@8.0.2(postcss@8.4.47): postcss-sorting@8.0.2(postcss@8.4.49):
dependencies: dependencies:
postcss: 8.4.47 postcss: 8.4.49
postcss-value-parser@4.2.0: {} postcss-value-parser@4.2.0: {}
postcss@8.4.47: postcss@8.4.47:
dependencies: dependencies:
nanoid: 3.3.7 nanoid: 3.3.8
picocolors: 1.1.0 picocolors: 1.1.1
source-map-js: 1.2.1 source-map-js: 1.2.1
postcss@8.4.49: postcss@8.4.49:
...@@ -6065,6 +6201,8 @@ snapshots: ...@@ -6065,6 +6201,8 @@ snapshots:
dependencies: dependencies:
side-channel: 1.1.0 side-channel: 1.1.0
quansync@0.2.10: {}
queue-microtask@1.2.3: {} queue-microtask@1.2.3: {}
readable-stream@2.3.8: readable-stream@2.3.8:
...@@ -6320,8 +6458,8 @@ snapshots: ...@@ -6320,8 +6458,8 @@ snapshots:
stylelint-order@6.0.4(stylelint@16.9.0(typescript@5.6.2)): stylelint-order@6.0.4(stylelint@16.9.0(typescript@5.6.2)):
dependencies: dependencies:
postcss: 8.4.47 postcss: 8.4.49
postcss-sorting: 8.0.2(postcss@8.4.47) postcss-sorting: 8.0.2(postcss@8.4.49)
stylelint: 16.9.0(typescript@5.6.2) stylelint: 16.9.0(typescript@5.6.2)
stylelint-scss@6.6.0(stylelint@16.9.0(typescript@5.6.2)): stylelint-scss@6.6.0(stylelint@16.9.0(typescript@5.6.2)):
...@@ -6364,9 +6502,9 @@ snapshots: ...@@ -6364,9 +6502,9 @@ snapshots:
micromatch: 4.0.8 micromatch: 4.0.8
normalize-path: 3.0.0 normalize-path: 3.0.0
picocolors: 1.1.0 picocolors: 1.1.0
postcss: 8.4.47 postcss: 8.4.49
postcss-resolve-nested-selector: 0.1.6 postcss-resolve-nested-selector: 0.1.6
postcss-safe-parser: 7.0.0(postcss@8.4.47) postcss-safe-parser: 7.0.0(postcss@8.4.49)
postcss-selector-parser: 6.1.2 postcss-selector-parser: 6.1.2
postcss-value-parser: 4.2.0 postcss-value-parser: 4.2.0
resolve-from: 5.0.0 resolve-from: 5.0.0
...@@ -6440,6 +6578,11 @@ snapshots: ...@@ -6440,6 +6578,11 @@ snapshots:
tinyexec@0.3.0: {} tinyexec@0.3.0: {}
tinyglobby@0.2.12:
dependencies:
fdir: 6.4.3(picomatch@4.0.2)
picomatch: 4.0.2
tinymce@7.7.1: {} tinymce@7.7.1: {}
to-fast-properties@2.0.0: {} to-fast-properties@2.0.0: {}
...@@ -6502,6 +6645,13 @@ snapshots: ...@@ -6502,6 +6645,13 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
unconfig@7.3.1:
dependencies:
'@quansync/fs': 0.1.2
defu: 6.1.4
jiti: 2.4.2
quansync: 0.2.10
undici-types@6.19.8: {} undici-types@6.19.8: {}
unicorn-magic@0.1.0: {} unicorn-magic@0.1.0: {}
...@@ -6514,7 +6664,7 @@ snapshots: ...@@ -6514,7 +6664,7 @@ snapshots:
estree-walker: 3.0.3 estree-walker: 3.0.3
fast-glob: 3.3.2 fast-glob: 3.3.2
local-pkg: 0.5.0 local-pkg: 0.5.0
magic-string: 0.30.11 magic-string: 0.30.17
mlly: 1.7.1 mlly: 1.7.1
pathe: 1.1.2 pathe: 1.1.2
pkg-types: 1.2.0 pkg-types: 1.2.0
...@@ -6603,7 +6753,7 @@ snapshots: ...@@ -6603,7 +6753,7 @@ snapshots:
dependencies: dependencies:
browserslist: 4.23.3 browserslist: 4.23.3
escalade: 3.2.0 escalade: 3.2.0
picocolors: 1.1.0 picocolors: 1.1.1
uri-js@4.4.1: uri-js@4.4.1:
dependencies: dependencies:
...@@ -6613,6 +6763,13 @@ snapshots: ...@@ -6613,6 +6763,13 @@ snapshots:
validator@13.12.0: {} validator@13.12.0: {}
vant@4.9.18(vue@3.5.13(typescript@5.6.2)):
dependencies:
'@vant/popperjs': 1.3.0
'@vant/use': 1.6.0(vue@3.5.13(typescript@5.6.2))
'@vue/shared': 3.5.13
vue: 3.5.13(typescript@5.6.2)
vdirs@0.1.8(vue@3.5.13(typescript@5.6.2)): vdirs@0.1.8(vue@3.5.13(typescript@5.6.2)):
dependencies: dependencies:
evtd: 0.2.4 evtd: 0.2.4
...@@ -6651,7 +6808,7 @@ snapshots: ...@@ -6651,7 +6808,7 @@ snapshots:
vite@5.4.6(@types/node@20.16.5)(sass@1.79.1)(terser@5.39.0): vite@5.4.6(@types/node@20.16.5)(sass@1.79.1)(terser@5.39.0):
dependencies: dependencies:
esbuild: 0.21.5 esbuild: 0.21.5
postcss: 8.4.47 postcss: 8.4.49
rollup: 4.21.3 rollup: 4.21.3
optionalDependencies: optionalDependencies:
'@types/node': 20.16.5 '@types/node': 20.16.5
......
export default { export default {
plugins: { plugins: {
'@unocss/postcss': {},
autoprefixer: {}, autoprefixer: {},
'postcss-mobile-forever': {
appSelector: '#app',
viewportWidth: 375,
desktopWidth: 500,
maxDisplayWidth: 500,
include: [/src\/views\/share\/share-application-mobile/, /src\/views\/share\/components\/mobile\//],
},
}, },
} }
...@@ -42,3 +42,9 @@ export function fetchUserPasswordUpdate<T>(authCode: string, password: string) { ...@@ -42,3 +42,9 @@ export function fetchUserPasswordUpdate<T>(authCode: string, password: string) {
export function fetchGoogleClientId<T>() { export function fetchGoogleClientId<T>() {
return request.post<T>('/googleConfigRest/getClientId.json') return request.post<T>('/googleConfigRest/getClientId.json')
} }
export function fetchForgetMemberPassword<T>(account: string, authCode: string, password: string) {
return request.post<T>('/bizMemberInfoRest/forgetMemberPassword.json', null, {
params: { account, authCode, password },
})
}
...@@ -29,7 +29,7 @@ const { t } = useI18n() ...@@ -29,7 +29,7 @@ const { t } = useI18n()
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
height: 240, height: 240,
width: 500, width: 500,
borderRadius: 6, borderRadius: 10,
btnLoading: false, btnLoading: false,
btnDisabled: false, btnDisabled: false,
cancelBtnText: '', cancelBtnText: '',
......
...@@ -43,6 +43,7 @@ common_module: ...@@ -43,6 +43,7 @@ common_module:
publish_success_message: 'Release success' publish_success_message: 'Release success'
clear_success_message: 'Clear successfully' clear_success_message: 'Clear successfully'
add_success_message: 'New success' add_success_message: 'New success'
reset_success_message: 'Reset successful'
loading: 'Loading' loading: 'Loading'
updating: 'Uploading' updating: 'Uploading'
successful_update: 'Update successfully' successful_update: 'Update successfully'
...@@ -152,6 +153,7 @@ common_module: ...@@ -152,6 +153,7 @@ common_module:
not_certified_yet: 'Not certified yet' not_certified_yet: 'Not certified yet'
authenticated: 'Authenticated' authenticated: 'Authenticated'
cancel_authorization: 'Cancel authorization' cancel_authorization: 'Cancel authorization'
get_code: 'Get Code'
dialogue_module: dialogue_module:
continue_question_message: 'You can keep asking questions' continue_question_message: 'You can keep asking questions'
...@@ -211,6 +213,7 @@ router_title_module: ...@@ -211,6 +213,7 @@ router_title_module:
order_manage: 'Order Management' order_manage: 'Order Management'
data_statistic: 'Data statistic' data_statistic: 'Data statistic'
plugin_center: 'Plugin center' plugin_center: 'Plugin center'
reset_password: 'Reset password'
login_module: login_module:
app_welcome_words: 'Hi, welcome to Model Link' app_welcome_words: 'Hi, welcome to Model Link'
...@@ -227,6 +230,28 @@ login_module: ...@@ -227,6 +230,28 @@ login_module:
successful: 'Obtain success' successful: 'Obtain success'
get_verification_code: 'Get verification code' get_verification_code: 'Get verification code'
other_login_methods: 'Other login methods' other_login_methods: 'Other login methods'
mobile_welcome_words: 'Hello, welcome to Model Link!'
sign_in_with_mobile_number: 'Sign in with mobile number'
verification_code_login: 'Verification code login'
password_login: 'Password login'
email_login: 'Email login'
forgot_password: 'Forgot password?'
agreement_prefix: 'I have read and agree to the'
agreement_terms: 'User Agreement'
agreement_privacy: 'Privacy Policy'
agreement_and: 'and'
confirm_to_logout: 'Confirm to logout?'
you_can_login_again_after_logout: 'You can login again after logout'
please_review_and_accept_the_agreement: 'Please review and accept agreement'
agree_and_continue: 'Agree & Continue'
disagree: 'Disagree'
reset_password_module:
reset_login_password: 'Reset login password'
please_enter_your_registered_phone_number: "Please enter your registered phone number. We'll reset your password for you"
please_enter_your_new_password: 'Please enter your new password'
back_to_login: 'Back to login'
reset_password: 'Reset Password'
home_module: home_module:
agent_welcome_message: 'Hi, welcome to Model Link' agent_welcome_message: 'Hi, welcome to Model Link'
......
...@@ -43,6 +43,7 @@ common_module: ...@@ -43,6 +43,7 @@ common_module:
publish_success_message: '发布成功' publish_success_message: '发布成功'
clear_success_message: '清空成功' clear_success_message: '清空成功'
add_success_message: '新增成功' add_success_message: '新增成功'
reset_success_message: '重置成功'
loading: '加载中' loading: '加载中'
updating: '更新中' updating: '更新中'
successful_update: '更新成功' successful_update: '更新成功'
...@@ -151,6 +152,8 @@ common_module: ...@@ -151,6 +152,8 @@ common_module:
not_certified_yet: '未认证' not_certified_yet: '未认证'
authenticated: '已认证' authenticated: '已认证'
cancel_authorization: '取消授权' cancel_authorization: '取消授权'
get_code: '获取验证码'
dialogue_module: dialogue_module:
continue_question_message: '你可以继续提问' continue_question_message: '你可以继续提问'
...@@ -210,6 +213,7 @@ router_title_module: ...@@ -210,6 +213,7 @@ router_title_module:
order_manage: '订单管理' order_manage: '订单管理'
data_statistic: '数据统计' data_statistic: '数据统计'
plugin_center: '插件中心' plugin_center: '插件中心'
reset_password: '重置密码'
login_module: login_module:
app_welcome_words: 'Hi, 欢迎使用Model Link' app_welcome_words: 'Hi, 欢迎使用Model Link'
...@@ -226,6 +230,28 @@ login_module: ...@@ -226,6 +230,28 @@ login_module:
successful: '获取成功' successful: '获取成功'
get_verification_code: '获取验证码' get_verification_code: '获取验证码'
other_login_methods: '其他登录方式' other_login_methods: '其他登录方式'
mobile_welcome_words: '您好,欢迎使用Model Link!'
sign_in_with_mobile_number: '推荐使用手机号码登录'
verification_code_login: '验证码登录'
password_login: '密码登录'
email_login: '邮箱登录'
forgot_password: '忘记密码?'
agreement_prefix: '我已阅读并同意'
agreement_terms: '用户协议'
agreement_privacy: '隐私政策'
agreement_and: '与'
confirm_to_logout: '确认要退出登录吗?'
you_can_login_again_after_logout: '退出登录仍可登录此账号'
please_review_and_accept_the_agreement: '请阅读并同意以下协议'
agree_and_continue: '同意并继续'
disagree: '不同意'
reset_password_module:
reset_login_password: '重置登录密码'
please_enter_your_registered_phone_number: '请输入您注册的手机号,我们将为您重置密码'
please_enter_your_new_password: '请输入您的新密码'
back_to_login: '返回登录'
reset_password: '重置密码'
home_module: home_module:
agent_welcome_message: 'Hi, 欢迎使用Model Link' agent_welcome_message: 'Hi, 欢迎使用Model Link'
......
...@@ -43,6 +43,7 @@ common_module: ...@@ -43,6 +43,7 @@ common_module:
publish_success_message: '發佈成功' publish_success_message: '發佈成功'
clear_success_message: '清空成功' clear_success_message: '清空成功'
add_success_message: '新增成功' add_success_message: '新增成功'
reset_success_message: '重置成功'
loading: '加載中' loading: '加載中'
updating: '更新中' updating: '更新中'
successful_update: '更新成功' successful_update: '更新成功'
...@@ -151,6 +152,7 @@ common_module: ...@@ -151,6 +152,7 @@ common_module:
not_certified_yet: '未認證' not_certified_yet: '未認證'
authenticated: '已認證' authenticated: '已認證'
cancel_authorization: '取消授權' cancel_authorization: '取消授權'
get_code: '獲取驗証碼'
dialogue_module: dialogue_module:
continue_question_message: '你可以繼續提問' continue_question_message: '你可以繼續提問'
...@@ -210,6 +212,7 @@ router_title_module: ...@@ -210,6 +212,7 @@ router_title_module:
order_manage: '訂單管理' order_manage: '訂單管理'
data_statistic: '數據統計' data_statistic: '數據統計'
plugin_center: '插件中心' plugin_center: '插件中心'
reset_password: '重置密碼'
login_module: login_module:
app_welcome_words: 'Hi, 歡迎使用Model Link' app_welcome_words: 'Hi, 歡迎使用Model Link'
...@@ -228,6 +231,28 @@ login_module: ...@@ -228,6 +231,28 @@ login_module:
other_login_methods: '其他登錄方式' other_login_methods: '其他登錄方式'
interrupt_dialogue_prompt: '當前回復尚未完成,是否確定打斷發起新會話?' interrupt_dialogue_prompt: '當前回復尚未完成,是否確定打斷發起新會話?'
interrupt_the_conversation_and_apply_the_history_prompt: '當前回復尚未完成,是否確定打斷對話應用其它記錄?' interrupt_the_conversation_and_apply_the_history_prompt: '當前回復尚未完成,是否確定打斷對話應用其它記錄?'
mobile_welcome_words: '您好,歡迎使用Model Link!'
sign_in_with_mobile_number: '推薦使用手機號碼登錄'
verification_code_login: '驗證碼登錄'
password_login: '密碼登錄'
email_login: '郵箱登錄'
forgot_password: '忘記密碼?'
agreement_prefix: '我已閲讀並同意'
agreement_terms: '用户協議'
agreement_privacy: '隱私政策'
agreement_and: '與'
confirm_to_logout: '確認要退出登錄嗎?'
you_can_login_again_after_logout: '退出登錄仍可登錄此賬號'
please_review_and_accept_the_agreement: '請閲讀並同意以下協議'
agree_and_continue: '同意並繼續'
disagree: '不同意'
reset_password_module:
reset_login_password: '重置登录密码'
please_enter_your_registered_phone_number: '请输入您注册的手机号,我们将为您重置密码'
please_enter_your_new_password: '请输入您的新密码'
back_to_login: '返回登录'
reset_password: '重置密码'
home_module: home_module:
agent_welcome_message: 'Hi, 歡迎使用Model Link' agent_welcome_message: 'Hi, 歡迎使用Model Link'
......
...@@ -3,7 +3,7 @@ import { useUserStore } from '@/store/modules/user' ...@@ -3,7 +3,7 @@ import { useUserStore } from '@/store/modules/user'
import i18n from '@/locales' import i18n from '@/locales'
/** 路由白名单 */ /** 路由白名单 */
const whitePathList = ['/login'] const whitePathList = ['/login', '/reset-password']
export function createRouterGuards(router: Router) { export function createRouterGuards(router: Router) {
router.beforeEach((to, _from, next) => { router.beforeEach((to, _from, next) => {
......
...@@ -8,7 +8,16 @@ export default [ ...@@ -8,7 +8,16 @@ export default [
rank: 1001, rank: 1001,
title: 'router_title_module.login', title: 'router_title_module.login',
}, },
component: () => import('@/views/login/login.vue'), component: () => import('@/views/login/index.vue'),
},
{
path: '/reset-password',
name: 'ResetPassword',
meta: {
rank: 1001,
title: 'router_title_module.reset_password',
},
component: () => import('@/views/reset-password/reset-password.vue'),
}, },
// { // {
// path: '/404', // path: '/404',
......
<script setup lang="ts">
import { CountdownInst, FormInst, FormItemRule, FormRules } from 'naive-ui'
import isMobilePhone from 'validator/es/lib/isMobilePhone'
import { computed, onMounted, ref, shallowReadonly, useTemplateRef, watchEffect } from 'vue'
import { useI18n } from 'vue-i18n'
import SparkMD5 from 'spark-md5'
import { Down, Mail, Lock, Phone } from '@icon-park/vue-next'
import isEmail from 'validator/es/lib/isEmail'
import { useRoute, useRouter } from 'vue-router'
import 'vant/es/dialog/style'
import { ss } from '@/utils/storage'
import { fetchEmailCode, fetchLogin, fetchSMSCode } from '@/apis/user'
import { useUserStore } from '@/store/modules/user'
import { UserInfo } from '@/store/types/user'
import { useSystemLanguageStore } from '@/store/modules/system-language'
type LoginMethod = 'password' | 'sms' | 'email'
enum StorageKeyEnum {
smsCountdownTime = 'SMS_COUNTDOWN_TIME',
emailCountdownTime = 'MAIL_COUNTDOWN_TIME',
}
interface LoginPayload {
loginChannel: 'MEMBER_PLATFOMR_SMS' | 'MEMBER_PLATFOMR_EMAIL' | 'MEMBER_PLATFOMR_PW'
account: string
password?: string
authCode?: string
}
const { t } = useI18n()
const systemLanguageStore = useSystemLanguageStore()
const userStore = useUserStore()
const router = useRouter()
const route = useRoute()
const currentLoginMethod = ref<LoginMethod>('sms')
const currentPhoneNumberArea = ref<'+86' | '+852'>('+86')
const loginBtnLoading = ref(false)
const passwordLoginFormRef = useTemplateRef<FormInst>('passwordLoginFormRef')
const smsLoginFormRef = useTemplateRef<FormInst>('smsLoginFormRef')
const emailLoginFormRef = useTemplateRef<FormInst>('emailLoginFormRef')
const countdownRef = useTemplateRef<CountdownInst>('countdownRef')
const passwordLoginForm = ref({
account: '',
password: '',
})
const smsLoginForm = ref({
phoneNumber: '',
code: '',
})
const emailLoginForm = ref({
email: '',
code: '',
})
const isAgreeTerms = ref(false)
const countdownActive = ref(true)
const isShowCountdown = ref(false)
const countdownDuration = ref<number>(60000)
const showAgreeTermsDialog = ref(false)
const userAgreementUrl = ref('https://tcn9nhw8kfwe.feishu.cn/wiki/Y1ELwRVESi9jw9kshc0chT2ZnSf?fromScene=spaceOverview')
const privacyPolicyUrl = ref('https://tcn9nhw8kfwe.feishu.cn/wiki/OBzxwhGBviHP8lk9ca0chUh9nCc?fromScene=spaceOverview')
const passwordLoginFormRules = shallowReadonly<FormRules>({
account: { required: true, message: t('login_module.please_enter_your_account_number'), trigger: 'blur' },
password: { required: true, message: t('login_module.please_enter_your_password'), trigger: 'blur' },
})
const smsLoginFormRules = shallowReadonly<FormRules>({
phoneNumber: {
key: 'phoneNumber',
required: true,
validator: (_rule: FormItemRule, value: string) => {
if (!value) {
return new Error(t('login_module.please_enter_your_cell_phone_number'))
} else if (!isMobilePhone(value, ['zh-CN', 'zh-HK'])) {
return new Error(t('login_module.please_enter_your_correct_cell_phone_number'))
}
return
},
trigger: 'blur',
},
code: { required: true, message: t('login_module.please_enter_the_verification_code'), trigger: 'blur' },
})
const emailLoginFormRules = shallowReadonly<FormRules>({
email: {
key: 'email',
required: true,
validator: (_rule: FormItemRule, value: string) => {
if (!value) {
return new Error(t('login_module.please_enter_your_email_address'))
} else if (!isEmail(value)) {
return new Error(t('login_module.please_enter_the_correct_email_address'))
}
return
},
trigger: 'blur',
},
code: { required: true, message: t('login_module.please_enter_the_verification_code'), trigger: 'blur' },
})
onMounted(() => {
document.title = t('common_module.login')
})
const phoneNumberAreaOptions = computed(() => {
return [
{
label: `+86⠀${t('login_module.mainland_china')}`,
value: '+86',
},
{
label: `+852 ${t('login_module.hong_kong_china')}`,
value: '+852',
},
]
})
const currentLoginMethodValue = computed(() => {
switch (currentLoginMethod.value) {
case 'password':
return t('login_module.password_login')
case 'sms':
return t('login_module.verification_code_login')
case 'email':
return t('login_module.email_login')
default:
return ''
}
})
const isEnglishLanguage = computed(() => {
return systemLanguageStore.currentLanguage === 'en'
})
const agreementPrefix = computed(() => {
return isEnglishLanguage.value ? '<' : '《'
})
const agreementSuffix = computed(() => {
return isEnglishLanguage.value ? '>' : '》'
})
watchEffect(() => {
let timeStringDraft = ''
switch (currentLoginMethod.value) {
case 'sms':
{
timeStringDraft = ss.get(StorageKeyEnum.smsCountdownTime)
}
break
case 'email':
{
timeStringDraft = ss.get(StorageKeyEnum.emailCountdownTime)
}
break
}
if (timeStringDraft) {
const time = Math.floor(Date.now() - parseInt(timeStringDraft))
if (time < 60000) {
countdownDuration.value = 60000 - time
countdownRef.value?.reset()
isShowCountdown.value = true
}
}
})
function onlyAllowNumber(value: string) {
return !value || /^\d+$/.test(value)
}
function noSideSpace(value: string) {
return !value.startsWith(' ') && !value.endsWith(' ')
}
function countdownRender({ seconds, minutes }: { seconds: number; minutes: number }) {
if (minutes && minutes === 1) {
return '60 s'
}
return `${seconds} s`
}
function onCountdownFinish() {
isShowCountdown.value = false
}
// 获取手机验证码
function handleSMSCodeGain() {
smsLoginFormRef.value?.validate(
(errors) => {
if (errors) return ''
countdownDuration.value = 60000
ss.set(StorageKeyEnum.smsCountdownTime, Date.now())
countdownRef.value?.reset()
isShowCountdown.value = true
fetchSMSCode(getInputPhoneNumber()).then((res) => {
if (res.code !== 0) return ''
window.$message.success(t('login_module.successful'))
})
},
(rule) => {
return rule.key === 'phoneNumber'
},
)
}
// 获取邮箱验证码
function handleEmailCodeGain() {
emailLoginFormRef.value?.validate(
(errors) => {
if (errors) return ''
countdownDuration.value = 60000
ss.set(StorageKeyEnum.emailCountdownTime, Date.now())
countdownRef.value?.reset()
isShowCountdown.value = true
fetchEmailCode(encodeURIComponent(emailLoginForm.value.email)).then((res) => {
if (res.code !== 0) return ''
window.$message.success(t('login_module.successful'))
})
},
(rule) => {
return rule.key === 'email'
},
)
}
// 获取输入的手机号码
function getInputPhoneNumber() {
return currentPhoneNumberArea.value !== '+86'
? encodeURIComponent(`${currentPhoneNumberArea.value}${smsLoginForm.value.phoneNumber}`)
: smsLoginForm.value.phoneNumber
}
// 忘记密码
function handleToResetPassword() {
const redirectUrl = decodeURIComponent((route.query.redirect as string) || '')
router.replace({
path: '/reset-password',
query: redirectUrl ? { redirect: encodeURIComponent(redirectUrl) } : {},
})
}
// 切换登录方式
function handleLoginMethodChange(method: LoginMethod) {
currentLoginMethod.value = method
isAgreeTerms.value = false
smsLoginForm.value = {
phoneNumber: '',
code: '',
}
emailLoginForm.value = {
email: '',
code: '',
}
passwordLoginForm.value = {
account: '',
password: '',
}
}
// 登录
function handleLoginSubmit(method: LoginMethod) {
if (!isAgreeTerms.value) {
showAgreeTermsDialog.value = true
return
}
let payload: LoginPayload = {
loginChannel: 'MEMBER_PLATFOMR_PW',
account: '',
password: '',
}
new Promise((resolve) => {
switch (method) {
case 'password':
{
passwordLoginFormRef.value?.validate((errors) => {
if (errors) return ''
payload = {
loginChannel: 'MEMBER_PLATFOMR_PW',
account: passwordLoginForm.value.account,
password: SparkMD5.hash(passwordLoginForm.value.password),
}
resolve(true)
})
}
break
case 'sms':
{
smsLoginFormRef.value?.validate((errors) => {
if (errors) return ''
payload = {
loginChannel: 'MEMBER_PLATFOMR_SMS',
account: smsLoginForm.value.phoneNumber,
authCode: smsLoginForm.value.code,
}
resolve(true)
})
}
break
case 'email':
{
emailLoginFormRef.value?.validate((errors) => {
if (errors) return ''
payload = {
loginChannel: 'MEMBER_PLATFOMR_EMAIL',
account: emailLoginForm.value.email,
authCode: emailLoginForm.value.code,
}
resolve(true)
})
}
break
}
}).then(() => {
loginBtnLoading.value = true
fetchLogin<UserInfo & { token: string }>(payload)
.then((res) => {
if (res.code !== 0) return ''
userStore.updateToken(res.data.token)
userStore.updateUserInfo({
avatarUrl: res.data.avatarUrl,
memberId: res.data.memberId,
mobilePhone: res.data.mobilePhone,
nickName: res.data.nickName,
remark: res.data.remark,
email: res.data.email,
})
const redirectUrl = decodeURIComponent((route.query.redirect as string) || '')
router.replace({ path: redirectUrl ? redirectUrl : '/' })
window.$message.success(t('login_module.login_success'))
ss.remove(StorageKeyEnum.smsCountdownTime)
ss.remove(StorageKeyEnum.emailCountdownTime)
})
.finally(() => {
loginBtnLoading.value = false
})
})
}
function handleAgreeAndContinue() {
showAgreeTermsDialog.value = false
isAgreeTerms.value = true
handleLoginSubmit(currentLoginMethod.value)
}
// 打开用户协议
function handleOpenUserAgreement() {
window.open(userAgreementUrl.value, '_blank')
}
// 打开隐私协议
function handleOpenPrivacyPolicy() {
window.open(privacyPolicyUrl.value, '_blank')
}
</script>
<template>
<div
class="bg-px-login-h5_login_bg-png relative flex h-full w-full flex-col justify-between bg-cover bg-center bg-no-repeat py-[44px]"
>
<div class="px-[33px]">
<p class="font-family-medium text-[18px]">{{ t('login_module.mobile_welcome_words') }}</p>
<p class="text-gray-font-color mt-[9px] text-[12px]">{{ t('login_module.sign_in_with_mobile_number') }}</p>
<div class="text-theme-color border-b-theme-color font-family-medium my-[24px] inline-block text-[13px]">
<span>{{ currentLoginMethodValue }}</span>
<div class="bg-theme-color h-[2px] w-full rounded-[1px]" />
</div>
<!-- SMS登录 -->
<n-form
v-if="currentLoginMethod === 'sms'"
ref="smsLoginFormRef"
label-placement="left"
size="large"
:model="smsLoginForm"
:rules="smsLoginFormRules"
>
<n-form-item path="phoneNumber" feedback-class="text-[12px]!">
<n-input
v-model:value.trim="smsLoginForm.phoneNumber"
:allow-input="onlyAllowNumber"
:maxlength="currentPhoneNumberArea === '+852' ? 8 : 11"
:placeholder="t('login_module.please_enter_your_cell_phone_number')"
>
<template #prefix>
<div class="flex items-center">
<n-popselect v-model:value="currentPhoneNumberArea" :options="phoneNumberAreaOptions" trigger="click">
<div class="flex w-[54px] cursor-pointer items-center">
<div class="mr-[4px] text-[14px]">{{ currentPhoneNumberArea }}</div>
<Down theme="outline" size="18" fill="#333" :stroke-width="3" class="mt-[2px]" />
</div>
</n-popselect>
<div class="mx-[8px] h-[18px] w-[1px] bg-[#868686]"></div>
</div>
</template>
</n-input>
</n-form-item>
<n-form-item path="code" feedback-class="text-[12px]!">
<n-input
v-model:value="smsLoginForm.code"
:allow-input="onlyAllowNumber"
show-password-on="click"
:maxlength="6"
:placeholder="t('login_module.please_enter_the_verification_code')"
>
</n-input>
<n-button
v-show="!isShowCountdown"
class="text-[11px]! ml-[9px]! text-[#333]! px-[10px]! bg-white!"
:class="isEnglishLanguage ? 'w-[70px]!' : 'w-[90px]!'"
:disabled="!smsLoginForm.phoneNumber"
@click="handleSMSCodeGain"
>
<span>{{ t('common_module.get_code') }}</span>
</n-button>
<div
v-show="isShowCountdown"
class="flex-center rounded-theme ml-[9px]! text-gray-font-color h-[40px] flex-shrink-0 border bg-white text-center text-[12px]"
:class="isEnglishLanguage ? 'w-[70px]' : 'w-[90px]'"
>
<n-countdown
ref="countdownRef"
:duration="countdownDuration"
:active="countdownActive"
:render="countdownRender"
:on-finish="onCountdownFinish"
/>
</div>
</n-form-item>
<n-form-item class="mt-4">
<n-button
type="primary"
size="large"
block
:loading="loginBtnLoading"
:disabled="!smsLoginForm.phoneNumber || !smsLoginForm.code"
:focusable="false"
@click="handleLoginSubmit('sms')"
>
{{ t('common_module.login') }}
</n-button>
</n-form-item>
</n-form>
<!-- 密码登录 -->
<n-form
v-if="currentLoginMethod === 'password'"
ref="passwordLoginFormRef"
label-placement="left"
size="large"
:model="passwordLoginForm"
:rules="passwordLoginFormRules"
feedback-class="text-[12px]!"
>
<n-form-item path="account" feedback-class="text-[12px]!">
<n-input
v-model:value="passwordLoginForm.account"
:allow-input="noSideSpace"
:maxlength="50"
:placeholder="t('login_module.please_enter_your_account_number')"
/>
</n-form-item>
<n-form-item path="password" feedback-class="text-[12px]!">
<n-input
v-model:value="passwordLoginForm.password"
class="font-sans"
:allow-input="noSideSpace"
type="password"
show-password-on="click"
:placeholder="t('login_module.please_enter_your_password')"
/>
</n-form-item>
<div class="flex w-full justify-end">
<span class="text-[11px] text-[#518AFF]" @click="handleToResetPassword">
{{ t('login_module.forgot_password') }}
</span>
</div>
<n-form-item class="mt-4">
<n-button
type="primary"
size="large"
block
:loading="loginBtnLoading"
:disabled="!passwordLoginForm.account || !passwordLoginForm.password"
:focusable="false"
@click="handleLoginSubmit('password')"
>
{{ t('common_module.login') }}
</n-button>
</n-form-item>
</n-form>
<!-- 邮箱登录 -->
<n-form
v-if="currentLoginMethod === 'email'"
ref="emailLoginFormRef"
label-placement="left"
size="large"
:model="emailLoginForm"
:rules="emailLoginFormRules"
>
<n-form-item path="email" feedback-class="text-[12px]!">
<n-input
v-model:value="emailLoginForm.email"
:allow-input="noSideSpace"
:placeholder="t('login_module.please_enter_your_email_address')"
:maxlength="50"
>
</n-input>
</n-form-item>
<n-form-item path="code" feedback-class="text-[12px]!">
<n-input
v-model:value="emailLoginForm.code"
:allow-input="onlyAllowNumber"
show-password-on="click"
:maxlength="6"
:placeholder="t('login_module.please_enter_the_verification_code')"
>
</n-input>
<n-button
v-show="!isShowCountdown"
class="text-[11px]! ml-[9px]! text-[#333]! px-[10px]! bg-white!"
:class="isEnglishLanguage ? 'w-[70px]!' : 'w-[90px]!'"
:disabled="!emailLoginForm.email"
@click="handleEmailCodeGain"
>
<span>{{ t('common_module.get_code') }}</span>
</n-button>
<div
v-show="isShowCountdown"
class="flex-center rounded-theme ml-[9px]! text-gray-font-color h-[40px] flex-shrink-0 border bg-white text-center text-[12px]"
:class="isEnglishLanguage ? 'w-[70px]' : 'w-[90px]'"
>
<n-countdown
ref="countdownRef"
:duration="countdownDuration"
:active="countdownActive"
:render="countdownRender"
:on-finish="onCountdownFinish"
/>
</div>
</n-form-item>
<n-form-item class="mt-4">
<n-button
type="primary"
size="large"
block
:loading="loginBtnLoading"
:disabled="!emailLoginForm.email || !emailLoginForm.code"
:focusable="false"
@click="handleLoginSubmit('email')"
>
{{ t('common_module.login') }}
</n-button>
</n-form-item>
</n-form>
<div class="flex-center gap-[7px] text-[11px]">
<n-checkbox v-model:checked="isAgreeTerms" class="h-[12px] w-[12px]" />
<div>
{{ t('login_module.agreement_prefix') }}
<span class="text-[#518AFF]" @click="handleOpenUserAgreement">{{ t('login_module.agreement_terms') }}</span>
{{ t('login_module.agreement_and') }}
<span class="text-[#518AFF]" @click="handleOpenPrivacyPolicy">{{ t('login_module.agreement_privacy') }}</span>
</div>
</div>
</div>
<div class="absolute top-[450px] w-full px-[33px]">
<div class="mb-[44px] flex flex-col items-center">
<n-divider class="w-[70%]!">
<span class="text-gray-font-color text-[11px]">{{ t('login_module.other_login_methods') }}</span>
</n-divider>
<div class="flex-center gap-[15px]">
<div
v-show="currentLoginMethod !== 'password'"
class="flex-center h-[34px] w-[34px] cursor-pointer rounded-full bg-[#F2F2F2]"
@click="handleLoginMethodChange('password')"
>
<Lock theme="outline" size="16" fill="#000DFF" :stroke-width="3" />
</div>
<div
v-show="currentLoginMethod !== 'email'"
class="flex-center h-[34px] w-[34px] cursor-pointer rounded-full bg-[#F2F2F2]"
@click="handleLoginMethodChange('email')"
>
<Mail theme="outline" size="16" fill="#000DFF" :stroke-width="3" />
</div>
<div
v-show="currentLoginMethod !== 'sms'"
class="flex-center h-[34px] w-[34px] cursor-pointer rounded-full bg-[#F2F2F2]"
@click="handleLoginMethodChange('sms')"
>
<Phone theme="outline" size="16" fill="#000DFF" :stroke-width="3" />
</div>
</div>
</div>
</div>
<van-dialog v-model:show="showAgreeTermsDialog" :title="t('login_module.please_review_and_accept_the_agreement')">
<div class="my-[16px] flex justify-center gap-[6px] text-center">
<span class="text-[14px] text-[#518AFF]" @click="handleOpenUserAgreement">
{{ agreementPrefix }}{{ t('login_module.agreement_terms') }}{{ agreementSuffix }}
</span>
<span class="text-[14px] text-[#518AFF]" @click="handleOpenPrivacyPolicy">
{{ agreementPrefix }}{{ t('login_module.agreement_privacy') }}{{ agreementSuffix }}
</span>
</div>
<template #footer>
<div class="flex flex-col gap-[8px] px-[24px] pb-[8px] pt-[8px]">
<van-button type="primary" color="#000DFF" round class="h-[36px]" @click="handleAgreeAndContinue">
{{ t('login_module.agree_and_continue') }}
</van-button>
<span
class="text-gray-font-color h-[28px] text-center text-[14px] leading-7"
@click="showAgreeTermsDialog = false"
>
{{ t('login_module.disagree') }}
</span>
</div>
</template>
</van-dialog>
</div>
</template>
<style lang="scss" scoped>
:deep(.n-checkbox-box-wrapper),
:deep(.n-checkbox-box) {
width: 12px !important;
height: 12px !important;
border-radius: 100% !important;
}
:deep(.--n-feedback-height) {
height: 16px !important;
}
:deep(.n-input__placeholder) {
font-size: 13px !important;
}
</style>
<script setup lang="ts">
import { useLayoutConfig } from '@/composables/useLayoutConfig'
import H5Login from './h5-login.vue'
import PCLogin from './login.vue'
const { isMobile } = useLayoutConfig()
</script>
<template>
<component :is="isMobile ? H5Login : PCLogin"></component>
</template>
<script setup lang="ts">
import { CountdownInst, FormInst, FormItemRule, FormRules } from 'naive-ui'
import isMobilePhone from 'validator/es/lib/isMobilePhone'
import { computed, ref, shallowReadonly, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import { Down } from '@icon-park/vue-next'
import { useRoute, useRouter } from 'vue-router'
import SparkMD5 from 'spark-md5'
import { fetchForgetMemberPassword, fetchSMSCode, fetchVerifyCode } from '@/apis/user'
import { ss } from '@/utils/storage'
import { useSystemLanguageStore } from '@/store/modules/system-language'
interface PasswordInfoFormInterface {
authCode: string
password: string
confirmPassword: string
}
const { t } = useI18n()
const systemLanguageStore = useSystemLanguageStore()
const route = useRoute()
const router = useRouter()
const smsLoginFormRef = useTemplateRef<FormInst>('smsLoginFormRef')
const passwordInfoFormRef = useTemplateRef<FormInst>('passwordInfoFormRef')
const countdownRef = useTemplateRef<CountdownInst>('countdownRef')
const isFinishAuthCode = ref(false)
const submitBtnLoading = ref(false)
const countdownActive = ref(true)
const isShowCountdown = ref(false)
const countdownDuration = ref<number>(60000)
const currentPhoneNumberArea = ref<'+86' | '+852'>('+86')
const smsLoginForm = ref({
phoneNumber: '',
code: '',
})
const passwordInfoForm = ref<PasswordInfoFormInterface>({
authCode: '',
password: '',
confirmPassword: '',
})
const smsLoginFormRules = shallowReadonly<FormRules>({
phoneNumber: {
key: 'phoneNumber',
required: true,
validator: (_rule: FormItemRule, value: string) => {
if (!value) {
return new Error(t('login_module.please_enter_your_cell_phone_number'))
} else if (!isMobilePhone(value, ['zh-CN', 'zh-HK'])) {
return new Error(t('login_module.please_enter_your_correct_cell_phone_number'))
}
return
},
trigger: 'blur',
},
code: { required: true, message: t('login_module.please_enter_the_verification_code'), trigger: 'blur' },
})
const passwordFormRules = shallowReadonly({
password: {
required: true,
trigger: 'blur',
validator: (_rule: FormItemRule, value: string) => {
if (!value) {
return new Error(t('personal_settings_module.please_enter_your_new_password'))
} else if (value.length < 6) {
return new Error(t('personal_settings_module.the_password_contains_a_maximum_of_6_characters'))
}
return
},
},
confirmPassword: {
required: true,
trigger: 'blur',
validator: (_rule: FormItemRule, value: string) => {
if (!value) {
return new Error(t('personal_settings_module.please_enter_confirm_new_password'))
} else if (value !== passwordInfoForm.value.password) {
return new Error(
t('personal_settings_module.verify_that_the_new_password_is_inconsistent_with_the_new_password'),
)
}
return
},
},
})
const phoneNumberAreaOptions = computed(() => {
return [
{
label: `+86⠀${t('login_module.mainland_china')}`,
value: '+86',
},
{
label: `+852 ${t('login_module.hong_kong_china')}`,
value: '+852',
},
]
})
const isEnglishLanguage = computed(() => {
return systemLanguageStore.currentLanguage === 'en'
})
function noSideSpace(value: string) {
return !value.startsWith(' ') && !value.endsWith(' ')
}
function onlyAllowNumber(value: string) {
return !value || /^\d+$/.test(value)
}
function countdownRender({ seconds, minutes }: { seconds: number; minutes: number }) {
if (minutes && minutes === 1) {
return '60 s'
}
return `${seconds} s`
}
function onCountdownFinish() {
isShowCountdown.value = false
}
function handleSMSCodeGain() {
smsLoginFormRef.value?.validate(
(errors) => {
if (errors) return ''
countdownDuration.value = 60000
ss.set('SMS_COUNTDOWN_TIME', Date.now())
countdownRef.value?.reset()
isShowCountdown.value = true
fetchSMSCode(getInputPhoneNumber()).then((res) => {
if (res.code !== 0) return ''
window.$message.success(t('login_module.successful'))
})
},
(rule) => {
return rule.key === 'phoneNumber'
},
)
}
function getInputPhoneNumber() {
return currentPhoneNumberArea.value !== '+86'
? encodeURIComponent(`${currentPhoneNumberArea.value}${smsLoginForm.value.phoneNumber}`)
: smsLoginForm.value.phoneNumber
}
function handleBackLogin() {
const redirectUrl = decodeURIComponent((route.query.redirect as string) || '')
router.replace({ path: '/login', query: redirectUrl ? { redirect: encodeURIComponent(redirectUrl) } : {} })
}
function handleNextStep() {
smsLoginFormRef.value?.validate((errors) => {
if (errors) return ''
submitBtnLoading.value = true
fetchVerifyCode<string>(smsLoginForm.value.phoneNumber, smsLoginForm.value.code)
.then((res) => {
if (res.code === 0) {
isShowCountdown.value = false
isFinishAuthCode.value = true
passwordInfoForm.value.authCode = res.data
}
})
.finally(() => {
submitBtnLoading.value = false
})
})
}
function handleResetPassword() {
passwordInfoFormRef.value?.validate((errors) => {
if (errors) return ''
submitBtnLoading.value = true
fetchForgetMemberPassword(
smsLoginForm.value.phoneNumber,
passwordInfoForm.value.authCode,
SparkMD5.hash(passwordInfoForm.value.confirmPassword),
)
.then((res) => {
if (res.code === 0) {
window.$message.success(t('common_module.reset_success_message'))
handleBackLogin()
}
})
.finally(() => {
submitBtnLoading.value = false
})
})
}
</script>
<template>
<div
class="bg-px-login-h5_login_bg-png relative flex h-full w-full flex-col justify-between bg-cover bg-center bg-no-repeat py-[44px]"
>
<div class="px-[33px]">
<p class="font-family-medium text-[18px]">{{ t('reset_password_module.reset_login_password') }}</p>
<p class="text-gray-font-color mt-[9px] text-[12px]">
{{
isFinishAuthCode
? t('reset_password_module.please_enter_your_new_password')
: t('reset_password_module.please_enter_your_registered_phone_number')
}}
</p>
<!-- 验证码校验 -->
<n-form
v-if="!isFinishAuthCode"
ref="smsLoginFormRef"
label-placement="left"
size="large"
:model="smsLoginForm"
:rules="smsLoginFormRules"
class="mt-[30px]"
>
<n-form-item path="phoneNumber" feedback-class="text-[12px]!">
<n-input
v-model:value.trim="smsLoginForm.phoneNumber"
:allow-input="onlyAllowNumber"
:maxlength="currentPhoneNumberArea === '+852' ? 8 : 11"
:placeholder="t('login_module.please_enter_your_cell_phone_number')"
>
<template #prefix>
<div class="flex items-center">
<n-popselect v-model:value="currentPhoneNumberArea" :options="phoneNumberAreaOptions" trigger="click">
<div class="flex w-[54px] cursor-pointer items-center">
<div class="mr-[4px] text-[14px]">{{ currentPhoneNumberArea }}</div>
<Down theme="outline" size="18" fill="#333" :stroke-width="3" class="mt-[2px]" />
</div>
</n-popselect>
<div class="mx-[8px] h-[18px] w-[1px] bg-[#868686]"></div>
</div>
</template>
</n-input>
</n-form-item>
<n-form-item path="code" feedback-class="text-[12px]!">
<n-input
v-model:value="smsLoginForm.code"
:allow-input="onlyAllowNumber"
show-password-on="click"
:maxlength="6"
:placeholder="t('login_module.please_enter_the_verification_code')"
>
</n-input>
<n-button
v-show="!isShowCountdown"
class="text-[11px]! ml-[9px]! text-[#333]! px-[10px]! bg-white!"
:class="isEnglishLanguage ? 'w-[70px]!' : 'w-[90px]!'"
:disabled="!smsLoginForm.phoneNumber"
@click="handleSMSCodeGain"
>
<span>{{ t('common_module.get_code') }}</span>
</n-button>
<div
v-show="isShowCountdown"
class="flex-center rounded-theme ml-[9px]! text-gray-font-color h-[40px] flex-shrink-0 border bg-white text-center text-[12px]"
:class="isEnglishLanguage ? 'w-[70px]' : 'w-[90px]'"
>
<n-countdown
ref="countdownRef"
:duration="countdownDuration"
:active="countdownActive"
:render="countdownRender"
:on-finish="onCountdownFinish"
/>
</div>
</n-form-item>
<n-form-item class="mt-4">
<n-button
type="primary"
size="large"
block
:loading="submitBtnLoading"
:disabled="!smsLoginForm.phoneNumber || !smsLoginForm.code"
@click="handleNextStep"
>
{{ t('common_module.next_btn_text') }}
</n-button>
</n-form-item>
</n-form>
<!-- 重置密码 -->
<n-form
v-if="isFinishAuthCode"
ref="passwordInfoFormRef"
label-placement="left"
size="large"
:model="passwordInfoForm"
:rules="passwordFormRules"
class="mt-[30px]"
>
<n-form-item path="password">
<n-input
v-model:value="passwordInfoForm.password"
type="password"
show-password-on="click"
class="font-sans"
:allow-input="noSideSpace"
:placeholder="t('personal_settings_module.please_enter_your_new_password')"
/>
</n-form-item>
<n-form-item path="confirmPassword">
<n-input
v-model:value="passwordInfoForm.confirmPassword"
type="password"
show-password-on="click"
class="font-sans"
:allow-input="noSideSpace"
:placeholder="t('personal_settings_module.please_enter_confirm_new_password')"
/>
</n-form-item>
<n-form-item class="mt-4">
<n-button
type="primary"
size="large"
block
:loading="submitBtnLoading"
:disabled="!passwordInfoForm.password || !passwordInfoForm.confirmPassword"
@click="handleResetPassword"
>
{{ t('reset_password_module.reset_password') }}
</n-button>
</n-form-item>
</n-form>
</div>
<div class="flex-center absolute top-[500px] w-full">
<div
class="inline-block cursor-pointer border-b border-[#518AFF] text-[12px] leading-[14px] text-[#518AFF]"
@click="handleBackLogin"
>
{{ t('reset_password_module.back_to_login') }}
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
:deep(.--n-feedback-height) {
height: 16px !important;
}
:deep(.n-input__placeholder) {
font-size: 13px !important;
}
</style>
<script setup lang="ts"></script> <script setup lang="ts">
interface Props {
activeColor?: string
}
const { activeColor = '#000dff' } = defineProps<Props>()
</script>
<template> <template>
<div class="loader" /> <div class="loader" />
...@@ -14,16 +20,16 @@ ...@@ -14,16 +20,16 @@
@keyframes l5 { @keyframes l5 {
0% { 0% {
background: #000dff; background: v-bind('activeColor');
box-shadow: box-shadow:
13px 0 #000dff, 13px 0 v-bind('activeColor'),
-13px 0 #0002; -13px 0 #0002;
} }
33% { 33% {
background: #0002; background: #0002;
box-shadow: box-shadow:
13px 0 #000dff, 13px 0 v-bind('activeColor'),
-13px 0 #0002; -13px 0 #0002;
} }
...@@ -31,14 +37,14 @@ ...@@ -31,14 +37,14 @@
background: #0002; background: #0002;
box-shadow: box-shadow:
13px 0 #0002, 13px 0 #0002,
-13px 0 #000dff; -13px 0 v-bind('activeColor');
} }
100% { 100% {
background: #000dff; background: v-bind('activeColor');
box-shadow: box-shadow:
13px 0 #0002, 13px 0 #0002,
-13px 0 #000dff; -13px 0 v-bind('activeColor');
} }
} }
</style> </style>
<script setup lang="ts">
import { useUserStore } from '@/store/modules/user'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
interface Props {
agentTitle: string
}
const { t } = useI18n()
defineProps<Props>()
const emit = defineEmits<{
toCreateApplication: []
toLogin: []
}>()
const userStore = useUserStore()
const isLogin = computed(() => {
return userStore.isLogin
})
function handleToCreateApplication() {
emit('toCreateApplication')
}
function handleToLogin() {
emit('toLogin')
}
</script>
<template>
<header class="flex h-[48px] w-full items-center justify-between border-b border-[#e8e9eb] bg-white px-4">
<div class="bg-px-logo-png bg-contain! h-[24px] w-[100px] bg-center bg-no-repeat" />
<div>
<NButton
v-show="isLogin"
type="primary"
class="rounded-md! h-[32px]! text-xs! min-w-[80px]!"
@click="handleToCreateApplication"
>
{{ t('common_module.create_agent_btn_text') }}
</NButton>
<NButton v-show="!isLogin" type="primary" class="rounded-md! h-[32px]! text-xs! w-[80px]!" @click="handleToLogin">
<span class="text-xs"> {{ t('common_module.login_now') }}</span>
</NButton>
</div>
</header>
</template>
<script setup lang="ts">
import { debounce } from 'lodash-es'
import { nanoid } from 'nanoid'
import { computed, inject, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { Emitter } from 'mitt'
import { fetchRecommendQuestionList } from '@/apis/home-agent'
interface Props {
type: 'continuous' | 'featured'
}
defineProps<Props>()
const { t } = useI18n()
const emitter = inject<Emitter<MittEvents>>('emitter')
const continuousQuestionList = defineModel<string[]>('continuousQuestionList', { required: true })
const promptChangeBtnStatus = ref<'normal' | 'loading' | 'failed'>('normal')
const recommendQuestionList = computed(() => {
if (continuousQuestionList.value.length === 0) {
return []
}
return continuousQuestionList.value.map((item) => {
return {
id: nanoid(),
content: item,
}
})
})
function getRecommendQuestionList() {
return fetchRecommendQuestionList<string[]>().then((res) => {
continuousQuestionList.value = res.data
})
}
const handleRecommendQuestionListUpdate = debounce(
() => {
if (promptChangeBtnStatus.value === 'loading') {
return
}
promptChangeBtnStatus.value = 'loading'
getRecommendQuestionList()
.then(() => {
promptChangeBtnStatus.value = 'normal'
})
.catch(() => {
promptChangeBtnStatus.value = 'failed'
})
},
700,
{ leading: true, trailing: false },
)
function handleSelectContinueQuestion(continueQuestion: string) {
emitter?.emit('selectQuestion', continueQuestion)
}
</script>
<template>
<h3 class="continue-question-title">
{{
type === 'featured'
? t('common_module.recommended_questions') + ':'
: t('common_module.dialogue_module.continue_question_message')
}}
</h3>
<ul class="continue-question-wrapper">
<template v-if="continuousQuestionList.length && promptChangeBtnStatus !== 'loading'">
<li
v-for="questionItem in recommendQuestionList"
:key="questionItem.id"
class="question-item"
@click="handleSelectContinueQuestion(questionItem.content)"
>
{{ questionItem.content }}
</li>
</template>
<template v-else>
<n-skeleton class="question-item-skeleton" width="52%" round />
<n-skeleton class="question-item-skeleton" width="72%" round />
<n-skeleton class="question-item-skeleton" width="70%" round />
</template>
<li class="recommend-question-change-wrapper">
<span
class="recommend-question-change-container"
:class="{
'hover:text-[#096EE0]': promptChangeBtnStatus !== 'loading',
'cursor-not-allowed': promptChangeBtnStatus === 'loading',
'text-[#BDBDBD]': promptChangeBtnStatus === 'loading',
}"
@click="handleRecommendQuestionListUpdate"
>
<i
class="iconfont icon-huanyihuan recommend-question-change-icon"
:class="{ 'group-active:rotate-360': promptChangeBtnStatus !== 'loading' }"
></i>
<span>
{{ promptChangeBtnStatus === 'failed' ? t('common_module.retry') : t('common_module.exchange') }}
</span>
</span>
</li>
</ul>
</template>
<style lang="scss" scoped>
.continue-question-title {
@apply mt-[15px] text-[12px] text-[#999];
}
.continue-question-wrapper {
@apply w-full select-none;
.question-item {
@apply mt-[10px] w-fit cursor-pointer rounded-[10px] border border-[#d4d6d9] bg-[#ffffff80] px-[12px] py-[10px] text-[12px] hover:opacity-80;
}
.question-item-skeleton {
@apply mt-[10px] h-[40px];
}
.recommend-question-change-wrapper {
@apply mt-[10px] pl-[16px] text-[12px];
.recommend-question-change-container {
@apply group cursor-pointer text-[#0B7DFF] transition;
.recommend-question-change-icon {
@apply mr-[2px] inline-block text-[11px] transition-[rotate] duration-150 ease-in-out;
}
}
}
}
</style>
<script setup lang="ts">
import { computed, inject, onMounted, onUnmounted, ref, watch } from 'vue'
import { Emitter } from 'mitt'
import { useI18n } from 'vue-i18n'
import { nanoid } from 'nanoid'
import { useRoute } from 'vue-router'
import { CloseSmall } from '@icon-park/vue-next'
import { fetchCustomEventSource } from '@/composables/useEventSource'
import { useUserStore } from '@/store/modules/user'
import { UploadStatus } from '@/enums/upload-status'
import { useDialogueFile } from '@/composables/useDialogueFile'
import { useLayoutConfig } from '@/composables/useLayoutConfig'
import { TEXTTOSPEECH_WS_URL } from '@/config/base-url'
import WebSocketCtr from '@/utils/web-socket-ctr'
import { ChannelType } from '@/enums/channel'
import { useUploadImage } from '@/composables/useUploadImage'
import { showDialog } from 'vant'
interface Props {
agentId: string
dialogsId: string
messageList: Map<string, ConversationMessageItem>
continuousQuestionStatus: 'default' | 'close'
isEnableDocumentParse: boolean
isEnableUploadImage: boolean
isEnableVoice: boolean
answerAudioAutoPlay: boolean
answerAudioPlaying: boolean
timbreId: string
}
const { t } = useI18n()
const { query } = useRoute()
const props = defineProps<Props>()
const emit = defineEmits<{
addMessageItem: [messageId: string, value: ConversationMessageItem]
updateSpecifyMessageItem: [messageId: string, newObj: Partial<ConversationMessageItem>]
deleteMessageItem: [messageId: string]
updatePageScroll: []
clearAllMessage: []
toLogin: []
createContinueQuestions: [value: string]
resetContinueQuestionList: []
audioPlay: [messageItem: ConversationMessageItem, requestId?: string]
audioPause: []
}>()
const { isMobile } = useLayoutConfig()
const userStore = useUserStore()
const { uploadFileList, handleLimitUpload, handleUpload, handleRemoveFile } = useDialogueFile()
const { uploadImageList, handleLimitUploadImage, handleUploadImage, handleRemoveUploadImage } = useUploadImage()
const isAnswerResponseWait = defineModel<boolean>('isAnswerResponseLoading', { required: true })
const isAnswerResponseInterrupt = defineModel<boolean>('isAnswerResponseInterrupt', { required: true })
const emitter = inject<Emitter<MittEvents>>('emitter')
const inputMessageContent = ref('')
const currentReplyContentSentenceExtractIndex = ref(0)
const sentenceFragmentSerialNo = ref(0)
const sentenceExtractCheckEnabled = ref(false)
const assistantFullAnswerContent = ref('')
const sentenceSpeechException = ref(false)
const messageAudioLoading = ref(false)
const currentLatestMessageItemKeyMap = ref(new Map<'assistant' | 'user', string>())
let controller: AbortController | null = null
const isLogin = computed(() => {
return userStore.isLogin
})
const isAllowClearMessage = computed(() => {
return props.messageList.size > 0
})
const isSendBtnDisabled = computed(() => {
return !inputMessageContent.value.trim()
})
const inputPlaceholder = computed(() => {
return isLogin.value ? t('common_module.dialogue_module.question_input_placeholder') : ''
})
const isCreateContinueQuestions = computed(() => {
return props.continuousQuestionStatus === 'default'
})
const isInputMessageDisabled = computed(() => {
return (
uploadFileList.value.some((fileItem) => fileItem.status !== UploadStatus.FINISHED) ||
uploadImageList.value.some((imageItem) => imageItem.status !== UploadStatus.FINISHED)
)
})
const isUploadFileDisabled = computed(() => {
return uploadFileList.value.length === 1
})
watch(
() => uploadImageList.value.length,
() => {
emit('updatePageScroll')
},
)
const uploadFileIcon = (type: string) => {
return `https://gsst-poe-sit.gz.bcebos.com/icon/${type}.svg`
}
onMounted(() => {
emitter?.on('selectQuestion', (featuredQuestion) => {
if (!isLogin.value) {
window.$message.warning(t('common_module.not_login_text'))
return
}
inputMessageContent.value = featuredQuestion
handleMessageSend()
})
})
onUnmounted(() => {
blockMessageResponse()
emitter?.off('selectQuestion')
})
function messageItemFactory(): ConversationMessageItem {
return {
timestamp: Date.now(),
role: 'user',
textContent: '',
isEmptyContent: false,
isTextContentLoading: false,
isAnswerResponseLoading: false,
isVoiceLoading: false,
isVoicePlaying: false,
voiceFragmentUrlList: [],
pluginName: '',
imageUrl: '',
reasoningContent: '',
knowledgeContentResult: [],
}
}
function handleInputMessageEnter(event: KeyboardEvent) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault()
handleMessageSend()
}
}
function handleMessageSend(lastQuestionContent?: string) {
if (!isLogin.value) {
window.$message.warning(t('common_module.not_login_text'))
return
}
if (lastQuestionContent) {
inputMessageContent.value = lastQuestionContent
}
if (!inputMessageContent.value.trim() || isInputMessageDisabled.value) {
return
}
if (isAnswerResponseWait.value || messageAudioLoading.value) {
window.$message.warning(t('common_module.dialogue_module.do_not_operate_until_the_reply_is_complete'))
return
}
if (props.answerAudioPlaying) {
window.$message.warning(t('common_module.dialogue_module.stop_playing_and_then_operate'))
return
}
const latestUserMessageKey = nanoid()
const latestAssistantMessageKey = nanoid()
currentLatestMessageItemKeyMap.value.set('user', latestUserMessageKey)
currentLatestMessageItemKeyMap.value.set('assistant', latestAssistantMessageKey)
emit('resetContinueQuestionList')
emit('addMessageItem', latestUserMessageKey, {
...messageItemFactory(),
textContent: inputMessageContent.value,
imageUrl: uploadImageList.value?.[0]?.url || '',
})
emit('updatePageScroll')
emit('addMessageItem', latestAssistantMessageKey, {
...messageItemFactory(),
role: 'assistant',
isTextContentLoading: true,
isAnswerResponseLoading: true,
isVoiceLoading: true,
})
emit('updatePageScroll')
let replyTextContent = ''
let reasoningContent = ''
isAnswerResponseWait.value = true
isAnswerResponseInterrupt.value = false
currentReplyContentSentenceExtractIndex.value = 0
sentenceFragmentSerialNo.value = 0
sentenceExtractCheckEnabled.value = false
assistantFullAnswerContent.value = ''
sentenceSpeechException.value = false
messageAudioLoading.value = false
controller = new AbortController()
fetchCustomEventSource({
path: '/api/rest/agentApplicationRest/callAgentApplication.json',
payload: {
agentId: props.agentId,
dialogsId: props.dialogsId,
fileUrls: uploadFileList.value.map((item) => item.url),
input: inputMessageContent.value,
channel: query.channel || ChannelType.link_share,
imageUrl: uploadImageList.value?.[0]?.url || '',
},
controller,
onResponse: (data) => {
// 推理内容
if (data.reasoningContent) {
reasoningContent += data.reasoningContent
emit('updateSpecifyMessageItem', latestAssistantMessageKey, { reasoningContent })
emit('updatePageScroll')
return
}
// 插件
if (data.function && data.function.name) {
emit('updateSpecifyMessageItem', latestAssistantMessageKey, { pluginName: data.function.name })
emit('updatePageScroll')
return
}
// 回复内容
if (data.message) {
replyTextContent += data.message
emit('updateSpecifyMessageItem', latestAssistantMessageKey, {
textContent: replyTextContent,
isTextContentLoading: false,
})
emit('updatePageScroll')
assistantFullAnswerContent.value = (assistantFullAnswerContent.value + data.message).replace(
/\^\[[\d\\[\]-]+?\]\^/g,
'',
)
if (!sentenceExtractCheckEnabled.value && props.isEnableVoice) {
sentenceExtract(latestAssistantMessageKey)
sentenceExtractCheckEnabled.value = true
messageAudioLoading.value = true
}
}
},
onRequestError: () => {
errorMessageResponse()
},
onError: () => {
errorMessageResponse()
},
onFinally: () => {
emit('updateSpecifyMessageItem', latestAssistantMessageKey, {
isEmptyContent: !replyTextContent,
isTextContentLoading: false,
isAnswerResponseLoading: false,
})
isCreateContinueQuestions.value && emit('createContinueQuestions', replyTextContent)
emit('updatePageScroll')
isAnswerResponseWait.value = false
controller = null
userStore.isLogin && userStore.fetchUpdateEquityInfo()
},
})
inputMessageContent.value = ''
uploadImageList.value = []
}
function errorMessageResponse() {
emit('updateSpecifyMessageItem', currentLatestMessageItemKeyMap.value.get('assistant')!, {
isTextContentLoading: false,
textContent: '',
})
emit('deleteMessageItem', currentLatestMessageItemKeyMap.value.get('user')!)
emit('deleteMessageItem', currentLatestMessageItemKeyMap.value.get('assistant')!)
emit('audioPause')
blockMessageResponse()
}
function handleClearAllMessage() {
if (!isAllowClearMessage.value) return
emit('clearAllMessage')
}
function blockMessageResponse() {
controller?.abort()
isAnswerResponseWait.value = false
messageAudioLoading.value = false
userStore.isLogin && userStore.fetchUpdateEquityInfo()
}
function handleToLogin() {
emit('toLogin')
}
function handleSelectFile(cb: () => void) {
if (isUploadFileDisabled.value) {
window.$message.ctWarning('', t('common_module.dialogue_module.overwrite_file_tip')).then(() => {
cb()
})
return
}
cb()
}
function handleSelectImage(cb: () => void) {
if (uploadImageList.value.length > 0) {
showDialog({
title: '',
message: t('common_module.dialogue_module.overwrite_file_tip'),
showCancelButton: true,
cancelButtonText: t('common_module.cancel_btn_text'),
confirmButtonText: t('common_module.confirm_btn_text'),
confirmButtonColor: '#F25744',
}).then(() => {
cb()
})
return
}
cb()
}
function sentenceExtract(messageId: string) {
const symbolRegExp = /[。!?;.!?;~]/g
let sentenceDraft = assistantFullAnswerContent.value
.replace(/\s{5,}/gi, '')
.slice(currentReplyContentSentenceExtractIndex.value)
let matchResult = symbolRegExp.exec(sentenceDraft)
function matchExtract() {
const article = assistantFullAnswerContent.value.replace(/\s{5,}/gi, '')
if (matchResult && matchResult.index && matchResult.index > 60) {
sentenceDraft = article.slice(
currentReplyContentSentenceExtractIndex.value,
currentReplyContentSentenceExtractIndex.value + matchResult.index + matchResult['0'].length,
)
currentReplyContentSentenceExtractIndex.value += sentenceDraft.length
ttsSocketSendText(sentenceDraft, sentenceFragmentSerialNo.value, messageId)
sentenceFragmentSerialNo.value += 1
if (article.length - currentReplyContentSentenceExtractIndex.value > 60) {
sentenceDraft = article.slice(currentReplyContentSentenceExtractIndex.value)
matchResult = symbolRegExp.exec(sentenceDraft)
matchExtract()
} else {
setTimeout(() => sentenceExtract(messageId), 600)
}
} else if (!isAnswerResponseWait.value) {
/* 延时避免最后回复内容没有更新全 */
setTimeout(() => {
sentenceDraft = article.slice(currentReplyContentSentenceExtractIndex.value)
ttsSocketSendText(sentenceDraft, sentenceFragmentSerialNo.value, messageId)
sentenceFragmentSerialNo.value += 1
}, 700)
} else {
sentenceDraft = assistantFullAnswerContent.value
.replace(/\s{5,}/gi, '')
.slice(currentReplyContentSentenceExtractIndex.value)
matchResult = symbolRegExp.exec(sentenceDraft)
setTimeout(() => matchExtract(), 500)
}
}
if (matchResult) matchExtract()
else if (!isAnswerResponseWait.value) matchExtract()
else setTimeout(() => sentenceExtract(messageId), 600)
}
function ttsSocketSendText(text: string, audioUrlSerialNo: number, messageId: string) {
if (sentenceSpeechException.value) {
return
}
const ttsSocketCtl = new WebSocketCtr(TEXTTOSPEECH_WS_URL)
ttsSocketCtl.onMessage = (data: { audio: string; replyVoiceUrl: string; final: boolean }) => {
if (data.replyVoiceUrl) {
if (props.messageList.get(messageId)?.voiceFragmentUrlList) {
const voiceFragmentUrlListDraft = [...props.messageList.get(messageId)!.voiceFragmentUrlList]
voiceFragmentUrlListDraft[audioUrlSerialNo] = data.replyVoiceUrl
messageAudioLoading.value = false
emit('updateSpecifyMessageItem', messageId, { voiceFragmentUrlList: voiceFragmentUrlListDraft })
if (props.answerAudioAutoPlay && audioUrlSerialNo === 0 && voiceFragmentUrlListDraft[audioUrlSerialNo]) {
emit('audioPlay', props.messageList.get(messageId)!)
}
if (!props.answerAudioAutoPlay) {
emit('updateSpecifyMessageItem', messageId, { isVoiceLoading: false })
}
}
}
}
ttsSocketCtl.onMessageError = () => {
emit('updateSpecifyMessageItem', messageId, { isVoiceLoading: false, voiceFragmentUrlList: [] })
props.messageList.get(messageId)?.isVoicePlaying && emit('audioPause')
sentenceSpeechException.value = true
messageAudioLoading.value = false
window.$message.error(t('common_module.unplayable_tip'))
}
const content = (text || '').replace(/\^\[[\d\\[\]-]+?\]\^/g, '')
if (content && props.timbreId) {
ttsSocketCtl.connect(() => {
ttsSocketCtl.send({
codec: 'wav',
sampleRate: 16000,
speed: 0,
voiceType: props.timbreId,
volume: 0,
content,
})
})
}
}
defineExpose({
blockMessageResponse,
errorMessageResponse,
handleMessageSend,
})
</script>
<template>
<div class="footer-wrapper">
<div class="footer-main-container">
<n-upload
:show-file-list="false"
accept=".doc, .pdf, .docx, .txt, .md"
:disabled="!isLogin"
abstract
@before-upload="handleLimitUpload"
@change="handleUpload"
>
<n-upload-trigger #="{ handleClick }" abstract>
<n-popover style="width: 210px" trigger="hover">
<template #trigger>
<div
v-show="isEnableDocumentParse"
class="upload-file-btn"
:class="
isLogin ? 'text-theme-color cursor-pointer hover:opacity-80' : 'cursor-not-allowed text-[#b8babf]'
"
@click="handleSelectFile(handleClick)"
>
<i class="iconfont icon-upload" />
</div>
</template>
<span class="btn-tip"> {{ t('common_module.dialogue_module.upload_file_limit') }} </span>
</n-popover>
</n-upload-trigger>
</n-upload>
<n-upload
:show-file-list="false"
accept="image/png, image/jpeg, image/jpg, image/webp"
abstract
@before-upload="handleLimitUploadImage"
@change="handleUploadImage"
>
<n-upload-trigger #="{ handleClick }" abstract>
<n-popover style="width: 210px" trigger="hover">
<template #trigger>
<div
v-show="isEnableUploadImage"
class="upload-image-btn"
:class="
isLogin ? 'text-theme-color cursor-pointer hover:opacity-80' : 'cursor-not-allowed text-[#b8babf]'
"
@click="handleSelectImage(handleClick)"
>
<i class="iconfont icon-upload-image" />
</div>
</template>
<span class="btn-tip"> {{ t('common_module.dialogue_module.upload_image_limit') }} </span>
</n-popover>
</n-upload-trigger>
</n-upload>
<n-popover trigger="hover">
<template #trigger>
<div
class="clear-message-btn"
:class="
isAllowClearMessage
? 'text-theme-color cursor-pointer hover:opacity-80'
: 'cursor-not-allowed text-[#b8babf]'
"
@click="handleClearAllMessage"
>
<i class="iconfont icon-clear" />
</div>
</template>
<span class="btn-tip"> {{ t('common_module.dialogue_module.clear_message_popover_message') }}</span>
</n-popover>
<div class="footer-content-container">
<div class="upload-image-list-container">
<div
v-for="uploadImageItem in uploadImageList"
:key="uploadImageItem.id"
class="upload-image-item"
:class="{ 'upload-image-item-error': uploadImageItem.status === UploadStatus.ERROR }"
>
<div class="close-btn" @click="handleRemoveUploadImage(uploadImageItem.id)">
<CloseSmall theme="outline" size="16" fill="#fff" />
</div>
<div class="upload-image-item-content">
<n-spin v-show="uploadImageItem.status === UploadStatus.UPLOADING" :size="20" />
<n-image
v-show="uploadImageItem.status === UploadStatus.FINISHED"
object-fit="contain"
:src="uploadImageItem.url"
preview-disabled
class="upload-image"
/>
</div>
</div>
</div>
<ul v-show="uploadFileList.length > 0" class="upload-file-list-container">
<li
v-for="uploadFileItem in uploadFileList"
:key="uploadFileItem.id"
class="upload-file-item"
:class="uploadFileItem.status === 'error' ? 'border-error-font-color' : 'border-transparent'"
>
<div class="upload-file-item-container">
<div class="upload-file-item-content">
<img :src="uploadFileIcon(uploadFileItem.type!)" class="upload-file-icon" />
<div class="upload-file-name">
<n-ellipsis>
{{ uploadFileItem.name }}
</n-ellipsis>
</div>
</div>
<n-progress
v-show="!['finished', 'error'].includes(uploadFileItem.status)"
class="upload-file-item-progress"
type="line"
rail-color="#F3F3F3"
:height="4"
:percentage="uploadFileItem.percentage"
:show-indicator="false"
/>
<div
v-show="['finished', 'error'].includes(uploadFileItem.status)"
class="group-hover:block"
:class="isMobile ? 'block' : 'hidden'"
>
<n-popover trigger="hover" placement="top-end" :show-arrow="false">
<template #trigger>
<i
class="iconfont icon-close close-upload-file-icon"
:class="uploadFileItem.status === 'error' ? 'upload-file-error-icon' : 'upload-file-success-icon'"
@click="handleRemoveFile(uploadFileItem.id)"
/>
</template>
<span>{{ t('common_module.dialogue_module.cancel_associate_file_tip') }}</span>
</n-popover>
</div>
</div>
</li>
</ul>
<div class="footer-input-container">
<n-input
v-model:value="inputMessageContent"
type="textarea"
:autosize="{ minRows: 1, maxRows: 5 }"
:placeholder="inputPlaceholder"
:disabled="!isLogin || isInputMessageDisabled"
class="question-textarea"
@keydown="handleInputMessageEnter"
/>
<div
class="send-btn"
:class="
isSendBtnDisabled ||
isAnswerResponseWait ||
!isLogin ||
isInputMessageDisabled ||
answerAudioPlaying ||
messageAudioLoading
? 'opacity-60'
: 'cursor-pointer'
"
@click="handleMessageSend()"
/>
<div v-show="!isLogin" class="no-login-input-placeholder">
<span>{{ t('share_agent_module.please') }}</span>
<span class="to-login-text" @click="handleToLogin">
{{ t('common_module.login') }}
</span>
<span>{{ t('share_agent_module.after_action') }}</span>
</div>
</div>
</div>
</div>
<div class="footer-tip-container">
<span class="tip-text">
{{ t('common_module.dialogue_module.generate_warning_message') }}
</span>
</div>
</div>
</template>
<style lang="scss" scoped>
.footer-main-container {
@apply flex items-end gap-[10px];
.upload-file-btn,
.upload-image-btn,
.clear-message-btn {
@apply flex h-[35px] w-[35px] items-center justify-center rounded-full bg-[#F2F2F2] text-[12px];
i {
@apply flex h-[16px] w-[16px] items-center justify-center;
}
}
.footer-content-container {
@apply flex flex-1 flex-col;
.upload-image-list-container {
@apply flex gap-[14px];
.upload-image-item {
@apply border-inactive-border-color relative mb-[6px] h-[48px] w-[48px] rounded-[10px] border bg-white;
.close-btn {
@apply absolute right-[-4px] top-[-4px] flex h-[16px] w-[16px] cursor-pointer items-center justify-center rounded-full bg-[rgba(0,0,0,0.55)] hover:opacity-80;
}
&-content {
@apply flex h-full w-full items-center justify-center overflow-hidden rounded-[10px];
}
.upload-image {
@apply h-full w-full flex-shrink-0;
}
}
.upload-image-item-error {
@apply border-[#F25744]!;
}
}
.upload-file-list-container {
@apply mb-[6px] grid gap-[6px];
.upload-file-item {
@apply group relative flex h-[42px] w-full items-center overflow-hidden rounded-[10px] border bg-white/70;
&-container {
@apply flex w-full items-center justify-between;
.upload-file-item-content {
@apply flex w-full items-center overflow-hidden;
.upload-file-icon {
@apply h-[24px] w-[24px];
}
.upload-file-name {
@apply mx-[8px] flex flex-1 flex-col overflow-hidden text-[12px];
}
}
.upload-file-item-progress {
@apply w-[calc(100%-78px)]! absolute bottom-0 left-[54px];
}
.close-upload-file-icon {
@apply cursor-pointer text-[14px] hover:opacity-80;
}
.upload-file-error-icon {
@apply text-error-font-color;
}
.upload-file-success-icon {
@apply text-font-color;
}
}
}
}
.footer-input-container {
@apply relative flex-1;
.question-textarea {
@apply rounded-[20px]! shadow-[0_1px_#09122105,0_1px_1px_#09122105,0_3px_3px_#09122103,0_9px_9px_#09122103]! bg-[#F2F2F2]! pr-[38px];
}
.send-btn {
@apply bg-px-send-png absolute bottom-[9px] right-[16px] h-[18px] w-[18px];
}
.no-login-input-placeholder {
top: calc((100% - 12px) / 2);
@apply absolute left-[12px] flex max-w-[calc(100%-60px)] items-center overflow-hidden text-[12px] leading-none text-[#84868c];
span {
@apply shrink-0;
}
.to-login-text {
@apply text-theme-color cursor-pointer px-[4px] hover:opacity-80;
}
}
}
}
}
.footer-tip-container {
@apply mt-[7px];
.tip-text {
@apply flex w-full justify-center text-[11px] text-[#b8babf];
}
}
.btn-tip {
@apply text-[12px];
}
:deep(.question-textarea.n-input--textarea) {
--n-height: 35px !important;
--n-border: none !important;
--n-border-focus: none !important;
--n-border-hover: none !important;
--n-box-shadow-focus: none !important;
--n-border-disabled: none !important;
.n-input__placeholder {
font-size: 12px;
line-height: 24px;
}
}
</style>
<script setup lang="ts">
import { computed, ref, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import { CheckOne, Down } from '@icon-park/vue-next'
import CustomLoading from '../custom-loading.vue'
import MusicWavesLoading from './music-waves-loading.vue'
import MarkdownRender from '@/components/markdown-render/markdown-render.vue'
import { PersonalAppConfigState } from '@/store/types/personal-app-config'
interface Props {
role: 'user' | 'assistant'
messageItem: ConversationMessageItem
agentApplicationConfig: PersonalAppConfigState
}
const { t } = useI18n()
const props = defineProps<Props>()
const emit = defineEmits<{
audioPlay: []
audioPause: []
}>()
const markdownRenderRef = useTemplateRef<InstanceType<typeof MarkdownRender>>('markdownRenderRef')
const isShowReasoningContent = ref(true)
const timbreEnabled = computed(() => {
return !!props.agentApplicationConfig.voiceConfig.timbreId
})
const isShowAudioControl = computed(() => {
return props.role === 'assistant' && !props.messageItem.isVoiceLoading && timbreEnabled.value
})
const isPlayableAudio = computed(() => {
return isShowAudioControl.value && !!props.messageItem.voiceFragmentUrlList.length
})
const isShowVoiceLoading = computed(() => {
return (
props.role === 'assistant' &&
(props.messageItem.isAnswerResponseLoading || (props.messageItem.isVoiceLoading && timbreEnabled.value))
)
})
const isDeepSeekR1 = computed(() => {
return props.agentApplicationConfig.commModelConfig.largeModel === 'DeepSeek'
})
function handleAudioControl() {
if (!isPlayableAudio.value) {
return
}
if (props.messageItem.isVoicePlaying) {
emit('audioPause')
} else {
emit('audioPlay')
}
}
function handleShowReasoningContentSwitch() {
isShowReasoningContent.value = !isShowReasoningContent.value
}
</script>
<template>
<div class="message-item-wrapper" :class="[role === 'assistant' ? 'justify-end' : 'justify-start']">
<div class="message-item-container" :class="[role === 'user' ? 'items-end' : 'items-start']">
<!-- 大模型深度思考 -->
<template v-if="role === 'assistant' && isDeepSeekR1">
<div class="deep-seek-title-container">
<div class="deep-seek-name-container" @click="handleShowReasoningContentSwitch">
<span v-if="messageItem.isTextContentLoading" class="deep-seek-text">
{{ t('common_module.deep_thinking', { modelName: agentApplicationConfig.commModelConfig.largeModel }) }}
</span>
<span v-else class="deep-seek-text">
{{ t('common_module.have_thought_deeply') }}
</span>
<Down
theme="outline"
:size="18"
fill="#333"
:stroke-width="3"
class="transition-[rotate] duration-100 ease-linear"
:class="{ '-rotate-180': isShowReasoningContent }"
/>
</div>
</div>
<n-collapse-transition :show="isShowReasoningContent">
<div v-if="messageItem.reasoningContent" class="deep-seek-content-container">
<MarkdownRender
ref="markdownRenderRef"
:raw-text-content="
messageItem.reasoningContent
? messageItem.reasoningContent
: t('common_module.dialogue_module.empty_message_content')
"
color="#999"
:font-size="'3.2vw'"
/>
</div>
</n-collapse-transition>
</template>
<!-- 模型内容 -->
<div class="model-content-wrapper">
<div
class="model-content-container"
:class="[
{ 'user-model-content-container': role === 'user' },
{ 'assistant-model-content-container': role === 'assistant' },
]"
>
<img v-show="role === 'user' && messageItem.imageUrl" :src="messageItem.imageUrl" class="upload-image" />
<div v-show="role === 'assistant' && messageItem.pluginName" class="plugin-container">
<div v-show="messageItem.isTextContentLoading" class="plugin-loading" />
<CheckOne v-show="!messageItem.isTextContentLoading" theme="outline" size="16" fill="#40bd23" />
<span class="plugin-name">
{{
messageItem.isTextContentLoading
? t('common_module.plugin_in_progress', { pluginName: messageItem.pluginName })
: t('common_module.plugin_executed_successfully', { pluginName: messageItem.pluginName })
}}
</span>
</div>
<div v-if="messageItem.isTextContentLoading" class="content-loading">
<CustomLoading :active-color="'#000DFF'" />
</div>
<div v-else>
<p class="break-all">
<MarkdownRender
ref="markdownRenderRef"
:raw-text-content="
messageItem.isEmptyContent
? t('common_module.dialogue_module.empty_message_content')
: messageItem.textContent
"
:color="'#333'"
:font-size="'3.2vw'"
/>
</p>
<div v-show="isShowVoiceLoading || messageItem.isAnswerResponseLoading" class="answer-response-loading">
<CustomLoading :active-color="'#000DFF'" />
</div>
</div>
<!-- 移动端语音播放 -->
<div v-show="isShowAudioControl" class="audio-control-container">
<div
class="audio-control-icon"
:class="messageItem.isVoicePlaying ? 'bg-svg-pause' : 'bg-svg-play'"
@click="handleAudioControl"
/>
<MusicWavesLoading v-show="messageItem.isVoicePlaying && isPlayableAudio" bar-bg-color="#333" />
<n-popover style="max-width: 310px">
<template #trigger>
<span v-show="!isPlayableAudio"> {{ t('common_module.unplayable') }} </span>
</template>
{{ t('common_module.unplayable_tip') }}
</n-popover>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.message-item-wrapper {
@apply mb-[15px] flex flex-row-reverse last:mb-0;
.message-item-container {
@apply flex w-full flex-col overflow-x-auto;
.deep-seek-title-container {
@apply my-[7px] select-none text-[12px];
.deep-seek-name-container {
@apply inline-flex cursor-pointer;
.deep-seek-text {
@apply mr-[6px];
}
}
}
.deep-seek-content-container {
@apply my-[10px] border-l-[1px] border-solid border-l-[#ccc] px-[13px] py-[8px];
}
.model-content-wrapper {
@apply flex min-w-[80px] max-w-full flex-col;
.model-content-container {
@apply w-full flex-wrap rounded-[10px] border px-[12px] py-[11px];
.upload-image {
@apply max-h-[120px]! mb-[10px] rounded-[10px] object-contain;
}
.plugin-container {
@apply mb-[8px] flex items-center gap-[5px] font-['Microsoft_YaHei_UI'] text-[#999];
.plugin-loading {
@apply bg-px-plugin_loading-gif h-[14px] w-[14px] bg-contain bg-center bg-no-repeat;
}
.plugin-name {
@apply text-[12px] leading-4;
}
}
.content-loading {
@apply py-[6px] pl-[16px];
}
.answer-response-loading {
@apply mb-[5px] mt-[16px] px-[16px];
}
.audio-control-container {
@apply mt-[11px] flex items-center gap-[8px] text-[12px];
.audio-control-icon {
@apply h-[16px] w-[16px] cursor-pointer;
}
}
}
.user-model-content-container {
@apply rounded-tr-none border-[#DCDEFF] bg-[#DCDEFF] text-white;
}
.assistant-model-content-container {
@apply rounded-tl-none border-[#e8e9eb] bg-white text-[#333];
}
}
}
}
</style>
<script setup lang="ts">
import { computed, nextTick, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useScroll } from '@/composables/useScroll'
import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import { useBackBottom } from '@/composables/useBackBottom'
import MessageItem from './message-item.vue'
import ContinueQuestion from './continue-question.vue'
interface Props {
messageList: Map<string, ConversationMessageItem>
agentApplicationConfig: PersonalAppConfigState
continuousQuestionStatus: 'default' | 'close'
isAnswerResponseLoading: boolean
createContinueQuestionsException: boolean
isAnswerResponseInterrupt?: boolean
}
const props = defineProps<Props>()
defineEmits<{
audioPlay: [messageItem: ConversationMessageItem]
audioPause: []
}>()
const { t } = useI18n()
const { scrollRef, scrollToBottom } = useScroll()
const { visible, clickBackBottom, throttleScrollContainer } = useBackBottom(scrollRef, scrollToBottom)
const continuousQuestionList = defineModel<string[]>('continuousQuestionList', { required: true })
const isShowContinueQuestion = computed(() => {
return (
props.continuousQuestionStatus === 'default' &&
props.messageList.size > 1 &&
!props.isAnswerResponseLoading &&
!props.createContinueQuestionsException &&
!props.isAnswerResponseInterrupt
)
})
watch(
() => props.messageList.size,
() => {
clickBackBottom()
},
)
defineExpose({
scrollToBottom: handleScrollToBottom,
})
function handleScrollToBottom() {
nextTick(() => {
!visible.value && scrollToBottom()
})
}
</script>
<template>
<main ref="scrollRef" class="message-list-container" @scroll="throttleScrollContainer">
<div>
<MessageItem
v-for="[key, messageItem] in messageList"
:key="key"
:role="messageItem.role"
:message-item="messageItem"
:agent-application-config="agentApplicationConfig"
@audio-play="() => $emit('audioPlay', messageItem)"
@audio-pause="() => $emit('audioPause')"
/>
</div>
<p v-show="isAnswerResponseLoading" class="answer-response-loading-text">
{{ t('common_module.dialogue_module.do_not_exit_page') }}
</p>
<div v-show="isShowContinueQuestion">
<ContinueQuestion v-model:continuous-question-list="continuousQuestionList" :type="'continuous'" />
</div>
<div v-show="visible" class="back-bottom-btn" @click.stop="clickBackBottom">
<i class="iconfont icon-left back-bottom-btn-icon" />
</div>
</main>
</template>
<style lang="scss" scoped>
.message-list-container {
@apply h-full overflow-y-auto overflow-x-hidden px-[20px];
.answer-response-loading-text {
@apply my-[7px] ml-[4px] text-[12px] text-[#84868c];
}
.back-bottom-btn {
@apply flex-center hover:text-theme-color absolute bottom-[20px] right-[20px] h-[24px] w-[24px] cursor-pointer rounded-full bg-white shadow-[0_0_0_1px_#ededed];
&-icon {
@apply rotate-270 text-[14px];
}
}
}
</style>
<script setup lang="ts">
interface Props {
barBgColor?: string
height?: string
}
withDefaults(defineProps<Props>(), {
barBgColor: '#363636',
height: '12px',
})
</script>
<template>
<div class="music">
<div class="bar" />
<div class="bar" />
<div class="bar" />
<div class="bar" />
<div class="bar" />
<div class="bar" />
<div class="bar" />
<div class="bar" />
<div class="bar" />
<div class="bar" />
</div>
</template>
<style lang="scss" scoped>
.music {
display: flex;
align-items: center;
justify-content: space-between;
width: 42px;
height: v-bind(height);
.bar {
width: 2px;
/* stylelint-disable-next-line value-keyword-case */
background: v-bind(barBgColor);
border-radius: 50px;
animation: loader 1.5s ease-in-out infinite;
&:nth-child(1) {
/* background: purple; */
animation-delay: 1s;
}
&:nth-child(2) {
/* background: crimson; */
animation-delay: 0.8s;
}
&:nth-child(3) {
/* background: deeppink; */
animation-delay: 0.6s;
}
&:nth-child(4) {
/* background: orange; */
animation-delay: 0.4s;
}
&:nth-child(5) {
/* background: gold; */
animation-delay: 0.2s;
}
&:nth-child(6) {
/* background: gold; */
animation-delay: 0.2s;
}
&:nth-child(7) {
/* background: gold; */
animation-delay: 0.4s;
}
&:nth-child(8) {
/* background: deeppink; */
animation-delay: 0.6s;
}
&:nth-child(9) {
/* background: crimson; */
animation-delay: 0.8s;
}
&:nth-child(10) {
/* background: purple; */
animation-delay: 1s;
}
}
}
@keyframes loader {
0% {
height: 4px;
}
50% {
height: v-bind(height);
}
100% {
height: 4px;
}
}
</style>
<script setup lang="ts">
import { useUserStore } from '@/store/modules/user'
import { computed, CSSProperties } from 'vue'
import { useI18n } from 'vue-i18n'
import { Logout } from '@icon-park/vue-next'
interface Props {
agentTitle: string
}
const { t } = useI18n()
defineProps<Props>()
const emit = defineEmits<{
toCreateApplication: []
toLogin: []
toLogout: []
updateAutoPlaying: [value: boolean]
}>()
const userStore = useUserStore()
const isEnableVoice = defineModel<boolean>('isEnableVoice', { required: true })
const answerAudioAutoPlay = defineModel<boolean>('answerAudioAutoPlay', { required: true })
const railStyle = ({ focused, checked }: { focused: boolean; checked: boolean }) => {
const style: CSSProperties = {}
if (checked) {
style.background = '#CFD1FF'
} else {
if (focused) {
style.boxShadow = '0 0 0 2px #ddd'
}
}
return style
}
const isLogin = computed(() => {
return userStore.isLogin
})
function handleToCreateApplication() {
emit('toCreateApplication')
}
function handleToLogin() {
emit('toLogin')
}
function handleToLogout() {
emit('toLogout')
}
</script>
<template>
<header class="page-header-wrapper">
<div>
<div v-show="isEnableVoice" class="enable-voice-container">
<span class="auto-play">{{ t('common_module.voice_auto_play') }}</span>
<n-switch
v-model:value="answerAudioAutoPlay"
size="small"
:rail-style="railStyle"
@update:value="(isAutoPlay: boolean) => emit('updateAutoPlaying', isAutoPlay)"
>
<template #checked> {{ t('common_module.open') }} </template>
<template #unchecked> {{ t('common_module.close') }} </template>
<template #checked-icon>
<div class="checked-icon"></div>
</template>
</n-switch>
</div>
</div>
<div class="page-header-right-container">
<NButton
v-show="isLogin"
type="primary"
color="#EBECFF"
class="create-agent-btn"
@click="handleToCreateApplication"
>
{{ t('common_module.create_agent_btn_text') }}
</NButton>
<NButton v-show="!isLogin" color="#EBECFF" class="login-btn" @click="handleToLogin">
{{ t('common_module.login_now') }}
</NButton>
<div v-show="isLogin" class="logout-btn">
<Logout theme="outline" size="15" fill="#000DFF" :stroke-width="4" @click="handleToLogout" />
</div>
</div>
</header>
</template>
<style lang="scss" scoped>
.page-header-wrapper {
@apply flex h-[58px] w-full items-center justify-between px-[20px];
.enable-voice-container {
@apply flex items-center gap-[8px];
.auto-play {
@apply text-[12px];
}
.checked-icon {
@apply bg-theme-color h-full w-full rounded-full;
}
}
.page-header-right-container {
@apply flex-center gap-[10px];
.create-agent-btn {
@apply rounded-md! h-[28px]! text-[12px]! min-w-[80px]! text-theme-color!;
}
.login-btn {
@apply rounded-md! h-[28px]! text-[12px]! min-w-[80px]! text-theme-color!;
}
.logout-btn {
@apply flex-center h-[28px] w-[28px] rounded-[5px] bg-[#EBECFF];
}
}
}
</style>
<script setup lang="ts">
import { computed } from 'vue'
import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import ContinueQuestion from './continue-question.vue'
interface Props {
agentApplicationConfig: PersonalAppConfigState
}
const props = defineProps<Props>()
const agentApplicationConfig = computed(() => props.agentApplicationConfig)
const agentAvatar = computed(() => {
return (
agentApplicationConfig.value.baseInfo.agentAvatar ||
'https://gsst-poe-sit.gz.bcebos.com/data/20240911/1726041369632.webp'
)
})
</script>
<template>
<div class="preamble-wrapper">
<div class="avatar-container">
<img :src="agentAvatar" class="agent-avatar" />
</div>
<div class="preamble-container">
<p class="agent-title">
{{ agentApplicationConfig.baseInfo.agentTitle }}
</p>
<div class="flex w-full flex-col items-start justify-center">
<p v-show="agentApplicationConfig.commConfig.preamble" class="preamble">
{{ agentApplicationConfig.commConfig.preamble }}
</p>
<ContinueQuestion
v-model:continuous-question-list="agentApplicationConfig.commConfig.featuredQuestions"
:type="'featured'"
/>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.preamble-wrapper {
@apply flex w-full flex-col px-[20px];
.avatar-container {
@apply mb-[7.5px] mt-[25px] flex w-full justify-center;
.agent-avatar {
@apply rounded-theme h-[40px] w-[40px] object-cover;
}
}
.preamble-container {
@apply flex flex-col items-center justify-center;
.agent-title {
@apply font-family-medium mb-[17px] line-clamp-1 text-[13px] text-[#333];
}
.preamble {
@apply select-none break-all rounded-[10px] rounded-tl-none bg-[#DCDEFF] px-[12.5px] py-[11px] text-[12px];
}
}
}
</style>
...@@ -3,12 +3,14 @@ import { computed, onMounted, onUnmounted, ref, shallowRef } from 'vue' ...@@ -3,12 +3,14 @@ import { computed, onMounted, onUnmounted, ref, shallowRef } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { Howl } from 'howler' import { Howl } from 'howler'
import { showDialog } from 'vant'
import 'vant/es/dialog/style'
import { useEventListener } from '@vueuse/core' import { useEventListener } from '@vueuse/core'
import type { ValueOf } from 'type-fest' import type { ValueOf } from 'type-fest'
import PageHeader from './components/mobile-page-header.vue' import PageHeader from './components/mobile/page-header.vue'
import Preamble from './components/preamble.vue' import Preamble from './components/mobile/preamble.vue'
import MessageList from './components/message-list.vue' import MessageList from './components/mobile/message-list.vue'
import FooterInput from './components/footer-input.vue' import FooterInput from './components/mobile/footer-input.vue'
import { PersonalAppConfigState } from '@/store/types/personal-app-config' import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
import { defaultPersonalAppConfigState } from '@/store/modules/personal-app-config' import { defaultPersonalAppConfigState } from '@/store/modules/personal-app-config'
...@@ -160,10 +162,10 @@ function handleToLoginPage() { ...@@ -160,10 +162,10 @@ function handleToLoginPage() {
} }
function handleCreateApplicationPage() { function handleCreateApplicationPage() {
window.$dialog.info({ showDialog({
title: t('share_agent_module.create_agent_dialogue_title'), title: t('share_agent_module.create_agent_dialogue_title'),
content: t('share_agent_module.create_agent_dialogue_content'), message: t('share_agent_module.create_agent_dialogue_content'),
positiveText: t('share_agent_module.create_agent_dialogue_positive_text'), confirmButtonText: t('share_agent_module.create_agent_dialogue_positive_text'),
}) })
} }
...@@ -204,18 +206,20 @@ function handleUpdatePageScroll() { ...@@ -204,18 +206,20 @@ function handleUpdatePageScroll() {
} }
function handleClearAllMessage() { function handleClearAllMessage() {
window.$message showDialog({
.ctWarning( title: t('common_module.dialogue_module.clear_message_dialog_title'),
t('common_module.dialogue_module.clear_message_dialog_content'), message: t('common_module.dialogue_module.clear_message_dialog_content'),
t('common_module.dialogue_module.clear_message_dialog_title'), showCancelButton: true,
) cancelButtonText: t('common_module.cancel_btn_text'),
.then(() => { confirmButtonText: t('common_module.confirm_btn_text'),
handleAudioPause() confirmButtonColor: '#F25744',
footerInputRef.value?.blockMessageResponse() }).then(() => {
messageList.value.clear() handleAudioPause()
answerAudioPlaying.value = false footerInputRef.value?.blockMessageResponse()
window.$message.success(t('common_module.clear_success_message')) messageList.value.clear()
}) answerAudioPlaying.value = false
window.$message.success(t('common_module.clear_success_message'))
})
} }
async function handleCreateContinueQuestions(replyTextContent: string) { async function handleCreateContinueQuestions(replyTextContent: string) {
...@@ -355,32 +359,43 @@ function handleExitPage() { ...@@ -355,32 +359,43 @@ function handleExitPage() {
footerInputRef.value?.errorMessageResponse() footerInputRef.value?.errorMessageResponse()
} }
} }
function handleToLogoutPage() {
showDialog({
title: t('login_module.confirm_to_logout'),
message: t('login_module.you_can_login_again_after_logout'),
showCancelButton: true,
cancelButtonText: t('common_module.cancel_btn_text'),
confirmButtonText: t('common_module.logout'),
confirmButtonColor: '#F25744',
}).then(() => {
userStore.logout()
handleAudioPause()
footerInputRef.value?.blockMessageResponse()
messageList.value.clear()
answerAudioPlaying.value = false
})
}
</script> </script>
<template> <template>
<div v-loading="fullScreenLoading" class="h-full w-full"> <div v-loading="fullScreenLoading" class="share-mobile-container">
<PageHeader <PageHeader
v-model:is-enable-voice="isEnableVoice"
v-model:answer-audio-auto-play="answerAudioAutoPlay"
:agent-title="agentApplicationConfig.baseInfo.agentTitle" :agent-title="agentApplicationConfig.baseInfo.agentTitle"
@to-login="handleToLoginPage" @to-login="handleToLoginPage"
@to-logout="handleToLogoutPage"
@to-create-application="handleCreateApplicationPage" @to-create-application="handleCreateApplicationPage"
@update-auto-playing="handleUpdateAutoPlaying"
/> />
<div class="flex h-[calc(100%-48px)] w-full flex-col bg-[#f2f5f9]"> <div class="main-container">
<div class="mt-5 flex select-none justify-end px-4"> <div v-if="messageList.size === 0" class="w-full flex-1 overflow-auto">
<div v-show="isEnableVoice" class="flex items-center gap-2">
<span>{{ t('common_module.voice_auto_play') }}</span>
<n-switch v-model:value="answerAudioAutoPlay" size="small" @update:value="handleUpdateAutoPlaying">
<template #checked> {{ t('common_module.open') }} </template>
<template #unchecked> {{ t('common_module.close') }} </template>
</n-switch>
</div>
</div>
<div v-if="messageList.size === 0" class="w-full flex-1 overflow-auto px-4">
<Preamble :agent-application-config="agentApplicationConfig" /> <Preamble :agent-application-config="agentApplicationConfig" />
</div> </div>
<div v-if="messageList.size > 0" class="flex w-full flex-1 flex-col overflow-hidden pt-5"> <div v-if="messageList.size > 0" class="flex w-full flex-1 flex-col overflow-hidden">
<div class="relative flex-1 overflow-auto"> <div class="relative flex-1 overflow-auto">
<MessageList <MessageList
ref="messageListRef" ref="messageListRef"
...@@ -397,7 +412,7 @@ function handleExitPage() { ...@@ -397,7 +412,7 @@ function handleExitPage() {
</div> </div>
</div> </div>
<div class="footer-operation px-4"> <div class="footer-container">
<FooterInput <FooterInput
ref="footerInputRef" ref="footerInputRef"
v-model:is-answer-response-loading="isAnswerResponseLoading" v-model:is-answer-response-loading="isAnswerResponseLoading"
...@@ -429,9 +444,19 @@ function handleExitPage() { ...@@ -429,9 +444,19 @@ function handleExitPage() {
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.footer-operation { .share-mobile-container {
padding-bottom: constant(safe-area-inset-bottom); @apply bg-px-share-h5_bg-png h-full w-full bg-cover bg-no-repeat;
padding-bottom: env(safe-area-inset-bottom);
.main-container {
@apply flex h-[calc(100%-58px)] w-full flex-col;
}
.footer-container {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
@apply bg-white px-[10px] py-[7px];
}
} }
@include custom-scrollbar(4px); @include custom-scrollbar(4px);
......
...@@ -44,6 +44,7 @@ declare namespace I18n { ...@@ -44,6 +44,7 @@ declare namespace I18n {
publish_success_message: string publish_success_message: string
clear_success_message: string clear_success_message: string
add_success_message: string add_success_message: string
reset_success_message: string
loading: string loading: string
updating: string updating: string
successful_update: string successful_update: string
...@@ -151,6 +152,7 @@ declare namespace I18n { ...@@ -151,6 +152,7 @@ declare namespace I18n {
not_certified_yet: string not_certified_yet: string
authenticated: string authenticated: string
cancel_authorization: string cancel_authorization: string
get_code: string
dialogue_module: { dialogue_module: {
continue_question_message: string continue_question_message: string
...@@ -217,6 +219,7 @@ declare namespace I18n { ...@@ -217,6 +219,7 @@ declare namespace I18n {
order_manage: string order_manage: string
data_statistic: string data_statistic: string
plugin_center: string plugin_center: string
reset_password: string
} }
login_module: { login_module: {
...@@ -233,6 +236,29 @@ declare namespace I18n { ...@@ -233,6 +236,29 @@ declare namespace I18n {
login_success: string login_success: string
get_verification_code: string get_verification_code: string
other_login_methods: string other_login_methods: string
mobile_welcome_words: string
sign_in_with_mobile_number: string
verification_code_login: string
password_login: string
email_login: string
forgot_password: string
agreement_prefix: string
agreement_terms: string
agreement_privacy: string
agreement_and: string
confirm_to_logout: string
you_can_login_again_after_logout: string
please_review_and_accept_the_agreement: string
agree_and_continue: string
disagree: string
}
reset_password_module: {
reset_login_password: string
please_enter_your_registered_phone_number: string
please_enter_your_new_password: string
back_to_login: string
reset_password: string
} }
home_module: { home_module: {
......
import { defineConfig } from 'unocss' import { defineConfig, transformerDirectives, presetUno } from 'unocss'
export default defineConfig({ export default defineConfig({
rules: [ rules: [
...@@ -87,4 +87,6 @@ export default defineConfig({ ...@@ -87,4 +87,6 @@ export default defineConfig({
shortcuts: { shortcuts: {
'flex-center': 'flex items-center justify-center', 'flex-center': 'flex items-center justify-center',
}, },
presets: [presetUno()],
transformers: [transformerDirectives()],
}) })
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment