diff --git a/apps/server/src/modules/notification/dto/create-notification.dto.ts b/apps/server/src/modules/notification/dto/create-notification.dto.ts new file mode 100644 index 0000000..01b4b55 --- /dev/null +++ b/apps/server/src/modules/notification/dto/create-notification.dto.ts @@ -0,0 +1,47 @@ +import { IsIn, IsObject, IsOptional, IsString } from 'class-validator'; +import { NOTIFICATION_TYPES, type NotificationType } from '../notification.service'; + +export class CreateNotificationDto { + @IsString() + recipientId!: string; + + @IsOptional() + @IsString() + actorId?: string; + + @IsIn(NOTIFICATION_TYPES) + type!: NotificationType; + + @IsString() + title!: string; + + @IsOptional() + @IsString() + body?: string; + + @IsString() + resourceType!: string; + + @IsString() + resourceId!: string; + + @IsOptional() + @IsString() + resourceVersionId?: string; + + @IsOptional() + @IsString() + productId?: string; + + @IsOptional() + @IsString() + projectId?: string; + + @IsOptional() + @IsString() + versionId?: string; + + @IsOptional() + @IsObject() + metadata?: Record; +} diff --git a/apps/server/src/modules/notification/notification.controller.ts b/apps/server/src/modules/notification/notification.controller.ts index 5c51112..beea318 100644 --- a/apps/server/src/modules/notification/notification.controller.ts +++ b/apps/server/src/modules/notification/notification.controller.ts @@ -1,4 +1,5 @@ -import { Body, Controller, Get, Param, Patch, Query } from '@nestjs/common'; +import { Body, Controller, Get, Param, Patch, Post, Query } from '@nestjs/common'; +import { CreateNotificationDto } from './dto/create-notification.dto'; import { MarkNotificationReadDto } from './dto/mark-notification-read.dto'; import { NotificationService } from './notification.service'; @@ -19,6 +20,11 @@ export class NotificationController { }); } + @Post() + create(@Body() dto: CreateNotificationDto) { + return this.notificationService.create(dto); + } + @Patch(':id/read') markRead(@Param('id') id: string, @Body() dto: MarkNotificationReadDto) { return this.notificationService.markRead(id, dto.recipientId); diff --git a/apps/web/app/admin/governance/page.tsx b/apps/web/app/admin/governance/page.tsx new file mode 100644 index 0000000..887421c --- /dev/null +++ b/apps/web/app/admin/governance/page.tsx @@ -0,0 +1,149 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { Download, Plus, RefreshCcw, Trash2, Upload } from 'lucide-react'; +import { RouteGuard } from '@/components/auth/Guard'; +import { api } from '@/lib/api'; +import { useAuthStore } from '@/stores/useAuthStore'; + +type GovernanceKind = 'task_category' | 'requirement_type' | 'requirement_platform' | 'requirement_source'; + +interface GovernanceItem { + id: string; + kind?: string; + name: string; + code?: string | null; + group?: string | null; + isSystem?: boolean; +} + +const KIND_LABEL: Record = { + task_category: '任务类型', + requirement_type: '需求类型', + requirement_platform: '支持端', + requirement_source: '需求来源', +}; + +function GovernancePageInner() { + const user = useAuthStore((s) => s.user); + const [kind, setKind] = useState('task_category'); + const [items, setItems] = useState([]); + const [name, setName] = useState(''); + const [group, setGroup] = useState('other'); + const [exportText, setExportText] = useState(''); + const actorId = user?.id ?? ''; + + const reload = async () => { + const rows = await api.get(`/governance/dictionaries?kind=${kind}`); + setItems(rows); + }; + + useEffect(() => { + void reload().catch(() => setItems([])); + }, [kind]); + + const create = async () => { + if (!actorId || !name.trim()) return; + await api.post('/governance/dictionaries', { actorId, kind, name: name.trim(), group }); + setName(''); + await reload(); + }; + + const remove = async (item: GovernanceItem) => { + if (!actorId) return; + await api.deleteWithBody(`/governance/dictionaries/${kind}/${item.id}`, { actorId }); + await reload(); + }; + + const exportAll = async () => { + const data = await api.get('/governance/export'); + setExportText(JSON.stringify(data, null, 2)); + }; + + const importAll = async () => { + if (!actorId || !exportText.trim()) return; + const parsed = JSON.parse(exportText); + const sourceItems = Array.isArray(parsed.items) + ? parsed.items + : [...(parsed.dictionaries ?? []), ...(parsed.taskCategories ?? []).map((item: GovernanceItem) => ({ ...item, kind: 'task_category' }))]; + await api.post('/governance/import', { actorId, items: sourceItems }); + await reload(); + }; + + return ( +
+
+
+
+

治理设置

+

统一维护任务类型、需求类型、支持端与来源字典。

+
+ +
+ +
+ {Object.entries(KIND_LABEL).map(([key, label]) => ( + + ))} +
+ +
+
+ setName(event.target.value)} placeholder={`新增${KIND_LABEL[kind]}`} className="h-8 rounded-md border border-[var(--line)] bg-[var(--bg)] px-3 text-[12px] focus:border-[var(--accent)] focus:outline-none" /> + setGroup(event.target.value)} placeholder="分组" className="h-8 rounded-md border border-[var(--line)] bg-[var(--bg)] px-3 text-[12px] focus:border-[var(--accent)] focus:outline-none" /> + +
+
+ {items.map((item) => ( +
+ {item.name} + {item.code ?? '-'} + {item.group ?? '-'} + +
+ ))} + {items.length === 0 &&
暂无字典项
} +
+
+ +
+
+ + +
+