v1.9.0-rc.4 - TaskDrawer upgrades

This commit is contained in:
LINING-PC\lining
2026-02-02 22:38:19 +08:00
parent 8148969e1e
commit aacd7457fe
78 changed files with 406 additions and 305 deletions
+1 -1
View File
@@ -831,7 +831,7 @@ const handleTaskUpdateEvent = (e: { task: Task }) => {
// 任务添加事件处理器 // 任务添加事件处理器
const handleTaskAddEvent = (e: { task: Task }) => { const handleTaskAddEvent = (e: { task: Task }) => {
// 打印全部的tasks // 打印全部的tasks
console.log('当前所有任务:', tasks.value) //console.log('当前所有任务:', tasks.value)
const newTask = e.task const newTask = e.task
showMessage(`Demo 任务[${newTask.name}] 已创建`, 'success') showMessage(`Demo 任务[${newTask.name}] 已创建`, 'success')
+101 -101
View File
@@ -4,7 +4,7 @@
"id": "resource-001", "id": "resource-001",
"name": "资源1 - 试验主任", "name": "资源1 - 试验主任",
"type": "试验主任", "type": "试验主任",
"avatar": "https://i.pravatar.cc/150?img=1", "avatar": "/50-11.jpg",
"department": "临床部", "department": "临床部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -470,7 +470,7 @@
"id": "resource-002", "id": "resource-002",
"name": "资源2 - 数据分析师", "name": "资源2 - 数据分析师",
"type": "数据分析师", "type": "数据分析师",
"avatar": "https://i.pravatar.cc/150?img=2", "avatar": "/50-12.jpg",
"department": "质量部", "department": "质量部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -921,7 +921,7 @@
"id": "resource-003", "id": "resource-003",
"name": "资源3 - 质量专员", "name": "资源3 - 质量专员",
"type": "质量专员", "type": "质量专员",
"avatar": "https://i.pravatar.cc/150?img=3", "avatar": "/50-13.jpg",
"department": "注册部", "department": "注册部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -1387,7 +1387,7 @@
"id": "resource-004", "id": "resource-004",
"name": "资源4 - 注册专员", "name": "资源4 - 注册专员",
"type": "注册专员", "type": "注册专员",
"avatar": "https://i.pravatar.cc/150?img=4", "avatar": "/50-14.jpg",
"department": "生产部", "department": "生产部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -1808,7 +1808,7 @@
"id": "resource-005", "id": "resource-005",
"name": "资源5 - 项目经理", "name": "资源5 - 项目经理",
"type": "项目经理", "type": "项目经理",
"avatar": "https://i.pravatar.cc/150?img=5", "avatar": "/50-15.jpg",
"department": "市场部", "department": "市场部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -2259,7 +2259,7 @@
"id": "resource-006", "id": "resource-006",
"name": "资源6 - 工程师", "name": "资源6 - 工程师",
"type": "工程师", "type": "工程师",
"avatar": "https://i.pravatar.cc/150?img=6", "avatar": "/50-16.jpg",
"department": "工程部", "department": "工程部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -2650,7 +2650,7 @@
"id": "resource-007", "id": "resource-007",
"name": "资源7 - 测试工程师", "name": "资源7 - 测试工程师",
"type": "测试工程师", "type": "测试工程师",
"avatar": "https://i.pravatar.cc/150?img=7", "avatar": "/50-17.jpg",
"department": "测试部", "department": "测试部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -2658,7 +2658,7 @@
"技能C" "技能C"
], ],
"utilization": 93, "utilization": 93,
"color": "#ffc107", "color": "#a8a8a8",
"tasks": [ "tasks": [
{ {
"id": 7000, "id": 7000,
@@ -3101,7 +3101,7 @@
"id": "resource-008", "id": "resource-008",
"name": "资源8 - 研究员", "name": "资源8 - 研究员",
"type": "研究员", "type": "研究员",
"avatar": "https://i.pravatar.cc/150?img=8", "avatar": "/50-18.jpg",
"department": "研发部", "department": "研发部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -3447,7 +3447,7 @@
"id": "resource-009", "id": "resource-009",
"name": "资源9 - 试验主任", "name": "资源9 - 试验主任",
"type": "试验主任", "type": "试验主任",
"avatar": "https://i.pravatar.cc/150?img=9", "avatar": "/50-19.jpg",
"department": "临床部", "department": "临床部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -3913,7 +3913,7 @@
"id": "resource-010", "id": "resource-010",
"name": "资源10 - 数据分析师", "name": "资源10 - 数据分析师",
"type": "数据分析师", "type": "数据分析师",
"avatar": "https://i.pravatar.cc/150?img=10", "avatar": "/50-20.jpg",
"department": "质量部", "department": "质量部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -4244,7 +4244,7 @@
"id": "resource-011", "id": "resource-011",
"name": "资源11 - 质量专员", "name": "资源11 - 质量专员",
"type": "质量专员", "type": "质量专员",
"avatar": "https://i.pravatar.cc/150?img=11", "avatar": "/50-21.jpg",
"department": "注册部", "department": "注册部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -4665,7 +4665,7 @@
"id": "resource-012", "id": "resource-012",
"name": "资源12 - 注册专员", "name": "资源12 - 注册专员",
"type": "注册专员", "type": "注册专员",
"avatar": "https://i.pravatar.cc/150?img=12", "avatar": "/50-22.jpg",
"department": "生产部", "department": "生产部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -5011,7 +5011,7 @@
"id": "resource-013", "id": "resource-013",
"name": "资源13 - 项目经理", "name": "资源13 - 项目经理",
"type": "项目经理", "type": "项目经理",
"avatar": "https://i.pravatar.cc/150?img=13", "avatar": "/50-23.jpg",
"department": "市场部", "department": "市场部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -5387,7 +5387,7 @@
"id": "resource-014", "id": "resource-014",
"name": "资源14 - 工程师", "name": "资源14 - 工程师",
"type": "工程师", "type": "工程师",
"avatar": "https://i.pravatar.cc/150?img=14", "avatar": "/50-24.jpg",
"department": "工程部", "department": "工程部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -5778,7 +5778,7 @@
"id": "resource-015", "id": "resource-015",
"name": "资源15 - 测试工程师", "name": "资源15 - 测试工程师",
"type": "测试工程师", "type": "测试工程师",
"avatar": "https://i.pravatar.cc/150?img=15", "avatar": "/50-25.jpg",
"department": "测试部", "department": "测试部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -6214,7 +6214,7 @@
"id": "resource-016", "id": "resource-016",
"name": "资源16 - 研究员", "name": "资源16 - 研究员",
"type": "研究员", "type": "研究员",
"avatar": "https://i.pravatar.cc/150?img=16", "avatar": "/50-26.jpg",
"department": "研发部", "department": "研发部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -6530,7 +6530,7 @@
"id": "resource-017", "id": "resource-017",
"name": "资源17 - 试验主任", "name": "资源17 - 试验主任",
"type": "试验主任", "type": "试验主任",
"avatar": "https://i.pravatar.cc/150?img=17", "avatar": "/50-27.jpg",
"department": "临床部", "department": "临床部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -6981,7 +6981,7 @@
"id": "resource-018", "id": "resource-018",
"name": "资源18 - 数据分析师", "name": "资源18 - 数据分析师",
"type": "数据分析师", "type": "数据分析师",
"avatar": "https://i.pravatar.cc/150?img=18", "avatar": "/50-28.jpg",
"department": "质量部", "department": "质量部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -7357,7 +7357,7 @@
"id": "resource-019", "id": "resource-019",
"name": "资源19 - 质量专员", "name": "资源19 - 质量专员",
"type": "质量专员", "type": "质量专员",
"avatar": "https://i.pravatar.cc/150?img=19", "avatar": "/50-29.jpg",
"department": "注册部", "department": "注册部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -7718,7 +7718,7 @@
"id": "resource-020", "id": "resource-020",
"name": "资源20 - 注册专员", "name": "资源20 - 注册专员",
"type": "注册专员", "type": "注册专员",
"avatar": "https://i.pravatar.cc/150?img=20", "avatar": "/50-30.jpg",
"department": "生产部", "department": "生产部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -8154,7 +8154,7 @@
"id": "resource-021", "id": "resource-021",
"name": "资源21 - 项目经理", "name": "资源21 - 项目经理",
"type": "项目经理", "type": "项目经理",
"avatar": "https://i.pravatar.cc/150?img=21", "avatar": "/50-31.jpg",
"department": "市场部", "department": "市场部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -8515,7 +8515,7 @@
"id": "resource-022", "id": "resource-022",
"name": "资源22 - 工程师", "name": "资源22 - 工程师",
"type": "工程师", "type": "工程师",
"avatar": "https://i.pravatar.cc/150?img=22", "avatar": "/50-32.jpg",
"department": "工程部", "department": "工程部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -8921,7 +8921,7 @@
"id": "resource-023", "id": "resource-023",
"name": "资源23 - 测试工程师", "name": "资源23 - 测试工程师",
"type": "测试工程师", "type": "测试工程师",
"avatar": "https://i.pravatar.cc/150?img=23", "avatar": "/50-33.jpg",
"department": "测试部", "department": "测试部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -9297,7 +9297,7 @@
"id": "resource-024", "id": "resource-024",
"name": "资源24 - 研究员", "name": "资源24 - 研究员",
"type": "研究员", "type": "研究员",
"avatar": "https://i.pravatar.cc/150?img=24", "avatar": "/50-34.jpg",
"department": "研发部", "department": "研发部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -9718,7 +9718,7 @@
"id": "resource-025", "id": "resource-025",
"name": "资源25 - 试验主任", "name": "资源25 - 试验主任",
"type": "试验主任", "type": "试验主任",
"avatar": "https://i.pravatar.cc/150?img=25", "avatar": "/50-35.jpg",
"department": "临床部", "department": "临床部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -10064,7 +10064,7 @@
"id": "resource-026", "id": "resource-026",
"name": "资源26 - 数据分析师", "name": "资源26 - 数据分析师",
"type": "数据分析师", "type": "数据分析师",
"avatar": "https://i.pravatar.cc/150?img=26", "avatar": "/50-36.jpg",
"department": "质量部", "department": "质量部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -10455,7 +10455,7 @@
"id": "resource-027", "id": "resource-027",
"name": "资源27 - 质量专员", "name": "资源27 - 质量专员",
"type": "质量专员", "type": "质量专员",
"avatar": "https://i.pravatar.cc/150?img=27", "avatar": "/50-37.jpg",
"department": "注册部", "department": "注册部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -10771,7 +10771,7 @@
"id": "resource-028", "id": "resource-028",
"name": "资源28 - 注册专员", "name": "资源28 - 注册专员",
"type": "注册专员", "type": "注册专员",
"avatar": "https://i.pravatar.cc/150?img=28", "avatar": "/50-38.jpg",
"department": "生产部", "department": "生产部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -11192,7 +11192,7 @@
"id": "resource-029", "id": "resource-029",
"name": "资源29 - 项目经理", "name": "资源29 - 项目经理",
"type": "项目经理", "type": "项目经理",
"avatar": "https://i.pravatar.cc/150?img=29", "avatar": "/50-39.jpg",
"department": "市场部", "department": "市场部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -11598,7 +11598,7 @@
"id": "resource-030", "id": "resource-030",
"name": "资源30 - 工程师", "name": "资源30 - 工程师",
"type": "工程师", "type": "工程师",
"avatar": "https://i.pravatar.cc/150?img=30", "avatar": "/50-40.jpg",
"department": "工程部", "department": "工程部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -11944,7 +11944,7 @@
"id": "resource-031", "id": "resource-031",
"name": "资源31 - 测试工程师", "name": "资源31 - 测试工程师",
"type": "测试工程师", "type": "测试工程师",
"avatar": "https://i.pravatar.cc/150?img=31", "avatar": "/50-41.jpg",
"department": "测试部", "department": "测试部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -12410,7 +12410,7 @@
"id": "resource-032", "id": "resource-032",
"name": "资源32 - 研究员", "name": "资源32 - 研究员",
"type": "研究员", "type": "研究员",
"avatar": "https://i.pravatar.cc/150?img=32", "avatar": "/50-42.jpg",
"department": "研发部", "department": "研发部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -12816,7 +12816,7 @@
"id": "resource-033", "id": "resource-033",
"name": "资源33 - 试验主任", "name": "资源33 - 试验主任",
"type": "试验主任", "type": "试验主任",
"avatar": "https://i.pravatar.cc/150?img=33", "avatar": "/50-43.jpg",
"department": "临床部", "department": "临床部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -13222,7 +13222,7 @@
"id": "resource-034", "id": "resource-034",
"name": "资源34 - 数据分析师", "name": "资源34 - 数据分析师",
"type": "数据分析师", "type": "数据分析师",
"avatar": "https://i.pravatar.cc/150?img=34", "avatar": "/50-44.jpg",
"department": "质量部", "department": "质量部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -13688,7 +13688,7 @@
"id": "resource-035", "id": "resource-035",
"name": "资源35 - 质量专员", "name": "资源35 - 质量专员",
"type": "质量专员", "type": "质量专员",
"avatar": "https://i.pravatar.cc/150?img=35", "avatar": "/50-45.jpg",
"department": "注册部", "department": "注册部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -14139,7 +14139,7 @@
"id": "resource-036", "id": "resource-036",
"name": "资源36 - 注册专员", "name": "资源36 - 注册专员",
"type": "注册专员", "type": "注册专员",
"avatar": "https://i.pravatar.cc/150?img=36", "avatar": "/50-46.jpg",
"department": "生产部", "department": "生产部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -14590,7 +14590,7 @@
"id": "resource-037", "id": "resource-037",
"name": "资源37 - 项目经理", "name": "资源37 - 项目经理",
"type": "项目经理", "type": "项目经理",
"avatar": "https://i.pravatar.cc/150?img=37", "avatar": "/50-47.jpg",
"department": "市场部", "department": "市场部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -14951,7 +14951,7 @@
"id": "resource-038", "id": "resource-038",
"name": "资源38 - 工程师", "name": "资源38 - 工程师",
"type": "工程师", "type": "工程师",
"avatar": "https://i.pravatar.cc/150?img=38", "avatar": "/50-48.jpg",
"department": "工程部", "department": "工程部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -15357,7 +15357,7 @@
"id": "resource-039", "id": "resource-039",
"name": "资源39 - 测试工程师", "name": "资源39 - 测试工程师",
"type": "测试工程师", "type": "测试工程师",
"avatar": "https://i.pravatar.cc/150?img=39", "avatar": "/50-49.jpg",
"department": "测试部", "department": "测试部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -15673,7 +15673,7 @@
"id": "resource-040", "id": "resource-040",
"name": "资源40 - 研究员", "name": "资源40 - 研究员",
"type": "研究员", "type": "研究员",
"avatar": "https://i.pravatar.cc/150?img=40", "avatar": "/50-50.jpg",
"department": "研发部", "department": "研发部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -16109,7 +16109,7 @@
"id": "resource-041", "id": "resource-041",
"name": "资源41 - 试验主任", "name": "资源41 - 试验主任",
"type": "试验主任", "type": "试验主任",
"avatar": "https://i.pravatar.cc/150?img=41", "avatar": "/50-51.jpg",
"department": "临床部", "department": "临床部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -16485,7 +16485,7 @@
"id": "resource-042", "id": "resource-042",
"name": "资源42 - 数据分析师", "name": "资源42 - 数据分析师",
"type": "数据分析师", "type": "数据分析师",
"avatar": "https://i.pravatar.cc/150?img=42", "avatar": "/50-52.jpg",
"department": "质量部", "department": "质量部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -16906,7 +16906,7 @@
"id": "resource-043", "id": "resource-043",
"name": "资源43 - 质量专员", "name": "资源43 - 质量专员",
"type": "质量专员", "type": "质量专员",
"avatar": "https://i.pravatar.cc/150?img=43", "avatar": "/50-53.jpg",
"department": "注册部", "department": "注册部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -17252,7 +17252,7 @@
"id": "resource-044", "id": "resource-044",
"name": "资源44 - 注册专员", "name": "资源44 - 注册专员",
"type": "注册专员", "type": "注册专员",
"avatar": "https://i.pravatar.cc/150?img=44", "avatar": "/50-54.jpg",
"department": "生产部", "department": "生产部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -17583,7 +17583,7 @@
"id": "resource-045", "id": "resource-045",
"name": "资源45 - 项目经理", "name": "资源45 - 项目经理",
"type": "项目经理", "type": "项目经理",
"avatar": "https://i.pravatar.cc/150?img=45", "avatar": "/50-55.jpg",
"department": "市场部", "department": "市场部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -17944,7 +17944,7 @@
"id": "resource-046", "id": "resource-046",
"name": "资源46 - 工程师", "name": "资源46 - 工程师",
"type": "工程师", "type": "工程师",
"avatar": "https://i.pravatar.cc/150?img=46", "avatar": "/50-56.jpg",
"department": "工程部", "department": "工程部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -18380,7 +18380,7 @@
"id": "resource-047", "id": "resource-047",
"name": "资源47 - 测试工程师", "name": "资源47 - 测试工程师",
"type": "测试工程师", "type": "测试工程师",
"avatar": "https://i.pravatar.cc/150?img=47", "avatar": "/50-57.jpg",
"department": "测试部", "department": "测试部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -18726,7 +18726,7 @@
"id": "resource-048", "id": "resource-048",
"name": "资源48 - 研究员", "name": "资源48 - 研究员",
"type": "研究员", "type": "研究员",
"avatar": "https://i.pravatar.cc/150?img=48", "avatar": "/50-58.jpg",
"department": "研发部", "department": "研发部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -19147,7 +19147,7 @@
"id": "resource-049", "id": "resource-049",
"name": "资源49 - 试验主任", "name": "资源49 - 试验主任",
"type": "试验主任", "type": "试验主任",
"avatar": "https://i.pravatar.cc/150?img=49", "avatar": "/50-59.jpg",
"department": "临床部", "department": "临床部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -19493,7 +19493,7 @@
"id": "resource-050", "id": "resource-050",
"name": "资源50 - 数据分析师", "name": "资源50 - 数据分析师",
"type": "数据分析师", "type": "数据分析师",
"avatar": "https://i.pravatar.cc/150?img=50", "avatar": "/50-60.jpg",
"department": "质量部", "department": "质量部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -19914,7 +19914,7 @@
"id": "resource-051", "id": "resource-051",
"name": "资源51 - 质量专员", "name": "资源51 - 质量专员",
"type": "质量专员", "type": "质量专员",
"avatar": "https://i.pravatar.cc/150?img=51", "avatar": "/50-61.jpg",
"department": "注册部", "department": "注册部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -20320,7 +20320,7 @@
"id": "resource-052", "id": "resource-052",
"name": "资源52 - 注册专员", "name": "资源52 - 注册专员",
"type": "注册专员", "type": "注册专员",
"avatar": "https://i.pravatar.cc/150?img=52", "avatar": "/50-62.jpg",
"department": "生产部", "department": "生产部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -20651,7 +20651,7 @@
"id": "resource-053", "id": "resource-053",
"name": "资源53 - 项目经理", "name": "资源53 - 项目经理",
"type": "项目经理", "type": "项目经理",
"avatar": "https://i.pravatar.cc/150?img=53", "avatar": "/50-63.jpg",
"department": "市场部", "department": "市场部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -21027,7 +21027,7 @@
"id": "resource-054", "id": "resource-054",
"name": "资源54 - 工程师", "name": "资源54 - 工程师",
"type": "工程师", "type": "工程师",
"avatar": "https://i.pravatar.cc/150?img=54", "avatar": "/50-64.jpg",
"department": "工程部", "department": "工程部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -21343,7 +21343,7 @@
"id": "resource-055", "id": "resource-055",
"name": "资源55 - 测试工程师", "name": "资源55 - 测试工程师",
"type": "测试工程师", "type": "测试工程师",
"avatar": "https://i.pravatar.cc/150?img=55", "avatar": "/50-65.jpg",
"department": "测试部", "department": "测试部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -21779,7 +21779,7 @@
"id": "resource-056", "id": "resource-056",
"name": "资源56 - 研究员", "name": "资源56 - 研究员",
"type": "研究员", "type": "研究员",
"avatar": "https://i.pravatar.cc/150?img=56", "avatar": "/50-66.jpg",
"department": "研发部", "department": "研发部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -22245,7 +22245,7 @@
"id": "resource-057", "id": "resource-057",
"name": "资源57 - 试验主任", "name": "资源57 - 试验主任",
"type": "试验主任", "type": "试验主任",
"avatar": "https://i.pravatar.cc/150?img=57", "avatar": "/50-67.jpg",
"department": "临床部", "department": "临床部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -22561,7 +22561,7 @@
"id": "resource-058", "id": "resource-058",
"name": "资源58 - 数据分析师", "name": "资源58 - 数据分析师",
"type": "数据分析师", "type": "数据分析师",
"avatar": "https://i.pravatar.cc/150?img=58", "avatar": "/50-68.jpg",
"department": "质量部", "department": "质量部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -22937,7 +22937,7 @@
"id": "resource-059", "id": "resource-059",
"name": "资源59 - 质量专员", "name": "资源59 - 质量专员",
"type": "质量专员", "type": "质量专员",
"avatar": "https://i.pravatar.cc/150?img=59", "avatar": "/50-69.jpg",
"department": "注册部", "department": "注册部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -23373,7 +23373,7 @@
"id": "resource-060", "id": "resource-060",
"name": "资源60 - 注册专员", "name": "资源60 - 注册专员",
"type": "注册专员", "type": "注册专员",
"avatar": "https://i.pravatar.cc/150?img=60", "avatar": "/50-70.jpg",
"department": "生产部", "department": "生产部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -23794,7 +23794,7 @@
"id": "resource-061", "id": "resource-061",
"name": "资源61 - 项目经理", "name": "资源61 - 项目经理",
"type": "项目经理", "type": "项目经理",
"avatar": "https://i.pravatar.cc/150?img=61", "avatar": "/50-71.jpg",
"department": "市场部", "department": "市场部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -24110,7 +24110,7 @@
"id": "resource-062", "id": "resource-062",
"name": "资源62 - 工程师", "name": "资源62 - 工程师",
"type": "工程师", "type": "工程师",
"avatar": "https://i.pravatar.cc/150?img=62", "avatar": "/50-72.jpg",
"department": "工程部", "department": "工程部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -24531,7 +24531,7 @@
"id": "resource-063", "id": "resource-063",
"name": "资源63 - 测试工程师", "name": "资源63 - 测试工程师",
"type": "测试工程师", "type": "测试工程师",
"avatar": "https://i.pravatar.cc/150?img=63", "avatar": "/50-73.jpg",
"department": "测试部", "department": "测试部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -24982,7 +24982,7 @@
"id": "resource-064", "id": "resource-064",
"name": "资源64 - 研究员", "name": "资源64 - 研究员",
"type": "研究员", "type": "研究员",
"avatar": "https://i.pravatar.cc/150?img=64", "avatar": "/50-74.jpg",
"department": "研发部", "department": "研发部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -25313,7 +25313,7 @@
"id": "resource-065", "id": "resource-065",
"name": "资源65 - 试验主任", "name": "资源65 - 试验主任",
"type": "试验主任", "type": "试验主任",
"avatar": "https://i.pravatar.cc/150?img=65", "avatar": "/50-75.jpg",
"department": "临床部", "department": "临床部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -25719,7 +25719,7 @@
"id": "resource-066", "id": "resource-066",
"name": "资源66 - 数据分析师", "name": "资源66 - 数据分析师",
"type": "数据分析师", "type": "数据分析师",
"avatar": "https://i.pravatar.cc/150?img=66", "avatar": "/50-76.jpg",
"department": "质量部", "department": "质量部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -26125,7 +26125,7 @@
"id": "resource-067", "id": "resource-067",
"name": "资源67 - 质量专员", "name": "资源67 - 质量专员",
"type": "质量专员", "type": "质量专员",
"avatar": "https://i.pravatar.cc/150?img=67", "avatar": "/50-77.jpg",
"department": "注册部", "department": "注册部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -26531,7 +26531,7 @@
"id": "resource-068", "id": "resource-068",
"name": "资源68 - 注册专员", "name": "资源68 - 注册专员",
"type": "注册专员", "type": "注册专员",
"avatar": "https://i.pravatar.cc/150?img=68", "avatar": "/50-78.jpg",
"department": "生产部", "department": "生产部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -26937,7 +26937,7 @@
"id": "resource-069", "id": "resource-069",
"name": "资源69 - 项目经理", "name": "资源69 - 项目经理",
"type": "项目经理", "type": "项目经理",
"avatar": "https://i.pravatar.cc/150?img=69", "avatar": "/50-79.jpg",
"department": "市场部", "department": "市场部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -27403,7 +27403,7 @@
"id": "resource-070", "id": "resource-070",
"name": "资源70 - 工程师", "name": "资源70 - 工程师",
"type": "工程师", "type": "工程师",
"avatar": "https://i.pravatar.cc/150?img=70", "avatar": "/50-80.jpg",
"department": "工程部", "department": "工程部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -27794,7 +27794,7 @@
"id": "resource-071", "id": "resource-071",
"name": "资源71 - 测试工程师", "name": "资源71 - 测试工程师",
"type": "测试工程师", "type": "测试工程师",
"avatar": "https://i.pravatar.cc/150?img=71", "avatar": "/50-81.jpg",
"department": "测试部", "department": "测试部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -28140,7 +28140,7 @@
"id": "resource-072", "id": "resource-072",
"name": "资源72 - 研究员", "name": "资源72 - 研究员",
"type": "研究员", "type": "研究员",
"avatar": "https://i.pravatar.cc/150?img=72", "avatar": "/50-82.jpg",
"department": "研发部", "department": "研发部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -28561,7 +28561,7 @@
"id": "resource-073", "id": "resource-073",
"name": "资源73 - 试验主任", "name": "资源73 - 试验主任",
"type": "试验主任", "type": "试验主任",
"avatar": "https://i.pravatar.cc/150?img=73", "avatar": "/50-83.jpg",
"department": "临床部", "department": "临床部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -28982,7 +28982,7 @@
"id": "resource-074", "id": "resource-074",
"name": "资源74 - 数据分析师", "name": "资源74 - 数据分析师",
"type": "数据分析师", "type": "数据分析师",
"avatar": "https://i.pravatar.cc/150?img=74", "avatar": "/50-84.jpg",
"department": "质量部", "department": "质量部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -29298,7 +29298,7 @@
"id": "resource-075", "id": "resource-075",
"name": "资源75 - 质量专员", "name": "资源75 - 质量专员",
"type": "质量专员", "type": "质量专员",
"avatar": "https://i.pravatar.cc/150?img=75", "avatar": "/50-85.jpg",
"department": "注册部", "department": "注册部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -29689,7 +29689,7 @@
"id": "resource-076", "id": "resource-076",
"name": "资源76 - 注册专员", "name": "资源76 - 注册专员",
"type": "注册专员", "type": "注册专员",
"avatar": "https://i.pravatar.cc/150?img=76", "avatar": "/50-86.jpg",
"department": "生产部", "department": "生产部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -30110,7 +30110,7 @@
"id": "resource-077", "id": "resource-077",
"name": "资源77 - 项目经理", "name": "资源77 - 项目经理",
"type": "项目经理", "type": "项目经理",
"avatar": "https://i.pravatar.cc/150?img=77", "avatar": "/50-87.jpg",
"department": "市场部", "department": "市场部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -30546,7 +30546,7 @@
"id": "resource-078", "id": "resource-078",
"name": "资源78 - 工程师", "name": "资源78 - 工程师",
"type": "工程师", "type": "工程师",
"avatar": "https://i.pravatar.cc/150?img=78", "avatar": "/50-88.jpg",
"department": "工程部", "department": "工程部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -30967,7 +30967,7 @@
"id": "resource-079", "id": "resource-079",
"name": "资源79 - 测试工程师", "name": "资源79 - 测试工程师",
"type": "测试工程师", "type": "测试工程师",
"avatar": "https://i.pravatar.cc/150?img=79", "avatar": "/50-89.jpg",
"department": "测试部", "department": "测试部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -31328,7 +31328,7 @@
"id": "resource-080", "id": "resource-080",
"name": "资源80 - 研究员", "name": "资源80 - 研究员",
"type": "研究员", "type": "研究员",
"avatar": "https://i.pravatar.cc/150?img=80", "avatar": "/50-90.jpg",
"department": "研发部", "department": "研发部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -31704,7 +31704,7 @@
"id": "resource-081", "id": "resource-081",
"name": "资源81 - 试验主任", "name": "资源81 - 试验主任",
"type": "试验主任", "type": "试验主任",
"avatar": "https://i.pravatar.cc/150?img=81", "avatar": "/50-91.jpg",
"department": "临床部", "department": "临床部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -32020,7 +32020,7 @@
"id": "resource-082", "id": "resource-082",
"name": "资源82 - 数据分析师", "name": "资源82 - 数据分析师",
"type": "数据分析师", "type": "数据分析师",
"avatar": "https://i.pravatar.cc/150?img=82", "avatar": "/50-92.jpg",
"department": "质量部", "department": "质量部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -32366,7 +32366,7 @@
"id": "resource-083", "id": "resource-083",
"name": "资源83 - 质量专员", "name": "资源83 - 质量专员",
"type": "质量专员", "type": "质量专员",
"avatar": "https://i.pravatar.cc/150?img=83", "avatar": "/50-93.jpg",
"department": "注册部", "department": "注册部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -32712,7 +32712,7 @@
"id": "resource-084", "id": "resource-084",
"name": "资源84 - 注册专员", "name": "资源84 - 注册专员",
"type": "注册专员", "type": "注册专员",
"avatar": "https://i.pravatar.cc/150?img=84", "avatar": "/50-94.jpg",
"department": "生产部", "department": "生产部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -33058,7 +33058,7 @@
"id": "resource-085", "id": "resource-085",
"name": "资源85 - 项目经理", "name": "资源85 - 项目经理",
"type": "项目经理", "type": "项目经理",
"avatar": "https://i.pravatar.cc/150?img=85", "avatar": "/50-95.jpg",
"department": "市场部", "department": "市场部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -33374,7 +33374,7 @@
"id": "resource-086", "id": "resource-086",
"name": "资源86 - 工程师", "name": "资源86 - 工程师",
"type": "工程师", "type": "工程师",
"avatar": "https://i.pravatar.cc/150?img=86", "avatar": "/50-96.jpg",
"department": "工程部", "department": "工程部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -33825,7 +33825,7 @@
"id": "resource-087", "id": "resource-087",
"name": "资源87 - 测试工程师", "name": "资源87 - 测试工程师",
"type": "测试工程师", "type": "测试工程师",
"avatar": "https://i.pravatar.cc/150?img=87", "avatar": "/50-97.jpg",
"department": "测试部", "department": "测试部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -34186,7 +34186,7 @@
"id": "resource-088", "id": "resource-088",
"name": "资源88 - 研究员", "name": "资源88 - 研究员",
"type": "研究员", "type": "研究员",
"avatar": "https://i.pravatar.cc/150?img=88", "avatar": "/50-98.jpg",
"department": "研发部", "department": "研发部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -34517,7 +34517,7 @@
"id": "resource-089", "id": "resource-089",
"name": "资源89 - 试验主任", "name": "资源89 - 试验主任",
"type": "试验主任", "type": "试验主任",
"avatar": "https://i.pravatar.cc/150?img=89", "avatar": "/50-99.jpg",
"department": "临床部", "department": "临床部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -34953,7 +34953,7 @@
"id": "resource-090", "id": "resource-090",
"name": "资源90 - 数据分析师", "name": "资源90 - 数据分析师",
"type": "数据分析师", "type": "数据分析师",
"avatar": "https://i.pravatar.cc/150?img=90", "avatar": "/50-100.jpg",
"department": "质量部", "department": "质量部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -35404,7 +35404,7 @@
"id": "resource-091", "id": "resource-091",
"name": "资源91 - 质量专员", "name": "资源91 - 质量专员",
"type": "质量专员", "type": "质量专员",
"avatar": "https://i.pravatar.cc/150?img=91", "avatar": "/50-101.jpg",
"department": "注册部", "department": "注册部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -35795,7 +35795,7 @@
"id": "resource-092", "id": "resource-092",
"name": "资源92 - 注册专员", "name": "资源92 - 注册专员",
"type": "注册专员", "type": "注册专员",
"avatar": "https://i.pravatar.cc/150?img=92", "avatar": "/50-102.jpg",
"department": "生产部", "department": "生产部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -36186,7 +36186,7 @@
"id": "resource-093", "id": "resource-093",
"name": "资源93 - 项目经理", "name": "资源93 - 项目经理",
"type": "项目经理", "type": "项目经理",
"avatar": "https://i.pravatar.cc/150?img=93", "avatar": "/50-103.jpg",
"department": "市场部", "department": "市场部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -36517,7 +36517,7 @@
"id": "resource-094", "id": "resource-094",
"name": "资源94 - 工程师", "name": "资源94 - 工程师",
"type": "工程师", "type": "工程师",
"avatar": "https://i.pravatar.cc/150?img=94", "avatar": "/50-104.jpg",
"department": "工程部", "department": "工程部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -36863,7 +36863,7 @@
"id": "resource-095", "id": "resource-095",
"name": "资源95 - 测试工程师", "name": "资源95 - 测试工程师",
"type": "测试工程师", "type": "测试工程师",
"avatar": "https://i.pravatar.cc/150?img=95", "avatar": "/50-105.jpg",
"department": "测试部", "department": "测试部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -37194,7 +37194,7 @@
"id": "resource-096", "id": "resource-096",
"name": "资源96 - 研究员", "name": "资源96 - 研究员",
"type": "研究员", "type": "研究员",
"avatar": "https://i.pravatar.cc/150?img=96", "avatar": "/50-106.jpg",
"department": "研发部", "department": "研发部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -37615,7 +37615,7 @@
"id": "resource-097", "id": "resource-097",
"name": "资源97 - 试验主任", "name": "资源97 - 试验主任",
"type": "试验主任", "type": "试验主任",
"avatar": "https://i.pravatar.cc/150?img=97", "avatar": "/50-107.jpg",
"department": "临床部", "department": "临床部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -38066,7 +38066,7 @@
"id": "resource-098", "id": "resource-098",
"name": "资源98 - 数据分析师", "name": "资源98 - 数据分析师",
"type": "数据分析师", "type": "数据分析师",
"avatar": "https://i.pravatar.cc/150?img=98", "avatar": "/50-108.jpg",
"department": "质量部", "department": "质量部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -38517,7 +38517,7 @@
"id": "resource-099", "id": "resource-099",
"name": "资源99 - 质量专员", "name": "资源99 - 质量专员",
"type": "质量专员", "type": "质量专员",
"avatar": "https://i.pravatar.cc/150?img=99", "avatar": "/50-109.jpg",
"department": "注册部", "department": "注册部",
"skills": [ "skills": [
"技能A", "技能A",
@@ -38863,7 +38863,7 @@
"id": "resource-100", "id": "resource-100",
"name": "资源100 - 注册专员", "name": "资源100 - 注册专员",
"type": "注册专员", "type": "注册专员",
"avatar": "https://i.pravatar.cc/150?img=100", "avatar": "/50-110.jpg",
"department": "生产部", "department": "生产部",
"skills": [ "skills": [
"技能A", "技能A",
+13 -13
View File
@@ -4,7 +4,7 @@
"id": "resource-chen", "id": "resource-chen",
"name": "首席研究员 Dr. Chen", "name": "首席研究员 Dr. Chen",
"type": "研究员", "type": "研究员",
"avatar": "https://i.pravatar.cc/150?img=1", "avatar": "/50-11.jpg",
"department": "临床研究部", "department": "临床研究部",
"skills": ["临床试验管理", "项目规划", "团队领导"], "skills": ["临床试验管理", "项目规划", "团队领导"],
"utilization": 85, "utilization": 85,
@@ -49,7 +49,7 @@
"id": "resource-wang", "id": "resource-wang",
"name": "I期试验主任 Dr. Wang", "name": "I期试验主任 Dr. Wang",
"type": "试验主任", "type": "试验主任",
"avatar": "https://i.pravatar.cc/150?img=2", "avatar": "/50-12.jpg",
"department": "I期临床研究组", "department": "I期临床研究组",
"skills": ["I期试验设计", "药代动力学", "安全性评估"], "skills": ["I期试验设计", "药代动力学", "安全性评估"],
"utilization": 92, "utilization": 92,
@@ -94,7 +94,7 @@
"id": "resource-li", "id": "resource-li",
"name": "方案设计师 李明", "name": "方案设计师 李明",
"type": "方案设计师", "type": "方案设计师",
"avatar": "https://i.pravatar.cc/150?img=3", "avatar": "/50-13.jpg",
"department": "临床研究部", "department": "临床研究部",
"skills": ["方案设计", "伦理审查", "监管申报"], "skills": ["方案设计", "伦理审查", "监管申报"],
"utilization": 75, "utilization": 75,
@@ -128,7 +128,7 @@
"id": "resource-zhang", "id": "resource-zhang",
"name": "数据分析师 张伟", "name": "数据分析师 张伟",
"type": "数据分析师", "type": "数据分析师",
"avatar": "https://i.pravatar.cc/150?img=4", "avatar": "/50-14.jpg",
"department": "数据管理与统计部", "department": "数据管理与统计部",
"skills": ["统计分析", "数据可视化", "报告撰写"], "skills": ["统计分析", "数据可视化", "报告撰写"],
"utilization": 68, "utilization": 68,
@@ -173,7 +173,7 @@
"id": "resource-liu", "id": "resource-liu",
"name": "质量保证专员 刘芳", "name": "质量保证专员 刘芳",
"type": "质量保证", "type": "质量保证",
"avatar": "https://i.pravatar.cc/150?img=5", "avatar": "/50-15.jpg",
"department": "质量保证部", "department": "质量保证部",
"skills": ["GCP审计", "质量控制", "SOP管理"], "skills": ["GCP审计", "质量控制", "SOP管理"],
"utilization": 88, "utilization": 88,
@@ -208,7 +208,7 @@
"id": "resource-zhao", "id": "resource-zhao",
"name": "II期试验协调员 赵敏", "name": "II期试验协调员 赵敏",
"type": "试验协调员", "type": "试验协调员",
"avatar": "https://i.pravatar.cc/150?img=6", "avatar": "/50-16.jpg",
"department": "II期临床研究组", "department": "II期临床研究组",
"skills": ["试验协调", "中心管理", "受试者跟踪"], "skills": ["试验协调", "中心管理", "受试者跟踪"],
"utilization": 55, "utilization": 55,
@@ -235,7 +235,7 @@
"id": "resource-wu", "id": "resource-wu",
"name": "设备管理员 吴强", "name": "设备管理员 吴强",
"type": "设备管理", "type": "设备管理",
"avatar": "https://i.pravatar.cc/150?img=7", "avatar": "/50-17.jpg",
"department": "设备与后勤部", "department": "设备与后勤部",
"skills": ["设备维护", "库存管理", "供应链协调"], "skills": ["设备维护", "库存管理", "供应链协调"],
"utilization": 42, "utilization": 42,
@@ -262,7 +262,7 @@
"id": "resource-huang", "id": "resource-huang",
"name": "III期项目经理 黄建国", "name": "III期项目经理 黄建国",
"type": "项目经理", "type": "项目经理",
"avatar": "https://i.pravatar.cc/150?img=8", "avatar": "/50-18.jpg",
"department": "III期临床研究组", "department": "III期临床研究组",
"skills": ["大型试验管理", "多中心协调", "里程碑管理"], "skills": ["大型试验管理", "多中心协调", "里程碑管理"],
"utilization": 35, "utilization": 35,
@@ -282,7 +282,7 @@
"id": "resource-sun", "id": "resource-sun",
"name": "生物统计师 孙丽", "name": "生物统计师 孙丽",
"type": "生物统计师", "type": "生物统计师",
"avatar": "https://i.pravatar.cc/150?img=9", "avatar": "/50-19.jpg",
"department": "数据管理与统计部", "department": "数据管理与统计部",
"skills": ["统计建模", "样本量计算", "数据分析"], "skills": ["统计建模", "样本量计算", "数据分析"],
"utilization": 95, "utilization": 95,
@@ -338,7 +338,7 @@
"id": "resource-zhou", "id": "resource-zhou",
"name": "临床监查员 周杰", "name": "临床监查员 周杰",
"type": "临床监查员", "type": "临床监查员",
"avatar": "https://i.pravatar.cc/150?img=10", "avatar": "/50-20.jpg",
"department": "临床监查部", "department": "临床监查部",
"skills": ["现场监查", "数据核查", "问题解决"], "skills": ["现场监查", "数据核查", "问题解决"],
"utilization": 110, "utilization": 110,
@@ -394,7 +394,7 @@
"id": "resource-xu", "id": "resource-xu",
"name": "医学撰写员 徐静", "name": "医学撰写员 徐静",
"type": "医学撰写员", "type": "医学撰写员",
"avatar": "https://i.pravatar.cc/150?img=11", "avatar": "/50-21.jpg",
"department": "医学事务部", "department": "医学事务部",
"skills": ["研究报告撰写", "文献检索", "医学翻译"], "skills": ["研究报告撰写", "文献检索", "医学翻译"],
"utilization": 72, "utilization": 72,
@@ -439,7 +439,7 @@
"id": "resource-ma", "id": "resource-ma",
"name": "药物警戒专员 马云飞", "name": "药物警戒专员 马云飞",
"type": "药物警戒", "type": "药物警戒",
"avatar": "https://i.pravatar.cc/150?img=12", "avatar": "/50-22.jpg",
"department": "药物警戒部", "department": "药物警戒部",
"skills": ["不良事件管理", "安全性报告", "信号检测"], "skills": ["不良事件管理", "安全性报告", "信号检测"],
"utilization": 88, "utilization": 88,
@@ -475,7 +475,7 @@
"id": "resource-gao", "id": "resource-gao",
"name": "注册事务专员 高敏", "name": "注册事务专员 高敏",
"type": "注册事务", "type": "注册事务",
"avatar": "https://i.pravatar.cc/150?img=13", "avatar": "/50-23.jpg",
"department": "注册事务部", "department": "注册事务部",
"skills": ["IND申报", "监管沟通", "文件管理"], "skills": ["IND申报", "监管沟通", "文件管理"],
"utilization": 65, "utilization": 65,
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

+1 -1
View File
@@ -234,7 +234,7 @@ const addMilestone = () => {
type: 'milestone', type: 'milestone',
icon: 'diamond', icon: 'diamond',
}) })
console.log('milestones: ', milestones.value) //console.log('milestones: ', milestones.value)
newTask.value = { name: '', startDate: '', endDate: '' } newTask.value = { name: '', startDate: '', endDate: '' }
showAddMilestoneDialog.value = false showAddMilestoneDialog.value = false
} }
+1 -1
View File
@@ -220,7 +220,7 @@ const addMilestone = () => {
type: 'milestone', type: 'milestone',
icon: 'diamond', icon: 'diamond',
}) })
console.log('milestones: ', milestones.value) //console.log('milestones: ', milestones.value)
newTask.value = { name: '', startDate: '', endDate: '' } newTask.value = { name: '', startDate: '', endDate: '' }
showAddMilestoneDialog.value = false showAddMilestoneDialog.value = false
} }
+35
View File
@@ -0,0 +1,35 @@
// 替换数据集中的远程头像URL为本地路径
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const files = [
'demo/data.json',
'demo/data-resources.json',
'demo/data-resources-large.json',
]
files.forEach(filePath => {
const fullPath = path.join(__dirname, '..', filePath)
console.log(`Processing ${filePath}...`)
try {
let content = fs.readFileSync(fullPath, 'utf-8')
// 替换 https://i.pravatar.cc/150?img=X 为 /50-(X+10).jpg
content = content.replace(/https:\/\/i\.pravatar\.cc\/150\?img=(\d+)/g, (match, num) => {
const newNum = parseInt(num) + 10
return `/50-${newNum}.jpg`
})
fs.writeFileSync(fullPath, content, 'utf-8')
console.log(`${filePath} updated successfully`)
} catch (error) {
console.error(`✗ Error processing ${filePath}:`, error.message)
}
})
console.log('\nAll files processed!')
+144 -74
View File
@@ -13,6 +13,7 @@ interface AssigneeOption {
key?: string | number key?: string | number
value: string | number value: string | number
label: string label: string
avatar?: string // v1.9.0
} }
interface Props { interface Props {
@@ -367,6 +368,9 @@ watch(
// 使 nextTick resetForm // 使 nextTick resetForm
nextTick(() => { nextTick(() => {
Object.assign(formData, taskData) Object.assign(formData, taskData)
// v1.9.0 assignee/avatar resources
mapAssigneeToResources()
}) })
} else if (props.task && !props.isEdit) { } else if (props.task && !props.isEdit) {
// //
@@ -489,6 +493,10 @@ const handleSubmit = async () => {
} }
try { try {
submitting.value = true submitting.value = true
// v1.9.0 resources assignee/avatar
mapResourcesToAssignee()
const taskData: Task = { const taskData: Task = {
...formData, ...formData,
id: props.isEdit && props.task ? props.task.id : Date.now(), id: props.isEdit && props.task ? props.task.id : Date.now(),
@@ -667,18 +675,6 @@ function confirmTimer(desc: string) {
handleStartTimer(desc) handleStartTimer(desc)
} }
//
const handleAssigneeChanged = (event: Event) => {
const value = (event.target as HTMLSelectElement).value
// valueprops.assigneeOptionslabel
const selected = props.assigneeOptions?.find(option => option.value === value)
if (selected) {
//
// formData
formData.assigneeName = selected.label
}
}
// v1.9.0 // v1.9.0
const addResource = () => { const addResource = () => {
if (!formData.resources) { if (!formData.resources) {
@@ -715,6 +711,54 @@ const handleResourceChange = (index: number, field: 'id' | 'percent', value: str
} }
} }
// v1.9.0 task.assignee/avatar resources
const mapAssigneeToResources = () => {
if (!formData.resources) {
formData.resources = []
}
// resources使resources
if (formData.resources && formData.resources.length > 0) {
return
}
// assignee/avatar resources
const assignees = Array.isArray(formData.assignee) ? formData.assignee : (formData.assignee ? [formData.assignee] : [])
formData.resources = assignees.map((assigneeId) => {
// assigneeOption
const option = props.assigneeOptions?.find(opt => opt.value === assigneeId)
return {
id: assigneeId,
name: option?.label || String(assigneeId),
percent: 100, // 100%
}
})
}
// v1.9.0 resources task.assignee/avatar
const mapResourcesToAssignee = () => {
if (!formData.resources || formData.resources.length === 0) {
formData.assignee = ''
formData.avatar = ''
return
}
// assignee
const assignees = formData.resources.map(r => String(r.id)).filter(id => id)
formData.assignee = assignees.length > 1 ? assignees : (assignees[0] || '')
// avatar assigneeOptionsavatar
const avatars = formData.resources
.map(r => {
const option = props.assigneeOptions?.find(opt => opt.value === r.id)
return option?.avatar || ''
})
.filter(avatar => avatar)
formData.avatar = avatars.length > 1 ? avatars : (avatars[0] || '')
}
// //
const taskStatus = computed(() => { const taskStatus = computed(() => {
if (!props.task) return { text: '', color: '', bgColor: '', borderColor: '' } if (!props.task) return { text: '', color: '', bgColor: '', borderColor: '' }
@@ -949,24 +993,17 @@ const taskStatus = computed(() => {
<span v-if="errors.type" class="error-text">{{ errors.type }}</span> <span v-if="errors.type" class="error-text">{{ errors.type }}</span>
</div> </div>
<div class="form-group">
<label class="form-label" for="task-assignee">{{ t.assignee }}</label>
<select id="task-assignee" v-model="formData.assignee" class="form-select" @change="handleAssigneeChanged">
<option value="">{{ t.selectAssignee }}</option>
<option
v-for="assignee in props.assigneeOptions"
:key="assignee.key ?? assignee.value"
:value="assignee.value"
>
{{ assignee.label }}
</option>
</select>
</div>
<!-- v1.9.0 资源分配含占比配置 --> <!-- v1.9.0 资源分配含占比配置 -->
<div class="form-group"> <div class="form-group">
<label class="form-label">{{ t.resourceAllocation || '资源分配' }}</label> <label class="form-label">{{ t.resourceAllocation || '资源分配' }}</label>
<div class="resource-list"> <div class="resource-list">
<!-- 资源分配标题行 -->
<div class="resource-header">
<span class="resource-header-label">资源名称</span>
<span class="resource-header-label">占用比例</span>
<span class="resource-header-action"></span>
</div>
<div <div
v-for="(resource, index) in formData.resources" v-for="(resource, index) in formData.resources"
:key="index" :key="index"
@@ -982,32 +1019,22 @@ const taskStatus = computed(() => {
v-for="assignee in props.assigneeOptions" v-for="assignee in props.assigneeOptions"
:key="assignee.key ?? assignee.value" :key="assignee.key ?? assignee.value"
:value="assignee.value" :value="assignee.value"
:data-avatar="assignee.avatar"
> >
{{ assignee.label }} {{ assignee.label }}
</option> </option>
</select> </select>
<div class="percent-input-wrapper"> <select
<select v-model.number="resource.percent"
v-model.number="resource.percent" class="form-select percent-select"
class="form-select percent-select" @change="handleResourceChange(index, 'percent', ($event.target as HTMLSelectElement).value)"
@change="handleResourceChange(index, 'percent', ($event.target as HTMLSelectElement).value)" >
> <option :value="25">25%</option>
<option :value="25">25%</option> <option :value="50">50%</option>
<option :value="50">50%</option> <option :value="75">75%</option>
<option :value="75">75%</option> <option :value="100">100%</option>
<option :value="100">100%</option> </select>
</select>
<input
v-model.number="resource.percent"
type="number"
class="form-input percent-input"
min="20"
max="100"
@input="handleResourceChange(index, 'percent', ($event.target as HTMLInputElement).value)"
/>
<span class="percent-unit">%</span>
</div>
<button <button
type="button" type="button"
@@ -1015,7 +1042,12 @@ const taskStatus = computed(() => {
title="删除资源" title="删除资源"
@click="removeResource(index)" @click="removeResource(index)"
> >
× <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
<line x1="10" y1="11" x2="10" y2="17"></line>
<line x1="14" y1="11" x2="14" y2="17"></line>
</svg>
</button> </button>
</div> </div>
<button <button
@@ -1375,7 +1407,7 @@ const taskStatus = computed(() => {
.form-input, .form-input,
.form-select, .form-select,
.form-textarea { .form-textarea {
padding: 12px 16px; padding: 8px;
border: 1px solid var(--gantt-border-medium, #dcdfe6); border: 1px solid var(--gantt-border-medium, #dcdfe6);
border-radius: 4px; border-radius: 4px;
font-size: 14px; font-size: 14px;
@@ -1681,54 +1713,81 @@ const taskStatus = computed(() => {
gap: 8px; gap: 8px;
} }
/* 资源分配标题行 */
.resource-header {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: var(--gantt-bg-toolbar, #fafafa);
border-radius: 4px;
border: 1px solid var(--gantt-border-light, #ebeef5);
}
.resource-header-label {
font-size: 12px;
font-weight: 600;
color: var(--gantt-text-secondary, #606266);
white-space: nowrap;
overflow: visible;
}
.resource-header-label:first-child {
flex: 1;
min-width: 0;
text-align: center;
}
.resource-header-label:nth-child(2) {
width: 100px;
text-align: center;
}
.resource-header-action {
width: 48px;
flex-shrink: 0;
}
.resource-item { .resource-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding: 8px; padding: 8px 12px;
background: var(--gantt-bg-secondary, #f5f7fa); background: var(--gantt-bg-secondary, #f5f7fa);
border-radius: 4px; border-radius: 4px;
min-height: 48px;
} }
.resource-select { .resource-select {
flex: 1; flex: 1;
min-width: 140px; min-width: 180px;
max-width: 280px;
height: 36px;
} }
.percent-input-wrapper { .resource-select option {
display: flex; padding: 8px 12px;
align-items: center;
gap: 4px;
} }
.percent-select { .percent-select {
width: 80px; width: 110px;
} height: 36px;
flex-shrink: 0;
.percent-input {
width: 60px;
padding: 6px 8px;
}
.percent-unit {
font-size: 14px;
color: var(--gantt-text-secondary, #606266);
} }
.btn-remove-resource { .btn-remove-resource {
width: 28px; width: 32px;
height: 28px; height: 32px;
display: flex; display: grid;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: transparent; background: transparent;
border: 1px solid var(--gantt-border-base, #dcdfe6); border: none;
border-radius: 4px; border-radius: 4px;
color: var(--gantt-text-secondary, #606266); color: var(--gantt-danger, #f56c6c);
cursor: pointer; cursor: pointer;
font-size: 20px;
line-height: 1;
transition: all 0.2s; transition: all 0.2s;
flex-shrink: 0;
} }
.btn-remove-resource:hover { .btn-remove-resource:hover {
@@ -1739,7 +1798,7 @@ const taskStatus = computed(() => {
.btn-add-resource { .btn-add-resource {
width: 100%; width: 100%;
padding: 8px; padding: 10px;
background: transparent; background: transparent;
border: 1px dashed var(--gantt-border-base, #dcdfe6); border: 1px dashed var(--gantt-border-base, #dcdfe6);
border-radius: 4px; border-radius: 4px;
@@ -1747,6 +1806,7 @@ const taskStatus = computed(() => {
cursor: pointer; cursor: pointer;
font-size: 14px; font-size: 14px;
transition: all 0.2s; transition: all 0.2s;
min-height: 40px;
} }
.btn-add-resource:hover { .btn-add-resource:hover {
@@ -1754,6 +1814,16 @@ const taskStatus = computed(() => {
background: var(--gantt-primary-light, #ecf5ff); background: var(--gantt-primary-light, #ecf5ff);
} }
/* 暗黑模式 */
:global(html[data-theme='dark']) .resource-header {
background: var(--gantt-bg-toolbar, rgba(255, 255, 255, 0.03)) !important;
border-color: var(--gantt-border-light, rgba(255, 255, 255, 0.1)) !important;
}
:global(html[data-theme='dark']) .resource-header-label {
color: var(--gantt-text-secondary, #a8a8a8) !important;
}
:global(html[data-theme='dark']) .resource-item { :global(html[data-theme='dark']) .resource-item {
background: var(--gantt-bg-secondary, rgba(255, 255, 255, 0.05)) !important; background: var(--gantt-bg-secondary, rgba(255, 255, 255, 0.05)) !important;
} }
+6 -57
View File
@@ -127,30 +127,20 @@ const getConflictTasksForTask = (resourceId: string | number, taskId: string | n
const currentStart = new Date(currentTask.startDate).getTime() const currentStart = new Date(currentTask.startDate).getTime()
const currentEnd = new Date(currentTask.endDate).getTime() const currentEnd = new Date(currentTask.endDate).getTime()
// // v1.9.7
const getCurrentPercent = (task: Task): number => { // 100%resourceConflictsID
if (!task.resources || !Array.isArray(task.resources)) return 100 // 375%
const allocation = task.resources.find((r: any) => String(r.id) === String(resourceId))
return allocation?.percent ?? 100
}
const currentPercent = getCurrentPercent(currentTask)
//
const conflictTasks = resource.tasks.filter(task => { const conflictTasks = resource.tasks.filter(task => {
if (task.id === taskId) return false if (task.id === taskId) return false
if (!task.startDate || !task.endDate) return false if (!task.startDate || !task.endDate) return false
//
if (!conflictTaskIds.has(task.id)) return false if (!conflictTaskIds.has(task.id)) return false
const taskStart = new Date(task.startDate).getTime() const taskStart = new Date(task.startDate).getTime()
const taskEnd = new Date(task.endDate).getTime() const taskEnd = new Date(task.endDate).getTime()
// //
if (!(currentStart < taskEnd && taskStart < currentEnd)) return false return currentStart < taskEnd && taskStart < currentEnd
// 100%
const taskPercent = getCurrentPercent(task)
return currentPercent + taskPercent > 100
}) })
return conflictTasks return conflictTasks
@@ -186,7 +176,6 @@ let resourceTaskLayoutsCallCount = 0
const resourceTaskLayouts = computed(() => { const resourceTaskLayouts = computed(() => {
resourceTaskLayoutsCallCount++ resourceTaskLayoutsCallCount++
const startTime = performance.now() const startTime = performance.now()
console.log(`[🔍 Performance] resourceTaskLayouts computed #${resourceTaskLayoutsCallCount} triggered`)
const layoutMap = new Map<string | number, { const layoutMap = new Map<string | number, {
taskRowMap: Map<string | number, number>, taskRowMap: Map<string | number, number>,
@@ -195,7 +184,6 @@ const resourceTaskLayouts = computed(() => {
}>() }>()
if (viewMode.value !== 'resource') { if (viewMode.value !== 'resource') {
console.log(`[🔍 Performance] resourceTaskLayouts: skipped (not resource view)`)
return layoutMap return layoutMap
} }
@@ -233,7 +221,6 @@ const resourceTaskLayouts = computed(() => {
// //
if (resources.length > 0) { if (resources.length > 0) {
const hitRate = ((cacheHits / resources.length) * 100).toFixed(1) const hitRate = ((cacheHits / resources.length) * 100).toFixed(1)
console.log(`[Performance] resourceTaskLayouts: ${duration}ms | computed ${resources.length} resources, cache hit rate: ${hitRate}%`)
} }
return layoutMap return layoutMap
@@ -244,7 +231,6 @@ watch(dataSource, () => {
if (layoutCache.size > 100) { if (layoutCache.size > 100) {
const keysToDelete = Array.from(layoutCache.keys()).slice(0, layoutCache.size - 100) const keysToDelete = Array.from(layoutCache.keys()).slice(0, layoutCache.size - 100)
keysToDelete.forEach(key => layoutCache.delete(key)) keysToDelete.forEach(key => layoutCache.delete(key))
console.log(`[Performance] Layout cache cleaned: removed ${keysToDelete.length} entries`)
} }
}) })
@@ -285,11 +271,9 @@ let resourceRowPositionsCallCount = 0
const resourceRowPositions = computed(() => { const resourceRowPositions = computed(() => {
resourceRowPositionsCallCount++ resourceRowPositionsCallCount++
const startTime = performance.now() const startTime = performance.now()
console.log(`[🔍 Performance] resourceRowPositions computed #${resourceRowPositionsCallCount} triggered`)
const positions = new Map<string | number, number>() const positions = new Map<string | number, number>()
if (viewMode.value !== 'resource') { if (viewMode.value !== 'resource') {
console.log(`[🔍 Performance] resourceRowPositions: skipped (not resource view)`)
return positions return positions
} }
@@ -328,12 +312,6 @@ const resourceRowPositions = computed(() => {
const endTime = performance.now() const endTime = performance.now()
const duration = (endTime - startTime).toFixed(2) const duration = (endTime - startTime).toFixed(2)
if (processedCount < resources.length) {
console.log(`[Phase2] resourceRowPositions: ${duration}ms | lazy-computed ${processedCount}/${resources.length} resources`)
} else {
console.log(`[Performance] resourceRowPositions: ${duration}ms | processed ${resources.length} resources`)
}
return positions return positions
}) })
@@ -1595,9 +1573,6 @@ watch([timelineScrollLeft, timelineContainerWidth], ([newScrollLeft, newWidth])
// v1.9.5 P2-1 - TaskBar // v1.9.5 P2-1 - TaskBar
const visibleTimeRange = computed(() => { const visibleTimeRange = computed(() => {
visibleTimeRangeCallCount++ visibleTimeRangeCallCount++
if (visibleTimeRangeCallCount % 10 === 0) {
console.log(`[Performance] visibleTimeRange called ${visibleTimeRangeCallCount} times`)
}
const scrollLeft = debouncedScrollLeft.value const scrollLeft = debouncedScrollLeft.value
const containerWidth = debouncedContainerWidth.value || timelineContainerWidth.value const containerWidth = debouncedContainerWidth.value || timelineContainerWidth.value
@@ -1623,8 +1598,6 @@ const visibleTimeRange = computed(() => {
if (visibleTimeRangeCallCount % 10 === 0) { if (visibleTimeRangeCallCount % 10 === 0) {
const scale = currentTimeScale.value const scale = currentTimeScale.value
const daysDiff = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)) const daysDiff = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24))
console.log(`[Sprint2-Debug] visibleTimeRange: ${scale}, scrollLeft=${scrollLeft.toFixed(0)}, containerWidth=${containerWidth.toFixed(0)}, buffer=${bufferWidth.toFixed(0)}`)
console.log(`[Sprint2-Debug] Date range: ${startDate.toISOString().split('T')[0]} to ${endDate.toISOString().split('T')[0]} (${daysDiff} days)`)
} }
return { startDate, endDate } return { startDate, endDate }
@@ -1749,7 +1722,6 @@ const visibleTaskRange = computed(() => {
const startTime = performance.now() const startTime = performance.now()
const scrollTop = timelineBodyScrollTop.value const scrollTop = timelineBodyScrollTop.value
const containerHeight = timelineBodyHeight.value || 600 const containerHeight = timelineBodyHeight.value || 600
console.log(`[🔍 Performance] visibleTaskRange computed #${visibleTaskRangeCallCount} triggered | scrollTop: ${scrollTop}`)
if (viewMode.value === 'resource') { if (viewMode.value === 'resource') {
// //
@@ -1787,7 +1759,6 @@ const visibleTaskRange = computed(() => {
const endTime = performance.now() const endTime = performance.now()
const duration = (endTime - startTime).toFixed(2) const duration = (endTime - startTime).toFixed(2)
console.log(`[Performance] visibleTaskRange: ${duration}ms | range: ${startIndex}-${endIndex} / ${resources.length}`)
return { return {
startIndex: Math.max(0, startIndex), startIndex: Math.max(0, startIndex),
@@ -1977,7 +1948,6 @@ const rebuildResourceTaskQueues = () => {
// v1.9.6 Sprint4 - // v1.9.6 Sprint4 -
const totalResources = (dataSource.value as Resource[]).length const totalResources = (dataSource.value as Resource[]).length
const visibleResourcesCount = visibleResources.value.length const visibleResourcesCount = visibleResources.value.length
console.log(`[Sprint4] rebuildResourceTaskQueues: processing ${visibleResourcesCount}/${totalResources} visible resources`)
// v1.9.6 Sprint2(P5) - // v1.9.6 Sprint2(P5) -
const currentCache = new Map(taskBarRenderCache.value) const currentCache = new Map(taskBarRenderCache.value)
@@ -2055,13 +2025,9 @@ const rebuildResourceTaskQueues = () => {
resourceRenderPhase.value = 'visible' resourceRenderPhase.value = 'visible'
taskBarRenderCache.value = newCache // taskBarRenderCache.value = newCache //
// v1.9.6 Sprint4 -
console.log(`[Sprint4] Will render ${totalVisibleTaskBars} TaskBars (from ${visibleResourcesCount} visible resources)`)
// v1.9.6 Sprint2(P5) - // v1.9.6 Sprint2(P5) -
const totalTaskBars = newCache.size const totalTaskBars = newCache.size
const cacheHitRate = totalTaskBars > 0 ? ((cachedCount / totalTaskBars) * 100).toFixed(1) : '0.0' const cacheHitRate = totalTaskBars > 0 ? ((cachedCount / totalTaskBars) * 100).toFixed(1) : '0.0'
console.log(`[Sprint2-P5] TaskBar cache: ${cachedCount}/${totalTaskBars} already rendered (${cacheHitRate}% hit rate)`)
scheduleResourceBatchRender() scheduleResourceBatchRender()
} }
@@ -2102,7 +2068,6 @@ watch(
if (updatedCount > 0) { if (updatedCount > 0) {
taskBarRenderCache.value = cache taskBarRenderCache.value = cache
console.log(`[Sprint2-P5] Marked ${updatedCount} TaskBars as rendered (cache size: ${cache.size})`)
} }
}, },
{ deep: false }, { deep: false },
@@ -2123,7 +2088,6 @@ const visibleResourcesWithFilteredTasks = computed(() => {
// v1.9.6 Sprint2(P1) - // v1.9.6 Sprint2(P1) -
if (filteredTasksCallCount <= 5 || filteredTasksCallCount % 10 === 0) { if (filteredTasksCallCount <= 5 || filteredTasksCallCount % 10 === 0) {
const daysDiff = Math.ceil((visibleEndDate.getTime() - visibleStartDate.getTime()) / (1000 * 60 * 60 * 24)) const daysDiff = Math.ceil((visibleEndDate.getTime() - visibleStartDate.getTime()) / (1000 * 60 * 60 * 24))
console.log(`[Sprint2-Debug] #${filteredTasksCallCount} visibleTimeRange: ${visibleStartDate.toISOString().split('T')[0]} to ${visibleEndDate.toISOString().split('T')[0]} (${daysDiff} days)`)
} }
// //
@@ -2175,7 +2139,6 @@ const visibleResourcesWithFilteredTasks = computed(() => {
// v1.9.6 Sprint2(P1) - 10 // v1.9.6 Sprint2(P1) - 10
if (filteredTasksCallCount % 5 === 0 && totalOriginalTasks > 0) { if (filteredTasksCallCount % 5 === 0 && totalOriginalTasks > 0) {
const filterRate = ((1 - totalFilteredTasks / totalOriginalTasks) * 100).toFixed(1) const filterRate = ((1 - totalFilteredTasks / totalOriginalTasks) * 100).toFixed(1)
console.log(`[Sprint2-P1] visibleResourcesWithFilteredTasks #${filteredTasksCallCount}: ${totalFilteredTasks}/${totalOriginalTasks} taskbars, filtered: ${filterRate}%`)
} }
return result return result
@@ -2572,7 +2535,6 @@ const contentHeight = computed(() => {
const endTime = performance.now() const endTime = performance.now()
const duration = (endTime - startTime).toFixed(2) const duration = (endTime - startTime).toFixed(2)
console.log(`[Performance] contentHeight: ${duration}ms | processed ${resources.length} resources`)
return Math.max(totalHeight, minHeight, timelineBodyHeight.value) return Math.max(totalHeight, minHeight, timelineBodyHeight.value)
} }
@@ -2640,7 +2602,6 @@ const generateTimelineData = (): any => {
// 使 // 使
const result = getCachedTimelineData() const result = getCachedTimelineData()
const duration = (performance.now() - startTime).toFixed(2) const duration = (performance.now() - startTime).toFixed(2)
console.log(`[🔍 Performance] generateTimelineData: ${duration}ms | scale: ${currentTimeScale.value}`)
return result return result
} }
@@ -2872,7 +2833,6 @@ const getGlobalWeekPosition = (monthIndex: number, weekIndex: number) => {
// - // -
const updateTimeScale = (scale: TimelineScale) => { const updateTimeScale = (scale: TimelineScale) => {
perfMonitor2.start(`updateTimeScale-${scale}`) perfMonitor2.start(`updateTimeScale-${scale}`)
console.log(`[🔧 Action] updateTimeScale: ${currentTimeScale.value}${scale}`)
currentTimeScale.value = scale currentTimeScale.value = scale
@@ -2958,7 +2918,6 @@ const updateTimeScale = (scale: TimelineScale) => {
} }
// 线 // 线
console.log('[📊 Timeline Data] Regenerating timeline data for new scale...')
timelineData.value = generateTimelineData() timelineData.value = generateTimelineData()
// DOM // DOM
@@ -3021,18 +2980,15 @@ watch(
([newData, newScale]) => { ([newData, newScale]) => {
positionCacheWatchCount++ positionCacheWatchCount++
const watchStartTime = performance.now() const watchStartTime = performance.now()
console.log(`[🔍 Performance] positionCache watch triggered #${positionCacheWatchCount} | scale: ${newScale}`)
if (newData && newScale) { if (newData && newScale) {
// //
const cacheStartTime = performance.now() const cacheStartTime = performance.now()
positionCache.buildCache(newData as any[], newScale) positionCache.buildCache(newData as any[], newScale)
const cacheDuration = (performance.now() - cacheStartTime).toFixed(2) const cacheDuration = (performance.now() - cacheStartTime).toFixed(2)
console.log(`[🔍 Performance] positionCache.buildCache: ${cacheDuration}ms`)
} }
const totalDuration = (performance.now() - watchStartTime).toFixed(2) const totalDuration = (performance.now() - watchStartTime).toFixed(2)
console.log(`[🔍 Performance] positionCache watch completed: ${totalDuration}ms`)
}, },
{ immediate: true } // { immediate: true } //
) )
@@ -3615,7 +3571,6 @@ const scrollToDate = (date: Date | string) => {
// //
const updateTask = (updatedTask: Task) => { const updateTask = (updatedTask: Task) => {
perfMonitor2.start('updateTask') perfMonitor2.start('updateTask')
console.log('[🔧 Task Update] Task updated:', updatedTask.id)
// props // props
// //
@@ -3785,11 +3740,8 @@ const handleTaskBarDragEnd = (updatedTask: Task) => {
// //
if (newRowCount !== oldRowCount) { if (newRowCount !== oldRowCount) {
console.log(`[Auto-Layout] Resource ${targetResourceId} layout changed: ${oldRowCount} -> ${newRowCount} rows after drag`)
// //
taskBarRenderKey.value++ taskBarRenderKey.value++
} else {
console.log(`[Auto-Layout] Resource ${targetResourceId} layout unchanged (${newRowCount} rows), skip render key update`)
} }
} }
} }
@@ -3833,10 +3785,7 @@ const handleTaskBarResizeEnd = (updatedTask: Task) => {
// //
if (newRowCount !== oldRowCount) { if (newRowCount !== oldRowCount) {
console.log(`[Auto-Layout] Resource ${targetResourceId} layout changed: ${oldRowCount} -> ${newRowCount} rows after resize`)
taskBarRenderKey.value++ taskBarRenderKey.value++
} else {
console.log(`[Auto-Layout] Resource ${targetResourceId} layout unchanged (${newRowCount} rows), skip render key update`)
} }
} }
} }
@@ -104,7 +104,6 @@ watch(isDraggingTaskBar, (dragging) => {
nextTick(() => { nextTick(() => {
// 使TaskBarID // 使TaskBarID
if (lastChangedTaskId.value !== null) { if (lastChangedTaskId.value !== null) {
console.log(`[GanttConflicts] Incremental update for task ${lastChangedTaskId.value}`)
recalculateConflictsIncremental(lastChangedTaskId.value) recalculateConflictsIncremental(lastChangedTaskId.value)
lastChangedTaskId.value = null // lastChangedTaskId.value = null //
} else { } else {
@@ -136,12 +135,10 @@ watch(isDraggingTimeline, (dragging) => {
if (dragging) { if (dragging) {
// Canvas // Canvas
clearCanvas() clearCanvas()
console.log('[GanttConflicts] Timeline drag started, canvas cleared')
} else { } else {
// //
nextTick(() => { nextTick(() => {
recalculateConflicts() recalculateConflicts()
console.log('[GanttConflicts] Timeline drag ended, conflicts recalculated')
}) })
} }
}) })
@@ -194,7 +191,6 @@ function recalculateConflictsIncremental(changedTaskId: string | number) {
// //
const changedTask = props.tasks.find(t => t.id === changedTaskId) const changedTask = props.tasks.find(t => t.id === changedTaskId)
if (!changedTask) { if (!changedTask) {
console.log(`[GanttConflicts] Changed task ${changedTaskId} not found, fallback to full recalculation`)
recalculateConflicts() recalculateConflicts()
return return
} }
@@ -210,8 +206,6 @@ function recalculateConflictsIncremental(changedTaskId: string | number) {
return !(taskEnd < changedStart || taskStart > changedEnd) return !(taskEnd < changedStart || taskStart > changedEnd)
}) })
console.log(`[GanttConflicts] Incremental: ${affectedTasks.length} affected tasks (out of ${props.tasks.length} total)`)
// //
const newConflicts = detectConflicts(affectedTasks, props.resourceId) const newConflicts = detectConflicts(affectedTasks, props.resourceId)
@@ -222,8 +216,6 @@ function recalculateConflictsIncremental(changedTaskId: string | number) {
return !zone.tasks.some(task => affectedTaskIds.has(task.id)) return !zone.tasks.some(task => affectedTaskIds.has(task.id))
}) })
console.log(`[GanttConflicts] Incremental: kept ${unchangedConflicts.length} unchanged conflicts, adding ${newConflicts.length} new conflicts`)
// //
const allConflicts = [...unchangedConflicts.map(z => ({ const allConflicts = [...unchangedConflicts.map(z => ({
startDate: z.startDate, startDate: z.startDate,
@@ -301,8 +293,6 @@ function recalculateConflictsIncremental(changedTaskId: string | number) {
const endTime = performance.now() const endTime = performance.now()
const elapsed = endTime - startTime const elapsed = endTime - startTime
console.log(`[GanttConflicts] Incremental update completed in ${elapsed.toFixed(2)}ms, ${conflictZones.value.length} total conflict zones`)
// 使 // 使
renderConflictsIncremental() renderConflictsIncremental()
} }
@@ -321,9 +311,6 @@ function recalculateConflicts() {
return `${t.name}(${percent}%)` return `${t.name}(${percent}%)`
}).join(', ') }).join(', ')
console.log(`[GanttConflicts] Resource ${props.resourceId}: ${props.tasks.length} tasks [${tasksInfo}], detected ${conflicts.length} conflict zones`)
console.log(`[GanttConflicts] Props - scrollLeft: ${props.scrollLeft}, containerWidth: ${props.containerWidth}, width: ${props.width}`)
// v1.9.4 P1 - 使 // v1.9.4 P1 - 使
conflictZones.value = conflicts.map((zone) => { conflictZones.value = conflicts.map((zone) => {
// key // key
@@ -432,8 +419,6 @@ function recalculateConflicts() {
} else { } else {
renderConflictsIncremental() renderConflictsIncremental()
} }
console.log(`[GanttConflicts] Resource ${props.resourceId}: Rendered ${conflictZones.value.length} conflict zones on canvas`)
} }
// CanvasTaskBar使 // CanvasTaskBar使
@@ -784,19 +769,15 @@ function detectChangedZones(
function drawConflictZone(ctx: CanvasRenderingContext2D, zone: ConflictZone) { function drawConflictZone(ctx: CanvasRenderingContext2D, zone: ConflictZone) {
// v1.9.6 使 === undefined left=0 // v1.9.6 使 === undefined left=0
if (zone.left === undefined || zone.width === undefined || zone.width <= 0) { if (zone.left === undefined || zone.width === undefined || zone.width <= 0) {
console.log(`[GanttConflicts] drawConflictZone skipped: left=${zone.left}, width=${zone.width}`)
return return
} }
// //
const canvas = canvasRef.value const canvas = canvasRef.value
if (!canvas || zone.left + zone.width < 0 || zone.left > canvas.width) { if (!canvas || zone.left + zone.width < 0 || zone.left > canvas.width) {
console.log(`[GanttConflicts] drawConflictZone out of viewport: left=${zone.left}, width=${zone.width}, canvas.width=${canvas?.width}`)
return return
} }
console.log(`[GanttConflicts] Drawing zone: left=${zone.left}, width=${zone.width}, top=${zone.top}, height=${zone.height}, level=${zone.level}`)
// //
drawTextureBackground(ctx, zone) drawTextureBackground(ctx, zone)
@@ -811,18 +792,14 @@ function drawConflictZone(ctx: CanvasRenderingContext2D, zone: ConflictZone) {
function renderConflicts() { function renderConflicts() {
const canvas = canvasRef.value const canvas = canvasRef.value
if (!canvas) { if (!canvas) {
console.log('[GanttConflicts] renderConflicts: canvas not found')
return return
} }
const ctx = canvas.getContext('2d') const ctx = canvas.getContext('2d')
if (!ctx) { if (!ctx) {
console.log('[GanttConflicts] renderConflicts: context not found')
return return
} }
console.log(`[GanttConflicts] renderConflicts: canvas=${canvas.width}x${canvas.height}, zones=${conflictZones.value.length}`)
// Canvas // Canvas
ctx.clearRect(0, 0, canvas.width, canvas.height) ctx.clearRect(0, 0, canvas.width, canvas.height)
@@ -838,13 +815,11 @@ function renderConflicts() {
function clearCanvas() { function clearCanvas() {
const canvas = canvasRef.value const canvas = canvasRef.value
if (!canvas) { if (!canvas) {
console.log('[GanttConflicts] clearCanvas: canvas not found')
return return
} }
const ctx = canvas.getContext('2d') const ctx = canvas.getContext('2d')
if (!ctx) { if (!ctx) {
console.log('[GanttConflicts] clearCanvas: context not found')
return return
} }
@@ -853,8 +828,6 @@ function clearCanvas() {
// //
previousConflictZones.value = [] previousConflictZones.value = []
console.log('[GanttConflicts] Canvas cleared, ready for redraw')
} }
// //
@@ -987,7 +960,6 @@ function drawTopWarning(ctx: CanvasRenderingContext2D, zone: ConflictZone) {
onMounted(() => { onMounted(() => {
// v1.9.6 使 // v1.9.6 使
// IntersectionObserver // IntersectionObserver
console.log(`[GanttConflicts] Resource ${props.resourceId}: mounted, will calculate conflicts immediately`)
nextTick(() => { nextTick(() => {
recalculateConflicts() recalculateConflicts()
}) })
+104 -29
View File
@@ -197,6 +197,7 @@ const conflictInfoList = computed(() => {
// //
const currentPercent = props.resourcePercent || 100 const currentPercent = props.resourcePercent || 100
// v1.9.8
return props.conflictTasks.map(conflictTask => { return props.conflictTasks.map(conflictTask => {
if (!conflictTask.startDate || !conflictTask.endDate) return null if (!conflictTask.startDate || !conflictTask.endDate) return null
@@ -214,14 +215,10 @@ const conflictInfoList = computed(() => {
} }
} }
// //
const overlapStart = Math.max(currentStart, conflictStart) const overlapStart = Math.max(currentStart, conflictStart)
const overlapEnd = Math.min(currentEnd, conflictEnd) const overlapEnd = Math.min(currentEnd, conflictEnd)
//
const totalPercent = currentPercent + conflictPercent
const overloadPercent = totalPercent - 100
const formatDate = (timestamp: number) => { const formatDate = (timestamp: number) => {
const date = new Date(timestamp) const date = new Date(timestamp)
return `${date.getMonth() + 1}/${date.getDate()}` return `${date.getMonth() + 1}/${date.getDate()}`
@@ -231,14 +228,76 @@ const conflictInfoList = computed(() => {
taskName: conflictTask.name, taskName: conflictTask.name,
overlapStart: formatDate(overlapStart), overlapStart: formatDate(overlapStart),
overlapEnd: formatDate(overlapEnd), overlapEnd: formatDate(overlapEnd),
currentPercent, conflictPercent, //
conflictPercent,
totalPercent,
overloadPercent,
} }
}).filter(Boolean) }).filter(Boolean)
}) })
// v1.9.8 - 100%
const totalOverloadPercent = computed(() => {
if (!props.hasConflict || !props.conflictTasks || props.conflictTasks.length === 0) {
return 0
}
const currentTask = props.task
if (!currentTask.startDate || !currentTask.endDate) return 0
const currentStart = new Date(currentTask.startDate).getTime()
const currentEnd = new Date(currentTask.endDate).getTime()
const currentPercent = props.resourcePercent || 100
//
let maxTotalPercent = currentPercent
// +
const allTasks = [currentTask, ...props.conflictTasks]
//
allTasks.forEach((task1, i) => {
if (!task1.startDate || !task1.endDate) return
const start1 = new Date(task1.startDate).getTime()
const end1 = new Date(task1.endDate).getTime()
allTasks.forEach((task2, j) => {
if (i >= j || !task2.startDate || !task2.endDate) return
const start2 = new Date(task2.startDate).getTime()
const end2 = new Date(task2.endDate).getTime()
//
if (start1 < end2 && start2 < end1) {
//
const overlapStart = Math.max(start1, start2)
const overlapEnd = Math.min(end1, end2)
let intervalTotal = 0
allTasks.forEach(task => {
if (!task.startDate || !task.endDate) return
const tStart = new Date(task.startDate).getTime()
const tEnd = new Date(task.endDate).getTime()
//
if (tStart < overlapEnd && tEnd > overlapStart) {
let taskPercent = 100
if (task.resources && Array.isArray(task.resources)) {
const allocation = task.resources.find(
(r: any) => String(r.id) === String(props.currentResourceId),
)
if (allocation && allocation.percent !== undefined) {
taskPercent = allocation.percent
}
}
intervalTotal += taskPercent
}
})
maxTotalPercent = Math.max(maxTotalPercent, intervalTotal)
}
})
})
return Math.max(0, maxTotalPercent - 100)
})
// v1.9.4 P1 - // v1.9.4 P1 -
const handleMouseEnter = () => { const handleMouseEnter = () => {
// //
@@ -375,25 +434,26 @@ onUnmounted(() => {
</div> </div>
<!-- 冲突预警有冲突时才显示 --> <!-- 冲突预警有冲突时才显示 -->
<div v-if="hasConflict && conflictInfoList.length > 0" class="conflict-section"> <div v-if="hasConflict && conflictInfoList.length > 0" class="conflict-section">
<!-- 固定标题 -->
<div class="conflict-header"> <div class="conflict-header">
<svg class="warning-icon" viewBox="0 0 24 24" width="14" height="14"> <svg class="warning-icon" viewBox="0 0 24 24" width="14" height="14">
<path fill="currentColor" d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/> <path fill="currentColor" d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>
</svg> </svg>
<span class="conflict-title">资源超载警告</span> <span class="conflict-title">资源超载警告</span>
<span class="total-overload">+{{ totalOverloadPercent }}%</span>
</div> </div>
<div v-for="(info, index) in conflictInfoList" :key="index" class="conflict-item"> <!-- 可滚动的冲突列表 -->
<div class="conflict-task-name">{{ info.taskName }}冲突</div> <div class="conflict-list-container">
<div class="conflict-detail"> <div v-for="(info, index) in conflictInfoList" :key="index" class="conflict-item">
<span class="conflict-label">冲突时段</span> <div class="conflict-task-name">{{ info.taskName }}冲突</div>
<span class="conflict-value">{{ info.overlapStart }} ~ {{ info.overlapEnd }}</span> <div class="conflict-detail">
</div> <span class="conflict-label">冲突时段</span>
<div class="conflict-detail"> <span class="conflict-value">{{ info.overlapStart }} ~ {{ info.overlapEnd }}</span>
<span class="conflict-label">资源占用</span> </div>
<span class="conflict-value">{{ info.currentPercent }}% + {{ info.conflictPercent }}% = {{ info.totalPercent }}%</span> <div class="conflict-detail">
</div> <span class="conflict-label">任务投入占比</span>
<div class="conflict-detail overload-highlight"> <span class="conflict-value">{{ info.conflictPercent }}%</span>
<span class="conflict-label">超载</span> </div>
<span class="conflict-value">+{{ info.overloadPercent }}%</span>
</div> </div>
</div> </div>
</div> </div>
@@ -489,8 +549,14 @@ onUnmounted(() => {
.conflict-section { .conflict-section {
margin-top: 8px; margin-top: 8px;
padding-top: 8px; padding-top: 8px;
border-top: 1px solid rgba(255, 255, 255, 0.2); /* 添加滚动支持 */ border-top: 1px solid rgba(255, 255, 255, 0.2);
max-height: 200px; /* 限制冲突区域最大高度 */ display: flex;
flex-direction: column;
}
/* 可滚动的冲突列表容器 - v1.9.8 */
.conflict-list-container {
max-height: 200px; /* 限制冲突列表最大高度 */
overflow-y: auto; /* 垂直滚动 */ overflow-y: auto; /* 垂直滚动 */
overflow-x: hidden; overflow-x: hidden;
/* 细滚动条样式 */ /* 细滚动条样式 */
@@ -499,21 +565,22 @@ onUnmounted(() => {
} }
/* Webkit浏览器(Chrome、Safari、Edge)的细滚动条样式 */ /* Webkit浏览器(Chrome、Safari、Edge)的细滚动条样式 */
.conflict-section::-webkit-scrollbar { .conflict-list-container::-webkit-scrollbar {
width: 4px; /* 细滚动条 */ width: 4px; /* 细滚动条 */
} }
.conflict-section::-webkit-scrollbar-track { .conflict-list-container::-webkit-scrollbar-track {
background: transparent; /* 透明轨道 */ background: transparent; /* 透明轨道 */
} }
.conflict-section::-webkit-scrollbar-thumb { .conflict-list-container::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3); /* 半透明滑块 */ background: rgba(255, 255, 255, 0.3); /* 半透明滑块 */
border-radius: 2px; border-radius: 2px;
} }
.conflict-section::-webkit-scrollbar-thumb:hover { .conflict-list-container::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5); /* 悬停时更明显 */} background: rgba(255, 255, 255, 0.5); /* 悬停时更明显 */
}
.conflict-header { .conflict-header {
display: flex; display: flex;
@@ -527,6 +594,14 @@ onUnmounted(() => {
.conflict-title { .conflict-title {
font-size: 12px; font-size: 12px;
flex: 1;
}
.total-overload {
font-size: 13px;
font-weight: 700;
color: #ff5252;
margin-left: auto;
} }
.conflict-item { .conflict-item {