v0.2.0: CRM/ERP 系统升级 - 清理 .gitignore 并移除误提交的 venv/env/db 文件

- 更新 .gitignore:全面覆盖环境变量、数据库、日志、缓存、上传文件
- 移除误跟踪的 server/venv/、crm_data.db、.env 文件
- 新增 server/.env.example 模板
- 新增合同管理、利润核算、AI教练等功能模块
- 新增 Playwright e2e 测试套件
- 前后端多项功能升级和 bug 修复
This commit is contained in:
hankin
2026-05-11 07:24:19 +00:00
parent 0f4c6b7924
commit 815cbf9d8c
2526 changed files with 11875 additions and 804148 deletions
+51
View File
@@ -0,0 +1,51 @@
/**
* 前端路由守卫与权限 E2E 测试
* 覆盖: 未登录重定向 / 已登录访问 /login / Token 失效后跳转
*/
import { test, expect } from '@playwright/test';
// 不使用全局 auth
test.use({ storageState: { cookies: [], origins: [] } });
test.describe('路由守卫', () => {
test('未登录访问受保护页面被重定向到登录页', async ({ page }) => {
await page.goto('/customers');
await page.waitForURL(/\/login/);
await expect(page.locator('.login-title')).toBeVisible();
});
test('未登录访问订单页被重定向', async ({ page }) => {
await page.goto('/orders');
await page.waitForURL(/\/login/);
});
test('未登录访问设置页被重定向', async ({ page }) => {
await page.goto('/settings');
await page.waitForURL(/\/login/);
});
test('登录页直接可访问', async ({ page }) => {
await page.goto('/login');
// 不会重定向,直接显示登录页
await expect(page.locator('.login-title')).toContainText('CRM');
});
});
test.describe('Token 销毁后行为', () => {
test('清除 Token 后访问受保护页面被重定向', async ({ page }) => {
// 先登录
await page.goto('/login');
await page.getByPlaceholder('用户名').fill('admin');
await page.getByPlaceholder('密码').fill('123456');
await page.getByRole('button', { name: '登 录' }).click();
await page.waitForURL('/', { timeout: 10000 });
// 清除 localStorage(模拟 Token 销毁)
await page.evaluate(() => localStorage.clear());
// 访问受保护页面
await page.goto('/customers');
// 应被重定向到登录页
await page.waitForURL(/\/login/, { timeout: 5000 });
});
});
+27
View File
@@ -0,0 +1,27 @@
/**
* 全局认证 Setup
* 通过 UI 登录后持久化 storageState,后续所有测试复用登录态
*/
import { test as setup, expect } from '@playwright/test';
const authFile = 'e2e/.auth/user.json';
setup('authenticate', async ({ page }) => {
// 1. 访问登录页
await page.goto('/login');
await expect(page.locator('.login-title')).toContainText('天津硕博霖 CRM 系统');
// 2. 填写表单
await page.getByPlaceholder('用户名').fill('admin');
await page.getByPlaceholder('密码').fill('123456');
// 3. 点击登录
await page.getByRole('button', { name: '登 录' }).click();
// 4. 等待跳转到首页 (工作台)
await page.waitForURL('/', { timeout: 10000 });
await expect(page.locator('.el-menu--vertical').first()).toBeVisible({ timeout: 5000 });
// 5. 持久化认证状态 (localStorage token + cookies)
await page.context().storageState({ path: authFile });
});
+37
View File
@@ -0,0 +1,37 @@
/**
* 业务闭环 E2E 测试 (v4)
*/
import { test, expect } from '@playwright/test';
test.describe.serial('业务闭环: 客户 → 订单 → 发货', () => {
test('Step 1: 新增客户', async ({ page }) => {
await page.goto('/customers');
await page.waitForLoadState('networkidle');
await page.getByRole('button', { name: '新增客户' }).click();
await expect(page.locator('.el-dialog')).toBeVisible({ timeout: 3000 });
await page.getByPlaceholder('请输入客户公司名称').fill(`闭环_${Date.now()}`);
await page.getByPlaceholder('联系人姓名').fill('测试联系人');
await page.getByPlaceholder('所属行业').fill('测试行业');
await page.locator('.el-dialog__footer').getByRole('button', { name: /确|保存|提交/ }).click();
await expect(page.locator('.el-message--success')).toBeVisible({ timeout: 5000 });
});
test('Step 2: 创建订单', async ({ page }) => {
await page.goto('/orders');
await page.waitForLoadState('networkidle');
await page.getByRole('button', { name: '新建订单' }).click();
// el-drawer 打开
await expect(page.getByLabel('新建订单')).toBeVisible({ timeout: 5000 });
await expect(page.getByText('订单明细')).toBeVisible({ timeout: 3000 });
});
test('Step 3: 验证订单列表', async ({ page }) => {
await page.goto('/orders');
await page.waitForLoadState('networkidle');
await expect(page.locator('.el-table').first()).toBeVisible({ timeout: 5000 });
});
});
+35
View File
@@ -0,0 +1,35 @@
/**
* 合同管理 E2E 测试 (修正版)
*/
import { test, expect } from '@playwright/test';
test.describe('合同管理', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/contracts');
await page.waitForLoadState('networkidle');
});
test('合同列表正确加载', async ({ page }) => {
await expect(page.locator('.el-table').first()).toBeVisible({ timeout: 5000 });
// 用表头列来验证
await expect(page.getByRole('columnheader', { name: '合同编号' })).toBeVisible();
});
test('新增合同弹窗可打开', async ({ page }) => {
const addBtn = page.getByRole('button', { name: /新增|新建|创建/ });
if (await addBtn.isVisible()) {
await addBtn.click();
await page.waitForTimeout(1000);
// 可能是 dialog 或全屏
await expect(page.getByLabel('新增合同').first()).toBeVisible({ timeout: 5000 });
}
});
test('合同详情页导航', async ({ page }) => {
const viewBtn = page.locator('.el-table').getByRole('button', { name: /详情|查看/ }).first();
if (await viewBtn.isVisible({ timeout: 3000 })) {
await viewBtn.click();
await page.waitForURL(/contracts\/detail/, { timeout: 5000 });
}
});
});
+74
View File
@@ -0,0 +1,74 @@
/**
* 客户管理 E2E 测试 (修正版)
*/
import { test, expect } from '@playwright/test';
test.describe('客户管理', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/customers');
await page.waitForLoadState('networkidle');
});
test('客户列表正确加载', async ({ page }) => {
await expect(page.locator('.el-table').first()).toBeVisible({ timeout: 5000 });
await expect(page.getByRole('button', { name: '新增客户' })).toBeVisible();
});
test('新增客户完整流程', async ({ page }) => {
await page.getByRole('button', { name: '新增客户' }).click();
await expect(page.locator('.el-dialog')).toBeVisible({ timeout: 3000 });
const timestamp = Date.now();
await page.getByPlaceholder('请输入客户公司名称').fill(`E2E测试客户_${timestamp}`);
await page.getByPlaceholder('所属行业').fill('自动化测试');
await page.getByPlaceholder('联系人姓名').fill('张测试');
// 选择客户级别
await page.locator('.el-dialog .el-select').first().click();
await page.waitForTimeout(500);
// 选项: A级重点 / B级普通 / C级长尾
await page.getByRole('option').first().click();
// 提交
await page.locator('.el-dialog__footer').getByRole('button', { name: /确|保存|提交/ }).click();
await expect(page.locator('.el-dialog')).not.toBeVisible({ timeout: 5000 });
});
test('关键词搜索客户', async ({ page }) => {
const searchInput = page.getByPlaceholder(/搜索|客户名称|关键词/);
if (await searchInput.isVisible()) {
await searchInput.fill('中石化');
await page.getByRole('button', { name: '搜索' }).click();
await page.waitForTimeout(1000);
await expect(page.locator('.el-table').first()).toBeVisible();
}
});
test('查看客户档案详情', async ({ page }) => {
const viewBtn = page.locator('.el-table').getByRole('button', { name: '查看档案' }).first();
if (await viewBtn.isVisible({ timeout: 3000 })) {
await viewBtn.click();
await page.waitForURL(/customers\/detail/);
}
});
test('编辑客户信息', async ({ page }) => {
const editBtn = page.locator('.el-table').getByRole('button', { name: '编辑' }).first();
if (await editBtn.isVisible({ timeout: 3000 })) {
await editBtn.click();
await expect(page.locator('.el-dialog')).toBeVisible({ timeout: 3000 });
}
});
test('分页翻页', async ({ page }) => {
const pagination = page.locator('.el-pagination');
if (await pagination.isVisible()) {
const nextBtn = pagination.locator('.btn-next');
if (await nextBtn.isEnabled()) {
await nextBtn.click();
await page.waitForTimeout(1000);
await expect(page.locator('.el-table').first()).toBeVisible();
}
}
});
});
+38
View File
@@ -0,0 +1,38 @@
/**
* 工作台 Dashboard 测试
*/
import { test, expect } from '@playwright/test';
test.describe('工作台', () => {
test('统计卡片正确显示', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
// 实际卡片标题
await expect(page.getByText('本月新增订单')).toBeVisible({ timeout: 5000 });
await expect(page.getByText('待出库发货')).toBeVisible();
await expect(page.getByText('库存预警 SKU')).toBeVisible();
await expect(page.getByText('本月预计营收')).toBeVisible();
});
test('侧边栏菜单可点击导航', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
// 菜单在子菜单组中,需先展开 "业务线"
await page.getByText('业务线').click();
await page.waitForTimeout(500);
await page.getByRole('menuitem', { name: '客户管理' }).click();
await page.waitForURL('/customers');
await expect(page).toHaveURL('/customers');
});
test('快捷操作按钮可见', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
await expect(page.getByRole('button', { name: '新建订单' })).toBeVisible();
await expect(page.getByRole('button', { name: '安排发货' })).toBeVisible();
await expect(page.getByRole('button', { name: '库存入库' })).toBeVisible();
});
});
+36
View File
@@ -0,0 +1,36 @@
/**
* 财务模块 E2E 测试 (修正版)
*/
import { test, expect } from '@playwright/test';
test.describe('报销大盘', () => {
test('报销大盘页面加载', async ({ page }) => {
await page.goto('/finance');
await page.waitForLoadState('networkidle');
await expect(page.locator('body')).toContainText(/票据|发票|报销|费用/, { timeout: 5000 });
});
test('票据相关内容可见', async ({ page }) => {
await page.goto('/finance');
await page.waitForLoadState('networkidle');
// 只要页面有相关内容加载
await expect(page.locator('body')).toContainText(/报销|票据|费用|支出/, { timeout: 5000 });
});
});
test.describe('销项发票', () => {
test('销项发票页面加载', async ({ page }) => {
await page.goto('/finance/sales-invoices');
await page.waitForLoadState('networkidle');
await expect(page.locator('body')).toContainText(/销项发票|发票/, { timeout: 5000 });
});
test('销项发票列表', async ({ page }) => {
await page.goto('/finance/sales-invoices');
await page.waitForLoadState('networkidle');
const table = page.locator('.el-table').first();
if (await table.isVisible({ timeout: 5000 })) {
await expect(table).toBeVisible();
}
});
});
+68
View File
@@ -0,0 +1,68 @@
/**
* 登录页面测试
* 覆盖: 页面渲染 / 正确登录 / 错误密码 / 空字段校验 / 跳转
*/
import { test, expect } from '@playwright/test';
// 登录测试不使用全局 auth,独立登录
test.use({ storageState: { cookies: [], origins: [] } });
test.describe('登录页面', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login');
});
test('页面正确渲染', async ({ page }) => {
// 标题
await expect(page.locator('.login-title')).toContainText('天津硕博霖 CRM 系统');
await expect(page.locator('.login-subtitle')).toContainText('v2.0');
// 表单元素
await expect(page.getByPlaceholder('用户名')).toBeVisible();
await expect(page.getByPlaceholder('密码')).toBeVisible();
await expect(page.getByRole('button', { name: '登 录' })).toBeVisible();
});
test('正确账密登录后跳转工作台', async ({ page }) => {
await page.getByPlaceholder('用户名').fill('admin');
await page.getByPlaceholder('密码').fill('123456');
await page.getByRole('button', { name: '登 录' }).click();
// 等待跳转到首页
await page.waitForURL('/', { timeout: 10000 });
// 侧边栏菜单应出现
await expect(page.locator('.el-menu--vertical').first()).toBeVisible({ timeout: 5000 });
});
test('错误密码弹出错误提示', async ({ page }) => {
await page.getByPlaceholder('用户名').fill('admin');
await page.getByPlaceholder('密码').fill('wrong123');
await page.getByRole('button', { name: '登 录' }).click();
// Element Plus 的 ElMessage 错误提示
await expect(page.locator('.el-message--error')).toBeVisible({ timeout: 5000 });
});
test('空字段显示校验提示', async ({ page }) => {
// 直接点登录
await page.getByRole('button', { name: '登 录' }).click();
// el-form 校验提示
await expect(page.locator('.el-form-item__error')).toHaveCount(2);
await expect(page.locator('.el-form-item__error').first()).toContainText('请输入用户名');
});
test('密码少于6位提示校验', async ({ page }) => {
await page.getByPlaceholder('用户名').fill('admin');
await page.getByPlaceholder('密码').fill('123');
await page.getByRole('button', { name: '登 录' }).click();
await expect(page.locator('.el-form-item__error')).toContainText('密码至少 6 位');
});
test('未登录访问首页被重定向到登录页', async ({ page }) => {
await page.goto('/');
await page.waitForURL(/\/login/);
await expect(page.locator('.login-title')).toBeVisible();
});
});
+46
View File
@@ -0,0 +1,46 @@
/**
* 发货、利润、日志、复盘页面 (修正版)
*/
import { test, expect } from '@playwright/test';
test.describe('发货记录', () => {
test('发货页面加载', async ({ page }) => {
await page.goto('/shipping');
await page.waitForLoadState('networkidle');
// 验证页面加载了发货相关内容
await expect(page.locator('body')).toContainText(/发货|物流|订单/, { timeout: 5000 });
});
});
test.describe('利润核算', () => {
test('利润核算页面加载', async ({ page }) => {
await page.goto('/profit');
await page.waitForLoadState('networkidle');
await expect(page.locator('body')).toContainText(/利润|核算|成本/, { timeout: 5000 });
});
});
test.describe('销售日志', () => {
test('销售日志列表加载', async ({ page }) => {
await page.goto('/logs');
await page.waitForLoadState('networkidle');
await expect(page.locator('body')).toContainText(/销售日志|日志/, { timeout: 5000 });
});
test('新增日志入口', async ({ page }) => {
await page.goto('/logs');
await page.waitForLoadState('networkidle');
const addBtn = page.getByRole('button', { name: /新增|新建|撰写|写/ });
if (await addBtn.isVisible({ timeout: 3000 })) {
await expect(addBtn).toBeVisible();
}
});
});
test.describe('AI 智能复盘', () => {
test('复盘页面加载', async ({ page }) => {
await page.goto('/reports');
await page.waitForLoadState('networkidle');
await expect(page.locator('body')).toContainText(/复盘|报告|AI/, { timeout: 5000 });
});
});
+56
View File
@@ -0,0 +1,56 @@
/**
* 订单管理 E2E 测试 (v4 - 基于源码修正)
* 新建订单是 el-drawer size=85%
*/
import { test, expect } from '@playwright/test';
test.describe('订单管理', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/orders');
await page.waitForLoadState('networkidle');
});
test('订单列表正确加载', async ({ page }) => {
await expect(page.locator('.el-table').first()).toBeVisible({ timeout: 5000 });
await expect(page.getByRole('button', { name: '新建订单' })).toBeVisible();
});
test('打开新建订单抽屉', async ({ page }) => {
await page.getByRole('button', { name: '新建订单' }).click();
// el-drawer 打开
await expect(page.getByLabel('新建订单')).toBeVisible({ timeout: 5000 });
// 验证关键元素
await expect(page.getByText('订单明细')).toBeVisible();
});
test('新建订单页面元素验证', async ({ page }) => {
await page.getByRole('button', { name: '新建订单' }).click();
await expect(page.getByLabel('新建订单')).toBeVisible({ timeout: 5000 });
await expect(page.getByRole('button', { name: /确认开单/ })).toBeVisible();
await expect(page.getByRole('button', { name: '取消' })).toBeVisible();
await expect(page.getByText('添加产品行')).toBeVisible();
});
test('筛选订单', async ({ page }) => {
await page.getByRole('button', { name: '检索' }).click();
await page.waitForTimeout(1000);
await expect(page.locator('.el-table').first()).toBeVisible();
});
test('查看订单详情', async ({ page }) => {
const detailBtn = page.locator('.el-table').getByRole('button', { name: '详情' }).first();
if (await detailBtn.isVisible({ timeout: 3000 })) {
await detailBtn.click();
// 详情也是 el-drawer
await expect(page.getByLabel('订单全景详情')).toBeVisible({ timeout: 5000 });
}
});
test('发货操作入口', async ({ page }) => {
const shipBtn = page.locator('.el-table').getByRole('button', { name: '发货' }).first();
if (await shipBtn.isVisible({ timeout: 3000 }) && await shipBtn.isEnabled()) {
await expect(shipBtn).toBeVisible();
}
});
});
+51
View File
@@ -0,0 +1,51 @@
/**
* 产品与库存 E2E 测试
* 覆盖: 分类树 / SKU 列表 / 新增 SKU / 库存入库 / 流水查看
*/
import { test, expect } from '@playwright/test';
test.describe('产品与库存', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/products');
await page.waitForLoadState('networkidle');
});
test('页面正确加载', async ({ page }) => {
// 表格和操作按钮可见
await expect(page.locator('.el-table')).toBeVisible({ timeout: 5000 });
});
test('分类树可见', async ({ page }) => {
// 左侧分类树
const tree = page.locator('.el-tree');
if (await tree.isVisible({ timeout: 3000 })) {
await expect(tree).toBeVisible();
}
});
test('新增 SKU 弹窗', async ({ page }) => {
const addBtn = page.getByRole('button', { name: /新增|新建.*SKU|新建产品/ });
if (await addBtn.isVisible()) {
await addBtn.click();
await expect(page.locator('.el-dialog')).toBeVisible({ timeout: 3000 });
}
});
test('SKU 搜索', async ({ page }) => {
const searchInput = page.getByPlaceholder(/搜索|产品|SKU|关键词/);
if (await searchInput.isVisible()) {
await searchInput.fill('壳牌');
await page.waitForTimeout(1000);
// 表格应刷新
await expect(page.locator('.el-table')).toBeVisible();
}
});
test('库存操作入口', async ({ page }) => {
// 表格中应有库存/入库相关按钮
const inventoryBtn = page.locator('.el-table').getByRole('button', { name: /入库|库存|编辑/ }).first();
if (await inventoryBtn.isVisible({ timeout: 3000 })) {
await expect(inventoryBtn).toBeVisible();
}
});
});
+50
View File
@@ -0,0 +1,50 @@
/**
* 系统设置 E2E 测试 (根据真实截图修正)
* 三个 Tab:
* - 部门与员工管理
* - 角色与权限控制 (RBAC)
* - 企业账号配置
*/
import { test, expect } from '@playwright/test';
test.describe('系统设置', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/settings');
await page.waitForLoadState('networkidle');
});
test('设置页面正确加载', async ({ page }) => {
// 三个 Tab 标题
await expect(page.getByText('部门与员工管理')).toBeVisible({ timeout: 5000 });
await expect(page.getByText('角色与权限控制')).toBeVisible();
await expect(page.getByText('企业账号配置')).toBeVisible();
});
test('部门与员工管理 tab 内容', async ({ page }) => {
const deptTab = page.getByText('部门与员工管理').first();
await deptTab.click();
await page.waitForTimeout(1000);
// 应显示部门树或员工列表
await expect(page.locator('body')).toContainText(/部门|员工/, { timeout: 3000 });
});
test('角色与权限控制 tab', async ({ page }) => {
await page.getByText('角色与权限控制').first().click();
await page.waitForTimeout(1500);
// 左侧应有角色列表区域
await expect(page.getByText('平台运营角色')).toBeVisible({ timeout: 5000 });
await expect(page.getByText('新增角色')).toBeVisible();
});
test('新增角色按钮', async ({ page }) => {
await page.getByText('角色与权限控制').first().click();
await page.waitForTimeout(1000);
await expect(page.getByText('新增角色')).toBeVisible({ timeout: 3000 });
});
test('企业账号配置 tab', async ({ page }) => {
await page.getByText('企业账号配置').first().click();
await page.waitForTimeout(1000);
await expect(page.locator('body')).toContainText(/企业|账号/, { timeout: 3000 });
});
});