Version: 0.9.50.dev.260428

后端:
1. 工具执行结果协议升级为结构化 ToolExecutionResult——execute/tool_runtime、ToolRegistry、stream extra 与 timeline 持久化统一改为透传 observation_text / summary / argument_view / result_view,不再只回写纯文本结果;context_tools、upsert_task_class 与旧 schedule/web 工具通过兼容包装接入新协议
2. 日程写工具注册继续收口——place / move / swap / batch_move / unplace / queue_apply_head_move 从 registry 内联实现下沉为独立 handler,降低注册表内参数解析与业务逻辑混写
3. 工具结果展示基础能力补齐——新增 execution_result / schedule_operation_handlers 公共件,为日程操作结果、参数本地化展示、blocked/failed/done 状态统一建模

前端:
4. AssistantPanel 接入结构化工具卡片渲染——新增 ToolCardRenderer,tool_call / tool_result 支持 argument_view / result_view 展示;schedule_completed 恢复为时间线内的占位卡片块,避免排程卡片脱离原消息顺序
5. 时间线类型与渲染收敛——schedule_agent.ts 补齐 ToolView 协议,AssistantPanel 改为按块渲染 tool / schedule_card / business_card,并移除旧 demo/prototype 路由与页面,收束正式面板代码路径

仓库:
6. AGENTS.md 新增协作约束——禁止擅自回滚、覆盖或删除用户/其他代理产生的工作区改动
This commit is contained in:
LoveLosita
2026-04-28 11:55:34 +08:00
parent 32d5dd0262
commit 509e266626
17 changed files with 2431 additions and 2199 deletions

View File

@@ -3,11 +3,20 @@ import type { ApiResponse } from '@/types/api'
import type { PlacedItem, SchedulePreviewData } from '@/types/dashboard'
import { extractErrorMessage } from '@/utils/http'
export type ToolView = {
view_type?: string
version?: number
collapsed?: Record<string, any>
expanded?: Record<string, any>
}
export interface TimelineToolPayload {
name: string
status: 'start' | 'done' | 'blocked' | 'failed'
status: 'start' | 'done' | 'blocked' | 'failed' | string
summary: string
arguments_preview?: string
argument_view?: ToolView
result_view?: ToolView
}
export interface TimelineConfirmPayload {
@@ -27,12 +36,12 @@ export interface TaskQueryCardTaskItem {
export interface TaskQueryCardFilter {
key:
| 'quadrant'
| 'keyword'
| 'deadline_after'
| 'deadline_before'
| 'include_completed'
| 'sort'
| 'quadrant'
| 'keyword'
| 'deadline_after'
| 'deadline_before'
| 'include_completed'
| 'sort'
label: string
value: string | number | boolean
operator?: 'eq' | 'contains' | 'gte' | 'lt'
@@ -74,15 +83,15 @@ export interface TimelineEvent {
id: number
seq: number
kind:
| 'user_text'
| 'assistant_text'
| 'tool_call'
| 'tool_result'
| 'confirm_request'
| 'schedule_completed'
| 'interrupt'
| 'status'
| 'business_card'
| 'user_text'
| 'assistant_text'
| 'tool_call'
| 'tool_result'
| 'confirm_request'
| 'schedule_completed'
| 'interrupt'
| 'status'
| 'business_card'
role?: 'user' | 'assistant'
content?: string
payload?: {

View File

@@ -22,7 +22,8 @@ import {
getConversationTimeline,
type TimelineEvent,
type TimelineToolPayload,
type TimelineConfirmPayload
type TimelineConfirmPayload,
type ToolView
} from '@/api/schedule_agent'
import { refreshToken } from '@/api/auth'
import { useAuthStore } from '@/stores/auth'
@@ -41,6 +42,7 @@ import ScheduleFineTuneModal from '@/components/assistant/ScheduleFineTuneModal.
import { formatConversationTime, formatMessageTime } from '@/utils/date'
import { renderMarkdown } from '@/utils/markdown'
import BusinessCardRenderer from '@/components/assistant/cards/BusinessCardRenderer.vue'
import ToolCardRenderer from '@/components/dashboard/ToolCardRenderer.vue'
import type {
TimelineBusinessCardPayload,
TaskQueryCardData,
@@ -77,6 +79,8 @@ interface StreamToolExtraPayload {
status?: string
summary?: string
arguments_preview?: string
argument_view?: ToolView
result_view?: ToolView
}
interface StreamExtraPayload {
@@ -108,6 +112,8 @@ interface ToolTraceEvent {
summary: string
detail?: string
toolName?: string
argumentView?: ToolView
resultView?: ToolView
}
interface StatusTraceEvent {
@@ -265,6 +271,7 @@ const conversationContextStatsLoadingMap = reactive<Record<string, boolean>>({})
const conversationContextStatsReadyMap = reactive<Record<string, boolean>>({})
const conversationListItemRevealMap = reactive<Record<string, boolean>>({})
const scheduleResultMap = reactive<Record<string, SchedulePreviewData>>({})
const scheduleResultSeqMap = reactive<Record<string, number>>({})
const businessCardEventsMap = reactive<Record<string, TimelineBusinessCardPayload[]>>({})
const isFineTuneModalVisible = ref(false)
const fineTuneLoading = ref(false)
@@ -693,6 +700,7 @@ function clearToolTraceState(messageId: string) {
delete assistantContentBlocksMap[messageId]
delete assistantTimelineLastKindMap[messageId]
delete scheduleResultMap[messageId]
delete scheduleResultSeqMap[messageId]
delete businessCardEventsMap[messageId]
for (const key of Object.keys(toolTraceExpandedMap)) {
if (key.startsWith(`${messageId}:tool:`)) {
@@ -707,6 +715,8 @@ function appendToolTraceEvent(
summary: string,
detail = '',
toolName = '',
argumentView?: ToolView,
resultView?: ToolView,
) {
const normalizedSummary = summary.trim()
if (!normalizedSummary) {
@@ -745,6 +755,8 @@ function appendToolTraceEvent(
summary: normalizedSummary,
detail: normalizedDetail || undefined,
toolName: normalizedToolName || undefined,
argumentView,
resultView,
})
assistantTimelineLastKindMap[messageId] = 'tool'
}
@@ -1569,6 +1581,17 @@ function getDisplayAssistantBlocks(dm: DisplayMessage): DisplayAssistantBlock[]
})
}
if (scheduleResultMap[source.id]) {
blocks.push({
id: `${source.id}:schedule-card`,
type: 'schedule_card',
seq: scheduleResultSeqMap[source.id] || 1000000,
schedulePreview: scheduleResultMap[source.id],
sourceId: source.id,
source,
})
}
const contentBlocks = assistantContentBlocksMap[source.id] || []
if (contentBlocks.length > 0) {
hasContentBlock = true
@@ -1599,16 +1622,6 @@ function getDisplayAssistantBlocks(dm: DisplayMessage): DisplayAssistantBlock[]
}
}
const schedulePreview = scheduleResultMap[dm.id]
if (schedulePreview) {
blocks.push({
id: `${dm.id}:schedule-card`,
type: 'schedule_card',
seq: nextAssistantTimelineSeq(),
schedulePreview,
})
}
if (!hasContentBlock && dm.content) {
fallbackSeq += 1
blocks.push({
@@ -2038,17 +2051,32 @@ function rebuildStateFromTimeline(conversationId: string, events: TimelineEvent[
case 'tool_call':
if (event.payload?.tool) {
const t = event.payload.tool
appendToolTraceEvent(mid, mapToolEventState(t.status), normalizeToolSummary(t), buildToolDetail(t), t.name)
appendToolTraceEvent(mid, mapToolEventState(t.status), normalizeToolSummary(t), buildToolDetail(t), t.name, t.argument_view, t.result_view)
}
break
case 'tool_result':
if (event.payload?.tool) {
const t = event.payload.tool
appendToolTraceEvent(mid, mapToolEventState(t.status), normalizeToolSummary(t), buildToolDetail(t), t.name)
appendToolTraceEvent(mid, mapToolEventState(t.status), normalizeToolSummary(t), buildToolDetail(t), t.name, t.argument_view, t.result_view)
}
break
case 'schedule_completed':
// 为该 assistant message 添加一个 schedule_card 占位卡
scheduleResultMap[mid] = {
conversation_id: conversationId,
trace_id: '',
summary: '日程表编排已就绪',
candidate_plans: [],
hybrid_entries: [],
task_class_ids: [],
generated_at: event.created_at || new Date().toISOString(),
is_placeholder: true
} as any
scheduleResultSeqMap[mid] = event.seq || nextAssistantTimelineSeq()
break
case 'confirm_request':
confirmOnlyStreamMap[mid] = true
// 记录确认卡片
@@ -2532,6 +2560,24 @@ function handleStreamExtraEvent(extra: StreamExtraPayload | undefined, assistant
return
}
if (extra.kind === 'schedule_completed') {
// 为当前助理消息添加一个排程卡片占位符
const mid = assistantMessage.id
scheduleResultMap[mid] = {
conversation_id: selectedConversationId.value,
trace_id: '',
summary: '日程表编排已就绪',
candidate_plans: [],
hybrid_entries: [],
task_class_ids: [],
generated_at: new Date().toISOString(),
is_placeholder: true
} as any
scheduleResultSeqMap[mid] = nextAssistantTimelineSeq()
scheduleScrollMessagesToBottom(true)
return
}
if (extra.kind === 'tool_call' && extra.tool) {
appendToolTraceEvent(
assistantMessage.id,
@@ -2539,6 +2585,8 @@ function handleStreamExtraEvent(extra: StreamExtraPayload | undefined, assistant
normalizeToolSummary(extra.tool),
buildToolDetail(extra.tool),
`${extra.tool.name || ''}`,
extra.tool.argument_view,
extra.tool.result_view,
)
return
}
@@ -2550,6 +2598,8 @@ function handleStreamExtraEvent(extra: StreamExtraPayload | undefined, assistant
normalizeToolSummary(extra.tool),
buildToolDetail(extra.tool),
`${extra.tool.name || ''}`,
extra.tool.argument_view,
extra.tool.result_view,
)
if (extra.tool.status === 'done') {
void loadConversationContextStats(selectedConversationId.value, true)
@@ -3160,30 +3210,19 @@ onBeforeUnmount(() => {
<div v-else class="chat-message__assistant-flow">
<TransitionGroup name="inner-fade">
<div v-for="block in getDisplayAssistantBlocks(dm)" :key="block.id">
<article v-if="block.type === 'tool'" class="chat-message__tool">
<button
type="button"
class="chat-message__tool-head"
@click="block.event && toggleToolTraceExpanded(block.event.id)"
>
<span class="chat-message__tool-icon" aria-hidden="true">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" />
</svg>
</span>
<span class="chat-message__tool-summary">{{ block.event?.summary }}</span>
<em class="chat-message__tool-badge">{{ getToolTraceStateLabel(block.event?.state || 'completed') }}</em>
<span class="chat-message__tool-chevron" :class="{ 'chat-message__tool-chevron--expanded': block.event ? isToolTraceExpanded(block.event.id) : false }" aria-hidden="true">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</span>
</button>
<p v-if="block.event && isToolTraceExpanded(block.event.id) && block.event.detail" class="chat-message__tool-detail">
{{ block.event.detail }}
</p>
</article>
<ToolCardRenderer
v-if="block.type === 'tool' && block.event"
:payload="{
name: block.event.toolName || '',
status: block.event.state === 'called' ? 'start' : (block.event.state === 'completed' ? 'done' : block.event.state),
summary: block.event.summary,
arguments_preview: block.event.detail,
argument_view: block.event.argumentView,
result_view: block.event.resultView
}"
:expanded="isToolTraceExpanded(block.id)"
@toggle="toggleToolTraceExpanded(block.id)"
/>
<div v-else-if="block.type === 'status'" class="chat-message__status-line">
<span class="chat-message__status-icon" aria-hidden="true">

View File

@@ -0,0 +1,633 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { TimelineToolPayload, ToolView } from '@/api/schedule_agent'
const props = defineProps<{
payload: TimelineToolPayload
expanded: boolean
}>()
const emit = defineEmits<{
(e: 'toggle'): void
}>()
function getStatusLabel(status: string) {
const map: Record<string, string> = {
start: '进行中',
done: '已完成',
failed: '失败',
blocked: '已拦截',
}
return map[status] || status
}
// 模拟原有的 getOperationLabel 逻辑(如果后端没传标签)
function getOperationFallbackLabel(op: string) {
const map: Record<string, string> = {
move: '移动',
place: '放置',
swap: '交换',
batch_move: '批量移动',
unplace: '取消放置',
queue_apply_head_move: '队列首项确认',
}
return map[op] || op
}
</script>
<template>
<article
class="tool-card"
:class="[
`tool-card--${payload.status}`,
{ 'tool-card--expanded': expanded },
]"
>
<!-- 1. 折叠态头部 (优先取 result_view.collapsed) -->
<header class="tool-card__header" @click="emit('toggle')">
<div class="tool-card__icon-box">
<svg v-if="payload.status === 'failed'" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="15" y1="9" x2="9" y2="15"></line>
<line x1="9" y1="9" x2="15" y2="15"></line>
</svg>
<svg v-else width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" />
</svg>
</div>
<div class="tool-card__title-group">
<div class="tool-card__title-row">
<h3 class="tool-card__title">
{{ payload.result_view?.collapsed?.title || payload.summary }}
</h3>
<span class="tool-card__badge">
{{ payload.result_view?.collapsed?.status_label || getStatusLabel(payload.status) }}
</span>
</div>
<p class="tool-card__subtitle">
{{ payload.result_view?.collapsed?.subtitle || payload.arguments_preview }}
</p>
</div>
<!-- 简短指标区 -->
<div v-if="!expanded && payload.result_view?.collapsed?.metrics" class="tool-card__metrics">
<div v-for="(m, mi) in payload.result_view.collapsed.metrics" :key="mi" class="metric-item">
<span class="metric-value">{{ m.value }}</span>
<span class="metric-label">{{ m.label }}</span>
</div>
</div>
<div v-else-if="!expanded && payload.result_view?.collapsed?.operation_label" class="tool-card__metrics">
<div class="metric-item">
<span class="metric-label">{{ payload.result_view.collapsed.operation_label }}</span>
</div>
</div>
<div class="tool-card__chevron" :class="{ 'tool-card__chevron--expanded': expanded }">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</div>
</header>
<!-- 2. 展开态详情 -->
<transition name="tool-expand">
<section v-if="expanded" class="tool-card__content">
<div class="tool-card__divider"></div>
<!-- 2.1 参数展示 (优先读取 argument_view) -->
<div v-if="payload.argument_view" class="section-block section-arguments">
<h4 class="detail-section-title">参数详情</h4>
<p v-if="payload.argument_view.collapsed?.summary" class="arg-summary">
{{ payload.argument_view.collapsed.summary }}
</p>
<div v-if="payload.argument_view.expanded?.fields" class="arg-fields">
<div v-for="(f, fi) in payload.argument_view.expanded.fields" :key="fi" class="arg-field-item">
<span class="arg-label">{{ f.label }}</span>
<span class="arg-value">{{ f.display }}</span>
</div>
</div>
</div>
<!-- 2.2 结果渲染: schedule.operation_result -->
<div v-if="payload.result_view?.view_type === 'schedule.operation_result'" class="section-block view-operation">
<h4 class="detail-section-title">操作结果</h4>
<div v-if="payload.result_view.expanded?.changes?.length" class="changes-list">
<div v-for="(change, idx) in payload.result_view.expanded.changes" :key="idx" class="change-item">
<div class="change-item__header">
<span class="change-item__task-icon"></span>
<span class="change-item__task-name">{{ change.task_label }}</span>
<span v-if="change.status_label" class="change-item__status-tag">{{ change.status_label }}</span>
</div>
<div class="change-item__path">
<div class="slot-box slot-box--before">
<span class="slot-tag">之前</span>
<div class="slot-text">{{ change.before_label || '未排程' }}</div>
</div>
<div class="path-arrow">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="5" y1="12" x2="19" y2="12"></line>
<polyline points="12 5 19 12 12 19"></polyline>
</svg>
</div>
<div class="slot-box slot-box--after">
<span class="slot-tag">之后</span>
<div class="slot-text">{{ change.after_label || '未排程' }}</div>
</div>
</div>
</div>
</div>
<!-- 队列快照 (带标签) -->
<div v-if="payload.result_view.expanded?.queue_snapshot" class="queue-snapshot">
<h5 class="sub-section-title">{{ payload.result_view.expanded.queue_snapshot.summary_label || '队列变更' }}</h5>
<div class="queue-compare">
<div class="queue-side">
<span class="queue-count">{{ payload.result_view.expanded.queue_snapshot.before_label }}</span>
</div>
<div class="queue-arrow"></div>
<div class="queue-side">
<span class="queue-count highlight">{{ payload.result_view.expanded.queue_snapshot.after_label }}</span>
</div>
</div>
</div>
<!-- 失败信息 -->
<div v-if="payload.result_view.expanded?.failure_reason" class="failure-box">
<span class="failure-icon">!</span>
<p class="failure-text">{{ payload.result_view.expanded.failure_reason }}</p>
</div>
</div>
<!-- 2.3 结果渲染: legacy_text -->
<div v-else-if="payload.result_view?.view_type === 'legacy_text'" class="section-block view-legacy">
<h4 class="detail-section-title">{{ payload.result_view.expanded?.raw_text_label || '输出内容' }}</h4>
<div class="raw-text-container">
<pre class="raw-text">{{ payload.result_view.expanded?.raw_text }}</pre>
</div>
</div>
<!-- 2.4 旧协议兜底 -->
<div v-else-if="!payload.result_view" class="section-block view-old-fallback">
<h4 class="detail-section-title">工具输出 (兼容模式)</h4>
<div class="fallback-summary-box">
<p class="fallback-summary">{{ payload.summary }}</p>
<div v-if="payload.arguments_preview" class="fallback-json-box">
<span class="json-label">调用参数</span>
<code>{{ payload.arguments_preview }}</code>
</div>
</div>
</div>
<!-- 原始 Observation (仅开发/调试可见入口, 默认收起) -->
<details v-if="payload.result_view?.expanded?.raw_text" class="debug-details">
<summary>调试信息 (RAW Observation)</summary>
<pre class="debug-raw-pre">{{ payload.result_view.expanded.raw_text }}</pre>
</details>
</section>
</transition>
</article>
</template>
<style scoped>
/* Tool Card Styles */
.tool-card {
background: #ffffff;
border: 1px solid #eef2f6;
border-radius: 16px;
overflow: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.02);
margin: 8px 0;
}
.tool-card:hover {
border-color: #d1d5db;
transform: translateY(-1px);
}
.tool-card--expanded {
border-color: #3b82f6;
box-shadow: 0 8px 16px -4px rgba(59, 130, 246, 0.08);
}
.tool-card--failed {
border-left: 4px solid #f43f5e;
}
.tool-card--done {
border-left: 4px solid #10b981;
}
.tool-card__header {
padding: 12px 16px;
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
user-select: none;
}
.tool-card__icon-box {
width: 32px;
height: 32px;
background: #f8fafc;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: #64748b;
flex-shrink: 0;
border: 1px solid #f1f5f9;
}
.tool-card--done .tool-card__icon-box {
color: #10b981;
background: #f0fdf4;
border-color: #dcfce7;
}
.tool-card--failed .tool-card__icon-box {
color: #f43f5e;
background: #fff1f2;
border-color: #fee2e2;
}
.tool-card__title-group {
flex: 1;
min-width: 0;
}
.tool-card__title-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 1px;
}
.tool-card__title {
margin: 0;
font-size: 14px;
font-weight: 700;
color: #0f172a;
}
.tool-card__badge {
font-size: 10px;
font-weight: 600;
padding: 1px 8px;
border-radius: 6px;
background: #f1f5f9;
color: #64748b;
}
.tool-card--done .tool-card__badge {
background: #ecfdf5;
color: #059669;
}
.tool-card--failed .tool-card__badge {
background: #fff1f2;
color: #e11d48;
}
.tool-card__subtitle {
margin: 0;
font-size: 12px;
color: #94a3b8;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tool-card__metrics {
display: flex;
gap: 10px;
margin-left: 8px;
}
.metric-item {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.metric-value {
font-size: 13px;
font-weight: 800;
color: #334155;
line-height: 1;
}
.metric-label {
font-size: 9px;
color: #94a3b8;
font-weight: 600;
}
.tool-card__chevron {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
color: #cbd5e1;
transition: transform 0.3s;
}
.tool-card__chevron--expanded {
transform: rotate(180deg);
color: #3b82f6;
}
/* Content */
.tool-card__content {
padding: 0 16px 20px;
}
.tool-card__divider {
height: 1px;
background: #f1f5f9;
margin-bottom: 16px;
}
.section-block + .section-block {
margin-top: 20px;
}
.detail-section-title {
margin: 0 0 10px;
font-size: 11px;
font-weight: 800;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* Arguments */
.arg-summary {
font-size: 13px;
color: #475569;
font-weight: 500;
line-height: 1.5;
margin-bottom: 10px;
}
.arg-fields {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
background: #f8fafc;
padding: 12px;
border-radius: 12px;
border: 1px solid #f1f5f9;
}
.arg-field-item {
display: flex;
flex-direction: column;
gap: 1px;
}
.arg-label {
font-size: 10px;
color: #94a3b8;
font-weight: 500;
}
.arg-value {
font-size: 12px;
color: #1e293b;
font-weight: 600;
}
/* Operation Changes */
.changes-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.change-item {
background: #ffffff;
border: 1px solid #f1f5f9;
border-radius: 12px;
padding: 12px;
}
.change-item__header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 10px;
}
.change-item__task-icon {
width: 6px;
height: 6px;
background: #3b82f6;
border-radius: 2px;
}
.change-item__task-name {
font-size: 13px;
font-weight: 700;
color: #0f172a;
}
.change-item__status-tag {
font-size: 10px;
color: #2563eb;
background: #eff6ff;
padding: 0px 6px;
border-radius: 4px;
font-weight: 500;
}
.change-item__path {
display: flex;
align-items: center;
gap: 10px;
}
.slot-box {
flex: 1;
padding: 8px 10px;
border-radius: 10px;
}
.slot-box--before {
background: #fdfdfd;
border: 1px dashed #e2e8f0;
color: #64748b;
}
.slot-box--after {
background: #f0f7ff;
border: 1px solid #dbeafe;
color: #1e40af;
}
.slot-tag {
display: block;
font-size: 9px;
font-weight: 700;
margin-bottom: 2px;
opacity: 0.6;
}
.slot-text {
font-size: 12px;
font-weight: 600;
}
/* Queue Snapshot */
.sub-section-title {
margin: 0 0 10px;
font-size: 11px;
font-weight: 700;
color: #64748b;
}
.queue-snapshot {
margin-top: 16px;
padding: 12px;
background: #f8fafc;
border: 1px solid #eef2f6;
border-radius: 12px;
}
.queue-compare {
display: flex;
align-items: center;
gap: 16px;
}
.queue-side {
flex: 1;
}
.queue-count {
font-size: 13px;
font-weight: 700;
color: #64748b;
}
.queue-count.highlight {
color: #10b981;
}
.queue-arrow {
flex: 2;
height: 2px;
background: #e2e8f0;
}
/* Legacy Text */
.raw-text-container {
background: #1e293b;
border-radius: 12px;
padding: 12px;
}
.raw-text {
margin: 0;
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
line-height: 1.5;
color: #e2e8f0;
white-space: pre-wrap;
}
/* Fallback Old */
.fallback-summary-box {
padding: 12px;
background: #fefce8;
border: 1px solid #fef3c7;
border-radius: 12px;
}
.fallback-summary {
font-size: 13px;
color: #92400e;
font-weight: 600;
margin: 0 0 6px;
}
.fallback-json-box {
font-size: 11px;
color: #d97706;
}
/* Failure */
.failure-box {
margin-top: 14px;
padding: 12px;
background: #fff1f2;
border: 1px solid #fee2e2;
border-radius: 12px;
display: flex;
gap: 10px;
}
.failure-icon {
width: 18px;
height: 18px;
background: #f43f5e;
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 900;
flex-shrink: 0;
}
.failure-text {
margin: 0;
font-size: 12px;
color: #9f1239;
font-weight: 500;
line-height: 1.5;
}
/* Debug */
.debug-details {
margin-top: 24px;
border-top: 1px solid #f1f5f9;
padding-top: 12px;
}
.debug-details summary {
font-size: 11px;
color: #cbd5e1;
cursor: pointer;
}
.debug-raw-pre {
margin-top: 10px;
font-size: 10px;
padding: 10px;
border-radius: 8px;
background: #f8fafc;
color: #94a3b8;
max-height: 120px;
overflow-y: auto;
}
/* Animations */
.tool-expand-enter-active,
.tool-expand-leave-active {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
max-height: 1000px;
}
.tool-expand-enter-from,
.tool-expand-leave-to {
max-height: 0;
opacity: 0;
overflow: hidden;
}
</style>

View File

@@ -5,9 +5,6 @@ import AuthView from '@/views/AuthView.vue'
import AssistantView from '@/views/AssistantView.vue'
import DashboardView from '@/views/DashboardView.vue'
import ScheduleView from '@/views/ScheduleView.vue'
import ToolTracePrototypeView from '@/views/ToolTracePrototypeView.vue'
import TaskInteractiveDemo from '@/views/TaskInteractiveDemo.vue'
import DesignDemo from '@/views/DesignDemo.vue'
const router = createRouter({
history: createWebHistory(),
@@ -16,11 +13,6 @@ const router = createRouter({
path: '/',
redirect: '/dashboard',
},
{
path: '/demo-task',
name: 'demo-task',
component: TaskInteractiveDemo,
},
{
path: '/auth',
name: 'auth',
@@ -53,16 +45,6 @@ const router = createRouter({
requiresAuth: true,
},
},
{
path: '/prototype/tool-trace',
name: 'tool-trace-prototype',
component: ToolTracePrototypeView,
},
{
path: '/design-demo',
name: 'design-demo',
component: DesignDemo,
},
],
})

View File

@@ -1,253 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
// --- 数据结构定义 ---
interface Task {
id: string
title: string
priority_group: 1 | 2 | 3 | 4
deadline_at?: string
is_completed: boolean
}
// --- 四象限元数据 (深度对齐首页提示词 & 视觉) ---
const quadMeta: any = {
1: { title: '重要且紧急', caption: '优先处理', tone: 'danger', bg: 'linear-gradient(180deg, #fff1f2 0%, #fff7f7 100%)', text: '#ef4444' },
2: { title: '重要不紧急', caption: '持续推进', tone: 'primary', bg: 'linear-gradient(180deg, #eef7ff 0%, #f7fbff 100%)', text: '#3b82f6' },
3: { title: '简单不重要', caption: '顺手完成', tone: 'warning', bg: 'linear-gradient(180deg, #fff8df 0%, #fffdf1 100%)', text: '#f59e0b' },
4: { title: '不简单不重要', caption: '谨慎投入', tone: 'slate', bg: 'linear-gradient(180deg, #f2f5fb 0%, #f8fafc 100%)', text: '#64748b' }
}
// --- 卡片模拟数据 ---
const cardData = {
query: {
query: '我第一象限里还有哪些事情?',
group: 1 as const,
tasks: [
{ id: '1', title: '修复生产环境登录异常', priority_group: 1, deadline_at: '2024-05-20 09:00', is_completed: false },
{ id: '2', title: '提交年度安全审计报告', priority_group: 1, deadline_at: '今天 18:00', is_completed: false },
{ id: '3', title: '确认猎选系统的集成计划', priority_group: 1, deadline_at: '明天', is_completed: false }
] as Task[]
},
receipt: {
title: '联系供应商确认物料进度',
group: 2 as const,
id: 'TASK-520',
created_at: '刚才'
}
}
// --- 交互控制 ---
const activeView = ref<'query' | 'receipt'>('query')
const currentTone = ref<'danger' | 'primary' | 'warning' | 'slate'>('danger')
const switchTone = (tone: any) => {
currentTone.value = tone
// 模拟不同象限的查询结果
const toneToGroup: any = { danger: 1, primary: 2, warning: 3, slate: 4 }
cardData.query.group = toneToGroup[tone]
cardData.receipt.group = toneToGroup[tone]
}
</script>
<template>
<div class="design-demo-page">
<div class="page-background">
<div class="shape shape-1"></div>
<div class="shape shape-2"></div>
</div>
<div class="page-header">
<div class="chip">UI Refined V3.0</div>
<h1>业务卡片收敛方案</h1>
<p>首页风格同步 · 软渐变不晃眼 · 语义对齐</p>
</div>
<div class="demo-wrapper">
<!-- 预览控制台 -->
<aside class="demo-sidebar">
<div class="sidebar-block">
<h3>切换卡片类型</h3>
<div class="view-btns">
<button @click="activeView = 'query'" :class="{ active: activeView === 'query' }">查询记录</button>
<button @click="activeView = 'receipt'" :class="{ active: activeView === 'receipt' }">创建回执</button>
</div>
</div>
<div class="sidebar-block">
<h3>模拟目标象限</h3>
<div class="tone-btns">
<button v-for="(v, k) in quadMeta" :key="k" @click="switchTone(v.tone)" :class="[v.tone, { active: currentTone === v.tone }]">
{{ v.tone === 'danger' ? 'Q1' : v.tone === 'primary' ? 'Q2' : v.tone === 'warning' ? 'Q3' : 'Q4' }}
</button>
</div>
</div>
</aside>
<!-- 画布区域 -->
<main class="demo-canvas">
<!-- 场景 A任务查询结果 -->
<div v-if="activeView === 'query'" class="card-stage" :key="'query-' + currentTone">
<div class="card-label">预览跨象限/单象限查询结果列表</div>
<div class="chat-inline-mockup">
<div class="business-card-final query-results" :style="{ background: quadMeta[cardData.query.group].bg }">
<header class="card-header-final">
<div class="header-left">
<p class="eyebrow">{{ quadMeta[cardData.query.group].caption }}</p>
<h3>{{ cardData.query.query }}</h3>
</div>
<div class="count-badge">找到 {{ cardData.query.tasks.length }} </div>
</header>
<div class="card-content-final">
<div class="task-items-final">
<div v-for="task in cardData.query.tasks" :key="task.id" class="task-item-final">
<div class="item-check">
<div class="check-circle" :style="{ borderColor: quadMeta[cardData.query.group].text }"></div>
</div>
<div class="item-body">
<div class="item-title">{{ task.title }}</div>
<div class="item-meta">
<span class="q-pill" :style="{ color: quadMeta[cardData.query.group].text, background: quadMeta[cardData.query.group].text + '10' }">
Q{{ task.priority_group }} {{ quadMeta[task.priority_group].title }}
</span>
<span v-if="task.deadline_at" class="time-pill">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
{{ task.deadline_at }}
</span>
</div>
</div>
</div>
</div>
<button class="btn-more-final">查看完整任务列表</button>
</div>
</div>
</div>
</div>
<!-- 场景 B任务创建回执 -->
<div v-else class="card-stage" :key="'receipt-' + currentTone">
<div class="card-label">预览任务创建成功的轻量回执</div>
<div class="chat-inline-mockup">
<div class="business-card-final creation-receipt" :style="{ background: quadMeta[cardData.receipt.group].bg }">
<div class="receipt-inner">
<div class="receipt-header-final">
<div class="success-ring-v3" :style="{ background: quadMeta[cardData.receipt.group].text + '20', color: quadMeta[cardData.receipt.group].text }">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><polyline points="20 6 9 17 4 12"/></svg>
</div>
<div class="success-msg">
<strong>任务已由助手成功创建</strong>
<span>归类至{{ quadMeta[cardData.receipt.group].title }}</span>
</div>
</div>
<div class="receipt-task-card">
<div class="task-card-title">{{ cardData.receipt.title }}</div>
<div class="task-card-footer">
<span class="task-id-final">ID: {{ cardData.receipt.id }}</span>
<span class="task-time-final">创建于今日 {{ cardData.receipt.created_at }}</span>
</div>
</div>
<div class="receipt-actions-final">
<button class="btn-action-outline">调整象限</button>
<button class="btn-action-fill" :style="{ background: quadMeta[cardData.receipt.group].text }">打开详情</button>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
</template>
<style scoped>
.design-demo-page {
padding: 80px 24px;
background: #fdfdfe;
min-height: 100vh;
position: relative;
overflow: hidden;
font-family: 'Inter', -apple-system, sans-serif;
}
.page-background { position: fixed; inset: 0; z-index: -1; }
.shape { position: absolute; border-radius: 50%; filter: blur(80px); opacity: 0.1; }
.shape-1 { width: 500px; height: 500px; background: #3b82f6; top: -10%; left: -10%; }
.shape-2 { width: 400px; height: 400px; background: #f43f5e; bottom: -5%; right: -5%; }
.page-header { text-align: center; margin-bottom: 60px; }
.chip { display: inline-block; padding: 4px 12px; background: #f1f5f9; color: #475569; border-radius: 100px; font-size: 11px; font-weight: 800; margin-bottom: 12px; }
.page-header h1 { font-size: 32px; font-weight: 900; letter-spacing: -0.04em; color: #0f172a; margin-bottom: 8px; }
.page-header p { font-size: 16px; color: #64748b; font-weight: 500; }
.demo-wrapper { display: flex; gap: 48px; max-width: 1000px; margin: 0 auto; align-items: flex-start; }
.demo-sidebar { width: 200px; display: flex; flex-direction: column; gap: 32px; position: sticky; top: 80px; }
.sidebar-block h3 { font-size: 13px; font-weight: 800; color: #94a3b8; margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.05em; }
.view-btns, .tone-btns { display: flex; flex-direction: column; gap: 8px; }
.view-btns button, .tone-btns button { padding: 10px 14px; border: 1px solid #f1f5f9; background: white; border-radius: 12px; font-size: 13px; font-weight: 700; color: #475569; cursor: pointer; transition: all 0.2s; text-align: left; }
.view-btns button.active { background: #0f172a; color: white; border-color: #0f172a; }
.tone-btns { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.tone-btns button { text-align: center; }
.tone-btns button.danger.active { background: #fee2e2; color: #ef4444; border-color: #ef4444; }
.tone-btns button.primary.active { background: #dbeafe; color: #3b82f6; border-color: #3b82f6; }
.tone-btns button.warning.active { background: #fef3c7; color: #d97706; border-color: #d97706; }
.tone-btns button.slate.active { background: #f1f5f9; color: #475569; border-color: #475569; }
.demo-canvas { flex: 1; min-width: 0; }
.card-stage { animation: stage-in 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both; }
@keyframes stage-in { 0% { opacity: 0; transform: translateY(20px); } 100% { opacity: 1; transform: translateY(0); } }
.card-label { font-size: 12px; color: #94a3b8; margin-bottom: 12px; font-weight: 600; padding-left: 8px; }
.chat-inline-mockup { padding: 40px; background: rgba(255, 255, 255, 0.4); border-radius: 40px; border: 1px solid rgba(0,0,0,0.02); backdrop-filter: blur(20px); display: flex; justify-content: center; }
/* --- Final Business Card Refinement --- */
.business-card-final { width: 100%; max-width: 380px; border-radius: 28px; border: 1px solid rgba(17, 24, 39, 0.08); box-shadow: 0 4px 20px rgba(0,0,0,0.02); overflow: hidden; transition: all 0.3s; }
.business-card-final:hover { transform: translateY(-4px); box-shadow: 0 12px 40px rgba(15, 23, 42, 0.06); }
/* Header Sync with Homepage */
.card-header-final { padding: 24px 24px 16px; display: flex; justify-content: space-between; align-items: flex-start; }
.eyebrow { font-size: 11px; font-weight: 800; color: rgba(30, 41, 59, 0.5); text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 6px; }
.card-header-final h3 { font-size: 24px; font-weight: 850; color: #1e293b; margin: 0; line-height: 1.1; letter-spacing: -0.02em; }
.count-badge { padding: 4px 12px; background: rgba(255, 255, 255, 0.8); border-radius: 100px; font-size: 11px; font-weight: 700; color: #475569; box-shadow: 0 2px 8px rgba(0,0,0,0.02); }
/* Content List Sync */
.task-items-final { padding: 0 16px; display: flex; flex-direction: column; gap: 8px; }
.task-item-final { background: rgba(255, 255, 255, 0.95); border: 1px solid rgba(0,0,0,0.04); border-radius: 18px; padding: 14px 16px; display: flex; gap: 14px; align-items: center; }
.check-circle { width: 22px; height: 22px; border-radius: 50%; border: 2px solid #e2e8f0; }
.item-title { font-size: 15px; font-weight: 700; color: #122033; margin-bottom: 4px; }
.item-meta { display: flex; gap: 10px; align-items: center; }
.q-pill { font-size: 10px; font-weight: 800; padding: 1px 8px; border-radius: 4px; }
.time-pill { font-size: 10px; color: #94a3b8; display: flex; align-items: center; gap: 4px; font-weight: 500; }
.btn-more-final { width: calc(100% - 32px); margin: 16px 16px 20px; padding: 12px; border: none; background: rgba(255, 255, 255, 0.6); border-radius: 14px; font-size: 13px; font-weight: 800; color: #475569; cursor: pointer; transition: all 0.2s; }
.btn-more-final:hover { background: white; }
/* Receipt Card Refinement */
.receipt-inner { padding: 24px; display: flex; flex-direction: column; gap: 20px; }
.receipt-header-final { display: flex; gap: 14px; align-items: center; }
.success-ring-v3 { width: 44px; height: 44px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.success-msg { display: flex; flex-direction: column; }
.success-msg strong { font-size: 15px; font-weight: 850; color: #0f172a; }
.success-msg span { font-size: 12px; color: #64748b; font-weight: 500; }
.receipt-task-card { background: rgba(255, 255, 255, 0.95); border: 1px solid rgba(0,0,0,0.03); border-radius: 20px; padding: 20px; box-shadow: 0 4px 12px rgba(0,0,0,0.01); }
.task-card-title { font-size: 17px; font-weight: 800; color: #1e293b; margin-bottom: 12px; line-height: 1.4; }
.task-card-footer { display: flex; justify-content: space-between; font-size: 11px; font-weight: 600; color: #94a3b8; border-top: 1px solid #f1f5f9; padding-top: 10px; }
.receipt-actions-final { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.btn-action-outline { height: 42px; border: 1px solid #e2e8f0; background: white; border-radius: 12px; font-size: 13px; font-weight: 750; color: #475569; cursor: pointer; }
.btn-action-fill { height: 42px; border: none; border-radius: 12px; color: white; font-size: 13px; font-weight: 800; cursor: pointer; box-shadow: 0 8px 20px rgba(0,0,0,0.1); }
/* Responsive */
@media (max-width: 800px) {
.demo-wrapper { flex-direction: column; }
.demo-sidebar { width: 100%; position: static; gap: 20px; }
.tone-btns { grid-template-columns: repeat(4, 1fr); }
.chat-inline-mockup { padding: 20px; border-radius: 24px; }
}
</style>

View File

@@ -1,356 +0,0 @@
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
// --- 模拟数据 ---
const tasks = ref([
{ id: 1, title: '完成系统架构重构方案', is_completed: false, ddl: '2024-04-25T10:00:00', priority: 1 },
{ id: 2, title: '准备技术分享 PPT', is_completed: true, ddl: '2024-04-24T14:00:00', priority: 2 },
{ id: 3, title: '代码 Review用户模块', is_completed: false, ddl: '2024-04-23T18:00:00', priority: 3 },
])
const quadrantMap = {
1: { label: '重要且紧急', color: '#ef4444' },
2: { label: '重要不紧急', color: '#3b82f6' },
3: { label: '简单不重要', color: '#f59e0b' },
4: { label: '不简单不重要', color: '#64748b' },
}
// --- 交互状态 ---
const editDialogVisible = ref(false)
const currentEditingTask = reactive({
id: 0,
title: '',
ddl: '',
priority: 1
})
// 1. 切换状态 (独立响应区)
const toggleTask = (task: any) => {
task.is_completed = !task.is_completed
ElMessage.success({
message: task.is_completed ? '标记为已完成' : '已恢复为待办',
customClass: 'premium-msg'
})
}
// 2. 编辑任务 (正中主响应区)
const openEdit = (task: any) => {
currentEditingTask.id = task.id
currentEditingTask.title = task.title
currentEditingTask.ddl = task.ddl
currentEditingTask.priority = task.priority
editDialogVisible.value = true
}
// 3. 删除任务 (悬浮侧响应区)
const deleteTask = (task: any) => {
ElMessageBox.confirm('确定要删除这个任务吗?此操作不可撤销。', '确认删除', {
confirmButtonText: '确定删除',
cancelButtonText: '取消',
confirmButtonClass: 'flat-btn-danger',
cancelButtonClass: 'flat-btn-ghost',
center: true,
}).then(() => {
tasks.value = tasks.value.filter(t => t.id !== task.id)
ElMessage.success('任务已安全移除')
})
}
const saveEdit = () => {
const task = tasks.value.find(t => t.id === currentEditingTask.id)
if (task) {
task.title = currentEditingTask.title
task.ddl = currentEditingTask.ddl
task.priority = currentEditingTask.priority
editDialogVisible.value = false
ElMessage.success('任务已更新')
}
}
const formatSimpleDate = (dateStr: string) => {
if (!dateStr) return '无截止时间'
const d = new Date(dateStr)
return `${d.getMonth() + 1}-${d.getDate()} ${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`
}
</script>
<template>
<div class="demo-page">
<div class="demo-card">
<header class="card-header">
<div class="card-title">
<p class="subtitle">UPGRADED INTERACTION</p>
<h2>全参数扁平化示例</h2>
</div>
<span class="count-badge">{{ tasks.length }} </span>
</header>
<div class="task-list">
<TransitionGroup name="task-anime">
<div
v-for="task in tasks"
:key="task.id"
class="task-item"
:class="{ 'is-completed': task.is_completed }"
>
<!-- 区域1: 独立勾选框 -->
<div class="check-box-wrapper" @click.stop="toggleTask(task)">
<div class="check-box-inner">
<svg v-if="task.is_completed" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="4">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
</div>
</div>
<!-- 区域2: 文本主体内容 (点击触发编辑) -->
<div class="task-body" @click="openEdit(task)">
<div class="task-text-row">
<span class="task-text">{{ task.title }}</span>
<span class="priority-tag" :style="{ color: quadrantMap[task.priority as keyof typeof quadrantMap].color }">
{{ quadrantMap[task.priority as keyof typeof quadrantMap].label }}
</span>
</div>
<div class="task-meta">
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
{{ formatSimpleDate(task.ddl) }}
</div>
</div>
<!-- 区域3: 悬浮操作菜单 -->
<div class="hover-actions-panel">
<button class="action-btn-mini delete-btn" @click.stop="deleteTask(task)">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"></path></svg>
</button>
</div>
</div>
</TransitionGroup>
</div>
<div class="footer-tip">快捷操作点击任务主体进行全参数编辑</div>
</div>
<!-- 深度扁平化编辑弹窗 -->
<el-dialog
v-model="editDialogVisible"
title="编辑详细参数"
width="440px"
align-center
class="flat-dialog"
:show-close="false"
>
<div class="flat-form">
<div class="form-item">
<label>任务标题</label>
<input v-model="currentEditingTask.title" class="flat-input" placeholder="输入任务标题..." />
</div>
<div class="form-row">
<div class="form-item half">
<label>优先级象限</label>
<el-select v-model="currentEditingTask.priority" popper-class="flat-select-popper" class="flat-select">
<el-option v-for="(v, k) in quadrantMap" :key="k" :label="v.label" :value="Number(k)" />
</el-select>
</div>
<div class="form-item half">
<label>截止日期</label>
<el-date-picker
v-model="currentEditingTask.ddl"
type="datetime"
placeholder="选择时间"
format="YYYY-MM-DD HH:mm"
value-format="YYYY-MM-DD[T]HH:mm:ss"
class="flat-picker"
popper-class="flat-picker-popper"
/>
</div>
</div>
</div>
<template #footer>
<div class="flat-footer">
<button class="flat-btn ghost" @click="editDialogVisible = false">放弃修改</button>
<button class="flat-btn primary" @click="saveEdit">保存并同步</button>
</div>
</template>
</el-dialog>
</div>
</template>
<style scoped>
/* 基础布局 */
.demo-page {
min-height: 100vh;
background: #fdfdfd;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.demo-card {
width: 100%;
max-width: 460px;
background: #ffffff;
border-radius: 20px;
border: 1px solid #eeeeee;
box-shadow: 0 4px 24px rgba(0,0,0,0.03);
padding: 32px;
}
.card-header {
margin-bottom: 24px;
}
.subtitle {
font-size: 10px;
font-weight: 900;
color: #94a3b8;
letter-spacing: 0.2em;
margin: 0 0 4px;
}
.card-title h2 {
font-size: 22px;
font-weight: 800;
color: #0f172a;
margin: 0;
}
.count-badge {
font-size: 12px;
color: #64748b;
font-weight: 600;
margin-top: 4px;
display: inline-block;
}
/* 列表样式 */
.task-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.task-item {
position: relative;
background: #ffffff;
border: 1px solid #f1f5f9;
border-radius: 12px;
padding: 14px 16px;
display: flex;
align-items: center;
transition: all 0.2s ease;
cursor: pointer;
}
.task-item:hover {
border-color: #e2e8f0;
background: #f8fafc;
}
.check-box-inner {
width: 22px;
height: 22px;
border-radius: 6px;
border: 2px solid #e2e8f0;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
background: #fff;
margin-right: 14px;
}
.is-completed .check-box-inner {
background: #10b981;
border-color: #10b981;
color: #fff;
}
.task-body { flex: 1; min-width: 0; }
.task-text-row { display: flex; align-items: center; gap: 8px; margin-bottom: 2px; }
.task-text { font-size: 15px; font-weight: 600; color: #1e293b; }
.is-completed .task-text { color: #94a3b8; text-decoration: line-through; }
.priority-tag { font-size: 11px; font-weight: 700; background: rgba(0,0,0,0.03); padding: 2px 6px; border-radius: 4px; }
.task-meta { font-size: 12px; color: #94a3b8; display: flex; align-items: center; gap: 4px; }
.hover-actions-panel {
position: absolute;
right: 12px;
opacity: 0;
transition: all 0.2s;
}
.task-item:hover .hover-actions-panel { opacity: 1; }
.action-btn-mini { border: none; background: transparent; color: #ef4444; cursor: pointer; padding: 4px; border-radius: 6px; }
.action-btn-mini:hover { background: #fee2e2; }
/* 深度扁平化弹窗 - 关键美化部分 */
:global(.flat-dialog) {
border-radius: 16px !important;
border: 1px solid #f1f5f9 !important;
box-shadow: 0 20px 40px rgba(0,0,0,0.1) !important;
}
:global(.flat-dialog .el-dialog__header) {
padding: 24px 28px 10px !important;
text-align: left;
}
:global(.flat-dialog .el-dialog__title) {
font-size: 16px !important;
font-weight: 800 !important;
color: #0f172a;
}
:global(.flat-dialog .el-dialog__body) {
padding: 10px 28px 24px !important;
}
.flat-form { display: flex; flex-direction: column; gap: 20px; }
.form-row { display: flex; gap: 16px; }
.form-item { display: flex; flex-direction: column; gap: 8px; }
.form-item.half { flex: 1; }
.form-item label { font-size: 12px; font-weight: 800; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em; }
/* 纯扁平输入框 */
.flat-input {
height: 42px;
border: 1px solid #e2e8f0;
border-radius: 10px;
padding: 0 14px;
background: #f8fafc;
outline: none;
font-size: 14px;
transition: all 0.2s;
font-weight: 600;
}
.flat-input:focus { border-color: #3b82f6; background: #fff; }
/* Element Plus 深度覆盖 */
:global(.flat-select .el-select__wrapper),
:global(.flat-picker.el-input__wrapper) {
background-color: #f8fafc !important;
box-shadow: none !important;
border: 1px solid #e2e8f0 !important;
border-radius: 10px !important;
height: 42px !important;
}
.flat-footer { display: flex; justify-content: flex-end; gap: 10px; border-top: 1px solid #f1f5f9; padding-top: 20px; }
.flat-btn { height: 42px; padding: 0 20px; border-radius: 10px; font-weight: 700; font-size: 13px; cursor: pointer; border: none; transition: all 0.2s; }
.flat-btn.primary { background: #0f172a; color: #fff; }
.flat-btn.primary:hover { background: #334155; }
.flat-btn.ghost { background: transparent; color: #64748b; }
.flat-btn.ghost:hover { background: #f1f5f9; }
.footer-tip { margin-top: 24px; font-size: 12px; color: #94a3b8; text-align: center; }
/* 动画 */
.task-anime-enter-active { transition: all 0.3s ease; }
.task-anime-enter-from { opacity: 0; transform: scale(0.95); }
.task-anime-leave-to { opacity: 0; transform: scale(1.05); }
</style>

File diff suppressed because it is too large Load Diff