瀏覽代碼

feat: project issue & group

Future 21 小時之前
父節點
當前提交
de40ee0ba2

+ 1 - 1
package.json

@@ -68,7 +68,7 @@
     "echarts": "^6.0.0",
     "esdk-obs-browserjs": "^3.25.6",
     "eventemitter3": "^5.0.1",
-    "fmode-ng": "^0.0.222",
+    "fmode-ng": "^0.0.224",
     "highlight.js": "^11.11.1",
     "jquery": "^3.7.1",
     "markdown-it": "^14.1.0",

+ 37 - 0
src/modules/project/components/get-group-joinway.ts

@@ -0,0 +1,37 @@
+import { FmodeParse, WxworkCorp, WxworkSDK } from "fmode-ng/core"
+
+const Parse = FmodeParse.with("nova")
+export async function getGroupChat(gid:string){
+    let query = new Parse.Query("GroupChat")
+    let group = await query.get(gid);
+
+    let cid = group?.get("company")?.id || localStorage.getItem("company")
+    let wwcorp = new WxworkCorp(cid);
+    let needSave = false;
+    // group = await wwsdk.syncGroupChat(group.toJSON());
+    if(!group?.get("joinUrl")){
+        needSave = true;
+        let config_id1 = (await wwcorp.externalContact.groupChat.addJoinWay({
+          scene:1,
+          chat_id_list:[group.get("chat_id")],
+        }))?.config_id
+        let joinUrl = (await wwcorp.externalContact.groupChat.getJoinWay(config_id1))?.join_way
+        group.set("joinUrl",joinUrl)
+        needSave = true
+      }
+      if(!group?.get("joinQrcode")){
+        needSave = true;
+        let config_id2 = (await wwcorp.externalContact.groupChat.addJoinWay({
+          scene:2,
+          chat_id_list:[group.get("chat_id")],
+        }))?.config_id
+        let joinQrcode = (await wwcorp.externalContact.groupChat.getJoinWay(config_id2))?.join_way
+        group.set("joinQrcode",joinQrcode)
+        needSave = true
+      }
+    if(needSave){
+        group.save();
+    }
+    console.log("ggg",group)
+    return group
+}

+ 16 - 0
src/modules/project/components/project-issues-modal/project-issues-modal.component.html

@@ -56,10 +56,25 @@
             <option value="risk">风险</option>
           </select>
         </div>
+        <div class="form-item">
+          <label>负责人</label>
+          <select [(ngModel)]="selectedAssigneeId">
+            <option [ngValue]="''">自动选择</option>
+            <option *ngFor="let a of assignees" [ngValue]="a.id">{{ a.name }}</option>
+          </select>
+        </div>
         <div class="form-item">
           <label>截止日期</label>
           <input type="date" [(ngModel)]="newDueDate" />
         </div>
+        <div class="form-item">
+          <label>关联空间</label>
+          <input type="text" [(ngModel)]="newRelatedSpace" placeholder="如:客厅/主卧/厨房" />
+        </div>
+        <div class="form-item">
+          <label>相关阶段</label>
+          <input type="text" [(ngModel)]="newRelatedStage" placeholder="如:建模/渲染/后期" />
+        </div>
         <div class="form-item full">
           <label>描述</label>
           <textarea rows="3" [(ngModel)]="newDescription" placeholder="可选:详细说明"></textarea>
@@ -116,6 +131,7 @@
             <div class="comment-input">
               <input type="text" placeholder="添加评论或催办..." #cInput>
               <button class="primary" (click)="addComment(issue, cInput.value); cInput.value=''">发送</button>
+              <button class="primary" (click)="issueAlert(issue); cInput.value=''">催办</button>
             </div>
           </div>
         </div>

+ 105 - 16
src/modules/project/components/project-issues-modal/project-issues-modal.component.ts

@@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
 import { FmodeObject } from 'fmode-ng/parse';
 import { ProjectIssueService, ProjectIssue, IssueStatus, IssuePriority, IssueType, IssueCounts } from '../../services/project-issue.service';
+import { WxworkCorp } from 'fmode-ng/core';
 
 @Component({
   selector: 'app-project-issues-modal',
@@ -31,11 +32,20 @@ export class ProjectIssuesModalComponent implements OnInit, OnChanges {
   newType: IssueType = 'task';
   newDueDate?: string;
   newTagsText: string = '';
+  // 新增:负责人与关联信息
+  assignees: { id: string; name: string }[] = [];
+  selectedAssigneeId: string = '';
+  newRelatedSpace: string = '';
+  newRelatedStage: string = '';
 
   loading: boolean = false;
   error: string | null = null;
+  wwcorp:WxworkCorp | null = null;
 
-  constructor(private issueService: ProjectIssueService) {}
+  constructor(private issueService: ProjectIssueService) {
+    let cid = localStorage.getItem('company') || this.project?.get("company")?.id || ""
+    this.wwcorp = new WxworkCorp(cid);
+  }
 
   ngOnInit() {
     this.refresh();
@@ -50,12 +60,15 @@ export class ProjectIssuesModalComponent implements OnInit, OnChanges {
     }
   }
 
-  refresh() {
+  async refresh() {
     if (!this.project?.id) return;
     try {
       this.loading = true;
-      // 首次种子数据(仅内存,无副作用)
-      this.issueService.seed(this.project.id);
+      // 从后端刷新缓存并应用筛选
+      await this.issueService.refreshFromServer(this.project.id, {
+        status: this.filterStatus,
+        text: this.searchText
+      });
       this.issues = this.issueService.listIssues(this.project.id, {
         status: this.filterStatus,
         text: this.searchText
@@ -63,7 +76,7 @@ export class ProjectIssuesModalComponent implements OnInit, OnChanges {
       this.counts = this.issueService.getCounts(this.project.id);
       this.loading = false;
     } catch (err: any) {
-      this.error = err.message || '加载问题列表失败';
+      this.error = err?.message || '加载问题列表失败';
       this.loading = false;
     }
   }
@@ -78,7 +91,7 @@ export class ProjectIssuesModalComponent implements OnInit, OnChanges {
     this.refresh();
   }
 
-  startCreate() {
+  async startCreate() {
     this.creating = true;
     this.newTitle = '';
     this.newDescription = '';
@@ -86,13 +99,20 @@ export class ProjectIssuesModalComponent implements OnInit, OnChanges {
     this.newType = 'task';
     this.newDueDate = undefined;
     this.newTagsText = '';
+    this.selectedAssigneeId = '';
+    this.newRelatedSpace = '';
+    this.newRelatedStage = '';
+    // 加载负责人候选
+    if (this.project?.id) {
+      this.assignees = await this.issueService.listAssignees(this.project.id);
+    }
   }
 
   cancelCreate() {
     this.creating = false;
   }
 
-  submitCreate() {
+  async submitCreate() {
     if (!this.project?.id || !this.currentUser?.id) return;
     if (!this.newTitle.trim()) return;
 
@@ -103,42 +123,111 @@ export class ProjectIssuesModalComponent implements OnInit, OnChanges {
 
     const due = this.newDueDate ? new Date(this.newDueDate) : undefined;
 
-    this.issueService.createIssue(this.project.id, {
+    await this.issueService.createIssue(this.project.id, {
       title: this.newTitle.trim(),
       description: this.newDescription.trim(),
       priority: this.newPriority,
       type: this.newType,
       creatorId: this.currentUser.id,
-      assigneeId: undefined,
+      assigneeId: this.selectedAssigneeId || undefined,
       dueDate: due,
-      tags
+      tags,
+      relatedSpace: this.newRelatedSpace.trim() || undefined,
+      relatedStage: this.newRelatedStage.trim() || undefined
     });
 
     this.creating = false;
     this.refresh();
   }
 
-  setStatus(issue: ProjectIssue, status: IssueStatus) {
+  async setStatus(issue: ProjectIssue, status: IssueStatus) {
     if (!this.project?.id) return;
-    this.issueService.setStatus(this.project.id, issue.id, status);
+    await this.issueService.setStatus(this.project.id, issue.id, status);
     this.refresh();
   }
 
-  deleteIssue(issue: ProjectIssue) {
+  async deleteIssue(issue: ProjectIssue) {
     if (!this.project?.id) return;
-    this.issueService.deleteIssue(this.project.id, issue.id);
+    await this.issueService.deleteIssue(this.project.id, issue.id);
     this.refresh();
   }
 
-  addComment(issue: ProjectIssue, text: string) {
+  async addComment(issue: ProjectIssue, text: string) {
     if (!this.project?.id || !this.currentUser?.id) return;
     const content = text.trim();
     if (!content) return;
-    this.issueService.addComment(this.project.id, issue.id, this.currentUser.id, content);
+
+    const md = `**项目:** ${this.project?.get('title')}
+**问题:** ${issue.title}
+**优先级:** ${this.priorityLabel(issue.priority)}
+**类型:** ${this.typeLabel(issue.type)}
+**状态:** ${this.statusLabel(issue.status)}
+**评论人:** ${this.currentUser?.get('name') || ''}
+<span style="color:red">**消息:** ${content}</span>`;
+
+    this.wwcorp?.message.sendMarkdown({
+      agentid:"1000017",
+      touser: issue.assignee?.get('userid') || '',
+      content: md
+    });
+
+    await this.issueService.addComment(this.project.id, issue.id, this.currentUser.id, content);
     this.refresh();
   }
 
+  issueAlert(issue: ProjectIssue){
+    const md = `**项目:** ${this.project?.get('title')}
+**问题:** ${issue.title}
+**优先级:** ${this.priorityLabel(issue.priority)}
+**类型:** ${this.typeLabel(issue.type)}
+**状态:** ${this.statusLabel(issue.status)}
+**描述:** ${issue.description || '暂无'}
+**截止日期:** ${issue.dueDate ? new Date(issue.dueDate).toLocaleString() : '未设置'}
+**标签:** ${issue.tags?.join(', ') || '无'}`;
+
+    this.wwcorp?.message.sendMarkdown({
+      agentid:"1000017",
+      touser: issue.assignee?.get('userid') || '',
+      content: md
+    });
+  }
+
   onClose() {
     this.close.emit();
   }
+
+  // 辅助:状态中文标签
+  statusLabel(s: IssueStatus): string {
+    const map: Record<IssueStatus, string> = {
+      open: '待处理',
+      in_progress: '进行中',
+      resolved: '已解决',
+      closed: '已关闭'
+    };
+    return map[s] || s;
+  }
+
+  // 辅助:优先级中文标签
+  priorityLabel(p: IssuePriority): string {
+    const map: Record<IssuePriority, string> = {
+      low: '低',
+      medium: '中',
+      high: '高',
+      critical: '紧急',
+      urgent: '紧急'
+    };
+    return map[p] || p;
+  }
+
+  // 辅助:类型中文标签
+  typeLabel(t: IssueType): string {
+    const map: Record<IssueType, string> = {
+      task: '任务',
+      bug: '缺陷',
+      feature: '需求',
+      feedback:"反馈",
+      risk:"风险"
+    };
+    return map[t] || t;
+  }
 }

+ 17 - 6
src/modules/project/components/project-members-modal/project-members-modal.component.ts

@@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
 import { FmodeObject, FmodeQuery, FmodeParse } from 'fmode-ng/parse';
 import { WxworkCorp, WxworkSDK } from 'fmode-ng/core';
+import { getGroupChat } from '../get-group-joinway';
 
 const Parse = FmodeParse.with('nova');
 
@@ -81,8 +82,9 @@ export class ProjectMembersModalComponent implements OnInit {
 
   private checkWxworkEnvironment(): void {
     // 检查是否在企业微信环境中
-    this.wecorp = new WxworkCorp(this.cid);
-    this.wwsdk = new WxworkSDK({cid:this.cid,appId:'crm'});
+    let cid = this.project?.get("company")?.id || localStorage.getItem("company")
+    this.wecorp = new WxworkCorp(cid);
+    this.wwsdk = new WxworkSDK({cid:cid,appId:'crm'});
     console.log('✅ 企业微信环境检测成功');
   }
 
@@ -102,6 +104,10 @@ export class ProjectMembersModalComponent implements OnInit {
         gcQuery2.equalTo('project', this.project?.id);
         this.groupChat = await gcQuery2.first();
       }
+
+      if(this.groupChat?.id && !this.groupChat?.get("joinUrl")){
+        this.groupChat = await getGroupChat(this.groupChat?.id)
+      }
       const groupChatMembers = await this.loadGroupChatMembers();
 
       // 3. 合并成员数据
@@ -173,7 +179,7 @@ export class ProjectMembersModalComponent implements OnInit {
     groupChatMembers.forEach(groupMember => {
       // 查找是否已在项目团队中
       const existingMember = Array.from(memberMap.values()).find(
-        m => m.userid === groupMember.userid || m.name === groupMember.name
+        m => m.userid === groupMember.userid
       );
 
       if (existingMember) {
@@ -253,9 +259,6 @@ export class ProjectMembersModalComponent implements OnInit {
 
   async addMemberToGroupChat(member: ProjectMember): Promise<void> {
    
-    if(!this.isWechat){
-      alert("请在企业微信客户端添加")
-    }
     if (!member.userid) {
       alert('该成员没有用户ID,无法添加到群聊');
       return;
@@ -271,6 +274,14 @@ export class ProjectMembersModalComponent implements OnInit {
       console.log(`🚀 开始添加成员 ${member.name} (${member.userid}) 到群聊 ${chatId}`);
 
       // TODO: 实现正确的企业微信API调用
+      
+          const md = `**项目:** ${this.project?.get('title')}开始啦!就等你来了。\n<a href="${this.groupChat?.get("joinUrl")}">加入项目群</a>`;
+
+          this.wecorp?.message.sendMarkdown({
+            agentid:"1000017",
+            touser: member.userid || '',
+            content: md
+          });
       let result = await this.wwsdk?.ww.updateEnterpriseChat({
         chatId: chatId,
         userIdsToAdd: [member.userid]

+ 4 - 3
src/modules/project/pages/project-detail/project-detail.component.ts

@@ -128,10 +128,7 @@ export class ProjectDetailComponent implements OnInit {
       if (!this.currentUser?.id) {
         this.currentUser = await this.wxAuth?.currentProfile();
       }
-      console.log("777",this.currentUser)
-
       // 设置权限
-      console.log(this.currentUser)
       this.role = this.currentUser?.get('roleName') || '';
       this.canEdit = ['客服', '组员', '组长', '管理员'].includes(this.role);
       this.canViewCustomerPhone = ['客服', '组长', '管理员'].includes(this.role);
@@ -173,6 +170,8 @@ export class ProjectDetailComponent implements OnInit {
 
       }
 
+     
+
       if(!this.groupChat?.id){
         const gcQuery2 = new Parse.Query('GroupChat');
         gcQuery2.equalTo('project', this.projectId);
@@ -180,6 +179,8 @@ export class ProjectDetailComponent implements OnInit {
         this.groupChat = await gcQuery2.first();
       }
 
+      this.wxwork?.syncGroupChat(this.groupChat?.toJSON())
+
       if (!this.project) {
         throw new Error('无法加载项目信息');
       }

+ 261 - 89
src/modules/project/services/project-issue.service.ts

@@ -1,9 +1,12 @@
 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';
-export type IssueType = 'bug' | 'task' | 'feedback' | 'risk';
+export type IssuePriority = 'low' | 'medium' | 'high' | 'critical' | 'urgent';
+export type IssueType = 'bug' | 'task' | 'feedback' | 'risk' | 'feature';
 
 export interface IssueComment {
   id: string;
@@ -20,6 +23,7 @@ export interface ProjectIssue {
   priority: IssuePriority;
   type: IssueType;
   creatorId: string;
+  assignee?: FmodeObject;
   assigneeId?: string;
   createdAt: Date;
   updatedAt: Date;
@@ -27,6 +31,10 @@ export interface ProjectIssue {
   tags?: string[];
   description?: string;
   comments?: IssueComment[];
+  // 新增:关联信息
+  relatedSpace?: string;
+  relatedStage?: string;
+  productId?: string;
 }
 
 export interface IssueCounts {
@@ -41,138 +49,302 @@ export interface IssueCounts {
 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 = [...list];
+    let result: ProjectIssue[] = [...list];
 
     if (opts?.status && opts.status.length > 0) {
-      result = result.filter(i => opts.status!.includes(i.status));
+      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(i =>
-        (i.title || '').toLowerCase().includes(q) ||
-        (i.description || '').toLowerCase().includes(q) ||
-        (i.tags || []).some(t => t.toLowerCase().includes(q))
+      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, b) => +new Date(b.updatedAt) - +new Date(a.updatedAt));
+    return result.sort((a: ProjectIssue, b: ProjectIssue) => +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: []
-    };
+  // 从后端刷新到缓存
+  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(issue);
+    list.push(model);
     this.store.set(projectId, list);
-    return issue;
+
+    return model;
   }
 
-  /** 更新问题 */
-  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;
+  // 更新问题(持久化)
+  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;
 
-    const now = new Date();
-    const updated: ProjectIssue = { ...list[idx], ...updates, updatedAt: now };
-    list[idx] = updated;
+    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;
   }
 
-  /** 删除问题 */
-  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;
+  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;
   }
 
-  /** 添加评论(用于催办或讨论) */
-  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;
+  // 添加评论(持久化至 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() };
-    issue.comments = issue.comments || [];
-    issue.comments.push(comment);
-    issue.updatedAt = new Date();
-    this.store.set(projectId, list);
+    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;
   }
 
-  /** 快速修改状态 */
-  setStatus(projectId: string, issueId: string, status: IssueStatus): ProjectIssue | null {
-    return this.updateIssue(projectId, issueId, { status });
+  // 快速修改状态(持久化)
+  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 i of list) counts[i.status]++;
+    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';
-    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');
+    // 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, []);