统一编辑模板并移除首批台账工具(05-31-business-edit-template-remove-ledger-tools-wave1)
这个提交包含在:
@@ -74,11 +74,13 @@ reference, not a copy of Feishu or Ant Design branding.
|
||||
- List pages use a right-side operation column or card tail. Default row actions
|
||||
are `查看详情`, `处理`, `继续编辑` or `查看进度`; low-frequency or risky actions
|
||||
such as export, batch, void, download, payment and sensitive attachment
|
||||
handling move to `更多`, `台账工具` or the detail page.
|
||||
handling move to `更多`, clearly named permissioned actions, management
|
||||
entries or the detail page.
|
||||
- Business ledger pages use the same shape: page title and optional creation
|
||||
action, visible common filters, table/card list and right-side operation
|
||||
column. `台账工具` contains export, batch, statistics, templates, full-scope
|
||||
views and advanced filters.
|
||||
column. Do not use a generic `台账工具` entry; expose low-frequency abilities
|
||||
through concrete names such as `导出`, `统计`, `筛选`, `导入`, `新建方式`,
|
||||
`分类管理`, `批量处理` or `更多`.
|
||||
- Business ledger first screens are single-column list pages. Do not use a
|
||||
default left-list/right-detail workspace for procurement, contracts, projects,
|
||||
assets, payments, AP or invoices. Open details through a detail route, drawer
|
||||
@@ -90,8 +92,8 @@ reference, not a copy of Feishu or Ant Design branding.
|
||||
- Ledger filters fit the first PC row around `search`, `status` and `scope`
|
||||
when those capabilities exist. Supplier, legal entity, cost center, date
|
||||
ranges, contract type, sensitivity, export and test-data toggles belong in
|
||||
`筛选`, `台账工具` or a tab-specific advanced area; default-visible filters
|
||||
should not occupy multiple rows.
|
||||
`筛选`, `更多`, clearly named role-only actions or a tab-specific advanced
|
||||
area; default-visible filters should not occupy multiple rows.
|
||||
- Business list columns show object identity, counterpart/person, amount/date,
|
||||
state and operation. `下一步` is not a default list column or right-side help
|
||||
panel. Use short state labels such as `待补材料` or `待你处理`; longer guidance
|
||||
@@ -118,7 +120,7 @@ reference, not a copy of Feishu or Ant Design branding.
|
||||
| `工作台` | 工作台总览 | Compact counts, common entries, real reminders | No forced header action. Use `去处理`, `新建报销`, `查看全部` inside relevant sections; do not show helper sentences for ordinary items. |
|
||||
| `待我处理` | 任务处理列表 | `待处理` / `已处理` tabs, count, minimal filters, task list | Row action is `处理`; `已处理记录` is a tab, not a separate header button. |
|
||||
| `审批详情` | 详情工作区 | Summary, facts/evidence, right summary/action panel, history | One action bar: primary `通过`, secondary `退回`/`驳回`, `转审` in `更多`. |
|
||||
| `报销` / `采购` / `合同` / `项目` / `资产` / `付款` / `应付账款` / `票据` | 业务台账列表 | One-row common filters plus table/card list | Header creation only when the route owns creation; risky tools in `台账工具` or detail; no default left-list/right-detail split. |
|
||||
| `报销` / `采购` / `合同` / `项目` / `资产` / `付款` / `应付账款` / `票据` | 业务台账列表 | One-row common filters plus table/card list | Header creation only when the route owns creation; risky tools use `更多`, clearly named role-only actions or detail pages; no default left-list/right-detail split. |
|
||||
| Business details | 详情工作区 | Summary, facts, evidence, relations, history | Right-side state/action panel on PC; bottom primary action on mobile. |
|
||||
| `管理` | 管理配置页 | Grouped landing: `人员组织`, `审批流程`, `基础资料`, `系统设置`, `审计排障` | Management subpages use search/filter, table and right operation column. |
|
||||
| `个人设置` | 管理配置页 variant | `个人资料`, `账号安全`, `通知偏好`, `企业微信绑定` | No employee dossier, permission matrix or org maintenance in the default profile path. |
|
||||
@@ -339,11 +341,23 @@ matrices and actions explicit in each page.
|
||||
main facts/evidence/history on the left, status/actions/compact summary on
|
||||
the right. Keep the page's business fields, permission checks, state
|
||||
conditions and API calls explicit in the page component.
|
||||
- For business edit/create pages, prefer the thin components in
|
||||
`client/src/components/patterns/BusinessEditTemplate.tsx`:
|
||||
`BusinessEditTemplate`, `BusinessEditWorkspace`, `BusinessEditMain`,
|
||||
`BusinessEditAside`, `BusinessEditSection`, `BusinessEditFormGrid`,
|
||||
`BusinessEditValidationSummary` and `BusinessEditActionBar`. They standardize
|
||||
title/back/status/action, sectioned form layout, validation summary and bottom
|
||||
save/submit coordinates only; fields, permissions, mutations and dirty-state
|
||||
decisions stay explicit in the page.
|
||||
- `BusinessDetailActionPanel` is the default PC detail operation surface.
|
||||
Use one primary action for save/create when the detail owns editing; use a
|
||||
horizontal action bar for approval actions; move low-frequency actions into
|
||||
`更多` or a clearly named grouped section. Do not add a second page-local
|
||||
`操作` card or a separate `摘要` card under the same right column.
|
||||
- Do not introduce or preserve a generic `台账工具` button on redesigned business
|
||||
pages. Move each ability to a concrete enterprise action coordinate: `新建` /
|
||||
`新建方式`, `筛选`, `导出`, `统计`, `批量处理`, row `更多`, detail action panel
|
||||
or an explicitly named management entry such as `资产分类`.
|
||||
- Do not create a configuration-driven page generator for reimbursement,
|
||||
procurement, contracts, projects, assets, payments, AP or invoices. These
|
||||
pages have different permissions, state transitions, attachment rules and
|
||||
@@ -452,7 +466,7 @@ page-local action placement.
|
||||
- List filters live in the first command bar below the page header. Search,
|
||||
status, scope and other common filters stay on the left; ledger tools such as
|
||||
export, batch, statistics, templates and advanced filters stay on the right or
|
||||
inside `台账工具` / `更多`.
|
||||
inside `筛选`, `更多` or concrete role-only action groups.
|
||||
- Row-level actions live at the right edge of a table row or at the tail of a
|
||||
card. Prefer `查看详情`, `查看处理` and `继续编辑` over broad verbs that hide the
|
||||
result.
|
||||
@@ -567,8 +581,8 @@ of exposing every allowed tool at once.
|
||||
they must not duplicate approval action buttons.
|
||||
- Finance and cashier views may expose authorized reimbursement, payment,
|
||||
invoice, AP, receipt and exception operations. Full-scope, export, batch,
|
||||
delegated creation and statistics tools belong in `台账工具`, `更多` or
|
||||
role-only panels.
|
||||
delegated creation and statistics tools belong in concrete actions such as
|
||||
`导出`, `统计`, `批量处理`, `新建方式`, `更多` or role-only panels.
|
||||
- Owner and project-lead views emphasize team/project scope, blockers, risks and
|
||||
approvals. They should not drift into system configuration unless the route is
|
||||
a management route.
|
||||
@@ -666,8 +680,8 @@ screen.
|
||||
to the current user and the next action.
|
||||
- Place common filters on the left side of the command bar below the page
|
||||
header. Put export, batch, statistics, templates, full-scope, advanced
|
||||
filters, delegated creation and low-frequency module links in `台账工具`,
|
||||
`更多` or role-only panels.
|
||||
filters, delegated creation and low-frequency module links in `筛选`,
|
||||
concrete right-side actions, `更多` or role-only panels.
|
||||
- Batch actions appear only after selection, as a contextual command bar or
|
||||
bottom sticky bar. High-risk batch actions such as approve, pay, delete or
|
||||
publish require summary confirmation and result details.
|
||||
@@ -686,13 +700,13 @@ screen.
|
||||
boundary. The list is for discovery, filtering, status reading and navigation.
|
||||
High-risk or sensitive actions such as contract download, archive, payment,
|
||||
export, batch processing, void/cancel and sensitive attachment viewing belong
|
||||
in detail workspaces, detail command panels or `台账工具`, with permission,
|
||||
audit and confirmation.
|
||||
in detail workspaces, detail command panels, concrete role-only actions or
|
||||
`更多`, with permission, audit and confirmation.
|
||||
- Default ledger scope follows the current task role: ordinary employees see
|
||||
`我发起` / `我相关`, owners and project leads see `我负责` / `项目相关`, finance
|
||||
sees `待复核` / `待付款` / `异常`, and admins may enter or switch to `全范围`.
|
||||
Full scope, export and batch commands stay in `台账工具` or role-only panels,
|
||||
not ordinary first screens.
|
||||
Full scope, export and batch commands stay in concrete role-only actions or
|
||||
panels, not ordinary first screens.
|
||||
- Contract ledgers in V1 are registration, archive, due/renewal reminder and
|
||||
relation surfaces for project, procurement and payment context. Do not design
|
||||
`/contracts-registry` as e-signature, legal-review, clause-library,
|
||||
@@ -725,8 +739,8 @@ screen.
|
||||
creation; `应付账款` shows pending payment, exceptions, risks and differences
|
||||
before export; `票据` shows OCR status, failed invoices and bindable invoices
|
||||
before broad-scope handover tools. Batch creation, export, full-scope views
|
||||
and agency-accounting handover controls live in `台账工具` or selected-context
|
||||
panels, never as the first-screen primary action.
|
||||
and agency-accounting handover controls live in concrete role-only actions,
|
||||
`更多` or selected-context panels, never as the first-screen primary action.
|
||||
- Payment actions require the finance/payment context: amount, payee or target
|
||||
object, source records, offline payment explanation, evidence completeness,
|
||||
receipt requirement and correction boundary. Do not place `标记已付款`,
|
||||
@@ -749,7 +763,8 @@ and management workflows.
|
||||
- `cd client && npm run typecheck`
|
||||
- `cd client && npm run build`
|
||||
- Focused Playwright or screenshot checks when changing list filters, command
|
||||
bars, `台账工具`, batch selection behavior, empty states or mobile list cards.
|
||||
bars, advanced action placement, batch selection behavior, empty states or
|
||||
mobile list cards.
|
||||
|
||||
## Convention: Asset Registry Pages
|
||||
|
||||
@@ -920,8 +935,8 @@ state, facts and consequences before acting.
|
||||
- List and ledger rows show only evidence completeness summaries, missing
|
||||
evidence hints and a detail entry. Invoice preview, attachment download,
|
||||
contract file viewing, receipt download, evidence deletion and bulk packaging
|
||||
belong in detail workspaces, detail command panels, `台账工具` or authorized
|
||||
audit paths.
|
||||
belong in detail workspaces, detail command panels, `更多`, concrete
|
||||
role-only actions or authorized audit paths.
|
||||
- Sensitive evidence such as invoices, contracts, receipts and attachments must
|
||||
be permissioned; download and export belong in `更多` or audit paths when
|
||||
appropriate.
|
||||
@@ -1070,8 +1085,8 @@ summary unless a permissioned workflow requires more.
|
||||
- Download, batch download, export, print, attachment packaging, contract text
|
||||
export and finance ledger export must be controlled by server permissions and
|
||||
data scope.
|
||||
- Export is never the primary first-screen action. Put exports in `台账工具`,
|
||||
`更多` or role-only finance/admin panels.
|
||||
- Export is never the primary first-screen action. Put exports in a concrete
|
||||
`导出` action, `更多` or role-only finance/admin panels.
|
||||
- Before exporting sensitive ledgers, explain scope and sensitive contents.
|
||||
Finance, contract, asset and invoice exports should write audit records.
|
||||
- Export file names use business numbers and dates. Do not concatenate complete
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{"file": ".trellis/spec/hyb-oa/index.md", "reason": "hyb-oa spec entry and task read order"}
|
||||
{"file": ".trellis/spec/hyb-oa/project-governance.md", "reason": "Trellis task, solo-developer scope, commit and validation rules"}
|
||||
{"file": ".trellis/spec/hyb-oa/frontend-react-vite-design.md", "reason": "frontend template, navigation, list/detail/edit and visual design rules"}
|
||||
{"file": ".trellis/spec/hyb-oa/docs-and-evidence.md", "reason": "evidence and task documentation hygiene"}
|
||||
{"file": ".trellis/spec/hyb-oa/security-privacy.md", "reason": "export and sensitive ledger action placement constraints"}
|
||||
@@ -0,0 +1,4 @@
|
||||
{"file": ".trellis/spec/hyb-oa/index.md", "reason": "hyb-oa spec entry and task read order"}
|
||||
{"file": ".trellis/spec/hyb-oa/project-governance.md", "reason": "Trellis task, solo-developer scope, commit and validation rules"}
|
||||
{"file": ".trellis/spec/hyb-oa/frontend-react-vite-design.md", "reason": "frontend template, navigation, list/detail/edit and visual design rules"}
|
||||
{"file": ".trellis/spec/hyb-oa/security-privacy.md", "reason": "export and sensitive ledger action placement constraints"}
|
||||
@@ -0,0 +1,14 @@
|
||||
# Implementation Notes
|
||||
|
||||
## Plan
|
||||
|
||||
1. Add thin edit-template components that standardize form page coordinates without becoming a JSON form generator.
|
||||
2. Remove `台账工具` from reimbursement and asset list pages and migrate its contents into explicit actions, advanced filter areas, row actions or page header actions.
|
||||
3. Update frontend design specs so future pages follow the same template and action-placement rules.
|
||||
4. Run frontend typecheck/build and focused text checks for the first batch.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
- The edit template owns layout only: header/action strip, section spacing, validation summary, form grid and sticky bottom action bar.
|
||||
- Business pages keep their fields, permissions, mutations, state transitions and error handling explicitly in page code.
|
||||
- The generic `台账工具` name is removed from first-batch user-facing pages; every action should use a concrete verb such as `导出`, `统计`, `批量处理`, `新建`, `导入` or `更多`.
|
||||
@@ -0,0 +1,82 @@
|
||||
# 建立业务编辑模板并移除台账工具首批
|
||||
|
||||
## Goal
|
||||
|
||||
按已确认的 frontend-design 规则建立业务编辑页模板,并先在报销、资产两个高频页面取消 `台账工具` 入口,把原有能力迁移到具体、可理解的动作位置,降低国内小团队日常使用复杂度。
|
||||
|
||||
## What I Already Know
|
||||
|
||||
- 面向对象是国内小于 10 人的科技初创公司,系统应优先低复杂度、高频动作和成熟后台惯例。
|
||||
- 当前已有列表模板 `BusinessListTemplate.tsx` 和详情模板 `BusinessDetailTemplate.tsx`。
|
||||
- 当前还没有统一编辑模板。
|
||||
- 用户明确不想要 `台账工具` 这个泛化入口。
|
||||
- 本轮采用成熟 Ant Design / Ant Design Pro 式后台结构:页头主动作、状态/范围切换、筛选、表格、选中后批量条、详情右侧操作区、编辑页分区表单。
|
||||
|
||||
## Scope
|
||||
|
||||
- 新增 `BusinessEditTemplate` 及配套编辑页布局组件。
|
||||
- 首批只迁移报销列表和资产列表中的 `台账工具` 入口。
|
||||
- 更新 frontend 设计规范,固化三类模板和 `台账工具` 禁用规则。
|
||||
- 补充或调整必要的前端测试断言。
|
||||
|
||||
## Requirements
|
||||
|
||||
- 列表、详情、编辑模板必须只统一布局和交互槽位,不做 JSON 配置式页面生成器。
|
||||
- `台账工具` 按钮名不得继续出现在报销、资产首批页面。
|
||||
- 报销列表:
|
||||
- `新建报销` 保持页头主按钮。
|
||||
- 更多创建方式归入 `新建报销` 的下拉或明确命名入口。
|
||||
- 导出使用明确的 `导出` 入口。
|
||||
- 全范围/仅自己/审批中台账使用范围或筛选动作,不藏在 `台账工具`。
|
||||
- 资产列表:
|
||||
- 常用列表筛选和状态切换保持在列表上方。
|
||||
- 导出、盘点、导入等能力使用具体动作名或详情/管理入口,不使用 `台账工具`。
|
||||
- 编辑模板应支持:
|
||||
- 标题、返回、状态;
|
||||
- 右上角操作;
|
||||
- 底部固定操作条;
|
||||
- 分区表单;
|
||||
- 校验摘要;
|
||||
- 未保存提醒接入点。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] 新增业务编辑模板组件并可被后续页面复用。
|
||||
- [x] 报销页不再出现 `台账工具` 文案,原功能仍可通过具体按钮/下拉访问。
|
||||
- [x] 资产页不再出现 `台账工具` 文案,原功能仍可通过具体按钮/下拉访问或合理迁移。
|
||||
- [x] 前端规范记录三类模板和 `台账工具` 禁用规则。
|
||||
- [x] `cd client && npm run typecheck` 通过。
|
||||
- [x] `cd client && npm run build` 通过。
|
||||
- [x] 相关 Playwright/静态检查按需通过。
|
||||
|
||||
## Out Of Scope
|
||||
|
||||
- 不一次性迁移合同、采购、付款、应付账款、票据和项目。
|
||||
- 不重做业务状态机、审批规则或后端接口。
|
||||
- 不把所有业务字段改成配置生成。
|
||||
- 不做公网部署,除非用户后续明确批准。
|
||||
|
||||
## Test Plan
|
||||
|
||||
- `cd client && npm run typecheck`
|
||||
- `cd client && npm run build`
|
||||
- `rg -n "台账工具" client/src/pages/ReimbursementsPage.tsx client/src/pages/AssetRegistryPage.tsx`
|
||||
- 运行受影响页面相关的聚焦 Playwright 测试,或新增静态/组件断言覆盖入口文案。
|
||||
|
||||
## Technical Notes
|
||||
|
||||
- 设计裁决:`frontend-design` skill。
|
||||
- 现有列表模板:`client/src/components/patterns/BusinessListTemplate.tsx`
|
||||
- 现有详情模板:`client/src/components/patterns/BusinessDetailTemplate.tsx`
|
||||
- 首批页面:`client/src/pages/ReimbursementsPage.tsx`、`client/src/pages/AssetRegistryPage.tsx`
|
||||
|
||||
## Validation Result
|
||||
|
||||
- `cd client && npm run typecheck`:通过。
|
||||
- `cd client && npm run build`:通过。
|
||||
- `rg -n "台账工具|showLedgerTools|asset-ledger-tools|更多创建方式" client/src/pages/ReimbursementsPage.tsx client/src/pages/AssetRegistryPage.tsx`:无结果。
|
||||
- `git diff --check`:通过。
|
||||
- `python3 ./.trellis/scripts/task.py validate 05-31-business-edit-template-remove-ledger-tools-wave1`:通过。
|
||||
- `node scripts/qa/check-trellis-governance.mjs`:通过。
|
||||
- `cd client && npx playwright test tests/e2e/system-action-confirmation.spec.ts --workers=1 --reporter=list`:2 passed。
|
||||
- `cd client && npx playwright test tests/e2e/visual-shell.spec.ts --workers=1 --reporter=list`:5 passed。
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"id": "business-edit-template-remove-ledger-tools-wave1",
|
||||
"name": "business-edit-template-remove-ledger-tools-wave1",
|
||||
"title": "建立业务编辑模板并移除台账工具首批",
|
||||
"description": "Build the first business edit template and remove generic ledger-tools entry points from reimbursement and asset list pages.",
|
||||
"status": "in_progress",
|
||||
"dev_type": null,
|
||||
"scope": "frontend",
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "shunli",
|
||||
"assignee": "shunli",
|
||||
"createdAt": "2026-05-31",
|
||||
"completedAt": null,
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [
|
||||
"client/src/components/patterns/BusinessEditTemplate.tsx",
|
||||
"client/src/pages/ReimbursementsPage.tsx",
|
||||
"client/src/pages/AssetRegistryPage.tsx",
|
||||
".trellis/spec/hyb-oa/frontend-react-vite-design.md"
|
||||
],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { Button } from "../ui/button";
|
||||
import { WorkspaceHeader } from "./ListWorkspace";
|
||||
import { SectionPanel } from "./SectionPanel";
|
||||
|
||||
export function BusinessEditTemplate({
|
||||
eyebrow,
|
||||
title,
|
||||
backTo,
|
||||
backLabel = "返回",
|
||||
status,
|
||||
actions,
|
||||
meta,
|
||||
validation,
|
||||
bottomActions,
|
||||
children,
|
||||
className,
|
||||
testId,
|
||||
}: {
|
||||
eyebrow?: string;
|
||||
title: string;
|
||||
backTo?: string;
|
||||
backLabel?: string;
|
||||
status?: ReactNode;
|
||||
actions?: ReactNode;
|
||||
meta?: ReactNode;
|
||||
validation?: ReactNode;
|
||||
bottomActions?: ReactNode;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
testId?: string;
|
||||
}) {
|
||||
const headerActions = actions || status || backTo ? (
|
||||
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||
{status ? <div className="shrink-0">{status}</div> : null}
|
||||
{actions}
|
||||
{backTo ? (
|
||||
<Button asChild type="button" variant="secondary">
|
||||
<Link to={backTo}>{backLabel}</Link>
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className={cn("page-scaffold", bottomActions && "pb-24 md:pb-0", className)} data-testid={testId}>
|
||||
<WorkspaceHeader eyebrow={eyebrow} title={title} meta={meta} actions={headerActions} />
|
||||
{validation ? <BusinessEditValidationSummary>{validation}</BusinessEditValidationSummary> : null}
|
||||
{children}
|
||||
{bottomActions ? <BusinessEditActionBar>{bottomActions}</BusinessEditActionBar> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function BusinessEditWorkspace({
|
||||
children,
|
||||
className,
|
||||
testId,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
testId?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("detail-workspace-grid", className)} data-testid={testId}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function BusinessEditMain({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return <div className={cn("space-y-5", className)}>{children}</div>;
|
||||
}
|
||||
|
||||
export function BusinessEditAside({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return <div className={cn("space-y-5", className)}>{children}</div>;
|
||||
}
|
||||
|
||||
export function BusinessEditSection({
|
||||
title,
|
||||
description,
|
||||
actions,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
title: string;
|
||||
description?: string;
|
||||
actions?: ReactNode;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<SectionPanel title={title} description={description} actions={actions} className={className}>
|
||||
{children}
|
||||
</SectionPanel>
|
||||
);
|
||||
}
|
||||
|
||||
export function BusinessEditFormGrid({
|
||||
children,
|
||||
className,
|
||||
columns = 2,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
columns?: 1 | 2 | 3;
|
||||
}) {
|
||||
const columnClass = {
|
||||
1: "grid-cols-1",
|
||||
2: "md:grid-cols-2",
|
||||
3: "md:grid-cols-2 xl:grid-cols-3",
|
||||
}[columns];
|
||||
|
||||
return <div className={cn("grid gap-4", columnClass, className)}>{children}</div>;
|
||||
}
|
||||
|
||||
export function BusinessEditValidationSummary({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700", className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function BusinessEditActionBar({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"mobile-sticky-action-surface fixed inset-x-0 bottom-0 z-50 border-t border-zinc-200 bg-white/95 shadow-[0_-10px_24px_rgba(15,23,42,0.10)] backdrop-blur md:static md:inset-auto md:mt-5 md:rounded-lg md:border md:bg-white md:p-3 md:shadow-none",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="mobile-sticky-action-grid md:flex md:justify-end md:gap-2">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -226,7 +226,8 @@ export function AssetRegistryPage() {
|
||||
const [assigneeFilter, setAssigneeFilter] = useState("");
|
||||
const [assetSearch, setAssetSearch] = useState("");
|
||||
const [mine, setMine] = useState(!canViewAll);
|
||||
const [showLedgerTools, setShowLedgerTools] = useState(false);
|
||||
const [showCategoryManager, setShowCategoryManager] = useState(false);
|
||||
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false);
|
||||
|
||||
const [selectedAssetId, setSelectedAssetId] = useState(searchParams.get("id") ?? "");
|
||||
const [isCreatingAsset, setIsCreatingAsset] = useState(false);
|
||||
@@ -377,6 +378,14 @@ export function AssetRegistryPage() {
|
||||
|
||||
const currentAsset = assetDetailQuery.data?.asset ?? null;
|
||||
const canEditCurrent = canManage && (isCreatingAsset || currentAsset?.permissions.canEdit);
|
||||
const hasAdvancedAssetFilters = Boolean(
|
||||
sensitivityFilter ||
|
||||
categoryFilter ||
|
||||
legalEntityFilter ||
|
||||
costCenterFilter ||
|
||||
assigneeFilter,
|
||||
);
|
||||
const showAssetAdvancedFilters = showAdvancedFilters || hasAdvancedAssetFilters;
|
||||
|
||||
function resetCategoryEditor(category?: AssetCategoryItem) {
|
||||
if (!category) {
|
||||
@@ -733,9 +742,15 @@ export function AssetRegistryPage() {
|
||||
<Button type="button" variant="secondary" onClick={() => navigate("/mobile/assets/scan") }>
|
||||
移动扫码查找
|
||||
</Button>
|
||||
<Button type="button" variant={showLedgerTools ? "default" : "secondary"} onClick={() => setShowLedgerTools((value) => !value)}>
|
||||
台账工具
|
||||
</Button>
|
||||
{canManage ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant={showCategoryManager ? "default" : "secondary"}
|
||||
onClick={() => setShowCategoryManager((value) => !value)}
|
||||
>
|
||||
资产分类
|
||||
</Button>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -744,8 +759,8 @@ export function AssetRegistryPage() {
|
||||
|
||||
{!isAssetDetailMode ? (
|
||||
<div className="grid gap-6">
|
||||
{showLedgerTools ? (
|
||||
<div className="card-std space-y-4 p-4 md:p-5" data-testid="asset-ledger-tools">
|
||||
{showCategoryManager && canManage ? (
|
||||
<div className="card-std space-y-4 p-4 md:p-5" data-testid="asset-category-manager">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-base font-semibold text-zinc-900">资产分类</h3>
|
||||
{canManage ? (
|
||||
@@ -786,56 +801,6 @@ export function AssetRegistryPage() {
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 rounded-lg border border-zinc-200 bg-white p-3">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-zinc-900">高级筛选</h4>
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
<select className="select-std" value={sensitivityFilter} onChange={(event) => setSensitivityFilter(event.target.value as AssetSensitivityLevel | "") }>
|
||||
{sensitivityOptions().map((item) => (
|
||||
<option key={item.value || "all"} value={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select className="select-std" value={categoryFilter} onChange={(event) => setCategoryFilter(event.target.value)}>
|
||||
<option value="">全部分类</option>
|
||||
{(categoriesQuery.data?.items ?? []).map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{formatTextWithCompactLongNumbers(item.name)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select className="select-std" value={legalEntityFilter} onChange={(event) => {
|
||||
setLegalEntityFilter(event.target.value);
|
||||
setCostCenterFilter("");
|
||||
}}>
|
||||
<option value="">全部法体</option>
|
||||
{(legalEntitiesQuery.data?.items ?? []).map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{formatTextWithCompactLongNumbers(item.name)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select className="select-std" value={costCenterFilter} onChange={(event) => setCostCenterFilter(event.target.value)}>
|
||||
<option value="">全部成本中心</option>
|
||||
{filteredFilterCostCenters.map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select className="select-std" value={assigneeFilter} onChange={(event) => setAssigneeFilter(event.target.value)} disabled={!canViewAll}>
|
||||
<option value="">全部归属人</option>
|
||||
{(assigneesQuery.data?.items ?? []).map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{categoriesQuery.isLoading ? <TableSkeleton columns={3} rows={5} /> : null}
|
||||
{categoriesQuery.isError ? (
|
||||
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">{resolveError(categoriesQuery.error)}</p>
|
||||
@@ -903,15 +868,91 @@ export function AssetRegistryPage() {
|
||||
|
||||
<div className="grid gap-4">
|
||||
<BusinessListToolbar
|
||||
actions={canViewAll ? (
|
||||
actions={(
|
||||
<>
|
||||
<Button type="button" size="sm" variant={mine ? "default" : "secondary"} onClick={() => setMine(true)}>
|
||||
我相关
|
||||
</Button>
|
||||
<Button type="button" size="sm" variant={!mine ? "default" : "secondary"} onClick={() => setMine(false)}>
|
||||
全范围
|
||||
{canViewAll ? (
|
||||
<>
|
||||
<Button type="button" size="sm" variant={mine ? "default" : "secondary"} onClick={() => setMine(true)}>
|
||||
我相关
|
||||
</Button>
|
||||
<Button type="button" size="sm" variant={!mine ? "default" : "secondary"} onClick={() => setMine(false)}>
|
||||
全范围
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant={showAssetAdvancedFilters ? "default" : "secondary"}
|
||||
data-testid="asset-filter-toggle"
|
||||
onClick={() => setShowAdvancedFilters((value) => !value)}
|
||||
>
|
||||
筛选
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
advanced={showAssetAdvancedFilters ? (
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-5">
|
||||
<select className="select-std" value={sensitivityFilter} onChange={(event) => setSensitivityFilter(event.target.value as AssetSensitivityLevel | "") }>
|
||||
{sensitivityOptions().map((item) => (
|
||||
<option key={item.value || "all"} value={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select className="select-std" value={categoryFilter} onChange={(event) => setCategoryFilter(event.target.value)}>
|
||||
<option value="">全部分类</option>
|
||||
{(categoriesQuery.data?.items ?? []).map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{formatTextWithCompactLongNumbers(item.name)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select className="select-std" value={legalEntityFilter} onChange={(event) => {
|
||||
setLegalEntityFilter(event.target.value);
|
||||
setCostCenterFilter("");
|
||||
}}>
|
||||
<option value="">全部法体</option>
|
||||
{(legalEntitiesQuery.data?.items ?? []).map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{formatTextWithCompactLongNumbers(item.name)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select className="select-std" value={costCenterFilter} onChange={(event) => setCostCenterFilter(event.target.value)}>
|
||||
<option value="">全部成本中心</option>
|
||||
{filteredFilterCostCenters.map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select className="select-std" value={assigneeFilter} onChange={(event) => setAssigneeFilter(event.target.value)} disabled={!canViewAll}>
|
||||
<option value="">全部归属人</option>
|
||||
{(assigneesQuery.data?.items ?? []).map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="flex items-center md:col-span-2 xl:col-span-5">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setSensitivityFilter("");
|
||||
setCategoryFilter("");
|
||||
setLegalEntityFilter("");
|
||||
setCostCenterFilter("");
|
||||
setAssigneeFilter("");
|
||||
setShowAdvancedFilters(false);
|
||||
}}
|
||||
>
|
||||
重置筛选
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
>
|
||||
<div className="grid gap-3 md:grid-cols-[minmax(0,1fr)_180px_190px]">
|
||||
|
||||
+113
-116
@@ -1,7 +1,6 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { Link, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Wrench } from "lucide-react";
|
||||
import { isApiError } from "../api/types";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { fetchMyDelegations } from "../api/delegations";
|
||||
@@ -154,7 +153,6 @@ export function ReimbursementsPage() {
|
||||
const [proxyOwnerEmployeeId, setProxyOwnerEmployeeId] = useState("");
|
||||
const [deleteTarget, setDeleteTarget] = useState<ReimbursementListItem | null>(null);
|
||||
const [exportConfirmFormat, setExportConfirmFormat] = useState<ExportFormat | null>(null);
|
||||
const [showLedgerTools, setShowLedgerTools] = useState(false);
|
||||
|
||||
const status = (searchParams.get("status") as ReimbursementStatus | null) ?? "draft";
|
||||
const page = Number(searchParams.get("page") ?? "1") || 1;
|
||||
@@ -170,9 +168,9 @@ export function ReimbursementsPage() {
|
||||
hasPermission(permissions, "invoice.manage");
|
||||
const isAdministration = (me?.roleCodes ?? []).includes("administration");
|
||||
const showProxyOwnerSelector = canProxySubmit && isAdministration;
|
||||
const canShowLedgerEntry = canViewAll || showProxyOwnerSelector || canUseFinanceTools;
|
||||
const showAdvancedTools = canShowLedgerEntry && (showLedgerTools || scope === "all" || showProxyOwnerSelector);
|
||||
const shouldLoadCreateOptions = showAdvancedTools && canCreateExpense;
|
||||
const canShowListActions = canViewAll || showProxyOwnerSelector || canUseFinanceTools;
|
||||
const showAdvancedTools = scope === "all" || showProxyOwnerSelector;
|
||||
const shouldLoadCreateOptions = canCreateExpense;
|
||||
|
||||
const listQuery = useQuery({
|
||||
queryKey: ["reimbursements", "list", status, page, scope],
|
||||
@@ -284,100 +282,27 @@ export function ReimbursementsPage() {
|
||||
title={scope === "all" ? "报销台账" : "我的报销"}
|
||||
actions={
|
||||
canCreateExpense ? (
|
||||
<Button
|
||||
data-testid="reimbursements-create"
|
||||
disabled={createMutation.isPending}
|
||||
onClick={() => startCreateReimbursement()}
|
||||
type="button"
|
||||
>
|
||||
{createMutation.isPending ? "创建中..." : "新建报销"}
|
||||
</Button>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
|
||||
{createMutation.error && isApiError(createMutation.error) ? (
|
||||
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{createMutation.error.code}:
|
||||
{mapApiErrorMessage(
|
||||
createMutation.error.code,
|
||||
createMutation.error.details,
|
||||
createMutation.error.message,
|
||||
)}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{exportError ? (
|
||||
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{exportError}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<BusinessListToolbar
|
||||
actions={
|
||||
canShowLedgerEntry ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant={showAdvancedTools ? "default" : "secondary"}
|
||||
onClick={() => setShowLedgerTools((current) => !current)}
|
||||
type="button"
|
||||
>
|
||||
<Wrench aria-hidden className="mr-1 size-4" />
|
||||
台账工具
|
||||
</Button>
|
||||
) : null
|
||||
}
|
||||
advanced={showAdvancedTools ? (
|
||||
<>
|
||||
<div className="grid gap-3 text-sm md:grid-cols-4">
|
||||
<div>
|
||||
<div className="text-xs text-zinc-500">当前列表</div>
|
||||
<div className="mt-1 font-semibold text-zinc-900">{stats.currentPageCount} 条 / 共 {listQuery.data?.total ?? 0} 条</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-zinc-500">当前金额</div>
|
||||
<div className="mt-1 font-semibold text-zinc-900">{formatCurrency(stats.currentPageAmount)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-zinc-500">审批中</div>
|
||||
<div className={stats.reviewing > 0 ? "mt-1 font-semibold text-amber-700" : "mt-1 font-semibold text-zinc-900"}>{stats.reviewing} 条</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-zinc-500">已支付</div>
|
||||
<div className="mt-1 font-semibold text-zinc-900">{stats.paid} 条</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex flex-wrap items-center gap-2">
|
||||
{showProxyOwnerSelector ? (
|
||||
<select
|
||||
className="select-sm"
|
||||
data-testid="reimbursements-owner-select"
|
||||
onChange={(event) => setProxyOwnerEmployeeId(event.target.value)}
|
||||
value={proxyOwnerEmployeeId}
|
||||
>
|
||||
<option value="">自己</option>
|
||||
{(proxyDelegationsQuery.data?.received ?? [])
|
||||
.filter((item) => item.status === "active")
|
||||
.map((item) => (
|
||||
<option key={item.id} value={item.principalEmployeeId}>
|
||||
{item.principal?.name ?? item.principalEmployeeId}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : null}
|
||||
|
||||
{canShowCreateOptionsMenu ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="sm" variant="secondary" type="button">
|
||||
更多创建方式
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-80">
|
||||
<div className="px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-zinc-400">
|
||||
从模板创建
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
data-testid="reimbursements-create"
|
||||
disabled={createMutation.isPending}
|
||||
type="button"
|
||||
>
|
||||
{createMutation.isPending ? "创建中..." : "新建报销"}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-80">
|
||||
<DropdownMenuItem onSelect={() => startCreateReimbursement()}>
|
||||
空白报销
|
||||
</DropdownMenuItem>
|
||||
{canShowCreateOptionsMenu ? (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<div className="px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-zinc-400">
|
||||
从模板创建
|
||||
</div>
|
||||
{templateOptions.length ? (
|
||||
templateOptions.map((template) => (
|
||||
<DropdownMenuItem
|
||||
@@ -404,13 +329,39 @@ export function ReimbursementsPage() {
|
||||
{travelRequest.requestNo ?? travelRequest.id} · {travelRequest.purpose}
|
||||
</DropdownMenuItem>
|
||||
))
|
||||
) : (
|
||||
<DropdownMenuItem disabled>当前无可关联申请</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : null}
|
||||
) : (
|
||||
<DropdownMenuItem disabled>当前无可关联申请</DropdownMenuItem>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
|
||||
{createMutation.error && isApiError(createMutation.error) ? (
|
||||
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{createMutation.error.code}:
|
||||
{mapApiErrorMessage(
|
||||
createMutation.error.code,
|
||||
createMutation.error.details,
|
||||
createMutation.error.message,
|
||||
)}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{exportError ? (
|
||||
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{exportError}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<BusinessListToolbar
|
||||
actions={
|
||||
canShowListActions ? (
|
||||
<>
|
||||
{canViewAll ? (
|
||||
<>
|
||||
<Button
|
||||
@@ -420,7 +371,7 @@ export function ReimbursementsPage() {
|
||||
onClick={() => setSearchParams({ status: "reviewing", page: "1", scope: "all" })}
|
||||
type="button"
|
||||
>
|
||||
审批中台账
|
||||
审批中
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="reimbursements-scope-own"
|
||||
@@ -441,22 +392,22 @@ export function ReimbursementsPage() {
|
||||
全范围
|
||||
</Button>
|
||||
<Button asChild size="sm" variant="secondary" type="button">
|
||||
<Link to="/reimbursements/dashboard">统计分析</Link>
|
||||
<Link to="/reimbursements/dashboard">统计</Link>
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
data-testid="reimbursements-export"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
disabled={exportMutation.isPending}
|
||||
>
|
||||
{exportMutation.isPending ? "导出中..." : "导出"}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="reimbursements-export"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
disabled={exportMutation.isPending}
|
||||
>
|
||||
{exportMutation.isPending ? "导出中..." : "导出"}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
@@ -473,7 +424,53 @@ export function ReimbursementsPage() {
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
advanced={showAdvancedTools ? (
|
||||
<>
|
||||
{scope === "all" ? (
|
||||
<div className="grid gap-3 text-sm md:grid-cols-4">
|
||||
<div>
|
||||
<div className="text-xs text-zinc-500">当前列表</div>
|
||||
<div className="mt-1 font-semibold text-zinc-900">{stats.currentPageCount} 条 / 共 {listQuery.data?.total ?? 0} 条</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-zinc-500">当前金额</div>
|
||||
<div className="mt-1 font-semibold text-zinc-900">{formatCurrency(stats.currentPageAmount)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-zinc-500">审批中</div>
|
||||
<div className={stats.reviewing > 0 ? "mt-1 font-semibold text-amber-700" : "mt-1 font-semibold text-zinc-900"}>{stats.reviewing} 条</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-zinc-500">已支付</div>
|
||||
<div className="mt-1 font-semibold text-zinc-900">{stats.paid} 条</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showProxyOwnerSelector ? (
|
||||
<div className={scope === "all" ? "mt-3 flex flex-wrap items-center gap-2" : "flex flex-wrap items-center gap-2"}>
|
||||
{showProxyOwnerSelector ? (
|
||||
<select
|
||||
className="select-sm"
|
||||
data-testid="reimbursements-owner-select"
|
||||
onChange={(event) => setProxyOwnerEmployeeId(event.target.value)}
|
||||
value={proxyOwnerEmployeeId}
|
||||
>
|
||||
<option value="">自己</option>
|
||||
{(proxyDelegationsQuery.data?.received ?? [])
|
||||
.filter((item) => item.status === "active")
|
||||
.map((item) => (
|
||||
<option key={item.id} value={item.principalEmployeeId}>
|
||||
{item.principal?.name ?? item.principalEmployeeId}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
>
|
||||
|
||||
@@ -85,8 +85,8 @@ test("E2E 资产流转主链路:建卡→领用归还→调拨→盘点", asyn
|
||||
await (await openNavLink(page, "资产")).click();
|
||||
await expect(page.getByRole("heading", { name: "资产台账" })).toBeVisible({ timeout: 30_000 });
|
||||
await expect(page.getByTestId("asset-category-code")).toHaveCount(0);
|
||||
await page.getByRole("button", { name: "台账工具" }).click();
|
||||
await expect(page.getByTestId("asset-ledger-tools")).toBeVisible();
|
||||
await page.getByRole("button", { name: "资产分类" }).click();
|
||||
await expect(page.getByTestId("asset-category-manager")).toBeVisible();
|
||||
await expect(page.getByTestId("asset-category-code")).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
await page.getByTestId("asset-category-reset").click();
|
||||
|
||||
@@ -162,6 +162,7 @@ test("ASSET1D 公网资产台账试录:FA/EA-SYS/EA-REPO/EA-BKP", async ({ pag
|
||||
|
||||
await login(page, admin);
|
||||
await page.goto("/assets");
|
||||
await page.getByRole("button", { name: "资产分类" }).click();
|
||||
await expect(page.getByTestId("asset-category-code")).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
const category = await createTrialCategory(page, suffix);
|
||||
|
||||
@@ -290,7 +290,6 @@ test.describe("R7E 全站高风险操作二次确认", () => {
|
||||
await page.getByTestId("action-confirm-submit").click();
|
||||
await expect.poll(() => calls.deleteReimbursement).toBe(1);
|
||||
|
||||
await page.getByRole("button", { name: "台账工具" }).click();
|
||||
await page.getByTestId("reimbursements-export").click();
|
||||
await page.getByTestId("reimbursements-export-csv").click();
|
||||
await expect(page.getByTestId("action-confirm-dialog")).toContainText("导出文件可能包含业务或个人信息");
|
||||
|
||||
@@ -390,8 +390,8 @@ test.describe("visual shell", () => {
|
||||
await page.goto("/reimbursements");
|
||||
await expect(page.getByRole("heading", { name: "我的报销" })).toBeVisible();
|
||||
await expect(page.getByTestId("reimbursements-create")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "台账工具" })).toBeVisible();
|
||||
await expect(page.getByTestId("reimbursements-export")).not.toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "台账工具" })).toHaveCount(0);
|
||||
await expect(page.getByTestId("reimbursements-export")).toBeVisible();
|
||||
await expect(page.getByTestId("reimbursements-status-tabs")).toBeVisible();
|
||||
await expect(page.getByTestId("reimbursements-table")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "批量操作" })).toHaveCount(0);
|
||||
|
||||
在新工单中引用
屏蔽一个用户