feat: 完善项目开发规范和测试文档体系
📝 主要改进: • Cursor配置模块化:拆分为前端、后端、移动端、文档、API测试5个专门规范文件 • API文档重新组织:按业务模块拆分到docs/开发文档/API/目录下 • 新增维修分类功能完整测试方案和自动化测试脚本 • 后端热启动脚本完善,支持Linux/Windows双平台 • 强化文档同步更新规范,确保代码与文档一致性 🔧 技术改进: • 优化Cursor AI工作流程和代码生成质量 • 完善API测试规范(AES+RSA双重加密登录方法) • 提升开发效率和代码质量标准化程度 �� 测试覆盖: • 维修分类管理功能端到端测试验证通过 • 包含页面加载、CRUD操作、搜索功能等核心测试用例 更新时间:2025-07-01
This commit is contained in:
1 parent
7a5b9d1bea
commit
249b1dfeb4
44 files changed
+14027
-2852
No files matched your search
@@ -0,0 +1,684 @@
|
||||
# Cursor Rules - Web 前端开发规范
|
||||
|
||||
## 适用范围
|
||||
|
||||
- **项目目录**: `web/` (基于 Vue 3 + TypeScript + Element Plus)
|
||||
- **开发环境**: Node.js 20.15.x+, npm 10.7.x+
|
||||
- **前端访问**: http://localhost (默认 80 端口)
|
||||
|
||||
---
|
||||
|
||||
# Web 前端开发规范 (web/)
|
||||
|
||||
## 1. 技术栈规范
|
||||
|
||||
### 核心技术
|
||||
|
||||
- **框架**: Vue 3.5.13 + TypeScript 5.8.3
|
||||
- **UI 组件库**: Element Plus 2.9.8
|
||||
- **构建工具**: Vite 7.0.9
|
||||
- **状态管理**: Pinia
|
||||
- **路由管理**: Vue Router 4
|
||||
- **HTTP 客户端**: Axios
|
||||
- **代码风格**: ESLint + Prettier
|
||||
- **Node.js**: 20.15.x+ (推荐 20.15.0)
|
||||
- **npm**: 10.7.x+
|
||||
|
||||
### 环境要求
|
||||
|
||||
- **开发环境**: 支持本地环境或阿里云测试环境
|
||||
- **数据库**: MySQL/Redis 支持本机安装或使用阿里云环境
|
||||
- **前端访问**: http://localhost (默认 80 端口)
|
||||
- **后端服务**: http://localhost:8080
|
||||
|
||||
## 2. Art Design Pro UI 规范
|
||||
|
||||
### 设计理念
|
||||
|
||||
基于 Art Design Pro 的设计语言,追求现代化、精致、用户友好的界面体验。注重细节、动效和用户交互的流畅性。
|
||||
|
||||
### 颜色系统
|
||||
|
||||
#### 主色调 (Primary Colors)
|
||||
|
||||
```css
|
||||
--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; /* 危险色 - 红色 */
|
||||
```
|
||||
|
||||
#### 背景色系统
|
||||
|
||||
```css
|
||||
/* 浅色主题 */
|
||||
--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)按钮设计,提供更加优雅、现代和一致的用户界面体验。
|
||||
|
||||
#### 核心规范
|
||||
|
||||
- **Primary 按钮**: 轮廓式主色设计,主色边框和文字,5%透明度主色背景
|
||||
- **Success 按钮**: 轮廓式成功色设计,成功色边框和文字,5%透明度成功色背景
|
||||
- **Warning 按钮**: 轮廓式警告色设计,警告色边框和文字,5%透明度警告色背景
|
||||
- **Danger 按钮**: 轮廓式危险色设计,危险色边框和文字,5%透明度危险色背景
|
||||
- **Info 按钮**: 轮廓式信息色设计,信息色边框和文字,5%透明度信息色背景
|
||||
- **Default 按钮**: 白色背景,灰色边框,灰色文字
|
||||
|
||||
#### 交互效果
|
||||
|
||||
- **悬浮状态**: 上移 1px + 背景透明度变为 10% + 边框透明度变为 80% + 彩色阴影
|
||||
- **点击状态**: 98%缩放 + 0.1s 快速过渡 + 背景透明度变为 15%
|
||||
- **禁用状态**: 50%透明度,禁用所有交互和变换
|
||||
|
||||
## 3. 组件开发规范
|
||||
|
||||
### Vue 组件结构标准
|
||||
|
||||
```vue
|
||||
<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>
|
||||
```
|
||||
|
||||
### 页面开发规范
|
||||
|
||||
#### 列表页面标准模板
|
||||
|
||||
```vue
|
||||
<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>
|
||||
```
|
||||
|
||||
#### 表单页面标准模板
|
||||
|
||||
```vue
|
||||
<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>
|
||||
```
|
||||
|
||||
## 4. API 开发规范
|
||||
|
||||
### API 接口定义标准
|
||||
|
||||
```typescript
|
||||
// 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',
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### TypeScript 类型定义
|
||||
|
||||
```typescript
|
||||
// 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;
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 状态管理规范
|
||||
|
||||
### Pinia Store 结构
|
||||
|
||||
```typescript
|
||||
// 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,
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
## 6. 路由管理规范
|
||||
|
||||
### 路由定义标准
|
||||
|
||||
```typescript
|
||||
// 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;
|
||||
```
|
||||
|
||||
## 7. 项目架构规范
|
||||
|
||||
### 目录结构标准
|
||||
|
||||
```
|
||||
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 # 主入口文件
|
||||
```
|
||||
|
||||
## 8. 代码规范
|
||||
|
||||
### 命名规范
|
||||
|
||||
- **文件命名**: 使用 kebab-case (如: user-list.vue)
|
||||
- **组件命名**: 使用 PascalCase (如: UserList)
|
||||
- **方法命名**: 使用 camelCase (如: getUserList)
|
||||
- **常量命名**: 使用 UPPER_SNAKE_CASE (如: API_BASE_URL)
|
||||
- **CSS 类名**: 使用 kebab-case (如: .user-list-container)
|
||||
|
||||
### ESLint 配置
|
||||
|
||||
```typescript
|
||||
// .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',
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Prettier 配置
|
||||
|
||||
```json
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"printWidth": 100,
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
```
|
||||
|
||||
## 9. 性能优化
|
||||
|
||||
### 代码分割
|
||||
|
||||
```typescript
|
||||
// 路由懒加载
|
||||
const Home = () => import('@/views/home/index.vue');
|
||||
|
||||
// 组件懒加载
|
||||
const LazyComponent = defineAsyncComponent(() => import('@/components/LazyComponent.vue'));
|
||||
```
|
||||
|
||||
### 缓存策略
|
||||
|
||||
```typescript
|
||||
// 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>
|
||||
```
|
||||
|
||||
## 10. 错误处理
|
||||
|
||||
### 全局错误处理
|
||||
|
||||
```typescript
|
||||
// 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);
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
## 11. 测试规范
|
||||
|
||||
### 单元测试
|
||||
|
||||
```typescript
|
||||
// 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 测试
|
||||
|
||||
```typescript
|
||||
// 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();
|
||||
});
|
||||
```
|
||||
Reference in new issue
Block a user