feat: 初始化订单审批系统项目- 创建项目基础结构和配置文件
- 添加全局样式和工具函数 - 实现订单状态和审批状态的格式化及颜色处理- 添加金额和日期格式化函数 - 实现防抖和节流函数 - 添加微信小程序环境检查和文件预览URL生成函数 - 实现复制到剪贴板功能 - 添加项目路由配置 - 实现HTTP客户端和拦截器 - 添加订单、审批记录等类型定义master
commit
781d598ae7
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(npm install)",
|
||||||
|
"Bash(npm run dev:*)"
|
||||||
|
],
|
||||||
|
"deny": [],
|
||||||
|
"ask": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
VITE_API_BASE_URL=http://localhost:8080
|
||||||
|
VITE_APP_TITLE=订单审批系统
|
||||||
|
VITE_FILE_BASE_URL=http://localhost:8080
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
VITE_API_BASE_URL=http://localhost:8080
|
||||||
|
VITE_APP_TITLE=订单审批系统
|
||||||
|
VITE_FILE_BASE_URL=http://localhost:8080
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
VITE_API_BASE_URL=https://your-api-domain.com
|
||||||
|
VITE_APP_TITLE=订单审批系统
|
||||||
|
VITE_FILE_BASE_URL=https://your-api-domain.com
|
||||||
|
|
@ -0,0 +1,203 @@
|
||||||
|
# 微信小程序H5审批系统
|
||||||
|
|
||||||
|
基于Vue 3 + TypeScript + Vant 4开发的移动端H5应用,主要用于在微信小程序WebView中运行的订单审批系统。
|
||||||
|
|
||||||
|
## 项目特性
|
||||||
|
|
||||||
|
- 🚀 Vue 3 + TypeScript + Vite 开发
|
||||||
|
- 📱 Vant 4 移动端组件库
|
||||||
|
- 🎯 专为微信小程序WebView优化
|
||||||
|
- 📋 订单列表查看和搜索
|
||||||
|
- 📄 订单详情展示
|
||||||
|
- ✅ 审批操作功能
|
||||||
|
- 📊 审批历史记录
|
||||||
|
- 📎 附件预览功能
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- **前端框架**: Vue 3
|
||||||
|
- **开发语言**: TypeScript
|
||||||
|
- **构建工具**: Vite
|
||||||
|
- **UI组件库**: Vant 4
|
||||||
|
- **路由管理**: Vue Router 4
|
||||||
|
- **状态管理**: Pinia
|
||||||
|
- **HTTP客户端**: Axios
|
||||||
|
- **样式预处理**: Sass
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
oms_h5/
|
||||||
|
├── public/ # 静态资源
|
||||||
|
├── src/
|
||||||
|
│ ├── api/ # API接口
|
||||||
|
│ ├── components/ # 公共组件
|
||||||
|
│ ├── views/ # 页面组件
|
||||||
|
│ │ ├── List/ # 列表页面
|
||||||
|
│ │ └── Detail/ # 详情页面
|
||||||
|
│ ├── store/ # 状态管理
|
||||||
|
│ ├── utils/ # 工具函数
|
||||||
|
│ ├── types/ # TypeScript类型定义
|
||||||
|
│ ├── styles/ # 全局样式
|
||||||
|
│ └── router/ # 路由配置
|
||||||
|
├── docs/ # 文档和设计稿
|
||||||
|
│ └── ui-mockups/ # UI设计图
|
||||||
|
├── package.json
|
||||||
|
└── vite.config.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## 开发环境搭建
|
||||||
|
|
||||||
|
### 环境要求
|
||||||
|
|
||||||
|
- Node.js >= 16
|
||||||
|
- npm >= 7 或 yarn >= 1.22
|
||||||
|
|
||||||
|
### 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
# 或
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 启动开发服务器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# 或
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 构建生产版本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
# 或
|
||||||
|
yarn build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 预览构建结果
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run preview
|
||||||
|
# 或
|
||||||
|
yarn preview
|
||||||
|
```
|
||||||
|
|
||||||
|
## 环境配置
|
||||||
|
|
||||||
|
项目支持多环境配置,通过`.env`文件管理:
|
||||||
|
|
||||||
|
- `.env` - 通用环境变量
|
||||||
|
- `.env.development` - 开发环境
|
||||||
|
- `.env.production` - 生产环境
|
||||||
|
|
||||||
|
主要配置项:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
VITE_API_BASE_URL=http://localhost:8080 # API基础URL
|
||||||
|
VITE_APP_TITLE=订单审批系统 # 应用标题
|
||||||
|
VITE_FILE_BASE_URL=http://localhost:8080 # 文件服务URL
|
||||||
|
```
|
||||||
|
|
||||||
|
## 主要功能
|
||||||
|
|
||||||
|
### 1. 订单列表
|
||||||
|
- ✅ 订单列表展示
|
||||||
|
- ✅ 下拉刷新和上拉加载
|
||||||
|
- ✅ 搜索功能
|
||||||
|
- ✅ 状态筛选
|
||||||
|
- ✅ 订单状态标识
|
||||||
|
|
||||||
|
### 2. 订单详情
|
||||||
|
- ✅ 订单基本信息展示
|
||||||
|
- ✅ 产品信息列表(软件/硬件/维保)
|
||||||
|
- ✅ 合同附件展示和预览
|
||||||
|
- ✅ 审批历史时间线
|
||||||
|
|
||||||
|
### 3. 审批操作
|
||||||
|
- ✅ 审批通过/驳回
|
||||||
|
- ✅ 审批意见输入
|
||||||
|
- ✅ 操作结果反馈
|
||||||
|
|
||||||
|
### 4. 微信小程序适配
|
||||||
|
- ✅ WebView环境检测
|
||||||
|
- ✅ 移动端样式适配
|
||||||
|
- ✅ 触摸手势支持
|
||||||
|
|
||||||
|
## API接口
|
||||||
|
|
||||||
|
### 订单列表接口
|
||||||
|
```
|
||||||
|
POST /project/order/list
|
||||||
|
```
|
||||||
|
|
||||||
|
### 订单详情接口
|
||||||
|
```
|
||||||
|
GET /project/order/h5/approve/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
### 审批操作接口
|
||||||
|
```
|
||||||
|
POST /project/order/order/approve
|
||||||
|
```
|
||||||
|
|
||||||
|
## 开发规范
|
||||||
|
|
||||||
|
### 代码风格
|
||||||
|
项目使用ESLint + Prettier进行代码规范检查和格式化。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run lint # 代码检查
|
||||||
|
npm run type-check # 类型检查
|
||||||
|
```
|
||||||
|
|
||||||
|
### Git提交规范
|
||||||
|
建议使用Angular提交信息规范:
|
||||||
|
|
||||||
|
```
|
||||||
|
feat: 新增功能
|
||||||
|
fix: 修复问题
|
||||||
|
docs: 文档更新
|
||||||
|
style: 代码格式调整
|
||||||
|
refactor: 代码重构
|
||||||
|
test: 测试相关
|
||||||
|
chore: 构建/工具链更新
|
||||||
|
```
|
||||||
|
|
||||||
|
## 部署说明
|
||||||
|
|
||||||
|
### 生产构建
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
构建完成后,`dist`目录包含所有静态资源文件。
|
||||||
|
|
||||||
|
### 服务器配置
|
||||||
|
由于是SPA应用,需要配置服务器将所有路由指向`index.html`。
|
||||||
|
|
||||||
|
#### Nginx配置示例
|
||||||
|
```nginx
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 微信小程序配置
|
||||||
|
1. 在微信小程序管理后台配置业务域名
|
||||||
|
2. 确保HTTPS协议部署
|
||||||
|
3. 配置webview组件的src属性
|
||||||
|
|
||||||
|
## 浏览器兼容性
|
||||||
|
|
||||||
|
- Chrome >= 64
|
||||||
|
- Firefox >= 78
|
||||||
|
- Safari >= 12
|
||||||
|
- 微信内置浏览器
|
||||||
|
- 各主流移动端浏览器
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
/* prettier-ignore */
|
||||||
|
// @ts-nocheck
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
// Generated by unplugin-auto-import
|
||||||
|
export {}
|
||||||
|
declare global {
|
||||||
|
const EffectScope: typeof import('vue')['EffectScope']
|
||||||
|
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
|
||||||
|
const computed: typeof import('vue')['computed']
|
||||||
|
const createApp: typeof import('vue')['createApp']
|
||||||
|
const createPinia: typeof import('pinia')['createPinia']
|
||||||
|
const customRef: typeof import('vue')['customRef']
|
||||||
|
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||||
|
const defineComponent: typeof import('vue')['defineComponent']
|
||||||
|
const defineStore: typeof import('pinia')['defineStore']
|
||||||
|
const effectScope: typeof import('vue')['effectScope']
|
||||||
|
const getActivePinia: typeof import('pinia')['getActivePinia']
|
||||||
|
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||||
|
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||||
|
const h: typeof import('vue')['h']
|
||||||
|
const inject: typeof import('vue')['inject']
|
||||||
|
const isProxy: typeof import('vue')['isProxy']
|
||||||
|
const isReactive: typeof import('vue')['isReactive']
|
||||||
|
const isReadonly: typeof import('vue')['isReadonly']
|
||||||
|
const isRef: typeof import('vue')['isRef']
|
||||||
|
const mapActions: typeof import('pinia')['mapActions']
|
||||||
|
const mapGetters: typeof import('pinia')['mapGetters']
|
||||||
|
const mapState: typeof import('pinia')['mapState']
|
||||||
|
const mapStores: typeof import('pinia')['mapStores']
|
||||||
|
const mapWritableState: typeof import('pinia')['mapWritableState']
|
||||||
|
const markRaw: typeof import('vue')['markRaw']
|
||||||
|
const nextTick: typeof import('vue')['nextTick']
|
||||||
|
const onActivated: typeof import('vue')['onActivated']
|
||||||
|
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||||
|
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
||||||
|
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
|
||||||
|
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||||
|
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||||
|
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||||
|
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||||
|
const onMounted: typeof import('vue')['onMounted']
|
||||||
|
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||||
|
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||||
|
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||||
|
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||||
|
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||||
|
const onUpdated: typeof import('vue')['onUpdated']
|
||||||
|
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
||||||
|
const provide: typeof import('vue')['provide']
|
||||||
|
const reactive: typeof import('vue')['reactive']
|
||||||
|
const readonly: typeof import('vue')['readonly']
|
||||||
|
const ref: typeof import('vue')['ref']
|
||||||
|
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||||
|
const setActivePinia: typeof import('pinia')['setActivePinia']
|
||||||
|
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
|
||||||
|
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||||
|
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||||
|
const shallowRef: typeof import('vue')['shallowRef']
|
||||||
|
const storeToRefs: typeof import('pinia')['storeToRefs']
|
||||||
|
const toRaw: typeof import('vue')['toRaw']
|
||||||
|
const toRef: typeof import('vue')['toRef']
|
||||||
|
const toRefs: typeof import('vue')['toRefs']
|
||||||
|
const toValue: typeof import('vue')['toValue']
|
||||||
|
const triggerRef: typeof import('vue')['triggerRef']
|
||||||
|
const unref: typeof import('vue')['unref']
|
||||||
|
const useAttrs: typeof import('vue')['useAttrs']
|
||||||
|
const useCssModule: typeof import('vue')['useCssModule']
|
||||||
|
const useCssVars: typeof import('vue')['useCssVars']
|
||||||
|
const useId: typeof import('vue')['useId']
|
||||||
|
const useLink: typeof import('vue-router')['useLink']
|
||||||
|
const useModel: typeof import('vue')['useModel']
|
||||||
|
const useRoute: typeof import('vue-router')['useRoute']
|
||||||
|
const useRouter: typeof import('vue-router')['useRouter']
|
||||||
|
const useSlots: typeof import('vue')['useSlots']
|
||||||
|
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
||||||
|
const watch: typeof import('vue')['watch']
|
||||||
|
const watchEffect: typeof import('vue')['watchEffect']
|
||||||
|
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||||
|
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||||
|
}
|
||||||
|
// for type re-export
|
||||||
|
declare global {
|
||||||
|
// @ts-ignore
|
||||||
|
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||||
|
import('vue')
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
/* prettier-ignore */
|
||||||
|
// @ts-nocheck
|
||||||
|
// Generated by unplugin-vue-components
|
||||||
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
|
export {}
|
||||||
|
|
||||||
|
declare module 'vue' {
|
||||||
|
export interface GlobalComponents {
|
||||||
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
VanButton: typeof import('vant/es')['Button']
|
||||||
|
VanEmpty: typeof import('vant/es')['Empty']
|
||||||
|
VanField: typeof import('vant/es')['Field']
|
||||||
|
VanIcon: typeof import('vant/es')['Icon']
|
||||||
|
VanList: typeof import('vant/es')['List']
|
||||||
|
VanLoading: typeof import('vant/es')['Loading']
|
||||||
|
VanNavBar: typeof import('vant/es')['NavBar']
|
||||||
|
VanPopup: typeof import('vant/es')['Popup']
|
||||||
|
VanPullRefresh: typeof import('vant/es')['PullRefresh']
|
||||||
|
VanSearch: typeof import('vant/es')['Search']
|
||||||
|
VanStep: typeof import('vant/es')['Step']
|
||||||
|
VanSteps: typeof import('vant/es')['Steps']
|
||||||
|
VanTab: typeof import('vant/es')['Tab']
|
||||||
|
VanTabs: typeof import('vant/es')['Tabs']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
# UI设计图存放目录
|
||||||
|
|
||||||
|
此目录用于存放项目相关的UI设计图和原型图。
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
ui-mockups/
|
||||||
|
├── README.md # 本文件
|
||||||
|
├── wireframes/ # 线框图
|
||||||
|
│ ├── list-page.png # 列表页面线框图
|
||||||
|
│ └── detail-page.png # 详情页面线框图
|
||||||
|
├── designs/ # 设计稿
|
||||||
|
│ ├── list-page.png # 列表页面设计稿
|
||||||
|
│ └── detail-page.png # 详情页面设计稿
|
||||||
|
├── prototypes/ # 交互原型
|
||||||
|
└── specifications/ # 设计规范
|
||||||
|
└── style-guide.md # 样式指南
|
||||||
|
```
|
||||||
|
|
||||||
|
## 设计要求
|
||||||
|
|
||||||
|
### 1. 移动端适配
|
||||||
|
- 屏幕宽度:375px - 414px
|
||||||
|
- 适配iPhone和Android主流设备
|
||||||
|
- 支持横竖屏切换
|
||||||
|
|
||||||
|
### 2. 微信小程序WebView适配
|
||||||
|
- 适配微信小程序导航栏高度
|
||||||
|
- 考虑安全区域(刘海屏等)
|
||||||
|
- 遵循微信小程序设计规范
|
||||||
|
|
||||||
|
### 3. 页面设计规范
|
||||||
|
|
||||||
|
#### 列表页面
|
||||||
|
- 顶部搜索栏
|
||||||
|
- 筛选条件(状态筛选)
|
||||||
|
- 工单卡片列表
|
||||||
|
- 下拉刷新、上拉加载更多
|
||||||
|
- 空状态提示
|
||||||
|
|
||||||
|
#### 详情页面
|
||||||
|
- 工单基本信息展示
|
||||||
|
- 产品信息列表
|
||||||
|
- 附件展示区域
|
||||||
|
- 审批历史时间线
|
||||||
|
- 底部审批操作按钮
|
||||||
|
|
||||||
|
### 4. 交互设计
|
||||||
|
- 页面切换动画
|
||||||
|
- 加载状态提示
|
||||||
|
- 操作反馈(成功/失败提示)
|
||||||
|
- 确认弹窗设计
|
||||||
|
|
||||||
|
### 5. 视觉设计
|
||||||
|
- 主色调:#1890ff(蓝色)
|
||||||
|
- 辅助色:#52c41a(成功绿)、#f5222d(错误红)
|
||||||
|
- 背景色:#f5f5f5
|
||||||
|
- 文字颜色:#333333(主要)、#666666(次要)、#999999(辅助)
|
||||||
|
|
||||||
|
## 文件命名规范
|
||||||
|
|
||||||
|
- 使用英文命名,单词间用连字符分隔
|
||||||
|
- 包含页面/组件名称和版本号
|
||||||
|
- 示例:`order-list-v1.0.png`、`approval-dialog-v2.1.png`
|
||||||
|
|
||||||
|
## 版本管理
|
||||||
|
|
||||||
|
每次设计更新时请:
|
||||||
|
1. 更新版本号
|
||||||
|
2. 在文件名中体现版本
|
||||||
|
3. 保留历史版本以便回溯
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 281 KiB |
|
|
@ -0,0 +1,30 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no" />
|
||||||
|
<meta name="format-detection" content="telephone=no" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
|
<title>订单审批系统</title>
|
||||||
|
<script>
|
||||||
|
// 防止页面缩放
|
||||||
|
document.addEventListener('touchstart', function (event) {
|
||||||
|
if (event.touches.length > 1) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 微信小程序webview适配
|
||||||
|
if (window.__wxjs_environment === 'miniprogram') {
|
||||||
|
// 微信小程序环境
|
||||||
|
console.log('Running in WeChat MiniProgram WebView');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"name": "oms-h5",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"description": "微信小程序H5审批系统",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||||
|
"type-check": "vue-tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"vue": "^3.3.4",
|
||||||
|
"vue-router": "^4.2.4",
|
||||||
|
"pinia": "^2.1.6",
|
||||||
|
"vant": "^4.6.6",
|
||||||
|
"axios": "^1.5.0",
|
||||||
|
"@vant/touch-emulator": "^1.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.5.9",
|
||||||
|
"@vitejs/plugin-vue": "^4.3.4",
|
||||||
|
"@vue/eslint-config-prettier": "^8.0.0",
|
||||||
|
"@vue/eslint-config-typescript": "^11.0.3",
|
||||||
|
"@vue/tsconfig": "^0.4.0",
|
||||||
|
"eslint": "^8.47.0",
|
||||||
|
"eslint-plugin-vue": "^9.17.0",
|
||||||
|
"prettier": "^3.0.2",
|
||||||
|
"sass": "^1.66.1",
|
||||||
|
"typescript": "~5.1.6",
|
||||||
|
"vite": "^4.4.9",
|
||||||
|
"vue-tsc": "^1.8.8",
|
||||||
|
"unplugin-auto-import": "^0.16.6",
|
||||||
|
"unplugin-vue-components": "^0.25.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// App根组件
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
#app {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: var(--van-background-color);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
import http from '@/utils/http'
|
||||||
|
import type { ApiResponse, Order, OrderDetailResponse, ListParams, ApprovalParams } from '@/types'
|
||||||
|
import type { AxiosResponse } from 'axios'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订单列表
|
||||||
|
*/
|
||||||
|
export const getOrderList = (params: ListParams): Promise<AxiosResponse<ApiResponse<{
|
||||||
|
total: number
|
||||||
|
rows: Order[]
|
||||||
|
}>>> => {
|
||||||
|
// 创建FormData对象
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
// 添加参数到FormData
|
||||||
|
if (params.approve) formData.append('approve', params.approve)
|
||||||
|
formData.append('page', params.page.toString())
|
||||||
|
formData.append('pageSize', params.pageSize.toString())
|
||||||
|
if (params.keyword) formData.append('keyword', params.keyword)
|
||||||
|
|
||||||
|
return http.post('/project/order/list', formData)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订单详情
|
||||||
|
*/
|
||||||
|
export const getOrderDetail = (id: string | number): Promise<AxiosResponse<ApiResponse<OrderDetailResponse>>> => {
|
||||||
|
return http.get(`/project/order/h5/approve/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交审批结果
|
||||||
|
*/
|
||||||
|
export const submitApproval = (params: any): Promise<AxiosResponse<ApiResponse<any>>> => {
|
||||||
|
// 创建FormData对象
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
// 将所有参数添加到FormData中
|
||||||
|
Object.keys(params).forEach(key => {
|
||||||
|
if (params[key] !== undefined && params[key] !== null) {
|
||||||
|
formData.append(key, params[key].toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return http.post('/project/order/order/approve', formData)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare module '*.vue' {
|
||||||
|
import type { DefineComponent } from 'vue'
|
||||||
|
const component: DefineComponent<{}, {}, any>
|
||||||
|
export default component
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_API_BASE_URL: string
|
||||||
|
readonly VITE_APP_TITLE: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
|
// Vant样式
|
||||||
|
import 'vant/lib/index.css'
|
||||||
|
// 触摸模拟器 (开发环境使用)
|
||||||
|
import '@vant/touch-emulator'
|
||||||
|
|
||||||
|
// 全局样式
|
||||||
|
import '@/styles/index.scss'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.use(createPinia())
|
||||||
|
app.use(router)
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
redirect: '/list'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/list',
|
||||||
|
name: 'OrderList',
|
||||||
|
component: () => import('@/views/List/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '订单列表'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/detail/:id',
|
||||||
|
name: 'OrderDetail',
|
||||||
|
component: () => import('@/views/Detail/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '订单详情'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes,
|
||||||
|
scrollBehavior() {
|
||||||
|
return { top: 0 }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 全局路由守卫
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
// 设置页面标题
|
||||||
|
if (to.meta?.title) {
|
||||||
|
document.title = to.meta.title as string
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
|
|
@ -0,0 +1,159 @@
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import type { Order, OrderDetailResponse, ListParams } from '@/types'
|
||||||
|
import { getOrderList, getOrderDetail } from '@/api/order'
|
||||||
|
|
||||||
|
interface OrderState {
|
||||||
|
// 列表相关
|
||||||
|
orderList: Order[]
|
||||||
|
loading: boolean
|
||||||
|
finished: boolean
|
||||||
|
currentPage: number
|
||||||
|
pageSize: number
|
||||||
|
total: number
|
||||||
|
keyword: string
|
||||||
|
|
||||||
|
// 详情相关
|
||||||
|
currentOrder: any | null // 临时改为any来避免类型问题
|
||||||
|
detailLoading: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useOrderStore = defineStore('order', {
|
||||||
|
state: (): OrderState => ({
|
||||||
|
orderList: [],
|
||||||
|
loading: false,
|
||||||
|
finished: false,
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 0,
|
||||||
|
keyword: '',
|
||||||
|
|
||||||
|
currentOrder: null,
|
||||||
|
detailLoading: false
|
||||||
|
}),
|
||||||
|
|
||||||
|
getters: {
|
||||||
|
// 获取当前订单基本信息
|
||||||
|
currentOrderInfo: (state) => state.currentOrder?.projectOrderInfo,
|
||||||
|
|
||||||
|
// 获取审批历史
|
||||||
|
approvalHistory: (state) => state.currentOrder?.approveLog || [],
|
||||||
|
|
||||||
|
// 获取当前用户信息
|
||||||
|
currentUser: (state) => state.currentOrder?.user,
|
||||||
|
|
||||||
|
// 检查是否还有更多数据
|
||||||
|
hasMore: (state) => state.orderList.length < state.total
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
/**
|
||||||
|
* 加载订单列表
|
||||||
|
*/
|
||||||
|
async loadOrderList(refresh = false) {
|
||||||
|
if (this.loading) return
|
||||||
|
|
||||||
|
if (refresh) {
|
||||||
|
this.currentPage = 1
|
||||||
|
this.finished = false
|
||||||
|
this.orderList = []
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params: ListParams = {
|
||||||
|
approve:'approve',
|
||||||
|
page: this.currentPage,
|
||||||
|
pageSize: this.pageSize,
|
||||||
|
keyword: this.keyword || undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await getOrderList(params)
|
||||||
|
const { total, rows } = response.data
|
||||||
|
|
||||||
|
if (refresh) {
|
||||||
|
this.orderList = rows
|
||||||
|
} else {
|
||||||
|
this.orderList.push(...rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.total = total
|
||||||
|
this.currentPage++
|
||||||
|
|
||||||
|
// 判断是否已加载完所有数据
|
||||||
|
if (this.orderList.length >= total) {
|
||||||
|
this.finished = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载订单列表失败:', error)
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索订单
|
||||||
|
*/
|
||||||
|
async searchOrders(keyword: string) {
|
||||||
|
this.keyword = keyword
|
||||||
|
await this.loadOrderList(true)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订单详情
|
||||||
|
*/
|
||||||
|
async fetchOrderDetail(id: string | number) {
|
||||||
|
this.detailLoading = true
|
||||||
|
console.log('开始获取订单详情,ID:', id)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await getOrderDetail(id)
|
||||||
|
console.log('API响应:', response)
|
||||||
|
console.log('响应数据:', response.data)
|
||||||
|
console.log('实际数据:', response.data.data)
|
||||||
|
|
||||||
|
// 直接获取数据对象
|
||||||
|
const orderData = response.data.data
|
||||||
|
console.log('订单数据对象:', orderData)
|
||||||
|
|
||||||
|
// 确保数据存在再赋值
|
||||||
|
if (orderData) {
|
||||||
|
this.currentOrder = orderData
|
||||||
|
console.log('赋值后的存储数据:', this.currentOrder)
|
||||||
|
console.log('项目订单信息:', this.currentOrder.projectOrderInfo)
|
||||||
|
} else {
|
||||||
|
console.error('订单数据为空')
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取订单详情失败:', error)
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
this.detailLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空当前订单详情
|
||||||
|
*/
|
||||||
|
clearCurrentOrder() {
|
||||||
|
this.currentOrder = null
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置列表状态
|
||||||
|
*/
|
||||||
|
resetListState() {
|
||||||
|
this.orderList = []
|
||||||
|
this.currentPage = 1
|
||||||
|
this.finished = false
|
||||||
|
this.loading = false
|
||||||
|
this.keyword = ''
|
||||||
|
this.total = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,294 @@
|
||||||
|
// 全局CSS变量
|
||||||
|
:root {
|
||||||
|
// 主色系
|
||||||
|
--primary-color: #1890ff;
|
||||||
|
--primary-color-dark: #096dd9;
|
||||||
|
--primary-color-light: #e6f7ff;
|
||||||
|
|
||||||
|
// 功能色系
|
||||||
|
--success-color: #52c41a;
|
||||||
|
--warning-color: #faad14;
|
||||||
|
--error-color: #f5222d;
|
||||||
|
--info-color: #1890ff;
|
||||||
|
|
||||||
|
// 中性色系
|
||||||
|
--text-color-primary: #333333;
|
||||||
|
--text-color-secondary: #666666;
|
||||||
|
--text-color-tertiary: #999999;
|
||||||
|
--text-color-disabled: #cccccc;
|
||||||
|
|
||||||
|
--background-color-primary: #ffffff;
|
||||||
|
--background-color-secondary: #f5f5f5;
|
||||||
|
--background-color-tertiary: #fafafa;
|
||||||
|
|
||||||
|
--border-color: #d9d9d9;
|
||||||
|
--divider-color: #f0f0f0;
|
||||||
|
|
||||||
|
// 间距
|
||||||
|
--spacing-xs: 4px;
|
||||||
|
--spacing-sm: 8px;
|
||||||
|
--spacing-md: 12px;
|
||||||
|
--spacing-lg: 16px;
|
||||||
|
--spacing-xl: 20px;
|
||||||
|
--spacing-xxl: 24px;
|
||||||
|
|
||||||
|
// 圆角
|
||||||
|
--border-radius-sm: 4px;
|
||||||
|
--border-radius-md: 8px;
|
||||||
|
--border-radius-lg: 12px;
|
||||||
|
|
||||||
|
// 阴影
|
||||||
|
--shadow-light: 0 1px 3px rgba(0, 0, 0, 0.12);
|
||||||
|
--shadow-medium: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
--shadow-heavy: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置样式
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--text-color-primary);
|
||||||
|
background-color: var(--background-color-secondary);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 滚动条样式
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: var(--background-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--border-color);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--text-color-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用工具类
|
||||||
|
.text-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-center {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-between {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-column {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-1 {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflow-hidden {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-ellipsis {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-primary {
|
||||||
|
color: var(--primary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-success {
|
||||||
|
color: var(--success-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-warning {
|
||||||
|
color: var(--warning-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-error {
|
||||||
|
color: var(--error-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-secondary {
|
||||||
|
color: var(--text-color-secondary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-tertiary {
|
||||||
|
color: var(--text-color-tertiary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义状态标签样式
|
||||||
|
.status-tag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.pending {
|
||||||
|
background-color: #fff7e6;
|
||||||
|
color: var(--warning-color);
|
||||||
|
border: 1px solid #ffd591;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.approved {
|
||||||
|
background-color: #f6ffed;
|
||||||
|
color: var(--success-color);
|
||||||
|
border: 1px solid #b7eb8f;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rejected {
|
||||||
|
background-color: #fff2f0;
|
||||||
|
color: var(--error-color);
|
||||||
|
border: 1px solid #ffb3b3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面容器
|
||||||
|
.page-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: var(--background-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-content {
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 卡片样式
|
||||||
|
.card {
|
||||||
|
background: var(--background-color-primary);
|
||||||
|
border-radius: var(--border-radius-md);
|
||||||
|
box-shadow: var(--shadow-light);
|
||||||
|
margin-bottom: var(--spacing-lg);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列表项样式
|
||||||
|
.list-item {
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
background: var(--background-color-primary);
|
||||||
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: var(--background-color-tertiary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 空状态样式
|
||||||
|
.empty-state {
|
||||||
|
padding: 60px var(--spacing-lg);
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-color-tertiary);
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 64px;
|
||||||
|
color: var(--text-color-disabled);
|
||||||
|
margin-bottom: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载状态样式
|
||||||
|
.loading-container {
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vant组件自定义样式
|
||||||
|
.van-nav-bar {
|
||||||
|
background: var(--background-color-primary);
|
||||||
|
|
||||||
|
.van-nav-bar__title {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.van-search {
|
||||||
|
padding: var(--spacing-lg) var(--spacing-lg) 0;
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
.van-search__content {
|
||||||
|
background: var(--background-color-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.van-pull-refresh {
|
||||||
|
background: var(--background-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.van-list {
|
||||||
|
background: var(--background-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式适配
|
||||||
|
@media (max-width: 375px) {
|
||||||
|
.page-content {
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header,
|
||||||
|
.card-body {
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,190 @@
|
||||||
|
// 通用API响应类型
|
||||||
|
export interface ApiResponse<T = any> {
|
||||||
|
code: number
|
||||||
|
msg: string | null
|
||||||
|
data: T
|
||||||
|
total?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订单状态类型
|
||||||
|
export type OrderStatus = '0' | '1' | '2' // 待审批、已审批、已拒绝
|
||||||
|
|
||||||
|
// 审批状态类型
|
||||||
|
export type ApprovalStatus = 1 | 2 | 3 // 待审批、驳回、通过
|
||||||
|
|
||||||
|
// 订单信息类型
|
||||||
|
export interface Order {
|
||||||
|
id: number
|
||||||
|
projectId: number
|
||||||
|
projectCode: string
|
||||||
|
projectName: string
|
||||||
|
versionCode: string
|
||||||
|
industryType: string
|
||||||
|
bgProperty: string
|
||||||
|
province: string
|
||||||
|
orderCode: string
|
||||||
|
customerName: string
|
||||||
|
customerCode: string
|
||||||
|
customerPhone: string
|
||||||
|
customerUserName: string
|
||||||
|
shipmentAmount: number
|
||||||
|
orderStatus: OrderStatus
|
||||||
|
duty: string
|
||||||
|
dutyName: string
|
||||||
|
agentName: string
|
||||||
|
agentCode: string
|
||||||
|
businessPerson: string
|
||||||
|
businessEmail: string
|
||||||
|
businessPhone: string
|
||||||
|
currencyType: string
|
||||||
|
partnerCode: string
|
||||||
|
partnerName: string
|
||||||
|
projectPartnerName: string
|
||||||
|
actualPurchaseAmount: number
|
||||||
|
deliveryTime: string
|
||||||
|
estimatedOrderTime: string
|
||||||
|
orderEndTime: string
|
||||||
|
approveTime: string
|
||||||
|
discountFold: number
|
||||||
|
supplier: string
|
||||||
|
createTime: string
|
||||||
|
updateTime: string
|
||||||
|
remark: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 审批记录类型
|
||||||
|
export interface ApprovalRecord {
|
||||||
|
id?: number
|
||||||
|
todoId: string
|
||||||
|
businessKey: string
|
||||||
|
processKey: string
|
||||||
|
processName: string
|
||||||
|
taskName: string
|
||||||
|
approveUserName: string
|
||||||
|
allApproveUserName: string
|
||||||
|
nextAllApproveUserName: string
|
||||||
|
approveUser: string
|
||||||
|
applyUserName: string
|
||||||
|
applyTime: string
|
||||||
|
extendField1: string
|
||||||
|
extendField2?: string
|
||||||
|
taskId: string
|
||||||
|
processInstanceId: string
|
||||||
|
approveOpinion?: string
|
||||||
|
approveStatus?: ApprovalStatus
|
||||||
|
approveTime: string
|
||||||
|
formKey?: string
|
||||||
|
roleName: string
|
||||||
|
recoveryType?: number
|
||||||
|
variables?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
// 附件文件类型
|
||||||
|
export interface AttachmentFile {
|
||||||
|
id: number
|
||||||
|
orderId: number
|
||||||
|
fileName: string
|
||||||
|
uploadUser: string
|
||||||
|
uploadUserName: string
|
||||||
|
uploadTime: string
|
||||||
|
filePath: string
|
||||||
|
fileType: string
|
||||||
|
fileSort: string
|
||||||
|
fileVersionCode: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 产品信息类型
|
||||||
|
export interface ProductInfo {
|
||||||
|
id: number
|
||||||
|
projectId: number
|
||||||
|
productBomCode: string
|
||||||
|
productName: string
|
||||||
|
model: string
|
||||||
|
productCode?: string
|
||||||
|
productDesc: string
|
||||||
|
quantity: number
|
||||||
|
cataloguePrice: number
|
||||||
|
catalogueAllPrice: number
|
||||||
|
price: number
|
||||||
|
allPrice: number
|
||||||
|
allPriceDisCount?: number
|
||||||
|
guidanceDiscount: number
|
||||||
|
vendorCode?: string
|
||||||
|
discount: number
|
||||||
|
type: string
|
||||||
|
value?: string
|
||||||
|
remark: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户信息类型
|
||||||
|
export interface UserInfo {
|
||||||
|
userId: number
|
||||||
|
userName: string
|
||||||
|
loginName: string
|
||||||
|
userType: string
|
||||||
|
email: string
|
||||||
|
phonenumber: string
|
||||||
|
sex: string
|
||||||
|
avatar: string
|
||||||
|
status: string
|
||||||
|
delFlag: string
|
||||||
|
loginIp: string
|
||||||
|
loginDate: string
|
||||||
|
pwdUpdateDate: string
|
||||||
|
admin: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订单详情响应类型
|
||||||
|
export interface OrderDetailResponse {
|
||||||
|
todo?: any
|
||||||
|
approveLog: ApprovalRecord[]
|
||||||
|
projectOrderInfo: Order & {
|
||||||
|
contractFileList: AttachmentFile[]
|
||||||
|
configFileList?: AttachmentFile[]
|
||||||
|
contractTableData: Record<string, AttachmentFile[]>
|
||||||
|
softwareProjectProductInfoList: ProductInfo[]
|
||||||
|
hardwareProjectProductInfoList: ProductInfo[]
|
||||||
|
maintenanceProjectProductInfoList: ProductInfo[]
|
||||||
|
}
|
||||||
|
user: UserInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列表查询参数
|
||||||
|
export interface ListParams {
|
||||||
|
approve?: string
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
keyword?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 审批操作参数
|
||||||
|
export interface ApprovalParams {
|
||||||
|
allApproveUserName?: string
|
||||||
|
applyTime?: string
|
||||||
|
applyUserName?: string
|
||||||
|
approveOpinion?: string
|
||||||
|
approveStatus?: ApprovalStatus
|
||||||
|
approveTime?: string
|
||||||
|
approveUser?: string
|
||||||
|
approveUserName?: string
|
||||||
|
businessKey?: string
|
||||||
|
createBy?: string
|
||||||
|
createTime?: string
|
||||||
|
extendField1?: string
|
||||||
|
extendField2?: string
|
||||||
|
formKey?: string
|
||||||
|
id?: number
|
||||||
|
nextAllApproveUserName?: string
|
||||||
|
processInstanceId?: string
|
||||||
|
processKey?: string
|
||||||
|
processName?: string
|
||||||
|
recoveryType?: number
|
||||||
|
remark?: string
|
||||||
|
roleName?: string
|
||||||
|
taskId?: string
|
||||||
|
taskName?: string
|
||||||
|
todoId?: string
|
||||||
|
updateBy?: string
|
||||||
|
updateTime?: string
|
||||||
|
[property: string]: any
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
import axios from 'axios'
|
||||||
|
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||||
|
import type { ApiResponse } from '@/types'
|
||||||
|
import { showToast, showFailToast } from 'vant'
|
||||||
|
|
||||||
|
class HttpClient {
|
||||||
|
private instance: AxiosInstance
|
||||||
|
|
||||||
|
constructor(baseURL: string = '/api') {
|
||||||
|
this.instance = axios.create({
|
||||||
|
baseURL,
|
||||||
|
timeout: 30000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json;charset=UTF-8'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.setupInterceptors()
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupInterceptors(): void {
|
||||||
|
// 请求拦截器
|
||||||
|
this.instance.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
// 在这里可以添加token等认证信息
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果数据是FormData,删除Content-Type让浏览器自动设置
|
||||||
|
if (config.data instanceof FormData) {
|
||||||
|
delete config.headers['Content-Type']
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 响应拦截器
|
||||||
|
this.instance.interceptors.response.use(
|
||||||
|
(response: AxiosResponse) => {
|
||||||
|
// 直接返回完整响应对象
|
||||||
|
return response
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
let message = '网络错误'
|
||||||
|
|
||||||
|
if (error.response) {
|
||||||
|
const { status, data } = error.response
|
||||||
|
switch (status) {
|
||||||
|
case 401:
|
||||||
|
message = '未授权,请重新登录'
|
||||||
|
// 这里可以处理登录跳转逻辑
|
||||||
|
break
|
||||||
|
case 403:
|
||||||
|
message = '拒绝访问'
|
||||||
|
break
|
||||||
|
case 404:
|
||||||
|
message = '请求地址不存在'
|
||||||
|
break
|
||||||
|
case 500:
|
||||||
|
message = '服务器内部错误'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
message = data?.msg || `请求失败 (${status})`
|
||||||
|
}
|
||||||
|
} else if (error.request) {
|
||||||
|
message = '网络连接失败'
|
||||||
|
}
|
||||||
|
|
||||||
|
showFailToast(message)
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async get<T = any>(
|
||||||
|
url: string,
|
||||||
|
config?: AxiosRequestConfig
|
||||||
|
): Promise<ApiResponse<T>> {
|
||||||
|
return this.instance.get(url, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async post<T = any>(
|
||||||
|
url: string,
|
||||||
|
data?: any,
|
||||||
|
config?: AxiosRequestConfig
|
||||||
|
): Promise<ApiResponse<T>> {
|
||||||
|
return this.instance.post(url, data, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async put<T = any>(
|
||||||
|
url: string,
|
||||||
|
data?: any,
|
||||||
|
config?: AxiosRequestConfig
|
||||||
|
): Promise<ApiResponse<T>> {
|
||||||
|
return this.instance.put(url, data, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async delete<T = any>(
|
||||||
|
url: string,
|
||||||
|
config?: AxiosRequestConfig
|
||||||
|
): Promise<ApiResponse<T>> {
|
||||||
|
return this.instance.delete(url, config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const http = new HttpClient()
|
||||||
|
export default http
|
||||||
|
|
@ -0,0 +1,211 @@
|
||||||
|
import type { OrderStatus, ApprovalStatus } from '@/types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化订单状态
|
||||||
|
*/
|
||||||
|
export const formatOrderStatus = (status: OrderStatus): string => {
|
||||||
|
const statusMap = {
|
||||||
|
'0': '待审批',
|
||||||
|
'1': '已审批',
|
||||||
|
'2': '已拒绝'
|
||||||
|
}
|
||||||
|
return statusMap[status] || '未知状态'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订单状态颜色
|
||||||
|
*/
|
||||||
|
export const getOrderStatusColor = (status: OrderStatus): string => {
|
||||||
|
const colorMap = {
|
||||||
|
'0': '#fa8c16', // 橙色 - 待审批
|
||||||
|
'1': '#52c41a', // 绿色 - 已审批
|
||||||
|
'2': '#f5222d' // 红色 - 已拒绝
|
||||||
|
}
|
||||||
|
return colorMap[status] || '#666666'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化审批状态
|
||||||
|
*/
|
||||||
|
export const formatApprovalStatus = (status?: ApprovalStatus): string => {
|
||||||
|
if (status === undefined || status === null) return '待审批'
|
||||||
|
|
||||||
|
const statusMap = {
|
||||||
|
1: '待审批',
|
||||||
|
2: '已驳回',
|
||||||
|
3: '已通过'
|
||||||
|
}
|
||||||
|
return statusMap[status] || '待审批'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取审批状态颜色
|
||||||
|
*/
|
||||||
|
export const getApprovalStatusColor = (status?: ApprovalStatus): string => {
|
||||||
|
if (status === undefined || status === null) return '#fa8c16'
|
||||||
|
|
||||||
|
const colorMap = {
|
||||||
|
1: '#fa8c16', // 橙色 - 待审批
|
||||||
|
2: '#f5222d', // 红色 - 已驳回
|
||||||
|
3: '#52c41a' // 绿色 - 已通过
|
||||||
|
}
|
||||||
|
return colorMap[status] || '#fa8c16'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化金额
|
||||||
|
*/
|
||||||
|
export const formatAmount = (amount: number): string => {
|
||||||
|
return new Intl.NumberFormat('zh-CN', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'CNY',
|
||||||
|
minimumFractionDigits: 2
|
||||||
|
}).format(amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期
|
||||||
|
*/
|
||||||
|
export const formatDate = (date: string | Date, format: string = 'YYYY-MM-DD'): string => {
|
||||||
|
if (!date) return ''
|
||||||
|
|
||||||
|
const d = new Date(date)
|
||||||
|
if (isNaN(d.getTime())) return ''
|
||||||
|
|
||||||
|
const year = d.getFullYear()
|
||||||
|
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(d.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(d.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(d.getMinutes()).padStart(2, '0')
|
||||||
|
const seconds = String(d.getSeconds()).padStart(2, '0')
|
||||||
|
|
||||||
|
return format
|
||||||
|
.replace('YYYY', year.toString())
|
||||||
|
.replace('MM', month)
|
||||||
|
.replace('DD', day)
|
||||||
|
.replace('HH', hours)
|
||||||
|
.replace('mm', minutes)
|
||||||
|
.replace('ss', seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化相对时间
|
||||||
|
*/
|
||||||
|
export const formatRelativeTime = (date: string | Date): string => {
|
||||||
|
if (!date) return ''
|
||||||
|
|
||||||
|
const d = new Date(date)
|
||||||
|
if (isNaN(d.getTime())) return ''
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
const diff = now.getTime() - d.getTime()
|
||||||
|
|
||||||
|
const minute = 60 * 1000
|
||||||
|
const hour = 60 * minute
|
||||||
|
const day = 24 * hour
|
||||||
|
|
||||||
|
if (diff < minute) {
|
||||||
|
return '刚刚'
|
||||||
|
} else if (diff < hour) {
|
||||||
|
return `${Math.floor(diff / minute)}分钟前`
|
||||||
|
} else if (diff < day) {
|
||||||
|
return `${Math.floor(diff / hour)}小时前`
|
||||||
|
} else if (diff < 3 * day) {
|
||||||
|
return `${Math.floor(diff / day)}天前`
|
||||||
|
} else {
|
||||||
|
return formatDate(date, 'YYYY-MM-DD')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 防抖函数
|
||||||
|
*/
|
||||||
|
export const debounce = <T extends (...args: any[]) => any>(
|
||||||
|
func: T,
|
||||||
|
wait: number
|
||||||
|
): ((...args: Parameters<T>) => void) => {
|
||||||
|
let timeout: NodeJS.Timeout | null = null
|
||||||
|
|
||||||
|
return (...args: Parameters<T>) => {
|
||||||
|
if (timeout) clearTimeout(timeout)
|
||||||
|
timeout = setTimeout(() => func(...args), wait)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 节流函数
|
||||||
|
*/
|
||||||
|
export const throttle = <T extends (...args: any[]) => any>(
|
||||||
|
func: T,
|
||||||
|
wait: number
|
||||||
|
): ((...args: Parameters<T>) => void) => {
|
||||||
|
let timeout: NodeJS.Timeout | null = null
|
||||||
|
let previous = 0
|
||||||
|
|
||||||
|
return (...args: Parameters<T>) => {
|
||||||
|
const now = Date.now()
|
||||||
|
const remaining = wait - (now - previous)
|
||||||
|
|
||||||
|
if (remaining <= 0 || remaining > wait) {
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
timeout = null
|
||||||
|
}
|
||||||
|
previous = now
|
||||||
|
func(...args)
|
||||||
|
} else if (!timeout) {
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
previous = Date.now()
|
||||||
|
timeout = null
|
||||||
|
func(...args)
|
||||||
|
}, remaining)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否在微信小程序环境
|
||||||
|
*/
|
||||||
|
export const isWeixinMiniProgram = (): boolean => {
|
||||||
|
return (window as any).__wxjs_environment === 'miniprogram'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件预览URL
|
||||||
|
*/
|
||||||
|
export const getFilePreviewUrl = (filePath: string): string => {
|
||||||
|
if (!filePath) return ''
|
||||||
|
|
||||||
|
// 如果已经是完整URL,直接返回
|
||||||
|
if (filePath.startsWith('http')) {
|
||||||
|
return filePath
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拼接基础URL
|
||||||
|
const baseUrl = import.meta.env.VITE_FILE_BASE_URL || '/api'
|
||||||
|
return `${baseUrl}${filePath}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复制到剪贴板
|
||||||
|
*/
|
||||||
|
export const copyToClipboard = async (text: string): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
if (navigator.clipboard) {
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
// 降级方案
|
||||||
|
const textarea = document.createElement('textarea')
|
||||||
|
textarea.value = text
|
||||||
|
document.body.appendChild(textarea)
|
||||||
|
textarea.select()
|
||||||
|
document.execCommand('copy')
|
||||||
|
document.body.removeChild(textarea)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('复制失败:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,999 @@
|
||||||
|
<template>
|
||||||
|
<div class="order-detail-page">
|
||||||
|
<!-- 导航栏 -->
|
||||||
|
<van-nav-bar
|
||||||
|
title="订单详情"
|
||||||
|
left-arrow
|
||||||
|
@click-left="goBack"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<van-loading v-if="detailLoading" class="loading-container" size="24px">
|
||||||
|
加载中...
|
||||||
|
</van-loading>
|
||||||
|
|
||||||
|
<div v-else-if="currentOrderInfo" class="page-content">
|
||||||
|
<!-- 项目信息标题 -->
|
||||||
|
<div class="project-header">
|
||||||
|
<div class="project-title">
|
||||||
|
{{ currentOrderInfo.projectName }}REV.{{ currentOrderInfo.versionCode }}
|
||||||
|
</div>
|
||||||
|
<div class="project-status">
|
||||||
|
待审批
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab标签页 -->
|
||||||
|
<van-tabs v-model:active="activeTab" sticky>
|
||||||
|
<!-- 订单信息 -->
|
||||||
|
<van-tab title="订单信息" name="order">
|
||||||
|
<div class="tab-content">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<span>基本信息</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">项目名称</span>
|
||||||
|
<span class="value">{{ currentOrderInfo.projectName || '' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">版本号</span>
|
||||||
|
<span class="value">{{ currentOrderInfo.versionCode || '' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">项目编号</span>
|
||||||
|
<span class="value">{{ currentOrderInfo.projectCode || '' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">最终客户</span>
|
||||||
|
<span class="value">{{ currentOrderInfo.customerName || '' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">BG</span>
|
||||||
|
<span class="value">{{ currentOrderInfo.bgProperty || '' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">行业</span>
|
||||||
|
<span class="value">{{ currentOrderInfo.industryType || '' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">代表处</span>
|
||||||
|
<span class="value">{{ currentOrderInfo.agentName || '' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">进货商接口人</span>
|
||||||
|
<span class="value">{{ currentOrderInfo.businessPerson || '' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">Email</span>
|
||||||
|
<span class="value">{{ currentOrderInfo.businessEmail || '' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">联系方式</span>
|
||||||
|
<span class="value">{{ currentOrderInfo.businessPhone || '' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">合同编号</span>
|
||||||
|
<span class="value">{{ currentOrderInfo.orderCode || '' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">执行单有效截止时间</span>
|
||||||
|
<span class="value">{{ currentOrderInfo.orderEndTime? formatDate(currentOrderInfo.orderEndTime, 'YYYY-MM-DD') : '' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">币种</span>
|
||||||
|
<span class="value">{{ currentOrderInfo.currencyType || '' }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">总代进货金额</span>
|
||||||
|
<span class="value">{{ currentOrderInfo.actualPurchaseAmount ? formatAmount(currentOrderInfo.actualPurchaseAmount) : '' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">总代出货金额</span>
|
||||||
|
<span class="value">{{ currentOrderInfo.shipmentAmount ? formatAmount(currentOrderInfo.shipmentAmount) : '' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">要求到货时间</span>
|
||||||
|
<span class="value">{{ currentOrderInfo.deliveryTime ? formatDate(currentOrderInfo.deliveryTime) : '' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">公司直发</span>
|
||||||
|
<span class="value">{{ currentOrderInfo.companyDelivery}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">下单通路</span>
|
||||||
|
<span class="value">{{ currentOrderInfo.orderChannel}}</span>
|
||||||
|
</div><div class="info-item">
|
||||||
|
<span class="label">供货商</span>
|
||||||
|
<span class="value">{{ currentOrderInfo.supplier}}</span>
|
||||||
|
</div><div class="info-item">
|
||||||
|
<span class="label">进货商</span>
|
||||||
|
<span class="value">{{ currentOrderInfo.partnerName}}</span>
|
||||||
|
</div><div class="info-item">
|
||||||
|
<span class="label">进货商类型</span>
|
||||||
|
<span class="value">{{ currentOrderInfo.level}}</span>
|
||||||
|
</div><div class="info-item">
|
||||||
|
<span class="label">进货商联系人</span>
|
||||||
|
<span class="value">{{ currentOrderInfo.partnerUserName}}</span>
|
||||||
|
</div><div class="info-item">
|
||||||
|
<span class="label">Email</span>
|
||||||
|
<span class="value">{{ currentOrderInfo.partnerEmail}}</span>
|
||||||
|
</div><div class="info-item">
|
||||||
|
<span class="label">联系方式</span>
|
||||||
|
<span class="value">{{ currentOrderInfo.partnerPhone}}</span>
|
||||||
|
</div><div class="info-item">
|
||||||
|
<span class="label">收货地址</span>
|
||||||
|
<span class="value">{{ currentOrderInfo.notifierAddress}}</span>
|
||||||
|
</div><div class="info-item">
|
||||||
|
<span class="label">收货人</span>
|
||||||
|
<span class="value">{{ currentOrderInfo.notifier}}</span>
|
||||||
|
</div><div class="info-item">
|
||||||
|
<span class="label">Email</span>
|
||||||
|
<span class="value">{{ currentOrderInfo.notifierEmail}}</span>
|
||||||
|
</div><div class="info-item">
|
||||||
|
<span class="label">联系方式</span>
|
||||||
|
<span class="value">{{ currentOrderInfo.notifierPhone}}</span>
|
||||||
|
</div><div class="info-item">
|
||||||
|
<span class="label">其他特别说明</span>
|
||||||
|
<span class="value">{{ currentOrderInfo.remark}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</van-tab>
|
||||||
|
|
||||||
|
<!-- 产品信息 -->
|
||||||
|
<van-tab title="产品信息" name="product">
|
||||||
|
<div class="tab-content">
|
||||||
|
<div v-if="hasProductInfo">
|
||||||
|
<!-- 软件产品 -->
|
||||||
|
<div v-if="currentOrderInfo.softwareProjectProductInfoList?.length">
|
||||||
|
<div v-for="(product, index) in currentOrderInfo.softwareProjectProductInfoList" :key="product.id" class="product-card">
|
||||||
|
<div class="product-header">
|
||||||
|
<span class="product-index">{{ index + 1 }}</span>
|
||||||
|
<div class="product-main-info">
|
||||||
|
<div class="product-code-price">
|
||||||
|
<span class="product-code">{{ product.productBomCode }}</span>
|
||||||
|
<span class="product-total-price">{{ formatAmount(product.allPrice) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="product-details">
|
||||||
|
<div class="product-row">
|
||||||
|
<span class="product-label">产品型号</span>
|
||||||
|
<span class="product-value">{{ product.model }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="product-row">
|
||||||
|
<span class="product-label">描述</span>
|
||||||
|
<span class="product-value">{{ product.productDesc }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="product-row">
|
||||||
|
<span class="product-label">数量</span>
|
||||||
|
<span class="product-value">{{ product.quantity }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="product-row">
|
||||||
|
<span class="product-label">单价</span>
|
||||||
|
<span class="product-value">{{ formatAmount(product.price) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 硬件产品 -->
|
||||||
|
<div v-if="currentOrderInfo.hardwareProjectProductInfoList?.length">
|
||||||
|
<div v-for="(product, index) in currentOrderInfo.hardwareProjectProductInfoList" :key="product.id" class="product-card">
|
||||||
|
<div class="product-header">
|
||||||
|
<span class="product-index">{{ index + 1 + (currentOrderInfo.softwareProjectProductInfoList?.length || 0) }}</span>
|
||||||
|
<div class="product-main-info">
|
||||||
|
<div class="product-code-price">
|
||||||
|
<span class="product-code">{{ product.productBomCode }}</span>
|
||||||
|
<span class="product-total-price">{{ formatAmount(product.allPrice) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="product-details">
|
||||||
|
<div class="product-row">
|
||||||
|
<span class="product-label">产品型号</span>
|
||||||
|
<span class="product-value">{{ product.model }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="product-row">
|
||||||
|
<span class="product-label">描述</span>
|
||||||
|
<span class="product-value">{{ product.productDesc }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="product-row">
|
||||||
|
<span class="product-label">数量</span>
|
||||||
|
<span class="product-value">{{ product.quantity }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="product-row">
|
||||||
|
<span class="product-label">单价</span>
|
||||||
|
<span class="product-value">{{ formatAmount(product.price) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 维保产品 -->
|
||||||
|
<div v-if="currentOrderInfo.maintenanceProjectProductInfoList?.length">
|
||||||
|
<div v-for="(product, index) in currentOrderInfo.maintenanceProjectProductInfoList" :key="product.id" class="product-card">
|
||||||
|
<div class="product-header">
|
||||||
|
<span class="product-index">{{ index + 1 + (currentOrderInfo.softwareProjectProductInfoList?.length || 0) + (currentOrderInfo.hardwareProjectProductInfoList?.length || 0) }}</span>
|
||||||
|
<div class="product-main-info">
|
||||||
|
<div class="product-code-price">
|
||||||
|
<span class="product-code">{{ product.productBomCode }}</span>
|
||||||
|
<span class="product-total-price">{{ formatAmount(product.allPrice) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="product-details">
|
||||||
|
<div class="product-row">
|
||||||
|
<span class="product-label">产品型号</span>
|
||||||
|
<span class="product-value">{{ product.model }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="product-row">
|
||||||
|
<span class="product-label">描述</span>
|
||||||
|
<span class="product-value">{{ product.productDesc }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="product-row">
|
||||||
|
<span class="product-label">数量</span>
|
||||||
|
<span class="product-value">{{ product.quantity }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="product-row">
|
||||||
|
<span class="product-label">单价</span>
|
||||||
|
<span class="product-value">{{ formatAmount(product.price) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 配置组总计 -->
|
||||||
|
<div class="product-summary">
|
||||||
|
<div class="summary-row">
|
||||||
|
<span class="summary-label">配置组总计</span>
|
||||||
|
<span class="summary-value">{{ getTotalProductCount() }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row">
|
||||||
|
<span class="summary-label">总价</span>
|
||||||
|
<span class="summary-value total-amount">{{ formatAmount(getTotalAmount()) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row">
|
||||||
|
<span class="summary-label">现金折扣率</span>
|
||||||
|
<span class="summary-value">{{ getDiscountRate() }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row final-total">
|
||||||
|
<span class="summary-label">折后总价</span>
|
||||||
|
<span class="summary-value final-amount">{{ formatAmount(getFinalTotalAmount()) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 无产品信息时的提示 -->
|
||||||
|
<div v-else class="empty-state">
|
||||||
|
<van-empty description="暂无产品信息"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</van-tab>
|
||||||
|
|
||||||
|
<!-- 合同附件 -->
|
||||||
|
<van-tab title="合同附件" name="attachment">
|
||||||
|
<div class="tab-content">
|
||||||
|
<div class="card" v-if="currentOrderInfo.contractFileList?.length">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="file-list">
|
||||||
|
<div v-for="file in currentOrderInfo.contractFileList" :key="file.id" class="file-item"
|
||||||
|
@click="previewFile(file)">
|
||||||
|
<van-icon name="description"/>
|
||||||
|
<div class="file-info">
|
||||||
|
<div class="file-name">{{ file.fileName }}</div>
|
||||||
|
<div class="file-meta">{{ file.uploadUserName }} · {{ formatDate(file.uploadTime) }}</div>
|
||||||
|
</div>
|
||||||
|
<van-icon name="arrow"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 无附件时的提示 -->
|
||||||
|
<div v-else class="empty-state">
|
||||||
|
<van-empty description="暂无合同附件"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</van-tab>
|
||||||
|
|
||||||
|
<!-- 审批历史 -->
|
||||||
|
<van-tab title="审批历史" name="approval">
|
||||||
|
<div class="tab-content">
|
||||||
|
<div class="card" v-if="approvalHistory.length">
|
||||||
|
<div class="card-body">
|
||||||
|
<van-steps direction="vertical" :active="approvalHistory.length">
|
||||||
|
<van-step v-for="(record, index) in approvalHistory" :key="record.todoId || index">
|
||||||
|
<template #inactive-icon>
|
||||||
|
<van-icon
|
||||||
|
:name="getStepIcon(record.approveStatus)"
|
||||||
|
:color="getApprovalStatusColor(record.approveStatus)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #active-icon>
|
||||||
|
<van-icon
|
||||||
|
:name="getStepIcon(record.approveStatus)"
|
||||||
|
:color="getApprovalStatusColor(record.approveStatus)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<div class="approval-item">
|
||||||
|
<div class="approval-user">提交人:{{ record.approveUserName }}</div>
|
||||||
|
<div v-if="record.nextAllApproveUserName" class="approval-next-user">
|
||||||
|
接受人:{{ record.nextAllApproveUserName }}
|
||||||
|
</div>
|
||||||
|
<div class="approval-time">{{ formatDate(record.approveTime, 'YYYY-MM-DD HH:mm') }}</div>
|
||||||
|
<div v-if="record.approveOpinion" class="approval-opinion">
|
||||||
|
审批意见:{{ record.approveOpinion }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</van-step>
|
||||||
|
</van-steps>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 无审批历史时的提示 -->
|
||||||
|
<div v-else class="empty-state">
|
||||||
|
<van-empty description="暂无审批历史"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</van-tab>
|
||||||
|
</van-tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 审批操作按钮 -->
|
||||||
|
<div v-if="showApprovalButtons" class="approval-actions">
|
||||||
|
<van-button
|
||||||
|
type="default"
|
||||||
|
size="large"
|
||||||
|
@click="showApprovalDialog(0)"
|
||||||
|
:loading="submitting"
|
||||||
|
>
|
||||||
|
驳回
|
||||||
|
</van-button>
|
||||||
|
<van-button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
@click="showApprovalDialog(1)"
|
||||||
|
:loading="submitting"
|
||||||
|
>
|
||||||
|
通过
|
||||||
|
</van-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 审批意见弹窗 -->
|
||||||
|
<van-popup
|
||||||
|
v-model:show="approvalDialogVisible"
|
||||||
|
position="bottom"
|
||||||
|
round
|
||||||
|
:style="{ height: '40%' }"
|
||||||
|
>
|
||||||
|
<div class="approval-dialog">
|
||||||
|
<div class="dialog-header">
|
||||||
|
<span>审批意见</span>
|
||||||
|
<van-icon name="cross" @click="approvalDialogVisible = false"/>
|
||||||
|
</div>
|
||||||
|
<div class="dialog-body">
|
||||||
|
<van-field
|
||||||
|
v-model="approvalOpinion"
|
||||||
|
type="textarea"
|
||||||
|
:placeholder="currentApprovalStatus === 0 ? '请输入驳回原因' : '请输入审批意见'"
|
||||||
|
rows="4"
|
||||||
|
autosize
|
||||||
|
maxlength="500"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<van-button
|
||||||
|
type="primary"
|
||||||
|
block
|
||||||
|
@click="submitApproval"
|
||||||
|
:loading="submitting"
|
||||||
|
>
|
||||||
|
确认{{ currentApprovalStatus === 0 ? '驳回' : '通过' }}
|
||||||
|
</van-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</van-popup>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {ref, computed, onMounted} from 'vue'
|
||||||
|
import {useRoute, useRouter} from 'vue-router'
|
||||||
|
import {storeToRefs} from 'pinia'
|
||||||
|
import {showToast, showSuccessToast} from 'vant'
|
||||||
|
import {useOrderStore} from '@/store/order'
|
||||||
|
import {submitApproval as submitApprovalApi} from '@/api/order'
|
||||||
|
import {
|
||||||
|
formatOrderStatus,
|
||||||
|
formatAmount,
|
||||||
|
formatDate,
|
||||||
|
formatApprovalStatus,
|
||||||
|
getApprovalStatusColor,
|
||||||
|
getFilePreviewUrl
|
||||||
|
} from '@/utils'
|
||||||
|
import type {OrderStatus, ApprovalStatus, AttachmentFile} from '@/types'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const orderStore = useOrderStore()
|
||||||
|
const {currentOrder, currentOrderInfo, approvalHistory, detailLoading} = storeToRefs(orderStore)
|
||||||
|
|
||||||
|
// 审批相关
|
||||||
|
const approvalDialogVisible = ref(false)
|
||||||
|
const approvalOpinion = ref('')
|
||||||
|
const currentApprovalStatus = ref<ApprovalStatus>(3)
|
||||||
|
const submitting = ref(false)
|
||||||
|
|
||||||
|
// Tab页签
|
||||||
|
const activeTab = ref('order')
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const hasProductInfo = computed(() => {
|
||||||
|
if (!currentOrderInfo.value) return false
|
||||||
|
return (
|
||||||
|
currentOrderInfo.value.softwareProjectProductInfoList?.length ||
|
||||||
|
currentOrderInfo.value.hardwareProjectProductInfoList?.length ||
|
||||||
|
currentOrderInfo.value.maintenanceProjectProductInfoList?.length
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const showApprovalButtons = computed(() => {
|
||||||
|
// 直接展示审批按钮,无需条件控制
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取状态样式类
|
||||||
|
const getStatusClass = (status: OrderStatus) => {
|
||||||
|
const classMap = {
|
||||||
|
'0': 'pending',
|
||||||
|
'1': 'approved',
|
||||||
|
'2': 'rejected'
|
||||||
|
}
|
||||||
|
return classMap[status] || 'pending'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取步骤图标
|
||||||
|
const getStepIcon = (status?: ApprovalStatus) => {
|
||||||
|
if (status === undefined || status === null) return 'clock'
|
||||||
|
const iconMap = {
|
||||||
|
1: 'clock',
|
||||||
|
2: 'close',
|
||||||
|
3: 'success'
|
||||||
|
}
|
||||||
|
return iconMap[status] || 'clock'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算产品总数量
|
||||||
|
const getTotalProductCount = () => {
|
||||||
|
if (!currentOrderInfo.value) return 0
|
||||||
|
|
||||||
|
const softwareCount = currentOrderInfo.value.softwareProjectProductInfoList?.length || 0
|
||||||
|
const hardwareCount = currentOrderInfo.value.hardwareProjectProductInfoList?.length || 0
|
||||||
|
const maintenanceCount = currentOrderInfo.value.maintenanceProjectProductInfoList?.length || 0
|
||||||
|
|
||||||
|
return softwareCount + hardwareCount + maintenanceCount
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算总金额
|
||||||
|
const getTotalAmount = () => {
|
||||||
|
if (!currentOrderInfo.value) return 0
|
||||||
|
|
||||||
|
let total = 0
|
||||||
|
|
||||||
|
// 软件产品总金额
|
||||||
|
if (currentOrderInfo.value.softwareProjectProductInfoList) {
|
||||||
|
total += currentOrderInfo.value.softwareProjectProductInfoList.reduce((sum, product) => sum + (product.allPrice || 0), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 硬件产品总金额
|
||||||
|
if (currentOrderInfo.value.hardwareProjectProductInfoList) {
|
||||||
|
total += currentOrderInfo.value.hardwareProjectProductInfoList.reduce((sum, product) => sum + (product.allPrice || 0), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 维保产品总金额
|
||||||
|
if (currentOrderInfo.value.maintenanceProjectProductInfoList) {
|
||||||
|
total += currentOrderInfo.value.maintenanceProjectProductInfoList.reduce((sum, product) => sum + (product.allPrice || 0), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取现金折扣率
|
||||||
|
const getDiscountRate = () => {
|
||||||
|
if (!currentOrderInfo.value || !currentOrderInfo.value.discountFold) {
|
||||||
|
return '100%'
|
||||||
|
}
|
||||||
|
return (currentOrderInfo.value.discountFold * 100).toFixed(1) + '%'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算折后总价
|
||||||
|
const getFinalTotalAmount = () => {
|
||||||
|
const totalAmount = getTotalAmount()
|
||||||
|
const discount = currentOrderInfo.value?.discountFold || 1
|
||||||
|
return totalAmount * discount
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回上一页
|
||||||
|
const goBack = () => {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预览文件
|
||||||
|
const previewFile = (file: AttachmentFile) => {
|
||||||
|
if (!file.filePath) {
|
||||||
|
showToast('文件路径不存在')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = getFilePreviewUrl(file.filePath)
|
||||||
|
window.open(url, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示审批弹窗
|
||||||
|
const showApprovalDialog = (status: ApprovalStatus) => {
|
||||||
|
currentApprovalStatus.value = status
|
||||||
|
approvalOpinion.value = ''
|
||||||
|
approvalDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交审批
|
||||||
|
const submitApproval = async () => {
|
||||||
|
if (!currentOrder.value || !currentOrder.value.todo) {
|
||||||
|
showToast('审批信息不完整')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const opinion = approvalOpinion.value.trim()
|
||||||
|
if (currentApprovalStatus.value === 0 && !opinion) {
|
||||||
|
showToast('请输入驳回原因')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
...currentOrder.value.todo, // 展开todo中的所有参数
|
||||||
|
|
||||||
|
approveOpinion: opinion || undefined , // 添加审批意见
|
||||||
|
variables:{
|
||||||
|
approveBtn:currentApprovalStatus.value,
|
||||||
|
comment:opinion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('提交审批参数:', params)
|
||||||
|
|
||||||
|
await submitApprovalApi(params)
|
||||||
|
|
||||||
|
showSuccessToast(currentApprovalStatus.value === 0 ? '驳回成功' : '审批通过')
|
||||||
|
approvalDialogVisible.value = false
|
||||||
|
|
||||||
|
// 重新加载详情
|
||||||
|
await orderStore.fetchOrderDetail(route.params.id as string)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('提交审批失败:', error)
|
||||||
|
showToast('提交审批失败')
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const id = route.params.id as string
|
||||||
|
if (id) {
|
||||||
|
console.log('详情页面加载,订单ID:', id)
|
||||||
|
try {
|
||||||
|
const result = await orderStore.fetchOrderDetail(id)
|
||||||
|
console.log('获取订单详情结果:', result)
|
||||||
|
console.log('当前订单信息:', currentOrder.value)
|
||||||
|
console.log('当前订单基本信息:', currentOrderInfo.value)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取订单详情失败:', error)
|
||||||
|
showToast('获取订单详情失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.order-detail-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: var(--background-color-secondary);
|
||||||
|
padding-bottom: 80px; // 为底部按钮留出空间
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-content {
|
||||||
|
// 移除原来的padding,让tabs组件自己控制间距
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-header {
|
||||||
|
background: var(--background-color-primary);
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color-primary);
|
||||||
|
flex: 1;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-status {
|
||||||
|
background: #FFF7E6;
|
||||||
|
color: #D48806;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
border: 1px solid #FFD591;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
padding: 60px var(--spacing-lg);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
padding: 60px var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
min-width: 100px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
color: var(--text-color-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: right;
|
||||||
|
word-break: break-all;
|
||||||
|
|
||||||
|
&.amount {
|
||||||
|
color: var(--error-color);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-section {
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-bottom: var(--spacing-lg);
|
||||||
|
padding-bottom: var(--spacing-lg);
|
||||||
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-color-primary);
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-item {
|
||||||
|
background: var(--background-color-tertiary);
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-color-primary);
|
||||||
|
margin-bottom: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
margin-bottom: var(--spacing-sm);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-color-tertiary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list {
|
||||||
|
.file-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
background: var(--background-color-tertiary);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
margin-bottom: var(--spacing-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.van-icon:first-child {
|
||||||
|
color: var(--primary-color);
|
||||||
|
margin-right: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-color-primary);
|
||||||
|
margin-bottom: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-meta {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-color-tertiary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.van-icon:last-child {
|
||||||
|
color: var(--text-color-tertiary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-item {
|
||||||
|
.approval-user,
|
||||||
|
.approval-time,
|
||||||
|
.approval-next-user {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
margin-bottom: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-next-user {
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-opinion {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-color-primary);
|
||||||
|
background: var(--background-color-tertiary);
|
||||||
|
padding: var(--spacing-sm);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-actions {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
background: var(--background-color-primary);
|
||||||
|
border-top: 1px solid var(--divider-color);
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-lg);
|
||||||
|
|
||||||
|
.van-button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-dialog {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.dialog-header {
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
.van-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-body {
|
||||||
|
flex: 1;
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
border-top: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.van-steps--vertical) {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.van-step__content) {
|
||||||
|
padding-bottom: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 产品相关样式
|
||||||
|
.product-card {
|
||||||
|
background: var(--background-color-primary);
|
||||||
|
border-radius: var(--border-radius-md);
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-header {
|
||||||
|
background: var(--background-color-secondary);
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-index {
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-main-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-code-price {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.product-code {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-total-price {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-details {
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: var(--spacing-sm);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-label {
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
min-width: 80px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-value {
|
||||||
|
color: var(--text-color-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: right;
|
||||||
|
flex: 1;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 产品总计样式
|
||||||
|
.product-summary {
|
||||||
|
background: var(--background-color-primary);
|
||||||
|
border-radius: var(--border-radius-md);
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
margin-top: var(--spacing-lg);
|
||||||
|
border: 2px solid var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-label {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-value {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color-primary);
|
||||||
|
|
||||||
|
&.total-amount {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.final-amount {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-total {
|
||||||
|
border-top: 2px solid var(--divider-color);
|
||||||
|
padding-top: var(--spacing-md);
|
||||||
|
margin-top: var(--spacing-md);
|
||||||
|
|
||||||
|
.summary-label {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,198 @@
|
||||||
|
<template>
|
||||||
|
<div class="order-list-page">
|
||||||
|
<!-- 搜索栏 -->
|
||||||
|
<van-search
|
||||||
|
v-model="searchKeyword"
|
||||||
|
placeholder="搜索订单编号、客户名称"
|
||||||
|
@search="handleSearch"
|
||||||
|
@clear="handleClear"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 下拉刷新 -->
|
||||||
|
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
|
||||||
|
<!-- 订单列表 -->
|
||||||
|
<van-list
|
||||||
|
v-model:loading="loading"
|
||||||
|
:finished="finished"
|
||||||
|
finished-text="没有更多了"
|
||||||
|
@load="onLoad"
|
||||||
|
>
|
||||||
|
<div v-for="order in orderList" :key="order.id" class="order-item" @click="goToDetail(order.id)">
|
||||||
|
<div class="order-header">
|
||||||
|
<div class="order-code">{{ order.orderCode }}</div>
|
||||||
|
<div class="status-tag" :class="getStatusClass(order.orderStatus)">
|
||||||
|
{{ formatOrderStatus(order.orderStatus) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="order-info">
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">项目名称:</span>
|
||||||
|
<span class="value">{{ order.projectName }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">客户名称:</span>
|
||||||
|
<span class="value">{{ order.customerName }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">订单金额:</span>
|
||||||
|
<span class="value amount">{{ formatAmount(order.shipmentAmount) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">创建时间:</span>
|
||||||
|
<span class="value">{{ formatDate(order.createTime, 'YYYY-MM-DD HH:mm') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<div v-if="!loading && orderList.length === 0" class="empty-state">
|
||||||
|
<van-empty description="暂无订单数据" />
|
||||||
|
</div>
|
||||||
|
</van-list>
|
||||||
|
</van-pull-refresh>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useOrderStore } from '@/store/order'
|
||||||
|
import { formatOrderStatus, formatAmount, formatDate } from '@/utils'
|
||||||
|
import type { OrderStatus } from '@/types'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const orderStore = useOrderStore()
|
||||||
|
const { orderList, loading, finished } = storeToRefs(orderStore)
|
||||||
|
|
||||||
|
// 搜索相关
|
||||||
|
const searchKeyword = ref('')
|
||||||
|
const refreshing = ref(false)
|
||||||
|
|
||||||
|
// 获取状态样式类
|
||||||
|
const getStatusClass = (status: OrderStatus) => {
|
||||||
|
const classMap = {
|
||||||
|
'0': 'pending',
|
||||||
|
'1': 'approved',
|
||||||
|
'2': 'rejected'
|
||||||
|
}
|
||||||
|
return classMap[status] || 'pending'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
const onLoad = async () => {
|
||||||
|
try {
|
||||||
|
await orderStore.loadOrderList()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载订单列表失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下拉刷新
|
||||||
|
const onRefresh = async () => {
|
||||||
|
try {
|
||||||
|
await orderStore.loadOrderList(true)
|
||||||
|
refreshing.value = false
|
||||||
|
} catch (error) {
|
||||||
|
refreshing.value = false
|
||||||
|
console.error('刷新失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索处理
|
||||||
|
const handleSearch = async () => {
|
||||||
|
try {
|
||||||
|
await orderStore.searchOrders(searchKeyword.value.trim())
|
||||||
|
} catch (error) {
|
||||||
|
console.error('搜索失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空搜索
|
||||||
|
const handleClear = async () => {
|
||||||
|
searchKeyword.value = ''
|
||||||
|
try {
|
||||||
|
await orderStore.searchOrders('')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('清空搜索失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转到详情页
|
||||||
|
const goToDetail = (id: number) => {
|
||||||
|
router.push(`/detail/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 重置状态并加载数据
|
||||||
|
orderStore.resetListState()
|
||||||
|
onLoad()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.order-list-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: var(--background-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-item {
|
||||||
|
background: var(--background-color-primary);
|
||||||
|
margin-bottom: var(--spacing-sm);
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: var(--background-color-tertiary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-code {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-info {
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: var(--spacing-sm);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
width: 80px;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
flex: 1;
|
||||||
|
color: var(--text-color-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
word-break: break-all;
|
||||||
|
|
||||||
|
&.amount {
|
||||||
|
color: var(--error-color);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
padding: 60px var(--spacing-lg);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||||
|
"exclude": ["src/**/__tests__/*"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
"types": ["vite/client"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import { resolve } from 'path'
|
||||||
|
import AutoImport from 'unplugin-auto-import/vite'
|
||||||
|
import Components from 'unplugin-vue-components/vite'
|
||||||
|
import { VantResolver } from 'unplugin-vue-components/resolvers'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
AutoImport({
|
||||||
|
imports: ['vue', 'vue-router', 'pinia'],
|
||||||
|
resolvers: [VantResolver()]
|
||||||
|
}),
|
||||||
|
Components({
|
||||||
|
resolvers: [VantResolver()]
|
||||||
|
})
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': resolve(__dirname, 'src')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
host: '0.0.0.0',
|
||||||
|
port: 3000,
|
||||||
|
open: true,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://192.168.2.134:28080',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||||
|
configure: (proxy, options) => {
|
||||||
|
// proxy.on('proxyReq', (proxyReq, req, res) => {
|
||||||
|
// // 设置请求头
|
||||||
|
// proxyReq.setHeader('Access-Control-Allow-Origin', '*')
|
||||||
|
// proxyReq.setHeader('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,PATCH,OPTIONS')
|
||||||
|
// proxyReq.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Content-Length, X-Requested-With')
|
||||||
|
// })
|
||||||
|
proxy.on('proxyReq', (proxyReq, req, res) => {
|
||||||
|
proxyReq.setHeader('Origin', options.target);
|
||||||
|
proxyReq.setHeader('Referer', options.target);
|
||||||
|
});
|
||||||
|
proxy.on('proxyRes', (proxyRes, req, res) => {
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
target: 'es2015',
|
||||||
|
outDir: 'dist',
|
||||||
|
assetsDir: 'assets',
|
||||||
|
sourcemap: false,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
chunkFileNames: 'js/[name]-[hash].js',
|
||||||
|
entryFileNames: 'js/[name]-[hash].js',
|
||||||
|
assetFileNames: '[ext]/[name]-[hash].[ext]'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue