feat(app-client): 重构采购审批页面并优化样式配置

- 重构采购审批页面,移除冗余的表单逻辑和校验代码
- 新增基础信息模块自动获取上一车次号功能
- 优化自定义主题配置,统一使用 Taro.pxTransform 处理单位
- 调整页面列表组件的数据加载逻辑,支持分页追加数据
- 优化成本相关组件的价格展示样式,统一字体大小和颜色
- 移除页面中冗余的状态管理和副作用逻辑
- 调整审批页面布局结构,提升用户体验
This commit is contained in:
shenyifei 2025-11-13 11:47:00 +08:00
parent bd4723b6ed
commit 9213b90d61
35 changed files with 1598 additions and 527 deletions

View File

@ -0,0 +1,141 @@
# API 接口实现规范
## 概述
本系统采用基于 axios 的 HTTP 客户端来与后端进行通信,所有 API 请求都经过统一的封装和管理。该文档旨在说明如何在系统中添加和使用新的 API 接口。
## 核心架构
### 1. 请求客户端 (request.ts)
系统在 `packages/app-client/src/services/request.ts` 文件中创建了一个全局的 axios 实例,用于处理所有 HTTP 请求。
```typescript
import axios from "axios";
import Taro from "@tarojs/taro";
const request = axios.create({
baseURL: process.env.TARO_API_DOMAIN,
});
```
该实例具有以下特性:
- 设置了基础 URL从环境变量 `TARO_API_DOMAIN` 获取
- 包含请求拦截器,自动添加认证信息(如 saToken 和角色信息)
- 包含响应拦截器,处理通用的响应逻辑(如权限验证、错误提示等)
### 2. 请求拦截器
在发送请求前,系统会自动添加以下头部信息:
- `saToken`: 用户的身份认证令牌,从本地存储中获取
- `Xh-Role-Slug`: 用户当前角色标识,从本地存储中获取
### 3. 响应拦截器
响应拦截器负责处理通用的业务逻辑:
- 成功响应时检查是否需要更新本地存储的认证信息
- 错误响应时根据错误码显示相应提示或执行特定操作(如 401 时跳转登录页)
## API 接口组织
### 1. 模块划分
API 接口按照业务模块进行组织,每个模块对应一个文件:
- 所有业务 API 存放在 `packages/app-client/src/services/business/` 目录下
- 每个文件代表一个业务模块(如 [platform.ts](file:///D:/xinfaleisheng/ERPTurbo_Client/packages/app-client/src/services/business/platform.ts)、[user.ts](file:///D:/xinfaleisheng/ERPTurbo_Client/packages/app-client/src/services/business/user.ts) 等)
- 每个模块导出一组相关的 API 函数
### 2. 接口定义格式
每个 API 函数遵循统一的格式:
```typescript
/** 接口说明 GET|POST|PUT|DELETE /api/path */
export async function apiFunctionName(
// 参数类型定义
params: BusinessAPI.paramsType,
options?: { [key: string]: any },
) {
return request<BusinessAPI.ResponseType>("/api/path", {
method: "GET|POST|PUT|DELETE",
// 根据请求方法设置 headers、params 或 data
...(options || {}),
});
}
```
### 3. 模块导出
所有业务模块在 `packages/app-client/src/services/business/index.ts` 中统一导出:
```typescript
import * as moduleName from "./moduleName";
export default {
moduleName,
// ...
};
```
## 添加新接口步骤
### 1. 确定业务模块
首先确定新接口属于哪个业务模块,如果现有模块都不合适,则需要创建新的模块文件。
### 2. 定义数据类型
`typings.d.ts` 文件中定义接口所需的请求参数类型和响应数据类型。
### 3. 编写接口函数
在对应的模块文件中添加新的接口函数,参考以下模板:
```typescript
/** 接口说明 动作 /api/path */
export async function functionName(
// 根据实际参数类型修改
body: BusinessAPI.RequestType,
options?: { [key: string]: any },
) {
// 根据实际响应类型修改
return request<BusinessAPI.ResponseType>("/api/path", {
method: "POST", // 根据实际情况修改为 POST/GET/PUT/DELETE
headers: {
"Content-Type": "application/json",
},
data: body, // GET 请求使用 params其他请求使用 data
...(options || {}),
});
}
```
### 4. 导出模块
确保在 `packages/app-client/src/services/business/index.ts` 中导出了包含新接口的模块。
## 使用示例
在组件或其他服务中使用 API 接口:
```typescript
import businessServices from "@/services/business";
// 调用平台列表接口
const {data: {data: platformList}} = await businessServices.platform.listPlatform({
// 参数
});
// 调用创建平台接口
const {data: {data: platform}} = await businessServices.platform.createPlatform({
// 数据体
});
```
## 最佳实践
1. **类型安全**: 始终使用 TypeScript 类型定义请求参数和响应数据
2. **错误处理**: 在调用 API 的地方适当处理可能发生的错误
3. **命名规范**: 接口函数名应该清晰表达其功能,通常采用动词+名词的形式
4. **注释说明**: 每个接口函数都需要添加注释说明接口用途和路径
5. **统一管理**: 所有 API 接口都应该按业务模块分类管理
6. **避免重复**: 相似的接口可以复用已有模式,保持一致性

View File

@ -0,0 +1,293 @@
---
trigger: manual
---
# Dialog 功能实现指南
## 概述
Dialog 是系统中用于显示重要信息或请求用户确认操作的模态对话框组件。它通常用于警告、确认操作或显示关键信息。本指南将详细介绍如何在系统中实现 Dialog 功能。
## 技术栈
系统使用的是基于 Taro 的 React 框架Dialog 组件来自 `@nutui/nutui-react-taro` 库。
## 基本用法
### 1. 导入 Dialog 组件
```tsx
import { Dialog } from "@nutui/nutui-react-taro";
```
### 2. 基本属性
Dialog 组件常用属性包括:
- `visible`: 控制 Dialog 是否显示boolean
- `title`: Dialog 标题
- `content`: Dialog 内容(可以是字符串或 JSX 元素)
- `onCancel`: 取消按钮的回调函数
- `onConfirm`: 确认按钮的回调函数
- `cancelText`: 取消按钮文本
- `confirmText`: 确认按钮文本
### 3. 基本结构
```tsx
<Dialog
visible={visible}
title="Dialog 标题"
content="Dialog 内容"
onCancel={() => setVisible(false)}
onConfirm={handleConfirm}
/>
```
## 实现方式
系统中有两种主要使用 Dialog 的方式:
### 1. JSX 组件形式
直接在 JSX 中使用 Dialog 组件:
```tsx
<Dialog
visible={inputVisible}
title="手动输入经销商"
content={
<View className={`flex h-10 w-full items-center rounded-md border-4 border-gray-300`}>
<Input
type="text"
placeholder="请输入经销商名称"
value={inputValue}
onChange={(value) => setInputValue(value)}
/>
</View>
}
onCancel={() => setInputVisible(false)}
onConfirm={handleConfirm}
/>
```
### 2. 函数调用形式
通过 Dialog 提供的静态方法调用:
```tsx
// 使用 Dialog.confirm
const onWithdraw = async () => {
Dialog.confirm({
title: "提示",
content: "确认撤回审核?",
onConfirm: async () => {
// 确认操作
},
onCancel: () => {
// 取消操作
},
});
};
// 使用 Dialog.open 和 Dialog.close
const handleApprove = () => {
Dialog.open("dialog", {
title: "审批通过",
content: "确定要审批通过该采购订单吗?",
confirmText: "确认",
cancelText: "取消",
onConfirm: async () => {
await confirmApprove();
Dialog.close("dialog");
},
onCancel: () => {
Dialog.close("dialog");
},
});
};
```
## 实现步骤
### 1. 状态管理
对于 JSX 组件形式的 Dialog需要定义控制 Dialog 显示/隐藏的状态:
```tsx
const [visible, setVisible] = useState(false);
```
### 2. 触发 Dialog 显示
通过事件触发 Dialog 显示,例如点击按钮:
```tsx
<Button onClick={() => setVisible(true)}>打开 Dialog</Button>
```
### 3. 处理确认和取消操作
实现 onConfirm 和 onCancel 回调函数:
```tsx
const handleConfirm = () => {
// 执行确认操作
setVisible(false);
};
const handleCancel = () => {
// 执行取消操作
setVisible(false);
};
```
### 4. 关闭 Dialog
Dialog 可以通过以下方式关闭:
- 点击取消按钮
- 点击确认按钮
- 在 onCancel 或 onConfirm 回调中手动设置 visible 为 false
- 使用 Dialog.close() 方法(函数调用形式)
## 常见使用场景
### 1. 确认操作
用于确认删除、撤回等重要操作:
```tsx
const handleDelete = () => {
Dialog.confirm({
title: "确认删除",
content: "确定要删除这条记录吗?",
onConfirm: async () => {
// 执行删除操作
},
});
};
```
### 2. 输入信息
用于请求用户输入信息:
```tsx
<Dialog
visible={inputVisible}
title="输入信息"
content={
<Input
placeholder="请输入信息"
value={inputValue}
onChange={(value) => setInputValue(value)}
/>
}
onCancel={() => setInputVisible(false)}
onConfirm={handleConfirm}
/>
```
### 3. 显示重要信息
用于显示重要提示或警告信息:
```tsx
const handleSubmit = async () => {
Dialog.open("submit-dialog", {
title: "提交提醒",
content: "提交后将无法修改,请确认信息无误。",
confirmText: "确认提交",
cancelText: "取消",
onConfirm: async () => {
// 执行提交操作
Dialog.close("submit-dialog");
},
onCancel: () => {
Dialog.close("submit-dialog");
},
});
};
```
## 最佳实践
### 1. 状态管理
- 使用 useState 管理 Dialog 的显示状态JSX 组件形式)
- 对于函数调用形式,使用唯一标识符管理不同的 Dialog 实例
### 2. 用户体验
- 提供明确的标题和内容说明
- 使用合适的确认和取消按钮文本
- 在操作完成后及时关闭 Dialog
### 3. 错误处理
- 在 onConfirm 回调中处理可能的错误
- 提供友好的错误提示信息
### 4. 样式统一
- 保持 Dialog 标题、按钮等样式与系统一致
- 使用系统定义的颜色和间距变量
## 注意事项
1. 确保在组件卸载时关闭 Dialog
2. 注意 Dialog 内容的可访问性
3. 避免同时显示多个 Dialog
4. 在异步操作中正确处理 Dialog 的关闭时机
5. 对于函数调用形式,记得在适当时机调用 Dialog.close()
## 示例代码
```tsx
import { Dialog, Button, View, Input } from "@nutui/nutui-react-taro";
import { useState } from "react";
export default function MyComponent() {
const [visible, setVisible] = useState(false);
const [inputValue, setInputValue] = useState("");
const handleConfirm = () => {
// 处理确认逻辑
console.log("用户输入:", inputValue);
setVisible(false);
setInputValue("");
};
const showConfirmDialog = () => {
Dialog.confirm({
title: "确认操作",
content: "确定要执行此操作吗?",
onConfirm: () => {
// 确认操作
console.log("用户确认操作");
},
});
};
return (
<View>
<Button onClick={() => setVisible(true)}>打开输入 Dialog</Button>
<Button onClick={showConfirmDialog}>显示确认 Dialog</Button>
<Dialog
visible={visible}
title="请输入信息"
content={
<Input
placeholder="请输入信息"
value={inputValue}
onChange={(value) => setInputValue(value)}
/>
}
onCancel={() => setVisible(false)}
onConfirm={handleConfirm}
/>
</View>
);
}
```

View File

@ -0,0 +1,210 @@
---
trigger: manual
---
# Popup 功能实现指南
## 概述
Popup 是系统中常用的 UI 组件,用于在当前页面上显示一个浮层,通常用于显示额外信息、表单或选择器等。本指南将详细介绍如何在系统中实现 Popup 功能。
## 技术栈
系统使用的是基于 Taro 的 React 框架Popup 组件来自 `@nutui/nutui-react-taro` 库。
## 基本用法
### 1. 导入 Popup 组件
```tsx
import { Popup } from "@nutui/nutui-react-taro";
```
### 2. 基本属性
Popup 组件常用属性包括:
- `visible`: 控制 Popup 是否显示boolean
- `position`: Popup 出现的位置("top" | "bottom" | "left" | "right" | "center"
- `title`: Popup 标题
- `onClose`: 关闭时的回调函数
- `onOverlayClick`: 点击遮罩层时的回调函数
- `closeable`: 是否显示关闭按钮
- `round`: 是否显示圆角
- `lockScroll`: 是否锁定背景滚动
### 3. 基本结构
```tsx
<Popup
visible={visible}
position="bottom"
title="Popup 标题"
onClose={() => setVisible(false)}
closeable
round
lockScroll
>
<View>Popup 内容</View>
</Popup>
```
## 实现步骤
### 1. 状态管理
在组件中定义控制 Popup 显示/隐藏的状态:
```tsx
const [visible, setVisible] = useState(false);
```
### 2. 触发 Popup 显示
通过事件触发 Popup 显示,例如点击按钮:
```tsx
<Button onClick={() => setVisible(true)}>打开 Popup</Button>
```
### 3. Popup 内容实现
Popup 内容根据具体需求实现,可以包含表单、列表、文本等。
### 4. 关闭 Popup
Popup 可以通过以下方式关闭:
- 点击关闭按钮(如果设置了 `closeable`
- 点击遮罩层(如果未阻止)
- 调用 `onClose` 回调函数
- 在 Popup 内部通过按钮等操作手动设置 visible 为 false
## 常见使用场景
### 1. 选择器
常用于实现各种选择器,如经销商选择、供应商选择等:
```tsx
<Popup
visible={visible}
position="bottom"
title="选择经销商"
onClose={() => setVisible(false)}
>
<View>经销商列表</View>
</Popup>
```
### 2. 表单编辑
用于编辑某一项信息:
```tsx
<Popup
visible={visible}
position="bottom"
title={`编辑${supplier.name}销售单价`}
onClose={() => setVisible(false)}
>
<View className="flex flex-col gap-3 p-2.5">
<Input
placeholder="请输入销售单价"
type="digit"
value={price}
onChange={(value) => setPrice(value)}
/>
<Button onClick={handleSave}>保存</Button>
</View>
</Popup>
```
### 3. 确认弹窗
用于确认操作:
```tsx
<Popup
visible={visible}
position="bottom"
title="确认操作"
onClose={() => setVisible(false)}
>
<View className="p-4">
<View className="mb-4">确定要执行此操作吗?</View>
<View className="flex gap-2">
<Button onClick={() => setVisible(false)}>取消</Button>
<Button onClick={handleConfirm}>确认</Button>
</View>
</View>
</Popup>
```
## 最佳实践
### 1. 状态管理
- 使用 useState 管理 Popup 的显示状态
- 对于复杂场景,可以使用 useReducer 或状态管理库
### 2. 性能优化
- 对于内容较多的 Popup考虑使用懒加载
- 在 Popup 关闭时清理相关状态
### 3. 用户体验
- 合理设置 Popup 的位置和大小
- 提供明确的关闭方式
- 添加适当的动画效果
### 4. 样式统一
- 保持 Popup 标题、按钮等样式与系统一致
- 使用系统定义的颜色和间距变量
## 注意事项
1. 确保在组件卸载时清理相关状态和副作用
2. 注意 Popup 内容的可访问性
3. 避免同时显示多个 Popup
4. 考虑移动端的适配问题
5. 在 Popup 关闭时重置相关表单数据(如需要)
## 示例代码
```tsx
import { Popup, Button, View } from "@nutui/nutui-react-taro";
import { useState } from "react";
export default function MyComponent() {
const [visible, setVisible] = useState(false);
return (
<View>
<Button onClick={() => setVisible(true)}>打开 Popup</Button>
<Popup
visible={visible}
position="bottom"
title="示例 Popup"
onClose={() => setVisible(false)}
closeable
round
>
<View className="p-4">
<View>这是 Popup 内容</View>
<Button
block
type="primary"
className="mt-4"
onClick={() => setVisible(false)}
>
关闭
</Button>
</View>
</Popup>
</View>
);
}
```

View File

@ -0,0 +1,304 @@
---
trigger: manual
---
# 文件上传功能实现指南
## 概述
文件上传是系统中常见的功能,用于上传图片、文档等文件。本指南将详细介绍如何在系统中实现文件上传功能。
## 技术栈
系统使用的是基于 Taro 的 React 框架,上传组件来自 `@nutui/nutui-react-taro` 库,文件上传通过 Taro 的 uploadFile API 实现。
## 基本用法
### 1. 导入相关组件和工具
```tsx
import { Uploader, UploaderFileItem } from "@nutui/nutui-react-taro";
import { uploadFile } from "@/utils/uploader";
```
### 2. 基本属性
Uploader 组件常用属性包括:
- `value`: 当前已上传的文件列表UploaderFileItem[]
- `onChange`: 文件列表变化时的回调函数
- `upload`: 自定义上传函数
- `sourceType`: 选择图片的来源(["album", "camera"]
- `maxCount`: 最大上传数量
- `multiple`: 是否支持多选
- `uploadIcon`: 自定义上传图标
- `uploadLabel`: 自定义上传标签文本
### 3. 基本结构
```tsx
<Uploader
value={fileList}
onChange={handleFileChange}
sourceType={["album", "camera"]}
maxCount={1}
upload={uploadFile}
multiple
/>
```
## 实现步骤
### 1. 状态管理
定义文件列表状态:
```tsx
const [fileList, setFileList] = useState<UploaderFileItem[]>([]);
```
### 2. 实现上传工具函数
系统中使用统一的上传工具函数 [utils/uploader.ts](file:///D:/xinfaleisheng/ERPTurbo_Client/packages/app-client/src/utils/uploader.ts)
```tsx
import Taro from "@tarojs/taro";
import { Toast } from "@nutui/nutui-react-taro";
export const uploadFile = async (file: File | string) => {
console.log("file", file);
const res = await Taro.uploadFile({
url: process.env.TARO_API_DOMAIN + `/auth/upload`,
// @ts-ignore
filePath: file.tempFilePath || file,
name: "file",
header: {
saToken: Taro.getStorageSync("saToken"),
"Content-Type": "multipart/form-data",
},
});
if (res.errMsg == "uploadFile:ok") {
const data = JSON.parse(res.data);
if (data.errCode == "401") {
Taro.removeStorageSync("user");
Toast.show("toast", {
icon: "warn",
title: "",
content: "超时请重试",
});
return Promise.reject();
} else {
return {
url: data?.data,
};
}
} else {
Toast.show("toast", {
icon: "fail",
title: "",
content: "上传失败",
});
return Promise.reject();
}
};
```
### 3. 处理文件变化
实现 onChange 回调函数处理文件变化:
```tsx
const handleFileChange = (files: UploaderFileItem[]) => {
setFileList(files);
// 处理上传后的逻辑
if (files.length > 0 && files[0].status === 'success') {
// 文件上传成功可以保存URL到业务数据中
const fileUrls = files.map(file => file.url).filter(url => url) as string[];
// 保存到业务状态中
setBusinessData(fileUrls);
}
};
```
### 4. 初始化已上传文件
如果有已上传的文件需要显示,需要将其转换为 UploaderFileItem 格式:
```tsx
useEffect(() => {
if (existingFileUrls && existingFileUrls.length > 0) {
const fileList = existingFileUrls.map((url, index) => ({
url: url,
name: `file-${index}`,
status: 'success'
}));
setFileList(fileList);
}
}, [existingFileUrls]);
```
## 常见使用场景
### 1. 单图片上传
用于头像上传等只需要一张图片的场景:
```tsx
<Uploader
value={avatarList}
onChange={handleAvatarChange}
sourceType={["album", "camera"]}
maxCount={1}
upload={uploadFile}
/>
```
### 2. 多图片上传
用于需要上传多张图片的场景,如合同、发票等:
```tsx
<Uploader
value={contractImgList}
onChange={handleContractImgChange}
sourceType={["album", "camera"]}
uploadIcon={<Icon name={"camera"} size={36} />}
uploadLabel={
<View className={"flex flex-col items-center"}>
<View className="text-sm">拍照上传合同</View>
</View>
}
maxCount={5}
upload={uploadFile}
multiple
/>
```
### 3. 不同类型文件上传
系统中常见的文件上传场景包括:
1. 空磅照片上传
2. 总磅照片上传
3. 发票照片上传
4. 合同照片上传
5. 头像上传
## 最佳实践
### 1. 状态管理
- 使用 useState 管理文件列表状态
- 将上传成功的文件URL保存到业务数据中
- 在组件卸载时清理相关状态
### 2. 用户体验
- 提供清晰的上传指引说明
- 显示上传进度和状态
- 支持图片预览功能
- 合理设置最大上传数量
### 3. 错误处理
- 在上传工具函数中统一处理上传错误
- 提供友好的错误提示信息
- 处理网络异常情况
### 4. 性能优化
- 对大文件上传提供进度提示
- 支持断点续传(如果后端支持)
- 合理设置图片压缩参数
## 注意事项
1. 确保在组件卸载时清理相关状态和副作用
2. 注意文件大小和格式限制
3. 处理上传失败的重试机制
4. 考虑移动端网络环境的适配
5. 遵循系统统一的上传接口规范
## 示例代码
```tsx
import { View } from "@tarojs/components";
import { Uploader, UploaderFileItem } from "@nutui/nutui-react-taro";
import { useState, useEffect } from "react";
import { uploadFile } from "@/utils/uploader";
export default function UploadExample() {
// 头像上传状态
const [avatarList, setAvatarList] = useState<UploaderFileItem[]>([]);
// 合同照片上传状态
const [contractImgList, setContractImgList] = useState<UploaderFileItem[]>([]);
// 初始化已上传的文件
useEffect(() => {
// 假设从服务器获取到已上传的文件URL
const existingAvatarUrl = "https://example.com/avatar.jpg";
if (existingAvatarUrl) {
setAvatarList([{
url: existingAvatarUrl,
name: 'avatar',
status: 'success'
}]);
}
}, []);
// 处理头像上传变化
const handleAvatarChange = (files: UploaderFileItem[]) => {
setAvatarList(files);
// 如果上传成功保存URL到业务数据
if (files.length > 0 && files[0].status === 'success' && files[0].url) {
// 保存头像URL到用户信息中
saveAvatarUrl(files[0].url);
}
};
// 处理合同照片上传变化
const handleContractImgChange = (files: UploaderFileItem[]) => {
setContractImgList(files);
// 保存所有成功上传的文件URL
const urls = files
.filter(file => file.status === 'success' && file.url)
.map(file => file.url!) as string[];
// 保存到业务数据中
saveContractImgUrls(urls);
};
return (
<View className="p-4">
<View className="mb-6">
<View className="mb-2 text-sm font-medium">头像上传</View>
<Uploader
value={avatarList}
onChange={handleAvatarChange}
sourceType={["album", "camera"]}
maxCount={1}
upload={uploadFile}
/>
</View>
<View>
<View className="mb-2 text-sm font-medium">合同照片上传</View>
<Uploader
value={contractImgList}
onChange={handleContractImgChange}
sourceType={["album", "camera"]}
maxCount={5}
upload={uploadFile}
multiple
/>
</View>
</View>
);
}
```

View File

@ -0,0 +1,95 @@
# 采购订单计算逻辑
## 1. 总包装费
- 计算公式: 辅料费 + 人工费 + 纸箱费 + 固定费用 + 其他费用 + 草帘费(根据开关决定是否计入)
- 开关控制: orderDealer?.strawMatCostFlag 控制草帘费是否计入成本
## 2. 西瓜成本1
- 计算公式: 采购成本 + 运费
- 开关控制: orderDealer?.freightCostFlag 控制运费是否计入成本
## 3. 采购成本
- 计算公式: 供应商采购成本 + 包装费
## 4. 销售金额
- 计算公式: Σ(各供应商重量 * 销售单价)
- 数据来源: orderSupplierList
- 重量选择: 根据 pricingMethod 决定使用 grossWeight 或 netWeight
## 5. 单斤成本
- 计算公式: 采购成本 / 总毛重
## 6. 纸箱利润
- 计算公式: 纸箱售卖费 - 纸箱成本费
## 7. 总毛重
- 计算公式: Σ(各供应商毛重)
- 数据来源: orderSupplierList
## 8. 总净重
- 计算公式: Σ(各供应商净重)
- 数据来源: orderSupplierList
## 9. 纸箱售卖费
- 计算公式: Σ(各供应商纸箱数量 * 纸箱售价)
- 数据来源: orderSupplierList -> orderPackageList
## 10. 纸箱成本费
- 计算公式: Σ(各供应商纸箱数量 * 纸箱成本价)
- 数据来源: orderSupplierList -> orderPackageList
## 11. 草帘费
- 计算公式: 根据开关和价格确定
- 数据来源: orderVehicle
- 开关控制:
- orderVehicle?.openStrawCurtain 必须开启
- orderVehicle?.strawCurtainPrice 必须存在
## 12. 运费
- 计算公式: 直接取值
- 数据来源: orderVehicle?.price
## 13. 个人利润
- 计算公式: (西瓜毛利 - 个人返点 - 成本差异) * 0.6 + 成本差异
- 涉及数据: 西瓜毛利、个人返点、成本差异
## 14. 分成后净利润(CXZY)
- 计算公式: 西瓜净利润 * 分成比例
- 数据来源: dealerVO
- 开关控制: dealerVO.enableShare 控制是否启用分成
## 15. 西瓜净利润
- 计算公式: 西瓜毛利 - 个人返点 - 成本差异
## 16. 西瓜毛利
- 计算公式: 市场报价 - 税费补贴 - 西瓜成本1 - 计提税金
## 17. 西瓜成本2
- 计算公式: 西瓜成本1 + 成本差异
## 18. 成本差异
- 计算公式: 直接取值
- 数据来源: orderDealer.costDifference
## 19. 个人返点
- 计算公式: 直接取值
- 数据来源: orderRebate?.amount
## 20. 计提税金
- 计算公式: 直接取值
- 数据来源: orderDealer.taxProvision
## 21. 税费补贴
- 计算公式: 直接取值
- 数据来源: orderDealer.taxSubsidy
## 22. 市场报价
- 计算公式: 销售金额 + 总包装费
## 23. 供应商采购成本
- 计算公式: Σ(各供应商净重 * 采购价)
- 数据来源: orderSupplierList
## 24. 辅料费/人工费等其他费用
- 计算公式: Σ(各费用项目价格 * 数量)
- 数据来源: orderCostList

View File

@ -28,6 +28,7 @@ config = {
"reviewer/list", "reviewer/list",
"reviewer/audit", "reviewer/audit",
"reviewer/history", "reviewer/history",
"reviewer/submitted",
// 审批员(老板) // 审批员(老板)
"approver/list", "approver/list",
@ -48,7 +49,7 @@ config = {
}, },
}, },
tabBar: { tabBar: {
custom: true, custom: false,
color: "#000000", color: "#000000",
selectedColor: "#DC143C", selectedColor: "#DC143C",
backgroundColor: "#ffffff", backgroundColor: "#ffffff",

View File

@ -3,7 +3,7 @@ import { useEffect, useMemo, useState } from "react";
import { View } from "@tarojs/components"; import { View } from "@tarojs/components";
import { SafeArea, Tabbar } from "@nutui/nutui-react-taro"; import { SafeArea, Tabbar } from "@nutui/nutui-react-taro";
import { globalStore } from "@/store/global-store"; import { globalStore } from "@/store/global-store";
import { Icon } from "@/components"; import { CustomTheme, Icon } from "@/components";
interface ICustomTabBarProps { interface ICustomTabBarProps {
role: AuthAPI.UserRoleVO["slug"]; role: AuthAPI.UserRoleVO["slug"];
@ -86,7 +86,7 @@ export default function CustomTabBar(props: ICustomTabBarProps) {
}; };
return ( return (
<> <CustomTheme>
<View <View
style={{ style={{
height: `calc(92rpx * ${scaleFactor})`, height: `calc(92rpx * ${scaleFactor})`,
@ -115,6 +115,6 @@ export default function CustomTabBar(props: ICustomTabBarProps) {
); );
})} })}
</Tabbar> </Tabbar>
</> </CustomTheme>
); );
} }

View File

@ -41,12 +41,12 @@ export function CustomTheme(props: CustomThemeProps) {
> >
<ConfigProvider <ConfigProvider
theme={{ theme={{
nutuiCellGroupWrapMargin: "0rpx", nutuiCellGroupWrapMargin: "0",
nutuiCellGroupTitleFontSize: "var(--text-lg)", nutuiCellGroupTitleFontSize: "var(--text-lg)",
nutuiCellGroupTitleLineHeight: "56rpx", nutuiCellGroupTitleLineHeight: Taro.pxTransform(56),
nutuiCellGroupTitlePadding: "0rpx 32rpx", nutuiCellGroupTitlePadding: `0 ${Taro.pxTransform(32)}`,
nutuiCellFontSize: "var(--text-xs)", nutuiCellFontSize: "var(--text-xs)",
nutuiCellLineHeight: "56rpx", nutuiCellLineHeight: Taro.pxTransform(56),
nutuiFontSizeS: "var(--text-sm)", nutuiFontSizeS: "var(--text-sm)",
nutuiTabbarInactiveColor: "#6B7280", nutuiTabbarInactiveColor: "#6B7280",
nutuiPriceLineColor: "var(--color-primary)", nutuiPriceLineColor: "var(--color-primary)",
@ -57,21 +57,21 @@ export function CustomTheme(props: CustomThemeProps) {
nutuiColorPrimaryDisabled: "#D1D5DB", nutuiColorPrimaryDisabled: "#D1D5DB",
nutuiColorPrimaryDisabledSpecial: "#D1D5DB", nutuiColorPrimaryDisabledSpecial: "#D1D5DB",
nutuiDisabledColor: "#D1D5DB", nutuiDisabledColor: "#D1D5DB",
nutuiStepsBaseHeadTextSize: "36rpx", nutuiStepsBaseHeadTextSize: Taro.pxTransform(36),
nutuiStepsBaseIconSize: "24rpx", nutuiStepsBaseIconSize: Taro.pxTransform(24),
nutuiFontSizeBase: "var(--text-sm)", nutuiFontSizeBase: "var(--text-sm)",
nutuiFormItemRequiredColor: "#ff0f23", nutuiFormItemRequiredColor: "#ff0f23",
nutuiFormItemBodySlotsTextAlign: "right", nutuiFormItemBodySlotsTextAlign: "right",
nutuiNoticebarHeight: "84rpx", nutuiNoticebarHeight: Taro.pxTransform(84),
nutuiSwitchHeight: "54rpx", nutuiSwitchHeight: Taro.pxTransform(54),
nutuiSwitchWidth: "54rpx", nutuiSwitchWidth: Taro.pxTransform(54),
nutuiSwitchLabelFontSize: "28rpx", nutuiSwitchLabelFontSize: Taro.pxTransform(28),
// nutuiInputFontSize: "40rpx", // nutuiInputFontSize: "40rpx",
nutuiInputBackgroundColor: "transparent", nutuiInputBackgroundColor: "transparent",
nutuiInputPadding: "0 40rpx", nutuiInputPadding: `0 ${Taro.pxTransform(40)}`,
nutuiInputFontSize: "var(--text-sm)", nutuiInputFontSize: "var(--text-sm)",
nutuiInputLineheight: nutuiInputLineheight:
"var(--tw-leading, var(--text-sm--line-height))", "var(--tw-leading, var(--text-sm--line-height))",
@ -79,26 +79,26 @@ export function CustomTheme(props: CustomThemeProps) {
nutuiButtonXlargeHeight: "calc(var(--spacing) * 12)", nutuiButtonXlargeHeight: "calc(var(--spacing) * 12)",
nutuiButtonXlargeFontSize: "var(--text-lg)", nutuiButtonXlargeFontSize: "var(--text-lg)",
nutuiButtonXlargePadding: "0rpx calc(var(--spacing) * 3)", nutuiButtonXlargePadding: "0 calc(var(--spacing) * 3)",
nutuiButtonXlargeBorderRadius: "calc(var(--spacing) * 2)", nutuiButtonXlargeBorderRadius: "calc(var(--spacing) * 2)",
nutuiButtonLargeHeight: "calc(var(--spacing) * 10)", nutuiButtonLargeHeight: "calc(var(--spacing) * 10)",
nutuiButtonLargeFontSize: "var(--text-base)", nutuiButtonLargeFontSize: "var(--text-base)",
nutuiButtonLargePadding: "0rpx calc(var(--spacing) * 3)", nutuiButtonLargePadding: "0 calc(var(--spacing) * 3)",
nutuiButtonLargeBorderRadius: "calc(var(--spacing) * 2)", nutuiButtonLargeBorderRadius: "calc(var(--spacing) * 2)",
// --nutui-button-small-height // --nutui-button-small-height
nutuiButtonSmallHeight: "calc(var(--spacing) * 8)", nutuiButtonSmallHeight: "calc(var(--spacing) * 8)",
nutuiButtonSmallFontSize: "var(--text-sm)", nutuiButtonSmallFontSize: "var(--text-sm)",
nutuiButtonSmallPadding: "0rpx calc(var(--spacing) * 3)", nutuiButtonSmallPadding: "0 calc(var(--spacing) * 3)",
nutuiButtonSmallBorderRadius: "calc(var(--spacing) * 2)", nutuiButtonSmallBorderRadius: "calc(var(--spacing) * 2)",
// --nutui-textarea-padding // --nutui-textarea-padding
nutuiTextareaPadding: "0rpx", nutuiTextareaPadding: "0",
nutuiUploaderImageBorder: "2px dashed var(--color-gray-300)", nutuiUploaderImageBorder: `${Taro.pxTransform(4)} dashed var(--color-gray-300)`,
nutuiUploaderImageWidth: "100%", nutuiUploaderImageWidth: "100%",
nutuiUploaderImageHeight: "320rpx", nutuiUploaderImageHeight: Taro.pxTransform(320),
nutuiUploaderPreviewMarginRight: "0", nutuiUploaderPreviewMarginRight: "0",
nutuiUploaderPreviewMarginBottom: "0", nutuiUploaderPreviewMarginBottom: "0",
@ -106,9 +106,7 @@ export function CustomTheme(props: CustomThemeProps) {
nutuiTabbarHeight: "calc(92rpx * var(--scale-factor))", nutuiTabbarHeight: "calc(92rpx * var(--scale-factor))",
}} }}
> >
<View className={"flex min-h-screen w-screen flex-col bg-neutral-100"}>
{children} {children}
</View>
</ConfigProvider> </ConfigProvider>
</View> </View>
); );

View File

@ -69,7 +69,6 @@ export default <T extends {}, Q extends Query = Query>(
useImperativeHandle(actionRef, () => { useImperativeHandle(actionRef, () => {
return { return {
reload: () => { reload: () => {
setData([]);
setQuery({ setQuery({
...query, ...query,
...params, ...params,
@ -78,10 +77,9 @@ export default <T extends {}, Q extends Query = Query>(
}); });
}, },
} as ActionType; } as ActionType;
}, [pagination.pageSize]); }, [pagination.pageSize, params]);
useEffect(() => { useEffect(() => {
setData([]);
setQuery({ setQuery({
...query, ...query,
...params, ...params,
@ -119,12 +117,18 @@ export default <T extends {}, Q extends Query = Query>(
request(query as any).then((res: Record<any>) => { request(query as any).then((res: Record<any>) => {
const list = res.data; const list = res.data;
if (res.success) { if (res.success) {
if (query.pageIndex === 1) {
// 如果是第一页,则替换数据
setData(list as any);
} else {
// 如果不是第一页,则追加数据
if (data) { if (data) {
data.push(...(list as any)); data.push(...(list as any));
setData(data.slice()); setData(data.slice());
} else { } else {
setData(list as any); setData(list as any);
} }
}
if (res.hasMore !== undefined) { if (res.hasMore !== undefined) {
setHasMore(res.hasMore); setHasMore(res.hasMore);
} }
@ -146,7 +150,7 @@ export default <T extends {}, Q extends Query = Query>(
} }
}; };
const refresh = async () => { const refresh = async () => {
actionRef.current.reload(); await actionRef.current.reload();
}; };
// 可视区域条数 // 可视区域条数
@ -211,10 +215,10 @@ export default <T extends {}, Q extends Query = Query>(
} }
onClear={() => actionRef.current.reload()} onClear={() => actionRef.current.reload()}
onSearch={(val) => { onSearch={(val) => {
setData([]);
setQuery({ setQuery({
...query, ...query,
[searchType]: val == "" ? undefined : val, [searchType]: val == "" ? undefined : val,
pageIndex: 1,
}); });
}} }}
leftIn={ leftIn={

View File

@ -2,7 +2,8 @@ import { ScrollView, Text, View } from "@tarojs/components";
import { Button, Input, Popup, Radio, SafeArea } from "@nutui/nutui-react-taro"; import { Button, Input, Popup, Radio, SafeArea } from "@nutui/nutui-react-taro";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { formatCurrency } from "@/utils/format"; import { formatCurrency } from "@/utils/format";
import { useState } from "react"; import { useEffect, useState } from "react";
import businessServices from "@/services/business";
export default function BasicInfoSection(props: { export default function BasicInfoSection(props: {
purchaseOrderVO: BusinessAPI.PurchaseOrderVO; purchaseOrderVO: BusinessAPI.PurchaseOrderVO;
@ -32,6 +33,58 @@ export default function BasicInfoSection(props: {
strawCurtainPrice: orderVehicle?.strawCurtainPrice || 0, strawCurtainPrice: orderVehicle?.strawCurtainPrice || 0,
}); });
// 上一车次号
const [lastVehicleNo, setLastVehicleNo] = useState<string | null>(null);
// 是否正在获取上一车次号
const [loadingLastVehicleNo, setLoadingLastVehicleNo] = useState(false);
// 获取上一车次号
const fetchLastVehicleNo = async () => {
// 如果已经有车次号,则不需要获取上一车次号
if (orderVehicle?.vehicleNo) {
return;
}
// 避免重复请求
if (loadingLastVehicleNo || lastVehicleNo) {
return;
}
setLoadingLastVehicleNo(true);
try {
const { data: res } =
await businessServices.purchaseOrder.getLastVehicleNo({
lastVehicleNoQry: {},
});
if (res.success && res.data) {
setLastVehicleNo(res.data);
// 解析车次号中的数字部分并加1
const numberPart = res.data.match(/(\d+)$/);
if (numberPart) {
const nextNumber = parseInt(numberPart[1]) + 1;
const nextVehicleNo = res.data.replace(
/(\d+)$/,
nextNumber.toString(),
);
// 更新车次号
updateVehicleNo(nextVehicleNo);
}
}
} catch (error) {
console.error("获取上一车次号失败:", error);
} finally {
setLoadingLastVehicleNo(false);
}
};
// 组件加载时获取上一车次号
useEffect(() => {
fetchLastVehicleNo();
}, []);
// 打开基础信息弹窗 // 打开基础信息弹窗
const openBasicInfoPopup = () => { const openBasicInfoPopup = () => {
if (readOnly) return; if (readOnly) return;
@ -83,7 +136,7 @@ export default function BasicInfoSection(props: {
}; };
// 更新运费类型 // 更新运费类型
const updatePriceType = (value: BusinessAPI.OrderVehicle['priceType']) => { const updatePriceType = (value: BusinessAPI.OrderVehicle["priceType"]) => {
if (onChange) { if (onChange) {
const updatedOrder = { const updatedOrder = {
...purchaseOrderVO, ...purchaseOrderVO,
@ -96,6 +149,14 @@ export default function BasicInfoSection(props: {
} }
}; };
// 显示的参考车次号
const displayReferenceVehicleNo = () => {
if (lastVehicleNo) {
return lastVehicleNo;
}
return "获取中...";
};
return ( return (
<> <>
{/* 基础信息编辑弹窗 */} {/* 基础信息编辑弹窗 */}
@ -408,12 +469,21 @@ export default function BasicInfoSection(props: {
<View className="text-neutral-darkest mb-1 text-sm font-bold"> <View className="text-neutral-darkest mb-1 text-sm font-bold">
</View> </View>
<View className="flex flex-row items-center justify-between">
{readOnly ? ( {readOnly ? (
<View
className="flex flex-row items-center justify-between rounded-md p-4"
style={{
background: "linear-gradient(101deg, #F8FAFF 25%, #F0F5FF 74%)",
}}
>
<View className="text-neutral-darkest text-sm font-medium"> <View className="text-neutral-darkest text-sm font-medium">
{orderVehicle?.vehicleNo || "请填写"} {orderVehicle?.vehicleNo
? "第" + orderVehicle?.vehicleNo + "车"
: "暂未生成车次"}
</View>
</View> </View>
) : ( ) : (
<View className="flex flex-row items-center justify-between">
<View <View
className={`flex h-10 flex-1 items-center rounded-md border-4 border-gray-300`} className={`flex h-10 flex-1 items-center rounded-md border-4 border-gray-300`}
> >
@ -424,11 +494,11 @@ export default function BasicInfoSection(props: {
onChange={(value) => updateVehicleNo(value)} onChange={(value) => updateVehicleNo(value)}
/> />
</View> </View>
)}
<View className="flex-1 text-center text-xs text-gray-500"> <View className="flex-1 text-center text-xs text-gray-500">
xxx {displayReferenceVehicleNo()}
</View> </View>
</View> </View>
)}
</View> </View>
{/* 运费信息 */} {/* 运费信息 */}
@ -442,7 +512,7 @@ export default function BasicInfoSection(props: {
background: "linear-gradient(101deg, #F8FAFF 25%, #F0F5FF 74%)", background: "linear-gradient(101deg, #F8FAFF 25%, #F0F5FF 74%)",
}} }}
> >
<Text className="text-red-500 pb-2 text-3xl font-bold"> <Text className="pb-2 text-3xl font-bold text-red-500">
{formatCurrency(orderVehicle?.price || 0)} {formatCurrency(orderVehicle?.price || 0)}
</Text> </Text>
<View className="text-neutral-darkest text-sm font-medium"> <View className="text-neutral-darkest text-sm font-medium">
@ -458,7 +528,11 @@ export default function BasicInfoSection(props: {
<Radio.Group <Radio.Group
direction={"horizontal"} direction={"horizontal"}
value={orderVehicle?.priceType} value={orderVehicle?.priceType}
onChange={(value) => updatePriceType(value as BusinessAPI.OrderVehicle['priceType'])} onChange={(value) =>
updatePriceType(
value as BusinessAPI.OrderVehicle["priceType"],
)
}
> >
<Radio value="MAIN_FREIGHT"></Radio> <Radio value="MAIN_FREIGHT"></Radio>
<Radio value="SHORT_TRANSPORT"></Radio> <Radio value="SHORT_TRANSPORT"></Radio>
@ -481,7 +555,7 @@ export default function BasicInfoSection(props: {
}} }}
> >
{orderVehicle?.openStrawCurtain ? ( {orderVehicle?.openStrawCurtain ? (
<Text className="text-red-500 pb-2 text-3xl font-bold"> <Text className="pb-2 text-3xl font-bold text-red-500">
{formatCurrency(orderVehicle?.strawCurtainPrice || 0)} {formatCurrency(orderVehicle?.strawCurtainPrice || 0)}
</Text> </Text>
) : ( ) : (

View File

@ -102,7 +102,7 @@ export default function CostDifferenceSection(props: {
<Text className="text-sm text-gray-500"></Text> <Text className="text-sm text-gray-500"></Text>
</View> </View>
<View className="relative"> <View className="relative">
<Text className="price-input text-primary w-full py-2 font-bold"> <Text className="w-full py-2 text-3xl font-bold text-red-500">
{costDifference || "0.00"} {costDifference || "0.00"}
</Text> </Text>
</View> </View>

View File

@ -53,18 +53,15 @@ export default function CostSummarySection(props: {
const columns = [ const columns = [
{ {
title: "成本合计", title: "成本合计",
width: 100,
key: "costType", key: "costType",
}, },
{ {
title: "金额(元)", title: "金额(元)",
width: 30,
key: "amount", key: "amount",
align: "center", align: "center",
}, },
{ {
title: "单斤成本(元/斤)", title: "单斤成本(元/斤)",
width: 60,
key: "unitCost", key: "unitCost",
align: "center", align: "center",
}, },

View File

@ -142,7 +142,7 @@ export default function MarketPriceSection(props: {
const salePrice = const salePrice =
editValues[supplier.orderSupplierId || ""]?.salePrice !== undefined editValues[supplier.orderSupplierId || ""]?.salePrice !== undefined
? editValues[supplier.orderSupplierId || ""].salePrice ? editValues[supplier.orderSupplierId || ""].salePrice
: supplier.salePrice; : supplier.purchasePrice;
return sum + (salePrice || 0); return sum + (salePrice || 0);
}, 0) / purchaseOrderVO.orderSupplierList.length }, 0) / purchaseOrderVO.orderSupplierList.length
: 0; : 0;
@ -248,7 +248,7 @@ export default function MarketPriceSection(props: {
<Text className="text-sm text-gray-500">/</Text> <Text className="text-sm text-gray-500">/</Text>
</View> </View>
<View className="relative flex"> <View className="relative flex">
<Text className="text-primary border-primary w-full border-b-2 pb-2 text-3xl font-bold focus:outline-none"> <Text className="text-red-500 border-red-500 w-full border-b-2 pb-2 text-3xl font-bold focus:outline-none">
{salePrice?.toFixed(2) || "0.00"} {salePrice?.toFixed(2) || "0.00"}
</Text> </Text>
</View> </View>
@ -260,7 +260,7 @@ export default function MarketPriceSection(props: {
<Text className="text-sm text-gray-500">/</Text> <Text className="text-sm text-gray-500">/</Text>
</View> </View>
<View className="relative"> <View className="relative">
<Text className="price-input text-primary w-full py-2 font-bold"> <Text className="w-full py-2 text-3xl font-bold text-red-500">
{salePrice?.toFixed(2) || "0.00"} {salePrice?.toFixed(2) || "0.00"}
</Text> </Text>
</View> </View>

View File

@ -242,7 +242,7 @@ export default function MaterialCostSection(props: {
<Text className="text-sm text-gray-500"></Text> <Text className="text-sm text-gray-500"></Text>
</View> </View>
<View className="relative"> <View className="relative">
<Text className="price-input text-primary w-full py-2 font-bold"> <Text className="w-full py-2 text-3xl font-bold text-red-500">
{amount.toFixed(2) || "0.00"} {amount.toFixed(2) || "0.00"}
</Text> </Text>
</View> </View>

View File

@ -134,7 +134,7 @@ export default function RebateCalcSection(props: {
<Text className="text-sm text-gray-500"></Text> <Text className="text-sm text-gray-500"></Text>
</View> </View>
<View className="relative"> <View className="relative">
<Text className="price-input text-primary w-full py-2 font-bold"> <Text className="w-full py-2 text-3xl font-bold text-red-500">
{calculateRebateAmount(orderRebate).toFixed(2) || "0.00"} {calculateRebateAmount(orderRebate).toFixed(2) || "0.00"}
</Text> </Text>
</View> </View>

View File

@ -105,7 +105,7 @@ export default function TaxProvisionSection(props: {
<Text className="text-sm text-gray-500"></Text> <Text className="text-sm text-gray-500"></Text>
</View> </View>
<View className="relative"> <View className="relative">
<Text className="price-input text-primary w-full py-2 font-bold"> <Text className="w-full py-2 text-3xl font-bold text-red-500">
{taxProvision || "0.00"} {taxProvision || "0.00"}
</Text> </Text>
</View> </View>

View File

@ -99,7 +99,7 @@ export default function TaxSubsidySection(props: {
<Text className="text-sm text-gray-500"></Text> <Text className="text-sm text-gray-500"></Text>
</View> </View>
<View className="relative"> <View className="relative">
<Text className="price-input text-primary w-full py-2 font-bold"> <Text className="w-full py-2 text-3xl font-bold text-red-500">
{taxSubsidy || "0.00"} {taxSubsidy || "0.00"}
</Text> </Text>
</View> </View>

View File

@ -20,7 +20,7 @@ const hocAuth = (
const [longitude, setLongitude] = useState<number>(); const [longitude, setLongitude] = useState<number>();
const [latitude, setLatitude] = useState<number>(); const [latitude, setLatitude] = useState<number>();
const [loading, setLoading] = useState<boolean>(false) const [loading, setLoading] = useState<boolean>(false);
// 控制是否已初始化 // 控制是否已初始化
const [isInitialized, setIsInitialized] = useState(false); const [isInitialized, setIsInitialized] = useState(false);
@ -53,10 +53,12 @@ const hocAuth = (
} }
}, []); }, []);
if (process.env.TARO_ENV === "weapp") {
useEffect(() => { useEffect(() => {
if (!user) { if (!user) {
return; return;
} }
Taro.onUserCaptureScreen(function () { Taro.onUserCaptureScreen(function () {
const query = buildParams({ const query = buildParams({
...router?.params, ...router?.params,
@ -83,6 +85,7 @@ const hocAuth = (
}); });
}; };
}, [user]); }, [user]);
}
if (!user) { if (!user) {
if (skeleton) { if (skeleton) {

View File

@ -6,6 +6,7 @@ import { userStore } from "@/store/user-store";
import { Dialog, Toast } from "@nutui/nutui-react-taro"; import { Dialog, Toast } from "@nutui/nutui-react-taro";
import { CustomTheme, ShareActionSheet } from "@/components"; import { CustomTheme, ShareActionSheet } from "@/components";
import { getCurrentInstance } from "@tarojs/runtime"; import { getCurrentInstance } from "@tarojs/runtime";
import { View } from "@tarojs/components";
let startTime: number; // 记录开始时间 let startTime: number; // 记录开始时间
let endTime: number; // 记录结束时间 let endTime: number; // 记录结束时间
@ -97,6 +98,7 @@ const base = (Component: React.FC) => (props: any) => {
return ( return (
<SWRConfig value={{ provider: () => map, revalidateOnFocus: false }}> <SWRConfig value={{ provider: () => map, revalidateOnFocus: false }}>
<CustomTheme> <CustomTheme>
<View className={"flex min-h-screen w-screen flex-col bg-neutral-100"}>
<Component <Component
{...props} {...props}
shareOptions={shareOptions} shareOptions={shareOptions}
@ -111,6 +113,7 @@ const base = (Component: React.FC) => (props: any) => {
onCancel={() => setShareVisible(false)} onCancel={() => setShareVisible(false)}
callback={shareOptions?.callback} callback={shareOptions?.callback}
/> />
</View>
</CustomTheme> </CustomTheme>
</SWRConfig> </SWRConfig>
); );

View File

@ -338,12 +338,12 @@ export default hocAuth(function Page(props: CommonComponent) {
{/* 选择身份 */} {/* 选择身份 */}
<Popup <Popup
className={"w-full"}
closeable closeable
destroyOnClose destroyOnClose
duration={150} duration={150}
visible={visible} visible={visible}
title="选择身份" title="选择身份"
position="bottom"
onClose={async () => { onClose={async () => {
if (visible) { if (visible) {
setVisible(false); setVisible(false);

View File

@ -4,130 +4,15 @@ import Taro, { useDidShow } from "@tarojs/taro";
import { business } from "@/services"; import { business } from "@/services";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { View } from "@tarojs/components"; import { View } from "@tarojs/components";
import purchaseOrder from "@/constant/purchaseOrder"; import { SafeArea } from "@nutui/nutui-react-taro";
import { import {
Button,
Dialog,
Input,
SafeArea,
Toast,
} from "@nutui/nutui-react-taro";
import {
BasicInfoSection,
CompanyInfoSection,
CostSummarySection,
DealerInfoSection,
EmptyBoxInfoSection,
LaborInfoSection,
MarketPriceSection,
PackageInfoSection,
PackagingCostSection,
PurchaseCostInfoSection,
PurchaseOrderFinalApprove, PurchaseOrderFinalApprove,
PurchaseOrderRejectApprove,
PurchaseOrderRejectFinal, PurchaseOrderRejectFinal,
RebateCalcSection,
SupplierInfoSection,
} from "@/components"; } from "@/components";
import buildUrl from "@/utils/buildUrl"; import buildUrl from "@/utils/buildUrl";
const sectionList = {
// 市场报价
marketPrice: MarketPriceSection,
// 销售方信息卡片
supplierInfo: CompanyInfoSection,
// 下游经销商信息
dealerInfo: DealerInfoSection,
// 基础信息
basicInfo: BasicInfoSection,
// 瓜农信息
farmerInfo: SupplierInfoSection,
// 采购成本
purchaseCostInfo: PurchaseCostInfoSection,
// 包装纸箱费
packageInfo: PackageInfoSection,
// 空箱费用
emptyBoxInfo: EmptyBoxInfoSection,
// 用工信息
laborInfo: LaborInfoSection,
// 包装费
packagingCost: PackagingCostSection,
// 成本合计
costSummary: CostSummarySection,
// 返点计算
rebateCalc: RebateCalcSection,
};
const sectionConfig = {
marketPrice: {
title: "市场报价",
containerClass: "border-l-8 border-l-lime-500",
contentClass: "p-4 bg-white rounded-b-lg shadow-sm overflow-x-auto",
},
supplierInfo: {
title: "销售方信息",
containerClass: "border-l-8 border-l-blue-500",
contentClass: "p-4 bg-white rounded-b-lg shadow-sm overflow-x-auto",
},
dealerInfo: {
title: "下游经销商信息",
containerClass: "border-l-8 border-l-green-500",
contentClass: "p-4 bg-white rounded-b-lg shadow-sm overflow-x-auto",
},
basicInfo: {
title: "基础信息",
containerClass: "border-l-8 border-l-yellow-500",
contentClass: "p-4 bg-white rounded-b-lg shadow-sm overflow-x-auto",
},
farmerInfo: {
title: "瓜农信息",
containerClass: "border-l-8 border-l-purple-500",
contentClass: "p-4 bg-white rounded-b-lg shadow-sm overflow-x-auto",
},
purchaseCostInfo: {
title: "采购成本",
containerClass: "border-l-8 border-l-red-500",
contentClass: "p-4 bg-white rounded-b-lg shadow-sm overflow-x-auto",
},
packageInfo: {
title: "包装纸箱费",
containerClass: "border-l-8 border-l-indigo-500",
contentClass: "p-4 bg-white rounded-b-lg shadow-sm overflow-x-auto",
},
emptyBoxInfo: {
title: "空箱费用",
containerClass: "border-l-8 border-l-pink-500",
contentClass: "p-4 bg-white rounded-b-lg shadow-sm overflow-x-auto",
},
laborInfo: {
title: "用工信息",
containerClass: "border-l-8 border-l-teal-500",
contentClass: "p-4 bg-white rounded-b-lg shadow-sm overflow-x-auto",
},
packagingCost: {
title: "包装费",
containerClass: "border-l-8 border-l-orange-500",
contentClass: "p-4 bg-white rounded-b-lg shadow-sm overflow-x-auto",
},
costSummary: {
title: "成本合计",
containerClass: "border-l-8 border-l-cyan-500",
contentClass: "p-4 bg-white rounded-b-lg shadow-sm overflow-x-auto",
},
rebateCalc: {
title: "返点计算",
containerClass: "border-l-8 border-l-amber-500",
contentClass: "p-4 bg-white rounded-b-lg shadow-sm overflow-x-auto",
},
profitCalc: {
title: "利润计算",
containerClass: "border-l-8 border-l-emerald-500",
contentClass: "p-4 bg-white rounded-b-lg shadow-sm overflow-x-auto",
},
};
export default hocAuth(function Page(props: CommonComponent) { export default hocAuth(function Page(props: CommonComponent) {
const { router, isInitialized, setIsInitialized, role } = props; const { router, isInitialized, setIsInitialized } = props;
const orderId = router.params const orderId = router.params
.orderId as BusinessAPI.PurchaseOrderVO["orderId"]; .orderId as BusinessAPI.PurchaseOrderVO["orderId"];
@ -135,147 +20,6 @@ export default hocAuth(function Page(props: CommonComponent) {
const [purchaseOrderVO, setPurchaseOrderVO] = const [purchaseOrderVO, setPurchaseOrderVO] =
useState<BusinessAPI.PurchaseOrderVO>(); useState<BusinessAPI.PurchaseOrderVO>();
const [originPrincipal, setOriginPrincipal] = useState("");
console.log("purchaseOrderVO-----", purchaseOrderVO);
const [collapsedSections, setCollapsedSections] = useState<
Record<string, boolean>
>({
supplierInfo: false,
dealerInfo: false,
basicInfo: false,
farmerInfo: false,
purchaseCostInfo: false,
packageInfo: false,
emptyBoxInfo: false,
laborInfo: false,
packagingCost: false,
costSummary: false,
marketPrice: false,
rebateCalc: false,
profitCalc: false,
});
// 暂存和提交审核的Dialog状态
const [saveDialogVisible, setSaveDialogVisible] = useState(false);
const [submitDialogVisible, setSubmitDialogVisible] = useState(false);
const toggleSection = (section: string) => {
setCollapsedSections((prev) => ({
...prev,
[section]: !prev[section],
}));
};
// 暂存操作
const handleSave = () => {
setSaveDialogVisible(true);
};
// 确认暂存
const confirmSave = async () => {
// 关闭对话框
setSaveDialogVisible(false);
// 这里应该调用暂存API
console.log("暂存数据:", purchaseOrderVO);
const { data } = await business.purchaseOrder.approvePurchaseOrder({
...purchaseOrderVO!,
draft: true,
});
if (data.success) {
Toast.show("toast", {
icon: "success",
title: "提示",
content: "暂存成功",
});
// 返回采购单页面
Taro.redirectTo({ url: "/pages/purchase/approver/list" });
}
};
// 提交老板审核操作
const handleSubmit = () => {
setSubmitDialogVisible(true);
};
// 确认提交老板审核
const confirmSubmit = async () => {
// 关闭对话框
setSubmitDialogVisible(false);
// 表单校验
const errorMsg = validateForm();
if (errorMsg) {
Toast.show("toast", {
icon: "fail",
title: "校验失败",
content: errorMsg,
});
return;
}
// 这里应该调用提交审核API
console.log("提交老板审核:", purchaseOrderVO);
const { data } = await business.purchaseOrder.approvePurchaseOrder({
...purchaseOrderVO!,
draft: false,
});
if (data.success) {
Toast.show("toast", {
icon: "success",
title: "提示",
content: "暂存成功",
});
// 返回采购单页面
Taro.redirectTo({ url: "/pages/purchase/approver/list" });
}
};
// 表单校验
const validateForm = () => {
// 校验销售方
if (!purchaseOrderVO?.orderCompany?.companyId) {
return "请选择销售方";
}
// 校验经销商
if (!purchaseOrderVO?.orderDealer?.dealerId) {
return "请选择经销商";
}
// 校验本车次号
if (!purchaseOrderVO?.orderVehicle?.vehicleNo) {
return "请输入本车次号";
}
// 校验运费类型
if (!purchaseOrderVO?.orderVehicle?.priceType) {
return "请选择运费类型";
}
// 校验市场报价的报价方式
if (!purchaseOrderVO?.pricingMethod) {
return "请选择市场报价的报价方式";
}
// 校验市场报价的销售单价
purchaseOrderVO.orderSupplierList.forEach(
(supplier: BusinessAPI.OrderSupplier) => {
if (!supplier.salePrice || supplier.salePrice <= 0) {
return "请填写市场报价的销售单价";
}
},
);
return null;
};
const init = async (orderId: BusinessAPI.PurchaseOrderVO["orderId"]) => { const init = async (orderId: BusinessAPI.PurchaseOrderVO["orderId"]) => {
const { data } = await business.purchaseOrder.showPurchaseOrder({ const { data } = await business.purchaseOrder.showPurchaseOrder({
purchaseOrderShowQry: { purchaseOrderShowQry: {
@ -307,135 +51,117 @@ export default hocAuth(function Page(props: CommonComponent) {
} }
return ( return (
<View className={"flex flex-col gap-2.5"} id={"purchase-order-audit"}> <>
{/* 顶部导航 */}
<View className="bg-white p-4">
{/* 展示产地负责人*/}
<View className="mb-3 flex flex-row items-center gap-3">
<View className="flex-shrink-0 text-base font-bold text-gray-900">
</View>
<View <View
className={`flex h-12 w-full items-center rounded-lg border-2 border-gray-300 bg-gray-50 px-3`} className={"flex flex-1 flex-col gap-2.5 p-2.5"}
id={"purchase-order-approve"}
> >
<Input <View>
type="text" <View className="flex justify-between space-x-2.5">
disabled={ <View className="price-card flex-1 rounded-lg bg-white p-3">
role === "boss" || <View className="mb-1 text-center text-sm text-gray-500">
(role === "reviewer" &&
purchaseOrderVO.state !== "WAITING_AUDIT") </View>
} <View className="text-center">
placeholder="请输入产地负责人姓名" <View className="price-highlight text-primary">5.80</View>
value={originPrincipal || purchaseOrderVO.createdByName} <View className="text-sm text-gray-500">/</View>
onChange={(value) => setOriginPrincipal(value)}
onBlur={() => {
// 更新采购订单中的产地负责人
setPurchaseOrderVO((prev) => ({
...prev!,
originPrincipal: originPrincipal,
}));
}}
className="w-full bg-transparent"
/>
</View> </View>
</View> </View>
<View className="price-card flex-1 rounded-lg bg-white p-3">
<View className="mb-2 rounded-lg bg-blue-50 p-3 text-lg font-bold text-gray-800"> <View className="mb-1 text-center text-sm text-gray-500">
{`${purchaseOrderVO?.orderVehicle?.vehicleNo || "-"}车 - ${purchaseOrderVO?.orderVehicle.origin}${purchaseOrderVO?.orderVehicle.destination}`}
</View> </View>
{purchaseOrderVO?.orderDealer && ( <View className="text-center">
<View className="text-neutral-darker mb-1 text-sm font-medium"> <View className="price-highlight text-green-500">8.20</View>
: {purchaseOrderVO?.orderDealer?.shortName} <View className="text-sm text-gray-500">/</View>
</View>
)}
<View className="inline-block rounded-md border-1 bg-gradient-to-r from-amber-500 to-orange-500 px-2 py-1 text-sm font-bold text-white shadow">
{
purchaseOrder.stateList.find(
(item) => item.value === purchaseOrderVO?.state,
)?.title
}
</View> </View>
</View> </View>
{/* 循环渲染各部分内容 */}
{Object.keys(sectionList).map((sectionKey) => {
const SectionComponent = sectionList[sectionKey];
const config = sectionConfig[sectionKey];
const isCollapsed = collapsedSections[sectionKey];
return (
<View className="overflow-hidden bg-white" key={sectionKey}>
<View
className={`z-10 flex items-center justify-between p-4 shadow-sm ${config.containerClass}`}
onClick={() => toggleSection(sectionKey)}
>
<View className="text-base font-bold text-gray-800">
{config.title}
</View>
<View className="text-neutral-darker text-lg">
{isCollapsed ? "▲" : "▼"}
</View> </View>
</View> </View>
{!isCollapsed && ( <View className="overflow-hidden rounded-lg bg-white shadow-sm">
<View className={config.contentClass}> <View className="border-b border-gray-100 px-4 py-3">
<SectionComponent <View className="text-sm font-bold"></View>
purchaseOrderVO={purchaseOrderVO} </View>
onChange={setPurchaseOrderVO} <View className="grid grid-cols-2 divide-x divide-y divide-gray-100">
readOnly={ <View className="cost-item flex flex-col px-3 py-2">
role === "boss" || <View className="text-sm text-gray-500">西</View>
(role === "reviewer" && <View className="font-medium">
purchaseOrderVO.state !== "WAITING_AUDIT") {purchaseOrderVO.orderVehicle.price}
} </View>
/> </View>
<View className="cost-item flex flex-col px-3 py-2">
<View className="text-sm text-gray-500"></View>
<View className="font-medium">
{purchaseOrderVO.orderVehicle.price}
</View>
</View>
<View className="cost-item flex flex-col px-3 py-2">
<View className="text-sm text-gray-500"></View>
<View className="font-medium">850</View>
</View>
<View className="cost-item flex flex-col px-3 py-2">
<View className="text-sm text-gray-500"></View>
<View className="font-medium">1,500</View>
</View>
<View className="cost-item flex flex-col px-3 py-2">
<View className="text-sm text-gray-500"></View>
<View className="font-medium">3,500</View>
<View className="text-xs text-gray-500">工头: 张三</View>
</View>
<View className="cost-item flex flex-col px-3 py-2">
<View className="text-sm text-gray-500"></View>
<View className="font-medium">600</View>
</View>
<View className="cost-item flex flex-col px-3 py-2">
<View className="text-sm text-gray-500"></View>
<View className="font-medium">300</View>
</View>
<View className="cost-item flex flex-col px-3 py-2">
<View className="text-sm text-gray-500"></View>
<View className="font-medium">800</View>
</View>
<View className="cost-item flex flex-col px-3 py-2">
<View className="text-sm text-gray-500"></View>
<View className="font-medium">200</View>
</View>
<View className="cost-item flex flex-col px-3 py-2">
<View className="text-sm text-gray-500"></View>
<View className="font-medium">
{purchaseOrderVO.orderDealer.taxSubsidy}
</View>
</View>
<View className="cost-item flex flex-col px-3 py-2">
<View className="text-sm text-gray-500"></View>
<View className="font-medium">
{purchaseOrderVO.orderDealer.taxSubsidy}
</View>
</View>
<View className="cost-total col-span-2 grid grid-cols-2 bg-yellow-50 px-3 py-2">
<View className="flex flex-col">
<View className="text-sm text-gray-500"></View>
<View className="font-bold">11,150</View>
</View>
<View className="flex flex-col">
<View className="text-sm text-gray-500"></View>
<View className="font-bold">
0.58 <View className="text-sm text-gray-500">/</View>
</View>
</View>
</View>
</View>
</View>
<View className="rounded-lg bg-white p-2.5 shadow-sm">
<View className="flex items-center justify-between">
<View className="text-gray-500"></View>
<View className="profit-highlight">4,250</View>
</View>
</View> </View>
)}
</View> </View>
);
})}
{/* 按钮操作 */} {/* 按钮操作 */}
<View className={"sticky bottom-0 z-10 bg-white"} id={"bottomBar"}> <View className={"sticky bottom-0 z-10 bg-white"} id={"bottomBar"}>
<View className="flex justify-between gap-2 border-t border-gray-200 p-2.5"> <View className="flex justify-between gap-2 border-t border-gray-200 p-2.5">
{role === "reviewer" && purchaseOrderVO.state === "WAITING_AUDIT" && ( {purchaseOrderVO.state === "WAITING_BOSS_APPROVE" && (
<>
<View className={"flex-1"}>
<PurchaseOrderRejectApprove
purchaseOrderVO={purchaseOrderVO}
size={"xlarge"}
onFinish={() => {
// 返回首页
Taro.redirectTo({ url: "/pages/purchase/approver/list" });
}}
/>
</View>
<View className={"flex-1"}>
<Button
block
type={"default"}
size={"xlarge"}
className="bg-gray-200 text-gray-700"
onClick={handleSave}
>
</Button>
</View>
<View className={"flex-1"}>
<Button
block
type={"primary"}
size={"xlarge"}
className="bg-primary text-white"
onClick={handleSubmit}
>
</Button>
</View>
</>
)}
{role === "boss" &&
purchaseOrderVO.state === "WAITING_BOSS_APPROVE" && (
<> <>
<View className={"flex-1"}> <View className={"flex-1"}>
<PurchaseOrderRejectFinal <PurchaseOrderRejectFinal
@ -466,24 +192,6 @@ export default hocAuth(function Page(props: CommonComponent) {
</View> </View>
<SafeArea position={"bottom"} /> <SafeArea position={"bottom"} />
</View> </View>
</>
{/* 暂存确认对话框 */}
<Dialog
visible={saveDialogVisible}
title="确认暂存"
content="确定要暂存当前采购订单吗?"
onCancel={() => setSaveDialogVisible(false)}
onConfirm={confirmSave}
/>
{/* 提交审核确认对话框 */}
<Dialog
visible={submitDialogVisible}
title="提交审核"
content="确定要提交给老板审核吗?"
onCancel={() => setSubmitDialogVisible(false)}
onConfirm={confirmSubmit}
/>
</View>
); );
}); });

View File

@ -168,7 +168,9 @@ export default hocAuth(function Page(props: CommonComponent) {
<View className="flex flex-row justify-between"> <View className="flex flex-row justify-between">
<Text className="text-sm text-gray-600">:</Text> <Text className="text-sm text-gray-600">:</Text>
<Text className="text-sm font-medium"> <Text className="text-sm font-medium">
{purchaseOrder?.orderVehicle?.vehicleNo || "-"} {purchaseOrder?.orderVehicle?.vehicleNo
? "第" + purchaseOrder?.orderVehicle?.vehicleNo + "车"
: "暂未生成车次"}
</Text> </Text>
</View> </View>

View File

@ -160,8 +160,9 @@ export default hocAuth(function Page(props: CommonComponent) {
<Text <Text
className={"text-neutral-darkest text-xl font-bold"} className={"text-neutral-darkest text-xl font-bold"}
> >
{purchaseOrderVO.orderVehicle?.vehicleNo || {purchaseOrderVO.orderVehicle?.vehicleNo
"暂未生成车次"} ? "第" + purchaseOrderVO.orderVehicle?.vehicleNo + "车"
: "暂未生成车次"}
</Text> </Text>
</View> </View>
<Text className={"text-neutral-dark text-sm"}> <Text className={"text-neutral-dark text-sm"}>

View File

@ -53,8 +53,9 @@ export default hocAuth(function Page(props: CommonComponent) {
<Text <Text
className={"text-neutral-darkest text-xl font-bold"} className={"text-neutral-darkest text-xl font-bold"}
> >
{purchaseOrderVO.orderVehicle?.vehicleNo || {purchaseOrderVO.orderVehicle?.vehicleNo
"暂未生成车次"} ? "第" + purchaseOrderVO.orderVehicle?.vehicleNo + "车"
: "暂未生成车次"}
</Text> </Text>
</View> </View>
<Text className={"text-neutral-dark text-sm"}> <Text className={"text-neutral-dark text-sm"}>

View File

@ -84,8 +84,9 @@ export default hocAuth(function Page(props: CommonComponent) {
<Text <Text
className={"text-neutral-darkest text-xl font-bold"} className={"text-neutral-darkest text-xl font-bold"}
> >
{purchaseOrderVO.orderVehicle?.vehicleNo || {purchaseOrderVO.orderVehicle?.vehicleNo
"暂未生成车次"} ? "第" + purchaseOrderVO.orderVehicle?.vehicleNo + "车"
: "暂未生成车次"}
</Text> </Text>
</View> </View>
<Text className={"text-neutral-dark text-sm"}> <Text className={"text-neutral-dark text-sm"}>

View File

@ -160,8 +160,9 @@ export default hocAuth(function Page(props: CommonComponent) {
<Text <Text
className={"text-neutral-darkest text-xl font-bold"} className={"text-neutral-darkest text-xl font-bold"}
> >
{purchaseOrderVO.orderVehicle?.vehicleNo || {purchaseOrderVO.orderVehicle?.vehicleNo
"暂未生成车次"} ? "第" + purchaseOrderVO.orderVehicle?.vehicleNo + "车"
: "暂未生成车次"}
</Text> </Text>
</View> </View>
<Text className={"text-neutral-dark text-sm"}> <Text className={"text-neutral-dark text-sm"}>

View File

@ -31,6 +31,7 @@ import {
TaxSubsidySection, TaxSubsidySection,
} from "@/components"; } from "@/components";
import { getPersonalProfit } from "@/utils/calcutePurchaseOrder"; import { getPersonalProfit } from "@/utils/calcutePurchaseOrder";
import buildUrl from "@/utils/buildUrl";
const sections = { const sections = {
// 市场报价 // 市场报价
@ -248,10 +249,14 @@ export default hocAuth(function Page(props: CommonComponent) {
Toast.show("toast", { Toast.show("toast", {
icon: "success", icon: "success",
title: "提示", title: "提示",
content: "暂存成功", content: "提交审核成功",
});
// 跳转到提交结果页面
Taro.redirectTo({
url: buildUrl("/pages/purchase/reviewer/submitted", {
orderId: purchaseOrderVO!.orderId
})
}); });
// 返回采购单页面
Taro.redirectTo({ url: "/pages/purchase/reviewer/list" });
} }
}; };
@ -452,7 +457,7 @@ export default hocAuth(function Page(props: CommonComponent) {
{/* 按钮操作 */} {/* 按钮操作 */}
<View className={"sticky bottom-0 z-10 bg-white"} id={"bottomBar"}> <View className={"sticky bottom-0 z-10 bg-white"} id={"bottomBar"}>
<View className="flex justify-between gap-2 border-t border-gray-200 p-2.5"> <View className="flex justify-between gap-2 border-t border-gray-200 p-2.5">
{role === "reviewer" && purchaseOrderVO.state === "WAITING_AUDIT" && ( {purchaseOrderVO.state === "WAITING_AUDIT" && (
<> <>
<View className={"flex-1"}> <View className={"flex-1"}>
<PurchaseOrderRejectApprove <PurchaseOrderRejectApprove
@ -488,6 +493,22 @@ export default hocAuth(function Page(props: CommonComponent) {
</View> </View>
</> </>
)} )}
{purchaseOrderVO.state === "WAITING_BOSS_APPROVE" && (
<>
<View className={"flex-1"}>
<Button
block
type={"default"}
size={"xlarge"}
onClick={() => {
Taro.redirectTo({ url: "/pages/main/index/index" });
}}
>
</Button>
</View>
</>
)}
</View> </View>
<SafeArea position={"bottom"} /> <SafeArea position={"bottom"} />
</View> </View>

View File

@ -160,8 +160,11 @@ export default hocAuth(function Page(props: CommonComponent) {
<Text <Text
className={"text-neutral-darkest text-xl font-bold"} className={"text-neutral-darkest text-xl font-bold"}
> >
{purchaseOrderVO.orderVehicle?.vehicleNo || {purchaseOrderVO.orderVehicle?.vehicleNo
"暂未生成车次"} ? "第" +
purchaseOrderVO.orderVehicle?.vehicleNo +
"车"
: "暂未生成车次"}
</Text> </Text>
</View> </View>
<Text className={"text-neutral-dark text-sm"}> <Text className={"text-neutral-dark text-sm"}>
@ -300,6 +303,24 @@ export default hocAuth(function Page(props: CommonComponent) {
</Button> </Button>
</View> </View>
)} )}
{purchaseOrderVO.state === "WAITING_BOSS_APPROVE" && (
<View className={"flex flex-row justify-end gap-2"}>
<Button
type={"primary"}
size={"small"}
onClick={(e) => {
Taro.navigateTo({
url: buildUrl("/pages/purchase/reviewer/submitted", {
orderId: purchaseOrderVO.orderId,
}),
});
e.stopPropagation();
}}
>
</Button>
</View>
)}
</View> </View>
</View> </View>
</View> </View>

View File

@ -53,8 +53,9 @@ export default hocAuth(function Page(props: CommonComponent) {
<Text <Text
className={"text-neutral-darkest text-xl font-bold"} className={"text-neutral-darkest text-xl font-bold"}
> >
{purchaseOrderVO.orderVehicle?.vehicleNo || {purchaseOrderVO.orderVehicle?.vehicleNo
"暂未生成车次"} ? "第" + purchaseOrderVO.orderVehicle?.vehicleNo + "车"
: "暂未生成车次"}
</Text> </Text>
</View> </View>
<Text className={"text-neutral-dark text-sm"}> <Text className={"text-neutral-dark text-sm"}>

View File

@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: "提交审核结果",
navigationBarBackgroundColor: "#fff",
});

View File

@ -0,0 +1,151 @@
import hocAuth from "@/hocs/auth";
import { CommonComponent } from "@/types/typings";
import { useEffect, useState } from "react";
import { Text, View } from "@tarojs/components";
import { Button, SafeArea, Toast } from "@nutui/nutui-react-taro";
import Taro from "@tarojs/taro";
import { business } from "@/services";
import buildUrl from "@/utils/buildUrl";
export default hocAuth(function Page(props: CommonComponent) {
const { router, setLoading } = props;
const orderId = router.params.orderId as string;
const [purchaseOrder, setPurchaseOrder] =
useState<BusinessAPI.PurchaseOrderVO>();
const init = async (orderId: string) => {
setLoading(true);
try {
// 获取采购单信息
const { data: purchaseData } =
await business.purchaseOrder.showPurchaseOrder({
purchaseOrderShowQry: {
orderId: orderId,
},
});
if (purchaseData.success) {
setPurchaseOrder(purchaseData.data);
}
} catch (error) {
Toast.show("toast", {
icon: "fail",
title: "提示",
content: "获取采购单信息失败",
});
} finally {
setLoading(false);
}
};
useEffect(() => {
if (orderId) {
init(orderId);
}
}, [orderId]);
// 查看采购单详情
const viewPurchaseOrderDetail = () => {
if (purchaseOrder?.orderId) {
Taro.navigateTo({
url: buildUrl("/pages/purchase/reviewer/audit", {
orderId: purchaseOrder.orderId,
}),
});
}
};
return (
<View className="flex flex-1 flex-col gap-2.5">
<View className="flex flex-1 flex-col items-center justify-start bg-gray-100 p-2.5">
<View className="mb-2.5 flex h-16 w-16 items-center justify-center rounded-full bg-green-100">
<Text className="text-2xl text-green-600"></Text>
</View>
<View className="mb-2.5 text-xl font-bold text-gray-800">
</View>
<View className="mb-2.5 text-sm text-gray-600"></View>
<View className="mb-2.5 text-xs text-gray-400">
</View>
<View className="w-full rounded-lg bg-white p-2.5 shadow-md">
<View className="mb-2.5 border-b border-gray-200 pb-2">
<Text className="text-lg font-semibold"></Text>
</View>
<View className="mb-2.5 flex flex-col gap-2.5">
<View className="flex flex-row justify-between">
<Text className="text-sm text-gray-600">:</Text>
<Text className="text-sm font-medium">
{purchaseOrder?.orderSn || "-"}
</Text>
</View>
<View className="flex flex-row justify-between">
<Text className="text-sm text-gray-600">:</Text>
<Text className="text-sm font-medium">
{purchaseOrder?.orderVehicle?.vehicleNo || "-"}
</Text>
</View>
<View className="flex flex-row justify-between">
<Text className="text-sm text-gray-600">:</Text>
<Text className="text-sm font-medium">
{purchaseOrder?.orderVehicle?.dealerName || "-"}
</Text>
</View>
<View className="flex flex-row justify-between">
<Text className="text-sm text-gray-600">:</Text>
<Text className="text-sm font-medium text-green-600">
</Text>
</View>
</View>
<View className="border-t border-gray-200 pt-2.5">
<View className="mb-2">
<Text className="text-lg font-semibold"></Text>
</View>
<View className="flex flex-col gap-3">
<Button
type="primary"
size={"xlarge"}
block
onClick={viewPurchaseOrderDetail}
>
</Button>
</View>
</View>
</View>
</View>
<View className={"sticky bottom-0 z-10 bg-white"}>
<View className="flex justify-between gap-2 border-t border-gray-200 p-2.5">
<View className="flex-1">
<Button
type="default"
size={"xlarge"}
block
onClick={() =>
Taro.switchTab({
url: buildUrl("/pages/main/index/index"),
})
}
>
</Button>
</View>
</View>
<SafeArea position={"bottom"} />
</View>
</View>
);
});

View File

@ -85,6 +85,26 @@ export async function finalApprovePurchaseOrder(
}); });
} }
/** 获取上一车车次号 GET /operation/getLastVehicleNo */
export async function getLastVehicleNo(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: BusinessAPI.getLastVehicleNoParams,
options?: { [key: string]: any },
) {
return request<BusinessAPI.SingleResponseString>(
"/operation/getLastVehicleNo",
{
method: "GET",
params: {
...params,
lastVehicleNoQry: undefined,
...params["lastVehicleNoQry"],
},
...(options || {}),
},
);
}
/** 采购订单列表 GET /operation/listPurchaseOrder */ /** 采购订单列表 GET /operation/listPurchaseOrder */
export async function listPurchaseOrder( export async function listPurchaseOrder(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象) // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)

View File

@ -1688,6 +1688,10 @@ declare namespace BusinessAPI {
userRoleList?: UserRoleVO[]; userRoleList?: UserRoleVO[];
}; };
type getLastVehicleNoParams = {
lastVehicleNoQry: LastVehicleNoQry;
};
type GiftBoxCreateCmd = { type GiftBoxCreateCmd = {
/** 礼盒ID */ /** 礼盒ID */
boxId: string; boxId: string;
@ -1786,6 +1790,11 @@ declare namespace BusinessAPI {
createdAt?: string; createdAt?: string;
}; };
type LastVehicleNoQry = {
/** 状态1_启用0_禁用 */
status?: boolean;
};
type listAgreementParams = { type listAgreementParams = {
agreementListQry: AgreementListQry; agreementListQry: AgreementListQry;
}; };
@ -4020,6 +4029,13 @@ declare namespace BusinessAPI {
data?: ShipOrderVO; data?: ShipOrderVO;
}; };
type SingleResponseString = {
success?: boolean;
errCode?: string;
errMessage?: string;
data?: string;
};
type SingleResponseSupplierVO = { type SingleResponseSupplierVO = {
success?: boolean; success?: boolean;
errCode?: string; errCode?: string;

View File

@ -26,7 +26,7 @@ export function calculateSupplierWeights(suppliers) {
// 计算本次使用纸箱的总重量(斤) // 计算本次使用纸箱的总重量(斤)
const usedBoxesWeight = calculateBoxesTotalWeight(supplier.orderPackageList, 'USED') + calculateBoxesTotalWeight(supplier.orderPackageList, 'OWN'); const usedBoxesWeight = calculateBoxesTotalWeight(supplier.orderPackageList, 'USED') + calculateBoxesTotalWeight(supplier.orderPackageList, 'OWN');
console.log("usedBoxesWeight", usedBoxesWeight, supplier)
if (!supplier.isPaper) { if (!supplier.isPaper) {
// 如果不是纸箱包装直接使用原始重量kg转斤 // 如果不是纸箱包装直接使用原始重量kg转斤
supplier.grossWeight = (supplier.totalWeight - supplier.emptyWeight) * 2; supplier.grossWeight = (supplier.totalWeight - supplier.emptyWeight) * 2;

File diff suppressed because one or more lines are too long