feat(nav-category): support dynamic home grid categories in admin and uniapp
这个提交包含在:
@@ -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;
|
||||
}
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户