feat/m3-increments #1

Merged
liuwenxin merged 100 commits from feat/m3-increments into main 2026-10-19 04:59:57 -04:00
7 changed files with 81 additions and 41 deletions
Showing only changes of commit cc4c51359c - Show all commits

View File

@ -105,11 +105,12 @@ pnpm build # 类型检查 + 生产构建
**构图**:登录页品牌内容(名称 / 标语 / 水线)**垂直居中聚成一块**,配一层柔和径向光晕作落点。
早期版本用 `space-between` 把品牌与标语顶到两端,中间空出大半屏,两处都成了孤岛——超宽屏会把这种疏离放大得更明显。**内容块要限宽**(`max-width`),否则宽屏下文字会被拉散。
### 写代码时的三条约定
### 写代码时的四条约定
- 覆盖 Element Plus 主题请改 **CSS 变量**(`--el-button-bg-color` 等),不要直接写 `background-color` —— 后者与 `.el-button--primary` 同权重,会受样式注入顺序影响而不稳定
- 中文可在任意字间断行,**关键文案的断行位置要显式控制**(如主标语拆成两个 `<span>`),交给浏览器自动换行会出现单字独占一行的难看结果
- 新页面需响应式到移动端、保留键盘焦点可见、并尊重 `prefers-reduced-motion`
- **确认框统一加 `customClass: 'ys-confirm-box'`,不要再传 `type: 'warning'`** —— Element Plus 的默认 warning 图标会渲染在正文左侧独立成列(多行正文时图标悬在文字块中间),与 v3 靛蓝体系也冲突。全局样式(`src/assets/styles/index.css`)已把图标改为 18px 描边样式并放在标题行左侧,正文与标题文字对齐
## 验证码图片
@ -181,3 +182,31 @@ E2E_ADMIN_PASSWORD=<开发库admin密码> python e2e/system_management.py
运行前置同上(需 `E2E_ADMIN_PASSWORD`)。**期间修复的缺陷**:public 端点 300s 响应缓存让登录页拿旧公司名(改 `fetch cache: no-store` + 时间戳绕过磁盘缓存);业务弹窗分配表与产品明细共用 `items-table` 类导致选择器错位(分配表加专属 `alloc-table` 类)。
**回归**:既有五脚本(登录 8 / 系统管理 13 / 基础数据 12 / 业务 16 / 参数 7 步)在 allowedRoles 路由扩展与品牌名动态化后全部复跑通过。
## 前端页面(2026-10-19 追加 · M4 ⑤ 报表统计)
| 页面 | 说明 |
| --- | --- |
| `/reports/personal-detail` 个人绩效明细(新组「报表统计」order19) | 人员/月份/客户类型/产品类别/岗位筛选;需规 2.4.1 全部列;行展开懒加载台账快照;页脚**全筛选合计**(注明非当前页);导出/打印 |
| `/reports/department-summary` 部门绩效汇总(order20) | 月份必填;全公司 5% 独立卡片;团队表(基数/12.5%/5%/占比/调整/净合计/月结冻结额);展开成员 → 跨页下钻个人明细;未分配桶 |
| `/reports/business-stats` 业务统计(order21) | 关键字/客户/归属人/状态/业务日期;发货额/回款额/绩效(累计口径提示);展开下钻回款与台账明细 |
| `/reports/payment-stats` 回款统计(order22) | 月份/回款日期/客户类型/产品类别;8 档区间表(空桶 0,档位由后端下发);展开桶下钻分页明细;客户类型/产品类别对比表 |
| `/reports/print/:code` 报表打印(顶层路由,`menu:false`) | 不挂 BasicLayout(打印不带侧栏/顶栏);页眉公司信息走 public 端点;循环翻页取全量(上限 2000 行截断提示);`@media print` A4 横版 + 每页表头;签字栏;PDF 由浏览器「另存为」覆盖 |
- 路由组「报表统计」四页 `allowedRoles: [admin, finance, leader]`(前端菜单 + 后端 `require_roles` 双层;admin 须显式列入)。
- api 封装 `src/api/reports.ts`(类型 + 查询 + `exportReport` 裸 axios blob;产品类别标签额外覆盖 `mixed`)。
- 权限:营销后勤「授权范围内」待业务权限清单 #6,当前不开放报表。
- type-check 与 build 通过(2026-10-19)。e2e `report_flow.py` 待补(需 `E2E_ADMIN_PASSWORD` 环境)。
## 端到端测试(2026-10-19 追加 · M3 台账与月结)
| 脚本 | 覆盖 | 结果 |
| --- | --- | --- |
| `e2e/calc_settlement_flow.py` | 绩效核算与月度结算 8 步:登录进台账页 → API 预置(客户/人员/业务/发货/回款/核算,随机远未来月份防碰撞)→ 按归属月筛选见 v1 行 → 重算对话框(原因必填)→ v2 生效 → 版本三态筛选(仅历史 / 仅当前 / 清空=全部)→ 生成月结草稿 → 提交审批(开关显式关闭=自动通过锁定)→ 详情抽屉(按人明细/净工资/锁定状态) | ✓ 全通过(2026-10-19 首跑,修复下述缺陷后) |
运行前置同上(需 `E2E_ADMIN_PASSWORD`);数据 `e2e_` 前缀 + 随机远未来月份(209x),核算/月结按设计不可删,行数据留档开发库。
**首跑即抓出三处真实前端缺陷**(M3 台账/月结两页自 2026-10-17 交付起一直「暂无数据」):
① `src/api/{calc,settlements,approvals}.ts` 误用默认导出 `request`(axios 实例,返回 AxiosResponse 未解包),列表 `list` 恒为 undefined → 三页永远「暂无数据」;改为具名导入 `{ request }`(信封解包版)。
② 三页均无 `onMounted` 初始加载 → 进页不取数;补 `onMounted(() => void load())`。
③ 三页 `AppPagination` 用 `v-model:page-num` + `@change`(组件无该事件、也未实现 `update:*`)→ 翻页失效;统一为 `:page-num/:page-size` + `@page-change/@size-change`(usePagedList 的 onPageChange/onSizeChange)。
**注意**:e2e 与 `pytest tests/integration` **不可并发**——conftest 每用例整体清空开发库,会清掉 e2e 造的数据造成假失败(本次排查耗时即源于此,两次假失败的报错均为「台账行消失 / 401 登录失效」)。

View File

@ -1,4 +1,4 @@
import request from './request'
import { request } from './request'
import type { PageData } from './types'
/** 审批单状态:1待财务复核 2待领导审核 3已通过 4已驳回 5已撤回 */

View File

@ -1,4 +1,4 @@
import request from './request'
import { request } from './request'
import type { PageData } from './types'
/** 绩效台账行(金额/比例一律 str) */

View File

@ -1,4 +1,4 @@
import request from './request'
import { request } from './request'
import type { PageData } from './types'
/** 月结状态:1草稿 2待审批 3已审定(=已锁定)4已驳回 */

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import * as calcApi from '@/api/calc'
@ -21,13 +21,15 @@ const filters = reactive({
is_active: 1 as number | undefined,
})
const { records, total, pageNum, pageSize, loading, load, search } = usePagedList<
LedgerRow,
{ settlement_month?: string; is_active?: number }
>(calcApi.listLedger, () => ({
settlement_month: filters.settlement_month.trim() || undefined,
is_active: filters.is_active,
}))
const { records, total, pageNum, pageSize, loading, load, search, onPageChange, onSizeChange } =
usePagedList<LedgerRow, { settlement_month?: string; is_active?: number }>(calcApi.listLedger, () => ({
settlement_month: filters.settlement_month.trim() || undefined,
is_active: filters.is_active,
}))
onMounted(() => {
void load()
})
function resetFilters(): void {
filters.settlement_month = ''
@ -121,7 +123,7 @@ async function submitRecalc(): Promise<void> {
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="90" fixed="right">
<el-table-column label="操作" width="90" fixed="right" align="center">
<template #default="{ row }">
<el-button
v-if="row.is_active === 1 && row.calc_type === 1"
@ -136,10 +138,11 @@ async function submitRecalc(): Promise<void> {
</el-table>
<AppPagination
v-model:page-num="pageNum"
v-model:page-size="pageSize"
:total="total"
@change="load"
:page-num="pageNum"
:page-size="pageSize"
@page-change="onPageChange"
@size-change="onSizeChange"
/>
<el-dialog v-model="recalcVisible" title="重算确认" width="460px">

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import {
@ -27,10 +27,12 @@ const filters = reactive({
status: undefined as number | undefined,
})
const { records, total, pageNum, pageSize, loading, load, search } = usePagedList<
Settlement,
{ status?: number }
>(listSettlements, () => ({ status: filters.status }))
const { records, total, pageNum, pageSize, loading, load, search, onPageChange, onSizeChange } =
usePagedList<Settlement, { status?: number }>(listSettlements, () => ({ status: filters.status }))
onMounted(() => {
void load()
})
function resetFilters(): void {
filters.status = undefined
@ -139,27 +141,30 @@ async function handleSubmit(row: Settlement): Promise<void> {
<el-table-column label="提交时间" min-width="150">
<template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
</el-table-column>
<el-table-column label="操作" width="130" fixed="right">
<el-table-column label="操作" width="140" fixed="right" align="center">
<template #default="{ row }">
<el-button link type="primary" @click="openDetail(row)">详情</el-button>
<el-button
v-if="row.status === 1 || row.status === 4"
link
type="success"
:loading="submitting"
@click="handleSubmit(row)"
>
提交审批
</el-button>
<div class="table-actions">
<el-button link type="primary" @click="openDetail(row)">详情</el-button>
<el-button
v-if="row.status === 1 || row.status === 4"
link
type="success"
:loading="submitting"
@click="handleSubmit(row)"
>
提交审批
</el-button>
</div>
</template>
</el-table-column>
</el-table>
<AppPagination
v-model:page-num="pageNum"
v-model:page-size="pageSize"
:total="total"
@change="load"
:page-num="pageNum"
:page-size="pageSize"
@page-change="onPageChange"
@size-change="onSizeChange"
/>
<el-drawer v-model="detailVisible" title="月结详情" size="560px">

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import {
@ -47,6 +47,10 @@ const { records, total, pageNum, pageSize, loading, load, search, onPageChange,
}),
)
onMounted(() => {
void load()
})
function resetFilters(): void {
filters.keyword = ''
filters.status = undefined
@ -177,7 +181,7 @@ async function submitAction(): Promise<void> {
<el-table-column label="提交时间" min-width="150">
<template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
</el-table-column>
<el-table-column label="操作" width="90" fixed="right">
<el-table-column label="操作" width="90" fixed="right" align="center">
<template #default="{ row }">
<el-button link type="primary" @click="openDetail(row)">详情</el-button>
</template>
@ -185,12 +189,11 @@ async function submitAction(): Promise<void> {
</el-table>
<AppPagination
v-model:page-num="pageNum"
v-model:page-size="pageSize"
:total="total"
@change="load"
@size-change="onSizeChange"
:page-num="pageNum"
:page-size="pageSize"
@page-change="onPageChange"
@size-change="onSizeChange"
/>
<el-drawer v-model="detailVisible" title="审批详情" size="480px">