refactor(order): 重构发货单功能模块
- 将 ShipOrderList 组件从 Delivery 模块迁移至 Order 模块 - 移除 Delivery 模块的导出并在 Order 模块中新增 OrderShipList 和相关组件 - 更新发货单列表的列配置,添加订单、经销商、公司等关联字段 - 修改发货单列表的字段映射,将经销商名称和车次号格式从短横线改为竖线分隔 - 在 BizPage 组件中添加 convertValue 属性支持数据转换 - 更新左侧菜单组件的依赖数组以包含 menuData - 调整发货单列表的国际化配置,更新字段标题和状态枚举值 - 新增 OrderShip 页面路由文件 - 移除项目根目录的 AGENTS.md 和 project.md 文件 - 从组件索引中移除 Delivery 模块并添加 SearchMenu 组件
This commit is contained in:
parent
f68b148319
commit
b4ce6e45dc
18
AGENTS.md
18
AGENTS.md
@ -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
332
CLAUDE.md
@ -1,18 +1,324 @@
|
||||
<!-- OPENSPEC:START -->
|
||||
# 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.
|
||||
## 项目结构
|
||||
|
||||
<!-- 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
|
||||
|
||||
@ -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 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 <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 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/<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.
|
||||
@ -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
|
||||
@ -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 ? (
|
||||
// <SearchInput key={'SearchInput'} />
|
||||
// ) : undefined,
|
||||
// <InfoCircleFilled key="InfoCircleFilled" />,
|
||||
// <QuestionCircleFilled key="QuestionCircleFilled" />,
|
||||
// <NotificationFilled key="NotificationFilled" />,
|
||||
// ];
|
||||
// },
|
||||
actionsRender: (props) => {
|
||||
if (props.isMobile) return [];
|
||||
if (typeof window === 'undefined') return [];
|
||||
return [
|
||||
props.layout !== 'side' && document.body.clientWidth > 1400 ? (
|
||||
<SearchMenu key={'SearchMenu'} menuData={props.menuData} />
|
||||
) : undefined,
|
||||
<InfoCircleFilled key="InfoCircleFilled" />,
|
||||
<QuestionCircleFilled key="QuestionCircleFilled" />,
|
||||
<NotificationFilled key="NotificationFilled" />,
|
||||
];
|
||||
},
|
||||
headerRender: (props, defaultDom) => {
|
||||
return (
|
||||
<>
|
||||
|
||||
@ -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={() => (
|
||||
<Space>
|
||||
<a>
|
||||
{`${orderVO.orderVehicle?.dealerName} - 第 ${orderVO.orderVehicle?.vehicleNo || '暂无'} 车 - ${orderVO.orderSn || '暂无'}`}
|
||||
{`${orderVO.orderVehicle?.dealerName} | 第 ${orderVO.orderVehicle?.vehicleNo || '暂无'} 车 | ${orderVO.orderSn || '暂无'}`}
|
||||
</a>
|
||||
</Space>
|
||||
)}
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -133,6 +133,7 @@ export type BizPageProps<
|
||||
) => React.ReactNode;
|
||||
fieldProps?: ProTableProps<BizVO, BizPageQry, BizValueType>;
|
||||
trigger?: () => React.ReactNode;
|
||||
convertValue?: (record: BizVO) => BizVO;
|
||||
} & ApiProps<Func>;
|
||||
|
||||
export interface BizTreeProps<
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export { default as ShipOrderList } from './ShipOrderList';
|
||||
@ -87,7 +87,7 @@ const LeftMenu: React.FC<ILeftMenuProps> = (props) => {
|
||||
);
|
||||
setActiveMenuKey(activeMenuKey);
|
||||
setOpenKeys([...openKeys, ...activeMenuKey]);
|
||||
}, [pathname]);
|
||||
}, [pathname, menuData]);
|
||||
|
||||
if (!menuData) {
|
||||
return null;
|
||||
|
||||
@ -120,7 +120,7 @@ export default function OrderCostPay(props: IOrderCostPayProps) {
|
||||
</div>
|
||||
<div style={{ fontWeight: 'bold' }}>
|
||||
{orderCostVO.orderVO
|
||||
? `${orderCostVO.orderVO.orderVehicle.dealerName} - 第 ${orderCostVO.orderVO.orderVehicle.vehicleNo || '暂无'} 车`
|
||||
? `${orderCostVO.orderVO.orderVehicle.dealerName} | 第 ${orderCostVO.orderVO.orderVehicle.vehicleNo || '暂无'} 车`
|
||||
: '-'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -56,7 +56,7 @@ export default function OrderList(props: IOrderListProps) {
|
||||
trigger={() => (
|
||||
<Space>
|
||||
<a>
|
||||
{`${orderVO.orderVehicle?.dealerName} - 第 ${orderVO.orderVehicle?.vehicleNo || '暂无'} 车 - ${orderVO.orderSn || '暂无'}`}
|
||||
{`${orderVO.orderVehicle?.dealerName} | 第 ${orderVO.orderVehicle?.vehicleNo || '暂无'} 车 | ${orderVO.orderSn || '暂无'}`}
|
||||
</a>
|
||||
</Space>
|
||||
)}
|
||||
|
||||
@ -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={() => (
|
||||
<Space>
|
||||
<a>
|
||||
{`${orderVO.orderVehicle?.dealerName} - 第 ${orderVO.orderVehicle?.vehicleNo || '暂无'} 车 - ${orderVO.orderSn || '暂无'}`}
|
||||
{`${orderVO.orderVehicle?.dealerName} | 第 ${orderVO.orderVehicle?.vehicleNo || '暂无'} 车 | ${orderVO.orderSn || '暂无'}`}
|
||||
</a>
|
||||
</Space>
|
||||
)}
|
||||
|
||||
@ -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<string>('ALL');
|
||||
|
||||
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' }),
|
||||
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.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 <Tag color={color}>{stateText}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
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}
|
||||
377
packages/app-operation/src/components/Order/OrderShipModal.tsx
Normal file
377
packages/app-operation/src/components/Order/OrderShipModal.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -63,8 +63,8 @@ export default function OrderSupplierModal(props: IOrderSupplierModalProps) {
|
||||
const columns: ProColumns<BusinessAPI.OrderSupplierVO>[] = [
|
||||
{
|
||||
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={() => (
|
||||
<Space>
|
||||
<a>
|
||||
{`${orderVO.orderVehicle?.dealerName} - 第 ${orderVO.orderVehicle?.vehicleNo || '暂无'} 车 - ${orderVO.orderSn || '暂无'}`}
|
||||
{`${orderVO.orderVehicle?.dealerName} | 第 ${orderVO.orderVehicle?.vehicleNo || '暂无'} 车 | ${orderVO.orderSn || '暂无'}`}
|
||||
</a>
|
||||
</Space>
|
||||
)}
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -23,7 +23,7 @@ export default function OrderSupplierInvoiceList(
|
||||
trigger={() => (
|
||||
<Space>
|
||||
<a>
|
||||
{`${orderVO.orderVehicle?.dealerName} - 第 ${orderVO.orderVehicle?.vehicleNo || '暂无'} 车 - ${orderVO.orderSn || '暂无'}`}
|
||||
{`${orderVO.orderVehicle?.dealerName} | 第 ${orderVO.orderVehicle?.vehicleNo || '暂无'} 车 | ${orderVO.orderSn || '暂无'}`}
|
||||
</a>
|
||||
</Space>
|
||||
)}
|
||||
|
||||
267
packages/app-operation/src/components/SearchMenu/index.tsx
Normal file
267
packages/app-operation/src/components/SearchMenu/index.tsx
Normal 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;
|
||||
100
packages/app-operation/src/components/SearchMenu/style.style.ts
Normal file
100
packages/app-operation/src/components/SearchMenu/style.style.ts
Normal 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;
|
||||
@ -137,7 +137,7 @@ export default function SupplierInvoiceList(props: ISupplierInvoiceListProps) {
|
||||
trigger={() => (
|
||||
<Space>
|
||||
<a>
|
||||
{`${item?.dealerName} - 第 ${item?.vehicleNo || '暂无'} 车`}
|
||||
{`${item?.dealerName} | 第 ${item?.vehicleNo || '暂无'} 车`}
|
||||
</a>
|
||||
</Space>
|
||||
)}
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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: '操作',
|
||||
},
|
||||
|
||||
5
packages/app-operation/src/pages/OrderShip.tsx
Normal file
5
packages/app-operation/src/pages/OrderShip.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import { OrderShipList } from '@/components';
|
||||
|
||||
export default function Page() {
|
||||
return <OrderShipList />;
|
||||
}
|
||||
@ -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<BusinessAPI.DealerVO | null>(null);
|
||||
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 (
|
||||
<PageContainer
|
||||
permission={''}
|
||||
fieldProps={{
|
||||
content: (
|
||||
<Steps
|
||||
current={current}
|
||||
onChange={onChange}
|
||||
items={[
|
||||
{
|
||||
title: '步骤一',
|
||||
subTitle: '选择客户与车次',
|
||||
},
|
||||
{
|
||||
title: '步骤二',
|
||||
subTitle: '核对并录入调整项',
|
||||
},
|
||||
]}
|
||||
></Steps>
|
||||
<ProCard>
|
||||
<Space
|
||||
style={{ width: '100%', justifyContent: 'center' }}
|
||||
size={16}
|
||||
>
|
||||
<Space
|
||||
size={4}
|
||||
style={{
|
||||
color: current === 0 ? '#1890ff' : '#999',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
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
|
||||
title={
|
||||
<Space>
|
||||
选择客户与车次
|
||||
<ProForm
|
||||
submitter={false}
|
||||
onFinish={(formData) => {
|
||||
setDealerVO(formData.dealerVO);
|
||||
{/* 对账单表单 */}
|
||||
<ProCard
|
||||
title={'创建对账单'}
|
||||
headerBordered
|
||||
subTitle={
|
||||
<Space>
|
||||
<Tag color="blue">步骤 {current + 1}</Tag>
|
||||
{current === 0 ? '选择经销商与车次' : '核对并录入调整项'}
|
||||
</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) => (
|
||||
<DealerSelect
|
||||
noStyle={true}
|
||||
key={'dealerId'}
|
||||
onFinish={() => {
|
||||
form.submit();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ProFormDependency>
|
||||
</ProForm>
|
||||
</Space>
|
||||
}
|
||||
headerBordered={true}
|
||||
direction={'column'}
|
||||
>
|
||||
{dealerVO && (
|
||||
<ShipOrderList
|
||||
ghost={true}
|
||||
mode={'page'}
|
||||
params={{
|
||||
dealerId: dealerVO.dealerId,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ProCard>
|
||||
)}
|
||||
{current === 1 && (
|
||||
<ProCard title={'核对并录入调整项'} headerBordered={true}></ProCard>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</ProFormDependency>
|
||||
</ProForm>
|
||||
{current === 0 && selectedDealer && (
|
||||
<>
|
||||
{/* 经销商信息 */}
|
||||
<ProCard title={'经销商信息'} style={{ marginTop: 16 }} bordered>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={8}>
|
||||
<div>
|
||||
<div style={{ color: '#999', marginBottom: 4 }}>
|
||||
经销商简称
|
||||
</div>
|
||||
<div style={{ fontWeight: 'bold' }}>
|
||||
{selectedDealer.shortName || '-'}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<div>
|
||||
<div style={{ color: '#999', marginBottom: 4 }}>
|
||||
经销商全称
|
||||
</div>
|
||||
<div style={{ fontWeight: 'bold' }}>
|
||||
{selectedDealer.fullName || '-'}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
import { ShipOrderList } from '@/components';
|
||||
|
||||
export default function Page() {
|
||||
return <ShipOrderList />;
|
||||
}
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -1,3 +1,2 @@
|
||||
packages:
|
||||
- 'packages/**'
|
||||
- 'shared/**'
|
||||
|
||||
Loading…
Reference in New Issue
Block a user