* 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:
81
src/App.vue
81
src/App.vue
@@ -1,86 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import PlayMusic from '@/components/PlayMusic/index.vue'
|
||||
import useStore from '@/store'
|
||||
import { themeChange } from '@/utils'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { provide } from 'vue'
|
||||
import { loadingKey, loadingState } from '@/components/Loading'
|
||||
// import PlayMusic from '@/components/PlayMusic/index.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const globalConfig = useStore().globalConfig
|
||||
const prizeConfig = useStore().prizeConfig
|
||||
const system = useStore().system
|
||||
const { getTheme: localTheme } = storeToRefs(globalConfig)
|
||||
const { getPrizeConfig: prizeList } = storeToRefs(prizeConfig)
|
||||
|
||||
const tipDialog = ref()
|
||||
|
||||
// 设置当前奖列表
|
||||
function setCurrentPrize() {
|
||||
if (prizeList.value.length <= 0) {
|
||||
return
|
||||
}
|
||||
for (let i = 0; i < prizeList.value.length; i++) {
|
||||
if (!prizeList.value[i].isUsed) {
|
||||
prizeConfig.setCurrentPrize(prizeList.value[i])
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// 判断是否手机端访问
|
||||
function judgeMobile() {
|
||||
const ua = navigator.userAgent
|
||||
const isAndroid = ua.includes('Android') || ua.includes('Adr')
|
||||
const isIOS = !!ua.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/)
|
||||
|
||||
system.setIsMobile(isAndroid || isIOS)
|
||||
|
||||
return isAndroid || isIOS
|
||||
}
|
||||
// 判断是否chrome或者edge访问
|
||||
function judgeChromeOrEdge() {
|
||||
const ua = navigator.userAgent
|
||||
const isChrome = ua.includes('Chrome')
|
||||
const isEdge = ua.includes('Edg')
|
||||
|
||||
system.setIsChrome(isChrome)
|
||||
|
||||
return isChrome || isEdge
|
||||
}
|
||||
onMounted(() => {
|
||||
themeChange(localTheme.value.name)
|
||||
setCurrentPrize()
|
||||
if (judgeMobile() || !judgeChromeOrEdge()) {
|
||||
tipDialog.value.showModal()
|
||||
}
|
||||
})
|
||||
provide(loadingKey, loadingState)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<dialog id="my_modal_1" ref="tipDialog" class="border-none modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="text-lg font-bold">
|
||||
{{ t('dialog.titleTip') }}
|
||||
</h3>
|
||||
<p v-if="judgeMobile()" class="py-4">
|
||||
{{ t('dialog.dialogPCWeb') }}
|
||||
</p>
|
||||
<p v-if=" !judgeChromeOrEdge()" class="py-4">
|
||||
{{ t('dialog.dialogLatestBrowser') }}
|
||||
</p>
|
||||
<div class="modal-action">
|
||||
<form method="dialog" class="flex justify-start w-full gap-3">
|
||||
<!-- if there is a button in form, it will close the modal -->
|
||||
<button class="btn">
|
||||
{{ t('button.confirm') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
<router-view />
|
||||
<PlayMusic class="absolute right-0 bottom-1/2" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
BIN
src/assets/audio/enter.wav
Normal file
BIN
src/assets/audio/enter.wav
Normal file
Binary file not shown.
31
src/assets/md/readme-en.md
Normal file
31
src/assets/md/readme-en.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Operation Guide
|
||||
|
||||
## Steps
|
||||
|
||||
1. Upon first entry, no data will be displayed. You can choose to use default data to view the overall display effect. It is recommended to import your own data for operation. The steps are as follows:
|
||||
|
||||
a. Personnel Configuration - Personnel List - Download Template, download the data template and modify it with your data (please note that the header cannot be modified).
|
||||
|
||||
b. After modification, click 'Upload File' on the same page to upload the modified Excel table.
|
||||
|
||||
2. Enter the Prize Configuration to modify your prize information. Try to keep the name short for better display; "All Participants" indicates whether this award will be drawn from all participants (those who have already won can still participate); "Winners" refers to the number of people to be drawn for this award; "Already Won" cannot be edited; "Selected" means this award has been used, unselecting it will reset the award but not the winners; "Image" is the prize image displayed on the home page (you can upload images in the image list); "Left Icon" is used to adjust the order of prizes.
|
||||
|
||||
Completing the above two steps allows normal use.
|
||||
|
||||
## Function Description
|
||||
|
||||
1. Add Temporary Draw: There is a '+' button in the prize list on the draw page. Clicking it allows you to add a temporary draw. Note: Only one temporary draw can be added at a time. After adding successfully, the current prize will be set to the temporary prize, and after drawing, it will return to the normal prize list.
|
||||
2. Music and Image List: You can upload files yourself for use. After uploading images successfully, you can select them in the prize configuration for display. After uploading music successfully, it will be added to the play list.
|
||||
3. Music Playback: Left-click with the mouse to play/pause, right-click to play the next song.
|
||||
4. Interface Configuration - Pattern Settings: You can use the mouse to click and customize the highlighted patterns on the home page.
|
||||
5. If you do not want to display the prize list on the home page, uncheck 'Always Show Prize List' in the interface configuration.
|
||||
6. When clicking buttons on the home page, the button value will not update immediately but will only update after the animation ends. This is a normal phenomenon.
|
||||
|
||||
## Shortcuts
|
||||
|
||||
Shortcuts are set up on the draw page.
|
||||
|
||||
| Shortcut | Description |
|
||||
| --- | --- |
|
||||
| Space | Enter Draw / Start / Draw Lucky Winner / Continue |
|
||||
| Esc | Cancel |
|
||||
31
src/assets/md/readme-zhCn.md
Normal file
31
src/assets/md/readme-zhCn.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# 操作说明
|
||||
|
||||
## 步骤
|
||||
|
||||
1. 首次进入,没有数据展示,可以选择使用默认数据进行使用,查看整体展示效果。推荐导入自己的数据来进行操作。步骤如下:
|
||||
|
||||
a. 人员配置-人员名单-下载模板,下载数据模板并修改填入数据(请注意表头不可修改)。
|
||||
|
||||
b. 修改好后在同一个页面点击‘上传文件’,上传修改后的excel表格。
|
||||
|
||||
2. 进入奖品配置,修改自己的奖品信息。名称尽量短一点,方便展示;是否全员参加意指该项奖项是否从全体人员中抽取(已中奖的依然可以参与);获奖人数指该奖项要抽取的人数;已获奖人数不可编辑;已抽取被选中时指该奖项已使用,取消选择会重置该奖项,但不会重置已获奖的人;图片是在首页展示时的奖品图片(可在图片列表自己上传);左侧图标调整奖品顺序用。
|
||||
|
||||
完成上面两项已可以正常使用。
|
||||
|
||||
## 功能说明
|
||||
|
||||
1. 增加临时抽奖:抽奖页面的奖项列表有个‘+’号按钮,点击可临时增加抽奖,注意:一次只能增加一项临时抽奖,新增成功后当前奖项即设置为该临时奖项,抽取成功后返回正常奖项列表.
|
||||
2. 音乐与图片列表,可自己上传文件进行使用,图片上传成功后就可以在奖项配置中进行选择图片展示,音乐上传成功后即加入了播放列表.
|
||||
3. 音乐播放:使用鼠标左键点击是播放/暂停,使用鼠标右键点击是播放下一首.
|
||||
4. 界面配置-图案设置中可使用鼠标点击自定义配置首页中的高亮图案.
|
||||
5. 若不想在首页展示奖品列表,将界面配置中的'是否常显奖项列表'选中.
|
||||
6. 首页点击按钮时按钮值不会立即更新,会等动画结束后才会更新为目标值,属于正常现象.
|
||||
|
||||
## 快捷键
|
||||
|
||||
在抽奖页面设置了快捷键。
|
||||
|
||||
| 快捷键 | 说明 |
|
||||
| --- | --- |
|
||||
| Space | 进入抽奖/开始/抽取幸运儿/继续 |
|
||||
| Esc | 取消 |
|
||||
10
src/auto-imports.d.ts
vendored
10
src/auto-imports.d.ts
vendored
@@ -1,10 +0,0 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
|
||||
}
|
||||
22
src/components.d.ts
vendored
22
src/components.d.ts
vendored
@@ -1,22 +0,0 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
DaiysuiTable: typeof import('./components/DaiysuiTable/index.vue')['default']
|
||||
EditSeparateDialog: typeof import('./components/NumberSeparate/EditSeparateDialog.vue')['default']
|
||||
HelloWorld: typeof import('./components/HelloWorld.vue')['default']
|
||||
ImageSync: typeof import('./components/ImageSync/index.vue')['default']
|
||||
PlayMusic: typeof import('./components/PlayMusic/index.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
StarsBackground: typeof import('./components/StarsBackground/index.vue')['default']
|
||||
SvgIcon: typeof import('./components/SvgIcon/index.vue')['default']
|
||||
ToTop: typeof import('./components/ToTop/index.vue')['default']
|
||||
}
|
||||
}
|
||||
@@ -47,9 +47,9 @@ const actionsColumns = computed<any[]>(() => {
|
||||
<tbody v-if="data.length > 0">
|
||||
<!-- row -->
|
||||
<tr v-for="item in data" :key="item.id" class="hover">
|
||||
<th>{{ item.id }}</th>
|
||||
<!-- <th>{{ item.id }}</th> -->
|
||||
<td v-for="(column, index) in dataColumns" :key="index">
|
||||
<span v-if="column.formatValue" v-html="column.formatValue(item)"></span>
|
||||
<span v-if="column.formatValue" v-html="column.formatValue(item)" />
|
||||
<span v-else>{{ item[column.props] }}</span>
|
||||
</td>
|
||||
<!-- action -->
|
||||
|
||||
73
src/components/Dialog/index.vue
Normal file
73
src/components/Dialog/index.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<script setup lang='ts'>
|
||||
import { onMounted, ref, toRefs } from 'vue'
|
||||
import i18n from '@/locales/i18n'
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
desc?: string
|
||||
cancelText?: string
|
||||
submitText?: string
|
||||
submitFunc?: () => void
|
||||
cancelFunc?: () => void
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
cancelText: i18n.global.t('button.cancel'),
|
||||
submitText: i18n.global.t('button.confirm'),
|
||||
cancelFunc: () => {},
|
||||
})
|
||||
const visible = defineModel('visible', {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
})
|
||||
|
||||
const dialogRef = ref <HTMLDialogElement | null> (null)
|
||||
function defaultCancelFunc() {
|
||||
dialogRef.value?.close()
|
||||
}
|
||||
|
||||
function showDialog() {
|
||||
dialogRef.value?.showModal()
|
||||
}
|
||||
defineExpose({
|
||||
showDialog,
|
||||
closed,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
dialogRef.value?.addEventListener('close', () => {
|
||||
visible.value = false
|
||||
})
|
||||
})
|
||||
const { title, desc, cancelText, submitText, submitFunc, cancelFunc = defaultCancelFunc } = toRefs(props)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<dialog id="my_modal" ref="dialogRef" class="border-none modal">
|
||||
<div class="modal-box">
|
||||
<h3 v-if="title" class="text-lg font-bold">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<p v-if="desc" class="py-4">
|
||||
{{ desc }}
|
||||
</p>
|
||||
<div>
|
||||
<slot name="content" />
|
||||
</div>
|
||||
<div class="modal-action">
|
||||
<form method="dialog" class="flex gap-3">
|
||||
<!-- if there is a button in form, it will close the modal -->
|
||||
<button class="btn" @click="cancelFunc">
|
||||
{{ cancelText }}
|
||||
</button>
|
||||
<button class="btn" @click="submitFunc">
|
||||
{{ submitText }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
38
src/components/Drawer/index.vue
Normal file
38
src/components/Drawer/index.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script setup lang='ts'>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const drawerTriggerRef = ref <HTMLDialogElement | null> (null)
|
||||
const visible = ref(false)
|
||||
function showDrawer() {
|
||||
drawerTriggerRef.value?.click()
|
||||
visible.value = true
|
||||
}
|
||||
function closeDrawer() {
|
||||
drawerTriggerRef.value?.click()
|
||||
visible.value = false
|
||||
}
|
||||
defineExpose({
|
||||
showDrawer,
|
||||
closeDrawer,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="drawer drawer-end h-0 w-0">
|
||||
<input id="my-drawer-1" type="checkbox" class="drawer-toggle">
|
||||
<div class="drawer-content w-0 h-0 absolute">
|
||||
<!-- Page content here -->
|
||||
<label ref="drawerTriggerRef" for="my-drawer-1" />
|
||||
</div>
|
||||
<div class="drawer-side">
|
||||
<label for="my-drawer-1" aria-label="close sidebar" class="drawer-overlay" />
|
||||
<div v-if="visible" class="menu bg-base-200 min-h-full w-80 p-4">
|
||||
<slot name="content" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
69
src/components/FileUpload/index.vue
Normal file
69
src/components/FileUpload/index.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<script setup lang='ts'>
|
||||
import type { IFileData } from './type'
|
||||
import { ListMusic, Upload, X } from 'lucide-vue-next'
|
||||
import { ref } from 'vue'
|
||||
import { getBlobObjectUrl, readFileAsJsonData, readFileData, readFileDataAsBlob } from '@/utils/file'
|
||||
|
||||
const props = defineProps<{
|
||||
limitType?: string
|
||||
mode?: 'file' | 'json'
|
||||
}>()
|
||||
|
||||
const emits = defineEmits<{
|
||||
uploadFile: [fileData: IFileData | null]
|
||||
}>()
|
||||
const originFileName = ref<string | null>(null)
|
||||
const fileData = ref<IFileData | null>(null)
|
||||
|
||||
async function handleFileChange(e: Event) {
|
||||
const file = ((e.target as HTMLInputElement).files as FileList)[0]
|
||||
const type = file.type
|
||||
if (props.mode === 'json') {
|
||||
const fileRes = await readFileAsJsonData(file)
|
||||
const jsonData = JSON.parse(fileRes)
|
||||
fileData.value = { data: jsonData, fileName: file.name, type }
|
||||
originFileName.value = file.name
|
||||
emits('uploadFile', fileData.value)
|
||||
return
|
||||
}
|
||||
const { data: blobData, fileName } = await readFileDataAsBlob(file)
|
||||
fileData.value = { data: blobData, fileName, type }
|
||||
originFileName.value = fileName
|
||||
emits('uploadFile', fileData.value)
|
||||
}
|
||||
function removeFile() {
|
||||
fileData.value = null
|
||||
emits('uploadFile', null)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col items-center mt-6">
|
||||
<input
|
||||
id="file-upload"
|
||||
:disabled="fileData !== null"
|
||||
type="file" class="w-full bg-red-400/50 max-h-52 cursor-pointer absolute" style="display: none;" :accept="limitType"
|
||||
@change="handleFileChange"
|
||||
>
|
||||
<label for="file-upload" :class="fileData ? 'cursor-not-allowed' : null" class="w-full h-52 cursor-pointer border-2 border-dashed flex items-center justify-center overflow-hidden">
|
||||
<img v-if="fileData && fileData.type.includes('image')" class="w-full object-cover stroke-0" :src="getBlobObjectUrl(fileData.data as Blob)" alt="">
|
||||
<ListMusic v-else-if="fileData && fileData.type.includes('audio')" class="w-2/3 h-2/3 stroke-1 text-gray-500/50" />
|
||||
<div v-else class="w-full h-full flex justify-center items-center flex-col gap-4">
|
||||
<Upload class="w-2/3 h-2/3 stroke-1 text-gray-500/50" />
|
||||
<span class="btn btn-neutral">点击上传</span>
|
||||
</div>
|
||||
</label>
|
||||
<div v-if="fileData" class="w-full flex items-center justify-between mt-2">
|
||||
<p class="max-w-[3/4] truncate text-sm">
|
||||
{{ originFileName }}
|
||||
</p>
|
||||
<button class="btn btn-xs btn-square btn-ghost" @click="removeFile">
|
||||
<X />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
5
src/components/FileUpload/type.ts
Normal file
5
src/components/FileUpload/type.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface IFileData {
|
||||
data: string | Blob | ArrayBuffer
|
||||
fileName: string
|
||||
type: string
|
||||
}
|
||||
@@ -1,35 +1,35 @@
|
||||
<script setup lang='ts'>
|
||||
import type { IFileData } from '../FileUpload/type'
|
||||
import type { IImage } from '@/types/storeType'
|
||||
import localforage from 'localforage'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
imgItem: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
})
|
||||
interface IProps {
|
||||
imgItem: IImage
|
||||
}
|
||||
const props = defineProps<IProps>()
|
||||
const imageDbStore = localforage.createInstance({
|
||||
name: 'imgStore',
|
||||
})
|
||||
|
||||
const imgUrl = ref('')
|
||||
|
||||
async function getImageStoreItem(item: any): Promise<string> {
|
||||
async function getImageStoreItem(item: IImage): Promise<string> {
|
||||
let image = ''
|
||||
if (item.url === 'Storage') {
|
||||
const key = item.id
|
||||
image = await imageDbStore.getItem(key) as string
|
||||
const imageData = await imageDbStore.getItem<IFileData>(key)
|
||||
image = URL.createObjectURL(imageData?.data as Blob)
|
||||
}
|
||||
else {
|
||||
image = item.url
|
||||
image = item.url as string
|
||||
}
|
||||
|
||||
return image
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const image = await getImageStoreItem(props.imgItem)
|
||||
imgUrl.value = image
|
||||
imgUrl.value = await getImageStoreItem(props.imgItem)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
5
src/components/Loading/index.ts
Normal file
5
src/components/Loading/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { LoadingOptions } from './loading-context'
|
||||
import Loading from './index.vue'
|
||||
import { loadingKey, loadingState } from './loading-context'
|
||||
|
||||
export { Loading, loadingKey, LoadingOptions, loadingState }
|
||||
24
src/components/Loading/index.vue
Normal file
24
src/components/Loading/index.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup lang='ts'>
|
||||
import type { LoadingOptions } from './loading-context'
|
||||
import { inject } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { loadingKey } from './loading-context'
|
||||
|
||||
// 注入全局状态
|
||||
const loading = inject(loadingKey) as LoadingOptions
|
||||
|
||||
const { t } = useI18n()
|
||||
// 解构状态(响应式)
|
||||
const { visible, text } = loading
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="visible" class="fixed top-0 left-0 w-screen h-screen bg-[rgba(0,0,0,0.5)] flex flex-col gap-6 justify-center items-center z-50">
|
||||
<span v-if="visible" class="loading loading-spinner loading-xl" />
|
||||
<span>{{ text ? text : t('button.loading') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
61
src/components/Loading/loading-context.ts
Normal file
61
src/components/Loading/loading-context.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
// src/contexts/loading-context.ts
|
||||
import type { InjectionKey, Ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
// 定义 Loading 配置类型
|
||||
export interface LoadingOptions {
|
||||
visible: Ref<boolean>
|
||||
text: Ref<string>
|
||||
fullscreen: Ref<boolean>
|
||||
zIndex: Ref<number>
|
||||
count: Ref<number>
|
||||
show: (options?: Partial<{ text: string, fullscreen: boolean, zIndex: number }>) => void
|
||||
hide: () => void
|
||||
}
|
||||
|
||||
// 注入密钥(Symbol 确保唯一性)
|
||||
export const loadingKey: InjectionKey<LoadingOptions> = Symbol('loading')
|
||||
|
||||
// 全局状态(单例)
|
||||
const visible = ref(false)
|
||||
const text = ref('')
|
||||
const fullscreen = ref(true)
|
||||
const zIndex = ref(9999)
|
||||
const count = ref(0)
|
||||
|
||||
// 显示 Loading
|
||||
function show(options?: Partial<{ text: string, fullscreen: boolean, zIndex: number }>) {
|
||||
count.value++
|
||||
if (count.value > 1)
|
||||
return
|
||||
visible.value = true
|
||||
if (options) {
|
||||
text.value = options.text || ''
|
||||
fullscreen.value = options.fullscreen ?? true
|
||||
zIndex.value = options.zIndex || 9999
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏 Loading
|
||||
function hide() {
|
||||
if (count.value <= 0)
|
||||
return
|
||||
count.value--
|
||||
if (count.value === 0) {
|
||||
visible.value = false
|
||||
text.value = ''
|
||||
fullscreen.value = true
|
||||
zIndex.value = 9999
|
||||
}
|
||||
}
|
||||
|
||||
// 导出全局状态(供根组件提供)
|
||||
export const loadingState: LoadingOptions = {
|
||||
visible,
|
||||
text,
|
||||
fullscreen,
|
||||
zIndex,
|
||||
count,
|
||||
show,
|
||||
hide,
|
||||
}
|
||||
24
src/components/PageHeader/index.vue
Normal file
24
src/components/PageHeader/index.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup lang='ts'>
|
||||
const props = defineProps<{
|
||||
title: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header>
|
||||
<h1 class="text-lg leading-10">
|
||||
{{ props.title }}
|
||||
</h1>
|
||||
<div class="button-group my-4">
|
||||
<slot name="buttons" />
|
||||
</div>
|
||||
<div class="divider mt-0" />
|
||||
<div>
|
||||
<slot name="alerts" />
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,135 +0,0 @@
|
||||
<script setup lang='ts'>
|
||||
import useStore from '@/store'
|
||||
import localforage from 'localforage'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const audioDbStore = localforage.createInstance({
|
||||
name: 'audioStore',
|
||||
})
|
||||
const audio = ref(new Audio())
|
||||
const settingRef = ref()
|
||||
// const audio = ref(new Audio())
|
||||
const globalConfig = useStore().globalConfig
|
||||
const { getMusicList: localMusicList, getCurrentMusic: currentMusic } = storeToRefs(globalConfig)
|
||||
// const localMusicListValue = ref(localMusicList)
|
||||
|
||||
async function play(item: any) {
|
||||
if (!item) {
|
||||
return
|
||||
}
|
||||
// if (!audio.value.paused && !skip) {
|
||||
// audio.value.pause()
|
||||
|
||||
// return
|
||||
// }
|
||||
let audioUrl = ''
|
||||
if (!item.url) {
|
||||
return
|
||||
}
|
||||
if (item.url === 'Storage') {
|
||||
audioUrl = await audioDbStore.getItem(item.name) as string
|
||||
}
|
||||
else {
|
||||
audioUrl = item.url
|
||||
}
|
||||
audio.value.pause()
|
||||
audio.value.src = audioUrl
|
||||
audio.value.play()
|
||||
}
|
||||
function playMusic(item: any, skip = false) {
|
||||
if (!item) {
|
||||
return
|
||||
}
|
||||
if (!currentMusic.value.paused && !skip) {
|
||||
globalConfig.setCurrentMusic(item, true)
|
||||
|
||||
return
|
||||
}
|
||||
globalConfig.setCurrentMusic(item, false)
|
||||
}
|
||||
function nextPlay() {
|
||||
// 播放下一首
|
||||
if (localMusicList.value.length >= 1) {
|
||||
let index = localMusicList.value.findIndex((item: any) => item.name === currentMusic.value.item.name)
|
||||
index++
|
||||
if (index >= localMusicList.value.length) {
|
||||
index = 0
|
||||
}
|
||||
globalConfig.setCurrentMusic(localMusicList.value[index], false)
|
||||
}
|
||||
}
|
||||
// 监听播放成后开始下一首
|
||||
function onPlayEnd() {
|
||||
audio.value.addEventListener('ended', nextPlay)
|
||||
}
|
||||
|
||||
function enterConfig() {
|
||||
router.push('/log-lottery/config')
|
||||
}
|
||||
function enterHome() {
|
||||
router.push('/log-lottery')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
globalConfig.setCurrentMusic(localMusicList.value[0], true)
|
||||
onPlayEnd()
|
||||
// 不使用空格控制audio
|
||||
})
|
||||
onUnmounted(() => {
|
||||
audio.value.removeEventListener('ended', nextPlay)
|
||||
})
|
||||
watch(currentMusic, (val: any) => {
|
||||
if (!val.paused && audio.value) {
|
||||
play(val.item)
|
||||
}
|
||||
else {
|
||||
audio.value.pause()
|
||||
}
|
||||
}, { deep: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="settingRef" class="flex flex-col gap-3">
|
||||
<div v-if="route.path.includes('/config')" class="tooltip tooltip-left" :data-tip="t('tooltip.toHome')">
|
||||
<div
|
||||
class="flex items-center justify-center w-10 h-10 p-0 m-0 cursor-pointer setting-container bg-slate-500/50 rounded-l-xl hover:bg-slate-500/80 hover:text-blue-400/90"
|
||||
@click="enterHome"
|
||||
>
|
||||
<svg-icon name="home" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="tooltip tooltip-left" :data-tip="t('tooltip.settingConfiguration')">
|
||||
<div
|
||||
class="flex items-center justify-center w-10 h-10 p-0 m-0 cursor-pointer setting-container bg-slate-500/50 rounded-l-xl hover:bg-slate-500/80 hover:text-blue-400/90"
|
||||
@click="enterConfig"
|
||||
>
|
||||
<svg-icon name="setting" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tooltip tooltip-left" :data-tip="currentMusic.item ? `${currentMusic.item.name}\n\r ${t('tooltip.nextSong')}` : t('tooltip.noSongPlay')">
|
||||
<div
|
||||
class="flex items-center justify-center w-10 h-10 p-0 m-0 cursor-pointer setting-container bg-slate-500/50 rounded-l-xl hover:bg-slate-500/80 hover:text-blue-400/90"
|
||||
@click="playMusic(currentMusic.item)" @click.right.prevent="nextPlay"
|
||||
>
|
||||
<svg-icon :name="currentMusic.paused ? 'play' : 'pause'" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
details {
|
||||
|
||||
// display: none;
|
||||
summary {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,71 +0,0 @@
|
||||
<script setup lang='ts'>
|
||||
import Sparticles from 'sparticles';
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { useElementSize } from '@vueuse/core';
|
||||
import localforage from 'localforage'
|
||||
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);
|
||||
let options = ref({ shape: 'star', parallax: 1.2, rotate: true, twinkle: true, speed: 10, count: 200 });
|
||||
function addSparticles(node: any, width: number, height: number) {
|
||||
new Sparticles(node, options.value, width, height);
|
||||
}
|
||||
// 页面大小改变时
|
||||
const listenWindowSize = () => {
|
||||
window.addEventListener('resize', () => {
|
||||
if (width.value && height.value) {
|
||||
addSparticles(starRef.value, width.value, height.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const getImageStoreItem = async (item: any): Promise<string> => {
|
||||
let image = ''
|
||||
if (item.url == 'Storage') {
|
||||
const key = item.id;
|
||||
image = await imageDbStore.getItem(key) as string
|
||||
}
|
||||
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 class="home-background w-screen h-screen overflow-hidden" v-if="homeBackground.url">
|
||||
<img :src="imgUrl" class="w-full h-full object-cover" alt="">
|
||||
</div>
|
||||
<div v-else class="w-screen h-screen overflow-hidden" ref="starRef">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
|
||||
</style>
|
||||
@@ -36,8 +36,8 @@ const symbolId = computed(() => `#${props.prefix}-${props.name}`)
|
||||
|
||||
<style scoped>
|
||||
.svg-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
/* width: 24px;
|
||||
height: 24px; */
|
||||
fill: currentColor;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup lang='ts'>
|
||||
|
||||
import { ChevronUp } from 'lucide-vue-next'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="fixed z-50 flex items-center justify-center w-10 h-10 rounded-full shadow-lg cursor-pointer right-12 bottom-12 bg-slate-700 hover:bg-slate-600">
|
||||
<svg-icon name="toTop" />
|
||||
<ChevronUp />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
113
src/components/Waterfall/index.vue
Normal file
113
src/components/Waterfall/index.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<script setup lang="ts">
|
||||
import type { Ref } from 'vue'
|
||||
import { throttle } from 'lodash-es' // lodash-es 节流
|
||||
import Masonry from 'masonry-layout'
|
||||
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
// 布局参数 Props
|
||||
interface MasonryWaterfallProps {
|
||||
columnWidth?: number | string // 列宽(px/选择器)
|
||||
gutter?: number // 列/行间距
|
||||
fitWidth?: boolean // 容器宽度自适应居中
|
||||
}
|
||||
|
||||
// 默认参数
|
||||
const props = withDefaults(defineProps<MasonryWaterfallProps>(), {
|
||||
columnWidth: 120,
|
||||
gutter: 16,
|
||||
fitWidth: true,
|
||||
})
|
||||
|
||||
// Vue Ref 管理 DOM 容器和 masonry 实例
|
||||
const masonryContainer: Ref<HTMLDivElement | null> = ref(null)
|
||||
const masonryInstance: Ref<Masonry | null> = ref(null)
|
||||
|
||||
// 初始化 masonry(仅执行一次,因卡片固定)
|
||||
async function initMasonry() {
|
||||
if (!masonryContainer.value)
|
||||
return
|
||||
|
||||
// 等待插槽内容(固定卡片)完全渲染
|
||||
await nextTick()
|
||||
|
||||
// 初始化 masonry 实例(固定卡片无需销毁旧实例)
|
||||
masonryInstance.value = new Masonry(masonryContainer.value, {
|
||||
itemSelector: '.masonry-container > *', // 匹配所有固定子项
|
||||
columnWidth: props.columnWidth,
|
||||
gutter: props.gutter,
|
||||
fitWidth: props.fitWidth,
|
||||
initLayout: true, // 固定卡片直接初始化布局
|
||||
})
|
||||
}
|
||||
|
||||
// 刷新布局(仅用于卡片内部内容高度变化)
|
||||
async function refreshLayout() {
|
||||
await nextTick()
|
||||
if (masonryInstance.value) {
|
||||
masonryInstance.value.layout?.()
|
||||
}
|
||||
}
|
||||
|
||||
// 窗口缩放节流重排(优化性能)
|
||||
const handleResize = throttle(() => {
|
||||
if (masonryInstance.value) {
|
||||
masonryInstance.value.layout?.()
|
||||
}
|
||||
}, 300)
|
||||
|
||||
// 生命周期:挂载时初始化,卸载时清理
|
||||
onMounted(async () => {
|
||||
await initMasonry()
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 销毁实例 + 释放内存
|
||||
if (masonryInstance.value) {
|
||||
masonryInstance.value.destroy?.()
|
||||
masonryInstance.value = null
|
||||
}
|
||||
// 移除监听 + 取消节流任务
|
||||
window.removeEventListener('resize', handleResize)
|
||||
handleResize.cancel()
|
||||
})
|
||||
|
||||
// 仅暴露刷新方法(适配卡片内部内容变化)
|
||||
defineExpose({ refreshLayout })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- masonry 容器:ref 绑定,接收固定插槽内容 -->
|
||||
<div ref="masonryContainer" class="masonry-container">
|
||||
<!-- 插槽:直接传入固定的卡片/组件 -->
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.masonry-container {
|
||||
width: 100%;
|
||||
/* max-width: 1400px; */
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 固定卡片基础样式 */
|
||||
.masonry-container > * {
|
||||
margin-bottom: v-bind('`${gutter}px`');
|
||||
box-sizing: border-box;
|
||||
break-inside: avoid; /* 兼容 Safari */
|
||||
min-height: 100px; /* 避免内容过矮导致布局异常 */
|
||||
}
|
||||
|
||||
/* 响应式适配:小屏调整列宽 */
|
||||
/* @media (max-width: 768px) {
|
||||
.masonry-container {
|
||||
padding: 10px;
|
||||
}
|
||||
.masonry-container > * {
|
||||
width: calc(50% - v-bind('`${gutter}px`')) !important;
|
||||
}
|
||||
} */
|
||||
</style>
|
||||
29
src/components/ui/button/Button.vue
Normal file
29
src/components/ui/button/Button.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import type { PrimitiveProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import type { ButtonVariants } from "."
|
||||
import { Primitive } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "."
|
||||
|
||||
interface Props extends PrimitiveProps {
|
||||
variant?: ButtonVariants["variant"]
|
||||
size?: ButtonVariants["size"]
|
||||
class?: HTMLAttributes["class"]
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
as: "button",
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
data-slot="button"
|
||||
:as="as"
|
||||
:as-child="asChild"
|
||||
:class="cn(buttonVariants({ variant, size }), props.class)"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
</template>
|
||||
38
src/components/ui/button/index.ts
Normal file
38
src/components/ui/button/index.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { VariantProps } from "class-variance-authority"
|
||||
import { cva } from "class-variance-authority"
|
||||
|
||||
export { default as Button } from "./Button.vue"
|
||||
|
||||
export const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
"default": "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
"sm": "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
"lg": "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
"icon": "size-9",
|
||||
"icon-sm": "size-8",
|
||||
"icon-lg": "size-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
)
|
||||
export type ButtonVariants = VariantProps<typeof buttonVariants>
|
||||
87
src/components/ui/command/Command.vue
Normal file
87
src/components/ui/command/Command.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import type { ListboxRootEmits, ListboxRootProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { ListboxRoot, useFilter, useForwardPropsEmits } from "reka-ui"
|
||||
import { reactive, ref, watch } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { provideCommandContext } from "."
|
||||
|
||||
const props = withDefaults(defineProps<ListboxRootProps & { class?: HTMLAttributes["class"] }>(), {
|
||||
modelValue: "",
|
||||
})
|
||||
|
||||
const emits = defineEmits<ListboxRootEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
|
||||
const allItems = ref<Map<string, string>>(new Map())
|
||||
const allGroups = ref<Map<string, Set<string>>>(new Map())
|
||||
|
||||
const { contains } = useFilter({ sensitivity: "base" })
|
||||
const filterState = reactive({
|
||||
search: "",
|
||||
filtered: {
|
||||
/** The count of all visible items. */
|
||||
count: 0,
|
||||
/** Map from visible item id to its search score. */
|
||||
items: new Map() as Map<string, number>,
|
||||
/** Set of groups with at least one visible item. */
|
||||
groups: new Set() as Set<string>,
|
||||
},
|
||||
})
|
||||
|
||||
function filterItems() {
|
||||
if (!filterState.search) {
|
||||
filterState.filtered.count = allItems.value.size
|
||||
// Do nothing, each item will know to show itself because search is empty
|
||||
return
|
||||
}
|
||||
|
||||
// Reset the groups
|
||||
filterState.filtered.groups = new Set()
|
||||
let itemCount = 0
|
||||
|
||||
// Check which items should be included
|
||||
for (const [id, value] of allItems.value) {
|
||||
const score = contains(value, filterState.search)
|
||||
filterState.filtered.items.set(id, score ? 1 : 0)
|
||||
if (score)
|
||||
itemCount++
|
||||
}
|
||||
|
||||
// Check which groups have at least 1 item shown
|
||||
for (const [groupId, group] of allGroups.value) {
|
||||
for (const itemId of group) {
|
||||
if (filterState.filtered.items.get(itemId)! > 0) {
|
||||
filterState.filtered.groups.add(groupId)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filterState.filtered.count = itemCount
|
||||
}
|
||||
|
||||
watch(() => filterState.search, () => {
|
||||
filterItems()
|
||||
})
|
||||
|
||||
provideCommandContext({
|
||||
allItems,
|
||||
allGroups,
|
||||
filterState,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ListboxRoot
|
||||
data-slot="command"
|
||||
v-bind="forwarded"
|
||||
:class="cn('bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</ListboxRoot>
|
||||
</template>
|
||||
31
src/components/ui/command/CommandDialog.vue
Normal file
31
src/components/ui/command/CommandDialog.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogRootEmits, DialogRootProps } from "reka-ui"
|
||||
import { useForwardPropsEmits } from "reka-ui"
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import Command from "./Command.vue"
|
||||
|
||||
const props = withDefaults(defineProps<DialogRootProps & {
|
||||
title?: string
|
||||
description?: string
|
||||
}>(), {
|
||||
title: "Command Palette",
|
||||
description: "Search for a command to run...",
|
||||
})
|
||||
const emits = defineEmits<DialogRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-slot="slotProps" v-bind="forwarded">
|
||||
<DialogContent class="overflow-hidden p-0 ">
|
||||
<DialogHeader class="sr-only">
|
||||
<DialogTitle>{{ title }}</DialogTitle>
|
||||
<DialogDescription>{{ description }}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Command>
|
||||
<slot v-bind="slotProps" />
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
27
src/components/ui/command/CommandEmpty.vue
Normal file
27
src/components/ui/command/CommandEmpty.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import type { PrimitiveProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { Primitive } from "reka-ui"
|
||||
import { computed } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useCommand } from "."
|
||||
|
||||
const props = defineProps<PrimitiveProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const { filterState } = useCommand()
|
||||
const isRender = computed(() => !!filterState.search && filterState.filtered.count === 0,
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
v-if="isRender"
|
||||
data-slot="command-empty"
|
||||
v-bind="delegatedProps" :class="cn('py-6 text-center text-sm', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
</template>
|
||||
45
src/components/ui/command/CommandGroup.vue
Normal file
45
src/components/ui/command/CommandGroup.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import type { ListboxGroupProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { ListboxGroup, ListboxGroupLabel, useId } from "reka-ui"
|
||||
import { computed, onMounted, onUnmounted } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { provideCommandGroupContext, useCommand } from "."
|
||||
|
||||
const props = defineProps<ListboxGroupProps & {
|
||||
class?: HTMLAttributes["class"]
|
||||
heading?: string
|
||||
}>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const { allGroups, filterState } = useCommand()
|
||||
const id = useId()
|
||||
|
||||
const isRender = computed(() => !filterState.search ? true : filterState.filtered.groups.has(id))
|
||||
|
||||
provideCommandGroupContext({ id })
|
||||
onMounted(() => {
|
||||
if (!allGroups.value.has(id))
|
||||
allGroups.value.set(id, new Set())
|
||||
})
|
||||
onUnmounted(() => {
|
||||
allGroups.value.delete(id)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ListboxGroup
|
||||
v-bind="delegatedProps"
|
||||
:id="id"
|
||||
data-slot="command-group"
|
||||
:class="cn('text-foreground overflow-hidden p-1', props.class)"
|
||||
:hidden="isRender ? undefined : true"
|
||||
>
|
||||
<ListboxGroupLabel v-if="heading" data-slot="command-group-heading" class="px-2 py-1.5 text-xs font-medium text-muted-foreground">
|
||||
{{ heading }}
|
||||
</ListboxGroupLabel>
|
||||
<slot />
|
||||
</ListboxGroup>
|
||||
</template>
|
||||
39
src/components/ui/command/CommandInput.vue
Normal file
39
src/components/ui/command/CommandInput.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import type { ListboxFilterProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { Search } from "lucide-vue-next"
|
||||
import { ListboxFilter, useForwardProps } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useCommand } from "."
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const props = defineProps<ListboxFilterProps & {
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
|
||||
const { filterState } = useCommand()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="command-input-wrapper"
|
||||
class="flex h-9 items-center gap-2 border-b px-3"
|
||||
>
|
||||
<Search class="size-4 shrink-0 opacity-50" />
|
||||
<ListboxFilter
|
||||
v-bind="{ ...forwardedProps, ...$attrs }"
|
||||
v-model="filterState.search"
|
||||
data-slot="command-input"
|
||||
auto-focus
|
||||
:class="cn('placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50', props.class)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
76
src/components/ui/command/CommandItem.vue
Normal file
76
src/components/ui/command/CommandItem.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<script setup lang="ts">
|
||||
import type { ListboxItemEmits, ListboxItemProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit, useCurrentElement } from "@vueuse/core"
|
||||
import { ListboxItem, useForwardPropsEmits, useId } from "reka-ui"
|
||||
import { computed, onMounted, onUnmounted, ref } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useCommand, useCommandGroup } from "."
|
||||
|
||||
const props = defineProps<ListboxItemProps & { class?: HTMLAttributes["class"] }>()
|
||||
const emits = defineEmits<ListboxItemEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
|
||||
const id = useId()
|
||||
const { filterState, allItems, allGroups } = useCommand()
|
||||
const groupContext = useCommandGroup()
|
||||
|
||||
const isRender = computed(() => {
|
||||
if (!filterState.search) {
|
||||
return true
|
||||
}
|
||||
else {
|
||||
const filteredCurrentItem = filterState.filtered.items.get(id)
|
||||
// If the filtered items is undefined means not in the all times map yet
|
||||
// Do the first render to add into the map
|
||||
if (filteredCurrentItem === undefined) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check with filter
|
||||
return filteredCurrentItem > 0
|
||||
}
|
||||
})
|
||||
|
||||
const itemRef = ref()
|
||||
const currentElement = useCurrentElement(itemRef)
|
||||
onMounted(() => {
|
||||
if (!(currentElement.value instanceof HTMLElement))
|
||||
return
|
||||
|
||||
// textValue to perform filter
|
||||
allItems.value.set(id, currentElement.value.textContent ?? (props.value?.toString() ?? ""))
|
||||
|
||||
const groupId = groupContext?.id
|
||||
if (groupId) {
|
||||
if (!allGroups.value.has(groupId)) {
|
||||
allGroups.value.set(groupId, new Set([id]))
|
||||
}
|
||||
else {
|
||||
allGroups.value.get(groupId)?.add(id)
|
||||
}
|
||||
}
|
||||
})
|
||||
onUnmounted(() => {
|
||||
allItems.value.delete(id)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ListboxItem
|
||||
v-if="isRender"
|
||||
v-bind="forwarded"
|
||||
:id="id"
|
||||
ref="itemRef"
|
||||
data-slot="command-item"
|
||||
:class="cn('data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4', props.class)"
|
||||
@select="() => {
|
||||
filterState.search = ''
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
</ListboxItem>
|
||||
</template>
|
||||
25
src/components/ui/command/CommandList.vue
Normal file
25
src/components/ui/command/CommandList.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import type { ListboxContentProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { ListboxContent, useForwardProps } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<ListboxContentProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ListboxContent
|
||||
data-slot="command-list"
|
||||
v-bind="forwarded"
|
||||
:class="cn('max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto', props.class)"
|
||||
>
|
||||
<div role="presentation">
|
||||
<slot />
|
||||
</div>
|
||||
</ListboxContent>
|
||||
</template>
|
||||
21
src/components/ui/command/CommandSeparator.vue
Normal file
21
src/components/ui/command/CommandSeparator.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import type { SeparatorProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { Separator } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<SeparatorProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Separator
|
||||
data-slot="command-separator"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('bg-border -mx-1 h-px', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</Separator>
|
||||
</template>
|
||||
17
src/components/ui/command/CommandShortcut.vue
Normal file
17
src/components/ui/command/CommandShortcut.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
data-slot="command-shortcut"
|
||||
:class="cn('text-muted-foreground ml-auto text-xs tracking-widest', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
25
src/components/ui/command/index.ts
Normal file
25
src/components/ui/command/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { Ref } from "vue"
|
||||
import { createContext } from "reka-ui"
|
||||
|
||||
export { default as Command } from "./Command.vue"
|
||||
export { default as CommandDialog } from "./CommandDialog.vue"
|
||||
export { default as CommandEmpty } from "./CommandEmpty.vue"
|
||||
export { default as CommandGroup } from "./CommandGroup.vue"
|
||||
export { default as CommandInput } from "./CommandInput.vue"
|
||||
export { default as CommandItem } from "./CommandItem.vue"
|
||||
export { default as CommandList } from "./CommandList.vue"
|
||||
export { default as CommandSeparator } from "./CommandSeparator.vue"
|
||||
export { default as CommandShortcut } from "./CommandShortcut.vue"
|
||||
|
||||
export const [useCommand, provideCommandContext] = createContext<{
|
||||
allItems: Ref<Map<string, string>>
|
||||
allGroups: Ref<Map<string, Set<string>>>
|
||||
filterState: {
|
||||
search: string
|
||||
filtered: { count: number, items: Map<string, number>, groups: Set<string> }
|
||||
}
|
||||
}>("Command")
|
||||
|
||||
export const [useCommandGroup, provideCommandGroupContext] = createContext<{
|
||||
id?: string
|
||||
}>("CommandGroup")
|
||||
19
src/components/ui/dialog/Dialog.vue
Normal file
19
src/components/ui/dialog/Dialog.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogRootEmits, DialogRootProps } from "reka-ui"
|
||||
import { DialogRoot, useForwardPropsEmits } from "reka-ui"
|
||||
|
||||
const props = defineProps<DialogRootProps>()
|
||||
const emits = defineEmits<DialogRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogRoot
|
||||
v-slot="slotProps"
|
||||
data-slot="dialog"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot v-bind="slotProps" />
|
||||
</DialogRoot>
|
||||
</template>
|
||||
15
src/components/ui/dialog/DialogClose.vue
Normal file
15
src/components/ui/dialog/DialogClose.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogCloseProps } from "reka-ui"
|
||||
import { DialogClose } from "reka-ui"
|
||||
|
||||
const props = defineProps<DialogCloseProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogClose
|
||||
data-slot="dialog-close"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</DialogClose>
|
||||
</template>
|
||||
53
src/components/ui/dialog/DialogContent.vue
Normal file
53
src/components/ui/dialog/DialogContent.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { X } from "lucide-vue-next"
|
||||
import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogPortal,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
import DialogOverlay from "./DialogOverlay.vue"
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const props = withDefaults(defineProps<DialogContentProps & { class?: HTMLAttributes["class"], showCloseButton?: boolean }>(), {
|
||||
showCloseButton: true,
|
||||
})
|
||||
const emits = defineEmits<DialogContentEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogContent
|
||||
data-slot="dialog-content"
|
||||
v-bind="{ ...$attrs, ...forwarded }"
|
||||
:class="
|
||||
cn(
|
||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<slot />
|
||||
|
||||
<DialogClose
|
||||
v-if="showCloseButton"
|
||||
data-slot="dialog-close"
|
||||
class="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<X />
|
||||
<span class="sr-only">Close</span>
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</template>
|
||||
23
src/components/ui/dialog/DialogDescription.vue
Normal file
23
src/components/ui/dialog/DialogDescription.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogDescriptionProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { DialogDescription, useForwardProps } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogDescription
|
||||
data-slot="dialog-description"
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('text-muted-foreground text-sm', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DialogDescription>
|
||||
</template>
|
||||
15
src/components/ui/dialog/DialogFooter.vue
Normal file
15
src/components/ui/dialog/DialogFooter.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
:class="cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
17
src/components/ui/dialog/DialogHeader.vue
Normal file
17
src/components/ui/dialog/DialogHeader.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
:class="cn('flex flex-col gap-2 text-center sm:text-left', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
21
src/components/ui/dialog/DialogOverlay.vue
Normal file
21
src/components/ui/dialog/DialogOverlay.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogOverlayProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { DialogOverlay } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<DialogOverlayProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogOverlay
|
||||
data-slot="dialog-overlay"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DialogOverlay>
|
||||
</template>
|
||||
59
src/components/ui/dialog/DialogScrollContent.vue
Normal file
59
src/components/ui/dialog/DialogScrollContent.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { X } from "lucide-vue-next"
|
||||
import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const props = defineProps<DialogContentProps & { class?: HTMLAttributes["class"] }>()
|
||||
const emits = defineEmits<DialogContentEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogPortal>
|
||||
<DialogOverlay
|
||||
class="fixed inset-0 z-50 grid place-items-center overflow-y-auto bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
|
||||
>
|
||||
<DialogContent
|
||||
:class="
|
||||
cn(
|
||||
'relative z-50 grid w-full max-w-lg my-8 gap-4 border border-border bg-background p-6 shadow-lg duration-200 sm:rounded-lg md:w-full',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
v-bind="{ ...$attrs, ...forwarded }"
|
||||
@pointer-down-outside="(event) => {
|
||||
const originalEvent = event.detail.originalEvent;
|
||||
const target = originalEvent.target as HTMLElement;
|
||||
if (originalEvent.offsetX > target.clientWidth || originalEvent.offsetY > target.clientHeight) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
|
||||
<DialogClose
|
||||
class="absolute top-4 right-4 p-0.5 transition-colors rounded-md hover:bg-secondary"
|
||||
>
|
||||
<X class="w-4 h-4" />
|
||||
<span class="sr-only">Close</span>
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
</DialogOverlay>
|
||||
</DialogPortal>
|
||||
</template>
|
||||
23
src/components/ui/dialog/DialogTitle.vue
Normal file
23
src/components/ui/dialog/DialogTitle.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogTitleProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { DialogTitle, useForwardProps } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogTitle
|
||||
data-slot="dialog-title"
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('text-lg leading-none font-semibold', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DialogTitle>
|
||||
</template>
|
||||
15
src/components/ui/dialog/DialogTrigger.vue
Normal file
15
src/components/ui/dialog/DialogTrigger.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogTriggerProps } from "reka-ui"
|
||||
import { DialogTrigger } from "reka-ui"
|
||||
|
||||
const props = defineProps<DialogTriggerProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogTrigger
|
||||
data-slot="dialog-trigger"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</DialogTrigger>
|
||||
</template>
|
||||
10
src/components/ui/dialog/index.ts
Normal file
10
src/components/ui/dialog/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export { default as Dialog } from "./Dialog.vue"
|
||||
export { default as DialogClose } from "./DialogClose.vue"
|
||||
export { default as DialogContent } from "./DialogContent.vue"
|
||||
export { default as DialogDescription } from "./DialogDescription.vue"
|
||||
export { default as DialogFooter } from "./DialogFooter.vue"
|
||||
export { default as DialogHeader } from "./DialogHeader.vue"
|
||||
export { default as DialogOverlay } from "./DialogOverlay.vue"
|
||||
export { default as DialogScrollContent } from "./DialogScrollContent.vue"
|
||||
export { default as DialogTitle } from "./DialogTitle.vue"
|
||||
export { default as DialogTrigger } from "./DialogTrigger.vue"
|
||||
19
src/components/ui/dropdown-menu/DropdownMenu.vue
Normal file
19
src/components/ui/dropdown-menu/DropdownMenu.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuRootEmits, DropdownMenuRootProps } from "reka-ui"
|
||||
import { DropdownMenuRoot, useForwardPropsEmits } from "reka-ui"
|
||||
|
||||
const props = defineProps<DropdownMenuRootProps>()
|
||||
const emits = defineEmits<DropdownMenuRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuRoot
|
||||
v-slot="slotProps"
|
||||
data-slot="dropdown-menu"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot v-bind="slotProps" />
|
||||
</DropdownMenuRoot>
|
||||
</template>
|
||||
39
src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue
Normal file
39
src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuCheckboxItemEmits, DropdownMenuCheckboxItemProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { Check } from "lucide-vue-next"
|
||||
import {
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuItemIndicator,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<DropdownMenuCheckboxItemProps & { class?: HTMLAttributes["class"] }>()
|
||||
const emits = defineEmits<DropdownMenuCheckboxItemEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuCheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
v-bind="forwarded"
|
||||
:class=" cn(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuItemIndicator>
|
||||
<slot name="indicator-icon">
|
||||
<Check class="size-4" />
|
||||
</slot>
|
||||
</DropdownMenuItemIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
</DropdownMenuCheckboxItem>
|
||||
</template>
|
||||
39
src/components/ui/dropdown-menu/DropdownMenuContent.vue
Normal file
39
src/components/ui/dropdown-menu/DropdownMenuContent.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuContentEmits, DropdownMenuContentProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuPortal,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<DropdownMenuContentProps & { class?: HTMLAttributes["class"] }>(),
|
||||
{
|
||||
sideOffset: 4,
|
||||
},
|
||||
)
|
||||
const emits = defineEmits<DropdownMenuContentEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent
|
||||
data-slot="dropdown-menu-content"
|
||||
v-bind="{ ...$attrs, ...forwarded }"
|
||||
:class="cn('bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--reka-dropdown-menu-content-available-height) min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</template>
|
||||
15
src/components/ui/dropdown-menu/DropdownMenuGroup.vue
Normal file
15
src/components/ui/dropdown-menu/DropdownMenuGroup.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuGroupProps } from "reka-ui"
|
||||
import { DropdownMenuGroup } from "reka-ui"
|
||||
|
||||
const props = defineProps<DropdownMenuGroupProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuGroup
|
||||
data-slot="dropdown-menu-group"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuGroup>
|
||||
</template>
|
||||
31
src/components/ui/dropdown-menu/DropdownMenuItem.vue
Normal file
31
src/components/ui/dropdown-menu/DropdownMenuItem.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuItemProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { DropdownMenuItem, useForwardProps } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = withDefaults(defineProps<DropdownMenuItemProps & {
|
||||
class?: HTMLAttributes["class"]
|
||||
inset?: boolean
|
||||
variant?: "default" | "destructive"
|
||||
}>(), {
|
||||
variant: "default",
|
||||
})
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "inset", "variant", "class")
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuItem
|
||||
data-slot="dropdown-menu-item"
|
||||
:data-inset="inset ? '' : undefined"
|
||||
:data-variant="variant"
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuItem>
|
||||
</template>
|
||||
23
src/components/ui/dropdown-menu/DropdownMenuLabel.vue
Normal file
23
src/components/ui/dropdown-menu/DropdownMenuLabel.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuLabelProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { DropdownMenuLabel, useForwardProps } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<DropdownMenuLabelProps & { class?: HTMLAttributes["class"], inset?: boolean }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class", "inset")
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuLabel
|
||||
data-slot="dropdown-menu-label"
|
||||
:data-inset="inset ? '' : undefined"
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('px-2 py-1.5 text-sm font-medium data-[inset]:pl-8', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuLabel>
|
||||
</template>
|
||||
21
src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue
Normal file
21
src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuRadioGroupEmits, DropdownMenuRadioGroupProps } from "reka-ui"
|
||||
import {
|
||||
DropdownMenuRadioGroup,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui"
|
||||
|
||||
const props = defineProps<DropdownMenuRadioGroupProps>()
|
||||
const emits = defineEmits<DropdownMenuRadioGroupEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuRadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuRadioGroup>
|
||||
</template>
|
||||
40
src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue
Normal file
40
src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuRadioItemEmits, DropdownMenuRadioItemProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { Circle } from "lucide-vue-next"
|
||||
import {
|
||||
DropdownMenuItemIndicator,
|
||||
DropdownMenuRadioItem,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<DropdownMenuRadioItemProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const emits = defineEmits<DropdownMenuRadioItemEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuRadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
v-bind="forwarded"
|
||||
:class="cn(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuItemIndicator>
|
||||
<slot name="indicator-icon">
|
||||
<Circle class="size-2 fill-current" />
|
||||
</slot>
|
||||
</DropdownMenuItemIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
</DropdownMenuRadioItem>
|
||||
</template>
|
||||
23
src/components/ui/dropdown-menu/DropdownMenuSeparator.vue
Normal file
23
src/components/ui/dropdown-menu/DropdownMenuSeparator.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuSeparatorProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import {
|
||||
DropdownMenuSeparator,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<DropdownMenuSeparatorProps & {
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuSeparator
|
||||
data-slot="dropdown-menu-separator"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('bg-border -mx-1 my-1 h-px', props.class)"
|
||||
/>
|
||||
</template>
|
||||
17
src/components/ui/dropdown-menu/DropdownMenuShortcut.vue
Normal file
17
src/components/ui/dropdown-menu/DropdownMenuShortcut.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
:class="cn('text-muted-foreground ml-auto text-xs tracking-widest', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
18
src/components/ui/dropdown-menu/DropdownMenuSub.vue
Normal file
18
src/components/ui/dropdown-menu/DropdownMenuSub.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuSubEmits, DropdownMenuSubProps } from "reka-ui"
|
||||
import {
|
||||
DropdownMenuSub,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui"
|
||||
|
||||
const props = defineProps<DropdownMenuSubProps>()
|
||||
const emits = defineEmits<DropdownMenuSubEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuSub v-slot="slotProps" data-slot="dropdown-menu-sub" v-bind="forwarded">
|
||||
<slot v-bind="slotProps" />
|
||||
</DropdownMenuSub>
|
||||
</template>
|
||||
27
src/components/ui/dropdown-menu/DropdownMenuSubContent.vue
Normal file
27
src/components/ui/dropdown-menu/DropdownMenuSubContent.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuSubContentEmits, DropdownMenuSubContentProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import {
|
||||
DropdownMenuSubContent,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes["class"] }>()
|
||||
const emits = defineEmits<DropdownMenuSubContentEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuSubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
v-bind="forwarded"
|
||||
:class="cn('bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuSubContent>
|
||||
</template>
|
||||
30
src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue
Normal file
30
src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuSubTriggerProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { ChevronRight } from "lucide-vue-next"
|
||||
import {
|
||||
DropdownMenuSubTrigger,
|
||||
useForwardProps,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<DropdownMenuSubTriggerProps & { class?: HTMLAttributes["class"], inset?: boolean }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class", "inset")
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuSubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
v-bind="forwardedProps"
|
||||
:class="cn(
|
||||
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<slot />
|
||||
<ChevronRight class="ml-auto size-4" />
|
||||
</DropdownMenuSubTrigger>
|
||||
</template>
|
||||
17
src/components/ui/dropdown-menu/DropdownMenuTrigger.vue
Normal file
17
src/components/ui/dropdown-menu/DropdownMenuTrigger.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuTriggerProps } from "reka-ui"
|
||||
import { DropdownMenuTrigger, useForwardProps } from "reka-ui"
|
||||
|
||||
const props = defineProps<DropdownMenuTriggerProps>()
|
||||
|
||||
const forwardedProps = useForwardProps(props)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuTrigger
|
||||
data-slot="dropdown-menu-trigger"
|
||||
v-bind="forwardedProps"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuTrigger>
|
||||
</template>
|
||||
16
src/components/ui/dropdown-menu/index.ts
Normal file
16
src/components/ui/dropdown-menu/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export { default as DropdownMenu } from "./DropdownMenu.vue"
|
||||
|
||||
export { default as DropdownMenuCheckboxItem } from "./DropdownMenuCheckboxItem.vue"
|
||||
export { default as DropdownMenuContent } from "./DropdownMenuContent.vue"
|
||||
export { default as DropdownMenuGroup } from "./DropdownMenuGroup.vue"
|
||||
export { default as DropdownMenuItem } from "./DropdownMenuItem.vue"
|
||||
export { default as DropdownMenuLabel } from "./DropdownMenuLabel.vue"
|
||||
export { default as DropdownMenuRadioGroup } from "./DropdownMenuRadioGroup.vue"
|
||||
export { default as DropdownMenuRadioItem } from "./DropdownMenuRadioItem.vue"
|
||||
export { default as DropdownMenuSeparator } from "./DropdownMenuSeparator.vue"
|
||||
export { default as DropdownMenuShortcut } from "./DropdownMenuShortcut.vue"
|
||||
export { default as DropdownMenuSub } from "./DropdownMenuSub.vue"
|
||||
export { default as DropdownMenuSubContent } from "./DropdownMenuSubContent.vue"
|
||||
export { default as DropdownMenuSubTrigger } from "./DropdownMenuSubTrigger.vue"
|
||||
export { default as DropdownMenuTrigger } from "./DropdownMenuTrigger.vue"
|
||||
export { DropdownMenuPortal } from "reka-ui"
|
||||
19
src/components/ui/popover/Popover.vue
Normal file
19
src/components/ui/popover/Popover.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import type { PopoverRootEmits, PopoverRootProps } from "reka-ui"
|
||||
import { PopoverRoot, useForwardPropsEmits } from "reka-ui"
|
||||
|
||||
const props = defineProps<PopoverRootProps>()
|
||||
const emits = defineEmits<PopoverRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PopoverRoot
|
||||
v-slot="slotProps"
|
||||
data-slot="popover"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot v-bind="slotProps" />
|
||||
</PopoverRoot>
|
||||
</template>
|
||||
15
src/components/ui/popover/PopoverAnchor.vue
Normal file
15
src/components/ui/popover/PopoverAnchor.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { PopoverAnchorProps } from "reka-ui"
|
||||
import { PopoverAnchor } from "reka-ui"
|
||||
|
||||
const props = defineProps<PopoverAnchorProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PopoverAnchor
|
||||
data-slot="popover-anchor"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</PopoverAnchor>
|
||||
</template>
|
||||
45
src/components/ui/popover/PopoverContent.vue
Normal file
45
src/components/ui/popover/PopoverContent.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import type { PopoverContentEmits, PopoverContentProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import {
|
||||
PopoverContent,
|
||||
PopoverPortal,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<PopoverContentProps & { class?: HTMLAttributes["class"] }>(),
|
||||
{
|
||||
align: "center",
|
||||
sideOffset: 4,
|
||||
},
|
||||
)
|
||||
const emits = defineEmits<PopoverContentEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PopoverPortal>
|
||||
<PopoverContent
|
||||
data-slot="popover-content"
|
||||
v-bind="{ ...$attrs, ...forwarded }"
|
||||
:class="
|
||||
cn(
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-md border p-4 shadow-md origin-(--reka-popover-content-transform-origin) outline-hidden',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</PopoverContent>
|
||||
</PopoverPortal>
|
||||
</template>
|
||||
15
src/components/ui/popover/PopoverTrigger.vue
Normal file
15
src/components/ui/popover/PopoverTrigger.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { PopoverTriggerProps } from "reka-ui"
|
||||
import { PopoverTrigger } from "reka-ui"
|
||||
|
||||
const props = defineProps<PopoverTriggerProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PopoverTrigger
|
||||
data-slot="popover-trigger"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</PopoverTrigger>
|
||||
</template>
|
||||
4
src/components/ui/popover/index.ts
Normal file
4
src/components/ui/popover/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default as Popover } from "./Popover.vue"
|
||||
export { default as PopoverAnchor } from "./PopoverAnchor.vue"
|
||||
export { default as PopoverContent } from "./PopoverContent.vue"
|
||||
export { default as PopoverTrigger } from "./PopoverTrigger.vue"
|
||||
42
src/components/ui/sonner/Sonner.vue
Normal file
42
src/components/ui/sonner/Sonner.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<script lang="ts" setup>
|
||||
import type { ToasterProps } from "vue-sonner"
|
||||
import { CircleCheckIcon, InfoIcon, Loader2Icon, OctagonXIcon, TriangleAlertIcon, XIcon } from "lucide-vue-next"
|
||||
import { Toaster as Sonner } from "vue-sonner"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<ToasterProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Sonner
|
||||
:class="cn('toaster group', props.class)"
|
||||
:style="{
|
||||
'--normal-bg': 'var(--popover)',
|
||||
'--normal-text': 'var(--popover-foreground)',
|
||||
'--normal-border': 'var(--border)',
|
||||
'--border-radius': 'var(--radius)',
|
||||
}"
|
||||
v-bind="props"
|
||||
>
|
||||
<template #success-icon>
|
||||
<CircleCheckIcon class="size-4" />
|
||||
</template>
|
||||
<template #info-icon>
|
||||
<InfoIcon class="size-4" />
|
||||
</template>
|
||||
<template #warning-icon>
|
||||
<TriangleAlertIcon class="size-4" />
|
||||
</template>
|
||||
<template #error-icon>
|
||||
<OctagonXIcon class="size-4" />
|
||||
</template>
|
||||
<template #loading-icon>
|
||||
<div>
|
||||
<Loader2Icon class="size-4 animate-spin" />
|
||||
</div>
|
||||
</template>
|
||||
<template #close-icon>
|
||||
<XIcon class="size-4" />
|
||||
</template>
|
||||
</Sonner>
|
||||
</template>
|
||||
1
src/components/ui/sonner/index.ts
Normal file
1
src/components/ui/sonner/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Toaster } from "./Sonner.vue"
|
||||
38
src/components/ui/switch/Switch.vue
Normal file
38
src/components/ui/switch/Switch.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import type { SwitchRootEmits, SwitchRootProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import {
|
||||
SwitchRoot,
|
||||
SwitchThumb,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<SwitchRootProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const emits = defineEmits<SwitchRootEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SwitchRoot
|
||||
v-slot="slotProps"
|
||||
data-slot="switch"
|
||||
v-bind="forwarded"
|
||||
:class="cn(
|
||||
'peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<SwitchThumb
|
||||
data-slot="switch-thumb"
|
||||
:class="cn('bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0')"
|
||||
>
|
||||
<slot name="thumb" v-bind="slotProps" />
|
||||
</SwitchThumb>
|
||||
</SwitchRoot>
|
||||
</template>
|
||||
1
src/components/ui/switch/index.ts
Normal file
1
src/components/ui/switch/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Switch } from "./Switch.vue"
|
||||
38
src/constant/theme.ts
Normal file
38
src/constant/theme.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
export const daisyuiThemes = [
|
||||
'valentine',
|
||||
'halloween',
|
||||
'cmyk',
|
||||
'winter',
|
||||
'corporate',
|
||||
'fantasy',
|
||||
'aqua',
|
||||
'pastel',
|
||||
'lemonade',
|
||||
'garden',
|
||||
'silk',
|
||||
'black',
|
||||
'coffee',
|
||||
'wireframe',
|
||||
'caramellatte',
|
||||
'acid',
|
||||
'cyberpunk',
|
||||
'nord',
|
||||
'bumblebee',
|
||||
'night',
|
||||
'dracula',
|
||||
'synthwave',
|
||||
'light',
|
||||
'dark',
|
||||
'forest',
|
||||
'emerald',
|
||||
'business',
|
||||
'cupcake',
|
||||
'autumn',
|
||||
'abyss',
|
||||
'dim',
|
||||
'lofi',
|
||||
'sunset',
|
||||
'luxury',
|
||||
'retro',
|
||||
|
||||
]
|
||||
@@ -2,54 +2,56 @@ import type { IPersonConfig } from '@/types/storeType'
|
||||
import { rgba } from '@/utils/color'
|
||||
|
||||
export function useElementStyle(element: any, person: IPersonConfig, index: number, patternList: number[], patternColor: string, cardColor: string, cardSize: { width: number, height: number }, textSize: number, mod: 'default' | 'lucky' | 'sphere' = 'default', type: 'add' | 'change' = 'add') {
|
||||
if (patternList.includes(index + 1) && mod === 'default') {
|
||||
element.style.backgroundColor = rgba(patternColor, Math.random() * 0.2 + 0.8)
|
||||
}
|
||||
else if (mod === 'sphere' || mod === 'default') {
|
||||
element.style.backgroundColor = rgba(cardColor, Math.random() * 0.5 + 0.25)
|
||||
}
|
||||
else if (mod === 'lucky') {
|
||||
element.style.backgroundColor = rgba(cardColor, 0.8)
|
||||
}
|
||||
element.style.border = `1px solid ${rgba(cardColor, 0.25)}`
|
||||
element.style.boxShadow = `0 0 12px ${rgba(cardColor, 0.5)}`
|
||||
element.style.width = `${cardSize.width}px`
|
||||
element.style.height = `${cardSize.height}px`
|
||||
if (mod === 'lucky') {
|
||||
element.className = 'lucky-element-card'
|
||||
}
|
||||
else {
|
||||
element.className = 'element-card'
|
||||
}
|
||||
if (type === 'add') {
|
||||
element.addEventListener('mouseenter', (ev: MouseEvent) => {
|
||||
const target = ev.target as HTMLElement
|
||||
target.style.border = `1px solid ${rgba(cardColor, 0.75)}`
|
||||
target.style.boxShadow = `0 0 12px ${rgba(cardColor, 0.75)}`
|
||||
})
|
||||
element.addEventListener('mouseleave', (ev: MouseEvent) => {
|
||||
const target = ev.target as HTMLElement
|
||||
target.style.border = `1px solid ${rgba(cardColor, 0.25)}`
|
||||
target.style.boxShadow = `0 0 12px ${rgba(cardColor, 0.5)}`
|
||||
})
|
||||
}
|
||||
element.children[0].style.fontSize = `${textSize * 0.5}px`
|
||||
if (person.uid) {
|
||||
element.children[0].textContent = person.uid
|
||||
}
|
||||
if (patternList.includes(index + 1) && mod === 'default') {
|
||||
element.style.backgroundColor = rgba(patternColor, Math.random() * 0.2 + 0.8)
|
||||
}
|
||||
else if (mod === 'sphere' || mod === 'default') {
|
||||
element.style.backgroundColor = rgba(cardColor, Math.random() * 0.5 + 0.25)
|
||||
}
|
||||
else if (mod === 'lucky') {
|
||||
element.style.backgroundColor = rgba(cardColor, 0.8)
|
||||
}
|
||||
element.style.border = `1px solid ${rgba(cardColor, 0.25)}`
|
||||
element.style.boxShadow = `0 0 12px ${rgba(cardColor, 0.5)}`
|
||||
element.style.width = `${cardSize.width}px`
|
||||
element.style.height = `${cardSize.height}px`
|
||||
if (mod === 'lucky') {
|
||||
element.className = 'lucky-element-card'
|
||||
}
|
||||
else {
|
||||
element.className = 'element-card'
|
||||
}
|
||||
if (type === 'add') {
|
||||
element.addEventListener('mouseenter', (ev: MouseEvent) => {
|
||||
const target = ev.target as HTMLElement
|
||||
target.style.border = `1px solid ${rgba(cardColor, 0.75)}`
|
||||
target.style.boxShadow = `0 0 12px ${rgba(cardColor, 0.75)}`
|
||||
})
|
||||
element.addEventListener('mouseleave', (ev: MouseEvent) => {
|
||||
const target = ev.target as HTMLElement
|
||||
target.style.border = `1px solid ${rgba(cardColor, 0.25)}`
|
||||
target.style.boxShadow = `0 0 12px ${rgba(cardColor, 0.5)}`
|
||||
})
|
||||
}
|
||||
element.children[0].style.fontSize = `${textSize * 0.5}px`
|
||||
if (person.uid) {
|
||||
element.children[0].textContent = person.uid
|
||||
}
|
||||
|
||||
element.children[1].style.fontSize = `${textSize}px`
|
||||
element.children[1].style.lineHeight = `${textSize * 3}px`
|
||||
element.children[1].style.textShadow = `0 0 12px ${rgba(cardColor, 0.95)}`
|
||||
if (person.name) {
|
||||
element.children[1].textContent = person.name
|
||||
}
|
||||
element.children[2].style.fontSize = `${textSize * 0.5}px`
|
||||
if (person.department || person.identity) {
|
||||
element.children[2].innerHTML = `${person.department ? person.department : ''}<br/>${person.identity ? person.identity : ''}`
|
||||
}
|
||||
element.children[1].style.fontSize = `${textSize}px`
|
||||
element.children[1].style.lineHeight = `${textSize * 3}px`
|
||||
element.children[1].style.textShadow = `0 0 12px ${rgba(cardColor, 0.95)}`
|
||||
if (person.name) {
|
||||
element.children[1].textContent = person.name
|
||||
}
|
||||
// element.children[2].style.fontSize = `${textSize * 0.5}px`
|
||||
// if (person.department || person.identity) {
|
||||
// element.children[2].innerHTML = `${person.department ? person.department : ''}<br/>${person.identity ? person.identity : ''}`
|
||||
// }
|
||||
|
||||
element.children[2].style.fontSize = textSize * 0.5 + 'px'
|
||||
element.children[2].style.fontSize = `${textSize * 0.5}px`
|
||||
// 设置部门和身份的默认值
|
||||
element.children[2].innerHTML = ''
|
||||
if (person.department || person.identity) {
|
||||
element.children[2].innerHTML = `${person.department ? person.department : ''}<br/>${person.identity ? person.identity : ''}`
|
||||
}
|
||||
@@ -63,36 +65,36 @@ export function useElementStyle(element: any, person: IPersonConfig, index: numb
|
||||
*/
|
||||
// TODO:不超过5个时:单行排列;超过5个时,6:上3下3;7:上3下4;8:上3下5;9:上4下5;10:上5下5
|
||||
export function useElementPosition(element: any, count: number, totalCount: number, cardSize: { width: number, height: number }, windowSize: { width: number, height: number }, cardIndex: number) {
|
||||
let xTable = 0
|
||||
let yTable = 0
|
||||
const centerPosition = {
|
||||
x: 0,
|
||||
y: windowSize.height / 2 - cardSize.height / 2,
|
||||
}
|
||||
// 有一行为偶数的特殊数量
|
||||
const specialPosition = [2, 4, 7, 9]
|
||||
// 不包含特殊值的 和 分两行中第一行为奇数值的
|
||||
if (!specialPosition.includes(totalCount) || (totalCount > 5 && cardIndex < 5)) {
|
||||
const index = cardIndex % 5
|
||||
if (index === 0) {
|
||||
xTable = centerPosition.x
|
||||
yTable = centerPosition.y - Math.floor(cardIndex / 5) * (cardSize.height + 60)
|
||||
let xTable = 0
|
||||
let yTable = 0
|
||||
const centerPosition = {
|
||||
x: 0,
|
||||
y: windowSize.height / 2 - cardSize.height * 0.9,
|
||||
}
|
||||
// 有一行为偶数的特殊数量
|
||||
const specialPosition = [2, 4, 7, 9]
|
||||
// 不包含特殊值的 和 分两行中第一行为奇数值的
|
||||
if (!specialPosition.includes(totalCount) || (totalCount > 5 && cardIndex < 5)) {
|
||||
const index = cardIndex % 5
|
||||
if (index === 0) {
|
||||
xTable = centerPosition.x
|
||||
yTable = centerPosition.y - Math.floor(cardIndex / 5) * (cardSize.height + 60)
|
||||
}
|
||||
else {
|
||||
xTable = index % 2 === 0 ? Math.ceil(index / 2) * (cardSize.width + 100) : -Math.ceil(index / 2) * (cardSize.width + 100)
|
||||
yTable = centerPosition.y - Math.floor(cardIndex / 5) * (cardSize.height + 60)
|
||||
}
|
||||
}
|
||||
else {
|
||||
xTable = index % 2 === 0 ? Math.ceil(index / 2) * (cardSize.width + 100) : -Math.ceil(index / 2) * (cardSize.width + 100)
|
||||
yTable = centerPosition.y - Math.floor(cardIndex / 5) * (cardSize.height + 60)
|
||||
const index = cardIndex % 5
|
||||
if (index === 0) {
|
||||
xTable = centerPosition.x + (cardSize.width + 100) / 2
|
||||
yTable = centerPosition.y - Math.floor(cardIndex / 5) * (cardSize.height + 60)
|
||||
}
|
||||
else {
|
||||
xTable = index % 2 === 0 ? Math.ceil(index / 2) * (cardSize.width + 100) + (cardSize.width + 100) / 2 : -(Math.ceil(index / 2) * (cardSize.width + 100)) + (cardSize.width + 100) / 2
|
||||
yTable = centerPosition.y - Math.floor(cardIndex / 5) * (cardSize.height + 60)
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
const index = cardIndex % 5
|
||||
if (index === 0) {
|
||||
xTable = centerPosition.x + (cardSize.width + 100) / 2
|
||||
yTable = centerPosition.y - Math.floor(cardIndex / 5) * (cardSize.height + 60)
|
||||
}
|
||||
else {
|
||||
xTable = index % 2 === 0 ? Math.ceil(index / 2) * (cardSize.width + 100) + (cardSize.width + 100) / 2 : -(Math.ceil(index / 2) * (cardSize.width + 100)) + (cardSize.width + 100) / 2
|
||||
yTable = centerPosition.y - Math.floor(cardIndex / 5) * (cardSize.height + 60)
|
||||
}
|
||||
}
|
||||
return { xTable, yTable }
|
||||
return { xTable, yTable }
|
||||
}
|
||||
|
||||
47
src/hooks/useLocalFonts.ts
Normal file
47
src/hooks/useLocalFonts.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
export function useLocalFonts() {
|
||||
const fonts = ref<Map<string, { name: string, value: string }[]>>(new Map())
|
||||
const disabled = ref(false)
|
||||
const formatFonts = (list: FontData[]): Map<string, { name: string, value: string }[]> => {
|
||||
const res: Map<string, { name: string, value: string }[]> = new Map()
|
||||
for (const item of list) {
|
||||
if (!res.has(item.family)) {
|
||||
res.set(item.family, [])
|
||||
}
|
||||
|
||||
const fontArray = res.get(item.family)
|
||||
if (!Array.isArray(fontArray)) {
|
||||
continue
|
||||
}
|
||||
if (item.family === item.fullName) {
|
||||
fontArray.push({ name: item.style, value: item.fullName })
|
||||
continue
|
||||
}
|
||||
const name = item.fullName.replace(item.family, '').trim()
|
||||
const value = item.fullName
|
||||
fontArray.push({ name, value })
|
||||
}
|
||||
return res
|
||||
}
|
||||
const getFonts = async () => {
|
||||
if (!window.queryLocalFonts) {
|
||||
return
|
||||
}
|
||||
const list = await window.queryLocalFonts()
|
||||
const res = formatFonts(list)
|
||||
fonts.value = res
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!window.queryLocalFonts) {
|
||||
disabled.value = true
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
disabled,
|
||||
fonts,
|
||||
getFonts,
|
||||
}
|
||||
}
|
||||
59
src/icons/chevron-down.svg
Normal file
59
src/icons/chevron-down.svg
Normal file
@@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-chevron-down-icon lucide-chevron-down"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="chevron-down.svg"
|
||||
inkscape:export-filename="..\Users\log\Users\log\Users\log\Users\log\Users\log\Users\log\Users\log\Users\log\Desktop\chevron-down.svg"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
inkscape:version="1.4 (86a8ad7, 2024-10-11)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showguides="true"
|
||||
inkscape:zoom="48.625"
|
||||
inkscape:cx="12.010283"
|
||||
inkscape:cy="12"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1369"
|
||||
inkscape:window-x="1592"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1">
|
||||
<sodipodi:guide
|
||||
position="-5.5115681,17.439589"
|
||||
orientation="1,0"
|
||||
id="guide1"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
position="6.5398458,15.979434"
|
||||
orientation="0,-1"
|
||||
id="guide2"
|
||||
inkscape:locked="false" />
|
||||
</sodipodi:namedview>
|
||||
<path
|
||||
d="M 0.98200514,9 12,15 23.03856,9.0616967"
|
||||
id="path1"
|
||||
sodipodi:nodetypes="ccc" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
54
src/icons/chevron-up.svg
Normal file
54
src/icons/chevron-up.svg
Normal file
@@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-chevron-up-icon lucide-chevron-up"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="chevron-up.svg"
|
||||
inkscape:export-filename="..\Users\log\Users\log\Desktop\chevron-up.svg"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
inkscape:version="1.4 (86a8ad7, 2024-10-11)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showguides="true"
|
||||
inkscape:zoom="48.625"
|
||||
inkscape:cx="12.010283"
|
||||
inkscape:cy="12"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1369"
|
||||
inkscape:window-x="1592"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1">
|
||||
<sodipodi:guide
|
||||
position="10.550129,8"
|
||||
orientation="0,-1"
|
||||
id="guide1"
|
||||
inkscape:locked="false" />
|
||||
</sodipodi:namedview>
|
||||
<path
|
||||
d="M 22.997429,14.958869 12,9 0.96143959,14.958869"
|
||||
id="path1"
|
||||
sodipodi:nodetypes="ccc" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -7,7 +7,7 @@ function skip(url: string) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="footer-container">
|
||||
<div class="footer-container h-[280px]">
|
||||
<ul class="flex justify-center">
|
||||
<li
|
||||
v-for="item in footerList.data"
|
||||
|
||||
85
src/layout/RightButton/index.vue
Normal file
85
src/layout/RightButton/index.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<script setup lang='ts'>
|
||||
import { useFullscreen } from '@vueuse/core'
|
||||
import { Maximize, Minimize } from 'lucide-vue-next'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { usePlayMusic } from './usePlayMusic'
|
||||
|
||||
const { playMusic, currentMusic, nextPlay } = usePlayMusic()
|
||||
const { isFullscreen, toggle: toggleScreen } = useFullscreen()
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const settingRef = ref()
|
||||
const fullScreenRef = ref()
|
||||
|
||||
function enterConfig() {
|
||||
router.push('/log-lottery/config')
|
||||
}
|
||||
function enterHome() {
|
||||
router.push('/log-lottery')
|
||||
}
|
||||
onMounted(() => {
|
||||
settingRef.value.addEventListener('mouseenter', () => {
|
||||
fullScreenRef.value.style.display = 'block'
|
||||
})
|
||||
settingRef.value.addEventListener('mouseleave', () => {
|
||||
fullScreenRef.value.style.display = 'none'
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="settingRef" class="flex flex-col gap-3">
|
||||
<div ref="fullScreenRef" class="tooltip tooltip-left hidden" @click="toggleScreen">
|
||||
<div
|
||||
v-if="isFullscreen"
|
||||
class="flex items-center justify-center w-10 h-10 p-0 m-0 cursor-pointer setting-container bg-slate-500/50 rounded-l-xl hover:bg-slate-500/80 hover:text-blue-400/90"
|
||||
>
|
||||
<Minimize />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="flex items-center justify-center w-10 h-10 p-0 m-0 cursor-pointer setting-container bg-slate-500/50 rounded-l-xl hover:bg-slate-500/80 hover:text-blue-400/90"
|
||||
>
|
||||
<Maximize />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="route.path.includes('/config')" class="tooltip tooltip-left" :data-tip="t('tooltip.toHome')">
|
||||
<div
|
||||
class="flex items-center justify-center w-10 h-10 p-0 m-0 cursor-pointer setting-container bg-slate-500/50 rounded-l-xl hover:bg-slate-500/80 hover:text-blue-400/90"
|
||||
@click="enterHome"
|
||||
>
|
||||
<svg-icon name="home" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="tooltip tooltip-left" :data-tip="t('tooltip.settingConfiguration')">
|
||||
<div
|
||||
class="flex items-center justify-center w-10 h-10 p-0 m-0 cursor-pointer setting-container bg-slate-500/50 rounded-l-xl hover:bg-slate-500/80 hover:text-blue-400/90"
|
||||
@click="enterConfig"
|
||||
>
|
||||
<svg-icon name="setting" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="tooltip tooltip-left" :data-tip="currentMusic.item ? `${currentMusic.item.name}\n\r ${t('tooltip.nextSong')}` : t('tooltip.noSongPlay')">
|
||||
<div
|
||||
class="flex items-center justify-center w-10 h-10 p-0 m-0 cursor-pointer setting-container bg-slate-500/50 rounded-l-xl hover:bg-slate-500/80 hover:text-blue-400/90"
|
||||
@click="playMusic(currentMusic.item)" @click.right.prevent="nextPlay"
|
||||
>
|
||||
<svg-icon :name="currentMusic.paused ? 'play' : 'pause'" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
details {
|
||||
|
||||
// display: none;
|
||||
summary {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
89
src/layout/RightButton/usePlayMusic.ts
Normal file
89
src/layout/RightButton/usePlayMusic.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import type { IFileData } from '@/components/FileUpload/type'
|
||||
import type { IMusic } from '@/types/storeType'
|
||||
import localforage from 'localforage'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import useStore from '@/store'
|
||||
|
||||
export function usePlayMusic() {
|
||||
const audioDbStore = localforage.createInstance({
|
||||
name: 'audioStore',
|
||||
})
|
||||
|
||||
const globalConfig = useStore().globalConfig
|
||||
const { getMusicList: localMusicList, getCurrentMusic: currentMusic } = storeToRefs(globalConfig)
|
||||
const audio = ref(new Audio())
|
||||
|
||||
async function play(item: IMusic) {
|
||||
if (!item) {
|
||||
return
|
||||
}
|
||||
// if (!audio.value.paused && !skip) {
|
||||
// audio.value.pause()
|
||||
|
||||
// return
|
||||
// }
|
||||
let audioUrl = ''
|
||||
if (!item.url) {
|
||||
return
|
||||
}
|
||||
if (item.url === 'Storage') {
|
||||
const key = item.id
|
||||
const audioData = await audioDbStore.getItem<IFileData>(key)
|
||||
audioUrl = URL.createObjectURL(audioData?.data as Blob)
|
||||
}
|
||||
else {
|
||||
audioUrl = item.url as string
|
||||
}
|
||||
audio.value.pause()
|
||||
audio.value.src = audioUrl
|
||||
audio.value.play()
|
||||
}
|
||||
function playMusic(item: IMusic, skip = false) {
|
||||
if (!item) {
|
||||
return
|
||||
}
|
||||
if (!currentMusic.value.paused && !skip) {
|
||||
globalConfig.setCurrentMusic(item, true)
|
||||
|
||||
return
|
||||
}
|
||||
globalConfig.setCurrentMusic(item, false)
|
||||
}
|
||||
function nextPlay() {
|
||||
// 播放下一首
|
||||
if (localMusicList.value.length >= 1) {
|
||||
let index = localMusicList.value.findIndex((item: IMusic) => item.name === currentMusic.value.item.name)
|
||||
index++
|
||||
if (index >= localMusicList.value.length) {
|
||||
index = 0
|
||||
}
|
||||
globalConfig.setCurrentMusic(localMusicList.value[index], false)
|
||||
}
|
||||
}
|
||||
// 监听播放成后开始下一首
|
||||
function onPlayEnd() {
|
||||
audio.value.addEventListener('ended', nextPlay)
|
||||
}
|
||||
onMounted(() => {
|
||||
globalConfig.setCurrentMusic(localMusicList.value[0], true)
|
||||
onPlayEnd()
|
||||
// 不使用空格控制audio
|
||||
})
|
||||
onUnmounted(() => {
|
||||
audio.value.removeEventListener('ended', nextPlay)
|
||||
})
|
||||
watch(currentMusic, (val: { item: IMusic, paused: boolean }) => {
|
||||
if (!val.paused && audio.value) {
|
||||
play(val.item)
|
||||
}
|
||||
else {
|
||||
audio.value.pause()
|
||||
}
|
||||
}, { deep: true })
|
||||
return {
|
||||
currentMusic,
|
||||
playMusic,
|
||||
nextPlay,
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import ToTop from '@/components/ToTop/index.vue'
|
||||
import { useScroll } from '@vueuse/core'
|
||||
// import Header from './Header/index.vue';
|
||||
// import Footer from './Footer/index.vue';
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import CustomModal from '@/components/Dialog/index.vue'
|
||||
import { Loading } from '@/components/Loading'
|
||||
import ToTop from '@/components/ToTop/index.vue'
|
||||
import RightButton from './RightButton/index.vue'
|
||||
import { useMounted } from './useMounted'
|
||||
|
||||
const tipDialog = ref()
|
||||
const { tipDesc } = useMounted(tipDialog)
|
||||
const { t } = useI18n()
|
||||
const mainContainer = ref<HTMLElement | null>(null)
|
||||
|
||||
const { y } = useScroll(mainContainer)
|
||||
|
||||
function scrollToTop() {
|
||||
y.value = 0
|
||||
mainContainer.value?.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-screen">
|
||||
<!-- <header class="shadow-2xl head-container h-14">
|
||||
<Header></Header>
|
||||
</header> -->
|
||||
<Loading />
|
||||
<ToTop v-if="y > 400" @click="scrollToTop" />
|
||||
<main ref="mainContainer" class="box-content w-screen h-screen overflow-x-hidden overflow-y-auto main-container">
|
||||
<router-view class="h-full main-container-content" />
|
||||
</main>
|
||||
<!-- <footer class="w-screen footer-container">
|
||||
<Footer></Footer>
|
||||
</footer> -->
|
||||
<RightButton class="absolute right-0 bottom-1/2" />
|
||||
<CustomModal ref="tipDialog" :title="t('dialog.titleTip')" :desc="tipDesc" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
65
src/layout/useMounted.ts
Normal file
65
src/layout/useMounted.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import type { Ref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, provide, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { loadingKey, loadingState } from '@/components/Loading'
|
||||
import useStore from '@/store'
|
||||
import { themeChange } from '@/utils'
|
||||
|
||||
export function useMounted(tipDialog: Ref<any>) {
|
||||
provide(loadingKey, loadingState)
|
||||
const globalConfig = useStore().globalConfig
|
||||
const prizeConfig = useStore().prizeConfig
|
||||
const system = useStore().system
|
||||
const { getTheme: localTheme } = storeToRefs(globalConfig)
|
||||
const { getPrizeConfig: prizeList } = storeToRefs(prizeConfig)
|
||||
const tipDesc = ref('')
|
||||
const { t } = useI18n()
|
||||
// 设置当前奖列表
|
||||
function setCurrentPrize() {
|
||||
if (prizeList.value.length <= 0) {
|
||||
return
|
||||
}
|
||||
for (let i = 0; i < prizeList.value.length; i++) {
|
||||
if (!prizeList.value[i].isUsed) {
|
||||
prizeConfig.setCurrentPrize(prizeList.value[i])
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// 判断是否手机端访问
|
||||
function judgeMobile() {
|
||||
const ua = navigator.userAgent
|
||||
const isAndroid = ua.includes('Android') || ua.includes('Adr')
|
||||
const isIOS = !!ua.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/)
|
||||
|
||||
system.setIsMobile(isAndroid || isIOS)
|
||||
|
||||
return isAndroid || isIOS
|
||||
}
|
||||
// 判断是否chrome或者edge访问
|
||||
function judgeChromeOrEdge() {
|
||||
const ua = navigator.userAgent
|
||||
const isChrome = ua.includes('Chrome')
|
||||
const isEdge = ua.includes('Edg')
|
||||
|
||||
system.setIsChrome(isChrome)
|
||||
|
||||
return isChrome || isEdge
|
||||
}
|
||||
onMounted(() => {
|
||||
themeChange(localTheme.value.name)
|
||||
setCurrentPrize()
|
||||
if (judgeMobile()) {
|
||||
tipDialog.value.showDialog()
|
||||
tipDesc.value = t('dialog.dialogPCWeb')
|
||||
}
|
||||
else if (!judgeChromeOrEdge()) {
|
||||
tipDialog.value.showDialog()
|
||||
tipDesc.value = t('dialog.dialogLatestBrowser')
|
||||
}
|
||||
})
|
||||
|
||||
return { tipDesc }
|
||||
}
|
||||
7
src/lib/utils.ts
Normal file
7
src/lib/utils.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { ClassValue } from "clsx"
|
||||
import { clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -1,153 +1,14 @@
|
||||
export default {
|
||||
button: {
|
||||
enterLottery: 'Enter Lottery',
|
||||
start: 'Start',
|
||||
selectLucky: 'Draw the Lucky',
|
||||
continue: 'Continue',
|
||||
confirm: 'Confirm',
|
||||
cancel: 'Cancel',
|
||||
setting: 'Setting',
|
||||
delete: 'Delete',
|
||||
allDelete: 'Delete All',
|
||||
downloadTemplate: 'Download Template',
|
||||
importData: 'Import Data',
|
||||
resetData: 'Reset Data',
|
||||
exportResult: 'Export Result',
|
||||
add: 'Add',
|
||||
resetDefault: 'Reset Default',
|
||||
resetAllData: 'Reset All Data',
|
||||
clearPattern: 'Clear Pattern',
|
||||
DefaultPattern: 'Default Pattern',
|
||||
upload: 'Upload',
|
||||
reset: 'Reset',
|
||||
play: 'Play',
|
||||
setLayout: 'Set Layout',
|
||||
close: 'Close',
|
||||
noInfoAndImport: 'No Info and import it',
|
||||
useDefault: 'Use Default Data',
|
||||
},
|
||||
sidebar: {
|
||||
personConfiguration: 'Person Configuration',
|
||||
personList: 'Person List',
|
||||
winnerList: 'Winner List',
|
||||
prizeConfiguration: 'Prize Configuration',
|
||||
globalSetting: 'Global Configuration',
|
||||
viewSetting: 'View Setting',
|
||||
imagesManagement: 'Images Management',
|
||||
musicManagement: 'Music Management',
|
||||
operatingInstructions: 'Operating Instructions',
|
||||
},
|
||||
viewTitle: {
|
||||
personManagement: 'Person Management',
|
||||
winnerManagement: 'Winner Management',
|
||||
prizeManagement: 'Prize Management',
|
||||
globalSetting: 'Global Setting',
|
||||
operatingInstructions: 'Operating Instructions',
|
||||
},
|
||||
table: {
|
||||
// person configuration
|
||||
number: 'Number',
|
||||
name: 'Name',
|
||||
prizeName: 'Name',
|
||||
department: 'Department',
|
||||
identity: 'Identity',
|
||||
isLucky: 'Is Lucky',
|
||||
operation: 'Operation',
|
||||
setLuckyNumber: 'Set Lucky Number',
|
||||
luckyPeopleNumber: 'Lucky People Number',
|
||||
import { button, data, dialog, error, footer, placeHolder, sidebar, table, tooltip, viewTitle } from './modules'
|
||||
|
||||
detail: 'Detail',
|
||||
noneData: 'No Data',
|
||||
// prize configuration
|
||||
fullParticipation: 'FullParticipation',
|
||||
numberParticipants: 'NumberParticipants',
|
||||
isDone: 'is Done',
|
||||
image: 'Image',
|
||||
onceNumber: 'Once Number',
|
||||
time: 'Time',
|
||||
// view setting
|
||||
title: 'Main Title',
|
||||
columnNumber: 'Column Number',
|
||||
theme: 'Theme',
|
||||
language: 'Language',
|
||||
cardColor: 'Card Color',
|
||||
winnerColor: 'Winner Color',
|
||||
textColor: 'Text Color',
|
||||
cardWidth: 'Card Width',
|
||||
cardHeight: 'Card Height',
|
||||
textSize: 'Text Size',
|
||||
highlightColor: 'HighLight Color',
|
||||
patternSetting: 'Pattern Setting',
|
||||
alwaysDisplay: 'Always Display Prize List',
|
||||
avatarDisplay: 'Show avatars or not',
|
||||
selectPicture: 'Select a Picture',
|
||||
backgroundImage: 'Select Background Image',
|
||||
},
|
||||
dialog: {
|
||||
titleTip: 'Tip!',
|
||||
titleTemporary: 'Add Temporary Activity',
|
||||
dialogPCWeb: 'Please use a PC browser to access for optimal display performance',
|
||||
dialogDelAllPerson: 'This operation will delete all personnel list data. Do you want to continue?',
|
||||
dialogResetWinner: 'This operation will clear the winning information of personnel. Do you want to continue?',
|
||||
dialogResetAllData: 'This operation will reset all data. Do you want to continue?',
|
||||
dialogSingleDrawLimit: 'Only 10 characters can be extracted in a single draw',
|
||||
dialogLatestBrowser: 'Please use the latest version of Chrome or Edge browser',
|
||||
tipResetPrize: 'Performing operations may reset data, please proceed with caution',
|
||||
},
|
||||
tooltip: {
|
||||
settingConfiguration: 'Setting/Configuration',
|
||||
nextSong: 'Right Click to Next Song',
|
||||
noSongPlay: 'No Song to Play',
|
||||
prizeList: 'Prize List',
|
||||
addActivity: 'Add Activity',
|
||||
downloadTemplateTip: 'After downloading the file, please fill in the data in Excel and save it in xlsx format',
|
||||
uploadExcelTip: 'Upload the modified Excel file',
|
||||
leftClick: 'Left Click to Slice',
|
||||
toHome: 'to Home',
|
||||
resetLayout: 'This item is time-consuming and performance intensive',
|
||||
defaultLayout: 'The default pattern setting is valid for 17 columns, please set the number of other columns yourself',
|
||||
doneCount: 'Number of winners',
|
||||
edit: 'Edit',
|
||||
delete: 'Delete',
|
||||
},
|
||||
error: {
|
||||
require: 'required field',
|
||||
requireNumber: 'please enter a number',
|
||||
minNumber1: 'the minimum is 1',
|
||||
maxNumber100: 'the maximum is 100',
|
||||
uploadSuccess: 'Upload Success',
|
||||
uploadFail: 'Upload Failed',
|
||||
notImage: 'Not Image',
|
||||
personIsAllDone: 'All Person Is Done',
|
||||
personNotEnough: 'Person Is Not Enough',
|
||||
startDraw: 'Now Draw {count} {leftover} people',
|
||||
completeInformation: 'Please provide complete information',
|
||||
},
|
||||
placeHolder: {
|
||||
enterTitle: 'Enter Title',
|
||||
name: 'Name',
|
||||
winnerCount: 'Lucky Person Count',
|
||||
},
|
||||
data: {
|
||||
yes: 'Yes',
|
||||
no: 'No',
|
||||
number: 'Number',
|
||||
isWin: 'isWin',
|
||||
avatar: 'avatar',
|
||||
department: 'Department',
|
||||
name: 'Name',
|
||||
identity: 'Identity',
|
||||
prizeName: 'Prize Name',
|
||||
prizeTime: 'Prize Time',
|
||||
operation: 'Operation',
|
||||
delete: 'Delete',
|
||||
removePerson: 'Remove the Person',
|
||||
defaultTitle: 'The Prelude to the Six Ministries of the Ming Dynasty Cabinet',
|
||||
xlsxName: 'personListTemplate-en.xlsx',
|
||||
readmeName: 'readme-en.md',
|
||||
},
|
||||
footer: {
|
||||
'self-reflection': 'Turn inward and examine yourself when you encounter difficulties in life.',
|
||||
'thiefEasy': 'Thief difficult mountain thief easily, breaking heart.',
|
||||
},
|
||||
export default {
|
||||
button: button.en,
|
||||
sidebar: sidebar.en,
|
||||
viewTitle: viewTitle.en,
|
||||
table: table.en,
|
||||
dialog: dialog.en,
|
||||
tooltip: tooltip.en,
|
||||
error: error.en,
|
||||
placeHolder: placeHolder.en,
|
||||
data: data.en,
|
||||
footer: footer.en,
|
||||
}
|
||||
|
||||
63
src/locales/modules/button.ts
Normal file
63
src/locales/modules/button.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
export const buttonEn = {
|
||||
enterLottery: 'Enter Lottery',
|
||||
start: 'Start',
|
||||
selectLucky: 'Draw the Lucky',
|
||||
continue: 'Continue',
|
||||
confirm: 'Confirm',
|
||||
cancel: 'Cancel',
|
||||
setting: 'Setting',
|
||||
delete: 'Delete',
|
||||
allDelete: 'Delete All',
|
||||
downloadTemplate: 'Download Template',
|
||||
importData: 'Import Data',
|
||||
resetData: 'Reset Data',
|
||||
exportResult: 'Export Result',
|
||||
add: 'Add',
|
||||
resetDefault: 'Reset Default',
|
||||
resetAllData: 'Reset All Data',
|
||||
clearPattern: 'Clear Pattern',
|
||||
DefaultPattern: 'Default Pattern',
|
||||
upload: 'Upload',
|
||||
reset: 'Reset',
|
||||
play: 'Play',
|
||||
setLayout: 'Set Layout',
|
||||
close: 'Close',
|
||||
noInfoAndImport: 'No Info and import it',
|
||||
useDefault: 'Use Default Data',
|
||||
loading: 'Loading...',
|
||||
}
|
||||
|
||||
export const buttonZhCn = {
|
||||
enterLottery: '进入抽奖',
|
||||
start: '开始',
|
||||
selectLucky: '抽取幸运儿',
|
||||
continue: '继续',
|
||||
confirm: '确认',
|
||||
cancel: '取消',
|
||||
setting: '设置',
|
||||
delete: '删除',
|
||||
allDelete: '删除全部',
|
||||
downloadTemplate: '下载模板',
|
||||
importData: '导入数据',
|
||||
resetData: '重置数据',
|
||||
exportResult: '导出结果',
|
||||
add: '添加',
|
||||
resetDefault: '重置为默认',
|
||||
resetAllData: '重置所有数据',
|
||||
clearPattern: '清除图案',
|
||||
DefaultPattern: '默认图案',
|
||||
upload: '上传',
|
||||
reset: '重置',
|
||||
play: '播放',
|
||||
setLayout: '重设布局',
|
||||
close: '关闭',
|
||||
noInfoAndImport: '暂无人员信息,前往导入',
|
||||
useDefault: '使用默认数据',
|
||||
loading: '加载中...',
|
||||
}
|
||||
|
||||
// 导出一个值
|
||||
export const button = {
|
||||
en: buttonEn,
|
||||
zhCn: buttonZhCn,
|
||||
}
|
||||
42
src/locales/modules/data.ts
Normal file
42
src/locales/modules/data.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
export const dataEn = {
|
||||
yes: 'Yes',
|
||||
no: 'No',
|
||||
number: 'Number',
|
||||
isWin: 'isWin',
|
||||
avatar: 'avatar',
|
||||
department: 'Department',
|
||||
name: 'Name',
|
||||
identity: 'Identity',
|
||||
prizeName: 'Prize Name',
|
||||
prizeTime: 'Prize Time',
|
||||
operation: 'Operation',
|
||||
delete: 'Delete',
|
||||
removePerson: 'Remove the Person',
|
||||
defaultTitle: 'The Prelude to the Six Ministries of the Ming Dynasty Cabinet',
|
||||
xlsxName: 'personListTemplate-en.xlsx',
|
||||
readmeName: 'readme-en.md',
|
||||
}
|
||||
|
||||
export const dataZhCn = {
|
||||
yes: '是',
|
||||
no: '否',
|
||||
number: '编号',
|
||||
isWin: '是否中奖',
|
||||
avatar: '头像',
|
||||
department: '部门',
|
||||
name: '姓名',
|
||||
identity: '身份',
|
||||
prizeName: '获奖',
|
||||
prizeTime: '获奖时间',
|
||||
operation: '操作',
|
||||
delete: '删除',
|
||||
removePerson: '移入未中奖名单',
|
||||
defaultTitle: '大明内阁六部御前奏对',
|
||||
xlsxName: '人口登记表-zhCn.xlsx',
|
||||
readmeName: 'readme-zhCn.md',
|
||||
}
|
||||
|
||||
export const data = {
|
||||
en: dataEn,
|
||||
zhCn: dataZhCn,
|
||||
}
|
||||
34
src/locales/modules/dialog.ts
Normal file
34
src/locales/modules/dialog.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
export const dialogEn = {
|
||||
titleTip: 'Tip!',
|
||||
titleTemporary: 'Add Temporary Activity',
|
||||
dialogPCWeb: 'Please use a PC browser to access for optimal display performance',
|
||||
dialogDelAllPerson: 'This operation will delete all personnel list data. Do you want to continue?',
|
||||
dialogResetWinner: 'This operation will clear the winning information of personnel. Do you want to continue?',
|
||||
dialogResetAllData: 'This operation will reset all data. Do you want to continue?',
|
||||
dialogSingleDrawLimit: 'Only 10 characters can be extracted in a single draw',
|
||||
dialogLatestBrowser: 'Please use the latest version of Chrome or Edge browser',
|
||||
tipResetPrize: 'Performing operations may reset data and cant recover, please proceed with caution',
|
||||
uploadFileTitle: 'Upload File',
|
||||
uploadImageTitle: 'Upload Image',
|
||||
uploadAudioTitle: 'Upload Audio',
|
||||
}
|
||||
|
||||
export const dialogZhCn = {
|
||||
titleTip: '提示!',
|
||||
titleTemporary: '增加临时抽奖',
|
||||
dialogPCWeb: '请使用PC进行访问以获得最佳显示效果',
|
||||
dialogDelAllPerson: '该操作会删除所有人员数据,是否继续?',
|
||||
dialogResetWinner: '该操作会清空人员中奖信息,是否继续?',
|
||||
dialogResetAllData: '该操作会重置所有数据,是否继续?',
|
||||
dialogSingleDrawLimit: '单次抽取只能抽取10位',
|
||||
dialogLatestBrowser: '请使用最新版Chrome或者Edge浏览器',
|
||||
tipResetPrize: '进行操作可能会重置数据并不可恢复,请谨慎操作',
|
||||
uploadFileTitle: '上传文件',
|
||||
uploadImageTitle: '上传图片',
|
||||
uploadAudioTitle: '上传音频',
|
||||
}
|
||||
|
||||
export const dialog = {
|
||||
en: dialogEn,
|
||||
zhCn: dialogZhCn,
|
||||
}
|
||||
60
src/locales/modules/error.ts
Normal file
60
src/locales/modules/error.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { success } from 'zod'
|
||||
|
||||
export const errorEn = {
|
||||
require: 'required field',
|
||||
requireNumber: 'please enter a number',
|
||||
minNumber1: 'the minimum is 1',
|
||||
maxNumber100: 'the maximum is 100',
|
||||
uploadSuccess: 'Upload Success',
|
||||
uploadFail: 'Upload Failed',
|
||||
notImage: 'Not Image',
|
||||
personIsAllDone: 'All Person Is Done',
|
||||
personNotEnough: 'Person Is Not Enough',
|
||||
startDraw: 'Now Draw {count} {leftover} people',
|
||||
completeInformation: 'Please provide complete information',
|
||||
notJsonFile: 'it isn\'t a JSON file',
|
||||
notAudioFile: 'it isn\'t an audio file',
|
||||
personNameEmpty: 'Please enter name',
|
||||
excelFileError: 'The header is inconsistent, please download the template, modify it, and then upload it',
|
||||
exportSuccess: 'Export Success',
|
||||
exportFail: 'Export Failed',
|
||||
importSuccess: 'Import Success',
|
||||
importFail: 'Import Failed',
|
||||
downloadSuccess: '下载成功',
|
||||
deleteSuccess: '删除成功',
|
||||
deleteFail: '删除失败',
|
||||
success: 'Success',
|
||||
fail: 'Failed',
|
||||
}
|
||||
|
||||
export const errorZhCn = {
|
||||
require: '必填项',
|
||||
requireNumber: '请输入数字',
|
||||
minNumber1: '最小为1',
|
||||
maxNumber100: '最大为100',
|
||||
uploadSuccess: '上传成功',
|
||||
uploadFail: '上传失败',
|
||||
notImage: '不是图片',
|
||||
personIsAllDone: '抽奖抽完了',
|
||||
personNotEnough: '抽奖人数不足',
|
||||
startDraw: '现在抽取{count}{leftover}人',
|
||||
completeInformation: '请填写完整信息',
|
||||
notJsonFile: '这不是一个JSON文件',
|
||||
notAudioFile: '这不是一个音频文件',
|
||||
personNameEmpty: '请填写姓名',
|
||||
excelFileError: '表头不一致,请先下载模板然后修改后再上传',
|
||||
exportSuccess: '导出成功',
|
||||
exportFail: '导出失败',
|
||||
importSuccess: '导入成功',
|
||||
importFail: '导入失败',
|
||||
downloadSuccess: '下载成功',
|
||||
deleteSuccess: '删除成功',
|
||||
deleteFail: '删除失败',
|
||||
success: '成功',
|
||||
fail: '失败',
|
||||
}
|
||||
|
||||
export const error = {
|
||||
en: errorEn,
|
||||
zhCn: errorZhCn,
|
||||
}
|
||||
14
src/locales/modules/footer.ts
Normal file
14
src/locales/modules/footer.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export const footerEn = {
|
||||
'self-reflection': 'Turn inward and examine yourself when you encounter difficulties in life.',
|
||||
'thiefEasy': 'Thief difficult mountain thief easily, breaking heart.',
|
||||
}
|
||||
|
||||
export const footerZhCn = {
|
||||
'self-reflection': '行有不得,反求诸己',
|
||||
'thiefEasy': '破山中贼易,破心中贼难',
|
||||
}
|
||||
|
||||
export const footer = {
|
||||
en: footerEn,
|
||||
zhCn: footerZhCn,
|
||||
}
|
||||
11
src/locales/modules/index.ts
Normal file
11
src/locales/modules/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
// 导出该文件夹下的内容
|
||||
export * from './button'
|
||||
export * from './data'
|
||||
export * from './dialog'
|
||||
export * from './error'
|
||||
export * from './footer'
|
||||
export * from './placeHolder'
|
||||
export * from './sidebar'
|
||||
export * from './table'
|
||||
export * from './tooltip'
|
||||
export * from './viewTitle'
|
||||
24
src/locales/modules/placeHolder.ts
Normal file
24
src/locales/modules/placeHolder.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export const placeHolderEn = {
|
||||
enterTitle: 'Enter Title',
|
||||
name: 'Name',
|
||||
winnerCount: 'Lucky Person Count',
|
||||
selectFont: 'Select Font',
|
||||
timedStop: 'will stop at a scheduled time after starting',
|
||||
imageName: 'Image Name',
|
||||
personName: 'Please enter name',
|
||||
}
|
||||
|
||||
export const placeHolderZhCn = {
|
||||
enterTitle: '输入标题',
|
||||
name: '名称',
|
||||
winnerCount: '中奖人数',
|
||||
selectFont: '选择字体',
|
||||
timedStop: '开始后定时抽取',
|
||||
imageName: '图片名称',
|
||||
personName: '请填写姓名',
|
||||
}
|
||||
|
||||
export const placeHolder = {
|
||||
en: placeHolderEn,
|
||||
zhCn: placeHolderZhCn,
|
||||
}
|
||||
28
src/locales/modules/sidebar.ts
Normal file
28
src/locales/modules/sidebar.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export const sidebarEn = {
|
||||
personConfiguration: 'Person Configuration',
|
||||
personList: 'Person List',
|
||||
winnerList: 'Winner List',
|
||||
prizeConfiguration: 'Prize Configuration',
|
||||
globalSetting: 'Global Configuration',
|
||||
viewSetting: 'View Setting',
|
||||
imagesManagement: 'Images Management',
|
||||
musicManagement: 'Music Management',
|
||||
operatingInstructions: 'Operating Instructions',
|
||||
}
|
||||
|
||||
export const sidebarZhCn = {
|
||||
personConfiguration: '人员配置',
|
||||
personList: '人员列表',
|
||||
winnerList: '中奖人员',
|
||||
prizeConfiguration: '奖品配置',
|
||||
globalSetting: '全局配置',
|
||||
viewSetting: '界面设置',
|
||||
imagesManagement: '图片管理',
|
||||
musicManagement: '音乐管理',
|
||||
operatingInstructions: '操作说明',
|
||||
}
|
||||
|
||||
export const sidebar = {
|
||||
en: sidebarEn,
|
||||
zhCn: sidebarZhCn,
|
||||
}
|
||||
109
src/locales/modules/table.ts
Normal file
109
src/locales/modules/table.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { reset } from 'canvas-confetti'
|
||||
import { time } from 'zod/v4/core/regexes.cjs'
|
||||
|
||||
export const tableEn = {
|
||||
// field block name
|
||||
abilitySetting: 'Ability Setting',
|
||||
dataSetting: 'Data Setting',
|
||||
layoutSetting: 'Layout Setting',
|
||||
patternSetting: 'Pattern Setting',
|
||||
textSetting: 'Text Setting',
|
||||
themeSetting: 'Theme Setting',
|
||||
// person configuration
|
||||
number: 'Number',
|
||||
name: 'Name',
|
||||
prizeName: 'Name',
|
||||
department: 'Department',
|
||||
identity: 'Identity',
|
||||
isLucky: 'Is Lucky',
|
||||
operation: 'Operation',
|
||||
setLuckyNumber: 'Set Lucky Number',
|
||||
luckyPeopleNumber: 'Lucky People Number',
|
||||
detail: 'Detail',
|
||||
noneData: 'No Data',
|
||||
// prize configuration
|
||||
fullParticipation: 'FullParticipation',
|
||||
numberParticipants: 'NumberParticipants',
|
||||
isDone: 'is Done',
|
||||
image: 'Image',
|
||||
onceNumber: 'Once Number',
|
||||
time: 'Time',
|
||||
// view setting
|
||||
title: 'Main Title',
|
||||
columnNumber: 'Column Number',
|
||||
theme: 'Theme',
|
||||
language: 'Language',
|
||||
cardColor: 'Card Color',
|
||||
winnerColor: 'Winner Color',
|
||||
textColor: 'Text Color',
|
||||
cardWidth: 'Card Width',
|
||||
cardHeight: 'Card Height',
|
||||
textSize: 'Text Size',
|
||||
highlightColor: 'HighLight Color',
|
||||
alwaysDisplay: 'Always Display Prize List',
|
||||
avatarDisplay: 'Show avatars or not',
|
||||
selectPicture: 'Select a Picture',
|
||||
backgroundImage: 'Select Background Image',
|
||||
timedStop: 'Timed Stop',
|
||||
playWinMusic: 'Play Win Music',
|
||||
resetAllData: 'Reset All Data',
|
||||
globalFont: 'Global Font',
|
||||
titleFont: 'Title Font',
|
||||
syncGlobalFont: 'Sync Global Font',
|
||||
}
|
||||
|
||||
export const tableZhCn = {
|
||||
// field block name
|
||||
abilitySetting: '功能设置',
|
||||
dataSetting: '数据设置',
|
||||
layoutSetting: '布局设置',
|
||||
patternSetting: '图案设置',
|
||||
textSetting: '文字设置',
|
||||
themeSetting: '主题设置',
|
||||
// person configuration
|
||||
number: '编号',
|
||||
name: '姓名',
|
||||
prizeName: '名称',
|
||||
department: '部门',
|
||||
identity: '身份',
|
||||
isLucky: '是否中奖',
|
||||
operation: '操作',
|
||||
setLuckyNumber: '设置中奖人数',
|
||||
luckyPeopleNumber: '中奖人数',
|
||||
detail: '详细信息',
|
||||
noneData: '暂无数据',
|
||||
// prize configuration
|
||||
fullParticipation: '可重复',
|
||||
numberParticipants: '抽奖人数',
|
||||
isDone: '已抽取',
|
||||
image: '图片',
|
||||
onceNumber: '单次抽取个数',
|
||||
time: '时间',
|
||||
// view setting
|
||||
title: '主标题',
|
||||
columnNumber: '列数',
|
||||
theme: '主题',
|
||||
language: '语言',
|
||||
cardColor: '卡片颜色',
|
||||
winnerColor: '中奖卡片颜色',
|
||||
textColor: '文字颜色',
|
||||
cardWidth: '卡片宽度',
|
||||
cardHeight: '卡片高度',
|
||||
textSize: '文字大小',
|
||||
highlightColor: '高亮颜色',
|
||||
alwaysDisplay: '常显奖项列表',
|
||||
avatarDisplay: '是否显示头像',
|
||||
selectPicture: '选择一张图片',
|
||||
backgroundImage: '选择背景图片',
|
||||
timedStop: '定时停止',
|
||||
playWinMusic: '播放中奖音乐',
|
||||
resetAllData: '重置数据',
|
||||
globalFont: '全局字体',
|
||||
titleFont: '标题字体',
|
||||
syncGlobalFont: '同步全局字体',
|
||||
}
|
||||
|
||||
export const table = {
|
||||
en: tableEn,
|
||||
zhCn: tableZhCn,
|
||||
}
|
||||
44
src/locales/modules/tooltip.ts
Normal file
44
src/locales/modules/tooltip.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
export const tooltipEn = {
|
||||
settingConfiguration: 'Setting/Configuration',
|
||||
nextSong: 'Right Click to Next Song',
|
||||
noSongPlay: 'No Song to Play',
|
||||
prizeList: 'Prize List',
|
||||
addActivity: 'Add Activity',
|
||||
downloadTemplateTip: 'After downloading the file, please fill in the data in Excel and save it in xlsx format',
|
||||
uploadExcelTip: 'Upload the modified Excel file',
|
||||
leftClick: 'Left Click to Slice',
|
||||
toHome: 'to Home',
|
||||
resetLayout: 'This item is time-consuming and performance intensive',
|
||||
defaultLayout: 'The default pattern setting is valid for 17 columns, please set the number of other columns yourself',
|
||||
doneCount: 'Number of winners',
|
||||
edit: 'Edit',
|
||||
delete: 'Delete',
|
||||
timedStop: 'After the lottery begins, it will stop at a scheduled time by default, set to 0, with the unit in seconds. A value of 0 disables the scheduled stopping function',
|
||||
uploadImage: 'Upload Image',
|
||||
pleaseGoto: 'Please go to',
|
||||
}
|
||||
|
||||
export const tooltipZhCn = {
|
||||
settingConfiguration: '设置/配置',
|
||||
nextSong: '右键点击下一首',
|
||||
noSongPlay: '没有音乐可以播放',
|
||||
prizeList: '奖项列表',
|
||||
addActivity: '添加抽奖',
|
||||
downloadTemplateTip: '下载文件后,请在excel中填写数据,并保存为xlsx格式',
|
||||
uploadExcelTip: '上传修改好的excel文件',
|
||||
leftClick: '左键切割',
|
||||
toHome: '主页',
|
||||
resetLayout: '该项比较耗费时间和性能',
|
||||
defaultLayout: '默认图案设置针对17列时有效,其他列数请自行设置',
|
||||
doneCount: '已抽取',
|
||||
edit: '编辑',
|
||||
delete: '删除',
|
||||
timedStop: '开始抽奖过后定时停止,默认为0,单位为秒,0为关闭定时停止功能',
|
||||
uploadImage: '上传图片',
|
||||
pleaseGoto: '请先前往',
|
||||
}
|
||||
|
||||
export const tooltip = {
|
||||
en: tooltipEn,
|
||||
zhCn: tooltipZhCn,
|
||||
}
|
||||
20
src/locales/modules/viewTitle.ts
Normal file
20
src/locales/modules/viewTitle.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export const viewTitleEn = {
|
||||
personManagement: 'Person Management',
|
||||
winnerManagement: 'Winner Management',
|
||||
prizeManagement: 'Prize Management',
|
||||
globalSetting: 'Global Setting',
|
||||
operatingInstructions: 'Operating Instructions',
|
||||
}
|
||||
|
||||
export const viewTitleZhCn = {
|
||||
personManagement: '人员管理',
|
||||
winnerManagement: '已中奖人员管理',
|
||||
prizeManagement: '奖项配置',
|
||||
globalSetting: '全局配置',
|
||||
operatingInstructions: '操作说明',
|
||||
}
|
||||
|
||||
export const viewTitle = {
|
||||
en: viewTitleEn,
|
||||
zhCn: viewTitleZhCn,
|
||||
}
|
||||
@@ -1,153 +1,14 @@
|
||||
export default {
|
||||
button: {
|
||||
enterLottery: '进入抽奖',
|
||||
start: '开始',
|
||||
selectLucky: '抽取幸运儿',
|
||||
continue: '继续',
|
||||
confirm: '确认',
|
||||
cancel: '取消',
|
||||
setting: '设置',
|
||||
delete: '删除',
|
||||
allDelete: '删除全部',
|
||||
downloadTemplate: '下载模板',
|
||||
importData: '导入数据',
|
||||
resetData: '重置数据',
|
||||
exportResult: '导出结果',
|
||||
add: '添加',
|
||||
resetDefault: '重置为默认',
|
||||
resetAllData: '重置所有数据',
|
||||
clearPattern: '清除图案',
|
||||
DefaultPattern: '默认图案',
|
||||
upload: '上传',
|
||||
reset: '重置',
|
||||
play: '播放',
|
||||
setLayout: '重设布局',
|
||||
close: '关闭',
|
||||
noInfoAndImport: '暂无人员信息,前往导入',
|
||||
useDefault: '使用默认数据',
|
||||
},
|
||||
sidebar: {
|
||||
personConfiguration: '人员配置',
|
||||
personList: '人员列表',
|
||||
winnerList: '中奖人员',
|
||||
prizeConfiguration: '奖品配置',
|
||||
globalSetting: '全局配置',
|
||||
viewSetting: '界面设置',
|
||||
imagesManagement: '图片管理',
|
||||
musicManagement: '音乐管理',
|
||||
operatingInstructions: '操作说明',
|
||||
},
|
||||
viewTitle: {
|
||||
personManagement: '人员管理',
|
||||
winnerManagement: '已中奖人员管理',
|
||||
prizeManagement: '奖项配置',
|
||||
globalSetting: '全局配置',
|
||||
operatingInstructions: '操作说明',
|
||||
},
|
||||
table: {
|
||||
// person configuration
|
||||
number: '编号',
|
||||
name: '姓名',
|
||||
prizeName: '名称',
|
||||
department: '部门',
|
||||
identity: '身份',
|
||||
isLucky: '是否中奖',
|
||||
operation: '操作',
|
||||
setLuckyNumber: '设置中奖人数',
|
||||
luckyPeopleNumber: '中奖人数',
|
||||
import { button, data, dialog, error, footer, placeHolder, sidebar, table, tooltip, viewTitle } from './modules'
|
||||
|
||||
detail: '详细信息',
|
||||
noneData: '暂无数据',
|
||||
// prize configuration
|
||||
fullParticipation: '全员参加',
|
||||
numberParticipants: '抽奖人数',
|
||||
isDone: '已抽取',
|
||||
image: '图片',
|
||||
onceNumber: '单次抽取个数',
|
||||
time: '时间',
|
||||
// view setting
|
||||
title: '主标题',
|
||||
columnNumber: '列数',
|
||||
theme: '主题',
|
||||
language: '语言',
|
||||
cardColor: '卡片颜色',
|
||||
winnerColor: '中奖卡片颜色',
|
||||
textColor: '文字颜色',
|
||||
cardWidth: '卡片宽度',
|
||||
cardHeight: '卡片高度',
|
||||
textSize: '文字大小',
|
||||
highlightColor: '高亮颜色',
|
||||
patternSetting: '图案设置',
|
||||
alwaysDisplay: '常显奖项列表',
|
||||
avatarDisplay: '是否显示头像',
|
||||
selectPicture: '选择一张图片',
|
||||
backgroundImage: '选择背景图片',
|
||||
},
|
||||
dialog: {
|
||||
titleTip: '提示!',
|
||||
titleTemporary: '增加临时抽奖',
|
||||
dialogPCWeb: '请使用PC进行访问以获得最佳显示效果',
|
||||
dialogDelAllPerson: '该操作会删除所有人员数据,是否继续?',
|
||||
dialogResetWinner: '该操作会清空人员中奖信息,是否继续?',
|
||||
dialogResetAllData: '该操作会重置所有数据,是否继续?',
|
||||
dialogSingleDrawLimit: '单次抽取只能抽取10位',
|
||||
dialogLatestBrowser: '请使用最新版Chrome或者Edge浏览器',
|
||||
tipResetPrize: '进行操作可能会重置数据,请谨慎操作',
|
||||
},
|
||||
tooltip: {
|
||||
settingConfiguration: '设置/配置',
|
||||
nextSong: '右键点击下一首',
|
||||
noSongPlay: '没有音乐可以播放',
|
||||
prizeList: '奖项列表',
|
||||
addActivity: '添加抽奖',
|
||||
downloadTemplateTip: '下载文件后,请在excel中填写数据,并保存为xlsx格式',
|
||||
uploadExcelTip: '上传修改好的excel文件',
|
||||
leftClick: '左键切割',
|
||||
toHome: '主页',
|
||||
resetLayout: '该项比较耗费时间和性能',
|
||||
defaultLayout: '默认图案设置针对17列时有效,其他列数请自行设置',
|
||||
doneCount: '已抽取',
|
||||
edit: '编辑',
|
||||
delete: '删除',
|
||||
},
|
||||
error: {
|
||||
require: '必填项',
|
||||
requireNumber: '请输入数字',
|
||||
minNumber1: '最小为1',
|
||||
maxNumber100: '最大为100',
|
||||
uploadSuccess: '上传成功',
|
||||
uploadFail: '上传失败',
|
||||
notImage: '不是图片',
|
||||
personIsAllDone: '抽奖抽完了',
|
||||
personNotEnough: '抽奖人数不足',
|
||||
startDraw: '现在抽取{count}{leftover}人',
|
||||
completeInformation: '请填写完整信息',
|
||||
},
|
||||
placeHolder: {
|
||||
enterTitle: '输入标题',
|
||||
name: '名称',
|
||||
winnerCount: '中奖人数',
|
||||
},
|
||||
data: {
|
||||
yes: '是',
|
||||
no: '否',
|
||||
number: '编号',
|
||||
isWin: '是否中奖',
|
||||
avatar: '头像',
|
||||
department: '部门',
|
||||
name: '姓名',
|
||||
identity: '身份',
|
||||
prizeName: '获奖',
|
||||
prizeTime: '获奖时间',
|
||||
operation: '操作',
|
||||
delete: '删除',
|
||||
removePerson: '移入未中奖名单',
|
||||
defaultTitle: '大明内阁六部御前奏对',
|
||||
xlsxName: '人口登记表-zhCn.xlsx',
|
||||
readmeName: 'readme-zhCn.md',
|
||||
},
|
||||
footer: {
|
||||
'self-reflection': '行有不得,反求诸己',
|
||||
'thiefEasy': '破山中贼易,破心中贼难',
|
||||
},
|
||||
export default {
|
||||
button: button.zhCn,
|
||||
sidebar: sidebar.zhCn,
|
||||
viewTitle: viewTitle.zhCn,
|
||||
table: table.zhCn,
|
||||
dialog: dialog.zhCn,
|
||||
tooltip: tooltip.zhCn,
|
||||
error: error.zhCn,
|
||||
placeHolder: placeHolder.zhCn,
|
||||
data: data.zhCn,
|
||||
footer: footer.zhCn,
|
||||
}
|
||||
|
||||
45
src/main.ts
45
src/main.ts
@@ -1,20 +1,49 @@
|
||||
import svgIcon from '@/components/SvgIcon/index.vue'
|
||||
import i18n from '@/locales/i18n'
|
||||
// pinia
|
||||
import { createPinia } from 'pinia'
|
||||
// pinia持久化
|
||||
import piniaPluginPersist from 'pinia-plugin-persist'
|
||||
import * as THREE from 'three'
|
||||
import { createApp } from 'vue'
|
||||
import VueDOMPurifyHTML from 'vue-dompurify-html'
|
||||
import svgIcon from '@/components/SvgIcon/index.vue'
|
||||
import i18n from '@/locales/i18n'
|
||||
// svg全局组件// 路由
|
||||
import router from '@/router'
|
||||
import App from './App.vue'
|
||||
import './style.css'
|
||||
import './style/markdown.css'
|
||||
import './style/style.scss'
|
||||
// 全局svg组件
|
||||
import 'virtual:svg-icons-register'
|
||||
// svg全局组件// 路由
|
||||
import router from '@/router'
|
||||
// pinia
|
||||
import { createPinia } from 'pinia'
|
||||
// pinia持久化
|
||||
import piniaPluginPersist from 'pinia-plugin-persist'
|
||||
|
||||
// 在应用初始化时尽早设置主题和字体,避免页面加载时的闪烁
|
||||
(function initializeThemeAndFont() {
|
||||
try {
|
||||
// 从localStorage获取全局配置
|
||||
const globalConfigStr = localStorage.getItem('globalConfig')
|
||||
|
||||
if (globalConfigStr) {
|
||||
const storageData = JSON.parse(globalConfigStr)
|
||||
// 根据persist策略,数据存储在globalConfig属性下
|
||||
const globalConfig = storageData.globalConfig || storageData
|
||||
|
||||
// 设置主题
|
||||
if (globalConfig.theme?.name) {
|
||||
const html = document.documentElement
|
||||
html.setAttribute('data-theme', globalConfig.theme.name)
|
||||
}
|
||||
|
||||
// 设置字体
|
||||
if (globalConfig.theme?.font) {
|
||||
// 更新CSS变量
|
||||
document.documentElement.style.setProperty('--app-font-family', `"${globalConfig.theme.font}", -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif`)
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.warn('Failed to set initial theme and font:', e)
|
||||
}
|
||||
})()
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
@@ -1,142 +1,146 @@
|
||||
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
|
||||
import Layout from '@/layout/index.vue'
|
||||
import i18n from '@/locales/i18n'
|
||||
import Home from '@/views/Home/index.vue'
|
||||
import { createRouter, createWebHistory,createWebHashHistory } from 'vue-router'
|
||||
|
||||
export const configRoutes = {
|
||||
path: '/log-lottery/config',
|
||||
name: 'Config',
|
||||
component: () => import('@/views/Config/index.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
redirect: '/log-lottery/config/person',
|
||||
},
|
||||
{
|
||||
path: '/log-lottery/config/person',
|
||||
name: 'PersonConfig',
|
||||
component: () => import('@/views/Config/Person/PersonConfig.vue'),
|
||||
meta: {
|
||||
title: i18n.global.t('sidebar.personConfiguration'),
|
||||
icon: 'person',
|
||||
},
|
||||
children: [
|
||||
path: '/log-lottery/config',
|
||||
name: 'Config',
|
||||
component: () => import('@/views/Config/index.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
redirect: '/log-lottery/config/person/all',
|
||||
path: '',
|
||||
redirect: '/log-lottery/config/person',
|
||||
},
|
||||
{
|
||||
path: '/log-lottery/config/person/all',
|
||||
name: 'AllPersonConfig',
|
||||
component: () => import('@/views/Config/Person/PersonAll.vue'),
|
||||
meta: {
|
||||
title: i18n.global.t('sidebar.personList'),
|
||||
icon: 'all',
|
||||
},
|
||||
path: '/log-lottery/config/person',
|
||||
name: 'PersonConfig',
|
||||
component: () => import('@/views/Config/Person/index.vue'),
|
||||
meta: {
|
||||
title: i18n.global.t('sidebar.personConfiguration'),
|
||||
icon: 'person',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
redirect: '/log-lottery/config/person/all',
|
||||
},
|
||||
{
|
||||
path: '/log-lottery/config/person/all',
|
||||
name: 'AllPersonConfig',
|
||||
component: () => import('@/views/Config/Person/PersonAll/index.vue'),
|
||||
meta: {
|
||||
title: i18n.global.t('sidebar.personList'),
|
||||
icon: 'all',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/log-lottery/config/person/already',
|
||||
name: 'AlreadyPerson',
|
||||
component: () => import('@/views/Config/Person/PersonAlready/index.vue'),
|
||||
meta: {
|
||||
title: i18n.global.t('sidebar.winnerList'),
|
||||
icon: 'already',
|
||||
},
|
||||
},
|
||||
// {
|
||||
// path:'other',
|
||||
// name:'OtherPersonConfig',
|
||||
// component:()=>import('@/views/Config/Person/OtherPersonConfig.vue'),
|
||||
// meta:{
|
||||
// title:'其他配置',
|
||||
// icon:'other'
|
||||
// }
|
||||
// }
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/log-lottery/config/person/already',
|
||||
name: 'AlreadyPerson',
|
||||
component: () => import('@/views/Config/Person/PersonAlready.vue'),
|
||||
meta: {
|
||||
title: i18n.global.t('sidebar.winnerList'),
|
||||
icon: 'already',
|
||||
},
|
||||
},
|
||||
// {
|
||||
// path:'other',
|
||||
// name:'OtherPersonConfig',
|
||||
// component:()=>import('@/views/Config/Person/OtherPersonConfig.vue'),
|
||||
// meta:{
|
||||
// title:'其他配置',
|
||||
// icon:'other'
|
||||
// }
|
||||
// }
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/log-lottery/config/prize',
|
||||
name: 'PrizeConfig',
|
||||
component: () => import('@/views/Config/Prize/PrizeConfig.vue'),
|
||||
meta: {
|
||||
title: i18n.global.t('sidebar.prizeConfiguration'),
|
||||
icon: 'prize',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/log-lottery/config/global',
|
||||
name: 'GlobalConfig',
|
||||
redirect: '/log-lottery/config/global/all',
|
||||
meta: {
|
||||
title: i18n.global.t('sidebar.globalSetting'),
|
||||
icon: 'global',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/log-lottery/config/global/face',
|
||||
name: 'FaceConfig',
|
||||
component: () => import('@/views/Config/Global/FaceConfig.vue'),
|
||||
meta: {
|
||||
title: i18n.global.t('sidebar.viewSetting'),
|
||||
icon: 'face',
|
||||
},
|
||||
path: '/log-lottery/config/prize',
|
||||
name: 'PrizeConfig',
|
||||
component: () => import('@/views/Config/Prize/PrizeConfig.vue'),
|
||||
meta: {
|
||||
title: i18n.global.t('sidebar.prizeConfiguration'),
|
||||
icon: 'prize',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/log-lottery/config/global/image',
|
||||
name: 'ImageConfig',
|
||||
component: () => import('@/views/Config/Global/ImageConfig.vue'),
|
||||
meta: {
|
||||
title: i18n.global.t('sidebar.imagesManagement'),
|
||||
icon: 'image',
|
||||
},
|
||||
path: '/log-lottery/config/global',
|
||||
name: 'GlobalConfig',
|
||||
redirect: '/log-lottery/config/global/all',
|
||||
meta: {
|
||||
title: i18n.global.t('sidebar.globalSetting'),
|
||||
icon: 'global',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/log-lottery/config/global/face',
|
||||
name: 'FaceConfig',
|
||||
component: () => import('@/views/Config/Global/FaceConfig/index.vue'),
|
||||
meta: {
|
||||
title: i18n.global.t('sidebar.viewSetting'),
|
||||
icon: 'face',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/log-lottery/config/global/image',
|
||||
name: 'ImageConfig',
|
||||
component: () => import('@/views/Config/Global/ImageConfig/index.vue'),
|
||||
meta: {
|
||||
title: i18n.global.t('sidebar.imagesManagement'),
|
||||
icon: 'image',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/log-lottery/config/global/music',
|
||||
name: 'MusicConfig',
|
||||
component: () => import('@/views/Config/Global/MusicConfig/index.vue'),
|
||||
meta: {
|
||||
title: i18n.global.t('sidebar.musicManagement'),
|
||||
icon: 'music',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/log-lottery/config/global/music',
|
||||
name: 'MusicConfig',
|
||||
component: () => import('@/views/Config/Global/MusicConfig.vue'),
|
||||
meta: {
|
||||
title: i18n.global.t('sidebar.musicManagement'),
|
||||
icon: 'music',
|
||||
},
|
||||
path: '/log-lottery/config/readme',
|
||||
name: 'Readme',
|
||||
component: () => import('@/views/Config/Readme/index.vue'),
|
||||
meta: {
|
||||
title: i18n.global.t('sidebar.operatingInstructions'),
|
||||
icon: 'readme',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/log-lottery/config/readme',
|
||||
name: 'Readme',
|
||||
component: () => import('@/views/Config/Readme/index.vue'),
|
||||
meta: {
|
||||
title: i18n.global.t('sidebar.operatingInstructions'),
|
||||
icon: 'readme',
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
}
|
||||
const routes = [
|
||||
{
|
||||
path: '/log-lottery',
|
||||
component: Layout,
|
||||
redirect: '/log-lottery/home',
|
||||
children: [
|
||||
{
|
||||
path: '/log-lottery/home',
|
||||
name: 'Home',
|
||||
component: Home,
|
||||
},
|
||||
{
|
||||
path: '/log-lottery/demo',
|
||||
name: 'Demo',
|
||||
component: () => import('@/views/Demo/index.vue'),
|
||||
},
|
||||
configRoutes,
|
||||
],
|
||||
},
|
||||
];
|
||||
const envMode=import.meta.env.MODE;
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/log-lottery',
|
||||
},
|
||||
{
|
||||
path: '/log-lottery',
|
||||
component: Layout,
|
||||
redirect: '/log-lottery/home',
|
||||
children: [
|
||||
{
|
||||
path: '/log-lottery/home',
|
||||
name: 'Home',
|
||||
component: Home,
|
||||
},
|
||||
{
|
||||
path: '/log-lottery/demo',
|
||||
name: 'Demo',
|
||||
component: () => import('@/views/Demo/index.vue'),
|
||||
},
|
||||
configRoutes,
|
||||
],
|
||||
},
|
||||
]
|
||||
const envMode = import.meta.env.MODE
|
||||
const router = createRouter({
|
||||
// 读取环境变量
|
||||
history: envMode==='file'?createWebHashHistory():createWebHistory(),
|
||||
routes,
|
||||
history: (envMode === 'file' || import.meta.env.TAURI_PLATFORM) ? createWebHashHistory() : createWebHistory(),
|
||||
routes,
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { IPersonConfig, IPrizeConfig } from '@/types/storeType'
|
||||
|
||||
const originUrl = 'https://to2026.xyz'
|
||||
|
||||
export const defaultPersonList = <IPersonConfig[]>
|
||||
type IPersonConfigWithoutUuid = Omit<IPersonConfig, 'uuid'>
|
||||
export const defaultPersonList = <IPersonConfigWithoutUuid[]>
|
||||
[
|
||||
{ uid: 'U100156001', name: '朱厚熜', department: '皇室', identity: '万岁爷', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 1, y: 1, id: 0, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||
{ uid: 'U100156002', name: '朱载垕', department: '皇室', identity: '裕王', avatar: 'https://img1.baidu.com/it/u=2165937980,813753762&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', x: 2, y: 1, id: 1, isWin: false, createTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', updateTime: 'Tue Jan 09 2024 23:20:07 GMT+0800 (China Standard Time)', prizeName: [], prizeTime: [], prizeId: [] },
|
||||
@@ -278,4 +278,4 @@ export const defaultImageList = [
|
||||
url: `${originUrl}/resource/image/image5.png`,
|
||||
},
|
||||
]
|
||||
export const defaultPatternList = [21, 38, 55, 54, 53, 70, 87, 88, 89, 23, 40, 57, 74, 91, 92, 93, 76, 59, 42, 25, 24, 27, 28, 29, 46, 63, 62, 61, 78, 95, 96, 97, 20, 19, 31, 48, 65, 66, 67, 84, 101, 100, 99, 32, 33]
|
||||
export const defaultPatternList = [21, 38, 55, 54, 53, 70, 87, 88, 89, 23, 40, 57, 74, 91, 92, 76, 59, 42, 25, 24, 27, 28, 29, 46, 63, 62, 61, 78, 95, 96, 97, 20, 19, 31, 48, 66, 67, 84, 101, 100, 32, 33, 93, 65, 82, 99]
|
||||
|
||||
@@ -1,280 +1,333 @@
|
||||
import type { IImage, IMusic } from '@/types/storeType'
|
||||
import i18n, { browserLanguage } from '@/locales/i18n'
|
||||
import { defineStore } from 'pinia'
|
||||
import i18n, { browserLanguage } from '@/locales/i18n'
|
||||
import { defaultImageList, defaultMusicList, defaultPatternList } from './data'
|
||||
// import { IPrizeConfig } from '@/types/storeType';
|
||||
export const useGlobalConfig = defineStore('global', {
|
||||
state() {
|
||||
return {
|
||||
globalConfig: {
|
||||
rowCount: 17,
|
||||
isSHowPrizeList: true,
|
||||
isShowAvatar: false,
|
||||
topTitle: i18n.global.t('data.defaultTitle'),
|
||||
language: browserLanguage,
|
||||
theme: {
|
||||
name: 'dracula',
|
||||
detail: { primary: '#0f5fd3' },
|
||||
cardColor: '#ff79c6',
|
||||
cardWidth: 140,
|
||||
cardHeight: 200,
|
||||
textColor: '#ffffff',
|
||||
luckyCardColor: '#ECB1AC',
|
||||
textSize: 30,
|
||||
patternColor: '#1b66c9',
|
||||
patternList: defaultPatternList as number[],
|
||||
background: {}, // 背景颜色或图片
|
||||
state() {
|
||||
return {
|
||||
globalConfig: {
|
||||
rowCount: 17,
|
||||
isSHowPrizeList: true,
|
||||
isShowAvatar: false,
|
||||
topTitle: i18n.global.t('data.defaultTitle'),
|
||||
language: browserLanguage,
|
||||
definiteTime: null as number | null,
|
||||
winMusic: false,
|
||||
theme: {
|
||||
name: 'dracula',
|
||||
detail: { primary: '#0f5fd3' },
|
||||
cardColor: '#ff79c6',
|
||||
cardWidth: 140,
|
||||
cardHeight: 200,
|
||||
textColor: '#00000000',
|
||||
luckyCardColor: '#ECB1AC',
|
||||
textSize: 30,
|
||||
patternColor: '#1b66c9',
|
||||
patternList: defaultPatternList as number[],
|
||||
background: {}, // 背景颜色或图片
|
||||
font: '微软雅黑',
|
||||
titleFont: '微软雅黑',
|
||||
titleFontSyncGlobal: true,
|
||||
},
|
||||
musicList: defaultMusicList as IMusic[],
|
||||
imageList: defaultImageList as IImage[],
|
||||
},
|
||||
currentMusic: {
|
||||
item: defaultMusicList[0] as IMusic,
|
||||
paused: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
// 获取全部配置
|
||||
getGlobalConfig(state) {
|
||||
return state.globalConfig
|
||||
},
|
||||
musicList: defaultMusicList as IMusic[],
|
||||
imageList: defaultImageList as IImage[],
|
||||
},
|
||||
currentMusic: {
|
||||
item: defaultMusicList[0],
|
||||
paused: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
// 获取全部配置
|
||||
getGlobalConfig(state) {
|
||||
return state.globalConfig
|
||||
},
|
||||
// 获取标题
|
||||
getTopTitle(state) {
|
||||
return state.globalConfig.topTitle
|
||||
},
|
||||
// 获取行数
|
||||
getRowCount(state) {
|
||||
return state.globalConfig.rowCount
|
||||
},
|
||||
// 获取主题
|
||||
getTheme(state) {
|
||||
return state.globalConfig.theme
|
||||
},
|
||||
// 获取卡片颜色
|
||||
getCardColor(state) {
|
||||
return state.globalConfig.theme.cardColor
|
||||
},
|
||||
// 获取中奖颜色
|
||||
getLuckyColor(state) {
|
||||
return state.globalConfig.theme.luckyCardColor
|
||||
},
|
||||
// 获取文字颜色
|
||||
getTextColor(state) {
|
||||
return state.globalConfig.theme.textColor
|
||||
},
|
||||
// 获取卡片宽高
|
||||
getCardSize(state) {
|
||||
return {
|
||||
width: state.globalConfig.theme.cardWidth,
|
||||
height: state.globalConfig.theme.cardHeight,
|
||||
}
|
||||
},
|
||||
// 获取文字大小
|
||||
getTextSize(state) {
|
||||
return state.globalConfig.theme.textSize
|
||||
},
|
||||
// 获取图案颜色
|
||||
getPatterColor(state) {
|
||||
return state.globalConfig.theme.patternColor
|
||||
},
|
||||
// 获取图案列表
|
||||
getPatternList(state) {
|
||||
return state.globalConfig.theme.patternList
|
||||
},
|
||||
// 获取音乐列表
|
||||
getMusicList(state) {
|
||||
return state.globalConfig.musicList
|
||||
},
|
||||
// 获取当前音乐
|
||||
getCurrentMusic(state) {
|
||||
return state.currentMusic
|
||||
},
|
||||
// 获取图片列表
|
||||
getImageList(state) {
|
||||
return state.globalConfig.imageList
|
||||
},
|
||||
// 获取是否显示奖品列表
|
||||
getIsShowPrizeList(state) {
|
||||
return state.globalConfig.isSHowPrizeList
|
||||
},
|
||||
// 获取当前语言
|
||||
getLanguage(state) {
|
||||
return state.globalConfig.language
|
||||
},
|
||||
// 获取背景图片设置
|
||||
getBackground(state) {
|
||||
return state.globalConfig.theme.background
|
||||
},
|
||||
// 获取是否显示头像
|
||||
getIsShowAvatar(state) {
|
||||
return state.globalConfig.isShowAvatar
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
// 设置rowCount
|
||||
setRowCount(rowCount: number) {
|
||||
this.globalConfig.rowCount = rowCount
|
||||
},
|
||||
// 设置标题
|
||||
setTopTitle(topTitle: string) {
|
||||
this.globalConfig.topTitle = topTitle
|
||||
},
|
||||
// 设置主题
|
||||
setTheme(theme: any) {
|
||||
const { name, detail } = theme
|
||||
this.globalConfig.theme.name = name
|
||||
this.globalConfig.theme.detail = detail
|
||||
},
|
||||
// 设置卡片颜色
|
||||
setCardColor(cardColor: string) {
|
||||
this.globalConfig.theme.cardColor = cardColor
|
||||
},
|
||||
// 设置中奖颜色
|
||||
setLuckyCardColor(luckyCardColor: string) {
|
||||
this.globalConfig.theme.luckyCardColor = luckyCardColor
|
||||
},
|
||||
// 设置文字颜色
|
||||
setTextColor(textColor: string) {
|
||||
this.globalConfig.theme.textColor = textColor
|
||||
},
|
||||
// 设置卡片宽高
|
||||
setCardSize(cardSize: { width: number, height: number }) {
|
||||
this.globalConfig.theme.cardWidth = cardSize.width
|
||||
this.globalConfig.theme.cardHeight = cardSize.height
|
||||
},
|
||||
// 设置文字大小
|
||||
setTextSize(textSize: number) {
|
||||
this.globalConfig.theme.textSize = textSize
|
||||
},
|
||||
// 设置图案颜色
|
||||
setPatterColor(patterColor: string) {
|
||||
this.globalConfig.theme.patternColor = patterColor
|
||||
},
|
||||
// 设置图案列表
|
||||
setPatternList(patternList: number[]) {
|
||||
this.globalConfig.theme.patternList = patternList
|
||||
},
|
||||
// 重置图案列表
|
||||
resetPatternList() {
|
||||
this.globalConfig.theme.patternList = defaultPatternList
|
||||
},
|
||||
// 添加音乐
|
||||
addMusic(music: IMusic) {
|
||||
// 验证音乐是否已存在,看name字段
|
||||
for (let i = 0; i < this.globalConfig.musicList.length; i++) {
|
||||
if (this.globalConfig.musicList[i].name === music.name) {
|
||||
return
|
||||
}
|
||||
}
|
||||
this.globalConfig.musicList.push(music)
|
||||
},
|
||||
// 删除音乐
|
||||
removeMusic(musicId: string) {
|
||||
for (let i = 0; i < this.globalConfig.musicList.length; i++) {
|
||||
if (this.globalConfig.musicList[i].id === musicId) {
|
||||
this.globalConfig.musicList.splice(i, 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
// 设置当前播放音乐
|
||||
setCurrentMusic(musicItem: IMusic, paused: boolean = true) {
|
||||
this.currentMusic = {
|
||||
item: musicItem,
|
||||
paused,
|
||||
}
|
||||
},
|
||||
// 重置音乐列表
|
||||
resetMusicList() {
|
||||
this.globalConfig.musicList = JSON.parse(JSON.stringify(defaultMusicList)) as IMusic[]
|
||||
},
|
||||
// 清空音乐列表
|
||||
clearMusicList() {
|
||||
this.globalConfig.musicList = [] as IMusic[]
|
||||
},
|
||||
// 添加图片
|
||||
addImage(image: IImage) {
|
||||
for (let i = 0; i < this.globalConfig.imageList.length; i++) {
|
||||
if (this.globalConfig.imageList[i].name === image.name) {
|
||||
return
|
||||
}
|
||||
}
|
||||
this.globalConfig.imageList.push(image)
|
||||
},
|
||||
// 删除图片
|
||||
removeImage(imageId: string) {
|
||||
for (let i = 0; i < this.globalConfig.imageList.length; i++) {
|
||||
if (this.globalConfig.imageList[i].id === imageId) {
|
||||
this.globalConfig.imageList.splice(i, 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
// 重置图片列表
|
||||
resetImageList() {
|
||||
this.globalConfig.imageList = defaultImageList as IImage[]
|
||||
},
|
||||
// 清空图片列表
|
||||
clearImageList() {
|
||||
this.globalConfig.imageList = [] as IImage[]
|
||||
},
|
||||
// 设置是否显示奖品列表
|
||||
setIsShowPrizeList(isShowPrizeList: boolean) {
|
||||
this.globalConfig.isSHowPrizeList = isShowPrizeList
|
||||
},
|
||||
// 设置
|
||||
setLanguage(language: string) {
|
||||
this.globalConfig.language = language
|
||||
i18n.global.locale.value = language
|
||||
},
|
||||
// 设置背景图片
|
||||
setBackground(background: any) {
|
||||
this.globalConfig.theme.background = background
|
||||
},
|
||||
// 设置是否显示头像
|
||||
setIsShowAvatar(isShowAvatar: boolean) {
|
||||
this.globalConfig.isShowAvatar = isShowAvatar
|
||||
},
|
||||
// 重置所有配置
|
||||
reset() {
|
||||
this.globalConfig = {
|
||||
rowCount: 17,
|
||||
isSHowPrizeList: true,
|
||||
isShowAvatar: false,
|
||||
topTitle: i18n.global.t('data.defaultTitle'),
|
||||
language: browserLanguage,
|
||||
theme: {
|
||||
name: 'dracula',
|
||||
detail: { primary: '#0f5fd3' },
|
||||
cardColor: '#ff79c6',
|
||||
cardWidth: 140,
|
||||
cardHeight: 200,
|
||||
textColor: '#ffffff',
|
||||
luckyCardColor: '#ECB1AC',
|
||||
textSize: 30,
|
||||
patternColor: '#1b66c9',
|
||||
patternList: defaultPatternList as number[],
|
||||
background: {}, // 背景颜色或图片
|
||||
// 获取标题
|
||||
getTopTitle(state) {
|
||||
return state.globalConfig.topTitle
|
||||
},
|
||||
// 获取行数
|
||||
getRowCount(state) {
|
||||
return state.globalConfig.rowCount
|
||||
},
|
||||
// 获取主题
|
||||
getTheme(state) {
|
||||
return state.globalConfig.theme
|
||||
},
|
||||
// 获取卡片颜色
|
||||
getCardColor(state) {
|
||||
return state.globalConfig.theme.cardColor
|
||||
},
|
||||
// 获取中奖颜色
|
||||
getLuckyColor(state) {
|
||||
return state.globalConfig.theme.luckyCardColor
|
||||
},
|
||||
// 获取文字颜色
|
||||
getTextColor(state) {
|
||||
return state.globalConfig.theme.textColor
|
||||
},
|
||||
// 获取卡片宽高
|
||||
getCardSize(state) {
|
||||
return {
|
||||
width: state.globalConfig.theme.cardWidth,
|
||||
height: state.globalConfig.theme.cardHeight,
|
||||
}
|
||||
},
|
||||
// 获取文字大小
|
||||
getTextSize(state) {
|
||||
return state.globalConfig.theme.textSize
|
||||
},
|
||||
// 获取图案颜色
|
||||
getPatterColor(state) {
|
||||
return state.globalConfig.theme.patternColor
|
||||
},
|
||||
// 获取图案列表
|
||||
getPatternList(state) {
|
||||
return state.globalConfig.theme.patternList
|
||||
},
|
||||
// 获取音乐列表
|
||||
getMusicList(state) {
|
||||
return state.globalConfig.musicList
|
||||
},
|
||||
// 获取当前音乐
|
||||
getCurrentMusic(state) {
|
||||
return state.currentMusic
|
||||
},
|
||||
// 获取图片列表
|
||||
getImageList(state) {
|
||||
return state.globalConfig.imageList
|
||||
},
|
||||
// 获取是否显示奖品列表
|
||||
getIsShowPrizeList(state) {
|
||||
return state.globalConfig.isSHowPrizeList
|
||||
},
|
||||
// 获取当前语言
|
||||
getLanguage(state) {
|
||||
return state.globalConfig.language
|
||||
},
|
||||
// 获取背景图片设置
|
||||
getBackground(state) {
|
||||
return state.globalConfig.theme.background
|
||||
},
|
||||
// 获取字体
|
||||
getFont(state) {
|
||||
return state.globalConfig.theme.font
|
||||
},
|
||||
// 获取标题字体
|
||||
getTitleFont(state) {
|
||||
return state.globalConfig.theme.titleFont
|
||||
},
|
||||
// 获取标题字体同步全局
|
||||
getTitleFontSyncGlobal(state) {
|
||||
return state.globalConfig.theme.titleFontSyncGlobal
|
||||
},
|
||||
// 获取是否显示头像
|
||||
getIsShowAvatar(state) {
|
||||
return state.globalConfig.isShowAvatar
|
||||
},
|
||||
// 获取定时抽取时间
|
||||
getDefiniteTime(state) {
|
||||
return state.globalConfig.definiteTime
|
||||
},
|
||||
// 是否播放获奖音乐
|
||||
getWinMusic(state) {
|
||||
return state.globalConfig.winMusic
|
||||
},
|
||||
musicList: defaultMusicList as IMusic[],
|
||||
imageList: defaultImageList as IImage[],
|
||||
}
|
||||
this.currentMusic = {
|
||||
item: defaultMusicList[0],
|
||||
paused: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
persist: {
|
||||
enabled: true,
|
||||
strategies: [
|
||||
{
|
||||
// 如果要存储在localStorage中
|
||||
storage: localStorage,
|
||||
key: 'globalConfig',
|
||||
paths: ['globalConfig'],
|
||||
},
|
||||
],
|
||||
},
|
||||
actions: {
|
||||
// 设置全局配置
|
||||
setGlobalConfig(data: any) {
|
||||
this.globalConfig = data
|
||||
},
|
||||
// 设置rowCount
|
||||
setRowCount(rowCount: number) {
|
||||
this.globalConfig.rowCount = rowCount
|
||||
},
|
||||
// 设置标题
|
||||
setTopTitle(topTitle: string) {
|
||||
this.globalConfig.topTitle = topTitle
|
||||
},
|
||||
// 设置主题
|
||||
setTheme(theme: any) {
|
||||
const { name } = theme
|
||||
this.globalConfig.theme.name = name
|
||||
},
|
||||
// 设置卡片颜色
|
||||
setCardColor(cardColor: string) {
|
||||
this.globalConfig.theme.cardColor = cardColor
|
||||
},
|
||||
// 设置中奖颜色
|
||||
setLuckyCardColor(luckyCardColor: string) {
|
||||
this.globalConfig.theme.luckyCardColor = luckyCardColor
|
||||
},
|
||||
// 设置文字颜色
|
||||
setTextColor(textColor: string) {
|
||||
this.globalConfig.theme.textColor = textColor
|
||||
},
|
||||
// 设置卡片宽高
|
||||
setCardSize(cardSize: { width: number, height: number }) {
|
||||
this.globalConfig.theme.cardWidth = cardSize.width
|
||||
this.globalConfig.theme.cardHeight = cardSize.height
|
||||
},
|
||||
// 设置文字大小
|
||||
setTextSize(textSize: number) {
|
||||
this.globalConfig.theme.textSize = textSize
|
||||
},
|
||||
// 设置图案颜色
|
||||
setPatterColor(patterColor: string) {
|
||||
this.globalConfig.theme.patternColor = patterColor
|
||||
},
|
||||
// 设置图案列表
|
||||
setPatternList(patternList: number[]) {
|
||||
this.globalConfig.theme.patternList = patternList
|
||||
},
|
||||
// 重置图案列表
|
||||
resetPatternList() {
|
||||
this.globalConfig.theme.patternList = defaultPatternList
|
||||
},
|
||||
// 添加音乐
|
||||
addMusic(music: IMusic) {
|
||||
// 验证音乐是否已存在,看name字段
|
||||
for (let i = 0; i < this.globalConfig.musicList.length; i++) {
|
||||
if (this.globalConfig.musicList[i].name === music.name) {
|
||||
return
|
||||
}
|
||||
}
|
||||
this.globalConfig.musicList.push(music)
|
||||
},
|
||||
// 删除音乐
|
||||
removeMusic(musicId: string) {
|
||||
for (let i = 0; i < this.globalConfig.musicList.length; i++) {
|
||||
if (this.globalConfig.musicList[i].id === musicId) {
|
||||
this.globalConfig.musicList.splice(i, 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
// 设置当前播放音乐
|
||||
setCurrentMusic(musicItem: IMusic, paused: boolean = true) {
|
||||
this.currentMusic = {
|
||||
item: musicItem,
|
||||
paused,
|
||||
}
|
||||
},
|
||||
// 重置音乐列表
|
||||
resetMusicList() {
|
||||
this.globalConfig.musicList = JSON.parse(JSON.stringify(defaultMusicList)) as IMusic[]
|
||||
},
|
||||
// 清空音乐列表
|
||||
clearMusicList() {
|
||||
this.globalConfig.musicList = [] as IMusic[]
|
||||
},
|
||||
// 添加图片
|
||||
addImage(image: IImage) {
|
||||
for (let i = 0; i < this.globalConfig.imageList.length; i++) {
|
||||
if (this.globalConfig.imageList[i].name === image.name) {
|
||||
return
|
||||
}
|
||||
}
|
||||
this.globalConfig.imageList.push(image)
|
||||
},
|
||||
// 删除图片
|
||||
removeImage(imageId: string) {
|
||||
for (let i = 0; i < this.globalConfig.imageList.length; i++) {
|
||||
if (this.globalConfig.imageList[i].id === imageId) {
|
||||
this.globalConfig.imageList.splice(i, 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
// 重置图片列表
|
||||
resetImageList() {
|
||||
this.globalConfig.imageList = defaultImageList as IImage[]
|
||||
},
|
||||
// 清空图片列表
|
||||
clearImageList() {
|
||||
this.globalConfig.imageList = [] as IImage[]
|
||||
},
|
||||
// 设置是否显示奖品列表
|
||||
setIsShowPrizeList(isShowPrizeList: boolean) {
|
||||
this.globalConfig.isSHowPrizeList = isShowPrizeList
|
||||
},
|
||||
// 设置
|
||||
setLanguage(language: string) {
|
||||
this.globalConfig.language = language
|
||||
i18n.global.locale.value = language
|
||||
},
|
||||
// 设置背景图片
|
||||
setBackground(background: any) {
|
||||
this.globalConfig.theme.background = background
|
||||
},
|
||||
// 设置字体
|
||||
setFont(font: any) {
|
||||
this.globalConfig.theme.font = font
|
||||
},
|
||||
// 设置标题字体
|
||||
setTitleFont(titleFont: any) {
|
||||
this.globalConfig.theme.titleFont = titleFont
|
||||
},
|
||||
// 设置同步全局字体
|
||||
setTitleFontSyncGlobal(titleFontSyncGlobal: boolean) {
|
||||
this.globalConfig.theme.titleFontSyncGlobal = titleFontSyncGlobal
|
||||
},
|
||||
// 设置是否显示头像
|
||||
setIsShowAvatar(isShowAvatar: boolean) {
|
||||
this.globalConfig.isShowAvatar = isShowAvatar
|
||||
},
|
||||
// 设置定时抽取时间
|
||||
setDefiniteTime(definiteTime: number | null) {
|
||||
this.globalConfig.definiteTime = definiteTime
|
||||
},
|
||||
// 设置是否播放获奖音乐
|
||||
setIsPlayWinMusic(winMusic: boolean) {
|
||||
this.globalConfig.winMusic = winMusic
|
||||
},
|
||||
// 重置所有配置
|
||||
reset() {
|
||||
this.globalConfig = {
|
||||
rowCount: 17,
|
||||
winMusic: false,
|
||||
isSHowPrizeList: true,
|
||||
isShowAvatar: false,
|
||||
topTitle: i18n.global.t('data.defaultTitle'),
|
||||
language: browserLanguage,
|
||||
definiteTime: null,
|
||||
theme: {
|
||||
name: 'dracula',
|
||||
detail: { primary: '#0f5fd3' },
|
||||
cardColor: '#ff79c6',
|
||||
cardWidth: 140,
|
||||
cardHeight: 200,
|
||||
textColor: '#00000000',
|
||||
luckyCardColor: '#ECB1AC',
|
||||
textSize: 30,
|
||||
patternColor: '#1b66c9',
|
||||
patternList: defaultPatternList as number[],
|
||||
background: {}, // 背景颜色或图片
|
||||
font: '微软雅黑',
|
||||
titleFont: '微软雅黑',
|
||||
titleFontSyncGlobal: true,
|
||||
},
|
||||
musicList: defaultMusicList as IMusic[],
|
||||
imageList: defaultImageList as IImage[],
|
||||
}
|
||||
this.currentMusic = {
|
||||
item: defaultMusicList[0],
|
||||
paused: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
persist: {
|
||||
enabled: true,
|
||||
strategies: [
|
||||
{
|
||||
// 如果要存储在localStorage中
|
||||
storage: localStorage,
|
||||
key: 'globalConfig',
|
||||
paths: ['globalConfig'],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,159 +1,203 @@
|
||||
import type { IPersonConfig, IPrizeConfig } from '@/types/storeType'
|
||||
|
||||
import dayjs from 'dayjs'
|
||||
import { defineStore } from 'pinia'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { computed, ref, toRaw } from 'vue'
|
||||
import { IndexDb } from '@/utils/dexie'
|
||||
import { defaultPersonList } from './data'
|
||||
import { usePrizeConfig } from './prizeConfig'
|
||||
|
||||
export const usePersonConfig = defineStore('person', {
|
||||
state() {
|
||||
return {
|
||||
personConfig: {
|
||||
// 获取IPersonConfig的key组成数组
|
||||
export const personListKey = Object.keys(defaultPersonList[0])
|
||||
export const usePersonConfig = defineStore('person', () => {
|
||||
const personDb = new IndexDb('person', ['allPersonList', 'alreadyPersonList'], 1, ['createTime'])
|
||||
// NOTE: state
|
||||
const personConfig = ref({
|
||||
allPersonList: [] as IPersonConfig[],
|
||||
alreadyPersonList: [] as IPersonConfig[],
|
||||
},
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
})
|
||||
personDb.getDataSortedByDateTime('allPersonList', 'createTime').then((data) => {
|
||||
personConfig.value.allPersonList = data
|
||||
})
|
||||
personDb.getAllData('alreadyPersonList').then((data) => {
|
||||
personConfig.value.alreadyPersonList = data
|
||||
})
|
||||
|
||||
// NOTE: getter
|
||||
// 获取全部配置
|
||||
getPersonConfig(state) {
|
||||
return state.personConfig
|
||||
},
|
||||
const getPersonConfig = computed(() => personConfig.value)
|
||||
// 获取全部人员名单
|
||||
getAllPersonList(state) {
|
||||
return state.personConfig.allPersonList.filter((item: IPersonConfig) => {
|
||||
return item
|
||||
})
|
||||
},
|
||||
const getAllPersonList = computed(() => personConfig.value.allPersonList)
|
||||
// 获取未获此奖的人员名单
|
||||
getNotThisPrizePersonList(state: any) {
|
||||
const currentPrize = usePrizeConfig().prizeConfig.currentPrize
|
||||
const data = state.personConfig.allPersonList.filter((item: IPersonConfig) => {
|
||||
return !item.prizeId.includes(currentPrize.id as string)
|
||||
})
|
||||
|
||||
return data
|
||||
},
|
||||
// 获取已中奖人员名单
|
||||
getAlreadyPersonList(state) {
|
||||
return state.personConfig.allPersonList.filter((item: IPersonConfig) => {
|
||||
return item.isWin === true
|
||||
})
|
||||
},
|
||||
// 获取中奖人员详情
|
||||
getAlreadyPersonDetail(state) {
|
||||
return state.personConfig.alreadyPersonList
|
||||
},
|
||||
// 获取未中奖人员名单
|
||||
getNotPersonList(state) {
|
||||
return state.personConfig.allPersonList.filter((item: IPersonConfig) => {
|
||||
return item.isWin === false
|
||||
})
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
// 添加未中奖人员
|
||||
addNotPersonList(personList: IPersonConfig[]) {
|
||||
if (personList.length <= 0) {
|
||||
return
|
||||
}
|
||||
personList.forEach((item: IPersonConfig) => {
|
||||
this.personConfig.allPersonList.push(item)
|
||||
})
|
||||
},
|
||||
// 添加已中奖人员
|
||||
addAlreadyPersonList(personList: IPersonConfig[], prize: IPrizeConfig | null) {
|
||||
if (personList.length <= 0) {
|
||||
return
|
||||
}
|
||||
personList.forEach((person: IPersonConfig) => {
|
||||
this.personConfig.allPersonList.map((item: IPersonConfig) => {
|
||||
if (item.id === person.id && prize != null) {
|
||||
item.isWin = true
|
||||
// person.isWin = true
|
||||
item.prizeName.push(prize.name)
|
||||
// person.prizeName += prize.name
|
||||
item.prizeTime.push(dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss'))
|
||||
// person.prizeTime = new Date().toString()
|
||||
item.prizeId.push(prize.id as string)
|
||||
}
|
||||
|
||||
return item
|
||||
const getNotThisPrizePersonList = computed(() => {
|
||||
const currentPrize = usePrizeConfig().prizeConfig.currentPrize
|
||||
const data = personConfig.value.allPersonList.filter((item: IPersonConfig) => {
|
||||
return !item.prizeId.includes(currentPrize.id as string)
|
||||
})
|
||||
this.personConfig.alreadyPersonList.push(person)
|
||||
})
|
||||
},
|
||||
// 从已中奖移动到未中奖
|
||||
moveAlreadyToNot(person: IPersonConfig) {
|
||||
if (person.id === undefined || person.id == null) {
|
||||
return
|
||||
}
|
||||
const alreadyPersonListLength = this.personConfig.alreadyPersonList.length
|
||||
for (let i = 0; i < this.personConfig.allPersonList.length; i++) {
|
||||
if (person.id === this.personConfig.allPersonList[i].id) {
|
||||
this.personConfig.allPersonList[i].isWin = false
|
||||
this.personConfig.allPersonList[i].prizeName = []
|
||||
this.personConfig.allPersonList[i].prizeTime = []
|
||||
this.personConfig.allPersonList[i].prizeId = []
|
||||
|
||||
break
|
||||
return data
|
||||
})
|
||||
|
||||
// 获取已中奖人员名单
|
||||
const getAlreadyPersonList = computed(() => {
|
||||
return personConfig.value.allPersonList.filter((item: IPersonConfig) => {
|
||||
return item.isWin === true
|
||||
})
|
||||
})
|
||||
// 获取中奖人员详情
|
||||
const getAlreadyPersonDetail = computed(() => personConfig.value.alreadyPersonList)
|
||||
// 获取未中奖人员名单
|
||||
const getNotPersonList = computed(() => personConfig.value.allPersonList.filter((item: IPersonConfig) => {
|
||||
return item.isWin === false
|
||||
}))
|
||||
// NOTE: action
|
||||
// 添加全部未中奖人员
|
||||
function addNotPersonList(personList: IPersonConfig[]) {
|
||||
if (personList.length <= 0) {
|
||||
return
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < alreadyPersonListLength; i++) {
|
||||
this.personConfig.alreadyPersonList = this.personConfig.alreadyPersonList.filter((item: IPersonConfig) =>
|
||||
item.id !== person.id,
|
||||
)
|
||||
}
|
||||
},
|
||||
personList.forEach((item: IPersonConfig) => {
|
||||
personConfig.value.allPersonList.push(item)
|
||||
})
|
||||
personDb.setAllData('allPersonList', personList)
|
||||
}
|
||||
// 添加数据
|
||||
function addOnePerson(person: IPersonConfig[]) {
|
||||
if (person.length <= 0) {
|
||||
return
|
||||
}
|
||||
if (person.length > 1) {
|
||||
console.warn('只支持添加单个用户')
|
||||
return
|
||||
}
|
||||
person.forEach((item: IPersonConfig) => {
|
||||
personConfig.value.allPersonList.push(item)
|
||||
personDb.setData('allPersonList', item)
|
||||
})
|
||||
}
|
||||
// 添加已中奖人员
|
||||
function addAlreadyPersonList(personList: IPersonConfig[], prize: IPrizeConfig | null) {
|
||||
if (personList.length <= 0) {
|
||||
return
|
||||
}
|
||||
personList.forEach((person: IPersonConfig) => {
|
||||
personConfig.value.allPersonList.map((item: IPersonConfig) => {
|
||||
if (item.id === person.id && prize != null) {
|
||||
item.isWin = true
|
||||
// person.isWin = true
|
||||
item.prizeName.push(prize.name)
|
||||
// person.prizeName += prize.name
|
||||
item.prizeTime.push(dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss'))
|
||||
// person.prizeTime = new Date().toString()
|
||||
item.prizeId.push(prize.id as string)
|
||||
}
|
||||
return item
|
||||
})
|
||||
personConfig.value.alreadyPersonList.push(person)
|
||||
personDb.updateData('allPersonList', toRaw(person))
|
||||
personDb.setData('alreadyPersonList', toRaw(person))
|
||||
})
|
||||
}
|
||||
// 从已中奖移动到未中奖
|
||||
function moveAlreadyToNot(person: IPersonConfig) {
|
||||
if (person.id === undefined || person.id == null) {
|
||||
return
|
||||
}
|
||||
const alreadyPersonListLength = personConfig.value.alreadyPersonList.length
|
||||
for (let i = 0; i < personConfig.value.allPersonList.length; i++) {
|
||||
if (person.id === personConfig.value.allPersonList[i].id) {
|
||||
personConfig.value.allPersonList[i].isWin = false
|
||||
personConfig.value.allPersonList[i].prizeName = []
|
||||
personConfig.value.allPersonList[i].prizeTime = []
|
||||
personConfig.value.allPersonList[i].prizeId = []
|
||||
personDb.updateData('allPersonList', toRaw(personConfig.value.allPersonList[i]))
|
||||
break
|
||||
}
|
||||
}
|
||||
const alreadyPersonListRaw = toRaw(personConfig.value.alreadyPersonList)
|
||||
for (let i = 0; i < alreadyPersonListLength; i++) {
|
||||
personConfig.value.alreadyPersonList = alreadyPersonListRaw.filter((item: IPersonConfig) =>
|
||||
item.id !== person.id,
|
||||
)
|
||||
}
|
||||
personDb.deleteData('alreadyPersonList', person)
|
||||
}
|
||||
// 删除指定人员
|
||||
deletePerson(person: IPersonConfig) {
|
||||
if (person.id !== undefined || person.id != null) {
|
||||
this.personConfig.allPersonList = this.personConfig.allPersonList.filter((item: IPersonConfig) => item.id !== person.id)
|
||||
this.personConfig.alreadyPersonList = this.personConfig.alreadyPersonList.filter((item: IPersonConfig) => item.id !== person.id)
|
||||
}
|
||||
},
|
||||
function deletePerson(person: IPersonConfig) {
|
||||
if (person.id !== undefined || person.id != null) {
|
||||
const allPersonListRaw = toRaw(personConfig.value.allPersonList)
|
||||
const alreadyPersonListRaw = toRaw(personConfig.value.alreadyPersonList)
|
||||
personConfig.value.allPersonList = allPersonListRaw.filter((item: IPersonConfig) => item.id !== person.id)
|
||||
personConfig.value.alreadyPersonList = alreadyPersonListRaw.filter((item: IPersonConfig) => item.id !== person.id)
|
||||
personDb.deleteData('allPersonList', person)
|
||||
personDb.deleteData('alreadyPersonList', person)
|
||||
}
|
||||
}
|
||||
// 删除所有人员
|
||||
deleteAllPerson() {
|
||||
this.personConfig.allPersonList = []
|
||||
this.personConfig.alreadyPersonList = []
|
||||
},
|
||||
function deleteAllPerson() {
|
||||
personConfig.value.allPersonList = []
|
||||
personConfig.value.alreadyPersonList = []
|
||||
personDb.deleteAll('allPersonList')
|
||||
personDb.deleteAll('alreadyPersonList')
|
||||
}
|
||||
|
||||
// 删除所有人员
|
||||
resetPerson() {
|
||||
this.personConfig.allPersonList = []
|
||||
this.personConfig.alreadyPersonList = []
|
||||
},
|
||||
function resetPerson() {
|
||||
personConfig.value.allPersonList = []
|
||||
personConfig.value.alreadyPersonList = []
|
||||
personDb.deleteAll('allPersonList')
|
||||
personDb.deleteAll('alreadyPersonList')
|
||||
}
|
||||
// 重置已中奖人员
|
||||
resetAlreadyPerson() {
|
||||
// 把已中奖人员合并到未中奖人员,要验证是否已存在
|
||||
this.personConfig.allPersonList.forEach((item: IPersonConfig) => {
|
||||
item.isWin = false
|
||||
item.prizeName = []
|
||||
item.prizeTime = []
|
||||
item.prizeId = []
|
||||
})
|
||||
this.personConfig.alreadyPersonList = []
|
||||
},
|
||||
setDefaultPersonList() {
|
||||
this.personConfig.allPersonList = defaultPersonList
|
||||
this.personConfig.alreadyPersonList = []
|
||||
},
|
||||
function resetAlreadyPerson() {
|
||||
// 把已中奖人员合并到未中奖人员,要验证是否已存在
|
||||
personConfig.value.allPersonList.forEach((item: IPersonConfig) => {
|
||||
item.isWin = false
|
||||
item.prizeName = []
|
||||
item.prizeTime = []
|
||||
item.prizeId = []
|
||||
})
|
||||
personConfig.value.alreadyPersonList = []
|
||||
const allPersonListRaw = toRaw(personConfig.value.allPersonList)
|
||||
personDb.deleteAll('allPersonList')
|
||||
personDb.setAllData('allPersonList', allPersonListRaw)
|
||||
personDb.deleteAll('alreadyPersonList')
|
||||
}
|
||||
function setDefaultPersonList() {
|
||||
personConfig.value.allPersonList = defaultPersonList.map((item: any) => {
|
||||
item.uuid = uuidv4()
|
||||
return item
|
||||
})
|
||||
personConfig.value.alreadyPersonList = []
|
||||
personDb.setAllData('allPersonList', defaultPersonList)
|
||||
personDb.deleteAll('alreadyPersonList')
|
||||
}
|
||||
// 重置所有配置
|
||||
reset() {
|
||||
this.personConfig = {
|
||||
allPersonList: [] as IPersonConfig[],
|
||||
alreadyPersonList: [] as IPersonConfig[],
|
||||
}
|
||||
},
|
||||
},
|
||||
persist: {
|
||||
enabled: true,
|
||||
strategies: [
|
||||
{
|
||||
// 如果要存储在localStorage中
|
||||
storage: localStorage,
|
||||
key: 'personConfig',
|
||||
},
|
||||
],
|
||||
},
|
||||
function reset() {
|
||||
personConfig.value = {
|
||||
allPersonList: [] as IPersonConfig[],
|
||||
alreadyPersonList: [] as IPersonConfig[],
|
||||
}
|
||||
personDb.deleteAll('allPersonList')
|
||||
personDb.deleteAll('alreadyPersonList')
|
||||
}
|
||||
return {
|
||||
personConfig,
|
||||
getPersonConfig,
|
||||
getAllPersonList,
|
||||
getNotThisPrizePersonList,
|
||||
getAlreadyPersonList,
|
||||
getAlreadyPersonDetail,
|
||||
getNotPersonList,
|
||||
addNotPersonList,
|
||||
addOnePerson,
|
||||
addAlreadyPersonList,
|
||||
moveAlreadyToNot,
|
||||
deletePerson,
|
||||
deleteAllPerson,
|
||||
resetPerson,
|
||||
resetAlreadyPerson,
|
||||
setDefaultPersonList,
|
||||
reset,
|
||||
}
|
||||
})
|
||||
|
||||
154
src/style.css
154
src/style.css
@@ -1,6 +1,33 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@plugin "daisyui" {
|
||||
themes: all;
|
||||
}
|
||||
|
||||
@utility w-option-xs {
|
||||
max-width: calc(var(--container-xs) - 90px);
|
||||
}
|
||||
|
||||
@utility hide-scrollbar {
|
||||
/* 隐藏 IE/旧 Edge 滚动条 */
|
||||
-ms-overflow-style: none;
|
||||
/* 隐藏 Firefox 滚动条 */
|
||||
scrollbar-width: none;
|
||||
|
||||
/* 隐藏 WebKit 内核(Chrome/Edge/Safari)滚动条 */
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
/* 兜底,确保完全隐藏 */
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* @plugin "@tailwindcss/typography" */
|
||||
|
||||
body,
|
||||
html {
|
||||
@@ -9,6 +36,7 @@ html {
|
||||
overflow-y: overlay;
|
||||
overflow-y: hidden;
|
||||
overflow-x: hidden;
|
||||
font-family: var(--app-font-family);
|
||||
}
|
||||
|
||||
ul {
|
||||
@@ -53,4 +81,122 @@ ul {
|
||||
/* Opera(Old) */
|
||||
box-shadow: inset 1px 1px 1px rgba(0, 0, 0, 0.25);
|
||||
/* IE9+, News */
|
||||
}
|
||||
}
|
||||
|
||||
/* @theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
} */
|
||||
|
||||
/* :root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
*/
|
||||
|
||||
/* @layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
} */
|
||||
@@ -1,118 +1,56 @@
|
||||
@media (prefers-color-scheme: dark) {
|
||||
|
||||
.markdown-body,
|
||||
[data-theme="dark"] {
|
||||
/*dark*/
|
||||
color-scheme: dark;
|
||||
--color-prettylights-syntax-comment: #8b949e;
|
||||
--color-prettylights-syntax-constant: #79c0ff;
|
||||
--color-prettylights-syntax-entity: #d2a8ff;
|
||||
--color-prettylights-syntax-storage-modifier-import: #c9d1d9;
|
||||
--color-prettylights-syntax-entity-tag: #7ee787;
|
||||
--color-prettylights-syntax-keyword: #ff7b72;
|
||||
--color-prettylights-syntax-string: #a5d6ff;
|
||||
--color-prettylights-syntax-variable: #ffa657;
|
||||
--color-prettylights-syntax-brackethighlighter-unmatched: #f85149;
|
||||
--color-prettylights-syntax-invalid-illegal-text: #f0f6fc;
|
||||
--color-prettylights-syntax-invalid-illegal-bg: #8e1519;
|
||||
--color-prettylights-syntax-carriage-return-text: #f0f6fc;
|
||||
--color-prettylights-syntax-carriage-return-bg: #b62324;
|
||||
--color-prettylights-syntax-string-regexp: #7ee787;
|
||||
--color-prettylights-syntax-markup-list: #f2cc60;
|
||||
--color-prettylights-syntax-markup-heading: #1f6feb;
|
||||
--color-prettylights-syntax-markup-italic: #c9d1d9;
|
||||
--color-prettylights-syntax-markup-bold: #c9d1d9;
|
||||
--color-prettylights-syntax-markup-deleted-text: #ffdcd7;
|
||||
--color-prettylights-syntax-markup-deleted-bg: #67060c;
|
||||
--color-prettylights-syntax-markup-inserted-text: #aff5b4;
|
||||
--color-prettylights-syntax-markup-inserted-bg: #033a16;
|
||||
--color-prettylights-syntax-markup-changed-text: #ffdfb6;
|
||||
--color-prettylights-syntax-markup-changed-bg: #5a1e02;
|
||||
--color-prettylights-syntax-markup-ignored-text: #c9d1d9;
|
||||
--color-prettylights-syntax-markup-ignored-bg: #1158c7;
|
||||
--color-prettylights-syntax-meta-diff-range: #d2a8ff;
|
||||
--color-prettylights-syntax-brackethighlighter-angle: #8b949e;
|
||||
--color-prettylights-syntax-sublimelinter-gutter-mark: #484f58;
|
||||
--color-prettylights-syntax-constant-other-reference-link: #a5d6ff;
|
||||
--color-fg-default: #e6edf3;
|
||||
--color-fg-muted: #848d97;
|
||||
--color-fg-subtle: #6e7681;
|
||||
--color-canvas-default: #0d1117;
|
||||
--color-canvas-subtle: #161b22;
|
||||
--color-border-default: #30363d;
|
||||
--color-border-muted: #21262d;
|
||||
--color-neutral-muted: rgba(110, 118, 129, 0.4);
|
||||
--color-accent-fg: #2f81f7;
|
||||
--color-accent-emphasis: #1f6feb;
|
||||
--color-success-fg: #3fb950;
|
||||
--color-success-emphasis: #238636;
|
||||
--color-attention-fg: #d29922;
|
||||
--color-attention-emphasis: #9e6a03;
|
||||
--color-attention-subtle: rgba(187, 128, 9, 0.15);
|
||||
--color-danger-fg: #f85149;
|
||||
--color-danger-emphasis: #da3633;
|
||||
--color-done-fg: #a371f7;
|
||||
--color-done-emphasis: #8957e5;
|
||||
}
|
||||
/* 定义--color-fg-default */
|
||||
:root {
|
||||
--color-prettylights-syntax-comment: #8b949e;
|
||||
--color-prettylights-syntax-constant: #79c0ff;
|
||||
--color-prettylights-syntax-entity: #d2a8ff;
|
||||
--color-prettylights-syntax-storage-modifier-import: #c9d1d9;
|
||||
--color-prettylights-syntax-entity-tag: #7ee787;
|
||||
--color-prettylights-syntax-keyword: #ff7b72;
|
||||
--color-prettylights-syntax-string: #a5d6ff;
|
||||
--color-prettylights-syntax-variable: #ffa657;
|
||||
--color-prettylights-syntax-brackethighlighter-unmatched: #f85149;
|
||||
--color-prettylights-syntax-invalid-illegal-text: #f0f6fc;
|
||||
--color-prettylights-syntax-invalid-illegal-bg: #8e1519;
|
||||
--color-prettylights-syntax-carriage-return-text: #f0f6fc;
|
||||
--color-prettylights-syntax-carriage-return-bg: #b62324;
|
||||
--color-prettylights-syntax-string-regexp: #7ee787;
|
||||
--color-prettylights-syntax-markup-list: #f2cc60;
|
||||
--color-prettylights-syntax-markup-heading: #1f6feb;
|
||||
--color-prettylights-syntax-markup-italic: #c9d1d9;
|
||||
--color-prettylights-syntax-markup-bold: #c9d1d9;
|
||||
--color-prettylights-syntax-markup-deleted-text: #ffdcd7;
|
||||
--color-prettylights-syntax-markup-deleted-bg: #67060c;
|
||||
--color-prettylights-syntax-markup-inserted-text: #aff5b4;
|
||||
--color-prettylights-syntax-markup-inserted-bg: #033a16;
|
||||
--color-prettylights-syntax-markup-changed-text: #ffdfb6;
|
||||
--color-prettylights-syntax-markup-changed-bg: #5a1e02;
|
||||
--color-prettylights-syntax-markup-ignored-text: #c9d1d9;
|
||||
--color-prettylights-syntax-markup-ignored-bg: #1158c7;
|
||||
--color-prettylights-syntax-meta-diff-range: #d2a8ff;
|
||||
--color-prettylights-syntax-brackethighlighter-angle: #8b949e;
|
||||
--color-prettylights-syntax-sublimelinter-gutter-mark: #484f58;
|
||||
--color-prettylights-syntax-constant-other-reference-link: #a5d6ff;
|
||||
--color-fg-default: hsl(var(--bc) / 1);
|
||||
--color-fg-muted: #848d97;
|
||||
--color-fg-subtle: #6e7681;
|
||||
--color-canvas-default: hsl(var(--a1) / 1);
|
||||
--color-canvas-subtle: hsl(var(--a1) / 1);
|
||||
--color-border-default: #30363d;
|
||||
--color-border-muted: #21262d;
|
||||
--color-neutral-muted: rgba(110, 118, 129, 0.4);
|
||||
--color-accent-fg: #2f81f7;
|
||||
--color-accent-emphasis: #1f6feb;
|
||||
--color-success-fg: #3fb950;
|
||||
--color-success-emphasis: #238636;
|
||||
--color-attention-fg: #d29922;
|
||||
--color-attention-emphasis: #9e6a03;
|
||||
--color-attention-subtle: rgba(187, 128, 9, 0.15);
|
||||
--color-danger-fg: #f85149;
|
||||
--color-danger-emphasis: #da3633;
|
||||
--color-done-fg: #a371f7;
|
||||
--color-done-emphasis: #8957e5;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
|
||||
.markdown-body,
|
||||
[data-theme="light"] {
|
||||
/*light*/
|
||||
color-scheme: light;
|
||||
--color-prettylights-syntax-comment: #57606a;
|
||||
--color-prettylights-syntax-constant: #0550ae;
|
||||
--color-prettylights-syntax-entity: #6639ba;
|
||||
--color-prettylights-syntax-storage-modifier-import: #24292f;
|
||||
--color-prettylights-syntax-entity-tag: #116329;
|
||||
--color-prettylights-syntax-keyword: #cf222e;
|
||||
--color-prettylights-syntax-string: #0a3069;
|
||||
--color-prettylights-syntax-variable: #953800;
|
||||
--color-prettylights-syntax-brackethighlighter-unmatched: #82071e;
|
||||
--color-prettylights-syntax-invalid-illegal-text: #f6f8fa;
|
||||
--color-prettylights-syntax-invalid-illegal-bg: #82071e;
|
||||
--color-prettylights-syntax-carriage-return-text: #f6f8fa;
|
||||
--color-prettylights-syntax-carriage-return-bg: #cf222e;
|
||||
--color-prettylights-syntax-string-regexp: #116329;
|
||||
--color-prettylights-syntax-markup-list: #3b2300;
|
||||
--color-prettylights-syntax-markup-heading: #0550ae;
|
||||
--color-prettylights-syntax-markup-italic: #24292f;
|
||||
--color-prettylights-syntax-markup-bold: #24292f;
|
||||
--color-prettylights-syntax-markup-deleted-text: #82071e;
|
||||
--color-prettylights-syntax-markup-deleted-bg: #ffebe9;
|
||||
--color-prettylights-syntax-markup-inserted-text: #116329;
|
||||
--color-prettylights-syntax-markup-inserted-bg: #dafbe1;
|
||||
--color-prettylights-syntax-markup-changed-text: #953800;
|
||||
--color-prettylights-syntax-markup-changed-bg: #ffd8b5;
|
||||
--color-prettylights-syntax-markup-ignored-text: #eaeef2;
|
||||
--color-prettylights-syntax-markup-ignored-bg: #0550ae;
|
||||
--color-prettylights-syntax-meta-diff-range: #8250df;
|
||||
--color-prettylights-syntax-brackethighlighter-angle: #57606a;
|
||||
--color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f;
|
||||
--color-prettylights-syntax-constant-other-reference-link: #0a3069;
|
||||
--color-fg-default: #1F2328;
|
||||
--color-fg-muted: #656d76;
|
||||
--color-fg-subtle: #6e7781;
|
||||
--color-canvas-default: #ffffff;
|
||||
--color-canvas-subtle: #f6f8fa;
|
||||
--color-border-default: #d0d7de;
|
||||
--color-border-muted: hsla(210, 18%, 87%, 1);
|
||||
--color-neutral-muted: rgba(175, 184, 193, 0.2);
|
||||
--color-accent-fg: #0969da;
|
||||
--color-accent-emphasis: #0969da;
|
||||
--color-success-fg: #1a7f37;
|
||||
--color-success-emphasis: #1f883d;
|
||||
--color-attention-fg: #9a6700;
|
||||
--color-attention-emphasis: #9a6700;
|
||||
--color-attention-subtle: #fff8c5;
|
||||
--color-danger-fg: #d1242f;
|
||||
--color-danger-emphasis: #cf222e;
|
||||
--color-done-fg: #8250df;
|
||||
--color-done-emphasis: #8250df;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
-ms-text-size-adjust: 100%;
|
||||
|
||||
0
src/types/fileType.ts
Normal file
0
src/types/fileType.ts
Normal file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user