feat: add interactive component examples

This commit is contained in:
QiuSW
2026-07-09 01:16:47 +08:00
parent 357747dd17
commit 76cda6b49c
8 changed files with 337 additions and 64 deletions
+1 -1
View File
@@ -8,7 +8,7 @@
- 后端:Go 1.23.0、Wails v3.0.0-alpha.9、SQLite
- 前端:Vue 3、TypeScript、Vite、Tailwind CSS、Ant Design Vue
- 当前功能:多语言、主题切换、统一设置 schema、主窗口状态恢复、本地配置、快捷键、本地登录 provider、商品图片 AI 优化页面、本地模拟图片优化任务、审计日志、运行日志、P0 数据统计服务、OCR 示例
- 当前功能:多语言、主题切换、统一设置 schema、主窗口状态恢复、本地配置、快捷键、本地登录 provider、商品图片 AI 优化页面、本地模拟图片优化任务、审计日志、运行日志、P0 数据统计服务、常用组件示例页、OCR 示例
- 目标模板模块:登录、设置、主业务、审计日志、运行日志、数据统计、常用组件示例
## 环境准备
+1 -1
View File
@@ -66,7 +66,7 @@ frontend/src
- `frontend/src/modules/image-optimizer/`:放已接入的商品图片 AI 优化样例页面。
- `frontend/src/modules/logs/`:目标目录,放审计日志和运行日志页面。
- `frontend/src/modules/statistics/`:放统计卡片和趋势图。
- `frontend/src/modules/components-demo/`:目标目录,放常用组件示例。
- `frontend/src/modules/components-demo/`:放已接入的常用组件示例页。
- `frontend/src/components/template/`:放当前模块占位页共享壳。
- `frontend/src/components/`:通用展示组件和组合控件。
- `frontend/src/utils/`:国际化、主题、OS、热键等工具函数。
+1 -1
View File
@@ -46,7 +46,7 @@
| ID | 任务 | 依赖 | 验收要点 | 状态 |
| --- | --- | --- | --- | --- |
| T-030 | 建立常用组件示例页 | T-012 | 输入框、多选框、下拉框、多行文本、表格、筛选、分页、状态标签有可运行示例 | TODO |
| T-030 | 建立常用组件示例页 | T-012 | 输入框、多选框、下拉框、多行文本、表格、筛选、分页、状态标签有可运行示例 | DONE |
| T-031 | 文档化 Wails bindings 生成流程 | T-012 | README 或技术栈文档说明 `wails3 generate bindings` 的使用、失败处理和版本要求 | TODO |
| T-032 | 补充 Windows 打包验证文档 | T-031 | `wails3 package` 前置条件、输出目录和常见失败处理写入 README 或 docs | TODO |
| T-033 | 完成一次模板 smoke 清单 | T-023 | 登录、设置、业务样例、日志、统计、组件示例、托盘、热键都按清单验证并记录结果 | TODO |
+10 -2
View File
@@ -6,7 +6,7 @@
项目处于可复用桌面应用模板的 Phase 2 业务样例与日志统计收尾阶段。目标已经从迁移后的基础工程调整为“Windows-first、可扩展到其他平台的桌面端应用模板”。
当前代码已从迁移后的 Wails/Vue/SQLite 基线推进到主应用路由骨架、登录 provider 边界、统一设置 schema、主窗口状态恢复、商品图片 AI 优化页面、日志服务边界和 P0 数据统计阶段,已具备设置、多语言、主题、本地配置、热键、托盘、OCR 示例、模板级模块入口、本地登录能力、本地模拟图片优化任务能力、业务样例页面、审计日志、运行日志和 P0 统计查询能力。统计页面仍是骨架,尚未展示真实统计数据。
当前代码已从迁移后的 Wails/Vue/SQLite 基线推进到主应用路由骨架、登录 provider 边界、统一设置 schema、主窗口状态恢复、商品图片 AI 优化页面、日志服务边界、P0 数据统计和常用组件示例阶段,已具备设置、多语言、主题、本地配置、热键、托盘、OCR 示例、模板级模块入口、本地登录能力、本地模拟图片优化任务能力、业务样例页面、组件示例页、审计日志、运行日志和 P0 统计查询能力。统计页面仍是骨架,尚未展示真实统计数据。
## 已确认事实
@@ -44,6 +44,7 @@
- 已实现 AuditLogService:`Record`、`List`,登录、设置变更和图片任务创建会写入 `audit_logs`。
- 已实现 AppLogService:`Record`、`List`,图片任务创建失败和服务内部错误会写入 `app_logs`。
- 已实现 MetricsService:`GetDashboardMetrics`、`GetProcessingTrend`,可查询今日处理量、成功率、平均耗时、失败数、今日审计日志数和最近 N 天趋势。
- 已实现常用组件示例页:输入框、多选框、下拉框、多行文本、表格、筛选、分页和状态标签都有可运行示例。
## 当前命令
@@ -203,9 +204,16 @@ wails3 build
- WebView 文件选择器不返回绝对路径时,页面会提示用户粘贴绝对路径。
- `go test ./...`、`npm run build`、`go build -o bin\cmbone.exe .` 和 `cmd /c build.bat` 通过。
同日完成常用组件示例页:
- `frontend/src/modules/components-demo/ComponentsDemoPage.vue` 从静态骨架改为可交互示例。
- 表单区支持输入框、多选框、下拉框和多行文本,并实时展示当前值。
- 表格区支持关键字、分类、状态筛选,展示分页和状态标签。
- `go test ./...`、`npm run build`、`go build -o bin\cmbone.exe .` 和 `cmd /c build.bat` 通过。
## 下一步
从 [`06-tasks.md`](06-tasks.md) 领取第一个 `TODO` 任务。目前建议先做 `T-030`:建立常用组件示例页。
从 [`06-tasks.md`](06-tasks.md) 领取第一个 `TODO` 任务。目前建议先做 `T-031`:文档化 Wails bindings 生成流程。
## 当前 blocker
+1 -1
View File
@@ -97,7 +97,7 @@
| `frontend/src/modules/logs/LogsPage.vue` | 日志页骨架 | 审计日志和运行日志入口 |
| `frontend/src/modules/statistics/StatisticsPage.vue` | 统计页骨架 | P0 指标和 7 天趋势入口 |
| `frontend/src/modules/settings/SettingsPage.vue` | 设置页 | 复用存量设置组件,并展示后端统一设置 schema |
| `frontend/src/modules/components-demo/ComponentsDemoPage.vue` | 组件示例骨架 | 常用表单、筛选、表格和状态标签 |
| `frontend/src/modules/components-demo/ComponentsDemoPage.vue` | 组件示例页 | 输入框、多选框、下拉框、多行文本、表格、筛选、分页和状态标签 |
| `frontend/src/pages/Home/Index.vue` | 第二窗口入口 | OCR 示例页面 |
| `frontend/src/utils/ThemeManager.ts` | 主题工具 | 主题切换和持久化协作 |
| `frontend/src/utils/i18n.ts` | 国际化工具 | 语言资源和切换 |
+1 -1
View File
@@ -30,7 +30,7 @@
| `#/logs` | 操作日志 | 后端审计日志和运行日志服务已接入;页面列表待后续扩展 |
| `#/statistics` | 数据统计 | 后端 `MetricsService` 已接入趋势、成功率、耗时和失败任务;页面展示待后续扩展 |
| `#/settings` | 设置中心 | 已接入统一设置 schema,窗口配置已用于主窗口尺寸恢复 |
| `#/components` | 组件示例 | `T-030` 扩充输入框、多选框、下拉框、多行文本、表格、筛选、分页 |
| `#/components` | 组件示例 | 已接入输入框、多选框、下拉框、多行文本、表格、筛选、分页和状态标签 |
## 主窗口布局
@@ -1,21 +1,141 @@
<script setup lang="ts">
import { ref } from 'vue'
import { computed, ref } from 'vue'
import {
LeftOutlined,
ReloadOutlined,
RightOutlined,
SearchOutlined,
} from '@ant-design/icons-vue'
import ModulePageShell from '../../components/template/ModulePageShell.vue'
const checked = ref(true)
type ComponentStatus = 'Ready' | 'Draft' | 'Blocked'
type ComponentRow = {
id: number
name: string
category: string
owner: string
status: ComponentStatus
updatedAt: string
}
const productName = ref('商品主图优化')
const selectedChannels = ref(['desktop', 'audit'])
const category = ref('image')
const rows = [
{ name: '输入框', category: 'Form', status: 'Ready' },
{ name: '多选框', category: 'Form', status: 'Ready' },
{ name: '表格', category: 'Data', status: 'Ready' },
const note = ref('用于记录任务备注、失败原因或人工审核意见。')
const keyword = ref('')
const categoryFilter = ref('')
const statusFilter = ref('')
const currentPage = ref(1)
const pageSize = 5
const channelOptions = [
{ value: 'desktop', label: '桌面端' },
{ value: 'audit', label: '审计日志' },
{ value: 'statistics', label: '数据统计' },
]
const summary = [
{ label: '表单控件', value: '4', tone: 'blue' as const },
{ label: '表格', value: '1', tone: 'green' as const },
{ label: '筛选', value: '1', tone: 'amber' as const },
{ label: '状态标签', value: '3', tone: 'violet' as const },
const categoryOptions = [
{ value: 'image', label: '图片任务' },
{ value: 'logs', label: '日志查询' },
{ value: 'settings', label: '设置项' },
]
const rows: ComponentRow[] = [
{ id: 1, name: '文本输入', category: 'Form', owner: 'Settings', status: 'Ready', updatedAt: '2026-07-09' },
{ id: 2, name: '多选框组', category: 'Form', owner: 'Business', status: 'Ready', updatedAt: '2026-07-09' },
{ id: 3, name: '下拉选择', category: 'Form', owner: 'Settings', status: 'Ready', updatedAt: '2026-07-09' },
{ id: 4, name: '多行文本', category: 'Form', owner: 'Logs', status: 'Ready', updatedAt: '2026-07-09' },
{ id: 5, name: '任务表格', category: 'Data', owner: 'Business', status: 'Ready', updatedAt: '2026-07-09' },
{ id: 6, name: '筛选栏', category: 'Data', owner: 'Logs', status: 'Ready', updatedAt: '2026-07-09' },
{ id: 7, name: '分页控件', category: 'Data', owner: 'Dashboard', status: 'Ready', updatedAt: '2026-07-09' },
{ id: 8, name: '成功状态标签', category: 'Status', owner: 'Business', status: 'Ready', updatedAt: '2026-07-09' },
{ id: 9, name: '草稿状态标签', category: 'Status', owner: 'Settings', status: 'Draft', updatedAt: '2026-07-09' },
{ id: 10, name: '阻塞状态标签', category: 'Status', owner: 'Logs', status: 'Blocked', updatedAt: '2026-07-09' },
{ id: 11, name: '只读字段', category: 'Form', owner: 'Settings', status: 'Draft', updatedAt: '2026-07-09' },
{ id: 12, name: '表格空状态', category: 'Data', owner: 'Dashboard', status: 'Blocked', updatedAt: '2026-07-09' },
]
const summary = computed(() => [
{ label: '表单控件', value: String(rows.filter((row) => row.category === 'Form').length), tone: 'blue' as const },
{ label: '表格行数', value: String(filteredRows.value.length), tone: 'green' as const },
{ label: '当前页', value: `${visiblePage.value}/${totalPages.value}`, tone: 'amber' as const },
{ label: '状态标签', value: String(rows.filter((row) => row.category === 'Status').length), tone: 'violet' as const },
])
const filteredRows = computed(() => {
const term = keyword.value.trim().toLowerCase()
return rows.filter((row) => {
const matchesKeyword = !term
|| row.name.toLowerCase().includes(term)
|| row.owner.toLowerCase().includes(term)
|| row.category.toLowerCase().includes(term)
const matchesCategory = !categoryFilter.value || row.category === categoryFilter.value
const matchesStatus = !statusFilter.value || row.status === statusFilter.value
return matchesKeyword && matchesCategory && matchesStatus
})
})
const totalPages = computed(() => Math.max(1, Math.ceil(filteredRows.value.length / pageSize)))
const visiblePage = computed(() => Math.min(currentPage.value, totalPages.value))
const pagedRows = computed(() => {
const start = (visiblePage.value - 1) * pageSize
return filteredRows.value.slice(start, start + pageSize)
})
const formPreview = computed(() => {
const channelLabels = channelOptions
.filter((option) => selectedChannels.value.includes(option.value))
.map((option) => option.label)
.join('、')
const selectedCategory = categoryOptions.find((option) => option.value === category.value)?.label || '-'
return [
{ label: '名称', value: productName.value || '-' },
{ label: '模块', value: selectedCategory },
{ label: '场景', value: channelLabels || '-' },
{ label: '备注', value: note.value || '-' },
]
})
function applyFilters() {
currentPage.value = 1
}
function resetFilters() {
keyword.value = ''
categoryFilter.value = ''
statusFilter.value = ''
currentPage.value = 1
}
function previousPage() {
currentPage.value = Math.max(1, currentPage.value - 1)
}
function nextPage() {
currentPage.value = Math.min(totalPages.value, currentPage.value + 1)
}
function statusClass(status: ComponentStatus) {
if (status === 'Ready') {
return 'border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-900/60 dark:bg-emerald-950/40 dark:text-emerald-200'
}
if (status === 'Blocked') {
return 'border-rose-200 bg-rose-50 text-rose-700 dark:border-rose-900/60 dark:bg-rose-950/40 dark:text-rose-200'
}
return 'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-900/60 dark:bg-amber-950/40 dark:text-amber-200'
}
function statusLabel(status: ComponentStatus) {
if (status === 'Ready') {
return '可用'
}
if (status === 'Blocked') {
return '阻塞'
}
return '草稿'
}
</script>
<template>
@@ -23,72 +143,200 @@ const summary = [
eyebrow="Components"
title="组件示例"
description="常用业务控件的运行示例,后续按真实模块沉淀组合组件。"
status="路由可访问"
status="示例可运行"
:items="summary"
>
<div class="grid grid-cols-1 gap-4 xl:grid-cols-[360px_1fr]">
<section class="rounded-lg border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-900">
<h2 class="mb-4 text-base font-semibold">
表单
</h2>
<div class="space-y-4">
<label class="block">
<span class="mb-1 block text-sm font-medium">输入框</span>
<input class="h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950" value="商品主图" />
</label>
<label class="flex items-center gap-2 text-sm">
<input v-model="checked" class="h-4 w-4 rounded border-slate-300" type="checkbox" />
<span>多选框</span>
</label>
<label class="block">
<span class="mb-1 block text-sm font-medium">下拉框</span>
<select v-model="category" class="h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950">
<option value="image">图片任务</option>
<option value="logs">日志查询</option>
<option value="settings">设置项</option>
</select>
</label>
<label class="block">
<span class="mb-1 block text-sm font-medium">多行文本</span>
<textarea class="min-h-[96px] w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-700 dark:bg-slate-950">用于记录任务备注。</textarea>
</label>
</div>
</section>
<div class="grid grid-cols-1 gap-4 xl:grid-cols-[360px_minmax(0,1fr)]">
<aside class="space-y-4">
<section class="rounded-lg border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-900">
<h2 class="mb-4 text-base font-semibold text-slate-950 dark:text-white">
表单控件
</h2>
<div class="space-y-4">
<label class="block">
<span class="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-200">输入框</span>
<input
v-model="productName"
class="h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950"
>
</label>
<section class="rounded-lg border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900">
<div class="grid grid-cols-1 gap-3 border-b border-slate-200 p-4 dark:border-slate-800 md:grid-cols-[1fr_180px_auto]">
<input class="h-10 rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950" placeholder="筛选关键字" />
<select class="h-10 rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950">
<option>全部分类</option>
<option>Form</option>
<option>Data</option>
<fieldset class="space-y-2">
<legend class="text-sm font-medium text-slate-700 dark:text-slate-200">
多选框
</legend>
<label
v-for="option in channelOptions"
:key="option.value"
class="flex min-h-[28px] items-center gap-2 text-sm text-slate-700 dark:text-slate-200"
>
<input
v-model="selectedChannels"
class="h-4 w-4 rounded border-slate-300"
type="checkbox"
:value="option.value"
>
<span>{{ option.label }}</span>
</label>
</fieldset>
<label class="block">
<span class="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-200">下拉框</span>
<select
v-model="category"
class="h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950"
>
<option v-for="option in categoryOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</label>
<label class="block">
<span class="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-200">多行文本</span>
<textarea
v-model="note"
class="min-h-[108px] w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-700 dark:bg-slate-950"
/>
</label>
</div>
</section>
<section class="rounded-lg border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-900">
<h2 class="mb-4 text-base font-semibold text-slate-950 dark:text-white">
当前值
</h2>
<dl class="space-y-3 text-sm">
<div v-for="item in formPreview" :key="item.label">
<dt class="text-xs font-medium text-slate-500 dark:text-slate-400">
{{ item.label }}
</dt>
<dd class="mt-1 break-all text-slate-900 dark:text-white">
{{ item.value }}
</dd>
</div>
</dl>
</section>
</aside>
<section class="min-w-0 rounded-lg border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900">
<div class="grid grid-cols-1 gap-3 border-b border-slate-200 p-4 dark:border-slate-800 lg:grid-cols-[minmax(180px,1fr)_160px_160px_auto_auto]">
<input
v-model="keyword"
class="h-10 rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950"
placeholder="筛选关键字"
@keyup.enter="applyFilters"
>
<select
v-model="categoryFilter"
class="h-10 rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950"
@change="applyFilters"
>
<option value="">全部分类</option>
<option value="Form">Form</option>
<option value="Data">Data</option>
<option value="Status">Status</option>
</select>
<button class="h-10 rounded-md bg-slate-950 px-4 text-sm font-medium text-white dark:bg-white dark:text-slate-950" type="button">
筛选
<select
v-model="statusFilter"
class="h-10 rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950"
@change="applyFilters"
>
<option value="">全部状态</option>
<option value="Ready">可用</option>
<option value="Draft">草稿</option>
<option value="Blocked">阻塞</option>
</select>
<button
class="inline-flex h-10 items-center justify-center gap-2 rounded-md bg-slate-950 px-4 text-sm font-medium text-white dark:bg-white dark:text-slate-950"
type="button"
@click="applyFilters"
>
<SearchOutlined />
<span>筛选</span>
</button>
<button
class="inline-flex h-10 items-center justify-center gap-2 rounded-md border border-slate-300 bg-white px-4 text-sm font-medium text-slate-700 hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-200 dark:hover:bg-slate-900"
type="button"
@click="resetFilters"
>
<ReloadOutlined />
<span>重置</span>
</button>
</div>
<div class="overflow-x-auto">
<table class="min-w-full text-left text-sm">
<thead class="bg-slate-50 text-xs uppercase text-slate-500 dark:bg-slate-950 dark:text-slate-400">
<tr>
<th class="px-4 py-3 font-medium">名称</th>
<th class="px-4 py-3 font-medium">分类</th>
<th class="px-4 py-3 font-medium">状态</th>
<th class="w-[90px] px-4 py-3 font-medium">编号</th>
<th class="min-w-[180px] px-4 py-3 font-medium">名称</th>
<th class="w-[130px] px-4 py-3 font-medium">分类</th>
<th class="w-[140px] px-4 py-3 font-medium">归属</th>
<th class="w-[120px] px-4 py-3 font-medium">状态</th>
<th class="w-[140px] px-4 py-3 font-medium">更新时间</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-200 dark:divide-slate-800">
<tr v-for="row in rows" :key="row.name">
<td class="px-4 py-3">{{ row.name }}</td>
<td class="px-4 py-3">{{ row.category }}</td>
<tr v-if="pagedRows.length === 0">
<td class="px-4 py-8 text-center text-slate-500 dark:text-slate-400" colspan="6">
暂无数据
</td>
</tr>
<tr v-for="row in pagedRows" :key="row.id" class="hover:bg-slate-50 dark:hover:bg-slate-950">
<td class="px-4 py-3 font-medium text-slate-900 dark:text-white">
#{{ row.id }}
</td>
<td class="px-4 py-3 text-slate-900 dark:text-white">
{{ row.name }}
</td>
<td class="px-4 py-3 text-slate-600 dark:text-slate-300">
{{ row.category }}
</td>
<td class="px-4 py-3 text-slate-600 dark:text-slate-300">
{{ row.owner }}
</td>
<td class="px-4 py-3">
<span class="rounded-md bg-emerald-50 px-2 py-1 text-xs font-medium text-emerald-700 dark:bg-emerald-950/40 dark:text-emerald-200">
{{ row.status }}
<span class="inline-flex rounded-md border px-2 py-1 text-xs font-medium" :class="statusClass(row.status)">
{{ statusLabel(row.status) }}
</span>
</td>
<td class="px-4 py-3 text-slate-500 dark:text-slate-400">
{{ row.updatedAt }}
</td>
</tr>
</tbody>
</table>
</div>
<div class="flex flex-col gap-3 border-t border-slate-200 p-4 dark:border-slate-800 md:flex-row md:items-center md:justify-between">
<div class="text-sm text-slate-500 dark:text-slate-400">
{{ filteredRows.length }} 条结果
</div>
<div class="flex items-center gap-2">
<button
class="inline-flex h-9 w-9 items-center justify-center rounded-md border border-slate-300 text-slate-700 disabled:cursor-not-allowed disabled:opacity-40 dark:border-slate-700 dark:text-slate-200"
type="button"
:disabled="currentPage === 1"
title="上一页"
@click="previousPage"
>
<LeftOutlined />
</button>
<span class="min-w-[72px] text-center text-sm text-slate-700 dark:text-slate-200">
{{ visiblePage }} / {{ totalPages }}
</span>
<button
class="inline-flex h-9 w-9 items-center justify-center rounded-md border border-slate-300 text-slate-700 disabled:cursor-not-allowed disabled:opacity-40 dark:border-slate-700 dark:text-slate-200"
type="button"
:disabled="currentPage === totalPages"
title="下一页"
@click="nextPage"
>
<RightOutlined />
</button>
</div>
</div>
</section>
</div>
</ModulePageShell>
+17
View File
@@ -324,3 +324,20 @@
- 非阻塞警告:
- `cmd /c build.bat` 仍提示 `wails3 build` 是 `wails3 task` 的 alias。
- 前端构建仍提示 Browserslist 数据过期和 chunk 大小警告。
### 建立常用组件示例页
- 任务:完成 `T-030`,输入框、多选框、下拉框、多行文本、表格、筛选、分页、状态标签有可运行示例。
- 结果:
- `frontend/src/modules/components-demo/ComponentsDemoPage.vue` 从静态骨架改为可交互示例。
- 表单区支持输入框、多选框、下拉框、多行文本,并实时展示当前值。
- 表格区支持关键字、分类、状态筛选。
- 表格底部支持分页,状态列支持可用、草稿、阻塞三类状态标签。
- 验证:
- `cd frontend && npm run build` 通过。
- `go test ./...` 通过。
- `go build -o bin\cmbone.exe .` 通过。
- `cmd /c build.bat` 通过。
- 非阻塞警告:
- `cmd /c build.bat` 仍提示 `wails3 build` 是 `wails3 task` 的 alias。
- 前端构建仍提示 Browserslist 数据过期和 chunk 大小警告。