feat(nav-category): support dynamic home grid categories in admin and uniapp

这个提交包含在:
xcbyy
2026-10-10 09:08:18 +08:00
父节点 f010cfb7ab
当前提交 51682fffea
共有 11 个文件被更改,包括 1077 次插入 和 6 次删除

查看文件

@@ -0,0 +1,130 @@
// 已读取AGENTS.md
import { ContentSecurityService } from '../security/content-security.service';
export interface NavCategoryItem {
id: number;
code: string;
name: string;
icon: string;
bgClass?: string;
cat?: string;
targetUrl: string;
badgeText?: string;
sortOrder: number;
status: number; // 1: 启用, 0: 停用
createdAt?: string;
updatedAt?: string;
}
export class NavCategoryService {
private categories: NavCategoryItem[] = [
{ id: 1, code: 'esports', name: '电子竞技', icon: 'gameAll', bgClass: 'bg-indigo', cat: 'all', targetUrl: '/pages/activity/list?category=ALL', badgeText: 'HOT', sortOrder: 1, status: 1 },
{ id: 2, code: 'meal', name: '美食探店', icon: 'utensils', bgClass: 'bg-amber', cat: 'offline_meal', targetUrl: '/pages/activity/list?category=MEAL', badgeText: 'AA制', sortOrder: 2, status: 1 },
{ id: 3, code: 'sing', name: 'KTV嗨唱', icon: 'micVocal', bgClass: 'bg-rose', cat: 'offline_sing', targetUrl: '/pages/activity/list?category=SING', badgeText: '热度高', sortOrder: 3, status: 1 },
{ id: 4, code: 'billiards', name: '同城台球', icon: 'billiards', bgClass: 'bg-sky', cat: 'offline_play', targetUrl: '/pages/activity/list?category=ALL', badgeText: '', sortOrder: 4, status: 1 },
{ id: 5, code: 'hiking', name: '户外爬山', icon: 'mountain', bgClass: 'bg-emerald', cat: 'offline_play', targetUrl: '/pages/activity/list?category=ALL', badgeText: '周末', sortOrder: 5, status: 1 },
{ id: 6, code: 'drink', name: '下午茶歇', icon: 'coffee', bgClass: 'bg-teal', cat: 'offline_drink', targetUrl: '/pages/activity/list?category=DRINK', badgeText: '', sortOrder: 6, status: 1 },
{ id: 7, code: 'boardgame', name: '剧本桌游', icon: 'dice', bgClass: 'bg-purple', cat: 'offline_play', targetUrl: '/pages/activity/list?category=ALL', badgeText: '', sortOrder: 7, status: 1 },
{ id: 8, code: 'fitness', name: '运动健身', icon: 'fitness', bgClass: 'bg-orange', cat: 'offline_play', targetUrl: '/pages/activity/list?category=ALL', badgeText: '', sortOrder: 8, status: 1 },
{ id: 9, code: 'bar', name: '同城微醺', icon: 'warmFlame', bgClass: 'bg-rose', cat: 'offline_drink', targetUrl: '/pages/activity/list?category=DRINK', badgeText: '清吧', sortOrder: 9, status: 1 },
{ id: 10, code: 'more', name: '全部活动', icon: 'gridAll', bgClass: 'bg-blue', cat: 'all', targetUrl: '/pages/activity/list?category=ALL', badgeText: '', sortOrder: 10, status: 1 }
];
private nextId = 11;
private secService = new ContentSecurityService();
getActiveCategories(): NavCategoryItem[] {
return this.categories
.filter((c) => c.status === 1)
.sort((a, b) => a.sortOrder - b.sortOrder);
}
getAllCategories(): NavCategoryItem[] {
return [...this.categories].sort((a, b) => a.sortOrder - b.sortOrder);
}
createCategory(payload: {
code?: string;
name: string;
icon: string;
bgClass?: string;
cat?: string;
targetUrl?: string;
badgeText?: string;
sortOrder?: number;
status?: number;
}): NavCategoryItem {
if (!payload.name || !payload.name.trim()) {
throw new Error('品类名称不能为空');
}
if (!payload.icon || !payload.icon.trim()) {
throw new Error('品类图标不能为空');
}
const sec = this.secService.checkText(payload.name + ' ' + (payload.badgeText || ''));
if (!sec.pass) {
const err = new Error(sec.message);
(err as any).code = sec.code;
throw err;
}
const item: NavCategoryItem = {
id: this.nextId++,
code: payload.code || `cat_${Date.now()}`,
name: payload.name.trim(),
icon: payload.icon.trim(),
bgClass: payload.bgClass || 'bg-indigo',
cat: payload.cat || 'all',
targetUrl: payload.targetUrl || '/pages/activity/list?category=ALL',
badgeText: payload.badgeText || '',
sortOrder: payload.sortOrder !== undefined ? Number(payload.sortOrder) : 0,
status: payload.status !== undefined ? Number(payload.status) : 1,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
this.categories.push(item);
return item;
}
updateCategory(id: number, payload: Partial<Omit<NavCategoryItem, 'id'>>): NavCategoryItem {
const idx = this.categories.findIndex((c) => c.id === id);
if (idx === -1) {
throw new Error('品类导航不存在或已被删除');
}
if (payload.name) {
const sec = this.secService.checkText(payload.name + ' ' + (payload.badgeText || ''));
if (!sec.pass) {
const err = new Error(sec.message);
(err as any).code = sec.code;
throw err;
}
}
const current = this.categories[idx];
const updated: NavCategoryItem = {
...current,
...payload,
updatedAt: new Date().toISOString()
};
this.categories[idx] = updated;
return updated;
}
toggleStatus(id: number, status: number): NavCategoryItem {
if (status !== 0 && status !== 1) {
throw new Error('状态参数必须为 0 或 1');
}
return this.updateCategory(id, { status });
}
deleteCategory(id: number): boolean {
const len = this.categories.length;
this.categories = this.categories.filter((c) => c.id !== id);
if (this.categories.length === len) {
throw new Error('品类导航不存在或已被删除');
}
return true;
}
}