96 UI optimization (#136)
* fix(home): 🐛 解决多次切换路由后页面卡顿的问题 #96 卸载路由时清除requestAnimationFrame * feat: ✨ 文件存储使用Blob格式 * style: 💄 修改部分类型any为具体类型 * feat: ✨ 界面设置中模块使用瀑布流布局 #96 * fix: 🐛 md文档更换文件夹解决控制台警告 * style: 💄 switch按钮改回使用daisyui组件 * refactor: ♻️ 所有人员列表提取tableColumn * style: 💄 奖项列表中的图片类型修复 * fix(globalConfig): 修复当前音乐项类型缺失问题 * feat: ✨ single person not done * feat: ✨ 可添加单人 #96 * build(.gitignore): 添加 auto-imports.d.ts 到忽略文件 * fix: 🐛 上传、下载excel文件时修复路径错误 打包成应用和网页端的baseUrl不一样,使用环境变量来表示 * fix: 🐛 导入人员列表时处理有值为空的情况 * style: 💄 改变toaster的组件 * fix: 🐛 上传文件、解析数据与存储/读取数据的处理 、 * fix(Config): 更新备案信息链接样式 将备案信息的段落标签替换为可点击的链接标签,使用户能够直接跳转到工信部备案查询页面。同时添加了悬停效果样式,提升用户体验。 * feat: ✨ 首页奖项列表样式修改 not done #96 * chore(deps): ✏️ 更新依赖版本 * chore: ✏️ gsap list demo * build: 🏗️ docker构建优化 * chore: ✏️ gsap scroll demo * style: 💄 gsap demno * feat: ✨ demo smooth scroll gsap scrolltrigger * feat(Demo): 添加更多颜色选项并注释GSAP动画 * refactor(PrizeList): 重构奖品列表组件结构 * feat(PrizeList): 重构奖品列表组件并添加滚动动画 * feat: ✨ 增加定时抽取功能 #96 * feat: ✨ 添加定时抽取功能的说明 * feat: ✨ 优化gsap #96 项数不多时不触发gsap * style: 💄 文本修改 * feat: ✨ 优化 * feat: ✨ 优化奖项列表 * fix(Home): 修复奖品列表滚动检测逻辑 * fix(home): 修复抽奖停止逻辑避免重复执行;调整卡片垂直居中位置计算 * feat: ✨ 播放中奖音频 #96
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
# 使用官方的 Node 镜像作为基础镜像
|
# 使用更小的 Node 镜像作为构建基础镜像
|
||||||
FROM node:20.12.2
|
FROM node:22-alpine as builder
|
||||||
|
|
||||||
# 设置工作目录
|
# 设置工作目录
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|||||||
30
README.md
30
README.md
@@ -134,17 +134,31 @@ npm run build
|
|||||||
|
|
||||||
## Docker支持
|
## Docker支持
|
||||||
|
|
||||||
构建镜像
|
以下任意方式选一种即可
|
||||||
|
|
||||||
```bash
|
1. 拉取镜像
|
||||||
docker build -t log-lottery .
|
|
||||||
```
|
|
||||||
|
|
||||||
运行容器
|
```bash
|
||||||
|
docker pull log1997/log-lottery:latest
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
运行容器
|
||||||
docker run -d -p 9279:80 log-lottery
|
|
||||||
```
|
```bash
|
||||||
|
docker run -d --name log-lottery -p 9279:80 log1997/log-lottery:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 手动构建镜像
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t log-lottery .
|
||||||
|
```
|
||||||
|
|
||||||
|
运行容器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d -p 9279:80 log-lottery
|
||||||
|
```
|
||||||
|
|
||||||
容器运行成功后即可在本地通过<http://localhost:9279/log-lottery/>访问
|
容器运行成功后即可在本地通过<http://localhost:9279/log-lottery/>访问
|
||||||
|
|
||||||
|
|||||||
96
package.json
96
package.json
@@ -20,92 +20,94 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tweenjs/tween.js": "23.1.2",
|
"@tweenjs/tween.js": "23.1.2",
|
||||||
"@vueuse/core": "^14.1.0",
|
"@vueuse/core": "^14.1.0",
|
||||||
"axios": "^1.7.8",
|
"axios": "^1.13.2",
|
||||||
"canvas-confetti": "^1.9.3",
|
"canvas-confetti": "^1.9.4",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.19",
|
||||||
"dexie": "^4.2.1",
|
"dexie": "^4.2.1",
|
||||||
"github-markdown-css": "^5.8.0",
|
"github-markdown-css": "^5.8.1",
|
||||||
|
"gsap": "^3.14.2",
|
||||||
"localforage": "^1.10.0",
|
"localforage": "^1.10.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.22",
|
||||||
"lucide-vue-next": "^0.559.0",
|
"lucide-vue-next": "^0.562.0",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"masonry-layout": "^4.2.2",
|
"masonry-layout": "^4.2.2",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.4",
|
||||||
"pinia-plugin-persist": "^1.0.0",
|
"pinia-plugin-persist": "^1.0.0",
|
||||||
"reka-ui": "^2.6.1",
|
"reka-ui": "^2.7.0",
|
||||||
"sparticles": "^1.3.1",
|
"sparticles": "^1.3.1",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"three": "0.166.0",
|
"three": "0.166.0",
|
||||||
"three-css3d": "1.0.6",
|
"three-css3d": "1.0.6",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.26",
|
||||||
"vue-dompurify-html": "^5.2.0",
|
"vue-dompurify-html": "^5.3.0",
|
||||||
"vue-draggable-plus": "^0.6.0",
|
"vue-draggable-plus": "^0.6.0",
|
||||||
"vue-i18n": "^11.2.2",
|
"vue-i18n": "^11.2.7",
|
||||||
"vue-router": "^4.5.0",
|
"vue-router": "^4.6.4",
|
||||||
"vue-sonner": "^2.0.9",
|
"vue-sonner": "^2.0.9",
|
||||||
"vue-toast-notification": "^3",
|
"vue-toast-notification": "^3",
|
||||||
"vue3-colorpicker": "^2.3.0",
|
"vue3-colorpicker": "^2.3.0",
|
||||||
"xlsx": "^0.18.5",
|
"xlsx": "^0.18.5",
|
||||||
"zod": "^4.1.11"
|
"zod": "^4.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@antfu/eslint-config": "^6.4.1",
|
"@antfu/eslint-config": "^6.7.3",
|
||||||
"@eslint/eslintrc": "^3.2.0",
|
"@eslint/eslintrc": "^3.3.3",
|
||||||
"@eslint/js": "^9.15.0",
|
"@eslint/js": "^9.39.2",
|
||||||
"@iconify-json/ep": "^1.2.1",
|
"@iconify-json/ep": "^1.2.3",
|
||||||
"@iconify-json/fluent": "^1.2.8",
|
"@iconify-json/fluent": "^1.2.36",
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@tailwindcss/vite": "^4.1.13",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@tauri-apps/cli": "^2.9.5",
|
"@tauri-apps/cli": "^2.9.6",
|
||||||
"@testing-library/vue": "^8.1.0",
|
"@testing-library/vue": "^8.1.0",
|
||||||
"@types/canvas-confetti": "^1.6.4",
|
"@types/canvas-confetti": "^1.9.0",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/markdown-it": "^14.1.2",
|
"@types/markdown-it": "^14.1.2",
|
||||||
"@types/masonry-layout": "^4.2.8",
|
"@types/masonry-layout": "^4.2.8",
|
||||||
"@types/node": "^25.0.0",
|
"@types/node": "^25.0.3",
|
||||||
"@types/three": "0.166.0",
|
"@types/three": "0.166.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.49.0",
|
"@typescript-eslint/eslint-plugin": "^8.50.1",
|
||||||
"@typescript-eslint/parser": "^8.49.0",
|
"@typescript-eslint/parser": "^8.50.1",
|
||||||
"@vitejs/plugin-legacy": "^7.2.1",
|
"@vitejs/plugin-legacy": "^7.2.1",
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.3",
|
||||||
"@vitest/ui": "^4.0.15",
|
"@vitest/ui": "^4.0.16",
|
||||||
"@vue/test-utils": "^2.4.6",
|
"@vue/test-utils": "^2.4.6",
|
||||||
"@vue/tsconfig": "^0.8.1",
|
"@vue/tsconfig": "^0.8.1",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.23",
|
||||||
"baseline-browser-mapping": "^2.8.32",
|
"baseline-browser-mapping": "^2.9.11",
|
||||||
"child_process": "^1.0.2",
|
"child_process": "^1.0.2",
|
||||||
"daisyui": "^5.1.13",
|
"daisyui": "^5.5.14",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.39.2",
|
||||||
"eslint-plugin-vue": "^10.4.0",
|
"eslint-plugin-vue": "^10.6.2",
|
||||||
"globals": "^16.4.0",
|
"fast-glob": "^3.3.3",
|
||||||
|
"globals": "^16.5.0",
|
||||||
"happy-dom": "^20.0.11",
|
"happy-dom": "^20.0.11",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"jsdom": "^27.0.0",
|
"jsdom": "^27.3.0",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"postcss": "^8.4.49",
|
"postcss": "^8.5.6",
|
||||||
"rollup-plugin-visualizer": "^6.0.3",
|
"rollup-plugin-visualizer": "^6.0.5",
|
||||||
"sass": "^1.81.0",
|
"sass": "^1.97.1",
|
||||||
"sass-loader": "^16.0.3",
|
"sass-loader": "^16.0.6",
|
||||||
"tailwindcss": "^4.1.17",
|
"tailwindcss": "^4.1.18",
|
||||||
"terser": "^5.36.0",
|
"terser": "^5.44.1",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"unplugin-auto-import": "^20.1.0",
|
"unplugin-auto-import": "^20.3.0",
|
||||||
"unplugin-icons": "^22.3.0",
|
"unplugin-icons": "^22.5.0",
|
||||||
"unplugin-vue-components": "^30.0.0",
|
"unplugin-vue-components": "^30.0.0",
|
||||||
"vite": "^7.1.6",
|
"vite": "^7.3.0",
|
||||||
"vite-plugin-compression": "^0.5.1",
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vite-plugin-inspect": "^11.3.3",
|
"vite-plugin-inspect": "^11.3.3",
|
||||||
"vite-plugin-svg-icons": "^2.0.1",
|
"vite-plugin-svg-icons": "^2.0.1",
|
||||||
"vite-plugin-vue-devtools": "^8.0.2",
|
"vite-plugin-vue-devtools": "^8.0.5",
|
||||||
"vitest": "^4.0.15",
|
"vitest": "^4.0.16",
|
||||||
"vue-tsc": "^3.0.7"
|
"vue-tsc": "^3.2.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.x"
|
"node": ">=22.x"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.8.1+sha1.a4eff733d0c4ccc179997f0ef4986f6e92427781"
|
"packageManager": "pnpm@10.26.1"
|
||||||
}
|
}
|
||||||
6925
pnpm-lock.yaml
generated
6925
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
BIN
src/assets/audio/enter.wav
Normal file
BIN
src/assets/audio/enter.wav
Normal file
Binary file not shown.
@@ -36,8 +36,8 @@ const symbolId = computed(() => `#${props.prefix}-${props.name}`)
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.svg-icon {
|
.svg-icon {
|
||||||
width: 24px;
|
/* width: 24px;
|
||||||
height: 24px;
|
height: 24px; */
|
||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export function useElementPosition(element: any, count: number, totalCount: numb
|
|||||||
let yTable = 0
|
let yTable = 0
|
||||||
const centerPosition = {
|
const centerPosition = {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: windowSize.height / 2 - cardSize.height / 2,
|
y: windowSize.height / 2 - cardSize.height * 0.9,
|
||||||
}
|
}
|
||||||
// 有一行为偶数的特殊数量
|
// 有一行为偶数的特殊数量
|
||||||
const specialPosition = [2, 4, 7, 9]
|
const specialPosition = [2, 4, 7, 9]
|
||||||
|
|||||||
59
src/icons/chevron-down.svg
Normal file
59
src/icons/chevron-down.svg
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="lucide lucide-chevron-down-icon lucide-chevron-down"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="chevron-down.svg"
|
||||||
|
inkscape:export-filename="..\Users\log\Users\log\Users\log\Users\log\Users\log\Users\log\Users\log\Users\log\Desktop\chevron-down.svg"
|
||||||
|
inkscape:export-xdpi="96"
|
||||||
|
inkscape:export-ydpi="96"
|
||||||
|
inkscape:version="1.4 (86a8ad7, 2024-10-11)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showguides="true"
|
||||||
|
inkscape:zoom="48.625"
|
||||||
|
inkscape:cx="12.010283"
|
||||||
|
inkscape:cy="12"
|
||||||
|
inkscape:window-width="2560"
|
||||||
|
inkscape:window-height="1369"
|
||||||
|
inkscape:window-x="1592"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg1">
|
||||||
|
<sodipodi:guide
|
||||||
|
position="-5.5115681,17.439589"
|
||||||
|
orientation="1,0"
|
||||||
|
id="guide1"
|
||||||
|
inkscape:locked="false" />
|
||||||
|
<sodipodi:guide
|
||||||
|
position="6.5398458,15.979434"
|
||||||
|
orientation="0,-1"
|
||||||
|
id="guide2"
|
||||||
|
inkscape:locked="false" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<path
|
||||||
|
d="M 0.98200514,9 12,15 23.03856,9.0616967"
|
||||||
|
id="path1"
|
||||||
|
sodipodi:nodetypes="ccc" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
54
src/icons/chevron-up.svg
Normal file
54
src/icons/chevron-up.svg
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="lucide lucide-chevron-up-icon lucide-chevron-up"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="chevron-up.svg"
|
||||||
|
inkscape:export-filename="..\Users\log\Users\log\Desktop\chevron-up.svg"
|
||||||
|
inkscape:export-xdpi="96"
|
||||||
|
inkscape:export-ydpi="96"
|
||||||
|
inkscape:version="1.4 (86a8ad7, 2024-10-11)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showguides="true"
|
||||||
|
inkscape:zoom="48.625"
|
||||||
|
inkscape:cx="12.010283"
|
||||||
|
inkscape:cy="12"
|
||||||
|
inkscape:window-width="2560"
|
||||||
|
inkscape:window-height="1369"
|
||||||
|
inkscape:window-x="1592"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg1">
|
||||||
|
<sodipodi:guide
|
||||||
|
position="10.550129,8"
|
||||||
|
orientation="0,-1"
|
||||||
|
id="guide1"
|
||||||
|
inkscape:locked="false" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<path
|
||||||
|
d="M 22.997429,14.958869 12,9 0.96143959,14.958869"
|
||||||
|
id="path1"
|
||||||
|
sodipodi:nodetypes="ccc" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -12,6 +12,8 @@ export const useGlobalConfig = defineStore('global', {
|
|||||||
isShowAvatar: false,
|
isShowAvatar: false,
|
||||||
topTitle: i18n.global.t('data.defaultTitle'),
|
topTitle: i18n.global.t('data.defaultTitle'),
|
||||||
language: browserLanguage,
|
language: browserLanguage,
|
||||||
|
definiteTime: null as number | null,
|
||||||
|
winMusic: false,
|
||||||
theme: {
|
theme: {
|
||||||
name: 'dracula',
|
name: 'dracula',
|
||||||
detail: { primary: '#0f5fd3' },
|
detail: { primary: '#0f5fd3' },
|
||||||
@@ -125,6 +127,14 @@ export const useGlobalConfig = defineStore('global', {
|
|||||||
getIsShowAvatar(state) {
|
getIsShowAvatar(state) {
|
||||||
return state.globalConfig.isShowAvatar
|
return state.globalConfig.isShowAvatar
|
||||||
},
|
},
|
||||||
|
// 获取定时抽取时间
|
||||||
|
getDefiniteTime(state) {
|
||||||
|
return state.globalConfig.definiteTime
|
||||||
|
},
|
||||||
|
// 是否播放获奖音乐
|
||||||
|
getWinMusic(state) {
|
||||||
|
return state.globalConfig.winMusic
|
||||||
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
// 设置全局配置
|
// 设置全局配置
|
||||||
@@ -266,14 +276,24 @@ export const useGlobalConfig = defineStore('global', {
|
|||||||
setIsShowAvatar(isShowAvatar: boolean) {
|
setIsShowAvatar(isShowAvatar: boolean) {
|
||||||
this.globalConfig.isShowAvatar = isShowAvatar
|
this.globalConfig.isShowAvatar = isShowAvatar
|
||||||
},
|
},
|
||||||
|
// 设置定时抽取时间
|
||||||
|
setDefiniteTime(definiteTime: number | null) {
|
||||||
|
this.globalConfig.definiteTime = definiteTime
|
||||||
|
},
|
||||||
|
// 设置是否播放获奖音乐
|
||||||
|
setIsPlayWinMusic(winMusic: boolean) {
|
||||||
|
this.globalConfig.winMusic = winMusic
|
||||||
|
},
|
||||||
// 重置所有配置
|
// 重置所有配置
|
||||||
reset() {
|
reset() {
|
||||||
this.globalConfig = {
|
this.globalConfig = {
|
||||||
rowCount: 17,
|
rowCount: 17,
|
||||||
|
winMusic: false,
|
||||||
isSHowPrizeList: true,
|
isSHowPrizeList: true,
|
||||||
isShowAvatar: false,
|
isShowAvatar: false,
|
||||||
topTitle: i18n.global.t('data.defaultTitle'),
|
topTitle: i18n.global.t('data.defaultTitle'),
|
||||||
language: browserLanguage,
|
language: browserLanguage,
|
||||||
|
definiteTime: null,
|
||||||
theme: {
|
theme: {
|
||||||
name: 'dracula',
|
name: 'dracula',
|
||||||
detail: { primary: '#0f5fd3' },
|
detail: { primary: '#0f5fd3' },
|
||||||
|
|||||||
@@ -11,6 +11,22 @@
|
|||||||
max-width: calc(var(--container-xs) - 90px);
|
max-width: calc(var(--container-xs) - 90px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@utility hide-scrollbar {
|
||||||
|
/* 隐藏 IE/旧 Edge 滚动条 */
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
/* 隐藏 Firefox 滚动条 */
|
||||||
|
scrollbar-width: none;
|
||||||
|
|
||||||
|
/* 隐藏 WebKit 内核(Chrome/Edge/Safari)滚动条 */
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
/* 兜底,确保完全隐藏 */
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/* @plugin "@tailwindcss/typography" */
|
/* @plugin "@tailwindcss/typography" */
|
||||||
|
|
||||||
body,
|
body,
|
||||||
|
|||||||
@@ -1,11 +1,44 @@
|
|||||||
<script setup lang='ts'>
|
<script setup lang='ts'>
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import GridWaterfall from '@/components/Waterfall/index.vue'
|
import GridWaterfall from '@/components/Waterfall/index.vue'
|
||||||
import { DataSetting, LayoutSetting, PatternSetting, TextSetting, ThemeSetting } from './parts'
|
import { AbilitySetting, DataSetting, LayoutSetting, PatternSetting, TextSetting, ThemeSetting } from './parts'
|
||||||
import { useViewModel } from './useViewModel'
|
import { useViewModel } from './useViewModel'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const { resetData, topTitleValue, languageValue, textSizeValue, currentFontValue, currentTitleFontValue, titleFontSyncGlobalValue, languageList, formErr, formData, cardSizeValue, isShowPrizeListValue, isShowAvatarValue, resetPersonLayout, isRowCountChange, themeValue, backgroundImageValue, cardColorValue, luckyCardColorValue, textColorValue, patternColorValue, imageList, rowCount, cardColor, patternColor, patternList, clearPattern, resetPattern, exportAllConfigData, importAllConfigData } = useViewModel()
|
const {
|
||||||
|
resetData,
|
||||||
|
topTitleValue,
|
||||||
|
languageValue,
|
||||||
|
textSizeValue,
|
||||||
|
currentFontValue,
|
||||||
|
currentTitleFontValue,
|
||||||
|
titleFontSyncGlobalValue,
|
||||||
|
languageList,
|
||||||
|
formErr,
|
||||||
|
formData,
|
||||||
|
cardSizeValue,
|
||||||
|
isShowPrizeListValue,
|
||||||
|
isShowAvatarValue,
|
||||||
|
resetPersonLayout,
|
||||||
|
isRowCountChange,
|
||||||
|
themeValue,
|
||||||
|
backgroundImageValue,
|
||||||
|
cardColorValue,
|
||||||
|
luckyCardColorValue,
|
||||||
|
textColorValue,
|
||||||
|
patternColorValue,
|
||||||
|
imageList,
|
||||||
|
rowCount,
|
||||||
|
cardColor,
|
||||||
|
patternColor,
|
||||||
|
patternList,
|
||||||
|
clearPattern,
|
||||||
|
resetPattern,
|
||||||
|
exportAllConfigData,
|
||||||
|
importAllConfigData,
|
||||||
|
definiteTimeValue,
|
||||||
|
isWinMusicValue,
|
||||||
|
} = useViewModel()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -54,6 +87,8 @@ const { resetData, topTitleValue, languageValue, textSizeValue, currentFontValue
|
|||||||
:clear-pattern="clearPattern"
|
:clear-pattern="clearPattern"
|
||||||
:reset-pattern="resetPattern"
|
:reset-pattern="resetPattern"
|
||||||
/>
|
/>
|
||||||
|
<!-- 功能设置 -->
|
||||||
|
<AbilitySetting v-model:definite-time="definiteTimeValue" v-model:win-music="isWinMusicValue" />
|
||||||
</GridWaterfall>
|
</GridWaterfall>
|
||||||
<!-- </div> -->
|
<!-- </div> -->
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
44
src/views/Config/Global/FaceConfig/parts/AbilitySetting.vue
Normal file
44
src/views/Config/Global/FaceConfig/parts/AbilitySetting.vue
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<script setup lang='ts'>
|
||||||
|
const definiteTime = defineModel<number | null>('definiteTime', { required: true })
|
||||||
|
const winMusic = defineModel<boolean>('winMusic', { required: true })
|
||||||
|
</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="开始抽奖过后定时停止,默认为0,单位为秒,0为关闭定时停止功能">
|
||||||
|
<button class="btn btn-circle h-4 hover:bg-base-300">
|
||||||
|
?
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="definiteTime" type="number" placeholder="开始后定时抽取"
|
||||||
|
class="w-full max-w-xs input input-bordered"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<div class="flex items-center justify-between w-full max-w-xs gap-2 mb-3 form-control">
|
||||||
|
<div class="label">
|
||||||
|
<span class="label-text">播放获奖音乐</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="checkbox" :checked="winMusic" class="border-solid checkbox checkbox-secondary border"
|
||||||
|
@change="winMusic = !winMusic"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -38,7 +38,7 @@ const isShowAvatarValue = defineModel<boolean>('isShowAvatarValue', { required:
|
|||||||
class="w-full input input-bordered join-item"
|
class="w-full input input-bordered join-item"
|
||||||
>
|
>
|
||||||
<div class="tooltip join-item" :data-tip="t('tooltip.resetLayout')">
|
<div class="tooltip join-item" :data-tip="t('tooltip.resetLayout')">
|
||||||
<button class="btn btn-neutral w-[120px] join-item" :disabled="isRowCountChange !== 1" @click="resetPersonLayout">
|
<button class="btn btn-neutral w-30 join-item" :disabled="isRowCountChange !== 1" @click="resetPersonLayout">
|
||||||
<span>{{ t('button.setLayout') }}</span>
|
<span>{{ t('button.setLayout') }}</span>
|
||||||
<span v-show="isRowCountChange === 2" class="loading loading-ring loading-md" />
|
<span v-show="isRowCountChange === 2" class="loading loading-ring loading-md" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export { default as AbilitySetting } from './AbilitySetting.vue'
|
||||||
export { default as DataSetting } from './DataSetting.vue'
|
export { default as DataSetting } from './DataSetting.vue'
|
||||||
export { default as LayoutSetting } from './LayoutSetting.vue'
|
export { default as LayoutSetting } from './LayoutSetting.vue'
|
||||||
export { default as PatternSetting } from './PatternSetting.vue'
|
export { default as PatternSetting } from './PatternSetting.vue'
|
||||||
|
|||||||
@@ -11,7 +11,28 @@ export function useViewModel() {
|
|||||||
const globalConfig = useStore().globalConfig
|
const globalConfig = useStore().globalConfig
|
||||||
const personConfig = useStore().personConfig
|
const personConfig = useStore().personConfig
|
||||||
const prizeConfig = useStore().prizeConfig
|
const prizeConfig = useStore().prizeConfig
|
||||||
const { getGlobalConfig: globalConfigData, getTopTitle: topTitle, getTheme: localTheme, getPatterColor: patternColor, getPatternList: patternList, getCardColor: cardColor, getLuckyColor: luckyCardColor, getTextColor: textColor, getCardSize: cardSize, getTextSize: textSize, getRowCount: rowCount, getIsShowPrizeList: isShowPrizeList, getLanguage: userLanguage, getBackground: backgroundImage, getFont: currentFont, getTitleFont: currentTitleFont, getTitleFontSyncGlobal: titleFontSyncGlobal, getImageList: imageList, getIsShowAvatar: isShowAvatar,
|
const {
|
||||||
|
getGlobalConfig: globalConfigData,
|
||||||
|
getTopTitle: topTitle,
|
||||||
|
getTheme: localTheme,
|
||||||
|
getPatterColor: patternColor,
|
||||||
|
getPatternList: patternList,
|
||||||
|
getCardColor: cardColor,
|
||||||
|
getLuckyColor: luckyCardColor,
|
||||||
|
getTextColor: textColor,
|
||||||
|
getCardSize: cardSize,
|
||||||
|
getTextSize: textSize,
|
||||||
|
getRowCount: rowCount,
|
||||||
|
getIsShowPrizeList: isShowPrizeList,
|
||||||
|
getLanguage: userLanguage,
|
||||||
|
getBackground: backgroundImage,
|
||||||
|
getFont: currentFont,
|
||||||
|
getTitleFont: currentTitleFont,
|
||||||
|
getTitleFontSyncGlobal: titleFontSyncGlobal,
|
||||||
|
getImageList: imageList,
|
||||||
|
getIsShowAvatar: isShowAvatar,
|
||||||
|
getDefiniteTime: definiteTime,
|
||||||
|
getWinMusic: isWinMusic,
|
||||||
} = storeToRefs(globalConfig)
|
} = storeToRefs(globalConfig)
|
||||||
const { getAlreadyPersonList: alreadyPersonList, getNotPersonList: notPersonList } = storeToRefs(personConfig)
|
const { getAlreadyPersonList: alreadyPersonList, getNotPersonList: notPersonList } = storeToRefs(personConfig)
|
||||||
|
|
||||||
@@ -32,6 +53,8 @@ export function useViewModel() {
|
|||||||
const currentFontValue = ref(structuredClone(currentFont.value))
|
const currentFontValue = ref(structuredClone(currentFont.value))
|
||||||
const currentTitleFontValue = ref(structuredClone(currentTitleFont.value))
|
const currentTitleFontValue = ref(structuredClone(currentTitleFont.value))
|
||||||
const titleFontSyncGlobalValue = ref(structuredClone(titleFontSyncGlobal.value))
|
const titleFontSyncGlobalValue = ref(structuredClone(titleFontSyncGlobal.value))
|
||||||
|
const definiteTimeValue = ref(structuredClone(definiteTime.value))
|
||||||
|
const isWinMusicValue = ref(structuredClone(isWinMusic.value))
|
||||||
const formData = ref({
|
const formData = ref({
|
||||||
rowCount: rowCountValue,
|
rowCount: rowCountValue,
|
||||||
})
|
})
|
||||||
@@ -170,6 +193,12 @@ export function useViewModel() {
|
|||||||
watch(isShowAvatarValue, () => {
|
watch(isShowAvatarValue, () => {
|
||||||
globalConfig.setIsShowAvatar(isShowAvatarValue.value)
|
globalConfig.setIsShowAvatar(isShowAvatarValue.value)
|
||||||
})
|
})
|
||||||
|
watch(definiteTimeValue, () => {
|
||||||
|
globalConfig.setDefiniteTime(definiteTimeValue.value)
|
||||||
|
})
|
||||||
|
watch(isWinMusicValue, () => {
|
||||||
|
globalConfig.setIsPlayWinMusic(isWinMusicValue.value)
|
||||||
|
})
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
@@ -203,5 +232,7 @@ export function useViewModel() {
|
|||||||
resetPattern,
|
resetPattern,
|
||||||
exportAllConfigData,
|
exportAllConfigData,
|
||||||
importAllConfigData,
|
importAllConfigData,
|
||||||
|
definiteTimeValue,
|
||||||
|
isWinMusicValue,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,241 @@
|
|||||||
<script setup lang='ts'>
|
<script setup lang="ts">
|
||||||
import WaterFall from '@/components/Waterfall/index.vue'
|
import gsap from 'gsap'
|
||||||
|
import { ScrollTrigger } from 'gsap/ScrollTrigger'
|
||||||
|
import { onBeforeUnmount, onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
|
||||||
|
gsap.registerPlugin(ScrollTrigger)
|
||||||
|
|
||||||
|
const list = ref<any[]>([])
|
||||||
|
|
||||||
|
list.value = [{
|
||||||
|
label: 1,
|
||||||
|
value: 1,
|
||||||
|
color: 'red',
|
||||||
|
}, {
|
||||||
|
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() {
|
||||||
|
ctx.value.revert() // <- Easy Cleanup!
|
||||||
|
}
|
||||||
|
function scrollHandler() {
|
||||||
|
const scrollHeight = scrollContainerRef.value.scrollHeight
|
||||||
|
const scrollTop = scrollContainerRef.value.scrollTop
|
||||||
|
const containerHeight = scrollContainerRef.value.clientHeight
|
||||||
|
// 滚动滑到底部
|
||||||
|
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(() => {
|
||||||
|
initGsapAnimation()
|
||||||
|
listenScrollContainer()
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
removeScrollContainer()
|
||||||
|
})
|
||||||
|
onUnmounted(() => {
|
||||||
|
disposeGsapAnimation()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<WaterFall>
|
<div class="h-full w-48 flex flex-col justify-center overflow-hidden relative">
|
||||||
<div class="bg-pink-500 h-20 w-48" />
|
<div class="w-full h-16 flex justify-center scroll-button scroll-button-up">
|
||||||
<div class="bg-blue-500 h-30 w-48" />
|
<SvgIcon v-show="showUpButton" name="chevron-up" size="64px" class="text-gray-200/80 cursor-pointer" @click="handleScroll(-100)" />
|
||||||
<div class="bg-yellow-500 h-40 w-48" />
|
</div>
|
||||||
<div class="bg-green-500 h-20 w-48" />
|
<div ref="scrollContainerRef" class="h-150 w-48 overflow-y-auto overflow-x-hidden relative scroll-smooth hide-scrollbar">
|
||||||
<div class="bg-red-500 h-30 w-48" />
|
<ul class="li-container relative bg-slate-500/50">
|
||||||
<div class="bg-purple-500 h-50 w-48" />
|
<li
|
||||||
</WaterFall>
|
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>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style lang="scss" scoped>
|
||||||
|
.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>
|
</style>
|
||||||
|
|||||||
@@ -18,48 +18,7 @@
|
|||||||
-webkit-animation: show-operate 0.6s;
|
-webkit-animation: show-operate 0.6s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.current-prize {
|
|
||||||
position: relative;
|
|
||||||
display: block;
|
|
||||||
overflow: hidden;
|
|
||||||
isolation: isolate;
|
|
||||||
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-prize::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 400%;
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(115deg, #4fcf70, #fad648, #a767e5, #12bcfe, #44ce7b);
|
|
||||||
background-size: 25% 100%;
|
|
||||||
animation: an-at-keyframe-css-at-rule-that-translates-via-the-transform-property-the-background-by-negative-25-percent-of-its-width-so-that-it-gives-a-nice-border-animation_-We-use-the-translate-property-to-have-a-nice-transition-so-it_s-not-a-jerk-of-a-start-or-stop .75s linear infinite;
|
|
||||||
// animation-play-state: paused;
|
|
||||||
translate: -5% 0%;
|
|
||||||
transition: translate 0.25s ease-out;
|
|
||||||
animation-play-state: running;
|
|
||||||
transition-duration: 0.75s;
|
|
||||||
translate: 0% 0%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-prize::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
inset: 4px;
|
|
||||||
border-top-left-radius: 20px;
|
|
||||||
border-bottom-right-radius: 20px;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes an-at-keyframe-css-at-rule-that-translates-via-the-transform-property-the-background-by-negative-25-percent-of-its-width-so-that-it-gives-a-nice-border-animation_-We-use-the-translate-property-to-have-a-nice-transition-so-it_s-not-a-jerk-of-a-start-or-stop {
|
|
||||||
to {
|
|
||||||
transform: translateX(-25%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes slide-right {
|
@-webkit-keyframes slide-right {
|
||||||
0% {
|
0% {
|
||||||
|
|||||||
@@ -1,319 +1,65 @@
|
|||||||
<script setup lang='ts'>
|
<script setup lang='ts'>
|
||||||
import type { IPrizeConfig } from '@/types/storeType'
|
import type { IPrizeConfig } from '@/types/storeType'
|
||||||
import { storeToRefs } from 'pinia'
|
import { ref } from 'vue'
|
||||||
import { onMounted, ref } from 'vue'
|
// import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import defaultPrizeImage from '@/assets/images/龙.png'
|
|
||||||
import ImageSync from '@/components/ImageSync/index.vue'
|
|
||||||
|
|
||||||
import EditSeparateDialog from '@/components/NumberSeparate/EditSeparateDialog.vue'
|
import EditSeparateDialog from '@/components/NumberSeparate/EditSeparateDialog.vue'
|
||||||
|
import OfficialPrizeList from './parts/OfficialPrizeList/index.vue'
|
||||||
import i18n from '@/locales/i18n'
|
import OperationButton from './parts/OperationButton.vue'
|
||||||
import useStore from '@/store'
|
import TemporaryDialog from './parts/TemporaryDialog.vue'
|
||||||
|
import TemporaryList from './parts/TemporaryList.vue'
|
||||||
const { t } = useI18n()
|
import { usePrizeList } from './usePrizeList'
|
||||||
const prizeConfig = useStore().prizeConfig
|
|
||||||
const globalConfig = useStore().globalConfig
|
|
||||||
const system = useStore().system
|
|
||||||
const { getPrizeConfig: localPrizeList, getCurrentPrize: currentPrize, getTemporaryPrize: temporaryPrize } = storeToRefs(prizeConfig)
|
|
||||||
const { getIsShowPrizeList: isShowPrizeList, getImageList: localImageList } = storeToRefs(globalConfig)
|
|
||||||
const { getIsMobile: isMobile } = storeToRefs(system)
|
|
||||||
const prizeListRef = ref()
|
|
||||||
const prizeListContainerRef = ref()
|
|
||||||
|
|
||||||
const temporaryPrizeRef = ref()
|
const temporaryPrizeRef = ref()
|
||||||
|
const {
|
||||||
|
temporaryPrize,
|
||||||
|
changePersonCount,
|
||||||
|
selectPrize,
|
||||||
|
localImageList,
|
||||||
|
addTemporaryPrize,
|
||||||
|
submitTemporaryPrize,
|
||||||
|
submitData,
|
||||||
|
deleteTemporaryPrize,
|
||||||
|
prizeShow,
|
||||||
|
currentPrize,
|
||||||
|
localPrizeList,
|
||||||
|
isMobile,
|
||||||
|
} = usePrizeList(temporaryPrizeRef)
|
||||||
const selectedPrize = ref<IPrizeConfig | null>()
|
const selectedPrize = ref<IPrizeConfig | null>()
|
||||||
// 获取prizeListRef高度
|
|
||||||
function getPrizeListHeight() {
|
|
||||||
let height = 200
|
|
||||||
if (prizeListRef.value) {
|
|
||||||
height = (prizeListRef.value as HTMLElement).offsetHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
return height
|
|
||||||
}
|
|
||||||
const prizeShow = ref(structuredClone(isShowPrizeList.value))
|
|
||||||
|
|
||||||
function addTemporaryPrize() {
|
|
||||||
temporaryPrizeRef.value.showModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteTemporaryPrize() {
|
|
||||||
temporaryPrize.value.isShow = false
|
|
||||||
prizeConfig.setTemporaryPrize(temporaryPrize.value)
|
|
||||||
}
|
|
||||||
function submitTemporaryPrize() {
|
|
||||||
if (!temporaryPrize.value.name || !temporaryPrize.value.count) {
|
|
||||||
// eslint-disable-next-line no-alert
|
|
||||||
alert(i18n.global.t('error.completeInformation'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
temporaryPrize.value.isShow = true
|
|
||||||
temporaryPrize.value.id = new Date().getTime().toString()
|
|
||||||
prizeConfig.setCurrentPrize(temporaryPrize.value)
|
|
||||||
}
|
|
||||||
function selectPrize(item: IPrizeConfig) {
|
|
||||||
selectedPrize.value = item
|
|
||||||
selectedPrize.value.isUsedCount = 0
|
|
||||||
selectedPrize.value.isUsed = false
|
|
||||||
|
|
||||||
if (selectedPrize.value.separateCount.countList.length > 1) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
selectedPrize.value.separateCount = {
|
|
||||||
enable: true,
|
|
||||||
countList: [
|
|
||||||
{
|
|
||||||
id: '0',
|
|
||||||
count: item.count,
|
|
||||||
isUsedCount: 0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function submitData(value: any) {
|
|
||||||
selectedPrize.value!.separateCount.countList = value
|
|
||||||
selectedPrize.value = null
|
|
||||||
}
|
|
||||||
function changePersonCount() {
|
|
||||||
temporaryPrize.value.separateCount.countList = []
|
|
||||||
}
|
|
||||||
function setCurrentPrize() {
|
|
||||||
for (let i = 0; i < localPrizeList.value.length; i++) {
|
|
||||||
if (localPrizeList.value[i].isUsedCount < localPrizeList.value[i].count) {
|
|
||||||
prizeConfig.setCurrentPrize(localPrizeList.value[i])
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onMounted(() => {
|
|
||||||
prizeListContainerRef.value.style.height = `${getPrizeListHeight()}px`
|
|
||||||
setCurrentPrize()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center">
|
<div v-if="localPrizeList.length" class="flex h-2/3 items-center overflow-hidden">
|
||||||
<dialog id="my_modal_1" ref="temporaryPrizeRef" class="border-none modal">
|
<TemporaryDialog
|
||||||
<div class="modal-box">
|
ref="temporaryPrizeRef"
|
||||||
<h3 class="text-lg font-bold">
|
v-model:temporary-prize="temporaryPrize"
|
||||||
{{ t('dialog.titleTemporary') }}
|
:change-person-count="changePersonCount"
|
||||||
</h3>
|
:select-prize="selectPrize"
|
||||||
<div class="flex flex-col gap-3">
|
:local-image-list="localImageList"
|
||||||
<label class="flex w-full max-w-xs">
|
:add-temporary-prize="addTemporaryPrize"
|
||||||
<div class="label">
|
:submit-temporary-prize="submitTemporaryPrize"
|
||||||
<span class="label-text">{{ t('table.name') }}:</span>
|
/>
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
v-model="temporaryPrize.name" type="text" :placeholder="t('placeHolder.name')"
|
|
||||||
class="max-w-xs input-sm input input-bordered"
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<label class="flex w-full max-w-xs">
|
|
||||||
<div class="label">
|
|
||||||
<span class="label-text">{{ t('table.fullParticipation') }}</span>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="checkbox" :checked="temporaryPrize.isAll"
|
|
||||||
class="mt-2 border-solid checkbox checkbox-secondary border"
|
|
||||||
@change="temporaryPrize.isAll = !temporaryPrize.isAll"
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<label class="flex w-full max-w-xs">
|
|
||||||
<div class="label">
|
|
||||||
<span class="label-text">{{ t('table.setLuckyNumber') }}</span>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
v-model="temporaryPrize.count" type="number" :placeholder="t('placeHolder.winnerCount')" class="max-w-xs input-sm input input-bordered"
|
|
||||||
@change="changePersonCount"
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<label class="flex w-full max-w-xs">
|
|
||||||
<div class="label">
|
|
||||||
<span class="label-text">{{ t('table.luckyPeopleNumber') }}</span>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
v-model="temporaryPrize.isUsedCount" disabled type="number" :placeholder="t('placeHolder.winnerCount')"
|
|
||||||
class="max-w-xs input-sm input input-bordered"
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<label v-if="temporaryPrize.separateCount" class="flex w-full max-w-xs">
|
|
||||||
<div class="label">
|
|
||||||
<span class="label-text">{{ t('table.onceNumber') }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-start h-full" @click="selectPrize(temporaryPrize)">
|
|
||||||
<ul
|
|
||||||
v-if="temporaryPrize.separateCount.countList.length"
|
|
||||||
class="flex flex-wrap w-full h-full gap-1 p-0 pt-1 m-0 cursor-pointer"
|
|
||||||
>
|
|
||||||
<li
|
|
||||||
v-for="se in temporaryPrize.separateCount.countList"
|
|
||||||
:key="se.id" class="relative flex items-center justify-center w-8 h-8 bg-slate-600/60 separated"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="flex items-center justify-center w-full h-full tooltip"
|
|
||||||
:data-tip="`${t('tooltip.doneCount') + se.isUsedCount}/${se.count}`"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="absolute left-0 z-50 h-full bg-blue-300/80"
|
|
||||||
:style="`width:${se.isUsedCount * 100 / se.count}%`"
|
|
||||||
/>
|
|
||||||
<span>{{ se.count }}</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<button v-else class="btn btn-secondary btn-xs">{{ t('button.setting') }}</button>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<label class="flex w-full max-w-xs">
|
|
||||||
<div class="label">
|
|
||||||
<span class="label-text">{{ t('table.image') }}</span>
|
|
||||||
</div>
|
|
||||||
<select v-model="temporaryPrize.picture" class="flex-1 w-12 select select-warning select-sm">
|
|
||||||
<option v-if="temporaryPrize.picture.id" :value="{ id: '', name: '', url: '' }">❌
|
|
||||||
</option>
|
|
||||||
<option disabled selected>{{ t('table.selectPicture') }}</option>
|
|
||||||
<option v-for="picItem in localImageList" :key="picItem.id" class="w-auto" :value="picItem">{{
|
|
||||||
picItem.name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="modal-action">
|
|
||||||
<form method="dialog" class="flex gap-3">
|
|
||||||
<button class="btn btn-sm" @click="submitTemporaryPrize">
|
|
||||||
{{ t('button.confirm') }}
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-sm">
|
|
||||||
{{ t('button.cancel') }}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</dialog>
|
|
||||||
<EditSeparateDialog
|
<EditSeparateDialog
|
||||||
:total-number="selectedPrize?.count" :separated-number="selectedPrize?.separateCount.countList"
|
:total-number="selectedPrize?.count" :separated-number="selectedPrize?.separateCount.countList"
|
||||||
@submit-data="submitData"
|
@submit-data="submitData"
|
||||||
/>
|
/>
|
||||||
<div ref="prizeListContainerRef">
|
<div class="h-full">
|
||||||
<div v-if="temporaryPrize.isShow" class="h-20 w-72" :class="temporaryPrize.isShow ? 'current-prize' : ''">
|
<TemporaryList
|
||||||
<div class="relative flex flex-row items-center justify-between w-full h-full shadow-xl card bg-base-100">
|
v-if="temporaryPrize.isShow"
|
||||||
<div
|
:temporary-prize="temporaryPrize"
|
||||||
v-if="temporaryPrize.isUsed"
|
:add-temporary-prize="addTemporaryPrize"
|
||||||
class="absolute z-50 w-full h-full bg-gray-800/70 item-mask rounded-xl"
|
:delete-temporary-prize="deleteTemporaryPrize"
|
||||||
/>
|
/>
|
||||||
<figure class="w-10 h-10 rounded-xl">
|
<OfficialPrizeList
|
||||||
<ImageSync v-if="temporaryPrize.picture.url" :img-item="temporaryPrize.picture" />
|
v-show="!temporaryPrize.isShow"
|
||||||
<img v-else :src="defaultPrizeImage" alt="Prize" class="object-cover h-full rounded-xl">
|
v-model:prize-show="prizeShow"
|
||||||
</figure>
|
:temporary-prize-show="temporaryPrize.isShow"
|
||||||
<div class="items-center p-0 text-center card-body">
|
:local-prize-list="localPrizeList"
|
||||||
<div class="tooltip tooltip-left" :data-tip="temporaryPrize.name">
|
:current-prize="currentPrize"
|
||||||
<h2 class="p-0 m-0 overflow-hidden w-28 card-title whitespace-nowrap text-ellipsis">
|
:is-mobile="isMobile"
|
||||||
{{
|
:add-temporary-prize="addTemporaryPrize"
|
||||||
temporaryPrize.name }}
|
/>
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<p class="absolute z-40 p-0 m-0 text-gray-300/80 mt-9">
|
|
||||||
{{ temporaryPrize.isUsedCount }}/{{
|
|
||||||
temporaryPrize.count }}
|
|
||||||
</p>
|
|
||||||
<progress
|
|
||||||
class="w-3/4 h-6 progress progress-primary" :value="temporaryPrize.isUsedCount"
|
|
||||||
:max="temporaryPrize.count"
|
|
||||||
/>
|
|
||||||
<!-- <p class="p-0 m-0">{{ item.isUsedCount }}/{{ item.count }}</p> -->
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-1 mr-2">
|
|
||||||
<div class="tooltip tooltip-left" :data-tip="t('tooltip.edit')">
|
|
||||||
<div class="cursor-pointer hover:text-blue-400" @click="addTemporaryPrize">
|
|
||||||
<svg-icon name="edit" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="tooltip tooltip-left" :data-tip="t('tooltip.delete')">
|
|
||||||
<div class="cursor-pointer hover:text-blue-400" @click="deleteTemporaryPrize">
|
|
||||||
<svg-icon name="delete" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<transition name="prize-list" :appear="true">
|
|
||||||
<div v-if="prizeShow && !isMobile && !temporaryPrize.isShow" class="flex items-center">
|
|
||||||
<ul ref="prizeListRef" class="flex flex-col gap-1 p-2 rounded-xl bg-slate-500/50">
|
|
||||||
<li
|
|
||||||
v-for="item in localPrizeList" :key="item.id"
|
|
||||||
:class="currentPrize.id === item.id ? 'current-prize' : ''"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="item.isShow"
|
|
||||||
class="relative flex flex-row items-center justify-between w-64 h-20 px-3 gap-6 shadow-xl card bg-base-100"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="item.isUsed"
|
|
||||||
class="absolute z-50 w-full h-full bg-gray-800/70 item-mask rounded-xl"
|
|
||||||
/>
|
|
||||||
<figure class="w-10 h-10 rounded-xl">
|
|
||||||
<ImageSync v-if="item.picture.url" :img-item="item.picture" />
|
|
||||||
<img
|
|
||||||
v-else :src="defaultPrizeImage" alt="Prize"
|
|
||||||
class="object-cover h-full rounded-xl"
|
|
||||||
>
|
|
||||||
</figure>
|
|
||||||
<div class="items-center p-0 card-body">
|
|
||||||
<div class="tooltip tooltip-left w-full pl-1" :data-tip="item.name">
|
|
||||||
<h2
|
|
||||||
class="w-24 p-0 m-0 overflow-hidden card-title whitespace-nowrap text-ellipsis"
|
|
||||||
>
|
|
||||||
{{ item.name }}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<p class="absolute z-40 p-0 m-0 text-gray-300/80 mt-9">
|
|
||||||
{{ item.isUsedCount }}/{{
|
|
||||||
item.count }}
|
|
||||||
</p>
|
|
||||||
<progress
|
|
||||||
class="w-full h-6 progress bg-[#52545b] progress-primary" :value="item.isUsedCount"
|
|
||||||
:max="item.count"
|
|
||||||
/>
|
|
||||||
<!-- <p class="p-0 m-0">{{ item.isUsedCount }}/{{ item.count }}</p> -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<div class="flex flex-col gap-3">
|
|
||||||
<div class="tooltip tooltip-right" :data-tip="t('tooltip.prizeList')">
|
|
||||||
<div
|
|
||||||
class="flex items-center w-6 h-8 rounded-r-lg cursor-pointer prize-option bg-slate-500/50"
|
|
||||||
@click="prizeShow = !prizeShow"
|
|
||||||
>
|
|
||||||
<svg-icon name="arrow_left" class="w-full h-full" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="tooltip tooltip-right" :data-tip="t('tooltip.addActivity')">
|
|
||||||
<div
|
|
||||||
class="flex items-center w-6 h-8 rounded-r-lg cursor-pointer prize-option bg-slate-500/50"
|
|
||||||
@click="addTemporaryPrize"
|
|
||||||
>
|
|
||||||
<svg-icon name="add" class="w-full h-full" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</div>
|
</div>
|
||||||
|
<OperationButton v-if="!temporaryPrize.isShow" v-model:prize-show="prizeShow" :add-temporary-prize="addTemporaryPrize" />
|
||||||
<transition name="prize-operate" :appear="true">
|
|
||||||
<div v-show="!prizeShow" class="tooltip tooltip-right" :data-tip="t('tooltip.prizeList')">
|
|
||||||
<div
|
|
||||||
class="flex items-center w-6 h-8 rounded-r-lg cursor-pointer prize-option bg-slate-500/50"
|
|
||||||
@click="prizeShow = !prizeShow"
|
|
||||||
>
|
|
||||||
<svg-icon name="arrow_right" class="w-full h-full" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-container {
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-container-end {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
height: 90%;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-scroll {}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-prize {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
overflow: clip;
|
||||||
|
isolation: isolate;
|
||||||
|
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-prize::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 400%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(115deg, #4fcf70, #fad648, #a767e5, #12bcfe, #44ce7b);
|
||||||
|
background-size: 25% 100%;
|
||||||
|
animation: an-at-keyframe-css-at-rule-that-translates-via-the-transform-property-the-background-by-negative-25-percent-of-its-width-so-that-it-gives-a-nice-border-animation_-We-use-the-translate-property-to-have-a-nice-transition-so-it_s-not-a-jerk-of-a-start-or-stop .75s linear infinite;
|
||||||
|
// animation-play-state: paused;
|
||||||
|
translate: -5% 0%;
|
||||||
|
transition: translate 0.25s ease-out;
|
||||||
|
animation-play-state: running;
|
||||||
|
transition-duration: 0.75s;
|
||||||
|
translate: 0% 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-prize::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 4px;
|
||||||
|
border-top-left-radius: 20px;
|
||||||
|
border-bottom-right-radius: 20px;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes an-at-keyframe-css-at-rule-that-translates-via-the-transform-property-the-background-by-negative-25-percent-of-its-width-so-that-it-gives-a-nice-border-animation_-We-use-the-translate-property-to-have-a-nice-transition-so-it_s-not-a-jerk-of-a-start-or-stop {
|
||||||
|
to {
|
||||||
|
transform: translateX(-25%);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
<script setup lang='ts'>
|
||||||
|
import type { IPrizeConfig } from '@/types/storeType'
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import defaultPrizeImage from '@/assets/images/龙.png'
|
||||||
|
import { useGsap } from './useGsap'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
isMobile: boolean
|
||||||
|
localPrizeList: IPrizeConfig[]
|
||||||
|
currentPrize: IPrizeConfig
|
||||||
|
temporaryPrizeShow: boolean
|
||||||
|
addTemporaryPrize: () => void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const prizeShow = defineModel<boolean>('prizeShow')
|
||||||
|
const scrollContainerRef = ref<any>(null)
|
||||||
|
const ulContainerRef = ref<any>(null)
|
||||||
|
const isScroll = ref(false)
|
||||||
|
const liRefs = ref([])
|
||||||
|
|
||||||
|
const {
|
||||||
|
showUpButton,
|
||||||
|
showDownButton,
|
||||||
|
handleScroll,
|
||||||
|
} = useGsap(scrollContainerRef, liRefs, isScroll, prizeShow, props.temporaryPrizeShow)
|
||||||
|
|
||||||
|
// 获取ulContainerRef的高度
|
||||||
|
function getUlContainerHeight() {
|
||||||
|
if (ulContainerRef.value) {
|
||||||
|
return ulContainerRef.value.offsetHeight
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
// 获取scrollContainerRef的高度
|
||||||
|
function getScrollContainerHeight() {
|
||||||
|
if (scrollContainerRef.value) {
|
||||||
|
return scrollContainerRef.value.offsetHeight
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIsScroll() {
|
||||||
|
const ulHeight = getUlContainerHeight()
|
||||||
|
const scrollHeight = getScrollContainerHeight()
|
||||||
|
if (ulHeight > scrollHeight + 20) {
|
||||||
|
isScroll.value = true
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
isScroll.value = false
|
||||||
|
scrollContainerRef.value.style.height = `${ulHeight}px`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch ([prizeShow, () => props.temporaryPrizeShow], (val) => {
|
||||||
|
if (!val[0]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setTimeout (() => {
|
||||||
|
getIsScroll()
|
||||||
|
}, 0)
|
||||||
|
}, { immediate: true })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<transition name="prize-list" class="h-full" :appear="true">
|
||||||
|
<div v-show="prizeShow && !isMobile && !temporaryPrizeShow" class="flex items-center h-full relative ">
|
||||||
|
<div v-if="isScroll" class="w-full h-16 flex justify-center scroll-button scroll-button-up absolute top-0 z-50">
|
||||||
|
<SvgIcon v-show="showUpButton" name="chevron-up" size="64px" class="text-gray-200/80 cursor-pointer" @click="handleScroll(-150)" />
|
||||||
|
</div>
|
||||||
|
<div ref="scrollContainerRef" :class="isScroll ? (showDownButton ? 'scroll-container' : 'scroll-container-end') : 'no-scroll bg-slate-500/50'" class="h-full no-before overflow-y-auto overflow-x-hidden scroll-smooth hide-scrollbar before:bg-slate-500/50 z-20 rounded-xl">
|
||||||
|
<ul ref="ulContainerRef" class="flex flex-col gap-1 p-2">
|
||||||
|
<li
|
||||||
|
v-for="item in localPrizeList"
|
||||||
|
ref="liRefs" :key="item.id"
|
||||||
|
:class="currentPrize.id === item.id ? 'current-prize' : ''"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="item.isShow"
|
||||||
|
class="relative flex flex-row items-center justify-between w-64 h-20 px-3 gap-6 shadow-xl card bg-base-100"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="item.isUsed"
|
||||||
|
class="absolute z-50 w-full left-0 h-full bg-gray-800/70 item-mask rounded-xl"
|
||||||
|
/>
|
||||||
|
<figure class="w-10 h-10 rounded-xl">
|
||||||
|
<ImageSync v-if="item.picture.url" :img-item="item.picture" />
|
||||||
|
<img
|
||||||
|
v-else :src="defaultPrizeImage" alt="Prize"
|
||||||
|
class="object-cover h-full rounded-xl"
|
||||||
|
>
|
||||||
|
</figure>
|
||||||
|
<div class="items-center p-0 card-body">
|
||||||
|
<div class="tooltip tooltip-left w-full pl-1" :data-tip="item.name">
|
||||||
|
<h2
|
||||||
|
class="w-24 p-0 m-0 overflow-hidden card-title whitespace-nowrap text-ellipsis"
|
||||||
|
>
|
||||||
|
{{ item.name }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<p class="absolute z-40 p-0 m-0 text-gray-300/80 mt-9">
|
||||||
|
{{ item.isUsedCount }}/{{
|
||||||
|
item.count }}
|
||||||
|
</p>
|
||||||
|
<progress
|
||||||
|
class="w-full h-6 progress bg-[#52545b] progress-primary" :value="item.isUsedCount"
|
||||||
|
:max="item.count"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div v-if="isScroll" class="h-24" />
|
||||||
|
</div>
|
||||||
|
<div v-if="isScroll" class="w-full h-16 flex justify-center scroll-button scroll-button-down absolute bottom-0 z-50">
|
||||||
|
<SvgIcon v-show="showDownButton" name="chevron-down" size="64px" class="text-gray-200/80 cursor-pointer" @click="handleScroll(150)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@use "./index.scss";
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import type { Ref } from 'vue'
|
||||||
|
import gsap from 'gsap'
|
||||||
|
import { ScrollTrigger } from 'gsap/ScrollTrigger'
|
||||||
|
import { onBeforeUnmount, onUnmounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
export function useGsap(scrollContainerRef: any, liRefs: any, isScroll: Ref<boolean>, prizeShow: any, temporaryPrizeShow: boolean) {
|
||||||
|
gsap.registerPlugin(ScrollTrigger)
|
||||||
|
|
||||||
|
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() {
|
||||||
|
if (!ctx.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.value.revert() // <- Easy Cleanup!
|
||||||
|
}
|
||||||
|
function scrollHandler() {
|
||||||
|
const scrollHeight = scrollContainerRef.value.scrollHeight
|
||||||
|
const scrollTop = scrollContainerRef.value.scrollTop
|
||||||
|
const containerHeight = scrollContainerRef.value.clientHeight
|
||||||
|
// 滚动滑到底部
|
||||||
|
if (scrollTop + containerHeight >= scrollHeight - 10) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
watch([isScroll, prizeShow, temporaryPrizeShow], ([val1, val2, val3]) => {
|
||||||
|
if (val1 && val2 && !val3) {
|
||||||
|
setTimeout(() => {
|
||||||
|
initGsapAnimation()
|
||||||
|
listenScrollContainer()
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (!isScroll.value)
|
||||||
|
return
|
||||||
|
removeScrollContainer()
|
||||||
|
})
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (!isScroll.value)
|
||||||
|
return
|
||||||
|
disposeGsapAnimation()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
showUpButton,
|
||||||
|
showDownButton,
|
||||||
|
handleScroll,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<script setup lang='ts'>
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
addTemporaryPrize: () => void
|
||||||
|
}>()
|
||||||
|
const { t } = useI18n()
|
||||||
|
const prizeShow = defineModel('prizeShow', {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<transition name="prize-operate" :appear="true">
|
||||||
|
<div>
|
||||||
|
<div v-show="prizeShow" class="tooltip tooltip-right flex flex-col gap-3" :data-tip="t('tooltip.prizeList')">
|
||||||
|
<div class="tooltip tooltip-right" :data-tip="t('tooltip.prizeList')">
|
||||||
|
<div
|
||||||
|
class="flex items-center w-6 h-8 rounded-r-lg cursor-pointer prize-option bg-slate-500/50"
|
||||||
|
@click="prizeShow = !prizeShow"
|
||||||
|
>
|
||||||
|
<svg-icon name="arrow_left" class="w-full h-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tooltip tooltip-right" :data-tip="t('tooltip.addActivity')">
|
||||||
|
<div
|
||||||
|
class="flex items-center w-6 h-8 rounded-r-lg cursor-pointer prize-option bg-slate-500/50"
|
||||||
|
@click="addTemporaryPrize"
|
||||||
|
>
|
||||||
|
<svg-icon name="add" class="w-full h-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-show="!prizeShow" class="tooltip tooltip-right" :data-tip="t('tooltip.prizeList')">
|
||||||
|
<div
|
||||||
|
class="flex items-center w-6 h-8 rounded-r-lg cursor-pointer prize-option bg-slate-500/50"
|
||||||
|
@click="prizeShow = !prizeShow"
|
||||||
|
>
|
||||||
|
<svg-icon name="arrow_right" class="w-full h-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
127
src/views/Home/components/PrizeList/parts/TemporaryDialog.vue
Normal file
127
src/views/Home/components/PrizeList/parts/TemporaryDialog.vue
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
<script setup lang='ts'>
|
||||||
|
import type { IImage, IPrizeConfig } from '@/types/storeType'
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
changePersonCount: () => void
|
||||||
|
selectPrize: (prize: IPrizeConfig) => void
|
||||||
|
localImageList: IImage[]
|
||||||
|
submitTemporaryPrize: () => void
|
||||||
|
addTemporaryPrize: () => void
|
||||||
|
}>()
|
||||||
|
const { t } = useI18n()
|
||||||
|
const dialogRef = ref<HTMLDialogElement | null>(null)
|
||||||
|
const temporaryPrize = defineModel<IPrizeConfig>('temporaryPrize', { required: true })
|
||||||
|
function showDialog() {
|
||||||
|
dialogRef.value?.showModal()
|
||||||
|
}
|
||||||
|
defineExpose({
|
||||||
|
showDialog,
|
||||||
|
closed,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<dialog id="my_modal_1" ref="dialogRef" class="border-none modal">
|
||||||
|
<div class="modal-box">
|
||||||
|
<h3 class="text-lg font-bold">
|
||||||
|
{{ t('dialog.titleTemporary') }}
|
||||||
|
</h3>
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<label class="flex w-full max-w-xs">
|
||||||
|
<div class="label">
|
||||||
|
<span class="label-text">{{ t('table.name') }}:</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-model="temporaryPrize.name" type="text" :placeholder="t('placeHolder.name')"
|
||||||
|
class="max-w-xs input-sm input input-bordered"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<label class="flex w-full max-w-xs">
|
||||||
|
<div class="label">
|
||||||
|
<span class="label-text">{{ t('table.fullParticipation') }}</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="checkbox" :checked="temporaryPrize.isAll"
|
||||||
|
class="mt-2 border-solid checkbox checkbox-secondary border"
|
||||||
|
@change="temporaryPrize.isAll = !temporaryPrize.isAll"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<label class="flex w-full max-w-xs">
|
||||||
|
<div class="label">
|
||||||
|
<span class="label-text">{{ t('table.setLuckyNumber') }}</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-model="temporaryPrize.count" type="number" :placeholder="t('placeHolder.winnerCount')" class="max-w-xs input-sm input input-bordered"
|
||||||
|
@change="changePersonCount"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<label class="flex w-full max-w-xs">
|
||||||
|
<div class="label">
|
||||||
|
<span class="label-text">{{ t('table.luckyPeopleNumber') }}</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-model="temporaryPrize.isUsedCount" disabled type="number" :placeholder="t('placeHolder.winnerCount')"
|
||||||
|
class="max-w-xs input-sm input input-bordered"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<label v-if="temporaryPrize.separateCount" class="flex w-full max-w-xs">
|
||||||
|
<div class="label">
|
||||||
|
<span class="label-text">{{ t('table.onceNumber') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-start h-full" @click="selectPrize(temporaryPrize)">
|
||||||
|
<ul
|
||||||
|
v-if="temporaryPrize.separateCount.countList.length"
|
||||||
|
class="flex flex-wrap w-full h-full gap-1 p-0 pt-1 m-0 cursor-pointer"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-for="se in temporaryPrize.separateCount.countList"
|
||||||
|
:key="se.id" class="relative flex items-center justify-center w-8 h-8 bg-slate-600/60 separated"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center w-full h-full tooltip"
|
||||||
|
:data-tip="`${t('tooltip.doneCount') + se.isUsedCount}/${se.count}`"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="absolute left-0 z-50 h-full bg-blue-300/80"
|
||||||
|
:style="`width:${se.isUsedCount * 100 / se.count}%`"
|
||||||
|
/>
|
||||||
|
<span>{{ se.count }}</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<button v-else class="btn btn-secondary btn-xs">{{ t('button.setting') }}</button>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label class="flex w-full max-w-xs">
|
||||||
|
<div class="label">
|
||||||
|
<span class="label-text">{{ t('table.image') }}</span>
|
||||||
|
</div>
|
||||||
|
<select v-model="temporaryPrize.picture" class="flex-1 w-12 select select-warning select-sm">
|
||||||
|
<option v-if="temporaryPrize.picture.id" :value="{ id: '', name: '', url: '' }">❌
|
||||||
|
</option>
|
||||||
|
<option disabled selected>{{ t('table.selectPicture') }}</option>
|
||||||
|
<option v-for="picItem in localImageList" :key="picItem.id" class="w-auto" :value="picItem">{{
|
||||||
|
picItem.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="modal-action">
|
||||||
|
<form method="dialog" class="flex gap-3">
|
||||||
|
<button class="btn btn-sm" @click="submitTemporaryPrize">
|
||||||
|
{{ t('button.confirm') }}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm">
|
||||||
|
{{ t('button.cancel') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
60
src/views/Home/components/PrizeList/parts/TemporaryList.vue
Normal file
60
src/views/Home/components/PrizeList/parts/TemporaryList.vue
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<script setup lang='ts'>
|
||||||
|
import type { IPrizeConfig } from '@/types/storeType'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import defaultPrizeImage from '@/assets/images/龙.png'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
temporaryPrize: IPrizeConfig
|
||||||
|
addTemporaryPrize: () => void
|
||||||
|
deleteTemporaryPrize: () => void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="h-20 w-72" :class="temporaryPrize.isShow ? 'current-prize' : ''">
|
||||||
|
<div class="relative flex flex-row items-center justify-between w-full h-full shadow-xl card bg-base-100">
|
||||||
|
<div
|
||||||
|
v-if="temporaryPrize.isUsed"
|
||||||
|
class="absolute z-50 w-full h-full bg-gray-800/70 item-mask rounded-xl"
|
||||||
|
/>
|
||||||
|
<figure class="w-10 h-10 rounded-xl">
|
||||||
|
<ImageSync v-if="temporaryPrize.picture.url" :img-item="temporaryPrize.picture" />
|
||||||
|
<img v-else :src="defaultPrizeImage" alt="Prize" class="object-cover h-full rounded-xl">
|
||||||
|
</figure>
|
||||||
|
<div class="items-center p-0 text-center card-body">
|
||||||
|
<div class="tooltip tooltip-left" :data-tip="temporaryPrize.name">
|
||||||
|
<h2 class="p-0 m-0 overflow-hidden w-28 card-title whitespace-nowrap text-ellipsis">
|
||||||
|
{{ temporaryPrize.name }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<p class="absolute z-40 p-0 m-0 text-gray-300/80 mt-9">
|
||||||
|
{{ temporaryPrize.isUsedCount }}/{{ temporaryPrize.count }}
|
||||||
|
</p>
|
||||||
|
<progress
|
||||||
|
class="w-3/4 h-6 progress progress-primary" :value="temporaryPrize.isUsedCount"
|
||||||
|
:max="temporaryPrize.count"
|
||||||
|
/>
|
||||||
|
<!-- <p class="p-0 m-0">{{ item.isUsedCount }}/{{ item.count }}</p> -->
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-1 mr-2">
|
||||||
|
<div class="tooltip tooltip-left" :data-tip="t('tooltip.edit')">
|
||||||
|
<div class="cursor-pointer hover:text-blue-400" @click="addTemporaryPrize">
|
||||||
|
<svg-icon name="edit" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tooltip tooltip-left" :data-tip="t('tooltip.delete')">
|
||||||
|
<div class="cursor-pointer hover:text-blue-400" @click="deleteTemporaryPrize">
|
||||||
|
<svg-icon name="delete" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
98
src/views/Home/components/PrizeList/usePrizeList.ts
Normal file
98
src/views/Home/components/PrizeList/usePrizeList.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import type { IPrizeConfig } from '@/types/storeType'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import i18n from '@/locales/i18n'
|
||||||
|
|
||||||
|
import useStore from '@/store'
|
||||||
|
|
||||||
|
export function usePrizeList(temporaryPrizeRef: any) {
|
||||||
|
const prizeConfig = useStore().prizeConfig
|
||||||
|
const globalConfig = useStore().globalConfig
|
||||||
|
const system = useStore().system
|
||||||
|
const {
|
||||||
|
getPrizeConfig: localPrizeList,
|
||||||
|
getCurrentPrize: currentPrize,
|
||||||
|
getTemporaryPrize: temporaryPrize,
|
||||||
|
} = storeToRefs(prizeConfig)
|
||||||
|
const {
|
||||||
|
getIsShowPrizeList: isShowPrizeList,
|
||||||
|
getImageList: localImageList,
|
||||||
|
}
|
||||||
|
= storeToRefs(globalConfig)
|
||||||
|
const { getIsMobile: isMobile } = storeToRefs(system)
|
||||||
|
|
||||||
|
const selectedPrize = ref<IPrizeConfig | null>()
|
||||||
|
const prizeShow = ref(structuredClone(isShowPrizeList.value))
|
||||||
|
|
||||||
|
function addTemporaryPrize() {
|
||||||
|
temporaryPrizeRef.value.showDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteTemporaryPrize() {
|
||||||
|
temporaryPrize.value.isShow = false
|
||||||
|
prizeConfig.setTemporaryPrize(temporaryPrize.value)
|
||||||
|
}
|
||||||
|
function submitTemporaryPrize() {
|
||||||
|
if (!temporaryPrize.value.name || !temporaryPrize.value.count) {
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
alert(i18n.global.t('error.completeInformation'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
temporaryPrize.value.isShow = true
|
||||||
|
temporaryPrize.value.id = new Date().getTime().toString()
|
||||||
|
prizeConfig.setCurrentPrize(temporaryPrize.value)
|
||||||
|
}
|
||||||
|
function selectPrize(item: IPrizeConfig) {
|
||||||
|
selectedPrize.value = item
|
||||||
|
selectedPrize.value.isUsedCount = 0
|
||||||
|
selectedPrize.value.isUsed = false
|
||||||
|
|
||||||
|
if (selectedPrize.value.separateCount.countList.length > 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
selectedPrize.value.separateCount = {
|
||||||
|
enable: true,
|
||||||
|
countList: [
|
||||||
|
{
|
||||||
|
id: '0',
|
||||||
|
count: item.count,
|
||||||
|
isUsedCount: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function submitData(value: any) {
|
||||||
|
selectedPrize.value!.separateCount.countList = value
|
||||||
|
selectedPrize.value = null
|
||||||
|
}
|
||||||
|
function changePersonCount() {
|
||||||
|
temporaryPrize.value.separateCount.countList = []
|
||||||
|
}
|
||||||
|
function setCurrentPrize() {
|
||||||
|
for (let i = 0; i < localPrizeList.value.length; i++) {
|
||||||
|
if (localPrizeList.value[i].isUsedCount < localPrizeList.value[i].count) {
|
||||||
|
prizeConfig.setCurrentPrize(localPrizeList.value[i])
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
setCurrentPrize()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
temporaryPrize,
|
||||||
|
changePersonCount,
|
||||||
|
selectPrize,
|
||||||
|
currentPrize,
|
||||||
|
localImageList,
|
||||||
|
addTemporaryPrize,
|
||||||
|
submitTemporaryPrize,
|
||||||
|
submitData,
|
||||||
|
deleteTemporaryPrize,
|
||||||
|
prizeShow,
|
||||||
|
localPrizeList,
|
||||||
|
isMobile,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ 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 enterAudio from '@/assets/audio/enter.wav'
|
||||||
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'
|
||||||
@@ -16,6 +17,7 @@ import { rgba } from '@/utils/color'
|
|||||||
import { LotteryStatus } from './type'
|
import { LotteryStatus } from './type'
|
||||||
import { confettiFire, createSphereVertices, createTableVertices, getRandomElements, initTableData } from './utils'
|
import { confettiFire, createSphereVertices, createTableVertices, getRandomElements, initTableData } from './utils'
|
||||||
|
|
||||||
|
const maxAudioLimit = 10
|
||||||
export function useViewModel() {
|
export function useViewModel() {
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
// store里面存储的值
|
// store里面存储的值
|
||||||
@@ -26,7 +28,21 @@ export function useViewModel() {
|
|||||||
getNotThisPrizePersonList: notThisPrizePersonList,
|
getNotThisPrizePersonList: notThisPrizePersonList,
|
||||||
} = storeToRefs(personConfig)
|
} = storeToRefs(personConfig)
|
||||||
const { getCurrentPrize: currentPrize } = storeToRefs(prizeConfig)
|
const { getCurrentPrize: currentPrize } = storeToRefs(prizeConfig)
|
||||||
const { getCardColor: cardColor, getPatterColor: patternColor, getPatternList: patternList, getTextColor: textColor, getLuckyColor: luckyColor, getCardSize: cardSize, getTextSize: textSize, getRowCount: rowCount, getIsShowAvatar: isShowAvatar, getTitleFont: titleFont, getTitleFontSyncGlobal: titleFontSyncGlobal } = storeToRefs(globalConfig)
|
const {
|
||||||
|
getCardColor: cardColor,
|
||||||
|
getPatterColor: patternColor,
|
||||||
|
getPatternList: patternList,
|
||||||
|
getTextColor: textColor,
|
||||||
|
getLuckyColor: luckyColor,
|
||||||
|
getCardSize: cardSize,
|
||||||
|
getTextSize: textSize,
|
||||||
|
getRowCount: rowCount,
|
||||||
|
getIsShowAvatar: isShowAvatar,
|
||||||
|
getTitleFont: titleFont,
|
||||||
|
getTitleFontSyncGlobal: titleFontSyncGlobal,
|
||||||
|
getDefiniteTime: definiteTime,
|
||||||
|
getWinMusic: isPlayWinMusic,
|
||||||
|
} = storeToRefs(globalConfig)
|
||||||
// three初始值
|
// three初始值
|
||||||
const ballRotationY = ref(0)
|
const ballRotationY = ref(0)
|
||||||
const containerRef = ref<HTMLElement>()
|
const containerRef = ref<HTMLElement>()
|
||||||
@@ -53,7 +69,7 @@ export function useViewModel() {
|
|||||||
const intervalTimer = ref<any>(null)
|
const intervalTimer = ref<any>(null)
|
||||||
const isInitialDone = ref<boolean>(false)
|
const isInitialDone = ref<boolean>(false)
|
||||||
const animationFrameId = ref<any>(null)
|
const animationFrameId = ref<any>(null)
|
||||||
|
const playingAudios = ref<HTMLAudioElement[]>([])
|
||||||
function initThreeJs() {
|
function initThreeJs() {
|
||||||
const felidView = 40
|
const felidView = 40
|
||||||
const width = window.innerWidth
|
const width = window.innerWidth
|
||||||
@@ -367,14 +383,6 @@ export function useViewModel() {
|
|||||||
personPool.value.splice(index, 1)
|
personPool.value.splice(index, 1)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// for (let i = 0; i < luckyCount.value; i++) {
|
|
||||||
// if (personPool.value.length > 0) {
|
|
||||||
// // 解决随机元素概率过于不均等问题
|
|
||||||
// const randomIndex = Math.floor(Math.random() * (personPool.value.length - 1))
|
|
||||||
// luckyTargets.value.push(personPool.value[randomIndex])
|
|
||||||
// personPool.value.splice(randomIndex, 1)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
toast.open({
|
toast.open({
|
||||||
// message: `现在抽取${currentPrize.value.name} ${leftover}人`,
|
// message: `现在抽取${currentPrize.value.name} ${leftover}人`,
|
||||||
@@ -385,6 +393,13 @@ export function useViewModel() {
|
|||||||
})
|
})
|
||||||
currentStatus.value = LotteryStatus.running
|
currentStatus.value = LotteryStatus.running
|
||||||
rollBall(10, 3000)
|
rollBall(10, 3000)
|
||||||
|
if (definiteTime.value) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (currentStatus.value === LotteryStatus.running) {
|
||||||
|
stopLottery()
|
||||||
|
}
|
||||||
|
}, definiteTime.value * 1000)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @description: 停止抽奖,抽出幸运人
|
* @description: 停止抽奖,抽出幸运人
|
||||||
@@ -429,11 +444,41 @@ export function useViewModel() {
|
|||||||
.easing(TWEEN.Easing.Exponential.InOut)
|
.easing(TWEEN.Easing.Exponential.InOut)
|
||||||
.start()
|
.start()
|
||||||
.onComplete(() => {
|
.onComplete(() => {
|
||||||
|
if (isPlayWinMusic.value) {
|
||||||
|
playWinMusic()
|
||||||
|
}
|
||||||
confettiFire()
|
confettiFire()
|
||||||
resetCamera()
|
resetCamera()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// 播放音频,中将卡片越多audio对象越多,声音越大
|
||||||
|
function playWinMusic() {
|
||||||
|
if (playingAudios.value.length > maxAudioLimit) {
|
||||||
|
console.log('音频播放数量已达到上限,请勿重复播放')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const enterNewAudio = new Audio(enterAudio)
|
||||||
|
playingAudios.value.push(enterNewAudio)
|
||||||
|
enterNewAudio.play()
|
||||||
|
.then(() => {
|
||||||
|
// 当音频播放结束后,从数组中移除
|
||||||
|
enterNewAudio.onended = () => {
|
||||||
|
const index = playingAudios.value.indexOf(enterNewAudio)
|
||||||
|
if (index > -1) {
|
||||||
|
playingAudios.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('播放音频失败:', error)
|
||||||
|
// 如果播放失败,也从数组中移除
|
||||||
|
const index = playingAudios.value.indexOf(enterNewAudio)
|
||||||
|
if (index > -1) {
|
||||||
|
playingAudios.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* @description: 继续,意味着这抽奖作数,计入数据库
|
* @description: 继续,意味着这抽奖作数,计入数据库
|
||||||
*/
|
*/
|
||||||
@@ -441,7 +486,6 @@ export function useViewModel() {
|
|||||||
if (!canOperate.value) {
|
if (!canOperate.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
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++) {
|
||||||
@@ -627,11 +671,6 @@ export function useViewModel() {
|
|||||||
intervalTimer.value = null
|
intervalTimer.value = null
|
||||||
window.removeEventListener('keydown', listenKeyboard)
|
window.removeEventListener('keydown', listenKeyboard)
|
||||||
})
|
})
|
||||||
// watch(() => allPersonList.value, (newVal) => {
|
|
||||||
// if (newVal.length) {
|
|
||||||
// init()
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
setDefaultPersonList,
|
setDefaultPersonList,
|
||||||
|
|||||||
Reference in New Issue
Block a user