|
@@ -0,0 +1,182 @@
|
|
|
+import { Injectable } from '@angular/core';
|
|
|
+import { v4 as uuidv4 } from 'uuid';
|
|
|
+
|
|
|
+export type IssueStatus = 'open' | 'in_progress' | 'resolved' | 'closed';
|
|
|
+export type IssuePriority = 'low' | 'medium' | 'high' | 'critical';
|
|
|
+export type IssueType = 'bug' | 'task' | 'feedback' | 'risk';
|
|
|
+
|
|
|
+export interface IssueComment {
|
|
|
+ id: string;
|
|
|
+ authorId: string;
|
|
|
+ content: string;
|
|
|
+ createdAt: Date;
|
|
|
+}
|
|
|
+
|
|
|
+export interface ProjectIssue {
|
|
|
+ id: string;
|
|
|
+ projectId: string;
|
|
|
+ title: string;
|
|
|
+ status: IssueStatus;
|
|
|
+ priority: IssuePriority;
|
|
|
+ type: IssueType;
|
|
|
+ creatorId: string;
|
|
|
+ assigneeId?: string;
|
|
|
+ createdAt: Date;
|
|
|
+ updatedAt: Date;
|
|
|
+ dueDate?: Date;
|
|
|
+ tags?: string[];
|
|
|
+ description?: string;
|
|
|
+ comments?: IssueComment[];
|
|
|
+}
|
|
|
+
|
|
|
+export interface IssueCounts {
|
|
|
+ total: number;
|
|
|
+ open: number;
|
|
|
+ in_progress: number;
|
|
|
+ resolved: number;
|
|
|
+ closed: number;
|
|
|
+}
|
|
|
+
|
|
|
+@Injectable({ providedIn: 'root' })
|
|
|
+export class ProjectIssueService {
|
|
|
+ private store = new Map<string, ProjectIssue[]>();
|
|
|
+
|
|
|
+ /** 列表查询(支持状态过滤与文本搜索) */
|
|
|
+ listIssues(projectId: string, opts?: { status?: IssueStatus[]; text?: string }): ProjectIssue[] {
|
|
|
+ const list = this.ensure(projectId);
|
|
|
+ let result = [...list];
|
|
|
+
|
|
|
+ if (opts?.status && opts.status.length > 0) {
|
|
|
+ result = result.filter(i => opts.status!.includes(i.status));
|
|
|
+ }
|
|
|
+
|
|
|
+ if (opts?.text && opts.text.trim()) {
|
|
|
+ const q = opts.text.trim().toLowerCase();
|
|
|
+ result = result.filter(i =>
|
|
|
+ (i.title || '').toLowerCase().includes(q) ||
|
|
|
+ (i.description || '').toLowerCase().includes(q) ||
|
|
|
+ (i.tags || []).some(t => t.toLowerCase().includes(q))
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ return result.sort((a, b) => +new Date(b.updatedAt) - +new Date(a.updatedAt));
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 创建问题 */
|
|
|
+ createIssue(projectId: string, payload: { title: string; description?: string; priority?: IssuePriority; type?: IssueType; dueDate?: Date; tags?: string[]; creatorId: string; assigneeId?: string }): ProjectIssue {
|
|
|
+ const now = new Date();
|
|
|
+ const issue: ProjectIssue = {
|
|
|
+ id: uuidv4(),
|
|
|
+ projectId,
|
|
|
+ title: payload.title,
|
|
|
+ description: payload.description || '',
|
|
|
+ priority: payload.priority || 'medium',
|
|
|
+ type: payload.type || 'task',
|
|
|
+ status: 'open',
|
|
|
+ creatorId: payload.creatorId,
|
|
|
+ assigneeId: payload.assigneeId,
|
|
|
+ createdAt: now,
|
|
|
+ updatedAt: now,
|
|
|
+ dueDate: payload.dueDate,
|
|
|
+ tags: payload.tags || [],
|
|
|
+ comments: []
|
|
|
+ };
|
|
|
+
|
|
|
+ const list = this.ensure(projectId);
|
|
|
+ list.push(issue);
|
|
|
+ this.store.set(projectId, list);
|
|
|
+ return issue;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 更新问题 */
|
|
|
+ updateIssue(projectId: string, issueId: string, updates: Partial<ProjectIssue>): ProjectIssue | null {
|
|
|
+ const list = this.ensure(projectId);
|
|
|
+ const idx = list.findIndex(i => i.id === issueId);
|
|
|
+ if (idx === -1) return null;
|
|
|
+
|
|
|
+ const now = new Date();
|
|
|
+ const updated: ProjectIssue = { ...list[idx], ...updates, updatedAt: now };
|
|
|
+ list[idx] = updated;
|
|
|
+ this.store.set(projectId, list);
|
|
|
+ return updated;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 删除问题 */
|
|
|
+ deleteIssue(projectId: string, issueId: string): boolean {
|
|
|
+ const list = this.ensure(projectId);
|
|
|
+ const lenBefore = list.length;
|
|
|
+ const filtered = list.filter(i => i.id !== issueId);
|
|
|
+ this.store.set(projectId, filtered);
|
|
|
+ return filtered.length < lenBefore;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 添加评论(用于催办或讨论) */
|
|
|
+ addComment(projectId: string, issueId: string, authorId: string, content: string): IssueComment | null {
|
|
|
+ const list = this.ensure(projectId);
|
|
|
+ const issue = list.find(i => i.id === issueId);
|
|
|
+ if (!issue) return null;
|
|
|
+
|
|
|
+ const comment: IssueComment = { id: uuidv4(), authorId, content, createdAt: new Date() };
|
|
|
+ issue.comments = issue.comments || [];
|
|
|
+ issue.comments.push(comment);
|
|
|
+ issue.updatedAt = new Date();
|
|
|
+ this.store.set(projectId, list);
|
|
|
+ return comment;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 快速修改状态 */
|
|
|
+ setStatus(projectId: string, issueId: string, status: IssueStatus): ProjectIssue | null {
|
|
|
+ return this.updateIssue(projectId, issueId, { status });
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 统计汇总 */
|
|
|
+ getCounts(projectId: string): IssueCounts {
|
|
|
+ const list = this.ensure(projectId);
|
|
|
+ const counts: IssueCounts = { total: list.length, open: 0, in_progress: 0, resolved: 0, closed: 0 };
|
|
|
+ for (const i of list) counts[i.status]++;
|
|
|
+ return counts;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 首次访问种子数据 */
|
|
|
+ seed(projectId: string) {
|
|
|
+ const list = this.ensure(projectId);
|
|
|
+ if (list.length > 0) return;
|
|
|
+
|
|
|
+ const now = new Date();
|
|
|
+ const creator = 'seed-user';
|
|
|
+ this.createIssue(projectId, {
|
|
|
+ title: '确认客厅配色与材质样板',
|
|
|
+ description: '需要确认客厅主色调与地面材质,影响方案深化。',
|
|
|
+ priority: 'high',
|
|
|
+ type: 'task',
|
|
|
+ creatorId: creator,
|
|
|
+ dueDate: new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000),
|
|
|
+ tags: ['配色', '材质']
|
|
|
+ });
|
|
|
+ this.createIssue(projectId, {
|
|
|
+ title: '主卧效果图灯光偏暗',
|
|
|
+ description: '客户反馈主卧效果图灯光偏暗,需要调整光源与明暗对比。',
|
|
|
+ priority: 'medium',
|
|
|
+ type: 'feedback',
|
|
|
+ creatorId: creator,
|
|
|
+ tags: ['灯光', '效果图']
|
|
|
+ });
|
|
|
+ const second = this.createIssue(projectId, {
|
|
|
+ title: '厨房柜体尺寸与现场不符',
|
|
|
+ description: '现场复尺发现图纸尺寸与实际有偏差,需要同步调整。',
|
|
|
+ priority: 'critical',
|
|
|
+ type: 'bug',
|
|
|
+ creatorId: creator,
|
|
|
+ tags: ['复尺', '尺寸']
|
|
|
+ });
|
|
|
+ this.setStatus(projectId, second.id, 'in_progress');
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 内部:确保项目列表存在 */
|
|
|
+ private ensure(projectId: string): ProjectIssue[] {
|
|
|
+ if (!this.store.has(projectId)) {
|
|
|
+ this.store.set(projectId, []);
|
|
|
+ }
|
|
|
+ return this.store.get(projectId)!;
|
|
|
+ }
|
|
|
+}
|