Files
admin/dashboard/backend/src/routes/www/sections.ts
T
2026-08-13 14:23:01 +08:00

491 lines
19 KiB
TypeScript

import { Router, type Request, type Response } from 'express';
import { eq, and, max } from 'drizzle-orm';
import { db } from '../../config/database.js';
import { wwwSectionBlocks, wwwSpecGroups, wwwSpecItems } from '../../schemas/index.js';
import logger from '../../config/logger.js';
import { ApiResponse } from '../../utils/response.js';
import { authMiddleware } from '../../middleware/auth.js';
const router = Router();
router.use(authMiddleware);
// ===== 3.1 Section 区块 =====
// GET /api/www/sections?page_type=home&product_id=1
router.get('/api/www/sections', async (req: Request, res: Response) => {
try {
const pageType = req.query.page_type as string;
const productId = req.query.product_id ? Number(req.query.product_id) : undefined;
if (!pageType) {
res.json(ApiResponse.error('page_type 参数必填'));
return;
}
const conditions = [eq(wwwSectionBlocks.pageType, pageType)];
if (productId) conditions.push(eq(wwwSectionBlocks.productId, productId));
const rows = await db.select().from(wwwSectionBlocks)
.where(and(...conditions))
.orderBy(wwwSectionBlocks.sortOrder);
res.json(ApiResponse.success({ items: rows, total: rows.length }));
} catch (e: unknown) {
logger.error(`[www/sections] GET error: ${e instanceof Error ? e.message : e}`);
res.json(ApiResponse.error('获取 Section 列表失败'));
}
});
// GET /api/www/sections/:id(含子数据)
router.get('/api/www/sections/:id', async (req: Request, res: Response) => {
try {
const id = Number(req.params.id);
const [section] = await db.select().from(wwwSectionBlocks).where(eq(wwwSectionBlocks.id, id));
if (!section) {
res.json(ApiResponse.noData('Section 不存在'));
return;
}
// 如果是 spec_table 布局,附带参数分组数据
let specGroups: unknown[] = [];
if (section.layout === 'spec_table') {
const groups = await db.select().from(wwwSpecGroups)
.where(eq(wwwSpecGroups.sectionBlockId, id))
.orderBy(wwwSpecGroups.sortOrder);
specGroups = await Promise.all(groups.map(async (g) => {
const items = await db.select().from(wwwSpecItems)
.where(eq(wwwSpecItems.specGroupId, g.id))
.orderBy(wwwSpecItems.sortOrder);
return { ...g, items };
}));
}
res.json(ApiResponse.success({ ...section, specGroups }));
} catch (e: unknown) {
logger.error(`[www/sections/:id] GET error: ${e instanceof Error ? e.message : e}`);
res.json(ApiResponse.error('获取 Section 详情失败'));
}
});
// POST /api/www/sections
router.post('/api/www/sections', async (req: Request, res: Response) => {
try {
const b = req.body;
if (!b.page_type || !b.layout) {
res.json(ApiResponse.error('page_type 和 layout 为必填项'));
return;
}
// 未指定排序时,自动追加到同 page_type(及同产品)已有区块的最后,避免默认 0 导致乱序
let sortOrder = Number(b.sort_order) || 0;
if (b.sort_order === undefined || b.sort_order === null) {
const scope = [eq(wwwSectionBlocks.pageType, b.page_type)];
if (b.product_id) scope.push(eq(wwwSectionBlocks.productId, b.product_id));
const [agg] = await db.select({ max: max(wwwSectionBlocks.sortOrder) }).from(wwwSectionBlocks).where(and(...scope));
sortOrder = (agg?.max ?? -1) + 1;
}
const result = await db.insert(wwwSectionBlocks).values({
pageType: b.page_type,
productId: b.product_id || null,
layout: b.layout,
theme: b.theme || 'light',
overlineColor: b.overline_color || '',
titleColor: b.title_color || '',
bodyColor: b.body_color || '',
bgType: b.bg_type || 'color',
bgValue: b.bg_value || '',
overlineZh: b.overline_zh || '',
overlineEn: b.overline_en || '',
titleZh: b.title_zh || '',
titleEn: b.title_en || '',
subtitleZh: b.subtitle_zh || '',
subtitleEn: b.subtitle_en || '',
contentZh: b.content_zh || null,
contentEn: b.content_en || null,
ctaPrimaryZh: b.cta_primary_zh || '',
ctaPrimaryEn: b.cta_primary_en || '',
ctaPrimaryUrl: b.cta_primary_url || '',
ctaSecondaryZh: b.cta_secondary_zh || '',
ctaSecondaryEn: b.cta_secondary_en || '',
ctaSecondaryUrl: b.cta_secondary_url || '',
mediaUrlZh: b.media_url_zh || '',
mediaUrlEn: b.media_url_en || '',
videoUrl: b.video_url || '',
config: b.config || null,
isVisible: b.is_visible !== undefined ? (b.is_visible ? 1 : 0) : 1,
sortOrder,
});
const newId = result[0].insertId;
const [created] = await db.select().from(wwwSectionBlocks).where(eq(wwwSectionBlocks.id, newId));
res.json(ApiResponse.success(created, 'Section 已创建'));
} catch (e: unknown) {
logger.error(`[www/sections] POST error: ${e instanceof Error ? e.message : e}`);
res.json(ApiResponse.error('创建 Section 失败'));
}
});
// PUT /api/www/sections/:id
router.put('/api/www/sections/:id', async (req: Request, res: Response) => {
try {
const id = Number(req.params.id);
const [existing] = await db.select().from(wwwSectionBlocks).where(eq(wwwSectionBlocks.id, id));
if (!existing) {
res.json(ApiResponse.noData('Section 不存在'));
return;
}
const b = req.body;
await db.update(wwwSectionBlocks).set({
layout: b.layout ?? existing.layout,
theme: b.theme ?? existing.theme,
overlineColor: b.overline_color ?? existing.overlineColor,
titleColor: b.title_color ?? existing.titleColor,
bodyColor: b.body_color ?? existing.bodyColor,
bgType: b.bg_type ?? existing.bgType,
bgValue: b.bg_value ?? existing.bgValue,
overlineZh: b.overline_zh ?? existing.overlineZh,
overlineEn: b.overline_en ?? existing.overlineEn,
titleZh: b.title_zh ?? existing.titleZh,
titleEn: b.title_en ?? existing.titleEn,
subtitleZh: b.subtitle_zh ?? existing.subtitleZh,
subtitleEn: b.subtitle_en ?? existing.subtitleEn,
contentZh: b.content_zh !== undefined ? b.content_zh : existing.contentZh,
contentEn: b.content_en !== undefined ? b.content_en : existing.contentEn,
ctaPrimaryZh: b.cta_primary_zh ?? existing.ctaPrimaryZh,
ctaPrimaryEn: b.cta_primary_en ?? existing.ctaPrimaryEn,
ctaPrimaryUrl: b.cta_primary_url ?? existing.ctaPrimaryUrl,
ctaSecondaryZh: b.cta_secondary_zh ?? existing.ctaSecondaryZh,
ctaSecondaryEn: b.cta_secondary_en ?? existing.ctaSecondaryEn,
ctaSecondaryUrl: b.cta_secondary_url ?? existing.ctaSecondaryUrl,
mediaUrlZh: b.media_url_zh ?? existing.mediaUrlZh,
mediaUrlEn: b.media_url_en ?? existing.mediaUrlEn,
videoUrl: b.video_url ?? existing.videoUrl,
config: b.config !== undefined ? b.config : existing.config,
isVisible: b.is_visible !== undefined ? (b.is_visible ? 1 : 0) : existing.isVisible,
sortOrder: b.sort_order ?? existing.sortOrder,
}).where(eq(wwwSectionBlocks.id, id));
const [updated] = await db.select().from(wwwSectionBlocks).where(eq(wwwSectionBlocks.id, id));
res.json(ApiResponse.success(updated, 'Section 已更新'));
} catch (e: unknown) {
logger.error(`[www/sections/:id] PUT error: ${e instanceof Error ? e.message : e}`);
res.json(ApiResponse.error('更新 Section 失败'));
}
});
// DELETE /api/www/sections/:id
router.delete('/api/www/sections/:id', async (req: Request, res: Response) => {
try {
const id = Number(req.params.id);
const [existing] = await db.select().from(wwwSectionBlocks).where(eq(wwwSectionBlocks.id, id));
if (!existing) {
res.json(ApiResponse.noData('Section 不存在'));
return;
}
// 级联删除参数规格
const groups = await db.select().from(wwwSpecGroups).where(eq(wwwSpecGroups.sectionBlockId, id));
for (const g of groups) {
await db.delete(wwwSpecItems).where(eq(wwwSpecItems.specGroupId, g.id));
}
await db.delete(wwwSpecGroups).where(eq(wwwSpecGroups.sectionBlockId, id));
await db.delete(wwwSectionBlocks).where(eq(wwwSectionBlocks.id, id));
res.json(ApiResponse.success(null, '删除成功'));
} catch (e: unknown) {
logger.error(`[www/sections/:id] DELETE error: ${e instanceof Error ? e.message : e}`);
res.json(ApiResponse.error('删除 Section 失败'));
}
});
// PATCH /api/www/sections/sort
router.patch('/api/www/sections/sort', async (req: Request, res: Response) => {
try {
const items: { id: number; sort_order: number }[] = req.body;
if (!Array.isArray(items)) {
res.json(ApiResponse.error('请求体应为数组'));
return;
}
for (const item of items) {
await db.update(wwwSectionBlocks).set({ sortOrder: item.sort_order }).where(eq(wwwSectionBlocks.id, item.id));
}
res.json(ApiResponse.success(null, '排序已更新'));
} catch (e: unknown) {
logger.error(`[www/sections/sort] PATCH error: ${e instanceof Error ? e.message : e}`);
res.json(ApiResponse.error('更新排序失败'));
}
});
// POST /api/www/sections/:id/duplicate
router.post('/api/www/sections/:id/duplicate', async (req: Request, res: Response) => {
try {
const id = Number(req.params.id);
const [source] = await db.select().from(wwwSectionBlocks).where(eq(wwwSectionBlocks.id, id));
if (!source) {
res.json(ApiResponse.noData('Section 不存在'));
return;
}
const { id: _, createdAt: __, updatedAt: ___, ...data } = source;
const result = await db.insert(wwwSectionBlocks).values({
...data,
titleZh: `${source.titleZh} (副本)`,
titleEn: `${source.titleEn} (copy)`,
sortOrder: source.sortOrder + 1,
});
const newId = result[0].insertId;
// 复制参数规格
if (source.layout === 'spec_table') {
const groups = await db.select().from(wwwSpecGroups).where(eq(wwwSpecGroups.sectionBlockId, id));
for (const g of groups) {
const gResult = await db.insert(wwwSpecGroups).values({
sectionBlockId: newId,
titleZh: g.titleZh,
titleEn: g.titleEn,
sortOrder: g.sortOrder,
defaultVisible: g.defaultVisible,
});
const newGroupId = gResult[0].insertId;
const items = await db.select().from(wwwSpecItems).where(eq(wwwSpecItems.specGroupId, g.id));
if (items.length > 0) {
await db.insert(wwwSpecItems).values(items.map((item) => ({
specGroupId: newGroupId,
nameZh: item.nameZh,
nameEn: item.nameEn,
valueZh: item.valueZh,
valueEn: item.valueEn,
sortOrder: item.sortOrder,
})));
}
}
}
const [created] = await db.select().from(wwwSectionBlocks).where(eq(wwwSectionBlocks.id, newId));
res.json(ApiResponse.success(created, 'Section 已复制'));
} catch (e: unknown) {
logger.error(`[www/sections/duplicate] POST error: ${e instanceof Error ? e.message : e}`);
res.json(ApiResponse.error('复制 Section 失败'));
}
});
// ===== 3.2 Spec Table 参数规格 =====
// GET /api/www/sections/:section_id/spec-groups
router.get('/api/www/sections/:section_id/spec-groups', async (req: Request, res: Response) => {
try {
const sectionId = Number(req.params.section_id);
const groups = await db.select().from(wwwSpecGroups)
.where(eq(wwwSpecGroups.sectionBlockId, sectionId))
.orderBy(wwwSpecGroups.sortOrder);
// 附带每组的参数项
const result = await Promise.all(groups.map(async (g) => {
const items = await db.select().from(wwwSpecItems)
.where(eq(wwwSpecItems.specGroupId, g.id))
.orderBy(wwwSpecItems.sortOrder);
return { ...g, items };
}));
res.json(ApiResponse.success({ items: result, total: result.length }));
} catch (e: unknown) {
logger.error(`[www/spec-groups] GET error: ${e instanceof Error ? e.message : e}`);
res.json(ApiResponse.error('获取参数分组失败'));
}
});
// POST /api/www/sections/:section_id/spec-groups
router.post('/api/www/sections/:section_id/spec-groups', async (req: Request, res: Response) => {
try {
const sectionId = Number(req.params.section_id);
const { title_zh, title_en, default_visible } = req.body;
if (!title_zh || !title_en) {
res.json(ApiResponse.error('title_zh 和 title_en 为必填项'));
return;
}
const result = await db.insert(wwwSpecGroups).values({
sectionBlockId: sectionId,
titleZh: title_zh,
titleEn: title_en,
defaultVisible: default_visible || 5,
});
const newId = result[0].insertId;
const [created] = await db.select().from(wwwSpecGroups).where(eq(wwwSpecGroups.id, newId));
res.json(ApiResponse.success(created, '参数分组已创建'));
} catch (e: unknown) {
logger.error(`[www/spec-groups] POST error: ${e instanceof Error ? e.message : e}`);
res.json(ApiResponse.error('创建参数分组失败'));
}
});
// PUT /api/www/sections/:section_id/spec-groups/:group_id
router.put('/api/www/sections/:section_id/spec-groups/:group_id', async (req: Request, res: Response) => {
try {
const groupId = Number(req.params.group_id);
const [existing] = await db.select().from(wwwSpecGroups).where(eq(wwwSpecGroups.id, groupId));
if (!existing) {
res.json(ApiResponse.noData('参数分组不存在'));
return;
}
const { title_zh, title_en, default_visible } = req.body;
await db.update(wwwSpecGroups).set({
titleZh: title_zh ?? existing.titleZh,
titleEn: title_en ?? existing.titleEn,
defaultVisible: default_visible ?? existing.defaultVisible,
}).where(eq(wwwSpecGroups.id, groupId));
const [updated] = await db.select().from(wwwSpecGroups).where(eq(wwwSpecGroups.id, groupId));
res.json(ApiResponse.success(updated, '参数分组已更新'));
} catch (e: unknown) {
logger.error(`[www/spec-groups] PUT error: ${e instanceof Error ? e.message : e}`);
res.json(ApiResponse.error('更新参数分组失败'));
}
});
// DELETE /api/www/sections/:section_id/spec-groups/:group_id
router.delete('/api/www/sections/:section_id/spec-groups/:group_id', async (req: Request, res: Response) => {
try {
const groupId = Number(req.params.group_id);
const [existing] = await db.select().from(wwwSpecGroups).where(eq(wwwSpecGroups.id, groupId));
if (!existing) {
res.json(ApiResponse.noData('参数分组不存在'));
return;
}
await db.delete(wwwSpecItems).where(eq(wwwSpecItems.specGroupId, groupId));
await db.delete(wwwSpecGroups).where(eq(wwwSpecGroups.id, groupId));
res.json(ApiResponse.success(null, '删除成功'));
} catch (e: unknown) {
logger.error(`[www/spec-groups] DELETE error: ${e instanceof Error ? e.message : e}`);
res.json(ApiResponse.error('删除参数分组失败'));
}
});
// PATCH /api/www/sections/:section_id/spec-groups/sort
router.patch('/api/www/sections/:section_id/spec-groups/sort', async (req: Request, res: Response) => {
try {
const items: { id: number; sort_order: number }[] = req.body;
if (!Array.isArray(items)) {
res.json(ApiResponse.error('请求体应为数组'));
return;
}
for (const item of items) {
await db.update(wwwSpecGroups).set({ sortOrder: item.sort_order }).where(eq(wwwSpecGroups.id, item.id));
}
res.json(ApiResponse.success(null, '排序已更新'));
} catch (e: unknown) {
logger.error(`[www/spec-groups/sort] PATCH error: ${e instanceof Error ? e.message : e}`);
res.json(ApiResponse.error('更新排序失败'));
}
});
// GET /api/www/spec-groups/:group_id/items
router.get('/api/www/spec-groups/:group_id/items', async (req: Request, res: Response) => {
try {
const groupId = Number(req.params.group_id);
const items = await db.select().from(wwwSpecItems)
.where(eq(wwwSpecItems.specGroupId, groupId))
.orderBy(wwwSpecItems.sortOrder);
res.json(ApiResponse.success({ items, total: items.length }));
} catch (e: unknown) {
logger.error(`[www/spec-items] GET error: ${e instanceof Error ? e.message : e}`);
res.json(ApiResponse.error('获取参数项失败'));
}
});
// POST /api/www/spec-groups/:group_id/items
router.post('/api/www/spec-groups/:group_id/items', async (req: Request, res: Response) => {
try {
const groupId = Number(req.params.group_id);
const { name_zh, name_en, value_zh, value_en } = req.body;
if (!name_zh || !name_en || !value_zh || !value_en) {
res.json(ApiResponse.error('name_zh、name_en、value_zh、value_en 为必填项'));
return;
}
const result = await db.insert(wwwSpecItems).values({
specGroupId: groupId,
nameZh: name_zh,
nameEn: name_en,
valueZh: value_zh,
valueEn: value_en,
});
const newId = result[0].insertId;
const [created] = await db.select().from(wwwSpecItems).where(eq(wwwSpecItems.id, newId));
res.json(ApiResponse.success(created, '参数项已创建'));
} catch (e: unknown) {
logger.error(`[www/spec-items] POST error: ${e instanceof Error ? e.message : e}`);
res.json(ApiResponse.error('创建参数项失败'));
}
});
// PUT /api/www/spec-items/:id
router.put('/api/www/spec-items/:id', async (req: Request, res: Response) => {
try {
const id = Number(req.params.id);
const [existing] = await db.select().from(wwwSpecItems).where(eq(wwwSpecItems.id, id));
if (!existing) {
res.json(ApiResponse.noData('参数项不存在'));
return;
}
const { name_zh, name_en, value_zh, value_en } = req.body;
await db.update(wwwSpecItems).set({
nameZh: name_zh ?? existing.nameZh,
nameEn: name_en ?? existing.nameEn,
valueZh: value_zh ?? existing.valueZh,
valueEn: value_en ?? existing.valueEn,
}).where(eq(wwwSpecItems.id, id));
const [updated] = await db.select().from(wwwSpecItems).where(eq(wwwSpecItems.id, id));
res.json(ApiResponse.success(updated, '参数项已更新'));
} catch (e: unknown) {
logger.error(`[www/spec-items] PUT error: ${e instanceof Error ? e.message : e}`);
res.json(ApiResponse.error('更新参数项失败'));
}
});
// DELETE /api/www/spec-items/:id
router.delete('/api/www/spec-items/:id', async (req: Request, res: Response) => {
try {
const id = Number(req.params.id);
const [existing] = await db.select().from(wwwSpecItems).where(eq(wwwSpecItems.id, id));
if (!existing) {
res.json(ApiResponse.noData('参数项不存在'));
return;
}
await db.delete(wwwSpecItems).where(eq(wwwSpecItems.id, id));
res.json(ApiResponse.success(null, '删除成功'));
} catch (e: unknown) {
logger.error(`[www/spec-items] DELETE error: ${e instanceof Error ? e.message : e}`);
res.json(ApiResponse.error('删除参数项失败'));
}
});
// PATCH /api/www/spec-groups/:group_id/items/sort
router.patch('/api/www/spec-groups/:group_id/items/sort', async (req: Request, res: Response) => {
try {
const items: { id: number; sort_order: number }[] = req.body;
if (!Array.isArray(items)) {
res.json(ApiResponse.error('请求体应为数组'));
return;
}
for (const item of items) {
await db.update(wwwSpecItems).set({ sortOrder: item.sort_order }).where(eq(wwwSpecItems.id, item.id));
}
res.json(ApiResponse.success(null, '排序已更新'));
} catch (e: unknown) {
logger.error(`[www/spec-items/sort] PATCH error: ${e instanceof Error ? e.message : e}`);
res.json(ApiResponse.error('更新排序失败'));
}
});
export default router;