From b4ce6e45dcd49e391a8ae497a519732d443644d2 Mon Sep 17 00:00:00 2001 From: shenyifei Date: Thu, 8 Jan 2026 16:15:03 +0800 Subject: [PATCH] =?UTF-8?q?refactor(order):=20=E9=87=8D=E6=9E=84=E5=8F=91?= =?UTF-8?q?=E8=B4=A7=E5=8D=95=E5=8A=9F=E8=83=BD=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 ShipOrderList 组件从 Delivery 模块迁移至 Order 模块 - 移除 Delivery 模块的导出并在 Order 模块中新增 OrderShipList 和相关组件 - 更新发货单列表的列配置,添加订单、经销商、公司等关联字段 - 修改发货单列表的字段映射,将经销商名称和车次号格式从短横线改为竖线分隔 - 在 BizPage 组件中添加 convertValue 属性支持数据转换 - 更新左侧菜单组件的依赖数组以包含 menuData - 调整发货单列表的国际化配置,更新字段标题和状态枚举值 - 新增 OrderShip 页面路由文件 - 移除项目根目录的 AGENTS.md 和 project.md 文件 - 从组件索引中移除 Delivery 模块并添加 SearchMenu 组件 --- AGENTS.md | 18 - CLAUDE.md | 332 ++++++++++++- openspec/AGENTS.md | 456 ------------------ openspec/project.md | 175 ------- packages/app-operation/src/app.tsx | 37 +- .../src/components/Biz/BizContainer.tsx | 4 +- .../src/components/Biz/BizPage.tsx | 10 +- .../src/components/Biz/typing.ts | 1 + .../src/components/Delivery/index.ts | 1 - .../src/components/LeftMenu/index.tsx | 2 +- .../src/components/Order/OrderCostPay.tsx | 2 +- .../src/components/Order/OrderList.tsx | 2 +- .../src/components/Order/OrderModal.tsx | 4 +- .../OrderShipList.tsx} | 151 +++--- .../src/components/Order/OrderShipModal.tsx | 377 +++++++++++++++ .../components/Order/OrderSupplierModal.tsx | 6 +- .../src/components/Order/index.ts | 4 + .../PaymentTask/OrderSupplierInvoiceList.tsx | 2 +- .../src/components/SearchMenu/index.tsx | 267 ++++++++++ .../src/components/SearchMenu/style.style.ts | 100 ++++ .../Supplier/SupplierInvoiceList.tsx | 2 +- .../app-operation/src/components/index.ts | 2 +- packages/app-operation/src/locales/zh-CN.ts | 28 +- .../app-operation/src/pages/OrderShip.tsx | 5 + .../src/pages/ReconciliationCreate.tsx | 437 ++++++++++++++--- .../app-operation/src/pages/ShipOrder.tsx | 5 - .../src/services/auth/typings.d.ts | 6 +- .../src/services/business/typings.d.ts | 56 +-- pnpm-workspace.yaml | 1 - 29 files changed, 1630 insertions(+), 863 deletions(-) delete mode 100644 AGENTS.md delete mode 100644 openspec/AGENTS.md delete mode 100644 openspec/project.md delete mode 100644 packages/app-operation/src/components/Delivery/index.ts rename packages/app-operation/src/components/{Delivery/ShipOrderList.tsx => Order/OrderShipList.tsx} (58%) create mode 100644 packages/app-operation/src/components/Order/OrderShipModal.tsx create mode 100644 packages/app-operation/src/components/SearchMenu/index.tsx create mode 100644 packages/app-operation/src/components/SearchMenu/style.style.ts create mode 100644 packages/app-operation/src/pages/OrderShip.tsx delete mode 100644 packages/app-operation/src/pages/ShipOrder.tsx diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 0669699..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,18 +0,0 @@ - -# OpenSpec Instructions - -These instructions are for AI assistants working in this project. - -Always open `@/openspec/AGENTS.md` when the request: -- Mentions planning or proposals (words like proposal, spec, change, plan) -- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work -- Sounds ambiguous and you need the authoritative spec before coding - -Use `@/openspec/AGENTS.md` to learn: -- How to create and apply change proposals -- Spec format and conventions -- Project structure and guidelines - -Keep this managed block so 'openspec update' can refresh the instructions. - - \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 0669699..4bfa265 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,18 +1,324 @@ - -# OpenSpec Instructions +# CLAUDE.md -These instructions are for AI assistants working in this project. +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. -Always open `@/openspec/AGENTS.md` when the request: -- Mentions planning or proposals (words like proposal, spec, change, plan) -- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work -- Sounds ambiguous and you need the authoritative spec before coding +## 项目概述 -Use `@/openspec/AGENTS.md` to learn: -- How to create and apply change proposals -- Spec format and conventions -- Project structure and guidelines +ERPTurbo_Admin 是一个基于 React 生态系统的企业级管理后台系统,采用 Monorepo 架构。使用 UmiJS Max 作为应用框架,Ant Design 作为 UI 组件库。 -Keep this managed block so 'openspec update' can refresh the instructions. +## 项目结构 - \ No newline at end of file +``` +ERPTurbo_Admin/ +├── packages/ +│ ├── app-operation/ # 运营管理系统主应用 +│ │ ├── src/ +│ │ │ ├── pages/ # 页面组件(约定式路由) +│ │ │ ├── components/ # 业务组件 +│ │ │ │ ├── Biz/ # Biz 核心组件库 +│ │ │ │ ├── BasicData/ # 基础数据组件 +│ │ │ │ ├── Company/ # 公司组件 +│ │ │ │ ├── Order/ # 订单组件 +│ │ │ │ └── ... # 其他业务组件 +│ │ │ ├── services/ # API 服务层(自动生成) +│ │ │ ├── models/ # 状态管理 +│ │ │ ├── locales/ # 国际化文件 +│ │ │ └── app.tsx # 应用入口 +│ │ └── dist/ # 构建输出 +│ └── app-sso-server/ # SSO 单点登录系统 +├── shared/ # 共享资源 +├── swagger/ # OpenAPI 文档 +├── .husky/ # Git Hooks 配置 +├── .lingma/ # 开发规则文档 +│ └── rules/ +│ ├── design.md # 架构设计规则 +│ ├── biz.md # Biz 组件设计规则 +│ └── add-new-page.md # 添加页面指南 +└── pnpm-workspace.yaml # PNPM 工作区配置 +``` + +## 核心命令 + +### 包管理器 +- **PNPM** - 项目使用 PNPM 作为包管理器 +- 工作区配置在 `pnpm-workspace.yaml` + +### 根级别命令(在项目根目录执行) + +```bash +# 安装依赖 +pnpm install + +# 启动所有应用的开发服务器 +pnpm dev + +# 构建所有应用 +pnpm build + +# 格式化所有应用的代码 +pnpm format +``` + +### 应用级别命令(在具体应用目录下执行) + +```bash +# 进入应用目录 +cd packages/app-operation + +# 启动开发服务器 +pnpm dev + +# 构建生产版本 +pnpm build + +# 代码格式化 +pnpm format + +# 生成 OpenAPI 客户端代码 +pnpm openapi + +# 初始化/设置 +pnpm setup +``` + +### 单一组件/页面开发 + +```bash +# 在 app-operation 中开发特定功能 +cd packages/app-operation + +# 启动单个应用 +pnpm dev + +# 运行测试(如果需要) +pnpm test +``` + +## 架构设计 + +### 技术栈 + +| 技术 | 版本 | 用途 | +|------|------|------| +| React | ^18.0.0 | 前端核心框架 | +| UmiJS Max | ^4.4.9 | 应用框架和构建工具 | +| Ant Design | ^5.25.2 | UI 组件库 | +| Ant Design Pro Components | ^2.8.6 | 高级业务组件 | +| TypeScript | ^5.6.3 | 类型检查 | +| TailwindCSS | ^3.4.17 | CSS 框架 | +| PNPM | ^9.0.0 | 包管理器 | + +### 核心架构模式 + +#### 1. BizContainer 组件模式 +所有业务页面基于 `BizContainer` 组件构建,这是项目的核心业务组件: + +- **位置**: `packages/app-operation/src/components/Biz/BizContainer.tsx` +- **功能**: 统一的增删改查容器组件 +- **特点**: 支持列表、详情、创建、更新、删除、导入导出、树形结构等 + +```tsx + + rowKey={'id'} + permission={'operation-new-component'} + func={business.newComponent} + method={'newComponent'} + methodUpper={'NewComponent'} + intlPrefix={'newComponent'} + // ... 其他配置 +/> +``` + +#### 2. Biz 组件族 +围绕 BizContainer 的配套组件: + +- **BizPage** - 分页表格组件 +- **BizTree** - 树形结构组件 +- **BizDrag** - 拖拽排序组件 +- **BizCreate** - 创建操作组件 +- **BizUpdate** - 更新操作组件 +- **BizDetail** - 详情查看组件 +- **BizDestroy** - 删除操作组件 +- **BizImport** - 批量导入组件 + +#### 3. 权限控制系统 + +**页面级权限**: +```tsx + + {/* 页面内容 */} + +``` + +**按钮级权限**: +```tsx + + + +``` + +#### 4. 动态路由与菜单 + +- 使用约定式路由(pages 目录) +- 路由配置从服务端获取 +- 支持嵌套路由和面包屑导航 + +## 开发指南 + +### 添加新页面 + +详细步骤请参考 `.lingma/rules/add-new-page.md`,基本流程: + +1. **创建页面组件** + ```tsx + // packages/app-operation/src/pages/NewPage.tsx + import { NewComponentList } from '@/components'; + + export default function Page() { + return ; + } + ``` + +2. **创建业务组件**(如果需要) + - 使用 `BizContainer` 构建标准 CRUD 页面 + - 定义 `columns`(表格列)和 `formContext`(表单字段) + - 配置国际化前缀 `intlPrefix` + +3. **配置国际化** + - 在 `packages/app-operation/src/locales/zh-CN.ts` 中添加翻译 + +### API 集成 + +1. **OpenAPI 自动生成** + ```bash + cd packages/app-operation + pnpm openapi + ``` + - 从 `swagger/` 目录读取 OpenAPI 规范 + - 自动生成 `src/services/` 下的 API 客户端代码 + - 生成 TypeScript 类型定义 + +2. **API 调用** + ```tsx + import { business } from '@/services'; + + // 调用 API + const result = await business.newComponent.page({}); + ``` + +### 代码规范 + +- **格式化**: 使用 Prettier,配置在各个包的 package.json 中 +- **代码检查**: Husky + lint-staged,在提交前自动检查 +- **类型检查**: TypeScript 严格模式 +- **国际化**: 所有 UI 文本必须支持多语言 + +## 关键配置文件 + +### package.json (根目录) +- 定义了工作区结构 +- 包含全局脚本:`dev`、`build`、`format` + +### pnpm-workspace.yaml +- 定义 PNPM 工作区配置 +- 包含 packages/* 和 shared/** 目录 + +### .husky/ +- Git Hooks 配置 +- pre-commit: 运行 lint-staged 检查 +- commit-msg: 提交消息规范检查 + +### swagger/ +- OpenAPI 规范文件 +- 用于生成 API 客户端代码 + +## 重要目录说明 + +### packages/app-operation/src/components/ + +#### Biz/ 目录 +核心业务组件库,包含: +- `BizContainer.tsx` - 核心容器组件 +- `BizPage.tsx` - 列表页面组件 +- `BizCreate.tsx` / `BizUpdate.tsx` / `BizDetail.tsx` / `BizDestroy.tsx` - CRUD 操作组件 +- `BizTree.tsx` - 树形组件 +- `BizDrag.tsx` - 拖拽组件 +- `ButtonAccess.tsx` - 权限按钮组件 + +#### 业务组件目录 +- `BasicData/` - 基础数据管理 +- `Company/` - 公司管理 +- `Order/` - 订单管理 +- `Material/` - 素材库 +- `Employee/` - 员工管理 +- `Supplier/` - 供应商管理 + +### packages/app-operation/src/services/ +- API 服务层 +- 自动生成,不应手动编辑 +- 通过 `pnpm openapi` 命令更新 + +### packages/app-operation/src/locales/ +- 国际化文件 +- `zh-CN.ts` - 中文翻译 +- `en-US.ts` - 英文翻译 + +## 开发注意事项 + +1. **不要直接修改 services/ 目录** + - 这些文件由 OpenAPI 自动生成 + - 修改 swagger/ 规范后重新生成 + +2. **遵循组件命名约定** + - 页面组件:文件名 + `Page` 后缀,导出为 `Page` + - 业务组件:语义化命名,如 `NewComponentList` + +3. **国际化是必需的** + - 所有用户可见文本必须使用 `useIntl()` 和 `intl.formatMessage()` + - 定义 `intlPrefix` 前缀 + +4. **权限控制** + - 页面使用 `PageContainer` 包装 + - 操作按钮使用 `ButtonAccess` 包装 + - 权限标识格式:`operation-{resource}-{action}` + +5. **响应式设计** + - 使用 `isMobile` 属性自动适配移动端 + - 表单宽度和布局会自动调整 + +6. **状态管理** + - 使用 UmiJS 内置状态管理(initialState、model) + - 组件间通信使用 `actionRef` + +## 故障排除 + +### 构建问题 +- 确保运行 `pnpm install` 安装依赖 +- 清理构建缓存:`rm -rf packages/app-operation/.umi` + +### API 更新后类型错误 +- 重新生成 API 客户端:`pnpm openapi` +- 检查 swagger/ 目录下的规范文件 + +### 权限问题 +- 检查 `access.ts` 文件中的权限配置 +- 确认路由权限标识与服务端一致 + +### 国际化文本缺失 +- 检查 `locales/zh-CN.ts` 中是否添加对应翻译 +- 确认 `intlPrefix` 配置正确 + +## 相关文档 + +- 项目架构设计:`.lingma/rules/design.md` +- Biz 组件设计规则:`.lingma/rules/biz.md` +- 添加新页面指南:`.lingma/rules/add-new-page.md` +- UmiJS Max 文档:https://umijs.org/docs/max/introduce +- Ant Design 文档:https://ant.design +- PNPM 文档:https://pnpm.io diff --git a/openspec/AGENTS.md b/openspec/AGENTS.md deleted file mode 100644 index 96ab0bb..0000000 --- a/openspec/AGENTS.md +++ /dev/null @@ -1,456 +0,0 @@ -# OpenSpec Instructions - -Instructions for AI coding assistants using OpenSpec for spec-driven development. - -## TL;DR Quick Checklist - -- Search existing work: `openspec spec list --long`, `openspec list` (use `rg` only for full-text search) -- Decide scope: new capability vs modify existing capability -- Pick a unique `change-id`: kebab-case, verb-led (`add-`, `update-`, `remove-`, `refactor-`) -- Scaffold: `proposal.md`, `tasks.md`, `design.md` (only if needed), and delta specs per affected capability -- Write deltas: use `## ADDED|MODIFIED|REMOVED|RENAMED Requirements`; include at least one `#### Scenario:` per requirement -- Validate: `openspec validate [change-id] --strict` and fix issues -- Request approval: Do not start implementation until proposal is approved - -## Three-Stage Workflow - -### Stage 1: Creating Changes -Create proposal when you need to: -- Add features or functionality -- Make breaking changes (API, schema) -- Change architecture or patterns -- Optimize performance (changes behavior) -- Update security patterns - -Triggers (examples): -- "Help me create a change proposal" -- "Help me plan a change" -- "Help me create a proposal" -- "I want to create a spec proposal" -- "I want to create a spec" - -Loose matching guidance: -- Contains one of: `proposal`, `change`, `spec` -- With one of: `create`, `plan`, `make`, `start`, `help` - -Skip proposal for: -- Bug fixes (restore intended behavior) -- Typos, formatting, comments -- Dependency updates (non-breaking) -- Configuration changes -- Tests for existing behavior - -**Workflow** -1. Review `openspec/project.md`, `openspec list`, and `openspec list --specs` to understand current context. -2. Choose a unique verb-led `change-id` and scaffold `proposal.md`, `tasks.md`, optional `design.md`, and spec deltas under `openspec/changes//`. -3. Draft spec deltas using `## ADDED|MODIFIED|REMOVED Requirements` with at least one `#### Scenario:` per requirement. -4. Run `openspec validate --strict` and resolve any issues before sharing the proposal. - -### Stage 2: Implementing Changes -Track these steps as TODOs and complete them one by one. -1. **Read proposal.md** - Understand what's being built -2. **Read design.md** (if exists) - Review technical decisions -3. **Read tasks.md** - Get implementation checklist -4. **Implement tasks sequentially** - Complete in order -5. **Confirm completion** - Ensure every item in `tasks.md` is finished before updating statuses -6. **Update checklist** - After all work is done, set every task to `- [x]` so the list reflects reality -7. **Approval gate** - Do not start implementation until the proposal is reviewed and approved - -### Stage 3: Archiving Changes -After deployment, create separate PR to: -- Move `changes/[name]/` → `changes/archive/YYYY-MM-DD-[name]/` -- Update `specs/` if capabilities changed -- Use `openspec archive --skip-specs --yes` for tooling-only changes (always pass the change ID explicitly) -- Run `openspec validate --strict` to confirm the archived change passes checks - -## Before Any Task - -**Context Checklist:** -- [ ] Read relevant specs in `specs/[capability]/spec.md` -- [ ] Check pending changes in `changes/` for conflicts -- [ ] Read `openspec/project.md` for conventions -- [ ] Run `openspec list` to see active changes -- [ ] Run `openspec list --specs` to see existing capabilities - -**Before Creating Specs:** -- Always check if capability already exists -- Prefer modifying existing specs over creating duplicates -- Use `openspec show [spec]` to review current state -- If request is ambiguous, ask 1–2 clarifying questions before scaffolding - -### Search Guidance -- Enumerate specs: `openspec spec list --long` (or `--json` for scripts) -- Enumerate changes: `openspec list` (or `openspec change list --json` - deprecated but available) -- Show details: - - Spec: `openspec show --type spec` (use `--json` for filters) - - Change: `openspec show --json --deltas-only` -- Full-text search (use ripgrep): `rg -n "Requirement:|Scenario:" openspec/specs` - -## Quick Start - -### CLI Commands - -```bash -# Essential commands -openspec list # List active changes -openspec list --specs # List specifications -openspec show [item] # Display change or spec -openspec validate [item] # Validate changes or specs -openspec archive [--yes|-y] # Archive after deployment (add --yes for non-interactive runs) - -# Project management -openspec init [path] # Initialize OpenSpec -openspec update [path] # Update instruction files - -# Interactive mode -openspec show # Prompts for selection -openspec validate # Bulk validation mode - -# Debugging -openspec show [change] --json --deltas-only -openspec validate [change] --strict -``` - -### Command Flags - -- `--json` - Machine-readable output -- `--type change|spec` - Disambiguate items -- `--strict` - Comprehensive validation -- `--no-interactive` - Disable prompts -- `--skip-specs` - Archive without spec updates -- `--yes`/`-y` - Skip confirmation prompts (non-interactive archive) - -## Directory Structure - -``` -openspec/ -├── project.md # Project conventions -├── specs/ # Current truth - what IS built -│ └── [capability]/ # Single focused capability -│ ├── spec.md # Requirements and scenarios -│ └── design.md # Technical patterns -├── changes/ # Proposals - what SHOULD change -│ ├── [change-name]/ -│ │ ├── proposal.md # Why, what, impact -│ │ ├── tasks.md # Implementation checklist -│ │ ├── design.md # Technical decisions (optional; see criteria) -│ │ └── specs/ # Delta changes -│ │ └── [capability]/ -│ │ └── spec.md # ADDED/MODIFIED/REMOVED -│ └── archive/ # Completed changes -``` - -## Creating Change Proposals - -### Decision Tree - -``` -New request? -├─ Bug fix restoring spec behavior? → Fix directly -├─ Typo/format/comment? → Fix directly -├─ New feature/capability? → Create proposal -├─ Breaking change? → Create proposal -├─ Architecture change? → Create proposal -└─ Unclear? → Create proposal (safer) -``` - -### Proposal Structure - -1. **Create directory:** `changes/[change-id]/` (kebab-case, verb-led, unique) - -2. **Write proposal.md:** -```markdown -# Change: [Brief description of change] - -## Why -[1-2 sentences on problem/opportunity] - -## What Changes -- [Bullet list of changes] -- [Mark breaking changes with **BREAKING**] - -## Impact -- Affected specs: [list capabilities] -- Affected code: [key files/systems] -``` - -3. **Create spec deltas:** `specs/[capability]/spec.md` -```markdown -## ADDED Requirements -### Requirement: New Feature -The system SHALL provide... - -#### Scenario: Success case -- **WHEN** user performs action -- **THEN** expected result - -## MODIFIED Requirements -### Requirement: Existing Feature -[Complete modified requirement] - -## REMOVED Requirements -### Requirement: Old Feature -**Reason**: [Why removing] -**Migration**: [How to handle] -``` -If multiple capabilities are affected, create multiple delta files under `changes/[change-id]/specs//spec.md`—one per capability. - -4. **Create tasks.md:** -```markdown -## 1. Implementation -- [ ] 1.1 Create database schema -- [ ] 1.2 Implement API endpoint -- [ ] 1.3 Add frontend component -- [ ] 1.4 Write tests -``` - -5. **Create design.md when needed:** -Create `design.md` if any of the following apply; otherwise omit it: -- Cross-cutting change (multiple services/modules) or a new architectural pattern -- New external dependency or significant data model changes -- Security, performance, or migration complexity -- Ambiguity that benefits from technical decisions before coding - -Minimal `design.md` skeleton: -```markdown -## Context -[Background, constraints, stakeholders] - -## Goals / Non-Goals -- Goals: [...] -- Non-Goals: [...] - -## Decisions -- Decision: [What and why] -- Alternatives considered: [Options + rationale] - -## Risks / Trade-offs -- [Risk] → Mitigation - -## Migration Plan -[Steps, rollback] - -## Open Questions -- [...] -``` - -## Spec File Format - -### Critical: Scenario Formatting - -**CORRECT** (use #### headers): -```markdown -#### Scenario: User login success -- **WHEN** valid credentials provided -- **THEN** return JWT token -``` - -**WRONG** (don't use bullets or bold): -```markdown -- **Scenario: User login** ❌ -**Scenario**: User login ❌ -### Scenario: User login ❌ -``` - -Every requirement MUST have at least one scenario. - -### Requirement Wording -- Use SHALL/MUST for normative requirements (avoid should/may unless intentionally non-normative) - -### Delta Operations - -- `## ADDED Requirements` - New capabilities -- `## MODIFIED Requirements` - Changed behavior -- `## REMOVED Requirements` - Deprecated features -- `## RENAMED Requirements` - Name changes - -Headers matched with `trim(header)` - whitespace ignored. - -#### When to use ADDED vs MODIFIED -- ADDED: Introduces a new capability or sub-capability that can stand alone as a requirement. Prefer ADDED when the change is orthogonal (e.g., adding "Slash Command Configuration") rather than altering the semantics of an existing requirement. -- MODIFIED: Changes the behavior, scope, or acceptance criteria of an existing requirement. Always paste the full, updated requirement content (header + all scenarios). The archiver will replace the entire requirement with what you provide here; partial deltas will drop previous details. -- RENAMED: Use when only the name changes. If you also change behavior, use RENAMED (name) plus MODIFIED (content) referencing the new name. - -Common pitfall: Using MODIFIED to add a new concern without including the previous text. This causes loss of detail at archive time. If you aren’t explicitly changing the existing requirement, add a new requirement under ADDED instead. - -Authoring a MODIFIED requirement correctly: -1) Locate the existing requirement in `openspec/specs//spec.md`. -2) Copy the entire requirement block (from `### Requirement: ...` through its scenarios). -3) Paste it under `## MODIFIED Requirements` and edit to reflect the new behavior. -4) Ensure the header text matches exactly (whitespace-insensitive) and keep at least one `#### Scenario:`. - -Example for RENAMED: -```markdown -## RENAMED Requirements -- FROM: `### Requirement: Login` -- TO: `### Requirement: User Authentication` -``` - -## Troubleshooting - -### Common Errors - -**"Change must have at least one delta"** -- Check `changes/[name]/specs/` exists with .md files -- Verify files have operation prefixes (## ADDED Requirements) - -**"Requirement must have at least one scenario"** -- Check scenarios use `#### Scenario:` format (4 hashtags) -- Don't use bullet points or bold for scenario headers - -**Silent scenario parsing failures** -- Exact format required: `#### Scenario: Name` -- Debug with: `openspec show [change] --json --deltas-only` - -### Validation Tips - -```bash -# Always use strict mode for comprehensive checks -openspec validate [change] --strict - -# Debug delta parsing -openspec show [change] --json | jq '.deltas' - -# Check specific requirement -openspec show [spec] --json -r 1 -``` - -## Happy Path Script - -```bash -# 1) Explore current state -openspec spec list --long -openspec list -# Optional full-text search: -# rg -n "Requirement:|Scenario:" openspec/specs -# rg -n "^#|Requirement:" openspec/changes - -# 2) Choose change id and scaffold -CHANGE=add-two-factor-auth -mkdir -p openspec/changes/$CHANGE/{specs/auth} -printf "## Why\n...\n\n## What Changes\n- ...\n\n## Impact\n- ...\n" > openspec/changes/$CHANGE/proposal.md -printf "## 1. Implementation\n- [ ] 1.1 ...\n" > openspec/changes/$CHANGE/tasks.md - -# 3) Add deltas (example) -cat > openspec/changes/$CHANGE/specs/auth/spec.md << 'EOF' -## ADDED Requirements -### Requirement: Two-Factor Authentication -Users MUST provide a second factor during login. - -#### Scenario: OTP required -- **WHEN** valid credentials are provided -- **THEN** an OTP challenge is required -EOF - -# 4) Validate -openspec validate $CHANGE --strict -``` - -## Multi-Capability Example - -``` -openspec/changes/add-2fa-notify/ -├── proposal.md -├── tasks.md -└── specs/ - ├── auth/ - │ └── spec.md # ADDED: Two-Factor Authentication - └── notifications/ - └── spec.md # ADDED: OTP email notification -``` - -auth/spec.md -```markdown -## ADDED Requirements -### Requirement: Two-Factor Authentication -... -``` - -notifications/spec.md -```markdown -## ADDED Requirements -### Requirement: OTP Email Notification -... -``` - -## Best Practices - -### Simplicity First -- Default to <100 lines of new code -- Single-file implementations until proven insufficient -- Avoid frameworks without clear justification -- Choose boring, proven patterns - -### Complexity Triggers -Only add complexity with: -- Performance data showing current solution too slow -- Concrete scale requirements (>1000 users, >100MB data) -- Multiple proven use cases requiring abstraction - -### Clear References -- Use `file.ts:42` format for code locations -- Reference specs as `specs/auth/spec.md` -- Link related changes and PRs - -### Capability Naming -- Use verb-noun: `user-auth`, `payment-capture` -- Single purpose per capability -- 10-minute understandability rule -- Split if description needs "AND" - -### Change ID Naming -- Use kebab-case, short and descriptive: `add-two-factor-auth` -- Prefer verb-led prefixes: `add-`, `update-`, `remove-`, `refactor-` -- Ensure uniqueness; if taken, append `-2`, `-3`, etc. - -## Tool Selection Guide - -| Task | Tool | Why | -|------|------|-----| -| Find files by pattern | Glob | Fast pattern matching | -| Search code content | Grep | Optimized regex search | -| Read specific files | Read | Direct file access | -| Explore unknown scope | Task | Multi-step investigation | - -## Error Recovery - -### Change Conflicts -1. Run `openspec list` to see active changes -2. Check for overlapping specs -3. Coordinate with change owners -4. Consider combining proposals - -### Validation Failures -1. Run with `--strict` flag -2. Check JSON output for details -3. Verify spec file format -4. Ensure scenarios properly formatted - -### Missing Context -1. Read project.md first -2. Check related specs -3. Review recent archives -4. Ask for clarification - -## Quick Reference - -### Stage Indicators -- `changes/` - Proposed, not yet built -- `specs/` - Built and deployed -- `archive/` - Completed changes - -### File Purposes -- `proposal.md` - Why and what -- `tasks.md` - Implementation steps -- `design.md` - Technical decisions -- `spec.md` - Requirements and behavior - -### CLI Essentials -```bash -openspec list # What's in progress? -openspec show [item] # View details -openspec validate --strict # Is it correct? -openspec archive [--yes|-y] # Mark complete (add --yes for automation) -``` - -Remember: Specs are truth. Changes are proposals. Keep them in sync. diff --git a/openspec/project.md b/openspec/project.md deleted file mode 100644 index 2777c51..0000000 --- a/openspec/project.md +++ /dev/null @@ -1,175 +0,0 @@ -# Project Context - -## Purpose -ERPTurbo_Admin 是寻鸿科技的企业资源规划(ERP)管理系统,为企业提供全面的生产管理、库存管理、经销商管理、订单处理等核心业务功能。系统旨在提高企业运营效率,实现业务流程数字化和智能化管理。 - -## Tech Stack - -### 前端框架 -- **React 18** - 主要UI框架,使用TypeScript开发 -- **UmiJS Max 4.4.9** - 企业级React应用框架,提供路由、构建、部署等一体化解决方案 -- **Ant Design 5.25.2** - 企业级UI设计语言和组件库 -- **Ant Design Pro Components 2.8.6** - 高级业务组件 -- **NutUI React 3.0.18** - 移动端UI组件库(用于移动端适配) - -### 状态管理 -- **Valtio** - 现代化状态管理库 -- **DVA** - 数据流方案(兼容性考虑) -- **React Query** - 服务端状态管理和数据获取 - -### 表单处理 -- **Formily 2.3.2** - 企业级表单解决方案 -- **@formily/antd-v5** - Formily与Ant Design v5的集成 - -### 样式和主题 -- **TailwindCSS 3.4.17** - 原子化CSS框架 -- **antd-style 3.7.1** - Ant Design样式增强方案 -- **Styled Components 6.1.17** - CSS-in-JS样式库 - -### 工具库 -- **Lodash 4.17.21** - 实用工具函数库 -- **Day.js 1.11.13** - 轻量级日期处理库 -- **Axios 1.7.4** - HTTP客户端 -- **UUID 10.0.0** - 唯一标识符生成 -- **CryptoJS 4.2.0** - 加密解密功能 - -### 编辑器和文档 -- **@wangeditor-next/editor** - 富文本编辑器 -- **Markdown-it 14.1.0** - Markdown解析器 -- **XLSX 0.18.5** - Excel文件处理 - -### 地图服务 -- **腾讯地图API** - 地图功能集成 - -### 开发工具 -- **TypeScript 5.6.3** - 类型安全的JavaScript超集 -- **PNPM** - 包管理器,支持工作空间 -- **Prettier 3.3.3** - 代码格式化 -- **ESLint** - 代码质量检查 -- **Husky 9.1.6** - Git钩子管理 -- **Lint-staged 15.2.10** - 暂存文件检查 - -## Project Conventions - -### 代码风格 -- **缩进**: 使用Tab缩进,大小为4个空格宽度 -- **行长度**: 最大80字符 -- **文件编码**: UTF-8 -- **换行符**: LF (Unix风格) -- **尾随空格**: 删除所有尾随空格 -- **文件结尾**: 所有文件以换行符结尾 - -### 命名约定 -- **组件文件**: PascalCase (例: `UserList.tsx`) -- **函数/变量**: camelCase (例: `getUserList`) -- **常量**: SCREAMING_SNAKE_CASE (例: `API_BASE_URL`) -- **文件/目录**: kebab-case (例: `user-management/`) -- **接口/类型**: PascalCase,以I或T开头可选 (例: `UserVO`, `APIResponse`) - -### 目录结构 -``` -packages/app-operation/src/ -├── components/ # 通用组件 -├── pages/ # 页面组件 -├── services/ # API服务 -├── models/ # 数据模型 -├── utils/ # 工具函数 -├── constants/ # 常量定义 -├── layout/ # 布局组件 -├── locales/ # 国际化文件 -├── assets/ # 静态资源 -└── wrappers/ # 高阶组件包装器 -``` - -### 架构模式 -- **微前端架构**: 使用PNPM工作空间管理多个应用 -- **分层架构**: - - 表现层 (Presentation): React组件和页面 - - 业务层 (Business): 业务逻辑和状态管理 - - 数据层 (Data): API调用和数据持久化 -- **模块化设计**: 按业务领域划分模块 (dealer, product, operation等) -- **组件化开发**: 优先使用可复用组件 - -### 测试策略 -- **当前状态**: 项目暂无专门的测试配置 -- **推荐策略**: - - 单元测试:使用Jest + Testing Library - - 集成测试:使用Cypress或Playwright - - 代码覆盖率:目标 > 80% - - 测试文件命名: `*.test.tsx` 或 `*.spec.tsx` - -### Git工作流 -- **分支策略**: - - `master`: 主分支,生产环境代码 - - `feature/*`: 功能开发分支 - - `hotfix/*`: 紧急修复分支 -- **提交规范**: 使用Conventional Commits - - `feat:` 新功能 - - `fix:` 修复bug - - `refactor:` 重构代码 - - `docs:` 文档更新 - - `style:` 代码格式调整 - - `test:` 测试相关 - - `chore:` 构建过程或辅助工具变动 -- **代码质量**: 使用Husky + Lint-staged进行预提交检查 - -### API设计规范 -- **RESTful API**: 遵循REST设计原则 -- **OpenAPI**: 使用Swagger进行API文档化 -- **数据格式**: 统一使用JSON -- **错误处理**: 标准化错误响应格式 -- **分页**: 统一分页参数和响应格式 - -## Domain Context - -### 业务模块 -- **经销商管理 (Dealer)**: 经销商信息、返点计算、配置管理 -- **产品管理 (Product)**: 产品数据、规格管理 -- **订单管理 (Order)**: 订单处理、发货管理 -- **基础数据 (Basic Data)**: 生产预付、工人预付、固定费用 -- **纸箱管理 (Box)**: 纸箱规格管理 -- **系统设置 (Setting)**: 智能识别配置、系统参数 - -### 核心概念 -- **用户角色**: 普通用户、管理员、经销商 -- **权限管理**: 基于角色的访问控制 (RBAC) -- **多租户**: 支持多平台/渠道隔离 -- **国际化**: 默认中文,支持多语言扩展 - -## Important Constraints - -### 技术约束 -- **浏览器支持**: 现代浏览器 (Chrome 80+, Firefox 75+, Safari 13+) -- **响应式设计**: 支持桌面端和移动端 -- **性能要求**: 页面加载时间 < 3秒 -- **安全要求**: - - XSS防护 - - CSRF防护 - - 敏感数据加密 - - API鉴权 - -### 业务约束 -- **数据一致性**: 关键业务数据必须保持一致性 -- **审计日志**: 重要操作需要记录审计日志 -- **数据备份**: 定期备份关键业务数据 -- **合规要求**: 符合企业数据管理规范 - -## External Dependencies - -### 云服务 -- **阿里云OSS**: 文件存储服务 -- **腾讯地图API**: 地图功能服务 - -### API服务 -- **业务API**: 自建后端API服务 -- **认证服务**: SSO单点登录服务 - -### 第三方集成 -- **支付接口**: 支付宝、微信支付(预留) -- **短信服务**: 短信通知服务(预留) -- **邮件服务**: 邮件通知服务(预留) - -### 开发环境 -- **Node.js**: >= 16.0.0 -- **PNPM**: >= 8.0.0 -- **Git**: >= 2.30.0 diff --git a/packages/app-operation/src/app.tsx b/packages/app-operation/src/app.tsx index 9a2c538..71aca24 100644 --- a/packages/app-operation/src/app.tsx +++ b/packages/app-operation/src/app.tsx @@ -2,13 +2,17 @@ // 全局初始化数据配置,用于 Layout 用户信息和权限初始化 // 更多信息见文档:https://umijs.org/docs/api/runtime-config#getinitialstate -import { VersionChecker, LeftMenu } from '@/components'; +import { LeftMenu, SearchMenu, VersionChecker } from '@/components'; import Avatar from '@/layout/guide/Avatar'; import { auth } from '@/services'; import { Navigate } from '@@/exports'; import { RunTimeLayoutConfig } from '@@/plugin-layout/types'; import { RequestConfig } from '@@/plugin-request/request'; -import { InfoCircleFilled } from '@ant-design/icons'; +import { + InfoCircleFilled, + NotificationFilled, + QuestionCircleFilled, +} from '@ant-design/icons'; import '@nutui/nutui-react/dist/style.scss'; import { history } from '@umijs/max'; import { Alert, Button, message, notification, Spin } from 'antd'; @@ -176,10 +180,7 @@ export const render = async (oldRender: () => void) => { }); window.localStorage.setItem('admin', JSON.stringify(adminVO)); - window.localStorage.setItem( - 'userRoleVOList', - JSON.stringify([]), - ); + window.localStorage.setItem('userRoleVOList', JSON.stringify([])); window.localStorage.setItem('roleSlug', 'operation'); } @@ -298,18 +299,18 @@ export const layout: RunTimeLayoutConfig = ({ initialState }) => { // encodeURIComponent(process.env.UMI_APP_OPERATION_URL || ''); // } }, - // actionsRender: (props) => { - // if (props.isMobile) return []; - // if (typeof window === 'undefined') return []; - // return [ - // props.layout !== 'side' && document.body.clientWidth > 1400 ? ( - // - // ) : undefined, - // , - // , - // , - // ]; - // }, + actionsRender: (props) => { + if (props.isMobile) return []; + if (typeof window === 'undefined') return []; + return [ + props.layout !== 'side' && document.body.clientWidth > 1400 ? ( + + ) : undefined, + , + , + , + ]; + }, headerRender: (props, defaultDom) => { return ( <> diff --git a/packages/app-operation/src/components/Biz/BizContainer.tsx b/packages/app-operation/src/components/Biz/BizContainer.tsx index 5388728..f0ab182 100644 --- a/packages/app-operation/src/components/Biz/BizContainer.tsx +++ b/packages/app-operation/src/components/Biz/BizContainer.tsx @@ -872,7 +872,7 @@ export default function BizContainer< return ( data?.map((item) => { return { - label: item.fullName, + label: `${item.shortName} | ${item.fullName}`, value: item.companyId, }; }) || [] @@ -1020,7 +1020,7 @@ export default function BizContainer< trigger={() => ( - {`${orderVO.orderVehicle?.dealerName} - 第 ${orderVO.orderVehicle?.vehicleNo || '暂无'} 车 - ${orderVO.orderSn || '暂无'}`} + {`${orderVO.orderVehicle?.dealerName} | 第 ${orderVO.orderVehicle?.vehicleNo || '暂无'} 车 | ${orderVO.orderSn || '暂无'}`} )} diff --git a/packages/app-operation/src/components/Biz/BizPage.tsx b/packages/app-operation/src/components/Biz/BizPage.tsx index b0be08f..15915c5 100644 --- a/packages/app-operation/src/components/Biz/BizPage.tsx +++ b/packages/app-operation/src/components/Biz/BizPage.tsx @@ -22,6 +22,7 @@ export default function BizPage< trigger, isMobile, intlPrefix, + convertValue, } = props; const [open, setOpen] = useState(false); const intl = useIntl(); @@ -34,7 +35,7 @@ export default function BizPage< persistenceType: 'sessionStorage', persistenceKey: method + 'ColumnStateKey', defaultValue: { - // option: { show: true, fixed: 'right' }, + orderId: { show: true, fixed: 'left' }, }, }, scroll: { x: 'max-content' }, @@ -78,7 +79,12 @@ export default function BizPage< }); return { - data, + data: data.map((item: BizVO) => { + if (convertValue) { + return convertValue(item); + } + return item; + }), success, total: totalCount, }; diff --git a/packages/app-operation/src/components/Biz/typing.ts b/packages/app-operation/src/components/Biz/typing.ts index 14b24d4..82466bc 100644 --- a/packages/app-operation/src/components/Biz/typing.ts +++ b/packages/app-operation/src/components/Biz/typing.ts @@ -133,6 +133,7 @@ export type BizPageProps< ) => React.ReactNode; fieldProps?: ProTableProps; trigger?: () => React.ReactNode; + convertValue?: (record: BizVO) => BizVO; } & ApiProps; export interface BizTreeProps< diff --git a/packages/app-operation/src/components/Delivery/index.ts b/packages/app-operation/src/components/Delivery/index.ts deleted file mode 100644 index ea3c280..0000000 --- a/packages/app-operation/src/components/Delivery/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as ShipOrderList } from './ShipOrderList'; diff --git a/packages/app-operation/src/components/LeftMenu/index.tsx b/packages/app-operation/src/components/LeftMenu/index.tsx index 24f2191..e603c8e 100644 --- a/packages/app-operation/src/components/LeftMenu/index.tsx +++ b/packages/app-operation/src/components/LeftMenu/index.tsx @@ -87,7 +87,7 @@ const LeftMenu: React.FC = (props) => { ); setActiveMenuKey(activeMenuKey); setOpenKeys([...openKeys, ...activeMenuKey]); - }, [pathname]); + }, [pathname, menuData]); if (!menuData) { return null; diff --git a/packages/app-operation/src/components/Order/OrderCostPay.tsx b/packages/app-operation/src/components/Order/OrderCostPay.tsx index 76a8b52..ded404f 100644 --- a/packages/app-operation/src/components/Order/OrderCostPay.tsx +++ b/packages/app-operation/src/components/Order/OrderCostPay.tsx @@ -120,7 +120,7 @@ export default function OrderCostPay(props: IOrderCostPayProps) {
{orderCostVO.orderVO - ? `${orderCostVO.orderVO.orderVehicle.dealerName} - 第 ${orderCostVO.orderVO.orderVehicle.vehicleNo || '暂无'} 车` + ? `${orderCostVO.orderVO.orderVehicle.dealerName} | 第 ${orderCostVO.orderVO.orderVehicle.vehicleNo || '暂无'} 车` : '-'}
diff --git a/packages/app-operation/src/components/Order/OrderList.tsx b/packages/app-operation/src/components/Order/OrderList.tsx index cc7d678..68d826a 100644 --- a/packages/app-operation/src/components/Order/OrderList.tsx +++ b/packages/app-operation/src/components/Order/OrderList.tsx @@ -56,7 +56,7 @@ export default function OrderList(props: IOrderListProps) { trigger={() => ( - {`${orderVO.orderVehicle?.dealerName} - 第 ${orderVO.orderVehicle?.vehicleNo || '暂无'} 车 - ${orderVO.orderSn || '暂无'}`} + {`${orderVO.orderVehicle?.dealerName} | 第 ${orderVO.orderVehicle?.vehicleNo || '暂无'} 车 | ${orderVO.orderSn || '暂无'}`} )} diff --git a/packages/app-operation/src/components/Order/OrderModal.tsx b/packages/app-operation/src/components/Order/OrderModal.tsx index a15af3a..049149b 100644 --- a/packages/app-operation/src/components/Order/OrderModal.tsx +++ b/packages/app-operation/src/components/Order/OrderModal.tsx @@ -1,3 +1,4 @@ +import { OrderFormItem, OrderList, SelectModal } from '@/components'; import { business } from '@/services'; import { formatParam } from '@/utils/formatParam'; import { pagination } from '@/utils/pagination'; @@ -8,7 +9,6 @@ import { ProColumns, ProFormSelect, } from '@ant-design/pro-components'; -import { OrderFormItem, OrderList, SelectModal } from '@/components'; import { Alert, ModalProps, Row, Space, Tag } from 'antd'; import React, { useEffect, useRef, useState } from 'react'; @@ -74,7 +74,7 @@ export default function OrderModal(props: IOrderModalProps) { trigger={() => ( - {`${orderVO.orderVehicle?.dealerName} - 第 ${orderVO.orderVehicle?.vehicleNo || '暂无'} 车 - ${orderVO.orderSn || '暂无'}`} + {`${orderVO.orderVehicle?.dealerName} | 第 ${orderVO.orderVehicle?.vehicleNo || '暂无'} 车 | ${orderVO.orderSn || '暂无'}`} )} diff --git a/packages/app-operation/src/components/Delivery/ShipOrderList.tsx b/packages/app-operation/src/components/Order/OrderShipList.tsx similarity index 58% rename from packages/app-operation/src/components/Delivery/ShipOrderList.tsx rename to packages/app-operation/src/components/Order/OrderShipList.tsx index 2f41230..340536c 100644 --- a/packages/app-operation/src/components/Delivery/ShipOrderList.tsx +++ b/packages/app-operation/src/components/Order/OrderShipList.tsx @@ -3,7 +3,6 @@ import { business } from '@/services'; import { useIntl } from '@@/exports'; import { ProColumns } from '@ant-design/pro-components'; import { ProDescriptionsItemProps } from '@ant-design/pro-descriptions'; -import { Tag } from 'antd'; import React, { useState } from 'react'; interface IOrderShipListProps { @@ -13,7 +12,7 @@ interface IOrderShipListProps { onValueChange?: () => void; mode?: ModeType; trigger?: () => React.ReactNode; - params: BusinessAPI.OrderShipPageQry; + params?: BusinessAPI.OrderShipPageQry; } export default function OrderShipList(props: IOrderShipListProps) { @@ -32,26 +31,40 @@ export default function OrderShipList(props: IOrderShipListProps) { const [activeKey, setActiveKey] = useState('ALL'); const columns: ProColumns[] = [ + { + title: intl.formatMessage({ id: intlPrefix + '.column.order' }), + dataIndex: 'orderVO', + key: 'orderId', + valueType: 'order', + }, + { + title: intl.formatMessage({ id: intlPrefix + '.column.dealer' }), + dataIndex: 'dealerVO', + key: 'dealerId', + valueType: 'dealer', + }, + { + title: intl.formatMessage({ id: intlPrefix + '.column.company' }), + dataIndex: 'companyVO', + key: 'companyId', + valueType: 'company', + }, { title: intl.formatMessage({ id: intlPrefix + '.column.orderSn' }), dataIndex: 'orderSn', key: 'orderSn', - renderText: (text: string) => {text}, }, { - title: intl.formatMessage({ id: intlPrefix + '.column.dealerName' }), - dataIndex: 'dealerName', - key: 'dealerName', + title: intl.formatMessage({ id: intlPrefix + '.column.shippingAddress' }), + dataIndex: 'shippingAddress', + key: 'shippingAddress', }, { - title: intl.formatMessage({ id: intlPrefix + '.column.vehicleNo' }), - dataIndex: ['orderVehicle', 'vehicleNo'], - key: 'vehicleNo', - render: (_, record) => { - return record.orderVehicle?.vehicleNo - ? '第' + record.orderVehicle?.vehicleNo + '车' - : '-'; - }, + title: intl.formatMessage({ + id: intlPrefix + '.column.receivingAddress', + }), + dataIndex: 'receivingAddress', + key: 'receivingAddress', }, { title: intl.formatMessage({ id: intlPrefix + '.column.shippingDate' }), @@ -59,52 +72,69 @@ export default function OrderShipList(props: IOrderShipListProps) { key: 'shippingDate', valueType: 'date', }, + { + title: intl.formatMessage({ + id: intlPrefix + '.column.estimatedArrivalDate', + }), + dataIndex: 'estimatedArrivalDate', + key: 'estimatedArrivalDate', + valueType: 'date', + }, + { + title: intl.formatMessage({ id: intlPrefix + '.column.watermelonGrade' }), + dataIndex: 'watermelonGrade', + key: 'watermelonGrade', + search: false, + }, + { + title: intl.formatMessage({ id: intlPrefix + '.column.type' }), + dataIndex: 'type', + key: 'type', + valueType: 'select', + valueEnum: { + PURCHASE_SHIP: intl.formatMessage({ + id: intlPrefix + '.column.type.enum.purchaseShip', + }), + TRANSFER_SHIP: intl.formatMessage({ + id: intlPrefix + '.column.type.enum.transferShip', + }), + CHANGE_SHIP: intl.formatMessage({ + id: intlPrefix + '.column.type.enum.changeShip', + }), + RETURN_SHIP: intl.formatMessage({ + id: intlPrefix + '.column.type.enum.returnShip', + }), + }, + }, { title: intl.formatMessage({ id: intlPrefix + '.column.state' }), dataIndex: 'state', key: 'state', valueType: 'select', - render: (_, record) => { - const stateText = intl.formatMessage({ - id: `${intlPrefix}.column.state.${record.state?.toLowerCase() || 'unknown'}`, - }); - - let color = 'default'; - switch (record.state) { - case 'DRAFT': - color = 'default'; - break; - case 'WAIT_SHIPMENT': - color = 'default'; - break; - case 'WAIT_PAYMENT': - color = 'default'; - break; - case 'PARTIAL_PAYMENT': - color = 'processing'; - break; - case 'FULL_PAYMENT': - color = 'success'; - break; - case 'REJECT_FINISH': - color = 'error'; - break; - case 'FINISH': - color = 'success'; - break; - default: - color = 'default'; - } - - return {stateText}; - }, - }, - { - title: intl.formatMessage({ id: intlPrefix + '.column.createdAt' }), - dataIndex: 'createdAt', - key: 'createdAt', - valueType: 'dateTime', search: false, + valueEnum: { + DRAFT: intl.formatMessage({ + id: intlPrefix + '.column.state.enum.draft', + }), + WAIT_SHIPMENT: intl.formatMessage({ + id: intlPrefix + '.column.state.enum.waitShipment', + }), + WAIT_PAYMENT: intl.formatMessage({ + id: intlPrefix + '.column.state.enum.waitPayment', + }), + PARTIAL_PAYMENT: intl.formatMessage({ + id: intlPrefix + '.column.state.enum.partialPayment', + }), + FULL_PAYMENT: intl.formatMessage({ + id: intlPrefix + '.column.state.enum.fullPayment', + }), + REJECT_FINISH: intl.formatMessage({ + id: intlPrefix + '.column.state.enum.rejectFinish', + }), + FINISH: intl.formatMessage({ + id: intlPrefix + '.column.state.enum.finish', + }), + }, }, ]; @@ -210,6 +240,19 @@ export default function OrderShipList(props: IOrderShipListProps) { }, }, columns, + convertValue: (orderShip: BusinessAPI.OrderShipVO) => { + return { + ...orderShip, + dealerVO: { + dealerId: orderShip.dealerId, + shortName: orderShip.dealerName, + }, + companyVO: { + companyId: orderShip.companyId, + shortName: orderShip.companyName, + }, + }; + }, }} create={false} update={false} diff --git a/packages/app-operation/src/components/Order/OrderShipModal.tsx b/packages/app-operation/src/components/Order/OrderShipModal.tsx new file mode 100644 index 0000000..61c2af4 --- /dev/null +++ b/packages/app-operation/src/components/Order/OrderShipModal.tsx @@ -0,0 +1,377 @@ +import { SelectModal } from '@/components'; +import { business } from '@/services'; +import { formatParam } from '@/utils/formatParam'; +import { pagination } from '@/utils/pagination'; +import { useIntl } from '@@/exports'; +import { + ActionType, + LightFilter, + ProColumns, + ProFormSelect, +} from '@ant-design/pro-components'; +import { Alert, ModalProps, Space, Tag } from 'antd'; +import React, { useEffect, useRef, useState } from 'react'; + +export interface IOrderShipModalProps extends ModalProps { + title: string; + selectedList?: BusinessAPI.OrderShipVO[]; + onFinish: (orderShipVOList: BusinessAPI.OrderShipVO[]) => void; + type: 'checkbox' | 'radio' | undefined; + params?: BusinessAPI.OrderShipPageQry; + num?: number; + tips?: string; + extraFilter?: React.ReactNode[]; + extraColumns?: ProColumns[]; +} + +export default function OrderShipModal(props: IOrderShipModalProps) { + const { + title, + onFinish, + type, + selectedList, + params: initParams, + num = 10, + tips, + extraFilter = [], + extraColumns: initExtraColumns = [], + ...rest + } = props; + const actionRef = useRef(); + const sessionKey = `orderShipList`; + const intl = useIntl(); + const intlPrefix = 'orderShip'; + const [params, setParams] = useState( + initParams || {}, + ); + + useEffect(() => { + if (initParams) { + setParams({ + ...params, + ...initParams, + }); + } + }, [initParams]); + + const columns: ProColumns[] = [ + { + title: intl.formatMessage({ id: intlPrefix + '.column.orderSn' }), + dataIndex: 'orderSn', + key: 'orderSn', + renderText: (text: string) => {text}, + }, + { + title: intl.formatMessage({ id: intlPrefix + '.column.dealerName' }), + dataIndex: 'dealerName', + key: 'dealerName', + }, + { + title: intl.formatMessage({ id: intlPrefix + '.column.vehicleNo' }), + dataIndex: ['orderVO', 'orderVehicle', 'vehicleNo'], + key: 'vehicleNo', + search: false, + render: (_, record) => { + return record.orderVO?.orderVehicle?.vehicleNo + ? '第' + record.orderVO?.orderVehicle?.vehicleNo + '车' + : '-'; + }, + }, + { + title: intl.formatMessage({ id: intlPrefix + '.column.shippingDate' }), + dataIndex: 'shippingDate', + key: 'shippingDate', + valueType: 'date', + }, + { + title: intl.formatMessage({ id: intlPrefix + '.column.state' }), + dataIndex: 'state', + key: 'state', + valueType: 'select', + render: (_, record) => { + const stateText = intl.formatMessage({ + id: `${intlPrefix}.column.state.${record.state?.toLowerCase() || 'unknown'}`, + }); + + let color = 'default'; + switch (record.state) { + case 'DRAFT': + color = 'default'; + break; + case 'WAIT_SHIPMENT': + color = 'default'; + break; + case 'WAIT_PAYMENT': + color = 'processing'; + break; + case 'PARTIAL_PAYMENT': + color = 'processing'; + break; + case 'FULL_PAYMENT': + color = 'success'; + break; + case 'REJECT_FINISH': + color = 'error'; + break; + case 'FINISH': + color = 'success'; + break; + default: + color = 'default'; + } + + return {stateText}; + }, + valueEnum: { + DRAFT: { + text: intl.formatMessage({ + id: intlPrefix + '.column.state.draft', + }), + status: 'default', + }, + WAIT_SHIPMENT: { + text: intl.formatMessage({ + id: intlPrefix + '.column.state.wait_shipment', + }), + status: 'default', + }, + WAIT_PAYMENT: { + text: intl.formatMessage({ + id: intlPrefix + '.column.state.wait_payment', + }), + status: 'processing', + }, + PARTIAL_PAYMENT: { + text: intl.formatMessage({ + id: intlPrefix + '.column.state.partial_payment', + }), + status: 'processing', + }, + FULL_PAYMENT: { + text: intl.formatMessage({ + id: intlPrefix + '.column.state.full_payment', + }), + status: 'success', + }, + REJECT_FINISH: { + text: intl.formatMessage({ + id: intlPrefix + '.column.state.reject_finish', + }), + status: 'error', + }, + FINISH: { + text: intl.formatMessage({ + id: intlPrefix + '.column.state.finish', + }), + status: 'success', + }, + }, + }, + ...(initExtraColumns || []), + ]; + + function setOrderShipVOStorage(orderShipVO: BusinessAPI.OrderShipVO) { + const localOrderShipList = localStorage.getItem(sessionKey); + const orderShipList = localOrderShipList + ? JSON.parse(localOrderShipList) + : []; + orderShipList.forEach((item: BusinessAPI.OrderShipVO, index: number) => { + if (item.orderShipId === orderShipVO.orderShipId) { + orderShipList.splice(index, 1); + } + }); + if (orderShipList.length < 5) { + orderShipList.unshift(orderShipVO); + localStorage.setItem(sessionKey, JSON.stringify(orderShipList)); + } else { + orderShipList.pop(); + orderShipList.unshift(orderShipVO); + localStorage.setItem(sessionKey, JSON.stringify(orderShipList)); + } + } + + return ( + + rowKey={'orderShipId'} + modalProps={{ + title: title || '选择发货单', + ...rest, + destroyOnHidden: true, + afterOpenChange: (open) => { + if (!open) { + setParams({ + ...initParams, + }); + } + }, + }} + selectedList={selectedList} + tableProps={{ + rowKey: 'orderShipId', + columns: columns, + columnsState: { + persistenceType: 'sessionStorage', + persistenceKey: 'orderShipModalColumnStateKey', + }, + params: { + ...params, + }, + request: async (params, sorter, filter) => { + const { data, success, totalCount } = + await business.orderShip.pageOrderShip({ + orderShipPageQry: formatParam( + params, + sorter, + filter, + ), + }); + + return { + data: data || [], + total: totalCount, + success, + }; + }, + pagination: { + ...pagination(), + position: ['bottomRight'], + }, + tableAlertRender: ({ selectedRowKeys, selectedRows }) => { + const selectedRowsMap = new Map(); + selectedRows.forEach((item: BusinessAPI.OrderShipVO) => { + if (item) { + if (!selectedRowsMap.has(item.orderShipId)) { + selectedRowsMap.set(item.orderShipId, item); + } + } + }); + selectedList?.forEach((item: BusinessAPI.OrderShipVO) => { + if (!selectedRowsMap.has(item.orderShipId)) { + selectedRowsMap.set(item.orderShipId, item); + } + }); + let selectedTempList: BusinessAPI.OrderShipVO[] = []; + selectedRowsMap.forEach((item: BusinessAPI.OrderShipVO) => { + if (selectedRowKeys.includes(item.orderShipId)) { + selectedTempList.push(item); + } + }); + return ( + + 已选 {selectedRowKeys.length} 项 + + {selectedTempList?.map((item: BusinessAPI.OrderShipVO) => { + return ( + item && {item.orderSn} + ); + })} + + + ); + }, + ...(tips && { + tableExtraRender: () => { + return tips && ; + }, + }), + ...(type === 'radio' && { + tableExtraRender: () => { + const localOrderShipList = localStorage.getItem(sessionKey); + if (localOrderShipList) { + const orderShipList = JSON.parse(localOrderShipList); + return ( + <> + {tips && } + + {orderShipList.map((item: BusinessAPI.OrderShipVO) => { + return ( + { + // 直接使用 localStorage 中保存的数据 + onFinish([item]); + setOrderShipVOStorage(item); + }} + key={item.orderShipId} + > + {item.orderSn} + + ); + })} + + + ); + } + }, + }), + actionRef: actionRef, + toolbar: { + filter: ( + { + setParams({ + ...initParams, + ...values, + }); + }} + > + {extraFilter} + + + ), + search: { + placeholder: '请输入发货单编号', + onSearch: async (value: string) => { + setParams({ + ...params, + orderSn: value, + }); + }, + }, + }, + }} + onFinish={(orderShipVOList) => { + if (type === 'radio') { + if (orderShipVOList.length > 0) { + setOrderShipVOStorage(orderShipVOList[0]); + } + } + + onFinish(orderShipVOList); + }} + num={num} + type={type} + /> + ); +} diff --git a/packages/app-operation/src/components/Order/OrderSupplierModal.tsx b/packages/app-operation/src/components/Order/OrderSupplierModal.tsx index 4d6a4eb..375a689 100644 --- a/packages/app-operation/src/components/Order/OrderSupplierModal.tsx +++ b/packages/app-operation/src/components/Order/OrderSupplierModal.tsx @@ -63,8 +63,8 @@ export default function OrderSupplierModal(props: IOrderSupplierModalProps) { const columns: ProColumns[] = [ { title: intl.formatMessage({ id: intlPrefix + '.column.order' }), - dataIndex: ['orderVO', 'orderSn'], - key: 'orderSn', + dataIndex: 'orderVO', + key: 'orderId', search: false, render: (_, orderSupplierVO: BusinessAPI.OrderSupplierVO) => { const orderVO = orderSupplierVO.orderVO; @@ -77,7 +77,7 @@ export default function OrderSupplierModal(props: IOrderSupplierModalProps) { trigger={() => ( - {`${orderVO.orderVehicle?.dealerName} - 第 ${orderVO.orderVehicle?.vehicleNo || '暂无'} 车 - ${orderVO.orderSn || '暂无'}`} + {`${orderVO.orderVehicle?.dealerName} | 第 ${orderVO.orderVehicle?.vehicleNo || '暂无'} 车 | ${orderVO.orderSn || '暂无'}`} )} diff --git a/packages/app-operation/src/components/Order/index.ts b/packages/app-operation/src/components/Order/index.ts index c66ca54..b2a220b 100644 --- a/packages/app-operation/src/components/Order/index.ts +++ b/packages/app-operation/src/components/Order/index.ts @@ -8,5 +8,9 @@ export type { IOrderModalProps } from './OrderModal'; export { default as OrderRebateList } from './OrderRebateList'; export { default as OrderSearch } from './OrderSearch'; export { default as OrderSelect } from './OrderSelect'; +export { default as OrderShipList } from './OrderShipList'; +export { default as OrderShipModal } from './OrderShipModal'; +export type { IOrderShipModalProps } from './OrderShipModal'; export { default as OrderStallList } from './OrderStallList'; export { default as OrderSupplierList } from './OrderSupplierList'; +export { default as OrderSupplierModal } from './OrderSupplierModal'; diff --git a/packages/app-operation/src/components/PaymentTask/OrderSupplierInvoiceList.tsx b/packages/app-operation/src/components/PaymentTask/OrderSupplierInvoiceList.tsx index 526faf3..106159b 100644 --- a/packages/app-operation/src/components/PaymentTask/OrderSupplierInvoiceList.tsx +++ b/packages/app-operation/src/components/PaymentTask/OrderSupplierInvoiceList.tsx @@ -23,7 +23,7 @@ export default function OrderSupplierInvoiceList( trigger={() => ( - {`${orderVO.orderVehicle?.dealerName} - 第 ${orderVO.orderVehicle?.vehicleNo || '暂无'} 车 - ${orderVO.orderSn || '暂无'}`} + {`${orderVO.orderVehicle?.dealerName} | 第 ${orderVO.orderVehicle?.vehicleNo || '暂无'} 车 | ${orderVO.orderSn || '暂无'}`} )} diff --git a/packages/app-operation/src/components/SearchMenu/index.tsx b/packages/app-operation/src/components/SearchMenu/index.tsx new file mode 100644 index 0000000..1dd8862 --- /dev/null +++ b/packages/app-operation/src/components/SearchMenu/index.tsx @@ -0,0 +1,267 @@ +import { + ClockCircleOutlined, + SearchOutlined, + StarOutlined, +} from '@ant-design/icons'; +import { MenuDataItem } from '@ant-design/pro-components'; +import { history } from '@umijs/max'; +import type { AutoCompleteProps } from 'antd'; +import { AutoComplete, Input } from 'antd'; +import React, { useMemo, useState } from 'react'; +import useSearchMenuStyle from './style.style'; + +interface SearchMenuProps { + menuData?: MenuDataItem[]; +} + +interface FrequentMenuItem extends MenuDataItem { + frequency: number; + lastUsed: number; +} + +/** 本地存储键 */ +const STORAGE_KEY = 'erp_frequent_menus'; +/** 保留常用菜单数量 */ +const MAX_FREQUENT_MENUS = 8; +/** 最低使用频率阈值 */ +const MIN_FREQUENCY = 1; + +/** + * 递归扁平化菜单数据,生成搜索列表 + */ +const flattenMenuData = (menuData: MenuDataItem[]): MenuDataItem[] => { + const result: MenuDataItem[] = []; + + menuData.forEach((item) => { + if (item.hideInMenu) return; + + const currentPath = item.path; + + if (item.name) { + result.push({ + ...item, + path: currentPath, + }); + } + + if (item.children && item.children.length > 0) { + result.push(...flattenMenuData(item.children)); + } + }); + + return result; +}; + +/** 从本地获取常用菜单 */ +const getFrequentMenus = (): Record => { + try { + const data = localStorage.getItem(STORAGE_KEY); + return data ? JSON.parse(data) : {}; + } catch { + return {}; + } +}; + +/** 保存常用菜单到本地 */ +const saveFrequentMenus = (menus: Record) => { + localStorage.setItem(STORAGE_KEY, JSON.stringify(menus)); +}; + +/** + * 记录菜单使用 + */ +const recordMenuUsage = (path: string, name?: string) => { + if (!path) return; + + const frequentMenus = getFrequentMenus(); + const now = Date.now(); + + if (frequentMenus[path]) { + frequentMenus[path].frequency += 1; + frequentMenus[path].lastUsed = now; + } else { + frequentMenus[path] = { + path, + name: name || '', + frequency: 1, + lastUsed: now, + }; + } + + saveFrequentMenus(frequentMenus); +}; + +const SearchMenu: React.FC = ({ menuData = [] }) => { + const { styles } = useSearchMenuStyle(); + const [open, setOpen] = useState(false); + const [searchValue, setSearchValue] = useState(''); + + // 扁平化菜单数据用于搜索 + const flatMenus = useMemo(() => flattenMenuData(menuData), [menuData]); + + // 根据搜索关键词过滤菜单 + const searchResults = useMemo(() => { + if (!searchValue.trim()) return []; + + const keyword = searchValue.toLowerCase(); + return flatMenus.filter( + (item) => + item.name?.toLowerCase().includes(keyword) || + item.path?.toLowerCase().includes(keyword), + ); + }, [flatMenus, searchValue]); + + // 获取常用菜单(按频率和使用时间排序) + const frequentMenus = useMemo(() => { + const frequentData = getFrequentMenus(); + + // 将常用菜单与扁平化菜单合并,获取完整信息 + const mergedMenus = flatMenus + .map((item) => { + const freq = frequentData[item.path!]; + return freq + ? { + ...item, + frequency: freq.frequency, + lastUsed: freq.lastUsed, + } + : { ...item, frequency: 0, lastUsed: 0 }; + }) + .filter((item) => item.frequency >= MIN_FREQUENCY); + + // 按频率降序,使用时间降序排序 + return mergedMenus + .sort((a, b) => { + if (b.frequency !== a.frequency) { + return b.frequency - a.frequency; + } + return b.lastUsed - a.lastUsed; + }) + .slice(0, MAX_FREQUENT_MENUS); + }, [flatMenus]); + + // 处理菜单点击 + const handleMenuClick = (path: string, name?: string) => { + if (path) { + recordMenuUsage(path, name); + history.push(path); + setOpen(false); + setSearchValue(''); + } + }; + + // 生成 AutoComplete 的 options + const options: AutoCompleteProps['options'] = useMemo(() => { + // 如果有搜索关键词,显示搜索结果 + if (searchValue.trim()) { + return searchResults.map((item) => ({ + value: item.path, + label: ( +
handleMenuClick(item.path!, item.name)} + > + {item.name} + {item.path} +
+ ), + })); + } + + // 如果没有搜索关键词,显示常用菜单 + if (frequentMenus.length > 0) { + return [ + { + value: 'frequent-header', + label: ( +
+ + 常用菜单 +
+ ), + disabled: true, + }, + ...frequentMenus.map((item) => ({ + value: item.path, + label: ( +
handleMenuClick(item.path!, item.name)} + > +
+ {item.name} + {item.frequency} 次 +
+ {item.path} +
+ ), + })), + ]; + } + + // 没有常用菜单时显示提示 + return [ + { + value: 'empty-header', + label: ( +
+ + 暂无常用菜单 +
+ ), + disabled: true, + }, + { + value: 'empty-tip', + label: ( +
点击菜单后会自动记录使用频率
+ ), + disabled: true, + }, + ]; + }, [searchResults, searchValue, frequentMenus, styles]); + + // 处理搜索值变化 + const handleSearch: AutoCompleteProps['onSearch'] = (value) => { + setSearchValue(value); + }; + + // 处理下拉框显示状态变化 + const handleDropdownOpen = (open: boolean) => { + setOpen(open); + if (!open) { + setSearchValue(''); + } + }; + + // 处理选中选项 + const handleSelect: AutoCompleteProps['onSelect'] = (value) => { + const selectedMenu = flatMenus.find((item) => item.path === value); + handleMenuClick(value as string, selectedMenu?.name); + }; + + return ( +
+ + } + allowClear + className={styles.searchInput} + onClick={() => setOpen(true)} + /> + +
+ ); +}; + +export default SearchMenu; diff --git a/packages/app-operation/src/components/SearchMenu/style.style.ts b/packages/app-operation/src/components/SearchMenu/style.style.ts new file mode 100644 index 0000000..3420b6a --- /dev/null +++ b/packages/app-operation/src/components/SearchMenu/style.style.ts @@ -0,0 +1,100 @@ +import { createStyles } from 'antd-style'; + +const useSearchMenuStyle = createStyles(({ token }) => { + return { + searchMenuContainer: { + display: 'flex', + alignItems: 'center', + marginRight: token.marginLG, + }, + searchInput: { + width: '200px', + borderRadius: token.borderRadiusLG, + backgroundColor: token.colorBgLayout, + border: 'none', + transition: `width ${token.motionDurationSlow}, background-color ${token.motionDurationSlow}`, + + '&:hover, &:focus': { + backgroundColor: token.colorBgElevated, + width: '280px', + }, + + '.anticon': { + color: token.colorTextPlaceholder, + }, + }, + searchIcon: { + color: token.colorTextPlaceholder, + }, + dropdown: { + padding: `${token.paddingXS}px ${token.paddingXXS}px`, + }, + sectionHeader: { + display: 'flex', + alignItems: 'center', + gap: token.paddingXS, + padding: `${token.paddingXXS}px ${token.paddingXS}px`, + fontSize: token.fontSizeSM, + fontWeight: token.fontWeightMedium, + color: token.colorTextSecondary, + borderBottom: `1px solid ${token.colorBorderSecondary}`, + marginBottom: token.marginXXS, + }, + sectionIcon: { + fontSize: token.fontSize, + }, + searchItem: { + display: 'flex', + flexDirection: 'column', + padding: `${token.paddingXXS}px ${token.paddingXS}px`, + borderRadius: token.borderRadiusSM, + cursor: 'pointer', + transition: `background-color ${token.motionDurationFast}`, + + '&:hover': { + backgroundColor: token.colorBgTextHover, + }, + }, + menuRow: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + gap: token.paddingSM, + }, + menuName: { + fontSize: token.fontSize, + color: token.colorText, + fontWeight: token.fontWeightMedium, + lineHeight: token.lineHeight, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + flex: 1, + }, + menuPath: { + fontSize: token.fontSizeSM, + color: token.colorTextSecondary, + marginTop: token.marginXXS, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + maxWidth: '260px', + }, + frequency: { + fontSize: token.fontSizeSM, + color: token.colorTextTertiary, + backgroundColor: token.colorBgLayout, + padding: `2px ${token.paddingXXS}px`, + borderRadius: token.borderRadiusSM, + flexShrink: 0, + }, + emptyTip: { + padding: `${token.paddingSM}px ${token.paddingLG}px`, + fontSize: token.fontSizeSM, + color: token.colorTextTertiary, + textAlign: 'center', + }, + }; +}); + +export default useSearchMenuStyle; diff --git a/packages/app-operation/src/components/Supplier/SupplierInvoiceList.tsx b/packages/app-operation/src/components/Supplier/SupplierInvoiceList.tsx index 3987b59..ce4cef0 100644 --- a/packages/app-operation/src/components/Supplier/SupplierInvoiceList.tsx +++ b/packages/app-operation/src/components/Supplier/SupplierInvoiceList.tsx @@ -137,7 +137,7 @@ export default function SupplierInvoiceList(props: ISupplierInvoiceListProps) { trigger={() => ( - {`${item?.dealerName} - 第 ${item?.vehicleNo || '暂无'} 车`} + {`${item?.dealerName} | 第 ${item?.vehicleNo || '暂无'} 车`} )} diff --git a/packages/app-operation/src/components/index.ts b/packages/app-operation/src/components/index.ts index 52947a5..8cb3750 100644 --- a/packages/app-operation/src/components/index.ts +++ b/packages/app-operation/src/components/index.ts @@ -11,7 +11,6 @@ export { default as CaptchaModal } from './CaptchaModal'; export * from './Channel'; export * from './Company'; export * from './Dealer'; -export * from './Delivery'; export * from './Editor'; export * from './Employee'; export * from './Expense'; @@ -30,6 +29,7 @@ export * from './Permission'; export * from './Platform'; export * from './Remark'; export * from './Role'; +export { default as SearchMenu } from './SearchMenu'; export * from './Setting'; export * from './Supplier'; export { default as UploadMaterial } from './UploadMaterial'; diff --git a/packages/app-operation/src/locales/zh-CN.ts b/packages/app-operation/src/locales/zh-CN.ts index 752b7c4..e14911e 100644 --- a/packages/app-operation/src/locales/zh-CN.ts +++ b/packages/app-operation/src/locales/zh-CN.ts @@ -2110,17 +2110,27 @@ export default { }, column: { orderSn: '发货单编号', - dealerName: '经销商名称', - vehicleNo: '车次号', + order: '采购单', + dealer: '经销商', + shippingAddress: '发货地', + receivingAddress: '收货地', shippingDate: '发货日期', + estimatedArrivalDate: '预计到仓时间', + watermelonGrade: '西瓜品级', + company: '入账公司', + type: '发货单类型', + 'type.enum.purchaseShip': '采购发货单', + 'type.enum.transferShip': '调货发货', + 'type.enum.changeShip': '改签发货', + 'type.enum.returnShip': '退货发货', state: '状态', - 'state.draft': '草稿', - 'state.wait_shipment': '待发货', - 'state.wait_payment': '待回款', - 'state.partial_payment': '部分回款', - 'state.full_payment': '已回款', - 'state.reject_finish': '拒收完结', - 'state.finish': '已完结', + 'state.enum.draft': '草稿', + 'state.enum.waitShipment': '待发货', + 'state.enum.waitPayment': '待回款', + 'state.enum.partialPayment': '部分回款', + 'state.enum.fullPayment': '已回款', + 'state.enum.rejectFinish': '拒收完结', + 'state.enum.finish': '已完结', createdAt: '创建时间', option: '操作', }, diff --git a/packages/app-operation/src/pages/OrderShip.tsx b/packages/app-operation/src/pages/OrderShip.tsx new file mode 100644 index 0000000..7eb362a --- /dev/null +++ b/packages/app-operation/src/pages/OrderShip.tsx @@ -0,0 +1,5 @@ +import { OrderShipList } from '@/components'; + +export default function Page() { + return ; +} diff --git a/packages/app-operation/src/pages/ReconciliationCreate.tsx b/packages/app-operation/src/pages/ReconciliationCreate.tsx index 47c8e89..685abb1 100644 --- a/packages/app-operation/src/pages/ReconciliationCreate.tsx +++ b/packages/app-operation/src/pages/ReconciliationCreate.tsx @@ -1,91 +1,394 @@ -import { - DealerSelect, - PageContainer, - OrderList, - ShipOrderList, -} from '@/components'; +import { DealerSelect, PageContainer } from '@/components'; +import OrderShipModal from '@/components/Order/OrderShipModal'; +import { formatCurrency } from '@/utils/format'; import { ProCard, ProForm, ProFormDependency, + ProFormTextArea, } from '@ant-design/pro-components'; -import { Space, Steps } from 'antd'; +import { Button, Col, Row, Space, Table, Tag } from 'antd'; import { useState } from 'react'; export default function Page() { const [current, setCurrent] = useState(0); - const onChange = (value: number) => { - console.log('onChange:', value); - setCurrent(value); - }; + const [selectedDealer, setSelectedDealer] = + useState(null); + const [selectedShipOrderList, setSelectedShipOrderList] = useState< + BusinessAPI.OrderShipVO[] + >([]); + const [orderShipModalOpen, setOrderShipModalOpen] = useState(false); - const [dealerVO, setDealerVO] = useState(); + // 计算总金额 + const totalAmount = selectedShipOrderList.reduce((sum, order) => { + // 使用发货单明细的总金额 + const orderTotal = + order.orderShipItemList?.reduce( + (total, item) => total + (item.totalAmount || 0), + 0, + ) || 0; + return sum + orderTotal; + }, 0); + const orderCount = selectedShipOrderList.length; return ( + + + + + 1 + + + 选择经销商与车次 + + + + → + + + + 2 + + + 核对并录入调整项 + + + + ), }} > - {current === 0 && ( - - 选择客户与车次 - { - setDealerVO(formData.dealerVO); + {/* 对账单表单 */} + + 步骤 {current + 1} + {current === 0 ? '选择经销商与车次' : '核对并录入调整项'} + + } + > + + + {(_, form) => ( + { + setSelectedDealer(dealerVOList[0]); + // 切换经销商时清空已选车次 + setSelectedShipOrderList([]); + form.submit(); }} - > - - {(_, form) => ( - { - form.submit(); - }} - /> - )} - - - - } - headerBordered={true} - direction={'column'} - > - {dealerVO && ( - - )} - - )} - {current === 1 && ( - - )} + /> + )} + + + {current === 0 && selectedDealer && ( + <> + {/* 经销商信息 */} + + + +
+
+ 经销商简称 +
+
+ {selectedDealer.shortName || '-'} +
+
+ + +
+
+ 经销商全称 +
+
+ {selectedDealer.fullName || '-'} +
+
+ + +
+
+ 经销商类型 +
+
+ {selectedDealer.dealerType === 'MARKET' ? '市场' : '超市'} +
+
+ +
+
+ + {/* 选择车次 */} + setOrderShipModalOpen(true)} + > + 添加车次 + + } + > + {selectedShipOrderList.length > 0 ? ( + + rowKey="orderShipId" + dataSource={selectedShipOrderList} + columns={[ + { + title: '发货单号', + dataIndex: 'orderSn', + key: 'orderSn', + render: (text: string) => ( + {text} + ), + }, + { + title: '车辆编号', + key: 'vehicleNo', + render: (_, record: BusinessAPI.OrderShipVO) => { + return record.orderVO?.orderVehicle?.vehicleNo + ? `第${record.orderVO?.orderVehicle?.vehicleNo}车` + : '-'; + }, + }, + { + title: '发货日期', + dataIndex: 'shippingDate', + key: 'shippingDate', + render: (_, record: BusinessAPI.OrderShipVO) => { + return record.shippingDate || '-'; + }, + }, + { + title: '总金额', + key: 'totalAmount', + render: (_, record: BusinessAPI.OrderShipVO) => { + const total = + record.orderShipItemList?.reduce( + (sum, item) => sum + (item.totalAmount || 0), + 0, + ) || 0; + return {formatCurrency(total)}; + }, + }, + { + title: '操作', + key: 'action', + render: (_, record: BusinessAPI.OrderShipVO) => ( + + ), + }, + ]} + pagination={false} + size="small" + summary={(pageData) => { + let totalAmount = 0; + pageData.forEach((record) => { + const total = + record.orderShipItemList?.reduce( + (sum, item) => sum + (item.totalAmount || 0), + 0, + ) || 0; + totalAmount += total; + }); + return ( + + + + 合计 + + + + {formatCurrency(totalAmount)} + + + + + + ); + }} + /> + ) : ( +
+ 暂无数据,请点击「添加车次」按钮添加 +
+ )} +
+ + {/* 任务摘要 */} + {selectedShipOrderList.length > 0 && ( + + + +
+
+ 对账经销商 +
+
+ {selectedDealer?.fullName || selectedDealer?.shortName} +
+
+ + +
+
+ 车次数量 +
+
{orderCount} 车
+
+ + +
+
+ 对账总额 +
+
+ {formatCurrency(totalAmount)} +
+
+ + +
+
+ 对账状态 +
+
+ 待对账 +
+
+ +
+
+ )} + + {/* 下一步按钮 */} + {selectedShipOrderList.length > 0 && ( +
+ +
+ )} + + )} + + {current === 0 && ( + <> + + + + + )} +
+ {/* 选择发货单模态框 */} + { + // 过滤掉已经选择的发货单 + const newOrders = orderShipList.filter( + (item) => + !selectedShipOrderList.some( + (selected) => selected.orderShipId === item.orderShipId, + ), + ); + setSelectedShipOrderList([...selectedShipOrderList, ...newOrders]); + setOrderShipModalOpen(false); + }} + onCancel={() => setOrderShipModalOpen(false)} + params={{ + dealerId: selectedDealer?.dealerId, + }} + tips="请选择该经销商未对账的发货单" + num={999} + />
); } diff --git a/packages/app-operation/src/pages/ShipOrder.tsx b/packages/app-operation/src/pages/ShipOrder.tsx deleted file mode 100644 index b0a5829..0000000 --- a/packages/app-operation/src/pages/ShipOrder.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { ShipOrderList } from '@/components'; - -export default function Page() { - return ; -} diff --git a/packages/app-operation/src/services/auth/typings.d.ts b/packages/app-operation/src/services/auth/typings.d.ts index d73fdb3..b3445c1 100644 --- a/packages/app-operation/src/services/auth/typings.d.ts +++ b/packages/app-operation/src/services/auth/typings.d.ts @@ -100,7 +100,7 @@ declare namespace AuthAPI { /** 用户ID */ userId: string; /** 角色ID */ - roleIdList: number[]; + roleIdList: string[]; /** 角色信息 */ userRoleList?: UserRoleVO[]; }; @@ -195,7 +195,7 @@ declare namespace AuthAPI { type RoleMenuTreeQry = { /** 角色权限 */ - roleId?: number[]; + roleId?: string[]; /** 平台ID */ platformId: string; }; @@ -329,7 +329,7 @@ declare namespace AuthAPI { /** 备注 */ remark?: string; /** 客户标签 */ - labelId?: number[]; + labelId?: string[]; /** 用户ID */ userId: string; }; diff --git a/packages/app-operation/src/services/business/typings.d.ts b/packages/app-operation/src/services/business/typings.d.ts index ca13201..c19555f 100644 --- a/packages/app-operation/src/services/business/typings.d.ts +++ b/packages/app-operation/src/services/business/typings.d.ts @@ -208,7 +208,7 @@ declare namespace BusinessAPI { /** 品牌图片URL */ image?: string; /** 纸箱规格ID */ - specIds?: number[]; + specIds?: string[]; /** 备注 */ remark?: string; /** 状态:1_启用;0_禁用 */ @@ -279,7 +279,7 @@ declare namespace BusinessAPI { /** 品牌图片URL */ image?: string; /** 纸箱规格ID */ - specIds?: number[]; + specIds?: string[]; /** 备注 */ remark?: string; /** 状态:1_启用;0_禁用 */ @@ -296,7 +296,7 @@ declare namespace BusinessAPI { /** 品牌图片URL */ image?: string; /** 纸箱规格ID */ - specIds?: number[]; + specIds?: string[]; /** 备注 */ remark?: string; /** 状态:1_启用;0_禁用 */ @@ -1021,7 +1021,7 @@ declare namespace BusinessAPI { /** 状态:1_启用;0_禁用 */ status: boolean; /** 成本项ID */ - costItemIds?: number[]; + costItemIds?: string[]; }; type CostDestroyCmd = { @@ -1249,7 +1249,7 @@ declare namespace BusinessAPI { /** 状态:1_启用;0_禁用 */ status: boolean; /** 成本项ID */ - costItemIds?: number[]; + costItemIds?: string[]; }; type CostVO = { @@ -1280,7 +1280,7 @@ declare namespace BusinessAPI { /** 状态:1_启用;0_禁用 */ status: boolean; /** 项目id集合 */ - costItemIds?: number[]; + costItemIds?: string[]; /** 创建时间 */ createdAt?: string; /** 项目列表 */ @@ -1961,7 +1961,7 @@ declare namespace BusinessAPI { /** 登录密码 */ password: string; /** 角色ID */ - roleId: number[]; + roleId: string[]; }; type EmployeeDestroyCmd = { @@ -2054,7 +2054,7 @@ declare namespace BusinessAPI { /** 用户ID */ userId: string; /** 角色ID */ - roleIdList: number[]; + roleIdList: string[]; /** 角色信息 */ userRoleList?: UserRoleVO[]; }; @@ -2487,7 +2487,7 @@ declare namespace BusinessAPI { /** 平台id */ platformId: string; /** 角色Id */ - roleId?: number[]; + roleId?: string[]; /** 是否隐藏 */ hideInMenu?: boolean; /** 权限Id */ @@ -2560,7 +2560,7 @@ declare namespace BusinessAPI { /** 平台id */ platformId: string; /** 角色Id */ - roleId?: number[]; + roleId?: string[]; /** 是否隐藏 */ hideInMenu?: boolean; /** 权限Id */ @@ -2978,7 +2978,7 @@ declare namespace BusinessAPI { | 'LOGISTICS_TYPE' | 'EXPENSE_TYPE'; /** 关联项目id */ - costItemIds?: number[]; + costItemIds?: string[]; /** 是否选中 */ selected: boolean; /** 是否已付款 */ @@ -3015,7 +3015,7 @@ declare namespace BusinessAPI { | 'LOGISTICS_TYPE' | 'EXPENSE_TYPE'; /** 关联项目id */ - costItemIds?: number[]; + costItemIds?: string[]; /** 是否付款 */ isPaid?: boolean; }; @@ -3148,7 +3148,7 @@ declare namespace BusinessAPI { | 'LOGISTICS_TYPE' | 'EXPENSE_TYPE'; /** 关联项目id */ - costItemIds?: number[]; + costItemIds?: string[]; /** 创建时间 */ createdAt: string; /** 采购订单状态: 0_草稿;1_审核中;2_已完成;3_已关闭; */ @@ -3870,7 +3870,7 @@ declare namespace BusinessAPI { /** 产品名称 */ productName?: string; /** 关联费用id */ - costIds?: number[]; + costIds?: string[]; /** 成本模板 */ costTemplate?: string; /** 是否已付定金 */ @@ -5180,7 +5180,7 @@ declare namespace BusinessAPI { /** 产品名称 */ name: string; /** 关联成本费用id */ - costIds?: number[]; + costIds?: string[]; /** 成本模板 */ costTemplate?: string; /** 备注 */ @@ -5247,7 +5247,7 @@ declare namespace BusinessAPI { /** 产品名称 */ name: string; /** 关联成本费用id */ - costIds?: number[]; + costIds?: string[]; /** 成本模板 */ costTemplate?: string; /** 备注 */ @@ -5272,7 +5272,7 @@ declare namespace BusinessAPI { /** 状态:1_启用;0_禁用 */ status: boolean; /** 成本ID集合 */ - costIds?: number[]; + costIds?: string[]; /** 成本费用 */ costVOList?: CostVO[]; /** 成本模板 */ @@ -5299,7 +5299,7 @@ declare namespace BusinessAPI { /** 角色详情 */ description?: string; /** 角色id */ - menuId: number[]; + menuId: string[]; }; type RoleDestroyCmd = { @@ -5321,7 +5321,7 @@ declare namespace BusinessAPI { /** 角色编号 */ roleId?: string; /** 应用角色Id */ - roleIdList?: number[]; + roleIdList?: string[]; /** 平台Id */ platformId?: string; /** 平台Id */ @@ -5368,7 +5368,7 @@ declare namespace BusinessAPI { /** 角色详情 */ description?: string; /** 角色id */ - menuId: number[]; + menuId: string[]; /** 角色ID */ roleId: string; }; @@ -5389,9 +5389,9 @@ declare namespace BusinessAPI { /** 平台 */ platformVO?: PlatformVO; /** 权限列表 */ - permissionId: number[]; + permissionId: string[]; /** 菜单列表 */ - menuId: number[]; + menuId: string[]; /** 创建时间 */ createdAt: string; }; @@ -6265,7 +6265,7 @@ declare namespace BusinessAPI { /** 备注 */ remark?: string; /** 客户标签 */ - labelId?: number[]; + labelId?: string[]; }; type UserDestroyCmd = { @@ -6287,7 +6287,7 @@ declare namespace BusinessAPI { /** 状态:1_启用;0_禁用; */ status?: boolean; /** 用户ID */ - userIdList?: number[]; + userIdList?: string[]; /** 用户名 */ name?: string; }; @@ -6330,9 +6330,9 @@ declare namespace BusinessAPI { /** 是否是管理员 */ isAdmin?: boolean; /** 会员id列表 */ - userIdList?: number[]; + userIdList?: string[]; /** 排除的用户id列表 */ - excludeUserIdList?: number[]; + excludeUserIdList?: string[]; /** 小区id */ communityId?: number; offset?: number; @@ -6342,7 +6342,7 @@ declare namespace BusinessAPI { /** 用户ID */ userId: string; /** 角色ID */ - roleIdList?: number[]; + roleIdList?: string[]; /** 是否覆盖 */ cover: boolean; }; @@ -6387,7 +6387,7 @@ declare namespace BusinessAPI { /** 备注 */ remark?: string; /** 客户标签 */ - labelId?: number[]; + labelId?: string[]; /** 用户ID */ userId: string; }; diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 00d1b2a..600b4bb 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,2 @@ packages: - 'packages/**' - - 'shared/**'