refactor(order): 重构发货单功能模块

- 将 ShipOrderList 组件从 Delivery 模块迁移至 Order 模块
- 移除 Delivery 模块的导出并在 Order 模块中新增 OrderShipList 和相关组件
- 更新发货单列表的列配置,添加订单、经销商、公司等关联字段
- 修改发货单列表的字段映射,将经销商名称和车次号格式从短横线改为竖线分隔
- 在 BizPage 组件中添加 convertValue 属性支持数据转换
- 更新左侧菜单组件的依赖数组以包含 menuData
- 调整发货单列表的国际化配置,更新字段标题和状态枚举值
- 新增 OrderShip 页面路由文件
- 移除项目根目录的 AGENTS.md 和 project.md 文件
- 从组件索引中移除 Delivery 模块并添加 SearchMenu 组件
This commit is contained in:
shenyifei 2026-01-08 16:15:03 +08:00
parent f68b148319
commit b4ce6e45dc
29 changed files with 1630 additions and 863 deletions

View File

@ -1,18 +0,0 @@
<!-- OPENSPEC:START -->
# 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.
<!-- OPENSPEC:END -->

332
CLAUDE.md
View File

@ -1,18 +1,324 @@
<!-- OPENSPEC:START --> # CLAUDE.md
# OpenSpec Instructions
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: ERPTurbo_Admin 是一个基于 React 生态系统的企业级管理后台系统,采用 Monorepo 架构。使用 UmiJS Max 作为应用框架Ant Design 作为 UI 组件库。
- 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. ## 项目结构
<!-- OPENSPEC:END --> ```
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
<BizContainer<
typeof business.newComponent, // API 函数类型
BusinessAPI.NewComponentVO, // 视图对象类型
BusinessAPI.NewComponentPageQry, // 查询参数类型
BusinessAPI.NewComponentCreateCmd, // 创建命令类型
BusinessAPI.NewComponentUpdateCmd // 更新命令类型
>
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
<PageContainer permission="operation-new-component">
{/* 页面内容 */}
</PageContainer>
```
**按钮级权限**
```tsx
<ButtonAccess permission="operation-new-component-create">
<Button>新增</Button>
</ButtonAccess>
```
#### 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 <NewComponentList />;
}
```
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

View File

@ -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/<id>/`.
3. Draft spec deltas using `## ADDED|MODIFIED|REMOVED Requirements` with at least one `#### Scenario:` per requirement.
4. Run `openspec validate <id> --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 <change-id> --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 12 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 <spec-id> --type spec` (use `--json` for filters)
- Change: `openspec show <change-id> --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 <change-id> [--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/<capability>/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 arent 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/<capability>/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 <change-id> [--yes|-y] # Mark complete (add --yes for automation)
```
Remember: Specs are truth. Changes are proposals. Keep them in sync.

View File

@ -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

View File

@ -2,13 +2,17 @@
// 全局初始化数据配置,用于 Layout 用户信息和权限初始化 // 全局初始化数据配置,用于 Layout 用户信息和权限初始化
// 更多信息见文档https://umijs.org/docs/api/runtime-config#getinitialstate // 更多信息见文档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 Avatar from '@/layout/guide/Avatar';
import { auth } from '@/services'; import { auth } from '@/services';
import { Navigate } from '@@/exports'; import { Navigate } from '@@/exports';
import { RunTimeLayoutConfig } from '@@/plugin-layout/types'; import { RunTimeLayoutConfig } from '@@/plugin-layout/types';
import { RequestConfig } from '@@/plugin-request/request'; 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 '@nutui/nutui-react/dist/style.scss';
import { history } from '@umijs/max'; import { history } from '@umijs/max';
import { Alert, Button, message, notification, Spin } from 'antd'; 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('admin', JSON.stringify(adminVO));
window.localStorage.setItem( window.localStorage.setItem('userRoleVOList', JSON.stringify([]));
'userRoleVOList',
JSON.stringify([]),
);
window.localStorage.setItem('roleSlug', 'operation'); window.localStorage.setItem('roleSlug', 'operation');
} }
@ -298,18 +299,18 @@ export const layout: RunTimeLayoutConfig = ({ initialState }) => {
// encodeURIComponent(process.env.UMI_APP_OPERATION_URL || ''); // encodeURIComponent(process.env.UMI_APP_OPERATION_URL || '');
// } // }
}, },
// actionsRender: (props) => { actionsRender: (props) => {
// if (props.isMobile) return []; if (props.isMobile) return [];
// if (typeof window === 'undefined') return []; if (typeof window === 'undefined') return [];
// return [ return [
// props.layout !== 'side' && document.body.clientWidth > 1400 ? ( props.layout !== 'side' && document.body.clientWidth > 1400 ? (
// <SearchInput key={'SearchInput'} /> <SearchMenu key={'SearchMenu'} menuData={props.menuData} />
// ) : undefined, ) : undefined,
// <InfoCircleFilled key="InfoCircleFilled" />, <InfoCircleFilled key="InfoCircleFilled" />,
// <QuestionCircleFilled key="QuestionCircleFilled" />, <QuestionCircleFilled key="QuestionCircleFilled" />,
// <NotificationFilled key="NotificationFilled" />, <NotificationFilled key="NotificationFilled" />,
// ]; ];
// }, },
headerRender: (props, defaultDom) => { headerRender: (props, defaultDom) => {
return ( return (
<> <>

View File

@ -872,7 +872,7 @@ export default function BizContainer<
return ( return (
data?.map((item) => { data?.map((item) => {
return { return {
label: item.fullName, label: `${item.shortName} | ${item.fullName}`,
value: item.companyId, value: item.companyId,
}; };
}) || [] }) || []
@ -1020,7 +1020,7 @@ export default function BizContainer<
trigger={() => ( trigger={() => (
<Space> <Space>
<a> <a>
{`${orderVO.orderVehicle?.dealerName} -${orderVO.orderVehicle?.vehicleNo || '暂无'}- ${orderVO.orderSn || '暂无'}`} {`${orderVO.orderVehicle?.dealerName} |${orderVO.orderVehicle?.vehicleNo || '暂无'}| ${orderVO.orderSn || '暂无'}`}
</a> </a>
</Space> </Space>
)} )}

View File

@ -22,6 +22,7 @@ export default function BizPage<
trigger, trigger,
isMobile, isMobile,
intlPrefix, intlPrefix,
convertValue,
} = props; } = props;
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const intl = useIntl(); const intl = useIntl();
@ -34,7 +35,7 @@ export default function BizPage<
persistenceType: 'sessionStorage', persistenceType: 'sessionStorage',
persistenceKey: method + 'ColumnStateKey', persistenceKey: method + 'ColumnStateKey',
defaultValue: { defaultValue: {
// option: { show: true, fixed: 'right' }, orderId: { show: true, fixed: 'left' },
}, },
}, },
scroll: { x: 'max-content' }, scroll: { x: 'max-content' },
@ -78,7 +79,12 @@ export default function BizPage<
}); });
return { return {
data, data: data.map((item: BizVO) => {
if (convertValue) {
return convertValue(item);
}
return item;
}),
success, success,
total: totalCount, total: totalCount,
}; };

View File

@ -133,6 +133,7 @@ export type BizPageProps<
) => React.ReactNode; ) => React.ReactNode;
fieldProps?: ProTableProps<BizVO, BizPageQry, BizValueType>; fieldProps?: ProTableProps<BizVO, BizPageQry, BizValueType>;
trigger?: () => React.ReactNode; trigger?: () => React.ReactNode;
convertValue?: (record: BizVO) => BizVO;
} & ApiProps<Func>; } & ApiProps<Func>;
export interface BizTreeProps< export interface BizTreeProps<

View File

@ -1 +0,0 @@
export { default as ShipOrderList } from './ShipOrderList';

View File

@ -87,7 +87,7 @@ const LeftMenu: React.FC<ILeftMenuProps> = (props) => {
); );
setActiveMenuKey(activeMenuKey); setActiveMenuKey(activeMenuKey);
setOpenKeys([...openKeys, ...activeMenuKey]); setOpenKeys([...openKeys, ...activeMenuKey]);
}, [pathname]); }, [pathname, menuData]);
if (!menuData) { if (!menuData) {
return null; return null;

View File

@ -120,7 +120,7 @@ export default function OrderCostPay(props: IOrderCostPayProps) {
</div> </div>
<div style={{ fontWeight: 'bold' }}> <div style={{ fontWeight: 'bold' }}>
{orderCostVO.orderVO {orderCostVO.orderVO
? `${orderCostVO.orderVO.orderVehicle.dealerName} -${orderCostVO.orderVO.orderVehicle.vehicleNo || '暂无'}` ? `${orderCostVO.orderVO.orderVehicle.dealerName} |${orderCostVO.orderVO.orderVehicle.vehicleNo || '暂无'}`
: '-'} : '-'}
</div> </div>
</div> </div>

View File

@ -56,7 +56,7 @@ export default function OrderList(props: IOrderListProps) {
trigger={() => ( trigger={() => (
<Space> <Space>
<a> <a>
{`${orderVO.orderVehicle?.dealerName} -${orderVO.orderVehicle?.vehicleNo || '暂无'}- ${orderVO.orderSn || '暂无'}`} {`${orderVO.orderVehicle?.dealerName} |${orderVO.orderVehicle?.vehicleNo || '暂无'}| ${orderVO.orderSn || '暂无'}`}
</a> </a>
</Space> </Space>
)} )}

View File

@ -1,3 +1,4 @@
import { OrderFormItem, OrderList, SelectModal } from '@/components';
import { business } from '@/services'; import { business } from '@/services';
import { formatParam } from '@/utils/formatParam'; import { formatParam } from '@/utils/formatParam';
import { pagination } from '@/utils/pagination'; import { pagination } from '@/utils/pagination';
@ -8,7 +9,6 @@ import {
ProColumns, ProColumns,
ProFormSelect, ProFormSelect,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { OrderFormItem, OrderList, SelectModal } from '@/components';
import { Alert, ModalProps, Row, Space, Tag } from 'antd'; import { Alert, ModalProps, Row, Space, Tag } from 'antd';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
@ -74,7 +74,7 @@ export default function OrderModal(props: IOrderModalProps) {
trigger={() => ( trigger={() => (
<Space> <Space>
<a> <a>
{`${orderVO.orderVehicle?.dealerName} -${orderVO.orderVehicle?.vehicleNo || '暂无'}- ${orderVO.orderSn || '暂无'}`} {`${orderVO.orderVehicle?.dealerName} |${orderVO.orderVehicle?.vehicleNo || '暂无'}| ${orderVO.orderSn || '暂无'}`}
</a> </a>
</Space> </Space>
)} )}

View File

@ -3,7 +3,6 @@ import { business } from '@/services';
import { useIntl } from '@@/exports'; import { useIntl } from '@@/exports';
import { ProColumns } from '@ant-design/pro-components'; import { ProColumns } from '@ant-design/pro-components';
import { ProDescriptionsItemProps } from '@ant-design/pro-descriptions'; import { ProDescriptionsItemProps } from '@ant-design/pro-descriptions';
import { Tag } from 'antd';
import React, { useState } from 'react'; import React, { useState } from 'react';
interface IOrderShipListProps { interface IOrderShipListProps {
@ -13,7 +12,7 @@ interface IOrderShipListProps {
onValueChange?: () => void; onValueChange?: () => void;
mode?: ModeType; mode?: ModeType;
trigger?: () => React.ReactNode; trigger?: () => React.ReactNode;
params: BusinessAPI.OrderShipPageQry; params?: BusinessAPI.OrderShipPageQry;
} }
export default function OrderShipList(props: IOrderShipListProps) { export default function OrderShipList(props: IOrderShipListProps) {
@ -32,26 +31,40 @@ export default function OrderShipList(props: IOrderShipListProps) {
const [activeKey, setActiveKey] = useState<string>('ALL'); const [activeKey, setActiveKey] = useState<string>('ALL');
const columns: ProColumns<BusinessAPI.OrderShipVO, BizValueType>[] = [ const columns: ProColumns<BusinessAPI.OrderShipVO, BizValueType>[] = [
{
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' }), title: intl.formatMessage({ id: intlPrefix + '.column.orderSn' }),
dataIndex: 'orderSn', dataIndex: 'orderSn',
key: 'orderSn', key: 'orderSn',
renderText: (text: string) => <span className="font-medium">{text}</span>,
}, },
{ {
title: intl.formatMessage({ id: intlPrefix + '.column.dealerName' }), title: intl.formatMessage({ id: intlPrefix + '.column.shippingAddress' }),
dataIndex: 'dealerName', dataIndex: 'shippingAddress',
key: 'dealerName', key: 'shippingAddress',
}, },
{ {
title: intl.formatMessage({ id: intlPrefix + '.column.vehicleNo' }), title: intl.formatMessage({
dataIndex: ['orderVehicle', 'vehicleNo'], id: intlPrefix + '.column.receivingAddress',
key: 'vehicleNo', }),
render: (_, record) => { dataIndex: 'receivingAddress',
return record.orderVehicle?.vehicleNo key: 'receivingAddress',
? '第' + record.orderVehicle?.vehicleNo + '车'
: '-';
},
}, },
{ {
title: intl.formatMessage({ id: intlPrefix + '.column.shippingDate' }), title: intl.formatMessage({ id: intlPrefix + '.column.shippingDate' }),
@ -59,52 +72,69 @@ export default function OrderShipList(props: IOrderShipListProps) {
key: 'shippingDate', key: 'shippingDate',
valueType: 'date', 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' }), title: intl.formatMessage({ id: intlPrefix + '.column.state' }),
dataIndex: 'state', dataIndex: 'state',
key: 'state', key: 'state',
valueType: 'select', 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 <Tag color={color}>{stateText}</Tag>;
},
},
{
title: intl.formatMessage({ id: intlPrefix + '.column.createdAt' }),
dataIndex: 'createdAt',
key: 'createdAt',
valueType: 'dateTime',
search: false, 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, columns,
convertValue: (orderShip: BusinessAPI.OrderShipVO) => {
return {
...orderShip,
dealerVO: {
dealerId: orderShip.dealerId,
shortName: orderShip.dealerName,
},
companyVO: {
companyId: orderShip.companyId,
shortName: orderShip.companyName,
},
};
},
}} }}
create={false} create={false}
update={false} update={false}

View File

@ -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<BusinessAPI.OrderShipVO>[];
}
export default function OrderShipModal(props: IOrderShipModalProps) {
const {
title,
onFinish,
type,
selectedList,
params: initParams,
num = 10,
tips,
extraFilter = [],
extraColumns: initExtraColumns = [],
...rest
} = props;
const actionRef = useRef<ActionType>();
const sessionKey = `orderShipList`;
const intl = useIntl();
const intlPrefix = 'orderShip';
const [params, setParams] = useState<BusinessAPI.OrderShipPageQry>(
initParams || {},
);
useEffect(() => {
if (initParams) {
setParams({
...params,
...initParams,
});
}
}, [initParams]);
const columns: ProColumns<BusinessAPI.OrderShipVO>[] = [
{
title: intl.formatMessage({ id: intlPrefix + '.column.orderSn' }),
dataIndex: 'orderSn',
key: 'orderSn',
renderText: (text: string) => <span className="font-medium">{text}</span>,
},
{
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 <Tag color={color}>{stateText}</Tag>;
},
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 (
<SelectModal<BusinessAPI.OrderShipVO, BusinessAPI.OrderShipPageQry>
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<typeof params>(
params,
sorter,
filter,
),
});
return {
data: data || [],
total: totalCount,
success,
};
},
pagination: {
...pagination(),
position: ['bottomRight'],
},
tableAlertRender: ({ selectedRowKeys, selectedRows }) => {
const selectedRowsMap = new Map<string, BusinessAPI.OrderShipVO>();
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 (
<Space size={12}>
<span> {selectedRowKeys.length} </span>
<Space wrap={true}>
{selectedTempList?.map((item: BusinessAPI.OrderShipVO) => {
return (
item && <span key={item.orderShipId}>{item.orderSn}</span>
);
})}
</Space>
</Space>
);
},
...(tips && {
tableExtraRender: () => {
return tips && <Alert type={'info'} message={tips} />;
},
}),
...(type === 'radio' && {
tableExtraRender: () => {
const localOrderShipList = localStorage.getItem(sessionKey);
if (localOrderShipList) {
const orderShipList = JSON.parse(localOrderShipList);
return (
<>
{tips && <Alert type={'info'} message={tips} />}
<Space wrap={true} style={{ marginTop: 8 }}>
{orderShipList.map((item: BusinessAPI.OrderShipVO) => {
return (
<Tag
style={{
cursor: 'pointer',
}}
onClick={() => {
// 直接使用 localStorage 中保存的数据
onFinish([item]);
setOrderShipVOStorage(item);
}}
key={item.orderShipId}
>
{item.orderSn}
</Tag>
);
})}
</Space>
</>
);
}
},
}),
actionRef: actionRef,
toolbar: {
filter: (
<LightFilter
onFinish={async (values) => {
setParams({
...initParams,
...values,
});
}}
>
{extraFilter}
<ProFormSelect
label={'发货单状态'}
name={'state'}
placeholder={'请选择发货单状态'}
valueEnum={{
DRAFT: intl.formatMessage({
id: intlPrefix + '.column.state.draft',
}),
WAIT_SHIPMENT: intl.formatMessage({
id: intlPrefix + '.column.state.wait_shipment',
}),
WAIT_PAYMENT: intl.formatMessage({
id: intlPrefix + '.column.state.wait_payment',
}),
PARTIAL_PAYMENT: intl.formatMessage({
id: intlPrefix + '.column.state.partial_payment',
}),
FULL_PAYMENT: intl.formatMessage({
id: intlPrefix + '.column.state.full_payment',
}),
FINISH: intl.formatMessage({
id: intlPrefix + '.column.state.finish',
}),
}}
fieldProps={{
showSearch: true,
allowClear: true,
autoClearSearchValue: true,
}}
/>
</LightFilter>
),
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}
/>
);
}

View File

@ -63,8 +63,8 @@ export default function OrderSupplierModal(props: IOrderSupplierModalProps) {
const columns: ProColumns<BusinessAPI.OrderSupplierVO>[] = [ const columns: ProColumns<BusinessAPI.OrderSupplierVO>[] = [
{ {
title: intl.formatMessage({ id: intlPrefix + '.column.order' }), title: intl.formatMessage({ id: intlPrefix + '.column.order' }),
dataIndex: ['orderVO', 'orderSn'], dataIndex: 'orderVO',
key: 'orderSn', key: 'orderId',
search: false, search: false,
render: (_, orderSupplierVO: BusinessAPI.OrderSupplierVO) => { render: (_, orderSupplierVO: BusinessAPI.OrderSupplierVO) => {
const orderVO = orderSupplierVO.orderVO; const orderVO = orderSupplierVO.orderVO;
@ -77,7 +77,7 @@ export default function OrderSupplierModal(props: IOrderSupplierModalProps) {
trigger={() => ( trigger={() => (
<Space> <Space>
<a> <a>
{`${orderVO.orderVehicle?.dealerName} -${orderVO.orderVehicle?.vehicleNo || '暂无'}- ${orderVO.orderSn || '暂无'}`} {`${orderVO.orderVehicle?.dealerName} |${orderVO.orderVehicle?.vehicleNo || '暂无'}| ${orderVO.orderSn || '暂无'}`}
</a> </a>
</Space> </Space>
)} )}

View File

@ -8,5 +8,9 @@ export type { IOrderModalProps } from './OrderModal';
export { default as OrderRebateList } from './OrderRebateList'; export { default as OrderRebateList } from './OrderRebateList';
export { default as OrderSearch } from './OrderSearch'; export { default as OrderSearch } from './OrderSearch';
export { default as OrderSelect } from './OrderSelect'; 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 OrderStallList } from './OrderStallList';
export { default as OrderSupplierList } from './OrderSupplierList'; export { default as OrderSupplierList } from './OrderSupplierList';
export { default as OrderSupplierModal } from './OrderSupplierModal';

View File

@ -23,7 +23,7 @@ export default function OrderSupplierInvoiceList(
trigger={() => ( trigger={() => (
<Space> <Space>
<a> <a>
{`${orderVO.orderVehicle?.dealerName} -${orderVO.orderVehicle?.vehicleNo || '暂无'}- ${orderVO.orderSn || '暂无'}`} {`${orderVO.orderVehicle?.dealerName} |${orderVO.orderVehicle?.vehicleNo || '暂无'}| ${orderVO.orderSn || '暂无'}`}
</a> </a>
</Space> </Space>
)} )}

View File

@ -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<string, FrequentMenuItem> => {
try {
const data = localStorage.getItem(STORAGE_KEY);
return data ? JSON.parse(data) : {};
} catch {
return {};
}
};
/** 保存常用菜单到本地 */
const saveFrequentMenus = (menus: Record<string, FrequentMenuItem>) => {
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<SearchMenuProps> = ({ 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: (
<div
className={styles.searchItem}
onClick={() => handleMenuClick(item.path!, item.name)}
>
<span className={styles.menuName}>{item.name}</span>
<span className={styles.menuPath}>{item.path}</span>
</div>
),
}));
}
// 如果没有搜索关键词,显示常用菜单
if (frequentMenus.length > 0) {
return [
{
value: 'frequent-header',
label: (
<div className={styles.sectionHeader}>
<StarOutlined className={styles.sectionIcon} />
<span></span>
</div>
),
disabled: true,
},
...frequentMenus.map((item) => ({
value: item.path,
label: (
<div
className={styles.searchItem}
onClick={() => handleMenuClick(item.path!, item.name)}
>
<div className={styles.menuRow}>
<span className={styles.menuName}>{item.name}</span>
<span className={styles.frequency}>{item.frequency} </span>
</div>
<span className={styles.menuPath}>{item.path}</span>
</div>
),
})),
];
}
// 没有常用菜单时显示提示
return [
{
value: 'empty-header',
label: (
<div className={styles.sectionHeader}>
<ClockCircleOutlined className={styles.sectionIcon} />
<span></span>
</div>
),
disabled: true,
},
{
value: 'empty-tip',
label: (
<div className={styles.emptyTip}>使</div>
),
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 (
<div className={styles.searchMenuContainer}>
<AutoComplete
open={open}
onDropdownVisibleChange={handleDropdownOpen}
onSearch={handleSearch}
onSelect={handleSelect}
options={options}
dropdownClassName={styles.dropdown}
dropdownMatchSelectWidth={320}
backfill
>
<Input
placeholder="搜索菜单..."
prefix={<SearchOutlined className={styles.searchIcon} />}
allowClear
className={styles.searchInput}
onClick={() => setOpen(true)}
/>
</AutoComplete>
</div>
);
};
export default SearchMenu;

View File

@ -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;

View File

@ -137,7 +137,7 @@ export default function SupplierInvoiceList(props: ISupplierInvoiceListProps) {
trigger={() => ( trigger={() => (
<Space> <Space>
<a> <a>
{`${item?.dealerName} -${item?.vehicleNo || '暂无'}`} {`${item?.dealerName} |${item?.vehicleNo || '暂无'}`}
</a> </a>
</Space> </Space>
)} )}

View File

@ -11,7 +11,6 @@ export { default as CaptchaModal } from './CaptchaModal';
export * from './Channel'; export * from './Channel';
export * from './Company'; export * from './Company';
export * from './Dealer'; export * from './Dealer';
export * from './Delivery';
export * from './Editor'; export * from './Editor';
export * from './Employee'; export * from './Employee';
export * from './Expense'; export * from './Expense';
@ -30,6 +29,7 @@ export * from './Permission';
export * from './Platform'; export * from './Platform';
export * from './Remark'; export * from './Remark';
export * from './Role'; export * from './Role';
export { default as SearchMenu } from './SearchMenu';
export * from './Setting'; export * from './Setting';
export * from './Supplier'; export * from './Supplier';
export { default as UploadMaterial } from './UploadMaterial'; export { default as UploadMaterial } from './UploadMaterial';

View File

@ -2110,17 +2110,27 @@ export default {
}, },
column: { column: {
orderSn: '发货单编号', orderSn: '发货单编号',
dealerName: '经销商名称', order: '采购单',
vehicleNo: '车次号', dealer: '经销商',
shippingAddress: '发货地',
receivingAddress: '收货地',
shippingDate: '发货日期', shippingDate: '发货日期',
estimatedArrivalDate: '预计到仓时间',
watermelonGrade: '西瓜品级',
company: '入账公司',
type: '发货单类型',
'type.enum.purchaseShip': '采购发货单',
'type.enum.transferShip': '调货发货',
'type.enum.changeShip': '改签发货',
'type.enum.returnShip': '退货发货',
state: '状态', state: '状态',
'state.draft': '草稿', 'state.enum.draft': '草稿',
'state.wait_shipment': '待发货', 'state.enum.waitShipment': '待发货',
'state.wait_payment': '待回款', 'state.enum.waitPayment': '待回款',
'state.partial_payment': '部分回款', 'state.enum.partialPayment': '部分回款',
'state.full_payment': '已回款', 'state.enum.fullPayment': '已回款',
'state.reject_finish': '拒收完结', 'state.enum.rejectFinish': '拒收完结',
'state.finish': '已完结', 'state.enum.finish': '已完结',
createdAt: '创建时间', createdAt: '创建时间',
option: '操作', option: '操作',
}, },

View File

@ -0,0 +1,5 @@
import { OrderShipList } from '@/components';
export default function Page() {
return <OrderShipList />;
}

View File

@ -1,91 +1,394 @@
import { import { DealerSelect, PageContainer } from '@/components';
DealerSelect, import OrderShipModal from '@/components/Order/OrderShipModal';
PageContainer, import { formatCurrency } from '@/utils/format';
OrderList,
ShipOrderList,
} from '@/components';
import { import {
ProCard, ProCard,
ProForm, ProForm,
ProFormDependency, ProFormDependency,
ProFormTextArea,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { Space, Steps } from 'antd'; import { Button, Col, Row, Space, Table, Tag } from 'antd';
import { useState } from 'react'; import { useState } from 'react';
export default function Page() { export default function Page() {
const [current, setCurrent] = useState(0); const [current, setCurrent] = useState(0);
const onChange = (value: number) => { const [selectedDealer, setSelectedDealer] =
console.log('onChange:', value); useState<BusinessAPI.DealerVO | null>(null);
setCurrent(value); const [selectedShipOrderList, setSelectedShipOrderList] = useState<
}; BusinessAPI.OrderShipVO[]
>([]);
const [orderShipModalOpen, setOrderShipModalOpen] = useState(false);
const [dealerVO, setDealerVO] = useState<BusinessAPI.DealerVO>(); // 计算总金额
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 ( return (
<PageContainer <PageContainer
permission={''} permission={''}
fieldProps={{ fieldProps={{
content: ( content: (
<Steps <ProCard>
current={current} <Space
onChange={onChange} style={{ width: '100%', justifyContent: 'center' }}
items={[ size={16}
{ >
title: '步骤一', <Space
subTitle: '选择客户与车次', size={4}
}, style={{
{ color: current === 0 ? '#1890ff' : '#999',
title: '步骤二', }}
subTitle: '核对并录入调整项', >
}, <span
]} style={{
></Steps> width: 24,
height: 24,
borderRadius: '50%',
background: current === 0 ? '#1890ff' : '#f0f0f0',
color: current === 0 ? '#fff' : '#999',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
1
</span>
<span style={{ fontWeight: current === 0 ? 500 : 400 }}>
</span>
</Space>
<span
style={{
color: current === 1 ? '#1890ff' : '#d9d9d9',
}}
>
</span>
<Space
size={4}
style={{
color: current === 1 ? '#1890ff' : '#999',
}}
>
<span
style={{
width: 24,
height: 24,
borderRadius: '50%',
background: current === 1 ? '#1890ff' : '#f0f0f0',
color: current === 1 ? '#fff' : '#999',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
2
</span>
<span style={{ fontWeight: current === 1 ? 500 : 400 }}>
</span>
</Space>
</Space>
</ProCard>
), ),
}} }}
> >
{current === 0 && ( {/* 对账单表单 */}
<ProCard <ProCard
title={ title={'创建对账单'}
<Space> headerBordered
subTitle={
<ProForm <Space>
submitter={false} <Tag color="blue"> {current + 1}</Tag>
onFinish={(formData) => { {current === 0 ? '选择经销商与车次' : '核对并录入调整项'}
setDealerVO(formData.dealerVO); </Space>
}
>
<ProForm submitter={false}>
<ProFormDependency name={['dealerId', 'dealerVO']}>
{(_, form) => (
<DealerSelect
noStyle={true}
key={'dealerId'}
onFinish={(dealerVOList: BusinessAPI.DealerVO[]) => {
setSelectedDealer(dealerVOList[0]);
// 切换经销商时清空已选车次
setSelectedShipOrderList([]);
form.submit();
}} }}
> />
<ProFormDependency name={['dealerId', 'dealerVO']}> )}
{(_, form) => ( </ProFormDependency>
<DealerSelect </ProForm>
noStyle={true} {current === 0 && selectedDealer && (
key={'dealerId'} <>
onFinish={() => { {/* 经销商信息 */}
form.submit(); <ProCard title={'经销商信息'} style={{ marginTop: 16 }} bordered>
}} <Row gutter={[16, 16]}>
/> <Col span={8}>
)} <div>
</ProFormDependency> <div style={{ color: '#999', marginBottom: 4 }}>
</ProForm>
</Space> </div>
} <div style={{ fontWeight: 'bold' }}>
headerBordered={true} {selectedDealer.shortName || '-'}
direction={'column'} </div>
> </div>
{dealerVO && ( </Col>
<ShipOrderList <Col span={8}>
ghost={true} <div>
mode={'page'} <div style={{ color: '#999', marginBottom: 4 }}>
params={{
dealerId: dealerVO.dealerId, </div>
}} <div style={{ fontWeight: 'bold' }}>
/> {selectedDealer.fullName || '-'}
)} </div>
</ProCard> </div>
)} </Col>
{current === 1 && ( <Col span={8}>
<ProCard title={'核对并录入调整项'} headerBordered={true}></ProCard> <div>
)} <div style={{ color: '#999', marginBottom: 4 }}>
</div>
<div style={{ fontWeight: 'bold' }}>
{selectedDealer.dealerType === 'MARKET' ? '市场' : '超市'}
</div>
</div>
</Col>
</Row>
</ProCard>
{/* 选择车次 */}
<ProCard
title={'选择车次'}
style={{ marginTop: 16 }}
bordered
extra={
<Button
type="primary"
size="middle"
onClick={() => setOrderShipModalOpen(true)}
>
</Button>
}
>
{selectedShipOrderList.length > 0 ? (
<Table<BusinessAPI.OrderShipVO>
rowKey="orderShipId"
dataSource={selectedShipOrderList}
columns={[
{
title: '发货单号',
dataIndex: 'orderSn',
key: 'orderSn',
render: (text: string) => (
<span className="font-medium">{text}</span>
),
},
{
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 <span>{formatCurrency(total)}</span>;
},
},
{
title: '操作',
key: 'action',
render: (_, record: BusinessAPI.OrderShipVO) => (
<Button
type="link"
danger
onClick={() => {
setSelectedShipOrderList(
selectedShipOrderList.filter(
(item) =>
item.orderShipId !== record.orderShipId,
),
);
}}
>
</Button>
),
},
]}
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 (
<Table.Summary fixed>
<Table.Summary.Row>
<Table.Summary.Cell index={0} colSpan={3}>
<strong></strong>
</Table.Summary.Cell>
<Table.Summary.Cell index={1}>
<strong style={{ color: '#ff4d4f' }}>
{formatCurrency(totalAmount)}
</strong>
</Table.Summary.Cell>
<Table.Summary.Cell index={2} />
</Table.Summary.Row>
</Table.Summary>
);
}}
/>
) : (
<div
style={{
textAlign: 'center',
padding: '40px 0',
color: '#999',
}}
>
</div>
)}
</ProCard>
{/* 任务摘要 */}
{selectedShipOrderList.length > 0 && (
<ProCard title={'对账摘要'} style={{ marginTop: 16 }} bordered>
<Row gutter={[16, 16]}>
<Col span={6}>
<div>
<div style={{ color: '#999', marginBottom: 4 }}>
</div>
<div style={{ fontWeight: 'bold' }}>
{selectedDealer?.fullName || selectedDealer?.shortName}
</div>
</div>
</Col>
<Col span={6}>
<div>
<div style={{ color: '#999', marginBottom: 4 }}>
</div>
<div style={{ fontWeight: 'bold' }}>{orderCount} </div>
</div>
</Col>
<Col span={6}>
<div>
<div style={{ color: '#999', marginBottom: 4 }}>
</div>
<div style={{ fontWeight: 'bold', color: '#ff4d4f' }}>
{formatCurrency(totalAmount)}
</div>
</div>
</Col>
<Col span={6}>
<div>
<div style={{ color: '#999', marginBottom: 4 }}>
</div>
<div>
<Tag color="orange"></Tag>
</div>
</div>
</Col>
</Row>
</ProCard>
)}
{/* 下一步按钮 */}
{selectedShipOrderList.length > 0 && (
<div style={{ marginTop: 16, textAlign: 'right' }}>
<Button
type="primary"
onClick={() => {
setCurrent(1);
}}
>
</Button>
</div>
)}
</>
)}
{current === 0 && (
<>
<ProCard
title={'核对并录入调整项'}
style={{ marginTop: 16 }}
bordered
>
<ProFormTextArea
name="remark"
label="备注"
placeholder="请输入备注信息"
/>
</ProCard>
</>
)}
</ProCard>
{/* 选择发货单模态框 */}
<OrderShipModal
open={orderShipModalOpen}
title="选择需要对账的发货单"
type="checkbox"
selectedList={selectedShipOrderList}
onFinish={(orderShipList) => {
// 过滤掉已经选择的发货单
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}
/>
</PageContainer> </PageContainer>
); );
} }

View File

@ -1,5 +0,0 @@
import { ShipOrderList } from '@/components';
export default function Page() {
return <ShipOrderList />;
}

View File

@ -100,7 +100,7 @@ declare namespace AuthAPI {
/** 用户ID */ /** 用户ID */
userId: string; userId: string;
/** 角色ID */ /** 角色ID */
roleIdList: number[]; roleIdList: string[];
/** 角色信息 */ /** 角色信息 */
userRoleList?: UserRoleVO[]; userRoleList?: UserRoleVO[];
}; };
@ -195,7 +195,7 @@ declare namespace AuthAPI {
type RoleMenuTreeQry = { type RoleMenuTreeQry = {
/** 角色权限 */ /** 角色权限 */
roleId?: number[]; roleId?: string[];
/** 平台ID */ /** 平台ID */
platformId: string; platformId: string;
}; };
@ -329,7 +329,7 @@ declare namespace AuthAPI {
/** 备注 */ /** 备注 */
remark?: string; remark?: string;
/** 客户标签 */ /** 客户标签 */
labelId?: number[]; labelId?: string[];
/** 用户ID */ /** 用户ID */
userId: string; userId: string;
}; };

View File

@ -208,7 +208,7 @@ declare namespace BusinessAPI {
/** 品牌图片URL */ /** 品牌图片URL */
image?: string; image?: string;
/** 纸箱规格ID */ /** 纸箱规格ID */
specIds?: number[]; specIds?: string[];
/** 备注 */ /** 备注 */
remark?: string; remark?: string;
/** 状态1_启用0_禁用 */ /** 状态1_启用0_禁用 */
@ -279,7 +279,7 @@ declare namespace BusinessAPI {
/** 品牌图片URL */ /** 品牌图片URL */
image?: string; image?: string;
/** 纸箱规格ID */ /** 纸箱规格ID */
specIds?: number[]; specIds?: string[];
/** 备注 */ /** 备注 */
remark?: string; remark?: string;
/** 状态1_启用0_禁用 */ /** 状态1_启用0_禁用 */
@ -296,7 +296,7 @@ declare namespace BusinessAPI {
/** 品牌图片URL */ /** 品牌图片URL */
image?: string; image?: string;
/** 纸箱规格ID */ /** 纸箱规格ID */
specIds?: number[]; specIds?: string[];
/** 备注 */ /** 备注 */
remark?: string; remark?: string;
/** 状态1_启用0_禁用 */ /** 状态1_启用0_禁用 */
@ -1021,7 +1021,7 @@ declare namespace BusinessAPI {
/** 状态1_启用0_禁用 */ /** 状态1_启用0_禁用 */
status: boolean; status: boolean;
/** 成本项ID */ /** 成本项ID */
costItemIds?: number[]; costItemIds?: string[];
}; };
type CostDestroyCmd = { type CostDestroyCmd = {
@ -1249,7 +1249,7 @@ declare namespace BusinessAPI {
/** 状态1_启用0_禁用 */ /** 状态1_启用0_禁用 */
status: boolean; status: boolean;
/** 成本项ID */ /** 成本项ID */
costItemIds?: number[]; costItemIds?: string[];
}; };
type CostVO = { type CostVO = {
@ -1280,7 +1280,7 @@ declare namespace BusinessAPI {
/** 状态1_启用0_禁用 */ /** 状态1_启用0_禁用 */
status: boolean; status: boolean;
/** 项目id集合 */ /** 项目id集合 */
costItemIds?: number[]; costItemIds?: string[];
/** 创建时间 */ /** 创建时间 */
createdAt?: string; createdAt?: string;
/** 项目列表 */ /** 项目列表 */
@ -1961,7 +1961,7 @@ declare namespace BusinessAPI {
/** 登录密码 */ /** 登录密码 */
password: string; password: string;
/** 角色ID */ /** 角色ID */
roleId: number[]; roleId: string[];
}; };
type EmployeeDestroyCmd = { type EmployeeDestroyCmd = {
@ -2054,7 +2054,7 @@ declare namespace BusinessAPI {
/** 用户ID */ /** 用户ID */
userId: string; userId: string;
/** 角色ID */ /** 角色ID */
roleIdList: number[]; roleIdList: string[];
/** 角色信息 */ /** 角色信息 */
userRoleList?: UserRoleVO[]; userRoleList?: UserRoleVO[];
}; };
@ -2487,7 +2487,7 @@ declare namespace BusinessAPI {
/** 平台id */ /** 平台id */
platformId: string; platformId: string;
/** 角色Id */ /** 角色Id */
roleId?: number[]; roleId?: string[];
/** 是否隐藏 */ /** 是否隐藏 */
hideInMenu?: boolean; hideInMenu?: boolean;
/** 权限Id */ /** 权限Id */
@ -2560,7 +2560,7 @@ declare namespace BusinessAPI {
/** 平台id */ /** 平台id */
platformId: string; platformId: string;
/** 角色Id */ /** 角色Id */
roleId?: number[]; roleId?: string[];
/** 是否隐藏 */ /** 是否隐藏 */
hideInMenu?: boolean; hideInMenu?: boolean;
/** 权限Id */ /** 权限Id */
@ -2978,7 +2978,7 @@ declare namespace BusinessAPI {
| 'LOGISTICS_TYPE' | 'LOGISTICS_TYPE'
| 'EXPENSE_TYPE'; | 'EXPENSE_TYPE';
/** 关联项目id */ /** 关联项目id */
costItemIds?: number[]; costItemIds?: string[];
/** 是否选中 */ /** 是否选中 */
selected: boolean; selected: boolean;
/** 是否已付款 */ /** 是否已付款 */
@ -3015,7 +3015,7 @@ declare namespace BusinessAPI {
| 'LOGISTICS_TYPE' | 'LOGISTICS_TYPE'
| 'EXPENSE_TYPE'; | 'EXPENSE_TYPE';
/** 关联项目id */ /** 关联项目id */
costItemIds?: number[]; costItemIds?: string[];
/** 是否付款 */ /** 是否付款 */
isPaid?: boolean; isPaid?: boolean;
}; };
@ -3148,7 +3148,7 @@ declare namespace BusinessAPI {
| 'LOGISTICS_TYPE' | 'LOGISTICS_TYPE'
| 'EXPENSE_TYPE'; | 'EXPENSE_TYPE';
/** 关联项目id */ /** 关联项目id */
costItemIds?: number[]; costItemIds?: string[];
/** 创建时间 */ /** 创建时间 */
createdAt: string; createdAt: string;
/** 采购订单状态: 0_草稿1_审核中2_已完成3_已关闭 */ /** 采购订单状态: 0_草稿1_审核中2_已完成3_已关闭 */
@ -3870,7 +3870,7 @@ declare namespace BusinessAPI {
/** 产品名称 */ /** 产品名称 */
productName?: string; productName?: string;
/** 关联费用id */ /** 关联费用id */
costIds?: number[]; costIds?: string[];
/** 成本模板 */ /** 成本模板 */
costTemplate?: string; costTemplate?: string;
/** 是否已付定金 */ /** 是否已付定金 */
@ -5180,7 +5180,7 @@ declare namespace BusinessAPI {
/** 产品名称 */ /** 产品名称 */
name: string; name: string;
/** 关联成本费用id */ /** 关联成本费用id */
costIds?: number[]; costIds?: string[];
/** 成本模板 */ /** 成本模板 */
costTemplate?: string; costTemplate?: string;
/** 备注 */ /** 备注 */
@ -5247,7 +5247,7 @@ declare namespace BusinessAPI {
/** 产品名称 */ /** 产品名称 */
name: string; name: string;
/** 关联成本费用id */ /** 关联成本费用id */
costIds?: number[]; costIds?: string[];
/** 成本模板 */ /** 成本模板 */
costTemplate?: string; costTemplate?: string;
/** 备注 */ /** 备注 */
@ -5272,7 +5272,7 @@ declare namespace BusinessAPI {
/** 状态1_启用0_禁用 */ /** 状态1_启用0_禁用 */
status: boolean; status: boolean;
/** 成本ID集合 */ /** 成本ID集合 */
costIds?: number[]; costIds?: string[];
/** 成本费用 */ /** 成本费用 */
costVOList?: CostVO[]; costVOList?: CostVO[];
/** 成本模板 */ /** 成本模板 */
@ -5299,7 +5299,7 @@ declare namespace BusinessAPI {
/** 角色详情 */ /** 角色详情 */
description?: string; description?: string;
/** 角色id */ /** 角色id */
menuId: number[]; menuId: string[];
}; };
type RoleDestroyCmd = { type RoleDestroyCmd = {
@ -5321,7 +5321,7 @@ declare namespace BusinessAPI {
/** 角色编号 */ /** 角色编号 */
roleId?: string; roleId?: string;
/** 应用角色Id */ /** 应用角色Id */
roleIdList?: number[]; roleIdList?: string[];
/** 平台Id */ /** 平台Id */
platformId?: string; platformId?: string;
/** 平台Id */ /** 平台Id */
@ -5368,7 +5368,7 @@ declare namespace BusinessAPI {
/** 角色详情 */ /** 角色详情 */
description?: string; description?: string;
/** 角色id */ /** 角色id */
menuId: number[]; menuId: string[];
/** 角色ID */ /** 角色ID */
roleId: string; roleId: string;
}; };
@ -5389,9 +5389,9 @@ declare namespace BusinessAPI {
/** 平台 */ /** 平台 */
platformVO?: PlatformVO; platformVO?: PlatformVO;
/** 权限列表 */ /** 权限列表 */
permissionId: number[]; permissionId: string[];
/** 菜单列表 */ /** 菜单列表 */
menuId: number[]; menuId: string[];
/** 创建时间 */ /** 创建时间 */
createdAt: string; createdAt: string;
}; };
@ -6265,7 +6265,7 @@ declare namespace BusinessAPI {
/** 备注 */ /** 备注 */
remark?: string; remark?: string;
/** 客户标签 */ /** 客户标签 */
labelId?: number[]; labelId?: string[];
}; };
type UserDestroyCmd = { type UserDestroyCmd = {
@ -6287,7 +6287,7 @@ declare namespace BusinessAPI {
/** 状态1_启用0_禁用 */ /** 状态1_启用0_禁用 */
status?: boolean; status?: boolean;
/** 用户ID */ /** 用户ID */
userIdList?: number[]; userIdList?: string[];
/** 用户名 */ /** 用户名 */
name?: string; name?: string;
}; };
@ -6330,9 +6330,9 @@ declare namespace BusinessAPI {
/** 是否是管理员 */ /** 是否是管理员 */
isAdmin?: boolean; isAdmin?: boolean;
/** 会员id列表 */ /** 会员id列表 */
userIdList?: number[]; userIdList?: string[];
/** 排除的用户id列表 */ /** 排除的用户id列表 */
excludeUserIdList?: number[]; excludeUserIdList?: string[];
/** 小区id */ /** 小区id */
communityId?: number; communityId?: number;
offset?: number; offset?: number;
@ -6342,7 +6342,7 @@ declare namespace BusinessAPI {
/** 用户ID */ /** 用户ID */
userId: string; userId: string;
/** 角色ID */ /** 角色ID */
roleIdList?: number[]; roleIdList?: string[];
/** 是否覆盖 */ /** 是否覆盖 */
cover: boolean; cover: boolean;
}; };
@ -6387,7 +6387,7 @@ declare namespace BusinessAPI {
/** 备注 */ /** 备注 */
remark?: string; remark?: string;
/** 客户标签 */ /** 客户标签 */
labelId?: number[]; labelId?: string[];
/** 用户ID */ /** 用户ID */
userId: string; userId: string;
}; };

View File

@ -1,3 +1,2 @@
packages: packages:
- 'packages/**' - 'packages/**'
- 'shared/**'