272 lines
8.0 KiB
JavaScript
272 lines
8.0 KiB
JavaScript
const express = require('express');
|
||
const multer = require('multer');
|
||
const path = require('path');
|
||
const fs = require('fs');
|
||
|
||
const app = express();
|
||
const PORT = process.env.PORT || 3000;
|
||
|
||
// 中间件配置
|
||
app.use(express.json());
|
||
app.use(express.urlencoded({ extended: true }));
|
||
app.use(express.static('public'));
|
||
app.use('/uploads', express.static('uploads'));
|
||
|
||
// 确保必要的目录存在
|
||
const dataDir = path.join(__dirname, 'data');
|
||
const uploadsDir = path.join(__dirname, 'uploads');
|
||
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir);
|
||
if (!fs.existsSync(uploadsDir)) fs.mkdirSync(uploadsDir);
|
||
|
||
// 数据文件路径
|
||
const dataFile = path.join(dataDir, 'data.json');
|
||
|
||
// 初始化数据文件
|
||
if (!fs.existsSync(dataFile)) {
|
||
fs.writeFileSync(dataFile, JSON.stringify({ records: [], nextId: 1 }, null, 2));
|
||
}
|
||
|
||
// 读取数据
|
||
function readData() {
|
||
const raw = fs.readFileSync(dataFile, 'utf-8');
|
||
return JSON.parse(raw);
|
||
}
|
||
|
||
// 写入数据
|
||
function writeData(data) {
|
||
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
||
}
|
||
|
||
// 配置 multer 图片上传
|
||
const storage = multer.diskStorage({
|
||
destination: (req, file, cb) => {
|
||
cb(null, uploadsDir);
|
||
},
|
||
filename: (req, file, cb) => {
|
||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
||
cb(null, uniqueSuffix + path.extname(file.originalname));
|
||
}
|
||
});
|
||
|
||
const upload = multer({
|
||
storage: storage,
|
||
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
|
||
fileFilter: (req, file, cb) => {
|
||
const allowedTypes = /jpeg|jpg|png|gif|webp/;
|
||
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
|
||
const mimetype = allowedTypes.test(file.mimetype);
|
||
|
||
if (extname && mimetype) {
|
||
cb(null, true);
|
||
} else {
|
||
cb(new Error('只支持图片格式:jpeg, jpg, png, gif, webp'));
|
||
}
|
||
}
|
||
});
|
||
|
||
// URL 格式校验
|
||
function isValidUrl(string) {
|
||
try {
|
||
const url = new URL(string);
|
||
return url.protocol === 'http:' || url.protocol === 'https:';
|
||
} catch (_) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 格式化时间为精确到分钟
|
||
function getFormattedTime() {
|
||
const now = new Date();
|
||
const year = now.getFullYear();
|
||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||
const day = String(now.getDate()).padStart(2, '0');
|
||
const hours = String(now.getHours()).padStart(2, '0');
|
||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
||
}
|
||
|
||
// API: 获取所有记录
|
||
app.get('/api/list', (req, res) => {
|
||
try {
|
||
const data = readData();
|
||
res.json({ success: true, records: data.records });
|
||
} catch (error) {
|
||
res.status(500).json({ success: false, message: '读取数据失败: ' + error.message });
|
||
}
|
||
});
|
||
|
||
// API: 普通用户提交记录(不含图片)
|
||
app.post('/api/submit', (req, res) => {
|
||
try {
|
||
const { name, link, note } = req.body;
|
||
|
||
// 数据校验
|
||
if (!name || !name.trim()) {
|
||
return res.status(400).json({ success: false, message: '人名不能为空' });
|
||
}
|
||
if (!link || !link.trim()) {
|
||
return res.status(400).json({ success: false, message: '牌谱链接不能为空' });
|
||
}
|
||
if (!isValidUrl(link)) {
|
||
return res.status(400).json({ success: false, message: '牌谱链接格式不正确' });
|
||
}
|
||
// 备注改为非必填,仅验证长度
|
||
if (note && note.trim() && note.length > 200) {
|
||
return res.status(400).json({ success: false, message: '备注说明不能超过 200 字' });
|
||
}
|
||
|
||
const data = readData();
|
||
|
||
// 创建新记录(无图片)
|
||
const newRecord = {
|
||
id: data.nextId,
|
||
submitTime: getFormattedTime(),
|
||
name: name.trim(),
|
||
link: link.trim(),
|
||
note: note ? note.trim() : '',
|
||
nagaLink: '',
|
||
nagaNote: '',
|
||
images: []
|
||
};
|
||
|
||
data.records.push(newRecord);
|
||
data.nextId += 1;
|
||
writeData(data);
|
||
|
||
res.json({ success: true, message: '提交成功', record: newRecord });
|
||
} catch (error) {
|
||
res.status(500).json({ success: false, message: '提交失败: ' + error.message });
|
||
}
|
||
});
|
||
|
||
// API: 管理员更新 Naga 字段和图片
|
||
app.post('/api/admin/update', upload.array('images', 10), (req, res) => {
|
||
try {
|
||
const { id, nagaLink, nagaNote, existingImages } = req.body;
|
||
|
||
if (!id) {
|
||
return res.status(400).json({ success: false, message: '记录 ID 不能为空' });
|
||
}
|
||
|
||
// Naga 链接校验(如果填写)
|
||
if (nagaLink && nagaLink.trim() && !isValidUrl(nagaLink)) {
|
||
return res.status(400).json({ success: false, message: 'Naga 链接格式不正确' });
|
||
}
|
||
|
||
// Naga 说明字数校验(如果填写)
|
||
if (nagaNote && nagaNote.length > 200) {
|
||
return res.status(400).json({ success: false, message: 'Naga 说明不能超过 200 字' });
|
||
}
|
||
|
||
const data = readData();
|
||
const record = data.records.find(r => r.id === parseInt(id));
|
||
|
||
if (!record) {
|
||
return res.status(404).json({ success: false, message: '记录不存在' });
|
||
}
|
||
|
||
// 处理新上传的图片
|
||
const newImages = req.files ? req.files.map(file => `/uploads/${file.filename}`) : [];
|
||
|
||
// 合并已有图片和新图片
|
||
let allImages = [];
|
||
if (existingImages) {
|
||
// existingImages 可能是字符串或数组
|
||
if (typeof existingImages === 'string') {
|
||
try {
|
||
allImages = JSON.parse(existingImages);
|
||
} catch (e) {
|
||
allImages = existingImages ? [existingImages] : [];
|
||
}
|
||
} else if (Array.isArray(existingImages)) {
|
||
allImages = existingImages;
|
||
}
|
||
}
|
||
allImages = [...allImages, ...newImages];
|
||
|
||
// 更新 Naga 字段和图片
|
||
record.nagaLink = nagaLink ? nagaLink.trim() : '';
|
||
record.nagaNote = nagaNote ? nagaNote.trim() : '';
|
||
record.images = allImages;
|
||
|
||
writeData(data);
|
||
|
||
res.json({ success: true, message: '更新成功', record: record });
|
||
} catch (error) {
|
||
res.status(500).json({ success: false, message: '更新失败: ' + error.message });
|
||
}
|
||
});
|
||
|
||
// API: 管理员删除单张图片
|
||
app.delete('/api/admin/delete-image', (req, res) => {
|
||
try {
|
||
const { id, imagePath } = req.body;
|
||
|
||
if (!id || !imagePath) {
|
||
return res.status(400).json({ success: false, message: '参数不完整' });
|
||
}
|
||
|
||
const data = readData();
|
||
const record = data.records.find(r => r.id === parseInt(id));
|
||
|
||
if (!record) {
|
||
return res.status(404).json({ success: false, message: '记录不存在' });
|
||
}
|
||
|
||
// 从记录中移除图片路径
|
||
const imageIndex = record.images.indexOf(imagePath);
|
||
if (imageIndex > -1) {
|
||
record.images.splice(imageIndex, 1);
|
||
}
|
||
|
||
// 删除物理文件
|
||
const filePath = path.join(__dirname, imagePath.replace(/^\//, ''));
|
||
if (fs.existsSync(filePath)) {
|
||
fs.unlinkSync(filePath);
|
||
}
|
||
|
||
writeData(data);
|
||
|
||
res.json({ success: true, message: '图片删除成功', record: record });
|
||
} catch (error) {
|
||
res.status(500).json({ success: false, message: '删除图片失败: ' + error.message });
|
||
}
|
||
});
|
||
|
||
// API: 删除记录
|
||
app.delete('/api/admin/delete/:id', (req, res) => {
|
||
try {
|
||
const id = parseInt(req.params.id);
|
||
const data = readData();
|
||
const index = data.records.findIndex(r => r.id === id);
|
||
|
||
if (index === -1) {
|
||
return res.status(404).json({ success: false, message: '记录不存在' });
|
||
}
|
||
|
||
// 删除关联的图片文件
|
||
const record = data.records[index];
|
||
if (record.images && record.images.length > 0) {
|
||
record.images.forEach(imagePath => {
|
||
const filePath = path.join(__dirname, imagePath.replace(/^\//, ''));
|
||
if (fs.existsSync(filePath)) {
|
||
fs.unlinkSync(filePath);
|
||
}
|
||
});
|
||
}
|
||
|
||
data.records.splice(index, 1);
|
||
writeData(data);
|
||
|
||
res.json({ success: true, message: '删除成功' });
|
||
} catch (error) {
|
||
res.status(500).json({ success: false, message: '删除失败: ' + error.message });
|
||
}
|
||
});
|
||
|
||
// 启动服务器
|
||
app.listen(PORT, '0.0.0.0', () => {
|
||
console.log(`服务器运行在端口 ${PORT}`);
|
||
console.log(`普通访客页面: http://localhost:${PORT}`);
|
||
console.log(`管理员页面: http://localhost:${PORT}/admin.html`);
|
||
}); |