diff --git a/.version b/.version new file mode 100644 index 0000000..7813681 --- /dev/null +++ b/.version @@ -0,0 +1 @@ +5 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 8049fbe..ae1a5e1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,14 +20,14 @@ FROM node:18-alpine WORKDIR /app -# 安装 dumb-init 和 envsubst -RUN apk add --no-cache dumb-init gettext +# 安装 dumb-init、envsubst 和 SQLite 编译依赖 +RUN apk add --no-cache dumb-init gettext python3 make g++ sqlite-dev # 复制后端依赖文件 COPY backend/package*.json ./ # 安装后端依赖(仅生产依赖) -RUN npm ci --only=production +RUN npm install --only=production # 复制后端源码 COPY backend/ ./ @@ -35,8 +35,8 @@ COPY backend/ ./ # 从前端构建阶段复制构建产物 COPY --from=frontend-builder /app/frontend/dist ./public -# 创建上传目录 -RUN mkdir -p uploads +# 创建上传和数据目录 +RUN mkdir -p uploads data # 复制启动脚本 COPY docker-entrypoint.sh /usr/local/bin/ diff --git a/PRIZE-FEATURE.md b/PRIZE-FEATURE.md new file mode 100644 index 0000000..e69de29 diff --git a/backend/.env.example b/backend/.env.example index 8ff80e7..9796360 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,17 +1,6 @@ PORT=3000 -DB_HOST=localhost -DB_USER=root -DB_PASSWORD=your-db-password -DB_NAME=quanyoumi JWT_SECRET=your-secret-key-change-in-production -# MinIO 配置 -MINIO_ENDPOINT=localhost # 后端连接 MinIO 的地址 -MINIO_PORT=9000 -MINIO_USE_SSL=false -MINIO_ACCESS_KEY=jFywUsaCb4zNr2LV7hTP -MINIO_SECRET_KEY=minioadmin -MINIO_BUCKET=quanyoumi -# MINIO_PUBLIC_URL(可选):前端访问文件的公开地址,不填则自动用 MINIO_ENDPOINT 拼接 -# 生产环境如果用了反向代理(如 Nginx),填写外网地址,例如:https://cdn.yourdomain.com -# MINIO_PUBLIC_URL=http://localhost:9000 +# 高德地图配置 +AMAP_KEY=your-amap-key +AMAP_SECURITY_CODE=your-amap-security-code diff --git a/backend/init-admin.js b/backend/init-admin.js index f474dfa..9b60001 100644 --- a/backend/init-admin.js +++ b/backend/init-admin.js @@ -5,8 +5,21 @@ const { Admin, sequelize } = require('./models'); async function initAdmin() { try { + // 使用 force: false 避免 alter 表时的问题 + // 如果表不存在则创建,存在则跳过 await sequelize.sync({ force: false }); + // 检查是否已存在管理员账号 + const existingAdmin = await Admin.findOne({ where: { username: 'admin' } }); + + if (existingAdmin) { + console.log('✓ 管理员账号已存在'); + console.log(' 用户名: admin'); + console.log(' 提示: 如需重置密码,请删除数据库文件后重新初始化'); + process.exit(0); + return; + } + const hashedPassword = bcrypt.hashSync('admin123', 10); await Admin.create({ username: 'admin', @@ -14,12 +27,17 @@ async function initAdmin() { name: '管理员' }); - console.log('管理员账号创建成功'); - console.log('用户名: admin'); - console.log('密码: admin123'); + console.log('✓ 管理员账号创建成功'); + console.log(' 用户名: admin'); + console.log(' 密码: admin123'); process.exit(0); } catch (error) { - console.error('创建失败:', error.message); + console.error('✗ 初始化失败:', error.message); + if (error.name === 'SequelizeUniqueConstraintError') { + console.error(' 原因: 管理员账号已存在'); + } else { + console.error(' 详细错误:', error); + } process.exit(1); } } diff --git a/backend/middleware/upload.js b/backend/middleware/upload.js index b852c14..d51b460 100644 --- a/backend/middleware/upload.js +++ b/backend/middleware/upload.js @@ -1,11 +1,27 @@ const multer = require('multer'); const path = require('path'); -const { v4: uuidv4 } = require('uuid'); -const { uploadBuffer } = require('../utils/minio'); +const fs = require('fs'); + +// 确保上传目录存在 +const uploadDir = path.join(__dirname, '../uploads'); +if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }); +} + +// 配置本地存储 +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, uploadDir); + }, + filename: (req, file, cb) => { + const uniqueName = `${Date.now()}-${Math.random().toString(36).substring(7)}${path.extname(file.originalname)}`; + cb(null, uniqueName); + } +}); -// 使用内存存储,文件不落盘,不限制大小 const upload = multer({ - storage: multer.memoryStorage(), + storage, + limits: { fileSize: 50 * 1024 * 1024 }, // 50MB fileFilter: (req, file, cb) => { const extname = /jpeg|jpg|png|gif|mp4|mov|avi/.test( path.extname(file.originalname).toLowerCase() @@ -16,15 +32,14 @@ const upload = multer({ } }); -// 将 req.files 或 req.file 上传到 MinIO,返回 url 数组或单个 url -const uploadFilesToMinio = async (files) => { +// 返回文件访问URL +const getFileUrls = (files) => { const list = Array.isArray(files) ? files : [files]; - return Promise.all(list.map(async (file) => { - const ext = path.extname(file.originalname).toLowerCase(); - const filename = `${Date.now()}-${uuidv4()}${ext}`; - const url = await uploadBuffer(file.buffer, filename, file.mimetype); - return { ...file, filename, url }; + return list.map(file => ({ + url: `/uploads/${file.filename}`, + filename: file.filename, + mimetype: file.mimetype })); }; -module.exports = { upload, uploadFilesToMinio }; +module.exports = { upload, getFileUrls }; diff --git a/backend/migrations/0000-init-schema.js b/backend/migrations/0000-init-schema.js new file mode 100644 index 0000000..d5d1faa --- /dev/null +++ b/backend/migrations/0000-init-schema.js @@ -0,0 +1,84 @@ +/** + * 迁移: 初始化数据库结构(确保所有表和字段存在) + * 版本: 0000 + * 日期: 2025-01-09 + */ + +module.exports = { + async up(manager) { + console.log(' - 检查并创建所有必需的表和字段...'); + + // Activities 表的额外字段 + const activityFields = [ + { name: 'registrationEnabled', sql: 'ALTER TABLE Activities ADD COLUMN registrationEnabled INTEGER DEFAULT 1;' }, + { name: 'coverImage', sql: 'ALTER TABLE Activities ADD COLUMN coverImage TEXT;' }, + { name: 'price', sql: 'ALTER TABLE Activities ADD COLUMN price DECIMAL(10, 2) DEFAULT 0;' }, + { name: 'budget', sql: 'ALTER TABLE Activities ADD COLUMN budget DECIMAL(10, 2) DEFAULT 0;' }, + { name: 'locationLat', sql: 'ALTER TABLE Activities ADD COLUMN locationLat DECIMAL(10, 7);' }, + { name: 'locationLng', sql: 'ALTER TABLE Activities ADD COLUMN locationLng DECIMAL(10, 7);' } + ]; + + for (const field of activityFields) { + await manager.executeSql(field.sql); + } + + // Registrations 表的额外字段 + const registrationFields = [ + { name: 'peopleCount', sql: 'ALTER TABLE Registrations ADD COLUMN peopleCount INTEGER DEFAULT 1;' }, + { name: 'note', sql: 'ALTER TABLE Registrations ADD COLUMN note TEXT;' } + ]; + + for (const field of registrationFields) { + await manager.executeSql(field.sql); + } + + // Expenses 表的额外字段 + const expenseFields = [ + { name: 'category', sql: 'ALTER TABLE Expenses ADD COLUMN category TEXT;' }, + { name: 'receipt', sql: 'ALTER TABLE Expenses ADD COLUMN receipt TEXT;' }, + { name: 'note', sql: 'ALTER TABLE Expenses ADD COLUMN note TEXT;' }, + { name: 'media', sql: 'ALTER TABLE Expenses ADD COLUMN media TEXT;' } + ]; + + for (const field of expenseFields) { + await manager.executeSql(field.sql); + } + + // Photos 表的额外字段 + const photoFields = [ + { name: 'description', sql: 'ALTER TABLE Photos ADD COLUMN description TEXT;' }, + { name: 'uploadedBy', sql: 'ALTER TABLE Photos ADD COLUMN uploadedBy TEXT;' }, + { name: 'status', sql: 'ALTER TABLE Photos ADD COLUMN status TEXT DEFAULT "approved";' } + ]; + + for (const field of photoFields) { + await manager.executeSql(field.sql); + } + + // Sponsors 表的额外字段 + const sponsorFields = [ + { name: 'logo', sql: 'ALTER TABLE Sponsors ADD COLUMN logo TEXT;' }, + { name: 'type', sql: 'ALTER TABLE Sponsors ADD COLUMN type TEXT DEFAULT "sponsor";' }, + { name: 'link', sql: 'ALTER TABLE Sponsors ADD COLUMN link TEXT;' } + ]; + + for (const field of sponsorFields) { + await manager.executeSql(field.sql); + } + + // Admins 表的额外字段 + const adminFields = [ + { name: 'name', sql: 'ALTER TABLE Admins ADD COLUMN name TEXT;' } + ]; + + for (const field of adminFields) { + await manager.executeSql(field.sql); + } + + console.log(' - 初始化完成'); + }, + + async down(manager) { + console.log(' - 回滚不支持'); + } +}; diff --git a/backend/migrations/0001-add-danmaku-fields.js b/backend/migrations/0001-add-danmaku-fields.js new file mode 100644 index 0000000..3785689 --- /dev/null +++ b/backend/migrations/0001-add-danmaku-fields.js @@ -0,0 +1,24 @@ +/** + * 迁移: 添加弹幕功能字段 + * 版本: 0001 + * 日期: 2025-01-09 + */ + +module.exports = { + async up(manager) { + console.log(' - 添加 danmakuEnabled 字段...'); + await manager.executeSql(` + ALTER TABLE Activities ADD COLUMN danmakuEnabled INTEGER DEFAULT 0; + `); + + console.log(' - 添加 danmakuContent 字段...'); + await manager.executeSql(` + ALTER TABLE Activities ADD COLUMN danmakuContent TEXT; + `); + }, + + async down(manager) { + // SQLite 不支持 DROP COLUMN,需要重建表 + console.log(' - 回滚不支持,请手动处理'); + } +}; diff --git a/backend/migrations/0002-add-sponsor-sortorder.js b/backend/migrations/0002-add-sponsor-sortorder.js new file mode 100644 index 0000000..f9d36a9 --- /dev/null +++ b/backend/migrations/0002-add-sponsor-sortorder.js @@ -0,0 +1,18 @@ +/** + * 迁移: 添加赞助商排序字段 + * 版本: 0002 + * 日期: 2025-01-09 + */ + +module.exports = { + async up(manager) { + console.log(' - 添加 Sponsors.sortOrder 字段...'); + await manager.executeSql(` + ALTER TABLE Sponsors ADD COLUMN sortOrder INTEGER DEFAULT 0; + `); + }, + + async down(manager) { + console.log(' - 回滚不支持'); + } +}; diff --git a/backend/migrations/0003-add-checkin-fields.js b/backend/migrations/0003-add-checkin-fields.js new file mode 100644 index 0000000..16b408d --- /dev/null +++ b/backend/migrations/0003-add-checkin-fields.js @@ -0,0 +1,23 @@ +/** + * 迁移: 添加签到功能字段 + * 版本: 0003 + * 日期: 2025-01-09 + */ + +module.exports = { + async up(manager) { + console.log(' - 添加 Registrations.checkInStatus 字段...'); + await manager.executeSql(` + ALTER TABLE Registrations ADD COLUMN checkInStatus INTEGER DEFAULT 0; + `); + + console.log(' - 添加 Registrations.checkInTime 字段...'); + await manager.executeSql(` + ALTER TABLE Registrations ADD COLUMN checkInTime DATETIME; + `); + }, + + async down(manager) { + console.log(' - 回滚不支持'); + } +}; diff --git a/backend/migrations/0004-add-prize-table.js b/backend/migrations/0004-add-prize-table.js new file mode 100644 index 0000000..cfa4853 --- /dev/null +++ b/backend/migrations/0004-add-prize-table.js @@ -0,0 +1,31 @@ +/** + * 迁移: 添加奖品表 + * 版本: 0004 + * 日期: 2025-01-10 + */ + +module.exports = { + async up(manager) { + console.log(' - 创建 Prizes 表...'); + + await manager.executeSql(` + CREATE TABLE IF NOT EXISTS Prizes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + activityId INTEGER NOT NULL, + name TEXT NOT NULL, + level TEXT NOT NULL CHECK(level IN ('一等奖', '二等奖', '三等奖', '参与奖', '伴手礼')), + description TEXT, + image TEXT, + quantity INTEGER DEFAULT 1, + sortOrder INTEGER DEFAULT 0, + createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, + updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP + ); + `); + }, + + async down(manager) { + console.log(' - 删除 Prizes 表...'); + await manager.executeSql('DROP TABLE IF EXISTS Prizes;'); + } +}; diff --git a/backend/migrations/README.md b/backend/migrations/README.md new file mode 100644 index 0000000..dd56b23 --- /dev/null +++ b/backend/migrations/README.md @@ -0,0 +1,190 @@ +# 数据库迁移系统 + +## 概述 + +本项目使用版本化的数据库迁移系统,支持增量更新和自动同步。 + +## 特性 + +- ✅ 版本管理:每个迁移有唯一版本号 +- ✅ 增量执行:只执行未执行的迁移 +- ✅ 自动运行:服务器启动时自动执行迁移 +- ✅ 幂等性:重复执行不会出错 +- ✅ 迁移记录:记录所有已执行的迁移 + +## 迁移文件命名规范 + +``` +XXXX-description.js +``` + +- `XXXX`: 4位数字版本号(如 0001, 0002) +- `description`: 迁移描述(使用短横线分隔) + +示例: +- `0001-add-danmaku-fields.js` +- `0002-add-user-avatar.js` + +## 创建新迁移 + +### 1. 创建迁移文件 + +在 `backend/migrations/` 目录下创建新文件: + +```javascript +/** + * 迁移: 添加用户头像字段 + * 版本: 0002 + * 日期: 2025-01-09 + */ + +module.exports = { + async up(manager) { + console.log(' - 添加 avatar 字段...'); + await manager.executeSql(` + ALTER TABLE Users ADD COLUMN avatar TEXT; + `); + }, + + async down(manager) { + // 回滚逻辑(可选) + console.log(' - 回滚不支持'); + } +}; +``` + +### 2. 迁移方法 + +#### `up(manager)` - 升级 +执行数据库变更的方法。 + +可用的 manager 方法: +- `executeSql(sql)`: 执行SQL语句 +- `db`: 直接访问数据库连接 + +#### `down(manager)` - 回滚(可选) +回滚数据库变更的方法。 + +注意:SQLite 不支持 DROP COLUMN,回滚可能需要重建表。 + +## 执行迁移 + +### 自动执行(推荐) + +服务器启动时会自动执行所有未执行的迁移: + +```bash +# 开发环境 +cd backend +npm start + +# 生产环境(Docker) +docker-compose up -d +# 或 +docker-compose restart +``` + +### 手动执行 + +#### 在容器内执行: + +```bash +# Windows +migrate.bat + +# Linux/Mac +chmod +x migrate.sh +./migrate.sh +``` + +#### 直接在容器中执行: + +```bash +docker exec quanyoumi-app node /app/backend/migrations/index.js +``` + +## 迁移状态 + +迁移执行记录保存在 `migrations` 表中: + +```sql +SELECT * FROM migrations; +``` + +输出示例: +``` +id | version | name | executed_at +---+---------+-----------------------+--------------------- +1 | 1 | add-danmaku-fields | 2025-01-09 10:30:00 +2 | 2 | add-user-avatar | 2025-01-09 11:00:00 +``` + +## 常见问题 + +### Q: 迁移失败怎么办? + +A: 检查错误日志,修复问题后重新启动服务器。已执行的迁移不会重复执行。 + +### Q: 如何回滚迁移? + +A: 目前不支持自动回滚。如需回滚,请手动修改数据库或创建新的迁移来撤销更改。 + +### Q: 可以修改已执行的迁移吗? + +A: 不建议。已执行的迁移不应修改。如需更改,请创建新的迁移。 + +### Q: 迁移在什么时候执行? + +A: 服务器启动时自动执行。也可以手动运行 `migrate.bat` 或 `migrate.sh`。 + +## 最佳实践 + +1. **版本号递增**:新迁移的版本号应大于所有现有迁移 +2. **描述清晰**:文件名和注释应清楚说明迁移目的 +3. **测试迁移**:在开发环境测试后再部署到生产环境 +4. **幂等性**:迁移应该可以安全地重复执行 +5. **小步迭代**:每个迁移只做一件事,便于追踪和回滚 + +## 示例 + +### 添加新字段 + +```javascript +module.exports = { + async up(manager) { + await manager.executeSql(` + ALTER TABLE Activities ADD COLUMN featured INTEGER DEFAULT 0; + `); + } +}; +``` + +### 创建新表 + +```javascript +module.exports = { + async up(manager) { + await manager.executeSql(` + CREATE TABLE IF NOT EXISTS Comments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + activityId INTEGER NOT NULL, + userId INTEGER NOT NULL, + content TEXT NOT NULL, + createdAt DATETIME DEFAULT CURRENT_TIMESTAMP + ); + `); + } +}; +``` + +### 修改数据 + +```javascript +module.exports = { + async up(manager) { + await manager.executeSql(` + UPDATE Activities SET status = 'completed' WHERE endTime < datetime('now'); + `); + } +}; +``` diff --git a/backend/migrations/index.js b/backend/migrations/index.js new file mode 100644 index 0000000..0dbc3d9 --- /dev/null +++ b/backend/migrations/index.js @@ -0,0 +1,141 @@ +const sqlite3 = require('sqlite3').verbose(); +const path = require('path'); +const fs = require('fs'); + +const dbPath = path.join(__dirname, '..', 'data', 'database.sqlite'); +const migrationsDir = __dirname; + +class MigrationManager { + constructor() { + this.db = new sqlite3.Database(dbPath); + } + + // 初始化迁移表 + async initMigrationTable() { + return new Promise((resolve, reject) => { + this.db.run(` + CREATE TABLE IF NOT EXISTS migrations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + version INTEGER NOT NULL UNIQUE, + name TEXT NOT NULL, + executed_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `, (err) => { + if (err) reject(err); + else resolve(); + }); + }); + } + + // 获取已执行的迁移版本 + async getExecutedMigrations() { + return new Promise((resolve, reject) => { + this.db.all('SELECT version FROM migrations ORDER BY version', (err, rows) => { + if (err) reject(err); + else resolve(rows.map(r => r.version)); + }); + }); + } + + // 记录迁移执行 + async recordMigration(version, name) { + return new Promise((resolve, reject) => { + this.db.run( + 'INSERT INTO migrations (version, name) VALUES (?, ?)', + [version, name], + (err) => { + if (err) reject(err); + else resolve(); + } + ); + }); + } + + // 执行SQL语句 + async executeSql(sql) { + return new Promise((resolve, reject) => { + this.db.run(sql, (err) => { + if (err) { + // 忽略字段已存在的错误 + if (err.message.includes('duplicate column name')) { + resolve(); + } else { + reject(err); + } + } else { + resolve(); + } + }); + }); + } + + // 加载所有迁移文件 + loadMigrations() { + const files = fs.readdirSync(migrationsDir) + .filter(f => f.match(/^\d{4}-.*\.js$/) && f !== 'index.js') + .sort(); + + return files.map(file => { + const version = parseInt(file.split('-')[0]); + const name = file.replace(/^\d{4}-/, '').replace('.js', ''); + const migration = require(path.join(migrationsDir, file)); + return { version, name, file, migration }; + }); + } + + // 执行迁移 + async runMigrations() { + try { + console.log('🔄 开始数据库迁移...\n'); + + // 初始化迁移表 + await this.initMigrationTable(); + + // 获取已执行的迁移 + const executed = await this.getExecutedMigrations(); + console.log(`📊 已执行迁移: ${executed.length} 个`); + + // 加载所有迁移文件 + const migrations = this.loadMigrations(); + console.log(`📁 发现迁移文件: ${migrations.length} 个\n`); + + // 执行未执行的迁移 + let count = 0; + for (const { version, name, file, migration } of migrations) { + if (!executed.includes(version)) { + console.log(`⏳ 执行迁移 [${version}] ${name}...`); + + try { + await migration.up(this); + await this.recordMigration(version, name); + console.log(`✅ 迁移 [${version}] ${name} 完成\n`); + count++; + } catch (error) { + console.error(`❌ 迁移 [${version}] ${name} 失败:`, error.message); + throw error; + } + } else { + console.log(`⏭️ 跳过已执行的迁移 [${version}] ${name}`); + } + } + + console.log(`\n✨ 迁移完成!共执行 ${count} 个新迁移`); + + } catch (error) { + console.error('\n❌ 迁移失败:', error); + throw error; + } finally { + this.db.close(); + } + } +} + +// 如果直接运行此文件 +if (require.main === module) { + const manager = new MigrationManager(); + manager.runMigrations() + .then(() => process.exit(0)) + .catch(() => process.exit(1)); +} + +module.exports = MigrationManager; diff --git a/backend/models/Activity.js b/backend/models/Activity.js index c388c3d..877c962 100644 --- a/backend/models/Activity.js +++ b/backend/models/Activity.js @@ -38,6 +38,15 @@ const Activity = sequelize.define('Activity', { type: DataTypes.DECIMAL(10, 2), defaultValue: 0, comment: '活动预算' + }, + danmakuEnabled: { + type: DataTypes.BOOLEAN, + defaultValue: false, + comment: '弹幕开关' + }, + danmakuContent: { + type: DataTypes.TEXT, + comment: '弹幕内容JSON' } }); diff --git a/backend/models/Prize.js b/backend/models/Prize.js new file mode 100644 index 0000000..2a5d122 --- /dev/null +++ b/backend/models/Prize.js @@ -0,0 +1,35 @@ +const { DataTypes } = require('sequelize'); +const sequelize = require('./db'); + +const Prize = sequelize.define('Prize', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + activityId: { + type: DataTypes.INTEGER, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: false + }, + level: { + type: DataTypes.ENUM('一等奖', '二等奖', '三等奖', '参与奖', '伴手礼'), + allowNull: false + }, + description: DataTypes.TEXT, + image: DataTypes.STRING, + quantity: { + type: DataTypes.INTEGER, + defaultValue: 1 + }, + sortOrder: { + type: DataTypes.INTEGER, + defaultValue: 0, + comment: '排序值,数字越小越靠前' + } +}); + +module.exports = Prize; diff --git a/backend/models/Sponsor.js b/backend/models/Sponsor.js index ab4bbf0..7d78e41 100644 --- a/backend/models/Sponsor.js +++ b/backend/models/Sponsor.js @@ -20,7 +20,12 @@ const Sponsor = sequelize.define('Sponsor', { type: DataTypes.ENUM('title', 'sponsor'), defaultValue: 'sponsor' }, - link: DataTypes.STRING + link: DataTypes.STRING, + sortOrder: { + type: DataTypes.INTEGER, + defaultValue: 0, + comment: '排序值,数字越小越靠前' + } }); module.exports = Sponsor; diff --git a/backend/models/db.js b/backend/models/db.js index 0fc3cc8..88b187e 100644 --- a/backend/models/db.js +++ b/backend/models/db.js @@ -1,14 +1,17 @@ const { Sequelize } = require('sequelize'); +const path = require('path'); +const fs = require('fs'); -const sequelize = new Sequelize( - process.env.DB_NAME || 'quanyoumi', - process.env.DB_USER || 'root', - process.env.DB_PASSWORD || '', - { - host: process.env.DB_HOST || 'localhost', - dialect: 'mysql', - logging: false - } -); +// 确保data目录存在 +const dataDir = path.join(__dirname, '../data'); +if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir, { recursive: true }); +} + +const sequelize = new Sequelize({ + dialect: 'sqlite', + storage: path.join(dataDir, 'database.sqlite'), + logging: false +}); module.exports = sequelize; diff --git a/backend/models/index.js b/backend/models/index.js index cffc31b..fb667fd 100644 --- a/backend/models/index.js +++ b/backend/models/index.js @@ -4,11 +4,15 @@ const Registration = require('./Registration'); const Expense = require('./Expense'); const Photo = require('./Photo'); const Sponsor = require('./Sponsor'); +const Prize = require('./Prize'); const Admin = require('./Admin'); // 同步数据库 -sequelize.sync({ alter: true }).then(() => { - console.log('数据库同步完成'); +// 使用 force: false 避免 SQLite alter 表时的约束冲突问题 +sequelize.sync({ force: false }).then(() => { + console.log('✓ 数据库同步完成'); +}).catch(err => { + console.error('✗ 数据库同步失败:', err.message); }); module.exports = { @@ -18,5 +22,6 @@ module.exports = { Expense, Photo, Sponsor, + Prize, Admin }; diff --git a/backend/package-lock.json b/backend/package-lock.json index f9035e2..823a4b7 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,15 +9,14 @@ "version": "1.0.0", "dependencies": { "bcryptjs": "^2.4.3", + "better-sqlite3": "^11.10.0", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", "jsonwebtoken": "^9.0.2", - "minio": "^8.0.7", "multer": "^1.4.5-lts.1", - "mysql2": "^3.6.0", "sequelize": "^6.33.0", - "uuid": "^9.0.1" + "sql.js": "^1.14.1" }, "devDependencies": { "nodemon": "^3.0.1" @@ -92,21 +91,6 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmmirror.com/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "license": "MIT" - }, - "node_modules/aws-ssl-profiles": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", - "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", - "license": "MIT", - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", @@ -117,12 +101,43 @@ "node": "18 || 20 || >=22" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/bcryptjs": { "version": "2.4.3", "resolved": "https://registry.npmmirror.com/bcryptjs/-/bcryptjs-2.4.3.tgz", "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", "license": "MIT" }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmmirror.com/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -136,16 +151,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/block-stream2": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/block-stream2/-/block-stream2-2.1.0.tgz", - "integrity": "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==", + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", "license": "MIT", "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, - "node_modules/block-stream2/node_modules/readable-stream": { + "node_modules/bl/node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", @@ -209,19 +235,28 @@ "node": ">=8" } }, - "node_modules/browser-or-node": { - "version": "2.1.1", - "resolved": "https://registry.npmmirror.com/browser-or-node/-/browser-or-node-2.1.1.tgz", - "integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==", - "license": "MIT" - }, - "node_modules/buffer-crc32": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/buffer-crc32/-/buffer-crc32-1.0.0.tgz", - "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "license": "MIT", - "engines": { - "node": ">=8.0.0" + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" } }, "node_modules/buffer-equal-constant-time": { @@ -393,22 +428,28 @@ "ms": "2.0.0" } }, - "node_modules/decode-uri-component": { - "version": "0.2.2", - "resolved": "https://registry.npmmirror.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz", - "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, "engines": { - "node": ">=0.10" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/denque": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "license": "Apache-2.0", + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", "engines": { - "node": ">=0.10" + "node": ">=4.0.0" } }, "node_modules/depd": { @@ -430,6 +471,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.6.1.tgz", @@ -486,6 +536,15 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", @@ -531,11 +590,14 @@ "node": ">= 0.6" } }, - "node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "license": "MIT" + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } }, "node_modules/express": { "version": "4.22.1", @@ -583,40 +645,11 @@ "url": "https://opencollective.com/express" } }, - "node_modules/fast-xml-builder": { - "version": "1.1.4", - "resolved": "https://registry.npmmirror.com/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", - "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "path-expression-matcher": "^1.1.3" - } - }, - "node_modules/fast-xml-parser": { - "version": "5.5.10", - "resolved": "https://registry.npmmirror.com/fast-xml-parser/-/fast-xml-parser-5.5.10.tgz", - "integrity": "sha512-go2J2xODMc32hT+4Xr/bBGXMaIoiCwrwp2mMtAvKyvEFW6S/v5Gn2pBmE4nvbwNjGhpcAiOwEv7R6/GZ6XRa9w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "fast-xml-builder": "^1.1.4", - "path-expression-matcher": "^1.2.1", - "strnum": "^2.2.2" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" }, "node_modules/fill-range": { "version": "7.1.1", @@ -631,15 +664,6 @@ "node": ">=8" } }, - "node_modules/filter-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/filter-obj/-/filter-obj-1.1.0.tgz", - "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/finalhandler": { "version": "1.3.2", "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.2.tgz", @@ -676,6 +700,12 @@ "node": ">= 0.6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", @@ -700,15 +730,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/generate-function": { - "version": "2.3.1", - "resolved": "https://registry.npmmirror.com/generate-function/-/generate-function-2.3.1.tgz", - "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", - "license": "MIT", - "dependencies": { - "is-property": "^1.0.2" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -746,6 +767,12 @@ "node": ">= 0.4" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmmirror.com/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", @@ -837,6 +864,26 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -859,6 +906,12 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -914,12 +967,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", - "license": "MIT" - }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", @@ -1023,27 +1070,6 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, - "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmmirror.com/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0" - }, - "node_modules/lru.min": { - "version": "1.1.4", - "resolved": "https://registry.npmmirror.com/lru.min/-/lru.min-1.1.4.tgz", - "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", - "license": "MIT", - "engines": { - "bun": ">=1.0.0", - "deno": ">=1.30.0", - "node": ">=8.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wellwelwel" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -1113,6 +1139,18 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", @@ -1138,39 +1176,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minio": { - "version": "8.0.7", - "resolved": "https://registry.npmmirror.com/minio/-/minio-8.0.7.tgz", - "integrity": "sha512-E737MgufW8CeQAsTAtnEMrxZ9scMSf29kkhZoXzDTKj/Jszzo2SfeZUH9wbDQH2Rsq6TCtl/yQL0+XdVKZansQ==", - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.4", - "block-stream2": "^2.1.0", - "browser-or-node": "^2.1.1", - "buffer-crc32": "^1.0.0", - "eventemitter3": "^5.0.1", - "fast-xml-parser": "^5.3.4", - "ipaddr.js": "^2.0.1", - "lodash": "^4.17.21", - "mime-types": "^2.1.35", - "query-string": "^7.1.3", - "stream-json": "^1.8.0", - "through2": "^4.0.2", - "xml2js": "^0.5.0 || ^0.6.2" - }, - "engines": { - "node": "^16 || ^18 || >=20" - } - }, - "node_modules/minio/node_modules/ipaddr.js": { - "version": "2.3.0", - "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-2.3.0.tgz", - "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-0.5.6.tgz", @@ -1183,6 +1188,12 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmmirror.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmmirror.com/moment/-/moment-2.30.1.tgz", @@ -1229,55 +1240,11 @@ "node": ">= 6.0.0" } }, - "node_modules/mysql2": { - "version": "3.20.0", - "resolved": "https://registry.npmmirror.com/mysql2/-/mysql2-3.20.0.tgz", - "integrity": "sha512-eCLUs7BNbgA6nf/MZXsaBO1SfGs0LtLVrJD3WeWq+jPLDWkSufTD+aGMwykfUVPdZnblaUK1a8G/P63cl9FkKg==", - "license": "MIT", - "dependencies": { - "aws-ssl-profiles": "^1.1.2", - "denque": "^2.1.0", - "generate-function": "^2.3.1", - "iconv-lite": "^0.7.2", - "long": "^5.3.2", - "lru.min": "^1.1.4", - "named-placeholders": "^1.1.6", - "sql-escaper": "^1.3.3" - }, - "engines": { - "node": ">= 8.0" - }, - "peerDependencies": { - "@types/node": ">= 8" - } - }, - "node_modules/mysql2/node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/named-placeholders": { - "version": "1.1.6", - "resolved": "https://registry.npmmirror.com/named-placeholders/-/named-placeholders-1.1.6.tgz", - "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", - "license": "MIT", - "dependencies": { - "lru.min": "^1.1.0" - }, - "engines": { - "node": ">=8.0.0" - } + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" }, "node_modules/negotiator": { "version": "0.6.3", @@ -1288,6 +1255,18 @@ "node": ">= 0.6" } }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmmirror.com/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/nodemon": { "version": "3.1.14", "resolved": "https://registry.npmmirror.com/nodemon/-/nodemon-3.1.14.tgz", @@ -1385,6 +1364,15 @@ "node": ">= 0.8" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", @@ -1394,21 +1382,6 @@ "node": ">= 0.8" } }, - "node_modules/path-expression-matcher": { - "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/path-expression-matcher/-/path-expression-matcher-1.3.0.tgz", - "integrity": "sha512-tkolHg8cWjEFA8+TqYGk0w6aNEZVcb7pVxW8KXZpU+ebaBr3s1ogbLssjK1cL74TtEuKl/qy6cb90RnwTFt3kw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/path-to-regexp": { "version": "0.1.13", "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.13.tgz", @@ -1434,6 +1407,33 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmmirror.com/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -1460,6 +1460,16 @@ "dev": true, "license": "MIT" }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/qs": { "version": "6.14.2", "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.2.tgz", @@ -1475,24 +1485,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/query-string": { - "version": "7.1.3", - "resolved": "https://registry.npmmirror.com/query-string/-/query-string-7.1.3.tgz", - "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", - "license": "MIT", - "dependencies": { - "decode-uri-component": "^0.2.2", - "filter-obj": "^1.1.0", - "split-on-first": "^1.0.0", - "strict-uri-encode": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz", @@ -1517,6 +1509,21 @@ "node": ">= 0.8" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", @@ -1583,15 +1590,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, - "node_modules/sax": { - "version": "1.6.0", - "resolved": "https://registry.npmmirror.com/sax/-/sax-1.6.0.tgz", - "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=11.0.0" - } - }, "node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", @@ -1728,15 +1726,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/sequelize/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmmirror.com/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/serve-static": { "version": "1.16.3", "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.3.tgz", @@ -1778,13 +1767,13 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -1830,6 +1819,51 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -1843,29 +1877,11 @@ "node": ">=10" } }, - "node_modules/split-on-first": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/split-on-first/-/split-on-first-1.1.0.tgz", - "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/sql-escaper": { - "version": "1.3.3", - "resolved": "https://registry.npmmirror.com/sql-escaper/-/sql-escaper-1.3.3.tgz", - "integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==", - "license": "MIT", - "engines": { - "bun": ">=1.0.0", - "deno": ">=2.0.0", - "node": ">=12.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" - } + "node_modules/sql.js": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/sql.js/-/sql.js-1.14.1.tgz", + "integrity": "sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==", + "license": "MIT" }, "node_modules/statuses": { "version": "2.0.2", @@ -1876,21 +1892,6 @@ "node": ">= 0.8" } }, - "node_modules/stream-chain": { - "version": "2.2.5", - "resolved": "https://registry.npmmirror.com/stream-chain/-/stream-chain-2.2.5.tgz", - "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==", - "license": "BSD-3-Clause" - }, - "node_modules/stream-json": { - "version": "1.9.1", - "resolved": "https://registry.npmmirror.com/stream-json/-/stream-json-1.9.1.tgz", - "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==", - "license": "BSD-3-Clause", - "dependencies": { - "stream-chain": "^2.2.5" - } - }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/streamsearch/-/streamsearch-1.1.0.tgz", @@ -1899,15 +1900,6 @@ "node": ">=10.0.0" } }, - "node_modules/strict-uri-encode": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", - "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", @@ -1923,17 +1915,14 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, - "node_modules/strnum": { - "version": "2.2.3", - "resolved": "https://registry.npmmirror.com/strnum/-/strnum-2.2.3.tgz", - "integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, "node_modules/supports-color": { "version": "5.5.0", @@ -1948,16 +1937,41 @@ "node": ">=4" } }, - "node_modules/through2": { - "version": "4.0.2", - "resolved": "https://registry.npmmirror.com/through2/-/through2-4.0.2.tgz", - "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "license": "MIT", "dependencies": { - "readable-stream": "3" + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" } }, - "node_modules/through2/node_modules/readable-stream": { + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", @@ -2009,6 +2023,18 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz", @@ -2066,13 +2092,9 @@ } }, "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmmirror.com/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], + "version": "8.3.2", + "resolved": "https://registry.npmmirror.com/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "license": "MIT", "bin": { "uuid": "dist/bin/uuid" @@ -2105,27 +2127,11 @@ "@types/node": "*" } }, - "node_modules/xml2js": { - "version": "0.6.2", - "resolved": "https://registry.npmmirror.com/xml2js/-/xml2js-0.6.2.tgz", - "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", - "license": "MIT", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmmirror.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "license": "MIT", - "engines": { - "node": ">=4.0" - } + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" }, "node_modules/xtend": { "version": "4.0.2", diff --git a/backend/package.json b/backend/package.json index c4dc3e0..ca4000e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -9,15 +9,15 @@ }, "dependencies": { "bcryptjs": "^2.4.3", + "better-sqlite3": "^11.10.0", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", "jsonwebtoken": "^9.0.2", - "minio": "^8.0.7", "multer": "^1.4.5-lts.1", - "mysql2": "^3.6.0", "sequelize": "^6.33.0", - "uuid": "^9.0.1" + "sql.js": "^1.14.1", + "sqlite3": "^5.1.7" }, "devDependencies": { "nodemon": "^3.0.1" diff --git a/backend/routes/activity.js b/backend/routes/activity.js index 3881fc3..a249cab 100644 --- a/backend/routes/activity.js +++ b/backend/routes/activity.js @@ -1,7 +1,7 @@ const express = require('express'); const router = express.Router(); -const { Activity, Photo, Sponsor, Expense, Registration } = require('../models'); -const { upload, uploadFilesToMinio } = require('../middleware/upload'); +const { Activity, Photo, Sponsor, Expense, Registration, Prize } = require('../models'); +const { upload, getFileUrls } = require('../middleware/upload'); // 获取活动列表 router.get('/activities', async (req, res) => { @@ -23,10 +23,17 @@ router.get('/activities/:id', async (req, res) => { const photos = await Photo.findAll({ where: { activityId: req.params.id, status: 'approved' } }); - const sponsors = await Sponsor.findAll({ where: { activityId: req.params.id } }); + const sponsors = await Sponsor.findAll({ + where: { activityId: req.params.id }, + order: [['sortOrder', 'ASC'], ['id', 'ASC']] + }); const expenses = await Expense.findAll({ where: { activityId: req.params.id } }); + const prizes = await Prize.findAll({ + where: { activityId: req.params.id }, + order: [['sortOrder', 'ASC'], ['id', 'ASC']] + }); - res.json({ activity, photos, sponsors, expenses }); + res.json({ activity, photos, sponsors, expenses, prizes }); } catch (error) { res.status(500).json({ error: error.message }); } @@ -46,7 +53,7 @@ router.get('/activities/:id/expenses', async (req, res) => { router.post('/activities/:id/photos', upload.array('photos', 10), async (req, res) => { try { const { uploadedBy } = req.body; - const uploaded = await uploadFilesToMinio(req.files); + const uploaded = getFileUrls(req.files); const photos = uploaded.map(file => ({ activityId: req.params.id, url: file.url, diff --git a/backend/routes/admin.js b/backend/routes/admin.js index fbdce2d..9640501 100644 --- a/backend/routes/admin.js +++ b/backend/routes/admin.js @@ -3,9 +3,10 @@ const router = express.Router(); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const auth = require('../middleware/auth'); -const { upload, uploadFilesToMinio } = require('../middleware/upload'); -const { deleteFile } = require('../utils/minio'); -const { Admin, Activity, Registration, Expense, Photo, Sponsor } = require('../models'); +const { upload, getFileUrls } = require('../middleware/upload'); +const { Admin, Activity, Registration, Expense, Photo, Sponsor, Prize } = require('../models'); +const fs = require('fs'); +const path = require('path'); // 管理员登录 router.post('/login', async (req, res) => { @@ -100,7 +101,7 @@ router.post('/expenses', auth, upload.array('media', 5), async (req, res) => { let media = []; if (req.files && req.files.length > 0) { - const uploaded = await uploadFilesToMinio(req.files); + const uploaded = getFileUrls(req.files); media = uploaded.map(file => ({ url: file.url, type: file.mimetype.startsWith('video/') ? 'video' : 'image' @@ -152,7 +153,7 @@ router.put('/expenses/:id', auth, upload.array('media', 5), async (req, res) => // 处理新上传的媒体 if (req.files && req.files.length > 0) { - const uploaded = await uploadFilesToMinio(req.files); + const uploaded = getFileUrls(req.files); const newMedia = uploaded.map(file => ({ url: file.url, type: file.mimetype.startsWith('video/') ? 'video' : 'image' @@ -184,7 +185,7 @@ router.delete('/expenses/:id', auth, async (req, res) => { router.post('/photos', auth, upload.array('photos', 10), async (req, res) => { try { const { activityId } = req.body; - const uploaded = await uploadFilesToMinio(req.files); + const uploaded = getFileUrls(req.files); const photos = uploaded.map(file => ({ activityId, url: file.url, @@ -231,11 +232,12 @@ router.delete('/photos/:id', auth, async (req, res) => { return res.status(404).json({ error: '照片不存在' }); } - // 从URL提取文件名 + // 删除本地文件 const filename = photo.url.split('/').pop(); - - // 从MinIO删除文件 - await deleteFile(filename); + const filePath = path.join(__dirname, '../uploads', filename); + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } // 从数据库删除记录 await photo.destroy(); @@ -249,13 +251,20 @@ router.delete('/photos/:id', auth, async (req, res) => { // 添加赞助商 router.post('/sponsors', auth, upload.single('logo'), async (req, res) => { try { - const { activityId, name, type, link } = req.body; + const { activityId, name, type, link, sortOrder } = req.body; let logo = null; if (req.file) { - const [uploaded] = await uploadFilesToMinio(req.file); + const [uploaded] = getFileUrls(req.file); logo = uploaded.url; } - const sponsor = await Sponsor.create({ activityId, name, logo, type, link }); + const sponsor = await Sponsor.create({ + activityId, + name, + logo, + type, + link, + sortOrder: sortOrder || 0 + }); res.json(sponsor); } catch (error) { res.status(500).json({ error: error.message }); @@ -265,11 +274,15 @@ router.post('/sponsors', auth, upload.single('logo'), async (req, res) => { // 更新赞助商 router.put('/sponsors/:id', auth, upload.single('logo'), async (req, res) => { try { - const { name, type, link } = req.body; + const { name, type, link, sortOrder } = req.body; const updateData = { name, type, link }; + if (sortOrder !== undefined) { + updateData.sortOrder = sortOrder; + } + if (req.file) { - const [uploaded] = await uploadFilesToMinio(req.file); + const [uploaded] = getFileUrls(req.file); updateData.logo = uploaded.url; } @@ -291,4 +304,57 @@ router.delete('/sponsors/:id', auth, async (req, res) => { } }); +// 添加奖品 +router.post('/prizes', auth, upload.single('image'), async (req, res) => { + try { + const { activityId, name, level, description, quantity, sortOrder } = req.body; + let image = null; + if (req.file) { + const [uploaded] = getFileUrls([req.file]); + image = uploaded.url; + } + const prize = await Prize.create({ + activityId, + name, + level, + description, + image, + quantity: quantity || 1, + sortOrder: sortOrder || 0 + }); + res.json(prize); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// 更新奖品 +router.put('/prizes/:id', auth, upload.single('image'), async (req, res) => { + try { + const { name, level, description, quantity, sortOrder } = req.body; + const updateData = { name, level, description, quantity, sortOrder }; + + if (req.file) { + const [uploaded] = getFileUrls([req.file]); + updateData.image = uploaded.url; + } + + await Prize.update(updateData, { where: { id: req.params.id } }); + const prize = await Prize.findByPk(req.params.id); + res.json(prize); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// 删除奖品 +router.delete('/prizes/:id', auth, async (req, res) => { + try { + await Prize.destroy({ where: { id: req.params.id } }); + res.json({ message: '删除成功' }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + module.exports = router; diff --git a/backend/server.js b/backend/server.js index 8812c16..deb2c3e 100644 --- a/backend/server.js +++ b/backend/server.js @@ -9,6 +9,9 @@ const app = express(); app.use(cors()); app.use(express.json()); +// 静态文件服务 - 提供上传的文件 +app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); + // API 路由 app.use('/api', require('./routes/activity')); app.use('/api', require('./routes/registration')); @@ -26,11 +29,25 @@ if (process.env.NODE_ENV === 'production') { } const PORT = process.env.PORT || 3000; -app.listen(PORT, () => { - console.log(`服务器运行在 http://localhost:${PORT}`); - if (process.env.NODE_ENV === 'production') { - console.log('运行模式: 生产环境(前后端一体)'); - } else { - console.log('运行模式: 开发环境(仅后端API)'); + +// 启动服务器前运行数据库迁移 +const MigrationManager = require('./migrations/index'); +const runMigrations = async () => { + try { + const manager = new MigrationManager(); + await manager.runMigrations(); + } catch (error) { + console.error('数据库迁移失败,但服务器将继续启动'); } +}; + +runMigrations().then(() => { + app.listen(PORT, () => { + console.log(`服务器运行在 http://localhost:${PORT}`); + if (process.env.NODE_ENV === 'production') { + console.log('运行模式: 生产环境(前后端一体)'); + } else { + console.log('运行模式: 开发环境(仅后端API)'); + } + }); }); diff --git a/build.bat b/build.bat index baf8326..fb7f8bb 100644 --- a/build.bat +++ b/build.bat @@ -1,10 +1,26 @@ @echo off REM 泉友米车友会 - Docker 构建脚本 (Windows) -REM 版本: 0.0.1 + +REM ========================================== +REM 版本号配置(修改这里即可) +REM ========================================== +set VERSION_FILE=.version +set MAJOR=0 +set MINOR=0 + +REM 读取或初始化补丁版本号 +if exist %VERSION_FILE% ( + set /p PATCH=<%VERSION_FILE% +) else ( + set PATCH=1 +) + +REM 组合完整版本号 +set VERSION=%MAJOR%.%MINOR%.%PATCH% echo ========================================== echo 泉友米车友会 Docker 镜像构建 -echo 版本: 0.0.1 +echo 版本: %VERSION% echo ========================================== REM 检查 Docker 是否安装 @@ -17,7 +33,7 @@ if errorlevel 1 ( echo. echo 开始构建 Docker 镜像... -docker build -t quanyoumi-app:0.0.1 -t quanyoumi-app:latest . +docker build -t quanyoumi-app:%VERSION% -t quanyoumi-app:latest . if errorlevel 1 ( echo. @@ -26,23 +42,30 @@ if errorlevel 1 ( exit /b 1 ) +REM 构建成功后自动递增版本号 +set /a NEXT_PATCH=%PATCH%+1 +echo %NEXT_PATCH%>%VERSION_FILE% + echo. echo ========================================== -echo 构建完成! +echo 构建完成!版本号已更新 echo ========================================== echo. +echo 当前版本: %VERSION% +echo 下次版本: %MAJOR%.%MINOR%.%NEXT_PATCH% +echo. echo 镜像信息: docker images | findstr quanyoumi-app echo. REM 标记镜像到阿里云仓库 echo 正在标记镜像到阿里云仓库... -docker tag quanyoumi-app:0.0.1 registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:0.0.1 +docker tag quanyoumi-app:%VERSION% registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:%VERSION% docker tag quanyoumi-app:latest registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:latest echo. echo 正在推送镜像到阿里云仓库... -docker push registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:0.0.1 +docker push registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:%VERSION% docker push registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:latest if errorlevel 1 ( @@ -58,12 +81,17 @@ echo ========================================== echo 推送完成! echo ========================================== echo. +echo 当前版本: %VERSION% +echo 下次版本: %MAJOR%.%MINOR%.%NEXT_PATCH% +echo. echo 镜像地址: -echo registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:0.0.1 +echo registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:%VERSION% echo registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:latest echo. echo 在 NAS 上使用: -echo docker pull registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:0.0.1 +echo docker pull registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:%VERSION% echo docker-compose up -d echo. +echo 提示: 如需修改主版本号或次版本号,请编辑 build.bat 文件顶部的 MAJOR 和 MINOR 变量 +echo. pause diff --git a/build.sh b/build.sh index 4f4ef91..6a4d37f 100644 --- a/build.sh +++ b/build.sh @@ -1,13 +1,29 @@ #!/bin/bash # 泉友米车友会 - Docker 构建脚本 -# 版本: 0.0.1 set -e +# ========================================== +# 版本号配置(修改这里即可) +# ========================================== +VERSION_FILE=".version" +MAJOR=0 +MINOR=0 + +# 读取或初始化补丁版本号 +if [ -f "$VERSION_FILE" ]; then + PATCH=$(cat "$VERSION_FILE") +else + PATCH=1 +fi + +# 组合完整版本号 +VERSION="$MAJOR.$MINOR.$PATCH" + echo "==========================================" echo "泉友米车友会 Docker 镜像构建" -echo "版本: 0.0.1" +echo "版本: $VERSION" echo "==========================================" # 检查 Docker 是否安装 @@ -19,25 +35,32 @@ fi # 构建镜像 echo "" echo "开始构建 Docker 镜像..." -docker build -t quanyoumi-app:0.0.1 -t quanyoumi-app:latest . +docker build -t quanyoumi-app:$VERSION -t quanyoumi-app:latest . + +# 构建成功后自动递增版本号 +NEXT_PATCH=$((PATCH + 1)) +echo $NEXT_PATCH > "$VERSION_FILE" echo "" echo "==========================================" -echo "构建完成!" +echo "构建完成!版本号已更新" echo "==========================================" echo "" +echo "当前版本: $VERSION" +echo "下次版本: $MAJOR.$MINOR.$NEXT_PATCH" +echo "" echo "镜像信息:" docker images | grep quanyoumi-app echo "" # 标记镜像到阿里云仓库 echo "正在标记镜像到阿里云仓库..." -docker tag quanyoumi-app:0.0.1 registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:0.0.1 +docker tag quanyoumi-app:$VERSION registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:$VERSION docker tag quanyoumi-app:latest registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:latest echo "" echo "正在推送镜像到阿里云仓库..." -docker push registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:0.0.1 +docker push registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:$VERSION docker push registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:latest echo "" @@ -45,11 +68,16 @@ echo "==========================================" echo "推送完成!" echo "==========================================" echo "" +echo "当前版本: $VERSION" +echo "下次版本: $MAJOR.$MINOR.$NEXT_PATCH" +echo "" echo "镜像地址:" -echo " registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:0.0.1" +echo " registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:$VERSION" echo " registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:latest" echo "" echo "在 NAS 上使用:" -echo " docker pull registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:0.0.1" +echo " docker pull registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:$VERSION" echo " docker-compose up -d" echo "" +echo "提示: 如需修改主版本号或次版本号,请编辑 build.sh 文件顶部的 MAJOR 和 MINOR 变量" +echo "" diff --git a/clean-db.bat b/clean-db.bat new file mode 100644 index 0000000..947e950 --- /dev/null +++ b/clean-db.bat @@ -0,0 +1,19 @@ +@echo off +echo ========================================== +echo 清理数据库文件 +echo ========================================== + +echo 停止容器... +docker-compose down + +echo 删除数据库文件... +if exist backend\data\database.sqlite ( + del /f backend\data\database.sqlite + echo 数据库文件已删除 +) else ( + echo 数据库文件不存在 +) + +echo. +echo 清理完成!现在可以运行 start.bat 重新启动 +pause diff --git a/docker-compose.yml b/docker-compose.yml index 6797397..f2bb2f2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ version: '3.8' services: # 应用服务(前端+后端) app: - image: registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:0.0.1 + image: registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:0.0.2 container_name: quanyoumi-app restart: unless-stopped ports: @@ -11,28 +11,14 @@ services: environment: - NODE_ENV=production - PORT=3000 - # 数据库配置 - - DB_HOST=db - - DB_PORT=3306 - - DB_USER=root - - DB_PASSWORD=quanyoumi2024 - - DB_NAME=quanyoumi # JWT 密钥(请修改为随机字符串) - JWT_SECRET=quanyoumi_jwt_secret_change_this_in_production_2024 # 高德地图配置(必填) - AMAP_KEY=fd5b77136ba34fe6b4f0f86dca7782d9 - AMAP_SECURITY_CODE=7f61638e5635d944a534ff747dc615ac - # MinIO 配置 - - MINIO_ENDPOINT=minio - - MINIO_PORT=9000 - - MINIO_ACCESS_KEY=minioadmin - - MINIO_SECRET_KEY=minioadmin123 - - MINIO_BUCKET=quanyoumi volumes: + - app-data:/app/data - app-uploads:/app/uploads - depends_on: - - db - - minio networks: - quanyoumi-network healthcheck: @@ -42,54 +28,8 @@ services: retries: 3 start_period: 10s - # MySQL 数据库 - db: - image: mysql:8.0 - container_name: quanyoumi-db - restart: unless-stopped - environment: - - MYSQL_ROOT_PASSWORD=quanyoumi2024 - - MYSQL_DATABASE=quanyoumi - - MYSQL_CHARACTER_SET_SERVER=utf8mb4 - - MYSQL_COLLATION_SERVER=utf8mb4_unicode_ci - volumes: - - mysql-data:/var/lib/mysql - - ./database.sql:/docker-entrypoint-initdb.d/init.sql - networks: - - quanyoumi-network - healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-pquanyoumi2024"] - interval: 10s - timeout: 5s - retries: 5 - start_period: 30s - - # MinIO 对象存储 - minio: - image: minio/minio:latest - container_name: quanyoumi-minio - restart: unless-stopped - command: server /data --console-address ":9001" - environment: - - MINIO_ROOT_USER=minioadmin - - MINIO_ROOT_PASSWORD=minioadmin123 - volumes: - - minio-data:/data - ports: - - "9001:9001" - networks: - - quanyoumi-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] - interval: 30s - timeout: 20s - retries: 3 - start_period: 10s - volumes: - mysql-data: - driver: local - minio-data: + app-data: driver: local app-uploads: driver: local diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 4511471..2686e46 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -5,6 +5,24 @@ echo "==========================================" echo "泉友米车友会 - 启动中..." echo "==========================================" +# 检查数据库文件 +DB_FILE="/app/data/database.sqlite" +if [ -f "$DB_FILE" ]; then + echo "✓ 发现已存在的数据库文件" + # 检查是否有备份表残留(这会导致sync失败) + if sqlite3 "$DB_FILE" "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%_backup%';" 2>/dev/null | grep -q "_backup"; then + echo "⚠ 检测到备份表残留,清理中..." + sqlite3 "$DB_FILE" "SELECT 'DROP TABLE IF EXISTS ' || name || ';' FROM sqlite_master WHERE type='table' AND name LIKE '%_backup%';" | sqlite3 "$DB_FILE" 2>/dev/null || true + echo "✓ 备份表已清理" + fi +else + echo "✓ 首次启动,将创建新数据库" +fi + +# 初始化数据库 +echo "初始化数据库..." +node init-admin.js + # 替换前端 index.html 中的环境变量占位符 if [ -f "/app/public/index.html" ]; then echo "配置高德地图 Key..." diff --git a/frontend/map-test.html b/frontend/map-test.html deleted file mode 100644 index b6e73ed..0000000 --- a/frontend/map-test.html +++ /dev/null @@ -1,201 +0,0 @@ - - - - - - 高德地图搜索测试 - - - -

高德地图搜索功能测试

- - - -
- -
- -
- - - - diff --git a/frontend/public/avatars/wang.png b/frontend/public/avatars/wang.png new file mode 100644 index 0000000..5f0ae7d Binary files /dev/null and b/frontend/public/avatars/wang.png differ diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 93cb2b8..524609d 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -1,7 +1,7 @@ import axios from 'axios'; const api = axios.create({ - baseURL: '/api', + baseURL: 'https://mi.xyvan.cn/api', timeout: 10000 }); @@ -13,6 +13,29 @@ api.interceptors.request.use(config => { return config; }); +// 响应拦截器:确保所有错误都能被正确捕获 +api.interceptors.response.use( + response => response, + error => { + // 统一错误处理 + console.error('API请求错误:', error); + return Promise.reject(error); + } +); + +// 获取完整的图片URL +export const getImageUrl = (path) => { + if (!path) return ''; + if (path.startsWith('http://') || path.startsWith('https://')) { + return path; + } + // 开发环境使用代理,生产环境使用完整域名 + if (import.meta.env.DEV) { + return `http://localhost:3000${path}`; + } + return `https://mi.xyvan.cn${path}`; +}; + export default { // 活动相关 getActivities: (params) => api.get('/activities', { params }), @@ -65,5 +88,14 @@ export default { headers: { 'Content-Type': 'multipart/form-data' }, timeout: 60000 }), - deleteSponsor: (id) => api.delete(`/admin/sponsors/${id}`) + deleteSponsor: (id) => api.delete(`/admin/sponsors/${id}`), + addPrize: (data) => api.post('/admin/prizes', data, { + headers: { 'Content-Type': 'multipart/form-data' }, + timeout: 60000 + }), + updatePrize: (id, data) => api.put(`/admin/prizes/${id}`, data, { + headers: { 'Content-Type': 'multipart/form-data' }, + timeout: 60000 + }), + deletePrize: (id) => api.delete(`/admin/prizes/${id}`) }; diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 4925153..bff66b4 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -14,7 +14,12 @@ const routes = [ { path: '/activity/:id', name: 'ActivityDetail', - component: () => import('../views/ActivityDetail.vue') + component: () => import('../views/ActivityDetail/index.vue') + }, + { + path: '/danmaku-demo', + name: 'DanmakuDemo', + component: () => import('../views/ActivityDetail/DanmakuDemo.vue') }, { path: '/register/:id', diff --git a/frontend/src/views/ActivityDetail.vue b/frontend/src/views/ActivityDetail.vue deleted file mode 100644 index 0a4e7d9..0000000 --- a/frontend/src/views/ActivityDetail.vue +++ /dev/null @@ -1,1442 +0,0 @@ - - - - - diff --git a/frontend/src/views/ActivityDetail/DanmakuDemo.vue b/frontend/src/views/ActivityDetail/DanmakuDemo.vue new file mode 100644 index 0000000..e02e160 --- /dev/null +++ b/frontend/src/views/ActivityDetail/DanmakuDemo.vue @@ -0,0 +1,292 @@ + + + + + diff --git a/frontend/src/views/ActivityDetail/SponsorDanmaku.vue b/frontend/src/views/ActivityDetail/SponsorDanmaku.vue new file mode 100644 index 0000000..20ddea5 --- /dev/null +++ b/frontend/src/views/ActivityDetail/SponsorDanmaku.vue @@ -0,0 +1,266 @@ + + + + + diff --git a/frontend/src/views/ActivityDetail/index.vue b/frontend/src/views/ActivityDetail/index.vue new file mode 100644 index 0000000..7607414 --- /dev/null +++ b/frontend/src/views/ActivityDetail/index.vue @@ -0,0 +1,638 @@ + + + + + diff --git a/frontend/src/views/ActivityDetail/styles.css b/frontend/src/views/ActivityDetail/styles.css new file mode 100644 index 0000000..4787bed --- /dev/null +++ b/frontend/src/views/ActivityDetail/styles.css @@ -0,0 +1,936 @@ +.detail { + min-height: 100vh; + background: #f8f9fa; + padding-bottom: 140px; +} + +.detail-header { + position: relative; + height: 280px; + background-size: cover; + background-position: center; + background-color: #e8e8e8; +} + +.header-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(180deg, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0.6) 100%); +} + +.header-back { + position: absolute; + top: 16px; + left: 16px; + z-index: 2; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255,255,255,0.3); + border-radius: 50%; + color: #fff; + font-size: 20px; + backdrop-filter: blur(10px); +} + +.header-content { + position: absolute; + bottom: 24px; + left: 16px; + right: 16px; + z-index: 1; +} + +.activity-title { + font-size: 24px; + font-weight: bold; + color: #fff; + margin: 0; + text-shadow: 0 2px 8px rgba(0,0,0,0.3); +} + +.content { + padding: 16px; + margin-top: -20px; + position: relative; + z-index: 1; +} + +.info-card { + background: #fff; + border-radius: 16px; + padding: 16px; + box-shadow: 0 2px 12px rgba(0,0,0,0.08); + margin-bottom: 16px; +} + +.info-item { + display: flex; + align-items: center; + padding: 12px 0; + border-bottom: 1px solid #f0f0f0; +} + +.info-item:last-child { + border-bottom: none; +} + +.info-arrow { + font-size: 16px; + color: #999; + margin-left: 8px; +} + +.info-icon { + font-size: 24px; + color: #FF6700; + margin-right: 12px; +} + +.info-text { + flex: 1; +} + +.info-label { + font-size: 12px; + color: #999; + margin-bottom: 4px; +} + +.info-value { + font-size: 14px; + color: #333; + font-weight: 500; +} + +.price-value { + color: #FF6700; + font-weight: 600; + font-size: 16px; +} + +.section { + background: #fff; + border-radius: 16px; + padding: 16px; + margin-bottom: 16px; + box-shadow: 0 2px 12px rgba(0,0,0,0.08); +} + +.section-title { + font-size: 16px; + font-weight: bold; + margin: 0 0 16px; + color: #333; +} + +.section-title .count { + color: #FF6700; + font-size: 14px; +} + +.description { + font-size: 14px; + line-height: 1.8; + color: #666; + white-space: pre-line; +} + +/* Timeline样式 */ +.timeline { + position: relative; + padding-left: 32px; +} + +.timeline::before { + content: ''; + position: absolute; + left: 8px; + top: 8px; + bottom: 8px; + width: 2px; + background: linear-gradient(180deg, #FF6700 0%, #FF8533 100%); +} + +.timeline-item { + position: relative; + padding-bottom: 24px; +} + +.timeline-item-last { + padding-bottom: 0; +} + +.timeline-dot { + position: absolute; + left: -28px; + top: 4px; + width: 12px; + height: 12px; + border-radius: 50%; + background: #fff; + border: 3px solid #FF6700; + box-shadow: 0 0 0 3px rgba(255, 103, 0, 0.1); + z-index: 1; +} + +.timeline-content { + background: #fff; + border-radius: 12px; + padding: 12px 16px; + border: 1px solid #f0f0f0; + box-shadow: 0 2px 8px rgba(0,0,0,0.04); + transition: all 0.3s; + cursor: pointer; +} + +.timeline-content:active { + transform: scale(0.98); +} + +.timeline-content.expanded { + box-shadow: 0 4px 12px rgba(255, 103, 0, 0.15); + border-color: #FF6700; +} + +.timeline-time { + font-size: 13px; + color: #FF6700; + font-weight: 600; + margin-bottom: 6px; + display: flex; + align-items: center; +} + +.timeline-time::before { + content: '🕐'; + margin-right: 6px; +} + +.time-text { + margin-right: auto; +} + +.expand-icon { + font-size: 16px; + color: #999; + transition: transform 0.3s; + margin-left: 8px; +} + +.timeline-title { + font-size: 15px; + color: #333; + font-weight: 600; + margin-bottom: 8px; +} + +.timeline-brief { + font-size: 13px; + color: #666; + line-height: 1.6; + margin-bottom: 0; +} + +.timeline-detail { + max-height: 0; + overflow: hidden; + transition: max-height 0.3s ease-out; +} + +.timeline-detail.expanded { + max-height: 3000px; + transition: max-height 0.5s ease-in; +} + +.detail-content { + margin-top: 12px; +} + +.flow-steps { + margin-bottom: 12px; +} + +.step-item { + padding: 8px 12px; + margin-bottom: 8px; + background: #fff; + border-radius: 6px; + border-left: 3px solid #FF6700; + font-size: 13px; + color: #666; + line-height: 1.6; +} + +.step-item:last-child { + margin-bottom: 0; +} + +.tip-box { + padding: 10px 12px; + background: #fff7e6; + border-radius: 6px; + font-size: 13px; + color: #ff9800; + line-height: 1.6; +} + +.game-rule { + margin-top: 12px; + padding: 12px; + background: #f8f9fa; + border-radius: 8px; + border-left: 3px solid #FF6700; +} + +.rule-title { + font-size: 14px; + font-weight: 600; + color: #333; + margin-bottom: 8px; +} + +.rule-content { + font-size: 13px; + color: #666; + line-height: 1.8; +} + +.score-badge { + display: inline-block; + padding: 2px 8px; + background: linear-gradient(135deg, #FF6700 0%, #FF8533 100%); + color: #fff; + border-radius: 12px; + font-size: 12px; + font-weight: 600; + margin: 0 2px; +} + +/* 游戏卡片样式 */ +.game-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.game-card { + padding: 12px; + background: #fff; + border-radius: 8px; + border: 1px solid #e8e8e8; + cursor: pointer; + transition: all 0.2s; +} + +.game-card:active { + transform: scale(0.98); +} + +.game-card.expanded { + border-color: #FF6700; + background: #fff7e6; +} + +.game-card.highlight-game { + background: linear-gradient(135deg, #fff7e6 0%, #fff 100%); + border-color: #FF6700; +} + +.game-header { + display: flex; + align-items: center; + justify-content: space-between; +} + +.game-title { + font-size: 14px; + font-weight: 600; + color: #333; +} + +.game-detail { + max-height: 0; + overflow: hidden; + transition: max-height 0.3s ease-out; + margin-top: 0; +} + +.game-detail.expanded { + max-height: 500px; + transition: max-height 0.5s ease-in; + margin-top: 12px; +} + +.game-desc { + font-size: 13px; + color: #666; + line-height: 1.8; + padding: 10px; + background: #fff; + border-radius: 6px; +} + +/* 其他组件样式 */ +.sponsor-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; + max-width: 600px; + margin: 0 auto; +} + +.sponsor-card { + text-align: center; + padding: 12px; + background: #fff; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,0.04); + transition: transform 0.2s; +} + +.sponsor-card:active { + transform: scale(0.98); +} + +.sponsor-logo { + width: 60px; + height: 60px; + margin: 0 auto 10px; + background-size: cover; + background-position: center; + background-color: #f5f5f5; + border-radius: 10px; +} + +.sponsor-name { + font-size: 13px; + color: #333; + font-weight: 600; + line-height: 1.4; + width: 100%; + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + word-break: break-word; +} + +.photo-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 8px; +} + +.photo-item { + aspect-ratio: 1; + border-radius: 8px; + overflow: hidden; +} + +.photo-img { + width: 100%; + height: 100%; + background-size: cover; + background-position: center; + background-color: #f5f5f5; +} + +.media-section { + margin-bottom: 20px; +} + +.media-label { + font-size: 14px; + font-weight: 500; + color: #666; + margin-bottom: 12px; +} + +.video-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.video-item { + border-radius: 12px; + overflow: hidden; + background: #000; +} + +.video-player { + width: 100%; + max-height: 300px; + display: block; +} + +.member-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.member-item { + display: flex; + align-items: center; + gap: 12px; +} + +.member-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + background: linear-gradient(135deg, #FF6700 0%, #FF8533 100%); + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + font-weight: bold; +} + +.member-info { + flex: 1; +} + +.member-name { + font-size: 14px; + color: #333; + font-weight: 500; + margin-bottom: 4px; +} + +.member-car { + font-size: 12px; + color: #999; +} + +.expense-list { + display: flex; + flex-direction: column; + gap: 16px; +} + +.expense-item { + display: flex; + flex-direction: column; + padding: 16px; + background: #fff; + border-radius: 12px; + border: 1px solid #f0f0f0; + box-shadow: 0 2px 8px rgba(0,0,0,0.04); +} + +.expense-info { + flex: 1; +} + +.expense-name { + font-size: 15px; + color: #333; + font-weight: 600; + margin-bottom: 6px; +} + +.expense-category { + display: inline-block; + font-size: 11px; + color: #FF6700; + background: #fff7e6; + padding: 2px 8px; + border-radius: 4px; + margin-bottom: 8px; +} + +.expense-media { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 8px; + margin-top: 12px; +} + +.expense-media-item { + width: 100%; + aspect-ratio: 1; + border-radius: 8px; + overflow: hidden; + background: #f5f5f5; + cursor: pointer; + transition: transform 0.2s; +} + +.expense-media-item:active { + transform: scale(0.95); +} + +.expense-video { + position: relative; + width: 100%; + height: 100%; + cursor: pointer; +} + +.expense-video video { + width: 100%; + height: 100%; + object-fit: cover; +} + +.expense-video .play-icon { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 28px; + color: #fff; + filter: drop-shadow(0 2px 4px rgba(0,0,0,0.5)); +} + +.expense-amount { + font-size: 15px; + color: #FF6700; + font-weight: bold; + text-align: right; + margin-top: 12px; + padding-top: 12px; + border-top: 1px dashed #f0f0f0; +} + +.upload-section { + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid #f0f0f0; +} + +.upload-tip { + font-size: 12px; + color: #999; + margin-top: 8px; +} + +/* 底部按钮和弹窗样式 */ +.footer-action { + position: fixed; + bottom: 0; + left: 0; + right: 0; + padding: 12px 16px; + background: #fff; + box-shadow: 0 -2px 12px rgba(0,0,0,0.08); + z-index: 10; + display: flex; + flex-direction: column; + gap: 8px; +} + +.action-button { + height: 48px; + background: linear-gradient(135deg, #FF6700 0%, #FF8533 100%); + border: none; + font-weight: 600; +} + +.action-button .van-icon { + margin-right: 4px; +} + +.login-button { + margin-top: 0; + background: linear-gradient(135deg, #FF6700 0%, #FF8533 100%) !important; + color: #fff !important; +} + +.share-btn { + position: fixed; + top: 16px; + right: 16px; + z-index: 20; + display: flex; + align-items: center; + gap: 4px; + padding: 8px 16px; + background: rgba(255,255,255,0.95); + border-radius: 20px; + color: #FF6700; + font-size: 14px; + font-weight: 500; + box-shadow: 0 2px 8px rgba(0,0,0,0.15); + backdrop-filter: blur(10px); + cursor: pointer; +} + +.map-container { + height: 100%; + display: flex; + flex-direction: column; +} + +.map-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + border-bottom: 1px solid #f0f0f0; +} + +.map-header h3 { + margin: 0; + font-size: 16px; +} + +.map-header .van-icon { + font-size: 20px; + color: #999; +} + +.login-form { + padding: 16px; +} + +/* 车牌号字段样式优化 - 标签和输入框同行显示 */ +.login-form .van-field { + display: flex; + align-items: center; + flex-wrap: nowrap; +} + +.login-form .van-field__label { + width: 60px; + flex-shrink: 0; + margin-right: 8px; +} + +.login-form .van-field__control { + flex: 1; + display: flex; + align-items: center; +} + +.login-form .van-field__control--custom { + padding: 0; +} + +.permission-tip { + display: flex; + align-items: center; + gap: 8px; + padding: 12px; + background: #fff7e6; + border-radius: 8px; + color: #ff9800; + font-size: 14px; +} + +.permission-tip .van-icon { + font-size: 18px; +} + +.video-player-popup { + padding: 20px; + background: #000; + border-radius: 16px; +} + +.popup-video { + width: 100%; + max-height: 60vh; + border-radius: 12px; +} + +/* 车牌输入样式 */ +.license-plate-input { + display: flex; + align-items: center; + flex-wrap: nowrap; + gap: 3px; + padding: 5px 8px; + background: linear-gradient(135deg, #FF6700 0%, #FF8533 100%); + border-radius: 6px; + box-shadow: 0 2px 8px rgba(255, 103, 0, 0.3); + width: fit-content; +} + +.plate-prefix { + color: #fff; + font-weight: 700; + font-size: 15px; + text-shadow: 0 1px 2px rgba(0,0,0,0.2); +} + +.plate-letter { + width: 20px; + height: 26px; + border: 2px solid rgba(255,255,255,0.9); + border-radius: 3px; + background: #fff; + text-align: center; + font-size: 15px; + font-weight: 700; + color: #333; + outline: none; + text-transform: uppercase; + padding: 0; +} + +.plate-dot { + color: #fff; + font-size: 16px; + font-weight: 700; + margin: 0 1px; +} + +.plate-digits { + width: 95px; + height: 26px; + border: 2px solid rgba(255,255,255,0.9); + border-radius: 3px; + background: #fff; + text-align: center; + font-size: 15px; + font-weight: 700; + color: #333; + outline: none; + letter-spacing: 1px; + text-transform: uppercase; + padding: 0; +} + +.plate-letter::placeholder, +.plate-digits::placeholder { + color: #ddd; + font-weight: 400; +} + +/* 奖品礼品样式 */ +.prize-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.prize-card { + display: flex; + gap: 12px; + padding: 12px; + background: linear-gradient(135deg, #fff 0%, #fffbf5 100%); + border-radius: 12px; + border: 1px solid #ffe8cc; + box-shadow: 0 2px 8px rgba(255, 103, 0, 0.08); + transition: transform 0.2s; +} + +.prize-card:active { + transform: scale(0.98); +} + +.prize-image { + width: 70px; + height: 70px; + flex-shrink: 0; + border-radius: 8px; + background-size: cover; + background-position: center; + background-color: #f5f5f5; + display: flex; + align-items: center; + justify-content: center; + border: 2px solid #ffe8cc; +} + +.prize-content { + flex: 1; + display: flex; + flex-direction: column; + gap: 6px; + min-width: 0; +} + +.prize-header { + display: flex; + flex-direction: column; + gap: 4px; +} + +.prize-level { + display: inline-block; + padding: 3px 10px; + border-radius: 12px; + font-size: 11px; + font-weight: 600; + white-space: nowrap; + width: fit-content; +} + +.level-一等奖 { + background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%); + color: #fff; + box-shadow: 0 2px 6px rgba(255, 215, 0, 0.4); +} + +.level-二等奖 { + background: linear-gradient(135deg, #C0C0C0 0%, #A8A8A8 100%); + color: #fff; + box-shadow: 0 2px 6px rgba(192, 192, 192, 0.4); +} + +.level-三等奖 { + background: linear-gradient(135deg, #CD7F32 0%, #B8733E 100%); + color: #fff; + box-shadow: 0 2px 6px rgba(205, 127, 50, 0.4); +} + +.level-参与奖 { + background: linear-gradient(135deg, #FF6700 0%, #FF8533 100%); + color: #fff; + box-shadow: 0 2px 6px rgba(255, 103, 0, 0.3); +} + +.level-伴手礼 { + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + color: #fff; + box-shadow: 0 2px 6px rgba(240, 147, 251, 0.3); +} + +.prize-name { + font-size: 14px; + font-weight: 600; + color: #333; + line-height: 1.4; + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + word-break: break-word; +} + +.prize-desc { + font-size: 12px; + color: #666; + line-height: 1.5; + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + word-break: break-word; +} + +.prize-footer { + display: flex; + align-items: center; + justify-content: flex-start; + margin-top: auto; + padding-top: 6px; + border-top: 1px dashed #ffe8cc; +} + +.prize-quantity { + font-size: 12px; + color: #FF6700; + font-weight: 500; +} diff --git a/frontend/src/views/ActivityDetail/useActivityDetail.js b/frontend/src/views/ActivityDetail/useActivityDetail.js new file mode 100644 index 0000000..77a6632 --- /dev/null +++ b/frontend/src/views/ActivityDetail/useActivityDetail.js @@ -0,0 +1,256 @@ +import { ref, computed, onMounted } from 'vue'; +import { useRoute } from 'vue-router'; +import { showImagePreview, showToast } from 'vant'; +import api from '../../api'; + +export function useActivityDetail() { + const route = useRoute(); + const activity = ref({}); + const photos = ref([]); + const videos = ref([]); + const sponsors = ref([]); + const prizes = ref([]); + const registrations = ref([]); + const expenses = ref([]); + const userMedia = ref([]); + const showMapPopup = ref(false); + const showLoginDialog = ref(false); + const showExpenseVideoPopup = ref(false); + const currentExpenseVideo = ref(''); + const loginForm = ref({ phone: '', carNumber: '' }); + const expandedTimelineIndex = ref(null); + const expandedGameId = ref(null); + const carLetter = ref(''); + const carDigits = ref(''); + const userRegistration = ref(null); + let map = null; + + const handleLetterInput = (e) => { + const value = e.target.value.toUpperCase().replace(/[^A-Z]/g, ''); + carLetter.value = value; + loginForm.value.carNumber = value + carDigits.value; + + if (value.length === 1) { + e.target.nextElementSibling?.nextElementSibling?.focus(); + } + }; + + const handleDigitsInput = (e) => { + const value = e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, ''); + carDigits.value = value; + loginForm.value.carNumber = carLetter.value + value; + }; + + const toggleTimeline = (index) => { + expandedTimelineIndex.value = expandedTimelineIndex.value === index ? null : index; + expandedGameId.value = null; + }; + + const toggleGame = (gameId) => { + expandedGameId.value = expandedGameId.value === gameId ? null : gameId; + }; + + const totalExpense = computed(() => { + return expenses.value.reduce((sum, e) => sum + parseFloat(e.amount), 0).toFixed(2); + }); + + const isApproved = computed(() => { + return userRegistration.value && userRegistration.value.status === 'approved'; + }); + + const hasRegistered = computed(() => { + return userRegistration.value !== null; + }); + + const loadData = async () => { + const { data } = await api.getActivity(route.params.id); + activity.value = data.activity; + + const allMedia = data.photos || []; + photos.value = allMedia.filter(m => m.type === 'image'); + videos.value = allMedia.filter(m => m.type === 'video'); + + sponsors.value = data.sponsors; + prizes.value = data.prizes || []; + expenses.value = data.expenses || []; + + const { data: regs } = await api.getActivityRegistrations(route.params.id); + registrations.value = regs; + + const savedReg = localStorage.getItem(`user_reg_${route.params.id}`); + if (savedReg) { + userRegistration.value = JSON.parse(savedReg); + } + }; + + const formatDate = (date) => { + return new Date(date).toLocaleString('zh-CN'); + }; + + const previewImage = (url) => { + showImagePreview([url]); + }; + + const playExpenseVideo = (url) => { + currentExpenseVideo.value = url; + showExpenseVideoPopup.value = true; + }; + + const showMap = () => { + if (!activity.value.locationLat || !activity.value.locationLng) { + showToast('暂无地图信息'); + return; + } + showMapPopup.value = true; + setTimeout(() => { + initMap(); + }, 300); + }; + + const initMap = async () => { + try { + const AMap = await window.loadAMap(); + + if (map) { + map.destroy(); + } + + const center = [activity.value.locationLng, activity.value.locationLat]; + map = new AMap.Map('amap-container', { zoom: 15, center }); + new AMap.Marker({ position: center, map, title: activity.value.location }); + } catch (e) { + console.error('地图加载失败:', e); + showToast('地图加载失败: ' + e.message); + } + }; + + const uploadUserMedia = async (files) => { + if (!isApproved.value) { + showToast('请先登录并等待审核通过'); + return; + } + + const loadingToast = showToast({ + type: 'loading', + message: '上传中...', + forbidClick: true, + duration: 0 + }); + + try { + const formData = new FormData(); + formData.append('uploadedBy', userRegistration.value.name); + + if (Array.isArray(files)) { + files.forEach(file => formData.append('photos', file.file)); + } else { + formData.append('photos', files.file); + } + + await api.uploadUserPhotos(route.params.id, formData); + showToast('上传成功,等待审核'); + userMedia.value = []; + } catch (error) { + showToast('上传失败'); + } finally { + loadingToast.clear(); + } + }; + + const handleLogin = () => { + showLoginDialog.value = true; + }; + + const submitLogin = async () => { + if (!loginForm.value.phone || !loginForm.value.carNumber) { + showToast('请输入手机号和车牌号'); + return; + } + + try { + const carNumberWithPrefix = `闽${loginForm.value.carNumber}`; + + const { data } = await api.verifyUser(route.params.id, { + phone: loginForm.value.phone, + carNumber: carNumberWithPrefix + }); + + userRegistration.value = data.registration; + localStorage.setItem(`user_reg_${route.params.id}`, JSON.stringify(data.registration)); + + showLoginDialog.value = false; + + if (data.registration.status === 'approved') { + showToast('登录成功'); + } else if (data.registration.status === 'pending') { + showToast('您的报名正在审核中'); + } else { + showToast('您的报名未通过审核'); + } + } catch (error) { + showToast(error.response?.data?.error || '验证失败'); + } + }; + + const handleShare = () => { + const shareUrl = `${window.location.origin}/share/${route.params.id}`; + + if (navigator.share) { + navigator.share({ + title: activity.value.title, + text: `${activity.value.title} - 泉友米车友会`, + url: shareUrl + }).catch(() => { + copyShareLink(shareUrl); + }); + } else { + copyShareLink(shareUrl); + } + }; + + const copyShareLink = (url) => { + navigator.clipboard.writeText(url).then(() => { + showToast('分享链接已复制'); + }).catch(() => { + showToast('复制失败,请手动复制:' + url); + }); + }; + + onMounted(loadData); + + return { + activity, + photos, + videos, + sponsors, + prizes, + registrations, + expenses, + userMedia, + showMapPopup, + showLoginDialog, + showExpenseVideoPopup, + currentExpenseVideo, + loginForm, + carLetter, + carDigits, + userRegistration, + expandedTimelineIndex, + expandedGameId, + totalExpense, + isApproved, + hasRegistered, + formatDate, + previewImage, + playExpenseVideo, + showMap, + uploadUserMedia, + handleLogin, + handleLetterInput, + handleDigitsInput, + submitLogin, + handleShare, + toggleTimeline, + toggleGame + }; +} diff --git a/frontend/src/views/ActivityDetail_fixed.vue b/frontend/src/views/ActivityDetail_fixed.vue deleted file mode 100644 index 33ea90c..0000000 --- a/frontend/src/views/ActivityDetail_fixed.vue +++ /dev/null @@ -1,1577 +0,0 @@ - - - - - - - -
- -
-
- - - -
- - -
-
- - - - - - diff --git a/frontend/src/views/LandingPage.vue b/frontend/src/views/LandingPage.vue index e6f8baa..5a22574 100644 --- a/frontend/src/views/LandingPage.vue +++ b/frontend/src/views/LandingPage.vue @@ -713,27 +713,25 @@ onUnmounted(() => { } .sponsors-grid { - display: flex; - justify-content: center; - align-items: stretch; - gap: 16px; + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 20px; margin-top: 32px; - flex-wrap: wrap; + max-width: 800px; + margin-left: auto; + margin-right: auto; } .sponsor-card { background: #fff; border-radius: 12px; - padding: 12px; + padding: 16px; display: flex; flex-direction: column; align-items: center; text-align: center; box-shadow: 0 2px 12px rgba(0,0,0,0.06); transition: transform 0.3s, box-shadow 0.3s; - flex: 0 1 140px; - max-width: 150px; - min-width: 120px; } .sponsor-card:hover { @@ -742,12 +740,12 @@ onUnmounted(() => { } .sponsor-logo { - width: 45px; - height: 45px; + width: 60px; + height: 60px; display: flex; align-items: center; justify-content: center; - margin-bottom: 8px; + margin-bottom: 12px; } .sponsor-logo svg { @@ -768,20 +766,30 @@ onUnmounted(() => { } .sponsor-name { - font-size: 11px; + font-size: 13px; font-weight: 600; color: #333; - margin: 0 0 4px; - line-height: 1.3; - word-break: break-all; + margin: 0 0 6px; + line-height: 1.4; + width: 100%; + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + word-break: break-word; } .sponsor-desc { - font-size: 10px; + font-size: 11px; color: #999; margin: 0; line-height: 1.4; - word-break: break-all; + width: 100%; + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + word-break: break-word; } /* CTA区域 */ @@ -930,28 +938,26 @@ onUnmounted(() => { } .sponsors-grid { - gap: 12px; + grid-template-columns: 1fr; + gap: 16px; } .sponsor-card { - flex: 0 1 calc(20% - 10px); - min-width: 100px; - max-width: 130px; - padding: 10px; + padding: 16px; } .sponsor-logo { - width: 40px; - height: 40px; - margin-bottom: 6px; + width: 50px; + height: 50px; + margin-bottom: 10px; } .sponsor-name { - font-size: 10px; + font-size: 12px; } .sponsor-desc { - font-size: 9px; + font-size: 10px; } } diff --git a/frontend/src/views/Register.vue b/frontend/src/views/Register.vue index f9603b0..e6f4a33 100644 --- a/frontend/src/views/Register.vue +++ b/frontend/src/views/Register.vue @@ -347,7 +347,7 @@ const onSubmit = async () => { } .plate-digits { - width: 110px; + width: 95px; height: 28px; border: 2px solid rgba(255,255,255,0.9); border-radius: 3px; diff --git a/frontend/src/views/admin/ActivityManage.vue b/frontend/src/views/admin/ActivityManage.vue index 1639d12..8e44e45 100644 --- a/frontend/src/views/admin/ActivityManage.vue +++ b/frontend/src/views/admin/ActivityManage.vue @@ -64,6 +64,16 @@ /> + + + @@ -139,7 +149,7 @@