You've already forked rollingDraw
优化: 重构项目结构并修复字段配置保存问题
- 删除旧的独立子项目(admin、display),统一使用单应用架构 - 将 AdminLayout.vue 重命名为 Admin.vue - 在管理后台添加大屏预览快捷按钮 - 修复字段配置修改后刷新页面丢失的问题 - 新增 updateFields 方法确保字段配置持久化到 IndexedDB - 更新 IFLOW.md 和 README.md 文档 - 清理未使用的文件和测试数据
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -11,7 +11,7 @@ yarn-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
.playwright-mcp
|
.playwright-mcp/
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 110 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 107 KiB |
215
IFLOW.md
215
IFLOW.md
@@ -0,0 +1,215 @@
|
|||||||
|
# Rolling Draw - 抽奖系统
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
|
||||||
|
Rolling Draw 是一个基于 Vue 3 的现代化抽奖系统,提供完整的管理后台和大屏展示功能。系统支持自定义字段、批量导入、多轮次抽奖、中奖记录导出等功能,适用于各类活动抽奖场景。
|
||||||
|
|
||||||
|
**作者:** 上海潘哆呐科技有限公司
|
||||||
|
**技术栈:** Vue 3 + Vite + Vue Router + Pinia + Element Plus + IndexedDB
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
rollingDraw/
|
||||||
|
├── src/
|
||||||
|
│ ├── views/
|
||||||
|
│ │ ├── Admin.vue # 管理后台布局
|
||||||
|
│ │ ├── Display.vue # 大屏展示页面
|
||||||
|
│ │ └── admin/
|
||||||
|
│ │ ├── Participants.vue # 参与者管理
|
||||||
|
│ │ ├── Prizes.vue # 奖品管理
|
||||||
|
│ │ ├── Rounds.vue # 轮次管理
|
||||||
|
│ │ └── Winners.vue # 中奖记录
|
||||||
|
│ ├── router/
|
||||||
|
│ │ └── index.js # 路由配置
|
||||||
|
│ ├── store/
|
||||||
|
│ │ └── index.js # Pinia 状态管理
|
||||||
|
│ ├── styles/
|
||||||
|
│ │ ├── element-plus.css # Element Plus 样式覆盖
|
||||||
|
│ │ ├── global.css # 全局样式
|
||||||
|
│ │ └── variables.css # CSS 变量定义
|
||||||
|
│ ├── App.vue # 主应用组件
|
||||||
|
│ └── main.js # 应用入口
|
||||||
|
├── utils/
|
||||||
|
│ └── indexedDB.js # IndexedDB 工具封装
|
||||||
|
├── index.html # HTML 模板
|
||||||
|
├── package.json # 项目配置
|
||||||
|
├── vite.config.js # Vite 配置
|
||||||
|
└── .gitignore # Git 忽略文件
|
||||||
|
```
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
### 核心框架
|
||||||
|
- **Vue 3** (v3.4.0) - 渐进式 JavaScript 框架
|
||||||
|
- **Vite** (v5.0.0) - 下一代前端构建工具
|
||||||
|
- **Vue Router** (v4.2.5) - 官方路由管理器
|
||||||
|
- **Pinia** (v2.1.7) - Vue 状态管理库
|
||||||
|
|
||||||
|
### UI 组件库
|
||||||
|
- **Element Plus** (v2.5.0) - Vue 3 组件库
|
||||||
|
- **@element-plus/icons-vue** (v2.3.1) - Element Plus 图标库
|
||||||
|
|
||||||
|
### 数据持久化
|
||||||
|
- **IndexedDB** - 浏览器本地数据库,用于存储抽奖数据
|
||||||
|
- **localStorage** - 用于跨标签页通信
|
||||||
|
|
||||||
|
## 主要功能
|
||||||
|
|
||||||
|
### 1. 参与者管理
|
||||||
|
- ✅ 单个添加参与者
|
||||||
|
- ✅ 批量导入(CSV 格式)
|
||||||
|
- ✅ 导出参与者名单
|
||||||
|
- ✅ 自定义字段配置
|
||||||
|
- ✅ 删除参与者
|
||||||
|
- ✅ 清空名单
|
||||||
|
|
||||||
|
### 2. 奖品管理
|
||||||
|
- ✅ 添加奖品
|
||||||
|
- ✅ 设置库存数量
|
||||||
|
- ✅ 实时库存管理
|
||||||
|
- ✅ 删除奖品
|
||||||
|
|
||||||
|
### 3. 轮次管理
|
||||||
|
- ✅ 创建抽奖轮次
|
||||||
|
- ✅ 关联奖品
|
||||||
|
- ✅ 设置抽取人数
|
||||||
|
- ✅ 轮次状态跟踪
|
||||||
|
|
||||||
|
### 4. 抽奖控制
|
||||||
|
- ✅ 开始/停止抽奖
|
||||||
|
- ✅ Fisher-Yates 洗牌算法
|
||||||
|
- ✅ 防重复中奖
|
||||||
|
- ✅ 库存自动扣减
|
||||||
|
- ✅ 重置抽奖数据
|
||||||
|
|
||||||
|
### 5. 中奖记录
|
||||||
|
- ✅ 实时记录中奖信息
|
||||||
|
- ✅ 导出中奖名单
|
||||||
|
- ✅ 按轮次筛选
|
||||||
|
|
||||||
|
### 6. 大屏展示
|
||||||
|
- ✅ 滚动动画效果
|
||||||
|
- ✅ 结果展示模式
|
||||||
|
- ✅ 自定义背景图片
|
||||||
|
- ✅ 快捷键控制
|
||||||
|
- ✅ 多列布局支持
|
||||||
|
|
||||||
|
### 7. 数据同步
|
||||||
|
- ✅ IndexedDB 本地存储
|
||||||
|
- ✅ 跨标签页实时同步
|
||||||
|
- ✅ 数据持久化
|
||||||
|
|
||||||
|
## 路由配置
|
||||||
|
|
||||||
|
| 路径 | 组件 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `/` | 重定向到 `/admin/participants` | 默认路由 |
|
||||||
|
| `/admin/participants` | Participants.vue | 参与者管理 |
|
||||||
|
| `/admin/prizes` | Prizes.vue | 奖品管理 |
|
||||||
|
| `/admin/rounds` | Rounds.vue | 轮次管理 |
|
||||||
|
| `/admin/winners` | Winners.vue | 中奖记录 |
|
||||||
|
| `/display` | Display.vue | 大屏展示 |
|
||||||
|
|
||||||
|
## 数据存储
|
||||||
|
|
||||||
|
### IndexedDB 数据结构
|
||||||
|
- **lottery_fields** - 字段配置
|
||||||
|
- **lottery_participants** - 参与者列表
|
||||||
|
- **lottery_prizes** - 奖品列表
|
||||||
|
- **lottery_rounds** - 轮次列表
|
||||||
|
- **lottery_winners** - 中奖记录
|
||||||
|
- **lottery_isRolling** - 抽奖状态
|
||||||
|
- **lottery_currentRound** - 当前轮次
|
||||||
|
- **lottery_displayMode** - 显示模式
|
||||||
|
- **lottery_backgroundImage** - 背景图片
|
||||||
|
- **lottery_columnsPerRow** - 每行列数
|
||||||
|
|
||||||
|
## 快捷键
|
||||||
|
|
||||||
|
### 大屏展示端
|
||||||
|
- **← →** - 切换轮次
|
||||||
|
- **Space** - 开始/停止抽奖
|
||||||
|
|
||||||
|
## 开发指南
|
||||||
|
|
||||||
|
### 安装依赖
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 启动开发服务器
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 构建生产版本
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 预览生产构建
|
||||||
|
```bash
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
## 项目特性
|
||||||
|
|
||||||
|
### 响应式设计
|
||||||
|
- 支持桌面端和大屏展示
|
||||||
|
- 自适应布局
|
||||||
|
- 流畅的动画效果
|
||||||
|
|
||||||
|
### 数据安全
|
||||||
|
- 本地存储,无需服务器
|
||||||
|
- 数据持久化
|
||||||
|
- 防止重复中奖
|
||||||
|
|
||||||
|
### 扩展性
|
||||||
|
- 自定义字段配置
|
||||||
|
- 可配置的轮次和奖品
|
||||||
|
- 灵活的展示布局
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **数据备份**:建议定期导出参与者名单和中奖记录进行备份
|
||||||
|
2. **浏览器兼容性**:需要支持 IndexedDB 的现代浏览器
|
||||||
|
3. **图片限制**:背景图片最大支持 10MB
|
||||||
|
4. **CSV 格式**:导入文件第一行为字段名,后续行为数据
|
||||||
|
|
||||||
|
## 技术亮点
|
||||||
|
|
||||||
|
- **Vue 3 Composition API** - 更好的代码组织和复用
|
||||||
|
- **Pinia 状态管理** - 轻量级、类型友好的状态管理
|
||||||
|
- **IndexedDB** - 大容量本地数据存储
|
||||||
|
- **Fisher-Yates 洗牌算法** - 公平的随机抽取
|
||||||
|
- **跨标签页同步** - 实时数据更新
|
||||||
|
- **CSS 变量** - 主题定制和样式管理
|
||||||
|
|
||||||
|
## 开发规范
|
||||||
|
|
||||||
|
### 代码风格
|
||||||
|
- 使用 Vue 3 Composition API
|
||||||
|
- 使用 `<script setup>` 语法糖
|
||||||
|
- 遵循 ESLint 规则
|
||||||
|
- 组件命名使用 PascalCase
|
||||||
|
|
||||||
|
### 命名规范
|
||||||
|
- 文件名:PascalCase (组件) / kebab-case (工具)
|
||||||
|
- 变量名:camelCase
|
||||||
|
- 常量名:UPPER_SNAKE_CASE
|
||||||
|
- 函数名:camelCase
|
||||||
|
|
||||||
|
### 目录结构
|
||||||
|
- 组件放在 `src/views/` 目录
|
||||||
|
- 工具函数放在 `utils/` 目录
|
||||||
|
- 样式文件放在 `src/styles/` 目录
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
版权所有 © 上海潘哆呐科技有限公司
|
||||||
|
|
||||||
|
## 联系方式
|
||||||
|
|
||||||
|
- **邮箱**: work@pandorastudio.cn
|
||||||
|
- **公司**: 上海潘哆呐科技有限公司
|
||||||
388
README.md
388
README.md
@@ -1,71 +1,128 @@
|
|||||||
# 抽奖系统
|
# Rolling Draw 抽奖系统
|
||||||
|
|
||||||
基于 Vue 3 + Vite + Element Plus 开发的现代化抽奖系统,支持名单管理、奖品配置、多轮抽奖等功能。
|
<div align="center">
|
||||||
|
|
||||||
## 项目简介
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
这是一个功能完整的抽奖系统,适用于年会、活动抽奖等场景。系统采用前后端分离架构,数据存储在浏览器本地 IndexedDB 中,无需后端服务器即可运行。
|
一个功能强大、界面美观的现代化抽奖系统
|
||||||
|
|
||||||
### 主要特性
|
[在线演示](#) · [功能特性](#功能特性) · [快速开始](#快速开始) · [使用文档](#使用文档)
|
||||||
|
|
||||||
- 📋 **名单管理**:支持CSV批量导入/导出,支持自定义字段配置
|
</div>
|
||||||
- 🎁 **奖品管理**:灵活配置奖品和库存数量
|
|
||||||
- 🔄 **多轮抽奖**:支持多轮次抽奖,每轮可设置不同奖品和人数
|
|
||||||
- 📺 **大屏展示**:独立的展示端,支持滚动动画效果
|
|
||||||
- ⌨️ **快捷键操作**:支持键盘快捷键控制抽奖流程
|
|
||||||
- 🎨 **背景自定义**:支持自定义大屏背景图片
|
|
||||||
- 💾 **本地存储**:使用 IndexedDB 本地存储,无需服务器
|
|
||||||
|
|
||||||
## 技术栈
|
---
|
||||||
|
|
||||||
- **框架**:Vue 3.4.0
|
## 📖 项目简介
|
||||||
- **构建工具**:Vite 5.0.0
|
|
||||||
- **路由**:Vue Router 4.2.5
|
|
||||||
- **状态管理**:Pinia 2.1.7
|
|
||||||
- **UI组件库**:Element Plus 2.5.0
|
|
||||||
- **数据存储**:IndexedDB
|
|
||||||
|
|
||||||
## 项目结构
|
Rolling Draw 是一个基于 Vue 3 + Vite 开发的现代化抽奖系统,提供完整的管理后台和大屏展示功能。系统支持自定义字段、批量导入、多轮次抽奖、中奖记录导出等功能,适用于年会抽奖、活动抽奖等各类场景。
|
||||||
|
|
||||||
```
|
### ✨ 核心特点
|
||||||
rollingDraw/
|
|
||||||
├── src/ # 主应用源码
|
|
||||||
│ ├── views/ # 页面组件
|
|
||||||
│ │ ├── admin/ # 管理端页面
|
|
||||||
│ │ │ ├── Participants.vue # 名单管理
|
|
||||||
│ │ │ ├── Prizes.vue # 奖品管理
|
|
||||||
│ │ │ ├── Rounds.vue # 轮次管理
|
|
||||||
│ │ │ └── Winners.vue # 中奖记录
|
|
||||||
│ │ ├── Admin.vue # 管理端入口
|
|
||||||
│ │ ├── Display.vue # 展示端入口
|
|
||||||
│ │ └── AdminLayout.vue # 管理端布局
|
|
||||||
│ ├── router/ # 路由配置
|
|
||||||
│ ├── store/ # 状态管理
|
|
||||||
│ └── styles/ # 样式文件
|
|
||||||
├── admin/ # 独立管理端应用
|
|
||||||
├── display/ # 独立展示端应用
|
|
||||||
├── utils/ # 工具类
|
|
||||||
│ └── indexedDB.js # IndexedDB封装
|
|
||||||
├── index.html # 入口HTML
|
|
||||||
├── vite.config.js # Vite配置
|
|
||||||
└── package.json # 项目配置
|
|
||||||
```
|
|
||||||
|
|
||||||
## 快速开始
|
- 🎨 **现代化界面** - 基于 Element Plus 的精美 UI 设计
|
||||||
|
- 📱 **响应式设计** - 完美适配桌面端和大屏展示
|
||||||
|
- 💾 **本地存储** - 基于 IndexedDB 的数据持久化
|
||||||
|
- 🔄 **实时同步** - 支持跨标签页数据同步
|
||||||
|
- 🎯 **公平抽奖** - 采用 Fisher-Yates 洗牌算法
|
||||||
|
- 📊 **数据管理** - 支持导入导出,方便数据管理
|
||||||
|
- ⌨️ **快捷操作** - 大屏端支持快捷键控制
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 功能特性
|
||||||
|
|
||||||
|
### 参与者管理
|
||||||
|
- ✅ 单个添加参与者
|
||||||
|
- ✅ 批量导入(支持 CSV 格式)
|
||||||
|
- ✅ 导出参与者名单
|
||||||
|
- ✅ 自定义字段配置
|
||||||
|
- ✅ 删除参与者
|
||||||
|
- ✅ 清空名单
|
||||||
|
|
||||||
|
### 奖品管理
|
||||||
|
- ✅ 添加奖品
|
||||||
|
- ✅ 设置库存数量
|
||||||
|
- ✅ 实时库存管理
|
||||||
|
- ✅ 删除奖品
|
||||||
|
|
||||||
|
### 轮次管理
|
||||||
|
- ✅ 创建抽奖轮次
|
||||||
|
- ✅ 关联奖品
|
||||||
|
- ✅ 设置抽取人数
|
||||||
|
- ✅ 轮次状态跟踪
|
||||||
|
|
||||||
|
### 抽奖控制
|
||||||
|
- ✅ 开始/停止抽奖
|
||||||
|
- ✅ Fisher-Yates 洗牌算法
|
||||||
|
- ✅ 防重复中奖
|
||||||
|
- ✅ 库存自动扣减
|
||||||
|
- ✅ 重置抽奖数据
|
||||||
|
|
||||||
|
### 中奖记录
|
||||||
|
- ✅ 实时记录中奖信息
|
||||||
|
- ✅ 导出中奖名单
|
||||||
|
- ✅ 按轮次筛选
|
||||||
|
|
||||||
|
### 大屏展示
|
||||||
|
- ✅ 滚动动画效果
|
||||||
|
- ✅ 结果展示模式
|
||||||
|
- ✅ 自定义背景图片
|
||||||
|
- ✅ 快捷键控制(← → 切换轮次,Space 开始/停止)
|
||||||
|
- ✅ 多列布局支持(可配置每行列数)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ 技术栈
|
||||||
|
|
||||||
|
### 核心框架
|
||||||
|
- **Vue 3** (v3.4.0) - 渐进式 JavaScript 框架
|
||||||
|
- **Vite** (v5.0.0) - 下一代前端构建工具
|
||||||
|
- **Vue Router** (v4.2.5) - 官方路由管理器
|
||||||
|
- **Pinia** (v2.1.7) - Vue 状态管理库
|
||||||
|
|
||||||
|
### UI 组件库
|
||||||
|
- **Element Plus** (v2.5.0) - Vue 3 组件库
|
||||||
|
- **@element-plus/icons-vue** (v2.3.1) - Element Plus 图标库
|
||||||
|
|
||||||
|
### 数据持久化
|
||||||
|
- **IndexedDB** - 浏览器本地数据库
|
||||||
|
- **localStorage** - 跨标签页通信
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 安装
|
||||||
|
|
||||||
|
### 环境要求
|
||||||
|
|
||||||
|
- Node.js >= 16.0.0
|
||||||
|
- npm >= 8.0.0
|
||||||
|
|
||||||
### 安装依赖
|
### 安装依赖
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# 克隆项目
|
||||||
|
git clone https://git.pandorastudio.cn/product/rollingDraw.git
|
||||||
|
|
||||||
|
# 进入项目目录
|
||||||
|
cd rollingDraw
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎬 快速开始
|
||||||
|
|
||||||
### 开发模式
|
### 开发模式
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
访问 http://localhost:5173
|
访问 [http://localhost:5173](http://localhost:5173) 查看应用
|
||||||
|
|
||||||
### 构建生产版本
|
### 构建生产版本
|
||||||
|
|
||||||
@@ -73,91 +130,212 @@ npm run dev
|
|||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
### 预览生产版本
|
构建产物将生成在 `dist` 目录
|
||||||
|
|
||||||
|
### 预览生产构建
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run preview
|
npm run preview
|
||||||
```
|
```
|
||||||
|
|
||||||
## 功能说明
|
---
|
||||||
|
|
||||||
### 管理端(/admin)
|
## 📂 项目结构
|
||||||
|
|
||||||
#### 名单管理
|
```
|
||||||
- **单个添加**:通过表单逐个添加参与者
|
rollingDraw/
|
||||||
- **批量导入**:支持 CSV 文件批量导入
|
├── src/
|
||||||
- CSV 格式:第一行为字段名,后续行为数据
|
│ ├── views/
|
||||||
- 示例:
|
│ │ ├── Admin.vue # 管理后台布局
|
||||||
```csv
|
│ │ ├── Display.vue # 大屏展示页面
|
||||||
姓名,部门,工号
|
│ │ └── admin/
|
||||||
张三,技术部,001
|
│ │ ├── Participants.vue # 参与者管理
|
||||||
李四,市场部,002
|
│ │ ├── Prizes.vue # 奖品管理
|
||||||
```
|
│ │ ├── Rounds.vue # 轮次管理
|
||||||
- **字段配置**:自定义参与者字段(姓名、部门、工号等)
|
│ │ └── Winners.vue # 中奖记录
|
||||||
- **导出名单**:将当前名单导出为 CSV 文件
|
│ ├── router/
|
||||||
- **清空名单**:一键清空所有参与者
|
│ │ └── index.js # 路由配置
|
||||||
|
│ ├── store/
|
||||||
|
│ │ └── index.js # Pinia 状态管理
|
||||||
|
│ ├── styles/
|
||||||
|
│ │ ├── element-plus.css # Element Plus 样式覆盖
|
||||||
|
│ │ ├── global.css # 全局样式
|
||||||
|
│ │ └── variables.css # CSS 变量定义
|
||||||
|
│ ├── App.vue # 主应用组件
|
||||||
|
│ └── main.js # 应用入口
|
||||||
|
├── utils/
|
||||||
|
│ └── indexedDB.js # IndexedDB 工具封装
|
||||||
|
├── index.html # HTML 模板
|
||||||
|
├── package.json # 项目配置
|
||||||
|
├── vite.config.js # Vite 配置
|
||||||
|
└── README.md # 项目文档
|
||||||
|
```
|
||||||
|
|
||||||
#### 奖品管理
|
---
|
||||||
- 添加奖品(名称、库存数量)
|
|
||||||
- 查看奖品使用情况(已用/总库存)
|
|
||||||
- 删除奖品
|
|
||||||
|
|
||||||
#### 轮次管理
|
## 📖 使用文档
|
||||||
- 创建抽奖轮次
|
|
||||||
- 为每个轮次配置奖品和抽取人数
|
|
||||||
- 查看轮次完成状态
|
|
||||||
- 删除轮次
|
|
||||||
|
|
||||||
#### 中奖记录
|
### 路由说明
|
||||||
- 查看所有中奖记录
|
|
||||||
- 导出中奖名单
|
|
||||||
- 清空中奖记录(重置抽奖)
|
|
||||||
|
|
||||||
#### 背景设置
|
| 路径 | 组件 | 说明 |
|
||||||
- 上传自定义背景图片(支持 JPG、PNG、GIF)
|
|------|------|------|
|
||||||
- 预览背景效果
|
| `/` | 重定向 | 默认重定向到参与者管理 |
|
||||||
- 清除背景图片
|
| `/admin/participants` | Participants | 参与者管理页面 |
|
||||||
|
| `/admin/prizes` | Prizes | 奖品管理页面 |
|
||||||
|
| `/admin/rounds` | Rounds | 轮次管理页面 |
|
||||||
|
| `/admin/winners` | Winners | 中奖记录页面 |
|
||||||
|
| `/display` | Display | 大屏展示页面 |
|
||||||
|
|
||||||
### 展示端(/display)
|
### 快捷键
|
||||||
|
|
||||||
#### 显示模式
|
#### 大屏展示端
|
||||||
- **滚动模式**:抽奖时名单滚动显示
|
- **← →** - 切换轮次
|
||||||
- **结果模式**:展示中奖者名单,支持多列布局
|
- **Space** - 开始/停止抽奖
|
||||||
|
|
||||||
#### 快捷键
|
### CSV 导入格式
|
||||||
- `←` `→`:切换抽奖轮次
|
|
||||||
- `Space`:开始/停止抽奖
|
|
||||||
|
|
||||||
## 数据存储
|
```
|
||||||
|
姓名,部门,工号
|
||||||
|
张三,技术部,1001
|
||||||
|
李四,市场部,1002
|
||||||
|
王五,财务部,1003
|
||||||
|
```
|
||||||
|
|
||||||
系统使用浏览器本地 IndexedDB 存储数据,数据库名称为 `LotteryDB`,包含以下数据:
|
**注意:**
|
||||||
|
- 第一行为字段名
|
||||||
|
- 后续行为数据
|
||||||
|
- 支持中文字段名
|
||||||
|
- 使用逗号分隔
|
||||||
|
|
||||||
- `participants`:参与者名单
|
### 数据存储
|
||||||
- `prizes`:奖品列表
|
|
||||||
- `rounds`:抽奖轮次
|
|
||||||
- `winners`:中奖记录
|
|
||||||
- `fields`:字段配置
|
|
||||||
- `backgroundImage`:背景图片
|
|
||||||
|
|
||||||
**注意**:清除浏览器数据会导致所有抽奖数据丢失,请定期导出备份。
|
系统使用 IndexedDB 存储所有数据,包括:
|
||||||
|
|
||||||
## 开发说明
|
- **lottery_fields** - 字段配置
|
||||||
|
- **lottery_participants** - 参与者列表
|
||||||
|
- **lottery_prizes** - 奖品列表
|
||||||
|
- **lottery_rounds** - 轮次列表
|
||||||
|
- **lottery_winners** - 中奖记录
|
||||||
|
- **lottery_isRolling** - 抽奖状态
|
||||||
|
- **lottery_currentRound** - 当前轮次
|
||||||
|
- **lottery_displayMode** - 显示模式
|
||||||
|
- **lottery_backgroundImage** - 背景图片
|
||||||
|
- **lottery_columnsPerRow** - 每行列数
|
||||||
|
|
||||||
### 路径别名
|
---
|
||||||
|
|
||||||
- `@`:指向 `src/` 目录
|
## 🔧 开发指南
|
||||||
- `@utils`:指向 `utils/` 目录
|
|
||||||
|
|
||||||
### 样式变量
|
### 代码规范
|
||||||
|
|
||||||
项目使用 CSS 变量定义主题颜色和样式,可在 `src/styles/variables.css` 中修改。
|
- 使用 Vue 3 Composition API
|
||||||
|
- 使用 `<script setup>` 语法糖
|
||||||
|
- 遵循 ESLint 规则
|
||||||
|
- 组件命名使用 PascalCase
|
||||||
|
|
||||||
## 作者
|
### 命名规范
|
||||||
|
|
||||||
上海潘哆呐科技有限公司
|
- 文件名:PascalCase (组件) / kebab-case (工具)
|
||||||
|
- 变量名:camelCase
|
||||||
|
- 常量名:UPPER_SNAKE_CASE
|
||||||
|
- 函数名:camelCase
|
||||||
|
|
||||||
邮箱:work@pandorastudio.cn
|
### 添加新功能
|
||||||
|
|
||||||
## 许可证
|
1. 在 `src/views/` 创建新组件
|
||||||
|
2. 在 `src/router/index.js` 添加路由
|
||||||
|
3. 在 `src/store/index.js` 添加状态管理
|
||||||
|
4. 在相应页面添加入口
|
||||||
|
|
||||||
私有项目
|
---
|
||||||
|
|
||||||
|
## 🎨 自定义样式
|
||||||
|
|
||||||
|
### CSS 变量
|
||||||
|
|
||||||
|
在 `src/styles/variables.css` 中定义了全局 CSS 变量:
|
||||||
|
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
/* 颜色 */
|
||||||
|
--color-primary: #409EFF;
|
||||||
|
--color-secondary: #67C23A;
|
||||||
|
--color-accent: #E6A23C;
|
||||||
|
|
||||||
|
/* 间距 */
|
||||||
|
--spacing-sm: 8px;
|
||||||
|
--spacing-md: 12px;
|
||||||
|
--spacing-lg: 16px;
|
||||||
|
|
||||||
|
/* 字体 */
|
||||||
|
--font-family-primary: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 主题定制
|
||||||
|
|
||||||
|
修改 `src/styles/variables.css` 中的 CSS 变量即可自定义主题。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 注意事项
|
||||||
|
|
||||||
|
1. **数据备份**:建议定期导出参与者名单和中奖记录进行备份
|
||||||
|
2. **浏览器兼容性**:需要支持 IndexedDB 的现代浏览器(Chrome、Firefox、Edge、Safari)
|
||||||
|
3. **图片限制**:背景图片最大支持 10MB
|
||||||
|
4. **CSV 格式**:导入文件第一行为字段名,后续行为数据
|
||||||
|
5. **数据清理**:使用浏览器清除缓存会删除所有数据
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 常见问题
|
||||||
|
|
||||||
|
### Q: 数据会丢失吗?
|
||||||
|
A: 数据存储在浏览器的 IndexedDB 中,清除浏览器缓存会丢失数据,建议定期导出备份。
|
||||||
|
|
||||||
|
### Q: 支持哪些浏览器?
|
||||||
|
A: 支持所有现代浏览器(Chrome、Firefox、Edge、Safari)的最新版本。
|
||||||
|
|
||||||
|
### Q: 可以同时打开多个标签页吗?
|
||||||
|
A: 可以,系统支持跨标签页数据同步。
|
||||||
|
|
||||||
|
### Q: 如何清空所有数据?
|
||||||
|
A: 在管理后台的"中奖记录"页面点击"清空"按钮。
|
||||||
|
|
||||||
|
### Q: 抽奖算法公平吗?
|
||||||
|
A: 采用 Fisher-Yates 洗牌算法,确保每个参与者被抽中的概率相同。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 许可证
|
||||||
|
|
||||||
|
版权所有 © 2025 上海潘哆呐科技有限公司
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 联系方式
|
||||||
|
|
||||||
|
- **公司**: 上海潘哆呐科技有限公司
|
||||||
|
- **邮箱**: work@pandorastudio.cn
|
||||||
|
- **项目地址**: https://git.pandorastudio.cn/product/rollingDraw.git
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🙏 致谢
|
||||||
|
|
||||||
|
感谢以下开源项目:
|
||||||
|
|
||||||
|
- [Vue.js](https://vuejs.org/)
|
||||||
|
- [Vite](https://vitejs.dev/)
|
||||||
|
- [Element Plus](https://element-plus.org/)
|
||||||
|
- [Pinia](https://pinia.vuejs.org/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
**如果这个项目对你有帮助,请给一个 ⭐️**
|
||||||
|
|
||||||
|
Made with ❤️ by Pandora Studio
|
||||||
|
|
||||||
|
</div>
|
||||||
24
admin/.gitignore
vendored
24
admin/.gitignore
vendored
@@ -1,24 +0,0 @@
|
|||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
node_modules
|
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
|
||||||
.DS_Store
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>admin</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script type="module" src="/src/main.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
1740
admin/package-lock.json
generated
1740
admin/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "admin",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "tsc && vite build",
|
|
||||||
"preview": "vite preview"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@vitejs/plugin-vue": "^6.0.3",
|
|
||||||
"typescript": "~5.9.3",
|
|
||||||
"vite": "^7.2.4"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@element-plus/icons-vue": "^2.3.2",
|
|
||||||
"element-plus": "^2.13.1",
|
|
||||||
"pinia": "^3.0.4"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
1286
admin/src/App.vue
1286
admin/src/App.vue
File diff suppressed because it is too large
Load Diff
@@ -1,9 +0,0 @@
|
|||||||
export function setupCounter(element: HTMLButtonElement) {
|
|
||||||
let counter = 0
|
|
||||||
const setCounter = (count: number) => {
|
|
||||||
counter = count
|
|
||||||
element.innerHTML = `count is ${counter}`
|
|
||||||
}
|
|
||||||
element.addEventListener('click', () => setCounter(counter + 1))
|
|
||||||
setCounter(0)
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { createApp } from 'vue'
|
|
||||||
import { createPinia } from 'pinia'
|
|
||||||
import ElementPlus from 'element-plus'
|
|
||||||
import 'element-plus/dist/index.css'
|
|
||||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
|
||||||
import App from './App.vue'
|
|
||||||
|
|
||||||
// 引入全局样式
|
|
||||||
import './styles/variables.css'
|
|
||||||
import './styles/global.css'
|
|
||||||
import './styles/components.css'
|
|
||||||
import './styles/element-plus.css'
|
|
||||||
|
|
||||||
const app = createApp(App)
|
|
||||||
app.use(createPinia())
|
|
||||||
app.use(ElementPlus)
|
|
||||||
|
|
||||||
// 注册所有图标
|
|
||||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
|
||||||
app.component(key, component)
|
|
||||||
}
|
|
||||||
|
|
||||||
app.mount('#app')
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import './style.css'
|
|
||||||
import typescriptLogo from './typescript.svg'
|
|
||||||
import viteLogo from '/vite.svg'
|
|
||||||
import { setupCounter } from './counter.ts'
|
|
||||||
|
|
||||||
document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
|
|
||||||
<div>
|
|
||||||
<a href="https://vite.dev" target="_blank">
|
|
||||||
<img src="${viteLogo}" class="logo" alt="Vite logo" />
|
|
||||||
</a>
|
|
||||||
<a href="https://www.typescriptlang.org/" target="_blank">
|
|
||||||
<img src="${typescriptLogo}" class="logo vanilla" alt="TypeScript logo" />
|
|
||||||
</a>
|
|
||||||
<h1>Vite + TypeScript</h1>
|
|
||||||
<div class="card">
|
|
||||||
<button id="counter" type="button"></button>
|
|
||||||
</div>
|
|
||||||
<p class="read-the-docs">
|
|
||||||
Click on the Vite and TypeScript logos to learn more
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
|
|
||||||
setupCounter(document.querySelector<HTMLButtonElement>('#counter')!)
|
|
||||||
@@ -1,491 +0,0 @@
|
|||||||
import { defineStore } from 'pinia'
|
|
||||||
import { ref, watch } from 'vue'
|
|
||||||
import { indexedDB } from '@utils/indexedDB'
|
|
||||||
|
|
||||||
export const useLotteryStore = defineStore('lottery', () => {
|
|
||||||
// 状态
|
|
||||||
const fields = ref([])
|
|
||||||
const participants = ref([])
|
|
||||||
const prizes = ref([])
|
|
||||||
const rounds = ref([])
|
|
||||||
const winners = ref([])
|
|
||||||
const isRolling = ref(false)
|
|
||||||
const currentRound = ref(null)
|
|
||||||
const displayMode = ref('scroll')
|
|
||||||
const backgroundImage = ref('')
|
|
||||||
const isInitialized = ref(false)
|
|
||||||
|
|
||||||
// 初始化 - 从IndexedDB加载数据
|
|
||||||
const initialize = async () => {
|
|
||||||
if (isInitialized.value) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = await indexedDB.getAll()
|
|
||||||
|
|
||||||
fields.value = data.lottery_fields || []
|
|
||||||
participants.value = data.lottery_participants || []
|
|
||||||
prizes.value = data.lottery_prizes || []
|
|
||||||
rounds.value = data.lottery_rounds || []
|
|
||||||
winners.value = data.lottery_winners || []
|
|
||||||
isRolling.value = data.lottery_isRolling === 'true'
|
|
||||||
currentRound.value = data.lottery_currentRound || null
|
|
||||||
displayMode.value = data.lottery_displayMode || 'scroll'
|
|
||||||
backgroundImage.value = data.lottery_backgroundImage || ''
|
|
||||||
|
|
||||||
isInitialized.value = true
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to initialize from IndexedDB:', error)
|
|
||||||
// 使用默认值
|
|
||||||
fields.value = [{key: 'name', label: '姓名', required: true}]
|
|
||||||
isInitialized.value = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存单个数据到IndexedDB
|
|
||||||
const saveData = async (key, value) => {
|
|
||||||
try {
|
|
||||||
console.log(`Saving ${key} to IndexedDB...`)
|
|
||||||
// 深拷贝数据以避免Vue响应式包装器
|
|
||||||
const clonedValue = JSON.parse(JSON.stringify(value))
|
|
||||||
await indexedDB.set(key, clonedValue)
|
|
||||||
|
|
||||||
// 触发localStorage事件通知其他标签页
|
|
||||||
localStorage.setItem('lottery_data_changed', Date.now().toString())
|
|
||||||
|
|
||||||
console.log(`Successfully saved ${key}`)
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to save ${key} to IndexedDB:`, error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ 字段管理 ============
|
|
||||||
const addField = async (field) => {
|
|
||||||
const newField = {
|
|
||||||
id: Date.now(),
|
|
||||||
key: field.key,
|
|
||||||
label: field.label,
|
|
||||||
required: field.required || false
|
|
||||||
}
|
|
||||||
fields.value.push(newField)
|
|
||||||
await saveData('lottery_fields', fields.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateField = async (field) => {
|
|
||||||
const index = fields.value.findIndex(f => f.id === field.id)
|
|
||||||
if (index > -1) {
|
|
||||||
fields.value[index] = field
|
|
||||||
await saveData('lottery_fields', fields.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeField = async (id) => {
|
|
||||||
const index = fields.value.findIndex(f => f.id === id)
|
|
||||||
if (index > -1) {
|
|
||||||
fields.value.splice(index, 1)
|
|
||||||
await saveData('lottery_fields', fields.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ 参与者管理 ============
|
|
||||||
const addParticipant = async (data) => {
|
|
||||||
const participant = { id: Date.now(), ...data }
|
|
||||||
participants.value.push(participant)
|
|
||||||
await saveData('lottery_participants', participants.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateParticipant = async (id, data) => {
|
|
||||||
const index = participants.value.findIndex(p => p.id === id)
|
|
||||||
if (index > -1) {
|
|
||||||
participants.value[index] = { ...participants.value[index], ...data }
|
|
||||||
await saveData('lottery_participants', participants.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeParticipant = async (id) => {
|
|
||||||
const index = participants.value.findIndex(p => p.id === id)
|
|
||||||
if (index > -1) {
|
|
||||||
participants.value.splice(index, 1)
|
|
||||||
await saveData('lottery_participants', participants.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearParticipants = async () => {
|
|
||||||
participants.value = []
|
|
||||||
await saveData('lottery_participants', participants.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const importParticipantsFromFile = (file) => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.onload = async (e) => {
|
|
||||||
try {
|
|
||||||
const content = e.target.result
|
|
||||||
const lines = content.split('\n').map(l => l.trim()).filter(l => l)
|
|
||||||
|
|
||||||
// 检测是否是CSV格式(包含逗号)
|
|
||||||
const isCSV = lines.some(line => line.includes(','))
|
|
||||||
|
|
||||||
if (isCSV) {
|
|
||||||
// CSV格式导入
|
|
||||||
const headers = lines[0].split(',').map(h => h.trim())
|
|
||||||
const data = []
|
|
||||||
|
|
||||||
// 自动添加缺失的字段到配置
|
|
||||||
headers.forEach(header => {
|
|
||||||
if (!fields.value.some(f => f.key === header)) {
|
|
||||||
fields.value.push({
|
|
||||||
id: Date.now() + Math.random(),
|
|
||||||
key: header,
|
|
||||||
label: header,
|
|
||||||
required: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 保存更新后的字段配置
|
|
||||||
await saveData('lottery_fields', fields.value)
|
|
||||||
|
|
||||||
for (let i = 1; i < lines.length; i++) {
|
|
||||||
const values = lines[i].split(',').map(v => v.trim())
|
|
||||||
const participant = { id: Date.now() + i }
|
|
||||||
headers.forEach((header, index) => {
|
|
||||||
participant[header] = values[index] || ''
|
|
||||||
})
|
|
||||||
data.push(participant)
|
|
||||||
}
|
|
||||||
|
|
||||||
participants.value.push(...data)
|
|
||||||
await saveData('lottery_participants', participants.value)
|
|
||||||
resolve(data.length)
|
|
||||||
} else {
|
|
||||||
// 简单文本格式导入(每行一个姓名)
|
|
||||||
const data = lines.map(line => ({
|
|
||||||
id: Date.now() + Math.random(),
|
|
||||||
name: line
|
|
||||||
}))
|
|
||||||
participants.value.push(...data)
|
|
||||||
await saveData('lottery_participants', participants.value)
|
|
||||||
resolve(data.length)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
reject(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reader.onerror = () => reject(new Error('文件读取失败'))
|
|
||||||
reader.readAsText(file)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ 奖品管理 ============
|
|
||||||
const addPrize = async (prize) => {
|
|
||||||
prizes.value.push({
|
|
||||||
id: Date.now(),
|
|
||||||
name: prize.name,
|
|
||||||
stock: parseInt(prize.stock) || 0,
|
|
||||||
used: 0
|
|
||||||
})
|
|
||||||
await saveData('lottery_prizes', prizes.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatePrize = async (prize) => {
|
|
||||||
const index = prizes.value.findIndex(p => p.id === prize.id)
|
|
||||||
if (index > -1) {
|
|
||||||
prizes.value[index] = prize
|
|
||||||
await saveData('lottery_prizes', prizes.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const removePrize = async (id) => {
|
|
||||||
const index = prizes.value.findIndex(p => p.id === id)
|
|
||||||
if (index > -1) {
|
|
||||||
prizes.value.splice(index, 1)
|
|
||||||
await saveData('lottery_prizes', prizes.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPrizeAvailable = (prizeId) => {
|
|
||||||
const prize = prizes.value.find(p => p.id === prizeId)
|
|
||||||
return prize ? prize.stock - prize.used : 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ 轮次管理 ============
|
|
||||||
const addRound = async (round) => {
|
|
||||||
rounds.value.push({
|
|
||||||
id: Date.now(),
|
|
||||||
name: round.name,
|
|
||||||
prizeId: round.prizeId,
|
|
||||||
count: parseInt(round.count) || 1,
|
|
||||||
completed: false
|
|
||||||
})
|
|
||||||
await saveData('lottery_rounds', rounds.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateRound = async (round) => {
|
|
||||||
const index = rounds.value.findIndex(r => r.id === round.id)
|
|
||||||
if (index > -1) {
|
|
||||||
rounds.value[index] = round
|
|
||||||
await saveData('lottery_rounds', rounds.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeRound = async (id) => {
|
|
||||||
const index = rounds.value.findIndex(r => r.id === id)
|
|
||||||
if (index > -1) {
|
|
||||||
rounds.value.splice(index, 1)
|
|
||||||
await saveData('lottery_rounds', rounds.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPrizeForRound = (roundId) => {
|
|
||||||
const round = rounds.value.find(r => r.id === roundId)
|
|
||||||
if (!round) return null
|
|
||||||
return prizes.value.find(p => p.id === round.prizeId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ 抽奖控制 ============
|
|
||||||
const startLottery = async (round) => {
|
|
||||||
const prize = getPrizeForRound(round.id)
|
|
||||||
if (!prize) {
|
|
||||||
throw new Error('奖品不存在')
|
|
||||||
}
|
|
||||||
if (prize.used >= prize.stock) {
|
|
||||||
throw new Error('奖品库存不足')
|
|
||||||
}
|
|
||||||
if (round.completed) {
|
|
||||||
throw new Error('该轮次已完成')
|
|
||||||
}
|
|
||||||
if (participants.value.length === 0) {
|
|
||||||
throw new Error('参与者名单为空')
|
|
||||||
}
|
|
||||||
|
|
||||||
currentRound.value = round
|
|
||||||
isRolling.value = true
|
|
||||||
displayMode.value = 'scroll'
|
|
||||||
|
|
||||||
await saveData('lottery_currentRound', currentRound.value)
|
|
||||||
await saveData('lottery_isRolling', isRolling.value)
|
|
||||||
await saveData('lottery_displayMode', displayMode.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const stopLottery = async () => {
|
|
||||||
if (!currentRound.value) return
|
|
||||||
|
|
||||||
const round = rounds.value.find(r => r.id === currentRound.value.id)
|
|
||||||
const prize = prizes.value.find(p => p.id === round.prizeId)
|
|
||||||
|
|
||||||
// 获取可用的参与者(未中奖)
|
|
||||||
const availableParticipants = participants.value.filter(
|
|
||||||
p => !winners.value.some(w => w.id === p.id)
|
|
||||||
)
|
|
||||||
|
|
||||||
// 计算本次抽奖人数
|
|
||||||
const maxAvailable = prize.stock - prize.used
|
|
||||||
const roundCount = Math.min(round.count, maxAvailable, availableParticipants.length)
|
|
||||||
|
|
||||||
if (roundCount === 0) {
|
|
||||||
isRolling.value = false
|
|
||||||
displayMode.value = 'result'
|
|
||||||
await saveData('lottery_isRolling', isRolling.value)
|
|
||||||
await saveData('lottery_displayMode', displayMode.value)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 随机抽取
|
|
||||||
const shuffled = [...availableParticipants].sort(() => Math.random() - 0.5)
|
|
||||||
const newWinners = shuffled.slice(0, roundCount)
|
|
||||||
|
|
||||||
// 记录中奖者
|
|
||||||
newWinners.forEach(participant => {
|
|
||||||
winners.value.push({
|
|
||||||
id: participant.id,
|
|
||||||
...participant,
|
|
||||||
prizeId: prize.id,
|
|
||||||
prizeName: prize.name,
|
|
||||||
roundId: round.id,
|
|
||||||
roundName: round.name,
|
|
||||||
time: new Date().toISOString()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// 更新奖品使用量
|
|
||||||
prize.used += roundCount
|
|
||||||
|
|
||||||
// 更新轮次状态
|
|
||||||
round.completed = roundCount >= round.count || prize.used >= prize.stock
|
|
||||||
|
|
||||||
isRolling.value = false
|
|
||||||
displayMode.value = 'result'
|
|
||||||
|
|
||||||
await saveData('lottery_winners', winners.value)
|
|
||||||
await saveData('lottery_prizes', prizes.value)
|
|
||||||
await saveData('lottery_rounds', rounds.value)
|
|
||||||
await saveData('lottery_isRolling', isRolling.value)
|
|
||||||
await saveData('lottery_displayMode', displayMode.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetLottery = async () => {
|
|
||||||
winners.value = []
|
|
||||||
isRolling.value = false
|
|
||||||
currentRound.value = null
|
|
||||||
displayMode.value = 'scroll'
|
|
||||||
|
|
||||||
// 重置奖品使用量
|
|
||||||
prizes.value.forEach(p => {
|
|
||||||
p.used = 0
|
|
||||||
})
|
|
||||||
|
|
||||||
// 重置轮次状态
|
|
||||||
rounds.value.forEach(r => {
|
|
||||||
r.completed = false
|
|
||||||
})
|
|
||||||
|
|
||||||
await saveData('lottery_winners', winners.value)
|
|
||||||
await saveData('lottery_isRolling', isRolling.value)
|
|
||||||
await saveData('lottery_currentRound', currentRound.value)
|
|
||||||
await saveData('lottery_displayMode', displayMode.value)
|
|
||||||
await saveData('lottery_prizes', prizes.value)
|
|
||||||
await saveData('lottery_rounds', rounds.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ 导出功能 ============
|
|
||||||
const exportWinners = () => {
|
|
||||||
if (winners.value.length === 0) return
|
|
||||||
|
|
||||||
// 获取所有字段
|
|
||||||
const participantFields = Object.keys(winners.value[0]).filter(
|
|
||||||
key => !['id', 'prizeId', 'prizeName', 'roundId', 'roundName', 'time'].includes(key)
|
|
||||||
)
|
|
||||||
|
|
||||||
const headers = [...participantFields, '奖品', '轮次', '时间']
|
|
||||||
const rows = winners.value.map(w => {
|
|
||||||
const row = {}
|
|
||||||
participantFields.forEach(field => {
|
|
||||||
row[field] = w[field] || ''
|
|
||||||
})
|
|
||||||
row['奖品'] = w.prizeName
|
|
||||||
row['轮次'] = w.roundName
|
|
||||||
row['时间'] = new Date(w.time).toLocaleString()
|
|
||||||
return row
|
|
||||||
})
|
|
||||||
|
|
||||||
// 转换为CSV格式
|
|
||||||
const csv = [headers.join(','), ...rows.map(row => headers.map(h => row[h]).join(','))].join('\n')
|
|
||||||
|
|
||||||
// 创建下载
|
|
||||||
const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8' })
|
|
||||||
const url = URL.createObjectURL(blob)
|
|
||||||
const link = document.createElement('a')
|
|
||||||
link.href = url
|
|
||||||
link.download = `中奖名单_${new Date().toLocaleDateString()}.csv`
|
|
||||||
link.click()
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
const exportParticipants = () => {
|
|
||||||
if (participants.value.length === 0) return
|
|
||||||
|
|
||||||
// 获取所有字段
|
|
||||||
const allKeys = new Set()
|
|
||||||
participants.value.forEach(p => {
|
|
||||||
Object.keys(p).forEach(key => {
|
|
||||||
if (key !== 'id') allKeys.add(key)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const headers = Array.from(allKeys)
|
|
||||||
const rows = participants.value.map(p => headers.map(h => p[h] || '').join(','))
|
|
||||||
|
|
||||||
const csv = [headers.join(','), ...rows].join('\n')
|
|
||||||
const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8' })
|
|
||||||
const url = URL.createObjectURL(blob)
|
|
||||||
const link = document.createElement('a')
|
|
||||||
link.href = url
|
|
||||||
link.download = `抽奖名单_${new Date().toLocaleDateString()}.csv`
|
|
||||||
link.click()
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ 显示控制 ============
|
|
||||||
const switchDisplayMode = async (mode) => {
|
|
||||||
displayMode.value = mode
|
|
||||||
await saveData('lottery_displayMode', displayMode.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const setBackgroundImage = async (imageData) => {
|
|
||||||
try {
|
|
||||||
console.log('setBackgroundImage called with image data length:', imageData ? imageData.length : 0)
|
|
||||||
await saveData('lottery_backgroundImage', imageData)
|
|
||||||
backgroundImage.value = imageData
|
|
||||||
console.log('setBackgroundImage completed successfully')
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to save background image:', error)
|
|
||||||
throw new Error('保存背景图片失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearBackgroundImage = async () => {
|
|
||||||
try {
|
|
||||||
await indexedDB.delete('lottery_backgroundImage')
|
|
||||||
backgroundImage.value = ''
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to clear background image:', error)
|
|
||||||
throw new Error('清除背景图片失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
// 状态
|
|
||||||
fields,
|
|
||||||
participants,
|
|
||||||
prizes,
|
|
||||||
rounds,
|
|
||||||
winners,
|
|
||||||
isRolling,
|
|
||||||
currentRound,
|
|
||||||
displayMode,
|
|
||||||
backgroundImage,
|
|
||||||
isInitialized,
|
|
||||||
|
|
||||||
// 初始化
|
|
||||||
initialize,
|
|
||||||
|
|
||||||
// 字段管理
|
|
||||||
addField,
|
|
||||||
updateField,
|
|
||||||
removeField,
|
|
||||||
|
|
||||||
// 参与者管理
|
|
||||||
addParticipant,
|
|
||||||
updateParticipant,
|
|
||||||
removeParticipant,
|
|
||||||
clearParticipants,
|
|
||||||
importParticipantsFromFile,
|
|
||||||
|
|
||||||
// 奖品管理
|
|
||||||
addPrize,
|
|
||||||
updatePrize,
|
|
||||||
removePrize,
|
|
||||||
getPrizeAvailable,
|
|
||||||
|
|
||||||
// 轮次管理
|
|
||||||
addRound,
|
|
||||||
updateRound,
|
|
||||||
removeRound,
|
|
||||||
getPrizeForRound,
|
|
||||||
|
|
||||||
// 抽奖控制
|
|
||||||
startLottery,
|
|
||||||
stopLottery,
|
|
||||||
resetLottery,
|
|
||||||
|
|
||||||
// 导出功能
|
|
||||||
exportWinners,
|
|
||||||
exportParticipants,
|
|
||||||
|
|
||||||
// 显示控制
|
|
||||||
switchDisplayMode,
|
|
||||||
setBackgroundImage,
|
|
||||||
clearBackgroundImage
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
:root {
|
|
||||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
||||||
line-height: 1.5;
|
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
color-scheme: light dark;
|
|
||||||
color: rgba(255, 255, 255, 0.87);
|
|
||||||
background-color: #242424;
|
|
||||||
|
|
||||||
font-synthesis: none;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #646cff;
|
|
||||||
text-decoration: inherit;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #535bf2;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
min-width: 320px;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 3.2em;
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
height: 6em;
|
|
||||||
padding: 1.5em;
|
|
||||||
will-change: filter;
|
|
||||||
transition: filter 300ms;
|
|
||||||
}
|
|
||||||
.logo:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #646cffaa);
|
|
||||||
}
|
|
||||||
.logo.vanilla:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #3178c6aa);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.read-the-docs {
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
padding: 0.6em 1.2em;
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: 500;
|
|
||||||
font-family: inherit;
|
|
||||||
background-color: #1a1a1a;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 0.25s;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
border-color: #646cff;
|
|
||||||
}
|
|
||||||
button:focus,
|
|
||||||
button:focus-visible {
|
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
:root {
|
|
||||||
color: #213547;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #747bff;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,249 +0,0 @@
|
|||||||
/* 组件样式 */
|
|
||||||
|
|
||||||
/* ===== 按钮组件 ===== */
|
|
||||||
.el-button {
|
|
||||||
font-family: var(--font-family-primary);
|
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
border-radius: var(--border-radius-md);
|
|
||||||
transition: var(--transition-normal);
|
|
||||||
box-shadow: var(--shadow-small);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button--primary {
|
|
||||||
background-color: var(--color-primary);
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button--primary:hover {
|
|
||||||
background-color: var(--color-secondary);
|
|
||||||
border-color: var(--color-secondary);
|
|
||||||
box-shadow: var(--shadow-button);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button--success {
|
|
||||||
background-color: var(--color-success);
|
|
||||||
border-color: var(--color-success);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button--warning {
|
|
||||||
background-color: var(--color-warning);
|
|
||||||
border-color: var(--color-warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button--danger {
|
|
||||||
background-color: var(--color-danger);
|
|
||||||
border-color: var(--color-danger);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 卡片组件 ===== */
|
|
||||||
.el-card {
|
|
||||||
border-radius: var(--border-radius-lg);
|
|
||||||
box-shadow: var(--shadow-medium);
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-card__header {
|
|
||||||
font-family: var(--font-family-secondary);
|
|
||||||
font-weight: var(--font-weight-semibold);
|
|
||||||
font-size: var(--font-size-lg);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
padding: var(--spacing-xl);
|
|
||||||
border-bottom: 1px solid var(--color-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-card__body {
|
|
||||||
padding: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 表单组件 ===== */
|
|
||||||
.el-form-item__label {
|
|
||||||
font-family: var(--font-family-primary);
|
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-input__wrapper {
|
|
||||||
border-radius: var(--border-radius-md);
|
|
||||||
transition: var(--transition-normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-input__wrapper:hover {
|
|
||||||
box-shadow: var(--shadow-small);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-input__wrapper.is-focus {
|
|
||||||
box-shadow: var(--shadow-medium);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 表格组件 ===== */
|
|
||||||
.el-table {
|
|
||||||
border-radius: var(--border-radius-lg);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-table th.el-table__cell {
|
|
||||||
background-color: var(--color-primary);
|
|
||||||
color: var(--color-text-white);
|
|
||||||
font-family: var(--font-family-secondary);
|
|
||||||
font-weight: var(--font-weight-semibold);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-table tr {
|
|
||||||
transition: var(--transition-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-table--enable-row-hover .el-table__body tr:hover > td {
|
|
||||||
background-color: rgba(var(--color-secondary-rgb), 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 对话框组件 ===== */
|
|
||||||
.el-dialog {
|
|
||||||
border-radius: var(--border-radius-xl);
|
|
||||||
box-shadow: var(--shadow-large);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dialog__header {
|
|
||||||
font-family: var(--font-family-secondary);
|
|
||||||
font-weight: var(--font-weight-semibold);
|
|
||||||
font-size: var(--font-size-xl);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
padding: var(--spacing-2xl);
|
|
||||||
border-bottom: 1px solid var(--color-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dialog__body {
|
|
||||||
padding: var(--spacing-2xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dialog__footer {
|
|
||||||
padding: var(--spacing-xl) var(--spacing-2xl);
|
|
||||||
border-top: 1px solid var(--color-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 标签组件 ===== */
|
|
||||||
.el-tag {
|
|
||||||
border-radius: var(--border-radius-md);
|
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-tag--success {
|
|
||||||
background-color: rgba(var(--color-success), 0.1);
|
|
||||||
color: var(--color-success);
|
|
||||||
border-color: var(--color-success);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-tag--warning {
|
|
||||||
background-color: rgba(var(--color-warning), 0.1);
|
|
||||||
color: var(--color-warning);
|
|
||||||
border-color: var(--color-warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-tag--danger {
|
|
||||||
background-color: rgba(var(--color-danger), 0.1);
|
|
||||||
color: var(--color-danger);
|
|
||||||
border-color: var(--color-danger);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 消息提示组件 ===== */
|
|
||||||
.el-message {
|
|
||||||
border-radius: var(--border-radius-lg);
|
|
||||||
box-shadow: var(--shadow-large);
|
|
||||||
font-family: var(--font-family-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-message--success {
|
|
||||||
background-color: var(--color-success);
|
|
||||||
border-color: var(--color-success);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-message--warning {
|
|
||||||
background-color: var(--color-warning);
|
|
||||||
border-color: var(--color-warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-message--error {
|
|
||||||
background-color: var(--color-danger);
|
|
||||||
border-color: var(--color-danger);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 自定义卡片样式 ===== */
|
|
||||||
.custom-card {
|
|
||||||
background: var(--color-background);
|
|
||||||
border-radius: var(--border-radius-lg);
|
|
||||||
box-shadow: var(--shadow-medium);
|
|
||||||
padding: var(--spacing-2xl);
|
|
||||||
transition: var(--transition-normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-card:hover {
|
|
||||||
box-shadow: var(--shadow-large);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 标题样式 ===== */
|
|
||||||
.page-title {
|
|
||||||
font-family: var(--font-family-secondary);
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
font-size: var(--font-size-3xl);
|
|
||||||
color: var(--color-primary);
|
|
||||||
margin-bottom: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
font-family: var(--font-family-secondary);
|
|
||||||
font-weight: var(--font-weight-semibold);
|
|
||||||
font-size: var(--font-size-xl);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
margin-bottom: var(--spacing-lg);
|
|
||||||
padding-bottom: var(--spacing-sm);
|
|
||||||
border-bottom: 2px solid var(--color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 统计卡片 ===== */
|
|
||||||
.stat-card {
|
|
||||||
background: linear-gradient(135deg, var(--color-primary), var(--color-accent));
|
|
||||||
color: var(--color-text-white);
|
|
||||||
border-radius: var(--border-radius-xl);
|
|
||||||
padding: var(--spacing-2xl);
|
|
||||||
box-shadow: var(--shadow-large);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-card .stat-value {
|
|
||||||
font-family: var(--font-family-secondary);
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
font-size: var(--font-size-4xl);
|
|
||||||
margin-bottom: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-card .stat-label {
|
|
||||||
font-family: var(--font-family-primary);
|
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
font-size: var(--font-size-md);
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 操作按钮组 ===== */
|
|
||||||
.action-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-md);
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 表单区域 ===== */
|
|
||||||
.form-section {
|
|
||||||
background: var(--color-background);
|
|
||||||
border-radius: var(--border-radius-lg);
|
|
||||||
padding: var(--spacing-2xl);
|
|
||||||
box-shadow: var(--shadow-medium);
|
|
||||||
margin-bottom: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-section-title {
|
|
||||||
font-family: var(--font-family-secondary);
|
|
||||||
font-weight: var(--font-weight-semibold);
|
|
||||||
font-size: var(--font-size-lg);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
margin-bottom: var(--spacing-lg);
|
|
||||||
padding-bottom: var(--spacing-sm);
|
|
||||||
border-bottom: 1px solid var(--color-border);
|
|
||||||
}
|
|
||||||
@@ -1,567 +0,0 @@
|
|||||||
/* Element Plus 组件样式覆盖 - 基于参考项目设计 */
|
|
||||||
|
|
||||||
/* ===== 按钮组件 ===== */
|
|
||||||
.el-button {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: var(--spacing-sm);
|
|
||||||
border: 1px solid transparent;
|
|
||||||
border-radius: var(--border-radius-md);
|
|
||||||
font-size: var(--font-size-base);
|
|
||||||
font-weight: var(--font-weight-semibold);
|
|
||||||
font-family: var(--font-family-primary);
|
|
||||||
text-decoration: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: var(--transition-color), var(--transition-background), var(--transition-border), var(--transition-box-shadow), var(--transition-transform);
|
|
||||||
background: none;
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
padding: var(--spacing-lg) var(--spacing-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button:focus {
|
|
||||||
outline: 2px solid var(--color-secondary);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button:disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button--primary {
|
|
||||||
background: var(--color-primary);
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
color: var(--color-text-white);
|
|
||||||
box-shadow: var(--shadow-button);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button--primary:hover {
|
|
||||||
background: var(--color-primary);
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 6px 20px rgba(var(--color-primary-rgb), 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button--primary:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
box-shadow: var(--shadow-button);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button--success {
|
|
||||||
background: var(--color-success);
|
|
||||||
border-color: var(--color-success);
|
|
||||||
color: var(--color-text-white);
|
|
||||||
box-shadow: 0 4px 16px rgba(var(--color-success-rgb), 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button--success:hover {
|
|
||||||
background: var(--color-success);
|
|
||||||
border-color: var(--color-success);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 6px 20px rgba(var(--color-success-rgb), 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button--success:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
box-shadow: 0 4px 16px rgba(var(--color-success-rgb), 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button--danger {
|
|
||||||
background: var(--color-danger);
|
|
||||||
border-color: var(--color-danger);
|
|
||||||
color: var(--color-text-white);
|
|
||||||
box-shadow: 0 4px 16px rgba(var(--color-danger-rgb), 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button--danger:hover {
|
|
||||||
background: var(--color-danger);
|
|
||||||
border-color: var(--color-danger);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 6px 20px rgba(var(--color-danger-rgb), 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button--danger:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
box-shadow: 0 4px 16px rgba(var(--color-danger-rgb), 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button--warning {
|
|
||||||
background: var(--color-warning);
|
|
||||||
border-color: var(--color-warning);
|
|
||||||
color: var(--color-text-white);
|
|
||||||
box-shadow: 0 4px 16px rgba(var(--color-warning-rgb), 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button--warning:hover {
|
|
||||||
background: var(--color-warning);
|
|
||||||
border-color: var(--color-warning);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 6px 20px rgba(var(--color-warning-rgb), 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button--warning:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
box-shadow: 0 4px 16px rgba(var(--color-warning-rgb), 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button--info {
|
|
||||||
background: var(--color-info);
|
|
||||||
border-color: var(--color-info);
|
|
||||||
color: var(--color-text-white);
|
|
||||||
box-shadow: 0 4px 16px rgba(var(--color-info-rgb), 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button--info:hover {
|
|
||||||
background: var(--color-info);
|
|
||||||
border-color: var(--color-info);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 6px 20px rgba(var(--color-info-rgb), 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button--info:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
box-shadow: 0 4px 16px rgba(var(--color-info-rgb), 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button--default {
|
|
||||||
background: var(--color-background);
|
|
||||||
border-color: var(--color-border);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button--default:hover {
|
|
||||||
background: rgba(var(--color-primary-rgb), 0.1);
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button--default:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button--small {
|
|
||||||
padding: var(--spacing-sm) var(--spacing-lg);
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
height: var(--button-height-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button--small.el-button--primary:hover,
|
|
||||||
.el-button--small.el-button--success:hover,
|
|
||||||
.el-button--small.el-button--info:hover,
|
|
||||||
.el-button--small.el-button--warning:hover,
|
|
||||||
.el-button--small.el-button--danger:hover {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button--small.el-button--primary:active,
|
|
||||||
.el-button--small.el-button--success:active,
|
|
||||||
.el-button--small.el-button--info:active,
|
|
||||||
.el-button--small.el-button--warning:active,
|
|
||||||
.el-button--small.el-button--danger:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button.is-circle {
|
|
||||||
border-radius: var(--border-radius-full);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button.is-link {
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
box-shadow: none;
|
|
||||||
padding: var(--spacing-sm) var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button.is-link:hover {
|
|
||||||
background: transparent;
|
|
||||||
transform: none;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button--text {
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
box-shadow: none;
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button--text:hover {
|
|
||||||
background: rgba(var(--color-primary-rgb), 0.1);
|
|
||||||
color: var(--color-primary);
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 卡片组件 ===== */
|
|
||||||
.el-card {
|
|
||||||
border-radius: var(--border-radius-lg);
|
|
||||||
box-shadow: var(--shadow-small);
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-card__header {
|
|
||||||
font-family: var(--font-family-secondary);
|
|
||||||
font-weight: var(--font-weight-semibold);
|
|
||||||
font-size: var(--font-size-lg);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
padding: var(--spacing-xl);
|
|
||||||
border-bottom: 1px solid var(--color-border);
|
|
||||||
background: var(--color-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-card__body {
|
|
||||||
padding: var(--spacing-xl);
|
|
||||||
background: var(--color-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 表格组件 ===== */
|
|
||||||
.el-table {
|
|
||||||
border-radius: var(--border-radius-lg);
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: var(--shadow-small);
|
|
||||||
font-family: var(--font-family-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-table .el-table__header-wrapper {
|
|
||||||
background: var(--color-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-table th {
|
|
||||||
background: var(--color-background);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
font-weight: var(--font-weight-semibold);
|
|
||||||
font-family: var(--font-family-secondary);
|
|
||||||
border-bottom: 1px solid var(--color-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-table td {
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
border-bottom: 1px solid var(--color-border);
|
|
||||||
font-size: var(--font-size-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-table .el-table__row:hover > td {
|
|
||||||
background: rgba(var(--color-primary-rgb), 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-table .el-table__row {
|
|
||||||
transition: var(--transition-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 对话框组件 ===== */
|
|
||||||
.el-dialog {
|
|
||||||
border-radius: var(--border-radius-lg);
|
|
||||||
box-shadow: var(--shadow-large);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dialog__header {
|
|
||||||
background: var(--color-primary);
|
|
||||||
color: var(--color-text-white);
|
|
||||||
padding: var(--spacing-2xl);
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dialog__title {
|
|
||||||
color: var(--color-text-white);
|
|
||||||
font-weight: var(--font-weight-semibold);
|
|
||||||
font-family: var(--font-family-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dialog__headerbtn {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
right: var(--spacing-lg);
|
|
||||||
transform: translateY(-50%);
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: var(--border-radius-full);
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
transition: var(--transition-background), var(--transition-transform);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dialog__headerbtn .el-dialog__close {
|
|
||||||
color: var(--color-text-white);
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dialog__headerbtn:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.2);
|
|
||||||
transform: translateY(-50%) scale(1.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dialog__body {
|
|
||||||
padding: var(--spacing-2xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dialog__footer {
|
|
||||||
padding: var(--spacing-lg) var(--spacing-2xl) var(--spacing-2xl);
|
|
||||||
border-top: 1px solid var(--color-border);
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 表单组件 ===== */
|
|
||||||
.el-form-item__label {
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
font-family: var(--font-family-primary);
|
|
||||||
font-size: var(--font-size-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-input__wrapper {
|
|
||||||
border-radius: var(--border-radius-md);
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
background: var(--color-background);
|
|
||||||
transition: var(--transition-border), var(--transition-box-shadow);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-input__wrapper:hover {
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-input__wrapper.is-focus {
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
box-shadow: var(--shadow-small);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-input__inner::placeholder {
|
|
||||||
color: var(--color-text-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-textarea__inner {
|
|
||||||
border-radius: var(--border-radius-md);
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
background: var(--color-background);
|
|
||||||
transition: var(--transition-border), var(--transition-box-shadow);
|
|
||||||
font-family: var(--font-family-primary);
|
|
||||||
font-size: var(--font-size-base);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
line-height: var(--line-height-relaxed);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-textarea__inner:hover {
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-textarea__inner:focus {
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
box-shadow: var(--shadow-small);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-textarea__inner::placeholder {
|
|
||||||
color: var(--color-text-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 选择器组件 ===== */
|
|
||||||
.el-select .el-input__wrapper {
|
|
||||||
border-radius: var(--border-radius-md);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-select-dropdown {
|
|
||||||
border-radius: var(--border-radius-lg);
|
|
||||||
border: none;
|
|
||||||
box-shadow: var(--shadow-large);
|
|
||||||
padding: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-select-dropdown__item {
|
|
||||||
border-radius: var(--border-radius-md);
|
|
||||||
padding: var(--spacing-lg) var(--spacing-xl);
|
|
||||||
transition: var(--transition-color), var(--transition-background);
|
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-select-dropdown__item:hover {
|
|
||||||
background: rgba(var(--color-primary-rgb), 0.1);
|
|
||||||
color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-select-dropdown__item.is-selected {
|
|
||||||
background: var(--color-primary);
|
|
||||||
color: var(--color-text-white);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 数字输入框 ===== */
|
|
||||||
.el-input-number {
|
|
||||||
width: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-input-number .el-input__wrapper {
|
|
||||||
border-radius: var(--border-radius-sm);
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
background: var(--color-background);
|
|
||||||
transition: var(--transition-border), var(--transition-box-shadow);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-input-number .el-input__wrapper:hover {
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-input-number .el-input__wrapper.is-focus {
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
box-shadow: var(--shadow-small);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 上传组件 ===== */
|
|
||||||
.el-upload-dragger {
|
|
||||||
border-radius: var(--border-radius-lg);
|
|
||||||
border: 2px dashed var(--color-border);
|
|
||||||
transition: var(--transition-border), var(--transition-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-upload-dragger:hover {
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
background: rgba(var(--color-primary-rgb), 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 标签组件 ===== */
|
|
||||||
.el-tag {
|
|
||||||
border-radius: var(--border-radius-full);
|
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
font-family: var(--font-family-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-tag--success {
|
|
||||||
background: var(--color-success);
|
|
||||||
border-color: var(--color-success);
|
|
||||||
color: var(--color-text-white);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-tag--info {
|
|
||||||
background: var(--color-info);
|
|
||||||
border-color: var(--color-info);
|
|
||||||
color: var(--color-text-white);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-tag--warning {
|
|
||||||
background: var(--color-warning);
|
|
||||||
border-color: var(--color-warning);
|
|
||||||
color: var(--color-text-white);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-tag--danger {
|
|
||||||
background: var(--color-danger);
|
|
||||||
border-color: var(--color-danger);
|
|
||||||
color: var(--color-text-white);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 消息提示 ===== */
|
|
||||||
.el-message {
|
|
||||||
border-radius: var(--border-radius-lg);
|
|
||||||
box-shadow: var(--shadow-large);
|
|
||||||
font-family: var(--font-family-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-message--success {
|
|
||||||
background: var(--color-success);
|
|
||||||
border-color: var(--color-success);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-message--warning {
|
|
||||||
background: var(--color-warning);
|
|
||||||
border-color: var(--color-warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-message--error {
|
|
||||||
background: var(--color-danger);
|
|
||||||
border-color: var(--color-danger);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-message--info {
|
|
||||||
background: var(--color-info);
|
|
||||||
border-color: var(--color-info);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 复选框 ===== */
|
|
||||||
.el-checkbox {
|
|
||||||
font-family: var(--font-family-primary);
|
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-checkbox__input.is-checked .el-checkbox__inner {
|
|
||||||
background-color: var(--color-primary);
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-checkbox__inner:hover {
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 分页 ===== */
|
|
||||||
.el-pagination {
|
|
||||||
font-family: var(--font-family-primary);
|
|
||||||
font-size: var(--font-size-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-pagination .el-pager li {
|
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
border-radius: var(--border-radius-sm);
|
|
||||||
margin: 0 2px;
|
|
||||||
transition: var(--transition-color), var(--transition-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-pagination .el-pager li.active {
|
|
||||||
background: var(--color-primary);
|
|
||||||
color: var(--color-text-white);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-pagination .el-pager li:hover {
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-pagination button {
|
|
||||||
border-radius: var(--border-radius-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 开关 ===== */
|
|
||||||
.el-switch {
|
|
||||||
--el-switch-on-color: var(--color-success);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-switch__core {
|
|
||||||
border-radius: var(--border-radius-full);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 下拉菜单 ===== */
|
|
||||||
.el-dropdown-menu {
|
|
||||||
border-radius: var(--border-radius-lg);
|
|
||||||
border: none;
|
|
||||||
box-shadow: var(--shadow-large);
|
|
||||||
padding: var(--spacing-md);
|
|
||||||
background: var(--color-background);
|
|
||||||
overflow: hidden;
|
|
||||||
font-family: var(--font-family-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dropdown-menu__item {
|
|
||||||
border-radius: var(--border-radius-md);
|
|
||||||
padding: var(--spacing-lg) var(--spacing-xl);
|
|
||||||
transition: var(--transition-color), var(--transition-background);
|
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dropdown-menu__item:hover {
|
|
||||||
background: rgba(var(--color-primary-rgb), 0.1);
|
|
||||||
color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dropdown-menu__item.is-divided {
|
|
||||||
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
|
||||||
margin-top: 8px;
|
|
||||||
padding-top: 16px;
|
|
||||||
}
|
|
||||||
@@ -1,322 +0,0 @@
|
|||||||
/* 全局基础样式 */
|
|
||||||
|
|
||||||
/* ===== 重置样式 ===== */
|
|
||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
font-size: 16px;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: var(--font-family-primary);
|
|
||||||
font-size: var(--font-size-base);
|
|
||||||
font-weight: var(--font-weight-normal);
|
|
||||||
line-height: var(--line-height-normal);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
background-color: var(--color-background);
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 通用样式 ===== */
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
|
||||||
font-family: var(--font-family-secondary);
|
|
||||||
font-weight: var(--font-weight-semibold);
|
|
||||||
line-height: var(--line-height-tight);
|
|
||||||
margin-bottom: var(--spacing-md);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: var(--font-size-4xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: var(--font-size-3xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: var(--font-size-2xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
font-size: var(--font-size-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
h5 {
|
|
||||||
font-size: var(--font-size-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
h6 {
|
|
||||||
font-size: var(--font-size-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin-bottom: var(--spacing-md);
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--color-secondary);
|
|
||||||
text-decoration: none;
|
|
||||||
transition: var(--transition-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
ul, ol {
|
|
||||||
margin-bottom: var(--spacing-md);
|
|
||||||
padding-left: var(--spacing-2xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
margin-bottom: var(--spacing-sm);
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 工具类 ===== */
|
|
||||||
.container {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 1400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 var(--spacing-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-center {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-left {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-right {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-primary {
|
|
||||||
color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-secondary {
|
|
||||||
color: var(--color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-muted {
|
|
||||||
color: var(--color-text-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
.font-bold {
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
}
|
|
||||||
|
|
||||||
.font-semibold {
|
|
||||||
font-weight: var(--font-weight-semibold);
|
|
||||||
}
|
|
||||||
|
|
||||||
.font-medium {
|
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 间距工具类 ===== */
|
|
||||||
.m-0 { margin: 0; }
|
|
||||||
.mt-0 { margin-top: 0; }
|
|
||||||
.mr-0 { margin-right: 0; }
|
|
||||||
.mb-0 { margin-bottom: 0; }
|
|
||||||
.ml-0 { margin-left: 0; }
|
|
||||||
.mx-0 { margin-left: 0; margin-right: 0; }
|
|
||||||
.my-0 { margin-top: 0; margin-bottom: 0; }
|
|
||||||
|
|
||||||
.m-1 { margin: var(--spacing-xs); }
|
|
||||||
.mt-1 { margin-top: var(--spacing-xs); }
|
|
||||||
.mr-1 { margin-right: var(--spacing-xs); }
|
|
||||||
.mb-1 { margin-bottom: var(--spacing-xs); }
|
|
||||||
.ml-1 { margin-left: var(--spacing-xs); }
|
|
||||||
.mx-1 { margin-left: var(--spacing-xs); margin-right: var(--spacing-xs); }
|
|
||||||
.my-1 { margin-top: var(--spacing-xs); margin-bottom: var(--spacing-xs); }
|
|
||||||
|
|
||||||
.m-2 { margin: var(--spacing-sm); }
|
|
||||||
.mt-2 { margin-top: var(--spacing-sm); }
|
|
||||||
.mr-2 { margin-right: var(--spacing-sm); }
|
|
||||||
.mb-2 { margin-bottom: var(--spacing-sm); }
|
|
||||||
.ml-2 { margin-left: var(--spacing-sm); }
|
|
||||||
.mx-2 { margin-left: var(--spacing-sm); margin-right: var(--spacing-sm); }
|
|
||||||
.my-2 { margin-top: var(--spacing-sm); margin-bottom: var(--spacing-sm); }
|
|
||||||
|
|
||||||
.m-3 { margin: var(--spacing-md); }
|
|
||||||
.mt-3 { margin-top: var(--spacing-md); }
|
|
||||||
.mr-3 { margin-right: var(--spacing-md); }
|
|
||||||
.mb-3 { margin-bottom: var(--spacing-md); }
|
|
||||||
.ml-3 { margin-left: var(--spacing-md); }
|
|
||||||
.mx-3 { margin-left: var(--spacing-md); margin-right: var(--spacing-md); }
|
|
||||||
.my-3 { margin-top: var(--spacing-md); margin-bottom: var(--spacing-md); }
|
|
||||||
|
|
||||||
.m-4 { margin: var(--spacing-lg); }
|
|
||||||
.mt-4 { margin-top: var(--spacing-lg); }
|
|
||||||
.mr-4 { margin-right: var(--spacing-lg); }
|
|
||||||
.mb-4 { margin-bottom: var(--spacing-lg); }
|
|
||||||
.ml-4 { margin-left: var(--spacing-lg); }
|
|
||||||
.mx-4 { margin-left: var(--spacing-lg); margin-right: var(--spacing-lg); }
|
|
||||||
.my-4 { margin-top: var(--spacing-lg); margin-bottom: var(--spacing-lg); }
|
|
||||||
|
|
||||||
.p-0 { padding: 0; }
|
|
||||||
.pt-0 { padding-top: 0; }
|
|
||||||
.pr-0 { padding-right: 0; }
|
|
||||||
.pb-0 { padding-bottom: 0; }
|
|
||||||
.pl-0 { padding-left: 0; }
|
|
||||||
.px-0 { padding-left: 0; padding-right: 0; }
|
|
||||||
.py-0 { padding-top: 0; padding-bottom: 0; }
|
|
||||||
|
|
||||||
.p-1 { padding: var(--spacing-xs); }
|
|
||||||
.pt-1 { padding-top: var(--spacing-xs); }
|
|
||||||
.pr-1 { padding-right: var(--spacing-xs); }
|
|
||||||
.pb-1 { padding-bottom: var(--spacing-xs); }
|
|
||||||
.pl-1 { padding-left: var(--spacing-xs); }
|
|
||||||
.px-1 { padding-left: var(--spacing-xs); padding-right: var(--spacing-xs); }
|
|
||||||
.py-1 { padding-top: var(--spacing-xs); padding-bottom: var(--spacing-xs); }
|
|
||||||
|
|
||||||
.p-2 { padding: var(--spacing-sm); }
|
|
||||||
.pt-2 { padding-top: var(--spacing-sm); }
|
|
||||||
.pr-2 { padding-right: var(--spacing-sm); }
|
|
||||||
.pb-2 { padding-bottom: var(--spacing-sm); }
|
|
||||||
.pl-2 { padding-left: var(--spacing-sm); }
|
|
||||||
.px-2 { padding-left: var(--spacing-sm); padding-right: var(--spacing-sm); }
|
|
||||||
.py-2 { padding-top: var(--spacing-sm); padding-bottom: var(--spacing-sm); }
|
|
||||||
|
|
||||||
.p-3 { padding: var(--spacing-md); }
|
|
||||||
.pt-3 { padding-top: var(--spacing-md); }
|
|
||||||
.pr-3 { padding-right: var(--spacing-md); }
|
|
||||||
.pb-3 { padding-bottom: var(--spacing-md); }
|
|
||||||
.pl-3 { padding-left: var(--spacing-md); }
|
|
||||||
.px-3 { padding-left: var(--spacing-md); padding-right: var(--spacing-md); }
|
|
||||||
.py-3 { padding-top: var(--spacing-md); padding-bottom: var(--spacing-md); }
|
|
||||||
|
|
||||||
.p-4 { padding: var(--spacing-lg); }
|
|
||||||
.pt-4 { padding-top: var(--spacing-lg); }
|
|
||||||
.pr-4 { padding-right: var(--spacing-lg); }
|
|
||||||
.pb-4 { padding-bottom: var(--spacing-lg); }
|
|
||||||
.pl-4 { padding-left: var(--spacing-lg); }
|
|
||||||
.px-4 { padding-left: var(--spacing-lg); padding-right: var(--spacing-lg); }
|
|
||||||
.py-4 { padding-top: var(--spacing-lg); padding-bottom: var(--spacing-lg); }
|
|
||||||
|
|
||||||
/* ===== Flexbox 工具类 ===== */
|
|
||||||
.d-flex {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-row {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-column {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.justify-start {
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.justify-center {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.justify-end {
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.justify-between {
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align-start {
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align-center {
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align-end {
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-wrap {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-1 {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 滚动条样式 ===== */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: var(--color-border);
|
|
||||||
border-radius: var(--border-radius-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: var(--color-text-light);
|
|
||||||
border-radius: var(--border-radius-sm);
|
|
||||||
transition: var(--transition-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: var(--color-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== Vue 应用样式 ===== */
|
|
||||||
#app {
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 过渡动画 */
|
|
||||||
.fade-enter-active,
|
|
||||||
.fade-leave-active {
|
|
||||||
transition: opacity var(--transition-normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-enter-from,
|
|
||||||
.fade-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slide-fade-enter-active {
|
|
||||||
transition: all var(--transition-normal) ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slide-fade-leave-active {
|
|
||||||
transition: all var(--transition-fast) ease-in;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slide-fade-enter-from {
|
|
||||||
transform: translateY(-10px);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slide-fade-leave-to {
|
|
||||||
transform: translateY(10px);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
/* 全局样式变量 - 基于琳盛网站设计系统 */
|
|
||||||
|
|
||||||
/* ===== 色彩系统 ===== */
|
|
||||||
:root {
|
|
||||||
/* 主色调 - 深棕红色 */
|
|
||||||
--color-primary: rgb(29, 17, 17);
|
|
||||||
--color-primary-rgb: 29, 17, 17;
|
|
||||||
|
|
||||||
/* 次要色 - 青绿色 */
|
|
||||||
--color-secondary: rgb(0, 165, 141);
|
|
||||||
--color-secondary-rgb: 0, 165, 141;
|
|
||||||
|
|
||||||
/* 强调色 - 深蓝色 */
|
|
||||||
--color-accent: rgb(34, 37, 121);
|
|
||||||
--color-accent-rgb: 34, 37, 121;
|
|
||||||
|
|
||||||
/* 背景色 */
|
|
||||||
--color-background: rgb(255, 255, 255);
|
|
||||||
--color-background-rgb: 255, 255, 255;
|
|
||||||
|
|
||||||
/* 文字颜色 */
|
|
||||||
--color-text-primary: rgb(29, 17, 17);
|
|
||||||
--color-text-secondary: rgb(64, 64, 64);
|
|
||||||
--color-text-light: rgb(120, 120, 120);
|
|
||||||
--color-text-white: rgb(255, 255, 255);
|
|
||||||
|
|
||||||
/* 边框颜色 */
|
|
||||||
--color-border: rgb(225, 225, 225);
|
|
||||||
--color-border-light: rgba(0, 0, 0, 0.1);
|
|
||||||
|
|
||||||
/* 状态颜色 */
|
|
||||||
--color-success: rgb(0, 165, 141);
|
|
||||||
--color-warning: rgb(243, 156, 18);
|
|
||||||
--color-danger: rgb(220, 53, 69);
|
|
||||||
--color-info: rgb(23, 162, 184);
|
|
||||||
|
|
||||||
/* 阴影 */
|
|
||||||
--shadow-small: 0 2px 8px rgba(0, 0, 0, 0.05);
|
|
||||||
--shadow-medium: 0 4px 16px rgba(0, 0, 0, 0.1);
|
|
||||||
--shadow-large: 0 8px 24px rgba(0, 0, 0, 0.15);
|
|
||||||
--shadow-button: 0 4px 16px rgba(var(--color-primary-rgb), 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 字体系统 ===== */
|
|
||||||
:root {
|
|
||||||
/* 主字体 - Poppins */
|
|
||||||
--font-family-primary: 'Poppins', 'Noto Sans SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
|
|
||||||
/* 辅助字体 - Montserrat */
|
|
||||||
--font-family-secondary: 'Montserrat', 'Noto Sans SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
|
|
||||||
/* 等宽字体 */
|
|
||||||
--font-family-mono: 'Fira Code', 'Consolas', 'Monaco', 'Courier New', monospace;
|
|
||||||
|
|
||||||
/* 字体大小 */
|
|
||||||
--font-size-xs: 12px;
|
|
||||||
--font-size-sm: 13px;
|
|
||||||
--font-size-base: 14px;
|
|
||||||
--font-size-md: 16px;
|
|
||||||
--font-size-lg: 18px;
|
|
||||||
--font-size-xl: 20px;
|
|
||||||
--font-size-2xl: 24px;
|
|
||||||
--font-size-3xl: 28px;
|
|
||||||
--font-size-4xl: 32px;
|
|
||||||
|
|
||||||
/* 字体粗细 */
|
|
||||||
--font-weight-light: 300;
|
|
||||||
--font-weight-normal: 400;
|
|
||||||
--font-weight-medium: 500;
|
|
||||||
--font-weight-semibold: 600;
|
|
||||||
--font-weight-bold: 700;
|
|
||||||
|
|
||||||
/* 行高 */
|
|
||||||
--line-height-tight: 1.2;
|
|
||||||
--line-height-normal: 1.4;
|
|
||||||
--line-height-relaxed: 1.6;
|
|
||||||
--line-height-loose: 1.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 间距系统 ===== */
|
|
||||||
:root {
|
|
||||||
--spacing-xs: 4px;
|
|
||||||
--spacing-sm: 8px;
|
|
||||||
--spacing-md: 12px;
|
|
||||||
--spacing-lg: 16px;
|
|
||||||
--spacing-xl: 20px;
|
|
||||||
--spacing-2xl: 24px;
|
|
||||||
--spacing-3xl: 32px;
|
|
||||||
--spacing-4xl: 40px;
|
|
||||||
--spacing-5xl: 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 圆角系统 ===== */
|
|
||||||
:root {
|
|
||||||
--border-radius-none: 0px;
|
|
||||||
--border-radius-sm: 4px;
|
|
||||||
--border-radius-md: 8px;
|
|
||||||
--border-radius-lg: 12px;
|
|
||||||
--border-radius-xl: 16px;
|
|
||||||
--border-radius-2xl: 20px;
|
|
||||||
--border-radius-full: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 过渡动画 ===== */
|
|
||||||
:root {
|
|
||||||
--transition-fast: 0.15s ease;
|
|
||||||
--transition-normal: 0.3s ease;
|
|
||||||
--transition-slow: 0.5s ease;
|
|
||||||
|
|
||||||
/* 简化的过渡效果 */
|
|
||||||
--transition-color: color var(--transition-normal);
|
|
||||||
--transition-background: background-color var(--transition-normal);
|
|
||||||
--transition-border: border-color var(--transition-normal);
|
|
||||||
--transition-transform: transform var(--transition-normal);
|
|
||||||
--transition-box-shadow: box-shadow var(--transition-normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 断点系统 ===== */
|
|
||||||
:root {
|
|
||||||
--breakpoint-sm: 576px;
|
|
||||||
--breakpoint-md: 768px;
|
|
||||||
--breakpoint-lg: 992px;
|
|
||||||
--breakpoint-xl: 1200px;
|
|
||||||
--breakpoint-2xl: 1400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== Z-index 层级 ===== */
|
|
||||||
:root {
|
|
||||||
--z-index-dropdown: 1000;
|
|
||||||
--z-index-sticky: 1020;
|
|
||||||
--z-index-fixed: 1030;
|
|
||||||
--z-index-modal-backdrop: 1040;
|
|
||||||
--z-index-modal: 1050;
|
|
||||||
--z-index-popover: 1060;
|
|
||||||
--z-index-tooltip: 1070;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 组件特定变量 ===== */
|
|
||||||
:root {
|
|
||||||
/* 按钮高度 */
|
|
||||||
--button-height-sm: 32px;
|
|
||||||
--button-height-md: 40px;
|
|
||||||
--button-height-lg: 48px;
|
|
||||||
--button-height-xl: 56px;
|
|
||||||
|
|
||||||
/* 输入框高度 */
|
|
||||||
--input-height-sm: 32px;
|
|
||||||
--input-height-md: 40px;
|
|
||||||
--input-height-lg: 48px;
|
|
||||||
|
|
||||||
/* 侧边栏 */
|
|
||||||
--sidebar-width-collapsed: 80px;
|
|
||||||
--sidebar-width-expanded: 260px;
|
|
||||||
--sidebar-height: 80px;
|
|
||||||
|
|
||||||
/* 头部导航 */
|
|
||||||
--header-height: 108px;
|
|
||||||
|
|
||||||
/* 卡片 */
|
|
||||||
--card-padding: var(--spacing-2xl);
|
|
||||||
--card-border-radius: var(--border-radius-lg);
|
|
||||||
--card-shadow: var(--shadow-medium);
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#007ACC" d="M0 128v128h256V0H0z"></path><path fill="#FFF" d="m56.612 128.85l-.081 10.483h33.32v94.68h23.568v-94.68h33.321v-10.28c0-5.69-.122-10.444-.284-10.566c-.122-.162-20.4-.244-44.983-.203l-44.74.122l-.121 10.443Zm149.955-10.742c6.501 1.625 11.459 4.51 16.01 9.224c2.357 2.52 5.851 7.111 6.136 8.208c.08.325-11.053 7.802-17.798 11.988c-.244.162-1.22-.894-2.317-2.52c-3.291-4.795-6.745-6.867-12.028-7.233c-7.76-.528-12.759 3.535-12.718 10.321c0 1.992.284 3.17 1.097 4.795c1.707 3.536 4.876 5.649 14.832 9.956c18.326 7.883 26.168 13.084 31.045 20.48c5.445 8.249 6.664 21.415 2.966 31.208c-4.063 10.646-14.14 17.879-28.323 20.276c-4.388.772-14.79.65-19.504-.203c-10.28-1.828-20.033-6.908-26.047-13.572c-2.357-2.6-6.949-9.387-6.664-9.874c.122-.163 1.178-.813 2.356-1.504c1.138-.65 5.446-3.129 9.509-5.485l7.355-4.267l1.544 2.276c2.154 3.29 6.867 7.801 9.712 9.305c8.167 4.307 19.383 3.698 24.909-1.26c2.357-2.153 3.332-4.388 3.332-7.68c0-2.966-.366-4.266-1.91-6.501c-1.99-2.845-6.054-5.242-17.595-10.24c-13.206-5.69-18.895-9.224-24.096-14.832c-3.007-3.25-5.852-8.452-7.03-12.8c-.975-3.617-1.22-12.678-.447-16.335c2.723-12.76 12.353-21.659 26.25-24.3c4.51-.853 14.994-.528 19.424.569Z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2022",
|
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"module": "ESNext",
|
|
||||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
||||||
"types": ["vite/client"],
|
|
||||||
"skipLibCheck": true,
|
|
||||||
|
|
||||||
/* Bundler mode */
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"verbatimModuleSyntax": true,
|
|
||||||
"moduleDetection": "force",
|
|
||||||
"noEmit": true,
|
|
||||||
|
|
||||||
/* Linting */
|
|
||||||
"strict": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"erasableSyntaxOnly": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noUncheckedSideEffectImports": true
|
|
||||||
},
|
|
||||||
"include": ["src"]
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { defineConfig } from 'vite'
|
|
||||||
import vue from '@vitejs/plugin-vue'
|
|
||||||
import path from 'path'
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [vue()],
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@': path.resolve(__dirname, './src'),
|
|
||||||
'@utils': path.resolve(__dirname, '../utils')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
24
display/.gitignore
vendored
24
display/.gitignore
vendored
@@ -1,24 +0,0 @@
|
|||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
node_modules
|
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
|
||||||
.DS_Store
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>display</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script type="module" src="/src/main.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
1739
display/package-lock.json
generated
1739
display/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "display",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "tsc && vite build",
|
|
||||||
"preview": "vite preview"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@vitejs/plugin-vue": "^6.0.3",
|
|
||||||
"typescript": "~5.9.3",
|
|
||||||
"vite": "^7.2.4"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"element-plus": "^2.13.1",
|
|
||||||
"pinia": "^3.0.4"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,451 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="display-container" @keydown="handleKeydown" tabindex="0">
|
|
||||||
<!-- 快捷键提示 -->
|
|
||||||
<div class="shortcut-hints" v-if="!store.isRolling">
|
|
||||||
<div class="hint-item">
|
|
||||||
<span class="key">← →</span>
|
|
||||||
<span class="desc">切换轮次</span>
|
|
||||||
</div>
|
|
||||||
<div class="hint-item">
|
|
||||||
<span class="key">Space</span>
|
|
||||||
<span class="desc">开始/停止</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 当前轮次信息 -->
|
|
||||||
<div class="current-round-info" v-if="selectedRound">
|
|
||||||
<div class="round-name">{{ selectedRound.name }}</div>
|
|
||||||
<div class="prize-name">{{ getPrizeName(selectedRound.prizeId) }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 滚动模式 -->
|
|
||||||
<div v-if="store.displayMode === 'scroll'" class="scroll-mode">
|
|
||||||
<div class="scroll-list" :style="{ transform: `translateY(${scrollOffset}px)` }">
|
|
||||||
<div
|
|
||||||
v-for="(person, index) in displayList"
|
|
||||||
:key="person.id || index"
|
|
||||||
class="scroll-item"
|
|
||||||
:class="{ 'highlight': index === highlightIndex }"
|
|
||||||
>
|
|
||||||
{{ getParticipantName(person) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 抽奖结果模式 -->
|
|
||||||
<div v-else class="result-mode">
|
|
||||||
<div class="winners-list">
|
|
||||||
<div
|
|
||||||
v-for="(winner, index) in currentWinners"
|
|
||||||
:key="winner.id || index"
|
|
||||||
class="winner-item"
|
|
||||||
:style="{ animationDelay: `${index * 0.2}s` }"
|
|
||||||
>
|
|
||||||
<div class="winner-name">{{ getParticipantName(winner) }}</div>
|
|
||||||
<div class="winner-info" v-if="store.fields.length > 1">
|
|
||||||
{{ getParticipantDetails(winner) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 状态指示器 -->
|
|
||||||
<div class="status-indicator" :class="{ 'active': store.isRolling }">
|
|
||||||
{{ store.isRolling ? '抽奖中...' : '等待抽奖' }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
||||||
import { useLotteryStore } from './store'
|
|
||||||
|
|
||||||
const store = useLotteryStore()
|
|
||||||
|
|
||||||
const selectedRound = ref(null)
|
|
||||||
const scrollOffset = ref(0)
|
|
||||||
const highlightIndex = ref(0)
|
|
||||||
let scrollInterval = null
|
|
||||||
let highlightInterval = null
|
|
||||||
|
|
||||||
const displayList = computed(() => {
|
|
||||||
if (store.participants.length === 0) return ['暂无名单']
|
|
||||||
return store.participants
|
|
||||||
})
|
|
||||||
|
|
||||||
const getPrizeName = (prizeId) => {
|
|
||||||
const prize = store.prizes.find(p => p.id === prizeId)
|
|
||||||
return prize ? prize.name : ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const getParticipantName = (person) => {
|
|
||||||
if (typeof person === 'string') return person
|
|
||||||
|
|
||||||
// 首先尝试从配置的字段中查找 name 字段
|
|
||||||
const nameField = store.fields.find(f => f.key === 'name')
|
|
||||||
if (nameField && person[nameField.key]) {
|
|
||||||
return person[nameField.key]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有 name 字段,尝试从配置的字段中找第一个有值的
|
|
||||||
const firstFieldWithValue = store.fields.find(f => person[f.key])
|
|
||||||
if (firstFieldWithValue) {
|
|
||||||
return person[firstFieldWithValue.key]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果配置的字段都没有值,尝试从参与者对象的所有字段中找
|
|
||||||
const allKeys = Object.keys(person).filter(key => key !== 'id')
|
|
||||||
if (allKeys.length > 0) {
|
|
||||||
// 优先找包含 "name" 的字段
|
|
||||||
const nameKey = allKeys.find(key => key.toLowerCase().includes('name'))
|
|
||||||
if (nameKey) {
|
|
||||||
return person[nameKey]
|
|
||||||
}
|
|
||||||
// 否则返回第一个字段的值
|
|
||||||
return person[allKeys[0]]
|
|
||||||
}
|
|
||||||
|
|
||||||
return JSON.stringify(person)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getParticipantDetails = (person) => {
|
|
||||||
const details = []
|
|
||||||
|
|
||||||
// 首先显示配置的字段
|
|
||||||
store.fields.forEach(field => {
|
|
||||||
if (field.key !== 'name' && person[field.key]) {
|
|
||||||
details.push(`${field.label}: ${person[field.key]}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 如果配置的字段没有显示任何内容,尝试显示所有字段
|
|
||||||
if (details.length === 0) {
|
|
||||||
const allKeys = Object.keys(person).filter(key => key !== 'id')
|
|
||||||
allKeys.forEach(key => {
|
|
||||||
if (key.toLowerCase() !== 'name' && person[key]) {
|
|
||||||
details.push(`${key}: ${person[key]}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return details.join(' | ')
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentWinners = computed(() => {
|
|
||||||
if (!store.currentRound) return []
|
|
||||||
return store.winners
|
|
||||||
.filter(w => w.roundId === store.currentRound.id)
|
|
||||||
.sort((a, b) => new Date(a.time) - new Date(b.time))
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleKeydown = (e) => {
|
|
||||||
// 空格键:开始/停止抽奖
|
|
||||||
if (e.code === 'Space') {
|
|
||||||
e.preventDefault()
|
|
||||||
if (store.isRolling) {
|
|
||||||
handleStopLottery()
|
|
||||||
} else if (selectedRound.value && !selectedRound.value.completed) {
|
|
||||||
handleStartLottery()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 左右方向键:切换轮次
|
|
||||||
else if (e.code === 'ArrowLeft' || e.code === 'ArrowRight') {
|
|
||||||
e.preventDefault()
|
|
||||||
if (store.rounds.length === 0) return
|
|
||||||
|
|
||||||
let currentIndex = -1
|
|
||||||
if (selectedRound.value) {
|
|
||||||
currentIndex = store.rounds.findIndex(r => r.id === selectedRound.value.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.code === 'ArrowLeft') {
|
|
||||||
// 向左切换
|
|
||||||
currentIndex = currentIndex <= 0 ? store.rounds.length - 1 : currentIndex - 1
|
|
||||||
} else {
|
|
||||||
// 向右切换
|
|
||||||
currentIndex = currentIndex >= store.rounds.length - 1 ? 0 : currentIndex + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
selectRound(store.rounds[currentIndex])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectRound = (round) => {
|
|
||||||
selectedRound.value = round
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleStartLottery = () => {
|
|
||||||
if (!selectedRound.value) return
|
|
||||||
try {
|
|
||||||
store.startLottery(selectedRound.value)
|
|
||||||
startScroll()
|
|
||||||
} catch (error) {
|
|
||||||
alert(error.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleStopLottery = () => {
|
|
||||||
store.stopLottery()
|
|
||||||
stopScroll()
|
|
||||||
}
|
|
||||||
|
|
||||||
const startScroll = () => {
|
|
||||||
if (scrollInterval) clearInterval(scrollInterval)
|
|
||||||
if (highlightInterval) clearInterval(highlightInterval)
|
|
||||||
|
|
||||||
scrollInterval = setInterval(() => {
|
|
||||||
scrollOffset.value -= 2
|
|
||||||
if (Math.abs(scrollOffset.value) >= 60) {
|
|
||||||
scrollOffset.value = 0
|
|
||||||
}
|
|
||||||
}, 50)
|
|
||||||
|
|
||||||
highlightInterval = setInterval(() => {
|
|
||||||
highlightIndex.value = (highlightIndex.value + 1) % displayList.value.length
|
|
||||||
}, 300)
|
|
||||||
}
|
|
||||||
|
|
||||||
const stopScroll = () => {
|
|
||||||
if (scrollInterval) {
|
|
||||||
clearInterval(scrollInterval)
|
|
||||||
scrollInterval = null
|
|
||||||
}
|
|
||||||
if (highlightInterval) {
|
|
||||||
clearInterval(highlightInterval)
|
|
||||||
highlightInterval = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
// 自动聚焦以便接收键盘事件
|
|
||||||
document.querySelector('.display-container')?.focus()
|
|
||||||
|
|
||||||
if (store.displayMode === 'scroll' && store.isRolling) {
|
|
||||||
startScroll()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
stopScroll()
|
|
||||||
store.stopSync()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.display-container {
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-accent) 100%);
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
position: relative;
|
|
||||||
outline: none;
|
|
||||||
font-family: var(--font-family-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.display-container::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-image: v-bind('store.backgroundImage ? `url(${store.backgroundImage})` : "none"');
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.display-container::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.3);
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.display-container > * {
|
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 快捷键提示 */
|
|
||||||
.shortcut-hints {
|
|
||||||
position: fixed;
|
|
||||||
top: var(--spacing-xl);
|
|
||||||
left: var(--spacing-xl);
|
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-lg);
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hint-item {
|
|
||||||
background: rgba(255, 255, 255, 0.15);
|
|
||||||
border-radius: var(--border-radius-md);
|
|
||||||
padding: var(--spacing-md) var(--spacing-lg);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--spacing-md);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.key {
|
|
||||||
background: var(--color-secondary);
|
|
||||||
color: var(--color-text-white);
|
|
||||||
padding: var(--spacing-sm) var(--spacing-md);
|
|
||||||
border-radius: var(--border-radius-sm);
|
|
||||||
font-weight: var(--font-weight-semibold);
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
min-width: 30px;
|
|
||||||
text-align: center;
|
|
||||||
box-shadow: 0 2px 8px rgba(var(--color-secondary-rgb), 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.desc {
|
|
||||||
color: var(--color-text-white);
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
font-family: var(--font-family-primary);
|
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 当前轮次信息 */
|
|
||||||
.current-round-info {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: var(--spacing-4xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.round-name {
|
|
||||||
font-size: var(--font-size-4xl);
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
color: var(--color-text-white);
|
|
||||||
margin-bottom: var(--spacing-lg);
|
|
||||||
text-shadow: 0 0 20px rgba(255, 255, 255, 0.5);
|
|
||||||
font-family: var(--font-family-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.prize-name {
|
|
||||||
font-size: 64px;
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
color: var(--color-secondary);
|
|
||||||
text-shadow: 0 0 30px rgba(var(--color-secondary-rgb), 0.8);
|
|
||||||
font-family: var(--font-family-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 滚动模式 */
|
|
||||||
.scroll-mode {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroll-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
transition: transform 0.1s linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroll-item {
|
|
||||||
font-size: var(--font-size-4xl);
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
color: rgba(255, 255, 255, 0.6);
|
|
||||||
padding: var(--spacing-md) var(--spacing-3xl);
|
|
||||||
text-align: center;
|
|
||||||
white-space: nowrap;
|
|
||||||
transition: var(--transition-normal);
|
|
||||||
font-family: var(--font-family-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroll-item.highlight {
|
|
||||||
color: var(--color-text-white);
|
|
||||||
font-size: 72px;
|
|
||||||
text-shadow: 0 0 30px rgba(255, 255, 255, 0.8);
|
|
||||||
transform: scale(1.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 结果模式 */
|
|
||||||
.result-mode {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.winners-list {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 30px;
|
|
||||||
justify-content: center;
|
|
||||||
max-width: 1200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.winner-item {
|
|
||||||
font-size: 56px;
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
color: var(--color-text-white);
|
|
||||||
padding: var(--spacing-xl) var(--spacing-4xl);
|
|
||||||
background: rgba(255, 255, 255, 0.15);
|
|
||||||
border-radius: var(--border-radius-xl);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
|
||||||
animation: fadeInUp 0.6s ease forwards;
|
|
||||||
opacity: 0;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
||||||
font-family: var(--font-family-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeInUp {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(30px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 状态指示器 */
|
|
||||||
.status-indicator {
|
|
||||||
position: fixed;
|
|
||||||
bottom: var(--spacing-4xl);
|
|
||||||
right: var(--spacing-4xl);
|
|
||||||
padding: var(--spacing-lg) var(--spacing-3xl);
|
|
||||||
background: rgba(255, 255, 255, 0.15);
|
|
||||||
border-radius: 50px;
|
|
||||||
color: var(--color-text-white);
|
|
||||||
font-size: var(--font-size-xl);
|
|
||||||
font-weight: var(--font-weight-semibold);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
transition: var(--transition-normal);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
||||||
font-family: var(--font-family-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-indicator.active {
|
|
||||||
background: rgba(var(--color-secondary-rgb), 0.3);
|
|
||||||
animation: pulse 1.5s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0%, 100% {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
export function setupCounter(element: HTMLButtonElement) {
|
|
||||||
let counter = 0
|
|
||||||
const setCounter = (count: number) => {
|
|
||||||
counter = count
|
|
||||||
element.innerHTML = `count is ${counter}`
|
|
||||||
}
|
|
||||||
element.addEventListener('click', () => setCounter(counter + 1))
|
|
||||||
setCounter(0)
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { createApp } from 'vue'
|
|
||||||
import { createPinia } from 'pinia'
|
|
||||||
import App from './App.vue'
|
|
||||||
import './styles/variables.css'
|
|
||||||
import './style.css'
|
|
||||||
|
|
||||||
const app = createApp(App)
|
|
||||||
app.use(createPinia())
|
|
||||||
app.mount('#app')
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import './style.css'
|
|
||||||
import typescriptLogo from './typescript.svg'
|
|
||||||
import viteLogo from '/vite.svg'
|
|
||||||
import { setupCounter } from './counter.ts'
|
|
||||||
|
|
||||||
document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
|
|
||||||
<div>
|
|
||||||
<a href="https://vite.dev" target="_blank">
|
|
||||||
<img src="${viteLogo}" class="logo" alt="Vite logo" />
|
|
||||||
</a>
|
|
||||||
<a href="https://www.typescriptlang.org/" target="_blank">
|
|
||||||
<img src="${typescriptLogo}" class="logo vanilla" alt="TypeScript logo" />
|
|
||||||
</a>
|
|
||||||
<h1>Vite + TypeScript</h1>
|
|
||||||
<div class="card">
|
|
||||||
<button id="counter" type="button"></button>
|
|
||||||
</div>
|
|
||||||
<p class="read-the-docs">
|
|
||||||
Click on the Vite and TypeScript logos to learn more
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
|
|
||||||
setupCounter(document.querySelector<HTMLButtonElement>('#counter')!)
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
import { defineStore } from 'pinia'
|
|
||||||
import { ref, watch } from 'vue'
|
|
||||||
import { indexedDB } from '@utils/indexedDB'
|
|
||||||
|
|
||||||
export const useLotteryStore = defineStore('lottery', () => {
|
|
||||||
// 初始化数据
|
|
||||||
const fields = ref([])
|
|
||||||
const participants = ref([])
|
|
||||||
const prizes = ref([])
|
|
||||||
const rounds = ref([])
|
|
||||||
const winners = ref([])
|
|
||||||
const isRolling = ref(false)
|
|
||||||
const currentRound = ref(null)
|
|
||||||
const displayMode = ref('scroll')
|
|
||||||
const backgroundImage = ref('')
|
|
||||||
const isInitialized = ref(false)
|
|
||||||
|
|
||||||
// 从IndexedDB初始化数据
|
|
||||||
const initFromIndexedDB = async () => {
|
|
||||||
if (isInitialized.value) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log('Display: Initializing from IndexedDB...')
|
|
||||||
const data = await indexedDB.getAll()
|
|
||||||
console.log('Display: Retrieved data keys:', Object.keys(data))
|
|
||||||
|
|
||||||
fields.value = data.lottery_fields || []
|
|
||||||
participants.value = data.lottery_participants || []
|
|
||||||
prizes.value = data.lottery_prizes || []
|
|
||||||
rounds.value = data.lottery_rounds || []
|
|
||||||
winners.value = data.lottery_winners || []
|
|
||||||
isRolling.value = data.lottery_isRolling === 'true'
|
|
||||||
currentRound.value = data.lottery_currentRound || null
|
|
||||||
displayMode.value = data.lottery_displayMode || 'scroll'
|
|
||||||
backgroundImage.value = data.lottery_backgroundImage || ''
|
|
||||||
|
|
||||||
console.log('Display: Initialized background image:', backgroundImage.value ? 'Yes' : 'No')
|
|
||||||
console.log('Display: Initialization completed')
|
|
||||||
|
|
||||||
isInitialized.value = true
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Display: Failed to initialize from IndexedDB:', error)
|
|
||||||
// 使用默认值
|
|
||||||
fields.value = [{key: 'name', label: '姓名', required: true}]
|
|
||||||
isInitialized.value = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化
|
|
||||||
initFromIndexedDB()
|
|
||||||
|
|
||||||
// 监听localStorage变化来触发数据同步
|
|
||||||
const handleStorageChange = (e) => {
|
|
||||||
if (e.key === 'lottery_data_changed') {
|
|
||||||
console.log('Display: Storage change detected, reloading data...')
|
|
||||||
initFromIndexedDB()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.addEventListener('storage', handleStorageChange)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 轮询同步IndexedDB数据(作为备用)
|
|
||||||
let syncInterval = null
|
|
||||||
const startSync = () => {
|
|
||||||
if (syncInterval) return
|
|
||||||
|
|
||||||
console.log('Display: Starting sync interval...')
|
|
||||||
|
|
||||||
syncInterval = setInterval(async () => {
|
|
||||||
try {
|
|
||||||
const data = await indexedDB.getAll()
|
|
||||||
|
|
||||||
// 只更新变化的值,避免不必要的重渲染
|
|
||||||
if (JSON.stringify(data.lottery_fields) !== JSON.stringify(fields.value)) {
|
|
||||||
fields.value = data.lottery_fields || []
|
|
||||||
}
|
|
||||||
if (JSON.stringify(data.lottery_participants) !== JSON.stringify(participants.value)) {
|
|
||||||
participants.value = data.lottery_participants || []
|
|
||||||
}
|
|
||||||
if (JSON.stringify(data.lottery_prizes) !== JSON.stringify(prizes.value)) {
|
|
||||||
prizes.value = data.lottery_prizes || []
|
|
||||||
}
|
|
||||||
if (JSON.stringify(data.lottery_rounds) !== JSON.stringify(rounds.value)) {
|
|
||||||
rounds.value = data.lottery_rounds || []
|
|
||||||
}
|
|
||||||
if (JSON.stringify(data.lottery_winners) !== JSON.stringify(winners.value)) {
|
|
||||||
winners.value = data.lottery_winners || []
|
|
||||||
}
|
|
||||||
if (data.lottery_isRolling !== isRolling.value) {
|
|
||||||
isRolling.value = data.lottery_isRolling === 'true'
|
|
||||||
}
|
|
||||||
if (JSON.stringify(data.lottery_currentRound) !== JSON.stringify(currentRound.value)) {
|
|
||||||
currentRound.value = data.lottery_currentRound || null
|
|
||||||
}
|
|
||||||
if (data.lottery_displayMode !== displayMode.value) {
|
|
||||||
displayMode.value = data.lottery_displayMode || 'scroll'
|
|
||||||
}
|
|
||||||
if (data.lottery_backgroundImage !== backgroundImage.value) {
|
|
||||||
console.log('Display: Background image changed from', backgroundImage.value ? 'Yes' : 'No', 'to', data.lottery_backgroundImage ? 'Yes' : 'No')
|
|
||||||
backgroundImage.value = data.lottery_backgroundImage || ''
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Display: Failed to sync data from IndexedDB:', error)
|
|
||||||
}
|
|
||||||
}, 1000) // 每秒同步一次
|
|
||||||
}
|
|
||||||
|
|
||||||
const stopSync = () => {
|
|
||||||
if (syncInterval) {
|
|
||||||
clearInterval(syncInterval)
|
|
||||||
syncInterval = null
|
|
||||||
}
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.removeEventListener('storage', handleStorageChange)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 启动同步
|
|
||||||
startSync()
|
|
||||||
|
|
||||||
// ============ 只读方法 ============
|
|
||||||
const getPrizeForRound = (roundId) => {
|
|
||||||
const round = rounds.value.find(r => r.id === roundId)
|
|
||||||
if (!round) return null
|
|
||||||
return prizes.value.find(p => p.id === round.prizeId)
|
|
||||||
}
|
|
||||||
|
|
||||||
const switchDisplayMode = (mode) => {
|
|
||||||
displayMode.value = mode
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
// 状态
|
|
||||||
fields,
|
|
||||||
participants,
|
|
||||||
prizes,
|
|
||||||
rounds,
|
|
||||||
winners,
|
|
||||||
isRolling,
|
|
||||||
currentRound,
|
|
||||||
displayMode,
|
|
||||||
backgroundImage,
|
|
||||||
isInitialized,
|
|
||||||
|
|
||||||
// 只读方法
|
|
||||||
getPrizeForRound,
|
|
||||||
switchDisplayMode,
|
|
||||||
startSync,
|
|
||||||
stopSync
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
:root {
|
|
||||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
||||||
line-height: 1.5;
|
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
color-scheme: light dark;
|
|
||||||
color: rgba(255, 255, 255, 0.87);
|
|
||||||
background-color: #242424;
|
|
||||||
|
|
||||||
font-synthesis: none;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #646cff;
|
|
||||||
text-decoration: inherit;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #535bf2;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
:root {
|
|
||||||
/* 颜色系统 - 参考项目配色 */
|
|
||||||
--color-primary: rgb(29, 17, 17);
|
|
||||||
--color-primary-rgb: 29, 17, 17;
|
|
||||||
|
|
||||||
--color-secondary: rgb(0, 165, 141);
|
|
||||||
--color-secondary-rgb: 0, 165, 141;
|
|
||||||
|
|
||||||
--color-accent: rgb(34, 37, 121);
|
|
||||||
--color-accent-rgb: 34, 37, 121;
|
|
||||||
|
|
||||||
--color-success: rgb(0, 165, 141);
|
|
||||||
--color-warning: rgb(255, 193, 7);
|
|
||||||
--color-danger: rgb(220, 53, 69);
|
|
||||||
--color-info: rgb(23, 162, 184);
|
|
||||||
|
|
||||||
--color-text-primary: rgb(51, 51, 51);
|
|
||||||
--color-text-secondary: rgb(108, 117, 125);
|
|
||||||
--color-text-light: rgb(173, 181, 189);
|
|
||||||
--color-text-white: rgb(255, 255, 255);
|
|
||||||
|
|
||||||
--color-background: rgb(248, 249, 250);
|
|
||||||
--color-border: rgb(222, 226, 230);
|
|
||||||
--color-border-light: rgb(233, 236, 239);
|
|
||||||
|
|
||||||
/* 字体系统 */
|
|
||||||
--font-family-primary: 'Poppins', 'Noto Sans SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
||||||
--font-family-secondary: 'Montserrat', 'Noto Sans SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
||||||
|
|
||||||
--font-size-xs: 12px;
|
|
||||||
--font-size-sm: 14px;
|
|
||||||
--font-size-base: 16px;
|
|
||||||
--font-size-lg: 18px;
|
|
||||||
--font-size-xl: 20px;
|
|
||||||
--font-size-2xl: 24px;
|
|
||||||
--font-size-3xl: 32px;
|
|
||||||
--font-size-4xl: 48px;
|
|
||||||
|
|
||||||
--font-weight-light: 300;
|
|
||||||
--font-weight-normal: 400;
|
|
||||||
--font-weight-medium: 500;
|
|
||||||
--font-weight-semibold: 600;
|
|
||||||
--font-weight-bold: 700;
|
|
||||||
|
|
||||||
/* 间距系统 */
|
|
||||||
--spacing-xs: 4px;
|
|
||||||
--spacing-sm: 8px;
|
|
||||||
--spacing-md: 12px;
|
|
||||||
--spacing-lg: 16px;
|
|
||||||
--spacing-xl: 20px;
|
|
||||||
--spacing-2xl: 24px;
|
|
||||||
--spacing-3xl: 32px;
|
|
||||||
--spacing-4xl: 40px;
|
|
||||||
|
|
||||||
/* 圆角系统 */
|
|
||||||
--border-radius-sm: 4px;
|
|
||||||
--border-radius-md: 8px;
|
|
||||||
--border-radius-lg: 12px;
|
|
||||||
--border-radius-xl: 16px;
|
|
||||||
--border-radius-full: 50%;
|
|
||||||
|
|
||||||
/* 阴影系统 */
|
|
||||||
--shadow-small: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
||||||
--shadow-medium: 0 4px 16px rgba(0, 0, 0, 0.12);
|
|
||||||
--shadow-large: 0 8px 32px rgba(0, 0, 0, 0.16);
|
|
||||||
--shadow-button: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
||||||
|
|
||||||
/* 过渡动画 */
|
|
||||||
--transition-fast: 0.15s ease;
|
|
||||||
--transition-normal: 0.3s ease;
|
|
||||||
--transition-slow: 0.5s ease;
|
|
||||||
--transition-color: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease;
|
|
||||||
--transition-background: background-color 0.3s ease;
|
|
||||||
--transition-border: border-color 0.3s ease;
|
|
||||||
--transition-box-shadow: box-shadow 0.3s ease;
|
|
||||||
--transition-transform: transform 0.3s ease;
|
|
||||||
|
|
||||||
/* 行高 */
|
|
||||||
--line-height-tight: 1.2;
|
|
||||||
--line-height-normal: 1.5;
|
|
||||||
--line-height-relaxed: 1.75;
|
|
||||||
|
|
||||||
/* 布局 */
|
|
||||||
--header-height: 64px;
|
|
||||||
--sidebar-height: 64px;
|
|
||||||
--button-height-sm: 32px;
|
|
||||||
--button-height-md: 40px;
|
|
||||||
--button-height-lg: 48px;
|
|
||||||
|
|
||||||
/* 卡片 */
|
|
||||||
--card-border-radius: 12px;
|
|
||||||
--card-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#007ACC" d="M0 128v128h256V0H0z"></path><path fill="#FFF" d="m56.612 128.85l-.081 10.483h33.32v94.68h23.568v-94.68h33.321v-10.28c0-5.69-.122-10.444-.284-10.566c-.122-.162-20.4-.244-44.983-.203l-44.74.122l-.121 10.443Zm149.955-10.742c6.501 1.625 11.459 4.51 16.01 9.224c2.357 2.52 5.851 7.111 6.136 8.208c.08.325-11.053 7.802-17.798 11.988c-.244.162-1.22-.894-2.317-2.52c-3.291-4.795-6.745-6.867-12.028-7.233c-7.76-.528-12.759 3.535-12.718 10.321c0 1.992.284 3.17 1.097 4.795c1.707 3.536 4.876 5.649 14.832 9.956c18.326 7.883 26.168 13.084 31.045 20.48c5.445 8.249 6.664 21.415 2.966 31.208c-4.063 10.646-14.14 17.879-28.323 20.276c-4.388.772-14.79.65-19.504-.203c-10.28-1.828-20.033-6.908-26.047-13.572c-2.357-2.6-6.949-9.387-6.664-9.874c.122-.163 1.178-.813 2.356-1.504c1.138-.65 5.446-3.129 9.509-5.485l7.355-4.267l1.544 2.276c2.154 3.29 6.867 7.801 9.712 9.305c8.167 4.307 19.383 3.698 24.909-1.26c2.357-2.153 3.332-4.388 3.332-7.68c0-2.966-.366-4.266-1.91-6.501c-1.99-2.845-6.054-5.242-17.595-10.24c-13.206-5.69-18.895-9.224-24.096-14.832c-3.007-3.25-5.852-8.452-7.03-12.8c-.975-3.617-1.22-12.678-.447-16.335c2.723-12.76 12.353-21.659 26.25-24.3c4.51-.853 14.994-.528 19.424.569Z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2022",
|
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"module": "ESNext",
|
|
||||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
||||||
"types": ["vite/client"],
|
|
||||||
"skipLibCheck": true,
|
|
||||||
|
|
||||||
/* Bundler mode */
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"verbatimModuleSyntax": true,
|
|
||||||
"moduleDetection": "force",
|
|
||||||
"noEmit": true,
|
|
||||||
|
|
||||||
/* Linting */
|
|
||||||
"strict": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"erasableSyntaxOnly": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noUncheckedSideEffectImports": true
|
|
||||||
},
|
|
||||||
"include": ["src"]
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { defineConfig } from 'vite'
|
|
||||||
import vue from '@vitejs/plugin-vue'
|
|
||||||
import path from 'path'
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [vue()],
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@': path.resolve(__dirname, './src'),
|
|
||||||
'@utils': path.resolve(__dirname, '../utils')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -9,7 +9,7 @@ const router = createRouter({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin',
|
path: '/admin',
|
||||||
component: () => import('@/views/AdminLayout.vue'),
|
component: () => import('@/views/Admin.vue'),
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'participants',
|
path: 'participants',
|
||||||
|
|||||||
@@ -112,6 +112,11 @@ export const useLotteryStore = defineStore('lottery', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateFields = async (newFields) => {
|
||||||
|
fields.value = newFields
|
||||||
|
await saveData('lottery_fields', fields.value)
|
||||||
|
}
|
||||||
|
|
||||||
// ============ 参与者管理 ============
|
// ============ 参与者管理 ============
|
||||||
const addParticipant = async (participant) => {
|
const addParticipant = async (participant) => {
|
||||||
participants.value.push(participant)
|
participants.value.push(participant)
|
||||||
@@ -454,6 +459,7 @@ export const useLotteryStore = defineStore('lottery', () => {
|
|||||||
addField,
|
addField,
|
||||||
updateField,
|
updateField,
|
||||||
removeField,
|
removeField,
|
||||||
|
updateFields,
|
||||||
|
|
||||||
// 参与者管理
|
// 参与者管理
|
||||||
addParticipant,
|
addParticipant,
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
# Views directory
|
|
||||||
1156
src/views/Admin.vue
1156
src/views/Admin.vue
File diff suppressed because it is too large
Load Diff
@@ -1,412 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="admin-layout">
|
|
||||||
<el-container>
|
|
||||||
<el-header>
|
|
||||||
<div class="header-content">
|
|
||||||
<h1>抽奖管理系统</h1>
|
|
||||||
<div style="display: flex; gap: 10px">
|
|
||||||
<el-button type="info" @click="openDisplaySettings">
|
|
||||||
<el-icon><Setting /></el-icon>
|
|
||||||
显示设置
|
|
||||||
</el-button>
|
|
||||||
<el-button type="info" @click="showBackgroundDialog = true">
|
|
||||||
<el-icon><Picture /></el-icon>
|
|
||||||
背景设置
|
|
||||||
</el-button>
|
|
||||||
<el-button type="info" @click="showShortcutGuide = true">
|
|
||||||
<el-icon><QuestionFilled /></el-icon>
|
|
||||||
快捷键指南
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-header>
|
|
||||||
|
|
||||||
<el-container>
|
|
||||||
<el-aside width="200px">
|
|
||||||
<el-menu
|
|
||||||
:default-active="activeMenu"
|
|
||||||
router
|
|
||||||
class="admin-menu"
|
|
||||||
>
|
|
||||||
<el-menu-item index="/admin/participants">
|
|
||||||
<el-icon><User /></el-icon>
|
|
||||||
<span>名单管理</span>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="/admin/prizes">
|
|
||||||
<el-icon><Present /></el-icon>
|
|
||||||
<span>奖品管理</span>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="/admin/rounds">
|
|
||||||
<el-icon><List /></el-icon>
|
|
||||||
<span>轮次管理</span>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="/admin/winners">
|
|
||||||
<el-icon><Trophy /></el-icon>
|
|
||||||
<span>中奖记录</span>
|
|
||||||
</el-menu-item>
|
|
||||||
</el-menu>
|
|
||||||
</el-aside>
|
|
||||||
|
|
||||||
<el-main>
|
|
||||||
<router-view></router-view>
|
|
||||||
</el-main>
|
|
||||||
</el-container>
|
|
||||||
</el-container>
|
|
||||||
|
|
||||||
<!-- 快捷键指南对话框 -->
|
|
||||||
<el-dialog v-model="showShortcutGuide" title="大屏端快捷键指南" width="500px">
|
|
||||||
<div class="shortcut-guide">
|
|
||||||
<div class="shortcut-item">
|
|
||||||
<span class="shortcut-key">← →</span>
|
|
||||||
<span class="shortcut-desc">切换轮次(左右方向键)</span>
|
|
||||||
</div>
|
|
||||||
<div class="shortcut-item">
|
|
||||||
<span class="shortcut-key">Space</span>
|
|
||||||
<span class="shortcut-desc">开始/停止抽奖</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<template #footer>
|
|
||||||
<el-button type="primary" @click="showShortcutGuide = false">知道了</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<!-- 背景图片配置对话框 -->
|
|
||||||
<el-dialog v-model="showBackgroundDialog" title="大屏端背景图片" width="500px">
|
|
||||||
<div class="background-config">
|
|
||||||
<div v-if="store.backgroundImage" class="current-background">
|
|
||||||
<div class="background-label">当前背景图片:</div>
|
|
||||||
<img :src="store.backgroundImage" class="background-preview" />
|
|
||||||
<el-button type="danger" @click="clearBackgroundImage" style="margin-top: 10px">
|
|
||||||
清除背景
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="upload-section">
|
|
||||||
<div class="background-label">上传新背景:</div>
|
|
||||||
<el-upload
|
|
||||||
:auto-upload="false"
|
|
||||||
:on-change="handleBackgroundChange"
|
|
||||||
:limit="1"
|
|
||||||
accept="image/*"
|
|
||||||
drag
|
|
||||||
>
|
|
||||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
|
||||||
<div class="el-upload__text">
|
|
||||||
拖拽图片到此处或 <em>点击上传</em>
|
|
||||||
</div>
|
|
||||||
<template #tip>
|
|
||||||
<div class="el-upload__tip">
|
|
||||||
支持 JPG、PNG、GIF 等图片格式,最大支持10MB
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-upload>
|
|
||||||
|
|
||||||
<div v-if="backgroundImagePreview" class="preview-section">
|
|
||||||
<div class="background-label">预览:</div>
|
|
||||||
<img :src="backgroundImagePreview" class="background-preview" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="showBackgroundDialog = false">取消</el-button>
|
|
||||||
<el-button type="primary" @click="saveBackgroundImage" :disabled="!backgroundImagePreview">保存</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<!-- 显示设置对话框 -->
|
|
||||||
<el-dialog v-model="showDisplaySettingsDialog" title="大屏端显示设置" width="400px">
|
|
||||||
<el-form label-width="120px">
|
|
||||||
<el-form-item label="每行显示人数">
|
|
||||||
<el-input-number
|
|
||||||
v-model="tempColumnsPerRow"
|
|
||||||
:min="1"
|
|
||||||
:max="10"
|
|
||||||
:step="1"
|
|
||||||
/>
|
|
||||||
<div style="margin-top: 8px; color: var(--color-text-light); font-size: 12px;">
|
|
||||||
设置大屏端名单每行显示的人数,建议值:2-5
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="showDisplaySettingsDialog = false">取消</el-button>
|
|
||||||
<el-button type="primary" @click="saveDisplaySettings">保存</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed, onMounted } from 'vue'
|
|
||||||
import { useRoute } from 'vue-router'
|
|
||||||
import { useLotteryStore } from '../store'
|
|
||||||
import { User, Present, List, Trophy, Picture, QuestionFilled, UploadFilled, Setting } from '@element-plus/icons-vue'
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const store = useLotteryStore()
|
|
||||||
|
|
||||||
const activeMenu = computed(() => route.path)
|
|
||||||
|
|
||||||
// 初始化store
|
|
||||||
onMounted(async () => {
|
|
||||||
await store.initialize()
|
|
||||||
})
|
|
||||||
|
|
||||||
// 快捷键指南
|
|
||||||
const showShortcutGuide = ref(false)
|
|
||||||
|
|
||||||
// 显示设置
|
|
||||||
const showDisplaySettingsDialog = ref(false)
|
|
||||||
const tempColumnsPerRow = ref(3)
|
|
||||||
|
|
||||||
const openDisplaySettings = () => {
|
|
||||||
tempColumnsPerRow.value = store.columnsPerRow
|
|
||||||
showDisplaySettingsDialog.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveDisplaySettings = async () => {
|
|
||||||
try {
|
|
||||||
await store.setColumnsPerRow(tempColumnsPerRow.value)
|
|
||||||
showDisplaySettingsDialog.value = false
|
|
||||||
ElMessage.success('显示设置已保存')
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error('保存显示设置失败:' + error.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 背景图片配置
|
|
||||||
const showBackgroundDialog = ref(false)
|
|
||||||
const backgroundImageFile = ref(null)
|
|
||||||
const backgroundImagePreview = ref('')
|
|
||||||
|
|
||||||
const handleBackgroundChange = (file) => {
|
|
||||||
const maxSize = 10 * 1024 * 1024
|
|
||||||
if (file.raw.size > maxSize) {
|
|
||||||
ElMessage.error('图片大小不能超过10MB,请使用较小的图片')
|
|
||||||
backgroundImageFile.value = null
|
|
||||||
backgroundImagePreview.value = ''
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
backgroundImageFile.value = file.raw
|
|
||||||
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.onload = (e) => {
|
|
||||||
backgroundImagePreview.value = e.target.result
|
|
||||||
}
|
|
||||||
reader.readAsDataURL(file.raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveBackgroundImage = async () => {
|
|
||||||
if (backgroundImagePreview.value) {
|
|
||||||
try {
|
|
||||||
await store.setBackgroundImage(backgroundImagePreview.value)
|
|
||||||
showBackgroundDialog.value = false
|
|
||||||
backgroundImagePreview.value = ''
|
|
||||||
backgroundImageFile.value = null
|
|
||||||
ElMessage.success('背景图片已设置')
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error('保存背景图片失败:' + error.message)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ElMessage.warning('请选择图片')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearBackgroundImage = async () => {
|
|
||||||
try {
|
|
||||||
await store.clearBackgroundImage()
|
|
||||||
ElMessage.success('背景图片已清除')
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error('清除背景图片失败:' + error.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.admin-layout {
|
|
||||||
min-height: 100vh;
|
|
||||||
background: var(--color-background);
|
|
||||||
font-family: var(--font-family-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-header {
|
|
||||||
background: var(--color-background);
|
|
||||||
border-bottom: 1px solid var(--color-border);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0 var(--spacing-2xl);
|
|
||||||
box-shadow: var(--shadow-small);
|
|
||||||
position: relative;
|
|
||||||
z-index: 10;
|
|
||||||
height: var(--header-height);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-header::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 2px;
|
|
||||||
background: var(--color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-header h1 {
|
|
||||||
margin: 0;
|
|
||||||
font-family: var(--font-family-secondary);
|
|
||||||
font-size: var(--font-size-2xl);
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-header .el-button {
|
|
||||||
background: transparent;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
transition: var(--transition-color), var(--transition-background);
|
|
||||||
font-family: var(--font-family-primary);
|
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
border-radius: var(--border-radius-md);
|
|
||||||
padding: var(--spacing-md) var(--spacing-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-header .el-button:hover {
|
|
||||||
background: rgba(var(--color-primary-rgb), 0.1);
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
color: var(--color-primary);
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-aside {
|
|
||||||
background: var(--color-background);
|
|
||||||
border-right: 1px solid var(--color-border);
|
|
||||||
padding: var(--spacing-lg) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-menu {
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-menu .el-menu-item {
|
|
||||||
margin: var(--spacing-sm) var(--spacing-lg);
|
|
||||||
border-radius: var(--border-radius-md);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
font-family: var(--font-family-primary);
|
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-menu .el-menu-item:hover {
|
|
||||||
background: rgba(var(--color-primary-rgb), 0.1);
|
|
||||||
color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-menu .el-menu-item.is-active {
|
|
||||||
background: var(--color-primary);
|
|
||||||
color: var(--color-text-white);
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-menu .el-menu-item .el-icon {
|
|
||||||
margin-right: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-main {
|
|
||||||
padding: var(--spacing-3xl);
|
|
||||||
max-width: 1400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 快捷键指南样式 */
|
|
||||||
.shortcut-guide {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-around;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: var(--spacing-3xl);
|
|
||||||
padding: var(--spacing-2xl) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shortcut-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: var(--spacing-xl);
|
|
||||||
min-width: 140px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shortcut-key {
|
|
||||||
background: var(--color-secondary);
|
|
||||||
color: var(--color-text-white);
|
|
||||||
font-family: var(--font-family-secondary);
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
font-size: var(--font-size-3xl);
|
|
||||||
margin-bottom: var(--spacing-md);
|
|
||||||
box-shadow: 0 4px 12px rgba(var(--color-secondary-rgb), 0.3);
|
|
||||||
padding: var(--spacing-md) var(--spacing-lg);
|
|
||||||
border-radius: var(--border-radius-lg);
|
|
||||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
|
||||||
min-width: 80px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shortcut-desc {
|
|
||||||
color: var(--color-text-light);
|
|
||||||
font-size: var(--font-size-base);
|
|
||||||
text-align: center;
|
|
||||||
line-height: var(--line-height-relaxed);
|
|
||||||
font-family: var(--font-family-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 背景图片配置样式 */
|
|
||||||
.background-config {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-background {
|
|
||||||
padding: var(--spacing-lg);
|
|
||||||
background: var(--color-border-light);
|
|
||||||
border-radius: var(--border-radius-md);
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-section {
|
|
||||||
padding: var(--spacing-lg);
|
|
||||||
background: var(--color-border-light);
|
|
||||||
border-radius: var(--border-radius-md);
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.background-label {
|
|
||||||
font-size: var(--font-size-base);
|
|
||||||
font-weight: var(--font-weight-semibold);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
margin-bottom: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.background-preview {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 400px;
|
|
||||||
max-height: 250px;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: var(--border-radius-md);
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-section {
|
|
||||||
margin-top: var(--spacing-lg);
|
|
||||||
padding-top: var(--spacing-lg);
|
|
||||||
border-top: 1px solid var(--color-border);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -4,18 +4,10 @@
|
|||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span>名单管理</span>
|
<span>名单管理</span>
|
||||||
<div>
|
<div>
|
||||||
<el-button type="info" size="small" @click="showFieldConfig">
|
<el-button type="info" size="small" @click="showFieldConfig"> 字段配置 </el-button>
|
||||||
字段配置
|
<el-button type="primary" size="small" @click="showImportDialog = true"> 导入 </el-button>
|
||||||
</el-button>
|
<el-button type="success" size="small" @click="exportParticipants"> 导出 </el-button>
|
||||||
<el-button type="primary" size="small" @click="showImportDialog = true">
|
<el-button type="danger" size="small" @click="clearParticipants"> 清空 </el-button>
|
||||||
导入
|
|
||||||
</el-button>
|
|
||||||
<el-button type="success" size="small" @click="exportParticipants">
|
|
||||||
导出
|
|
||||||
</el-button>
|
|
||||||
<el-button type="danger" size="small" @click="clearParticipants">
|
|
||||||
清空
|
|
||||||
</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -23,21 +15,11 @@
|
|||||||
<!-- 单个添加表单 -->
|
<!-- 单个添加表单 -->
|
||||||
<div class="single-add-form">
|
<div class="single-add-form">
|
||||||
<el-form :model="newParticipantData" label-width="80px" size="small">
|
<el-form :model="newParticipantData" label-width="80px" size="small">
|
||||||
<el-form-item
|
<el-form-item v-for="field in store.fields" :key="field.id" :label="field.label" :required="field.required">
|
||||||
v-for="field in store.fields"
|
<el-input v-model="newParticipantData[field.key]" :placeholder="`输入${field.label}`" />
|
||||||
:key="field.id"
|
|
||||||
:label="field.label"
|
|
||||||
:required="field.required"
|
|
||||||
>
|
|
||||||
<el-input
|
|
||||||
v-model="newParticipantData[field.key]"
|
|
||||||
:placeholder="`输入${field.label}`"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" @click="addParticipant" style="width: 100%">
|
<el-button type="primary" @click="addParticipant" style="width: 100%"> 添加 </el-button>
|
||||||
添加
|
|
||||||
</el-button>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
@@ -45,39 +27,17 @@
|
|||||||
<!-- 参与者表格 -->
|
<!-- 参与者表格 -->
|
||||||
<div class="participant-table">
|
<div class="participant-table">
|
||||||
<el-table :data="store.participants" style="width: 100%" stripe max-height="400">
|
<el-table :data="store.participants" style="width: 100%" stripe max-height="400">
|
||||||
<el-table-column
|
<el-table-column v-for="field in store.fields" :key="field.id" :prop="field.key" :label="field.label" :min-width="120" show-overflow-tooltip />
|
||||||
v-for="field in store.fields"
|
|
||||||
:key="field.id"
|
|
||||||
:prop="field.key"
|
|
||||||
:label="field.label"
|
|
||||||
:min-width="120"
|
|
||||||
show-overflow-tooltip
|
|
||||||
/>
|
|
||||||
<el-table-column label="操作" fixed="right" width="150" align="center">
|
<el-table-column label="操作" fixed="right" width="150" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button
|
<el-button type="primary" size="small" @click="showParticipantDetail(row)" v-if="store.fields.length > 1"> 详情 </el-button>
|
||||||
type="primary"
|
<el-button type="danger" size="small" @click="removeParticipant(row.id)"> 删除 </el-button>
|
||||||
size="small"
|
|
||||||
@click="showParticipantDetail(row)"
|
|
||||||
v-if="store.fields.length > 1"
|
|
||||||
>
|
|
||||||
详情
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
type="danger"
|
|
||||||
size="small"
|
|
||||||
@click="removeParticipant(row.id)"
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="participant-count">
|
<div class="participant-count"> 共 {{ store.participants.length }} 人 </div>
|
||||||
共 {{ store.participants.length }} 人
|
|
||||||
</div>
|
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 字段配置对话框 -->
|
<!-- 字段配置对话框 -->
|
||||||
@@ -101,22 +61,11 @@
|
|||||||
|
|
||||||
<!-- 导入对话框 -->
|
<!-- 导入对话框 -->
|
||||||
<el-dialog v-model="showImportDialog" title="导入名单" width="500px">
|
<el-dialog v-model="showImportDialog" title="导入名单" width="500px">
|
||||||
<el-upload
|
<el-upload ref="uploadRef" :auto-upload="false" :on-change="handleFileChange" :limit="1" accept=".csv" drag>
|
||||||
ref="uploadRef"
|
|
||||||
:auto-upload="false"
|
|
||||||
:on-change="handleFileChange"
|
|
||||||
:limit="1"
|
|
||||||
accept=".csv"
|
|
||||||
drag
|
|
||||||
>
|
|
||||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||||
<div class="el-upload__text">
|
<div class="el-upload__text"> 拖拽文件到此处或 <em>点击上传</em> </div>
|
||||||
拖拽文件到此处或 <em>点击上传</em>
|
|
||||||
</div>
|
|
||||||
<template #tip>
|
<template #tip>
|
||||||
<div class="el-upload__tip">
|
<div class="el-upload__tip"> 支持 .csv 格式文件,第一行为字段名,后续行为数据 </div>
|
||||||
支持 .csv 格式文件,第一行为字段名,后续行为数据
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -153,27 +102,31 @@ const addField = () => {
|
|||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
key: '',
|
key: '',
|
||||||
label: '',
|
label: '',
|
||||||
required: false
|
required: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeField = (id) => {
|
const removeField = id => {
|
||||||
const index = tempFields.value.findIndex(f => f.id === id)
|
const index = tempFields.value.findIndex(f => f.id === id)
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
tempFields.value.splice(index, 1)
|
tempFields.value.splice(index, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveFields = () => {
|
const saveFields = async () => {
|
||||||
const validFields = tempFields.value.filter(f => f.key && f.label)
|
const validFields = tempFields.value.filter(f => f.key && f.label)
|
||||||
if (validFields.length === 0) {
|
if (validFields.length === 0) {
|
||||||
ElMessage.error('至少需要一个有效字段')
|
ElMessage.error('至少需要一个有效字段')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
store.fields.splice(0, store.fields.length, ...validFields)
|
try {
|
||||||
showFieldDialog.value = false
|
await store.updateFields(validFields)
|
||||||
ElMessage.success('字段配置已保存')
|
showFieldDialog.value = false
|
||||||
|
ElMessage.success('字段配置已保存')
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('保存字段配置失败:' + error.message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 名单管理
|
// 名单管理
|
||||||
@@ -189,9 +142,13 @@ const initNewParticipantData = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => store.fields, () => {
|
watch(
|
||||||
initNewParticipantData()
|
() => store.fields,
|
||||||
}, { deep: true })
|
() => {
|
||||||
|
initNewParticipantData()
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
initNewParticipantData()
|
initNewParticipantData()
|
||||||
|
|
||||||
@@ -210,14 +167,14 @@ const addParticipant = () => {
|
|||||||
|
|
||||||
store.addParticipant({
|
store.addParticipant({
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
...newParticipantData.value
|
...newParticipantData.value,
|
||||||
})
|
})
|
||||||
|
|
||||||
initNewParticipantData()
|
initNewParticipantData()
|
||||||
ElMessage.success('添加成功')
|
ElMessage.success('添加成功')
|
||||||
}
|
}
|
||||||
|
|
||||||
const showParticipantDetail = (person) => {
|
const showParticipantDetail = person => {
|
||||||
const details = []
|
const details = []
|
||||||
|
|
||||||
store.fields.forEach(field => {
|
store.fields.forEach(field => {
|
||||||
@@ -240,16 +197,16 @@ const showParticipantDetail = (person) => {
|
|||||||
message: details.join('\n'),
|
message: details.join('\n'),
|
||||||
type: 'info',
|
type: 'info',
|
||||||
duration: 0,
|
duration: 0,
|
||||||
showClose: true
|
showClose: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeParticipant = (id) => {
|
const removeParticipant = id => {
|
||||||
store.removeParticipant(id)
|
store.removeParticipant(id)
|
||||||
ElMessage.success('删除成功')
|
ElMessage.success('删除成功')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFileChange = (file) => {
|
const handleFileChange = file => {
|
||||||
selectedFile.value = file.raw
|
selectedFile.value = file.raw
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,20 +237,18 @@ const clearParticipants = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ElMessageBox.confirm(
|
ElMessageBox.confirm('确定要清空所有名单吗?此操作不可恢复。', '确认清空', {
|
||||||
'确定要清空所有名单吗?此操作不可恢复。',
|
confirmButtonText: '确定',
|
||||||
'确认清空',
|
cancelButtonText: '取消',
|
||||||
{
|
type: 'warning',
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning'
|
|
||||||
}
|
|
||||||
).then(() => {
|
|
||||||
store.clearParticipants()
|
|
||||||
ElMessage.success('已清空名单')
|
|
||||||
}).catch(() => {
|
|
||||||
// 用户取消操作
|
|
||||||
})
|
})
|
||||||
|
.then(() => {
|
||||||
|
store.clearParticipants()
|
||||||
|
ElMessage.success('已清空名单')
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 用户取消操作
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
姓名,部门,职位
|
|
||||||
张三,技术部,工程师
|
|
||||||
李四,产品部,产品经理
|
|
||||||
王五,市场部,市场专员
|
|
||||||
赵六,人事部,HR
|
|
||||||
钱七,财务部,会计
|
|
||||||
|
Reference in New Issue
Block a user