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
286f1972
Commit
286f1972
authored
May 14, 2026
by
edy
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix(ui): refine task panel layout
parent
6acc6e51
Changes
2
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
381 additions
and
121 deletions
+381
-121
TaskPanelView.tsx
apps/ui/src/features/tasks/TaskPanelView.tsx
+107
-9
tasks.css
apps/ui/src/styles/tasks.css
+274
-112
No files found.
apps/ui/src/features/tasks/TaskPanelView.tsx
View file @
286f1972
import
{
useEffect
,
useMemo
,
useRef
,
useState
}
from
"react"
import
{
useCallback
,
useEffect
,
useMemo
,
useRef
,
useState
}
from
"react"
import
{
createPortal
}
from
"react-dom"
import
type
{
TaskPanelArtifact
,
TaskPanelItem
,
TaskPanelStatus
}
from
"@qjclaw/shared-types"
import
type
{
TaskPanelArtifact
,
TaskPanelItem
,
TaskPanelStatus
}
from
"@qjclaw/shared-types"
import
{
renderExpertIcon
}
from
"../../components/icons/AppIcons"
import
{
renderExpertIcon
}
from
"../../components/icons/AppIcons"
import
{
Panel
}
from
"../../components/ui/Panel"
import
{
Panel
}
from
"../../components/ui/Panel"
...
@@ -114,7 +115,10 @@ function TaskArtifactList({
...
@@ -114,7 +115,10 @@ function TaskArtifactList({
function
TaskArtifacts
({
item
}:
{
item
:
TaskPanelItem
})
{
function
TaskArtifacts
({
item
}:
{
item
:
TaskPanelItem
})
{
const
[
copiedArtifactId
,
setCopiedArtifactId
]
=
useState
(
""
)
const
[
copiedArtifactId
,
setCopiedArtifactId
]
=
useState
(
""
)
const
[
isMenuOpen
,
setIsMenuOpen
]
=
useState
(
false
)
const
[
isMenuOpen
,
setIsMenuOpen
]
=
useState
(
false
)
const
[
popoverPosition
,
setPopoverPosition
]
=
useState
<
{
top
:
number
;
left
:
number
;
width
:
number
;
maxHeight
:
number
}
|
null
>
(
null
)
const
menuRef
=
useRef
<
HTMLDivElement
|
null
>
(
null
)
const
menuRef
=
useRef
<
HTMLDivElement
|
null
>
(
null
)
const
triggerRef
=
useRef
<
HTMLButtonElement
|
null
>
(
null
)
const
popoverRef
=
useRef
<
HTMLDivElement
|
null
>
(
null
)
const
copiedTimerRef
=
useRef
<
number
|
null
>
(
null
)
const
copiedTimerRef
=
useRef
<
number
|
null
>
(
null
)
useEffect
(()
=>
{
useEffect
(()
=>
{
...
@@ -125,6 +129,27 @@ function TaskArtifacts({ item }: { item: TaskPanelItem }) {
...
@@ -125,6 +129,27 @@ function TaskArtifacts({ item }: { item: TaskPanelItem }) {
}
}
},
[])
},
[])
const
updatePopoverPosition
=
useCallback
(()
=>
{
const
triggerElement
=
triggerRef
.
current
if
(
!
triggerElement
)
{
return
}
const
margin
=
16
const
gap
=
8
const
triggerRect
=
triggerElement
.
getBoundingClientRect
()
const
width
=
Math
.
min
(
520
,
Math
.
max
(
280
,
window
.
innerWidth
-
margin
*
2
))
const
left
=
Math
.
min
(
Math
.
max
(
triggerRect
.
left
,
margin
),
window
.
innerWidth
-
width
-
margin
)
const
belowTop
=
triggerRect
.
bottom
+
gap
const
spaceBelow
=
window
.
innerHeight
-
belowTop
-
margin
const
spaceAbove
=
triggerRect
.
top
-
margin
-
gap
const
preferBelow
=
spaceBelow
>=
160
||
spaceBelow
>=
spaceAbove
const
maxHeight
=
Math
.
min
(
220
,
Math
.
max
(
140
,
preferBelow
?
spaceBelow
:
spaceAbove
))
const
top
=
preferBelow
?
belowTop
:
Math
.
max
(
margin
,
triggerRect
.
top
-
gap
-
maxHeight
)
setPopoverPosition
({
top
,
left
,
width
,
maxHeight
})
},
[])
useEffect
(()
=>
{
useEffect
(()
=>
{
setIsMenuOpen
(
false
)
setIsMenuOpen
(
false
)
setCopiedArtifactId
(
""
)
setCopiedArtifactId
(
""
)
...
@@ -137,7 +162,12 @@ function TaskArtifacts({ item }: { item: TaskPanelItem }) {
...
@@ -137,7 +162,12 @@ function TaskArtifacts({ item }: { item: TaskPanelItem }) {
const
handlePointerDown
=
(
event
:
PointerEvent
)
=>
{
const
handlePointerDown
=
(
event
:
PointerEvent
)
=>
{
const
menuElement
=
menuRef
.
current
const
menuElement
=
menuRef
.
current
if
(
menuElement
&&
event
.
target
instanceof
Node
&&
!
menuElement
.
contains
(
event
.
target
))
{
const
popoverElement
=
popoverRef
.
current
if
(
event
.
target
instanceof
Node
&&
!
menuElement
?.
contains
(
event
.
target
)
&&
!
popoverElement
?.
contains
(
event
.
target
)
)
{
setIsMenuOpen
(
false
)
setIsMenuOpen
(
false
)
}
}
}
}
...
@@ -157,6 +187,21 @@ function TaskArtifacts({ item }: { item: TaskPanelItem }) {
...
@@ -157,6 +187,21 @@ function TaskArtifacts({ item }: { item: TaskPanelItem }) {
}
}
},
[
isMenuOpen
])
},
[
isMenuOpen
])
useEffect
(()
=>
{
if
(
!
isMenuOpen
)
{
return
}
updatePopoverPosition
()
window
.
addEventListener
(
"resize"
,
updatePopoverPosition
)
window
.
addEventListener
(
"scroll"
,
updatePopoverPosition
,
true
)
return
()
=>
{
window
.
removeEventListener
(
"resize"
,
updatePopoverPosition
)
window
.
removeEventListener
(
"scroll"
,
updatePopoverPosition
,
true
)
}
},
[
isMenuOpen
,
updatePopoverPosition
])
const
copyArtifactUrl
=
async
(
artifactId
:
string
,
artifactUrl
:
string
)
=>
{
const
copyArtifactUrl
=
async
(
artifactId
:
string
,
artifactUrl
:
string
)
=>
{
try
{
try
{
await
navigator
.
clipboard
.
writeText
(
artifactUrl
)
await
navigator
.
clipboard
.
writeText
(
artifactUrl
)
...
@@ -196,24 +241,57 @@ function TaskArtifacts({ item }: { item: TaskPanelItem }) {
...
@@ -196,24 +241,57 @@ function TaskArtifacts({ item }: { item: TaskPanelItem }) {
return
(
return
(
<
div
className=
"task-panel-artifact-menu"
ref=
{
menuRef
}
>
<
div
className=
"task-panel-artifact-menu"
ref=
{
menuRef
}
>
<
button
<
button
ref=
{
triggerRef
}
type=
"button"
type=
"button"
className=
"task-panel-artifact-trigger"
className=
"task-panel-artifact-trigger"
aria
-
haspopup=
"menu"
aria
-
haspopup=
"menu"
aria
-
expanded=
{
isMenuOpen
}
aria
-
expanded=
{
isMenuOpen
}
aria
-
controls=
{
item
.
id
+
"-artifact-menu"
}
aria
-
controls=
{
item
.
id
+
"-artifact-menu"
}
onClick=
{
()
=>
setIsMenuOpen
((
current
)
=>
!
current
)
}
onClick=
{
()
=>
{
if
(
!
isMenuOpen
)
{
updatePopoverPosition
()
}
setIsMenuOpen
((
current
)
=>
!
current
)
}
}
>
>
{
item
.
artifacts
.
length
}
个产物
{
item
.
artifacts
.
length
}
个产物
</
button
>
</
button
>
{
isMenuOpen
?
(
{
isMenuOpen
&&
popoverPosition
?
createPortal
((
<
div
className=
"task-panel-artifact-popover"
id=
{
item
.
id
+
"-artifact-menu"
}
role=
"menu"
>
<
div
ref=
{
popoverRef
}
className=
"task-panel-artifact-popover"
id=
{
item
.
id
+
"-artifact-menu"
}
role=
"menu"
style=
{
{
top
:
popoverPosition
.
top
,
left
:
popoverPosition
.
left
,
width
:
popoverPosition
.
width
,
maxHeight
:
popoverPosition
.
maxHeight
}
}
>
<
TaskArtifactList
<
TaskArtifactList
artifacts=
{
item
.
artifacts
}
artifacts=
{
item
.
artifacts
}
copiedArtifactId=
{
copiedArtifactId
}
copiedArtifactId=
{
copiedArtifactId
}
onCopy=
{
(
artifactId
,
artifactUrl
)
=>
void
copyArtifactUrl
(
artifactId
,
artifactUrl
)
}
onCopy=
{
(
artifactId
,
artifactUrl
)
=>
void
copyArtifactUrl
(
artifactId
,
artifactUrl
)
}
/>
/>
</
div
>
</
div
>
)
:
null
}
),
document
.
body
)
:
null
}
</
div
>
)
}
function
TaskPanelLoadingState
()
{
return
(
<
div
className=
"task-panel-loading-state"
role=
"status"
aria
-
label=
"任务列表加载中"
>
{
Array
.
from
({
length
:
4
},
(
_
,
index
)
=>
(
<
div
className=
"task-panel-loading-row"
key=
{
index
}
aria
-
hidden=
"true"
>
<
span
className=
"task-panel-loading-avatar"
/>
<
span
className=
"task-panel-loading-line task-panel-loading-line-name"
/>
<
span
className=
"task-panel-loading-line task-panel-loading-line-task"
/>
<
span
className=
"task-panel-loading-pill"
/>
<
span
className=
"task-panel-loading-line task-panel-loading-line-artifact"
/>
</
div
>
))
}
</
div
>
</
div
>
)
)
}
}
...
@@ -224,8 +302,27 @@ export function TaskPanelView() {
...
@@ -224,8 +302,27 @@ export function TaskPanelView() {
const
[
selectedTaskIds
,
setSelectedTaskIds
]
=
useState
<
Record
<
string
,
string
>>
({})
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
[
greetingText
,
setGreetingText
]
=
useState
(
""
)
const
dateInputRef
=
useRef
<
HTMLInputElement
|
null
>
(
null
)
const
dateInputRef
=
useRef
<
HTMLInputElement
|
null
>
(
null
)
useEffect
(()
=>
{
const
greeting
=
"Hi,今日任务请查收~"
let
index
=
0
setGreetingText
(
""
)
const
timer
=
window
.
setInterval
(()
=>
{
index
+=
1
setGreetingText
(
greeting
.
slice
(
0
,
index
))
if
(
index
>=
greeting
.
length
)
{
window
.
clearInterval
(
timer
)
}
},
42
)
return
()
=>
{
window
.
clearInterval
(
timer
)
}
},
[])
useEffect
(()
=>
{
useEffect
(()
=>
{
let
active
=
true
let
active
=
true
setLoading
(
true
)
setLoading
(
true
)
...
@@ -278,7 +375,8 @@ export function TaskPanelView() {
...
@@ -278,7 +375,8 @@ export function TaskPanelView() {
<
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
><
span
>
任务面板
</
span
></
h1
>
<
h1
className=
"task-panel-heading"
>
任务面板
</
h1
>
<
p
className=
"task-panel-greeting"
aria
-
label=
"任务面板问候语"
>
{
greetingText
}
</
p
>
</
div
>
</
div
>
<
div
className=
"task-panel-date-field"
>
<
div
className=
"task-panel-date-field"
>
<
button
<
button
...
@@ -315,7 +413,7 @@ export function TaskPanelView() {
...
@@ -315,7 +413,7 @@ export function TaskPanelView() {
</
div
>
</
div
>
<
ScrollArea
className=
"scroll-panel task-panel-scroll"
aria
-
busy=
{
loading
}
>
<
ScrollArea
className=
"scroll-panel task-panel-scroll"
aria
-
busy=
{
loading
}
>
{
loading
?
<
div
className=
"empty-state task-panel-state"
>
任务列表加载中...
</
div
>
:
null
}
{
loading
?
<
TaskPanelLoadingState
/
>
:
null
}
{
!
loading
&&
errorText
?
<
div
className=
"notice error task-panel-state"
role=
"alert"
>
{
errorText
}
</
div
>
:
null
}
{
!
loading
&&
errorText
?
<
div
className=
"notice error task-panel-state"
role=
"alert"
>
{
errorText
}
</
div
>
:
null
}
{
!
loading
&&
!
errorText
&&
!
items
.
length
?
<
div
className=
"empty-state task-panel-state"
>
当天暂无任务
</
div
>
:
null
}
{
!
loading
&&
!
errorText
&&
!
items
.
length
?
<
div
className=
"empty-state task-panel-state"
>
当天暂无任务
</
div
>
:
null
}
{
!
loading
&&
!
errorText
&&
items
.
length
?
(
{
!
loading
&&
!
errorText
&&
items
.
length
?
(
...
@@ -348,7 +446,7 @@ export function TaskPanelView() {
...
@@ -348,7 +446,7 @@ export function TaskPanelView() {
}
}
}
}
>
>
{
row
.
tasks
.
map
((
task
)
=>
(
{
row
.
tasks
.
map
((
task
)
=>
(
<
option
key=
{
task
.
id
}
value=
{
task
.
id
}
>
{
task
.
taskTitle
}
</
option
>
<
option
key=
{
task
.
id
}
title=
{
task
.
taskTitle
}
value=
{
task
.
id
}
>
{
task
.
taskTitle
}
</
option
>
))
}
))
}
</
select
>
</
select
>
</
div
>
</
div
>
...
...
apps/ui/src/styles/tasks.css
View file @
286f1972
This diff is collapsed.
Click to expand it.
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