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 镜像作为基础镜像
|
||||
FROM node:20.12.2
|
||||
# 使用更小的 Node 镜像作为构建基础镜像
|
||||
FROM node:22-alpine as builder
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
30
README.md
30
README.md
@@ -134,17 +134,31 @@ npm run build
|
||||
|
||||
## Docker支持
|
||||
|
||||
构建镜像
|
||||
以下任意方式选一种即可
|
||||
|
||||
```bash
|
||||
docker build -t log-lottery .
|
||||
```
|
||||
1. 拉取镜像
|
||||
|
||||
运行容器
|
||||
```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/>访问
|
||||
|
||||
|
||||
96
package.json
96
package.json
@@ -20,92 +20,94 @@
|
||||
"dependencies": {
|
||||
"@tweenjs/tween.js": "23.1.2",
|
||||
"@vueuse/core": "^14.1.0",
|
||||
"axios": "^1.7.8",
|
||||
"canvas-confetti": "^1.9.3",
|
||||
"axios": "^1.13.2",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"dayjs": "^1.11.19",
|
||||
"dexie": "^4.2.1",
|
||||
"github-markdown-css": "^5.8.0",
|
||||
"github-markdown-css": "^5.8.1",
|
||||
"gsap": "^3.14.2",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lucide-vue-next": "^0.559.0",
|
||||
"lodash-es": "^4.17.22",
|
||||
"lucide-vue-next": "^0.562.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"masonry-layout": "^4.2.2",
|
||||
"pinia": "^3.0.3",
|
||||
"pinia": "^3.0.4",
|
||||
"pinia-plugin-persist": "^1.0.0",
|
||||
"reka-ui": "^2.6.1",
|
||||
"reka-ui": "^2.7.0",
|
||||
"sparticles": "^1.3.1",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"three": "0.166.0",
|
||||
"three-css3d": "1.0.6",
|
||||
"uuid": "^13.0.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-dompurify-html": "^5.2.0",
|
||||
"vue": "^3.5.26",
|
||||
"vue-dompurify-html": "^5.3.0",
|
||||
"vue-draggable-plus": "^0.6.0",
|
||||
"vue-i18n": "^11.2.2",
|
||||
"vue-router": "^4.5.0",
|
||||
"vue-i18n": "^11.2.7",
|
||||
"vue-router": "^4.6.4",
|
||||
"vue-sonner": "^2.0.9",
|
||||
"vue-toast-notification": "^3",
|
||||
"vue3-colorpicker": "^2.3.0",
|
||||
"xlsx": "^0.18.5",
|
||||
"zod": "^4.1.11"
|
||||
"zod": "^4.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^6.4.1",
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.15.0",
|
||||
"@iconify-json/ep": "^1.2.1",
|
||||
"@iconify-json/fluent": "^1.2.8",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
"@tauri-apps/cli": "^2.9.5",
|
||||
"@antfu/eslint-config": "^6.7.3",
|
||||
"@eslint/eslintrc": "^3.3.3",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@iconify-json/ep": "^1.2.3",
|
||||
"@iconify-json/fluent": "^1.2.36",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tauri-apps/cli": "^2.9.6",
|
||||
"@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/markdown-it": "^14.1.2",
|
||||
"@types/masonry-layout": "^4.2.8",
|
||||
"@types/node": "^25.0.0",
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/three": "0.166.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.49.0",
|
||||
"@typescript-eslint/parser": "^8.49.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.50.1",
|
||||
"@typescript-eslint/parser": "^8.50.1",
|
||||
"@vitejs/plugin-legacy": "^7.2.1",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vitest/ui": "^4.0.15",
|
||||
"@vitejs/plugin-vue": "^6.0.3",
|
||||
"@vitest/ui": "^4.0.16",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"baseline-browser-mapping": "^2.8.32",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"baseline-browser-mapping": "^2.9.11",
|
||||
"child_process": "^1.0.2",
|
||||
"daisyui": "^5.1.13",
|
||||
"eslint": "^9.15.0",
|
||||
"eslint-plugin-vue": "^10.4.0",
|
||||
"globals": "^16.4.0",
|
||||
"daisyui": "^5.5.14",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-vue": "^10.6.2",
|
||||
"fast-glob": "^3.3.3",
|
||||
"globals": "^16.5.0",
|
||||
"happy-dom": "^20.0.11",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^27.0.0",
|
||||
"jsdom": "^27.3.0",
|
||||
"path": "^0.12.7",
|
||||
"postcss": "^8.4.49",
|
||||
"rollup-plugin-visualizer": "^6.0.3",
|
||||
"sass": "^1.81.0",
|
||||
"sass-loader": "^16.0.3",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"terser": "^5.36.0",
|
||||
"postcss": "^8.5.6",
|
||||
"rollup-plugin-visualizer": "^6.0.5",
|
||||
"sass": "^1.97.1",
|
||||
"sass-loader": "^16.0.6",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"terser": "^5.44.1",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "~5.9.3",
|
||||
"unplugin-auto-import": "^20.1.0",
|
||||
"unplugin-icons": "^22.3.0",
|
||||
"unplugin-auto-import": "^20.3.0",
|
||||
"unplugin-icons": "^22.5.0",
|
||||
"unplugin-vue-components": "^30.0.0",
|
||||
"vite": "^7.1.6",
|
||||
"vite": "^7.3.0",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-inspect": "^11.3.3",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vite-plugin-vue-devtools": "^8.0.2",
|
||||
"vitest": "^4.0.15",
|
||||
"vue-tsc": "^3.0.7"
|
||||
"vite-plugin-vue-devtools": "^8.0.5",
|
||||
"vitest": "^4.0.16",
|
||||
"vue-tsc": "^3.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"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>
|
||||
.svg-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
/* width: 24px;
|
||||
height: 24px; */
|
||||
fill: currentColor;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -69,7 +69,7 @@ export function useElementPosition(element: any, count: number, totalCount: numb
|
||||
let yTable = 0
|
||||
const centerPosition = {
|
||||
x: 0,
|
||||
y: windowSize.height / 2 - cardSize.height / 2,
|
||||
y: windowSize.height / 2 - cardSize.height * 0.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,
|
||||
topTitle: i18n.global.t('data.defaultTitle'),
|
||||
language: browserLanguage,
|
||||
definiteTime: null as number | null,
|
||||
winMusic: false,
|
||||
theme: {
|
||||
name: 'dracula',
|
||||
detail: { primary: '#0f5fd3' },
|
||||
@@ -125,6 +127,14 @@ export const useGlobalConfig = defineStore('global', {
|
||||
getIsShowAvatar(state) {
|
||||
return state.globalConfig.isShowAvatar
|
||||
},
|
||||
// 获取定时抽取时间
|
||||
getDefiniteTime(state) {
|
||||
return state.globalConfig.definiteTime
|
||||
},
|
||||
// 是否播放获奖音乐
|
||||
getWinMusic(state) {
|
||||
return state.globalConfig.winMusic
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
// 设置全局配置
|
||||
@@ -266,14 +276,24 @@ export const useGlobalConfig = defineStore('global', {
|
||||
setIsShowAvatar(isShowAvatar: boolean) {
|
||||
this.globalConfig.isShowAvatar = isShowAvatar
|
||||
},
|
||||
// 设置定时抽取时间
|
||||
setDefiniteTime(definiteTime: number | null) {
|
||||
this.globalConfig.definiteTime = definiteTime
|
||||
},
|
||||
// 设置是否播放获奖音乐
|
||||
setIsPlayWinMusic(winMusic: boolean) {
|
||||
this.globalConfig.winMusic = winMusic
|
||||
},
|
||||
// 重置所有配置
|
||||
reset() {
|
||||
this.globalConfig = {
|
||||
rowCount: 17,
|
||||
winMusic: false,
|
||||
isSHowPrizeList: true,
|
||||
isShowAvatar: false,
|
||||
topTitle: i18n.global.t('data.defaultTitle'),
|
||||
language: browserLanguage,
|
||||
definiteTime: null,
|
||||
theme: {
|
||||
name: 'dracula',
|
||||
detail: { primary: '#0f5fd3' },
|
||||
|
||||
@@ -11,6 +11,22 @@
|
||||
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" */
|
||||
|
||||
body,
|
||||
|
||||
@@ -1,11 +1,44 @@
|
||||
<script setup lang='ts'>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
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'
|
||||
|
||||
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>
|
||||
|
||||
<template>
|
||||
@@ -54,6 +87,8 @@ const { resetData, topTitleValue, languageValue, textSizeValue, currentFontValue
|
||||
:clear-pattern="clearPattern"
|
||||
:reset-pattern="resetPattern"
|
||||
/>
|
||||
<!-- 功能设置 -->
|
||||
<AbilitySetting v-model:definite-time="definiteTimeValue" v-model:win-music="isWinMusicValue" />
|
||||
</GridWaterfall>
|
||||
<!-- </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"
|
||||
>
|
||||
<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 v-show="isRowCountChange === 2" class="loading loading-ring loading-md" />
|
||||
</button>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { default as AbilitySetting } from './AbilitySetting.vue'
|
||||
export { default as DataSetting } from './DataSetting.vue'
|
||||
export { default as LayoutSetting } from './LayoutSetting.vue'
|
||||
export { default as PatternSetting } from './PatternSetting.vue'
|
||||
|
||||
@@ -11,7 +11,28 @@ export function useViewModel() {
|
||||
const globalConfig = useStore().globalConfig
|
||||
const personConfig = useStore().personConfig
|
||||
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)
|
||||
const { getAlreadyPersonList: alreadyPersonList, getNotPersonList: notPersonList } = storeToRefs(personConfig)
|
||||
|
||||
@@ -32,6 +53,8 @@ export function useViewModel() {
|
||||
const currentFontValue = ref(structuredClone(currentFont.value))
|
||||
const currentTitleFontValue = ref(structuredClone(currentTitleFont.value))
|
||||
const titleFontSyncGlobalValue = ref(structuredClone(titleFontSyncGlobal.value))
|
||||
const definiteTimeValue = ref(structuredClone(definiteTime.value))
|
||||
const isWinMusicValue = ref(structuredClone(isWinMusic.value))
|
||||
const formData = ref({
|
||||
rowCount: rowCountValue,
|
||||
})
|
||||
@@ -170,6 +193,12 @@ export function useViewModel() {
|
||||
watch(isShowAvatarValue, () => {
|
||||
globalConfig.setIsShowAvatar(isShowAvatarValue.value)
|
||||
})
|
||||
watch(definiteTimeValue, () => {
|
||||
globalConfig.setDefiniteTime(definiteTimeValue.value)
|
||||
})
|
||||
watch(isWinMusicValue, () => {
|
||||
globalConfig.setIsPlayWinMusic(isWinMusicValue.value)
|
||||
})
|
||||
onMounted(() => {
|
||||
})
|
||||
return {
|
||||
@@ -203,5 +232,7 @@ export function useViewModel() {
|
||||
resetPattern,
|
||||
exportAllConfigData,
|
||||
importAllConfigData,
|
||||
definiteTimeValue,
|
||||
isWinMusicValue,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,241 @@
|
||||
<script setup lang='ts'>
|
||||
import WaterFall from '@/components/Waterfall/index.vue'
|
||||
<script setup lang="ts">
|
||||
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>
|
||||
|
||||
<template>
|
||||
<WaterFall>
|
||||
<div class="bg-pink-500 h-20 w-48" />
|
||||
<div class="bg-blue-500 h-30 w-48" />
|
||||
<div class="bg-yellow-500 h-40 w-48" />
|
||||
<div class="bg-green-500 h-20 w-48" />
|
||||
<div class="bg-red-500 h-30 w-48" />
|
||||
<div class="bg-purple-500 h-50 w-48" />
|
||||
</WaterFall>
|
||||
<div class="h-full w-48 flex flex-col justify-center overflow-hidden relative">
|
||||
<div class="w-full h-16 flex justify-center scroll-button scroll-button-up">
|
||||
<SvgIcon v-show="showUpButton" name="chevron-up" size="64px" class="text-gray-200/80 cursor-pointer" @click="handleScroll(-100)" />
|
||||
</div>
|
||||
<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>
|
||||
</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>
|
||||
|
||||
@@ -18,48 +18,7 @@
|
||||
-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 {
|
||||
0% {
|
||||
|
||||
@@ -1,319 +1,65 @@
|
||||
<script setup lang='ts'>
|
||||
import type { IPrizeConfig } from '@/types/storeType'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import defaultPrizeImage from '@/assets/images/龙.png'
|
||||
import ImageSync from '@/components/ImageSync/index.vue'
|
||||
|
||||
import { ref } from 'vue'
|
||||
// import { useI18n } from 'vue-i18n'
|
||||
import EditSeparateDialog from '@/components/NumberSeparate/EditSeparateDialog.vue'
|
||||
|
||||
import i18n from '@/locales/i18n'
|
||||
import useStore from '@/store'
|
||||
|
||||
const { t } = useI18n()
|
||||
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()
|
||||
import OfficialPrizeList from './parts/OfficialPrizeList/index.vue'
|
||||
import OperationButton from './parts/OperationButton.vue'
|
||||
import TemporaryDialog from './parts/TemporaryDialog.vue'
|
||||
import TemporaryList from './parts/TemporaryList.vue'
|
||||
import { usePrizeList } from './usePrizeList'
|
||||
|
||||
const temporaryPrizeRef = ref()
|
||||
const {
|
||||
temporaryPrize,
|
||||
changePersonCount,
|
||||
selectPrize,
|
||||
localImageList,
|
||||
addTemporaryPrize,
|
||||
submitTemporaryPrize,
|
||||
submitData,
|
||||
deleteTemporaryPrize,
|
||||
prizeShow,
|
||||
currentPrize,
|
||||
localPrizeList,
|
||||
isMobile,
|
||||
} = usePrizeList(temporaryPrizeRef)
|
||||
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>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center">
|
||||
<dialog id="my_modal_1" ref="temporaryPrizeRef" 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>
|
||||
<div v-if="localPrizeList.length" class="flex h-2/3 items-center overflow-hidden">
|
||||
<TemporaryDialog
|
||||
ref="temporaryPrizeRef"
|
||||
v-model:temporary-prize="temporaryPrize"
|
||||
:change-person-count="changePersonCount"
|
||||
:select-prize="selectPrize"
|
||||
:local-image-list="localImageList"
|
||||
:add-temporary-prize="addTemporaryPrize"
|
||||
:submit-temporary-prize="submitTemporaryPrize"
|
||||
/>
|
||||
<EditSeparateDialog
|
||||
:total-number="selectedPrize?.count" :separated-number="selectedPrize?.separateCount.countList"
|
||||
@submit-data="submitData"
|
||||
/>
|
||||
<div ref="prizeListContainerRef">
|
||||
<div v-if="temporaryPrize.isShow" 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>
|
||||
<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 class="h-full">
|
||||
<TemporaryList
|
||||
v-if="temporaryPrize.isShow"
|
||||
:temporary-prize="temporaryPrize"
|
||||
:add-temporary-prize="addTemporaryPrize"
|
||||
:delete-temporary-prize="deleteTemporaryPrize"
|
||||
/>
|
||||
<OfficialPrizeList
|
||||
v-show="!temporaryPrize.isShow"
|
||||
v-model:prize-show="prizeShow"
|
||||
:temporary-prize-show="temporaryPrize.isShow"
|
||||
:local-prize-list="localPrizeList"
|
||||
:current-prize="currentPrize"
|
||||
:is-mobile="isMobile"
|
||||
:add-temporary-prize="addTemporaryPrize"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<OperationButton v-if="!temporaryPrize.isShow" v-model:prize-show="prizeShow" :add-temporary-prize="addTemporaryPrize" />
|
||||
</div>
|
||||
</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 { nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import enterAudio from '@/assets/audio/enter.wav'
|
||||
import { useElementPosition, useElementStyle } from '@/hooks/useElement'
|
||||
import i18n from '@/locales/i18n'
|
||||
import useStore from '@/store'
|
||||
@@ -16,6 +17,7 @@ import { rgba } from '@/utils/color'
|
||||
import { LotteryStatus } from './type'
|
||||
import { confettiFire, createSphereVertices, createTableVertices, getRandomElements, initTableData } from './utils'
|
||||
|
||||
const maxAudioLimit = 10
|
||||
export function useViewModel() {
|
||||
const toast = useToast()
|
||||
// store里面存储的值
|
||||
@@ -26,7 +28,21 @@ export function useViewModel() {
|
||||
getNotThisPrizePersonList: notThisPrizePersonList,
|
||||
} = storeToRefs(personConfig)
|
||||
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初始值
|
||||
const ballRotationY = ref(0)
|
||||
const containerRef = ref<HTMLElement>()
|
||||
@@ -53,7 +69,7 @@ export function useViewModel() {
|
||||
const intervalTimer = ref<any>(null)
|
||||
const isInitialDone = ref<boolean>(false)
|
||||
const animationFrameId = ref<any>(null)
|
||||
|
||||
const playingAudios = ref<HTMLAudioElement[]>([])
|
||||
function initThreeJs() {
|
||||
const felidView = 40
|
||||
const width = window.innerWidth
|
||||
@@ -367,14 +383,6 @@ export function useViewModel() {
|
||||
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({
|
||||
// message: `现在抽取${currentPrize.value.name} ${leftover}人`,
|
||||
@@ -385,6 +393,13 @@ export function useViewModel() {
|
||||
})
|
||||
currentStatus.value = LotteryStatus.running
|
||||
rollBall(10, 3000)
|
||||
if (definiteTime.value) {
|
||||
setTimeout(() => {
|
||||
if (currentStatus.value === LotteryStatus.running) {
|
||||
stopLottery()
|
||||
}
|
||||
}, definiteTime.value * 1000)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @description: 停止抽奖,抽出幸运人
|
||||
@@ -429,11 +444,41 @@ export function useViewModel() {
|
||||
.easing(TWEEN.Easing.Exponential.InOut)
|
||||
.start()
|
||||
.onComplete(() => {
|
||||
if (isPlayWinMusic.value) {
|
||||
playWinMusic()
|
||||
}
|
||||
confettiFire()
|
||||
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: 继续,意味着这抽奖作数,计入数据库
|
||||
*/
|
||||
@@ -441,7 +486,6 @@ export function useViewModel() {
|
||||
if (!canOperate.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const customCount = currentPrize.value.separateCount
|
||||
if (customCount && customCount.enable && customCount.countList.length > 0) {
|
||||
for (let i = 0; i < customCount.countList.length; i++) {
|
||||
@@ -627,11 +671,6 @@ export function useViewModel() {
|
||||
intervalTimer.value = null
|
||||
window.removeEventListener('keydown', listenKeyboard)
|
||||
})
|
||||
// watch(() => allPersonList.value, (newVal) => {
|
||||
// if (newVal.length) {
|
||||
// init()
|
||||
// }
|
||||
// })
|
||||
|
||||
return {
|
||||
setDefaultPersonList,
|
||||
|
||||
Reference in New Issue
Block a user