web/ (基于 Vue 3 + TypeScript + Element Plus)基于 Art Design Pro 的设计语言,追求现代化、精致、用户友好的界面体验。注重细节、动效和用户交互的流畅性。
--art-primary: 93, 135, 255; /* 主品牌色 - 蓝色 */
--art-secondary: 73, 190, 255; /* 辅助色 - 天蓝色 */
--art-success: 19, 222, 185; /* 成功色 - 绿色 */
--art-warning: 255, 174, 31; /* 警告色 - 橙色 */
--art-error: 250, 137, 107; /* 错误色 - 红色 */
--art-info: 83, 155, 255; /* 信息色 - 蓝色 */
--art-danger: 255, 77, 79; /* 危险色 - 红色 */
/* 浅色主题 */
--art-bg-color: #fafbfc; /* 页面底色 */
--art-main-bg-color: #ffffff; /* 主要内容背景 */
--art-bg-primary: 236, 242, 255; /* 主色背景 */
--art-bg-secondary: 232, 247, 255; /* 辅助背景 */
--art-bg-success: 230, 255, 250; /* 成功背景 */
--art-bg-warning: 254, 245, 229; /* 警告背景 */
--art-bg-error: 253, 237, 232; /* 错误背景 */
/* 暗色主题 */
html.dark {
--art-bg-color: #070707; /* 页面底色 */
--art-main-bg-color: #161618; /* 主要内容背景 */
}
全面采用轮廓式(Outline)按钮设计,提供更加优雅、现代和一致的用户界面体验。
<template>
<div class="component-container">
<!-- 组件内容 -->
</div>
</template>
<script setup lang="ts" name="ComponentName">
// 1. 引入依赖
import { ref, reactive, computed, onMounted } from 'vue';
import type { ComponentProps, ComponentEmits } from './types';
// 2. 定义Props和Emits
const props = defineProps<ComponentProps>();
const emits = defineEmits<ComponentEmits>();
// 3. 响应式数据
const loading = ref(false);
const formData = reactive({});
// 4. 计算属性
const computedValue = computed(() => {
return /* 计算逻辑 */;
});
// 5. 方法定义
const handleClick = () => {
// 方法实现
};
// 6. 生命周期
onMounted(() => {
// 组件挂载后的逻辑
});
</script>
<style lang="scss" scoped>
.component-container {
// 样式实现
}
</style>
<template>
<div class="p-2 page-container">
<!-- 搜索区域 -->
<transition
:enter-active-class="proxy?.animate.searchAnimate.enter"
:leave-active-class="proxy?.animate.searchAnimate.leave"
>
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form
ref="queryFormRef"
:model="queryParams"
:inline="true"
label-width="100px"
>
<!-- 搜索表单项 -->
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">
搜索
</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<!-- 表格区域 -->
<el-card shadow="never" class="button-card table-card">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd">
新增
</el-button>
</el-col>
<right-toolbar
v-model:showSearch="showSearch"
@queryTable="getList"
></right-toolbar>
</el-row>
</template>
<!-- 数据表格 -->
<el-table v-loading="loading" border :data="dataList" :height="tableHeight">
<el-table-column type="selection" width="55" align="center" />
<!-- 数据列 -->
<el-table-column label="操作" align="center" width="160">
<template #default="scope">
<!-- 操作按钮 -->
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</el-card>
</div>
</template>
<template>
<div class="app-container">
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
<el-card shadow="never">
<template #header>
<span>基本信息</span>
</template>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="名称" prop="name">
<el-input v-model="form.name" placeholder="请输入名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" placeholder="请选择状态">
<el-option label="启用" value="0" />
<el-option label="禁用" value="1" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item>
<el-button type="primary" @click="submitForm">提交</el-button>
<el-button @click="resetForm">重置</el-button>
</el-form-item>
</el-card>
</el-form>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue';
import type { FormInstance, FormRules } from 'element-plus';
// 表单引用
const formRef = ref<FormInstance>();
// 表单数据
const form = reactive({
name: '',
status: '0',
});
// 验证规则
const rules = reactive<FormRules>({
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
});
// 提交表单
const submitForm = async () => {
if (!formRef.value) return;
await formRef.value.validate((valid) => {
if (valid) {
// 提交逻辑
console.log('提交表单:', form);
}
});
};
// 重置表单
const resetForm = () => {
if (!formRef.value) return;
formRef.value.resetFields();
};
</script>
// api/[module]/index.ts
import request from '@/utils/request';
import type { AxiosPromise } from 'axios';
import type { DataVO, DataForm, DataQuery } from './types';
/**
* 查询数据列表
* @param query 查询参数
*/
export const listData = (query?: DataQuery): AxiosPromise<DataVO[]> => {
return request({
url: '/api/data/list',
method: 'get',
params: query,
});
};
/**
* 查询数据详情
* @param id 数据ID
*/
export const getData = (id: string | number): AxiosPromise<DataVO> => {
return request({
url: `/api/data/${id}`,
method: 'get',
});
};
/**
* 新增数据
* @param data 数据信息
*/
export const addData = (data: DataForm): AxiosPromise<void> => {
return request({
url: '/api/data',
method: 'post',
data: data,
});
};
/**
* 修改数据
* @param data 数据信息
*/
export const updateData = (data: DataForm): AxiosPromise<void> => {
return request({
url: '/api/data',
method: 'put',
data: data,
});
};
/**
* 删除数据
* @param ids 数据ID数组
*/
export const delData = (ids: Array<string | number>): AxiosPromise<void> => {
return request({
url: `/api/data/${ids}`,
method: 'delete',
});
};
// api/[module]/types.ts
// 查询参数
export interface DataQuery extends PageQuery {
name?: string;
status?: string;
createTime?: [DateModelType, DateModelType];
}
// 数据表单
export interface DataForm {
id?: string | number;
name: string;
status: string;
remark?: string;
}
// 数据视图对象
export interface DataVO {
id: string | number;
name: string;
status: string;
createTime: string;
updateTime: string;
remark?: string;
}
// stores/modules/[module].ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import type { DataVO, DataQuery } from '@/api/[module]/types';
import { listData, getData } from '@/api/[module]';
export const useDataStore = defineStore('[module]', () => {
// 状态
const dataList = ref<DataVO[]>([]);
const loading = ref(false);
const total = ref(0);
// 计算属性
const hasData = computed(() => dataList.value.length > 0);
// 方法
const getList = async (query: DataQuery) => {
loading.value = true;
try {
const { data } = await listData(query);
dataList.value = data.rows;
total.value = data.total;
} finally {
loading.value = false;
}
};
const getInfo = async (id: string | number) => {
const { data } = await getData(id);
return data;
};
const reset = () => {
dataList.value = [];
total.value = 0;
};
return {
// 状态
dataList,
loading,
total,
// 计算属性
hasData,
// 方法
getList,
getInfo,
reset,
};
});
// router/modules/[module].ts
import type { RouteRecordRaw } from 'vue-router';
import Layout from '@/layout/index.vue';
const moduleRouter: RouteRecordRaw = {
path: '/[module]',
component: Layout,
redirect: '/[module]/list',
name: 'Module',
meta: {
title: '模块管理',
icon: 'example',
alwaysShow: true,
},
children: [
{
path: 'list',
component: () => import('@/views/[module]/index.vue'),
name: 'ModuleList',
meta: {
title: '列表管理',
activeMenu: '/[module]/list',
},
},
{
path: 'add',
component: () => import('@/views/[module]/form.vue'),
name: 'ModuleAdd',
meta: {
title: '新增数据',
activeMenu: '/[module]/list',
},
hidden: true,
},
{
path: 'edit/:id(\\d+)',
component: () => import('@/views/[module]/form.vue'),
name: 'ModuleEdit',
meta: {
title: '编辑数据',
activeMenu: '/[module]/list',
},
hidden: true,
},
],
};
export default moduleRouter;
web/src/
├── api/ # API接口定义
│ ├── [module]/ # 按模块分类
│ │ ├── index.ts # API接口
│ │ └── types.ts # 类型定义
│ └── types.ts # 通用类型定义
├── assets/ # 静态资源
│ ├── icons/ # SVG图标
│ ├── images/ # 图片资源
│ └── styles/ # 全局样式
├── components/ # 通用组件
│ ├── [ComponentName]/ # 组件目录
│ │ ├── index.vue # 组件主文件
│ │ └── types.ts # 组件类型
│ └── index.ts # 组件导出
├── composables/ # 组合式函数
├── directive/ # 自定义指令
├── hooks/ # 自定义钩子
├── layout/ # 布局组件
├── plugins/ # 插件配置
├── router/ # 路由配置
│ ├── modules/ # 路由模块
│ └── index.ts # 路由入口
├── stores/ # 状态管理
│ ├── modules/ # Store模块
│ └── index.ts # Store入口
├── types/ # 全局类型定义
├── utils/ # 工具函数
│ ├── request.ts # HTTP请求封装
│ ├── auth.ts # 认证工具
│ └── index.ts # 工具导出
├── views/ # 页面视图
│ ├── [module]/ # 按模块分类
│ │ ├── index.vue # 列表页面
│ │ ├── form.vue # 表单页面
│ │ └── components/ # 页面组件
│ └── layout/ # 布局页面
├── App.vue # 应用入口
└── main.ts # 主入口文件
// .eslintrc.js
module.exports = {
extends: ['@vue/eslint-config-typescript', '@vue/eslint-config-prettier'],
rules: {
// Vue规则
'vue/multi-word-component-names': 'off',
'vue/no-v-html': 'off',
// TypeScript规则
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/no-explicit-any': 'warn',
// 通用规则
'no-console': 'warn',
'no-debugger': 'error',
},
};
{
"semi": true,
"singleQuote": true,
"trailingComma": "none",
"tabWidth": 2,
"useTabs": false,
"printWidth": 100,
"endOfLine": "lf"
}
// 路由懒加载
const Home = () => import('@/views/home/index.vue');
// 组件懒加载
const LazyComponent = defineAsyncComponent(() => import('@/components/LazyComponent.vue'));
// HTTP缓存
import { ElMessage } from 'element-plus';
const request = axios.create({
timeout: 10000,
headers: {
'Cache-Control': 'no-cache'
}
});
// 组件缓存
<router-view v-slot="{ Component }">
<keep-alive :include="cachedViews">
<component :is="Component" :key="route.path" />
</keep-alive>
</router-view>
// main.ts
app.config.errorHandler = (err, vm, info) => {
console.error('全局错误:', err);
ElMessage.error('系统异常,请联系管理员');
};
// 请求错误处理
request.interceptors.response.use(
(response) => {
const { code, msg } = response.data;
if (code === 200) {
return response.data;
} else {
ElMessage.error(msg || '请求失败');
return Promise.reject(new Error(msg));
}
},
(error) => {
ElMessage.error('网络异常');
return Promise.reject(error);
},
);
// tests/components/UserList.spec.ts
import { mount } from '@vue/test-utils';
import UserList from '@/components/UserList/index.vue';
describe('UserList', () => {
it('should render correctly', () => {
const wrapper = mount(UserList, {
props: {
users: [],
},
});
expect(wrapper.exists()).toBe(true);
});
});
// e2e/user.spec.ts
import { test, expect } from '@playwright/test';
test('用户列表页面', async ({ page }) => {
await page.goto('/user/list');
await expect(page.locator('.user-list')).toBeVisible();
});
// 主色调
--art-primary: 50, 50, 222; // 深蓝色,用于主要按钮、链接等
--art-secondary: 73, 190, 255; // 次要蓝色
--art-success: 0, 167, 58; // 成功色
--art-warning: 142, 91, 0; // 警告色
--art-danger: 255, 77, 79; // 危险色
--art-info: 0, 75, 178; // 信息色
--art-error: 250, 137, 107; // 错误色
// 文本颜色
--art-text-gray-600: #333333; // 主要文本
--art-text-gray-700: #000000; // 强调文本
// 主色调
--art-primary: 93, 135, 255; // 亮蓝色
--art-bg-color: #070707; // 背景色
--art-main-bg-color: #161618; // 主要背景色
变量使用方式
// 正确用法
color: rgb(var(--art-primary));
background-color: rgb(var(--art-bg-primary));
// 错误用法
color: var(--art-primary); // ❌ 缺少rgb()
// 全局变量
--el-color-primary: rgb(var(--art-primary)) !important;
--el-text-color-regular: #000000 !important;
菜单组件样式
--el-menu-hover-bg-color: rgb(var(--art-primary)) !important;
--el-menu-active-color: rgb(var(--art-primary)) !important;
html.dark 类实现// 背景色
.bg-primary {
background-color: rgb(var(--art-bg-primary)) !important;
}
.bg-success {
background-color: rgb(var(--art-bg-success)) !important;
}
.bg-warning {
background-color: rgb(var(--art-bg-warning)) !important;
}
.bg-danger {
background-color: rgb(var(--art-bg-danger)) !important;
}
// 文本颜色
.text-primary {
color: rgb(var(--art-primary)) !important;
}
.text-success {
color: rgb(var(--art-success)) !important;
}
.text-warning {
color: rgb(var(--art-warning)) !important;
}
.text-danger {
color: rgb(var(--art-danger)) !important;
}