25 KiB
Stats Dashboard Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 重新设计 StatsView.vue 数据统计页面,采用赛博朋克风格的迷你图表布局
Architecture: 单页垂直滚动布局,6个模块垂直排列,每个模块包含汇总数字卡片和迷你趋势图。复用现有 CSS 变量和 ChatView.vue 的设计模式。使用纯 CSS 实现迷你图表(条形图/折线图),避免引入新的图表依赖。
Tech Stack: Vue 3 + TypeScript + Pinia + lucide-vue-next(已有)+ echarts(已有,但优先用CSS实现)
File Structure
frontend/src/
├── views/
│ └── StatsView.vue # 完全重写
├── components/stats/
│ ├── MetricCard.vue # 新建 - 指标卡片组件
│ ├── MiniLineChart.vue # 新建 - 迷你折线图
│ ├── MiniBarChart.vue # 新建 - 迷你柱状图
│ ├── SectionHeader.vue # 新建 - 区块标题
│ └── SummaryRow.vue # 新建 - 汇总行
└── style.css # 可能需要新增CSS变量
Task 1: 创建 SectionHeader 组件
Files:
-
Create:
frontend/src/components/stats/SectionHeader.vue -
Modify:
frontend/src/views/StatsView.vue(引入组件) -
Step 1: 创建 SectionHeader.vue
<script setup lang="ts">
defineProps<{
title: string
tag?: 'cyan' | 'purple' | 'amber'
}>()
</script>
<template>
<div class="section-header">
<div class="section-title">
<span class="section-slash">//</span>
<span class="section-name">{{ title }}</span>
</div>
<span v-if="tag" class="section-tag" :class="tag">
{{ title.split(' ')[0] }}
</span>
</div>
</template>
<style scoped>
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 0 12px;
border-bottom: 1px solid var(--border-dim);
margin-bottom: 16px;
}
.section-title {
font-family: var(--font-mono);
font-size: 12px;
letter-spacing: 0.1em;
}
.section-slash {
color: var(--text-dim);
margin-right: 8px;
}
.section-name {
color: var(--text-primary);
text-transform: uppercase;
}
.section-tag {
padding: 3px 10px;
border-radius: 3px;
font-family: var(--font-display);
font-size: 9px;
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.section-tag.cyan {
background: var(--accent-cyan-dim);
color: var(--accent-cyan);
border: 1px solid rgba(0, 245, 212, 0.2);
}
.section-tag.purple {
background: var(--accent-purple-dim);
color: var(--accent-purple);
border: 1px solid rgba(123, 44, 191, 0.2);
}
.section-tag.amber {
background: var(--accent-amber-dim);
color: var(--accent-amber);
border: 1px solid rgba(249, 168, 37, 0.2);
}
</style>
- Step 2: 在 StatsView.vue 中引入并使用 SectionHeader
在 <script setup> 中添加:
import SectionHeader from '@/components/stats/SectionHeader.vue'
在模板中使用:
<SectionHeader title="SYSTEM HEALTH" tag="cyan" />
Task 2: 创建 MetricCard 组件
Files:
-
Create:
frontend/src/components/stats/MetricCard.vue -
Step 1: 创建 MetricCard.vue
<script setup lang="ts">
import type { Component } from 'vue'
defineProps<{
icon: Component
label: string
value: string | number
accentColor?: string
}>()
</script>
<template>
<div class="metric-card" :style="{ '--card-accent': accentColor || 'var(--accent-cyan)' }">
<div class="metric-icon">
<component :is="icon" :size="16" />
</div>
<div class="metric-info">
<span class="metric-value">{{ value }}</span>
<span class="metric-label">{{ label }}</span>
</div>
</div>
</template>
<style scoped>
.metric-card {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
background: var(--bg-card);
border: 1px solid var(--border-dim);
border-radius: var(--radius-md);
transition: all var(--transition-mid);
}
.metric-card:hover {
border-color: var(--card-accent);
box-shadow: 0 0 12px color-mix(in srgb, var(--card-accent) 30%, transparent);
transform: translateY(-2px);
}
.metric-icon {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: var(--radius-sm);
background: color-mix(in srgb, var(--card-accent) 12%, transparent);
color: var(--card-accent);
}
.metric-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.metric-value {
font-family: var(--font-mono);
font-size: 20px;
font-weight: 600;
color: var(--card-accent);
text-shadow: 0 0 10px color-mix(in srgb, var(--card-accent) 40%, transparent);
}
.metric-label {
font-family: var(--font-mono);
font-size: 10px;
color: var(--text-dim);
letter-spacing: 0.08em;
text-transform: uppercase;
}
</style>
Task 3: 创建 SummaryRow 组件
Files:
-
Create:
frontend/src/components/stats/SummaryRow.vue -
Step 1: 创建 SummaryRow.vue
<script setup lang="ts">
defineProps<{
items: { label: string; value: string | number }[]
columns?: number
}>()
</script>
<template>
<div class="summary-row" :style="{ '--cols': columns || 4 }">
<div v-for="item in items" :key="item.label" class="summary-item">
<span class="summary-value">{{ item.value }}</span>
<span class="summary-label">{{ item.label }}</span>
</div>
</div>
</template>
<style scoped>
.summary-row {
display: grid;
grid-template-columns: repeat(var(--cols), 1fr);
gap: 16px;
margin-bottom: 20px;
}
.summary-item {
display: flex;
flex-direction: column;
gap: 4px;
padding: 12px 16px;
background: var(--bg-card);
border: 1px solid var(--border-dim);
border-radius: var(--radius-md);
transition: all var(--transition-fast);
}
.summary-item:hover {
border-color: var(--border-mid);
}
.summary-value {
font-family: var(--font-mono);
font-size: 18px;
font-weight: 600;
color: var(--accent-cyan);
text-shadow: 0 0 8px var(--accent-cyan-glow);
}
.summary-label {
font-family: var(--font-mono);
font-size: 9px;
color: var(--text-dim);
letter-spacing: 0.1em;
text-transform: uppercase;
}
</style>
Task 4: 创建 MiniBarChart 组件(CSS实现)
Files:
-
Create:
frontend/src/components/stats/MiniBarChart.vue -
Step 1: 创建 MiniBarChart.vue
<script setup lang="ts">
const props = withDefaults(defineProps<{
data: number[]
color?: string
height?: number
maxBars?: number
}>(), {
color: 'var(--accent-cyan)',
height: 32,
maxBars: 7
})
const displayData = computed(() => {
if (props.data.length <= props.maxBars) return props.data
// 采样:取最后 N 个
return props.data.slice(-props.maxBars)
})
const maxValue = computed(() => Math.max(...displayData.value, 1))
</script>
<template>
<div class="mini-bar-chart" :style="{ '--chart-height': height + 'px', '--chart-color': color }">
<div class="bars">
<div
v-for="(value, i) in displayData"
:key="i"
class="bar"
:style="{ height: (value / maxValue * 100) + '%' }"
/>
</div>
</div>
</template>
<style scoped>
.mini-bar-chart {
width: 100%;
height: var(--chart-height);
}
.bars {
display: flex;
align-items: flex-end;
gap: 2px;
height: 100%;
}
.bar {
flex: 1;
background: var(--chart-color);
border-radius: 2px 2px 0 0;
opacity: 0.7;
transition: opacity var(--transition-fast);
min-height: 2px;
}
.bar:hover {
opacity: 1;
}
</style>
Task 5: 创建 MiniLineChart 组件(CSS实现)
Files:
-
Create:
frontend/src/components/stats/MiniLineChart.vue -
Step 1: 创建 MiniLineChart.vue
<script setup lang="ts">
import { computed } from 'vue'
const props = withDefaults(defineProps<{
data: { date: string; value: number }[]
color?: string
height?: number
maxPoints?: number
}>(), {
color: 'var(--accent-cyan)',
height: 60,
maxPoints: 30
})
const displayData = computed(() => {
if (props.data.length <= props.maxPoints) return props.data
// 采样:均匀抽取
const step = Math.ceil(props.data.length / props.maxPoints)
return props.data.filter((_, i) => i % step === 0)
})
const maxValue = computed(() => Math.max(...displayData.value.map(d => d.value), 1))
const points = computed(() => {
return displayData.value.map((d, i) => {
const x = (i / (displayData.value.length - 1)) * 100
const y = 100 - (d.value / maxValue.value * 100)
return `${x},${y}`
}).join(' ')
})
</script>
<template>
<div class="mini-line-chart" :style="{ '--chart-height': height + 'px', '--chart-color': color }">
<svg :viewBox="`0 0 100 100`" preserveAspectRatio="none" class="chart-svg">
<!-- 背景网格 -->
<line x1="0" y1="50" x2="100" y2="50" class="grid-line" />
<!-- 折线 -->
<polyline :points="points" class="line" />
<!-- 数据点 -->
<circle
v-for="(d, i) in displayData"
:key="i"
:cx="(i / (displayData.length - 1)) * 100"
:cy="100 - (d.value / maxValue * 100)"
r="2"
class="dot"
/>
</svg>
</div>
</template>
<style scoped>
.mini-line-chart {
width: 100%;
height: var(--chart-height);
}
.chart-svg {
width: 100%;
height: 100%;
}
.grid-line {
stroke: var(--border-dim);
stroke-width: 0.5;
}
.line {
fill: none;
stroke: var(--chart-color);
stroke-width: 1.5;
stroke-linecap: round;
stroke-linejoin: round;
opacity: 0.8;
}
.dot {
fill: var(--chart-color);
opacity: 0;
transition: opacity var(--transition-fast);
}
.mini-line-chart:hover .dot {
opacity: 1;
}
</style>
Task 6: 重写 StatsView.vue
Files:
-
Modify:
frontend/src/views/StatsView.vue -
Step 1: 重写 script setup 部分
import { ref, onMounted, computed } from 'vue'
import * as statsApi from '@/api/stats'
import { Cpu, HardDrive, MemoryStick, Clock, MessageSquare, BookOpen, CheckSquare, TrendingUp, Tag } from 'lucide-vue-next'
import SectionHeader from '@/components/stats/SectionHeader.vue'
import MetricCard from '@/components/stats/MetricCard.vue'
import SummaryRow from '@/components/stats/SummaryRow.vue'
import MiniLineChart from '@/components/stats/MiniLineChart.vue'
import MiniBarChart from '@/components/stats/MiniBarChart.vue'
const isLoading = ref(true)
const hasError = ref(false)
// 数据状态
const systemHealth = ref<any>(null)
const conversationStats = ref<any>(null)
const knowledgeStats = ref<any>(null)
const kanbanStats = ref<any>(null)
const communityStats = ref<any>(null)
const personalInsights = ref<any>(null)
function formatUptime(seconds: number) {
const days = Math.floor(seconds / 86400)
const hours = Math.floor((seconds % 86400) / 3600)
const mins = Math.floor((seconds % 3600) / 60)
if (days > 0) return `${days}d ${hours}h`
if (hours > 0) return `${hours}h ${mins}m`
return `${mins}m`
}
function formatNumber(num: number): string {
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'
if (num >= 1000) return (num / 1000).toFixed(1) + 'K'
return num.toString()
}
onMounted(async () => {
try {
const promises = [
statsApi.getSystemHealth().catch(() => null),
]
const [sys] = await Promise.all(promises)
systemHealth.value = sys?.data || null
// 尝试加载用户相关数据(需要认证)
const userPromises = [
statsApi.getConversationStats().catch(() => null),
statsApi.getKnowledgeStats().catch(() => null),
statsApi.getKanbanStats().catch(() => null),
statsApi.getCommunityStats().catch(() => null),
statsApi.getPersonalInsights().catch(() => null),
]
const [conv, know, kanban, community, insights] = await Promise.all(userPromises)
conversationStats.value = conv?.data || null
knowledgeStats.value = know?.data || null
kanbanStats.value = kanban?.data || null
communityStats.value = community?.data || null
personalInsights.value = insights?.data || null
} catch (e) {
hasError.value = true
console.error('Failed to load stats:', e)
} finally {
isLoading.value = false
}
})
// 图表数据转换
const convChartData = computed(() =>
conversationStats.value?.daily_conversations?.map((d: any) => ({ date: d.date, value: d.count })) || []
)
const msgChartData = computed(() =>
conversationStats.value?.daily_messages?.map((d: any) => ({ date: d.date, value: d.count })) || []
)
const inputTokenData = computed(() =>
conversationStats.value?.daily_input_tokens?.map((d: any) => ({ date: d.date, value: d.input_tokens })) || []
)
const outputTokenData = computed(() =>
conversationStats.value?.daily_output_tokens?.map((d: any) => ({ date: d.date, value: d.output_tokens })) || []
)
const knowChartData = computed(() =>
knowledgeStats.value?.daily_new_tags?.map((d: any) => ({ date: d.date, value: d.count })) || []
)
const kanbanNewData = computed(() =>
kanbanStats.value?.daily_new_tasks?.map((d: any) => d.count) || []
)
const kanbanDoneData = computed(() =>
kanbanStats.value?.daily_completed_tasks?.map((d: any) => d.count) || []
)
const communityChartData = computed(() =>
communityStats.value?.daily_posts?.map((d: any) => ({ date: d.date, value: d.count })) || []
)
const hourlyActivityData = computed(() =>
personalInsights.value?.hourly_activity?.map((h: any) => h.count) || []
)
- Step 2: 重写 template 部分
<template>
<div class="stats-view">
<div class="stats-header">
<h1>// DATA METRICS</h1>
</div>
<div v-if="isLoading" class="loading-state">
<div class="loading-spinner"></div>
<span>Loading metrics...</span>
</div>
<div v-else-if="hasError" class="error-state">
<span>Failed to load stats</span>
<button @click="() => window.location.reload()">Refresh</button>
</div>
<div v-else class="stats-content">
<!-- SYSTEM HEALTH -->
<section class="stats-section">
<SectionHeader title="SYSTEM HEALTH" tag="cyan" />
<div class="metrics-grid">
<MetricCard
:icon="Cpu"
label="CPU Usage"
:value="systemHealth ? systemHealth.cpu_percent + '%' : '--'"
accentColor="var(--accent-cyan)"
/>
<MetricCard
:icon="MemoryStick"
label="Memory"
:value="systemHealth ? systemHealth.memory_percent + '%' : '--'"
accentColor="var(--accent-purple)"
/>
<MetricCard
:icon="HardDrive"
label="Disk"
:value="systemHealth ? systemHealth.disk_percent + '%' : '--'"
accentColor="var(--accent-amber)"
/>
<MetricCard
:icon="Clock"
label="Uptime"
:value="systemHealth ? formatUptime(systemHealth.uptime_seconds) : '--'"
accentColor="var(--accent-green)"
/>
</div>
</section>
<!-- CONVERSATIONS -->
<section class="stats-section">
<SectionHeader title="CONVERSATIONS" tag="cyan" />
<SummaryRow
v-if="conversationStats"
:items="[
{ label: 'Total Conversations', value: formatNumber(conversationStats.totals?.conversations || 0) },
{ label: 'Total Messages', value: formatNumber(conversationStats.totals?.messages || 0) },
{ label: 'Input Tokens', value: formatNumber(conversationStats.totals?.input_tokens || 0) },
{ label: 'Output Tokens', value: formatNumber(conversationStats.totals?.output_tokens || 0) },
]"
/>
<div class="chart-box" v-if="convChartData.length > 0">
<div class="chart-label">30-Day Trend</div>
<MiniLineChart :data="convChartData" color="var(--accent-cyan)" :height="80" />
</div>
<div v-else class="empty-state">
<span>No conversation data yet</span>
</div>
</section>
<!-- KNOWLEDGE -->
<section class="stats-section">
<SectionHeader title="KNOWLEDGE BASE" tag="purple" />
<SummaryRow
v-if="knowledgeStats"
:items="[
{ label: 'New Tags', value: formatNumber(knowledgeStats.totals?.new_tags || 0) },
{ label: 'Documents', value: formatNumber(knowledgeStats.totals?.documents || 0) },
{ label: 'Tag Relations', value: formatNumber(knowledgeStats.totals?.tag_relations || 0) },
]"
:columns="3"
/>
<div class="chart-box" v-if="knowChartData.length > 0">
<div class="chart-label">Tag Growth</div>
<MiniLineChart :data="knowChartData" color="var(--accent-purple)" :height="80" />
</div>
<div v-else class="empty-state">
<span>No knowledge data yet</span>
</div>
</section>
<!-- KANBAN -->
<section class="stats-section">
<SectionHeader title="KANBAN" tag="cyan" />
<SummaryRow
v-if="kanbanStats"
:items="[
{ label: 'Pending Tasks', value: kanbanStats.current_pending_tasks || 0 },
{ label: 'New (30d)', value: formatNumber(kanbanStats.totals?.new_tasks || 0) },
{ label: 'Done (30d)', value: formatNumber(kanbanStats.totals?.completed_tasks || 0) },
]"
:columns="3"
/>
<div class="chart-box" v-if="kanbanNewData.length > 0">
<div class="chart-label">Tasks: New vs Completed</div>
<div class="bar-chart-group">
<MiniBarChart :data="kanbanNewData" color="var(--accent-cyan)" :height="60" />
<MiniBarChart :data="kanbanDoneData" color="var(--accent-green)" :height="60" />
</div>
</div>
<div v-else class="empty-state">
<span>No kanban data yet</span>
</div>
</section>
<!-- COMMUNITY -->
<section class="stats-section">
<SectionHeader title="COMMUNITY" tag="amber" />
<SummaryRow
v-if="communityStats"
:items="[
{ label: 'Posts', value: formatNumber(communityStats.totals?.posts || 0) },
{ label: 'Replies', value: formatNumber(communityStats.totals?.replies || 0) },
{ label: 'AI Executions', value: formatNumber(communityStats.totals?.ai_executions || 0) },
]"
:columns="3"
/>
<div class="chart-box" v-if="communityChartData.length > 0">
<div class="chart-label">Activity Trend</div>
<MiniLineChart :data="communityChartData" color="var(--accent-amber)" :height="80" />
</div>
<div v-else class="empty-state">
<span>No community data yet</span>
</div>
</section>
<!-- INSIGHTS -->
<section class="stats-section">
<SectionHeader title="PERSONAL INSIGHTS" tag="cyan" />
<div class="insights-grid" v-if="personalInsights">
<div class="insight-card">
<h4>Hourly Activity</h4>
<MiniBarChart
v-if="hourlyActivityData.length > 0"
:data="hourlyActivityData"
color="var(--accent-cyan)"
:height="80"
:maxBars="24"
/>
<div v-else class="empty-state small">No activity data</div>
</div>
<div class="insight-card">
<h4>Top Tags</h4>
<ul class="tag-list" v-if="personalInsights.top_tags?.length">
<li v-for="tag in personalInsights.top_tags" :key="tag.tag_path">
<Tag :size="12" />
<span class="tag-name">{{ tag.tag_path }}</span>
<span class="tag-count">{{ tag.usage_count }}</span>
</li>
</ul>
<div v-else class="empty-state small">No tags yet</div>
</div>
<div class="insight-card">
<h4>Token Trend</h4>
<div class="token-trend">
<span class="trend-value" :class="personalInsights.token_trend_percent > 0 ? 'up' : 'down'">
<TrendingUp :size="16" />
{{ personalInsights.token_trend_percent }}%
</span>
<span class="trend-label">vs last month</span>
</div>
</div>
</div>
<div v-else class="empty-state">
<span>Login to see personal insights</span>
</div>
</section>
</div>
</div>
</template>
- Step 3: 重写 style 部分
<style scoped>
.stats-view {
height: 100%;
overflow-y: auto;
padding: 24px;
background: var(--bg-void);
}
.stats-header {
margin-bottom: 24px;
}
.stats-header h1 {
font-family: var(--font-mono);
font-size: 14px;
font-weight: 600;
letter-spacing: 0.15em;
color: var(--accent-cyan);
}
.stats-content {
display: flex;
flex-direction: column;
gap: 8px;
}
.stats-section {
background: rgba(10, 15, 26, 0.6);
border: 1px solid var(--border-dim);
border-radius: var(--radius-lg);
padding: 20px;
margin-bottom: 16px;
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
@media (max-width: 1199px) {
.metrics-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 767px) {
.metrics-grid { grid-template-columns: 1fr; }
}
.chart-box {
background: var(--bg-card);
border: 1px solid var(--border-dim);
border-radius: var(--radius-md);
padding: 16px;
}
.chart-label {
font-family: var(--font-mono);
font-size: 9px;
letter-spacing: 0.1em;
color: var(--text-dim);
text-transform: uppercase;
margin-bottom: 12px;
}
.bar-chart-group {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.insights-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
@media (max-width: 1199px) {
.insights-grid { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 767px) {
.insights-grid { grid-template-columns: 1fr; }
}
.insight-card {
background: var(--bg-card);
border: 1px solid var(--border-dim);
border-radius: var(--radius-md);
padding: 16px;
}
.insight-card h4 {
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0.1em;
color: var(--text-dim);
text-transform: uppercase;
margin-bottom: 12px;
}
.tag-list {
list-style: none;
padding: 0;
margin: 0;
}
.tag-list li {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 0;
border-bottom: 1px solid var(--border-dim);
font-size: 12px;
}
.tag-list li:last-child {
border-bottom: none;
}
.tag-name {
flex: 1;
color: var(--text-secondary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tag-count {
font-family: var(--font-mono);
color: var(--accent-cyan);
font-size: 11px;
}
.token-trend {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 16px 0;
}
.trend-value {
display: flex;
align-items: center;
gap: 6px;
font-family: var(--font-mono);
font-size: 24px;
font-weight: 600;
}
.trend-value.up {
color: var(--accent-red);
}
.trend-value.down {
color: var(--accent-green);
}
.trend-label {
font-family: var(--font-mono);
font-size: 9px;
color: var(--text-dim);
letter-spacing: 0.08em;
text-transform: uppercase;
}
.loading-state,
.error-state,
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
color: var(--text-dim);
font-family: var(--font-mono);
font-size: 12px;
gap: 12px;
}
.loading-spinner {
width: 24px;
height: 24px;
border: 2px solid var(--border-dim);
border-top-color: var(--accent-cyan);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.empty-state.small {
padding: 20px;
}
button {
padding: 8px 16px;
background: var(--accent-cyan-dim);
border: 1px solid var(--border-mid);
border-radius: var(--radius-md);
color: var(--accent-cyan);
font-family: var(--font-mono);
font-size: 11px;
cursor: pointer;
transition: all var(--transition-fast);
}
button:hover {
background: rgba(0, 245, 212, 0.2);
box-shadow: var(--glow-cyan);
}
</style>
Task 7: 添加缺少的 CSS 变量(如需要)
Files:
-
Modify:
frontend/src/style.css -
Step 1: 检查并添加缺失的 CSS 变量
如果 --accent-purple 不存在,添加:
--accent-purple: #a855f7;
--accent-purple-dim: rgba(123, 44, 191, 0.15);
Task 8: 验证与测试
Files:
-
Test:
frontend/src/views/StatsView.vue -
Test:
frontend/src/components/stats/*.vue -
Step 1: 运行 TypeScript 检查
cd frontend && npx vue-tsc --noEmit
- Step 2: 运行开发服务器测试
cd frontend && npm run dev
- Step 3: 验证页面渲染
- 打开 http://localhost:5173/stats
- 确认无 console errors
- 确认页面布局正确
执行选项
1. Subagent-Driven (推荐) - 我为每个任务派遣独立的子代理,任务间进行审查,快速迭代
2. Inline Execution - 在当前会话中按批次执行任务
选择哪种方式?