feat: add main app route skeleton

This commit is contained in:
QiuSW
2026-07-08 23:06:48 +08:00
parent 4c1de99d65
commit 811a9c889f
16 changed files with 849 additions and 30 deletions
+6 -4
View File
@@ -60,8 +60,9 @@ frontend/src
- `frontend/src/modules/settings/`:目标目录,放设置页和设置项。
- `frontend/src/modules/image-optimizer/`:目标目录,放商品图片 AI 优化样例。
- `frontend/src/modules/logs/`:目标目录,放审计日志和运行日志页面。
- `frontend/src/modules/metrics/`:目标目录,放统计卡片和趋势图。
- `frontend/src/modules/statistics/`:放统计卡片和趋势图。
- `frontend/src/modules/components-demo/`:目标目录,放常用组件示例。
- `frontend/src/components/template/`:放当前模块占位页共享壳。
- `frontend/src/components/`:通用展示组件和组合控件。
- `frontend/src/utils/`:国际化、主题、OS、热键等工具函数。
- `frontend/bindings/`:由 Wails 生成或按 Wails 合约维护,不写业务逻辑。
@@ -189,9 +190,10 @@ image_task_results(
## 窗口与页面
- 主窗口路由:`/#/`
- 主窗口路由:`/#/` 重定向到 `/#/dashboard`
- 第二窗口路由:`/#/second`
- 目标主应用路由:登录页、Dashboard、图片优化、日志、统计、设置、组件示例。
- 已实现主应用骨架路由:`/#/login`、`/#/dashboard`、`/#/image-optimizer`、`/#/logs`、`/#/statistics`、`/#/settings`、`/#/components`。
- 兼容目标文档路由:`/#/business/image-optimizer`、`/#/logs/audit`、`/#/logs/app` 会重定向到当前主入口。
- 托盘菜单由 `AppService` 配置。
- 热键触发目标可以是打开主窗口、打开快捷功能或创建快速任务。
@@ -206,7 +208,7 @@ image_task_results(
- 第二窗口保持当前小窗定位,不参与最大化、尺寸恢复和主布局填充逻辑。
- 后续增强再保存主窗口尺寸和最大化状态,下次启动恢复用户选择。
实现状态:主窗口初始尺寸、居中、可调整大小、根容器和主布局填充已落地;下一步是主应用路由骨架。窗口尺寸持久化依赖设置 schema 后续实现。
实现状态:主窗口初始尺寸、居中、可调整大小、根容器和主布局填充已落地;主应用路由骨架已落地。窗口尺寸持久化依赖设置 schema 后续实现。
## 关键风险
+1 -1
View File
@@ -28,7 +28,7 @@
| --- | --- | --- | --- | --- |
| T-010 | 确定 Windows-first 窗口与布局策略 | T-002 | 主窗口默认不最大化,初始 `1280x800`,最小 `1024x720`,居中,可调整大小;页面根容器 `width: 100%`、`height: 100vh`;内部用 flex/grid 填满窗口;第二窗口保持小窗 | DONE |
| T-011 | 实现主窗口启动参数与根布局策略 | T-010 | Go 侧设置主窗口初始宽高、居中、可调整大小;前端根容器和主布局填满窗口;阅读型内容有最大宽度,列表/仪表盘/设置页可自适应拉伸 | DONE |
| T-012 | 搭建主应用路由骨架 | T-011 | `login`、`dashboard`、`image-optimizer`、`logs`、`statistics`、`settings`、`components` 路由可访问 | TODO |
| T-012 | 搭建主应用路由骨架 | T-011 | `login`、`dashboard`、`image-optimizer`、`logs`、`statistics`、`settings`、`components` 路由可访问 | DONE |
| T-013 | 实现登录 provider 边界 | T-012 | 支持本地固定账号 provider;预留 remote provider;账号密码不写死在前端;登录事件写审计日志 | TODO |
| T-014 | 收敛设置模块 schema | T-012 | 主题、语言、窗口行为、快捷键、数据目录、日志策略都有统一配置定义和默认值 | TODO |
| T-015 | 持久化窗口尺寸和最大化状态 | T-014 | 保存主窗口尺寸和最大化状态;下次启动恢复用户上次选择;第二窗口不参与该逻辑 | TODO |
+13 -3
View File
@@ -6,7 +6,7 @@
项目处于可复用桌面应用模板的产品定位校准阶段。目标已经从迁移后的基础工程调整为“Windows-first、可扩展到其他平台的桌面端应用模板”。
当前代码仍是迁移后的 Wails/Vue/SQLite 基线,已具备设置、多语言、主题、本地配置、热键、托盘和 OCR 示例。登录、商品图片 AI 优化、审计日志、运行日志和数据统计仍是目标模块,尚未落地实现。
当前代码已从迁移后的 Wails/Vue/SQLite 基线推进到主应用路由骨架阶段,已具备设置、多语言、主题、本地配置、热键、托盘、OCR 示例和模板级模块入口。登录、商品图片 AI 优化、审计日志、运行日志和数据统计仍是骨架页面,尚未接入真实后端服务和数据模型。
## 已确认事实
@@ -24,7 +24,9 @@
- 前端框架:Vue 3、Vite、TypeScript、Tailwind CSS、Ant Design Vue
- 当前 Wails 前端 bindings:由 `wails3 build` / `wails3 generate bindings` 生成的 `.js` 文件。
- 本地数据库:SQLite,用户配置目录下 `cmbone/cmbone.db`
- 当前主窗口路由:`/#/`,已保留为主应用入口,不再自动跳转到第二窗口。
- 当前主窗口路由:`/#/` 重定向到 `/#/dashboard`。
- 当前主应用模块路由:`/#/login`、`/#/dashboard`、`/#/image-optimizer`、`/#/logs`、`/#/statistics`、`/#/settings`、`/#/components`。
- 当前兼容重定向路由:`/#/business/image-optimizer`、`/#/logs/audit`、`/#/logs/app`。
- 当前第二窗口路由:`/#/second`
- 已实现主窗口策略:默认不最大化,初始 `1280x800`,最小 `1024x720`,居中,可调整大小。
- 已实现页面布局策略:根容器 `width: 100%`、`height: 100vh`,内部 flex/grid 填满窗口。
@@ -114,9 +116,17 @@ wails3 build
- 新增 `npm run typecheck`,正式质量门禁仍由 `npm run build` 覆盖类型检查和生产构建。
- 已验证 `dev.bat` 能进入 `windows:run`,拉起 `bin/cmbone.exe`,并创建可见主窗口。
同日完成主应用路由骨架:
- 新增 `frontend/src/layouts/MainLayout.vue`,主窗口改为侧边导航、顶部窗口区和路由内容区。
- 新增登录、工作台、商品图片 AI 优化、操作日志、数据统计、设置、组件示例模块页。
- 新增 `frontend/src/components/template/ModulePageShell.vue`,用于模块标题、状态和摘要卡片。
- `frontend/src/router/index.ts` 已切换为主应用模块路由,第二窗口继续保留 `/#/second`。
- `npm run typecheck`、`npm run build`、`npm run build:dev`、`go test ./...`、`go build -o bin\cmbone.exe .`、`cmd /c build.bat` 和 `dev.bat` 烟测通过。
## 下一步
从 [`06-tasks.md`](06-tasks.md) 领取第一个 `TODO` 任务。目前建议先做 `T-012`:搭建主应用路由骨架。
从 [`06-tasks.md`](06-tasks.md) 领取第一个 `TODO` 任务。目前建议先做 `T-013`:实现登录 provider 边界。
## 当前 blocker
+11 -3
View File
@@ -48,8 +48,16 @@
| 文件 | 方法 / 内容 | 说明 |
| --- | --- | --- |
| `frontend/src/main.ts` | Vue 入口 | 注册应用、路由和国际化 |
| `frontend/src/router/index.ts` | routes | `#/` 和 `#/second` 路由 |
| `frontend/src/components/Main/Index.vue` | 主窗口入口 | 设置页面总布局 |
| `frontend/src/router/index.ts` | routes | 登录、主应用模块和第二窗口路由 |
| `frontend/src/layouts/MainLayout.vue` | 主窗口布局 | 侧边导航、顶部窗口区和 `RouterView` 内容区 |
| `frontend/src/components/template/ModulePageShell.vue` | 模块页共享壳 | 模块标题、状态和摘要卡片 |
| `frontend/src/modules/auth/LoginPage.vue` | 登录页骨架 | 后续接入 AuthProvider |
| `frontend/src/modules/dashboard/DashboardPage.vue` | 工作台骨架 | P0 指标和近期任务入口 |
| `frontend/src/modules/image-optimizer/ImageOptimizerPage.vue` | 主业务骨架 | 商品图片 AI 优化任务入口 |
| `frontend/src/modules/logs/LogsPage.vue` | 日志页骨架 | 审计日志和运行日志入口 |
| `frontend/src/modules/statistics/StatisticsPage.vue` | 统计页骨架 | P0 指标和 7 天趋势入口 |
| `frontend/src/modules/settings/SettingsPage.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` | 国际化工具 | 语言资源和切换 |
@@ -63,5 +71,5 @@
| `frontend/src/modules/settings/` | 设置中心 |
| `frontend/src/modules/image-optimizer/` | 商品图片 AI 优化业务样例 |
| `frontend/src/modules/logs/` | 审计日志和运行日志 |
| `frontend/src/modules/metrics/` | 数据统计 |
| `frontend/src/modules/statistics/` | 数据统计 |
| `frontend/src/modules/components-demo/` | 常用组件示例 |
+19 -11
View File
@@ -8,22 +8,29 @@
| 路由 | 组件 | 窗口 | 职责 |
| --- | --- | --- | --- |
| `#/` | `frontend/src/components/Main/Index.vue` | 主窗口 | 当前设置中心、导航、主题、语言、快捷键、标签、关于等 |
| `#/` | `frontend/src/layouts/MainLayout.vue` | 主窗口 | 重定向到 `#/dashboard` |
| `#/login` | `frontend/src/modules/auth/LoginPage.vue` | 主窗口 | 登录入口骨架,后续接入本地固定账号 provider 和真实后端 provider |
| `#/dashboard` | `frontend/src/modules/dashboard/DashboardPage.vue` | 主窗口 | 工作台,展示 P0 指标和近期任务入口 |
| `#/image-optimizer` | `frontend/src/modules/image-optimizer/ImageOptimizerPage.vue` | 主窗口 | 商品图片 AI 优化业务样例骨架 |
| `#/logs` | `frontend/src/modules/logs/LogsPage.vue` | 主窗口 | 审计日志和运行日志统一入口骨架 |
| `#/statistics` | `frontend/src/modules/statistics/StatisticsPage.vue` | 主窗口 | 第一批统计指标和趋势骨架 |
| `#/settings` | `frontend/src/modules/settings/SettingsPage.vue` | 主窗口 | 设置中心,当前复用存量主题、语言和快捷键设置 |
| `#/components` | `frontend/src/modules/components-demo/ComponentsDemoPage.vue` | 主窗口 | 常用输入、选择、文本域、表格、筛选和状态标签示例 |
| `#/second` | `frontend/src/pages/Home/Index.vue` | 第二窗口 | 当前 OCR 示例和轻量快捷功能 |
| `#/business/image-optimizer` | redirect | 主窗口 | 兼容目标路由,重定向到 `#/image-optimizer` |
| `#/logs/audit` | redirect | 主窗口 | 兼容目标路由,重定向到 `#/logs` |
| `#/logs/app` | redirect | 主窗口 | 兼容目标路由,重定向到 `#/logs` |
## 目标路由
## 目标路由演进
| 路由 | 页面 | 职责 |
| --- | --- | --- |
| `#/login` | 登录页 | 本地固定账号登录或真实后端登录 |
| `#/dashboard` | 工作台 | 展示 P0 统计指标和最近任务 |
| `#/business/image-optimizer` | 商品图片 AI 优化 | 创建优化任务、查看任务列表、状态、结果和失败原因 |
| `#/logs/audit` | 审计日志 | 查询用户操作记录 |
| `#/logs/app` | 运行日志 | 查询系统错误和排错信息 |
| `#/statistics` | 数据统计 | 查看趋势、成功率、耗时和失败任务 |
| `#/settings` | 设置中心 | 主题、语言、窗口、快捷键、数据目录、日志策略 |
| `#/components` | 组件示例 | 输入框、多选框、下拉框、多行文本、表格、筛选、分页 |
| `#/about` | 关于 | 应用版本、运行环境和文档入口 |
| `#/login` | 登录页 | `T-013` 接入本地固定账号登录或真实后端登录 |
| `#/image-optimizer` | 商品图片 AI 优化 | `T-020` / `T-023` 接入任务模型、任务列表、状态、结果和失败原因 |
| `#/logs` | 操作日志 | `T-021` 接入审计日志和运行日志查询 |
| `#/statistics` | 数据统计 | `T-022` 接入趋势、成功率、耗时和失败任务 |
| `#/settings` | 设置中心 | `T-014` 接入统一设置 schema |
| `#/components` | 组件示例 | `T-030` 扩充输入框、多选框、下拉框、多行文本、表格、筛选、分页 |
## 主窗口布局
@@ -55,3 +62,4 @@ Windows 10+ 是主要体验目标。主窗口策略已经实现到当前代码
- 第二窗口保持轻量快捷功能,不承载长期复杂业务流程。
- 页面内容不得只占左上角小块;除阅读型内容外,默认应填满主内容区域。
- 路由变化必须同步本文和相关任务验收。
- 主窗口模块页默认放在 `frontend/src/modules/`,只把跨模块复用 UI 放到 `frontend/src/components/`。
@@ -0,0 +1,79 @@
<script setup lang="ts">
type SummaryTone = 'blue' | 'green' | 'amber' | 'red' | 'slate' | 'violet'
type SummaryItem = {
label: string
value: string
helper?: string
tone?: SummaryTone
}
withDefaults(defineProps<{
eyebrow?: string
title: string
description: string
status?: string
items?: SummaryItem[]
}>(), {
eyebrow: '',
status: '骨架已就绪',
items: () => [],
})
const toneClasses: Record<SummaryTone, string> = {
blue: 'border-blue-200 bg-blue-50 text-blue-900 dark:border-blue-900/50 dark:bg-blue-950/40 dark:text-blue-100',
green: 'border-emerald-200 bg-emerald-50 text-emerald-900 dark:border-emerald-900/50 dark:bg-emerald-950/40 dark:text-emerald-100',
amber: 'border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-900/50 dark:bg-amber-950/40 dark:text-amber-100',
red: 'border-rose-200 bg-rose-50 text-rose-900 dark:border-rose-900/50 dark:bg-rose-950/40 dark:text-rose-100',
slate: 'border-slate-200 bg-white text-slate-900 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100',
violet: 'border-violet-200 bg-violet-50 text-violet-900 dark:border-violet-900/50 dark:bg-violet-950/40 dark:text-violet-100',
}
function toneClass(tone?: SummaryTone) {
return toneClasses[tone || 'slate']
}
</script>
<template>
<section class="min-h-full w-full">
<div class="flex w-full flex-col gap-4">
<header class="flex flex-col gap-3 border-b border-slate-200 pb-4 dark:border-slate-800 md:flex-row md:items-start md:justify-between">
<div class="min-w-0">
<p v-if="eyebrow" class="mb-1 text-xs font-semibold uppercase text-slate-500 dark:text-slate-400">
{{ eyebrow }}
</p>
<h1 class="mb-1 text-2xl font-semibold text-slate-950 dark:text-white">
{{ title }}
</h1>
<p class="max-w-3xl text-sm leading-6 text-slate-600 dark:text-slate-300">
{{ description }}
</p>
</div>
<span v-if="status" class="inline-flex shrink-0 items-center rounded-md border border-slate-200 bg-white px-3 py-1 text-xs font-medium text-slate-600 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300">
{{ status }}
</span>
</header>
<div v-if="items.length" class="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-4">
<div
v-for="item in items"
:key="item.label"
class="rounded-lg border p-4"
:class="toneClass(item.tone)"
>
<p class="text-xs font-medium opacity-75">
{{ item.label }}
</p>
<p class="mt-2 text-2xl font-semibold">
{{ item.value }}
</p>
<p v-if="item.helper" class="mt-1 text-xs opacity-70">
{{ item.helper }}
</p>
</div>
</div>
<slot />
</div>
</section>
</template>
+192
View File
@@ -0,0 +1,192 @@
<script setup lang="ts">
import { computed, onMounted, ref, type Component } from 'vue'
import { RouterLink, RouterView, useRoute } from 'vue-router'
import {
BorderOutlined,
BulbOutlined,
CloseOutlined,
DashboardOutlined,
ExceptionOutlined,
FileDoneOutlined,
LineOutlined,
ProfileOutlined,
ScheduleOutlined,
SettingOutlined,
SwitcherOutlined,
TagsOutlined,
} from '@ant-design/icons-vue'
import { Window } from '@wailsio/runtime'
import { OpenSecondWindow } from '../../bindings/cmbone/internal/services/appservice'
import { getOS, OS_READY } from '../utils/osinfo'
type NavItem = {
path: string
label: string
detail: string
icon: Component
}
const route = useRoute()
const isWindows = ref(false)
const isMacOS = ref(false)
const isMaximized = ref(false)
const navItems: NavItem[] = [
{ path: '/dashboard', label: '工作台', detail: '指标与近期任务', icon: DashboardOutlined },
{ path: '/image-optimizer', label: '图片优化', detail: '电商商品图样例', icon: TagsOutlined },
{ path: '/logs', label: '操作日志', detail: '审计与排错', icon: ScheduleOutlined },
{ path: '/statistics', label: '数据统计', detail: '趋势和成功率', icon: FileDoneOutlined },
{ path: '/settings', label: '设置', detail: '主题、语言、快捷键', icon: SettingOutlined },
{ path: '/components', label: '组件示例', detail: '表单和表格', icon: BulbOutlined },
]
const currentNav = computed(() => navItems.find(isActive) || navItems[0])
function isActive(item: NavItem) {
return route.path === item.path || route.path.startsWith(`${item.path}/`)
}
async function toggleMaximize() {
await Window.ToggleMaximise()
isMaximized.value = await Window.IsMaximised()
}
onMounted(async () => {
await OS_READY
const osName = getOS()
isMacOS.value = osName === 'darwin'
isWindows.value = osName === 'windows'
if (isWindows.value) {
isMaximized.value = await Window.IsMaximised()
}
})
</script>
<template>
<div class="flex h-screen w-full min-w-0 overflow-hidden bg-slate-100 font-sans text-slate-950 dark:bg-slate-950 dark:text-white">
<aside
class="flex w-64 shrink-0 flex-col border-r border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900"
:class="isMacOS ? 'pt-10' : 'pt-4'"
>
<div class="border-b border-slate-200 px-4 pb-4 dark:border-slate-800">
<div class="text-base font-semibold text-slate-950 dark:text-white">
cmbone
</div>
<div class="mt-1 text-xs text-slate-500 dark:text-slate-400">
桌面应用模板
</div>
</div>
<nav class="min-h-0 flex-1 overflow-y-auto px-3 py-3">
<RouterLink
v-for="item in navItems"
:key="item.path"
:to="item.path"
class="mb-1 flex min-h-[54px] items-center gap-3 rounded-lg px-3 py-2 transition-colors"
:class="isActive(item)
? 'bg-slate-950 text-white shadow-sm dark:bg-white dark:text-slate-950'
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-950 dark:text-slate-300 dark:hover:bg-slate-800 dark:hover:text-white'"
>
<component :is="item.icon" class="shrink-0 text-base" />
<span class="min-w-0">
<span class="block truncate text-sm font-medium">{{ item.label }}</span>
<span class="block truncate text-xs opacity-70">{{ item.detail }}</span>
</span>
</RouterLink>
</nav>
<div class="border-t border-slate-200 p-4 dark:border-slate-800">
<div class="flex items-center gap-3">
<div class="flex h-9 w-9 items-center justify-center rounded-lg bg-slate-950 text-sm font-semibold text-white dark:bg-white dark:text-slate-950">
C
</div>
<div class="min-w-0">
<p class="mb-0 truncate text-sm font-medium text-slate-900 dark:text-white">
本地演示用户
</p>
<p class="mb-0 truncate text-xs text-slate-500 dark:text-slate-400">
Local Provider
</p>
</div>
</div>
</div>
</aside>
<section class="flex min-w-0 flex-1 flex-col">
<header
class="flex h-12 shrink-0 items-center justify-between border-b border-slate-200 bg-white px-4 dark:border-slate-800 dark:bg-slate-900"
:class="isWindows ? 'drag-region' : ''"
>
<div class="drag-area min-w-0">
<p class="mb-0 truncate text-sm font-medium text-slate-700 dark:text-slate-200">
{{ currentNav.label }}
</p>
</div>
<div class="no-drag flex items-center gap-1">
<button class="toolbar-button" title="打开第二窗口" @click="OpenSecondWindow">
<ExceptionOutlined />
</button>
<RouterLink class="toolbar-button" title="登录" to="/login">
<ProfileOutlined />
</RouterLink>
<template v-if="isWindows">
<button class="toolbar-button" title="最小化" @click="Window.Minimise()">
<LineOutlined />
</button>
<button class="toolbar-button" title="最大化" @click="toggleMaximize">
<SwitcherOutlined v-if="isMaximized" />
<BorderOutlined v-else />
</button>
<button class="toolbar-button hover:bg-rose-50 hover:text-rose-600 dark:hover:bg-rose-950/40" title="隐藏窗口" @click="Window.Hide()">
<CloseOutlined />
</button>
</template>
</div>
</header>
<main class="min-h-0 flex-1 overflow-y-auto p-4">
<RouterView />
</main>
</section>
</div>
</template>
<style scoped>
.drag-region {
-webkit-app-region: drag;
--wails-draggable: drag;
}
.drag-area {
-webkit-app-region: drag;
--wails-draggable: drag;
}
.no-drag,
.toolbar-button {
-webkit-app-region: no-drag;
--wails-draggable: no-drag;
}
.toolbar-button {
display: inline-flex;
width: 32px;
height: 32px;
align-items: center;
justify-content: center;
border-radius: 6px;
border: 0;
background: transparent;
color: inherit;
cursor: pointer;
}
.toolbar-button:hover {
background: rgb(241 245 249);
}
:global(.dark) .toolbar-button:hover {
background: rgb(30 41 59);
}
</style>
+58
View File
@@ -0,0 +1,58 @@
<script setup lang="ts">
import { ref } from 'vue'
import { RouterLink, useRouter } from 'vue-router'
import { DashboardOutlined } from '@ant-design/icons-vue'
const router = useRouter()
const username = ref('')
const password = ref('')
const provider = ref('local')
function submitLogin() {
router.push('/dashboard')
}
</script>
<template>
<main class="flex h-screen w-full items-center justify-center bg-slate-100 p-6 text-slate-950 dark:bg-slate-950 dark:text-white">
<section class="w-full max-w-md rounded-lg border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900">
<div class="mb-6 flex items-center justify-between gap-4">
<div>
<h1 class="mb-1 text-2xl font-semibold">
登录
</h1>
<p class="mb-0 text-sm text-slate-500 dark:text-slate-400">
本地和真实后端 provider 的入口
</p>
</div>
<RouterLink class="inline-flex h-9 w-9 items-center justify-center rounded-md border border-slate-200 text-slate-600 hover:bg-slate-100 dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-800" to="/dashboard" title="返回工作台">
<DashboardOutlined />
</RouterLink>
</div>
<form class="space-y-4" @submit.prevent="submitLogin">
<label class="block">
<span class="mb-1 block text-sm font-medium">Provider</span>
<select v-model="provider" class="h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm outline-none focus:border-slate-500 dark:border-slate-700 dark:bg-slate-950">
<option value="local">Local</option>
<option value="remote">Remote</option>
</select>
</label>
<label class="block">
<span class="mb-1 block text-sm font-medium">账号</span>
<input v-model="username" class="h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm outline-none focus:border-slate-500 dark:border-slate-700 dark:bg-slate-950" autocomplete="username" />
</label>
<label class="block">
<span class="mb-1 block text-sm font-medium">密码</span>
<input v-model="password" class="h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm outline-none focus:border-slate-500 dark:border-slate-700 dark:bg-slate-950" type="password" autocomplete="current-password" />
</label>
<button class="h-10 w-full rounded-md bg-slate-950 text-sm font-medium text-white hover:bg-slate-800 dark:bg-white dark:text-slate-950 dark:hover:bg-slate-200" type="submit">
进入应用
</button>
</form>
</section>
</main>
</template>
@@ -0,0 +1,95 @@
<script setup lang="ts">
import { ref } from 'vue'
import ModulePageShell from '../../components/template/ModulePageShell.vue'
const checked = ref(true)
const category = ref('image')
const rows = [
{ name: '输入框', category: 'Form', status: 'Ready' },
{ name: '多选框', category: 'Form', status: 'Ready' },
{ name: '表格', category: 'Data', status: 'Ready' },
]
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 },
]
</script>
<template>
<ModulePageShell
eyebrow="Components"
title="组件示例"
description="常用业务控件的运行示例,后续按真实模块沉淀组合组件。"
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>
<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>
</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">
筛选
</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>
</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>
<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>
</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
</ModulePageShell>
</template>
@@ -0,0 +1,72 @@
<script setup lang="ts">
import ModulePageShell from '../../components/template/ModulePageShell.vue'
const metrics = [
{ label: '今日处理图片', value: '0', helper: '等待任务模型', tone: 'blue' as const },
{ label: 'AI 优化成功率', value: '0%', helper: '等待结果数据', tone: 'green' as const },
{ label: '平均处理耗时', value: '0 ms', helper: '等待运行记录', tone: 'amber' as const },
{ label: '失败任务数', value: '0', helper: '等待错误记录', tone: 'red' as const },
]
const recentItems = [
{ title: '商品图片 AI 优化', status: '目标业务样例', time: 'T-020' },
{ title: '审计日志', status: '目标模块', time: 'T-021' },
{ title: 'P0 数据统计', status: '目标模块', time: 'T-022' },
]
</script>
<template>
<ModulePageShell
eyebrow="Dashboard"
title="工作台"
description="模板主入口,汇总 P0 指标、近期任务和运行状态。"
status="路由可访问"
:items="metrics"
>
<div class="grid grid-cols-1 gap-4 xl:grid-cols-[1fr_360px]">
<section class="rounded-lg border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-900">
<div class="mb-3 flex items-center justify-between">
<h2 class="mb-0 text-base font-semibold text-slate-950 dark:text-white">
近期任务
</h2>
<span class="text-xs text-slate-500 dark:text-slate-400">Phase 2</span>
</div>
<div class="divide-y divide-slate-200 dark:divide-slate-800">
<div v-for="item in recentItems" :key="item.title" class="grid grid-cols-[1fr_auto] gap-3 py-3">
<div class="min-w-0">
<p class="mb-1 truncate text-sm font-medium text-slate-900 dark:text-white">
{{ item.title }}
</p>
<p class="mb-0 text-xs text-slate-500 dark:text-slate-400">
{{ item.status }}
</p>
</div>
<span class="text-xs font-medium text-slate-500 dark:text-slate-400">
{{ item.time }}
</span>
</div>
</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-3 text-base font-semibold text-slate-950 dark:text-white">
运行状态
</h2>
<div class="space-y-3 text-sm">
<div class="flex justify-between gap-3">
<span class="text-slate-500 dark:text-slate-400">主窗口</span>
<span class="font-medium text-slate-900 dark:text-white">Ready</span>
</div>
<div class="flex justify-between gap-3">
<span class="text-slate-500 dark:text-slate-400">本地数据库</span>
<span class="font-medium text-slate-900 dark:text-white">SQLite</span>
</div>
<div class="flex justify-between gap-3">
<span class="text-slate-500 dark:text-slate-400">目标平台</span>
<span class="font-medium text-slate-900 dark:text-white">Windows 10+</span>
</div>
</div>
</section>
</div>
</ModulePageShell>
</template>
@@ -0,0 +1,76 @@
<script setup lang="ts">
import ModulePageShell from '../../components/template/ModulePageShell.vue'
const summary = [
{ label: '待处理', value: '0', tone: 'slate' as const },
{ label: '处理中', value: '0', tone: 'blue' as const },
{ label: '成功', value: '0', tone: 'green' as const },
{ label: '失败', value: '0', tone: 'red' as const },
]
</script>
<template>
<ModulePageShell
eyebrow="Business"
title="商品图片 AI 优化"
description="第一个主业务样例,用于沉淀任务创建、状态流转、结果预览和失败反馈。"
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="block w-full text-sm text-slate-600 file:mr-3 file:rounded-md file:border-0 file:bg-slate-950 file:px-3 file:py-2 file:text-sm file:font-medium file:text-white dark:text-slate-300 dark:file:bg-white dark:file:text-slate-950" type="file" accept="image/*" />
</label>
<label class="block">
<span class="mb-1 block text-sm font-medium">优化类型</span>
<select 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>清晰度增强</option>
<option>背景优化</option>
<option>压缩导出</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>
<button class="h-10 w-full rounded-md bg-slate-950 text-sm font-medium text-white dark:bg-white dark:text-slate-950" type="button">
创建任务
</button>
</div>
</section>
<section class="rounded-lg border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900">
<div class="border-b border-slate-200 p-4 dark:border-slate-800">
<h2 class="mb-0 text-base font-semibold">
任务列表
</h2>
</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="px-4 py-3 font-medium">耗时</th>
</tr>
</thead>
<tbody>
<tr>
<td class="px-4 py-6 text-slate-500 dark:text-slate-400" colspan="4">
暂无任务
</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
</ModulePageShell>
</template>
+65
View File
@@ -0,0 +1,65 @@
<script setup lang="ts">
import ModulePageShell from '../../components/template/ModulePageShell.vue'
const summary = [
{ label: '今日审计日志', value: '0', tone: 'blue' as const },
{ label: '运行错误', value: '0', tone: 'red' as const },
{ label: '最近登录', value: '-', tone: 'slate' as const },
{ label: '最近任务失败', value: '-', tone: 'amber' as const },
]
const rows = [
{ type: '审计', module: 'auth', message: '登录事件', time: '-' },
{ type: '运行', module: 'image-optimizer', message: '任务错误', time: '-' },
]
</script>
<template>
<ModulePageShell
eyebrow="Logs"
title="操作日志"
description="审计日志用于追踪用户动作,运行日志用于定位系统问题。"
status="路由可访问"
:items="summary"
>
<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-4">
<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>审计</option>
<option>运行</option>
</select>
<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>auth</option>
<option>image-optimizer</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">
查询
</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="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.type}-${row.module}`">
<td class="px-4 py-3">{{ row.type }}</td>
<td class="px-4 py-3">{{ row.module }}</td>
<td class="px-4 py-3">{{ row.message }}</td>
<td class="px-4 py-3 text-slate-500 dark:text-slate-400">{{ row.time }}</td>
</tr>
</tbody>
</table>
</div>
</section>
</ModulePageShell>
</template>
@@ -0,0 +1,25 @@
<script setup lang="ts">
import ModulePageShell from '../../components/template/ModulePageShell.vue'
import GeneralSettings from '../../components/General/Index.vue'
const summary = [
{ label: '主题', value: '可配置', tone: 'slate' as const },
{ label: '语言', value: '可配置', tone: 'blue' as const },
{ label: '快捷键', value: '已接入', tone: 'green' as const },
{ label: '窗口行为', value: '待 schema', tone: 'amber' as const },
]
</script>
<template>
<ModulePageShell
eyebrow="Settings"
title="设置中心"
description="设置模块承载主题、语言、窗口行为、快捷键、数据目录和日志策略。"
status="路由可访问"
:items="summary"
>
<section class="rounded-lg border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-900">
<GeneralSettings />
</section>
</ModulePageShell>
</template>
@@ -0,0 +1,47 @@
<script setup lang="ts">
import ModulePageShell from '../../components/template/ModulePageShell.vue'
const summary = [
{ label: '今日处理图片数', value: '0', tone: 'blue' as const },
{ label: 'AI 优化成功率', value: '0%', tone: 'green' as const },
{ label: '平均处理耗时', value: '0 ms', tone: 'amber' as const },
{ label: '今日操作日志数', value: '0', tone: 'violet' as const },
]
const trend = [
{ day: 'D-6', value: 20 },
{ day: 'D-5', value: 35 },
{ day: 'D-4', value: 28 },
{ day: 'D-3', value: 52 },
{ day: 'D-2', value: 44 },
{ day: 'D-1', value: 62 },
{ day: 'Today', value: 10 },
]
</script>
<template>
<ModulePageShell
eyebrow="Statistics"
title="数据统计"
description="第一批指标覆盖处理量、成功率、耗时、失败数、操作日志和最近 7 天趋势。"
status="路由可访问"
:items="summary"
>
<section class="rounded-lg border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-900">
<div class="mb-4 flex items-center justify-between">
<h2 class="mb-0 text-base font-semibold">
最近 7 天处理趋势
</h2>
<span class="text-xs text-slate-500 dark:text-slate-400">Mock</span>
</div>
<div class="flex h-64 items-end gap-3">
<div v-for="item in trend" :key="item.day" class="flex min-w-0 flex-1 flex-col items-center gap-2">
<div class="flex w-full items-end rounded-md bg-slate-100 p-1 dark:bg-slate-950" style="height: 210px">
<div class="w-full rounded-md bg-slate-900 dark:bg-slate-200" :style="{ height: `${item.value}%` }"></div>
</div>
<span class="truncate text-xs text-slate-500 dark:text-slate-400">{{ item.day }}</span>
</div>
</div>
</section>
</ModulePageShell>
</template>
+67 -8
View File
@@ -1,13 +1,72 @@
import { createRouter, createWebHashHistory} from 'vue-router'
import MainPage from '../components/Main/Index.vue'
import SecondPage from '../pages/Home/Index.vue'
import { createRouter, createWebHashHistory, type RouteRecordRaw } from 'vue-router'
const routes = [
{ path: '/', component: MainPage },
{ path: '/second', component: SecondPage }
const routes: RouteRecordRaw[] = [
{
path: '/login',
name: 'login',
component: () => import('../modules/auth/LoginPage.vue'),
},
{
path: '/second',
name: 'second',
component: () => import('../pages/Home/Index.vue'),
},
{
path: '/',
component: () => import('../layouts/MainLayout.vue'),
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'dashboard',
component: () => import('../modules/dashboard/DashboardPage.vue'),
},
{
path: 'image-optimizer',
name: 'image-optimizer',
component: () => import('../modules/image-optimizer/ImageOptimizerPage.vue'),
},
{
path: 'business/image-optimizer',
redirect: '/image-optimizer',
},
{
path: 'logs',
name: 'logs',
component: () => import('../modules/logs/LogsPage.vue'),
},
{
path: 'logs/audit',
redirect: '/logs',
},
{
path: 'logs/app',
redirect: '/logs',
},
{
path: 'statistics',
name: 'statistics',
component: () => import('../modules/statistics/StatisticsPage.vue'),
},
{
path: 'settings',
name: 'settings',
component: () => import('../modules/settings/SettingsPage.vue'),
},
{
path: 'components',
name: 'components',
component: () => import('../modules/components-demo/ComponentsDemoPage.vue'),
},
],
},
{
path: '/:pathMatch(.*)*',
redirect: '/dashboard',
},
]
export default createRouter({
history: createWebHashHistory(), // Use createWebHashHistory for hash-based routing
history: createWebHashHistory(),
routes
})
})
+23
View File
@@ -150,3 +150,26 @@
- `cmd /c build.bat` 仍提示 `wails3 build` 是 `wails3 task` 的 alias。
- `npm install` 仍报告 3 个 moderate audit findings。
- 前端构建仍提示 Browserslist 数据过期和 chunk 大小警告。
### 搭建主应用路由骨架
- 任务:完成 `T-012`,让 `login`、`dashboard`、`image-optimizer`、`logs`、`statistics`、`settings`、`components` 路由可访问。
- 结果:
- 新增 `frontend/src/layouts/MainLayout.vue`,主窗口由侧边导航、顶部窗口区和路由内容区组成。
- 新增 `frontend/src/components/template/ModulePageShell.vue`,复用模块标题、状态和摘要卡片。
- 新增 `frontend/src/modules/auth/LoginPage.vue`、`dashboard/DashboardPage.vue`、`image-optimizer/ImageOptimizerPage.vue`、`logs/LogsPage.vue`、`statistics/StatisticsPage.vue`、`settings/SettingsPage.vue`、`components-demo/ComponentsDemoPage.vue`。
- `frontend/src/router/index.ts` 切换为 hash 路由骨架,`/#/` 重定向到 `/#/dashboard`,`/#/second` 继续用于第二窗口。
- `/#/business/image-optimizer`、`/#/logs/audit`、`/#/logs/app` 暂时重定向到当前主入口。
- 更新路由、架构、方法地图、当前状态和任务看板文档。
- 验证:
- `cd frontend && npm run typecheck` 通过。
- `cd frontend && npm run build` 通过。
- `cd frontend && npm run build:dev` 通过。
- `go test ./...` 通过。
- `go build -o bin\cmbone.exe .` 通过。
- `cmd /c build.bat` 通过。
- `dev.bat` 烟测通过,主窗口 `main` 可见,日志显示加载 `frontend/src/layouts/MainLayout.vue` 和 `frontend/src/modules/dashboard/DashboardPage.vue`。
- 非阻塞警告:
- 并行运行 `npm run build:dev` 和 `go test/go build` 会因为 `frontend/dist` 被重写导致 Go embed 读取旧 hash 文件失败;正确做法是前端构建完成后再运行 Go 命令。
- `dev.bat` 日志仍有 `SetProcessDpiAwarenessContext failed ... Access is denied`,不影响窗口创建和页面加载。
- Browserslist 数据过期、Vite chunk 体积警告和 `wails3 build` alias 警告仍存在。