Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Q
qjclaw-dmg
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
AI-甘富林
qjclaw-dmg
Commits
6acc6e51
Commit
6acc6e51
authored
May 13, 2026
by
edy
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(ui): add artifact menu for task panel
parent
0a63c6b5
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
711 additions
and
96 deletions
+711
-96
TaskPanelView.tsx
apps/ui/src/features/tasks/TaskPanelView.tsx
+269
-36
taskPanelData.ts
apps/ui/src/features/tasks/taskPanelData.ts
+31
-4
tasks.css
apps/ui/src/styles/tasks.css
+411
-56
No files found.
apps/ui/src/features/tasks/TaskPanelView.tsx
View file @
6acc6e51
import
{
useEffect
,
useMemo
,
useState
}
from
"react"
import
{
useEffect
,
useMemo
,
useRef
,
useState
}
from
"react"
import
type
{
TaskPanelItem
,
TaskPanelStatus
}
from
"@qjclaw/shared-types"
import
type
{
TaskPanelArtifact
,
TaskPanelItem
,
TaskPanelStatus
}
from
"@qjclaw/shared-types"
import
{
renderExpertIcon
}
from
"../../components/icons/AppIcons"
import
{
Panel
}
from
"../../components/ui/Panel"
import
{
Panel
}
from
"../../components/ui/Panel"
import
{
ScrollArea
}
from
"../../components/ui/ScrollArea"
import
{
ScrollArea
}
from
"../../components/ui/ScrollArea"
import
{
StatusChip
,
type
StatusChipTone
}
from
"../../components/ui/StatusChip
"
import
type
{
ExpertVisualKey
}
from
"../shell/ExpertTree
"
import
{
getDefaultTaskPanelDate
,
loadTaskPanelItems
}
from
"./taskPanelData"
import
{
getDefaultTaskPanelDate
,
loadTaskPanelItems
}
from
"./taskPanelData"
const
statusLabels
:
Record
<
TaskPanelStatus
,
string
>
=
{
const
statusLabels
:
Record
<
TaskPanelStatus
,
string
>
=
{
...
@@ -12,47 +13,218 @@ const statusLabels: Record<TaskPanelStatus, string> = {
...
@@ -12,47 +13,218 @@ const statusLabels: Record<TaskPanelStatus, string> = {
failed
:
"失败"
failed
:
"失败"
}
}
const
statusTones
:
Record
<
TaskPanelStatus
,
StatusChipTone
>
=
{
function
resolveTaskExpertIconKey
(
expertName
:
string
):
ExpertVisualKey
{
pending
:
"warning"
,
const
seed
=
expertName
.
toLowerCase
()
running
:
"info"
,
if
(
/xiaohongshu|xhs|rednote|小红书/
.
test
(
seed
))
{
completed
:
"positive"
,
return
"xiaohongshu"
failed
:
"warning"
}
if
(
/douyin|抖音/
.
test
(
seed
))
{
return
"douyin"
}
if
(
/tiktok/
.
test
(
seed
))
{
return
"tiktok"
}
if
(
/wechat|weixin|公众号|微信/
.
test
(
seed
))
{
return
"wechat"
}
if
(
/zhihu|知乎/
.
test
(
seed
))
{
return
"zhihu"
}
if
(
/content-account|planner|账号规划|内容账号规划/
.
test
(
seed
))
{
return
"planner"
}
if
(
/precision-leads|线索|lead/
.
test
(
seed
))
{
return
"leads"
}
if
(
/sales-champion|销售冠军|销冠|sales champion/
.
test
(
seed
))
{
return
"sales"
}
if
(
/poster|海报/
.
test
(
seed
))
{
return
"poster"
}
if
(
/
(
^|
[\s
-
])
x
(
$|
[\s
-
])
|twitter/
.
test
(
seed
))
{
return
"x"
}
if
(
/geo/
.
test
(
seed
))
{
return
"geo"
}
if
(
/browser|automation|chrome|playwright|web|浏览器|自动化/
.
test
(
seed
))
{
return
"browser"
}
return
"general"
}
}
function
TaskStatus
({
item
}:
{
item
:
TaskPanelItem
})
{
function
TaskStatus
({
item
}:
{
item
:
TaskPanelItem
})
{
return
(
return
(
<
div
className=
"task-panel-status"
>
<
div
className=
{
"task-panel-status task-panel-status-"
+
item
.
status
}
>
<
StatusChip
tone=
{
statusTones
[
item
.
status
]
}
>
<
span
className=
"task-panel-status-icon"
title=
{
item
.
statusDetail
}
aria
-
hidden=
"true"
>
<
span
className=
{
"task-panel-status-dot task-panel-status-dot-"
+
item
.
status
}
aria
-
hidden=
"true"
/>
{
item
.
status
===
"completed"
?
<
span
>
✓
</
span
>
:
null
}
{
statusLabels
[
item
.
status
]
}
{
item
.
status
===
"running"
?
(
</
StatusChip
>
<
span
className=
"task-panel-running-dots"
aria
-
hidden=
"true"
>
<
span
>
{
item
.
statusDetail
}
</
span
>
<
span
/>
<
span
/>
<
span
/>
</
span
>
)
:
null
}
{
item
.
status
===
"pending"
?
<
span
>
•
</
span
>
:
null
}
{
item
.
status
===
"failed"
?
<
span
>
!
</
span
>
:
null
}
</
span
>
<
strong
className=
"task-panel-status-text"
title=
{
item
.
statusDetail
}
>
{
statusLabels
[
item
.
status
]
}
</
strong
>
</
div
>
</
div
>
)
)
}
}
function
TaskArtifactList
({
artifacts
,
copiedArtifactId
,
onCopy
}:
{
artifacts
:
TaskPanelArtifact
[]
copiedArtifactId
:
string
onCopy
:
(
artifactId
:
string
,
artifactUrl
:
string
)
=>
void
})
{
return
(
<
ul
className=
"task-panel-artifact-list"
aria
-
label=
"产物清单"
>
{
artifacts
.
map
((
artifact
)
=>
(
<
li
key=
{
artifact
.
id
}
className=
{
copiedArtifactId
===
artifact
.
id
?
"task-panel-artifact-item-copied"
:
undefined
}
title=
{
artifact
.
url
??
artifact
.
name
}
>
<
span
className=
"task-panel-artifact-name"
title=
{
artifact
.
name
}
>
{
artifact
.
name
}
</
span
>
{
artifact
.
url
?
(
<
button
type=
"button"
className=
"task-panel-artifact-url"
title=
{
artifact
.
url
}
onClick=
{
()
=>
onCopy
(
artifact
.
id
,
artifact
.
url
??
""
)
}
>
{
artifact
.
url
}
</
button
>
)
:
null
}
{
copiedArtifactId
===
artifact
.
id
?
(
<
span
className=
"task-panel-artifact-copied"
aria
-
live=
"polite"
>
已复制
</
span
>
)
:
null
}
</
li
>
))
}
</
ul
>
)
}
function
TaskArtifacts
({
item
}:
{
item
:
TaskPanelItem
})
{
function
TaskArtifacts
({
item
}:
{
item
:
TaskPanelItem
})
{
const
[
copiedArtifactId
,
setCopiedArtifactId
]
=
useState
(
""
)
const
[
isMenuOpen
,
setIsMenuOpen
]
=
useState
(
false
)
const
menuRef
=
useRef
<
HTMLDivElement
|
null
>
(
null
)
const
copiedTimerRef
=
useRef
<
number
|
null
>
(
null
)
useEffect
(()
=>
{
return
()
=>
{
if
(
copiedTimerRef
.
current
!==
null
)
{
window
.
clearTimeout
(
copiedTimerRef
.
current
)
}
}
},
[])
useEffect
(()
=>
{
setIsMenuOpen
(
false
)
setCopiedArtifactId
(
""
)
},
[
item
.
id
])
useEffect
(()
=>
{
if
(
!
isMenuOpen
)
{
return
}
const
handlePointerDown
=
(
event
:
PointerEvent
)
=>
{
const
menuElement
=
menuRef
.
current
if
(
menuElement
&&
event
.
target
instanceof
Node
&&
!
menuElement
.
contains
(
event
.
target
))
{
setIsMenuOpen
(
false
)
}
}
const
handleKeyDown
=
(
event
:
KeyboardEvent
)
=>
{
if
(
event
.
key
===
"Escape"
)
{
setIsMenuOpen
(
false
)
}
}
document
.
addEventListener
(
"pointerdown"
,
handlePointerDown
,
true
)
document
.
addEventListener
(
"keydown"
,
handleKeyDown
)
return
()
=>
{
document
.
removeEventListener
(
"pointerdown"
,
handlePointerDown
,
true
)
document
.
removeEventListener
(
"keydown"
,
handleKeyDown
)
}
},
[
isMenuOpen
])
const
copyArtifactUrl
=
async
(
artifactId
:
string
,
artifactUrl
:
string
)
=>
{
try
{
await
navigator
.
clipboard
.
writeText
(
artifactUrl
)
setCopiedArtifactId
(
artifactId
)
if
(
copiedTimerRef
.
current
!==
null
)
{
window
.
clearTimeout
(
copiedTimerRef
.
current
)
}
copiedTimerRef
.
current
=
window
.
setTimeout
(()
=>
{
setCopiedArtifactId
(
""
)
copiedTimerRef
.
current
=
null
},
1400
)
}
catch
{
// Copy feedback is best-effort and should not affect task data.
}
}
if
(
!
item
.
artifacts
.
length
)
{
if
(
!
item
.
artifacts
.
length
)
{
return
<
span
className=
"task-panel-muted"
>
暂无产物
</
span
>
return
(
<
div
className=
"task-panel-artifact-center"
>
<
span
className=
"task-panel-muted"
>
暂无产物
</
span
>
</
div
>
)
}
if
(
item
.
artifacts
.
length
<=
2
)
{
return
(
<
div
className=
"task-panel-artifact-center"
>
<
TaskArtifactList
artifacts=
{
item
.
artifacts
}
copiedArtifactId=
{
copiedArtifactId
}
onCopy=
{
(
artifactId
,
artifactUrl
)
=>
void
copyArtifactUrl
(
artifactId
,
artifactUrl
)
}
/>
</
div
>
)
}
}
return
(
return
(
<
ul
className=
"task-panel-artifact-list"
aria
-
label=
{
item
.
taskTitle
+
"产物清单"
}
>
<
div
className=
"task-panel-artifact-menu"
ref=
{
menuRef
}
>
{
item
.
artifacts
.
map
((
artifact
)
=>
(
<
button
<
li
key=
{
artifact
.
id
}
>
type=
"button"
<
span
className=
"task-panel-artifact-name"
>
{
artifact
.
name
}
</
span
>
className=
"task-panel-artifact-trigger"
{
artifact
.
kind
?
<
span
className=
"task-panel-artifact-kind"
>
{
artifact
.
kind
}
</
span
>
:
null
}
aria
-
haspopup=
"menu"
</
li
>
aria
-
expanded=
{
isMenuOpen
}
))
}
aria
-
controls=
{
item
.
id
+
"-artifact-menu"
}
</
ul
>
onClick=
{
()
=>
setIsMenuOpen
((
current
)
=>
!
current
)
}
>
{
item
.
artifacts
.
length
}
个产物
</
button
>
{
isMenuOpen
?
(
<
div
className=
"task-panel-artifact-popover"
id=
{
item
.
id
+
"-artifact-menu"
}
role=
"menu"
>
<
TaskArtifactList
artifacts=
{
item
.
artifacts
}
copiedArtifactId=
{
copiedArtifactId
}
onCopy=
{
(
artifactId
,
artifactUrl
)
=>
void
copyArtifactUrl
(
artifactId
,
artifactUrl
)
}
/>
</
div
>
)
:
null
}
</
div
>
)
)
}
}
export
function
TaskPanelView
()
{
export
function
TaskPanelView
()
{
const
[
selectedDate
,
setSelectedDate
]
=
useState
(
getDefaultTaskPanelDate
)
const
[
selectedDate
,
setSelectedDate
]
=
useState
(
getDefaultTaskPanelDate
)
const
[
items
,
setItems
]
=
useState
<
TaskPanelItem
[]
>
([])
const
[
items
,
setItems
]
=
useState
<
TaskPanelItem
[]
>
([])
const
[
selectedTaskIds
,
setSelectedTaskIds
]
=
useState
<
Record
<
string
,
string
>>
({})
const
[
loading
,
setLoading
]
=
useState
(
true
)
const
[
loading
,
setLoading
]
=
useState
(
true
)
const
[
errorText
,
setErrorText
]
=
useState
(
""
)
const
[
errorText
,
setErrorText
]
=
useState
(
""
)
const
dateInputRef
=
useRef
<
HTMLInputElement
|
null
>
(
null
)
useEffect
(()
=>
{
useEffect
(()
=>
{
let
active
=
true
let
active
=
true
...
@@ -82,24 +254,64 @@ export function TaskPanelView() {
...
@@ -82,24 +254,64 @@ export function TaskPanelView() {
}
}
},
[
selectedDate
])
},
[
selectedDate
])
const
completedCount
=
useMemo
(()
=>
items
.
filter
((
item
)
=>
item
.
status
===
"completed"
).
length
,
[
items
])
const
displayDate
=
useMemo
(()
=>
selectedDate
.
replaceAll
(
"-"
,
"/"
),
[
selectedDate
])
const
displayMonth
=
useMemo
(()
=>
`
${
Number
(
selectedDate
.
slice
(
5
,
7
))}
月`
,
[
selectedDate
])
const
expertRows
=
useMemo
(()
=>
{
const
groups
=
new
Map
<
string
,
TaskPanelItem
[]
>
()
for
(
const
item
of
items
)
{
const
groupItems
=
groups
.
get
(
item
.
expertName
)
if
(
groupItems
)
{
groupItems
.
push
(
item
)
}
else
{
groups
.
set
(
item
.
expertName
,
[
item
])
}
}
return
Array
.
from
(
groups
,
([
expertName
,
tasks
])
=>
{
const
selectedTask
=
tasks
.
find
((
task
)
=>
task
.
id
===
selectedTaskIds
[
expertName
])
??
tasks
[
0
]
return
{
expertName
,
tasks
,
selectedTask
}
})
},
[
items
,
selectedTaskIds
])
return
(
return
(
<
div
className=
"page-stack task-panel-page-stack"
>
<
div
className=
"page-stack task-panel-page-stack"
>
<
Panel
className=
"task-panel-page"
bodyClassName=
"task-panel-body"
>
<
Panel
className=
"task-panel-page"
bodyClassName=
"task-panel-body"
>
<
div
className=
"task-panel-header"
>
<
div
className=
"task-panel-header"
>
<
div
className=
"task-panel-title-group"
>
<
div
className=
"task-panel-title-group"
>
<
h1
>
任务面板
</
h1
>
<
h1
><
span
>
任务面板
</
span
></
h1
>
<
p
>
{
items
.
length
?
`${items.length} 个任务,${completedCount} 个已完成`
:
"按日期查看专家任务与产物"
}
</
p
>
</
div
>
</
div
>
<
label
className=
"task-panel-date-field"
>
<
div
className=
"task-panel-date-field"
>
<
span
>
日期
</
span
>
<
button
type=
"button"
className=
"task-panel-date-pill"
aria
-
label=
{
"选择日期,当前为 "
+
displayDate
}
onClick=
{
()
=>
{
const
dateInput
=
dateInputRef
.
current
if
(
!
dateInput
)
{
return
}
if
(
typeof
dateInput
.
showPicker
===
"function"
)
{
dateInput
.
showPicker
()
}
else
{
dateInput
.
click
()
}
}
}
>
<
span
className=
"task-panel-date-calendar"
aria
-
hidden=
"true"
>
<
span
className=
"task-panel-date-calendar-month"
>
{
displayMonth
}
</
span
>
<
span
className=
"task-panel-date-calendar-day"
>
{
selectedDate
.
slice
(
-
2
)
}
</
span
>
</
span
>
<
span
className=
"task-panel-date-value"
>
{
displayDate
}
</
span
>
</
button
>
<
input
<
input
ref=
{
dateInputRef
}
type=
"date"
type=
"date"
aria
-
hidden=
"true"
tabIndex=
{
-
1
}
value=
{
selectedDate
}
value=
{
selectedDate
}
onChange=
{
(
event
)
=>
setSelectedDate
(
event
.
currentTarget
.
value
)
}
onChange=
{
(
event
)
=>
setSelectedDate
(
event
.
currentTarget
.
value
)
}
/>
/>
</
label
>
</
div
>
</
div
>
</
div
>
<
ScrollArea
className=
"scroll-panel task-panel-scroll"
aria
-
busy=
{
loading
}
>
<
ScrollArea
className=
"scroll-panel task-panel-scroll"
aria
-
busy=
{
loading
}
>
...
@@ -109,21 +321,42 @@ export function TaskPanelView() {
...
@@ -109,21 +321,42 @@ export function TaskPanelView() {
{
!
loading
&&
!
errorText
&&
items
.
length
?
(
{
!
loading
&&
!
errorText
&&
items
.
length
?
(
<
div
className=
"task-panel-table"
role=
"table"
aria
-
label=
"任务面板"
>
<
div
className=
"task-panel-table"
role=
"table"
aria
-
label=
"任务面板"
>
<
div
className=
"task-panel-row task-panel-row-head"
role=
"row"
>
<
div
className=
"task-panel-row task-panel-row-head"
role=
"row"
>
<
div
role=
"columnheader"
>
专家
</
div
>
<
div
role=
"columnheader"
>
数字员工
</
div
>
<
div
role=
"columnheader"
>
任务列表
</
div
>
<
div
role=
"columnheader"
>
执行状态
</
div
>
<
div
role=
"columnheader"
>
执行状态
</
div
>
<
div
role=
"columnheader"
>
产物清单
</
div
>
<
div
role=
"columnheader"
>
产物清单
</
div
>
</
div
>
</
div
>
{
items
.
map
((
item
)
=>
(
{
expertRows
.
map
((
row
)
=>
(
<
article
key=
{
item
.
id
}
className=
"task-panel-row"
role=
"row"
>
<
article
key=
{
row
.
expertName
}
className=
"task-panel-row"
role=
"row"
>
<
div
className=
"task-panel-expert-cell"
role=
"cell"
>
<
div
className=
"task-panel-expert-cell"
role=
"cell"
>
<
strong
>
{
item
.
expertName
}
</
strong
>
<
span
className=
{
"task-panel-expert-icon task-panel-expert-icon-"
+
resolveTaskExpertIconKey
(
row
.
expertName
)
}
aria
-
hidden=
"true"
>
<
span
>
{
item
.
taskTitle
}
</
span
>
{
renderExpertIcon
(
resolveTaskExpertIconKey
(
row
.
expertName
))
}
</
span
>
<
strong
title=
{
row
.
expertName
}
>
{
row
.
expertName
}
</
strong
>
</
div
>
<
div
className=
"task-panel-task-cell"
role=
"cell"
>
<
select
aria
-
label=
{
row
.
expertName
+
"任务列表"
}
title=
{
row
.
selectedTask
.
taskTitle
}
value=
{
row
.
selectedTask
.
id
}
onChange=
{
(
event
)
=>
{
const
selectedTaskId
=
event
.
currentTarget
.
value
setSelectedTaskIds
((
current
)
=>
({
...
current
,
[
row
.
expertName
]:
selectedTaskId
}))
}
}
>
{
row
.
tasks
.
map
((
task
)
=>
(
<
option
key=
{
task
.
id
}
value=
{
task
.
id
}
>
{
task
.
taskTitle
}
</
option
>
))
}
</
select
>
</
div
>
</
div
>
<
div
role=
"cell"
>
<
div
role=
"cell"
>
<
TaskStatus
item=
{
item
}
/>
<
TaskStatus
item=
{
row
.
selectedTask
}
/>
</
div
>
</
div
>
<
div
role=
"cell"
>
<
div
role=
"cell"
>
<
TaskArtifacts
item=
{
item
}
/>
<
TaskArtifacts
item=
{
row
.
selectedTask
}
/>
</
div
>
</
div
>
</
article
>
</
article
>
))
}
))
}
...
...
apps/ui/src/features/tasks/taskPanelData.ts
View file @
6acc6e51
...
@@ -26,8 +26,22 @@ export const mockTaskPanelItems: TaskPanelItem[] = [
...
@@ -26,8 +26,22 @@ export const mockTaskPanelItems: TaskPanelItem[] = [
status
:
"running"
,
status
:
"running"
,
statusDetail
:
"正在汇总账号定位、目标人群和栏目节奏"
,
statusDetail
:
"正在汇总账号定位、目标人群和栏目节奏"
,
artifacts
:
[
artifacts
:
[
{
id
:
"artifact-content-outline"
,
name
:
"选题规划草稿.md"
,
kind
:
"文档"
},
{
id
:
"artifact-content-outline"
,
name
:
"选题规划草稿.md"
,
kind
:
"文档"
,
url
:
"/Users/edy/Documents/qianjiangclaw/tasks/content/选题规划草稿.md"
},
{
id
:
"artifact-content-calendar"
,
name
:
"发布日历.xlsx"
,
kind
:
"表格"
}
{
id
:
"artifact-content-calendar"
,
name
:
"发布日历.xlsx"
,
kind
:
"表格"
,
url
:
"/Users/edy/Documents/qianjiangclaw/tasks/content/发布日历.xlsx"
},
{
id
:
"artifact-content-persona"
,
name
:
"目标人群画像.md"
,
kind
:
"文档"
,
url
:
"/Users/edy/Documents/qianjiangclaw/tasks/content/目标人群画像.md"
},
{
id
:
"artifact-content-topics"
,
name
:
"栏目选题池.csv"
,
kind
:
"表格"
,
url
:
"/Users/edy/Documents/qianjiangclaw/tasks/content/栏目选题池.csv"
},
{
id
:
"artifact-content-brief"
,
name
:
"账号定位简报.pdf"
,
kind
:
"文档"
,
url
:
"/Users/edy/Documents/qianjiangclaw/tasks/content/账号定位简报.pdf"
}
]
},
{
id
:
"mock-task-content-review"
,
date
:
getDefaultTaskPanelDate
(),
expertName
:
"内容账号规划专家"
,
taskTitle
:
"复盘昨日内容表现"
,
status
:
"completed"
,
statusDetail
:
"已完成互动数据摘要与优化建议"
,
artifacts
:
[
{
id
:
"artifact-content-review"
,
name
:
"昨日内容复盘与下轮优化建议.md"
,
kind
:
"文档"
,
url
:
"/Users/edy/Documents/qianjiangclaw/tasks/content/昨日内容复盘与下轮优化建议.md"
}
]
]
},
},
{
{
...
@@ -38,7 +52,18 @@ export const mockTaskPanelItems: TaskPanelItem[] = [
...
@@ -38,7 +52,18 @@ export const mockTaskPanelItems: TaskPanelItem[] = [
status
:
"completed"
,
status
:
"completed"
,
statusDetail
:
"已完成回答大纲与首版正文"
,
statusDetail
:
"已完成回答大纲与首版正文"
,
artifacts
:
[
artifacts
:
[
{
id
:
"artifact-zhihu-answer"
,
name
:
"知乎回答初稿.md"
,
kind
:
"文档"
}
{
id
:
"artifact-zhihu-answer"
,
name
:
"知乎回答初稿.md"
,
kind
:
"文档"
,
url
:
"/Users/edy/Documents/qianjiangclaw/tasks/zhihu/知乎回答初稿.md"
}
]
},
{
id
:
"mock-task-zhihu-research"
,
date
:
getDefaultTaskPanelDate
(),
expertName
:
"知乎专家"
,
taskTitle
:
"整理竞品问答素材"
,
status
:
"running"
,
statusDetail
:
"正在提取高赞回答结构和关键词"
,
artifacts
:
[
{
id
:
"artifact-zhihu-research"
,
name
:
"竞品问答素材汇总-长文件名用于验证省略显示效果.xlsx"
,
kind
:
"表格"
,
url
:
"/Users/edy/Documents/qianjiangclaw/tasks/zhihu/竞品问答素材汇总-长文件名用于验证省略显示效果.xlsx"
}
]
]
},
},
{
{
...
@@ -57,7 +82,9 @@ export const mockTaskPanelItems: TaskPanelItem[] = [
...
@@ -57,7 +82,9 @@ export const mockTaskPanelItems: TaskPanelItem[] = [
taskTitle
:
"生成活动海报文案"
,
taskTitle
:
"生成活动海报文案"
,
status
:
"failed"
,
status
:
"failed"
,
statusDetail
:
"素材包缺少主视觉图片"
,
statusDetail
:
"素材包缺少主视觉图片"
,
artifacts
:
[]
artifacts
:
[
{
id
:
"artifact-poster-brief"
,
name
:
"活动海报文案草稿.txt"
,
kind
:
"文档"
,
url
:
"/Users/edy/Documents/qianjiangclaw/tasks/poster/活动海报文案草稿.txt"
}
]
}
}
]
]
...
...
apps/ui/src/styles/tasks.css
View file @
6acc6e51
...
@@ -34,42 +34,130 @@
...
@@ -34,42 +34,130 @@
.task-panel-title-group
{
.task-panel-title-group
{
display
:
grid
;
display
:
grid
;
gap
:
6px
;
min-width
:
0
;
min-width
:
0
;
}
}
.task-panel-title-group
h1
{
.task-panel-title-group
h1
{
display
:
inline-flex
;
width
:
fit-content
;
margin
:
0
;
margin
:
0
;
padding
:
4px
;
border-radius
:
999px
;
border
:
1px
solid
rgba
(
191
,
219
,
254
,
0.92
);
background
:
linear-gradient
(
180deg
,
#ffffff
0%
,
#eef7ff
100%
);
box-shadow
:
0
10px
22px
rgba
(
37
,
99
,
235
,
0.1
),
inset
0
1px
0
rgba
(
255
,
255
,
255
,
0.96
);
color
:
#1d344f
;
color
:
#1d344f
;
font-size
:
2
8px
;
font-size
:
1
8px
;
line-height
:
1.2
;
line-height
:
1.2
;
}
}
.task-panel-title-group
p
{
.task-panel-title-group
h1
span
{
margin
:
0
;
display
:
inline-flex
;
color
:
#61758f
;
min-height
:
36px
;
font-size
:
14px
;
align-items
:
center
;
line-height
:
1.6
;
padding
:
0
18px
;
border-radius
:
999px
;
background
:
#ffffff
;
box-shadow
:
inset
0
0
0
1px
rgba
(
226
,
232
,
240
,
0.9
);
font-weight
:
800
;
white-space
:
nowrap
;
}
}
.task-panel-date-field
{
.task-panel-date-field
{
display
:
grid
;
position
:
relative
;
gap
:
6px
;
display
:
inline-grid
;
flex
:
0
0
auto
;
flex
:
0
0
auto
;
color
:
#53637f
;
font-size
:
12px
;
font-weight
:
700
;
}
}
.task-panel-date-field
input
{
.task-panel-date-field
input
{
min-height
:
38px
;
position
:
absolute
;
padding
:
0
12px
;
right
:
0
;
border-radius
:
12px
;
bottom
:
0
;
border
:
1px
solid
#d8e1ef
;
width
:
1px
;
background
:
#ffffff
;
height
:
1px
;
opacity
:
0
;
pointer-events
:
none
;
}
.task-panel-header
.task-panel-date-pill
{
display
:
inline-flex
;
align-items
:
center
;
gap
:
8px
;
min-height
:
42px
;
padding
:
0
;
border
:
0
;
border-radius
:
0
;
background
:
transparent
;
box-shadow
:
none
;
color
:
#1f2f49
;
color
:
#1f2f49
;
cursor
:
pointer
;
font
:
inherit
;
font
:
inherit
;
font-size
:
13px
;
transition
:
color
180ms
ease
;
}
.task-panel-header
.task-panel-date-pill
:hover
,
.task-panel-header
.task-panel-date-pill
:focus-visible
{
background
:
transparent
;
box-shadow
:
none
;
color
:
#1d4ed8
;
}
.task-panel-header
.task-panel-date-pill
:focus-visible
{
outline
:
2px
solid
rgba
(
37
,
99
,
235
,
0.28
);
outline-offset
:
2px
;
}
.task-panel-date-calendar
{
display
:
grid
;
grid-template-rows
:
10px
1
fr
;
width
:
32px
;
height
:
32px
;
overflow
:
hidden
;
place-items
:
center
;
border-radius
:
9px
;
background
:
linear-gradient
(
180deg
,
#ffffff
0%
,
#eef4ff
100%
);
border
:
1px
solid
#d7e1ef
;
box-shadow
:
inset
0
1px
0
rgba
(
255
,
255
,
255
,
0.94
),
0
5px
10px
rgba
(
15
,
23
,
42
,
0.08
);
}
.task-panel-date-calendar-month
{
display
:
grid
;
width
:
100%
;
height
:
100%
;
place-items
:
center
;
background
:
#ff5f57
;
color
:
#ffffff
;
font-size
:
7px
;
font-weight
:
800
;
line-height
:
1
;
}
.task-panel-date-calendar-day
{
color
:
#1f2f49
;
font-size
:
12px
;
font-weight
:
800
;
line-height
:
1
;
}
.task-panel-date-value
{
display
:
inline-flex
;
min-height
:
42px
;
align-items
:
center
;
padding
:
0
16px
;
border
:
1px
solid
rgba
(
203
,
213
,
225
,
0.92
);
border-radius
:
999px
;
background
:
linear-gradient
(
180deg
,
#ffffff
0%
,
#f8fbff
100%
);
box-shadow
:
none
;
font-size
:
14px
;
font-weight
:
800
;
line-height
:
1
;
white-space
:
nowrap
;
}
.task-panel-header
.task-panel-date-pill
:hover
.task-panel-date-value
,
.task-panel-header
.task-panel-date-pill
:focus-visible
.task-panel-date-value
{
border-color
:
#93c5fd
;
color
:
#1d4ed8
;
}
}
.task-panel-scroll
{
.task-panel-scroll
{
...
@@ -83,16 +171,20 @@
...
@@ -83,16 +171,20 @@
.task-panel-table
{
.task-panel-table
{
display
:
grid
;
display
:
grid
;
gap
:
10px
;
gap
:
8px
;
min-width
:
920px
;
}
}
.task-panel-row
{
.task-panel-row
{
display
:
grid
;
display
:
grid
;
grid-template-columns
:
minmax
(
190px
,
1
fr
)
minmax
(
190px
,
1
fr
)
minmax
(
220px
,
1.15
fr
);
grid-template-columns
:
165px
225px
170px
minmax
(
250px
,
1
fr
);
gap
:
16px
;
gap
:
14px
;
align-items
:
start
;
align-items
:
center
;
padding
:
16px
;
min-height
:
104px
;
border-radius
:
18px
;
max-height
:
104px
;
padding
:
14px
16px
;
overflow
:
visible
;
border-radius
:
16px
;
border
:
1px
solid
rgba
(
215
,
216
,
229
,
0.96
);
border
:
1px
solid
rgba
(
215
,
216
,
229
,
0.96
);
background
:
rgba
(
255
,
255
,
255
,
0.92
);
background
:
rgba
(
255
,
255
,
255
,
0.92
);
box-shadow
:
0
12px
28px
rgba
(
17
,
24
,
39
,
0.04
);
box-shadow
:
0
12px
28px
rgba
(
17
,
24
,
39
,
0.04
);
...
@@ -100,8 +192,11 @@
...
@@ -100,8 +192,11 @@
.task-panel-row-head
{
.task-panel-row-head
{
min-height
:
44px
;
min-height
:
44px
;
max-height
:
44px
;
align-items
:
center
;
align-items
:
center
;
text-align
:
center
;
padding
:
10px
16px
;
padding
:
10px
16px
;
overflow
:
hidden
;
border-radius
:
14px
;
border-radius
:
14px
;
background
:
rgba
(
239
,
246
,
255
,
0.78
);
background
:
rgba
(
239
,
246
,
255
,
0.78
);
color
:
#53637f
;
color
:
#53637f
;
...
@@ -109,70 +204,287 @@
...
@@ -109,70 +204,287 @@
font-weight
:
800
;
font-weight
:
800
;
}
}
.task-panel-expert-cell
,
.task-panel-row
>
div
{
.task-panel-status
{
min-width
:
0
;
}
.task-panel-row
:not
(
.task-panel-row-head
)
>
div
:nth-child
(
3
)
{
padding-left
:
54px
;
}
.task-panel-row
:not
(
.task-panel-row-head
)
>
div
:nth-child
(
4
)
{
display
:
flex
;
position
:
relative
;
align-items
:
center
;
justify-content
:
center
;
padding-left
:
0
;
}
.task-panel-expert-cell
{
display
:
grid
;
display
:
grid
;
gap
:
7px
;
grid-template-columns
:
36px
minmax
(
0
,
1
fr
);
align-items
:
center
;
gap
:
10px
;
min-width
:
0
;
min-width
:
0
;
}
}
.task-panel-expert-icon
{
display
:
grid
;
width
:
36px
;
height
:
36px
;
place-items
:
center
;
border-radius
:
12px
;
color
:
#2563eb
;
background
:
#eff6ff
;
border
:
1px
solid
#dbeafe
;
}
.task-panel-expert-icon
svg
{
width
:
21px
;
height
:
21px
;
}
.task-panel-expert-icon-planner
{
color
:
#0f766e
;
background
:
#ecfdf5
;
border-color
:
#bbf7d0
;
}
.task-panel-expert-icon-zhihu
{
color
:
#1d4ed8
;
background
:
#eff6ff
;
border-color
:
#bfdbfe
;
}
.task-panel-expert-icon-leads
{
color
:
#0f766e
;
background
:
#ecfeff
;
border-color
:
#a5f3fc
;
}
.task-panel-expert-icon-poster
{
color
:
#be123c
;
background
:
#fff1f2
;
border-color
:
#fecdd3
;
}
.task-panel-expert-cell
strong
{
.task-panel-expert-cell
strong
{
min-width
:
0
;
color
:
#1c324d
;
color
:
#1c324d
;
font-size
:
15px
;
font-size
:
13px
;
font-weight
:
500
;
line-height
:
1.4
;
line-height
:
1.4
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
}
.task-panel-task-cell
{
min-width
:
0
;
}
.task-panel-task-cell
select
{
width
:
100%
;
min-width
:
0
;
min-height
:
38px
;
padding
:
0
34px
0
12px
;
border-radius
:
12px
;
border
:
1px
solid
#d8e1ef
;
background
:
#ffffff
;
color
:
#1f2f49
;
font
:
inherit
;
font-size
:
13px
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
}
.task-panel-status
{
display
:
inline-flex
;
align-items
:
center
;
gap
:
8px
;
min-width
:
0
;
}
.task-panel-status-icon
{
display
:
grid
;
flex
:
0
0
auto
;
width
:
34px
;
height
:
28px
;
place-items
:
center
;
border-radius
:
999px
;
font-size
:
13px
;
font-weight
:
800
;
line-height
:
1
;
}
.task-panel-status-text
{
min-width
:
0
;
color
:
#1f2f49
;
font-size
:
13px
;
font-weight
:
600
;
line-height
:
1.5
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
}
.task-panel-running-dots
{
display
:
inline-flex
;
align-items
:
center
;
gap
:
3px
;
}
.task-panel-running-dots
span
{
width
:
5px
;
height
:
5px
;
border-radius
:
999px
;
background
:
currentColor
;
animation
:
task-status-dot-bounce
1s
ease-in-out
infinite
;
}
.task-panel-running-dots
span
:nth-child
(
2
)
{
animation-delay
:
0.14s
;
}
.task-panel-running-dots
span
:nth-child
(
3
)
{
animation-delay
:
0.28s
;
}
}
.task-panel-expert-cell
span
,
.task-panel-status
>
span
:not
(
.status-chip
),
.task-panel-muted
{
.task-panel-muted
{
color
:
#61758f
;
color
:
#61758f
;
font-size
:
13px
;
font-size
:
13px
;
line-height
:
1.6
;
line-height
:
1.6
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
}
}
.task-panel-status
.status-chip
{
.task-panel-status-completed
.task-panel-status-icon
{
width
:
fit-content
;
color
:
#047857
;
gap
:
6px
;
background
:
#ecfdf5
;
border
:
1px
solid
#a7f3d0
;
}
}
.task-panel-status-dot
{
.task-panel-status-pending
.task-panel-status-icon
{
width
:
7px
;
color
:
#92400e
;
height
:
7px
;
background
:
#fffbeb
;
border-radius
:
999px
;
border
:
1px
solid
#fde68a
;
background
:
currentColor
;
}
}
.task-panel-status-dot-running
{
.task-panel-status-running
.task-panel-status-icon
{
animation
:
task-status-pulse
1.2s
ease-in-out
infinite
;
color
:
#1d4ed8
;
background
:
#eff6ff
;
border
:
1px
solid
#bfdbfe
;
}
}
.task-panel-status-
dot-failed
{
.task-panel-status-
failed
.task-panel-status-icon
{
color
:
#b42318
;
color
:
#b42318
;
background
:
#fef2f2
;
border
:
1px
solid
#fecaca
;
}
}
.task-panel-artifact-list
{
.task-panel-artifact-list
{
display
:
grid
;
display
:
grid
;
gap
:
8px
;
gap
:
6px
;
width
:
min
(
100%
,
420px
);
margin
:
0
;
margin
:
0
;
padding
:
0
;
padding
:
0
;
list-style
:
none
;
list-style
:
none
;
min-width
:
0
;
overflow
:
visible
;
}
}
.task-panel-artifact-
list
li
{
.task-panel-artifact-
center
{
display
:
flex
;
display
:
flex
;
width
:
100%
;
min-width
:
0
;
align-items
:
center
;
align-items
:
center
;
justify-content
:
space-between
;
justify-content
:
center
;
gap
:
10px
;
}
.task-panel-artifact-menu
{
position
:
relative
;
display
:
flex
;
width
:
100%
;
min-width
:
0
;
min-width
:
0
;
padding
:
8px
10px
;
align-items
:
center
;
justify-content
:
center
;
z-index
:
4
;
}
.task-panel-artifact-trigger
{
display
:
inline-flex
;
min-height
:
34px
;
align-items
:
center
;
justify-content
:
center
;
padding
:
0
14px
;
border-radius
:
999px
;
border
:
1px
solid
#cfe0f5
;
background
:
#ffffff
;
color
:
#1f2f49
;
box-shadow
:
0
8px
18px
rgba
(
15
,
23
,
42
,
0.06
);
font
:
inherit
;
font-size
:
13px
;
font-weight
:
800
;
line-height
:
1
;
white-space
:
nowrap
;
cursor
:
pointer
;
}
.task-panel-artifact-trigger
:hover
,
.task-panel-artifact-trigger
:focus-visible
,
.task-panel-artifact-trigger
[
aria-expanded
=
"true"
]
{
border-color
:
#93c5fd
;
color
:
#1d4ed8
;
}
.task-panel-artifact-trigger
:focus-visible
{
outline
:
2px
solid
rgba
(
37
,
99
,
235
,
0.28
);
outline-offset
:
2px
;
}
.task-panel-artifact-popover
{
position
:
absolute
;
top
:
calc
(
100%
+
8px
);
left
:
50%
;
width
:
min
(
520px
,
calc
(
100vw
-
64px
));
max-height
:
220px
;
overflow-y
:
auto
;
padding
:
8px
;
border-radius
:
14px
;
border
:
1px
solid
rgba
(
203
,
213
,
225
,
0.96
);
background
:
#ffffff
;
box-shadow
:
0
18px
42px
rgba
(
15
,
23
,
42
,
0.14
);
transform
:
translateX
(
-50%
);
z-index
:
20
;
}
.task-panel-artifact-popover
.task-panel-artifact-list
{
width
:
100%
;
}
.task-panel-artifact-list
li
{
display
:
grid
;
position
:
relative
;
grid-template-columns
:
minmax
(
110px
,
0.86
fr
)
minmax
(
0
,
1.34
fr
);
align-items
:
center
;
gap
:
4px
;
min-width
:
0
;
min-height
:
34px
;
padding
:
6px
10px
;
border-radius
:
12px
;
border-radius
:
12px
;
background
:
#f8fbff
;
background
:
#f8fbff
;
border
:
1px
solid
#e3ebf5
;
border
:
1px
solid
#e3ebf5
;
}
}
.task-panel-artifact-name
{
.task-panel-artifact-list
li
.task-panel-artifact-item-copied
{
padding-right
:
58px
;
}
.task-panel-artifact-name
,
.task-panel-artifact-url
{
min-width
:
0
;
min-width
:
0
;
color
:
#1f2f49
;
font-size
:
13px
;
font-size
:
13px
;
line-height
:
1.5
;
line-height
:
1.5
;
overflow
:
hidden
;
overflow
:
hidden
;
...
@@ -180,21 +492,64 @@
...
@@ -180,21 +492,64 @@
white-space
:
nowrap
;
white-space
:
nowrap
;
}
}
.task-panel-artifact-kind
{
.task-panel-artifact-name
{
flex
:
0
0
auto
;
color
:
#1f2f49
;
font-weight
:
700
;
}
.task-panel-artifact-url
{
color
:
#60728c
;
color
:
#60728c
;
justify-self
:
end
;
width
:
100%
;
min-height
:
24px
;
padding
:
0
;
border
:
0
;
background
:
transparent
;
font
:
inherit
;
text-align
:
right
;
cursor
:
pointer
;
}
.task-panel-artifact-url
:hover
,
.task-panel-artifact-url
:focus-visible
{
color
:
#1d4ed8
;
text-decoration
:
underline
;
text-underline-offset
:
3px
;
}
.task-panel-artifact-url
:focus-visible
{
outline
:
2px
solid
rgba
(
37
,
99
,
235
,
0.34
);
outline-offset
:
2px
;
border-radius
:
6px
;
}
.task-panel-artifact-copied
{
position
:
absolute
;
right
:
10px
;
top
:
50%
;
color
:
#047857
;
font-size
:
12px
;
font-size
:
12px
;
font-weight
:
700
;
font-weight
:
700
;
line-height
:
1.5
;
text-align
:
right
;
transform
:
translateY
(
-50%
);
white-space
:
nowrap
;
}
}
@keyframes
task-status-pulse
{
@keyframes
task-status-dot-bounce
{
0
%,
100
%
{
opacity
:
0.42
;
}
0
%,
80
%,
100
%
{
50
%
{
opacity
:
1
;
}
opacity
:
0.42
;
transform
:
translateY
(
0
);
}
40
%
{
opacity
:
1
;
transform
:
translateY
(
-3px
);
}
}
}
@media
(
max-width
:
980px
)
{
@media
(
max-width
:
980px
)
{
.task-panel-
row
{
.task-panel-
scroll
{
grid-template-columns
:
1
fr
;
overflow-x
:
auto
;
}
}
}
}
...
@@ -205,6 +560,6 @@
...
@@ -205,6 +560,6 @@
}
}
.task-panel-date-field
{
.task-panel-date-field
{
width
:
100%
;
width
:
fit-content
;
}
}
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment