* feat: 更新依赖,解决启动报错

* feat: 依赖升级后的样式修改

* refactor: 首页提取样式至单独的文件

* refactor: 重构首页代码,提取处理函数

* feat: 使用web worker来进行文件导入计算

* refactor: 提取组件,优化代码

* refactor: 人员列表代码结构重组

* feat:  修复问题

* feat: 添加Loading效果

* feat: indexdb存储人员名单,使用dexie

* feat(config): 重构界面配置项布局与样式

将原有的线性表单布局调整为分组的 fieldset 布局,提升视觉层次和可维护性。
新增文本设置、布局设置、主题设置、图案设置和其他设置等分类区域。
优化部分输入控件的结构和样式类名,增强用户体验和代码可读性。

* feat(data): 调整默认图案列表顺序

2025改为2026

feat(ui): 添加配置项注释说明

为 FaceConfig.vue 中的各个设置字段添加了 HTML 注释,便于识别和维护不同功能区域。包括:
- 文本设置(主标题、语言、文字大小)
- 布局设置(列数、卡片宽度、卡片高度)
- 主题设置(主题、背景图片)
- 图案设置
- 其他设置(是否常显奖项列表、是否显示头像)

* feat(PageHeader): 新增 PageHeader 组件并应用于多个配置页面

新增了 PageHeader 通用组件,用于统一页面标题和操作按钮区域的布局。
该组件包含 title 属性以及 buttons 和 alerts 两个具名插槽,便于复用和维护。
已在以下页面中集成使用:
- 图片管理页(ImageConfig.vue)
- 音乐管理页(MusicConfig.vue)
- 人员管理页(PersonAll/index.vue)
- 中奖者管理页(PersonAlready.vue)
- 奖品管理页(PrizeConfig.vue)

* fix(style): 调整 markdown 样式以支持主题变量

移除 media 查询,统一使用 CSS 变量定义颜色;修复组件中 i18n 导入位置问题。

* ```
fix(Config): 注释掉设置卡片颜色的逻辑

将设置卡片颜色的相关代码注释掉,完全使用用户使用的颜色,避免在主题切换时出现异常。
```

* ```
refactor(FaceConfig): 移除未使用的颜色验证工具函数

从 FaceConfig 组件中移除了不再需要的 isHex 和 isRgbOrRgba 工具函数导入,
因为这些函数当前并未在组件中使用。这有助于减少不必要的代码依赖并提高
组件的简洁性。
```

* fix(MusicConfig): 优化本地音乐列表项的显示样式

移除列表项中的 divide-y 类,为音乐名称添加 title 属性以显示完整名称,
并将音乐名称渲染为可点击的链接样式。

* build(workflow): 更新Node.js版本到22.x

将GitHub Actions工作流中的Node.js版本从20.x升级到22.x,
以使用最新的稳定版Node.js环境进行构建和测试。

* feat(router、config): 更新配置页面组件路径

将 FaceConfig、ImageConfig 和 MusicConfig 组件的导入路径从直接引用 `.vue` 文件改为引用 `index.vue`。

* build(eslint): 更新 ESLint 配置以忽略更多文件并添加警告规则

在原有忽略文件列表中新增了 '*.config.js' 文件类型,同时添加了
'no-console' 和 'no-debugger' 规则,并将其设置为警告级别,以提高
代码质量和一致性。

* feat(components): 添加图片上传组件及对话框功能

新增 `ImageUpload` 组件用于文件选择与预览,并集成到 `UploadDialog` 中实现图片上传逻辑。
更新了 `Dialog` 组件以支持可选属性和 model 绑定,增强其灵活性和可用性。
引入 `lucide-vue-next` 图标库支持图标渲染。
重构图片配置页面,移除旧上传逻辑,使用新的弹窗方式进行图片上传操作。

* feat(imgConfig): 优化图片存储结构

新增uuid依赖用于生成唯一ID,改进图片上传功能中的本地存储结构,
使用uuid替代时间戳作为键名以避免冲突,并调整从indexedDB读取数据的方式。

* feat: 音乐管理功能优化

* fix(MusicConfig): 移除上传对话框中的图片上传提示功能

移除了 UploadDialog 组件中不再使用的图片上传状态提示逻辑,
包括相关的响应式变量 imgUploadToast 及其对应的模板代码。
此变更简化了组件结构并移除了未使用的错误提示功能。

* fix(Config): 添加图片上传提示信息

在全局人脸配置页面添加了图片上传提示文本,提醒用户需要先上传图片到图片管理模块。

* feat(layout): 重构页面布局与音乐播放组件

将 PlayMusic 组件迁移至 layout/RightButton 并重构成通用右下角按钮组件,
提取音乐播放逻辑到独立 hook `usePlayMusic`,优化模态框提示逻辑并统一滚动行为。

* feat(prize-config): 引入拖拽功能并优化奖品配置界面

- 添加 `vue-draggable-plus` 和 `lodash-es` 依赖以支持列表拖拽排序
- 使用 `cloneDeep` 深拷贝奖品列表,避免直接修改原始数据
- 移除原有的手动排序逻辑(上下移动按钮),改用可视化拖拽方式
- 调整 UI 布局和样式,增强用户体验与可操作性
- 在 Demo 页面中添加 draggable 示例用于验证功能实现

* feat(Config): 动态显示当前年份并更新版权信息

将页脚中的版权年份从硬编码的 2024 改为动态获取的当前年份,并将作者名称添加跳转到 GitHub 主页的链接。

* fix(ui): 优化图片列表下拉选择器及表单项的样式与布局

* feat(person): 导入人员时添加uuid唯一标识字段 #91

* feat(utils): 添加安全洗牌与随机抽样函数 Feature #91

* test(random): 添加随机元素抽取函数的全面测试用例 Test #91

- 新增 Random.test.ts 文件,对 getRandomElements 函数进行详尽测试
- 包括基础功能、边界情况(count 为 0、负数、超出数组长度)
- 支持空数组、单元素数组、字符串数组、对象数组等多种数据类型
- 增加概率性测试,验证多次调用结果不完全一致
- 20万次循环验证各元素被抽中概率接近理论值,确保算法公平性

* feat(home): 重构抽奖逻辑并优化初始化流程 Feature #91

* feat(Home): 添加初始化完成状态控制头部标题显示 Fix #91

* feat(HeaderTitle): 添加加载状态显示 #91

* feat(store): 调整默认人员列表类型定义

* feat(deps): 添加tauri打包(遗留pnpm build:file打包好的文件跨域无法访问) #94

* feat(icons): 添加tauri多种平台的应用图标资源

* fix(tauri): 更新 Cargo.toml 配置格式 #94

* build(workflow): 更新 GitHub Actions 工作流配置 #94

* build(workflow): 更新发布流程并移除旧的构建文件

* chore(release): 调整 GitHub Actions 工作流中的缩进格式

* build(deps): 固定 three.js 相关依赖版本

* build(workflows): 添加 fast-glob 依赖到 GitHub 工作流

* build(release): 添加发布名称和文件名以包含版本信息

* build(github): 升级下载构件操作到v4版本

* ci(release): 上传web构建产物作为GitHub Actions工件

* feat(Config): 支持多语言 README 文档展示

* feat(Global/FaceConfig): 添加跳转到图片管理页面的链接 #96

* chore: 格式化

* feat(i18n): 更新抽奖配置相关国际化文案 #96

* build: git钩子

* build: pre-psuh

* feat(components): 添加 shadcn-vue 组件配置文件及配套组件库 #96

* feat(font): 添加本地字体选择功能(not done) #96

* feat: 更换字体v,以及页面加载之前就设置字体 #96

* chore: 删除v console.log #96

* build: pre-push

* feat(globalConfig): 设置默认字体为微软雅黑 #96

* feat: 设置标题字体与全局字体同步 #96

* build(.gitignore): 添加 husky 目录到忽略文件

* docs(readme): 格式

* docs(readme): 移除关于build:file命令的过时文档

* build: 🏗️ 使用低版本node会报错,强制限制使用node 22.x版本

可在.npmrc里面修改为不强制使用

* feat:  上传文件时校验是否模板文件,否则提示 #96

* feat(layout): 添加全屏切换功能 #96

* refactor: ♻️ 界面设置页面重构,抽离逻辑 #96

* fix(background): 修复从本地存储获取图片数据的问题 #96

* refactor: ♻️ 重构奖项配置模块 #96

* feat(personalready):  已中奖的员页面修改switch组件 #96

* refactor(personalready): ♻️ 已中奖的界面代码重构 #96

* style: 💄 删除shadcn-vue的样式预设,只使用daisyui的样式 #96

* feat:  标题样式跟随主题,设置文本颜色会覆盖主题的标题颜色 #96

* fix(Global/FaceConfig): 调整主题设置中文本颜色按钮的高度样式 #96

* fix(home): 🐛 解决多次切换路由后页面卡顿的问题 #96 (#119)

卸载路由时清除requestAnimationFrame

* 96 UI optimization (#122)

* fix(home): 🐛 解决多次切换路由后页面卡顿的问题 #96

卸载路由时清除requestAnimationFrame

* feat:  文件存储使用Blob格式

* style: 💄 修改部分类型any为具体类型

* feat:  界面设置中模块使用瀑布流布局 #96

* fix: 🐛 md文档更换文件夹解决控制台警告

* style: 💄 switch按钮改回使用daisyui组件

* refactor: ♻️ 所有人员列表提取tableColumn

* style: 💄 奖项列表中的图片类型修复

* Confilct dev date 12 22 (#131)

* 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: 🐛 上传文件、解析数据与存储/读取数据的处理

、

* 96 UI optimization (#132)

* 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): 更新备案信息链接样式

将备案信息的段落标签替换为可点击的链接标签,使用户能够直接跳转到工信部备案查询页面。同时添加了悬停效果样式,提升用户体验。

* 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

* 96 UI optimization (#137)

* 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

* style: 💄 下载模板成功后进行提示

* 96 UI optimization (#141)

* 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

* style: 💄 下载模板成功后进行提示

* docs: 📝 readme更新

* ci: 👷 git action触发改为推送release版本时执行

* Feature action (#143)

* ci: 👷 整合github action配置文件

* docs: 📝 贡献文档修改

* Feature action (#144)

* ci: 👷 整合github action配置文件

* docs: 📝 贡献文档修改

* style: 💄 更新版本

* style: 💄 cargo.lock版本更新

* Feature action (#149)

* ci: 👷 整合github action配置文件

* docs: 📝 贡献文档修改

* style: 💄 更新版本

* style: 💄 cargo.lock版本更新

* feat(husky): 增强Git标签版本校验脚本

添加了对Git标签指向提交与release分支一致性的校验功能。
脚本现在会检查tag指向的提交是否与当前或任何release分支的最新提交一致,
确保发布流程的准确性。如果当前在release分支上,直接比较分支HEAD与tag指向的提交;
如果不在release分支上,则遍历所有release分支查找匹配的提交。

* feat:  国际化

* Feature action (#150)

* ci: 👷 整合github action配置文件

* docs: 📝 贡献文档修改

* style: 💄 更新版本

* style: 💄 cargo.lock版本更新

* feat(husky): 增强Git标签版本校验脚本

添加了对Git标签指向提交与release分支一致性的校验功能。
脚本现在会检查tag指向的提交是否与当前或任何release分支的最新提交一致,
确保发布流程的准确性。如果当前在release分支上,直接比较分支HEAD与tag指向的提交;
如果不在release分支上,则遍历所有release分支查找匹配的提交。

* feat:  国际化

* fix: 🐛 修复国际化问题;修复字体大小未生效问题

* fix: 🐛 修复部分问题

* docs: 📝 update readme
This commit is contained in:
LOG1997
2025-12-30 17:31:19 +08:00
committed by GitHub
parent f952607ca5
commit f15494d6f1
265 changed files with 19456 additions and 9475 deletions

View File

@@ -0,0 +1,115 @@
<script setup lang='ts'>
import type { CSSProperties } from 'vue'
import { computed, toRefs } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { rgbToHex } from '@/utils/color'
interface Props {
textSize: number
textColor: string
topTitle: string
tableData: any[]
setDefaultPersonList: () => void
isInitialDone: boolean
titleFont: string
titleFontSyncGlobal: boolean
}
const props = defineProps<Props>()
const router = useRouter()
const { tableData, textSize, textColor, topTitle, setDefaultPersonList, titleFont, titleFontSyncGlobal } = toRefs(props)
const isTextColor = computed(() => {
return rgbToHex(textColor.value) !== '#00000000'
})
const titleStyle = computed(() => {
const style: CSSProperties = {
fontSize: `${textSize.value * 1.5}px`,
}
if (!titleFontSyncGlobal.value) {
style.fontFamily = titleFont.value
}
if (isTextColor.value) {
style.color = textColor.value
}
return style
})
const { t } = useI18n()
</script>
<template>
<div class="absolute z-10 flex flex-col items-center justify-center -translate-x-1/2 left-1/2">
<h2
class="pt-12 m-0 mb-12 tracking-wide text-center leading-12"
:class="{ 'animate-pulse bg-linear-to-r from-primary via-secondary to-accent bg-clip-text text-transparent': !isTextColor }"
:style="titleStyle"
>
{{ topTitle }}
</h2>
<div v-if="isInitialDone" class="flex gap-3">
<button
v-if="tableData.length <= 0" class="cursor-pointer btn btn-outline btn-secondary btn-lg"
@click="router.push('config')"
>
{{ t('button.noInfoAndImport') }}
</button>
<button
v-if="tableData.length <= 0" class="cursor-pointer btn btn-outline btn-secondary btn-lg"
@click="setDefaultPersonList"
>
{{ t('button.useDefault') }}
</button>
</div>
<!-- 加载中 -->
<div v-else class="flex gap-3 items-center">
<span class="loading loading-spinner loading-xl" />
<span>{{ t('button.loading') }}</span>
</div>
</div>
</template>
<style scoped lang="scss">
.header-title {
-webkit-animation: tracking-in-expand-fwd 0.8s cubic-bezier(0.215, 0.610, 0.355, 1.000) both;
animation: tracking-in-expand-fwd 0.8s cubic-bezier(0.215, 0.610, 0.355, 1.000) both;
}
@-webkit-keyframes tracking-in-expand-fwd {
0% {
letter-spacing: -0.5em;
-webkit-transform: translateZ(-700px);
transform: translateZ(-700px);
opacity: 0;
}
40% {
opacity: 0.6;
}
100% {
-webkit-transform: translateZ(0);
transform: translateZ(0);
opacity: 1;
}
}
@keyframes tracking-in-expand-fwd {
0% {
letter-spacing: -0.5em;
-webkit-transform: translateZ(-700px);
transform: translateZ(-700px);
opacity: 0;
}
40% {
opacity: 0.6;
}
100% {
-webkit-transform: translateZ(0);
transform: translateZ(0);
opacity: 1;
}
}
</style>

View File

@@ -0,0 +1,335 @@
#menu {
position: absolute;
z-index: 100;
width: 100%;
bottom: 50px;
text-align: center;
margin: 0 auto;
font-size: 32px;
.start {
// 居中
display: flex;
justify-content: center;
}
.btn-stars {
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
width: 13rem;
overflow: hidden;
height: 3rem;
background-size: 300% 300%;
backdrop-filter: blur(1rem);
border-radius: 5rem;
transition: 0.5s;
animation: gradient_301 5s ease infinite;
border: double 4px transparent;
background-image: linear-gradient(#212121, #212121), linear-gradient(137.48deg, #ffdb3b 10%, #FE53BB 45%, #8F51EA 67%, #0044ff 87%);
background-origin: border-box;
background-clip: content-box, border-box;
-webkit-animation: pulsate-fwd 1.2s ease-in-out infinite both;
animation: pulsate-fwd 1.2s ease-in-out infinite both;
&:hover #container-stars {
z-index: 1;
background-color: #212121;
}
&:hover {
transform: scale(1.1)
}
&:active {
border: double 4px #FE53BB;
background-origin: border-box;
background-clip: content-box, border-box;
animation: none;
}
&:active .circle {
background: #FE53BB;
}
strong {
z-index: 2;
font-family: 'Avalors Personal Use';
font-size: 12px;
letter-spacing: 5px;
color: #FFFFFF;
text-shadow: 0 0 4px white;
}
#container-stars {
position: absolute;
z-index: -1;
width: 100%;
height: 100%;
overflow: hidden;
transition: 0.5s;
backdrop-filter: blur(1rem);
border-radius: 5rem;
#stars {
position: relative;
background: transparent;
width: 200rem;
height: 200rem;
&::after {
content: "";
position: absolute;
top: -10rem;
left: -100rem;
width: 100%;
height: 100%;
animation: animStarRotate 90s linear infinite;
}
&::after {
background-image: radial-gradient(#ffffff 1px, transparent 1%);
background-size: 50px 50px;
}
&::before {
content: "";
position: absolute;
top: 0;
left: -50%;
width: 170%;
height: 500%;
animation: animStar 60s linear infinite;
}
&::before {
background-image: radial-gradient(#ffffff 1px, transparent 1%);
background-size: 50px 50px;
opacity: 0.5;
}
}
}
#glow {
position: absolute;
display: flex;
width: 12rem;
.circle {
width: 100%;
height: 30px;
filter: blur(2rem);
animation: pulse_3011 4s infinite;
z-index: -1;
&:nth-of-type(1) {
background: rgba(254, 83, 186, 0.636);
}
&:nth-of-type(2) {
background: rgba(142, 81, 234, 0.704);
}
}
}
}
.btn-neon {
--glow-color: rgb(217, 176, 255);
--glow-spread-color: rgba(191, 123, 255, 0.781);
--enhanced-glow-color: rgb(231, 206, 255);
--btn-color: rgb(100, 61, 136);
-webkit-animation: pulsate-fwd 0.9s ease-in-out infinite both;
animation: pulsate-fwd 0.9s ease-in-out infinite both;
cursor: pointer;
border: .25em solid var(--glow-color);
padding: 1em 3em;
color: var(--glow-color);
font-size: 15px;
font-weight: bold;
background-color: var(--btn-color);
border-radius: 1em;
outline: none;
box-shadow: 0 0 1em .25em var(--glow-color),
0 0 4em 1em var(--glow-spread-color),
inset 0 0 .75em .25em var(--glow-color);
text-shadow: 0 0 .5em var(--glow-color);
position: relative;
transition: all 0.3s;
-webkit-animation: swing-in-top-fwd 0.5s cubic-bezier(0.175, 0.885, 0.320, 1.275) both;
animation: swing-in-top-fwd 0.5s cubic-bezier(0.175, 0.885, 0.320, 1.275) both;
&::after {
pointer-events: none;
content: "";
position: absolute;
top: 120%;
left: 0;
height: 100%;
width: 100%;
background-color: var(--glow-spread-color);
filter: blur(2em);
opacity: .7;
transform: perspective(1.5em) rotateX(35deg) scale(1, .6);
}
&:hover {
color: var(--btn-color);
background-color: var(--glow-color);
box-shadow: 0 0 1em .25em var(--glow-color),
0 0 4em 2em var(--glow-spread-color),
inset 0 0 .75em .25em var(--glow-color);
}
&:active {
box-shadow: 0 0 0.6em .25em var(--glow-color),
0 0 2.5em 2em var(--glow-spread-color),
inset 0 0 .5em .25em var(--glow-color);
}
}
.btn-cancel {
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
width: 13rem;
overflow: hidden;
height: 3rem;
background-size: 300% 300%;
backdrop-filter: blur(1rem);
border-radius: 5rem;
transition: 0.5s;
animation: gradient_301 5s ease infinite;
border: double 4px transparent;
background-image: linear-gradient(#212121, #212121), linear-gradient(137.48deg, #ffdb3b 10%, #FE53BB 45%, #8F51EA 67%, #0044ff 87%);
background-origin: border-box;
background-clip: content-box, border-box;
}
}
@keyframes animStar {
from {
transform: translateY(0);
}
to {
transform: translateY(-135rem);
}
}
@keyframes animStarRotate {
from {
transform: rotate(360deg);
}
to {
transform: rotate(0);
}
}
@keyframes gradient_301 {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
@keyframes pulse_3011 {
0% {
transform: scale(0.75);
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.7);
}
70% {
transform: scale(1);
box-shadow: 0 0 0 10px rgba(0, 0, 0, 0);
}
100% {
transform: scale(0.75);
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
}
}
// 按钮动画
@-webkit-keyframes pulsate-fwd {
0% {
-webkit-transform: scale(1);
transform: scale(1);
}
50% {
-webkit-transform: scale(1.1);
transform: scale(1.1);
}
100% {
-webkit-transform: scale(1);
transform: scale(1);
}
}
@keyframes pulsate-fwd {
0% {
-webkit-transform: scale(1);
transform: scale(1);
}
50% {
-webkit-transform: scale(1.2);
transform: scale(1.2);
}
100% {
-webkit-transform: scale(1);
transform: scale(1);
}
}
@-webkit-keyframes tracking-in-expand-fwd {
0% {
letter-spacing: -0.5em;
-webkit-transform: translateZ(-700px);
transform: translateZ(-700px);
opacity: 0;
}
40% {
opacity: 0.6;
}
100% {
-webkit-transform: translateZ(0);
transform: translateZ(0);
opacity: 1;
}
}
@keyframes tracking-in-expand-fwd {
0% {
letter-spacing: -0.5em;
-webkit-transform: translateZ(-700px);
transform: translateZ(-700px);
opacity: 0;
}
40% {
opacity: 0.6;
}
100% {
-webkit-transform: translateZ(0);
transform: translateZ(0);
opacity: 1;
}
}

View File

@@ -0,0 +1,79 @@
<script setup lang='ts'>
import { toRefs } from 'vue'
import { useI18n } from 'vue-i18n'
import { LotteryStatus } from '@/views/Home/type'
interface Props {
currentStatus: LotteryStatus
tableData: any[]
enterLottery: () => void
startLottery: () => void
stopLottery: () => void
continueLottery: () => void
quitLottery: () => void
}
const props = defineProps<Props>()
const { currentStatus, tableData, enterLottery, startLottery, stopLottery, continueLottery, quitLottery } = toRefs(props)
const { t } = useI18n()
</script>
<template>
<div id="menu">
<button v-if="currentStatus === LotteryStatus.init && tableData.length > 0" class="btn-neon" @click="enterLottery">
{{ t('button.enterLottery') }}
</button>
<div v-if="currentStatus === LotteryStatus.ready" class="start">
<button class="btn-stars" @click="startLottery">
<strong>{{ t('button.start') }}</strong>
<div id="container-stars">
<div id="stars" />
</div>
<div id="glow">
<div class="circle" />
<div class="circle" />
</div>
</button>
</div>
<button v-if="currentStatus === LotteryStatus.running" class="btn-neon btn glass btn-lg" @click="stopLottery">
{{ t('button.selectLucky') }}
</button>
<div v-if="currentStatus === LotteryStatus.end" class="flex justify-center gap-6 enStop">
<div class="start">
<button class="btn-stars" @click="continueLottery">
<strong>{{ t('button.continue') }}</strong>
<div id="container-stars">
<div id="stars" />
</div>
<div id="glow">
<div class="circle" />
<div class="circle" />
</div>
</button>
</div>
<div class="start">
<button class="btn-stars btn-cancel" @click="quitLottery">
<strong>{{ t('button.cancel') }}</strong>
<div id="container-stars">
<div id="stars" />
</div>
<div id="glow">
<div class="circle" />
<div class="circle" />
</div>
</button>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
@use './index.scss'
</style>

View File

@@ -0,0 +1,97 @@
.label {
width: 120px;
}
.prize-list-enter-active {
-webkit-animation: slide-right 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
animation: slide-right 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
}
.prize-list-leave-active {
-webkit-animation: slide-left 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
animation: slide-left 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
}
.prize-operate-enter-active {
// 延时显示
animation: show-operate 0.6s;
-webkit-animation: show-operate 0.6s;
}
@-webkit-keyframes slide-right {
0% {
-webkit-transform: translateX(0);
transform: translateX(0);
}
100% {
-webkit-transform: translateX(30px);
transform: translateX(30px);
}
}
@keyframes slide-right {
0% {
-webkit-transform: translateX(-200px);
transform: translateX(-200px);
}
100% {
-webkit-transform: translateX(0);
transform: translateX(0);
}
}
@-webkit-keyframes slide-left {
0% {
-webkit-transform: translateX(0);
transform: translateX(0);
}
100% {
-webkit-transform: translateX(-100px);
transform: translateX(-100px);
}
}
@keyframes slide-left {
0% {
-webkit-transform: translateX(0);
transform: translateX(0);
}
100% {
-webkit-transform: translateX(-400px);
transform: translateX(-400px);
}
}
@-webkit-keyframes show-operate {
0% {
opacity: 0;
}
99% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes show-operate {
0% {
opacity: 0;
}
99% {
opacity: 0;
}
100% {
opacity: 1;
}
}

View File

@@ -0,0 +1,68 @@
<script setup lang='ts'>
import type { IPrizeConfig } from '@/types/storeType'
import { ref } from 'vue'
// import { useI18n } from 'vue-i18n'
import EditSeparateDialog from '@/components/NumberSeparate/EditSeparateDialog.vue'
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>()
</script>
<template>
<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 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>
<OperationButton v-if="!temporaryPrize.isShow" v-model:prize-show="prizeShow" :add-temporary-prize="addTemporaryPrize" />
</div>
</template>
<style lang='scss' scoped>
@use "./index.scss";
</style>

View File

@@ -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%);
}
}

View File

@@ -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>

View File

@@ -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,
}
}

View File

@@ -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>

View 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>

View 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>

View 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,
}
}

View File

@@ -0,0 +1,72 @@
<script setup lang='ts'>
import { useElementSize } from '@vueuse/core'
import localforage from 'localforage'
import Sparticles from 'sparticles'
import { onMounted, onUnmounted, ref } from 'vue'
const props = defineProps({
homeBackground: {
type: Object,
default: () => ({
id: '',
name: '',
url: '',
}),
},
})
const imageDbStore = localforage.createInstance({
name: 'imgStore',
})
const imgUrl = ref('')
const starRef = ref()
const { width, height } = useElementSize(starRef)
const options = ref({ shape: 'star', parallax: 1.2, rotate: true, twinkle: true, speed: 10, count: 200 })
function addSparticles(node: any, width: number, height: number) {
const sparticleInstance = new Sparticles(node, options.value, width, height)
return sparticleInstance
}
// 页面大小改变时
function listenWindowSize() {
window.addEventListener('resize', () => {
if (width.value && height.value) {
addSparticles(starRef.value, width.value, height.value)
}
})
}
async function getImageStoreItem(item: any): Promise<string> {
let image = ''
if (item.url === 'Storage') {
const key = item.id
const imageData = await imageDbStore.getItem(key) as any
image = URL.createObjectURL(imageData.data)
}
else {
image = item.url
}
return image
}
onMounted(() => {
getImageStoreItem(props.homeBackground).then((image) => {
imgUrl.value = image
})
addSparticles(starRef.value, width.value, height.value)
listenWindowSize()
})
onUnmounted(() => {
window.removeEventListener('resize', listenWindowSize)
})
</script>
<template>
<div v-if="homeBackground.url" class="home-background w-screen h-screen overflow-hidden">
<img :src="imgUrl" class="w-full h-full object-cover" alt="">
</div>
<div v-else ref="starRef" class="w-screen h-screen overflow-hidden" />
</template>
<style lang='scss' scoped>
</style>