| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354 |
- import { Injectable } from '@angular/core';
- import { v4 as uuidv4 } from 'uuid';
- import { FmodeObject, FmodeParse } from 'fmode-ng/parse';
- const Parse: any = FmodeParse.with('nova');
- export type IssueStatus = 'open' | 'in_progress' | 'resolved' | 'closed';
- export type IssuePriority = 'low' | 'medium' | 'high' | 'critical' | 'urgent';
- export type IssueType = 'bug' | 'task' | 'feedback' | 'risk' | 'feature';
- 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;
- assignee?: FmodeObject;
- assigneeId?: string;
- createdAt: Date;
- updatedAt: Date;
- dueDate?: Date;
- tags?: string[];
- description?: string;
- comments?: IssueComment[];
- // 新增:关联信息
- relatedSpace?: string;
- relatedStage?: string;
- productId?: string;
- }
- 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[]>();
- // 状态中英文映射(兼容后台中文状态)
- private zh2en(status: string): IssueStatus {
- const map: any = {
- '待处理': 'open',
- '处理中': 'in_progress',
- '已解决': 'resolved',
- '已关闭': 'closed'
- };
- return (map[status] || status) as IssueStatus;
- }
- private en2zh(status: IssueStatus): string {
- const map: any = {
- open: '待处理',
- in_progress: '处理中',
- resolved: '已解决',
- closed: '已关闭'
- };
- return map[status] || status;
- }
- // 将 Parse.Object 转换为本地模型
- private parseToModel(obj: any): ProjectIssue {
- const data = obj.get('data') || {};
- const tags: string[] = data.tags || [];
- const comments: IssueComment[] = (data.comments || []).map((c: any) => ({
- id: c.id || uuidv4(),
- authorId: c.authorId,
- content: c.content,
- createdAt: c.createdAt ? new Date(c.createdAt) : new Date()
- }));
- const statusRaw = obj.get('status');
- const status: IssueStatus = statusRaw ? this.zh2en(statusRaw) : 'open';
- return {
- id: obj.id,
- projectId: obj.get('project')?.id || '',
- title: obj.get('title') || (obj.get('description') || '').slice(0, 40) || '未命名问题',
- description: obj.get('description') || '',
- priority: (obj.get('priority') || 'medium') as IssuePriority,
- type: (obj.get('issueType') || 'task') as IssueType,
- status,
- creatorId: obj.get('creator')?.id || obj.get('reportedBy')?.id || '',
- assignee: obj.get('assignee'),
- assigneeId: obj.get('assignee')?.id,
- createdAt: obj.createdAt || new Date(),
- updatedAt: obj.updatedAt || new Date(),
- dueDate: obj.get('dueDate') || undefined,
- tags,
- comments,
- relatedSpace: obj.get('relatedSpace') || data.relatedSpace || undefined,
- relatedStage: obj.get('relatedStage') || data.relatedStage || undefined,
- productId: obj.get('product')?.id || undefined
- };
- }
- // 同步缓存筛选
- listIssues(projectId: string, opts?: { status?: IssueStatus[]; text?: string }): ProjectIssue[] {
- const list = this.ensure(projectId);
- let result: ProjectIssue[] = [...list];
- if (opts?.status && opts.status.length > 0) {
- result = result.filter((issue: ProjectIssue) => opts.status!.includes(issue.status));
- }
- if (opts?.text && opts.text.trim()) {
- const q = opts.text.trim().toLowerCase();
- result = result.filter((issue: ProjectIssue) =>
- (issue.title || '').toLowerCase().includes(q) ||
- (issue.description || '').toLowerCase().includes(q) ||
- (issue.tags || []).some((t: string) => t.toLowerCase().includes(q))
- );
- }
- return result.sort((a: ProjectIssue, b: ProjectIssue) => +new Date(b.updatedAt) - +new Date(a.updatedAt));
- }
- // 从后端刷新到缓存
- async refreshFromServer(projectId: string, opts?: { status?: IssueStatus[]; text?: string }): Promise<void> {
- const query = new Parse.Query('ProjectIssue');
- query.equalTo('project', { __type: 'Pointer', className: 'Project', objectId: projectId });
- query.notEqualTo('isDeleted', true);
- query.include(['creator', 'assignee', 'product']);
- // 服务器端粗过滤(仅文本)
- if (opts?.text && opts.text.trim()) {
- const kw = opts.text.trim();
- query.matches('title', new RegExp(kw, 'i'));
- }
- const results = await query.find();
- const list: ProjectIssue[] = results.map((obj: any) => this.parseToModel(obj));
- // 客户端状态过滤
- const filtered: ProjectIssue[] = opts?.status && opts.status.length > 0 ? list.filter((issue: ProjectIssue) => opts!.status!.includes(issue.status)) : list;
- this.store.set(projectId, filtered);
- }
- // 列出负责人候选:基于 ProjectTeam
- async listAssignees(projectId: string): Promise<{ id: string; name: string }[]> {
- try {
- const teamQuery = new Parse.Query('ProjectTeam');
- teamQuery.equalTo('project', { __type: 'Pointer', className: 'Project', objectId: projectId });
- teamQuery.include(['profile']);
- teamQuery.notEqualTo('isDeleted', true);
- const teams = await teamQuery.find();
- const seen = new Set<string>();
- const list: { id: string; name: string }[] = [];
- for (const t of teams) {
- const p = t.get('profile');
- const pid = p?.id;
- const name = p?.get ? (p.get('name') || '未命名') : '未命名';
- if (pid && !seen.has(pid)) {
- seen.add(pid);
- list.push({ id: pid, name });
- }
- }
- // 兜底:加入项目负责人(owner)
- if (list.length === 0) {
- const pQuery = new Parse.Query('Project');
- pQuery.include(['owner']);
- const project = await pQuery.get(projectId);
- const owner = project.get('owner');
- if (owner?.id && !seen.has(owner.id)) {
- list.unshift({ id: owner.id, name: owner.get('name') || '项目负责人' });
- seen.add(owner.id);
- }
- }
- return list;
- } catch (e) {
- console.warn('listAssignees failed', e);
- return [];
- }
- }
- // 创建问题(持久化)
- async createIssue(
- projectId: string,
- payload: { title: string; description?: string; priority?: IssuePriority; type?: IssueType; dueDate?: Date; tags?: string[]; creatorId: string; assigneeId?: string; relatedSpace?: string; relatedStage?: string; productId?: string }
- ): Promise<ProjectIssue> {
- const Issue = Parse.Object.extend('ProjectIssue');
- const obj = new Issue();
- obj.set('project', { __type: 'Pointer', className: 'Project', objectId: projectId });
- obj.set('title', payload.title);
- obj.set('description', payload.description || '');
- obj.set('priority', payload.priority || 'medium');
- obj.set('issueType', payload.type || 'task');
- obj.set('status', this.en2zh('open'));
- obj.set('creator', { __type: 'Pointer', className: 'Profile', objectId: payload.creatorId });
- // 负责人:优先使用传入;否则尝试项目负责人;最后创建人
- let assigneeId = payload.assigneeId;
- if (!assigneeId) {
- try {
- const pQuery = new Parse.Query('Project');
- pQuery.include(['owner']);
- const project = await pQuery.get(projectId);
- assigneeId = project.get('owner')?.id || payload.creatorId;
- } catch {}
- }
- if (assigneeId) {
- obj.set('assignee', { __type: 'Pointer', className: 'Profile', objectId: assigneeId });
- }
- if (payload.dueDate) obj.set('dueDate', payload.dueDate);
- if (payload.relatedSpace) obj.set('relatedSpace', payload.relatedSpace);
- if (payload.relatedStage) obj.set('relatedStage', payload.relatedStage);
- if (payload.productId) obj.set('product', { __type: 'Pointer', className: 'Product', objectId: payload.productId });
- obj.set('isDeleted', false);
- obj.set('data', { tags: payload.tags || [], comments: [], relatedSpace: payload.relatedSpace, relatedStage: payload.relatedStage });
- const saved = await obj.save();
- const model = this.parseToModel(saved);
- const list = this.ensure(projectId);
- list.push(model);
- this.store.set(projectId, list);
- return model;
- }
- // 更新问题(持久化)
- async updateIssue(projectId: string, issueId: string, updates: Partial<ProjectIssue>): Promise<ProjectIssue | null> {
- const query = new Parse.Query('ProjectIssue');
- const obj = await query.get(issueId).catch(() => null);
- if (!obj) return null;
- if (updates.title !== undefined) obj.set('title', updates.title);
- if (updates.description !== undefined) obj.set('description', updates.description);
- if (updates.priority !== undefined) obj.set('priority', updates.priority);
- if (updates.type !== undefined) obj.set('issueType', updates.type);
- if (updates.dueDate !== undefined) obj.set('dueDate', updates.dueDate);
- if (updates.assigneeId !== undefined) {
- obj.set('assignee', updates.assigneeId ? { __type: 'Pointer', className: 'Profile', objectId: updates.assigneeId } : undefined);
- }
- if (updates.relatedSpace !== undefined) obj.set('relatedSpace', updates.relatedSpace || undefined);
- if (updates.relatedStage !== undefined) obj.set('relatedStage', updates.relatedStage || undefined);
- if (updates.productId !== undefined) obj.set('product', updates.productId ? { __type: 'Pointer', className: 'Product', objectId: updates.productId } : undefined);
- if (updates.status !== undefined) obj.set('status', this.en2zh(updates.status));
- if (updates.tags !== undefined) {
- const data = obj.get('data') || {};
- data.tags = updates.tags || [];
- obj.set('data', data);
- }
- const saved = await obj.save();
- // 更新缓存
- const list = this.ensure(projectId);
- const idx = list.findIndex((issue: ProjectIssue) => issue.id === issueId);
- const updated = this.parseToModel(saved);
- if (idx >= 0) list[idx] = updated; else list.push(updated);
- this.store.set(projectId, list);
- return updated;
- }
- async deleteIssue(projectId: string, issueId: string): Promise<boolean> {
- const query = new Parse.Query('ProjectIssue');
- const obj = await query.get(issueId).catch(() => null);
- if (!obj) return false;
- obj.set('isDeleted', true);
- await obj.save();
- const list = this.ensure(projectId).filter((issue: ProjectIssue) => issue.id !== issueId);
- this.store.set(projectId, list);
- return true;
- }
- // 添加评论(持久化至 data.comments)
- async addComment(projectId: string, issueId: string, authorId: string, content: string): Promise<IssueComment | null> {
- const query = new Parse.Query('ProjectIssue');
- const obj = await query.get(issueId).catch(() => null);
- if (!obj) return null;
- const comment: IssueComment = { id: uuidv4(), authorId, content, createdAt: new Date() };
- const data = obj.get('data') || {};
- data.comments = Array.isArray(data.comments) ? data.comments : [];
- data.comments.push({ ...comment });
- obj.set('data', data);
- await obj.save();
- // 更新缓存
- const list = this.ensure(projectId);
- const issue = list.find((i: ProjectIssue) => i.id === issueId);
- if (issue) {
- issue.comments = issue.comments || [];
- issue.comments.push(comment);
- issue.updatedAt = new Date();
- this.store.set(projectId, list);
- }
- return comment;
- }
- // 快速修改状态(持久化)
- async setStatus(projectId: string, issueId: string, status: IssueStatus): Promise<ProjectIssue | null> {
- return await 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 issue of list) {
- if (issue.status === 'open') counts.open++;
- else if (issue.status === 'in_progress') counts.in_progress++;
- else if (issue.status === 'resolved') counts.resolved++;
- else if (issue.status === 'closed') counts.closed++;
- }
- return counts;
- }
- // 首次访问种子数据(仍保留本地演示,不写入后端)
- seed(projectId: string) {
- const list = this.ensure(projectId);
- if (list.length > 0) return;
- const now = new Date();
- const creator = 'seed-user';
- // list.push({
- // id: uuidv4(), projectId, title: '确认客厅配色与材质样板', description: '需要确认客厅主色调与地面材质,影响方案深化。', priority: 'high', type: 'task', status: 'open', creatorId: creator, createdAt: now, updatedAt: now, dueDate: new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000), tags: ['配色', '材质'], comments: []
- // });
- // list.push({
- // id: uuidv4(), projectId, title: '主卧效果图灯光偏暗', description: '客户反馈主卧效果图灯光偏暗,需要调整光源与明暗对比。', priority: 'medium', type: 'feedback', status: 'open', creatorId: creator, createdAt: now, updatedAt: now, tags: ['灯光', '效果图'], comments: []
- // });
- // const secondId = uuidv4();
- // list.push({
- // id: secondId, projectId, title: '厨房柜体尺寸与现场不符', description: '现场复尺发现图纸尺寸与实际有偏差,需要同步调整。', priority: 'critical', type: 'bug', status: 'in_progress', creatorId: creator, createdAt: now, updatedAt: now, tags: ['复尺', '尺寸'], comments: []
- // });
- this.store.set(projectId, list);
- }
- // 内部:确保项目列表存在
- private ensure(projectId: string): ProjectIssue[] {
- if (!this.store.has(projectId)) {
- this.store.set(projectId, []);
- }
- return this.store.get(projectId)!;
- }
- }
|