目标:将组长端待办任务区域从现有的模拟数据样式,重构为基于真实项目问题板块的紧凑列表式布局。
核心逻辑:
ProjectIssue)中出现新问题或未处理的问题时,自动在组长端待办任务区域显示数据表:ProjectIssue(问题表)
筛选条件:
status = '待处理'(open)或 '处理中'(in_progress)isDeleted ≠ truecreatedAt 或 updatedAt 倒序排列关联数据:
project:关联的项目信息(项目名称、ID)creator:问题创建人assignee:负责人(默认为项目负责人或组长)priority:优先级(low/medium/high/critical/urgent)type:问题类型(bug/task/feedback/risk/feature)列表式紧凑布局:
┌─────────────────────────────────────────────────┐
│ 待办任务 (12) [刷新] [全部查看] │
├─────────────────────────────────────────────────┤
│ 🔴 [紧急] 厨房柜体尺寸与现场不符 │
│ 项目: 金地格林小镇 | 主卧 | 建模阶段 │
│ 创建于 2小时前 · 指派给: 王刚 │
│ [查看详情] [标记已读] │
├─────────────────────────────────────────────────┤
│ 🟠 [高] 主卧效果图灯光偏暗 │
│ 项目: 碧桂园天玺 | 主卧 | 渲染阶段 │
│ 创建于 5小时前 · 指派给: 李娜 │
│ [查看详情] [标记已读] │
├─────────────────────────────────────────────────┤
│ 🟡 [中] 确认客厅配色与材质样板 │
│ 项目: 万科城市之光 | 客厅 | 方案阶段 │
│ 创建于 1天前 · 指派给: 张三 │
│ [查看详情] [标记已读] │
└─────────────────────────────────────────────────┘
interface TodoTaskFromIssue {
id: string; // 问题ID
title: string; // 问题标题
description?: string; // 问题描述
priority: IssuePriority; // 优先级
type: IssueType; // 问题类型
status: IssueStatus; // 问题状态
projectId: string; // 项目ID
projectName: string; // 项目名称
relatedSpace?: string; // 关联空间(如:主卧、客厅)
relatedStage?: string; // 关联阶段(如:建模、渲染)
assigneeName?: string; // 负责人姓名
creatorName?: string; // 创建人姓名
createdAt: Date; // 创建时间
updatedAt: Date; // 更新时间
dueDate?: Date; // 截止时间
tags?: string[]; // 标签
}
const PRIORITY_CONFIG = {
urgent: {
label: '紧急',
icon: '🔴',
color: '#dc2626',
order: 0
},
critical: {
label: '紧急',
icon: '🔴',
color: '#dc2626',
order: 0
},
high: {
label: '高',
icon: '🟠',
color: '#ea580c',
order: 1
},
medium: {
label: '中',
icon: '🟡',
color: '#ca8a04',
order: 2
},
low: {
label: '低',
icon: '⚪',
color: '#9ca3af',
order: 3
}
};
async loadTodoTasksFromIssues(): Promise<TodoTaskFromIssue[]> {
try {
const query = new Parse.Query('ProjectIssue');
// 筛选条件:待处理 + 处理中
query.containedIn('status', ['待处理', '处理中']);
query.notEqualTo('isDeleted', true);
// 关联数据
query.include(['project', 'creator', 'assignee']);
// 排序:优先级 -> 更新时间
query.descending('updatedAt');
// 限制数量(首屏加载)
query.limit(20);
const results = await query.find();
// 数据转换
const tasks: TodoTaskFromIssue[] = results.map(obj => {
const project = obj.get('project');
const assignee = obj.get('assignee');
const creator = obj.get('creator');
return {
id: obj.id,
title: obj.get('title') || '未命名问题',
description: obj.get('description'),
priority: obj.get('priority') as IssuePriority,
type: obj.get('issueType') as IssueType,
status: this.zh2en(obj.get('status')) as IssueStatus,
projectId: project?.id || '',
projectName: project?.get('name') || '未知项目',
relatedSpace: obj.get('relatedSpace'),
relatedStage: obj.get('relatedStage'),
assigneeName: assignee?.get('name') || assignee?.get('realname') || '未指派',
creatorName: creator?.get('name') || creator?.get('realname') || '未知',
createdAt: obj.createdAt,
updatedAt: obj.updatedAt,
dueDate: obj.get('dueDate'),
tags: (obj.get('data')?.tags || []) as string[]
};
});
// 二次排序:优先级 -> 时间
return tasks.sort((a, b) => {
const priorityA = PRIORITY_CONFIG[a.priority].order;
const priorityB = PRIORITY_CONFIG[b.priority].order;
if (priorityA !== priorityB) {
return priorityA - priorityB;
}
return +new Date(b.updatedAt) - +new Date(a.updatedAt);
});
} catch (error) {
console.error('❌ 加载待办任务失败:', error);
return [];
}
}
private zh2en(status: string): IssueStatus {
const map: Record<string, IssueStatus> = {
'待处理': 'open',
'处理中': 'in_progress',
'已解决': 'resolved',
'已关闭': 'closed'
};
return map[status] || 'open';
}
src/app/pages/team-leader/dashboard/
├── dashboard.ts # 主组件逻辑
├── dashboard.html # 模板文件
├── dashboard.scss # 样式文件
└── components/
└── todo-task-item/ # 待办任务列表项组件(可选)
├── todo-task-item.ts
├── todo-task-item.html
└── todo-task-item.scss
export class Dashboard implements OnInit {
// 待办任务列表
todoTasksFromIssues: TodoTaskFromIssue[] = [];
loadingTodoTasks: boolean = false;
todoTaskError: string = '';
// 自动刷新定时器
private todoTaskRefreshTimer: any;
ngOnInit() {
// 初始加载
this.loadTodoTasksFromIssues();
// 启动自动刷新(每5分钟)
this.startAutoRefresh();
}
ngOnDestroy() {
// 清理定时器
if (this.todoTaskRefreshTimer) {
clearInterval(this.todoTaskRefreshTimer);
}
}
/**
* 加载待办任务(从问题板块)
*/
async loadTodoTasksFromIssues(): Promise<void> {
this.loadingTodoTasks = true;
this.todoTaskError = '';
try {
const query = new Parse.Query('ProjectIssue');
query.containedIn('status', ['待处理', '处理中']);
query.notEqualTo('isDeleted', true);
query.include(['project', 'creator', 'assignee']);
query.descending('updatedAt');
query.limit(20);
const results = await query.find();
this.todoTasksFromIssues = results.map(obj => {
const project = obj.get('project');
const assignee = obj.get('assignee');
const creator = obj.get('creator');
return {
id: obj.id,
title: obj.get('title') || '未命名问题',
description: obj.get('description'),
priority: obj.get('priority') as IssuePriority,
type: obj.get('issueType') as IssueType,
status: this.zh2en(obj.get('status')) as IssueStatus,
projectId: project?.id || '',
projectName: project?.get('name') || '未知项目',
relatedSpace: obj.get('relatedSpace'),
relatedStage: obj.get('relatedStage'),
assigneeName: assignee?.get('name') || assignee?.get('realname') || '未指派',
creatorName: creator?.get('name') || creator?.get('realname') || '未知',
createdAt: obj.createdAt,
updatedAt: obj.updatedAt,
dueDate: obj.get('dueDate'),
tags: (obj.get('data')?.tags || []) as string[]
};
});
// 排序:优先级 -> 时间
this.todoTasksFromIssues.sort((a, b) => {
const priorityA = this.getPriorityOrder(a.priority);
const priorityB = this.getPriorityOrder(b.priority);
if (priorityA !== priorityB) {
return priorityA - priorityB;
}
return +new Date(b.updatedAt) - +new Date(a.updatedAt);
});
console.log(`✅ 加载待办任务成功,共 ${this.todoTasksFromIssues.length} 条`);
} catch (error) {
console.error('❌ 加载待办任务失败:', error);
this.todoTaskError = '加载失败,请稍后重试';
} finally {
this.loadingTodoTasks = false;
}
}
/**
* 启动自动刷新
*/
startAutoRefresh(): void {
// 每5分钟刷新一次
this.todoTaskRefreshTimer = setInterval(() => {
this.loadTodoTasksFromIssues();
}, 5 * 60 * 1000);
}
/**
* 手动刷新
*/
refreshTodoTasks(): void {
this.loadTodoTasksFromIssues();
}
/**
* 跳转到项目问题详情
*/
navigateToIssue(task: TodoTaskFromIssue): void {
const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
// 跳转到项目详情页,并打开问题板块
this.router.navigate(
['/wxwork', cid, 'project', task.projectId, 'order'],
{
queryParams: {
openIssues: 'true',
highlightIssue: task.id
}
}
);
}
/**
* 标记问题为已读(可选功能)
*/
async markAsRead(task: TodoTaskFromIssue): Promise<void> {
try {
// 实现方式1: 本地隐藏(不修改数据库)
this.todoTasksFromIssues = this.todoTasksFromIssues.filter(t => t.id !== task.id);
// 实现方式2: 添加"已读"标记到数据库(需扩展数据模型)
// const query = new Parse.Query('ProjectIssue');
// const issue = await query.get(task.id);
// const readBy = issue.get('readBy') || [];
// if (!readBy.includes(currentUserId)) {
// readBy.push(currentUserId);
// issue.set('readBy', readBy);
// await issue.save();
// }
console.log(`✅ 标记问题为已读: ${task.title}`);
} catch (error) {
console.error('❌ 标记已读失败:', error);
}
}
/**
* 获取优先级配置
*/
getPriorityConfig(priority: IssuePriority) {
const config = {
urgent: { label: '紧急', icon: '🔴', color: '#dc2626', order: 0 },
critical: { label: '紧急', icon: '🔴', color: '#dc2626', order: 0 },
high: { label: '高', icon: '🟠', color: '#ea580c', order: 1 },
medium: { label: '中', icon: '🟡', color: '#ca8a04', order: 2 },
low: { label: '低', icon: '⚪', color: '#9ca3af', order: 3 }
};
return config[priority] || config.medium;
}
getPriorityOrder(priority: IssuePriority): number {
return this.getPriorityConfig(priority).order;
}
/**
* 获取问题类型中文名
*/
getIssueTypeLabel(type: IssueType): string {
const map: Record<IssueType, string> = {
bug: '问题',
task: '任务',
feedback: '反馈',
risk: '风险',
feature: '需求'
};
return map[type] || '任务';
}
/**
* 格式化相对时间
*/
formatRelativeTime(date: Date): string {
const now = new Date();
const diff = now.getTime() - new Date(date).getTime();
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (minutes < 60) {
return `${minutes}分钟前`;
} else if (hours < 24) {
return `${hours}小时前`;
} else if (days < 7) {
return `${days}天前`;
} else {
return new Date(date).toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' });
}
}
/**
* 状态映射
*/
private zh2en(status: string): IssueStatus {
const map: Record<string, IssueStatus> = {
'待处理': 'open',
'处理中': 'in_progress',
'已解决': 'resolved',
'已关闭': 'closed'
};
return map[status] || 'open';
}
}
<!-- 待办任务优先级排序 -->
<section class="todo-section">
<div class="section-header">
<h2>
待办任务
@if (todoTasksFromIssues.length > 0) {
<span class="task-count">({{ todoTasksFromIssues.length }})</span>
}
</h2>
<div class="section-actions">
<button
class="btn-refresh"
(click)="refreshTodoTasks()"
[disabled]="loadingTodoTasks"
title="刷新待办任务">
<svg viewBox="0 0 24 24" width="16" height="16" [class.rotating]="loadingTodoTasks">
<path fill="currentColor" d="M17.65 6.35A7.958 7.958 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>
</svg>
刷新
</button>
</div>
</div>
<!-- 加载状态 -->
@if (loadingTodoTasks) {
<div class="loading-state">
<div class="spinner"></div>
<p>加载中...</p>
</div>
}
<!-- 错误状态 -->
@if (todoTaskError && !loadingTodoTasks) {
<div class="error-state">
<svg viewBox="0 0 24 24" width="48" height="48" fill="#dc2626">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
</svg>
<p>{{ todoTaskError }}</p>
<button class="btn-retry" (click)="refreshTodoTasks()">重试</button>
</div>
}
<!-- 空状态 -->
@if (!loadingTodoTasks && !todoTaskError && todoTasksFromIssues.length === 0) {
<div class="empty-state">
<svg viewBox="0 0 24 24" width="64" height="64" fill="#d1d5db">
<path d="M19 3h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm2 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/>
</svg>
<p>暂无待办任务</p>
<p class="hint">所有项目问题都已处理完毕 🎉</p>
</div>
}
<!-- 待办任务列表 -->
@if (!loadingTodoTasks && !todoTaskError && todoTasksFromIssues.length > 0) {
<div class="todo-list-compact">
@for (task of todoTasksFromIssues; track task.id) {
<div class="todo-item-compact"
(click)="navigateToIssue(task)"
[attr.data-priority]="task.priority">
<!-- 优先级指示器 -->
<div class="priority-indicator"
[style.background-color]="getPriorityConfig(task.priority).color">
</div>
<!-- 主要内容 -->
<div class="task-content">
<!-- 标题行 -->
<div class="task-header">
<span class="priority-icon">{{ getPriorityConfig(task.priority).icon }}</span>
<span class="priority-label"
[style.color]="getPriorityConfig(task.priority).color">
[{{ getPriorityConfig(task.priority).label }}]
</span>
<h4 class="task-title">{{ task.title }}</h4>
<span class="issue-type-badge">{{ getIssueTypeLabel(task.type) }}</span>
</div>
<!-- 元信息行 -->
<div class="task-meta">
<span class="meta-item">
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
<path d="M20 6h-2.18c.11-.31.18-.65.18-1 0-1.66-1.34-3-3-3-1.05 0-1.96.54-2.5 1.35l-.5.67-.5-.68C10.96 2.54 10.05 2 9 2 7.34 2 6 3.34 6 5c0 .35.07.69.18 1H4c-1.11 0-1.99.89-1.99 2L2 19c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2zm-5-2c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zM9 4c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm11 15H4v-2h16v2zm0-5H4V8h5.08L7 10.83 8.62 12 11 8.76l1-1.36 1 1.36L15.38 12 17 10.83 14.92 8H20v6z"/>
</svg>
{{ task.projectName }}
</span>
@if (task.relatedSpace) {
<span class="meta-item">
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
<path d="M12 2L2 7v10c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V7l-10-5zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V9.03l7-3.11v7.07z"/>
</svg>
{{ task.relatedSpace }}
</span>
}
@if (task.relatedStage) {
<span class="meta-item">
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
<path d="M9 11H7v2h2v-2zm4 0h-2v2h2v-2zm4 0h-2v2h2v-2zm2-7h-1V2h-2v2H8V2H6v2H5c-1.11 0-1.99.9-1.99 2L3 20c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V9h14v11z"/>
</svg>
{{ task.relatedStage }}
</span>
}
</div>
<!-- 底部信息行 -->
<div class="task-footer">
<span class="time-info">
<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
<path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z"/>
</svg>
创建于 {{ formatRelativeTime(task.createdAt) }}
</span>
<span class="assignee-info">
<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
</svg>
指派给: {{ task.assigneeName }}
</span>
@if (task.tags && task.tags.length > 0) {
<div class="tags">
@for (tag of task.tags.slice(0, 2); track tag) {
<span class="tag">{{ tag }}</span>
}
@if (task.tags.length > 2) {
<span class="tag-more">+{{ task.tags.length - 2 }}</span>
}
</div>
}
</div>
</div>
<!-- 操作按钮 -->
<div class="task-actions">
<button
class="btn-view"
(click)="navigateToIssue(task); $event.stopPropagation()"
title="查看详情">
查看详情
</button>
<button
class="btn-mark-read"
(click)="markAsRead(task); $event.stopPropagation()"
title="标记已读">
标记已读
</button>
</div>
</div>
}
</div>
}
</section>
.todo-section {
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
h2 {
font-size: 20px;
font-weight: 600;
color: #111827;
display: flex;
align-items: center;
gap: 8px;
.task-count {
font-size: 16px;
color: #6b7280;
font-weight: 400;
}
}
.section-actions {
display: flex;
gap: 12px;
.btn-refresh {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
background: #f3f4f6;
border: 1px solid #e5e7eb;
border-radius: 6px;
font-size: 14px;
color: #374151;
cursor: pointer;
transition: all 0.2s;
&:hover:not(:disabled) {
background: #e5e7eb;
border-color: #d1d5db;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
svg.rotating {
animation: rotate 1s linear infinite;
}
}
}
}
// 加载/错误/空状态
.loading-state,
.error-state,
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 24px;
text-align: center;
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f4f6;
border-top-color: #667eea;
border-radius: 50%;
animation: rotate 1s linear infinite;
}
p {
margin-top: 16px;
font-size: 14px;
color: #6b7280;
&.hint {
font-size: 13px;
color: #9ca3af;
margin-top: 8px;
}
}
.btn-retry {
margin-top: 16px;
padding: 8px 20px;
background: #667eea;
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
&:hover {
background: #5568d3;
}
}
}
// 紧凑列表
.todo-list-compact {
display: flex;
flex-direction: column;
gap: 12px;
.todo-item-compact {
position: relative;
display: flex;
align-items: stretch;
background: #fafafa;
border: 1px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
transition: all 0.2s;
cursor: pointer;
&:hover {
background: #f9fafb;
border-color: #d1d5db;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
transform: translateY(-1px);
}
// 优先级指示条
.priority-indicator {
width: 4px;
flex-shrink: 0;
}
// 主要内容区
.task-content {
flex: 1;
padding: 16px;
min-width: 0;
.task-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
.priority-icon {
font-size: 16px;
flex-shrink: 0;
}
.priority-label {
font-size: 12px;
font-weight: 600;
flex-shrink: 0;
}
.task-title {
font-size: 15px;
font-weight: 500;
color: #111827;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.issue-type-badge {
padding: 2px 8px;
background: #e0e7ff;
color: #4f46e5;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
flex-shrink: 0;
}
}
.task-meta {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 8px;
flex-wrap: wrap;
.meta-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
color: #6b7280;
svg {
opacity: 0.6;
}
}
}
.task-footer {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
.time-info,
.assignee-info {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #9ca3af;
svg {
opacity: 0.7;
}
}
.tags {
display: flex;
align-items: center;
gap: 6px;
.tag {
padding: 2px 6px;
background: #f3f4f6;
color: #6b7280;
border-radius: 3px;
font-size: 11px;
}
.tag-more {
font-size: 11px;
color: #9ca3af;
}
}
}
}
// 操作按钮区
.task-actions {
display: flex;
flex-direction: column;
gap: 8px;
padding: 16px;
border-left: 1px solid #e5e7eb;
background: white;
button {
padding: 6px 12px;
border: 1px solid #d1d5db;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
&.btn-view {
background: #667eea;
color: white;
border-color: #667eea;
&:hover {
background: #5568d3;
border-color: #5568d3;
}
}
&.btn-mark-read {
background: white;
color: #6b7280;
&:hover {
background: #f9fafb;
border-color: #9ca3af;
}
}
}
}
}
}
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
// 响应式布局
@media (max-width: 768px) {
.todo-section {
padding: 16px;
.section-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.todo-list-compact {
.todo-item-compact {
flex-direction: column;
.task-actions {
flex-direction: row;
border-left: none;
border-top: 1px solid #e5e7eb;
padding: 12px;
button {
flex: 1;
}
}
}
}
}
}
┌─────────────────┐
│ ProjectIssue │ Parse Server 数据表
│ (问题表) │
└────────┬────────┘
│
│ Query: status in ['待处理','处理中']
│ isDeleted != true
│ include: ['project','creator','assignee']
│ descending: 'updatedAt'
│ limit: 20
│
▼
┌─────────────────┐
│ Dashboard.ts │ 组件逻辑层
│ │
│ - loadTodoTasks() ◄── 初始加载 (ngOnInit)
│ - refreshTodoTasks() ◄── 手动刷新
│ - startAutoRefresh() ◄── 自动刷新 (5分钟)
│ - navigateToIssue() ◄── 跳转详情
│ - markAsRead() ◄── 标记已读
└────────┬────────┘
│
│ todoTasksFromIssues: TodoTaskFromIssue[]
│
▼
┌─────────────────┐
│ Dashboard.html │ 模板渲染层
│ │
│ - 列表循环渲染
│ - 优先级样式
│ - 相对时间格式化
│ - 交互事件绑定
└─────────────────┘
TodoTaskFromIssue 接口loadTodoTasksFromIssues() 方法场景:所有问题都已解决或关闭
处理:
场景:问题积压,待办任务过多
处理:
场景:网络异常导致数据加载失败
处理:
场景:问题的 project、assignee 等关联数据为空
处理:
'未知项目'、'未指派'场景:用户在自动刷新期间手动点击刷新
处理:
loadingTodoTasks = true)技术方案:
功能:
功能:
功能:
功能:
ProjectIssue 表openIssues 和 highlightIssue 参数setIntervalsrc/app/pages/team-leader/dashboard/
├── dashboard.ts # ✅ 修改
├── dashboard.html # ✅ 修改
├── dashboard.scss # ✅ 修改
src/modules/project/services/
├── project-issue.service.ts # ✅ 已存在,无需修改
src/modules/project/components/
├── project-issues-modal/ # ✅ 已存在,无需修改
├── project-issues-modal.component.ts
├── project-issues-modal.component.html
└── project-issues-modal.component.scss
src/modules/project/pages/project-detail/
├── project-detail.component.ts # ✅ 可能需要接收 queryParams
├── project-detail.component.html # ✅ 可能需要支持高亮问题
本方案将组长端待办任务区域从静态模拟数据升级为基于真实项目问题板块的动态列表,实现了:
ProjectIssue 表读取预计开发周期:3-4 天
技术风险:低
业务价值:高 ✅