Dev (#151)
* 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:
115
src/views/Home/components/HeaderTitle/index.vue
Normal file
115
src/views/Home/components/HeaderTitle/index.vue
Normal 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>
|
||||
335
src/views/Home/components/OptionsButton/index.scss
Normal file
335
src/views/Home/components/OptionsButton/index.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
79
src/views/Home/components/OptionsButton/index.vue
Normal file
79
src/views/Home/components/OptionsButton/index.vue
Normal 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>
|
||||
97
src/views/Home/components/PrizeList/index.scss
Normal file
97
src/views/Home/components/PrizeList/index.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
68
src/views/Home/components/PrizeList/index.vue
Normal file
68
src/views/Home/components/PrizeList/index.vue
Normal 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>
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
72
src/views/Home/components/StarsBackground/index.vue
Normal file
72
src/views/Home/components/StarsBackground/index.vue
Normal 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>
|
||||
Reference in New Issue
Block a user