1116 lines
27 KiB
Markdown
1116 lines
27 KiB
Markdown
# 前端系统性重构 - 白色体系设计规范实施
|
||
|
||
> **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:** 基于前端优化方案与设计规范文档,对前端项目进行系统性重构,统一白色色彩体系,创建通用组件库,优化信息密度,提升用户体验。
|
||
|
||
**Architecture:** 采用分层重构策略:先建立设计令牌系统(CSS变量)→ 创建通用组件库 → 重构全局样式 → 重构导航系统 → 逐个模块替换为通用组件。确保每一步都可独立验证,保持功能完整性。
|
||
|
||
**Tech Stack:** Vue 3 (Composition API / `<script setup>`), Vite, Ant Design Vue 4, CSS Variables
|
||
|
||
---
|
||
|
||
## 文件结构总览
|
||
|
||
### 新建文件
|
||
| 文件路径 | 职责 |
|
||
|---------|------|
|
||
| `src/design-tokens.css` | 全局CSS设计令牌(颜色、间距、字体、圆角、阴影) |
|
||
| `src/components/common/BasePage.vue` | 统一页面容器组件 |
|
||
| `src/components/common/PageHeader.vue` | 统一页面标题栏组件 |
|
||
| `src/components/common/TabsBar.vue` | 统一标签栏组件 |
|
||
| `src/components/common/ContentCard.vue` | 统一内容卡片组件 |
|
||
| `src/components/common/StatsRow.vue` | 统计卡片行组件 |
|
||
| `src/components/common/DataTable.vue` | 统一数据表格组件 |
|
||
|
||
### 修改文件
|
||
| 文件路径 | 修改内容 |
|
||
|---------|---------|
|
||
| `src/main.js` | 引入 design-tokens.css |
|
||
| `src/style.css` | 重写为白色体系,移除彩色渐变,应用紧凑尺寸 |
|
||
| `src/index.css` | 简化为基础重置样式 |
|
||
| `src/views/Home.vue` | 导航从下拉框改为顶部标签栏 |
|
||
| `src/components/ReadModule.vue` | 使用通用组件替换重复结构 |
|
||
| `src/components/ExamModule.vue` | 使用通用组件替换重复结构 |
|
||
| `src/components/ManageModule.vue` | 使用通用组件替换重复结构 |
|
||
| `src/components/QAModule.vue` | 使用通用组件替换重复结构 |
|
||
| `src/components/MindModule.vue` | 使用通用组件替换重复结构 |
|
||
| `src/components/DashboardModule.vue` | 使用通用组件替换重复结构 |
|
||
| `src/components/PermissionModule.vue` | 使用通用组件替换重复结构 |
|
||
|
||
---
|
||
|
||
### Task 1: 创建设计令牌系统 (design-tokens.css)
|
||
|
||
**Files:**
|
||
- Create: `src/design-tokens.css`
|
||
- Modify: `src/main.js`
|
||
|
||
- [ ] **Step 1: 创建 design-tokens.css 文件**
|
||
|
||
```css
|
||
/* ========================================
|
||
设计令牌系统 - 白色体系
|
||
基于:前端优化方案与设计规范文档
|
||
======================================== */
|
||
|
||
:root {
|
||
/* ===== 背景色(白→灰度层级)===== */
|
||
--bg-page: #FFFFFF;
|
||
--bg-container: #F8F9FA;
|
||
--bg-card: #FFFFFF;
|
||
--bg-hover: #F1F3F5;
|
||
--bg-active: #E9ECEF;
|
||
--bg-disabled: #F8F9FA;
|
||
--bg-header: #FFFFFF;
|
||
--bg-sidebar: #F8F9FA;
|
||
|
||
/* ===== 边框色 ===== */
|
||
--border-light: #E9ECEF;
|
||
--border-default: #DEE2E6;
|
||
--border-medium: #CED4DA;
|
||
--border-dark: #ADB5BD;
|
||
|
||
/* ===== 文字色(灰度层级)===== */
|
||
--text-primary: #212529;
|
||
--text-secondary: #495057;
|
||
--text-tertiary: #6C757D;
|
||
--text-disabled: #ADB5BD;
|
||
--text-inverse: #FFFFFF;
|
||
|
||
/* ===== 功能色(仅状态标识)===== */
|
||
--color-success: #2B8A3E;
|
||
--color-warning: #E67700;
|
||
--color-error: #C92A2A;
|
||
--color-info: #1C7ED6;
|
||
|
||
/* ===== 功能色浅底 ===== */
|
||
--bg-success: #EBFBEE;
|
||
--bg-warning: #FFF9DB;
|
||
--bg-error: #FFF5F5;
|
||
--bg-info: #E7F5FF;
|
||
|
||
/* ===== 主色调(品牌色)===== */
|
||
--color-primary: #212529;
|
||
--color-primary-hover: #495057;
|
||
--color-primary-active: #000000;
|
||
--color-link: #1C7ED6;
|
||
|
||
/* ===== 阴影层级 ===== */
|
||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||
--shadow-md: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||
--shadow-xl: 0 8px 32px rgba(0, 0, 0, 0.10);
|
||
|
||
/* ===== 间距系统 ===== */
|
||
--space-xs: 4px;
|
||
--space-sm: 8px;
|
||
--space-md: 12px;
|
||
--space-lg: 16px;
|
||
--space-xl: 20px;
|
||
--space-2xl: 24px;
|
||
--space-3xl: 32px;
|
||
|
||
/* ===== 圆角系统 ===== */
|
||
--radius-sm: 4px;
|
||
--radius-md: 6px;
|
||
--radius-lg: 8px;
|
||
--radius-xl: 12px;
|
||
|
||
/* ===== 字体大小 ===== */
|
||
--font-xs: 11px;
|
||
--font-sm: 12px;
|
||
--font-md: 13px;
|
||
--font-lg: 14px;
|
||
--font-xl: 16px;
|
||
--font-2xl: 18px;
|
||
--font-3xl: 22px;
|
||
|
||
/* ===== 字体粗细 ===== */
|
||
--weight-normal: 400;
|
||
--weight-medium: 500;
|
||
--weight-semibold: 600;
|
||
--weight-bold: 700;
|
||
|
||
/* ===== 组件尺寸 ===== */
|
||
--header-height: 48px;
|
||
--page-header-height: 56px;
|
||
--stat-card-height: 72px;
|
||
--card-padding: 16px;
|
||
--table-row-height: 44px;
|
||
--form-gap: 16px;
|
||
--tab-btn-padding-v: 6px;
|
||
--tab-btn-padding-h: 14px;
|
||
--input-height: 36px;
|
||
|
||
/* ===== 过渡动画 ===== */
|
||
--transition-fast: 0.15s ease;
|
||
--transition-normal: 0.2s ease;
|
||
|
||
/* ===== 层级 ===== */
|
||
--z-dropdown: 1000;
|
||
--z-modal: 1050;
|
||
--z-tooltip: 1100;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 在 main.js 中引入 design-tokens.css**
|
||
|
||
在 `src/main.js` 中,在 `'./index.css'` 之后添加:
|
||
|
||
```javascript
|
||
import './design-tokens.css'
|
||
```
|
||
|
||
---
|
||
|
||
### Task 2: 创建通用组件库
|
||
|
||
#### Task 2.1: 创建 PageHeader 组件
|
||
|
||
**Files:**
|
||
- Create: `src/components/common/PageHeader.vue`
|
||
|
||
- [ ] **Step 1: 创建 PageHeader.vue**
|
||
|
||
```vue
|
||
<template>
|
||
<div class="page-header">
|
||
<div class="header-left">
|
||
<h1 v-if="title" class="page-title">{{ title }}</h1>
|
||
<p v-if="description" class="page-desc">{{ description }}</p>
|
||
</div>
|
||
<div v-if="$slots.actions" class="header-right">
|
||
<slot name="actions" />
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
defineProps({
|
||
title: { type: String, default: '' },
|
||
description: { type: String, default: '' }
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.page-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: var(--space-md) var(--space-xl);
|
||
background: var(--bg-header);
|
||
border-bottom: 1px solid var(--border-light);
|
||
min-height: var(--page-header-height);
|
||
}
|
||
.header-left {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-xs);
|
||
}
|
||
.header-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-sm);
|
||
}
|
||
.page-title {
|
||
font-size: var(--font-xl);
|
||
font-weight: var(--weight-semibold);
|
||
color: var(--text-primary);
|
||
margin: 0;
|
||
line-height: 1.4;
|
||
}
|
||
.page-desc {
|
||
font-size: var(--font-sm);
|
||
color: var(--text-tertiary);
|
||
margin: 0;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
#### Task 2.2: 创建 TabsBar 组件
|
||
|
||
**Files:**
|
||
- Create: `src/components/common/TabsBar.vue`
|
||
|
||
- [ ] **Step 1: 创建 TabsBar.vue**
|
||
|
||
```vue
|
||
<template>
|
||
<div class="tabs-bar">
|
||
<button
|
||
v-for="tab in tabs"
|
||
:key="tab.key"
|
||
:class="['tab-btn', { active: modelValue === tab.key }]"
|
||
@click="$emit('update:modelValue', tab.key)"
|
||
>
|
||
<span v-if="tab.icon" class="tab-icon" v-html="tab.icon"></span>
|
||
<span class="tab-text">{{ tab.label }}</span>
|
||
</button>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
defineProps({
|
||
tabs: { type: Array, required: true, default: () => [] },
|
||
modelValue: { type: String, default: '' }
|
||
})
|
||
defineEmits(['update:modelValue'])
|
||
</script>
|
||
|
||
<style scoped>
|
||
.tabs-bar {
|
||
display: flex;
|
||
gap: var(--space-xs);
|
||
padding: var(--space-sm) var(--space-lg);
|
||
background: var(--bg-header);
|
||
border-bottom: 1px solid var(--border-light);
|
||
overflow-x: auto;
|
||
}
|
||
.tab-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: var(--space-sm);
|
||
padding: var(--tab-btn-padding-v) var(--tab-btn-padding-h);
|
||
border: none;
|
||
border-radius: var(--radius-md);
|
||
background: transparent;
|
||
color: var(--text-secondary);
|
||
font-size: var(--font-md);
|
||
font-weight: var(--weight-medium);
|
||
cursor: pointer;
|
||
transition: all var(--transition-fast);
|
||
white-space: nowrap;
|
||
flex-shrink: 0;
|
||
}
|
||
.tab-btn:hover {
|
||
background: var(--bg-hover);
|
||
color: var(--text-primary);
|
||
}
|
||
.tab-btn.active {
|
||
background: var(--bg-active);
|
||
color: var(--text-primary);
|
||
font-weight: var(--weight-semibold);
|
||
}
|
||
.tab-icon {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 18px;
|
||
height: 18px;
|
||
font-size: 14px;
|
||
}
|
||
.tab-text {
|
||
line-height: 1;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
#### Task 2.3: 创建 ContentCard 组件
|
||
|
||
**Files:**
|
||
- Create: `src/components/common/ContentCard.vue`
|
||
|
||
- [ ] **Step 1: 创建 ContentCard.vue**
|
||
|
||
```vue
|
||
<template>
|
||
<div class="content-card" :class="{ 'has-header': $slots.header }">
|
||
<div v-if="$slots.header" class="card-header">
|
||
<slot name="header" />
|
||
</div>
|
||
<div class="card-body" :style="{ padding }">
|
||
<slot />
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
defineProps({
|
||
padding: { type: String, default: 'var(--card-padding)' }
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.content-card {
|
||
background: var(--bg-card);
|
||
border: 1px solid var(--border-light);
|
||
border-radius: var(--radius-lg);
|
||
box-shadow: var(--shadow-sm);
|
||
overflow: hidden;
|
||
}
|
||
.card-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: var(--space-md) var(--card-padding);
|
||
border-bottom: 1px solid var(--border-light);
|
||
gap: var(--space-md);
|
||
}
|
||
.card-body {
|
||
padding: var(--card-padding);
|
||
}
|
||
</style>
|
||
```
|
||
|
||
#### Task 2.4: 创建 StatsRow 组件
|
||
|
||
**Files:**
|
||
- Create: `src/components/common/StatsRow.vue`
|
||
|
||
- [ ] **Step 1: 创建 StatsRow.vue**
|
||
|
||
```vue
|
||
<template>
|
||
<div class="stats-row">
|
||
<div v-for="(stat, index) in stats" :key="index" class="stat-card">
|
||
<span class="stat-value">{{ stat.value }}</span>
|
||
<span class="stat-label">{{ stat.label }}</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
defineProps({
|
||
stats: { type: Array, required: true, default: () => [] }
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.stats-row {
|
||
display: flex;
|
||
gap: var(--space-md);
|
||
padding: var(--space-md) var(--space-xl);
|
||
background: var(--bg-header);
|
||
border-bottom: 1px solid var(--border-light);
|
||
}
|
||
.stat-card {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: var(--space-xs);
|
||
padding: var(--space-sm) var(--space-md);
|
||
background: var(--bg-container);
|
||
border-radius: var(--radius-md);
|
||
border: 1px solid var(--border-light);
|
||
min-height: var(--stat-card-height);
|
||
}
|
||
.stat-value {
|
||
font-size: var(--font-3xl);
|
||
font-weight: var(--weight-bold);
|
||
color: var(--text-primary);
|
||
line-height: 1.2;
|
||
}
|
||
.stat-label {
|
||
font-size: var(--font-sm);
|
||
color: var(--text-tertiary);
|
||
font-weight: var(--weight-medium);
|
||
}
|
||
</style>
|
||
```
|
||
|
||
#### Task 2.5: 创建 DataTable 组件
|
||
|
||
**Files:**
|
||
- Create: `src/components/common/DataTable.vue`
|
||
|
||
- [ ] **Step 1: 创建 DataTable.vue**
|
||
|
||
```vue
|
||
<template>
|
||
<a-table
|
||
:dataSource="dataSource"
|
||
:columns="columns"
|
||
:loading="loading"
|
||
:pagination="pagination"
|
||
:rowKey="rowKey"
|
||
size="small"
|
||
:scroll="{ x: scrollX, y: scrollY }"
|
||
:locale="emptyLocale"
|
||
class="data-table"
|
||
@change="handleTableChange"
|
||
>
|
||
<template v-for="slot in Object.keys($slots)" #[slot]="scope">
|
||
<slot :name="slot" v-bind="scope" />
|
||
</template>
|
||
</a-table>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed } from 'vue'
|
||
|
||
const props = defineProps({
|
||
dataSource: { type: Array, default: () => [] },
|
||
columns: { type: Array, default: () => [] },
|
||
loading: { type: Boolean, default: false },
|
||
pagination: { type: [Object, Boolean], default: true },
|
||
rowKey: { type: [String, Function], default: 'id' },
|
||
scrollX: { type: [Number, String], default: undefined },
|
||
scrollY: { type: [Number, String], default: undefined },
|
||
emptyText: { type: String, default: '暂无数据' }
|
||
})
|
||
|
||
const emit = defineEmits(['change'])
|
||
|
||
const emptyLocale = computed(() => ({
|
||
emptyText: props.emptyText
|
||
}))
|
||
|
||
const handleTableChange = (pagination, filters, sorter) => {
|
||
emit('change', { pagination, filters, sorter })
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.data-table {
|
||
width: 100%;
|
||
}
|
||
.data-table :deep(.ant-table-thead > tr > th) {
|
||
background: var(--bg-container);
|
||
color: var(--text-primary);
|
||
font-weight: var(--weight-semibold);
|
||
font-size: var(--font-md);
|
||
padding: var(--space-sm) var(--space-md);
|
||
border-bottom: 1px solid var(--border-default);
|
||
}
|
||
.data-table :deep(.ant-table-tbody > tr > td) {
|
||
font-size: var(--font-md);
|
||
padding: var(--space-sm) var(--space-md);
|
||
border-bottom: 1px solid var(--border-light);
|
||
}
|
||
.data-table :deep(.ant-table-tbody > tr:hover > td) {
|
||
background: var(--bg-hover);
|
||
}
|
||
.data-table :deep(.ant-pagination) {
|
||
margin-top: var(--space-md);
|
||
}
|
||
</style>
|
||
```
|
||
|
||
---
|
||
|
||
### Task 3: 重构全局样式 (style.css)
|
||
|
||
**Files:**
|
||
- Modify: `src/style.css`
|
||
|
||
- [ ] **Step 1: 完全重写 style.css 为白色体系**
|
||
|
||
将现有的 `src/style.css` 替换为以下内容:
|
||
|
||
```css
|
||
/* ========================================
|
||
全局样式 - 白色体系
|
||
基于设计令牌系统
|
||
======================================== */
|
||
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
html, body {
|
||
width: 100%;
|
||
height: 100%;
|
||
overflow-x: hidden;
|
||
overflow-y: hidden;
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||
line-height: 1.5;
|
||
color: var(--text-primary);
|
||
background: var(--bg-page);
|
||
}
|
||
|
||
#app {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
/* ===== 布局基础 ===== */
|
||
.ant-layout {
|
||
background: var(--bg-page);
|
||
}
|
||
|
||
.ant-layout-header {
|
||
background: var(--bg-header);
|
||
box-shadow: var(--shadow-sm);
|
||
}
|
||
|
||
/* ===== 卡片统一样式 ===== */
|
||
.ant-card {
|
||
border-radius: var(--radius-lg);
|
||
box-shadow: var(--shadow-sm);
|
||
border: 1px solid var(--border-light);
|
||
transition: all var(--transition-fast);
|
||
}
|
||
|
||
.ant-card:hover {
|
||
box-shadow: var(--shadow-md);
|
||
}
|
||
|
||
.ant-card-head {
|
||
border-bottom: 1px solid var(--border-light);
|
||
padding: var(--space-md) var(--card-padding);
|
||
background: var(--bg-page);
|
||
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
||
}
|
||
|
||
.ant-card-head-title {
|
||
font-weight: var(--weight-semibold);
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.ant-card-body {
|
||
padding: var(--card-padding);
|
||
}
|
||
|
||
/* ===== 标签页统一样式 ===== */
|
||
.ant-tabs-nav {
|
||
margin-bottom: var(--space-md);
|
||
padding: 0 var(--space-xs);
|
||
}
|
||
|
||
.ant-tabs-tab {
|
||
padding: var(--tab-btn-padding-v) var(--tab-btn-padding-h);
|
||
font-weight: var(--weight-medium);
|
||
color: var(--text-secondary);
|
||
transition: all var(--transition-fast);
|
||
}
|
||
|
||
.ant-tabs-tab:hover {
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.ant-tabs-tab-active {
|
||
color: var(--text-primary) !important;
|
||
font-weight: var(--weight-semibold);
|
||
}
|
||
|
||
.ant-tabs-ink-bar {
|
||
background: var(--color-primary);
|
||
height: 2px;
|
||
border-radius: 1px;
|
||
}
|
||
|
||
/* ===== 按钮统一样式 ===== */
|
||
.ant-btn {
|
||
border-radius: var(--radius-md);
|
||
font-weight: var(--weight-medium);
|
||
transition: all var(--transition-fast);
|
||
}
|
||
|
||
.ant-btn-primary {
|
||
background: var(--color-primary);
|
||
border-color: var(--color-primary);
|
||
}
|
||
|
||
.ant-btn-primary:hover {
|
||
background: var(--color-primary-hover);
|
||
border-color: var(--color-primary-hover);
|
||
}
|
||
|
||
.ant-btn-default {
|
||
border-color: var(--border-default);
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.ant-btn-default:hover {
|
||
border-color: var(--color-primary);
|
||
color: var(--color-primary);
|
||
}
|
||
|
||
/* ===== 输入框统一样式 ===== */
|
||
.ant-input {
|
||
border-radius: var(--radius-md);
|
||
transition: all var(--transition-fast);
|
||
border-color: var(--border-default);
|
||
font-size: var(--font-md);
|
||
height: var(--input-height);
|
||
}
|
||
|
||
.ant-input:focus,
|
||
.ant-input:hover,
|
||
.ant-input-focused {
|
||
border-color: var(--color-primary);
|
||
box-shadow: 0 0 0 2px rgba(33, 37, 41, 0.08);
|
||
}
|
||
|
||
/* ===== 选择器统一样式 ===== */
|
||
.ant-select-selector {
|
||
border-radius: var(--radius-md) !important;
|
||
border-color: var(--border-default) !important;
|
||
transition: all var(--transition-fast) !important;
|
||
}
|
||
|
||
.ant-select:hover .ant-select-selector {
|
||
border-color: var(--border-medium) !important;
|
||
}
|
||
|
||
.ant-select-focused .ant-select-selector {
|
||
border-color: var(--color-primary) !important;
|
||
box-shadow: 0 0 0 2px rgba(33, 37, 41, 0.08) !important;
|
||
}
|
||
|
||
.ant-select-selection-item {
|
||
color: var(--text-primary) !important;
|
||
font-weight: var(--weight-medium);
|
||
}
|
||
|
||
.ant-select-selection-placeholder {
|
||
color: var(--text-disabled) !important;
|
||
}
|
||
|
||
.ant-select-dropdown {
|
||
border-radius: var(--radius-lg) !important;
|
||
box-shadow: var(--shadow-lg) !important;
|
||
border: 1px solid var(--border-light) !important;
|
||
}
|
||
|
||
.ant-select-item {
|
||
border-radius: var(--radius-md) !important;
|
||
margin: var(--space-xs) var(--space-sm) !important;
|
||
padding: var(--space-sm) var(--space-md) !important;
|
||
font-weight: var(--weight-medium);
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.ant-select-item-option-selected {
|
||
background: var(--bg-active) !important;
|
||
color: var(--text-primary) !important;
|
||
font-weight: var(--weight-semibold);
|
||
}
|
||
|
||
.ant-select-item-option-active:not(.ant-select-item-option-disabled) {
|
||
background: var(--bg-hover) !important;
|
||
}
|
||
|
||
/* ===== 表格紧凑模式 ===== */
|
||
.ant-table {
|
||
border-radius: var(--radius-lg);
|
||
overflow: hidden;
|
||
box-shadow: var(--shadow-sm);
|
||
}
|
||
|
||
.ant-table-thead > tr > th {
|
||
background: var(--bg-container);
|
||
color: var(--text-primary);
|
||
font-weight: var(--weight-semibold);
|
||
font-size: var(--font-md);
|
||
padding: var(--space-sm) var(--space-md);
|
||
border-bottom: 1px solid var(--border-default);
|
||
}
|
||
|
||
.ant-table-tbody > tr > td {
|
||
font-size: var(--font-md);
|
||
padding: var(--space-sm) var(--space-md);
|
||
border-bottom: 1px solid var(--border-light);
|
||
}
|
||
|
||
.ant-table-tbody > tr:hover > td {
|
||
background: var(--bg-hover);
|
||
}
|
||
|
||
/* ===== 模态框统一样式 ===== */
|
||
.ant-modal-content {
|
||
border-radius: var(--radius-xl);
|
||
box-shadow: var(--shadow-xl);
|
||
}
|
||
|
||
.ant-modal-header {
|
||
border-radius: var(--radius-xl) var(--radius-xl) 0 0;
|
||
border-bottom: 1px solid var(--border-light);
|
||
padding: var(--space-lg) var(--space-2xl);
|
||
background: var(--bg-page);
|
||
}
|
||
|
||
.ant-modal-title {
|
||
font-weight: var(--weight-semibold);
|
||
font-size: var(--font-xl);
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.ant-modal-body {
|
||
padding: var(--space-2xl);
|
||
}
|
||
|
||
.ant-modal-footer {
|
||
border-top: 1px solid var(--border-light);
|
||
padding: var(--space-lg) var(--space-2xl);
|
||
border-radius: 0 0 var(--radius-xl) var(--radius-xl);
|
||
}
|
||
|
||
/* ===== 列表项统一样式 ===== */
|
||
.ant-list-item {
|
||
border-radius: var(--radius-md);
|
||
margin-bottom: var(--space-md);
|
||
padding: var(--space-md) var(--space-lg);
|
||
transition: all var(--transition-fast);
|
||
cursor: pointer;
|
||
border: 1px solid transparent;
|
||
}
|
||
|
||
.ant-list-item:hover {
|
||
background: var(--bg-hover);
|
||
border-color: var(--border-light);
|
||
}
|
||
|
||
/* ===== 标签统一样式 ===== */
|
||
.ant-tag {
|
||
border-radius: var(--radius-sm);
|
||
padding: 2px var(--space-sm);
|
||
font-weight: var(--weight-medium);
|
||
font-size: var(--font-sm);
|
||
}
|
||
|
||
/* ===== 表单统一样式 ===== */
|
||
.ant-form-item-label > label {
|
||
font-weight: var(--weight-medium);
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.ant-form-item {
|
||
margin-bottom: var(--form-gap);
|
||
}
|
||
|
||
/* ===== 进度条统一样式 ===== */
|
||
.ant-progress-bg {
|
||
border-radius: var(--radius-sm);
|
||
}
|
||
|
||
/* ===== 徽标统一样式 ===== */
|
||
.ant-badge-status-dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
}
|
||
|
||
/* ===== 滚动条统一样式 ===== */
|
||
::-webkit-scrollbar {
|
||
width: 6px;
|
||
height: 6px;
|
||
}
|
||
|
||
::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb {
|
||
background: var(--border-dark);
|
||
border-radius: 3px;
|
||
transition: all var(--transition-fast);
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb:hover {
|
||
background: var(--text-tertiary);
|
||
}
|
||
|
||
/* ===== 标题样式 ===== */
|
||
h1, h2, h3, h4, h5, h6 {
|
||
color: var(--text-primary);
|
||
font-weight: var(--weight-semibold);
|
||
line-height: 1.4;
|
||
}
|
||
|
||
h3 {
|
||
font-size: var(--font-xl);
|
||
margin-bottom: var(--space-md);
|
||
}
|
||
|
||
h4 {
|
||
font-size: var(--font-lg);
|
||
margin-bottom: var(--space-sm);
|
||
}
|
||
|
||
h5 {
|
||
font-size: var(--font-md);
|
||
margin-bottom: var(--space-sm);
|
||
}
|
||
|
||
/* ===== 全局布局约束 ===== */
|
||
html {
|
||
overflow: hidden;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
body {
|
||
overflow-x: hidden;
|
||
overflow-y: hidden;
|
||
width: 100vw;
|
||
max-width: 100vw;
|
||
height: 100vh;
|
||
}
|
||
|
||
#app {
|
||
width: 100vw;
|
||
max-width: 100vw;
|
||
height: 100vh;
|
||
overflow-x: hidden;
|
||
overflow-y: hidden;
|
||
}
|
||
|
||
.ant-layout {
|
||
max-width: 100vw;
|
||
width: 100vw;
|
||
height: 100vh;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.ant-card,
|
||
.ant-table-wrapper,
|
||
.ant-list,
|
||
.ant-tabs,
|
||
.ant-form {
|
||
max-width: 100% !important;
|
||
}
|
||
|
||
/* ===== 性能友好降级 ===== */
|
||
@media (prefers-reduced-motion: reduce) {
|
||
*,
|
||
*::before,
|
||
*::after {
|
||
animation-duration: 0.01ms !important;
|
||
animation-iteration-count: 1 !important;
|
||
transition-duration: 0.01ms !important;
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Task 4: 简化 index.css 基础样式
|
||
|
||
**Files:**
|
||
- Modify: `src/index.css`
|
||
|
||
- [ ] **Step 1: 简化 index.css 为纯基础重置**
|
||
|
||
```css
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||
sans-serif;
|
||
-webkit-font-smoothing: antialiased;
|
||
-moz-osx-font-smoothing: grayscale;
|
||
}
|
||
|
||
code {
|
||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||
monospace;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Task 5: 重构 Home.vue 导航系统
|
||
|
||
**Files:**
|
||
- Modify: `src/views/Home.vue`
|
||
|
||
- [ ] **Step 1: 将下拉选择器改为顶部标签栏导航**
|
||
|
||
关键修改点:
|
||
1. Header 高度从默认改为 48px (`var(--header-height)`)
|
||
2. 移除 `<a-select>` 下拉框
|
||
3. 新增顶部标签栏导航按钮组
|
||
4. 应用白色体系样式
|
||
5. 保持所有权限逻辑不变
|
||
|
||
```vue
|
||
<!-- Header 部分 - 从下拉框改为标签栏 -->
|
||
<a-layout-header style="height: var(--header-height); padding: 0 var(--space-xl); display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--border-light); background: var(--bg-header);">
|
||
<div style="display: flex; align-items: center; gap: var(--space-xl);">
|
||
<h1 style="color: var(--text-primary); font-size: var(--font-lg); font-weight: var(--weight-semibold); margin: 0;">制度文件管理学习AI智能体</h1>
|
||
|
||
<!-- 顶部标签栏导航 -->
|
||
<div class="top-nav-bar">
|
||
<button
|
||
v-for="mod in visibleModules"
|
||
:key="mod.key"
|
||
:class="['nav-tab', { active: currentModule === mod.key }]"
|
||
@click="handleModuleChange(mod.key)"
|
||
>
|
||
<span class="nav-icon" v-html="mod.icon"></span>
|
||
<span class="nav-label">{{ mod.label }}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧用户区域保持不变 -->
|
||
...
|
||
</a-layout-header>
|
||
```
|
||
|
||
```js
|
||
// 新增计算属性 - 可见模块列表
|
||
const visibleModules = computed(() => {
|
||
const moduleConfig = [
|
||
{ key: 'read', label: '文件查看', icon: '📄' },
|
||
{ key: 'manage', label: '文件管理', icon: '📁' },
|
||
{ key: 'qa', label: '知识问答', icon: '💬' },
|
||
{ key: 'exam', label: '考察训练', icon: '🔍' },
|
||
{ key: 'mind', label: '纲要学习', icon: '📋' },
|
||
{ key: 'dashboard', label: '综合看板', icon: '📊' },
|
||
{ key: 'permission', label: '权限管理', icon: '👥' }
|
||
]
|
||
return moduleConfig.filter(mod => hasPermission(mod.key))
|
||
})
|
||
```
|
||
|
||
```css
|
||
/* 新增顶部导航样式 */
|
||
.top-nav-bar {
|
||
display: flex;
|
||
gap: var(--space-xs);
|
||
}
|
||
.nav-tab {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: var(--space-sm);
|
||
padding: var(--space-sm) var(--space-md);
|
||
border: none;
|
||
border-radius: var(--radius-md);
|
||
background: transparent;
|
||
color: var(--text-secondary);
|
||
font-size: var(--font-md);
|
||
font-weight: var(--weight-medium);
|
||
cursor: pointer;
|
||
transition: all var(--transition-fast);
|
||
white-space: nowrap;
|
||
}
|
||
.nav-tab:hover {
|
||
background: var(--bg-hover);
|
||
color: var(--text-primary);
|
||
}
|
||
.nav-tab.active {
|
||
background: var(--bg-active);
|
||
color: var(--text-primary);
|
||
font-weight: var(--weight-semibold);
|
||
}
|
||
.nav-icon {
|
||
font-size: 14px;
|
||
}
|
||
.nav-label {
|
||
line-height: 1;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Task 6: 重构 ReadModule.vue
|
||
|
||
**Files:**
|
||
- Modify: `src/components/ReadModule.vue`
|
||
|
||
- [ ] **Step 1: 引入并使用 TabsBar 组件替代自定义 main-tabs-bar**
|
||
- [ ] **Step 2: 移除模块内重复的渐变样式类**
|
||
- [ ] **Step 3: 统一使用 CSS 变量定义的颜色值**
|
||
|
||
---
|
||
|
||
### Task 7: 重构 ExamModule.vue
|
||
|
||
**Files:**
|
||
- Modify: `src/components/ExamModule.vue`
|
||
|
||
- [ ] **Step 1: 引入并使用 TabsBar、PageHeader、StatsRow、ContentCard 组件**
|
||
- [ ] **Step 2: 替换 main-tabs-bar 为 TabsBar 组件**
|
||
- [ ] **Step 3: 替换 exam-header 为 PageHeader 组件**
|
||
- [ ] **Step 4: 替换 stats-row + stat-card 为 StatsRow 组件**
|
||
- [ ] **Step 5: 替换 exam-card 为 ContentCard 组件**
|
||
- [ ] **Step 6: 移除所有 btn-gradient-* 和 card-gradient-* 类的使用**
|
||
- [ ] **Step 7: 统一表单间距为 var(--form-gap)**
|
||
|
||
---
|
||
|
||
### Task 8: 重构 ManageModule.vue
|
||
|
||
**Files:**
|
||
- Modify: `src/components/ManageModule.vue`
|
||
|
||
- [ ] **Step 1: 引入并使用 TabsBar、PageHeader、ContentCard、DataTable 组件**
|
||
- [ ] **Step 2: 替换重复的布局结构为通用组件**
|
||
- [ ] **Step 3: 移除彩色渐变样式**
|
||
|
||
---
|
||
|
||
### Task 9: 重构 QAModule.vue
|
||
|
||
**Files:**
|
||
- Modify: `src/components/QAModule.vue`
|
||
|
||
- [ ] **Step 1: 引入并使用 PageHeader、ContentCard 组件**
|
||
- [ ] **Step 2: 简化页面头部和卡片容器**
|
||
|
||
---
|
||
|
||
### Task 10: 重构 MindModule.vue / DashboardModule.vue / PermissionModule.vue
|
||
|
||
**Files:**
|
||
- Modify: `src/components/MindModule.vue`
|
||
- Modify: `src/components/DashboardModule.vue`
|
||
- Modify: `src/components/PermissionModule.vue`
|
||
|
||
- [ ] **Step 1: 各模块分别引入对应通用组件**
|
||
- [ ] **Step 2: 替换重复布局结构**
|
||
- [ ] **Step 3: DashboardModule 特别处理 StatsRow 的使用**
|
||
|
||
---
|
||
|
||
### Task 11: 最终验证
|
||
|
||
- [ ] **Step 1: 启动开发服务器检查编译是否通过**
|
||
|
||
Run: `npm run dev`
|
||
|
||
Expected: 无编译错误
|
||
|
||
- [ ] **Step 2: 逐个模块检查视觉一致性**
|
||
|
||
检查项:
|
||
- 所有页面使用统一的白色背景
|
||
- Header 为纯白+底部边线
|
||
- 标签栏为灰色激活态
|
||
- 卡片为纯白+浅边框
|
||
- 按钮为深灰色主操作色
|
||
- 表格行高紧凑一致
|
||
- 无残留的彩色渐变元素
|
||
|
||
- [ ] **Step 3: 检查各模块功能完整性**
|
||
|
||
检查项:
|
||
- 登录流程正常
|
||
- 权限控制正常
|
||
- 模块切换正常
|
||
- 数据加载正常
|
||
- 交互操作正常
|
||
|
||
---
|
||
|
||
## 实施顺序总结
|
||
|
||
```
|
||
Task 1: design-tokens.css + main.js引入 ← 基础设施
|
||
Task 2: 6个通用组件 ← 组件库
|
||
Task 3: style.css 重写 ← 全局样式
|
||
Task 4: index.css 简化 ← 基础样式
|
||
Task 5: Home.vue 导航重构 ← 导航系统
|
||
Task 6: ReadModule.vue ← 模块重构
|
||
Task 7: ExamModule.vue ← 模块重构
|
||
Task 8: ManageModule.vue ← 模块重构
|
||
Task 9: QAModule.vue ← 模块重构
|
||
Task 10: MindModule/Dashboard/Permission ← 模块重构
|
||
Task 11: 最终验证 ← 质量保证
|
||
```
|