Commit 286f1972 authored by edy's avatar edy

fix(ui): refine task panel layout

parent 6acc6e51
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>
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment