From 2530ceb6b4b3b88e6576ed62527c68cac73d3867 Mon Sep 17 00:00:00 2001
From: Yvan <8574526@qq,com>
Date: Fri, 10 Apr 2026 09:32:29 +0800
Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=A5=96=E5=93=81?=
=?UTF-8?q?=E5=9B=BE=E7=89=87=E5=92=8C=E8=B5=9E=E5=8A=A9=E5=95=86logo?=
=?UTF-8?q?=E5=88=A0=E9=99=A4bug?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 修复编辑奖品时删除图片后保存,图片仍存在的问题
- 修复编辑赞助商时删除logo后保存,logo仍存在的问题
- 前端添加删除检测逻辑,传递removeImage/removeLogo标记
- 后端处理删除标记,将对应字段设为null
---
backend/routes/admin.js | 154 +++++++++++++++++++-
frontend/src/views/admin/ActivityManage.vue | 137 ++++++++++++++++-
frontend/src/views/admin/usePrizeManage.js | 33 ++++-
3 files changed, 310 insertions(+), 14 deletions(-)
diff --git a/backend/routes/admin.js b/backend/routes/admin.js
index 9640501..2733384 100644
--- a/backend/routes/admin.js
+++ b/backend/routes/admin.js
@@ -3,6 +3,7 @@ const router = express.Router();
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const auth = require('../middleware/auth');
+const authSuper = require('../middleware/authSuper');
const { upload, getFileUrls } = require('../middleware/upload');
const { Admin, Activity, Registration, Expense, Photo, Sponsor, Prize } = require('../models');
const fs = require('fs');
@@ -17,7 +18,132 @@ router.post('/login', async (req, res) => {
return res.status(401).json({ error: '用户名或密码错误' });
}
const token = jwt.sign({ id: admin.id }, process.env.JWT_SECRET, { expiresIn: '7d' });
- res.json({ token, admin: { id: admin.id, username: admin.username, name: admin.name } });
+ res.json({
+ token,
+ admin: {
+ id: admin.id,
+ username: admin.username,
+ name: admin.name,
+ role: admin.role
+ }
+ });
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+});
+
+// ========== 管理员管理API(仅超级管理员) ==========
+
+// 获取管理员列表
+router.get('/admins', authSuper, async (req, res) => {
+ try {
+ const admins = await Admin.findAll({
+ attributes: ['id', 'username', 'name', 'role', 'createdAt'],
+ order: [['createdAt', 'ASC']],
+ raw: true
+ });
+ res.json(admins);
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+});
+
+// 创建管理员
+router.post('/admins', authSuper, async (req, res) => {
+ try {
+ const { username, password, name, role } = req.body;
+
+ if (!username || !password) {
+ return res.status(400).json({ error: '用户名和密码不能为空' });
+ }
+
+ const existing = await Admin.findOne({ where: { username } });
+ if (existing) {
+ return res.status(400).json({ error: '用户名已存在' });
+ }
+
+ const hashedPassword = bcrypt.hashSync(password, 10);
+ const admin = await Admin.create({
+ username,
+ password: hashedPassword,
+ name: name || username,
+ role: role || 'admin'
+ });
+
+ res.json({
+ id: admin.id,
+ username: admin.username,
+ name: admin.name,
+ role: admin.role
+ });
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+});
+
+// 修改管理员信息
+router.put('/admins/:id', authSuper, async (req, res) => {
+ try {
+ const { name, role } = req.body;
+ const admin = await Admin.findByPk(req.params.id);
+
+ if (!admin) {
+ return res.status(404).json({ error: '管理员不存在' });
+ }
+
+ const updateData = {};
+ if (name !== undefined) updateData.name = name;
+ if (role !== undefined) updateData.role = role;
+
+ await Admin.update(updateData, { where: { id: req.params.id } });
+ const updated = await Admin.findByPk(req.params.id, {
+ attributes: ['id', 'username', 'name', 'role']
+ });
+
+ res.json(updated);
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+});
+
+// 修改管理员密码
+router.put('/admins/:id/password', authSuper, async (req, res) => {
+ try {
+ const { newPassword } = req.body;
+
+ if (!newPassword) {
+ return res.status(400).json({ error: '新密码不能为空' });
+ }
+
+ const admin = await Admin.findByPk(req.params.id);
+ if (!admin) {
+ return res.status(404).json({ error: '管理员不存在' });
+ }
+
+ const hashedPassword = bcrypt.hashSync(newPassword, 10);
+ await Admin.update({ password: hashedPassword }, { where: { id: req.params.id } });
+
+ res.json({ message: '密码修改成功' });
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+});
+
+// 删除管理员
+router.delete('/admins/:id', authSuper, async (req, res) => {
+ try {
+ const admin = await Admin.findByPk(req.params.id);
+ if (!admin) {
+ return res.status(404).json({ error: '管理员不存在' });
+ }
+
+ // 防止删除超级管理员
+ if (admin.role === 'super') {
+ return res.status(403).json({ error: '不能删除超级管理员' });
+ }
+
+ await Admin.destroy({ where: { id: req.params.id } });
+ res.json({ message: '删除成功' });
} catch (error) {
res.status(500).json({ error: error.message });
}
@@ -93,6 +219,16 @@ router.get('/registrations', auth, async (req, res) => {
}
});
+// 删除报名
+router.delete('/registrations/:id', auth, async (req, res) => {
+ try {
+ await Registration.destroy({ where: { id: req.params.id } });
+ res.json({ message: '删除成功' });
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+});
+
// 上传经费明细
router.post('/expenses', auth, upload.array('media', 5), async (req, res) => {
try {
@@ -274,7 +410,7 @@ 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, sortOrder } = req.body;
+ const { name, type, link, sortOrder, removeLogo } = req.body;
const updateData = { name, type, link };
if (sortOrder !== undefined) {
@@ -284,6 +420,8 @@ router.put('/sponsors/:id', auth, upload.single('logo'), async (req, res) => {
if (req.file) {
const [uploaded] = getFileUrls(req.file);
updateData.logo = uploaded.url;
+ } else if (removeLogo === 'true') {
+ updateData.logo = null;
}
await Sponsor.update(updateData, { where: { id: req.params.id } });
@@ -307,7 +445,7 @@ 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;
+ const { activityId, name, level, description, quantity, sortOrder, source, sponsor } = req.body;
let image = null;
if (req.file) {
const [uploaded] = getFileUrls([req.file]);
@@ -320,7 +458,9 @@ router.post('/prizes', auth, upload.single('image'), async (req, res) => {
description,
image,
quantity: quantity || 1,
- sortOrder: sortOrder || 0
+ sortOrder: sortOrder || 0,
+ source: source || 'purchase',
+ sponsor: sponsor || null
});
res.json(prize);
} catch (error) {
@@ -331,12 +471,14 @@ router.post('/prizes', auth, upload.single('image'), async (req, res) => {
// 更新奖品
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 };
+ const { name, level, description, quantity, sortOrder, source, sponsor, removeImage } = req.body;
+ const updateData = { name, level, description, quantity, sortOrder, source, sponsor };
if (req.file) {
const [uploaded] = getFileUrls([req.file]);
updateData.image = uploaded.url;
+ } else if (removeImage === 'true') {
+ updateData.image = null;
}
await Prize.update(updateData, { where: { id: req.params.id } });
diff --git a/frontend/src/views/admin/ActivityManage.vue b/frontend/src/views/admin/ActivityManage.vue
index 8e44e45..5de5729 100644
--- a/frontend/src/views/admin/ActivityManage.vue
+++ b/frontend/src/views/admin/ActivityManage.vue
@@ -138,7 +138,7 @@
-
@@ -178,7 +178,7 @@
-
@@ -214,7 +214,7 @@
-
@@ -732,6 +732,22 @@
required
/>
+
+
- {{ level }}
+ {{ level }}
+
+
+
+
+
+
+
+
+
+
+
+
+ 群友赞助
+
+
+
+ 赞助商提供
+
+
+
+ 采购
@@ -880,6 +920,16 @@
通过
拒绝
+
+ 删除报名
+
@@ -935,6 +985,7 @@ const {
prizes,
showPrizeForm,
showLevelPicker,
+ showSourcePicker,
prizeImage,
prizeForm,
prizeLevels,
@@ -942,6 +993,7 @@ const {
editPrize,
onPrizeImageRead,
selectLevel,
+ selectSource,
submitPrize,
deletePrizeItem,
resetPrizeForm
@@ -1084,14 +1136,29 @@ const formatDateTime = (dateTime) => {
return `${year}-${month}-${day} ${hours}:${minutes}`;
};
+// 安全解析datetime-local格式,确保以本地时区创建Date对象
+const parseDateTimeLocal = (dateTimeStr) => {
+ console.log('[时区转换] 输入的datetime-local字符串:', dateTimeStr);
+ const [datePart, timePart] = dateTimeStr.split('T');
+ const [year, month, day] = datePart.split('-').map(Number);
+ const [hours, minutes] = timePart.split(':').map(Number);
+ const localDate = new Date(year, month - 1, day, hours, minutes);
+ console.log('[时区转换] 创建的本地Date对象:', localDate.toString());
+ console.log('[时区转换] 转换为UTC (ISO):', localDate.toISOString());
+ console.log('[时区转换] 时区偏移(分钟):', localDate.getTimezoneOffset());
+ return localDate;
+};
+
// 开始时间确认
const confirmStartTime = () => {
if (!startTimeInput.value) {
showToast('请选择开始时间');
return;
}
- const selectedDate = new Date(startTimeInput.value);
+ console.log('[开始时间] 用户选择的时间:', startTimeInput.value);
+ const selectedDate = parseDateTimeLocal(startTimeInput.value);
activityForm.value.startTime = selectedDate.toISOString();
+ console.log('[开始时间] 保存到表单的UTC时间:', activityForm.value.startTime);
showStartTimePicker.value = false;
};
@@ -1101,12 +1168,14 @@ const confirmDeadline = () => {
showToast('请选择报名截止时间');
return;
}
- const selectedDate = new Date(deadlineInput.value);
+ console.log('[截止时间] 用户选择的时间:', deadlineInput.value);
+ const selectedDate = parseDateTimeLocal(deadlineInput.value);
if (activityForm.value.startTime && selectedDate > new Date(activityForm.value.startTime)) {
showToast('报名截止时间不能晚于活动开始时间');
return;
}
activityForm.value.registrationDeadline = selectedDate.toISOString();
+ console.log('[截止时间] 保存到表单的UTC时间:', activityForm.value.registrationDeadline);
showDeadlinePicker.value = false;
};
@@ -1162,17 +1231,31 @@ const loadData = async () => {
}
};
+// 添加新奖品
+const addNewPrize = () => {
+ resetPrizeForm();
+ showPrizeForm.value = true;
+};
+
const editActivity = () => {
activityForm.value = { ...activity.value };
+ console.log('[编辑活动] 原始活动数据:', activity.value);
+
// 设置日期时间选择器的值
if (activity.value.startTime) {
+ console.log('[编辑活动] 数据库的开始时间(UTC):', activity.value.startTime);
const startDate = new Date(activity.value.startTime);
+ console.log('[编辑活动] 转换为本地Date对象:', startDate.toString());
startTimeInput.value = formatDateTimeLocal(startDate);
+ console.log('[编辑活动] 显示在选择器的时间:', startTimeInput.value);
}
if (activity.value.registrationDeadline) {
+ console.log('[编辑活动] 数据库的截止时间(UTC):', activity.value.registrationDeadline);
const deadlineDate = new Date(activity.value.registrationDeadline);
+ console.log('[编辑活动] 转换为本地Date对象:', deadlineDate.toString());
deadlineInput.value = formatDateTimeLocal(deadlineDate);
+ console.log('[编辑活动] 显示在选择器的时间:', deadlineInput.value);
}
showActivityForm.value = true;
@@ -1283,6 +1366,13 @@ const editExpense = (expense) => {
showExpenseForm.value = true;
};
+// 添加新经费
+const addNewExpense = () => {
+ expenseForm.value = { id: null, item: '', amount: '', category: '', note: '', media: [] };
+ expenseMediaFiles.value = [];
+ showExpenseForm.value = true;
+};
+
const onExpenseMediaRead = (file) => {
// 文件读取后的处理,Vant会自动处理
};
@@ -1344,6 +1434,16 @@ const submitSponsor = async () => {
duration: 0
});
formData.append('logo', sponsorLogo.value[0].file);
+ } else if (sponsorLogo.value.length === 0 && sponsorForm.value.logo) {
+ // 用户删除了logo
+ formData.append('removeLogo', 'true');
+ console.log('用户删除了 Logo');
+ loadingToast = showToast({
+ type: 'loading',
+ message: '正在保存...',
+ forbidClick: true,
+ duration: 0
+ });
} else {
console.log('没有 Logo,仅保存数据');
loadingToast = showToast({
@@ -1411,6 +1511,13 @@ const editSponsor = (sponsor) => {
showSponsorForm.value = true;
};
+// 添加新赞助商
+const addNewSponsor = () => {
+ sponsorForm.value = { id: null, name: '', type: 'sponsor', link: '', sortOrder: 0 };
+ sponsorLogo.value = [];
+ showSponsorForm.value = true;
+};
+
const onLogoRead = (file) => {
// 文件选择后的回调,显示文件已选择
if (file.file) {
@@ -1515,6 +1622,24 @@ const approveFromDetail = async (status) => {
}
};
+// 删除报名
+const deleteRegistration = async () => {
+ try {
+ await showConfirmDialog({
+ title: '确认删除',
+ message: '删除后用户可以重新报名,确定要删除这条报名信息吗?'
+ });
+ await api.deleteRegistration(currentReg.value.id);
+ showToast('删除成功');
+ showRegDetailDialog.value = false;
+ loadData();
+ } catch (error) {
+ if (error !== 'cancel') {
+ showToast('删除失败');
+ }
+ }
+};
+
// 媒体管理相关方法
const uploadPhotos = async (files) => {
const fileArray = Array.isArray(files) ? files : [files];
diff --git a/frontend/src/views/admin/usePrizeManage.js b/frontend/src/views/admin/usePrizeManage.js
index 57b744a..d6f3498 100644
--- a/frontend/src/views/admin/usePrizeManage.js
+++ b/frontend/src/views/admin/usePrizeManage.js
@@ -6,6 +6,7 @@ export function usePrizeManage(activityId) {
const prizes = ref([]);
const showPrizeForm = ref(false);
const showLevelPicker = ref(false);
+ const showSourcePicker = ref(false);
const prizeImage = ref([]);
const prizeForm = ref({
id: null,
@@ -15,10 +16,12 @@ export function usePrizeManage(activityId) {
description: '',
quantity: 1,
sortOrder: 0,
+ source: 'purchase',
+ sponsor: '',
image: null
});
- const prizeLevels = ['一等奖', '二等奖', '三等奖', '参与奖', '伴手礼'];
+ const prizeLevels = ['特等奖', '一等奖', '二等奖', '三等奖', '四等奖', '参与奖', '伴手礼'];
const loadPrizes = async () => {
try {
@@ -38,6 +41,8 @@ export function usePrizeManage(activityId) {
description: prize.description || '',
quantity: prize.quantity,
sortOrder: prize.sortOrder || 0,
+ source: prize.source || 'purchase',
+ sponsor: prize.sponsor || '',
image: prize.image
};
// 回显图片 - 需要使用完整URL
@@ -60,12 +65,25 @@ export function usePrizeManage(activityId) {
showLevelPicker.value = false;
};
+ const selectSource = (source) => {
+ prizeForm.value.source = source;
+ if (source === 'purchase') {
+ prizeForm.value.sponsor = '';
+ }
+ showSourcePicker.value = false;
+ };
+
const submitPrize = async () => {
- if (!prizeForm.value.name || !prizeForm.value.level) {
+ if (!prizeForm.value.name || !prizeForm.value.level || !prizeForm.value.source) {
showToast('请填写必填项');
return;
}
+ if (prizeForm.value.source !== 'purchase' && !prizeForm.value.sponsor) {
+ showToast('请填写赞助者名称');
+ return;
+ }
+
const loadingToast = showToast({
type: 'loading',
message: '保存中...',
@@ -81,9 +99,16 @@ export function usePrizeManage(activityId) {
formData.append('description', prizeForm.value.description);
formData.append('quantity', prizeForm.value.quantity);
formData.append('sortOrder', prizeForm.value.sortOrder);
+ formData.append('source', prizeForm.value.source);
+ if (prizeForm.value.sponsor) {
+ formData.append('sponsor', prizeForm.value.sponsor);
+ }
if (prizeForm.value.imageFile) {
formData.append('image', prizeForm.value.imageFile);
+ } else if (prizeImage.value.length === 0 && prizeForm.value.image) {
+ // 用户删除了图片
+ formData.append('removeImage', 'true');
}
if (prizeForm.value.id) {
@@ -130,6 +155,8 @@ export function usePrizeManage(activityId) {
description: '',
quantity: 1,
sortOrder: 0,
+ source: 'purchase',
+ sponsor: '',
image: null
};
prizeImage.value = [];
@@ -139,6 +166,7 @@ export function usePrizeManage(activityId) {
prizes,
showPrizeForm,
showLevelPicker,
+ showSourcePicker,
prizeImage,
prizeForm,
prizeLevels,
@@ -146,6 +174,7 @@ export function usePrizeManage(activityId) {
editPrize,
onPrizeImageRead,
selectLevel,
+ selectSource,
submitPrize,
deletePrizeItem,
resetPrizeForm