chore: rebuild CentOS 7 release package
This commit is contained in:
@@ -13,7 +13,8 @@
|
||||
const PROJECT_COLLAPSE_STORAGE_KEY = 'cc-web-collapsed-projects';
|
||||
const SIDEBAR_COLLAPSE_STORAGE_KEY = 'cc-web-sidebar-collapsed';
|
||||
const SIDEBAR_DRAWER_MEDIA_QUERY = '(max-width: 768px)';
|
||||
const SIDEBAR_DESKTOP_COLLAPSE_MEDIA_QUERY = '(width > 768px) and (hover: hover) and (pointer: fine)';
|
||||
const SIDEBAR_DESKTOP_COLLAPSE_MEDIA_QUERY = '(width > 768px)';
|
||||
const SIDEBAR_HOVER_PREVIEW_MEDIA_QUERY = '(width > 768px) and (hover: hover) and (pointer: fine)';
|
||||
const CROSS_CONVERSATION_REPLY_COLLAPSE_STORAGE_KEY = 'cc-web-collapsed-cross-replies';
|
||||
const CROSS_CONVERSATION_REPLY_COLLAPSE_LIMIT = 500;
|
||||
const ASSISTANT_LAST_SECTION_BUTTON_CLASS = 'msg-last-section-btn';
|
||||
@@ -65,6 +66,13 @@
|
||||
return SLASH_COMMANDS.some((item) => item.cmd.toLowerCase() === normalized);
|
||||
}
|
||||
|
||||
function isGoalSetCommandText(text) {
|
||||
const match = /^\s*\/goal(?:\s+([\s\S]*))?$/i.exec(String(text || ''));
|
||||
if (!match) return false;
|
||||
const rest = String(match[1] || '').trim().toLowerCase();
|
||||
return !!rest && !['clear', 'pause', 'resume'].includes(rest);
|
||||
}
|
||||
|
||||
const MODE_LABELS = {
|
||||
default: '默认',
|
||||
plan: 'Plan',
|
||||
@@ -345,6 +353,7 @@
|
||||
const mobileInputMedia = window.matchMedia('(max-width: 768px), (pointer: coarse)');
|
||||
const sidebarDrawerMedia = window.matchMedia(SIDEBAR_DRAWER_MEDIA_QUERY);
|
||||
const sidebarDesktopCollapseMedia = window.matchMedia(SIDEBAR_DESKTOP_COLLAPSE_MEDIA_QUERY);
|
||||
const sidebarHoverPreviewMedia = window.matchMedia(SIDEBAR_HOVER_PREVIEW_MEDIA_QUERY);
|
||||
const chatMain = document.querySelector('.chat-main');
|
||||
const newChatSplit = sidebar.querySelector('.new-chat-split');
|
||||
const newChatBtn = $('#new-chat-btn');
|
||||
@@ -11247,8 +11256,17 @@
|
||||
return;
|
||||
}
|
||||
const requestId = createLocalId('slash');
|
||||
const clientMessageId = isGoalSetCommandText(text) ? createLocalId('user') : '';
|
||||
rememberPendingSlashDraft(requestId, text, currentSessionId, currentAgent);
|
||||
send({ type: 'message', text, sessionId: currentSessionId, mode: currentMode, agent: currentAgent, requestId });
|
||||
send({
|
||||
type: 'message',
|
||||
text,
|
||||
sessionId: currentSessionId,
|
||||
mode: currentMode,
|
||||
agent: currentAgent,
|
||||
requestId,
|
||||
...(clientMessageId ? { clientMessageId } : {}),
|
||||
});
|
||||
msgInput.value = '';
|
||||
autoResize();
|
||||
return;
|
||||
@@ -11310,7 +11328,7 @@
|
||||
});
|
||||
|
||||
sidebarOverlay.addEventListener('click', closeSidebar);
|
||||
[sidebarDrawerMedia, sidebarDesktopCollapseMedia].forEach((media) => {
|
||||
[sidebarDrawerMedia, sidebarDesktopCollapseMedia, sidebarHoverPreviewMedia].forEach((media) => {
|
||||
if (typeof media.addEventListener === 'function') {
|
||||
media.addEventListener('change', handleSidebarInputModeChange);
|
||||
} else if (typeof media.addListener === 'function') {
|
||||
|
||||
@@ -9925,7 +9925,7 @@ html[data-theme='gilded'] {
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@media (width > 768px) and (hover: hover) and (pointer: fine) {
|
||||
@media (width > 768px) {
|
||||
.menu-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -9951,11 +9951,6 @@ html[data-theme='gilded'] {
|
||||
box-shadow 180ms ease;
|
||||
}
|
||||
|
||||
.app.sidebar-collapsed .sidebar:is(:hover, :focus-within) {
|
||||
transform: translateX(0);
|
||||
box-shadow: var(--sidebar-hover-shadow);
|
||||
}
|
||||
|
||||
/* 收起后控制条会贴近左侧轨道,定位浮层需朝聊天画布展开,避免越过应用边界被裁切。 */
|
||||
.app.sidebar-collapsed .user-outline-panel {
|
||||
left: 0;
|
||||
@@ -9981,10 +9976,6 @@ html[data-theme='gilded'] {
|
||||
transition: opacity 120ms ease;
|
||||
}
|
||||
|
||||
.app.sidebar-collapsed .sidebar:is(:hover, :focus-within)::after {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
html[data-theme='gilded'] .app.sidebar-collapsed .sidebar::after {
|
||||
border-left-color: rgba(138, 97, 37, 0.34);
|
||||
background:
|
||||
@@ -10002,6 +9993,17 @@ html[data-theme='gilded'] {
|
||||
}
|
||||
}
|
||||
|
||||
@media (width > 768px) and (hover: hover) and (pointer: fine) {
|
||||
.app.sidebar-collapsed .sidebar:is(:hover, :focus-within) {
|
||||
transform: translateX(0);
|
||||
box-shadow: var(--sidebar-hover-shadow);
|
||||
}
|
||||
|
||||
.app.sidebar-collapsed .sidebar:is(:hover, :focus-within)::after {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.app.sidebar-collapsed .sidebar,
|
||||
.app.sidebar-collapsed .sidebar::after {
|
||||
|
||||
@@ -222,6 +222,14 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ccweb-task-board__clock {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ccweb-task-board__wall {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ccweb-task-board__lanes {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
@@ -1171,6 +1179,646 @@ html[data-theme='wasteland'] .ccweb-task-board__card[data-runtime-state='running
|
||||
}
|
||||
}
|
||||
|
||||
/* 横屏常亮模式:看板覆盖完整网页视口,宿主聊天界面不再参与排版。 */
|
||||
@media (orientation: landscape) and (max-height: 768px) {
|
||||
body:has(.task-board-panel:not([hidden])) .menu-btn {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.task-board-panel {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 400;
|
||||
display: block;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.task-board-panel__root {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.task-board-panel__header {
|
||||
position: absolute;
|
||||
top: calc(env(safe-area-inset-top, 0px) + 0.3rem);
|
||||
right: calc(env(safe-area-inset-right, 0px) + 0.35rem);
|
||||
z-index: 50;
|
||||
display: block;
|
||||
width: 1.9rem;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.task-board-panel__header .usage-dashboard__icon-button {
|
||||
width: 1.9rem;
|
||||
min-width: 1.9rem;
|
||||
height: 1.9rem;
|
||||
padding: 0;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 0.5rem;
|
||||
color: var(--text-secondary);
|
||||
background: var(--bg-tertiary);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.ccweb-task-board {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
width: auto;
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.ccweb-task-board__toolbar {
|
||||
display: none;
|
||||
grid-template-columns: minmax(0, auto) minmax(5.2rem, 1fr) auto;
|
||||
gap: 0.45rem;
|
||||
min-height: 2.3rem;
|
||||
padding:
|
||||
calc(env(safe-area-inset-top, 0px) + 0.22rem)
|
||||
calc(env(safe-area-inset-right, 0px) + 2.4rem)
|
||||
0.22rem
|
||||
calc(env(safe-area-inset-left, 0px) + 0.42rem);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.ccweb-task-board__heading {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.38rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ccweb-task-board__title {
|
||||
flex: 0 0 auto;
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: -0.015em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ccweb-task-board__summary {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ccweb-task-board__clock {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: clamp(0.65rem, 2.2vw, 1.2rem);
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
gap: clamp(0.35rem, 1.4vh, 0.65rem);
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.ccweb-task-board__clock-time {
|
||||
color: var(--text-primary);
|
||||
font-size: clamp(2.25rem, 6.4vw, 4.6rem);
|
||||
font-weight: 680;
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: -0.07em;
|
||||
line-height: 0.9;
|
||||
}
|
||||
|
||||
.ccweb-task-board__clock-date {
|
||||
color: var(--text-secondary);
|
||||
font-size: clamp(0.6rem, 1.2vw, 0.78rem);
|
||||
font-weight: 560;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
.ccweb-task-board__controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.28rem;
|
||||
}
|
||||
|
||||
.ccweb-task-board__search {
|
||||
flex: 0 1 min(9rem, 30vw);
|
||||
width: min(9rem, 30vw);
|
||||
transition: flex-basis 160ms ease, width 160ms ease;
|
||||
}
|
||||
|
||||
.ccweb-task-board__search:not(:focus-within) {
|
||||
flex-basis: 1.9rem;
|
||||
width: 1.9rem;
|
||||
}
|
||||
|
||||
.ccweb-task-board[data-search-active='true'] .ccweb-task-board__search,
|
||||
.ccweb-task-board__search:focus-within {
|
||||
flex-basis: min(9rem, 30vw);
|
||||
width: min(9rem, 30vw);
|
||||
}
|
||||
|
||||
.ccweb-task-board__search-glyph {
|
||||
left: 0.55rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.ccweb-task-board__input {
|
||||
height: 1.75rem;
|
||||
padding: 0 0.5rem 0 1.65rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.ccweb-task-board[data-search-active='false'] .ccweb-task-board__search:not(:focus-within) .ccweb-task-board__input {
|
||||
padding-right: 0;
|
||||
color: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ccweb-task-board[data-search-active='false'] .ccweb-task-board__search:not(:focus-within) .ccweb-task-board__input::placeholder {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.ccweb-task-board__button {
|
||||
min-height: 1.75rem;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 0.42rem;
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
.ccweb-task-board__workspace {
|
||||
display: block;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ccweb-task-board__wall {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding:
|
||||
env(safe-area-inset-top, 0px)
|
||||
env(safe-area-inset-right, 0px)
|
||||
env(safe-area-inset-bottom, 0px)
|
||||
env(safe-area-inset-left, 0px);
|
||||
overflow: hidden;
|
||||
background: var(--border-color);
|
||||
}
|
||||
|
||||
.ccweb-task-board__wall-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--task-board-wall-columns), minmax(0, 1fr));
|
||||
grid-template-rows: repeat(var(--task-board-wall-rows), minmax(0, 1fr));
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
gap: 1px;
|
||||
background: var(--border-color);
|
||||
}
|
||||
|
||||
.ccweb-task-board__wall-column {
|
||||
--task-board-status-color: var(--text-muted);
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.ccweb-task-board__wall-column-header,
|
||||
.ccweb-task-board__wall-column-identity,
|
||||
.ccweb-task-board__wall-card-meta,
|
||||
.ccweb-task-board__wall-card-runtime {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ccweb-task-board__wall-column-header {
|
||||
min-width: 0;
|
||||
min-height: clamp(2rem, 6.8vh, 2.55rem);
|
||||
padding: clamp(0.35rem, 1.2vh, 0.58rem) clamp(0.5rem, 1.4vw, 0.82rem);
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.ccweb-task-board__wall-column-identity {
|
||||
min-width: 0;
|
||||
gap: clamp(0.34rem, 0.8vw, 0.52rem);
|
||||
}
|
||||
|
||||
.ccweb-task-board__wall-column-dot {
|
||||
display: block;
|
||||
flex: 0 0 auto;
|
||||
width: clamp(0.38rem, 0.8vw, 0.5rem);
|
||||
height: clamp(0.38rem, 0.8vw, 0.5rem);
|
||||
border-radius: 50%;
|
||||
background: var(--task-board-status-color);
|
||||
box-shadow: 0 0 0 0.12rem color-mix(in srgb, var(--task-board-status-color) 18%, transparent);
|
||||
}
|
||||
|
||||
.ccweb-task-board__wall-column-title {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
color: var(--text-primary);
|
||||
font-size: clamp(0.7rem, 1.5vw, 0.98rem);
|
||||
font-weight: 680;
|
||||
letter-spacing: -0.015em;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ccweb-task-board__wall-column-count {
|
||||
flex: 0 0 auto;
|
||||
color: var(--text-secondary);
|
||||
font-size: clamp(0.6rem, 1.15vw, 0.76rem);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.ccweb-task-board__wall-column-page {
|
||||
flex: 0 0 auto;
|
||||
color: var(--text-muted);
|
||||
font-size: clamp(0.55rem, 1vw, 0.68rem);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.ccweb-task-board__wall-column-body {
|
||||
display: grid;
|
||||
grid-auto-rows: minmax(0, 1fr);
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: clamp(0.35rem, 1.1vw, 0.65rem);
|
||||
overflow: hidden;
|
||||
gap: clamp(0.28rem, 0.8vw, 0.5rem);
|
||||
}
|
||||
|
||||
.ccweb-task-board__wall-card {
|
||||
--task-board-status-color: var(--text-muted);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: clamp(0.4rem, 1.15vw, 0.72rem) clamp(0.45rem, 1.25vw, 0.78rem);
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
gap: clamp(0.22rem, 0.8vh, 0.42rem);
|
||||
border: 1px solid var(--border-color);
|
||||
border-left: 0.16rem solid var(--task-board-status-color);
|
||||
border-radius: clamp(0.35rem, 0.8vw, 0.55rem);
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-secondary);
|
||||
transform-origin: 0 50%;
|
||||
will-change: opacity, transform, clip-path;
|
||||
}
|
||||
|
||||
.ccweb-task-board__wall-card-title {
|
||||
margin: 0;
|
||||
color: var(--text-primary);
|
||||
font-size: clamp(0.68rem, 1.45vw, 1rem);
|
||||
font-weight: 690;
|
||||
letter-spacing: -0.018em;
|
||||
line-height: 1.28;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.ccweb-task-board__wall-card-summary {
|
||||
display: block;
|
||||
margin: 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: clamp(0.56rem, 1.12vw, 0.76rem);
|
||||
line-height: 1.38;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.ccweb-task-board__wall-card-meta {
|
||||
margin-top: auto;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 0.3rem 0.6rem;
|
||||
color: var(--text-muted);
|
||||
font-size: clamp(0.52rem, 1vw, 0.68rem);
|
||||
}
|
||||
|
||||
.ccweb-task-board__wall-card-runtime {
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.ccweb-task-board__wall-card-runtime-dot {
|
||||
width: 0.36rem;
|
||||
height: 0.36rem;
|
||||
border-radius: 50%;
|
||||
background: var(--text-muted);
|
||||
}
|
||||
|
||||
.ccweb-task-board__wall-card-runtime[data-running='true'] {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.ccweb-task-board__wall-card-runtime[data-running='true'] .ccweb-task-board__wall-card-runtime-dot {
|
||||
background: currentColor;
|
||||
animation: ccweb-task-board-pulse 850ms ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.ccweb-task-board__wall-card-updated {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.ccweb-task-board__wall-column-empty,
|
||||
.ccweb-task-board__wall-empty {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.38rem;
|
||||
color: var(--text-muted);
|
||||
font-size: clamp(0.58rem, 1.15vw, 0.76rem);
|
||||
}
|
||||
|
||||
.ccweb-task-board__wall-empty {
|
||||
flex-direction: column;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.ccweb-task-board__wall-column-empty-mark,
|
||||
.ccweb-task-board__wall-empty-mark {
|
||||
color: var(--text-muted);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.ccweb-task-board__lanes {
|
||||
display: none;
|
||||
grid-auto-flow: row;
|
||||
grid-auto-columns: auto;
|
||||
grid-auto-rows: auto;
|
||||
align-items: stretch;
|
||||
gap: 0;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
overflow-y: hidden;
|
||||
scroll-snap-type: none;
|
||||
}
|
||||
|
||||
.ccweb-task-board__lanes[data-column-layout='fit'] {
|
||||
grid-template-columns: repeat(var(--task-board-column-count), minmax(0, 1fr));
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.ccweb-task-board__lanes[data-column-layout='scroll'] {
|
||||
grid-template-columns: repeat(var(--task-board-column-count), minmax(10rem, 1fr));
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.ccweb-task-board__column {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
border-left: 1px solid var(--border-color);
|
||||
border-radius: 0;
|
||||
scroll-snap-align: none;
|
||||
}
|
||||
|
||||
.ccweb-task-board__column:last-child {
|
||||
border-right: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.ccweb-task-board__column-header {
|
||||
position: static;
|
||||
flex: 0 0 auto;
|
||||
min-height: 1.72rem;
|
||||
gap: 0.3rem;
|
||||
padding: 0.27rem 0.36rem;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.ccweb-task-board__column-title-wrap {
|
||||
gap: 0.32rem;
|
||||
}
|
||||
|
||||
.ccweb-task-board__column-dot {
|
||||
width: 0.4rem;
|
||||
height: 0.4rem;
|
||||
box-shadow: 0 0 0 0.12rem color-mix(in srgb, var(--task-board-status-color) 18%, transparent);
|
||||
}
|
||||
|
||||
.ccweb-task-board__column-title {
|
||||
font-size: clamp(0.6rem, 1.08vw, 0.72rem);
|
||||
}
|
||||
|
||||
.ccweb-task-board__column-count {
|
||||
min-width: 1.15rem;
|
||||
height: 1.08rem;
|
||||
padding: 0 0.25rem;
|
||||
font-size: 0.56rem;
|
||||
}
|
||||
|
||||
.ccweb-task-board__column-disabled,
|
||||
.ccweb-task-board__column-empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ccweb-task-board__column-body {
|
||||
flex: 1 1 auto;
|
||||
gap: 0.28rem;
|
||||
min-height: 0;
|
||||
padding: 0.3rem;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior-y: contain;
|
||||
scrollbar-width: thin;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.ccweb-task-board__card {
|
||||
flex: 0 0 auto;
|
||||
gap: 0.24rem;
|
||||
padding: 0.34rem;
|
||||
border-radius: 0.38rem;
|
||||
}
|
||||
|
||||
.ccweb-task-board__card:hover,
|
||||
.ccweb-task-board__card:focus-within {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.ccweb-task-board__card-top,
|
||||
.ccweb-task-board__card-summary,
|
||||
.ccweb-task-board__metadata {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ccweb-task-board__card-title {
|
||||
font-size: clamp(0.61rem, 1.05vw, 0.72rem);
|
||||
line-height: 1.26;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.ccweb-task-board__card[data-runtime-state='idle'] .ccweb-task-board__state-row {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ccweb-task-board__state-row {
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.ccweb-task-board__runtime-state {
|
||||
min-height: 1.05rem;
|
||||
padding: 0.08rem 0.23rem;
|
||||
border-radius: 0.3rem;
|
||||
font-size: 0.52rem;
|
||||
}
|
||||
|
||||
.ccweb-task-board__card-footer {
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
padding-top: 0.22rem;
|
||||
}
|
||||
|
||||
.ccweb-task-board__card-actions {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
width: 100%;
|
||||
gap: 0.22rem;
|
||||
}
|
||||
|
||||
.ccweb-task-board__status-select-wrap {
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ccweb-task-board__card-select {
|
||||
height: 1.36rem;
|
||||
padding: 0 1rem 0 0.32rem;
|
||||
border-radius: 0.35rem;
|
||||
font-size: 0.52rem;
|
||||
}
|
||||
|
||||
.ccweb-task-board__card-action {
|
||||
min-height: 1.36rem;
|
||||
padding: 0.15rem 0.34rem;
|
||||
border-radius: 0.35rem;
|
||||
font-size: 0.52rem;
|
||||
}
|
||||
|
||||
.ccweb-task-board__empty {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.ccweb-task-board__status-layer {
|
||||
display: none !important;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(19rem, 70vw);
|
||||
}
|
||||
|
||||
.ccweb-task-board__status-header {
|
||||
padding: 0.55rem 0.7rem;
|
||||
}
|
||||
|
||||
.ccweb-task-board__status-body {
|
||||
padding: 0.55rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (orientation: landscape) and (max-height: 430px) {
|
||||
.ccweb-task-board__toolbar {
|
||||
min-height: 2.15rem;
|
||||
padding-top: calc(env(safe-area-inset-top, 0px) + 0.2rem);
|
||||
padding-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.ccweb-task-board__title {
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.ccweb-task-board__summary {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ccweb-task-board__clock-time {
|
||||
font-size: clamp(2.25rem, 5.5vw, 3.25rem);
|
||||
}
|
||||
|
||||
.ccweb-task-board__clock-date {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ccweb-task-board__clock {
|
||||
padding: 0.55rem;
|
||||
}
|
||||
|
||||
.ccweb-task-board__wall-column-header {
|
||||
min-height: 1.8rem;
|
||||
padding: 0.28rem 0.48rem;
|
||||
}
|
||||
|
||||
.ccweb-task-board__wall-column-body {
|
||||
padding: 0.28rem;
|
||||
gap: 0.24rem;
|
||||
}
|
||||
|
||||
.ccweb-task-board__wall-card {
|
||||
padding: 0.32rem 0.42rem;
|
||||
gap: 0.18rem;
|
||||
}
|
||||
|
||||
.ccweb-task-board__wall-card-title {
|
||||
font-size: clamp(0.64rem, 1.4vw, 0.82rem);
|
||||
}
|
||||
|
||||
.ccweb-task-board__wall-card-summary {
|
||||
font-size: clamp(0.52rem, 1.05vw, 0.66rem);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.ccweb-task-board__column-header {
|
||||
min-height: 1.72rem;
|
||||
padding-block: 0.24rem;
|
||||
}
|
||||
|
||||
.ccweb-task-board__column-body {
|
||||
gap: 0.28rem;
|
||||
padding: 0.28rem;
|
||||
}
|
||||
|
||||
.ccweb-task-board__card {
|
||||
gap: 0.24rem;
|
||||
padding: 0.34rem;
|
||||
}
|
||||
|
||||
.ccweb-task-board__card-title {
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html[data-theme='wasteland'] .ccweb-task-board__card[data-runtime-state='running']::after {
|
||||
top: 50%;
|
||||
@@ -1197,11 +1845,17 @@ html[data-theme='wasteland'] .ccweb-task-board__card[data-runtime-state='running
|
||||
.ccweb-task-board__card:focus-within {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.ccweb-task-board__wall-card {
|
||||
will-change: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
.ccweb-task-board__column-dot,
|
||||
.ccweb-task-board__definition-dot {
|
||||
.ccweb-task-board__definition-dot,
|
||||
.ccweb-task-board__wall-column-dot,
|
||||
.ccweb-task-board__wall-card-runtime-dot {
|
||||
border: 1px solid CanvasText;
|
||||
}
|
||||
|
||||
|
||||
@@ -265,13 +265,24 @@
|
||||
error: null,
|
||||
destroyed: false,
|
||||
};
|
||||
const fitColumnLimit = Math.min(6, Math.max(1, Math.floor(finiteNumber(options.fitColumnLimit, 6))));
|
||||
const wallRotationMs = Math.max(20, finiteNumber(options.wallRotationMs, 12000));
|
||||
const wallUpdateHoldMs = Math.max(wallRotationMs, finiteNumber(options.wallUpdateHoldMs, 20000));
|
||||
const wallCardsPerPage = Math.min(4, Math.max(1, Math.floor(finiteNumber(options.wallCardsPerPage, 2))));
|
||||
|
||||
let searchTimer = null;
|
||||
let clockTimer = null;
|
||||
let wallRotationTimer = null;
|
||||
let wallHasRendered = false;
|
||||
let wallRotationCursor = 0;
|
||||
let previousFocus = null;
|
||||
let latestQueryRequestId = '';
|
||||
let hasRenderedBoard = false;
|
||||
const columnRecords = new Map();
|
||||
const cardRecords = new Map();
|
||||
const wallColumnRecords = new Map();
|
||||
const wallCardRecords = new Map();
|
||||
let wallVisibleSessionIds = new Set();
|
||||
let boardEmpty = null;
|
||||
|
||||
const shell = createNode(doc, 'section', 'ccweb-task-board');
|
||||
@@ -285,6 +296,12 @@
|
||||
summary.setAttribute('aria-live', 'polite');
|
||||
headingGroup.append(title, summary);
|
||||
|
||||
const clock = createNode(doc, 'aside', 'ccweb-task-board__clock');
|
||||
clock.setAttribute('aria-label', '当前时间');
|
||||
const clockTime = createNode(doc, 'time', 'ccweb-task-board__clock-time');
|
||||
const clockDate = createNode(doc, 'span', 'ccweb-task-board__clock-date');
|
||||
clock.append(clockTime, clockDate);
|
||||
|
||||
const controls = createNode(doc, 'div', 'ccweb-task-board__controls');
|
||||
controls.setAttribute('role', 'group');
|
||||
controls.setAttribute('aria-label', '任务看板搜索与操作');
|
||||
@@ -321,7 +338,21 @@
|
||||
const board = createNode(doc, 'div', 'ccweb-task-board__lanes');
|
||||
board.setAttribute('aria-label', '任务状态列');
|
||||
board.tabIndex = 0;
|
||||
workspace.appendChild(board);
|
||||
|
||||
const wall = createNode(doc, 'section', 'ccweb-task-board__wall');
|
||||
wall.setAttribute('aria-label', '横屏任务看板');
|
||||
const wallGrid = createNode(doc, 'div', 'ccweb-task-board__wall-grid');
|
||||
wallGrid.setAttribute('role', 'list');
|
||||
const wallEmpty = createNode(doc, 'section', 'ccweb-task-board__wall-empty');
|
||||
wallEmpty.setAttribute('role', 'status');
|
||||
wallEmpty.append(
|
||||
createNode(doc, 'span', 'ccweb-task-board__wall-empty-mark', '◇'),
|
||||
createNode(doc, 'span', '', '等待状态定义'),
|
||||
);
|
||||
wallGrid.append(clock, wallEmpty);
|
||||
wall.appendChild(wallGrid);
|
||||
|
||||
workspace.append(board, wall);
|
||||
|
||||
const statusLayer = createNode(doc, 'div', 'ccweb-task-board__status-layer');
|
||||
statusLayer.hidden = true;
|
||||
@@ -361,7 +392,13 @@
|
||||
searchInput,
|
||||
archiveButton,
|
||||
statusButton,
|
||||
clock,
|
||||
clockTime,
|
||||
clockDate,
|
||||
board,
|
||||
wall,
|
||||
wallGrid,
|
||||
wallEmpty,
|
||||
statusLayer,
|
||||
statusPanel,
|
||||
statusClose,
|
||||
@@ -374,6 +411,463 @@
|
||||
refs.liveRegion.textContent = text(message);
|
||||
}
|
||||
|
||||
function renderClock() {
|
||||
const now = new Date();
|
||||
const hours = String(now.getHours()).padStart(2, '0');
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||
const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
|
||||
refs.clockTime.textContent = `${hours}:${minutes}`;
|
||||
refs.clockTime.setAttribute('datetime', now.toISOString());
|
||||
refs.clockDate.textContent = `${now.getMonth() + 1}月${now.getDate()}日 · ${weekdays[now.getDay()]}`;
|
||||
}
|
||||
|
||||
function scheduleClock() {
|
||||
renderClock();
|
||||
if (typeof global.setTimeout !== 'function') return;
|
||||
const now = new Date();
|
||||
const delay = Math.max(1000, (60 - now.getSeconds()) * 1000 - now.getMilliseconds() + 20);
|
||||
clockTimer = global.setTimeout(scheduleClock, delay);
|
||||
if (clockTimer && typeof clockTimer.unref === 'function') clockTimer.unref();
|
||||
}
|
||||
|
||||
function wallTaskTimestamp(task) {
|
||||
return Date.parse(task?.updatedAt || '') || 0;
|
||||
}
|
||||
|
||||
function compareWallTasks(left, right) {
|
||||
if (left.isRunning !== right.isRunning) return left.isRunning ? -1 : 1;
|
||||
const timeDifference = wallTaskTimestamp(right) - wallTaskTimestamp(left);
|
||||
if (timeDifference) return timeDifference;
|
||||
if (left.order !== right.order) return left.order - right.order;
|
||||
return left.sessionId.localeCompare(right.sessionId);
|
||||
}
|
||||
|
||||
function getWallTasks() {
|
||||
return state.tasks
|
||||
.filter((task) => task.trackingEnabled && !task.archivedAt)
|
||||
.slice()
|
||||
.sort(compareWallTasks);
|
||||
}
|
||||
|
||||
function wallTaskSignature(task) {
|
||||
if (!task) return '';
|
||||
return JSON.stringify([
|
||||
task.sessionId,
|
||||
task.title,
|
||||
task.statusId,
|
||||
task.summary,
|
||||
task.reason,
|
||||
task.isRunning,
|
||||
task.runtimeState,
|
||||
task.runtimeDetail,
|
||||
task.updatedAt,
|
||||
task.version,
|
||||
]);
|
||||
}
|
||||
|
||||
function clearWallRotation() {
|
||||
if (wallRotationTimer === null) return;
|
||||
global.clearTimeout(wallRotationTimer);
|
||||
wallRotationTimer = null;
|
||||
}
|
||||
|
||||
function wallLayoutSize(statusCount) {
|
||||
const cellCount = Math.max(1, statusCount + 1);
|
||||
const preferredColumns = cellCount <= 4
|
||||
? Math.min(cellCount, 2)
|
||||
: Math.round(Math.sqrt(cellCount * (16 / 9)));
|
||||
const columns = Math.min(cellCount, Math.max(1, preferredColumns));
|
||||
return { cellCount, columns, rows: Math.max(1, Math.ceil(cellCount / columns)) };
|
||||
}
|
||||
|
||||
function wallTasksByStatus(definitions, tasks) {
|
||||
const grouped = new Map(definitions.map((definition) => [definition.id, []]));
|
||||
for (const task of tasks) grouped.get(task.statusId)?.push(task);
|
||||
return grouped;
|
||||
}
|
||||
|
||||
function overflowingWallStatusIds() {
|
||||
const definitions = getBoardDefinitions();
|
||||
const grouped = wallTasksByStatus(definitions, getWallTasks());
|
||||
const pageSize = wallLayoutSize(definitions.length || 1).rows >= 3 ? 1 : wallCardsPerPage;
|
||||
return definitions
|
||||
.filter((definition) => (grouped.get(definition.id)?.length || 0) > pageSize)
|
||||
.map((definition) => definition.id);
|
||||
}
|
||||
|
||||
function scheduleWallRotation(delay = wallRotationMs) {
|
||||
clearWallRotation();
|
||||
if (state.destroyed || prefersReducedBoardMotion() || !overflowingWallStatusIds().length) return;
|
||||
wallRotationTimer = global.setTimeout(() => {
|
||||
wallRotationTimer = null;
|
||||
advanceWallPage();
|
||||
}, delay);
|
||||
if (wallRotationTimer && typeof wallRotationTimer.unref === 'function') wallRotationTimer.unref();
|
||||
}
|
||||
|
||||
function readWallCardRect(card) {
|
||||
if (!card || typeof card.getBoundingClientRect !== 'function' || !refs.wall.contains(card)) return null;
|
||||
const rect = card.getBoundingClientRect();
|
||||
if (!rect || !Number.isFinite(rect.left) || !Number.isFinite(rect.top)) return null;
|
||||
return { left: rect.left, top: rect.top };
|
||||
}
|
||||
|
||||
function cancelWallCardAnimation(record, kind = '') {
|
||||
if (!record?.animations) return;
|
||||
const entries = kind
|
||||
? [[kind, record.animations.get(kind)]]
|
||||
: Array.from(record.animations.entries());
|
||||
for (const [animationKind, animation] of entries) {
|
||||
if (!animation) continue;
|
||||
animation.onfinish = null;
|
||||
animation.oncancel = null;
|
||||
record.animations.delete(animationKind);
|
||||
if (typeof animation.cancel !== 'function') continue;
|
||||
try {
|
||||
animation.cancel();
|
||||
} catch (_) {
|
||||
// 动画对象可能已由浏览器回收;卡片节点仍继续复用。
|
||||
}
|
||||
}
|
||||
if (!record.animations.size) delete record.element.dataset.motion;
|
||||
}
|
||||
|
||||
function runWallCardAnimation(record, kind, keyframes, options, onFinish) {
|
||||
cancelWallCardAnimation(record, kind);
|
||||
const card = record.element;
|
||||
card.dataset.motion = kind;
|
||||
const complete = () => {
|
||||
record.animations.delete(kind);
|
||||
if (!record.animations.size) delete card.dataset.motion;
|
||||
if (typeof onFinish === 'function') onFinish();
|
||||
};
|
||||
if (prefersReducedBoardMotion() || typeof card.animate !== 'function') {
|
||||
complete();
|
||||
return;
|
||||
}
|
||||
let animation;
|
||||
try {
|
||||
animation = card.animate(keyframes, { fill: 'both', ...options });
|
||||
} catch (_) {
|
||||
complete();
|
||||
return;
|
||||
}
|
||||
record.animations.set(kind, animation);
|
||||
animation.onfinish = () => {
|
||||
if (record.animations.get(kind) !== animation) return;
|
||||
animation.onfinish = null;
|
||||
animation.oncancel = null;
|
||||
try {
|
||||
animation.cancel();
|
||||
} catch (_) {
|
||||
// 基础样式接管完成帧;旧浏览器回收动画时无需阻断渲染。
|
||||
}
|
||||
complete();
|
||||
};
|
||||
animation.oncancel = () => {
|
||||
if (record.animations.get(kind) !== animation) return;
|
||||
animation.onfinish = null;
|
||||
animation.oncancel = null;
|
||||
record.animations.delete(kind);
|
||||
if (!record.animations.size) delete card.dataset.motion;
|
||||
};
|
||||
}
|
||||
|
||||
function animateWallCardPageLeave(sessionId, record) {
|
||||
runWallCardAnimation(record, 'page-leaving', [
|
||||
{ opacity: 1, transform: 'translate3d(0, 0, 0) scaleY(1)', clipPath: 'inset(0 0 0 0)' },
|
||||
{ opacity: 0, transform: 'translate3d(-0.7rem, 0, 0) scaleY(0.72)', clipPath: 'inset(0 88% 0 0)' },
|
||||
], {
|
||||
id: 'ccweb-task-board-wall-card-page-leave',
|
||||
duration: 300,
|
||||
easing: 'cubic-bezier(0.55, 0.05, 0.72, 0.35)',
|
||||
}, () => {
|
||||
if (!wallVisibleSessionIds.has(sessionId)) record.element.remove();
|
||||
});
|
||||
}
|
||||
|
||||
function animateWallCardPageEnter(record, index) {
|
||||
runWallCardAnimation(record, 'page-entering', [
|
||||
{ opacity: 0, transform: 'translate3d(0.7rem, 0, 0) scaleY(0.76)', clipPath: 'inset(0 0 100% 0)' },
|
||||
{ opacity: 1, transform: 'translate3d(0, 0, 0) scaleY(1)', clipPath: 'inset(0 0 0 0)' },
|
||||
], {
|
||||
id: 'ccweb-task-board-wall-card-page-enter',
|
||||
duration: 430,
|
||||
delay: Math.min(index, 3) * 55,
|
||||
easing: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
});
|
||||
}
|
||||
|
||||
function animateWallCardMove(record, before, after) {
|
||||
if (!before || !after) return;
|
||||
const deltaX = before.left - after.left;
|
||||
const deltaY = before.top - after.top;
|
||||
if (Math.abs(deltaX) < 0.5 && Math.abs(deltaY) < 0.5) return;
|
||||
runWallCardAnimation(record, 'moving', [
|
||||
{ transform: `translate3d(${deltaX}px, ${deltaY}px, 0)`, zIndex: 4 },
|
||||
{ transform: 'translate3d(0, 0, 0)', zIndex: 1 },
|
||||
], {
|
||||
id: 'ccweb-task-board-wall-card-move',
|
||||
duration: 520,
|
||||
easing: 'cubic-bezier(0.22, 1, 0.36, 1)',
|
||||
});
|
||||
}
|
||||
|
||||
function animateWallCardUpdate(record) {
|
||||
runWallCardAnimation(record, 'updating', [
|
||||
{ boxShadow: 'inset 0.16rem 0 0 var(--task-board-status-color)', backgroundColor: 'var(--bg-secondary)' },
|
||||
{ boxShadow: 'inset 0.3rem 0 0 var(--task-board-status-color), 0 0 0 0.16rem color-mix(in srgb, var(--task-board-status-color) 28%, transparent)', backgroundColor: 'color-mix(in srgb, var(--task-board-status-color) 10%, var(--bg-secondary))', offset: 0.36 },
|
||||
{ boxShadow: 'inset 0.16rem 0 0 var(--task-board-status-color)', backgroundColor: 'var(--bg-secondary)' },
|
||||
], {
|
||||
id: 'ccweb-task-board-wall-card-update',
|
||||
duration: 1050,
|
||||
easing: 'cubic-bezier(0.22, 1, 0.36, 1)',
|
||||
});
|
||||
}
|
||||
|
||||
function createWallColumnRecord(definition, definitionIndex) {
|
||||
const element = createNode(doc, 'section', 'ccweb-task-board__wall-column');
|
||||
element.setAttribute('role', 'listitem');
|
||||
const header = createNode(doc, 'header', 'ccweb-task-board__wall-column-header');
|
||||
const identity = createNode(doc, 'div', 'ccweb-task-board__wall-column-identity');
|
||||
const dot = createNode(doc, 'span', 'ccweb-task-board__wall-column-dot');
|
||||
dot.setAttribute('aria-hidden', 'true');
|
||||
const title = createNode(doc, 'h2', 'ccweb-task-board__wall-column-title');
|
||||
const count = createNode(doc, 'span', 'ccweb-task-board__wall-column-count');
|
||||
identity.append(dot, title, count);
|
||||
const page = createNode(doc, 'span', 'ccweb-task-board__wall-column-page');
|
||||
header.append(identity, page);
|
||||
const body = createNode(doc, 'div', 'ccweb-task-board__wall-column-body');
|
||||
body.setAttribute('role', 'list');
|
||||
element.append(header, body);
|
||||
return {
|
||||
element,
|
||||
header,
|
||||
title,
|
||||
count,
|
||||
page,
|
||||
body,
|
||||
empty: null,
|
||||
pageIndex: 0,
|
||||
totalPages: 1,
|
||||
definitionIndex,
|
||||
};
|
||||
}
|
||||
|
||||
function updateWallColumnRecord(record, definition, definitionIndex, taskCount, pageSize) {
|
||||
const titleId = `task-board-wall-column-${domToken(definition.id, definitionIndex)}`;
|
||||
record.element.dataset.statusId = definition.id;
|
||||
record.element.setAttribute('aria-labelledby', titleId);
|
||||
setStatusColor(record.element, definition.color);
|
||||
record.title.id = titleId;
|
||||
record.title.textContent = definition.label;
|
||||
record.count.textContent = String(taskCount);
|
||||
record.count.setAttribute('aria-label', `${taskCount} 个任务`);
|
||||
record.header.title = definition.prompt || '';
|
||||
record.body.setAttribute('aria-label', `${definition.label}任务`);
|
||||
record.totalPages = Math.max(1, Math.ceil(taskCount / pageSize));
|
||||
record.pageIndex = ((record.pageIndex % record.totalPages) + record.totalPages) % record.totalPages;
|
||||
record.page.textContent = record.totalPages > 1 ? `${record.pageIndex + 1} / ${record.totalPages}` : '';
|
||||
record.page.hidden = record.totalPages <= 1;
|
||||
record.definitionIndex = definitionIndex;
|
||||
}
|
||||
|
||||
function wallCardSignature(task, definition) {
|
||||
return JSON.stringify([wallTaskSignature(task), definition.label, definition.color]);
|
||||
}
|
||||
|
||||
function renderWallCard(task, definition, existingCard = null) {
|
||||
const card = existingCard || createNode(doc, 'article', 'ccweb-task-board__wall-card');
|
||||
card.replaceChildren();
|
||||
card.dataset.sessionId = task.sessionId;
|
||||
card.dataset.statusId = task.statusId;
|
||||
card.dataset.runtimeState = task.isRunning ? 'running' : 'idle';
|
||||
card.setAttribute('role', 'listitem');
|
||||
setStatusColor(card, definition.color);
|
||||
card.appendChild(createNode(doc, 'h3', 'ccweb-task-board__wall-card-title', task.title));
|
||||
const supportingText = task.summary || task.reason;
|
||||
if (supportingText) card.appendChild(createNode(doc, 'p', 'ccweb-task-board__wall-card-summary', supportingText));
|
||||
const meta = createNode(doc, 'div', 'ccweb-task-board__wall-card-meta');
|
||||
const runtimeLabel = task.runtimeState || (task.isRunning ? '运行中' : '');
|
||||
if (runtimeLabel) {
|
||||
const runtime = createNode(doc, 'span', 'ccweb-task-board__wall-card-runtime', runtimeLabel);
|
||||
runtime.dataset.running = task.isRunning ? 'true' : 'false';
|
||||
runtime.prepend(createNode(doc, 'span', 'ccweb-task-board__wall-card-runtime-dot'));
|
||||
meta.appendChild(runtime);
|
||||
}
|
||||
const updatedLabel = formatTime(task.updatedAt);
|
||||
if (updatedLabel) {
|
||||
const updated = createNode(doc, 'time', 'ccweb-task-board__wall-card-updated', updatedLabel);
|
||||
updated.setAttribute('datetime', String(task.updatedAt));
|
||||
meta.appendChild(updated);
|
||||
}
|
||||
if (meta.children.length) card.appendChild(meta);
|
||||
return card;
|
||||
}
|
||||
|
||||
function ensureWallColumnEmpty(record) {
|
||||
if (!record.empty) {
|
||||
record.empty = createNode(doc, 'div', 'ccweb-task-board__wall-column-empty');
|
||||
record.empty.append(
|
||||
createNode(doc, 'span', 'ccweb-task-board__wall-column-empty-mark', '·'),
|
||||
createNode(doc, 'span', '', '暂无任务'),
|
||||
);
|
||||
}
|
||||
if (record.empty.parentNode !== record.body) record.body.appendChild(record.empty);
|
||||
}
|
||||
|
||||
function renderWall(options = {}) {
|
||||
if (state.destroyed) return;
|
||||
const definitions = getBoardDefinitions();
|
||||
const tasks = getWallTasks();
|
||||
const grouped = wallTasksByStatus(definitions, tasks);
|
||||
const layout = wallLayoutSize(definitions.length || 1);
|
||||
const pageSize = layout.rows >= 3 ? 1 : wallCardsPerPage;
|
||||
refs.wallGrid.style.setProperty('--task-board-wall-columns', String(layout.columns));
|
||||
refs.wallGrid.style.setProperty('--task-board-wall-rows', String(layout.rows));
|
||||
refs.wallGrid.dataset.cellCount = String(definitions.length + 1);
|
||||
refs.wallGrid.dataset.rowCount = String(layout.rows);
|
||||
|
||||
const updatedSessionId = text(options.updatedSessionId);
|
||||
const updatedTask = updatedSessionId ? tasks.find((task) => task.sessionId === updatedSessionId) : null;
|
||||
const oldVisibleSessionIds = new Set(wallVisibleSessionIds);
|
||||
const oldRects = new Map();
|
||||
for (const [sessionId, record] of wallCardRecords) {
|
||||
const rect = readWallCardRect(record.element);
|
||||
if (rect) oldRects.set(sessionId, rect);
|
||||
cancelWallCardAnimation(record);
|
||||
}
|
||||
|
||||
if (!definitions.length) {
|
||||
refs.wallEmpty.hidden = false;
|
||||
syncNodeOrder(refs.wallGrid, [refs.clock, refs.wallEmpty]);
|
||||
wallVisibleSessionIds = new Set();
|
||||
for (const [sessionId, record] of wallCardRecords) {
|
||||
record.element.remove();
|
||||
wallCardRecords.delete(sessionId);
|
||||
}
|
||||
for (const [statusId, record] of wallColumnRecords) {
|
||||
record.element.remove();
|
||||
wallColumnRecords.delete(statusId);
|
||||
}
|
||||
clearWallRotation();
|
||||
wallHasRendered = true;
|
||||
return;
|
||||
}
|
||||
|
||||
refs.wallEmpty.hidden = true;
|
||||
refs.wallEmpty.remove();
|
||||
const desiredStatusIds = new Set();
|
||||
const desiredColumnNodes = [refs.clock];
|
||||
definitions.forEach((definition, definitionIndex) => {
|
||||
desiredStatusIds.add(definition.id);
|
||||
let record = wallColumnRecords.get(definition.id);
|
||||
if (!record) {
|
||||
record = createWallColumnRecord(definition, definitionIndex);
|
||||
wallColumnRecords.set(definition.id, record);
|
||||
}
|
||||
const columnTasks = grouped.get(definition.id) || [];
|
||||
if (updatedTask?.statusId === definition.id) {
|
||||
const updatedIndex = columnTasks.findIndex((task) => task.sessionId === updatedTask.sessionId);
|
||||
if (updatedIndex >= 0) record.pageIndex = Math.floor(updatedIndex / pageSize);
|
||||
}
|
||||
updateWallColumnRecord(record, definition, definitionIndex, columnTasks.length, pageSize);
|
||||
desiredColumnNodes.push(record.element);
|
||||
});
|
||||
syncNodeOrder(refs.wallGrid, desiredColumnNodes);
|
||||
|
||||
const desiredVisibleSessionIds = new Set();
|
||||
const taskSessionIds = new Set(tasks.map((task) => task.sessionId));
|
||||
const desiredCardsByStatus = new Map(definitions.map((definition) => [definition.id, []]));
|
||||
const visibleTransitions = [];
|
||||
|
||||
definitions.forEach((definition) => {
|
||||
const column = wallColumnRecords.get(definition.id);
|
||||
const columnTasks = grouped.get(definition.id) || [];
|
||||
const start = column.pageIndex * pageSize;
|
||||
const visibleTasks = columnTasks.slice(start, start + pageSize);
|
||||
for (const task of visibleTasks) {
|
||||
desiredVisibleSessionIds.add(task.sessionId);
|
||||
let record = wallCardRecords.get(task.sessionId);
|
||||
if (!record) {
|
||||
record = {
|
||||
element: createNode(doc, 'article', 'ccweb-task-board__wall-card'),
|
||||
signature: '',
|
||||
statusId: '',
|
||||
animations: new Map(),
|
||||
};
|
||||
wallCardRecords.set(task.sessionId, record);
|
||||
}
|
||||
const signature = wallCardSignature(task, definition);
|
||||
if (record.signature !== signature) {
|
||||
renderWallCard(task, definition, record.element);
|
||||
record.signature = signature;
|
||||
}
|
||||
record.statusId = task.statusId;
|
||||
desiredCardsByStatus.get(definition.id).push(record.element);
|
||||
visibleTransitions.push({ sessionId: task.sessionId, record });
|
||||
}
|
||||
});
|
||||
|
||||
wallVisibleSessionIds = desiredVisibleSessionIds;
|
||||
definitions.forEach((definition) => {
|
||||
const column = wallColumnRecords.get(definition.id);
|
||||
const desiredCards = desiredCardsByStatus.get(definition.id);
|
||||
if (desiredCards.length) {
|
||||
column.empty?.remove();
|
||||
syncNodeOrder(column.body, desiredCards);
|
||||
} else {
|
||||
ensureWallColumnEmpty(column);
|
||||
}
|
||||
});
|
||||
|
||||
for (const [sessionId, record] of Array.from(wallCardRecords.entries())) {
|
||||
if (desiredVisibleSessionIds.has(sessionId)) continue;
|
||||
if (!taskSessionIds.has(sessionId)) {
|
||||
cancelWallCardAnimation(record);
|
||||
record.element.remove();
|
||||
wallCardRecords.delete(sessionId);
|
||||
continue;
|
||||
}
|
||||
if (record.element.parentNode && wallHasRendered) animateWallCardPageLeave(sessionId, record);
|
||||
else record.element.remove();
|
||||
}
|
||||
|
||||
for (const transition of visibleTransitions) {
|
||||
const after = readWallCardRect(transition.record.element);
|
||||
const before = oldRects.get(transition.sessionId);
|
||||
if (wallHasRendered && before) animateWallCardMove(transition.record, before, after);
|
||||
else if (wallHasRendered && !oldVisibleSessionIds.has(transition.sessionId)) {
|
||||
const siblings = desiredCardsByStatus.get(transition.record.statusId) || [];
|
||||
animateWallCardPageEnter(transition.record, siblings.indexOf(transition.record.element));
|
||||
}
|
||||
if (transition.sessionId === updatedSessionId) animateWallCardUpdate(transition.record);
|
||||
}
|
||||
|
||||
for (const [statusId, record] of Array.from(wallColumnRecords.entries())) {
|
||||
if (desiredStatusIds.has(statusId)) continue;
|
||||
record.element.remove();
|
||||
wallColumnRecords.delete(statusId);
|
||||
}
|
||||
|
||||
wallHasRendered = true;
|
||||
if (updatedTask) scheduleWallRotation(wallUpdateHoldMs);
|
||||
else if (wallRotationTimer === null) scheduleWallRotation();
|
||||
}
|
||||
|
||||
function advanceWallPage() {
|
||||
const statusIds = overflowingWallStatusIds();
|
||||
if (!statusIds.length) {
|
||||
clearWallRotation();
|
||||
return;
|
||||
}
|
||||
const statusId = statusIds[wallRotationCursor % statusIds.length];
|
||||
wallRotationCursor = (wallRotationCursor + 1) % Math.max(statusIds.length, 1);
|
||||
const record = wallColumnRecords.get(statusId);
|
||||
if (record) record.pageIndex = (record.pageIndex + 1) % Math.max(record.totalPages, 1);
|
||||
renderWall({ rotatedStatusId: statusId });
|
||||
}
|
||||
|
||||
function emitStateChange(reason) {
|
||||
if (!adapters.onStateChange) return;
|
||||
adapters.onStateChange(getState(), reason);
|
||||
@@ -685,7 +1179,7 @@
|
||||
|
||||
function taskCardSignature(task, definition) {
|
||||
const selectableDefinitions = sortDefinitions(
|
||||
state.statusDefinitions.filter((item) => item.enabled || item.id === task.statusId),
|
||||
state.statusDefinitions.filter((item) => item.enabled && item.id !== task.statusId),
|
||||
).map((item) => [item.id, item.label, item.enabled, item.order]);
|
||||
return JSON.stringify([
|
||||
task.sessionId,
|
||||
@@ -750,13 +1244,14 @@
|
||||
const statusSelect = createNode(doc, 'select', 'ccweb-task-board__card-select');
|
||||
statusSelect.dataset.action = 'set-status';
|
||||
statusSelect.dataset.sessionId = task.sessionId;
|
||||
statusSelect.setAttribute('aria-label', `调整 ${task.title} 的任务状态`);
|
||||
const selectableDefinitions = state.statusDefinitions.filter((item) => item.enabled || item.id === task.statusId);
|
||||
statusSelect.setAttribute('aria-label', `移动 ${task.title} 到其他列`);
|
||||
statusSelect.appendChild(optionNode(doc, '', '移动到…', true));
|
||||
const selectableDefinitions = state.statusDefinitions.filter((item) => item.enabled && item.id !== task.statusId);
|
||||
for (const item of sortDefinitions(selectableDefinitions)) {
|
||||
statusSelect.appendChild(optionNode(doc, item.id, item.label, item.id === task.statusId));
|
||||
statusSelect.appendChild(optionNode(doc, item.id, item.label));
|
||||
}
|
||||
statusSelect.value = task.statusId;
|
||||
statusSelect.disabled = !adapters.send;
|
||||
statusSelect.value = '';
|
||||
statusSelect.disabled = !adapters.send || !selectableDefinitions.length;
|
||||
statusLabel.appendChild(statusSelect);
|
||||
|
||||
const archive = createNode(
|
||||
@@ -867,6 +1362,11 @@
|
||||
if (state.destroyed) return;
|
||||
const visibleTasks = sortTasks(state.tasks.filter(matchesFilters));
|
||||
const definitions = getBoardDefinitions();
|
||||
const columnCount = definitions.length;
|
||||
refs.board.style.setProperty('--task-board-column-count', String(Math.max(columnCount, 1)));
|
||||
refs.board.dataset.columnCount = String(columnCount);
|
||||
refs.board.dataset.columnLayout = columnCount <= fitColumnLimit ? 'fit' : 'scroll';
|
||||
refs.shell.dataset.searchActive = state.filters.search ? 'true' : 'false';
|
||||
const animateChanges = options.animate === true && hasRenderedBoard;
|
||||
const oldRects = new Map();
|
||||
// 数据事件只移动 keyed 节点;更新前后的位置差用于 FLIP,避免整板重绘闪烁。
|
||||
@@ -987,6 +1487,7 @@
|
||||
const archiveLabel = state.filters.archived ? '归档任务' : '当前任务';
|
||||
refs.summary.textContent = `${visibleTasks.length} 个${archiveLabel} · ${definitions.length} 个状态`;
|
||||
if (state.statusManagerOpen) renderStatusManager();
|
||||
renderWall({ updatedSessionId: options.wallUpdatedSessionId });
|
||||
hasRenderedBoard = true;
|
||||
}
|
||||
|
||||
@@ -1213,6 +1714,9 @@
|
||||
}
|
||||
|
||||
function applyData(data, options = {}) {
|
||||
const previousWallSignatures = new Map(
|
||||
state.tasks.map((task) => [task.sessionId, wallTaskSignature(task)]),
|
||||
);
|
||||
const envelope = collectEnvelope(data);
|
||||
const definitions = findArray(envelope, ['statusDefinitions', 'definitions', 'statuses']);
|
||||
const tasks = findArray(envelope, ['tasks', 'items']);
|
||||
@@ -1235,7 +1739,13 @@
|
||||
if (task) mergeTask(task);
|
||||
const removedSessionId = text(envelope.removedSessionId || envelope.deletedSessionId);
|
||||
if (removedSessionId) state.tasks = state.tasks.filter((item) => item.sessionId !== removedSessionId);
|
||||
renderBoard({ animate: true });
|
||||
const changedWallTask = wallHasRendered
|
||||
? state.tasks
|
||||
.filter((item) => previousWallSignatures.get(item.sessionId) !== wallTaskSignature(item))
|
||||
.filter((item) => !item.archivedAt)
|
||||
.sort(compareWallTasks)[0]
|
||||
: null;
|
||||
renderBoard({ animate: true, wallUpdatedSessionId: changedWallTask?.sessionId || '' });
|
||||
emitStateChange(options.reason || 'data');
|
||||
}
|
||||
|
||||
@@ -1384,13 +1894,16 @@
|
||||
const select = event.target?.closest?.('[data-action="set-status"]');
|
||||
if (!select) return;
|
||||
const sessionId = text(select.dataset.sessionId);
|
||||
const statusId = text(select.value);
|
||||
if (!sessionId || !statusId) return;
|
||||
const task = state.tasks.find((item) => item.sessionId === sessionId);
|
||||
sendRequest({
|
||||
type: MESSAGE_TYPES.statusSet,
|
||||
sessionId,
|
||||
statusId: text(select.value),
|
||||
statusId,
|
||||
...(task?.version === null ? {} : { expectedVersion: task?.version }),
|
||||
}, 'task-status');
|
||||
select.value = '';
|
||||
}
|
||||
|
||||
function onBoardClick(event) {
|
||||
@@ -1456,6 +1969,12 @@
|
||||
if (state.destroyed) return;
|
||||
state.destroyed = true;
|
||||
if (searchTimer !== null) global.clearTimeout(searchTimer);
|
||||
if (clockTimer !== null) global.clearTimeout(clockTimer);
|
||||
clearWallRotation();
|
||||
for (const record of wallCardRecords.values()) cancelWallCardAnimation(record);
|
||||
wallCardRecords.clear();
|
||||
wallColumnRecords.clear();
|
||||
wallVisibleSessionIds = new Set();
|
||||
clearBoardRecords();
|
||||
boardEmpty?.remove();
|
||||
refs.searchInput.removeEventListener('input', onSearchInput);
|
||||
@@ -1485,6 +2004,7 @@
|
||||
refs.shell.addEventListener('click', onShellClick);
|
||||
refs.statusBody.addEventListener('submit', onStatusSubmit);
|
||||
doc.addEventListener('keydown', onDocumentKeydown);
|
||||
scheduleClock();
|
||||
|
||||
if (isObject(options.filters)) {
|
||||
state.filters.search = text(options.filters.search || options.filters.query);
|
||||
|
||||
Reference in New Issue
Block a user