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 @@
-
-
-
-
-
-
-
-
-
-
活动时间
-
{{ formatDate(activity.startTime) }}
-
-
-
-
-
-
活动地点
-
{{ activity.location }}
-
-
-
-
-
-
-
报名截止
-
{{ formatDate(activity.registrationDeadline) }}
-
-
-
-
-
-
人均费用
-
{{ activity.price > 0 ? `¥${activity.price}` : '免费' }}
-
-
-
-
-
-
活动介绍
-
{{ activity.description }}
-
-
-
-
活动流程
-
-
-
-
-
14:00 - 14:30
-
签到与集结
-
- 车辆到场 → 指引至临时停车点 → 签到处签名 → 扫码入群 → 领取积分卡
-
-
📋 积分卡说明
-
- • 每人领取一张积分卡(红绿双色章)
- • 初始积分:0分
- • 游戏得分范围:+1至+5分
- • 游戏扣分范围:-1至-5分
- • 最终积分可兑换礼品
-
-
-
-
-
-
-
-
-
14:30 - 16:30
-
观赏素材拍摄
-
全员车前合影、签到墙合照、地标合影、动态全员上车、点火、拍“泉州”俩个字
-
-
-
-
-
-
16:30 - 18:00
-
互动游戏时间
-
-
-
🎮 游戏项目清单
-
-
-
🎯 套圈圈
-
得分规则:套中1个+1分,套中3个+3分,套中5个+5分
-
-
-
-
🏺 投壶
-
得分规则:投中3个+2分,投中5个+4分,全中+5分
-
-
-
-
🎲 摇筛子
-
得分规则:豹子+5分,顺子+3分,对子+1分
-
-
-
-
🃏 牛牛/划拳
-
得分规则:赢一局+1分,连赢3局+3分,连赢5局+5分
-
-
-
-
⚽ 足球射门
-
得分规则:进1球+1分,进3球+3分,进5球+5分
-
-
-
-
🎮 王者荣耀
-
得分规则:获胜+3分,MVP+5分,失败-1分
-
-
-
-
💧 抛水瓶
-
得分规则:立住1个+2分,立住3个+4分,立住5个+5分
-
-
-
-
🎴 扑克游戏
-
得分规则:根据游戏类型,赢家+2分,输家-1分
-
-
-
-
🐊 鳄鱼/插剑玩具
-
得分规则:挑战成功+3分,失败-2分
-
-
-
-
🪑 最终复活挑战 - 抢椅子
-
- 规则:音乐停止时抢座位,未抢到者淘汰
- 得分:冠军+5分,亚军+3分,季军+2分,淘汰-1分
-
-
-
-
-
-
-
-
-
-
18:00 - 19:30
-
落日BBQ
-
- 落日余晖下午餐串成一排、齐开大灯、拍摄氛围感大片、边烤肉边欣赏晚霞
-
-
- 💡 提示:分享美食照片到朋友圈并@活动官方账号可获得 +1分
-
-
-
-
-
-
-
-
-
19:00 - 20:00
-
篝火歌舞会
-
-
-
🎤 互动游戏环节
-
-
-
🥁 击鼓传花(10轮)
-
- 规则:音乐停止时持花者上台,吃柠檬或苦瓜
- 得分:勇敢完成+2分,拒绝挑战-3分
-
-
-
-
-
🤝 喊数抱团(10轮)
-
- 规则:主持人喊数字,参与者按数字抱团,落单者淘汰
- 惩罚:上台自我介绍(1分钟)
- 得分:成功抱团+1分,落单但完成自我介绍+2分,拒绝-2分
-
-
-
-
-
🎵 大合唱
-
- 全员参与,气氛热烈,每人+1分
-
-
-
-
-
🎭 个人才艺表演
-
- 规则:自愿上台唱歌、跳舞、表演才艺
- 提示:需提前到后台准备BGM
- 得分:参与表演+3分,精彩表演+5分
-
-
-
-
-
👶 小朋友互动
-
- 主持人邀请小朋友上台互动,参与即有礼品
- 家长陪同参与+2分
-
-
-
-
-
-
-
-
-
-
20:00 - 20:30
-
璀璨烟火秀
-
-
-
🎆 烟花表演
-
- 1. 米兰之夜 - 大型专业烟火表演
- 2. 仙女棒 - 每车配发手持烟花,拍照打卡
- 3. 加特林压轴 - 震撼收尾,全场欢呼
-
- 💡 提示:拍摄烟花视频并分享可获得 +2分
-
-
-
-
-
-
-
-
-
20:30 以后
-
积分兑奖 & 自由交流
-
-
-
🏆 积分兑换礼品
-
- 积分排行榜:
- • 第1名(20分以上):神秘大奖
- • 第2-3名(15-19分):精美礼品
- • 第4-10名(10-14分):纪念品
- • 参与奖(5-9分):小礼物
-
- 随后自由合影、整理垃圾、提醒安全驾驶返程
-
-
-
-
-
-
-
-
-
-
活动经费 ¥{{ totalExpense }}
-
-
-
-
{{ expense.item }}
-
{{ expense.category }}
-
-
-
金额:¥{{ expense.amount }}
-
-
-
-
-
-
-
- 您的报名正在审核中,审核通过后可查看活动经费
- 您的报名未通过审核
-
-
-
-
-
赞助商
-
-
-
-
-
活动照片与视频
-
-
-
-
-
-
-
- 上传照片/视频
-
-
您可以上传活动照片或视频,审核后将展示给所有人
-
-
-
-
-
- 审核通过后可上传照片/视频
- 您的报名未通过审核
-
-
-
-
-
-
报名名单 ({{ registrations.length }}人)
-
-
-
{{ reg.name.charAt(0) }}
-
-
{{ reg.name }}
-
{{ reg.carModel }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 分享
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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 @@
+
+
+
弹幕效果演示
+
鼠标悬停可暂停弹幕
+
+
+
+
+
+
![]()
+
{{ item.name.charAt(0) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
活动时间
+
{{ formatDate(activity.startTime) }}
+
+
+
+
+
+
活动地点
+
{{ activity.location }}
+
+
+
+
+
+
+
报名截止
+
{{ formatDate(activity.registrationDeadline) }}
+
+
+
+
+
+
人均费用
+
{{ activity.price > 0 ? `¥${activity.price}` : '免费' }}
+
+
+
+
+
+
活动介绍
+
{{ activity.description }}
+
+
+
+
+
+
+
+
活动流程
+
+
+
+
+
+
+ 14:00 - 14:30
+
+
+
签到与集结
+
车辆有序到场,签到领卡,开启精彩活动之旅
+
+
+
+
🚗 车辆到场 → 指引至临时停车点
+
✍️ 签到处签名确认参与
+
📱 扫码入群,加入活动大家庭
+
🎫 领取专属积分卡
+
+
+
📋 积分卡说明
+
+ • 每人领取一张积分卡(红绿双色章)
+ • 初始积分:0分
+ • 游戏得分范围:+1至+5分
+ • 游戏扣分范围:-1至-5分
+ • 最终积分可兑换精美礼品
+ • 积分越高,礼品越丰厚
+
+
+
+
+
+
+
+
+
+
+
+
+ 14:30 - 16:30
+
+
+
观赏素材拍摄
+
专业摄影团队记录精彩瞬间,留下珍贵回忆
+
+
+
+
📸 全员车前合影 - 展示车队阵容
+
🎯 签到墙合照 - 打卡留念
+
🏛️ 地标合影 - 记录活动地点
+
🎬 动态全员上车 - 拍摄车队启动瞬间
+
🔥 点火仪式 - 引擎轰鸣
+
✨ 拍摄"泉州"字样 - 城市印记
+
+
+ 💡 提示:这些素材将用于活动回顾视频制作,请积极配合拍摄
+
+
+
+
+
+
+
+
+
+
+
+ 16:30 - 18:00
+
+
+
互动游戏时间
+
10+趣味游戏项目,赢取积分兑换精美礼品
+
+
+
+
+
+
+
+
+
+ 18:00 - 19:30
+
+
+
落日BBQ
+
落日余晖下品尝美食,拍摄浪漫氛围大片
+
+
+
+
🌅 落日余晖下车辆排成一排
+
💡 齐开大灯营造氛围
+
📷 拍摄氛围感大片
+
🍖 边烤肉边欣赏晚霞
+
🎵 轻松音乐伴随美食时光
+
+
+ 💡 提示:分享美食照片到朋友圈并@活动官方账号可获得 +1分
+
+
+
+
+
+
+
+
+
+
+
+ 19:00 - 20:00
+
+
+
篝火歌舞会
+
5个精彩互动游戏,点燃现场欢乐气氛
+
+
+
+
+
+
+
+ 玩法:音乐响起时传递道具,音乐停止时持花者上台
+ 惩罚:吃柠檬或苦瓜(二选一)
+ 得分:勇敢完成+2分,拒绝挑战-3分
+ 轮数:共10轮,气氛逐渐升温
+
+
+
+
+
+
+
+
+ 玩法:主持人喊数字,参与者按数字抱团
+ 规则:落单者淘汰,上台自我介绍(1分钟)
+ 得分:成功抱团+1分,落单但完成自我介绍+2分,拒绝-2分
+ 互动:认识新朋友的好机会!
+
+
+
+
+
+
+
+
+ 玩法:全员参与,齐唱经典歌曲
+ 曲目:《朋友》《真心英雄》等
+ 得分:参与即可获得+1分
+ 氛围:点燃现场,凝聚团队
+
+
+
+
+
+
+
+
+ 玩法:自愿上台展示才艺(唱歌/跳舞/表演)
+ 准备:需提前到后台准备BGM
+ 得分:参与表演+3分,精彩表演+5分
+ 鼓励:展现自我,收获掌声!
+
+
+
+
+
+
+
+
+ 玩法:主持人邀请小朋友上台互动
+ 内容:简单游戏/才艺展示/问答
+ 礼品:参与即有小礼物
+ 得分:家长陪同参与+2分
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 20:00 - 20:30
+
+
+
璀璨烟火秀
+
三重烟花盛宴,点亮夜空绽放精彩
+
+
+
+
🎆 米兰之夜 - 大型专业烟火表演,绚丽夺目
+
✨ 仙女棒 - 每车配发手持烟花,拍照打卡
+
🔥 加特林压轴 - 震撼收尾,全场欢呼
+
📸 最佳拍摄时机 - 记录璀璨瞬间
+
+
+ 💡 提示:拍摄烟花视频并分享可获得 +2分
+
+
+
+
+
+
+
+
+
+
+
+ 20:30 以后
+
+
+
积分兑奖 & 自由交流
+
凭积分兑换精美礼品,自由合影欢乐交流
+
+
+
+
🏆 积分兑换礼品
+
+ 积分排行榜:
+ • 第1名(20分以上):神秘大奖 🎁
+ • 第2-3名(15-19分):精美礼品 🎀
+ • 第4-10名(10-14分):纪念品 🎈
+ • 参与奖(5-9分):小礼物 🎊
+
+ 活动收尾:
+ • 自由合影留念
+ • 整理现场垃圾
+ • 提醒安全驾驶返程
+ • 期待下次相聚
+
+
+
+
+
+
+
+
+
+
+
+
活动经费 ¥{{ totalExpense }}
+
+
+
+
{{ expense.item }}
+
{{ expense.category }}
+
+
+
金额:¥{{ expense.amount }}
+
+
+
+
+
+
+
+ 您的报名正在审核中,审核通过后可查看活动经费
+ 您的报名未通过审核
+
+
+
+
+
+
赞助商
+
+
+
+
+
+
🏆 奖品礼品
+
+
+
+
+
+
+
+
{{ prize.description }}
+
+
+
+
+
+
+
+
+
活动照片与视频
+
+
+
+
+
+
+
+ 上传照片/视频
+
+
您可以上传活动照片或视频,审核后将展示给所有人
+
+
+
+
+
+ 审核通过后可上传照片/视频
+ 您的报名未通过审核
+
+
+
+
+
+
+
报名名单 ({{ registrations.length }}人)
+
+
+
{{ reg.name.charAt(0) }}
+
+
{{ reg.name }}
+
{{ reg.carModel }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 分享
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
-
-
-
-
-
-
-
-
-
-
活动时间
-
{{ formatDate(activity.startTime) }}
-
-
-
-
-
-
活动地点
-
{{ activity.location }}
-
-
-
-
-
-
-
报名截止
-
{{ formatDate(activity.registrationDeadline) }}
-
-
-
-
-
-
人均费用
-
{{ activity.price > 0 ? `¥${activity.price}` : '免费' }}
-
-
-
-
-
-
活动介绍
-
{{ activity.description }}
-
-
-
-
活动流程
-
-
-
-
-
- 14:00 - 14:30
-
-
-
签到与集�?/div>
-
车辆到场 �?签到处签�?�?扫码入群 �?领取积分�?/div>
-
-
-
-
-
-
-
- 14:30 - 16:30
-
-
-
观赏素材拍摄
-
全员车前合影、签到墙合照、地标合�?/div>
-
-
-
-
-
-
-
- 16:30 - 18:00
-
-
-
互动游戏时间
-
套圈圈、投壶、摇筛子、足球射门等10+游戏项目
-
-
-
-
-
-
-
- 18:00 - 19:30
-
-
-
落日BBQ
-
落日余晖下烤肉、拍摄氛围感大片
-
-
-
-
-
-
-
- 19:00 - 20:00
-
-
-
篝火歌舞�?/div>
-
击鼓传花、喊数抱团、才艺表演等互动游戏
-
-
-
-
-
-
-
- 20:00 - 20:30
-
-
-
璀璨烟火秀
-
米兰之夜、仙女棒、加特林压轴
-
-
-
-
-
-
-
- 20:30 以后
-
-
-
积分兑奖 & 自由交流
-
积分兑换礼品、自由合影、安全返�?/div>
-
-
-
-
-
活动经费 ¥{{ totalExpense }}
-
-
-
-
{{ expense.item }}
-
{{ expense.category }}
-
-
-
金额:¥{{ expense.amount }}
-
-
-
-
-
-
-
- 您的报名正在审核中,审核通过后可查看活动经费
- 您的报名未通过审核
-
-
-
-
-
赞助商
-
-
-
-
-
活动照片与视频
-
-
-
-
-
-
-
- 上传照片/视频
-
-
您可以上传活动照片或视频,审核后将展示给所有人
-
-
-
-
-
- 审核通过后可上传照片/视频
- 您的报名未通过审核
-
-
-
-
-
-
报名名单 ({{ registrations.length }}�?
-
-
-
{{ reg.name.charAt(0) }}
-
-
{{ reg.name }}
-
{{ reg.carModel }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 分享
-
-
-
-
-
-
-
-
-
-
-
-
- �?/span>
-
- ·
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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 @@