41
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
41
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: 报告bug
|
||||||
|
labels: [bug]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
感谢反馈问题,请填写下方的表单完成本次问题的反馈
|
||||||
|
- type: input
|
||||||
|
id: log-lottery-version
|
||||||
|
attributes:
|
||||||
|
label: log-lottery版本
|
||||||
|
description: |
|
||||||
|
你所使用的 log-lottery 的准确版本。
|
||||||
|
placeholder: eg. v0.5.0
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: browser
|
||||||
|
attributes:
|
||||||
|
label: 浏览器及其版本
|
||||||
|
description: |
|
||||||
|
你是在哪个浏览器中发现的这个问题?最好可以提供浏览器的版本号。
|
||||||
|
placeholder: eg. Google Chrome 111.0.5563.110
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: what-happened
|
||||||
|
attributes:
|
||||||
|
label: 具体问题
|
||||||
|
description: |
|
||||||
|
出现了什么问题?
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: 报错信息
|
||||||
|
description: |
|
||||||
|
如果有的话,请粘贴你遇到的报错信息或日志。下面输入框中的内容在 issue 提交后会被自动格式化成代码块。
|
||||||
|
render: shell
|
||||||
24
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
24
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: 建议添加新功能
|
||||||
|
labels: [enhancement]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
感谢你抽时间提出优化建议,请填写下方的表单完成本次问题的反馈
|
||||||
|
- type: input
|
||||||
|
id: log-lottery-version
|
||||||
|
attributes:
|
||||||
|
label: log-lottery版本
|
||||||
|
description: |
|
||||||
|
你所使用的 log-lottery 的准确版本。
|
||||||
|
placeholder: eg. v0.5.0
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: feature-description
|
||||||
|
attributes:
|
||||||
|
label: 你的想法是什么?
|
||||||
|
placeholder: 请尽可能详细地告诉我你期望实现一个什么样的功能或者哪里需要进行优化
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
19
.github/ISSUE_TEMPLATE/general.yml
vendored
Normal file
19
.github/ISSUE_TEMPLATE/general.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
name: General Issue
|
||||||
|
description: 新开空白issue模板
|
||||||
|
body:
|
||||||
|
- type: input
|
||||||
|
id: log-lottery-version
|
||||||
|
attributes:
|
||||||
|
label: log-lottery版本
|
||||||
|
description: |
|
||||||
|
你所使用的 log-lottery 的准确版本。
|
||||||
|
placeholder: eg. v0.5.0
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: 问题描述
|
||||||
|
placeholder: 请尽可能详细地告诉我你所遇到的问题
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -113,6 +113,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Download web build artifact
|
- name: Download web build artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -75,7 +75,6 @@ web_modules/
|
|||||||
.yarn-integrity
|
.yarn-integrity
|
||||||
|
|
||||||
# dotenv environment variable files
|
# dotenv environment variable files
|
||||||
.env
|
|
||||||
.env.development.local
|
.env.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
.env.production.local
|
.env.production.local
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
. "$(dirname "$0")/h"
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
. "$(dirname "$0")/h"
|
|
||||||
22
.husky/_/h
22
.husky/_/h
@@ -1,22 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
[ "$HUSKY" = "2" ] && set -x
|
|
||||||
n=$(basename "$0")
|
|
||||||
s=$(dirname "$(dirname "$0")")/$n
|
|
||||||
|
|
||||||
[ ! -f "$s" ] && exit 0
|
|
||||||
|
|
||||||
if [ -f "$HOME/.huskyrc" ]; then
|
|
||||||
echo "husky - '~/.huskyrc' is DEPRECATED, please move your code to ~/.config/husky/init.sh"
|
|
||||||
fi
|
|
||||||
i="${XDG_CONFIG_HOME:-$HOME/.config}/husky/init.sh"
|
|
||||||
[ -f "$i" ] && . "$i"
|
|
||||||
|
|
||||||
[ "${HUSKY-}" = "0" ] && exit 0
|
|
||||||
|
|
||||||
export PATH="node_modules/.bin:$PATH"
|
|
||||||
sh -e "$s" "$@"
|
|
||||||
c=$?
|
|
||||||
|
|
||||||
[ $c != 0 ] && echo "husky - $n script failed (code $c)"
|
|
||||||
[ $c = 127 ] && echo "husky - command not found in PATH=$PATH"
|
|
||||||
exit $c
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
echo "husky - DEPRECATED
|
|
||||||
|
|
||||||
Please remove the following two lines from $0:
|
|
||||||
|
|
||||||
#!/usr/bin/env sh
|
|
||||||
. \"\$(dirname -- \"\$0\")/_/husky.sh\"
|
|
||||||
|
|
||||||
They WILL FAIL in v10.0.0
|
|
||||||
"
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
. "$(dirname "$0")/h"
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
. "$(dirname "$0")/h"
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
. "$(dirname "$0")/h"
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
. "$(dirname "$0")/h"
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
. "$(dirname "$0")/h"
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
. "$(dirname "$0")/h"
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
. "$(dirname "$0")/h"
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
. "$(dirname "$0")/h"
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
. "$(dirname "$0")/h"
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
. "$(dirname "$0")/h"
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
. "$(dirname "$0")/h"
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
. "$(dirname "$0")/h"
|
|
||||||
@@ -13,12 +13,15 @@
|
|||||||
[](<https://hub.docker.com/r/log1997/log-lottery>)
|
[](<https://hub.docker.com/r/log1997/log-lottery>)
|
||||||
[](https://github.com/LOG1997/log-lottery/releases)
|
[](https://github.com/LOG1997/log-lottery/releases)
|
||||||
[](https://github.com/LOG1997/log-lottery/releases)
|
[](https://github.com/LOG1997/log-lottery/releases)
|
||||||
|
[](https://github.com/LOG1997/log-lottery/commits/dev/)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
log-lottery是一个可配置可定制化的抽奖应用,炫酷3D球体,可用于年会抽奖等活动,支持奖品、人员、界面、图片音乐配置。
|
log-lottery是一个可配置可定制化的抽奖应用,炫酷3D球体,可用于年会抽奖等活动,支持奖品、人员、界面、图片音乐配置。
|
||||||
|
|
||||||
> 如果进入网站遇到图片无法显示或有报错的情况,请先到【全局配置】-【界面配置】菜单中点击【重置所有数据】按钮清除数据后进行更新。
|
> 如果进入网站遇到图片无法显示或有报错的情况,请先到【全局配置】-【界面配置】菜单中点击【重置所有数据】按钮清除数据后进行更新。
|
||||||
|
|
||||||
|
> 不支持内定功能
|
||||||
|
|
||||||
## 要求
|
## 要求
|
||||||
|
|
||||||
使用PC端最新版Chrome或Edge浏览器。
|
使用PC端最新版Chrome或Edge浏览器。
|
||||||
|
|||||||
@@ -19,8 +19,11 @@
|
|||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fingerprintjs/fingerprintjs": "^5.0.1",
|
||||||
|
"@headlessui/vue": "^1.7.23",
|
||||||
"@tweenjs/tween.js": "23.1.2",
|
"@tweenjs/tween.js": "23.1.2",
|
||||||
"@vueuse/core": "^14.1.0",
|
"@vueuse/core": "^14.1.0",
|
||||||
|
"@vueuse/integrations": "^14.1.0",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"canvas-confetti": "^1.9.4",
|
"canvas-confetti": "^1.9.4",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
@@ -36,6 +39,7 @@
|
|||||||
"masonry-layout": "^4.2.2",
|
"masonry-layout": "^4.2.2",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"pinia-plugin-persist": "^1.0.0",
|
"pinia-plugin-persist": "^1.0.0",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"reka-ui": "^2.7.0",
|
"reka-ui": "^2.7.0",
|
||||||
"sparticles": "^1.3.1",
|
"sparticles": "^1.3.1",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
|
|||||||
219
pnpm-lock.yaml
generated
219
pnpm-lock.yaml
generated
@@ -8,12 +8,21 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@fingerprintjs/fingerprintjs':
|
||||||
|
specifier: ^5.0.1
|
||||||
|
version: 5.0.1
|
||||||
|
'@headlessui/vue':
|
||||||
|
specifier: ^1.7.23
|
||||||
|
version: 1.7.23(vue@3.5.26(typescript@5.9.3))
|
||||||
'@tweenjs/tween.js':
|
'@tweenjs/tween.js':
|
||||||
specifier: 23.1.2
|
specifier: 23.1.2
|
||||||
version: 23.1.2
|
version: 23.1.2
|
||||||
'@vueuse/core':
|
'@vueuse/core':
|
||||||
specifier: ^14.1.0
|
specifier: ^14.1.0
|
||||||
version: 14.1.0(vue@3.5.26(typescript@5.9.3))
|
version: 14.1.0(vue@3.5.26(typescript@5.9.3))
|
||||||
|
'@vueuse/integrations':
|
||||||
|
specifier: ^14.1.0
|
||||||
|
version: 14.1.0(axios@1.13.2)(change-case@5.4.4)(qrcode@1.5.4)(vue@3.5.26(typescript@5.9.3))
|
||||||
axios:
|
axios:
|
||||||
specifier: ^1.13.2
|
specifier: ^1.13.2
|
||||||
version: 1.13.2
|
version: 1.13.2
|
||||||
@@ -59,6 +68,9 @@ importers:
|
|||||||
pinia-plugin-persist:
|
pinia-plugin-persist:
|
||||||
specifier: ^1.0.0
|
specifier: ^1.0.0
|
||||||
version: 1.0.0(pinia@3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)))(vue@3.5.26(typescript@5.9.3))
|
version: 1.0.0(pinia@3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)))(vue@3.5.26(typescript@5.9.3))
|
||||||
|
qrcode:
|
||||||
|
specifier: ^1.5.4
|
||||||
|
version: 1.5.4
|
||||||
reka-ui:
|
reka-ui:
|
||||||
specifier: ^2.7.0
|
specifier: ^2.7.0
|
||||||
version: 2.7.0(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))
|
version: 2.7.0(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))
|
||||||
@@ -1133,6 +1145,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
|
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
|
'@fingerprintjs/fingerprintjs@5.0.1':
|
||||||
|
resolution: {integrity: sha512-KbaeE/rk2WL8MfpRP6jTI4lSr42SJPjvkyrjP3QU6uUDkOMWWYC2Ts1sNSYcegHC8avzOoYTHBj+2fTqvZWQBA==}
|
||||||
|
|
||||||
'@floating-ui/core@1.7.3':
|
'@floating-ui/core@1.7.3':
|
||||||
resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==}
|
resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==}
|
||||||
|
|
||||||
@@ -1145,6 +1160,12 @@ packages:
|
|||||||
'@floating-ui/vue@1.1.9':
|
'@floating-ui/vue@1.1.9':
|
||||||
resolution: {integrity: sha512-BfNqNW6KA83Nexspgb9DZuz578R7HT8MZw1CfK9I6Ah4QReNWEJsXWHN+SdmOVLNGmTPDi+fDT535Df5PzMLbQ==}
|
resolution: {integrity: sha512-BfNqNW6KA83Nexspgb9DZuz578R7HT8MZw1CfK9I6Ah4QReNWEJsXWHN+SdmOVLNGmTPDi+fDT535Df5PzMLbQ==}
|
||||||
|
|
||||||
|
'@headlessui/vue@1.7.23':
|
||||||
|
resolution: {integrity: sha512-JzdCNqurrtuu0YW6QaDtR2PIYCKPUWq28csDyMvN4zmGccmE7lz40Is6hc3LA4HFeCI7sekZ/PQMTNmn9I/4Wg==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^3.2.0
|
||||||
|
|
||||||
'@humanfs/core@0.19.1':
|
'@humanfs/core@0.19.1':
|
||||||
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
||||||
engines: {node: '>=18.18.0'}
|
engines: {node: '>=18.18.0'}
|
||||||
@@ -1955,6 +1976,48 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vue: ^3.5.0
|
vue: ^3.5.0
|
||||||
|
|
||||||
|
'@vueuse/integrations@14.1.0':
|
||||||
|
resolution: {integrity: sha512-eNQPdisnO9SvdydTIXnTE7c29yOsJBD/xkwEyQLdhDC/LKbqrFpXHb3uS//7NcIrQO3fWVuvMGp8dbK6mNEMCA==}
|
||||||
|
peerDependencies:
|
||||||
|
async-validator: ^4
|
||||||
|
axios: ^1
|
||||||
|
change-case: ^5
|
||||||
|
drauu: ^0.4
|
||||||
|
focus-trap: ^7
|
||||||
|
fuse.js: ^7
|
||||||
|
idb-keyval: ^6
|
||||||
|
jwt-decode: ^4
|
||||||
|
nprogress: ^0.2
|
||||||
|
qrcode: ^1.5
|
||||||
|
sortablejs: ^1
|
||||||
|
universal-cookie: ^7 || ^8
|
||||||
|
vue: ^3.5.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
async-validator:
|
||||||
|
optional: true
|
||||||
|
axios:
|
||||||
|
optional: true
|
||||||
|
change-case:
|
||||||
|
optional: true
|
||||||
|
drauu:
|
||||||
|
optional: true
|
||||||
|
focus-trap:
|
||||||
|
optional: true
|
||||||
|
fuse.js:
|
||||||
|
optional: true
|
||||||
|
idb-keyval:
|
||||||
|
optional: true
|
||||||
|
jwt-decode:
|
||||||
|
optional: true
|
||||||
|
nprogress:
|
||||||
|
optional: true
|
||||||
|
qrcode:
|
||||||
|
optional: true
|
||||||
|
sortablejs:
|
||||||
|
optional: true
|
||||||
|
universal-cookie:
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@vueuse/metadata@12.8.2':
|
'@vueuse/metadata@12.8.2':
|
||||||
resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==}
|
resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==}
|
||||||
|
|
||||||
@@ -2203,6 +2266,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
|
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
camelcase@5.3.1:
|
||||||
|
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
caniuse-lite@1.0.30001761:
|
caniuse-lite@1.0.30001761:
|
||||||
resolution: {integrity: sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==}
|
resolution: {integrity: sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==}
|
||||||
|
|
||||||
@@ -2256,6 +2323,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==}
|
resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
|
cliui@6.0.0:
|
||||||
|
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
|
||||||
|
|
||||||
cliui@8.0.1:
|
cliui@8.0.1:
|
||||||
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -2417,6 +2487,10 @@ packages:
|
|||||||
supports-color:
|
supports-color:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
decamelize@1.2.0:
|
||||||
|
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
decimal.js@10.6.0:
|
decimal.js@10.6.0:
|
||||||
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
|
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
|
||||||
|
|
||||||
@@ -2503,6 +2577,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==}
|
resolution: {integrity: sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==}
|
||||||
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
|
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
|
||||||
|
|
||||||
|
dijkstrajs@1.0.3:
|
||||||
|
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
|
||||||
|
|
||||||
dom-accessibility-api@0.5.16:
|
dom-accessibility-api@0.5.16:
|
||||||
resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
|
resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
|
||||||
|
|
||||||
@@ -2916,6 +2993,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==}
|
resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
find-up@4.1.0:
|
||||||
|
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
find-up@5.0.0:
|
find-up@5.0.0:
|
||||||
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
|
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -3610,6 +3691,10 @@ packages:
|
|||||||
localforage@1.10.0:
|
localforage@1.10.0:
|
||||||
resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==}
|
resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==}
|
||||||
|
|
||||||
|
locate-path@5.0.0:
|
||||||
|
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
locate-path@6.0.0:
|
locate-path@6.0.0:
|
||||||
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -3975,14 +4060,26 @@ packages:
|
|||||||
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
|
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
p-limit@2.3.0:
|
||||||
|
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
p-limit@3.1.0:
|
p-limit@3.1.0:
|
||||||
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
|
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
p-locate@4.1.0:
|
||||||
|
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
p-locate@5.0.0:
|
p-locate@5.0.0:
|
||||||
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
|
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
p-try@2.2.0:
|
||||||
|
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
package-json-from-dist@1.0.1:
|
package-json-from-dist@1.0.1:
|
||||||
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
||||||
|
|
||||||
@@ -4083,6 +4180,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
|
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
|
pngjs@5.0.0:
|
||||||
|
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
|
||||||
|
engines: {node: '>=10.13.0'}
|
||||||
|
|
||||||
pnpm-workspace-yaml@1.4.3:
|
pnpm-workspace-yaml@1.4.3:
|
||||||
resolution: {integrity: sha512-Q8B3SWuuISy/Ciag4DFP7MCrJX07wfaekcqD2o/msdIj4x8Ql3bZ/NEKOXV7mTVh7m1YdiFWiMi9xH+0zuEGHw==}
|
resolution: {integrity: sha512-Q8B3SWuuISy/Ciag4DFP7MCrJX07wfaekcqD2o/msdIj4x8Ql3bZ/NEKOXV7mTVh7m1YdiFWiMi9xH+0zuEGHw==}
|
||||||
|
|
||||||
@@ -4161,6 +4262,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
qrcode@1.5.4:
|
||||||
|
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
|
||||||
|
engines: {node: '>=10.13.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
quansync@0.2.11:
|
quansync@0.2.11:
|
||||||
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
|
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
|
||||||
|
|
||||||
@@ -4248,6 +4354,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
require-main-filename@2.0.0:
|
||||||
|
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
|
||||||
|
|
||||||
reserved-identifiers@1.2.0:
|
reserved-identifiers@1.2.0:
|
||||||
resolution: {integrity: sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==}
|
resolution: {integrity: sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -4371,6 +4480,9 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
set-blocking@2.0.0:
|
||||||
|
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
||||||
|
|
||||||
set-function-length@1.2.2:
|
set-function-length@1.2.2:
|
||||||
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
|
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -5140,6 +5252,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==}
|
resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
which-module@2.0.1:
|
||||||
|
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
|
||||||
|
|
||||||
which-typed-array@1.1.19:
|
which-typed-array@1.1.19:
|
||||||
resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==}
|
resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -5166,6 +5281,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==}
|
resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==}
|
||||||
engines: {node: '>=0.8'}
|
engines: {node: '>=0.8'}
|
||||||
|
|
||||||
|
wrap-ansi@6.2.0:
|
||||||
|
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
wrap-ansi@7.0.0:
|
wrap-ansi@7.0.0:
|
||||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -5206,6 +5325,9 @@ packages:
|
|||||||
xmlchars@2.2.0:
|
xmlchars@2.2.0:
|
||||||
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
|
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
|
||||||
|
|
||||||
|
y18n@4.0.3:
|
||||||
|
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
|
||||||
|
|
||||||
y18n@5.0.8:
|
y18n@5.0.8:
|
||||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -5222,10 +5344,18 @@ packages:
|
|||||||
engines: {node: '>= 14.6'}
|
engines: {node: '>= 14.6'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
yargs-parser@18.1.3:
|
||||||
|
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
yargs-parser@21.1.1:
|
yargs-parser@21.1.1:
|
||||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
yargs@15.4.1:
|
||||||
|
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
yargs@17.7.2:
|
yargs@17.7.2:
|
||||||
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
|
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -6210,6 +6340,8 @@ snapshots:
|
|||||||
'@eslint/core': 0.17.0
|
'@eslint/core': 0.17.0
|
||||||
levn: 0.4.1
|
levn: 0.4.1
|
||||||
|
|
||||||
|
'@fingerprintjs/fingerprintjs@5.0.1': {}
|
||||||
|
|
||||||
'@floating-ui/core@1.7.3':
|
'@floating-ui/core@1.7.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@floating-ui/utils': 0.2.10
|
'@floating-ui/utils': 0.2.10
|
||||||
@@ -6230,6 +6362,11 @@ snapshots:
|
|||||||
- '@vue/composition-api'
|
- '@vue/composition-api'
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
|
'@headlessui/vue@1.7.23(vue@3.5.26(typescript@5.9.3))':
|
||||||
|
dependencies:
|
||||||
|
'@tanstack/vue-virtual': 3.13.13(vue@3.5.26(typescript@5.9.3))
|
||||||
|
vue: 3.5.26(typescript@5.9.3)
|
||||||
|
|
||||||
'@humanfs/core@0.19.1': {}
|
'@humanfs/core@0.19.1': {}
|
||||||
|
|
||||||
'@humanfs/node@0.16.7':
|
'@humanfs/node@0.16.7':
|
||||||
@@ -7070,6 +7207,16 @@ snapshots:
|
|||||||
'@vueuse/shared': 14.1.0(vue@3.5.26(typescript@5.9.3))
|
'@vueuse/shared': 14.1.0(vue@3.5.26(typescript@5.9.3))
|
||||||
vue: 3.5.26(typescript@5.9.3)
|
vue: 3.5.26(typescript@5.9.3)
|
||||||
|
|
||||||
|
'@vueuse/integrations@14.1.0(axios@1.13.2)(change-case@5.4.4)(qrcode@1.5.4)(vue@3.5.26(typescript@5.9.3))':
|
||||||
|
dependencies:
|
||||||
|
'@vueuse/core': 14.1.0(vue@3.5.26(typescript@5.9.3))
|
||||||
|
'@vueuse/shared': 14.1.0(vue@3.5.26(typescript@5.9.3))
|
||||||
|
vue: 3.5.26(typescript@5.9.3)
|
||||||
|
optionalDependencies:
|
||||||
|
axios: 1.13.2
|
||||||
|
change-case: 5.4.4
|
||||||
|
qrcode: 1.5.4
|
||||||
|
|
||||||
'@vueuse/metadata@12.8.2': {}
|
'@vueuse/metadata@12.8.2': {}
|
||||||
|
|
||||||
'@vueuse/metadata@14.1.0': {}
|
'@vueuse/metadata@14.1.0': {}
|
||||||
@@ -7321,6 +7468,8 @@ snapshots:
|
|||||||
|
|
||||||
callsites@3.1.0: {}
|
callsites@3.1.0: {}
|
||||||
|
|
||||||
|
camelcase@5.3.1: {}
|
||||||
|
|
||||||
caniuse-lite@1.0.30001761: {}
|
caniuse-lite@1.0.30001761: {}
|
||||||
|
|
||||||
canvas-confetti@1.9.4: {}
|
canvas-confetti@1.9.4: {}
|
||||||
@@ -7374,6 +7523,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
escape-string-regexp: 1.0.5
|
escape-string-regexp: 1.0.5
|
||||||
|
|
||||||
|
cliui@6.0.0:
|
||||||
|
dependencies:
|
||||||
|
string-width: 4.2.3
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
wrap-ansi: 6.2.0
|
||||||
|
|
||||||
cliui@8.0.1:
|
cliui@8.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
string-width: 4.2.3
|
string-width: 4.2.3
|
||||||
@@ -7518,6 +7673,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
|
decamelize@1.2.0: {}
|
||||||
|
|
||||||
decimal.js@10.6.0: {}
|
decimal.js@10.6.0: {}
|
||||||
|
|
||||||
decode-named-character-reference@1.2.0:
|
decode-named-character-reference@1.2.0:
|
||||||
@@ -7606,6 +7763,8 @@ snapshots:
|
|||||||
|
|
||||||
diff-sequences@27.5.1: {}
|
diff-sequences@27.5.1: {}
|
||||||
|
|
||||||
|
dijkstrajs@1.0.3: {}
|
||||||
|
|
||||||
dom-accessibility-api@0.5.16: {}
|
dom-accessibility-api@0.5.16: {}
|
||||||
|
|
||||||
dom-serializer@0.2.2:
|
dom-serializer@0.2.2:
|
||||||
@@ -8188,6 +8347,11 @@ snapshots:
|
|||||||
|
|
||||||
find-up-simple@1.0.1: {}
|
find-up-simple@1.0.1: {}
|
||||||
|
|
||||||
|
find-up@4.1.0:
|
||||||
|
dependencies:
|
||||||
|
locate-path: 5.0.0
|
||||||
|
path-exists: 4.0.0
|
||||||
|
|
||||||
find-up@5.0.0:
|
find-up@5.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
locate-path: 6.0.0
|
locate-path: 6.0.0
|
||||||
@@ -8837,6 +9001,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
lie: 3.1.1
|
lie: 3.1.1
|
||||||
|
|
||||||
|
locate-path@5.0.0:
|
||||||
|
dependencies:
|
||||||
|
p-locate: 4.1.0
|
||||||
|
|
||||||
locate-path@6.0.0:
|
locate-path@6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
p-locate: 5.0.0
|
p-locate: 5.0.0
|
||||||
@@ -9400,14 +9568,24 @@ snapshots:
|
|||||||
object-keys: 1.1.1
|
object-keys: 1.1.1
|
||||||
safe-push-apply: 1.0.0
|
safe-push-apply: 1.0.0
|
||||||
|
|
||||||
|
p-limit@2.3.0:
|
||||||
|
dependencies:
|
||||||
|
p-try: 2.2.0
|
||||||
|
|
||||||
p-limit@3.1.0:
|
p-limit@3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
yocto-queue: 0.1.0
|
yocto-queue: 0.1.0
|
||||||
|
|
||||||
|
p-locate@4.1.0:
|
||||||
|
dependencies:
|
||||||
|
p-limit: 2.3.0
|
||||||
|
|
||||||
p-locate@5.0.0:
|
p-locate@5.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
p-limit: 3.1.0
|
p-limit: 3.1.0
|
||||||
|
|
||||||
|
p-try@2.2.0: {}
|
||||||
|
|
||||||
package-json-from-dist@1.0.1: {}
|
package-json-from-dist@1.0.1: {}
|
||||||
|
|
||||||
package-manager-detector@1.6.0: {}
|
package-manager-detector@1.6.0: {}
|
||||||
@@ -9489,6 +9667,8 @@ snapshots:
|
|||||||
|
|
||||||
pluralize@8.0.0: {}
|
pluralize@8.0.0: {}
|
||||||
|
|
||||||
|
pngjs@5.0.0: {}
|
||||||
|
|
||||||
pnpm-workspace-yaml@1.4.3:
|
pnpm-workspace-yaml@1.4.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
yaml: 2.8.2
|
yaml: 2.8.2
|
||||||
@@ -9567,6 +9747,12 @@ snapshots:
|
|||||||
|
|
||||||
punycode@2.3.1: {}
|
punycode@2.3.1: {}
|
||||||
|
|
||||||
|
qrcode@1.5.4:
|
||||||
|
dependencies:
|
||||||
|
dijkstrajs: 1.0.3
|
||||||
|
pngjs: 5.0.0
|
||||||
|
yargs: 15.4.1
|
||||||
|
|
||||||
quansync@0.2.11: {}
|
quansync@0.2.11: {}
|
||||||
|
|
||||||
query-string@4.3.4:
|
query-string@4.3.4:
|
||||||
@@ -9670,6 +9856,8 @@ snapshots:
|
|||||||
|
|
||||||
require-from-string@2.0.2: {}
|
require-from-string@2.0.2: {}
|
||||||
|
|
||||||
|
require-main-filename@2.0.0: {}
|
||||||
|
|
||||||
reserved-identifiers@1.2.0: {}
|
reserved-identifiers@1.2.0: {}
|
||||||
|
|
||||||
resolve-from@4.0.0: {}
|
resolve-from@4.0.0: {}
|
||||||
@@ -9790,6 +9978,8 @@ snapshots:
|
|||||||
|
|
||||||
semver@7.7.3: {}
|
semver@7.7.3: {}
|
||||||
|
|
||||||
|
set-blocking@2.0.0: {}
|
||||||
|
|
||||||
set-function-length@1.2.2:
|
set-function-length@1.2.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
define-data-property: 1.1.4
|
define-data-property: 1.1.4
|
||||||
@@ -10627,6 +10817,8 @@ snapshots:
|
|||||||
is-weakmap: 2.0.2
|
is-weakmap: 2.0.2
|
||||||
is-weakset: 2.0.4
|
is-weakset: 2.0.4
|
||||||
|
|
||||||
|
which-module@2.0.1: {}
|
||||||
|
|
||||||
which-typed-array@1.1.19:
|
which-typed-array@1.1.19:
|
||||||
dependencies:
|
dependencies:
|
||||||
available-typed-arrays: 1.0.7
|
available-typed-arrays: 1.0.7
|
||||||
@@ -10652,6 +10844,12 @@ snapshots:
|
|||||||
|
|
||||||
word@0.3.0: {}
|
word@0.3.0: {}
|
||||||
|
|
||||||
|
wrap-ansi@6.2.0:
|
||||||
|
dependencies:
|
||||||
|
ansi-styles: 4.3.0
|
||||||
|
string-width: 4.2.3
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
|
||||||
wrap-ansi@7.0.0:
|
wrap-ansi@7.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
ansi-styles: 4.3.0
|
ansi-styles: 4.3.0
|
||||||
@@ -10686,6 +10884,8 @@ snapshots:
|
|||||||
|
|
||||||
xmlchars@2.2.0: {}
|
xmlchars@2.2.0: {}
|
||||||
|
|
||||||
|
y18n@4.0.3: {}
|
||||||
|
|
||||||
y18n@5.0.8: {}
|
y18n@5.0.8: {}
|
||||||
|
|
||||||
yallist@3.1.1: {}
|
yallist@3.1.1: {}
|
||||||
@@ -10697,8 +10897,27 @@ snapshots:
|
|||||||
|
|
||||||
yaml@2.8.2: {}
|
yaml@2.8.2: {}
|
||||||
|
|
||||||
|
yargs-parser@18.1.3:
|
||||||
|
dependencies:
|
||||||
|
camelcase: 5.3.1
|
||||||
|
decamelize: 1.2.0
|
||||||
|
|
||||||
yargs-parser@21.1.1: {}
|
yargs-parser@21.1.1: {}
|
||||||
|
|
||||||
|
yargs@15.4.1:
|
||||||
|
dependencies:
|
||||||
|
cliui: 6.0.0
|
||||||
|
decamelize: 1.2.0
|
||||||
|
find-up: 4.1.0
|
||||||
|
get-caller-file: 2.0.5
|
||||||
|
require-directory: 2.1.1
|
||||||
|
require-main-filename: 2.0.0
|
||||||
|
set-blocking: 2.0.0
|
||||||
|
string-width: 4.2.3
|
||||||
|
which-module: 2.0.1
|
||||||
|
y18n: 4.0.3
|
||||||
|
yargs-parser: 18.1.3
|
||||||
|
|
||||||
yargs@17.7.2:
|
yargs@17.7.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
cliui: 8.0.1
|
cliui: 8.0.1
|
||||||
|
|||||||
153
public/sw.js
Normal file
153
public/sw.js
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
/* eslint-disable no-restricted-globals */
|
||||||
|
// Service Worker 最小示例
|
||||||
|
|
||||||
|
// 安装事件
|
||||||
|
self.addEventListener('install', (event) => {
|
||||||
|
console.log('Service Worker: Installed', event)
|
||||||
|
// 跳过等待,立即激活
|
||||||
|
self.skipWaiting()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 激活事件
|
||||||
|
self.addEventListener('activate', (event) => {
|
||||||
|
console.log('Service Worker: Activated')
|
||||||
|
// 立即控制所有页面
|
||||||
|
event.waitUntil(self.clients.claim())
|
||||||
|
})
|
||||||
|
|
||||||
|
self.currentClient = null
|
||||||
|
// 监听页面消息
|
||||||
|
self.addEventListener('message', async (event) => {
|
||||||
|
// 处理来自页面的消息
|
||||||
|
self.currentClient = event.source
|
||||||
|
if (event.data && event.data.type) {
|
||||||
|
console.log('处理来自页面的消息:', event.data)
|
||||||
|
switch (event.data.type) {
|
||||||
|
case 'CONNECT_WS':
|
||||||
|
console.log('连接的URL:', event.data.payload.url, self.webSocketConnection)
|
||||||
|
if (!self.webSocketConnection || self.webSocketConnection.readyState !== WebSocket.OPEN) {
|
||||||
|
self.webSocketConnection = new WebSocket(event.data.payload.url)
|
||||||
|
}
|
||||||
|
console.log('新连接:', self.webSocketConnection)
|
||||||
|
self.webSocketConnection.onopen = () => {
|
||||||
|
console.log('连接成功了,可以发消息')
|
||||||
|
if (self.currentClient) {
|
||||||
|
self.currentClient.postMessage({
|
||||||
|
type: 'WS_OPEN',
|
||||||
|
success: true,
|
||||||
|
payload: {
|
||||||
|
message: '连接成功',
|
||||||
|
data: null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 接收到消息推送给客户端
|
||||||
|
self.webSocketConnection.onmessage = (message) => {
|
||||||
|
const formatMsg = JSON.parse(message.data)
|
||||||
|
|
||||||
|
console.log('服务器的消息', formatMsg)
|
||||||
|
if (self.currentClient) {
|
||||||
|
self.currentClient.postMessage({
|
||||||
|
type: 'WS_MESSAGE',
|
||||||
|
success: true,
|
||||||
|
payload: formatMsg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// else {
|
||||||
|
// event.source.postMessage({
|
||||||
|
// type: 'WS_MESSAGE',
|
||||||
|
// success: true,
|
||||||
|
// payload: {
|
||||||
|
// data: message.data,
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
}
|
||||||
|
// 连接错误
|
||||||
|
self.webSocketConnection.onerror = (error) => {
|
||||||
|
console.error('WebSocket error:', error)
|
||||||
|
if (self.currentClient) {
|
||||||
|
self.currentClient.postMessage({
|
||||||
|
type: 'WS_ERROR',
|
||||||
|
success: true,
|
||||||
|
payload: {
|
||||||
|
message: '连接错误',
|
||||||
|
data: error,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 连接关闭
|
||||||
|
self.webSocketConnection.onclose = () => {
|
||||||
|
console.log('WebSocket connection closed')
|
||||||
|
if (self.currentClient) {
|
||||||
|
self.currentClient.postMessage({
|
||||||
|
type: 'WS_CLOSE',
|
||||||
|
success: true,
|
||||||
|
payload: {
|
||||||
|
message: '已断开连接',
|
||||||
|
data: null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'SEND_WS_MESSAGE':
|
||||||
|
const user_msg = event.data.payload.message
|
||||||
|
console.log('发送信号改哦西:', user_msg, self.webSocketConnection)
|
||||||
|
self.webSocketConnection.send(user_msg)
|
||||||
|
break
|
||||||
|
case 'DISCONNECT_WS':
|
||||||
|
console.log('Disconnecting from WebSocket')
|
||||||
|
self.webSocketConnection.close()
|
||||||
|
event.source.postMessage({
|
||||||
|
type: 'WS_CLOSE',
|
||||||
|
success: true,
|
||||||
|
payload: {
|
||||||
|
message: '已断开连接',
|
||||||
|
data: null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'PING':
|
||||||
|
console.log('Ping from page')
|
||||||
|
self.webSocketConnection.send('PING')
|
||||||
|
event.source.postMessage({
|
||||||
|
type: 'PONG',
|
||||||
|
success: true,
|
||||||
|
payload: {
|
||||||
|
message: 'Service Worker is alive',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'GET_WS_STATUS':
|
||||||
|
// 返回WebSocket连接状态(如果有的话)
|
||||||
|
event.source.postMessage({
|
||||||
|
type: 'WS_STATUS',
|
||||||
|
success: true,
|
||||||
|
payload: {
|
||||||
|
status: self.webSocketConnection ? self.webSocketConnection.readyState : null,
|
||||||
|
connected: self.webSocketConnection && self.webSocketConnection.readyState === WebSocket.OPEN,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
console.log('Unknown message type:', event.data.type)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听通知点击事件
|
||||||
|
self.addEventListener('notificationclick', (event) => {
|
||||||
|
console.log('Notification clicked:', event.notification.title)
|
||||||
|
|
||||||
|
event.notification.close()
|
||||||
|
|
||||||
|
// 打开或聚焦到页面
|
||||||
|
event.waitUntil(
|
||||||
|
clients.openWindow(event.notification.data.url || '/'),
|
||||||
|
)
|
||||||
|
})
|
||||||
12
src/api/msg/index.ts
Normal file
12
src/api/msg/index.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import request from '../request'
|
||||||
|
|
||||||
|
export function api_sendMsg(userSignature: string, data: any) {
|
||||||
|
return request<{ status: string }>({
|
||||||
|
url: '/user-msg',
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
userSignature,
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
|
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import openModal from '@/components/ErrorModal'
|
||||||
|
|
||||||
class Request {
|
class Request {
|
||||||
private instance: AxiosInstance
|
private instance: AxiosInstance
|
||||||
@@ -15,14 +16,10 @@ class Request {
|
|||||||
this.instance.interceptors.request.use(
|
this.instance.interceptors.request.use(
|
||||||
(config: InternalAxiosRequestConfig) => {
|
(config: InternalAxiosRequestConfig) => {
|
||||||
// 在发送请求之前做些什么
|
// 在发送请求之前做些什么
|
||||||
console.log('请求拦截器被触发')
|
|
||||||
|
|
||||||
return config
|
return config
|
||||||
},
|
},
|
||||||
(error: any) => {
|
(error: any) => {
|
||||||
// 对请求错误做些什么
|
// 对请求错误做些什么
|
||||||
console.error('请求拦截器发生错误:', error)
|
|
||||||
|
|
||||||
return Promise.reject(error)
|
return Promise.reject(error)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -31,15 +28,16 @@ class Request {
|
|||||||
this.instance.interceptors.response.use(
|
this.instance.interceptors.response.use(
|
||||||
(response: AxiosResponse) => {
|
(response: AxiosResponse) => {
|
||||||
// 对响应数据做些什么
|
// 对响应数据做些什么
|
||||||
console.log('响应拦截器被触发')
|
return response
|
||||||
const responseData = response.data
|
|
||||||
|
|
||||||
return responseData
|
|
||||||
},
|
},
|
||||||
(error: any) => {
|
(error: any) => {
|
||||||
// 对响应错误做些什么
|
// 对响应错误做些什么
|
||||||
console.error('响应拦截器发生错误:', error)
|
if (error.response && error.response.data) {
|
||||||
|
const { code, msg } = error.response.data
|
||||||
|
openModal({ title: code, desc: msg })
|
||||||
|
return Promise.reject(error.response.data)
|
||||||
|
}
|
||||||
|
openModal({ title: '请求错误', desc: error.message })
|
||||||
return Promise.reject(error)
|
return Promise.reject(error)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
BIN
src/assets/audio/end.mp3
Normal file
BIN
src/assets/audio/end.mp3
Normal file
Binary file not shown.
BIN
src/assets/audio/worldcup.mp3
Normal file
BIN
src/assets/audio/worldcup.mp3
Normal file
Binary file not shown.
@@ -9,11 +9,15 @@ interface Props {
|
|||||||
submitText?: string
|
submitText?: string
|
||||||
submitFunc?: () => void
|
submitFunc?: () => void
|
||||||
cancelFunc?: () => void
|
cancelFunc?: () => void
|
||||||
|
footer?: null | 'center' | 'left' | 'right'
|
||||||
|
dialogClass?: string // 添加动态class属性
|
||||||
}
|
}
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
cancelText: i18n.global.t('button.cancel'),
|
cancelText: i18n.global.t('button.cancel'),
|
||||||
submitText: i18n.global.t('button.confirm'),
|
submitText: i18n.global.t('button.confirm'),
|
||||||
cancelFunc: () => {},
|
cancelFunc: () => {},
|
||||||
|
footer: 'right',
|
||||||
|
dialogClass: '',
|
||||||
})
|
})
|
||||||
const visible = defineModel('visible', {
|
const visible = defineModel('visible', {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -43,7 +47,7 @@ const { title, desc, cancelText, submitText, submitFunc, cancelFunc = defaultCan
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<dialog id="my_modal" ref="dialogRef" class="border-none modal">
|
<dialog id="my_modal" ref="dialogRef" class="border-none modal">
|
||||||
<div class="modal-box">
|
<div class="modal-box" :class="[dialogClass]">
|
||||||
<h3 v-if="title" class="text-lg font-bold">
|
<h3 v-if="title" class="text-lg font-bold">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</h3>
|
</h3>
|
||||||
@@ -53,7 +57,7 @@ const { title, desc, cancelText, submitText, submitFunc, cancelFunc = defaultCan
|
|||||||
<div>
|
<div>
|
||||||
<slot name="content" />
|
<slot name="content" />
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-action">
|
<div class="modal-action" :class="{ 'flex justify-center': footer === 'center' }">
|
||||||
<form method="dialog" class="flex gap-3">
|
<form method="dialog" class="flex gap-3">
|
||||||
<!-- if there is a button in form, it will close the modal -->
|
<!-- if there is a button in form, it will close the modal -->
|
||||||
<button class="btn" @click="cancelFunc">
|
<button class="btn" @click="cancelFunc">
|
||||||
|
|||||||
57
src/components/ErrorModal/index.ts
Normal file
57
src/components/ErrorModal/index.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { createVNode, render } from 'vue'
|
||||||
|
import ErrorModalVue from './index.vue'
|
||||||
|
|
||||||
|
// 定义弹窗调用函数
|
||||||
|
function openModal(options = {}) {
|
||||||
|
// 默认配置
|
||||||
|
const defaultOptions = {
|
||||||
|
title: '提示',
|
||||||
|
desc: '',
|
||||||
|
// 确认按钮回调
|
||||||
|
onConfirm: () => {},
|
||||||
|
// 关闭按钮回调
|
||||||
|
onClose: () => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并配置
|
||||||
|
const finalOptions = { ...defaultOptions, ...options }
|
||||||
|
|
||||||
|
// 创建容器
|
||||||
|
const container = document.createElement('div')
|
||||||
|
|
||||||
|
// 创建虚拟节点
|
||||||
|
const vnode = createVNode(ErrorModalVue, {
|
||||||
|
'title': finalOptions.title,
|
||||||
|
'desc': finalOptions.desc,
|
||||||
|
'modelValue': true, // 默认打开
|
||||||
|
'onUpdate:modelValue': (val: any) => {
|
||||||
|
if (!val) {
|
||||||
|
// 关闭时销毁组件
|
||||||
|
render(null, container)
|
||||||
|
document.body.removeChild(container)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'onConfirm': () => {
|
||||||
|
finalOptions.onConfirm()
|
||||||
|
},
|
||||||
|
'onClose': () => {
|
||||||
|
finalOptions.onClose()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 渲染组件到容器
|
||||||
|
render(vnode, container)
|
||||||
|
|
||||||
|
// 将容器添加到body
|
||||||
|
document.body.appendChild(container)
|
||||||
|
|
||||||
|
// 返回关闭方法(可选)
|
||||||
|
return {
|
||||||
|
close: () => {
|
||||||
|
render(null, container)
|
||||||
|
document.body.removeChild(container)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default openModal
|
||||||
127
src/components/ErrorModal/index.vue
Normal file
127
src/components/ErrorModal/index.vue
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
<script setup>
|
||||||
|
import { Dialog, DialogDescription, DialogPanel, DialogTitle } from '@headlessui/vue'
|
||||||
|
import { CircleAlert } from 'lucide-vue-next'
|
||||||
|
import { defineEmits, defineProps, ref, watch } from 'vue'
|
||||||
|
// 定义组件属性
|
||||||
|
const props = defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '提示',
|
||||||
|
},
|
||||||
|
desc: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
// 控制弹窗显隐
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 定义事件
|
||||||
|
const emit = defineEmits(['update:modelValue', 'close'])
|
||||||
|
|
||||||
|
// 内部显隐状态
|
||||||
|
const visible = ref(props.modelValue)
|
||||||
|
|
||||||
|
// 同步外部 modelValue 变化
|
||||||
|
watch(() => props.modelValue, (val) => {
|
||||||
|
visible.value = val
|
||||||
|
})
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
function handleClose() {
|
||||||
|
visible.value = false
|
||||||
|
emit('update:modelValue', false)
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<teleport to="body">
|
||||||
|
<Dialog :open="visible" class="relative z-50" @close="handleClose">
|
||||||
|
<!-- The backdrop, rendered as a fixed sibling to the panel container -->
|
||||||
|
<div class="fixed inset-0 bg-black/30" aria-hidden="true" />
|
||||||
|
<!-- Full-screen container to center the panel -->
|
||||||
|
<div class="fixed inset-0 flex w-screen items-center justify-center p-4">
|
||||||
|
<DialogPanel class="max-w-sm rounded bg-base-100 w-9/10 p-6 shadow-md">
|
||||||
|
<DialogTitle class="font-bold text-lg">
|
||||||
|
<p class="w-full flex items-center gap-2">
|
||||||
|
<CircleAlert class="text-red-500" />
|
||||||
|
<span>
|
||||||
|
{{ title || '提示' }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription class="py-4">
|
||||||
|
{{ desc }}
|
||||||
|
</DialogDescription>
|
||||||
|
<div class="mr-4 mt-4 flex justify-end">
|
||||||
|
<button class="btn" @click="handleClose">
|
||||||
|
确定
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</DialogPanel>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.modal-mask {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-container {
|
||||||
|
width: 400px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header button {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer button {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer button:first-child {
|
||||||
|
background: #409eff;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #409eff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
57
src/components/HoverTip/index.vue
Normal file
57
src/components/HoverTip/index.vue
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<script setup lang='ts'>
|
||||||
|
import { CircleAlert, CircleCheck, Info, TriangleAlert } from 'lucide-vue-next'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
tip: string
|
||||||
|
type?: 'success' | 'warning' | 'error' | 'info'
|
||||||
|
direction?: 'top' | 'bottom' | 'left' | 'right'
|
||||||
|
size?: '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
type: 'info',
|
||||||
|
direction: 'top',
|
||||||
|
size: 'sm',
|
||||||
|
})
|
||||||
|
|
||||||
|
const sizeObj = {
|
||||||
|
'2xs': 'w-2 h-2',
|
||||||
|
'xs': 'w-3 h-3',
|
||||||
|
'sm': 'w-4 h-4',
|
||||||
|
'md': 'w-5 h-5',
|
||||||
|
'lg': 'w-6 h-6',
|
||||||
|
'xl': 'w-7 h-7',
|
||||||
|
'2xl': 'w-8 h-8',
|
||||||
|
}
|
||||||
|
|
||||||
|
const tipClassNames = computed(() => {
|
||||||
|
return [
|
||||||
|
`tooltip-${props.direction}`,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const iconClassNames = computed(() => {
|
||||||
|
return [
|
||||||
|
`w-${sizeObj[props.size]}`,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="tooltip" :class="tipClassNames" :data-tip="tip">
|
||||||
|
<slot name="content" />
|
||||||
|
<slot>
|
||||||
|
<div :class="iconClassNames" class="hover:text-primary cursor-pointer">
|
||||||
|
<CircleAlert v-if="type === 'warning'" class="w-full h-full text-warning" />
|
||||||
|
<Info v-else-if="type === 'info'" class="w-full h-full" />
|
||||||
|
<CircleCheck v-else-if="type === 'success'" class="w-full h-full text-success" />
|
||||||
|
<TriangleAlert v-else-if="type === 'error'" class="w-full h-full text-error" />
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang='ts'>
|
<script setup lang='ts'>
|
||||||
import type { Separate } from '@/types/storeType'
|
import type { Separate } from '@/types/storeType'
|
||||||
import { onMounted, ref, toRefs, watch } from 'vue'
|
import { useVirtualList } from '@vueuse/core'
|
||||||
|
import { computed, onMounted, ref, toRefs, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -18,6 +19,27 @@ const { t } = useI18n()
|
|||||||
const separatedNumberRef = ref()
|
const separatedNumberRef = ref()
|
||||||
const { separatedNumber, totalNumber } = toRefs(props)
|
const { separatedNumber, totalNumber } = toRefs(props)
|
||||||
const scaleList = ref<number[]>([])
|
const scaleList = ref<number[]>([])
|
||||||
|
|
||||||
|
const ITEMS_PER_ROW = 10
|
||||||
|
const ROW_HEIGHT = 52
|
||||||
|
|
||||||
|
// Group numbers into rows for virtual list
|
||||||
|
const rows = computed(() => {
|
||||||
|
const result: number[][] = []
|
||||||
|
for (let i = 0; i < props.totalNumber; i += ITEMS_PER_ROW) {
|
||||||
|
const row: number[] = []
|
||||||
|
for (let j = i; j < Math.min(i + ITEMS_PER_ROW, props.totalNumber); j++) {
|
||||||
|
row.push(j + 1)
|
||||||
|
}
|
||||||
|
result.push(row)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
const { list, containerProps, wrapperProps } = useVirtualList(rows, {
|
||||||
|
itemHeight: ROW_HEIGHT,
|
||||||
|
})
|
||||||
|
|
||||||
function editScale(item: number) {
|
function editScale(item: number) {
|
||||||
if (item === totalNumber.value) {
|
if (item === totalNumber.value) {
|
||||||
return
|
return
|
||||||
@@ -73,30 +95,40 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<dialog id="my_modal_1" ref="separatedNumberRef" class="z-50 overflow-hidden border-none modal">
|
<dialog id="my_modal_1" ref="separatedNumberRef" class="z-50 overflow-hidden border-none modal">
|
||||||
<div class="overflow-hidden modal-box">
|
<div class="overflow-hidden modal-box max-h-[70vh] flex flex-col">
|
||||||
<h3 class="pb-6 text-lg font-bold">
|
<h3 class="pb-4 text-lg font-bold shrink-0">
|
||||||
{{ t('dialog.titleTip') }}
|
{{ t('dialog.titleTip') }}
|
||||||
</h3>
|
</h3>
|
||||||
<p class="pb-8">
|
<p class="pb-4 shrink-0">
|
||||||
{{ t('dialog.dialogSingleDrawLimit') }}
|
{{ t('dialog.dialogSingleDrawLimit') }}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex justify-between px-3 text-center separated-number">
|
<!-- Virtual scrolling container -->
|
||||||
<div
|
<div
|
||||||
v-for="item in props.totalNumber" :key="item"
|
v-bind="containerProps"
|
||||||
class="relative flex flex-col items-center cursor-pointer"
|
class="flex-1 min-h-0 px-3 overflow-y-auto"
|
||||||
|
style="max-height: calc(70vh - 180px);"
|
||||||
|
>
|
||||||
|
<div v-bind="wrapperProps">
|
||||||
|
<div
|
||||||
|
v-for="{ data: row, index } in list"
|
||||||
|
:key="index"
|
||||||
|
class="grid grid-cols-10 gap-1 text-center pb-2"
|
||||||
|
:style="{ height: `${ROW_HEIGHT}px` }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="absolute mb-12 text-center tooltip -top-5 hover:text-lg" :data-tip="t('tooltip.leftClick')"
|
v-for="item in row"
|
||||||
|
:key="item"
|
||||||
|
class="flex flex-col items-center justify-start cursor-pointer rounded hover:bg-base-200 transition-colors pt-1"
|
||||||
|
:data-tip="t('tooltip.leftClick')"
|
||||||
@click.left="editScale(item)"
|
@click.left="editScale(item)"
|
||||||
>
|
>
|
||||||
<span> {{ item }}</span>
|
<span>{{ item }}</span>
|
||||||
</div>
|
<span :class="scaleList.includes(item) ? 'text-red-500 font-extrabold' : ''" class="leading-none">|</span>
|
||||||
<div class="text-center" :class="scaleList.includes(item) ? 'text-red-500 font-extrabold' : ''">
|
|
||||||
|
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-action">
|
</div>
|
||||||
|
<div class="modal-action shrink-0">
|
||||||
<form method="dialog">
|
<form method="dialog">
|
||||||
<!-- if there is a button in form, it will close the modal -->
|
<!-- if there is a button in form, it will close the modal -->
|
||||||
<button class="btn" @click="clearData">
|
<button class="btn" @click="clearData">
|
||||||
|
|||||||
@@ -1,5 +1 @@
|
|||||||
export { default as Footer } from './Footer/index.vue'
|
export { default as HoverTip } from './HoverTip/index.vue'
|
||||||
/**
|
|
||||||
*title: 自动导出组件
|
|
||||||
*/
|
|
||||||
export { default as Header } from './Header/index.vue'
|
|
||||||
|
|||||||
1
src/constant/config.ts
Normal file
1
src/constant/config.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const SINGLE_TIME_MAX_PERSON_COUNT = 30
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
import type { IPersonConfig } from '@/types/storeType'
|
|
||||||
import { rgba } from '@/utils/color'
|
|
||||||
|
|
||||||
export function useElementStyle(element: any, person: IPersonConfig, index: number, patternList: number[], patternColor: string, cardColor: string, cardSize: { width: number, height: number }, textSize: number, mod: 'default' | 'lucky' | 'sphere' = 'default', type: 'add' | 'change' = 'add') {
|
|
||||||
if (patternList.includes(index + 1) && mod === 'default') {
|
|
||||||
element.style.backgroundColor = rgba(patternColor, Math.random() * 0.2 + 0.8)
|
|
||||||
}
|
|
||||||
else if (mod === 'sphere' || mod === 'default') {
|
|
||||||
element.style.backgroundColor = rgba(cardColor, Math.random() * 0.5 + 0.25)
|
|
||||||
}
|
|
||||||
else if (mod === 'lucky') {
|
|
||||||
element.style.backgroundColor = rgba(cardColor, 0.8)
|
|
||||||
}
|
|
||||||
element.style.border = `1px solid ${rgba(cardColor, 0.25)}`
|
|
||||||
element.style.boxShadow = `0 0 12px ${rgba(cardColor, 0.5)}`
|
|
||||||
element.style.width = `${cardSize.width}px`
|
|
||||||
element.style.height = `${cardSize.height}px`
|
|
||||||
if (mod === 'lucky') {
|
|
||||||
element.className = 'lucky-element-card'
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
element.className = 'element-card'
|
|
||||||
}
|
|
||||||
if (type === 'add') {
|
|
||||||
element.addEventListener('mouseenter', (ev: MouseEvent) => {
|
|
||||||
const target = ev.target as HTMLElement
|
|
||||||
target.style.border = `1px solid ${rgba(cardColor, 0.75)}`
|
|
||||||
target.style.boxShadow = `0 0 12px ${rgba(cardColor, 0.75)}`
|
|
||||||
})
|
|
||||||
element.addEventListener('mouseleave', (ev: MouseEvent) => {
|
|
||||||
const target = ev.target as HTMLElement
|
|
||||||
target.style.border = `1px solid ${rgba(cardColor, 0.25)}`
|
|
||||||
target.style.boxShadow = `0 0 12px ${rgba(cardColor, 0.5)}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
element.children[0].style.fontSize = `${textSize * 0.5}px`
|
|
||||||
if (person.uid) {
|
|
||||||
element.children[0].textContent = person.uid
|
|
||||||
}
|
|
||||||
|
|
||||||
element.children[1].style.fontSize = `${textSize}px`
|
|
||||||
element.children[1].style.lineHeight = `${textSize * 3}px`
|
|
||||||
element.children[1].style.textShadow = `0 0 12px ${rgba(cardColor, 0.95)}`
|
|
||||||
if (person.name) {
|
|
||||||
element.children[1].textContent = person.name
|
|
||||||
}
|
|
||||||
// element.children[2].style.fontSize = `${textSize * 0.5}px`
|
|
||||||
// if (person.department || person.identity) {
|
|
||||||
// element.children[2].innerHTML = `${person.department ? person.department : ''}<br/>${person.identity ? person.identity : ''}`
|
|
||||||
// }
|
|
||||||
|
|
||||||
element.children[2].style.fontSize = `${textSize * 0.5}px`
|
|
||||||
// 设置部门和身份的默认值
|
|
||||||
element.children[2].innerHTML = ''
|
|
||||||
if (person.department || person.identity) {
|
|
||||||
element.children[2].innerHTML = `${person.department ? person.department : ''}<br/>${person.identity ? person.identity : ''}`
|
|
||||||
}
|
|
||||||
element.children[3].src = person.avatar
|
|
||||||
return element
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 设置抽中卡片的位置
|
|
||||||
* 最少一个,最大十个
|
|
||||||
*/
|
|
||||||
// TODO:不超过5个时:单行排列;超过5个时,6:上3下3;7:上3下4;8:上3下5;9:上4下5;10:上5下5
|
|
||||||
export function useElementPosition(element: any, count: number, totalCount: number, cardSize: { width: number, height: number }, windowSize: { width: number, height: number }, cardIndex: number) {
|
|
||||||
let xTable = 0
|
|
||||||
let yTable = 0
|
|
||||||
const centerPosition = {
|
|
||||||
x: 0,
|
|
||||||
y: windowSize.height / 2 - cardSize.height * 0.9,
|
|
||||||
}
|
|
||||||
// 有一行为偶数的特殊数量
|
|
||||||
const specialPosition = [2, 4, 7, 9]
|
|
||||||
// 不包含特殊值的 和 分两行中第一行为奇数值的
|
|
||||||
if (!specialPosition.includes(totalCount) || (totalCount > 5 && cardIndex < 5)) {
|
|
||||||
const index = cardIndex % 5
|
|
||||||
if (index === 0) {
|
|
||||||
xTable = centerPosition.x
|
|
||||||
yTable = centerPosition.y - Math.floor(cardIndex / 5) * (cardSize.height + 60)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
xTable = index % 2 === 0 ? Math.ceil(index / 2) * (cardSize.width + 100) : -Math.ceil(index / 2) * (cardSize.width + 100)
|
|
||||||
yTable = centerPosition.y - Math.floor(cardIndex / 5) * (cardSize.height + 60)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const index = cardIndex % 5
|
|
||||||
if (index === 0) {
|
|
||||||
xTable = centerPosition.x + (cardSize.width + 100) / 2
|
|
||||||
yTable = centerPosition.y - Math.floor(cardIndex / 5) * (cardSize.height + 60)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
xTable = index % 2 === 0 ? Math.ceil(index / 2) * (cardSize.width + 100) + (cardSize.width + 100) / 2 : -(Math.ceil(index / 2) * (cardSize.width + 100)) + (cardSize.width + 100) / 2
|
|
||||||
yTable = centerPosition.y - Math.floor(cardIndex / 5) * (cardSize.height + 60)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { xTable, yTable }
|
|
||||||
}
|
|
||||||
323
src/hooks/useElement/index.ts
Normal file
323
src/hooks/useElement/index.ts
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
import type { IPersonConfig } from '@/types/storeType'
|
||||||
|
import { rgba } from '@/utils/color'
|
||||||
|
|
||||||
|
interface IUseElementStyle {
|
||||||
|
element: any
|
||||||
|
person: IPersonConfig
|
||||||
|
index: number
|
||||||
|
patternList: number[]
|
||||||
|
patternColor: string
|
||||||
|
cardColor: string
|
||||||
|
cardSize: { width: number, height: number }
|
||||||
|
scale: number
|
||||||
|
textSize: number
|
||||||
|
mod: 'default' | 'lucky' | 'sphere'
|
||||||
|
type?: 'add' | 'change'
|
||||||
|
|
||||||
|
}
|
||||||
|
export function useElementStyle(props: IUseElementStyle) {
|
||||||
|
const { element, person, index, patternList, patternColor, cardColor, cardSize, scale, textSize, mod, type } = props
|
||||||
|
if (patternList.includes(index + 1) && mod === 'default') {
|
||||||
|
element.style.backgroundColor = rgba(patternColor, Math.random() * 0.2 + 0.8)
|
||||||
|
}
|
||||||
|
else if (mod === 'sphere' || mod === 'default') {
|
||||||
|
element.style.backgroundColor = rgba(cardColor, Math.random() * 0.5 + 0.25)
|
||||||
|
}
|
||||||
|
else if (mod === 'lucky') {
|
||||||
|
element.style.backgroundColor = rgba(cardColor, 0.8)
|
||||||
|
}
|
||||||
|
element.style.border = `1px solid ${rgba(cardColor, 0.25)}`
|
||||||
|
element.style.boxShadow = `0 0 12px ${rgba(cardColor, 0.5)}`
|
||||||
|
element.style.width = `${cardSize.width * scale}px`
|
||||||
|
element.style.height = `${cardSize.height * scale}px`
|
||||||
|
if (mod === 'lucky') {
|
||||||
|
element.className = 'lucky-element-card'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
element.className = 'element-card'
|
||||||
|
}
|
||||||
|
if (type === 'add') {
|
||||||
|
element.addEventListener('mouseenter', (ev: MouseEvent) => {
|
||||||
|
const target = ev.target as HTMLElement
|
||||||
|
target.style.border = `1px solid ${rgba(cardColor, 0.75)}`
|
||||||
|
target.style.boxShadow = `0 0 12px ${rgba(cardColor, 0.75)}`
|
||||||
|
})
|
||||||
|
element.addEventListener('mouseleave', (ev: MouseEvent) => {
|
||||||
|
const target = ev.target as HTMLElement
|
||||||
|
target.style.border = `1px solid ${rgba(cardColor, 0.25)}`
|
||||||
|
target.style.boxShadow = `0 0 12px ${rgba(cardColor, 0.5)}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
element.children[0].style.fontSize = `${textSize * scale * 0.5}px`
|
||||||
|
if (person.uid) {
|
||||||
|
element.children[0].textContent = person.uid
|
||||||
|
}
|
||||||
|
|
||||||
|
element.children[1].style.fontSize = `${textSize * scale}px`
|
||||||
|
element.children[1].style.lineHeight = `${textSize * scale * 3}px`
|
||||||
|
element.children[1].style.textShadow = `0 0 12px ${rgba(cardColor, 0.95)}`
|
||||||
|
if (person.name) {
|
||||||
|
element.children[1].textContent = person.name
|
||||||
|
}
|
||||||
|
|
||||||
|
element.children[2].style.fontSize = `${textSize * scale * 0.5}px`
|
||||||
|
// 设置部门和身份的默认值
|
||||||
|
element.children[2].innerHTML = ''
|
||||||
|
if (person.department || person.identity) {
|
||||||
|
element.children[2].innerHTML = `${person.department ? person.department : ''}<br/>${person.identity ? person.identity : ''}`
|
||||||
|
}
|
||||||
|
element.children[3].src = person.avatar
|
||||||
|
return element
|
||||||
|
}
|
||||||
|
interface CardRule {
|
||||||
|
[key: number]: {
|
||||||
|
maxLine: number
|
||||||
|
scale: number
|
||||||
|
rule: number[]
|
||||||
|
length: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const cardRule: CardRule = {
|
||||||
|
1: {
|
||||||
|
maxLine: 5,
|
||||||
|
scale: 2,
|
||||||
|
rule: [1],
|
||||||
|
length: 1,
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
maxLine: 5,
|
||||||
|
scale: 2,
|
||||||
|
rule: [2],
|
||||||
|
length: 1,
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
maxLine: 5,
|
||||||
|
scale: 2,
|
||||||
|
rule: [3],
|
||||||
|
length: 1,
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
maxLine: 5,
|
||||||
|
scale: 2,
|
||||||
|
rule: [4],
|
||||||
|
length: 1,
|
||||||
|
},
|
||||||
|
5: {
|
||||||
|
maxLine: 5,
|
||||||
|
scale: 2,
|
||||||
|
rule: [5],
|
||||||
|
length: 1,
|
||||||
|
},
|
||||||
|
6: {
|
||||||
|
maxLine: 3,
|
||||||
|
scale: 2,
|
||||||
|
rule: [3, 3],
|
||||||
|
length: 2,
|
||||||
|
},
|
||||||
|
7: {
|
||||||
|
maxLine: 4,
|
||||||
|
scale: 2,
|
||||||
|
rule: [3, 4],
|
||||||
|
length: 2,
|
||||||
|
},
|
||||||
|
8: {
|
||||||
|
maxLine: 5,
|
||||||
|
scale: 2,
|
||||||
|
rule: [3, 5],
|
||||||
|
length: 2,
|
||||||
|
},
|
||||||
|
9: {
|
||||||
|
maxLine: 5,
|
||||||
|
scale: 2,
|
||||||
|
rule: [4, 5],
|
||||||
|
length: 2,
|
||||||
|
},
|
||||||
|
10: {
|
||||||
|
maxLine: 5,
|
||||||
|
scale: 2,
|
||||||
|
rule: [5, 5],
|
||||||
|
length: 2,
|
||||||
|
},
|
||||||
|
11: {
|
||||||
|
maxLine: 6,
|
||||||
|
scale: 1.8,
|
||||||
|
rule: [5, 6],
|
||||||
|
length: 2,
|
||||||
|
},
|
||||||
|
12: {
|
||||||
|
maxLine: 6,
|
||||||
|
scale: 1.8,
|
||||||
|
rule: [6, 6],
|
||||||
|
length: 2,
|
||||||
|
},
|
||||||
|
13: {
|
||||||
|
maxLine: 7,
|
||||||
|
scale: 1.6,
|
||||||
|
rule: [6, 7],
|
||||||
|
length: 2,
|
||||||
|
},
|
||||||
|
14: {
|
||||||
|
maxLine: 7,
|
||||||
|
scale: 1.6,
|
||||||
|
rule: [7, 7],
|
||||||
|
length: 2,
|
||||||
|
},
|
||||||
|
15: {
|
||||||
|
maxLine: 8,
|
||||||
|
scale: 1.5,
|
||||||
|
rule: [7, 8],
|
||||||
|
length: 2,
|
||||||
|
},
|
||||||
|
16: {
|
||||||
|
maxLine: 8,
|
||||||
|
scale: 1.5,
|
||||||
|
rule: [8, 8],
|
||||||
|
length: 2,
|
||||||
|
},
|
||||||
|
17: {
|
||||||
|
maxLine: 6,
|
||||||
|
scale: 1.8,
|
||||||
|
rule: [5, 6, 6],
|
||||||
|
length: 3,
|
||||||
|
},
|
||||||
|
18: {
|
||||||
|
maxLine: 6,
|
||||||
|
scale: 1.8,
|
||||||
|
rule: [6, 6, 6],
|
||||||
|
length: 3,
|
||||||
|
},
|
||||||
|
19: {
|
||||||
|
maxLine: 7,
|
||||||
|
scale: 1.6,
|
||||||
|
rule: [6, 6, 7],
|
||||||
|
length: 3,
|
||||||
|
},
|
||||||
|
20: {
|
||||||
|
maxLine: 5,
|
||||||
|
scale: 1.6,
|
||||||
|
rule: [6, 7, 7],
|
||||||
|
length: 3,
|
||||||
|
},
|
||||||
|
21: {
|
||||||
|
maxLine: 7,
|
||||||
|
scale: 1.6,
|
||||||
|
rule: [7, 7, 7],
|
||||||
|
length: 3,
|
||||||
|
},
|
||||||
|
22: {
|
||||||
|
maxLine: 8,
|
||||||
|
scale: 1.5,
|
||||||
|
rule: [7, 7, 8],
|
||||||
|
length: 3,
|
||||||
|
},
|
||||||
|
23: {
|
||||||
|
maxLine: 8,
|
||||||
|
scale: 1.5,
|
||||||
|
rule: [7, 8, 8],
|
||||||
|
length: 3,
|
||||||
|
},
|
||||||
|
24: {
|
||||||
|
maxLine: 8,
|
||||||
|
scale: 1.5,
|
||||||
|
rule: [8, 8, 8],
|
||||||
|
length: 3,
|
||||||
|
},
|
||||||
|
25: {
|
||||||
|
maxLine: 9,
|
||||||
|
scale: 1.3,
|
||||||
|
rule: [8, 8, 9],
|
||||||
|
length: 3,
|
||||||
|
},
|
||||||
|
26: {
|
||||||
|
maxLine: 9,
|
||||||
|
scale: 1.3,
|
||||||
|
rule: [8, 9, 9],
|
||||||
|
length: 3,
|
||||||
|
},
|
||||||
|
27: {
|
||||||
|
maxLine: 9,
|
||||||
|
scale: 1.3,
|
||||||
|
rule: [9, 9, 9],
|
||||||
|
length: 3,
|
||||||
|
},
|
||||||
|
28: {
|
||||||
|
maxLine: 10,
|
||||||
|
scale: 1.2,
|
||||||
|
rule: [9, 9, 10],
|
||||||
|
length: 3,
|
||||||
|
},
|
||||||
|
29: {
|
||||||
|
maxLine: 10,
|
||||||
|
scale: 1.2,
|
||||||
|
rule: [9, 10, 10],
|
||||||
|
length: 3,
|
||||||
|
},
|
||||||
|
30: {
|
||||||
|
maxLine: 10,
|
||||||
|
scale: 1.2,
|
||||||
|
rule: [10, 10, 10],
|
||||||
|
length: 3,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @description 设置抽中卡片的位置
|
||||||
|
*/
|
||||||
|
export function useElementPosition(
|
||||||
|
element: any,
|
||||||
|
count: number,
|
||||||
|
totalCount: number,
|
||||||
|
cardSize: { width: number, height: number },
|
||||||
|
windowSize: { width: number, height: number },
|
||||||
|
cardIndex: number,
|
||||||
|
): {
|
||||||
|
xTable: number
|
||||||
|
yTable: number
|
||||||
|
scale: number
|
||||||
|
} {
|
||||||
|
let xTable = 0
|
||||||
|
let yTable = 0
|
||||||
|
const centerPosition = {
|
||||||
|
x: 0,
|
||||||
|
y: windowSize.height / 2,
|
||||||
|
}
|
||||||
|
const { scale, rule, length } = cardRule[totalCount]
|
||||||
|
// 计算缩放后的卡片尺寸
|
||||||
|
const scaledCardWidth = cardSize.width * scale
|
||||||
|
const scaledCardHeight = cardSize.height * scale
|
||||||
|
// 计算当前卡片在第几行(从0开始)
|
||||||
|
let currentRow = 0
|
||||||
|
let cardIndexInRow = cardIndex // 当前卡片在其所在行中的索引
|
||||||
|
|
||||||
|
// 根据规则确定卡片在哪一行及行内索引
|
||||||
|
let cumulativeCount = 0
|
||||||
|
for (let i = 0; i < rule.length; i++) {
|
||||||
|
if (cardIndex < cumulativeCount + rule[i]) {
|
||||||
|
currentRow = i
|
||||||
|
cardIndexInRow = cardIndex - cumulativeCount
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cumulativeCount += rule[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算当前行的卡片数量
|
||||||
|
const cardsInCurrentRow = rule[currentRow]
|
||||||
|
|
||||||
|
// 计算每行的垂直中心位置
|
||||||
|
const verticalSpacing = scaledCardHeight * 1.1 // 垂直间距基于缩放后的高度
|
||||||
|
// 计算整体高度并调整居中
|
||||||
|
const totalHeight = (length - 1) * verticalSpacing + scaledCardHeight // 包含卡片本身的高度
|
||||||
|
const centerYOffset = -totalHeight / 2
|
||||||
|
|
||||||
|
// 修改此处逻辑,确保当length=2时,两行围绕中心点对称分布
|
||||||
|
centerPosition.y = windowSize.height / 2 - totalHeight / 2
|
||||||
|
|
||||||
|
yTable = centerPosition.y + currentRow * verticalSpacing + centerYOffset + scaledCardHeight / 2 // 添加卡片高度的一半作为修正
|
||||||
|
// 计算当前行的水平居中偏移
|
||||||
|
const horizontalSpacing = scaledCardWidth * 1.2 // 水平间距基于缩放后的宽度
|
||||||
|
const rowWidth = (cardsInCurrentRow - 1) * horizontalSpacing
|
||||||
|
const offsetX = -rowWidth / 2 // 行内水平居中
|
||||||
|
|
||||||
|
xTable = centerPosition.x + offsetX + cardIndexInRow * horizontalSpacing
|
||||||
|
|
||||||
|
return { xTable, yTable, scale }
|
||||||
|
}
|
||||||
28
src/hooks/useTimerWorker/index.ts
Normal file
28
src/hooks/useTimerWorker/index.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { onUnmounted } from 'vue'
|
||||||
|
import TimerWorker from './timerWorker.ts?worker'
|
||||||
|
|
||||||
|
export function useTimerWorker(interval: number) {
|
||||||
|
let timerWorker: Worker | null = null
|
||||||
|
const init = (callback: () => void) => {
|
||||||
|
close()
|
||||||
|
timerWorker = new TimerWorker()
|
||||||
|
timerWorker.postMessage({ interval })
|
||||||
|
if (timerWorker.onmessage)
|
||||||
|
return
|
||||||
|
timerWorker.addEventListener('message', () => callback())
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
timerWorker?.terminate()
|
||||||
|
timerWorker = null
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
close()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
init,
|
||||||
|
close,
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/hooks/useTimerWorker/timerworker.ts
Normal file
12
src/hooks/useTimerWorker/timerworker.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/* eslint-disable no-restricted-globals */
|
||||||
|
let intervalId: number | null = null
|
||||||
|
self.addEventListener('message', (e) => {
|
||||||
|
const { interval } = e.data
|
||||||
|
if (!interval)
|
||||||
|
throw new Error('invalid params')
|
||||||
|
if (intervalId)
|
||||||
|
clearInterval(intervalId)
|
||||||
|
intervalId = setInterval(() => {
|
||||||
|
self.postMessage(true)
|
||||||
|
}, interval)
|
||||||
|
})
|
||||||
114
src/hooks/useWebsocket.ts
Normal file
114
src/hooks/useWebsocket.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import type { IMsgType } from '@/types/msgType'
|
||||||
|
import type { WsMsgData } from '@/types/storeType'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { useTimerWorker } from './useTimerWorker'
|
||||||
|
|
||||||
|
export function useWebsocket() {
|
||||||
|
const { init: initWorker, close: closeWorker } = useTimerWorker(30 * 1000)
|
||||||
|
const status = ref<{ status: WebSocket['readyState'], connected: boolean }>()
|
||||||
|
const data = ref<WsMsgData>()
|
||||||
|
const registration = ref<ServiceWorkerRegistration | null>(null)
|
||||||
|
async function registerSW() {
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
try {
|
||||||
|
registration.value = await navigator.serviceWorker.register('/log-lottery/sw.js')
|
||||||
|
console.log('Service Worker 注册成功:', registration)
|
||||||
|
listenSWMessage()
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('Service Worker 注册失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.error('浏览器不支持 Service Worker')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function open(url: string) {
|
||||||
|
navigator.serviceWorker.ready.then((registration) => {
|
||||||
|
registration.active?.postMessage({
|
||||||
|
type: 'CONNECT_WS',
|
||||||
|
payload: { url },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
closeWorker()
|
||||||
|
navigator.serviceWorker.ready.then((registration) => {
|
||||||
|
registration.active?.postMessage({
|
||||||
|
type: 'DISCONNECT_WS',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function send(message: string) {
|
||||||
|
navigator.serviceWorker.ready.then((registration) => {
|
||||||
|
registration.active?.postMessage({
|
||||||
|
type: 'SEND_WS_MESSAGE',
|
||||||
|
payload: { message, id: uuidv4() },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function getStatus() {
|
||||||
|
navigator.serviceWorker.ready.then((registration) => {
|
||||||
|
registration.active?.postMessage({
|
||||||
|
type: 'GET_WS_STATUS',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听service worker消息
|
||||||
|
function listenSWMessage() {
|
||||||
|
navigator.serviceWorker.addEventListener('message', (event) => {
|
||||||
|
const msgType = event.data.type
|
||||||
|
switch (msgType) {
|
||||||
|
case 'WS_STATUS':
|
||||||
|
status.value = event.data.payload
|
||||||
|
break
|
||||||
|
case 'WS_MESSAGE':{
|
||||||
|
const receivedMsg: IMsgType = event.data.payload as IMsgType
|
||||||
|
data.value = {
|
||||||
|
...receivedMsg,
|
||||||
|
id: uuidv4(),
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'WS_ERROR':
|
||||||
|
console.error('ws error:', event.data.payload)
|
||||||
|
status.value = {
|
||||||
|
status: WebSocket.CLOSED,
|
||||||
|
connected: false,
|
||||||
|
}
|
||||||
|
closeWorker()
|
||||||
|
break
|
||||||
|
case 'WS_CLOSE':
|
||||||
|
status.value = {
|
||||||
|
status: WebSocket.CLOSED,
|
||||||
|
connected: false,
|
||||||
|
}
|
||||||
|
closeWorker()
|
||||||
|
break
|
||||||
|
case 'WS_OPEN':
|
||||||
|
status.value = {
|
||||||
|
status: WebSocket.OPEN,
|
||||||
|
connected: true,
|
||||||
|
}
|
||||||
|
initWorker(getStatus)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
registerSW()
|
||||||
|
getStatus()
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
open,
|
||||||
|
close,
|
||||||
|
send,
|
||||||
|
status,
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,32 @@
|
|||||||
<script setup lang='ts'>
|
<script setup lang='ts'>
|
||||||
import { useFullscreen } from '@vueuse/core'
|
import { useFullscreen } from '@vueuse/core'
|
||||||
import { Maximize, Minimize } from 'lucide-vue-next'
|
import { useQRCode } from '@vueuse/integrations/useQRCode'
|
||||||
import { onMounted, ref } from 'vue'
|
import { Maximize, Minimize, TabletSmartphone } from 'lucide-vue-next'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { onMounted, ref, shallowRef, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import CustomDialog from '@/components/Dialog/index.vue'
|
||||||
|
import useStore from '@/store'
|
||||||
|
import { getOriginUrl, getUniqueSignature } from '@/utils/auth'
|
||||||
import { usePlayMusic } from './usePlayMusic'
|
import { usePlayMusic } from './usePlayMusic'
|
||||||
|
|
||||||
|
const serverConfig = useStore().serverConfig
|
||||||
|
const {
|
||||||
|
getServerStatus: serverStatus,
|
||||||
|
} = storeToRefs(serverConfig)
|
||||||
const { playMusic, currentMusic, nextPlay } = usePlayMusic()
|
const { playMusic, currentMusic, nextPlay } = usePlayMusic()
|
||||||
const { isFullscreen, toggle: toggleScreen } = useFullscreen()
|
const { isFullscreen, toggle: toggleScreen } = useFullscreen()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
|
const customDialogRef = ref()
|
||||||
const settingRef = ref()
|
const settingRef = ref()
|
||||||
const fullScreenRef = ref()
|
const fullScreenRef = ref()
|
||||||
|
const mobileUrl = shallowRef<string>('')
|
||||||
|
const qrCodeImg = useQRCode(mobileUrl)
|
||||||
|
const visible = ref(true)
|
||||||
|
|
||||||
function enterConfig() {
|
function enterConfig() {
|
||||||
router.push('/log-lottery/config')
|
router.push('/log-lottery/config')
|
||||||
@@ -21,7 +34,26 @@ function enterConfig() {
|
|||||||
function enterHome() {
|
function enterHome() {
|
||||||
router.push('/log-lottery')
|
router.push('/log-lottery')
|
||||||
}
|
}
|
||||||
|
async function openMobileQrCode() {
|
||||||
|
const originUrl = getOriginUrl()
|
||||||
|
const userSignature = await getUniqueSignature()
|
||||||
|
mobileUrl.value = `${originUrl}/log-lottery/mobile?userSignature=${userSignature}`
|
||||||
|
customDialogRef.value.showDialog()
|
||||||
|
}
|
||||||
|
function handleSubmit() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => route, (val) => {
|
||||||
|
const { meta } = val
|
||||||
|
if (meta && meta.isMobile) {
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
if (!settingRef.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
settingRef.value.addEventListener('mouseenter', () => {
|
settingRef.value.addEventListener('mouseenter', () => {
|
||||||
fullScreenRef.value.style.display = 'block'
|
fullScreenRef.value.style.display = 'block'
|
||||||
})
|
})
|
||||||
@@ -32,7 +64,20 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="settingRef" class="flex flex-col gap-3">
|
<div v-if="visible" ref="settingRef" class="flex flex-col gap-3">
|
||||||
|
<CustomDialog
|
||||||
|
ref="customDialogRef"
|
||||||
|
title=""
|
||||||
|
:submit-func="handleSubmit"
|
||||||
|
footer="center"
|
||||||
|
dialog-class="h-120 p-6"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<div class="flex w-full justify-center h-90">
|
||||||
|
<img :src="qrCodeImg" alt="qr code">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</CustomDialog>
|
||||||
<div ref="fullScreenRef" class="tooltip tooltip-left hidden" @click="toggleScreen">
|
<div ref="fullScreenRef" class="tooltip tooltip-left hidden" @click="toggleScreen">
|
||||||
<div
|
<div
|
||||||
v-if="isFullscreen"
|
v-if="isFullscreen"
|
||||||
@@ -71,6 +116,11 @@ onMounted(() => {
|
|||||||
<svg-icon :name="currentMusic.paused ? 'play' : 'pause'" />
|
<svg-icon :name="currentMusic.paused ? 'play' : 'pause'" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="serverStatus" class="tooltip tooltip-left" data-tip="访问手机端">
|
||||||
|
<div class="flex items-center justify-center w-10 h-10 p-0 m-0 cursor-pointer setting-container bg-slate-500/50 rounded-l-xl hover:bg-slate-500/80 hover:text-blue-400/90" @click="openMobileQrCode">
|
||||||
|
<TabletSmartphone />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import type { Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { onMounted, provide, ref } from 'vue'
|
import { onMounted, provide, ref, toRaw, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
import { loadingKey, loadingState } from '@/components/Loading'
|
import { loadingKey, loadingState } from '@/components/Loading'
|
||||||
|
import { useWebsocket } from '@/hooks/useWebsocket'
|
||||||
import useStore from '@/store'
|
import useStore from '@/store'
|
||||||
import { themeChange } from '@/utils'
|
import { themeChange } from '@/utils'
|
||||||
|
import { IndexDb } from '@/utils/dexie'
|
||||||
|
|
||||||
export function useMounted(tipDialog: Ref<any>) {
|
export function useMounted(tipDialog: Ref<any>) {
|
||||||
provide(loadingKey, loadingState)
|
provide(loadingKey, loadingState)
|
||||||
@@ -15,6 +18,11 @@ export function useMounted(tipDialog: Ref<any>) {
|
|||||||
const { getPrizeConfig: prizeList, getTemporaryPrize: temporaryPrize } = storeToRefs(prizeConfig)
|
const { getPrizeConfig: prizeList, getTemporaryPrize: temporaryPrize } = storeToRefs(prizeConfig)
|
||||||
const tipDesc = ref('')
|
const tipDesc = ref('')
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const route = useRoute()
|
||||||
|
const msgListDb = new IndexDb('msgList', ['msgList'], 1, ['createTime'])
|
||||||
|
const enableWebsocket = import.meta.env.VITE_ENABLE_WEBSOCKET
|
||||||
|
const websocketData = enableWebsocket === 'true' ? useWebsocket() : { data: ref(null) }
|
||||||
|
const { data } = websocketData
|
||||||
// 设置当前奖列表
|
// 设置当前奖列表
|
||||||
function setCurrentPrize() {
|
function setCurrentPrize() {
|
||||||
if (prizeList.value.length <= 0) {
|
if (prizeList.value.length <= 0) {
|
||||||
@@ -52,10 +60,26 @@ export function useMounted(tipDialog: Ref<any>) {
|
|||||||
|
|
||||||
return isChrome || isEdge
|
return isChrome || isEdge
|
||||||
}
|
}
|
||||||
|
const isShowMobileWarn = () => {
|
||||||
|
const isMobilePage = judgeMobile()
|
||||||
|
const { meta } = route
|
||||||
|
let allowMobile = false
|
||||||
|
if (meta && meta.isMobile) {
|
||||||
|
allowMobile = true
|
||||||
|
}
|
||||||
|
return !allowMobile && isMobilePage
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => data.value, (newValue) => {
|
||||||
|
if (!newValue) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msgListDb.setData('msgList', toRaw(newValue))
|
||||||
|
}, { immediate: true, deep: true })
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
themeChange(localTheme.value.name)
|
themeChange(localTheme.value.name)
|
||||||
setCurrentPrize()
|
setCurrentPrize()
|
||||||
if (judgeMobile()) {
|
if (isShowMobileWarn()) {
|
||||||
tipDialog.value.showDialog()
|
tipDialog.value.showDialog()
|
||||||
tipDesc.value = t('dialog.dialogPCWeb')
|
tipDesc.value = t('dialog.dialogPCWeb')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const sidebarEn = {
|
|||||||
imagesManagement: 'Images Management',
|
imagesManagement: 'Images Management',
|
||||||
musicManagement: 'Music Management',
|
musicManagement: 'Music Management',
|
||||||
operatingInstructions: 'Operating Instructions',
|
operatingInstructions: 'Operating Instructions',
|
||||||
|
server: 'Server',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sidebarZhCn = {
|
export const sidebarZhCn = {
|
||||||
@@ -20,6 +21,7 @@ export const sidebarZhCn = {
|
|||||||
imagesManagement: '图片管理',
|
imagesManagement: '图片管理',
|
||||||
musicManagement: '音乐管理',
|
musicManagement: '音乐管理',
|
||||||
operatingInstructions: '操作说明',
|
operatingInstructions: '操作说明',
|
||||||
|
server: '服务器',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sidebar = {
|
export const sidebar = {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { SINGLE_TIME_MAX_PERSON_COUNT } from '@/constant/config'
|
||||||
|
|
||||||
export const tooltipEn = {
|
export const tooltipEn = {
|
||||||
settingConfiguration: 'Setting/Configuration',
|
settingConfiguration: 'Setting/Configuration',
|
||||||
nextSong: 'Right Click to Next Song',
|
nextSong: 'Right Click to Next Song',
|
||||||
@@ -16,6 +18,7 @@ export const tooltipEn = {
|
|||||||
timedStop: 'After the lottery begins, it will stop at a scheduled time by default, set to 0, with the unit in seconds. A value of 0 disables the scheduled stopping function',
|
timedStop: 'After the lottery begins, it will stop at a scheduled time by default, set to 0, with the unit in seconds. A value of 0 disables the scheduled stopping function',
|
||||||
uploadImage: 'Upload Image',
|
uploadImage: 'Upload Image',
|
||||||
pleaseGoto: 'Please go to',
|
pleaseGoto: 'Please go to',
|
||||||
|
onceNumberMax: `The maximum quantity for a single extraction is ${SINGLE_TIME_MAX_PERSON_COUNT}. If it exceeds ${SINGLE_TIME_MAX_PERSON_COUNT}, it will be automatically extracted in batches for you`,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const tooltipZhCn = {
|
export const tooltipZhCn = {
|
||||||
@@ -36,6 +39,7 @@ export const tooltipZhCn = {
|
|||||||
timedStop: '开始抽奖过后定时停止,默认为0,单位为秒,0为关闭定时停止功能',
|
timedStop: '开始抽奖过后定时停止,默认为0,单位为秒,0为关闭定时停止功能',
|
||||||
uploadImage: '上传图片',
|
uploadImage: '上传图片',
|
||||||
pleaseGoto: '请先前往',
|
pleaseGoto: '请先前往',
|
||||||
|
onceNumberMax: `单次抽取数量最大为${SINGLE_TIME_MAX_PERSON_COUNT},若设置超过${SINGLE_TIME_MAX_PERSON_COUNT}会自动为您分批次抽取`,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const tooltip = {
|
export const tooltip = {
|
||||||
|
|||||||
@@ -51,4 +51,8 @@ pinia.use(piniaPluginPersist)
|
|||||||
|
|
||||||
app.config.globalProperties.$THREE = THREE // 挂载到原型
|
app.config.globalProperties.$THREE = THREE // 挂载到原型
|
||||||
app.component('svg-icon', svgIcon)
|
app.component('svg-icon', svgIcon)
|
||||||
app.use(router).use(VueDOMPurifyHTML).use(pinia).use(i18n).mount('#app')
|
app.use(router)
|
||||||
|
app.use(VueDOMPurifyHTML)
|
||||||
|
app.use(pinia)
|
||||||
|
app.use(i18n)
|
||||||
|
app.mount('#app')
|
||||||
|
|||||||
@@ -101,6 +101,16 @@ export const configRoutes = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/log-lottery/config/server',
|
||||||
|
name: 'Server',
|
||||||
|
component: () => import('@/views/Config/Server/index.vue'),
|
||||||
|
meta: {
|
||||||
|
hidden: import.meta.env.VITE_ENABLE_WEBSOCKET !== 'true',
|
||||||
|
title: i18n.global.t('sidebar.server'),
|
||||||
|
icon: 'server',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/log-lottery/config/readme',
|
path: '/log-lottery/config/readme',
|
||||||
name: 'Readme',
|
name: 'Readme',
|
||||||
@@ -132,6 +142,14 @@ const routes = [
|
|||||||
name: 'Demo',
|
name: 'Demo',
|
||||||
component: () => import('@/views/Demo/index.vue'),
|
component: () => import('@/views/Demo/index.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/log-lottery/mobile',
|
||||||
|
name: 'Mobile',
|
||||||
|
meta: {
|
||||||
|
isMobile: true,
|
||||||
|
},
|
||||||
|
component: () => import('@/views/Mobile/index.vue'),
|
||||||
|
},
|
||||||
configRoutes,
|
configRoutes,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { IPersonConfig, IPrizeConfig } from '@/types/storeType'
|
import type { IPersonConfig, IPrizeConfig } from '@/types/storeType'
|
||||||
|
import { id } from 'zod/v4/locales'
|
||||||
|
|
||||||
const originUrl = 'https://to2026.xyz'
|
const originUrl = 'https://to2026.xyz'
|
||||||
type IPersonConfigWithoutUuid = Omit<IPersonConfig, 'uuid'>
|
type IPersonConfigWithoutUuid = Omit<IPersonConfig, 'uuid'>
|
||||||
@@ -279,3 +280,24 @@ export const defaultImageList = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
export const defaultPatternList = [21, 38, 55, 54, 53, 70, 87, 88, 89, 23, 40, 57, 74, 91, 92, 76, 59, 42, 25, 24, 27, 28, 29, 46, 63, 62, 61, 78, 95, 96, 97, 20, 19, 31, 48, 66, 67, 84, 101, 100, 32, 33, 93, 65, 82, 99]
|
export const defaultPatternList = [21, 38, 55, 54, 53, 70, 87, 88, 89, 23, 40, 57, 74, 91, 92, 76, 59, 42, 25, 24, 27, 28, 29, 46, 63, 62, 61, 78, 95, 96, 97, 20, 19, 31, 48, 66, 67, 84, 101, 100, 32, 33, 93, 65, 82, 99]
|
||||||
|
|
||||||
|
export const defaultServerHostList = [
|
||||||
|
{
|
||||||
|
id: 'default',
|
||||||
|
name: '默认服务器',
|
||||||
|
value: 'default',
|
||||||
|
host: 'https://to2026.xyz:8080',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'local',
|
||||||
|
name: '本地服务器',
|
||||||
|
value: 'local',
|
||||||
|
host: 'http://127.0.0.1:8080',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'custom',
|
||||||
|
name: '自定义服务器',
|
||||||
|
value: 'custom',
|
||||||
|
host: '',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useGlobalConfig } from './globalConfig'
|
import { useGlobalConfig } from './globalConfig'
|
||||||
import { usePersonConfig } from './personConfig'
|
import { usePersonConfig } from './personConfig'
|
||||||
import { usePrizeConfig } from './prizeConfig'
|
import { usePrizeConfig } from './prizeConfig'
|
||||||
|
import { useServerConfig } from './serverConfig'
|
||||||
import { useSystem } from './system'
|
import { useSystem } from './system'
|
||||||
|
|
||||||
export default function useStore() {
|
export default function useStore() {
|
||||||
@@ -9,5 +10,6 @@ export default function useStore() {
|
|||||||
prizeConfig: usePrizeConfig(),
|
prizeConfig: usePrizeConfig(),
|
||||||
globalConfig: useGlobalConfig(),
|
globalConfig: useGlobalConfig(),
|
||||||
system: useSystem(),
|
system: useSystem(),
|
||||||
|
serverConfig: useServerConfig(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
67
src/store/serverConfig.ts
Normal file
67
src/store/serverConfig.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import type { ServerType } from '@/types/storeType'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { defaultServerHostList } from './data'
|
||||||
|
|
||||||
|
export const useServerConfig = defineStore('server', {
|
||||||
|
state() {
|
||||||
|
return {
|
||||||
|
serverConfig: {
|
||||||
|
serverList: defaultServerHostList,
|
||||||
|
currentServer: defaultServerHostList[0],
|
||||||
|
serverStatus: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
// 获取服务器列表
|
||||||
|
getServerList(state) {
|
||||||
|
return state.serverConfig.serverList
|
||||||
|
},
|
||||||
|
// 获取当前服务器
|
||||||
|
getCurrentServer(state) {
|
||||||
|
return state.serverConfig.currentServer
|
||||||
|
},
|
||||||
|
// 获取服务器状态
|
||||||
|
getServerStatus(state) {
|
||||||
|
return state.serverConfig.serverStatus
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
// 设置服务器列表地址
|
||||||
|
updateServerList(userServer: ServerType) {
|
||||||
|
this.serverConfig.serverList.map((item) => {
|
||||||
|
if (item.id === userServer.id) {
|
||||||
|
item.host = userServer.host
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 设置当前服务器
|
||||||
|
setCurrentServer(userServer: ServerType) {
|
||||||
|
this.serverConfig.currentServer = userServer
|
||||||
|
},
|
||||||
|
// 设置服务器状态
|
||||||
|
setServerStatus(status: boolean) {
|
||||||
|
this.serverConfig.serverStatus = status
|
||||||
|
},
|
||||||
|
// 重置所有配置
|
||||||
|
resetDefault() {
|
||||||
|
this.serverConfig = {
|
||||||
|
serverList: defaultServerHostList,
|
||||||
|
currentServer: defaultServerHostList[0],
|
||||||
|
serverStatus: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
persist: {
|
||||||
|
enabled: true,
|
||||||
|
strategies: [
|
||||||
|
{
|
||||||
|
// 如果要存储在localStorage中
|
||||||
|
storage: localStorage,
|
||||||
|
key: 'serverConfig',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
cursor: default;
|
cursor: default;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
.card-id {
|
.card-id {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
@@ -21,7 +22,7 @@
|
|||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-avatar-name{
|
.card-avatar-name {
|
||||||
top: auto;
|
top: auto;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
@@ -37,7 +38,7 @@
|
|||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-avatar{
|
.card-avatar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -52,6 +53,8 @@
|
|||||||
|
|
||||||
.lucky-element-card {
|
.lucky-element-card {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
background-color: linear-gradient(-45deg, #e81cff 0%, #40c9ff 100%);
|
background-color: linear-gradient(-45deg, #e81cff 0%, #40c9ff 100%);
|
||||||
border: 1px solid linear-gradient(-45deg, #e81cff 0%, #40c9ff 100%);
|
border: 1px solid linear-gradient(-45deg, #e81cff 0%, #40c9ff 100%);
|
||||||
@@ -79,7 +82,8 @@
|
|||||||
// animation: tracking-in-expand-fwd 0.8s cubic-bezier(0.215, 0.61, 0.355, 1) both;
|
// animation: tracking-in-expand-fwd 0.8s cubic-bezier(0.215, 0.61, 0.355, 1) both;
|
||||||
// animation-delay: 0.6s;
|
// animation-delay: 0.6s;
|
||||||
}
|
}
|
||||||
.card-avatar-name{
|
|
||||||
|
.card-avatar-name {
|
||||||
top: auto;
|
top: auto;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
@@ -96,7 +100,7 @@
|
|||||||
bottom: 15px;
|
bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-avatar{
|
.card-avatar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|||||||
6
src/types/msgType.ts
Normal file
6
src/types/msgType.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export interface IMsgType {
|
||||||
|
data: string
|
||||||
|
image?: string | Blob | ArrayBuffer // TODO
|
||||||
|
dateTime: string
|
||||||
|
user?: string // TODO
|
||||||
|
}
|
||||||
@@ -52,3 +52,11 @@ export interface IImage {
|
|||||||
name: string
|
name: string
|
||||||
url: string | Blob | ArrayBuffer
|
url: string | Blob | ArrayBuffer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WsMsgData { data: string, id: string, dateTime: string }
|
||||||
|
export interface ServerType {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
value: string
|
||||||
|
host: string
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,20 @@
|
|||||||
|
import FingerprintJS from '@fingerprintjs/fingerprintjs'
|
||||||
|
|
||||||
export function getToken() {
|
export function getToken() {
|
||||||
return window.localStorage.getItem('userToken')
|
return window.localStorage.getItem('userToken')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户浏览器唯一标识
|
||||||
|
* @returns {Promise<string>} 唯一标识符
|
||||||
|
*/
|
||||||
|
export async function getUniqueSignature() {
|
||||||
|
const fp = await FingerprintJS.load()
|
||||||
|
const result = await fp.get()
|
||||||
|
return result.visitorId
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取origin url
|
||||||
|
export function getOriginUrl() {
|
||||||
|
return window.location.origin
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { EntityTable } from 'dexie'
|
|||||||
import type { DbData } from './type'
|
import type { DbData } from './type'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import Dexie from 'dexie'
|
import Dexie from 'dexie'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
class IndexDb {
|
class IndexDb {
|
||||||
name: string
|
name: string
|
||||||
@@ -38,6 +39,10 @@ class IndexDb {
|
|||||||
if (!data.type) {
|
if (!data.type) {
|
||||||
data.type = 'info'
|
data.type = 'info'
|
||||||
}
|
}
|
||||||
|
if (!data.id) {
|
||||||
|
data.id = uuidv4()
|
||||||
|
}
|
||||||
|
|
||||||
this.dbStore[tableName].add(data)
|
this.dbStore[tableName].add(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -160,6 +160,11 @@ export function useViewModel({ exportInputFileRef }: { exportInputFileRef: Ref<H
|
|||||||
const dataBinaryBinary = XLSX.utils.book_new()
|
const dataBinaryBinary = XLSX.utils.book_new()
|
||||||
XLSX.utils.book_append_sheet(dataBinaryBinary, dataBinary, 'Sheet1')
|
XLSX.utils.book_append_sheet(dataBinaryBinary, dataBinary, 'Sheet1')
|
||||||
XLSX.writeFile(dataBinaryBinary, 'data.xlsx')
|
XLSX.writeFile(dataBinaryBinary, 'data.xlsx')
|
||||||
|
toast.open({
|
||||||
|
message: t('error.exportSuccess'),
|
||||||
|
type: 'success',
|
||||||
|
position: 'top-right',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { Grip } from 'lucide-vue-next'
|
import { Grip } from 'lucide-vue-next'
|
||||||
import { VueDraggable } from 'vue-draggable-plus'
|
import { VueDraggable } from 'vue-draggable-plus'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { HoverTip } from '@/components/index'
|
||||||
import EditSeparateDialog from '@/components/NumberSeparate/EditSeparateDialog.vue'
|
import EditSeparateDialog from '@/components/NumberSeparate/EditSeparateDialog.vue'
|
||||||
import PageHeader from '@/components/PageHeader/index.vue'
|
import PageHeader from '@/components/PageHeader/index.vue'
|
||||||
import { usePrizeConfig } from './usePrizeConfig'
|
import { usePrizeConfig } from './usePrizeConfig'
|
||||||
@@ -69,7 +70,7 @@ const { t } = useI18n()
|
|||||||
@change="item.isAll = !item.isAll"
|
@change="item.isAll = !item.isAll"
|
||||||
>
|
>
|
||||||
</label>
|
</label>
|
||||||
<label class="w-1/2 max-w-xs form-control">
|
<label class="w-1/2 max-w-2xl form-control">
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<span class="label-text">{{ t('table.numberParticipants') }}</span>
|
<span class="label-text">{{ t('table.numberParticipants') }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -77,8 +78,8 @@ const { t } = useI18n()
|
|||||||
v-model="item.count" type="number" :placeholder="t('placeHolder.winnerCount')" class="w-full max-w-xs p-0 m-0 input-sm input input-bordered"
|
v-model="item.count" type="number" :placeholder="t('placeHolder.winnerCount')" class="w-full max-w-xs p-0 m-0 input-sm input input-bordered"
|
||||||
@change="changePrizePerson(item)"
|
@change="changePrizePerson(item)"
|
||||||
>
|
>
|
||||||
<div class="tooltip tooltip-bottom" :data-tip="`${t('table.isDone') + item.isUsedCount}/${item.count}`">
|
<div class="tooltip tooltip-bottom w-full" :data-tip="`${t('table.isDone') + item.isUsedCount}/${item.count}`">
|
||||||
<progress class="w-full progress" :value="item.isUsedCount" :max="item.count" />
|
<progress class="progress w-full" :value="item.isUsedCount" :max="item.count" />
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex items-center w-1/2 max-w-xs gap-2 form-control">
|
<label class="flex items-center w-1/2 max-w-xs gap-2 form-control">
|
||||||
@@ -105,7 +106,11 @@ const { t } = useI18n()
|
|||||||
<label v-if="item.separateCount" class="w-full max-w-xs form-control">
|
<label v-if="item.separateCount" class="w-full max-w-xs form-control">
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<span class="label-text">{{ t('table.onceNumber') }}</span>
|
<span class="label-text">{{ t('table.onceNumber') }}</span>
|
||||||
|
<HoverTip
|
||||||
|
:tip="t('tooltip.onceNumberMax')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-start w-full h-full" @click="selectPrize(item)">
|
<div class="flex justify-start w-full h-full" @click="selectPrize(item)">
|
||||||
<ul
|
<ul
|
||||||
v-if="item.separateCount.countList.length"
|
v-if="item.separateCount.countList.length"
|
||||||
|
|||||||
32
src/views/Config/Server/index.vue
Normal file
32
src/views/Config/Server/index.vue
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<script setup lang='ts'>
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import PageHeader from '@/components/PageHeader/index.vue'
|
||||||
|
import MsgListContainer from './parts/MsgListContainer.vue'
|
||||||
|
import ServerSetting from './parts/ServerSetting.vue'
|
||||||
|
import { useViewModel } from './useViewModel'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const { serverList, currentServerValue, wsStatus, handleConnectWs, closeWs, msgList } = useViewModel()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<PageHeader :title="t('sidebar.server')" />
|
||||||
|
<div>
|
||||||
|
<ServerSetting
|
||||||
|
v-model:current-server="currentServerValue"
|
||||||
|
:server-list="serverList"
|
||||||
|
:ws-status="wsStatus"
|
||||||
|
:open-ws="handleConnectWs"
|
||||||
|
:close-ws="closeWs"
|
||||||
|
/>
|
||||||
|
<MsgListContainer
|
||||||
|
:msg-list="msgList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
29
src/views/Config/Server/parts/MsgListContainer.vue
Normal file
29
src/views/Config/Server/parts/MsgListContainer.vue
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<script setup lang='ts'>
|
||||||
|
import type { WsMsgData } from '@/types/storeType'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
msgList: WsMsgData[]
|
||||||
|
}
|
||||||
|
defineProps<Props>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-1/2 h-1/2 border rounded-md shadow-lg">
|
||||||
|
<ul>
|
||||||
|
<li v-for="item in msgList" :key="item.id" class="mb-3">
|
||||||
|
<div class="chat chat-end">
|
||||||
|
<div class="chat-header">
|
||||||
|
<time class="text-xs opacity-50">{{ item.dateTime }}</time>
|
||||||
|
</div>
|
||||||
|
<div class="chat-bubble break-all whitespace-normal">
|
||||||
|
{{ item.data }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
107
src/views/Config/Server/parts/ServerSetting.vue
Normal file
107
src/views/Config/Server/parts/ServerSetting.vue
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
<script setup lang='ts'>
|
||||||
|
import type { ServerType } from '@/types/storeType'
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
// import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
serverList: ServerType[]
|
||||||
|
wsStatus: { status: WebSocket['readyState'], connected: boolean } | undefined
|
||||||
|
openWs: () => void
|
||||||
|
closeWs: () => void
|
||||||
|
}
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
const currentServer = defineModel<ServerType>('currentServer', { required: true })
|
||||||
|
const hostValue = ref('')
|
||||||
|
// const { t } = useI18n()
|
||||||
|
// 监听 currentServer 的 id 变化,重置 hostValue
|
||||||
|
watch(() => currentServer.value?.id, (newId, oldId) => {
|
||||||
|
if (newId !== oldId) {
|
||||||
|
hostValue.value = currentServer.value?.host || ''
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
// 监听 hostValue 变化,同步更新 currentServer.host
|
||||||
|
watch(hostValue, (newHost) => {
|
||||||
|
if (currentServer.value) {
|
||||||
|
currentServer.value.host = newHost
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 初始化 hostValue
|
||||||
|
hostValue.value = currentServer.value?.host || ''
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<fieldset class="p-4 border text-setting fieldset bg-base-200 border-base-300 rounded-box w-xs pb-10">
|
||||||
|
<legend class="fieldset-legend">
|
||||||
|
弹幕服务
|
||||||
|
</legend>
|
||||||
|
|
||||||
|
<label class="flex flex-row items-center form-control">
|
||||||
|
<div class="">
|
||||||
|
<div class="label flex flex-col justify-start items-start">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text text-left">弹幕服务地址</span>
|
||||||
|
<div class="tooltip" data-tip="改变弹幕服务地址后会断开连接">
|
||||||
|
<button class="btn btn-circle h-4 hover:bg-base-300">
|
||||||
|
?
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<div class="radio-group flex gap-9">
|
||||||
|
<ul class="flex gap-3">
|
||||||
|
<li v-for="item in serverList" :key="item.id" class="flex flex-col">
|
||||||
|
<label for="default-server">{{ item.name }}</label>
|
||||||
|
<input id="default-server" type="radio" name="radio-1" class="radio" :checked="currentServer?.value === item.value" @change="currentServer = item">
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="请输入服务地址"
|
||||||
|
:disabled="currentServer.value === 'default'"
|
||||||
|
class="w-full max-w-xs input input-bordered"
|
||||||
|
:value="hostValue"
|
||||||
|
@input="hostValue = ($event.target as HTMLInputElement).value"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label class="flex flex-row items-center form-control">
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<div class="label">
|
||||||
|
<span class="label-text">弹幕服务器连接状态</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<div class="ws-status">
|
||||||
|
<div v-if="wsStatus && wsStatus.connected">
|
||||||
|
<div aria-label="success" class="status status-success" />
|
||||||
|
<span>已连接</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="wsStatus && wsStatus.connected === false">
|
||||||
|
<div aria-label="error" class="status status-error" />
|
||||||
|
<span>已断开</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="wsStatus && wsStatus.status">
|
||||||
|
<div aria-label="error" class="status status-error" />
|
||||||
|
<span>操作中</span>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div aria-label="warning" class="status status-warning" />
|
||||||
|
<span>未连接</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<button v-if="wsStatus?.connected === true" class="btn btn-error btn-sm" @click="closeWs">断开</button>
|
||||||
|
<button v-else class="btn btn-primary btn-sm" @click="openWs">连接</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
68
src/views/Config/Server/useViewModel.ts
Normal file
68
src/views/Config/Server/useViewModel.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import type { ServerType, WsMsgData } from '@/types/storeType'
|
||||||
|
import { cloneDeep } from 'lodash-es'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { onMounted, ref, watch } from 'vue'
|
||||||
|
import { useWebsocket } from '@/hooks/useWebsocket'
|
||||||
|
import useStore from '@/store'
|
||||||
|
import { getUniqueSignature } from '@/utils/auth'
|
||||||
|
import { IndexDb } from '@/utils/dexie'
|
||||||
|
|
||||||
|
export function useViewModel() {
|
||||||
|
const serverConfig = useStore().serverConfig
|
||||||
|
const {
|
||||||
|
getServerList: serverList,
|
||||||
|
getCurrentServer: currentServer,
|
||||||
|
} = storeToRefs(serverConfig)
|
||||||
|
const currentServerValue = ref<ServerType>(cloneDeep(currentServer.value))
|
||||||
|
const wsUrl = ref<string>('ws://localhost:8080/echo')
|
||||||
|
const msgList = ref<WsMsgData[]>([])
|
||||||
|
const { open: openWs, close: closeWs, status: wsStatus } = useWebsocket()
|
||||||
|
const msgListDb = new IndexDb('msgList', ['msgList'], 1, ['createTime'])
|
||||||
|
const handleConnectWs = async () => {
|
||||||
|
const userSignature = await getUniqueSignature()
|
||||||
|
wsUrl.value = `ws://localhost:8080/echo?userSignature=${userSignature}`
|
||||||
|
openWs(wsUrl.value)
|
||||||
|
}
|
||||||
|
const getAllMsg = async () => {
|
||||||
|
msgListDb.getDataSortedByDateTime('msgList', 'dateTime').then((data) => {
|
||||||
|
msgList.value = data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => currentServerValue.value.id,
|
||||||
|
(newValue) => {
|
||||||
|
serverList.value.forEach((item) => {
|
||||||
|
if (item.id === newValue) {
|
||||||
|
currentServerValue.value = item
|
||||||
|
serverConfig.setCurrentServer(currentServerValue.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
watch(() => currentServer.value.host, (newValue) => {
|
||||||
|
currentServerValue.value.host = newValue
|
||||||
|
serverConfig.updateServerList(currentServerValue.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => wsStatus.value,
|
||||||
|
(newValue) => {
|
||||||
|
if (newValue && (newValue.connected === true || newValue.connected === false)) {
|
||||||
|
serverConfig.setServerStatus(newValue.connected)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getAllMsg()
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
serverList,
|
||||||
|
currentServerValue,
|
||||||
|
wsStatus,
|
||||||
|
handleConnectWs,
|
||||||
|
closeWs,
|
||||||
|
msgList,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,7 +37,7 @@ function skip(path: string) {
|
|||||||
<div class="flex min-h-[calc(100%-280px)]">
|
<div class="flex min-h-[calc(100%-280px)]">
|
||||||
<ul class="w-56 m-0 mr-3 min-w-56 menu bg-base-200 pt-14">
|
<ul class="w-56 m-0 mr-3 min-w-56 menu bg-base-200 pt-14">
|
||||||
<li v-for="item in menuList" :key="item.name">
|
<li v-for="item in menuList" :key="item.name">
|
||||||
<details v-if="item.children" open>
|
<details v-if="item.children && !item.meta.hidden" open>
|
||||||
<summary>{{ item.meta.title }}</summary>
|
<summary>{{ item.meta.title }}</summary>
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="subItem in item.children" :key="subItem.name">
|
<li v-for="subItem in item.children" :key="subItem.name">
|
||||||
@@ -62,9 +62,10 @@ function skip(path: string) {
|
|||||||
</ul>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
<a
|
<a
|
||||||
v-else :style="item.name === route.name ? 'background-color:rgba(12,12,12,0.2)' : ''"
|
v-else-if="!item.meta.hidden" :style="item.name === route.name ? 'background-color:rgba(12,12,12,0.2)' : ''"
|
||||||
@click="skip(item.path)"
|
@click="skip(item.path)"
|
||||||
>{{ item.meta!.title }}</a>
|
>{{ item.meta!.title }}</a>
|
||||||
|
<div v-else />
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<router-view class="flex-1 mt-5" />
|
<router-view class="flex-1 mt-5" />
|
||||||
|
|||||||
@@ -1,241 +1,50 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import gsap from 'gsap'
|
import { onMounted, onUnmounted, ref, shallowRef, watch } from 'vue'
|
||||||
import { ScrollTrigger } from 'gsap/ScrollTrigger'
|
import { useToast } from 'vue-toast-notification'
|
||||||
import { onBeforeUnmount, onMounted, onUnmounted, ref } from 'vue'
|
import { api_sendMsg } from '@/api/msg'
|
||||||
|
import { getOriginUrl, getUniqueSignature } from '@/utils/auth'
|
||||||
|
|
||||||
gsap.registerPlugin(ScrollTrigger)
|
const toast = useToast()
|
||||||
|
const mobileUrl = shallowRef<string>('')
|
||||||
|
const wsQuery = ref<{ userSignature: string }>({
|
||||||
|
userSignature: '',
|
||||||
|
})
|
||||||
|
|
||||||
const list = ref<any[]>([])
|
function connectUserMsg() {
|
||||||
|
api_sendMsg(wsQuery.value.userSignature, `hello world ${wsQuery.value.userSignature}`).then((res: any) => {
|
||||||
list.value = [{
|
toast.open({
|
||||||
label: 1,
|
message: res.msg || '发送成功',
|
||||||
value: 1,
|
type: 'success',
|
||||||
color: 'red',
|
position: 'top-right',
|
||||||
}, {
|
|
||||||
label: 2,
|
|
||||||
value: 2,
|
|
||||||
color: 'blue',
|
|
||||||
}, {
|
|
||||||
label: 3,
|
|
||||||
value: 3,
|
|
||||||
color: 'yellow',
|
|
||||||
}, {
|
|
||||||
label: 4,
|
|
||||||
value: 4,
|
|
||||||
color: 'green',
|
|
||||||
}, {
|
|
||||||
label: 5,
|
|
||||||
value: 5,
|
|
||||||
color: 'pink',
|
|
||||||
}, {
|
|
||||||
label: 6,
|
|
||||||
value: 6,
|
|
||||||
color: 'orange',
|
|
||||||
}, {
|
|
||||||
label: 7,
|
|
||||||
value: 7,
|
|
||||||
color: 'purple',
|
|
||||||
}, {
|
|
||||||
label: 8,
|
|
||||||
value: 8,
|
|
||||||
color: 'brown',
|
|
||||||
}, {
|
|
||||||
label: 9,
|
|
||||||
value: 9,
|
|
||||||
color: 'gray',
|
|
||||||
}, {
|
|
||||||
label: 10,
|
|
||||||
value: 10,
|
|
||||||
color: 'cyan',
|
|
||||||
}, {
|
|
||||||
label: 11,
|
|
||||||
value: 11,
|
|
||||||
color: 'white',
|
|
||||||
}, {
|
|
||||||
label: 12,
|
|
||||||
value: 12,
|
|
||||||
color: 'black',
|
|
||||||
}, {
|
|
||||||
label: 13,
|
|
||||||
value: 13,
|
|
||||||
color: 'orange',
|
|
||||||
}, {
|
|
||||||
label: 14,
|
|
||||||
value: 14,
|
|
||||||
color: 'yellow',
|
|
||||||
}, {
|
|
||||||
label: 15,
|
|
||||||
value: 14,
|
|
||||||
color: 'pink',
|
|
||||||
}, {
|
|
||||||
label: 15,
|
|
||||||
value: 15,
|
|
||||||
color: 'orange',
|
|
||||||
}, {
|
|
||||||
label: 16,
|
|
||||||
value: 16,
|
|
||||||
color: 'yellow',
|
|
||||||
}, {
|
|
||||||
label: 17,
|
|
||||||
value: 17,
|
|
||||||
color: 'green',
|
|
||||||
}, {
|
|
||||||
label: 18,
|
|
||||||
value: 18,
|
|
||||||
color: 'purple',
|
|
||||||
}]
|
|
||||||
|
|
||||||
// 为每个 li 元素创建引用
|
|
||||||
const liRefs = ref()
|
|
||||||
const scrollContainerRef = ref()
|
|
||||||
const ctx = ref()
|
|
||||||
const showUpButton = ref(false)
|
|
||||||
const showDownButton = ref(true)
|
|
||||||
|
|
||||||
function initGsapAnimation() {
|
|
||||||
ctx.value = gsap.context(() => {
|
|
||||||
liRefs.value.forEach((box: any) => {
|
|
||||||
gsap.fromTo(box, { rotationX: -90, rotateZ: -20, opacity: 0 }, {
|
|
||||||
rotationX: 0,
|
|
||||||
rotateZ: 0,
|
|
||||||
opacity: 1,
|
|
||||||
scrollTrigger: {
|
|
||||||
trigger: box,
|
|
||||||
scroller: scrollContainerRef.value, // <- Specify the scroller!
|
|
||||||
start: 'bottom 100%',
|
|
||||||
end: 'top 70%',
|
|
||||||
scrub: true,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}, scrollContainerRef.value) // <- Scope!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function disposeGsapAnimation() {
|
async function getFinger() {
|
||||||
ctx.value.revert() // <- Easy Cleanup!
|
const userSignature = await getUniqueSignature()
|
||||||
|
wsQuery.value.userSignature = userSignature
|
||||||
|
return userSignature
|
||||||
}
|
}
|
||||||
function scrollHandler() {
|
async function setMobileUrl() {
|
||||||
const scrollHeight = scrollContainerRef.value.scrollHeight
|
const originUrl = getOriginUrl()
|
||||||
const scrollTop = scrollContainerRef.value.scrollTop
|
const userSignature = await getFinger()
|
||||||
const containerHeight = scrollContainerRef.value.clientHeight
|
mobileUrl.value = `${originUrl}/log-lottery/mobile?userSignature=${userSignature}`
|
||||||
// 滚动滑到底部
|
|
||||||
if (scrollTop + containerHeight >= scrollHeight) {
|
|
||||||
showDownButton.value = false
|
|
||||||
showUpButton.value = true
|
|
||||||
}
|
|
||||||
// 在中间
|
|
||||||
else if (scrollTop && scrollTop + containerHeight < scrollHeight) {
|
|
||||||
showDownButton.value = true
|
|
||||||
showUpButton.value = true
|
|
||||||
}
|
|
||||||
// 滚动滑到顶部
|
|
||||||
else {
|
|
||||||
showDownButton.value = true
|
|
||||||
showUpButton.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function listenScrollContainer() {
|
|
||||||
scrollContainerRef.value.addEventListener('scroll', scrollHandler)
|
|
||||||
}
|
|
||||||
function removeScrollContainer() {
|
|
||||||
if (scrollContainerRef.value) {
|
|
||||||
scrollContainerRef.value.removeEventListener('scroll', scrollHandler)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleScroll(h: number) {
|
|
||||||
scrollContainerRef.value.scrollTop += h
|
|
||||||
}
|
}
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initGsapAnimation()
|
getFinger()
|
||||||
listenScrollContainer()
|
setMobileUrl()
|
||||||
})
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
removeScrollContainer()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
disposeGsapAnimation()
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-full w-48 flex flex-col justify-center overflow-hidden relative">
|
<div class="flex flex-col gap-3">
|
||||||
<div class="w-full h-16 flex justify-center scroll-button scroll-button-up">
|
<button class="btn btn-primary btn-sm w-32" @click="connectUserMsg">
|
||||||
<SvgIcon v-show="showUpButton" name="chevron-up" size="64px" class="text-gray-200/80 cursor-pointer" @click="handleScroll(-100)" />
|
connectUserMsg
|
||||||
</div>
|
</button>
|
||||||
<div ref="scrollContainerRef" class="h-150 w-48 overflow-y-auto overflow-x-hidden relative scroll-smooth hide-scrollbar">
|
|
||||||
<ul class="li-container relative bg-slate-500/50">
|
|
||||||
<li
|
|
||||||
v-for="item in list" :key="item.value" ref="liRefs" :style="{ backgroundColor: item.color }"
|
|
||||||
class="w-full h-28 text-center leading-30 cursor-pointer duration-300"
|
|
||||||
>
|
|
||||||
{{ item.label }}
|
|
||||||
</li>
|
|
||||||
<li class="h-16" />
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="w-full h-16 flex justify-center scroll-button scroll-button-down">
|
|
||||||
<SvgIcon v-show="showDownButton" name="chevron-down" size="64px" class="text-gray-200/80 cursor-pointer" @click="handleScroll(100)" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped></style>
|
||||||
.scroll-button::before,
|
|
||||||
.scroll-button::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: -1;
|
|
||||||
transform: translate(12px 12px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroll-button::before {
|
|
||||||
transform: translate(0, -6px);
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroll-button::after {
|
|
||||||
transform: translate(0, 6px);
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 添加动画效果 */
|
|
||||||
.scroll-button-down {
|
|
||||||
animation: bounce-down 2s infinite;
|
|
||||||
}
|
|
||||||
/* 添加动画效果 */
|
|
||||||
.scroll-button-up {
|
|
||||||
animation: bounce-up 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bounce-down {
|
|
||||||
0%, 20%, 50%, 80%, 100% {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
40% {
|
|
||||||
transform: translateY(-5px);
|
|
||||||
}
|
|
||||||
60% {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes bounce-up {
|
|
||||||
0%, 20%, 50%, 80%, 100% {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
40% {
|
|
||||||
transform: translateY(5px);
|
|
||||||
}
|
|
||||||
60% {
|
|
||||||
transform: translateY(2px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroll-button:hover {
|
|
||||||
transform: translateY(-3px);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ import { CSS3DObject, CSS3DRenderer } from 'three-css3d'
|
|||||||
import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls.js'
|
import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls.js'
|
||||||
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
|
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { useToast } from 'vue-toast-notification'
|
import { useToast } from 'vue-toast-notification'
|
||||||
|
import dongSound from '@/assets/audio/end.mp3'
|
||||||
import enterAudio from '@/assets/audio/enter.wav'
|
import enterAudio from '@/assets/audio/enter.wav'
|
||||||
|
import worldCupAudio from '@/assets/audio/worldcup.mp3'
|
||||||
|
import { SINGLE_TIME_MAX_PERSON_COUNT } from '@/constant/config'
|
||||||
import { useElementPosition, useElementStyle } from '@/hooks/useElement'
|
import { useElementPosition, useElementStyle } from '@/hooks/useElement'
|
||||||
import i18n from '@/locales/i18n'
|
import i18n from '@/locales/i18n'
|
||||||
import useStore from '@/store'
|
import useStore from '@/store'
|
||||||
@@ -70,6 +73,10 @@ export function useViewModel() {
|
|||||||
const isInitialDone = ref<boolean>(false)
|
const isInitialDone = ref<boolean>(false)
|
||||||
const animationFrameId = ref<any>(null)
|
const animationFrameId = ref<any>(null)
|
||||||
const playingAudios = ref<HTMLAudioElement[]>([])
|
const playingAudios = ref<HTMLAudioElement[]>([])
|
||||||
|
|
||||||
|
// 抽奖音乐相关
|
||||||
|
const lotteryMusic = ref<HTMLAudioElement | null>(null)
|
||||||
|
|
||||||
function initThreeJs() {
|
function initThreeJs() {
|
||||||
const felidView = 40
|
const felidView = 40
|
||||||
const width = window.innerWidth
|
const width = window.innerWidth
|
||||||
@@ -140,7 +147,19 @@ export function useViewModel() {
|
|||||||
element.appendChild(avatarEmpty)
|
element.appendChild(avatarEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
element = useElementStyle(element, tableData.value[i], i, patternList.value, patternColor.value, cardColor.value, cardSize.value, textSize.value)
|
element = useElementStyle({
|
||||||
|
element,
|
||||||
|
person: tableData.value[i],
|
||||||
|
index: i,
|
||||||
|
patternList: patternList.value,
|
||||||
|
patternColor: patternColor.value,
|
||||||
|
cardColor: cardColor.value,
|
||||||
|
cardSize: cardSize.value,
|
||||||
|
scale: 1,
|
||||||
|
textSize: textSize.value,
|
||||||
|
mod: 'default',
|
||||||
|
},
|
||||||
|
)
|
||||||
const object = new CSS3DObject(element)
|
const object = new CSS3DObject(element)
|
||||||
object.position.x = Math.random() * 4000 - 2000
|
object.position.x = Math.random() * 4000 - 2000
|
||||||
object.position.y = Math.random() * 4000 - 2000
|
object.position.y = Math.random() * 4000 - 2000
|
||||||
@@ -195,7 +214,18 @@ export function useViewModel() {
|
|||||||
if (luckyCardList.value.length) {
|
if (luckyCardList.value.length) {
|
||||||
luckyCardList.value.forEach((cardIndex: any) => {
|
luckyCardList.value.forEach((cardIndex: any) => {
|
||||||
const item = objects.value[cardIndex]
|
const item = objects.value[cardIndex]
|
||||||
useElementStyle(item.element, {} as any, i, patternList.value, patternColor.value, cardColor.value, cardSize.value, textSize.value, 'sphere')
|
useElementStyle({
|
||||||
|
element: item.element,
|
||||||
|
person: {} as any,
|
||||||
|
index: i,
|
||||||
|
patternList: patternList.value,
|
||||||
|
patternColor: patternColor.value,
|
||||||
|
cardColor: cardColor.value,
|
||||||
|
cardSize: cardSize.value,
|
||||||
|
scale: 1,
|
||||||
|
textSize: textSize.value,
|
||||||
|
mod: 'sphere',
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
luckyTargets.value = []
|
luckyTargets.value = []
|
||||||
@@ -312,6 +342,104 @@ export function useViewModel() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 开始抽奖音乐
|
||||||
|
*/
|
||||||
|
function startLotteryMusic() {
|
||||||
|
if (!isPlayWinMusic.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (lotteryMusic.value) {
|
||||||
|
lotteryMusic.value.pause()
|
||||||
|
lotteryMusic.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
lotteryMusic.value = new Audio(worldCupAudio)
|
||||||
|
lotteryMusic.value.loop = true
|
||||||
|
lotteryMusic.value.volume = 0.7
|
||||||
|
|
||||||
|
lotteryMusic.value.play().catch((error) => {
|
||||||
|
console.error('播放抽奖音乐失败:', error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 停止抽奖音乐
|
||||||
|
*/
|
||||||
|
function stopLotteryMusic() {
|
||||||
|
if (!isPlayWinMusic.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (lotteryMusic.value) {
|
||||||
|
lotteryMusic.value.pause()
|
||||||
|
lotteryMusic.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 播放结束音效
|
||||||
|
*/
|
||||||
|
function playEndSound() {
|
||||||
|
if (!isPlayWinMusic.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log('准备播放结束音效', dongSound)
|
||||||
|
|
||||||
|
// 清理已结束的音频
|
||||||
|
playingAudios.value = playingAudios.value.filter(audio => !audio.ended)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const endSound = new Audio(dongSound)
|
||||||
|
endSound.volume = 1.0
|
||||||
|
|
||||||
|
// 简化播放逻辑
|
||||||
|
const playPromise = endSound.play()
|
||||||
|
|
||||||
|
if (playPromise) {
|
||||||
|
playPromise
|
||||||
|
.then(() => {
|
||||||
|
console.log('结束音效播放成功')
|
||||||
|
playingAudios.value.push(endSound)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('播放失败:', err.name, err.message)
|
||||||
|
if (err.name === 'NotAllowedError') {
|
||||||
|
console.warn('自动播放被阻止,需用户交互后播放')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
endSound.onended = () => {
|
||||||
|
console.log('结束音效播放完成')
|
||||||
|
const index = playingAudios.value.indexOf(endSound)
|
||||||
|
if (index > -1)
|
||||||
|
playingAudios.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('创建音频对象失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 重置音频状态
|
||||||
|
*/
|
||||||
|
function resetAudioState() {
|
||||||
|
if (!isPlayWinMusic.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 停止抽奖音乐
|
||||||
|
stopLotteryMusic()
|
||||||
|
|
||||||
|
// 清理所有正在播放的音频
|
||||||
|
playingAudios.value.forEach((audio) => {
|
||||||
|
if (!audio.ended && !audio.paused) {
|
||||||
|
audio.pause()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
playingAudios.value = []
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description: 开始抽奖,由横铺变换为球体(或其他图形)
|
* @description: 开始抽奖,由横铺变换为球体(或其他图形)
|
||||||
* @returns 随机抽取球数据
|
* @returns 随机抽取球数据
|
||||||
@@ -321,6 +449,21 @@ export function useViewModel() {
|
|||||||
if (!canOperate.value) {
|
if (!canOperate.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 重置音频状态
|
||||||
|
resetAudioState()
|
||||||
|
|
||||||
|
// 预加载音频资源以解决浏览器自动播放策略
|
||||||
|
try {
|
||||||
|
const audioContext = window.AudioContext || (window as any).webkitAudioContext
|
||||||
|
if (audioContext) {
|
||||||
|
console.log('音频上下文可用')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.warn('音频上下文不可用:', e)
|
||||||
|
}
|
||||||
|
|
||||||
if (!intervalTimer.value) {
|
if (!intervalTimer.value) {
|
||||||
randomBallData()
|
randomBallData()
|
||||||
}
|
}
|
||||||
@@ -366,14 +509,15 @@ export function useViewModel() {
|
|||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
luckyCount.value = 10
|
// 默认置为单次抽奖最大个数
|
||||||
// 自定义抽奖个数
|
luckyCount.value = SINGLE_TIME_MAX_PERSON_COUNT
|
||||||
|
// 还剩多少人未抽
|
||||||
let leftover = currentPrize.value.count - currentPrize.value.isUsedCount
|
let leftover = currentPrize.value.count - currentPrize.value.isUsedCount
|
||||||
const customCount = currentPrize.value.separateCount
|
const customCount = currentPrize.value.separateCount
|
||||||
if (customCount && customCount.enable && customCount.countList.length > 0) {
|
if (customCount && customCount.enable && customCount.countList.length > 0) {
|
||||||
for (let i = 0; i < customCount.countList.length; i++) {
|
for (let i = 0; i < customCount.countList.length; i++) {
|
||||||
if (customCount.countList[i].isUsedCount < customCount.countList[i].count) {
|
if (customCount.countList[i].isUsedCount < customCount.countList[i].count) {
|
||||||
|
// 根据自定义人数来抽取
|
||||||
leftover = customCount.countList[i].count - customCount.countList[i].isUsedCount
|
leftover = customCount.countList[i].count - customCount.countList[i].isUsedCount
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -396,6 +540,10 @@ export function useViewModel() {
|
|||||||
position: 'top-right',
|
position: 'top-right',
|
||||||
duration: 8000,
|
duration: 8000,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 开始播放抽奖音乐
|
||||||
|
startLotteryMusic()
|
||||||
|
|
||||||
currentStatus.value = LotteryStatus.running
|
currentStatus.value = LotteryStatus.running
|
||||||
rollBall(10, 3000)
|
rollBall(10, 3000)
|
||||||
if (definiteTime.value) {
|
if (definiteTime.value) {
|
||||||
@@ -413,6 +561,12 @@ export function useViewModel() {
|
|||||||
if (!canOperate.value) {
|
if (!canOperate.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 停止抽奖音乐
|
||||||
|
stopLotteryMusic()
|
||||||
|
|
||||||
|
// 播放结束音效
|
||||||
|
playEndSound()
|
||||||
|
|
||||||
// clearInterval(intervalTimer.value)
|
// clearInterval(intervalTimer.value)
|
||||||
// intervalTimer.value = null
|
// intervalTimer.value = null
|
||||||
canOperate.value = false
|
canOperate.value = false
|
||||||
@@ -424,7 +578,14 @@ export function useViewModel() {
|
|||||||
luckyCardList.value.push(cardIndex)
|
luckyCardList.value.push(cardIndex)
|
||||||
const totalLuckyCount = luckyTargets.value.length
|
const totalLuckyCount = luckyTargets.value.length
|
||||||
const item = objects.value[cardIndex]
|
const item = objects.value[cardIndex]
|
||||||
const { xTable, yTable } = useElementPosition(item, rowCount.value, totalLuckyCount, { width: cardSize.value.width * 2, height: cardSize.value.height * 2 }, windowSize, index)
|
const { xTable, yTable, scale } = useElementPosition(
|
||||||
|
item,
|
||||||
|
rowCount.value,
|
||||||
|
totalLuckyCount,
|
||||||
|
{ width: cardSize.value.width, height: cardSize.value.height },
|
||||||
|
windowSize,
|
||||||
|
index,
|
||||||
|
)
|
||||||
new TWEEN.Tween(item.position)
|
new TWEEN.Tween(item.position)
|
||||||
.to({
|
.to({
|
||||||
x: xTable,
|
x: xTable,
|
||||||
@@ -433,7 +594,18 @@ export function useViewModel() {
|
|||||||
}, 1200)
|
}, 1200)
|
||||||
.easing(TWEEN.Easing.Exponential.InOut)
|
.easing(TWEEN.Easing.Exponential.InOut)
|
||||||
.onStart(() => {
|
.onStart(() => {
|
||||||
item.element = useElementStyle(item.element, person, cardIndex, patternList.value, patternColor.value, luckyColor.value, { width: cardSize.value.width * 2, height: cardSize.value.height * 2 }, textSize.value * 2, 'lucky')
|
item.element = useElementStyle({
|
||||||
|
element: item.element,
|
||||||
|
person,
|
||||||
|
index: cardIndex,
|
||||||
|
patternList: patternList.value,
|
||||||
|
patternColor: patternColor.value,
|
||||||
|
cardColor: luckyColor.value,
|
||||||
|
cardSize: { width: cardSize.value.width, height: cardSize.value.height },
|
||||||
|
scale,
|
||||||
|
textSize: textSize.value,
|
||||||
|
mod: 'lucky',
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.start()
|
.start()
|
||||||
.onComplete(() => {
|
.onComplete(() => {
|
||||||
@@ -449,9 +621,8 @@ export function useViewModel() {
|
|||||||
.easing(TWEEN.Easing.Exponential.InOut)
|
.easing(TWEEN.Easing.Exponential.InOut)
|
||||||
.start()
|
.start()
|
||||||
.onComplete(() => {
|
.onComplete(() => {
|
||||||
if (isPlayWinMusic.value) {
|
|
||||||
playWinMusic()
|
playWinMusic()
|
||||||
}
|
|
||||||
confettiFire()
|
confettiFire()
|
||||||
resetCamera()
|
resetCamera()
|
||||||
})
|
})
|
||||||
@@ -459,11 +630,20 @@ export function useViewModel() {
|
|||||||
}
|
}
|
||||||
// 播放音频,中将卡片越多audio对象越多,声音越大
|
// 播放音频,中将卡片越多audio对象越多,声音越大
|
||||||
function playWinMusic() {
|
function playWinMusic() {
|
||||||
|
if (!isPlayWinMusic.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 清理已结束的音频
|
||||||
|
playingAudios.value = playingAudios.value.filter(audio => !audio.ended && !audio.paused)
|
||||||
|
|
||||||
if (playingAudios.value.length > maxAudioLimit) {
|
if (playingAudios.value.length > maxAudioLimit) {
|
||||||
console.log('音频播放数量已达到上限,请勿重复播放')
|
console.log('音频播放数量已达到上限,请勿重复播放')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const enterNewAudio = new Audio(enterAudio)
|
const enterNewAudio = new Audio(enterAudio)
|
||||||
|
enterNewAudio.volume = 0.8
|
||||||
|
|
||||||
playingAudios.value.push(enterNewAudio)
|
playingAudios.value.push(enterNewAudio)
|
||||||
enterNewAudio.play()
|
enterNewAudio.play()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -483,6 +663,14 @@ export function useViewModel() {
|
|||||||
playingAudios.value.splice(index, 1)
|
playingAudios.value.splice(index, 1)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 播放错误时从数组中移除
|
||||||
|
enterNewAudio.onerror = () => {
|
||||||
|
const index = playingAudios.value.indexOf(enterNewAudio)
|
||||||
|
if (index > -1) {
|
||||||
|
playingAudios.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @description: 继续,意味着这抽奖作数,计入数据库
|
* @description: 继续,意味着这抽奖作数,计入数据库
|
||||||
@@ -514,6 +702,9 @@ export function useViewModel() {
|
|||||||
* @description: 放弃本次抽奖,回到初始状态
|
* @description: 放弃本次抽奖,回到初始状态
|
||||||
*/
|
*/
|
||||||
function quitLottery() {
|
function quitLottery() {
|
||||||
|
// 停止抽奖音乐
|
||||||
|
stopLotteryMusic()
|
||||||
|
|
||||||
enterLottery()
|
enterLottery()
|
||||||
currentStatus.value = LotteryStatus.init
|
currentStatus.value = LotteryStatus.init
|
||||||
}
|
}
|
||||||
@@ -543,7 +734,19 @@ export function useViewModel() {
|
|||||||
if (!objects.value[cardRandomIndexArr[i]]) {
|
if (!objects.value[cardRandomIndexArr[i]]) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
objects.value[cardRandomIndexArr[i]].element = useElementStyle(objects.value[cardRandomIndexArr[i]].element, allPersonList.value[personRandomIndexArr[i]], cardRandomIndexArr[i], patternList.value, patternColor.value, cardColor.value, { width: cardSize.value.width, height: cardSize.value.height }, textSize.value, mod, 'change')
|
objects.value[cardRandomIndexArr[i]].element = useElementStyle({
|
||||||
|
element: objects.value[cardRandomIndexArr[i]].element,
|
||||||
|
person: allPersonList.value[personRandomIndexArr[i]],
|
||||||
|
index: cardRandomIndexArr[i],
|
||||||
|
patternList: patternList.value,
|
||||||
|
patternColor: patternColor.value,
|
||||||
|
cardColor: cardColor.value,
|
||||||
|
cardSize: { width: cardSize.value.width, height: cardSize.value.height },
|
||||||
|
textSize: textSize.value,
|
||||||
|
scale: 1,
|
||||||
|
mod,
|
||||||
|
type: 'change',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}, 200)
|
}, 200)
|
||||||
}
|
}
|
||||||
@@ -590,6 +793,21 @@ export function useViewModel() {
|
|||||||
}
|
}
|
||||||
clearInterval(intervalTimer.value)
|
clearInterval(intervalTimer.value)
|
||||||
intervalTimer.value = null
|
intervalTimer.value = null
|
||||||
|
|
||||||
|
// 停止抽奖音乐
|
||||||
|
stopLotteryMusic()
|
||||||
|
|
||||||
|
// 清理所有音频资源
|
||||||
|
playingAudios.value.forEach((audio) => {
|
||||||
|
if (!audio.ended && !audio.paused) {
|
||||||
|
audio.pause()
|
||||||
|
}
|
||||||
|
// 释放音频资源
|
||||||
|
audio.src = ''
|
||||||
|
audio.load()
|
||||||
|
})
|
||||||
|
playingAudios.value = []
|
||||||
|
|
||||||
if (scene.value) {
|
if (scene.value) {
|
||||||
scene.value.traverse((object: Object3D) => {
|
scene.value.traverse((object: Object3D) => {
|
||||||
if ((object as any).material) {
|
if ((object as any).material) {
|
||||||
|
|||||||
90
src/views/Mobile/index.vue
Normal file
90
src/views/Mobile/index.vue
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<script setup lang='ts'>
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
|
import { useViewModel } from './useViewModel'
|
||||||
|
|
||||||
|
dayjs.extend(relativeTime)
|
||||||
|
dayjs.locale('zh-cn') // 设置为中文
|
||||||
|
|
||||||
|
const textareaRef = ref()
|
||||||
|
const messageArrayRef = ref()
|
||||||
|
// 存储定时器ID
|
||||||
|
const timer = ref()
|
||||||
|
// 创建一个响应式的时间戳,用于触发更新
|
||||||
|
const nowTimestamp = ref(Date.now())
|
||||||
|
const { sendMsg, userInputMsg, userMsgArray } = useViewModel()
|
||||||
|
async function handleEnterSend() {
|
||||||
|
sendMsg(userInputMsg.value)
|
||||||
|
textareaRef.value.blur()
|
||||||
|
messageArrayRef.value.scrollTop = messageArrayRef.value.scrollHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToBottom() {
|
||||||
|
if (!messageArrayRef.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
messageArrayRef.value.scrollTop = messageArrayRef.value.scrollHeight
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 带有实时更新的时间显示
|
||||||
|
const formattedMessages = computed(() => {
|
||||||
|
const _ = nowTimestamp.value
|
||||||
|
return userMsgArray.value.map(item => ({
|
||||||
|
...item,
|
||||||
|
formattedTime: dayjs(item.dateTime).fromNow(),
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
watch(() => userMsgArray.value.length, () => {
|
||||||
|
scrollToBottom()
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
timer.value = setInterval(() => {
|
||||||
|
nowTimestamp.value = Date.now()
|
||||||
|
}, 60000) // 每分钟更新一次
|
||||||
|
})
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (timer.value) {
|
||||||
|
clearInterval(timer.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col justify-around py-4">
|
||||||
|
<div class="h-12 drop-shadow-md shadow-lg">
|
||||||
|
<h2 class="text-center text-lg font-bold">
|
||||||
|
发送弹幕
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div ref="messageArrayRef" class="overflow-y-auto h-[calc(100vh-15rem)]">
|
||||||
|
<ul>
|
||||||
|
<li v-for="item in formattedMessages" :key="item.id" class="mb-3">
|
||||||
|
<div class="chat chat-end">
|
||||||
|
<div class="chat-header">
|
||||||
|
<time class="text-xs opacity-50">{{ item.formattedTime }}</time>
|
||||||
|
</div>
|
||||||
|
<div class="chat-bubble break-all whitespace-normal">
|
||||||
|
{{ item.msg }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="border-none rounded-2xl bg-base-200 mx-2 p-2 flex flex-col gap-3 items-center justify-center shadow-md mb-8 h-48">
|
||||||
|
<textarea ref="textareaRef" v-model="userInputMsg" class="textarea w-full rounded-xl border-none bg-transparent focus:outline-none focus:ring-0" placeholder="发送弹幕 | 只展示您发送过的弹幕" rows="5" cols="50" @keydown.enter.prevent="handleEnterSend" />
|
||||||
|
<div class="w-full flex justify-end">
|
||||||
|
<button class="btn btn-primary w-24 mb-4" @click="handleEnterSend">
|
||||||
|
发送
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
50
src/views/Mobile/useViewModel.ts
Normal file
50
src/views/Mobile/useViewModel.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import type { IMsgType } from '@/types/msgType'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { useToast } from 'vue-toast-notification'
|
||||||
|
import { api_sendMsg } from '@/api/msg'
|
||||||
|
import { IndexDb } from '@/utils/dexie'
|
||||||
|
|
||||||
|
export function useViewModel() {
|
||||||
|
const toast = useToast()
|
||||||
|
const route = useRoute()
|
||||||
|
const routeSignature = ref<string>('')
|
||||||
|
const userInputMsg = ref('')
|
||||||
|
const userMsgArray = ref<any[]>([])
|
||||||
|
const userMsgDb = new IndexDb('userMsg', ['userMsg'], 1, ['createTime'])
|
||||||
|
const getRouteSignature = async () => {
|
||||||
|
routeSignature.value = route.query.userSignature as string
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAllMsg = async () => {
|
||||||
|
userMsgDb.getDataSortedByDateTime('userMsg', 'dateTime').then((data) => {
|
||||||
|
userMsgArray.value = data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendMsg(msg: string) {
|
||||||
|
const msgData: IMsgType = {
|
||||||
|
data: msg,
|
||||||
|
dateTime: new Date().toLocaleString(),
|
||||||
|
}
|
||||||
|
api_sendMsg(routeSignature.value, msgData).then((res: any) => {
|
||||||
|
toast.open({
|
||||||
|
message: res.msg || '发送成功',
|
||||||
|
type: 'success',
|
||||||
|
position: 'top-right',
|
||||||
|
})
|
||||||
|
userMsgDb.setData('userMsg', { msg })
|
||||||
|
getAllMsg()
|
||||||
|
userInputMsg.value = ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
getRouteSignature()
|
||||||
|
getAllMsg()
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
sendMsg,
|
||||||
|
userInputMsg,
|
||||||
|
userMsgArray,
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/vue-use.d.ts
vendored
Normal file
3
src/vue-use.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
declare module '@vueuse/integrations/useQRCode' {
|
||||||
|
export { useQRCode } from '@vueuse/integrations/useQRCode/dist/index.d.ts'
|
||||||
|
}
|
||||||
@@ -3,22 +3,41 @@
|
|||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "Node",
|
"moduleResolution": "node",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
|
"jsxImportSource": "vue",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
// 单元测试的支持
|
// 单元测试的支持
|
||||||
"types": ["vitest/globals"],
|
"types": [
|
||||||
|
"vitest/globals"
|
||||||
|
],
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"lib": ["ESNext", "DOM"],
|
"lib": [
|
||||||
|
"ESNext",
|
||||||
|
"DOM",
|
||||||
|
"WebWorker"
|
||||||
|
],
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["src/*"]
|
"@/*": [
|
||||||
|
"src/*"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts","src/**/*.d.ts","src/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
"include": [
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
"src/**/*.ts",
|
||||||
|
"src/**/*.d.ts",
|
||||||
|
"src/*.d.ts",
|
||||||
|
"src/**/*.tsx",
|
||||||
|
"src/**/*.vue"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
@@ -5,5 +5,7 @@
|
|||||||
"moduleResolution": "Node",
|
"moduleResolution": "Node",
|
||||||
"allowSyntheticDefaultImports": true
|
"allowSyntheticDefaultImports": true
|
||||||
},
|
},
|
||||||
"include": ["vite.config.ts"]
|
"include": [
|
||||||
|
"vite.config.ts"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
@@ -100,11 +100,11 @@ export default defineConfig(({ mode }) => {
|
|||||||
},
|
},
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: env.VITE_BASE_URL,
|
target: 'http://127.0.0.1:8080',
|
||||||
// 是否跨域
|
// 是否跨域
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
// 路径重写
|
// 路径重写
|
||||||
rewrite: path => path.replace(/^\/api/, ''),
|
rewrite: path => path.replace(/^\/api/, '/api'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
1676
ws_server/Cargo.lock
generated
Normal file
1676
ws_server/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
13
ws_server/Cargo.toml
Normal file
13
ws_server/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "ws_server"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
actix-cors = "0.7.1"
|
||||||
|
actix-web = "4"
|
||||||
|
actix-ws = { version = "0.3.0" }
|
||||||
|
futures-util = "0.3.31"
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
tokio = { version = "1.49.0", features = ["rt", "time", "macros"] }
|
||||||
|
tokio-stream = { version = "^0.1.18", features = ["full"] }
|
||||||
228
ws_server/src/main.rs
Normal file
228
ws_server/src/main.rs
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
use actix_cors::Cors;
|
||||||
|
use actix_web::{
|
||||||
|
App, Error, HttpRequest, HttpResponse, HttpServer, Responder, post, rt, web, web::Payload,
|
||||||
|
};
|
||||||
|
use actix_ws::AggregatedMessage;
|
||||||
|
use futures_util::StreamExt as _;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use tokio::{
|
||||||
|
pin, select,
|
||||||
|
sync::{RwLock, broadcast},
|
||||||
|
time::interval,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 统一响应结构体
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct ApiResponse<T> {
|
||||||
|
code: i32,
|
||||||
|
success: bool,
|
||||||
|
msg: String,
|
||||||
|
data: Option<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> ApiResponse<T> {
|
||||||
|
fn success_with_data(data: T, msg: String) -> Self {
|
||||||
|
Self {
|
||||||
|
code: 200,
|
||||||
|
success: true,
|
||||||
|
msg,
|
||||||
|
data: Some(data),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn success_with_msg(msg: String) -> Self
|
||||||
|
where
|
||||||
|
T: Default, // 添加约束,确保T有默认值
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
code: 200,
|
||||||
|
success: true,
|
||||||
|
msg,
|
||||||
|
data: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加一个专门用于无数据响应的静态方法
|
||||||
|
fn success_without_data(msg: String) -> ApiResponse<()> {
|
||||||
|
ApiResponse {
|
||||||
|
code: 200,
|
||||||
|
success: true,
|
||||||
|
msg,
|
||||||
|
data: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn error(code: i32, msg: String) -> ApiResponse<()> {
|
||||||
|
ApiResponse {
|
||||||
|
code,
|
||||||
|
success: false,
|
||||||
|
msg,
|
||||||
|
data: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 定义消息类型(可以根据需求扩展为结构体)
|
||||||
|
type WsMessage = String;
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct AppState {
|
||||||
|
// 使用 HashMap 存储不同 user_signature 的广播通道
|
||||||
|
tx_map: web::Data<RwLock<HashMap<String, broadcast::Sender<WsMessage>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/user-msg")]
|
||||||
|
async fn user_msg(req: HttpRequest, req_body: String, data: web::Data<AppState>) -> impl Responder {
|
||||||
|
println!("All headers:");
|
||||||
|
// 获取usersignature参数
|
||||||
|
let target_user_signature = req
|
||||||
|
.headers()
|
||||||
|
.get("userSignature")
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.unwrap();
|
||||||
|
// 打印接收到的消息
|
||||||
|
println!("Received /user-msg: {}", req_body);
|
||||||
|
// 获取对应 user_signature 的发送端
|
||||||
|
let tx_map = data.tx_map.read().await;
|
||||||
|
if let Some(tx) = tx_map.get(target_user_signature) {
|
||||||
|
match tx.send(req_body.clone()) {
|
||||||
|
Ok(_) => HttpResponse::Ok().json(ApiResponse::<()>::success_without_data(
|
||||||
|
"发送成功".to_string(),
|
||||||
|
)),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to send message: {}", e);
|
||||||
|
HttpResponse::InternalServerError()
|
||||||
|
.json(ApiResponse::<()>::error(500, "系统服务异常".to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
HttpResponse::NotFound().json(ApiResponse::<()>::error(
|
||||||
|
404,
|
||||||
|
"后台服务未启动,请联系管理员".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn echo(
|
||||||
|
req: HttpRequest,
|
||||||
|
stream: Payload,
|
||||||
|
data: web::Data<AppState>,
|
||||||
|
) -> Result<HttpResponse, Error> {
|
||||||
|
let (res, mut session, stream) = actix_ws::handle(&req, stream)?;
|
||||||
|
println!("New WebSocket connection:{:?}", req.query_string());
|
||||||
|
let user_signature = req.query_string().split("=").nth(1).unwrap();
|
||||||
|
println!("user_signature: {}", user_signature);
|
||||||
|
|
||||||
|
// 订阅广播通道(每个连接创建独立的接收端)
|
||||||
|
// 为当前 user_signature 创建或获取广播通道
|
||||||
|
let tx = {
|
||||||
|
let mut tx_map = data.tx_map.write().await;
|
||||||
|
if !tx_map.contains_key(user_signature) {
|
||||||
|
// 为这个 user_signature 创建新的广播通道
|
||||||
|
let (new_tx, _) = broadcast::channel::<WsMessage>(1024);
|
||||||
|
tx_map.insert(user_signature.to_string(), new_tx.clone());
|
||||||
|
new_tx
|
||||||
|
} else {
|
||||||
|
tx_map.get(user_signature).unwrap().clone()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut rx = tx.subscribe();
|
||||||
|
let mut stream = stream
|
||||||
|
.aggregate_continuations()
|
||||||
|
.max_continuation_size(2_usize.pow(20));
|
||||||
|
|
||||||
|
// 启动异步任务处理 WebSocket 消息和广播消息
|
||||||
|
rt::spawn(async move {
|
||||||
|
// 同时监听:1. WebSocket 客户端消息 2. 广播通道消息
|
||||||
|
loop {
|
||||||
|
select! {
|
||||||
|
// 监听来自客户端的 WebSocket 消息(原 echo 逻辑)
|
||||||
|
msg = stream.next() => {
|
||||||
|
match msg {
|
||||||
|
Some(Ok(AggregatedMessage::Text(text))) => {
|
||||||
|
println!("WebSocket client sent: {}", text);
|
||||||
|
// 回声(可选:保留原 echo 功能)
|
||||||
|
// 如果是ping消息则不回声
|
||||||
|
if text != "ping" {
|
||||||
|
// 回声
|
||||||
|
if let Err(e) = session.text(text.clone()).await {
|
||||||
|
eprintln!("Failed to send text: {}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Ok(AggregatedMessage::Binary(bin))) => {
|
||||||
|
println!("WebSocket client sent binary: {:?}", bin);
|
||||||
|
if let Err(e) = session.binary(bin).await {
|
||||||
|
eprintln!("Failed to send binary: {}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Ok(AggregatedMessage::Ping(msg))) => {
|
||||||
|
println!("WebSocket client sent ping: {:?}", msg);
|
||||||
|
// if let Err(e) = session.pong(&msg).await {
|
||||||
|
// eprintln!("Failed to send pong: {}", e);
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
// 客户端断开连接或出错,退出循环
|
||||||
|
None | Some(Err(_)) => {
|
||||||
|
println!("WebSocket connection closed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 监听广播通道的消息(来自 /user-msg)
|
||||||
|
msg = rx.recv() => {
|
||||||
|
match msg {
|
||||||
|
Ok(text) => {
|
||||||
|
// 将广播消息发送给 WebSocket 客户端
|
||||||
|
if let Err(e) = session.text(text).await {
|
||||||
|
eprintln!("Failed to send broadcast msg: {}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(broadcast::error::RecvError::Closed) => {
|
||||||
|
eprintln!("Broadcast channel closed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(broadcast::error::RecvError::Lagged(_)) => {
|
||||||
|
eprintln!("Broadcast message lagged, missed some messages");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭 WebSocket 连接
|
||||||
|
let _ = session.close(None).await;
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
let tx_map = web::Data::new(RwLock::new(HashMap::new()));
|
||||||
|
let app_state = AppState { tx_map };
|
||||||
|
|
||||||
|
HttpServer::new(move || {
|
||||||
|
App::new()
|
||||||
|
// 注入应用状态(广播通道发送端)
|
||||||
|
.app_data(web::Data::new(app_state.clone()))
|
||||||
|
// 跨域配置(生产环境需限制 origin)
|
||||||
|
.wrap(
|
||||||
|
Cors::default()
|
||||||
|
.allow_any_origin()
|
||||||
|
.allow_any_method()
|
||||||
|
.allow_any_header()
|
||||||
|
.supports_credentials(),
|
||||||
|
)
|
||||||
|
.service(web::scope("/api").service(user_msg))
|
||||||
|
.route("/echo", web::get().to(echo))
|
||||||
|
})
|
||||||
|
.bind(("127.0.0.1", 8080))?
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user