1
Some checks failed
CI / build (20.x) (push) Waiting to run
CI / lint (push) Waiting to run
CI / test (push) Waiting to run
CI / deploy-preview (push) Blocked by required conditions
CI / security (push) Waiting to run
CI / build (18.x) (push) Has been cancelled

This commit is contained in:
97694731 2026-06-15 18:09:39 +08:00
parent 1d2a7d9a30
commit 44ba8a631c
283 changed files with 26563 additions and 8289 deletions

View File

@ -1,7 +1,7 @@
# cards_web 维护文档
# 🛠 维护文档
> 📅 文档更新2026-04-27
> 📂 项目路径:`D:\project\cards_web`
> 📅 文档更新2026-06-15
> 📂 项目路径:`D:\project\psi_web`
---
@ -12,15 +12,19 @@
| **项目名称** | cards图书进销存管理后台 |
| **版本** | 1.0.0 |
| **技术栈** | Vue 3 + Vite 2 + Element Plus + Pinia + Vue Router 4 |
| **源码规模** | 32 个 JS 文件21 个 Vue 组件 |
| **源码规模** | 31 个 API 文件33 个页面目录24 个组件文件 |
| **路由模式** | History 模式createWebHistory |
### 核心业务模块
- **波次管理wave** — 核心流程:相机拍照 → 条码识别 → 书籍信息编辑
- **出库管理outbound** — 出库单管理API 层已按 `shipping_order` 表结构重写
- **盘库管理stock-check**基于 `stock_check` / `stock_check_item`
- **采购 / 销售 / 发货单** — 订单全链路
- **波次管理wave** — 核心流程:相机拍照 → 条码识别 → 书籍信息编辑 → 波次创建/释放
- **出库管理outbound** — 出库单 CRUD、审核、导出、改库位基于 `shipping_order` 表结构
- **盘库管理stock-check**全盘/抽盘、逐条/批量提交、库存调整
- **采购 / 销售 / 发货单** — 订单全链路可追溯
- **商品、库存、供应商、仓库、库位** — 基础数据管理
- **分账管理** — 分账规则配置 + 扣款日志查询/导出
- **快递/物流** — 物流模板、运费模板、快递面单打印、单号回填
- **店铺管理** — 多店铺平台(拼多多、孔夫子、闲鱼)、授权管理
---
@ -31,14 +35,19 @@
```json
{
"vue": "^3.2.8",
"vue-router": "^4.0.11",
"pinia": "^3.0.4",
"vue-router": "^4.1.6",
"pinia": "^2.1.7",
"element-plus": "^2.13.7",
"axios": "^0.21.1",
"axios": "^1.7.9",
"crypto-js": "^4.2.0",
"@zxing/library": "^0.21.3",
"jsbarcode": "^3.12.3",
"json-bigint": "^1.0.0",
"qrcode": "^1.5.4",
"vite": "^2.5.2"
"sass-embedded": "^1.99.0",
"vite": "^2.5.2",
"@vitejs/plugin-vue": "^1.6.0",
"typescript": "^5.5.4"
}
```
@ -46,9 +55,10 @@
| 文件 | 作用 |
|------|------|
| `vite.config.js` | Vite 构建配置dev server 代理 |
| `vite.config.js` | Vite 构建配置dev server 代理、中间件、别名) |
| `.env` | 环境变量API 密钥配置) |
| `tsconfig.json` | TypeScript 配置(但项目实际以 JS 为主) |
| `tsconfig.json` | TypeScript 配置(项目以 JS 为主,部分 .ts 文件) |
| `package.json` | 依赖管理与脚本 |
### 2.3 环境变量(`.env`
@ -58,6 +68,20 @@ VITE_APP_CLIENT_ID=psi
VITE_APP_API_SECRET=psi_api_sign_secret
```
### 2.4 Vite 配置代理
| 代理规则 | 目标 | 说明 |
|---------|------|------|
| `/api` | `https://psi.api.buzhiyushu.cn` | 主业务 APIchangeOrigin |
| `/api/print` | `https://print.buzhiyushu.cn` | 打印服务changeOrigin |
### 2.5 开发服务器
- **端口**5174
- **Host**0.0.0.0
- **History API Fallback**true
- **自定义中间件**`/kongfz-login`(孔夫子旧书网登录代理)
---
## 三、API 架构
@ -66,38 +90,73 @@ VITE_APP_API_SECRET=psi_api_sign_secret
| 服务 | 地址 | 用途 |
|------|------|------|
| **主业务 API** | `192.168.101.213:9090` | 所有业务接口(已在 vite.config.js 中配置代理) |
| ~~ES 搜索~~ | ~~`192.168.101.162:9009`~~ | ⚠️ 已废弃,功能已合并到主业务 API |
| **主业务 API** | `https://psi.api.buzhiyushu.cn` | 所有业务接口(通过 Vite 代理 `/api` |
| **打印服务** | `https://print.buzhiyushu.cn` | 打印面单(通过 Vite 代理 `/api/print` |
| **店铺列表** | `https://api.buzhiyushu.cn` | 外部店铺列表/快递回填 |
| **店铺详情** | `https://new.taskpool.buzhiyushu.cn:3500` | 外部店铺详情 |
| **核价器** | `http://{用户配置的 IP}:{Port}` | 价格查询/配置(直连,不经过代理) |
> ⚠️ **2026-04-27 更新**`/api/es` 代理已从 `vite.config.js` 中移除,现仅保留 `/api``192.168.101.213:9090`
> ⚠️ 历史:`/api/es` 代理192.168.101.162:9009已于 2026-04-27 移除
### 3.2 请求封装(`src/utils/request.js`
- **baseURL**:开发环境 `/api`,生产环境通过 `VITE_API_BASE``192.168.101.213:9090/api`
- **签名机制**:所有非 mock 请求默认走签名(`signUtil.generateSign`
- **鉴权 Token**:从 `localStorage``admin_token` 读取,通过 `Bearer` 头部传递
- **请求转换**:自动将普通对象转为 FormData`objectToFormData`
- **响应处理**:统一处理 code≠200 的错误提示,按 HTTP 状态码跳转登录页
- **Mock 模式**`USE_MOCK = false`(默认关闭),可通过环境变量控制
- **baseURL**:开发环境 `/api`,生产环境 `https://psi.api.buzhiyushu.cn/api`
- **超时**10 秒
- **签名机制**:所有非 mock 请求默认走签名(`signUtil.generateSign`),跳过需设 `X-Need-Sign: false`
- **鉴权 Token**:从 `localStorage.admin_token` 读取,通过 `Bearer` 头部传递
- **请求转换**:自动将普通对象转为 FormData`objectToFormData`,支持嵌套/数组)
- **大整数**:使用 `json-bigint` 替代 `JSON.parse`
- **响应处理**:统一错误提示 + 按 HTTP 状态码跳转登录页
- **Mock 模式**`USE_MOCK = false`(默认关闭),支持商品/卡密/代理/订单/积分的模拟数据
- **额外导出**`fetchCurrentUser()` 获取当前用户信息
### 3.3 API 模块列表(`src/api/`
### 3.3 API 模块索引31 个 API 文件
| 文件 | 对应接口前缀 | 备注 |
|------|-------------|------|
| `barcode.js` | `/barcode/*` | 条码相关 |
| `inventory.js` | `/inventory/*` | 库存记录 |
| `location.js` | `/location/*` | 库位管理 |
| `outbound.js` | `/outbound/*` | 出库管理 |
| `product.js` | `/product/*` | 商品管理 |
| `purchase-order.js` | `/purchase-order/*` | 采购订单 |
| `sales-order.js` | `/sales-order/*` | 销售订单 |
| `shipping-order.js` | `/shipping-order/*` | 发货单 |
| `stock-check.js` | `/stock-check/*` | 盘库管理 |
| `supplier.js` | `/supplier/*` | 供应商管理 |
| `warehouse.js` | `/warehouse/*` | 仓库管理 |
| `wave-task.js` | `/wave/*` | 波次任务 |
| 文件 | API 基路径 | 说明 |
|------|-----------|------|
| `barcode.js` | `/barcode/generate` | 条码生成(套装书条码打印/调试用) |
| `book.js` | `/getBookInfo`, `/syncBook` | 书籍信息查询/同步 |
| `car.js` | `/car/*` | 小车 CRUD + 小车-店铺关联 |
| `config.js` | 直连 `http://{ip}:{port}` | 核价器价格查询、Token 管理、孔夫子登录) |
| `courierConfig.js` | `/config/*` | ⚠️ 快递配置(仅 `createConfig` 完整,其余为桩) |
| `dashboard.js` | `/dashboard/statist` | 仪表盘统计(今日/员工) |
| `district.js` | `/district/*` | 省市区数据 + 运费模板 CRUD |
| `employee.js` | `/admin/employee/*` | 代理 CRUD、积分加减、启用禁用 |
| `employeeType.js` | `/admin/user-type/*` | 员工类型管理 |
| `goodsInfo.js` | `/product_log/save` | 提交异常书目新旧字段对比FormData |
| `inventory.js` | `/inventory/*` | 库存汇总/明细/变动日志 |
| `location.js` | `/location/*` | 库位 CRUD、批量生成、Excel 导入 |
| `logistics.js` | `/logistics/*` | 物流模板 CRUD |
| `login.js` | `/login/:role` | 管理员/代理登录 |
| `outbound.js` | `/outbound-order/*` | 出库单 CRUD、审核、导出、改库位 |
| `print.js` | — | Lodop 打印方法重导出 |
| `product.js` | `/product/*`, `/getSuitBook`, `/ocr` | 商品 CRUD、图片管理、OCR、Excel 导入 |
| `purchase-order.js` | `/purchase-order/*`, `/wave/release` | 采购单 CRUD、带波次创建、波次释放 |
| `releaseRecord.js` | — | **Mock 数据**,无后端接口 |
| `reviewIllegalBook.js` | `/product_log/*` | 异常书目查询/删除 |
| `sales-order.js` | `/sales-order/*` | 销售单 CRUD、确认、取消、退货 |
| `shipping-order.js` | `/shipping-order/*` + 外部接口 | 发货单 CRUD + 快递面单/回填 |
| `shop.js` | `/shop/*` + 外部 API | 店铺 CRUD + 外部列表/详情 |
| `split.js` | `/split-account-config/*` | ⚠️ 旧版分账(不完整),建议用 `splitAccount.js` |
| `splitAccount.js` | `/split-account-config/*` | 分账配置 CRUD + 启用/禁用(完整版) |
| `splitDeductionLog.js` | `/split-account-deduction-log/*` | 分账扣款日志列表/详情/导出 |
| `stock-check.js` | `/inventory/stock-check/*` | 盘库单 CRUD、盘点、调整、退货 |
| `submIllegalBook.js` | `/product_log/audit` | ⚠️ 文件名 "subm" 应为 "submit",异常书目审核 |
| `supplier.js` | `/supplier/*` | 供应商 CRUD |
| `warehouse.js` | `/warehouse/*` | 仓库 CRUD |
| `wave-task.js` | `/wave/*` | 波次任务列表/详情、波次创建/释放 |
> 注意:`wave-task.js` 有 `.bak_20260426_172453` 备份文件,建议清理。
### 3.4 外部服务集成一览
| 服务 | 集成方式 | 说明 |
|------|---------|------|
| **核价器** | axios 直连 `http://{ip}:{port}` | 价格查询、孔夫子登录、Token 管理 |
| **快递打印** | axios 直连 | 面单创建print.buzhiyushu.cn、单号回填api.buzhiyushu.cn |
| **店铺列表** | 外部 axios + JSONbig | api.buzhiyushu.cn 店铺列表 |
| **店铺详情** | 外部 axios | new.taskpool.buzhiyushu.cn:3500 |
| **孔夫子登录** | Vite 自定义中间件 | 模拟浏览器 POST → 提取 PHPSESSID → 获取用户信息 |
| **OCR 识别** | 主 API `/ocr`(跳过签名) | 图片文字识别、提取书名字段 |
| **Lodop 打印** | 本地 8000 端口 | C-Lodop 控件,条码打印/调试 |
---
@ -105,47 +164,88 @@ VITE_APP_API_SECRET=psi_api_sign_secret
### 4.1 路由模式
- **Hash 模式**`createWebHashHistory`
- URL 格式:`http://host:port/#/path`
- **History 模式**`createWebHistory`
- URL 格式:`http://host:port/path`
- 需要服务端配置 history API fallbackVite dev 已配置,生产需 Nginx 配置)
### 4.2 路由守卫逻辑
| 路径 | 鉴权要求 | 说明 |
|------|---------|------|
| `/login` | 无 | 登录页,已登录自动跳转 `/dashboard` |
| `/dashboard` | `admin_token` 存在 | 仪表盘,入口页 |
| 其他业务页 | `admin_token` + `role=255` | 仅管理员可访问 |
| 步骤 | 逻辑 | 说明 |
|------|------|------|
| 1 | URL token 注入 | 检测 `?token=xxx` → 解析 JWT payload → 写入 localStorage嵌入模式 |
| 2 | 页面标题 | `document.title = "{title} - 进销存系统"` |
| 3 | 登录页 | 已登录自动跳转 `/dashboard` |
| 4 | 仪表盘 | 无 `adminUserInfo` 跳转 `/login` |
| 5 | 黑名单鉴权 | 缺少 `meta.isPublic` 的所有页面需登录 |
| 6 | 管理员校验 | `requiresAdmin: true``role !== 255` 跳转 `/dashboard` |
### 4.3 角色权限
| role 值 | 身份 | 权限 |
|---------|------|------|
| role 值 | 身份 | 权限范围 |
|---------|------|---------|
| `255` | 管理员 | 所有页面和操作 |
| `128` | 代理 | 降级为普通用户,仅限基础功能 |
| `128` | 代理 | 基础功能(波次、仓库、商品等),无权访问管理页面 |
### 4.4 路由统计
- 共 **32 条路由**(含 1 个登录页、1 个布局容器、30 个子页面)
- **管理员专用路由**:代理管理、添加代理、员工类型管理、异常书目审核、核价器配置
- **隐藏路由**:库位管理(`/location-manager`、PDA 管理(`/pdaManage`),通过 URL 直接访问但不在侧边栏显示
### 4.5 新增路由(自上次文档更新以来)
| 路径 | 名称 | 说明 |
|------|------|------|
| `/sales-order-info-list` | SalesOrderInfoList | 销售单详情列表 |
| `/shipping-order-list` | ShippingOrderList | 发货单详情列表 |
| `/wave-task` | wave-task | 波次任务 |
| `/shop-settings` | shop-settings | 店铺设置 |
| `/sorting-settings` | sorting-settings | 分拣设置 |
| `/split-config` | split-config | 分账配置 |
| `/split-log` | split-log | 分账扣款日志 |
| `/courier-config` | courier-config | 快递配置 |
| `/pdaManage` | pdaManage | PDA 管理(隐藏) |
---
## 五、业务数据模型(关键表结构)
## 五、业务数据模型要点
### 5.1 盘库模块
| 表名 | 用途 |
|------|------|
| `stock_check` | 盘库单主表 |
| `stock_check_item` | 盘库明细 |
| **状态流程** | `1`=待盘点 → `2`=盘点中 → `3`=已完成 / `4`=已取消 |
| **盘点类型** | `1`=全盘,`2`=抽盘 |
| `stock_check` | 盘库单主表(`warehouse_id`、`check_type`、`status`、`remark` |
| `stock_check_item` | 盘库明细表(`stock_check_id`、`product_id`、`location_id`、`system_quantity`、`actual_quantity`、`status` |
### 5.2 出库模块
**状态流转**`1`=待盘点 → `2`=盘点中 → `3`=已完成 / `4`=已取消
**盘点类型**`1`=全盘(盘点仓库全部商品),`2`=抽盘(指定部分商品)
- 基于 `shipping_order` 表结构API 层已重写,接口路径 `/shipping-order/*`
- 目前无后端 API前端先行开发待对接
### 5.2 出库/发货模块
- 基于 `shipping_order` 表结构
- 从销售单 → 出库单 → 发货单,三级流转
- 发货单状态:`1`=待发货,`2`=已发货,`3`=已签收,`4`=已取消
### 5.3 波次模块
- **波次任务**`/wave/task/list`、`/wave/task/detail`
- **波次列表**`/wave/list`
- 波次任务:`/wave/task/list`、`/wave/task/detail`
- 波次列表(选择器)`/wave/list`
- 支持按波次号扫码查询任务
- 波次出库:先 `createWaveOutbound` 创建 → 再 `createWaveOutboundRelease` 释放
- 采购带波次:先 `createPurchaseOrderWithWave` 创建 → 再 `releaseWave` 释放
### 5.4 库存模块
- 汇总(按商品+仓库聚合):`/inventory/list` → 总量/锁定/可用数量
- 明细(按仓库+库位+批次):`/inventory/detail`
- 变动日志:`/inventory/log/list` → 支持时间范围、变动类型筛选
### 5.5 分账模块
| 概念 | 说明 |
|------|------|
| `split-account-config` | 分账规则配置(`rule_value` 为 JSON 对象,含分账比例/固定金额等) |
| `split-account-deduction-log` | 扣款执行日志,支持按业务单号/配置名/时间范围查询导出 |
---
@ -153,21 +253,29 @@ VITE_APP_API_SECRET=psi_api_sign_secret
### 6.1 遗留问题
| # | 问题 | 说明 | 建议 |
|---|------|------|------|
| 1 | Vite 2 较旧 | `^2.5.2`,有安全更新 | 建议升级到 Vite 4+ |
| 2 | Dashboard 为空 | `Dashboard.vue` 未实现内容 | 待后端接口接入 |
| 3 | 库位管理路由隐藏 | `location` 路由正常注册,但 UI 可能未展示入口 | 检查侧边栏组件 |
| 4 | `.bak` 备份文件 | `wave-task.js.bak_20260426_172453` | 确认无问题后可删除 |
| 5 | 出库模块无后端 | `outbound.js` API 已写好,后端尚未实现 | 等待后端对接 |
| 6 | TypeScript 未启用 | `tsconfig.json` 存在但项目使用 JS | 统一规范或移除 |
| # | 问题 | 文件/区域 | 说明 | 优先级 |
|---|------|----------|------|--------|
| 1 | Vite 2 较旧 | `package.json` | `^2.5.2`,有安全更新 | 中 |
| 2 | TypeScript 未充分利用 | 项目范围 | `tsconfig.json` 存在,仅 `print.ts` 等少数文件使用 TS | 低 |
| 3 | Dashboard 内容为空 | `Dashboard.vue` | 仅显示基础统计 | 低 |
| 4 | `.bak` 备份文件残留 | 多处 | `camera.vue`4 个)、`orderList.vue`3 个)、`shippingOrder.vue` 等 | 低 |
| 5 | 出库模块待后端对接 | `outbound.js` | API 已写好,后端尚未完全实现 | 中 |
| 6 | 发布记录为 Mock | `releaseRecord.js` | 无后端接口,纯前端模拟数据 | 高 |
| 7 | `split.js` 不完整 | `src/api/split.js` | 仅 2 个函数1 个为桩,应使用 `splitAccount.js` | 中 |
| 8 | `courierConfig.js` 不完整 | `src/api/courierConfig.js` | 仅 `createConfig` 有实现 | 中 |
| 9 | 函数名拼写错误 | `employeeType.js` | `updataEmployeInfo` → 应为 `updateEmployeeInfo` | 低 |
| 10 | 文件名不一致 | `submIllegalBook.js` | "subm" → 应为 "submit" | 低 |
| 11 | `normalizeListResponse` 重复 | 约 15 个 API 文件 | 各文件独立定义,应抽离公共工具函数 | 中 |
| 12 | 旧版 Store 冗余 | `store/index.js` | 与 Pinia store 并存,逐步废弃中 | 低 |
| 13 | `goosList.vue` 拼写错误 | `components/wave/goosList.vue` | "goos" → 应为 "goods" | 低 |
### 6.2 维护日志
| 日期 | 操作 |
|------|------|
| 2026-04-26 | 首次读取项目,建立基础文档 |
| 2026-04-27 | 移除废弃的 ES 搜索服务代理 `192.168.101.162:9009`(功能已合并到主业务 API |
| 2026-06-15 | 全面更新技术文档 + 维护文档,补充完整 API 接口表31 个文件、路由表32 条路由、新增模块分账、快递、PDA、扫码等 |
| 2026-06-07 | 新增打印机调试模式:`car.vue` 添加"打印调试"按钮,`suitBookDialog.vue` 套装书首次打印走 `PRINT_SETUP``localStorage.printFlag` 控制是否跳过调试 |
| 2026-04-27 | 移除废弃的 ES 搜索服务代理 `192.168.101.162:9009`(功能已合并到主业务 API首次读取项目建立基础维护文档 |
---
@ -176,29 +284,55 @@ VITE_APP_API_SECRET=psi_api_sign_secret
### 7.1 启动开发服务器
```bash
cd D:\project\cards_web
cd D:\project\psi_web
npm run dev
# 访问 http://localhost:3000
# 访问 http://localhost:5174
```
### 7.2 添加新业务模块
### 7.2 生产构建
1. **API 层**:在 `src/api/` 下新建 `xxx.js`
```bash
npm run build
# 输出到 dist/
npm run serve
# 预览构建结果
```
### 7.3 添加新业务模块
1. **API 层**:在 `src/api/` 下新建 `xxx.js`,定义所有接口函数(含 JSDoc
2. **视图层**:在 `src/views/` 下新建目录和 `.vue` 组件
3. **路由注册**:在 `src/router/index.js``children` 中添加路由对象
4. **权限控制**:确保 `meta` 中正确设置 `requiresAuth` / `requiresAdmin`
5. **菜单配置**:在 `AdminLayout.vue` 的侧边栏添加菜单项(如需显示)
6. **列表标准化**:实现 `normalizeListResponse()` 标准化返回格式
### 7.3 API 签名机制
### 7.4 API 开发规范
所有业务请求通过 `signUtil.generateSign` 生成签名参数:
- GET 请求:`params` 签名
- POST/PUT/DELETE`body` 签名
- 所有业务请求通过 `request` 封装,自动处理签名和 FormData 转换
- GET 请求:参数通过 `params` 传递,自动签名
- POST/PUT/DELETE对象参数自动转为 FormData嵌套对象转为 `parentKey[childKey]` 格式)
- 如需跳过签名,在请求头添加 `X-Need-Sign: false`
- 涉及大整数的字段ID 类)注意使用 `toString()` 比较
- 图片/文件上传使用 `multipart/form-data`,删除 `Content-Type` 头让浏览器自动设置
如需跳过签名,可在请求头添加 `X-Need-Sign: false`
### 7.5 Mock 数据调试
### 7.4 Mock 数据调试
如需启用本地 mock 数据进行离线开发:
如需启用本地 mock 数据,将 `src/utils/request.js` 中的 `USE_MOCK` 改为 `true`
```javascript
// src/utils/request.js
const USE_MOCK = true // 改为 true
```
启用的模拟端点:商品列表、卡密管理、代理管理、订单管理、积分记录、卡密购买。
### 7.6 打印调试
打印基于 **C-Lodop** 控件(`http://localhost:8000/CLodopfuncs.js`
- 首次打印套装书条码时弹出打印设置对话框(`PRINT_SETUP`
- 确认后 `localStorage.printFlag = '1'`,后续直接静默打印(`PRINT`
- 波次页搜索栏有"打印调试"按钮,使用测试号 `9787000000000-01`
---
@ -208,25 +342,42 @@ npm run dev
```
src/
├── api/ 12 个业务 API 模块 + 1 个 .bak 备份
├── assets/ 静态资源
├── components/ 公共组件AdminLayout 等)
├── router/ 路由配置hash 模式
├── store/ Pinia 状态管理
├── utils/ 工具函数auth、request、sign、mock
└── views/ 18 个业务页面目录
├── api/ 31 个业务 API 模块
├── assets/ 静态资源
├── components/ 24 个公共组件文件
├── router/ 路由配置history 模式 + 完整路由守卫
├── store/ 1 个 Pinia store + 1 个旧版 reactive store
├── utils/ 8 个工具文件/目录auth、request、sign、mock、打印等
└── views/ 33 个页面目录
```
### 8.2 需清理的文件
### 8.2 需清理的文件
- `src/api/wave-task.js.bak_20260426_172453` — 备份文件,可删除
| 文件 | 原因 |
|------|------|
| `src/api/split.js` | 被 `splitAccount.js` 替代(仅保留 `createSplitConfig` 且用了不同的请求格式) |
| `src/store/index.js` | 旧版 store已被 Pinia 替代 |
| `*.bak` 文件(多个) | 备份残留,确认后可删除 |
| `src/api/waveTask.js.bak_*` | 波次任务备份文件 |
### 8.3 关键代码问题
| 问题 | 位置 | 影响 |
|------|------|------|
| `updataEmployeInfo` 拼写错误 | `src/api/employeeType.js` | 函数名拼写错误,调用方需使用错误名 |
| `submIllegalBook.js` 文件名 | `src/api/` | 文件名与内容不符(实际是 audit 功能) |
| `goosList.vue` 组件名 | `src/components/wave/` | 拼写错误 |
| `split.js` vs `splitAccount.js` | `src/api/` | 重复文件,实现不一致 |
| `normalizeListResponse` 重复定义 | 约 15 个 API 文件 | 代码重复,建议抽离 |
---
## 九、联系方式
- **后端接口负责人**`192.168.101.213:9090`
- **主业务接口前缀**:所有 API 通过 `/api` 代理
- **主业务 API**`https://psi.api.buzhiyushu.cn`(通过 `/api` 代理)
- **打印服务**`https://print.buzhiyushu.cn`(通过 `/api/print` 代理)
- **外部店铺**`https://api.buzhiyushu.cn` / `https://new.taskpool.buzhiyushu.cn:3500`
- **开发服务器**`http://localhost:5174`
---

89
USAGE.md Normal file
View File

@ -0,0 +1,89 @@
# 使用文档
> 📅 文档更新2026-06-07
---
## 一、波次管理 — 套装书条码打印
### 1.1 功能概述
波次扫描中遇到**套装书**(主书 + 副书组合)时,选中后会弹出套装书选择对话框,确认后自动打印条码。条码格式为 `{isbn}-{副标识}`(如 `9787000000000-01`)。
### 1.2 套装书操作流程
1. 扫描书籍 ISBN → 若为套装书(`is_suit=1`),自动弹出 **"选择套装书"** 对话框
2. 从列表中选择对应套装书或添加自定义项需填写副ISBN
3. 点击 **"确认选择"** → 自动生成并打印条码
### 1.3 条码打印 — 调试模式
首次打印条码时会弹出 **C-Lodop 打印设置对话框**(调试模式),可在其中调整:
- 纸张尺寸(默认 60mm × 30mm
- 打印机选择
- 打印方向、边距等参数
**调试模式触发规则:**
| 场景 | 行为 |
|------|------|
| 首次打印套装书条码 | 弹出调试对话框 → 确认后永久记录,后续不再弹出 |
| 后续打印套装书条码 | 直接打印,不再弹出对话框 |
| 手动点击"打印调试"按钮(搜索栏) | 固定使用测试号 `9787000000000-01` 生成条码 → 弹出调试对话框 |
**重置调试模式:**
清除浏览器 localStorage 中的 `printFlag` 即可让调试标记失效,下次打印重新弹出调试框:
```
localStorage.removeItem('printFlag')
```
(可在浏览器开发者工具 → Application → Local Storage 中操作)
---
## 二、波次管理 — 界面说明
### 2.1 波次页布局
| 区域 | 内容 |
|------|------|
| 顶部搜索栏 | ISBN 搜索、仓库选择、小车选择、容量、品相、固定货号、**打印调试按钮** |
| 左侧面板 | 拍照区域Camera 组件) |
| 中间面板 | 书籍信息预览(可编辑本数、自设价格) |
| 右侧面板 | 照片预览 |
| 底部列表 | 已扫描书籍列表 |
### 2.2 操作步骤
1. **选择小车** → 在搜索栏下拉框中选择要操作的小车
2. **扫描 ISBN** → 在搜索框输入或扫码枪扫描 ISBN
3. **拍照alt+a** → 拍摄书籍照片
4. **创建波次alt+x** → 将已扫描的书籍创建为波次任务
5. **继续扫描** → 追加更多书籍到已有波次
---
## 三、C-Lodop 打印设置
### 3.1 服务要求
- 本地需运行 **C-Lodop 打印服务**(默认地址 `http://localhost:8000`
- 如果未启动,点击"打印调试"或打印条码时会提示"加载 C-Lodop 失败"并引导下载
### 3.2 条码打印机配置
1. 进入 **打印机管理** 页面
2. 在 **"条码打印机"** 下拉框中选择对应打印机
3. 点击 **"确定"** 保存(存储于 `localStorage.printer_barcode`
### 3.3 常见问题
| 问题 | 排查 |
|------|------|
| 点击打印无反应 | 检查 C-Lodop 服务是否运行(浏览器访问 `http://localhost:8000` |
| 提示"请先配置条码打印机" | 进入打印机管理页面选择并保存打印机 |
| 条码打印位置偏移 | 使用调试模式打开打印设置对话框,调整边距参数 |
| 想重新弹出调试框 | 清除 `localStorage.printFlag` |
---
*本文档随功能更新持续维护*

View File

@ -1,384 +0,0 @@
<template>
<div class="employee-add">
<el-card>
<template>
<div class="card-header">
<span>添加代理</span>
<el-button @click="$router.push('/admin/employees')">
<el-icon>
<Back />
</el-icon>
</el-button>
</div>
</template>
<el-steps :active="activeStep" finish-status="success" simple class="steps">
<el-step title="填写信息" />
<el-step title="确认信息" />
<el-step title="完成" />
</el-steps>
<!-- 第一步填写信息 -->
<div v-if="activeStep === 1" class="step-content">
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px" class="add-form">
<el-form-item label="姓名" prop="name">
<el-input v-model="form.name" placeholder="请输入代理姓名" :prefix-icon="User" clearable />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="form.password" type="password" placeholder="请输入密码至少6位" :prefix-icon="Lock"
show-password clearable />
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input v-model="form.confirmPassword" type="password" placeholder="请再次输入密码" :prefix-icon="Lock"
show-password clearable />
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input v-model="form.phone" placeholder="请输入手机号" :prefix-icon="Lock" maxlength="11" clearable
show-word-limit />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="nextStep" :loading="checking">
下一步确认信息
</el-button>
</el-form-item>
</el-form>
<!-- 工号预览动态生成 -->
<el-card shadow="never" class="preview-card">
<template #header>
<span>工号预览</span>
</template>
<div class="preview-content">
<div class="preview-item">
<span class="label">工号格式</span>
<span class="value">5位数字自动生成</span>
</div>
<div class="preview-item">
<span class="label">账号格式</span>
<span class="value">dl_ + 工号 dl_00001</span>
</div>
<div class="preview-item">
<span class="label">示例工号</span>
<el-tag size="small">{{ previewEmployeeId }}</el-tag>
</div>
<div class="preview-item">
<span class="label">示例账号</span>
<el-tag size="small" type="success">{{ previewUsername }}</el-tag>
</div>
</div>
</el-card>
</div>
<!-- 第二步确认信息 -->
<div v-if="activeStep === 2" class="step-content">
<el-descriptions :column="1" border class="confirm-info">
<el-descriptions-item label="姓名">{{ form.name }}</el-descriptions-item>
<el-descriptions-item label="工号">{{ previewEmployeeId }}</el-descriptions-item>
<el-descriptions-item label="登录账号">{{ previewUsername }}</el-descriptions-item>
<el-descriptions-item label="手机号">{{ form.phone }}</el-descriptions-item>
<el-descriptions-item label="初始积分">0</el-descriptions-item>
<el-descriptions-item label="角色">代理</el-descriptions-item>
<el-descriptions-item label="状态">正常</el-descriptions-item>
</el-descriptions>
<div class="step-actions">
<el-button @click="prevStep">上一步</el-button>
<el-button type="primary" @click="submitForm" :loading="submitting">
确认添加
</el-button>
</div>
</div>
<!-- 第三步完成 -->
<div v-if="activeStep === 3" class="step-content result">
<el-result icon="success" :title="`添加成功`" :sub-title="`代理 ${resultData?.name} (${resultData?.username}) 已添加`">
<template #extra>
<div class="result-info" ref="resultInfoRef">
<el-alert type="info" :closable="false" show-icon>
<p class="title-center">进销存系统</p>
<p>登录网址https://psi.buzhiyushu.cn/</p>
<p>工号{{ resultData?.employee_id }}</p>
<p>账号{{ resultData?.username }}</p>
<p>初始密码{{ form.password }}</p>
<p>手机号{{ form.phone }}</p>
<p style="color: #f56c6c; margin-top: 10px;">请妥善保管账号信息</p>
</el-alert>
</div>
<div class="result-actions">
<el-button type="primary" @click="resetForm">继续添加</el-button>
<el-button @click="$router.push('/admin/employees')">查看列表</el-button>
<el-button type="success" @click="copyResultInfo">
<el-icon>
<DocumentCopy />
</el-icon>
一键复制
</el-button>
</div>
</template>
</el-result>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { User, Lock, Back, DocumentCopy } from '@element-plus/icons-vue'
import request from '@/utils/request'
import { copyToClipboard } from '@/utils/clipboard'
import { useUserStore } from '@/store/user'
const router = useRouter()
const userStore = useUserStore()
const activeStep = ref(1)
const checking = ref(false)
const submitting = ref(false)
const formRef = ref(null)
const resultData = ref(null)
const resultInfoRef = ref(null)
//
const form = reactive({
name: '',
password: '',
confirmPassword: '',
phone: ''
})
//
const validatePass = (rule, value, callback) => {
if (value === '') {
callback(new Error('请输入密码'))
} else if (value.length < 6) {
callback(new Error('密码长度不能小于6位'))
} else {
if (form.confirmPassword !== '') {
formRef.value?.validateField('confirmPassword')
}
callback()
}
}
const validatePass2 = (rule, value, callback) => {
if (value === '') {
callback(new Error('请再次输入密码'))
} else if (value !== form.password) {
callback(new Error('两次输入密码不一致'))
} else {
callback()
}
}
const rules = {
name: [
{ required: true, message: '请输入代理姓名', trigger: 'blur' },
{ min: 2, max: 20, message: '姓名长度应为2-20个字符', trigger: 'blur' }
],
password: [
{ validator: validatePass, trigger: 'blur' }
],
confirmPassword: [
{ validator: validatePass2, trigger: 'blur' }
],
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
]
}
//
const previewEmployeeId = ref('00001')
const previewUsername = computed(() => `dl_${previewEmployeeId.value}`)
//
const fetchNextEmployeeId = async () => {
try {
//
// 使
const res = await request.get('/admin/employee/list', { params: { page: 1, page_size: 1 } })
if (res.code === 200 && res.data.list.length > 0) {
const lastId = res.data.list[0].employee_id
const seq = parseInt(lastId) + 1
previewEmployeeId.value = String(seq).padStart(5, '0')
}
} catch (error) {
// console.error(':', error)
// 使
previewEmployeeId.value = String(Math.floor(Math.random() * 90000 + 10000)).padStart(5, '0')
}
}
//
const nextStep = async () => {
if (!formRef.value) return
await formRef.value.validate()
activeStep.value = 2
}
//
const prevStep = () => {
activeStep.value = 1
}
//
const submitForm = async () => {
submitting.value = true
try {
// fid login idabout_id login about_id
const currentInfo = userStore.getAdminInfo()
const params = {
name: form.name,
password: form.password,
phone: form.phone,
}
if (currentInfo?.id) {
params.fid = currentInfo.id
}
if (currentInfo?.about_id) {
params.about_id = currentInfo.about_id
}
const res = await request.post('/admin/employee/add', params)
if (res.code === 200) {
resultData.value = res.data
activeStep.value = 3
ElMessage.success('添加成功')
}
} catch (error) {
// console.error(':', error)
ElMessage.error('添加失败,请重试')
} finally {
submitting.value = false
}
}
//
const resetForm = () => {
form.name = ''
form.password = ''
form.confirmPassword = ''
form.phone = ''
activeStep.value = 1
resultData.value = null
fetchNextEmployeeId()
}
//
const copyResultInfo = async () => {
if (!resultData.value) return
const text = `书海寻源代理系统
登录网址https://wallet.buzhiyushu.cn/
工号${resultData.value.employee_id}
账号${resultData.value.username}
初始密码${form.password}
手机号${form.phone}
请妥善保管账号信息`
await copyToClipboard(text, '账号信息已复制到剪贴板')
}
//
onMounted(() => {
fetchNextEmployeeId()
})
</script>
<style scoped>
.employee-add {
padding: 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 16px;
font-weight: 600;
color: #303133;
}
.steps {
margin: 20px 0 40px;
}
.step-content {
min-height: 300px;
padding: 20px 0;
}
.add-form {
width: 500px;
margin: 0 auto;
}
.preview-card {
width: 500px;
margin: 30px auto 0;
background-color: #f8f9fa;
}
.preview-content {
padding: 10px;
}
.preview-item {
margin-bottom: 10px;
display: flex;
align-items: center;
}
.preview-item .label {
width: 80px;
color: #666;
font-size: 13px;
}
.preview-item .value {
color: #333;
font-size: 13px;
}
.confirm-info {
width: 500px;
margin: 0 auto;
}
.step-actions {
margin-top: 30px;
text-align: center;
}
.result {
display: flex;
justify-content: center;
}
.result-info {
margin: 20px 0;
text-align: left;
}
.result-actions {
margin-top: 20px;
display: flex;
gap: 10px;
justify-content: center;
}
:deep(.el-descriptions__label) {
width: 120px;
}
.title-center {
text-align: center;
font-weight: bold;
font-size: 16px;
margin-bottom: 10px;
}
</style>

View File

@ -1,494 +0,0 @@
<template>
<div class="employee-list">
<el-card>
<template #header>
<div class="card-header">
<span>代理列表</span>
<div class="header-actions">
<el-button type="primary" @click="$router.push('/admin/employees/add')">
<el-icon>
<Plus />
</el-icon>
</el-button>
</div>
</div>
</template>
<!-- 搜索条件 -->
<div class="search-form">
<el-form :inline="true" :model="queryParams">
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="全部状态" clearable style="width: 150px">
<el-option label="正常" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</el-form-item>
<el-form-item label="搜索">
<el-input v-model="queryParams.keyword" placeholder="工号/姓名/账号" clearable style="width: 200px"
:prefix-icon="Search" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon>
<Search />
</el-icon>
</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 统计卡片 -->
<el-row :gutter="20" class="stat-cards">
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-item">
<div class="stat-label">总代理数</div>
<div class="stat-value">{{ total }}</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-item">
<div class="stat-label">正常代理</div>
<div class="stat-value success">{{ activeCount }}</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-item">
<div class="stat-label">总积分</div>
<div class="stat-value warning">{{ totalPoints }}</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-item">
<div class="stat-label">平均积分</div>
<div class="stat-value info">{{ averagePoints }}</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 代理列表 -->
<el-table :data="employeeList" v-loading="loading" border style="width: 100%">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="employee_id" label="工号" width="100" align="center" />
<el-table-column prop="username" label="账号" width="150" align="center" />
<el-table-column prop="name" label="姓名" width="120" align="center" />
<el-table-column prop="phone" label="手机号" width="120" align="center" />
<el-table-column prop="score" label="积分" width="100" align="center">
<template #default="{ row }">
<span :class="{ 'points-warning': row.score < 100 }">{{ row.score }}</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
{{ row.status === 1 ? '正常' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="code" label="机械码" width="240" align="center" />
<el-table-column prop="level_info" label="等级" width="60" align="center" />
<el-table-column prop="last_login_at" label="最后登录" width="160" align="center">
<template #default="{ row }">
{{ row.last_login_at ? formatDate(row.last_login_at) : '-' }}
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" width="160" align="center" >
<template #default="{ row }">
{{ formatDate(row.created_at) }}
</template>
</el-table-column>
<!-- <el-table-column label="操作" width="320" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="showTopUp(row)">
<el-icon><Coin /></el-icon>
</el-button>
<el-button type="success" size="small" @click="viewAccess(row)">
<el-icon><DataLine /></el-icon>
</el-button>
<el-button
v-if="row.status === 1"
type="warning"
size="small"
@click="toggleStatus(row, 'disable')"
>
<el-icon><Switch /></el-icon>
</el-button>
<el-button
v-else
type="success"
size="small"
@click="toggleStatus(row, 'enable')"
>
<el-icon><Check /></el-icon>
</el-button>
<el-button type="danger" size="small" @click="showDeduct(row)">
<el-icon><Coin /></el-icon>
</el-button>
</template>
</el-table-column> -->
</el-table>
<!-- 分页 -->
<div class="pagination">
<el-pagination v-model:current-page="queryParams.page" v-model:page-size="queryParams.page_size" :total="total"
:page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange" @current-change="handleCurrentChange" />
</div>
</el-card>
<!-- 充值弹窗 -->
<el-dialog v-model="topUpVisible" title="积分充值" width="400px">
<el-form :model="topUpForm" ref="topUpFormRef" :rules="topUpRules" label-width="80px">
<el-form-item label="代理">
<span>{{ currentEmployee?.name }} ({{ currentEmployee?.employee_id }})</span>
</el-form-item>
<el-form-item label="当前积分">
<span>{{ currentEmployee?.points }}</span>
</el-form-item>
<el-form-item label="充值数量" prop="amount">
<el-input-number v-model="topUpForm.amount" :min="1" :max="100000" style="width: 200px" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="topUpForm.remark" placeholder="选填" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="topUpVisible = false">取消</el-button>
<el-button type="primary" @click="submitTopUp" :loading="topUpLoading">确认充值</el-button>
</span>
</template>
</el-dialog>
<!-- 扣减弹窗 -->
<el-dialog v-model="deductVisible" title="积分扣减" width="400px">
<el-form :model="deductForm" ref="deductFormRef" :rules="deductRules" label-width="80px">
<el-form-item label="代理">
<span>{{ currentEmployee?.name }} ({{ currentEmployee?.employee_id }})</span>
</el-form-item>
<el-form-item label="当前积分">
<span>{{ currentEmployee?.points }}</span>
</el-form-item>
<el-form-item label="扣减数量" prop="amount" class="deduct-label">
<el-input-number class="deduct-input" v-model="deductForm.amount" :min="1" :max="currentEmployee?.points || 1"
style="width: 200px" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="deductForm.remark" placeholder="选填" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="deductVisible = false">取消</el-button>
<el-button type="danger" @click="submitDeduct" :loading="deductLoading">确认扣减</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Search, Coin, DataLine, Switch, Check } from '@element-plus/icons-vue'
import request from '@/utils/request'
const router = useRouter() // router
const loading = ref(false)
const employeeList = ref([])
const total = ref(0)
const topUpVisible = ref(false)
const topUpLoading = ref(false)
const deductVisible = ref(false)
const deductLoading = ref(false)
const currentEmployee = ref(null)
const queryParams = reactive({
page: 1,
page_size: 20,
status: '',
keyword: ''
})
const topUpForm = reactive({
amount: 100,
remark: ''
})
const deductForm = reactive({
amount: 100,
remark: ''
})
const topUpRules = {
amount: [
{ required: true, message: '请输入充值数量', trigger: 'blur' },
{ type: 'number', min: 1, message: '充值数量必须大于0', trigger: 'blur' }
]
}
const deductRules = {
amount: [
{ required: true, message: '请输入扣减数量', trigger: 'blur' },
{ type: 'number', min: 1, message: '扣减数量必须大于0', trigger: 'blur' }
]
}
//
const activeCount = computed(() => {
return employeeList.value.filter(e => e.status === 1).length
})
const totalPoints = computed(() => {
return employeeList.value.reduce((sum, e) => sum + e.score, 0)
})
const averagePoints = computed(() => {
if (employeeList.value.length === 0) return 0
return Math.round(totalPoints.value / employeeList.value.length)
})
//
const formatDate = (timestamp) => {
if (!timestamp) return '-'
const date = new Date(timestamp * 1000)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hour = String(date.getHours()).padStart(2, '0')
const minute = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hour}:${minute}`
}
//
const fetchEmployeeList = async () => {
loading.value = true
try {
const params = {
page: queryParams.page,
page_size: queryParams.page_size
}
if (queryParams.status !== '') {
params.status = queryParams.status
}
if (queryParams.keyword) {
params.keyword = queryParams.keyword
}
const res = await request.get('/admin/employee/list', { params })
if (res.code === 200) {
employeeList.value = res.data.list
total.value = res.data.total
}
} catch (error) {
// console.error(':', error)
} finally {
loading.value = false
}
}
//
const handleSearch = () => {
queryParams.page = 1
fetchEmployeeList()
}
//
const resetSearch = () => {
queryParams.status = ''
queryParams.keyword = ''
queryParams.page = 1
fetchEmployeeList()
}
//
const handleSizeChange = (size) => {
queryParams.page_size = size
fetchEmployeeList()
}
//
const handleCurrentChange = (page) => {
queryParams.page = page
fetchEmployeeList()
}
//
const showTopUp = (row) => {
currentEmployee.value = row
topUpForm.amount = 100
topUpForm.remark = ''
topUpVisible.value = true
}
//
const submitTopUp = async () => {
topUpLoading.value = true
try {
const res = await request.post(`/admin/employee/topup/${currentEmployee.value.employee_id}`, {
amount: topUpForm.amount,
remark: topUpForm.remark
})
if (res.code === 200) {
ElMessage.success('充值成功')
topUpVisible.value = false
fetchEmployeeList() //
}
} catch (error) {
// console.error(':', error)
} finally {
topUpLoading.value = false
}
}
//
const showDeduct = (row) => {
currentEmployee.value = row
deductForm.amount = 100
deductForm.remark = ''
deductVisible.value = true
}
//
const submitDeduct = async () => {
deductLoading.value = true
try {
const res = await request.post(`/admin/employee/deduct/${currentEmployee.value.employee_id}`, {
amount: deductForm.amount,
remark: deductForm.remark
})
if (res.code === 200) {
ElMessage.success('扣减成功')
deductVisible.value = false
fetchEmployeeList() //
}
} catch (error) {
// console.error(':', error)
} finally {
deductLoading.value = false
}
}
//
const toggleStatus = async (row, action) => {
const actionText = action === 'enable' ? '启用' : '禁用'
try {
await ElMessageBox.confirm(`确定要${actionText}代理 ${row.name} 吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
const res = await request.post(`/admin/employee/${action}/${row.employee_id}`)
if (res.code === 200) {
ElMessage.success(`${actionText}成功`)
fetchEmployeeList() //
}
} catch (error) {
if (error !== 'cancel') {
// console.error(':', error)
}
}
}
//
const viewAccess = (row) => {
// ID
router.push({
path: '/admin/access',
query: {
id: row.id,
}
})
}
//
onMounted(() => {
fetchEmployeeList()
})
</script>
<style scoped>
.employee-list {
padding: 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
/* 可根据需要保留原有的字体、颜色等样式 */
font-size: 16px;
font-weight: 600;
color: #303133;
}
.search-form {
margin-bottom: 20px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 4px;
}
.stat-cards {
margin-bottom: 20px;
}
.stat-card {
text-align: center;
}
.stat-item {
padding: 10px;
}
.stat-label {
font-size: 14px;
color: #909399;
margin-bottom: 8px;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #303133;
}
.stat-value.success {
color: #67c23a;
}
.stat-value.warning {
color: #e6a23c;
}
.stat-value.info {
color: #409eff;
}
.pagination {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.points-warning {
color: #f56c6c;
font-weight: bold;
}
.deduct-input :deep(.el-input-number__decrease):hover,
.deduct-input :deep(.el-input-number__increase):hover {
color: #fff;
background-color: #f56c6c;
}
.deduct-label :deep(.el-form-item__label) {
color: #f56c6c;
font-weight: bold;
}
</style>

View File

@ -1,213 +0,0 @@
<template>
<div class="login-container">
<div class="login-box">
<div class="login-header">
<h2>进销存系统</h2>
<p>请选择登录角色</p>
</div>
<el-tabs v-model="activeTab" class="login-tabs">
<el-tab-pane label="管理员登录" name="admin">
<el-form ref="adminFormRef" :model="adminForm" :rules="rules" label-width="0" class="login-form">
<el-form-item prop="username">
<el-input v-model="adminForm.username" placeholder="请输入账号" :prefix-icon="User"
size="large" />
</el-form-item>
<el-form-item prop="password">
<el-input v-model="adminForm.password" type="password" placeholder="请输入密码"
:prefix-icon="Lock" size="large" show-password @keyup.enter="handleLogin" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" class="login-btn" size="large"
@click="handleLogin">
登录
</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="代理登录" name="employee">
<el-form ref="employeeFormRef" :model="employeeForm" :rules="rules" label-width="0"
class="login-form">
<el-form-item prop="username">
<el-input v-model="employeeForm.username" placeholder="请输入账号" :prefix-icon="User"
size="large" />
</el-form-item>
<el-form-item prop="password">
<el-input v-model="employeeForm.password" type="password" placeholder="请输入密码"
:prefix-icon="Lock" size="large" show-password @keyup.enter="handleLogin" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" class="login-btn" size="large"
@click="handleLogin">
登录
</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
<!-- <div class="login-footer">
<p>默认账号代理 init_00001 / 123456</p>
<p>管理员 init_00000 / admin123</p>
</div> -->
</div>
</div>
</template>
<script setup>
import {
ref,
reactive
} from 'vue'
import {
useRouter
} from 'vue-router'
import {
ElMessage
} from 'element-plus'
import {
User,
Lock
} from '@element-plus/icons-vue'
import request from '@/utils/request'
import {
setAdminToken,
setAdminUserInfo
} from '@/utils/auth'
import { useUserStore } from '@/store/user'
const userStore = useUserStore()
const router = useRouter()
const activeTab = ref('admin')
const loading = ref(false)
// ref
const employeeFormRef = ref(null)
const adminFormRef = ref(null)
const employeeForm = reactive({
username: '',
password: '',
about_id:0
// username: 'init_00001',
// password: '123456'
})
const adminForm = reactive({
username: '',
password: '',
about_id:0
// username: 'init_00000',
// password: 'admin123'
})
const rules = {
username: [{
required: true,
message: '请输入账号',
trigger: 'blur'
}],
password: [{
required: true,
message: '请输入密码',
trigger: 'blur'
},
{
min: 6,
message: '密码长度不能小于6位',
trigger: 'blur'
}
]
}
const handleLogin = async () => {
const formRef = activeTab.value === 'employee' ? employeeFormRef : adminFormRef
const formData = activeTab.value === 'employee' ? employeeForm : adminForm
await formRef.value.validate()
loading.value = true
try {
const type = activeTab.value === 'admin' ? '255' : '128'
const res = await request.post(`/login/${type}`, { ...formData, type: 1 })
if (res.code === 200) {
setAdminToken(res.data.token)
setAdminUserInfo(res.data)
userStore.setUserInfoAction(res.data)
//
localStorage.setItem('test_ip', '127.0.0.1')
localStorage.setItem('test_port', '8080')
ElMessage.success('登录成功')
//
router.push('/dashboard')
}
} catch (error) {
// console.error(':', error)
} finally {
loading.value = false
}
}
</script>
<style scoped>
.login-container {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.login-box {
width: 400px;
padding: 40px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.login-header {
text-align: center;
margin-bottom: 30px;
}
.login-header h2 {
font-size: 24px;
color: #333;
margin-bottom: 10px;
}
.login-header p {
color: #666;
font-size: 14px;
}
.login-tabs {
margin-bottom: 20px;
}
.login-form {
margin-top: 20px;
}
.login-btn {
width: 100%;
margin-top: 10px;
}
.login-footer {
margin-top: 30px;
text-align: center;
color: #999;
font-size: 12px;
}
.login-footer p {
margin: 5px 0;
}
</style>

View File

@ -1,475 +0,0 @@
<template>
<div class="review-page">
<el-card class="section-card">
<template #header>
<div class="card-header">
<span>异常书目审核</span>
</div>
</template>
<!-- 搜索筛选栏 -->
<div class="search-bar">
<div class="search-item">
<span>ISBN</span>
<el-input v-model="searchForm.isbn" placeholder="请输入ISBN" style="width: 180px" clearable />
</div>
<div class="search-item">
<span>状态</span>
<el-select v-model="searchForm.status" placeholder="请选择状态" style="width: 140px" clearable>
<el-option label="待审核" value="0" />
<el-option label="已通过" value="1" />
<el-option label="已驳回" value="2" />
</el-select>
</div>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</div>
<!-- 数据表格 -->
<el-table :data="tableData" v-loading="loading" border stripe style="width: 100%"
@selection-change="handleSelectionChange">
<el-table-column prop="status" label="状态" width="150" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" size="small">
{{ statusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="barcode" label="ISBN" width="160" align="center" />
<el-table-column label="书名" min-width="200" show-overflow-tooltip align="center">
<template #default="{ row }">
<el-popover placement="bottom" :width="280" trigger="hover" popper-class="diff-popover">
<template #reference>
<span :class="{ 'diff-cell': isChanged(row.old_name, row.new_name) }">{{ row.old_name }}</span>
</template>
<div class="diff-content">
<div class="diff-row old"><span class="diff-label">旧值</span><span class="diff-val">{{ row.old_name }}</span></div>
<div class="diff-row new"><span class="diff-label">新值</span><span class="diff-val">{{ row.new_name }}</span></div>
</div>
</el-popover>
</template>
</el-table-column>
<el-table-column label="作者" width="150" show-overflow-tooltip align="center">
<template #default="{ row }">
<el-popover placement="bottom" :width="280" trigger="hover" popper-class="diff-popover">
<template #reference>
<span :class="{ 'diff-cell': isChanged(row.old_author, row.new_author) }">{{ row.old_author }}</span>
</template>
<div class="diff-content">
<div class="diff-row old"><span class="diff-label">旧值</span><span class="diff-val">{{ row.old_author }}</span></div>
<div class="diff-row new"><span class="diff-label">新值</span><span class="diff-val">{{ row.new_author }}</span></div>
</div>
</el-popover>
</template>
</el-table-column>
<el-table-column label="出版社" width="150" show-overflow-tooltip align="center">
<template #default="{ row }">
<el-popover placement="bottom" :width="280" trigger="hover" popper-class="diff-popover">
<template #reference>
<span :class="{ 'diff-cell': isChanged(row.old_publisher, row.new_publisher) }">{{ row.old_publisher }}</span>
</template>
<div class="diff-content">
<div class="diff-row old"><span class="diff-label">旧值</span><span class="diff-val">{{ row.old_publisher }}</span></div>
<div class="diff-row new"><span class="diff-label">新值</span><span class="diff-val">{{ row.new_publisher }}</span></div>
</div>
</el-popover>
</template>
</el-table-column>
<el-table-column label="套装书" width="150" show-overflow-tooltip align="center">
<template #default="{ row }">
<el-popover placement="bottom" :width="280" trigger="hover" popper-class="diff-popover">
<template #reference>
<el-tag type="info" size="small" :class="{ 'diff-cell': isChanged(row.old_is_suit, row.new_is_suit) }">
{{ row.old_is_suit === 0 ? '非套装书' : '套装书' }}
</el-tag>
</template>
<div class="diff-content">
<div class="diff-row old"><span class="diff-label">旧值</span><span class="diff-val">{{ row.old_is_suit === 0 ? '非套装书' : '套装书' }}</span></div>
<div class="diff-row new"><span class="diff-label">新值</span><span class="diff-val">{{ row.new_is_suit === 0 ? '非套装书' : '套装书' }}</span></div>
</div>
</el-popover>
</template>
</el-table-column>
<el-table-column label="定价" width="150" show-overflow-tooltip align="center">
<template #default="{ row }">
<el-popover placement="bottom" :width="280" trigger="hover" popper-class="diff-popover">
<template #reference>
<span :class="{ 'diff-cell': isChanged(row.old_price, row.new_price) }">{{ row.old_price }}</span>
</template>
<div class="diff-content">
<div class="diff-row old"><span class="diff-label">旧值</span><span class="diff-val">{{ row.old_price }}</span></div>
<div class="diff-row new"><span class="diff-label">新值</span><span class="diff-val">{{ row.new_price }}</span></div>
</div>
</el-popover>
</template>
</el-table-column>
<el-table-column label="装帧" width="150" show-overflow-tooltip align="center">
<template #default="{ row }">
<el-popover placement="bottom" :width="280" trigger="hover" popper-class="diff-popover">
<template #reference>
<span :class="{ 'diff-cell': isChanged(row.old_binding_layout, row.new_binding_layout) }">{{ row.old_binding_layout }}</span>
</template>
<div class="diff-content">
<div class="diff-row old"><span class="diff-label">旧值</span><span class="diff-val">{{ row.old_binding_layout }}</span></div>
<div class="diff-row new"><span class="diff-label">新值</span><span class="diff-val">{{ row.new_binding_layout }}</span></div>
</div>
</el-popover>
</template>
</el-table-column>
<el-table-column label="页数" width="150" show-overflow-tooltip align="center">
<template #default="{ row }">
<el-popover placement="bottom" :width="280" trigger="hover" popper-class="diff-popover">
<template #reference>
<span :class="{ 'diff-cell': isChanged(row.old_page_count, row.new_page_count) }">{{ row.old_page_count }}</span>
</template>
<div class="diff-content">
<div class="diff-row old"><span class="diff-label">旧值</span><span class="diff-val">{{ row.old_page_count }}</span></div>
<div class="diff-row new"><span class="diff-label">新值</span><span class="diff-val">{{ row.new_page_count }}</span></div>
</div>
</el-popover>
</template>
</el-table-column>
<el-table-column label="字数" width="150" show-overflow-tooltip align="center">
<template #default="{ row }">
<el-popover placement="bottom" :width="280" trigger="hover" popper-class="diff-popover">
<template #reference>
<span :class="{ 'diff-cell': isChanged(row.old_word_count, row.new_word_count) }">{{ row.old_word_count }}</span>
</template>
<div class="diff-content">
<div class="diff-row old"><span class="diff-label">旧值</span><span class="diff-val">{{ row.old_word_count }}</span></div>
<div class="diff-row new"><span class="diff-label">新值</span><span class="diff-val">{{ row.new_word_count }}</span></div>
</div>
</el-popover>
</template>
</el-table-column>
<el-table-column label="出版时间" width="150" show-overflow-tooltip align="center">
<template #default="{ row }">
<el-popover placement="bottom" :width="280" trigger="hover" popper-class="diff-popover">
<template #reference>
<span :class="{ 'diff-cell': isPubTimeChanged(row.old_publication_time, row.new_publication_time) }">{{ formatPubTime(row.old_publication_time) }}</span>
</template>
<div class="diff-content">
<div class="diff-row old"><span class="diff-label">旧值</span><span class="diff-val">{{ formatPubTime(row.old_publication_time) }}</span></div>
<div class="diff-row new"><span class="diff-label">新值</span><span class="diff-val">{{ formatPubTime(row.new_publication_time) }}</span></div>
</div>
</el-popover>
</template>
</el-table-column>
<el-table-column label="提交时间" width="180" align="center">
<template #default="{ row }">
{{ formatDateTime(row.created_at) }}
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right" align="center">
<template #default="{ row }">
<el-button type="primary" @click="handleApprove(row)">
查看
</el-button>
<el-button type="danger" @click="handleReject(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination v-model:current-page="pagination.current" v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]" :total="pagination.total" layout="total, sizes, prev, pager, next, jumper"
@size-change="handlePageChange" @current-change="handlePageChange" />
</div>
</el-card>
<!-- 查看详情弹窗 -->
<el-dialog v-model="detailVisible" title="调剂记录详情" width="60%" :close-on-click-modal="false">
<template v-if="detailData">
<el-descriptions :column="2" border>
<el-descriptions-item label="ISBN">{{ detailData.isbn }}</el-descriptions-item>
<el-descriptions-item label="书名">{{ detailData.bookName }}</el-descriptions-item>
<el-descriptions-item label="作者">{{ detailData.author }}</el-descriptions-item>
<el-descriptions-item label="出版社">{{ detailData.publisher }}</el-descriptions-item>
<el-descriptions-item label="异常类型">{{ detailData.abnormalType }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="statusTagType(detailData.status)" size="small">
{{ statusText(detailData.status) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="异常说明" :span="2">
{{ detailData.abnormalDesc || '无' }}
</el-descriptions-item>
<el-descriptions-item label="提交时间">{{ detailData.createdAt }}</el-descriptions-item>
<el-descriptions-item v-if="detailData.reviewTime" label="审核时间">{{ detailData.reviewTime
}}</el-descriptions-item>
</el-descriptions>
</template>
</el-dialog>
<!-- 审核弹窗 - 使用 SubmIllegalBook 组件 -->
<SubmIllegalBook
v-model:visible="reviewDialogVisible"
:review-data="reviewRecord"
:goods="null"
@review="handleReviewResult"
/>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '@/utils/request'
import SubmIllegalBook from '@/components/dialog/submIllegalBook/SubmIllegalBook.vue'
//
const searchForm = reactive({
isbn: '',
status: ''
})
//
const tableData = ref<any[]>([])
const loading = ref(false)
const selectedRows = ref<any[]>([])
//
const pagination = reactive({
current: 1,
pageSize: 20,
total: 0
})
//
const detailVisible = ref(false)
const detailData = ref<any>(null)
//
const reviewDialogVisible = ref(false)
const reviewRecord = ref<any>(null)
//
function statusTagType(status: string): string {
const map: Record<string, string> = {
0: 'warning',
1: 'success',
2: 'danger'
}
return map[status] || 'info'
}
// yyyy-mm10
function formatPubTime(time: any): string {
if (time === null || time === undefined || time === '') return ''
const ts = Number(time)
if (isNaN(ts) || ts <= 0) return ''
const d = new Date(ts * 1000)
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
return `${y}-${m}`
}
// yyyy-mm-dd HH:mm:ss10
function formatDateTime(time: any): string {
if (time === null || time === undefined || time === '') return ''
const ts = Number(time)
if (isNaN(ts) || ts <= 0) return ''
const d = new Date(ts * 1000)
const y = d.getFullYear()
const M = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const h = String(d.getHours()).padStart(2, '0')
const m = String(d.getMinutes()).padStart(2, '0')
const s = String(d.getSeconds()).padStart(2, '0')
return `${y}-${M}-${day} ${h}:${m}:${s}`
}
//
function isChanged(oldVal: any, newVal: any): boolean {
//
if ((oldVal === null || oldVal === undefined || oldVal === '') &&
(newVal === null || newVal === undefined || newVal === '')) {
return false
}
return String(oldVal) !== String(newVal)
}
//
function isPubTimeChanged(oldTs: any, newTs: any): boolean {
return isChanged(formatPubTime(oldTs), formatPubTime(newTs))
}
//
function statusText(status: string): string {
const map: Record<string, string> = {
0: '待审核',
1: '已通过',
2: '已驳回'
}
return map[status] || status
}
//
function handleSearch() {
pagination.current = 1
fetchData()
}
//
function handleReset() {
searchForm.isbn = ''
searchForm.status = ''
handleSearch()
}
//
function handleSelectionChange(rows: any[]) {
selectedRows.value = rows
}
//
function handlePageChange() {
fetchData()
}
//
async function fetchData() {
loading.value = true
try {
const res = await request.get('/product_log/list', {
params: {
barcode: searchForm.isbn || undefined,
status: searchForm.status || undefined,
page: pagination.current,
page_size: pagination.pageSize
}
})
const data = res?.data
if (data) {
tableData.value = Array.isArray(data.list) ? data.list : Array.isArray(data) ? data : []
pagination.total = data.total || 0
} else {
tableData.value = []
pagination.total = 0
}
} catch (err) {
ElMessage.error('获取列表失败')
} finally {
loading.value = false
}
}
// -
function handleApprove(row: any) {
reviewRecord.value = row
reviewDialogVisible.value = true
}
//
async function handleReject(row: any) {
try {
await ElMessageBox.confirm('确认删除该条记录?', '提示', {
type: 'warning'
})
const formData = new FormData()
formData.append('id', String(row.id))
await request.post('/product_log/delete', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
ElMessage.success('已删除')
fetchData()
} catch {
//
}
}
// -
function handleReviewResult(action: 'approve' | 'reject', record: any) {
fetchData()
}
//
function handleViewDetail(row: any) {
detailData.value = row
detailVisible.value = true
}
onMounted(() => {
fetchData()
})
</script>
<style scoped>
.review-page {
padding: 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.section-card {
margin-bottom: 16px;
}
.search-bar {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 16px;
}
.search-item {
display: flex;
align-items: center;
gap: 8px;
}
.search-item span {
font-size: 14px;
color: #606266;
white-space: nowrap;
}
.pagination-wrapper {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
</style>
<style>
/* 新旧数据对比 - 单元格变红 */
.diff-cell {
color: #f56c6c !important;
font-weight: bold;
}
/* Popover 内容样式(全局,因 popover 渲染在 body 下) */
.diff-popover {
padding: 8px 12px !important;
}
.diff-content {
font-size: 13px;
line-height: 1.6;
}
.diff-row {
display: flex;
align-items: flex-start;
gap: 4px;
padding: 2px 0;
}
.diff-label {
white-space: nowrap;
color: #909399;
flex-shrink: 0;
}
.diff-val {
color: #303133;
word-break: break-all;
}
.diff-row.new .diff-val {
color: #e6a23c;
font-weight: bold;
}
</style>

View File

@ -1,771 +0,0 @@
<template>
<el-card class="shipping-order-manager">
<template #header>
<div class="card-header">发货单管理</div>
</template>
<div class="filter-bar">
<el-input v-model="searchParams.keyword" placeholder="发货单号" clearable style="width: 200px"
@keyup.enter="handleSearch">
<template #prefix>
<el-icon>
<Search />
</el-icon>
</template>
</el-input>
<el-select v-model="searchParams.status" placeholder="状态" clearable style="width: 120px">
<el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-select v-model="searchParams.warehouse_id" placeholder="仓库" clearable style="width: 140px">
<el-option v-for="item in warehouseOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<el-select v-model="searchParams.customer_id" placeholder="客户" clearable filterable style="width: 160px">
<el-option v-for="item in customerOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
<el-button :icon="Refresh" @click="resetSearch">重置</el-button>
</div>
<el-table :data="tableData" v-loading="loading" border stripe style="width: 100%"
@expand-change="handleExpandChange">
<!-- 发货单详情展开行 -->
<el-table-column type="expand">
<template #default="{ row }">
<div v-if="detailCache[row.id]" style="padding: 12px 20px">
<h4 style="margin: 0 0 10px; font-size: 14px; color: #303133">发货明细</h4>
<el-table :data="detailCache[row.id].items || []" border size="small">
<el-table-column prop="product_name" label="商品名称" min-width="120" show-overflow-tooltip align="center" />
<el-table-column prop="product_code" label="ISBN/条码" min-width="100" show-overflow-tooltip
align="center" />
<el-table-column label="库位" min-width="100" align="center">
<template #default="{ row: item }">
{{ locationMap[item.warehouse_code] || item.warehouse_code || '-' }}##{{
locationMap[item.location_name]
|| item.location_name || '-' }}
</template>
</el-table-column>
<el-table-column label="销售单号" min-width="140" align="center">
<template #default="{ row: item }">
<a v-if="item.sales_order_no" style="color: #409eff; cursor: pointer; text-decoration: underline;"
@click.stop="navigateToSalesOrder(item.sales_order_no)">
{{ item.sales_order_no }}
</a>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="出库单号" min-width="140" align="center">
<template #default="{ row: item }">
<a v-if="item.outbound_order_no" style="color: #409eff; cursor: pointer; text-decoration: underline;"
@click.stop="navigateToOutbound(item.outbound_order_no)">
{{ item.outbound_order_no }}
</a>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="association_order_no" label="平台单号" min-width="90" align="center">
<template #default="{ row: item }">
<span style="display: inline-flex; align-items: center; gap: 4px;">
<span v-if="item.association_order_no" style="color: #409eff; text-decoration: underline;">{{ item.association_order_no }}</span>
<span v-else>-</span>
<el-button v-if="item.association_order_no" type="primary" size="small" link
@click="copyToClipboard(item.association_order_no, '平台单号已复制')">
<el-icon>
<CopyDocument />
</el-icon>
</el-button>
</span>
</template>
</el-table-column>
<el-table-column label="销售时间" min-width="100" align="center">
<template #default="{ row: item }">{{ formatDateForSale(item.sales_order_created_at) }}</template>
</el-table-column>
<el-table-column label="售价" min-width="40" align="center">
<template #default="{ row: item }">
<span style="color: #e6a23c; font-weight: 600">{{ formatAmount(item.unit_price) }}</span>
</template>
</el-table-column>
<el-table-column prop="quantity" label="出库数量" min-width="60" align="center">
<template #default="{ row: item }">
<span style="color: #409eff; font-weight: 600">{{ item.quantity }}</span>
</template>
</el-table-column>
<el-table-column label="快递公司" min-width="100" align="center">
<template #default="{ row: item }">{{ logisticsCompanyMap[item.logistics_company] ||
item.logistics_company || '-' }}</template>
</el-table-column>
<el-table-column prop="logistics_no" label="快递单号" min-width="100" align="center">
<template #default="{ row: item }">{{ item.logistics_no || '-' }}</template>
</el-table-column>
<el-table-column prop="receiver_address" label="收货地址" min-width="100" align="center">
<template #default="{ row: item }">{{ item.receiver_address || '-' }}</template>
</el-table-column>
</el-table>
</div>
<div v-else style="padding: 20px; text-align: center; color: #909399">
<el-icon class="is-loading" style="margin-right: 6px">
<Loading />
</el-icon>...
</div>
</template>
</el-table-column>
<el-table-column prop="shipping_no" label="发货单号" min-width="160" show-overflow-tooltip align="center" />
<el-table-column label="客户" min-width="160" align="center">
<template #default="{ row }">
<template v-if="row.shop_list && row.shop_list.length > 0">
<div v-for="(shop, idx) in row.shop_list" :key="idx">
{{ shop.shop_name }}({{ shop.shop_type_text }})
</div>
</template>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="status_text" label="状态" min-width="80" show-overflow-tooltip align="center" />
<el-table-column prop="operator" label="操作员" min-width="90" align="center">
<template #default="{ row }">{{ row.operator || '-' }}</template>
</el-table-column>
<el-table-column label="创建时间" min-width="150" align="center">
<template #default="{ row }">{{ formatTimestamp(row.created_at) }}</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip align="center">
<template #default="{ row }">{{ row.remark || '-' }}</template>
</el-table-column>
<el-table-column label="操作" align="center" width="100">
<template #default="{ row }">
<el-button type="primary" size="small" link @click="handleStartShipping(row)">开始发货</el-button>
</template>
</el-table-column>
<el-table-column label="" align="center" width="70">
<template #default="{ row }">
<el-button type="primary" size="small" link>打单</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper">
<el-pagination v-model:current-page="pagination.current" v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]" :total="pagination.total" layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange" @current-change="handleCurrentChange" />
</div>
<!-- 扫码发货弹窗 -->
<el-dialog v-model="scanDialogVisible" title="扫码发货" width="1000px" :close-on-click-modal="false"
@opened="onScanDialogOpened" @closed="onScanDialogClosed">
<div style="padding: 10px">
<p style="margin-bottom: 12px; color: #606266; text-align: center">
<strong style="color:#409eff">{{ currentScanTotal }}</strong> 件商品已扫描 <strong style="color:#67c23a">{{
currentScanIndex }}</strong>
</p>
<el-table :data="scanList" border size="small" style="width: 100%" max-height="340"
:row-class-name="scanRowClassName">
<el-table-column type="index" label="#" width="40" align="center" />
<el-table-column prop="product_name" label="商品名称" min-width="130" show-overflow-tooltip />
<el-table-column prop="product_code" label="ISBN" width="120" align="center" />
<el-table-column prop="quantity" label="数量" width="60" align="center" />
<el-table-column prop="association_order_no" label="平台单号" min-width="150" align="center"
show-overflow-tooltip>
<template #default="{ row }">
<span v-if="row.association_order_no" style="display: inline-flex; align-items: center; gap: 4px;">
<span style="color: #409eff; text-decoration: underline;">{{ row.association_order_no }}</span>
<el-button type="primary" size="small" link
@click="copyToClipboard(row.association_order_no, '平台单号已复制')">
<el-icon>
<CopyDocument />
</el-icon>
</el-button>
</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="快递单号" min-width="120" align="center">
<template #default="{ row, $index }">
<span v-if="$index < currentScanIndex && row.logistics_no" style="color: #67c23a;">{{ row.logistics_no
}}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="售价" width="80" align="center">
<template #default="{ row }">
<span style="color: #e6a23c; font-weight: 600">{{ formatAmount(row.unit_price) }}</span>
</template>
</el-table-column>
<el-table-column label="状态" width="90" align="center">
<template #default="{ row, $index }">
<template v-if="$index < currentScanIndex || row.logistics_no">
<el-tag type="success" size="small">已扫</el-tag>
</template>
<template v-else-if="$index === processingIndex">
<el-tag type="warning" size="small" effect="dark">扫描中</el-tag>
</template>
<template v-else>
<el-tag type="info" size="small">待扫</el-tag>
</template>
</template>
</el-table-column>
</el-table>
<el-input ref="scanInputRef" v-model="scanCode" placeholder="请用扫码枪扫描 ISBN..." @keyup.enter="handleScanSubmit"
class="hidden-scan-input" />
<p style="margin-top: 8px; color: #c0c4cc; font-size: 12px; text-align: center">请使用扫码枪扫描商品条码</p>
</div>
</el-dialog>
</el-card>
</template>
<script lang="ts">
import { defineComponent, ref, reactive, onMounted, computed, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { Search, Refresh, View, Loading, CopyDocument } from '@element-plus/icons-vue'
import dayjs from 'dayjs'
import axios from 'axios'
import { copyToClipboard } from '@/utils/clipboard'
import { fetchShippingOrderList, fetchShippingOrderDetail, updateShippingOrderLogistics } from '@/api/shipping-order'
import { fetchWarehouseList } from '@/api/warehouse'
import { createPrintTask } from '@/api/print'
/** 状态映射 */
const STATUS_MAP: Record<number, { label: string; type: string }> = {
1: { label: '已创建', type: 'info' },
2: { label: '拣货中', type: 'warning' },
3: { label: '已完成', type: 'success' },
4: { label: '已取消', type: 'danger' }
}
export default defineComponent({
name: 'ShippingOrder',
setup() {
const router = useRouter()
const loading = ref<boolean>(false)
const tableData = ref<any[]>([])
/** 导航到销售订单页面 */
const navigateToSalesOrder = (salesOrderNo: string) => {
router.push({ name: 'sales-order', query: { keyword: salesOrderNo } })
}
/** 导航到出库单页面 */
const navigateToOutbound = (outboundOrderNo: string) => {
router.push({ name: 'outbound', query: { keyword: outboundOrderNo } })
}
const statusOptions = Object.entries(STATUS_MAP).map(([value, { label }]) => ({
value: Number(value),
label
}))
const searchParams = reactive<{
keyword: string
status: number | null
warehouse_id: number | null
customer_id: number | null
}>({
keyword: '',
status: null,
warehouse_id: null,
customer_id: null
})
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0
})
const detailVisible = ref<boolean>(false)
const detailData = ref<any>(null)
//
const warehouseOptions = ref<any[]>([])
const customerOptions = ref<any[]>([])
// ID
const warehouseMap = ref<Record<string, string>>({})
const customerMap = ref<Record<string, string>>({})
const salesOrderMap = ref<Record<string, string>>({})
const waveTaskMap = ref<Record<string, string>>({})
const locationMap = ref<Record<string, string>>({})
//
const detailCache = ref<Record<number, any>>({})
// ========== ==========
const scanDialogVisible = ref(false)
const scanCode = ref('')
const scanInputRef = ref<any>(null)
const scanList = ref<any[]>([]) //
const currentScanIndex = ref(0) //
const currentScanTotal = ref(0) //
const scanFocusTimer = ref<ReturnType<typeof setInterval> | null>(null)
const currentScanItem = computed(() => scanList.value[currentScanIndex.value] || null)
const formatAmount = (amount: number): string => {
if (!amount && amount !== 0) return '¥0.00'
return '¥' + (Number(amount) / 100).toFixed(2)
}
//
const isProcessing = ref(false)
const processingIndex = ref(-1)
/** 已扫描完成的行添加背景色 */
const scanRowClassName = ({ row, rowIndex }: { row: any; rowIndex: number }) => {
if (rowIndex < currentScanIndex.value || row.logistics_no) {
return 'scan-completed-row'
}
return ''
}
/** 点击 "开始发货" */
const handleStartShipping = async (row: any) => {
//
let detail = detailCache.value[row.id]
if (!detail || !detail.items) {
try {
detail = await fetchShippingOrderDetail(row.id)
detailCache.value[row.id] = detail
} catch (e) {
ElMessage.error('加载发货明细失败')
return
}
}
const items = detail?.items || []
if (items.length === 0) {
ElMessage.warning('该发货单没有明细')
return
}
scanList.value = items.map((item: any) => ({ ...item, shipping_order_id: row.id }))
//
const firstUnscanned = scanList.value.findIndex((item: any) => !item.logistics_no)
currentScanIndex.value = firstUnscanned >= 0 ? firstUnscanned : scanList.value.length
currentScanTotal.value = items.length
scanCode.value = ''
scanDialogVisible.value = true
}
/** 弹窗打开后聚焦并锁定焦点到隐藏 input */
const onScanDialogOpened = () => {
const focusInput = () => {
scanInputRef.value?.focus?.()
scanInputRef.value?.$el?.querySelector?.('input')?.focus?.()
}
//
nextTick(() => { focusInput() })
// 200ms
const intervalId = setInterval(() => {
if (!scanDialogVisible.value) {
clearInterval(intervalId)
return
}
//
const active = document.activeElement
const inputEl = scanInputRef.value?.$el?.querySelector?.('input')
if (inputEl && active !== inputEl) {
inputEl.focus()
}
}, 200)
// dialog interval
const origClose = onScanDialogClosed
const cleanup = () => {
clearInterval(intervalId)
origClose()
}
}
/** 弹窗关闭时清理 */
const onScanDialogClosed = () => {
scanCode.value = ''
scanList.value = []
currentScanIndex.value = 0
currentScanTotal.value = 0
}
/** 扫码回车处理 */
const handleScanSubmit = async () => {
const code = scanCode.value.trim()
if (!code) return
//
if (isProcessing.value) return
//
isProcessing.value = true
processingIndex.value = currentScanIndex.value
// ISBN
const matchIndex = scanList.value.findIndex(
(item: any) => item.product_code === code
)
if (matchIndex === -1) {
ElMessage.warning(`未找到 ISBN 为 ${code} 的商品`)
isProcessing.value = false
processingIndex.value = -1
scanCode.value = ''
return
}
//
if (matchIndex !== currentScanIndex.value) {
const temp = scanList.value[currentScanIndex.value]
scanList.value[currentScanIndex.value] = scanList.value[matchIndex]
scanList.value[matchIndex] = temp
}
const currentItem = scanList.value[currentScanIndex.value]
if (!currentItem) {
isProcessing.value = false
processingIndex.value = -1
return
}
const salesPersonId = currentItem.sales_person_id
if (!salesPersonId) {
ElMessage.warning('当前商品缺少 sales_person_id')
isProcessing.value = false
processingIndex.value = -1
scanCode.value = ''
return
}
try {
//
const res = await axios.get('https://api.buzhiyushu.cn/zhishu/fastMail/listApi', {
params: { shopId: salesPersonId }
})
console.log('fastMail/listApi 响应:', res.data)
// fastMailType=1
const data = res.data?.data
if (!Array.isArray(data) || data.length === 0) {
ElMessage.error('未获取到快递账号配置')
isProcessing.value = false
processingIndex.value = -1
scanCode.value = ''
return
}
const fastMail = data.find((item: any) => item.fastMailType === '1')
if (!fastMail || !currentItem.association_order_no) {
ElMessage.error('未找到默认快递配置或缺少平台单号')
isProcessing.value = false
processingIndex.value = -1
scanCode.value = ''
return
}
const formData = new FormData()
formData.append('type', fastMail.type)
formData.append('partnerId', fastMail.partnerId)
formData.append('secret', fastMail.secret)
formData.append('orderSn', currentItem.association_order_no)
formData.append('contact', currentItem.warehouse_contact_person)
formData.append('phoneNumber', currentItem.warehouse_contact_phone)
formData.append('province', currentItem.warehouse_province)
formData.append('city', currentItem.warehouse_city)
formData.append('area', currentItem.warehouse_district)
formData.append('town', currentItem.warehouse_address)
const createRes = await axios.post('https://print.buzhiyushu.cn/api/print/createOrderBatch', formData)
console.log('createOrderBatch 响应:', createRes.data)
// PDF
const createData = await axios.get('https://print.buzhiyushu.cn/api/print/createBmOrderDaYin', {
params: {
mailno: createRes.data.data[0].mail_no || createRes.data.data[0].mailno,
partnerId: fastMail.partnerId,
secret: fastMail.secret
}
})
console.log('createBmOrderDaYin 响应:', createData.data)
const printData = createRes.data.data[0]
printData.pdf_info = createData.data.pdfInfo
printData.itemList = createRes.data.erpGoodsOrderList[0].itemList;
console.log('createBmOrderDaYin 响应:', JSON.stringify(printData))
// createPrintTask
console.log('正在创建打印任务并打印...')
const LODOP = await createPrintTask('yunda', printData) as any
LODOP.SET_PRINTER_INDEX(localStorage.getItem('printer_express'));
const printResult = LODOP.PRINT()
console.log('打印结果:', printResult)
// updateShippingOrderLogistics
if (currentItem.sales_order_item_id) {
await updateShippingOrderLogistics({
shipping_order_id: currentItem.shipping_order_id,
sales_order_item_id: currentItem.sales_order_item_id,
logistics_company: fastMail.type,
logistics_no: createRes.data.data[0].mail_no || createRes.data.data[0].mailno
})
}
console.log('正在回填快递单号并更新状态...')
const submitCompany = await axios.post('https://api.buzhiyushu.cn/zhishu/orderExternalGoods/submitCompanyOrder', {
code: fastMail.type,
orderNo: createRes.data.data[0].mail_no || createRes.data.data[0].mailno,
erpOrderId: createRes.data.erpGoodsOrderList[0].id.toString()
}, {
headers: { 'Content-Type': 'application/json' }
});
console.log('回填快递单号 响应:', submitCompany.data)
//
scanList.value[currentScanIndex.value].logistics_no = createRes.data.data[0].mail_no || createRes.data.data[0].mailno
} catch (err) {
console.error('发货处理失败:', err)
ElMessage.error('发货处理失败,请重试')
isProcessing.value = false
processingIndex.value = -1
scanCode.value = ''
return
}
//
isProcessing.value = false
processingIndex.value = -1
currentScanIndex.value++
scanCode.value = ''
if (currentScanIndex.value >= scanList.value.length) {
ElMessage.success('全部扫描完成!')
scanDialogVisible.value = false
}
}
const statusLabel = (status: number): string => STATUS_MAP[status]?.label || '未知'
const statusTagType = (status: number): string => STATUS_MAP[status]?.type || 'info'
const formatTimestamp = (timestamp?: number | string | null): string => {
if (!timestamp && timestamp !== 0) return '-'
return dayjs.unix(Number(timestamp)).format('YYYY-MM-DD HH:mm')
}
const formatDate = (date?: number | string | null): string => {
if (!date) return '-'
if (typeof date === 'number' && date < 10000000000) {
return dayjs.unix(date).format('YYYY-MM-DD')
}
return dayjs(date).format('YYYY-MM-DD')
}
const formatDateForSale = (date?: number | string | null): string => {
if (!date) return '-'
if (typeof date === 'number' && date < 10000000000) {
return dayjs.unix(date).format('YYYY-MM-DD HH:mm')
}
return dayjs(date).format('YYYY-MM-DD HH:mm')
}
/** 快递公司映射 */
const logisticsCompanyMap: Record<string, string> = {
YUNDA: '韵达快递'
}
/** 加载下拉选项和名称映射 */
const loadOptions = async (): Promise<void> => {
try {
//
const warehouseRes = await fetchWarehouseList({ keyword: '', page: 1, pageSize: 9999 })
warehouseOptions.value = warehouseRes.list || []
const wMap: Record<string, string> = {}
for (const w of warehouseRes.list) {
wMap[String(w.id)] = w.name || w.code || String(w.id)
}
warehouseMap.value = wMap
// TODO: API
customerOptions.value = []
customerMap.value = {}
} catch (error) {
// console.error(':', error)
}
}
/** 加载发货单列表 */
const loadList = async (): Promise<void> => {
loading.value = true
try {
const res = await fetchShippingOrderList({
check_no: searchParams.keyword,
status: searchParams.status || undefined,
warehouse_id: searchParams.warehouse_id || undefined,
customer_id: searchParams.customer_id || undefined,
page: pagination.current,
pageSize: pagination.pageSize
})
tableData.value = res.list || []
pagination.total = res.total || 0
} catch (error) {
// console.error(':', error)
ElMessage.error('加载发货单列表失败')
} finally {
loading.value = false
}
}
/** 搜索 */
const handleSearch = (): void => {
pagination.current = 1
loadList()
}
/** 重置搜索 */
const resetSearch = (): void => {
searchParams.keyword = ''
searchParams.status = null
searchParams.warehouse_id = null
searchParams.customer_id = null
pagination.current = 1
loadList()
}
/** 分页大小变化 */
const handleSizeChange = (size: number): void => {
pagination.pageSize = size
loadList()
}
/** 页码变化 */
const handleCurrentChange = (page: number): void => {
pagination.current = page
loadList()
}
/** 展开行时加载详情 */
const handleExpandChange = async (row: any, expandedRows: any[]): Promise<void> => {
if (expandedRows.find((r: any) => r.id === row.id) && !detailCache.value[row.id]) {
try {
const detail = await fetchShippingOrderDetail(row.id)
detailCache.value[row.id] = detail
} catch (error) {
// console.error(':', error)
detailCache.value[row.id] = { items: [] }
}
}
}
/** 查看发货单详情 */
const handleView = async (row: any): Promise<void> => {
try {
const detail = await fetchShippingOrderDetail(row.id)
detailData.value = detail
detailVisible.value = true
} catch (error) {
// console.error(':', error)
ElMessage.error('加载详情失败')
}
}
onMounted(() => {
loadOptions()
loadList()
})
return {
loading,
tableData,
statusOptions,
searchParams,
pagination,
detailVisible,
detailData,
warehouseOptions,
customerOptions,
warehouseMap,
customerMap,
salesOrderMap,
waveTaskMap,
locationMap,
detailCache,
scanDialogVisible,
scanInputRef,
scanCode,
scanList,
currentScanIndex,
currentScanTotal,
currentScanItem,
isProcessing,
processingIndex,
formatAmount,
scanRowClassName,
handleStartShipping,
onScanDialogOpened,
onScanDialogClosed,
handleScanSubmit,
statusLabel,
statusTagType,
formatTimestamp,
formatDate,
formatDateForSale,
logisticsCompanyMap,
handleSearch,
resetSearch,
handleSizeChange,
handleCurrentChange,
handleExpandChange,
handleView,
navigateToSalesOrder,
navigateToOutbound,
copyToClipboard,
Search,
Refresh,
View,
Loading,
CopyDocument
}
}
})
</script>
<style scoped lang="scss">
.shipping-order-manager {
width: 100%;
}
.card-header {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.filter-bar {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 16px;
}
.pagination-wrapper {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
:deep(.el-table__expanded-cell) {
padding: 0;
}
/* 隐藏扫码输入框,仅保留焦点功能 */
.hidden-scan-input {
position: absolute;
left: -9999px;
opacity: 0;
width: 1px;
height: 1px;
}
/* 已完成扫描的行背景色 */
:deep(.el-table__row.scan-completed-row) {
background-color: #ecf5ff;
}
:deep(.el-table__row.scan-completed-row:hover > td) {
background-color: #ecf5ff;
}
</style>

View File

@ -1,97 +0,0 @@
import axios from 'axios'
/**
* 测试连接核价器
* @param {string} ip - IP 地址
* @param {string} port - 端口
* @returns {Promise}
*/
export const testConnection = (ip, port) => {
const params = new URLSearchParams()
params.append('isbn', '0')
params.append('out_id', '0')
params.append('quality', '0')
params.append('query_index', '1')
params.append('user_id', '0')
return axios.post(`http://${ip}:${port}/api/goods/query`, params.toString(), {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
timeout: 10000
}).then(res => res.data)
}
/**
* 保存新价格到核价器
* @param {string} ip - IP 地址
* @param {string} port - 端口
* @returns {Promise}
*/
export const saveNewPrice = (ip, port, newPrice, placeholderDownPrice, minShippingFee, minPrice, verifyIndex) => {
const formData = new URLSearchParams()
formData.append('new_price', newPrice)
formData.append('placeholder_down_price', placeholderDownPrice)
formData.append('min_shipping_fee', minShippingFee)
formData.append('min_price', minPrice)
formData.append('query_index', verifyIndex)
console.log(formData.toString())
return axios.post(`http://${ip}:${port}/api/config/price/set`, formData.toString(), {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
timeout: 10000
}).then(res => res.data)
}
/**
* 孔夫子旧书网登录直接请求核价器
* @param {string} username - 孔网用户名
* @param {string} password - 孔网密码
* @param {string} ip - 核价器 IP
* @param {string} port - 核价器端口
* @returns {Promise<{code: number, data: {token: string, nickname: string}, message: string}>}
*/
export const kongfzLogin = (username, password, ip, port) => {
const formData = new FormData()
formData.append('username', username)
formData.append('password', password)
return axios.post(`http://${ip}:${port}/api/kfz/login`, formData, {
timeout: 15000
}).then(res => res.data)
}
/**
* 批量提交 Token 到核价器
* @param {Array<{username: string, token: string}>} tokens - 账号 Token 列表
* @param {string} ip - 核价器 IP
* @param {string} port - 核价器端口
* @returns {Promise}
*/
export const batchAddTokens = (tokens, ip, port) => {
return axios.post(`http://${ip}:${port}/api/token/add`, tokens, {
headers: { 'Content-Type': 'application/json' },
timeout: 10000
}).then(res => res.data)
}
/**
* 从核价器获取已保存的 Token 列表
* @param {string} ip - 核价器 IP
* @param {string} port - 核价器端口
* @returns {Promise<{code: number, data: Array<{Username: string, Token: string, ID: number, IsEnable: boolean}>, message: string}>}
*/
export const fetchTokenList = (ip, port) => {
return axios.post(`http://${ip}:${port}/api/token/list`, {}, {
headers: { 'Content-Type': 'application/json' },
timeout: 10000
}).then(res => res.data)
}
/**
* 从核价器获取config配置
* @param {string} ip - 核价器 IP
* @param {string} port - 核价器端口
*/
export const fetchConfig = (ip, port) => {
return axios.post(`http://${ip}:${port}/api/config/price/get`, {}, {
headers: { 'Content-Type': 'application/json' },
timeout: 10000
}).then(res => res.data)
}

View File

@ -1,968 +0,0 @@
<template>
<div class="config-page">
<el-card class="config-card" shadow="always">
<template #header>
<div class="card-header">
<div class="header-left">
<el-icon :size="20">
<Setting />
</el-icon>
<span>核价器配置</span>
</div>
</div>
</template>
<el-form label-width="80px" label-position="top">
<div class="config-row">
<el-form-item class="flex-item">
<template #label>
<span class="label-with-icon">
<span>程序所在位置</span>
<el-tooltip content="verifyTool的位置" placement="top" trigger="click">
<el-icon style="cursor: pointer;">
<QuestionFilled />
</el-icon>
</el-tooltip>
</span>
</template>
<el-input v-model="dir" placeholder="如 C:\\verifyTool" clearable @clear="dir = ''" />
<el-tooltip content="通过自定义协议启动本机程序" placement="top" trigger="click">
<el-button
type="primary"
:icon="VideoPlay"
style="margin-left: 10px"
:disabled="!dir"
@click="handleOpenExe"
>
打开程序
</el-button>
</el-tooltip>
</el-form-item>
</div>
<div class="config-row">
<!-- IP地址 -->
<el-form-item class="flex-item">
<template #label>
<span class="label-with-icon">
<span>IP 地址</span>
<el-tooltip content="一般都是 127.0.0.1 如需特殊配置 请联系网管" placement="top" trigger="click">
<el-icon style="cursor: pointer;">
<QuestionFilled />
</el-icon>
</el-tooltip>
</span>
</template>
<el-input v-model="ip" placeholder="如 192.168.1.1" clearable />
</el-form-item>
<!-- 端口 -->
<el-form-item class="flex-item">
<template #label>
<span class="label-with-icon">
<span>端口</span>
<el-tooltip content="默认是8080 但是由于每台电脑的环境都不同 可能会出现端口冲突 在出现问题时候 请第一时间联系网管" placement="top" trigger="click">
<el-icon style="cursor: pointer;">
<QuestionFilled />
</el-icon>
</el-tooltip>
</span>
</template>
<el-input v-model="port" placeholder="如 8080" clearable />
</el-form-item>
<!-- 核价位置 -->
<el-form-item class="flex-item">
<template #label>
<span class="label-with-icon">
<span>核价位置</span>
<el-tooltip content="默认 1 根据实际情况自行填写" placement="top" trigger="click">
<el-icon style="cursor: pointer;">
<QuestionFilled />
</el-icon>
</el-tooltip>
</span>
</template>
<el-input v-model="verifyIndex" placeholder="请输入核价位置" clearable />
</el-form-item>
<!-- 核价失败默认价格 -->
<el-form-item class="flex-item">
<template #label>
<span class="label-with-icon">
<span>核价失败统一默认价格</span>
<el-tooltip content="例88888 便于再店铺中快速搜索到这些商品 商品修改价格" placement="top" trigger="click">
<el-icon style="cursor: pointer;">
<QuestionFilled />
</el-icon>
</el-tooltip>
</span>
</template>
<el-input v-model="newPrice" placeholder="请输入核价失败时的默认价格" clearable @input="e => onPriceInput(e, 'newPrice')"
@blur="onPriceBlur('newPrice')" />
</el-form-item>
</div>
<div class="config-row">
<!-- 占位降价 -->
<el-form-item class="flex-item">
<template #label>
<span class="label-with-icon">
<span>占位降价</span>
<el-tooltip content="占位降价金额" placement="top" trigger="click">
<el-icon style="cursor: pointer;">
<QuestionFilled />
</el-icon>
</el-tooltip>
</span>
</template>
<el-input v-model="placeholderDownPrice" placeholder="请输入占位降价" clearable
@input="e => onPriceInput(e, 'placeholderDownPrice')"
@blur="enforceMinPlaceholderDownPrice(); onPriceBlur('placeholderDownPrice')" />
</el-form-item>
<!-- 最低运费 -->
<el-form-item class="flex-item">
<template #label>
<span class="label-with-icon">
<span>最低运费</span>
<el-tooltip content="订单最低运费金额" placement="top" trigger="click">
<el-icon style="cursor: pointer;">
<QuestionFilled />
</el-icon>
</el-tooltip>
</span>
</template>
<el-input v-model="minShippingFee" placeholder="请输入最低运费" clearable
@input="e => onPriceInput(e, 'minShippingFee')" @blur="onPriceBlur('minShippingFee')" />
</el-form-item>
<!-- 最低书价 -->
<el-form-item class="flex-item">
<template #label>
<span class="label-with-icon">
<span>最低书价</span>
<el-tooltip content="单本书籍最低售价" placement="top" trigger="click">
<el-icon style="cursor: pointer;">
<QuestionFilled />
</el-icon>
</el-tooltip>
</span>
</template>
<el-input v-model="minPrice" placeholder="请输入最低书价" clearable @input="e => onPriceInput(e, 'minPrice')"
@blur="onPriceBlur('minPrice')" />
</el-form-item>
</div>
<div class="save-bar">
<el-tooltip content="测试核价器服务连接状态" placement="top" trigger="click">
<el-button type="primary" :loading="testLoading" @click="handleTest" size="small">测试连接</el-button>
</el-tooltip>
<el-tooltip content="保存当前核价器配置" placement="top" trigger="click">
<el-button type="success" @click="handleSave">保存配置</el-button>
</el-tooltip>
</div>
<transition name="fade">
<div v-if="result" class="result-box">
<el-alert :title="result.success ? '✅ 连接成功' : '❌ 连接失败'" :type="result.success ? 'success' : 'error'"
:description="result.message" show-icon :closable="false" />
</div>
</transition>
</el-form>
<el-divider />
<!-- 账号管理 -->
<div class="account-section">
<div class="section-title">
<el-icon :size="18">
<User />
</el-icon>
<span>绑定孔夫子旧书网账号</span>
<el-tooltip content="这里是为了波次提交之后店铺同步商品时候有一个相对准确市场价格(如果没有价格我们将走设置的统一默认价格)" placement="top" trigger="click">
<el-icon style="cursor: pointer; margin-left: 8px;">
<QuestionFilled />
</el-icon>
</el-tooltip>
</div>
<!-- 有数据 表格 + 追加账号按钮 -->
<template v-if="savedAccountList.length > 0">
<el-table :data="savedAccountList" border stripe size="small" style="width: 100%">
<el-table-column prop="username" label="用户名" min-width="140" align="center" />
<el-table-column prop="token" label="Token" min-width="260" show-overflow-tooltip align="center">
<template #default="{ row }">
<span class="token-text">{{ row.token }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template #default="{ row }">
<el-tooltip content="删除该账号绑定" placement="top" trigger="click">
<el-button type="danger" link :icon="Delete" @click="deleteSavedAccount(row)">删除</el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<!-- 追加账号按钮 -->
<div class="append-bar">
<el-button v-if="!showAppendForm" type="primary" plain :icon="Plus" @click="showAppendForm = true">
追加账号
</el-button>
</div>
<!-- 追加账号输入表单 -->
<transition name="fade">
<div v-if="showAppendForm" class="account-form">
<div v-for="(item, index) in appendAccounts" :key="index" class="account-row">
<div class="account-label">
追加账号{{ ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十'][index] || (index + 1) }}
</div>
<div class="account-inputs">
<!-- 账号输入框 -->
<el-form-item :label-width="'0'" style="margin-bottom: 0;">
<template #label>
<span class="label-with-icon" style="display: none;"></span>
</template>
<el-input v-model="item.account" placeholder="请输入账号" clearable autocomplete="off" />
</el-form-item>
<!-- 密码输入框 -->
<el-form-item :label-width="'0'" style="margin-bottom: 0;">
<el-input v-model="item.password" type="password" placeholder="请输入密码" clearable show-password
autocomplete="new-password" />
</el-form-item>
<el-tooltip v-if="appendAccounts.length > 1" content="删除此条账号输入" placement="top" trigger="click">
<el-button type="danger" :icon="Delete" circle
@click="removeAppendAccount(index)" />
</el-tooltip>
</div>
</div>
<div class="append-form-actions">
<el-tooltip content="再增加一组账号密码输入" placement="top" trigger="click">
<el-button plain :icon="Plus" @click="addAppendAccount">添加更多</el-button>
</el-tooltip>
<el-tooltip content="绑定当前填写的所有追加账号" placement="top" trigger="click">
<el-button type="primary" :loading="appendLoading" :icon="Link"
@click="handleAppendBind">绑定追加账号</el-button>
</el-tooltip>
<el-button @click="cancelAppend">取消</el-button>
</div>
</div>
</transition>
</template>
<!-- 无数据 输入表单 -->
<template v-else>
<div class="account-form">
<div v-for="(item, index) in accounts" :key="index" class="account-row">
<div class="account-label">账号{{ ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十'][index] }}</div>
<div class="account-inputs">
<el-input v-model="item.account" placeholder="请输入账号" clearable autocomplete="off" />
<el-input v-model="item.password" type="password" placeholder="请输入密码" clearable show-password
autocomplete="new-password" />
<el-tooltip v-if="accounts.length > 1" content="删除此条账号输入" placement="top" trigger="click">
<el-button type="danger" :icon="Delete" circle
@click="removeAccount(index)" />
</el-tooltip>
</div>
</div>
<el-tooltip content="再增加一组账号密码输入" placement="top" trigger="click">
<el-button type="primary" plain :icon="Plus" @click="addAccount" class="add-btn">添加更多账号</el-button>
</el-tooltip>
</div>
</template>
<!-- 绑定账号按钮无已绑定账号时显示 -->
<div v-if="savedAccountList.length === 0" class="bind-bar">
<el-tooltip content="绑定新的核价器账号" placement="top" trigger="click">
<el-button type="primary" :loading="bindLoading" :icon="Link" @click="handleBind">绑定账号</el-button>
</el-tooltip>
</div>
</div>
</el-card>
</div>
</template>
<script lang="ts">
import { ref, onMounted } from 'vue'
import axios from 'axios'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Delete, Plus, Setting, User, Link, VideoPlay } from '@element-plus/icons-vue'
import {
testConnection,
kongfzLogin,
batchAddTokens,
fetchTokenList,
saveNewPrice,
fetchConfig,
} from '@/api/config'
const STORAGE_KEY_FILE_DIR = 'file_dir'
const STORAGE_KEY_IP = 'test_ip'
const STORAGE_KEY_PORT = 'test_port'
const STORAGE_KEY_VERIFY_INDEX = 'verify_index'
const STORAGE_KEY_SAVED_ACCOUNTS = 'saved_accounts'
interface AccountEntry {
id?: number
account: string
token: string
username: string
}
interface FormAccount {
account: string
password: string
token?: string
username?: string
}
/** 从 localStorage 读取已保存的账号列表 */
function loadSavedAccounts(): AccountEntry[] {
try {
const raw = localStorage.getItem(STORAGE_KEY_SAVED_ACCOUNTS)
return raw ? JSON.parse(raw) : []
} catch {
return []
}
}
/** 持久化已保存的账号列表到 localStorage */
function saveSavedAccounts(list: AccountEntry[]): void {
localStorage.setItem(STORAGE_KEY_SAVED_ACCOUNTS, JSON.stringify(list))
}
export default {
name: 'Config',
setup() {
//
const dir = ref(localStorage.getItem(STORAGE_KEY_FILE_DIR) || '')
const ip = ref(localStorage.getItem(STORAGE_KEY_IP) || '127.0.0.1')
const port = ref(localStorage.getItem(STORAGE_KEY_PORT) || '8080')
const verifyIndex = ref(localStorage.getItem(STORAGE_KEY_VERIFY_INDEX) || '3')
const fmt = (v: string | null, d: string) => { const n = parseFloat(v ?? ''); return isNaN(n) ? d : n.toFixed(2) }
const newPrice = ref(fmt(localStorage.getItem('new_price'), '9999.00'))
const placeholderDownPrice = ref(fmt(localStorage.getItem('placeholder_down_price'), '0.01'))
const minShippingFee = ref(fmt(localStorage.getItem('min_shipping_fee'), '3.00'))
const minPrice = ref(fmt(localStorage.getItem('min_price'), '1.00'))
//
const accounts = ref<FormAccount[]>([{ account: '', password: '', token: '', username: '' }])
// Token
const savedAccountList = ref<AccountEntry[]>([])
//
const showAppendForm = ref(false)
const appendAccounts = ref<FormAccount[]>([{ account: '', password: '', token: '', username: '' }])
const appendLoading = ref(false)
/** 添加新的账号输入行 */
const addAccount = () => {
accounts.value.push({ account: '', password: '', token: '', username: '' })
}
/** 删除账号输入行 */
const removeAccount = (index: number) => {
accounts.value.splice(index, 1)
}
/** 添加新的追加账号输入行 */
const addAppendAccount = () => {
appendAccounts.value.push({ account: '', password: '', token: '', username: '' })
}
/** 删除追加账号输入行 */
const removeAppendAccount = (index: number) => {
appendAccounts.value.splice(index, 1)
}
/** 取消追加账号操作,重置表单 */
const cancelAppend = () => {
showAppendForm.value = false
appendAccounts.value = [{ account: '', password: '', token: '', username: '' }]
}
/** 占位降价失焦时强制最低值为 0.01 */
const enforceMinPlaceholderDownPrice = () => {
const val = parseFloat(placeholderDownPrice.value)
if (isNaN(val) || val < 0.01) {
placeholderDownPrice.value = '0.01'
ElMessage.warning('占位降价不能低于 0.01,已自动设为 0.01')
}
}
/** 金额输入过滤:只允许数字+小数点最多2位小数保持光标位置 */
const onPriceInput = (value: string, key: string) => {
const raw = value
const filtered = raw
.replace(/[^\d.]/g, '') //
.replace(/^\./, '0.') // 0
.replace(/\.{2,}/g, '.') //
.replace(/^(\d+\.?\d{0,2}).*$/, '$1') // 2
const map: Record<string, any> = { newPrice, placeholderDownPrice, minShippingFee, minPrice }
map[key].value = filtered
}
/** 失焦时自动补全 .00:纯整数 → 末尾补 .00 */
const onPriceBlur = (key: string) => {
const map: Record<string, any> = { newPrice, placeholderDownPrice, minShippingFee, minPrice }
const val = map[key].value as string
// .00
if (val && /^\d+$/.test(val)) {
map[key].value = val + '.00'
} else if (val && /^\d+\.$/.test(val)) {
// "12." "12.00"
map[key].value = val + '00'
} else if (val && /^\d+\.\d$/.test(val)) {
// "12.3" "12.30"
map[key].value = val + '0'
}
}
const testLoading = ref(false)
const bindLoading = ref(false)
const result = ref<{ success: boolean; message: string } | null>(null)
/** 通过自定义协议启动本地 kfz-goods-pricing.exe */
const handleOpenExe = () => {
if (!dir.value) {
ElMessage.warning('请先设置程序所在位置')
return
}
try {
// kfzgs://launcher.exe dir exe
window.location.href = `kfzgs://launch?dir=${encodeURIComponent(dir.value)}`
} catch (err: any) {
ElMessage.error(`启动失败: ${err.message}`)
}
}
/** 将单个账号写入已保存列表 */
const addToSavedList = (entry: AccountEntry) => {
const list = loadSavedAccounts()
const idx = list.findIndex(a => a.account === entry.account)
if (idx >= 0) {
list[idx] = entry
} else {
list.push(entry)
}
saveSavedAccounts(list)
savedAccountList.value = list
}
/** 删除已保存的账号 */
const deleteSavedAccount = async (entry: AccountEntry) => {
try {
await ElMessageBox.confirm(`确定删除账号「${entry.account}」的 Token 记录吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
//
await axios.get(`http://${ip.value}:${port.value}/api/token/delete`, {
params: { id: entry.id }
})
// username localStorage key
localStorage.removeItem(entry.username)
const list = loadSavedAccounts().filter(a => a.account !== entry.account)
saveSavedAccounts(list)
savedAccountList.value = list
ElMessage.success('已删除')
} catch {
//
}
}
/** 加载已保存的账号列表 */
const loadSavedAccountsList = () => {
savedAccountList.value = loadSavedAccounts()
}
/** 测试核价器连接 */
const handleTest = async () => {
if (!ip.value) {
ElMessage.warning('请输入 IP 地址')
return
}
if (!port.value) {
ElMessage.warning('请输入端口号')
return
}
testLoading.value = true
result.value = null
try {
const testRes = await testConnection(ip.value, port.value)
if (testRes.code === 200) {
result.value = {
success: true,
message: testRes.message
}
} else {
result.value = {
success: false,
message: testRes.message
}
}
console.log('测试结果:', testRes)
} catch (error: any) {
let message = '未知错误'
if (error.code === 'ECONNABORTED') {
message = '连接超时,请检查地址是否正确或联系服务供应方'
} else if (error.message) {
message = error.message
}
result.value = {
success: false,
message
}
console.log('测试失败:', error)
} finally {
testLoading.value = false
}
}
/** 绑定账号:登录孔网 + 批量提交 Token 到核价器 + 刷新列表 */
const handleBind = async () => {
if (!ip.value) {
ElMessage.warning('请先填写核价器 IP 地址')
return
}
if (!port.value) {
ElMessage.warning('请先填写核价器端口号')
return
}
bindLoading.value = true
try {
//
const successfulAccounts: { username: string; token: string }[] = []
for (let i = 0; i < accounts.value.length; i++) {
const item = accounts.value[i]
if (item.account && item.password) {
try {
const loginRes = await kongfzLogin(item.account, item.password, ip.value, port.value)
console.log('核价器登录响应:', loginRes)
if (loginRes?.code === 200 && loginRes?.data) {
const { token, nickname: username } = loginRes.data
accounts.value[i].token = token
accounts.value[i].username = username
localStorage.setItem(username, token)
addToSavedList({
account: item.account,
username,
token
})
successfulAccounts.push({ username, token })
console.log(`账号 ${item.account} 登录成功username: ${username}`)
} else {
console.log(`账号 ${item.account} 登录失败:`, loginRes?.message)
}
} catch (loginError: any) {
console.log(`账号 ${item.account} 登录失败:`, loginError.message)
}
}
}
// Token
if (successfulAccounts.length > 0) {
try {
const addRes = await batchAddTokens(successfulAccounts, ip.value, port.value)
console.log('token/add 响应:', addRes)
} catch (addError: any) {
// console.error('token/add :', addError.message)
}
}
//
try {
const res = await fetchTokenList(ip.value, port.value)
const tokenList: { Username: string; Token: string; ID: number; IsEnable: boolean }[] = res?.data
if (Array.isArray(tokenList)) {
const entries: AccountEntry[] = tokenList
.filter(t => t.IsEnable)
.map(t => ({
id: t.ID,
account: t.Username,
username: t.Username,
token: t.Token
}))
saveSavedAccounts(entries)
savedAccountList.value = entries
entries.forEach(e => localStorage.setItem(e.username, e.token))
}
} catch (fetchErr) {
// console.error(' Token :', fetchErr)
}
if (successfulAccounts.length > 0) {
ElMessage.success(`成功绑定 ${successfulAccounts.length} 个账号`)
} else {
ElMessage.info('没有成功绑定的账号,请检查账号密码是否正确')
}
} finally {
bindLoading.value = false
}
}
/** 追加账号绑定:与 handleBind 逻辑一致,但操作 appendAccounts */
const handleAppendBind = async () => {
if (!ip.value) {
ElMessage.warning('请先填写核价器 IP 地址')
return
}
if (!port.value) {
ElMessage.warning('请先填写核价器端口号')
return
}
appendLoading.value = true
try {
const successfulAccounts: { username: string; token: string }[] = []
for (let i = 0; i < appendAccounts.value.length; i++) {
const item = appendAccounts.value[i]
if (item.account && item.password) {
try {
const loginRes = await kongfzLogin(item.account, item.password, ip.value, port.value)
if (loginRes?.code === 200 && loginRes?.data) {
const { token, nickname: username } = loginRes.data
appendAccounts.value[i].token = token
appendAccounts.value[i].username = username
localStorage.setItem(username, token)
addToSavedList({
account: item.account,
username,
token
})
successfulAccounts.push({ username, token })
}
} catch (loginError: any) {
console.log(`追加账号 ${item.account} 登录失败:`, loginError.message)
}
}
}
if (successfulAccounts.length > 0) {
try {
await batchAddTokens(successfulAccounts, ip.value, port.value)
} catch (addError: any) {
// console.error(' token/add :', addError.message)
}
}
//
try {
const res = await fetchTokenList(ip.value, port.value)
const tokenList: { Username: string; Token: string; ID: number; IsEnable: boolean }[] = res?.data
if (Array.isArray(tokenList)) {
const entries: AccountEntry[] = tokenList
.filter(t => t.IsEnable)
.map(t => ({
id: t.ID,
account: t.Username,
username: t.Username,
token: t.Token
}))
saveSavedAccounts(entries)
savedAccountList.value = entries
entries.forEach(e => localStorage.setItem(e.username, e.token))
}
} catch (fetchErr) {
// console.error(' Token :', fetchErr)
}
if (successfulAccounts.length > 0) {
ElMessage.success(`成功追加绑定 ${successfulAccounts.length} 个账号`)
} else {
ElMessage.info('没有成功绑定的账号,请检查账号密码是否正确')
}
//
showAppendForm.value = false
appendAccounts.value = [{ account: '', password: '', token: '', username: '' }]
} finally {
appendLoading.value = false
}
}
/** 保存配置 */
const handleSave = async () => {
if (!ip.value) {
ElMessage.warning('请输入 IP 地址')
return
}
if (!port.value) {
ElMessage.warning('请输入端口号')
return
}
localStorage.setItem(STORAGE_KEY_FILE_DIR, dir.value)
localStorage.setItem(STORAGE_KEY_IP, ip.value)
localStorage.setItem(STORAGE_KEY_PORT, port.value)
localStorage.setItem(STORAGE_KEY_VERIFY_INDEX, verifyIndex.value)
localStorage.setItem('new_price', newPrice.value)
localStorage.setItem('placeholder_down_price', placeholderDownPrice.value)
localStorage.setItem('min_shipping_fee', minShippingFee.value)
localStorage.setItem('min_price', minPrice.value)
console.log('核价配置已保存:', { ip: ip.value, port: port.value, verifyIndex: verifyIndex.value })
const newpriceNum = parseFloat(newPrice.value)
const placeholderDownPriceNum = parseFloat(placeholderDownPrice.value)
const minShippingFeeNum = parseFloat(minShippingFee.value)
const minPriceNum = parseFloat(minPrice.value)
if (!isNaN(newpriceNum) && newpriceNum >= 0) {
const sendNewPrice = newpriceNum
const sendPlaceholderDownPrice = !isNaN(placeholderDownPriceNum) && placeholderDownPriceNum >= 0
? placeholderDownPriceNum
: 0.01
const sendMinShippingFee = !isNaN(minShippingFeeNum) && minShippingFeeNum >= 0
? minShippingFeeNum
: 3.00
const sendMinPrice = !isNaN(minPriceNum) && minPriceNum >= 0
? minPriceNum
: 1.00
try {
await saveNewPrice(ip.value, port.value, sendNewPrice, sendPlaceholderDownPrice, sendMinShippingFee, sendMinPrice,verifyIndex.value)
console.log('核价价格相关配置已保存')
} catch (err: any) {
console.log('核价价格相关配置保存失败:', err.message)
}
}
ElMessage.success(`配置已保存IP=${ip.value}, PORT=${port.value}, 核价位置=${verifyIndex.value}`)
}
/** 从服务器获取已保存的 Token 列表并同步到本地 */
const fetchSavedTokens = async (): Promise<void> => {
try {
const res = await fetchTokenList(ip.value, port.value)
const tokenList: { Username: string; Token: string; ID: number; IsEnable: boolean }[] = res?.data
if (Array.isArray(tokenList) && tokenList.length > 0) {
const entries: AccountEntry[] = tokenList
.filter(t => t.IsEnable)
.map(t => ({
id: t.ID,
account: t.Username,
username: t.Username,
token: t.Token
}))
saveSavedAccounts(entries)
savedAccountList.value = entries
// token localStorage key
entries.forEach(e => localStorage.setItem(e.username, e.token))
}
} catch (err) {
console.log('获取已保存 Token 列表失败(首次使用可忽略):', err)
}
}
/** 从核价器拉取价格配置,补全 localStorage 中缺失的字段 */
const loadPriceConfig = async (): Promise<void> => {
try {
const res = await fetchConfig(ip.value, port.value)
const data = res?.data
if (data) {
//
const fieldMap: { respKey: string; storeKey: string }[] = [
{ respKey: 'QueryIndex', storeKey: 'verify_index' },
{ respKey: 'NewPrice', storeKey: 'new_price' },
{ respKey: 'PlaceholderDownPrice', storeKey: 'placeholder_down_price' },
{ respKey: 'MinShippingFee', storeKey: 'min_shipping_fee' },
{ respKey: 'MinPrice', storeKey: 'min_price' },
]
console.log('从服务器获取的核价配置:', data)
for (const f of fieldMap) {
const val = (data as any)[f.respKey]
if (val !== undefined && val !== null) {
localStorage.setItem(f.storeKey, String(val))
console.log(`loadPriceConfig: 同步 ${f.storeKey} = ${val}`)
}
}
// ref
verifyIndex.value = localStorage.getItem(STORAGE_KEY_VERIFY_INDEX) || verifyIndex.value
newPrice.value = fmt(localStorage.getItem('new_price'), '0.00')
placeholderDownPrice.value = fmt(localStorage.getItem('placeholder_down_price'), '0.01')
minShippingFee.value = fmt(localStorage.getItem('min_shipping_fee'), '3.00')
minPrice.value = fmt(localStorage.getItem('min_price'), '1.00')
// Port
if (!port.value && (data as any).Port) {
port.value = String((data as any).Port)
}
}
} catch (err) {
console.log('获取核价器配置失败(首次使用可忽略):', err)
}
}
// + +
onMounted(() => {
loadSavedAccountsList()
fetchSavedTokens()
loadPriceConfig()
})
return {
dir,
ip,
port,
verifyIndex,
newPrice,
placeholderDownPrice,
minShippingFee,
minPrice,
accounts,
addAccount,
removeAccount,
showAppendForm,
appendAccounts,
appendLoading,
addAppendAccount,
removeAppendAccount,
cancelAppend,
enforceMinPlaceholderDownPrice,
onPriceInput,
onPriceBlur,
handleAppendBind,
testLoading,
bindLoading,
result,
savedAccountList,
deleteSavedAccount,
handleTest,
handleBind,
handleSave,
handleOpenExe,
Delete,
Plus,
Setting,
User,
Link,
VideoPlay
}
}
}
</script>
<style scoped lang="scss">
.config-page {
padding: 20px;
max-width: 860px;
margin: 0 auto;
}
.config-card {
border-radius: 12px;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.header-left {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 600;
}
.config-row {
display: flex;
gap: 16px;
.flex-item {
flex: 1;
min-width: 0;
}
}
.save-bar {
display: flex;
}
.result-box {
margin-top: 16px;
}
/* 账号区域 */
.account-section {
.section-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 16px;
}
.count-tag {
margin-left: 0;
}
}
.account-form {
.account-row {
margin-bottom: 16px;
.account-label {
font-size: 14px;
color: #606266;
margin-bottom: 8px;
font-weight: 500;
}
.account-inputs {
display: flex;
gap: 8px;
align-items: center;
}
}
}
.add-btn {
margin-top: 4px;
}
.bind-bar {
margin-top: 16px;
}
.append-bar {
margin-top: 16px;
}
.append-form-actions {
display: flex;
gap: 8px;
margin-top: 8px;
}
.token-text {
font-family: 'Courier New', monospace;
font-size: 12px;
}
/* 过渡动画 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
/* 响应式 */
@media (max-width: 640px) {
.config-row {
flex-direction: column;
gap: 0;
}
.account-inputs {
flex-wrap: wrap;
}
}
.label-with-icon {
display: inline-flex;
align-items: center;
gap: 4px;
}
</style>

View File

@ -1,369 +0,0 @@
<template>
<el-popover
placement="right-start"
:width="360"
trigger="hover"
:open-delay="500"
:close-delay="100"
:disabled="!isbn"
@show="handleShow"
@hide="handleHide"
>
<template #reference>
<span class="isbn-popover-trigger" :class="{ 'is-loading': loading }">
<slot />
</span>
</template>
<!-- 加载中 -->
<div v-if="loading" class="popover-loading">
<el-icon class="is-loading" :size="24"><Loading /></el-icon>
<span>正在查询书品信息...</span>
</div>
<!-- 查询失败 -->
<div v-else-if="error" class="popover-error">
<el-icon :size="24" color="#e6a23c"><WarningFilled /></el-icon>
<span>{{ error }}</span>
</div>
<!-- 查询成功 展示书品信息 -->
<div v-else-if="bookData" class="popover-content">
<div class="popover-header">
<span class="popover-isbn">{{ isbn }}</span>
<span v-if="bookData.isSuit" class="suit-badge">套装书</span>
</div>
<div class="popover-body">
<!-- 左侧封面图片 -->
<div class="popover-cover">
<img
v-if="bookData.book_pic?.pddPath"
:src="bookData.book_pic.pddPath"
alt="封面"
class="cover-image"
/>
<div v-else class="cover-placeholder">
<el-icon :size="32"><Picture /></el-icon>
</div>
</div>
<!-- 右侧书籍详情 -->
<div class="popover-info">
<div class="info-row">
<span class="info-label">书名</span>
<span class="info-value info-value-name">{{ bookData.bookName || '未知' }}</span>
</div>
<div class="info-row">
<span class="info-label">作者</span>
<span class="info-value">{{ bookData.author || '未知' }}</span>
</div>
<div class="info-row">
<span class="info-label">出版社</span>
<span class="info-value">{{ bookData.publisher || '未知' }}</span>
</div>
<div class="info-row">
<span class="info-label">出版时间</span>
<span class="info-value">{{ bookData.publishDate || '未知' }}</span>
</div>
<div class="info-row">
<span class="info-label">装帧</span>
<span class="info-value">{{ bookData.binding || '未知' }}</span>
</div>
<div class="info-row">
<span class="info-label">定价</span>
<span class="info-value info-value-price">¥{{ formatPrice(bookData.price) }}</span>
</div>
<div class="info-row">
<span class="info-label">页数</span>
<span class="info-value">{{ bookData.pageCount ?? '未知' }}</span>
</div>
<div class="info-row">
<span class="info-label">字数</span>
<span class="info-value">{{ bookData.wordCount ?? '未知' }}</span>
</div>
</div>
</div>
</div>
<!-- ISBN 为空 -->
<div v-else class="popover-empty">
<span>暂无ISBN</span>
</div>
</el-popover>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { Loading, WarningFilled, Picture } from '@element-plus/icons-vue'
import request from '@/utils/request'
interface BookPic {
localPath?: string
pddPath?: string
}
interface BookInfoResult {
bookName: string
author: string
publisher: string
publishDate: string
binding: string
price: number
pageCount: number
wordCount: number
book_pic?: BookPic
isSuit: boolean
}
const props = defineProps<{
/** ISBN 编号 */
isbn?: string
}>()
const loading = ref(false)
const error = ref<string | null>(null)
const bookData = ref<BookInfoResult | null>(null)
// ISBN
const cache = new Map<string, BookInfoResult>()
/** 格式化出版时间:处理年月日格式 */
function formatPublishDate(value: string | number | undefined | null): string {
if (value == null || value === '') return ''
const str = String(value)
// yyyy-mm-dd
if (/^\d{4}-\d{2}-\d{2}$/.test(str)) return str
// yyyyMMdd 8
if (/^\d{8}$/.test(str)) {
return `${str.slice(0, 4)}-${str.slice(4, 6)}-${str.slice(6, 8)}`
}
// yyyy-mm
if (/^\d{4}-\d{2}$/.test(str)) return `${str}-01`
//
const num = Number(value)
if (!isNaN(num) && num > 10000) {
const d = new Date(num * 1000)
if (!isNaN(d.getTime())) {
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
}
}
return str
}
/** 格式化价格:分 → 元 */
function formatPrice(priceInCents: number): string {
if (priceInCents == null) return '0.00'
return (priceInCents / 100).toFixed(2)
}
async function fetchBookInfo(isbn: string) {
//
if (cache.has(isbn)) {
bookData.value = cache.get(isbn)!
return
}
loading.value = true
error.value = null
bookData.value = null
try {
const payload = await request.get('/getBookInfo', {
params: { isbn }
})
const data = payload?.data
if (!data) {
error.value = '数据库中暂无该书数据'
return
}
const result: BookInfoResult = {
bookName: data.book_name || '',
author: data.author || '',
publisher: data.publisher || '',
publishDate: formatPublishDate(data.publication_time),
binding: data.binding_layout || '',
price: typeof data.fix_price === 'number' ? data.fix_price : 0,
pageCount: Number(data.page_count) || 0,
wordCount: Number(data.word_count) || 0,
book_pic: data.book_pic || undefined,
isSuit: data.is_suit === 1
}
//
cache.set(isbn, result)
bookData.value = result
} catch (err) {
console.warn('[goodsPop] 书籍信息查询失败:', err instanceof Error ? err.message : String(err))
error.value = '查询失败,请稍后重试'
} finally {
loading.value = false
}
}
function handleShow() {
if (!props.isbn) return
fetchBookInfo(props.isbn)
}
function handleHide() {
// 便
}
</script>
<style scoped>
.isbn-popover-trigger {
cursor: pointer;
border-bottom: 1px dashed #409eff;
transition: all 0.2s;
}
.isbn-popover-trigger:hover {
color: #409eff;
}
.isbn-popover-trigger.is-loading {
opacity: 0.7;
}
/* 加载状态 */
.popover-loading {
display: flex;
align-items: center;
gap: 8px;
padding: 20px;
justify-content: center;
color: #909399;
font-size: 13px;
}
.popover-loading .is-loading {
animation: rotating 1.5s linear infinite;
}
@keyframes rotating {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* 错误状态 */
.popover-error {
display: flex;
align-items: center;
gap: 8px;
padding: 20px;
justify-content: center;
color: #e6a23c;
font-size: 13px;
}
/* 空状态 */
.popover-empty {
padding: 20px;
text-align: center;
color: #c0c4cc;
font-size: 13px;
}
/* 内容主体 */
.popover-content {
padding: 4px;
}
.popover-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #ebeef5;
}
.popover-isbn {
font-size: 13px;
font-weight: 600;
color: #303133;
font-family: 'Courier New', monospace;
}
.suit-badge {
display: inline-block;
padding: 1px 8px;
font-size: 11px;
color: #e6a23c;
background: #fdf6ec;
border: 1px solid #f5dab1;
border-radius: 4px;
line-height: 1.6;
}
.popover-body {
display: flex;
gap: 14px;
}
/* 封面 */
.popover-cover {
flex-shrink: 0;
width: 90px;
height: 120px;
border-radius: 4px;
overflow: hidden;
border: 1px solid #ebeef5;
}
.cover-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.cover-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: #f5f7fa;
color: #c0c4cc;
}
/* 详情 */
.popover-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.info-row {
display: flex;
font-size: 12px;
line-height: 1.6;
}
.info-label {
flex-shrink: 0;
color: #909399;
width: 56px;
text-align: right;
margin-right: 4px;
}
.info-value {
flex: 1;
color: #303133;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.info-value-name {
font-weight: 600;
color: #409eff;
}
.info-value-price {
color: #f56c6c;
font-weight: 600;
}
</style>

View File

@ -1,93 +0,0 @@
import request from '@/utils/request'
/** 发货单 API 基础路径 */
const API_BASE = '/shipping-order'
/**
* 标准化列表接口返回的数据格式
* @param {Object} payload - 接口返回的原始响应对象
* @returns {{ list: Array, total: number }} 标准化后的列表数据
*/
const normalizeListResponse = (payload) => {
const data = payload?.data
if (!data) return { list: [], total: 0 }
if (Array.isArray(data)) return { list: data, total: data.length }
return {
list: Array.isArray(data.list) ? data.list : [],
total: typeof data.total === 'number' ? data.total : 0
}
}
/**
* 获取发货单列表支持分页和筛选
* @param {Object} params - 请求参数
* @param {string} [params.check_no] - 搜索关键字发货单号
* @param {number} [params.status] - 状态筛选
* @param {number} [params.customer_id] - 客户ID筛选
* @param {number} [params.warehouse_id] - 仓库ID筛选
* @param {number} [params.sales_order_id] - 销售订单ID筛选
* @param {number} [params.wave_task_id] - 波次任务ID筛选
* @param {number} [params.page] - 当前页码
* @param {number} [params.pageSize] - 每页条数
* @returns {Promise<{ list: Array, total: number }>} 标准化后的发货单列表
*/
export const fetchShippingOrderList = async ({ check_no, status, customer_id, warehouse_id, sales_order_id, wave_task_id, page, pageSize }) => {
const params = {
check_no: check_no || undefined,
status,
customer_id,
warehouse_id,
sales_order_id,
wave_task_id,
page,
page_size: pageSize
}
const response = await request.get(`${API_BASE}/list`, { params })
return normalizeListResponse(response)
}
/**
* 获取单个发货单详情含明细行
* @param {string|number} id - 发货单ID
* @returns {Promise<Object|null>} 发货单详情对象
*/
export const fetchShippingOrderDetail = async (id) => {
const response = await request.get(`${API_BASE}/detail`, { params: { id } })
return response?.data || null
}
/**
* 生成发货单form-data 格式
* @param {Object} params
* @param {number} params.total - 选中的出库单数量
* @param {number[]} params.outbound_order_ids - 选中的出库单 ID 数组
* @returns {Promise}
*/
export const createShippingOrder = async ({ total, outbound_order_ids }) => {
// 手动构建 FormData按照 outbound_order_ids[0]={id} 的格式
const formData = new FormData()
formData.append('total', String(total))
outbound_order_ids.forEach((id, index) => {
formData.append(`order_ids[${index}]`, String(id))
})
return request.post(`${API_BASE}/create`, formData)
}
/**
* 更新发货单物流信息form-data 格式
* @param {Object} params
* @param {number} params.shipping_order_id - 发货单ID
* @param {number} params.sales_order_item_id - 销售订单明细ID
* @param {string} params.logistics_company - 物流公司
* @param {string} params.logistics_no - 物流单号
* @returns {Promise}
*/
export const updateShippingOrderLogistics = async ({ shipping_order_id, sales_order_item_id, logistics_company, logistics_no }) => {
const formData = new FormData()
formData.append('shipping_order_id', String(shipping_order_id))
formData.append('total', '1')
formData.append('sales_order_item_id', String(sales_order_item_id))
formData.append('logistics_company', logistics_company)
formData.append('logistics_no', logistics_no)
return request.post(`${API_BASE}/update`, formData)
}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
.layout[data-v-2b474e07]{height:100%}.aside[data-v-2b474e07]{background-color:#304156;transition:width .3s;overflow:hidden;display:flex;flex-direction:column}.logo[data-v-2b474e07]{height:60px;line-height:60px;text-align:center;color:#fff;font-size:18px;font-weight:700;background-color:#1f2d3d;white-space:nowrap;overflow:hidden;flex-shrink:0}.logo-collapse[data-v-2b474e07]{font-size:20px}.menu[data-v-2b474e07]{border-right:none;flex:1;overflow-y:auto;overflow-x:hidden}.menu[data-v-2b474e07]::-webkit-scrollbar{width:6px}.menu[data-v-2b474e07]::-webkit-scrollbar-track{background:#304156}.menu[data-v-2b474e07]::-webkit-scrollbar-thumb{background:#4a5568;border-radius:3px}.menu[data-v-2b474e07]::-webkit-scrollbar-thumb:hover{background:#718096}.header[data-v-2b474e07]{background-color:#fff;border-bottom:1px solid #e6e9f0;display:flex;align-items:center;justify-content:space-between;padding:0 20px}.header-left[data-v-2b474e07]{display:flex;align-items:center}.collapse-icon[data-v-2b474e07]{font-size:20px;cursor:pointer;color:#606266}.header-right[data-v-2b474e07]{display:flex;align-items:center;gap:20px}.user-info[data-v-2b474e07]{display:flex;align-items:center;gap:5px;cursor:pointer;color:#606266;font-size:14px;outline:none}.user-info[data-v-2b474e07]:focus{outline:none}.points-badge[data-v-2b474e07]{background-color:#f56c6c;color:#fff;padding:4px 8px;border-radius:4px;font-size:12px}.main[data-v-2b474e07]{background-color:#f0f2f5;padding:10px}

View File

@ -1 +0,0 @@
.car-manager[data-v-49a939b7]{padding:0}.card-header[data-v-49a939b7]{font-size:16px;font-weight:600;color:#303133}.section-card[data-v-49a939b7]{margin-bottom:16px}.filter-bar[data-v-49a939b7]{display:flex;flex-wrap:wrap;gap:12px;margin-bottom:20px;align-items:center}.pagination-wrapper[data-v-49a939b7]{margin-top:20px;display:flex;justify-content:flex-end}[data-v-49a939b7] .el-table{border-radius:8px;overflow:hidden}[data-v-49a939b7] .el-dialog__body{padding-top:16px}.dialog-footer[data-v-49a939b7]{display:flex;justify-content:flex-end;gap:12px}.expand-content[data-v-49a939b7]{padding:16px 24px}.expand-header[data-v-49a939b7]{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.expand-title[data-v-49a939b7]{font-size:14px;font-weight:600;color:#303133}.secret-text[data-v-49a939b7]{color:#909399;font-family:monospace;letter-spacing:2px}.label-with-icon[data-v-49a939b7]{display:inline-flex;align-items:center;gap:4px}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
.dashboard[data-v-bf7f84f8]{padding:20px;background:#f5f7fa;min-height:calc(100vh - 120px)}.stats-row[data-v-bf7f84f8]{margin-bottom:20px}.stat-card[data-v-bf7f84f8]{border-radius:12px;transition:all .3s ease;border:none}.stat-card[data-v-bf7f84f8]:hover{transform:translateY(-4px);box-shadow:0 12px 24px #0000001a}.stat-card[data-v-bf7f84f8] .el-card__body{padding:20px}.stat-content[data-v-bf7f84f8]{display:flex;align-items:center}.stat-icon[data-v-bf7f84f8]{width:64px;height:64px;border-radius:16px;display:flex;align-items:center;justify-content:center;color:#fff;margin-right:20px;flex-shrink:0}.stat-info[data-v-bf7f84f8]{flex:1}.stat-title[data-v-bf7f84f8]{font-size:14px;color:#909399;margin-bottom:8px}.stat-value[data-v-bf7f84f8]{font-size:32px;font-weight:700;color:#303133;line-height:1.2;margin-bottom:4px}.stat-desc[data-v-bf7f84f8]{font-size:12px;color:#909399}.mt-20[data-v-bf7f84f8]{margin-top:20px}.table-card[data-v-bf7f84f8]{border-radius:12px;border:none}.table-card[data-v-bf7f84f8] .el-card__header{padding:16px 20px;border-bottom:1px solid #ebeef5}.table-card[data-v-bf7f84f8] .el-card__body{padding:0}.card-header[data-v-bf7f84f8]{display:flex;justify-content:space-between;align-items:center}.card-title[data-v-bf7f84f8]{font-size:16px;font-weight:600;color:#303133}.product-name[data-v-bf7f84f8]{color:#606266;font-weight:500}.price[data-v-bf7f84f8]{color:#f56c6c;font-weight:600}.total-count[data-v-bf7f84f8]{color:#409eff;font-weight:600;font-size:15px}[data-v-bf7f84f8] .el-table .el-table__header th{background:#fafafa;color:#606266;font-weight:600}[data-v-bf7f84f8] .el-table .el-table__body td{padding:12px 0}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
.employee-add[data-v-420844b2]{padding:0}.card-header[data-v-420844b2]{display:flex;justify-content:space-between;align-items:center;font-size:16px;font-weight:600;color:#303133}.steps[data-v-420844b2]{margin:20px 0 40px}.step-content[data-v-420844b2]{min-height:300px;padding:20px 0}.add-form[data-v-420844b2]{width:500px;margin:0 auto}.preview-card[data-v-420844b2]{width:500px;margin:30px auto 0;background-color:#f8f9fa}.preview-content[data-v-420844b2]{padding:10px}.preview-item[data-v-420844b2]{margin-bottom:10px;display:flex;align-items:center}.preview-item .label[data-v-420844b2]{width:80px;color:#666;font-size:13px}.preview-item .value[data-v-420844b2]{color:#333;font-size:13px}.confirm-info[data-v-420844b2]{width:500px;margin:0 auto}.step-actions[data-v-420844b2]{margin-top:30px;text-align:center}.result[data-v-420844b2]{display:flex;justify-content:center}.result-info[data-v-420844b2]{margin:20px 0;text-align:left}.result-actions[data-v-420844b2]{margin-top:20px;display:flex;gap:10px;justify-content:center}[data-v-420844b2] .el-descriptions__label{width:120px}.title-center[data-v-420844b2]{text-align:center;font-weight:700;font-size:16px;margin-bottom:10px}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
.employee-list[data-v-2d3c9b96]{padding:0}.card-header[data-v-2d3c9b96]{display:flex;justify-content:space-between;align-items:center;font-size:16px;font-weight:600;color:#303133}.search-form[data-v-2d3c9b96]{margin-bottom:20px;padding:20px;background-color:#f8f9fa;border-radius:4px}.stat-cards[data-v-2d3c9b96]{margin-bottom:20px}.stat-card[data-v-2d3c9b96]{text-align:center}.stat-item[data-v-2d3c9b96]{padding:10px}.stat-label[data-v-2d3c9b96]{font-size:14px;color:#909399;margin-bottom:8px}.stat-value[data-v-2d3c9b96]{font-size:24px;font-weight:700;color:#303133}.stat-value.success[data-v-2d3c9b96]{color:#67c23a}.stat-value.warning[data-v-2d3c9b96]{color:#e6a23c}.stat-value.info[data-v-2d3c9b96]{color:#409eff}.pagination[data-v-2d3c9b96]{margin-top:20px;display:flex;justify-content:flex-end}.points-warning[data-v-2d3c9b96]{color:#f56c6c;font-weight:700}.deduct-input[data-v-2d3c9b96] .el-input-number__decrease:hover,.deduct-input[data-v-2d3c9b96] .el-input-number__increase:hover{color:#fff;background-color:#f56c6c}.deduct-label[data-v-2d3c9b96] .el-form-item__label{color:#f56c6c;font-weight:700}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
.location-manager[data-v-633d9fa4]{padding:20px;background-color:#f5f7fa;min-height:100vh}.filter-bar[data-v-633d9fa4]{display:flex;flex-wrap:wrap;gap:12px;margin-bottom:20px;align-items:center;background:white;padding:16px 20px;border-radius:8px;box-shadow:0 1px 4px #0000000d}.pagination-wrapper[data-v-633d9fa4]{margin-top:20px;display:flex;justify-content:flex-end;background:white;padding:14px 20px;border-radius:8px}[data-v-633d9fa4] .el-table{border-radius:8px;overflow:hidden;box-shadow:0 1px 4px #0000000d}[data-v-633d9fa4] .el-dialog__body{padding-top:16px}.dialog-footer[data-v-633d9fa4]{display:flex;justify-content:flex-end;gap:12px}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
.filter-bar[data-v-68216036]{display:flex;flex-wrap:wrap;gap:12px;margin-bottom:20px;align-items:center;background:white;padding:16px 20px;border-radius:8px;box-shadow:0 1px 4px #0000000d}.pagination-wrapper[data-v-68216036]{margin-top:20px;display:flex;justify-content:flex-end;background:white;padding:14px 20px;border-radius:8px}.location-empty[data-v-68216036]{padding:40px 0;text-align:center;color:#999;background:#fafafa;border-radius:8px}[data-v-68216036] .el-table{border-radius:8px;overflow:hidden;box-shadow:0 1px 4px #0000000d}[data-v-68216036] .el-dialog__body{padding-top:16px}.dialog-footer[data-v-68216036]{display:flex;justify-content:flex-end;gap:12px}.location-dialog[data-v-68216036] .el-dialog__header{background:linear-gradient(90deg,#4b8afc,#6db6ff);border-bottom:none;color:#fff;border-radius:12px 12px 0 0}.location-dialog[data-v-68216036] .el-dialog__title{color:#fff;font-size:18px;font-weight:600}.location-dialog[data-v-68216036] .el-dialog__body{padding:24px 32px;background:#f6fbff}.location-dialog[data-v-68216036] .el-form{background:#ffffff;border-radius:16px;padding:16px;box-shadow:inset 0 0 0 1px #3454d114}.location-dialog[data-v-68216036] .el-form-item{margin-bottom:20px}.location-dialog[data-v-68216036] .el-form-item__label{color:#4b5563;font-weight:520}.location-dialog[data-v-68216036] .el-input,.location-dialog[data-v-68216036] .el-input-number,.location-dialog[data-v-68216036] .el-radio-group,.location-dialog[data-v-68216036] .el-switch{border-radius:10px}.location-dialog[data-v-68216036] .el-radio-group{gap:12px;display:flex;flex-wrap:wrap}.location-dialog[data-v-68216036] .el-dialog__footer{padding:18px 32px 24px;background:#ffffff}.batch-code-range-header[data-v-68216036]{display:flex;align-items:center;flex-wrap:wrap;gap:10px;margin-bottom:10px;border-radius:8px;color:#000000a6;font-size:12px;font-weight:600}.range-btn[data-v-68216036]{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;padding:0;border-radius:50%;border:1px solid rgba(16,24,40,.08);background:#ffffff;color:#333;transition:transform .12s ease,box-shadow .12s ease,background .12s ease;box-shadow:0 1px 2px #10182814}.range-btn[data-v-68216036]:hover{transform:translateY(-1px);box-shadow:0 4px 8px #1018281f}.range-btn-add[data-v-68216036]{border-color:#1677ff29;color:#176cff}.range-btn-add[data-v-68216036]:hover{background:#eff5ff}.range-btn-remove[data-v-68216036]{border-color:#ff4d4f29;color:#ff4d4f}.range-btn-remove[data-v-68216036]:hover{background:#fff2f2}.detail-loading[data-v-68216036]{display:flex;align-items:center;justify-content:center;gap:8px;padding:40px 0;color:#409eff}.detail-content[data-v-68216036]{padding:0 10px}.detail-section[data-v-68216036]{margin-bottom:24px}.detail-row[data-v-68216036]{display:flex;padding:8px 0;border-bottom:1px solid #f0f0f0}.detail-row[data-v-68216036]:last-child{border-bottom:none}.detail-label[data-v-68216036]{color:#909399;min-width:80px}.detail-code[data-v-68216036]{font-weight:600;color:#409eff}.status-enabled[data-v-68216036]{color:#67c23a}.status-disabled[data-v-68216036]{color:#f56c6c}.barcode-section[data-v-68216036]{margin-top:20px;padding-top:20px;border-top:1px solid #e4e7ed}.barcode-title[data-v-68216036]{font-size:14px;font-weight:600;color:#303133;margin-bottom:16px;text-align:center}.barcode-loading[data-v-68216036]{display:flex;align-items:center;justify-content:center;gap:8px;padding:30px 0;color:#409eff}.barcode-image-wrapper[data-v-68216036]{display:flex;flex-direction:column;align-items:center;gap:8px}.barcode-image[data-v-68216036]{max-width:100%;height:auto;border:1px solid #e4e7ed;border-radius:4px}.barcode-content-text[data-v-68216036]{font-size:14px;font-weight:600;color:#606266;font-family:Courier New,monospace}.barcode-empty[data-v-68216036]{text-align:center;color:#c0c4cc;padding:30px 0}.label-with-icon[data-v-68216036]{display:inline-flex;align-items:center;gap:4px}

View File

@ -1 +0,0 @@
import{_ as C,u as L,r as p,a as x,b as d,o as S,c as q,d as f,e as o,w as l,f as _,g as k,h as U,s as A,i as B,E,j as z,l as F}from"./index.a99ea9b6.js";import{r as K}from"./request.3edc855e.js";import{u as N}from"./user.dcd53bf0.js";import"./axios.e318b91e.js";const T={class:"login-container"},j={class:"login-box"},D={__name:"Login",setup(M){const R=N(),I=L(),u=p("admin"),i=p(!1),c=p(null),g=p(null),r=x({username:"",password:"",about_id:0}),n=x({username:"",password:"",about_id:0}),y={username:[{required:!0,message:"\u8BF7\u8F93\u5165\u8D26\u53F7",trigger:"blur"}],password:[{required:!0,message:"\u8BF7\u8F93\u5165\u5BC6\u7801",trigger:"blur"},{min:6,message:"\u5BC6\u7801\u957F\u5EA6\u4E0D\u80FD\u5C0F\u4E8E6\u4F4D",trigger:"blur"}]},m=async()=>{const b=u.value==="employee"?c:g,e=u.value==="employee"?r:n;await b.value.validate(),i.value=!0;try{const t=u.value==="admin"?"255":"128",a=await K.post(`/login/${t}`,{...e,type:1});a.code===200&&(A(a.data.token),B(a.data),R.setUserInfoAction(a.data),localStorage.setItem("test_ip","127.0.0.1"),localStorage.setItem("test_port","8080"),E.success("\u767B\u5F55\u6210\u529F"),I.push("/dashboard"))}catch{}finally{i.value=!1}};return(b,e)=>{const t=d("el-input"),a=d("el-form-item"),v=d("el-button"),w=d("el-form"),V=d("el-tab-pane"),h=d("el-tabs");return S(),q("div",T,[f("div",j,[e[7]||(e[7]=f("div",{class:"login-header"},[f("h2",null,"\u8FDB\u9500\u5B58\u7CFB\u7EDF"),f("p",null,"\u8BF7\u9009\u62E9\u767B\u5F55\u89D2\u8272")],-1)),o(h,{modelValue:u.value,"onUpdate:modelValue":e[4]||(e[4]=s=>u.value=s),class:"login-tabs"},{default:l(()=>[o(V,{label:"\u7BA1\u7406\u5458\u767B\u5F55",name:"admin"},{default:l(()=>[o(w,{ref_key:"adminFormRef",ref:g,model:n,rules:y,"label-width":"0",class:"login-form"},{default:l(()=>[o(a,{prop:"username"},{default:l(()=>[o(t,{modelValue:n.username,"onUpdate:modelValue":e[0]||(e[0]=s=>n.username=s),placeholder:"\u8BF7\u8F93\u5165\u8D26\u53F7","prefix-icon":_(z),size:"large"},null,8,["modelValue","prefix-icon"])]),_:1}),o(a,{prop:"password"},{default:l(()=>[o(t,{modelValue:n.password,"onUpdate:modelValue":e[1]||(e[1]=s=>n.password=s),type:"password",placeholder:"\u8BF7\u8F93\u5165\u5BC6\u7801","prefix-icon":_(F),size:"large","show-password":"",onKeyup:k(m,["enter"])},null,8,["modelValue","prefix-icon"])]),_:1}),o(a,null,{default:l(()=>[o(v,{type:"primary",loading:i.value,class:"login-btn",size:"large",onClick:m},{default:l(()=>[...e[5]||(e[5]=[U(" \u767B\u5F55 ",-1)])]),_:1},8,["loading"])]),_:1})]),_:1},8,["model"])]),_:1}),o(V,{label:"\u4EE3\u7406\u767B\u5F55",name:"employee"},{default:l(()=>[o(w,{ref_key:"employeeFormRef",ref:c,model:r,rules:y,"label-width":"0",class:"login-form"},{default:l(()=>[o(a,{prop:"username"},{default:l(()=>[o(t,{modelValue:r.username,"onUpdate:modelValue":e[2]||(e[2]=s=>r.username=s),placeholder:"\u8BF7\u8F93\u5165\u8D26\u53F7","prefix-icon":_(z),size:"large"},null,8,["modelValue","prefix-icon"])]),_:1}),o(a,{prop:"password"},{default:l(()=>[o(t,{modelValue:r.password,"onUpdate:modelValue":e[3]||(e[3]=s=>r.password=s),type:"password",placeholder:"\u8BF7\u8F93\u5165\u5BC6\u7801","prefix-icon":_(F),size:"large","show-password":"",onKeyup:k(m,["enter"])},null,8,["modelValue","prefix-icon"])]),_:1}),o(a,null,{default:l(()=>[o(v,{type:"primary",loading:i.value,class:"login-btn",size:"large",onClick:m},{default:l(()=>[...e[6]||(e[6]=[U(" \u767B\u5F55 ",-1)])]),_:1},8,["loading"])]),_:1})]),_:1},8,["model"])]),_:1})]),_:1},8,["modelValue"])])])}}};var O=C(D,[["__scopeId","data-v-6b005749"]]);export{O as default};

View File

@ -1 +0,0 @@
.login-container[data-v-6b005749]{height:100%;display:flex;justify-content:center;align-items:center;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%)}.login-box[data-v-6b005749]{width:400px;padding:40px;background:#fff;border-radius:8px;box-shadow:0 2px 12px #0000001a}.login-header[data-v-6b005749]{text-align:center;margin-bottom:30px}.login-header h2[data-v-6b005749]{font-size:24px;color:#333;margin-bottom:10px}.login-header p[data-v-6b005749]{color:#666;font-size:14px}.login-tabs[data-v-6b005749]{margin-bottom:20px}.login-form[data-v-6b005749]{margin-top:20px}.login-btn[data-v-6b005749]{width:100%;margin-top:10px}.login-footer[data-v-6b005749]{margin-top:30px;text-align:center;color:#999;font-size:12px}.login-footer p[data-v-6b005749]{margin:5px 0}

View File

@ -1 +0,0 @@
.outbound-manager[data-v-05a0e3f2]{width:100%}.card-header[data-v-05a0e3f2]{font-size:16px;font-weight:600;color:#303133}.filter-bar[data-v-05a0e3f2]{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:16px}.pagination-wrapper[data-v-05a0e3f2]{display:flex;justify-content:flex-end;margin-top:16px}.items-header[data-v-05a0e3f2]{display:flex;justify-content:flex-end}[data-v-05a0e3f2] .el-table__expanded-cell{padding:0}.outbound-search-bar[data-v-05a0e3f2]{display:flex;align-items:center;gap:12px;margin-bottom:14px}.selected-tip[data-v-05a0e3f2]{font-size:13px;color:#606266}.selected-tip strong[data-v-05a0e3f2]{color:#409eff;font-size:16px}.dialog-pagination-wrapper[data-v-05a0e3f2]{display:flex;justify-content:flex-end;margin-top:12px}.dialog-footer[data-v-05a0e3f2]{display:flex;justify-content:flex-end;gap:10px}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
.printer-manager[data-v-6e647c52]{padding:20px}.page-header[data-v-6e647c52]{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.page-header h2[data-v-6e647c52]{margin:0;font-size:22px;color:#303133}.printer-card[data-v-6e647c52]{min-height:200px}.printer-form[data-v-6e647c52]{max-width:600px}.printer-row[data-v-6e647c52]{display:flex;align-items:center;gap:12px}.printer-form[data-v-6e647c52] .el-form-item{display:flex;align-items:center;margin-bottom:18px}.printer-form[data-v-6e647c52] .el-form-item__label{align-self:center;line-height:normal}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
.product-list-wrapper[data-v-773aa012]{width:100%}.filter-bar[data-v-773aa012]{display:flex;flex-wrap:wrap;gap:12px;margin-bottom:20px;align-items:center;background:white;padding:16px 20px;border-radius:8px;box-shadow:0 1px 4px #0000000d}.pagination-wrapper[data-v-773aa012]{margin-top:20px;display:flex;justify-content:flex-end;background:white;padding:14px 20px;border-radius:8px}[data-v-773aa012] .el-table{border-radius:8px;overflow:hidden;box-shadow:0 1px 4px #0000000d}.shop-list-cell[data-v-773aa012]{display:flex;flex-direction:column;gap:4px;padding:4px 0}.shop-item[data-v-773aa012]{line-height:1.4}.product-by-location[data-v-2077780a]{width:100%}.filter-bar[data-v-2077780a]{display:flex;flex-wrap:wrap;gap:12px;margin-bottom:20px;align-items:center;background:white;padding:16px 20px;border-radius:8px;box-shadow:0 1px 4px #0000000d}.pagination-wrapper[data-v-2077780a]{margin-top:20px;display:flex;justify-content:flex-end;background:white;padding:14px 20px;border-radius:8px}[data-v-2077780a] .el-table{border-radius:8px;overflow:hidden;box-shadow:0 1px 4px #0000000d}.expand-content[data-v-2077780a]{padding:12px 20px;background:#fafafa}.expand-header[data-v-2077780a]{margin-bottom:12px}.expand-title[data-v-2077780a]{font-weight:600;font-size:14px;color:#303133}.expand-pagination[data-v-2077780a]{margin-top:12px;display:flex;justify-content:flex-end}.product-manager[data-v-2a00a25f]{width:100%}.card-header[data-v-2a00a25f]{font-size:16px;font-weight:600;color:#303133}[data-v-2a00a25f] .el-dialog__body{padding-top:16px}.dialog-footer[data-v-2a00a25f]{display:flex;justify-content:flex-end;gap:12px}

View File

@ -1 +0,0 @@
.purchase-order-manager[data-v-1b44da8e]{width:100%}.card-header[data-v-1b44da8e]{font-size:16px;font-weight:600;color:#303133}.filter-bar[data-v-1b44da8e]{display:flex;flex-wrap:wrap;gap:12px;margin-bottom:20px;align-items:center;background:white;padding:16px 20px;border-radius:8px;box-shadow:0 1px 4px #0000000d}.pagination-wrapper[data-v-1b44da8e]{margin-top:20px;display:flex;justify-content:flex-end;background:white;padding:14px 20px;border-radius:8px}[data-v-1b44da8e] .el-table{border-radius:8px;overflow:hidden;box-shadow:0 1px 4px #0000000d}[data-v-1b44da8e] .el-dialog__body{padding-top:16px}.dialog-footer[data-v-1b44da8e]{display:flex;justify-content:flex-end;gap:12px}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
.review-page[data-v-221993fc]{padding:0}.card-header[data-v-221993fc]{display:flex;justify-content:space-between;align-items:center}.section-card[data-v-221993fc]{margin-bottom:16px}.search-bar[data-v-221993fc]{display:flex;align-items:center;gap:16px;margin-bottom:16px}.search-item[data-v-221993fc]{display:flex;align-items:center;gap:8px}.search-item span[data-v-221993fc]{font-size:14px;color:#606266;white-space:nowrap}.pagination-wrapper[data-v-221993fc]{display:flex;justify-content:flex-end;margin-top:16px}.diff-cell{color:#f56c6c!important;font-weight:700}.diff-popover{padding:8px 12px!important}.diff-content{font-size:13px;line-height:1.6}.diff-row{display:flex;align-items:flex-start;gap:4px;padding:2px 0}.diff-label{white-space:nowrap;color:#909399;flex-shrink:0}.diff-val{color:#303133;word-break:break-all}.diff-row.new .diff-val{color:#e6a23c;font-weight:700}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
@charset "UTF-8";.sales-order-manager[data-v-6514288b]{width:100%}.card-header[data-v-6514288b]{font-size:16px;font-weight:600;color:#303133}.filter-bar[data-v-6514288b]{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:16px}.pagination-wrapper[data-v-6514288b]{display:flex;justify-content:flex-end;margin-top:16px}.items-header[data-v-6514288b]{display:flex;justify-content:flex-end}.total-amount[data-v-6514288b]{display:flex;justify-content:flex-end;align-items:center;margin-top:16px;padding:12px;background:#f5f7fa;border-radius:4px;font-size:14px}.total-amount .amount[data-v-6514288b]{color:#e6a23c;font-weight:600;font-size:18px;margin-left:8px}[data-v-6514288b] .el-table__expanded-cell{padding:0}.outbound-search-bar[data-v-6514288b]{display:flex;align-items:center;gap:16px;margin-bottom:16px}.outbound-search-bar .selected-tip[data-v-6514288b]{font-size:13px;color:#606266}.outbound-search-bar .selected-tip strong[data-v-6514288b]{color:#67c23a;font-size:15px}

View File

@ -1 +0,0 @@
@charset "UTF-8";.shipping-order-manager[data-v-cb65ffee]{width:100%}.card-header[data-v-cb65ffee]{font-size:16px;font-weight:600;color:#303133}.filter-bar[data-v-cb65ffee]{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:16px}.pagination-wrapper[data-v-cb65ffee]{display:flex;justify-content:flex-end;margin-top:16px}[data-v-cb65ffee] .el-table__expanded-cell{padding:0}.hidden-scan-input[data-v-cb65ffee]{position:absolute;left:-9999px;opacity:0;width:1px;height:1px}[data-v-cb65ffee] .el-table__row.scan-completed-row{background-color:#ecf5ff}[data-v-cb65ffee] .el-table__row.scan-completed-row:hover>td{background-color:#ecf5ff}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
.shop-manager[data-v-6b1a2acb]{width:100%}.card-header[data-v-6b1a2acb]{font-size:16px;font-weight:600;color:#303133}.filter-bar[data-v-6b1a2acb]{display:flex;flex-wrap:wrap;gap:12px;margin-bottom:20px;align-items:center;background:white;padding:16px 20px;border-radius:8px;box-shadow:0 1px 4px #0000000d}.pagination-wrapper[data-v-6b1a2acb]{margin-top:20px;display:flex;justify-content:flex-end;background:white;padding:14px 20px;border-radius:8px}[data-v-6b1a2acb] .el-table{border-radius:8px;overflow:hidden;box-shadow:0 1px 4px #0000000d}[data-v-6b1a2acb] .el-dialog__body{padding-top:16px}.dialog-footer[data-v-6b1a2acb]{display:flex;justify-content:flex-end;gap:12px}.shop-detail-container[data-v-6b1a2acb]{max-height:60vh;overflow-y:auto;padding-right:8px}.detail-section[data-v-6b1a2acb]{margin-bottom:24px}.section-title[data-v-6b1a2acb]{font-size:16px;font-weight:600;color:#303133;padding-left:10px;border-left:4px solid #409eff;margin-bottom:16px;line-height:1.2}.token-placeholder[data-v-6b1a2acb]{font-family:monospace;letter-spacing:2px;color:#909399}[data-v-6b1a2acb] .el-descriptions__label{width:130px;background-color:#fafafa}[data-v-6b1a2acb] .el-descriptions__content{word-break:break-all}.img-list[data-v-6b1a2acb]{display:flex;flex-wrap:wrap;gap:8px}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
.shop-settings[data-v-55d38e90]{max-width:1000px;margin:0 auto;padding:24px;background:#fff;border-radius:16px;box-shadow:0 4px 12px #0000000d;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif}.settings-title[data-v-55d38e90]{font-size:24px;font-weight:600;margin:0 0 20px;color:#1e293b;border-left:4px solid #3b82f6;padding-left:16px}.settings-tabs[data-v-55d38e90]{display:flex;gap:8px;border-bottom:1px solid #e2e8f0;margin-bottom:24px}.tab-btn[data-v-55d38e90]{padding:10px 20px;font-size:15px;font-weight:500;background:none;border:none;cursor:pointer;color:#64748b;border-radius:8px 8px 0 0;transition:all .2s}.tab-btn.active[data-v-55d38e90]{color:#3b82f6;background:#eff6ff;border-bottom:2px solid #3b82f6}.tab-content[data-v-55d38e90]{animation:fadeIn-55d38e90 .2s ease}@keyframes fadeIn-55d38e90{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.setting-section[data-v-55d38e90]{background:#f8fafc;border-radius:16px;padding:20px;margin-bottom:24px;border:1px solid #e2e8f0}.section-title[data-v-55d38e90]{font-size:18px;font-weight:600;margin:0 0 16px;color:#0f172a;padding-left:10px;border-left:3px solid #3b82f6}.setting-grid[data-v-55d38e90]{display:grid;grid-template-columns:repeat(2,1fr);gap:20px}.setting-row[data-v-55d38e90]{display:flex;gap:20px;flex-wrap:wrap}.setting-row.three-cols[data-v-55d38e90]{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}.setting-item[data-v-55d38e90]{flex:1;min-width:160px}.setting-item.full[data-v-55d38e90]{max-width:600px}.setting-label[data-v-55d38e90]{display:block;font-size:14px;font-weight:500;margin-bottom:8px;color:#334155}.setting-input[data-v-55d38e90]{width:100%;padding:8px 12px;border:1px solid #cbd5e1;border-radius:8px;font-size:14px;transition:.2s;background:white}.setting-input[data-v-55d38e90]:focus{outline:none;border-color:#3b82f6;box-shadow:0 0 0 2px #3b82f633}.price-input-group[data-v-55d38e90]{display:flex;align-items:center;gap:6px}.unit[data-v-55d38e90]{font-size:13px;color:#475569}.field-hint[data-v-55d38e90]{font-size:12px;color:#6c757d;margin-top:6px;line-height:1.4}.field-hint.warning[data-v-55d38e90]{color:#e67e22;background:#fff3e0;padding:6px 10px;border-radius:8px;margin-top:8px}.switch-item[data-v-55d38e90]{margin-bottom:20px}.switch-label[data-v-55d38e90]{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;font-weight:500;color:#1e293b}.switch[data-v-55d38e90]{position:relative;display:inline-block;width:48px;height:24px}.switch input[data-v-55d38e90]{opacity:0;width:0;height:0}.slider[data-v-55d38e90]{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background-color:#cbd5e1;transition:.3s;border-radius:34px}.slider[data-v-55d38e90]:before{position:absolute;content:"";height:18px;width:18px;left:3px;bottom:3px;background-color:#fff;transition:.3s;border-radius:50%}input:checked+.slider[data-v-55d38e90]{background-color:#3b82f6}input:checked+.slider[data-v-55d38e90]:before{transform:translate(24px)}.formula-hint[data-v-55d38e90]{background:#eef2ff;padding:12px;border-radius:12px;font-size:13px;color:#1e40af;margin-top:12px;line-height:1.5}.actions-bar[data-v-55d38e90]{display:flex;justify-content:flex-end;gap:16px;margin-top:16px;padding-top:16px;border-top:1px solid #e2e8f0}.btn-primary[data-v-55d38e90]{background:#3b82f6;color:#fff;border:none;padding:8px 24px;border-radius:40px;font-size:14px;font-weight:500;cursor:pointer;transition:.2s}.btn-primary[data-v-55d38e90]:hover{background:#2563eb}.btn-secondary[data-v-55d38e90]{background:white;border:1px solid #cbd5e1;padding:8px 24px;border-radius:40px;font-size:14px;cursor:pointer;transition:.2s}.btn-secondary[data-v-55d38e90]:hover{background:#f1f5f9}@media (max-width: 680px){.setting-grid[data-v-55d38e90],.setting-row.three-cols[data-v-55d38e90]{grid-template-columns:1fr}.shop-settings[data-v-55d38e90]{padding:16px}}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
.sorting-settings[data-v-483976f4]{max-width:800px;margin:20px auto;padding:20px;background:#fff;border-radius:8px;box-shadow:0 2px 12px #0000001a}h2[data-v-483976f4]{text-align:center;margin-bottom:24px}.tips-block[data-v-483976f4]{background-color:#f5f7fa;padding:8px 12px;border-radius:4px;color:#606266;font-size:13px;margin-bottom:12px;line-height:1.5;border-left:3px solid #409eff}.field-hint[data-v-483976f4]{font-size:12px;color:#909399;margin-top:4px}.switch-hint[data-v-483976f4]{margin-left:12px;font-size:12px;color:#909399}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
.stock-check-manager[data-v-0e6961ff]{padding:20px;background:#fff;border-radius:4px}.filter-bar[data-v-0e6961ff]{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:16px}.pagination-wrapper[data-v-0e6961ff]{display:flex;justify-content:flex-end;margin-top:16px}.expand-content[data-v-0e6961ff]{padding:12px 20px;background:#fafafa}.expand-actions[data-v-0e6961ff]{margin-top:12px;text-align:right}.text-primary[data-v-0e6961ff]{color:#409eff;font-weight:600}.text-success[data-v-0e6961ff]{color:#67c23a;font-weight:600}.text-warning[data-v-0e6961ff]{color:#e6a23c;font-weight:600}.text-danger[data-v-0e6961ff]{color:#f56c6c;font-weight:600}.text-info[data-v-0e6961ff]{color:#909399;font-weight:600}

View File

@ -1 +0,0 @@
.illegal-book-dialog-body[data-v-32aaa3fb]{display:flex;gap:24px;min-height:500px}.illegal-side[data-v-32aaa3fb]{flex:1;padding:16px;border-radius:8px;background-color:#fafafa}.illegal-side-left[data-v-32aaa3fb]{background-color:#f5f7fa;border:1px solid #e4e7ed}.illegal-side-right[data-v-32aaa3fb]{background-color:#fff;border:1px solid #dcdfe6}.illegal-side-title[data-v-32aaa3fb]{font-size:16px;font-weight:600;margin-bottom:16px;padding-bottom:8px;border-bottom:1px solid #e4e7ed;color:#303133}.book-content[data-v-32aaa3fb]{position:relative;overflow:hidden}.book-image-container[data-v-32aaa3fb]{float:left;margin-right:16px;margin-bottom:16px}.book-image[data-v-32aaa3fb]{width:300px;height:300px;overflow:hidden;border-radius:8px;box-shadow:0 4px 12px #0000001a}.book-image img[data-v-32aaa3fb]{max-width:100%;max-height:100%;object-fit:cover}.book-image[data-v-32aaa3fb]{position:relative}.book-image-overlay[data-v-32aaa3fb]{position:absolute;bottom:0;left:0;right:0;height:40px;background:rgba(0,0,0,.55);display:flex;align-items:center;justify-content:center;color:#fff;font-size:14px;letter-spacing:1px;transition:height .25s ease;cursor:pointer}.book-image:hover .book-image-overlay[data-v-32aaa3fb]{height:100%;background:rgba(0,0,0,.6)}.book-image-overlay span[data-v-32aaa3fb]{pointer-events:none}.book-image-placeholder[data-v-32aaa3fb]{width:300px;height:300px;display:flex;align-items:center;justify-content:center;border:1px solid #ddd;border-radius:8px;background-color:#f5f5f5}.book-image-placeholder p[data-v-32aaa3fb]{color:#666;text-align:center;margin:0;font-size:14px}.book-info-header[data-v-32aaa3fb]{overflow:hidden}.book-other-fields[data-v-32aaa3fb]{clear:both;display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:12px;margin-top:12px}.form-group[data-v-32aaa3fb]{margin-bottom:12px}.form-group label[data-v-32aaa3fb]{display:block;font-size:14px;font-weight:500;color:#666;margin-bottom:4px}.form-group.inline-label[data-v-32aaa3fb]{display:flex;align-items:center;gap:8px}.form-group.inline-label label[data-v-32aaa3fb]{display:block;flex-shrink:0;width:60px}.form-group.inline-label .form-input[data-v-32aaa3fb],.form-group.inline-label .form-textarea[data-v-32aaa3fb]{flex:1;width:100%;min-width:0}.form-input[data-v-32aaa3fb]{width:100%;padding:8px 12px;border:1px solid #ddd;border-radius:4px;font-size:14px;transition:border-color .3s ease;box-sizing:border-box}.form-input[data-v-32aaa3fb]:focus{outline:none;border-color:#409eff;box-shadow:0 0 0 2px #409eff33}.form-textarea[data-v-32aaa3fb]{width:100%;padding:8px 12px;border:1px solid #ddd;border-radius:4px;font-size:14px;resize:vertical;min-height:60px;transition:border-color .3s ease;box-sizing:border-box}.form-textarea[data-v-32aaa3fb]:focus{outline:none;border-color:#409eff;box-shadow:0 0 0 2px #409eff33}.illegal-side-right .form-input[data-v-32aaa3fb]:not([disabled]),.illegal-side-right .form-textarea[data-v-32aaa3fb]:not([disabled]){border-color:#409eff;background-color:#fff}.illegal-side-right .form-input[data-v-32aaa3fb]:not([disabled]):focus,.illegal-side-right .form-textarea[data-v-32aaa3fb]:not([disabled]):focus{box-shadow:0 0 0 2px #409eff4d}.illegal-side-right .el-select[data-v-32aaa3fb]{width:100%}.illegal-side-right .el-select .el-input__wrapper[data-v-32aaa3fb]{border:1px solid #ddd;border-radius:4px;box-shadow:none;padding:1px 12px;background-color:#fff;min-height:36px}.illegal-side-right .el-select .el-input__wrapper.is-focus[data-v-32aaa3fb]{border-color:#409eff;box-shadow:0 0 0 2px #409eff33}.illegal-side-right .el-select .el-input__inner[data-v-32aaa3fb]{font-size:14px;height:30px}.dialog-footer[data-v-32aaa3fb]{display:flex;justify-content:flex-end;gap:12px}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
.supplier-manager[data-v-56ea2431]{padding:20px;background-color:#f5f7fa;min-height:100vh}.filter-bar[data-v-56ea2431]{display:flex;flex-wrap:wrap;gap:12px;margin-bottom:20px;align-items:center;background:white;padding:16px 20px;border-radius:8px;box-shadow:0 1px 4px #0000000d}.pagination-wrapper[data-v-56ea2431]{margin-top:20px;display:flex;justify-content:flex-end;background:white;padding:14px 20px;border-radius:8px}[data-v-56ea2431] .el-table{border-radius:8px;overflow:hidden;box-shadow:0 1px 4px #0000000d}[data-v-56ea2431] .el-dialog__body{padding-top:16px}.dialog-footer[data-v-56ea2431]{display:flex;justify-content:flex-end;gap:12px}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
.warehouse-manager[data-v-12afdc00]{padding:20px;background-color:#f5f7fa;box-sizing:border-box}.filter-bar[data-v-12afdc00]{display:flex;flex-wrap:wrap;gap:12px;margin-bottom:20px;align-items:center;background:white;padding:16px 20px;border-radius:8px;box-shadow:0 1px 4px #0000000d}.pagination-wrapper[data-v-12afdc00]{margin-top:20px;display:flex;justify-content:flex-end;background:white;padding:14px 20px;border-radius:8px}[data-v-12afdc00] .el-table{border-radius:8px;overflow:hidden;box-shadow:0 1px 4px #0000000d}[data-v-12afdc00] .el-table__row.selected-warehouse-row,[data-v-12afdc00] .el-table__row.selected-warehouse-row>td,[data-v-12afdc00] .el-table__fixed-right .el-table__row.selected-warehouse-row td{background-color:#ffe6e6!important}[data-v-12afdc00] .el-table--striped .el-table__row.selected-warehouse-row.el-table__row--striped td,[data-v-12afdc00] .el-table--striped .el-table__row.selected-warehouse-row td{background-color:#ffe6e6!important}[data-v-12afdc00] .el-table__row.selected-warehouse-row:hover>td{background-color:#ffe6e6!important}.dialog-footer[data-v-12afdc00]{display:flex;justify-content:flex-end;gap:12px}.detail-loading[data-v-12afdc00]{display:flex;align-items:center;justify-content:center;padding:40px 0;color:#909399;gap:8px}.detail-content[data-v-12afdc00]{padding:0 10px}.detail-section[data-v-12afdc00]{margin-bottom:20px}.detail-row[data-v-12afdc00]{display:flex;padding:8px 0;border-bottom:1px solid #f0f0f0}.detail-row[data-v-12afdc00]:last-child{border-bottom:none}.detail-label[data-v-12afdc00]{color:#909399;min-width:80px;flex-shrink:0}.detail-code[data-v-12afdc00]{font-weight:600;color:#409eff}.status-enabled[data-v-12afdc00]{color:#67c23a;font-weight:500}.status-disabled[data-v-12afdc00]{color:#f56c6c;font-weight:500}.barcode-section[data-v-12afdc00]{margin-top:20px;padding-top:20px;border-top:1px solid #e4e7ed;text-align:center}.barcode-title[data-v-12afdc00]{font-size:14px;color:#606266;margin-bottom:12px}.barcode-loading[data-v-12afdc00]{display:flex;align-items:center;justify-content:center;padding:20px 0;color:#909399;gap:8px}.barcode-image-wrapper[data-v-12afdc00]{padding:12px;background:#fafafa;border-radius:6px;display:inline-block}.barcode-image[data-v-12afdc00]{max-width:200px;height:auto}.barcode-content-text[data-v-12afdc00]{margin-top:8px;font-size:13px;color:#606266;font-family:Courier New,monospace}.label-with-icon[data-v-12afdc00]{display:inline-flex;align-items:center;gap:4px}.warehouse-page[data-v-6300bf56]{padding:0}.section-card[data-v-6300bf56]{margin-bottom:16px}.section-card[data-v-6300bf56]:last-child{margin-bottom:0}.card-header[data-v-6300bf56]{font-size:16px;font-weight:600;color:#303133}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
.wave-task-manager[data-v-ba0457b6]{width:100%}.card-header[data-v-ba0457b6]{font-size:16px;font-weight:600;color:#303133}.filter-bar[data-v-ba0457b6]{display:flex;flex-wrap:wrap;gap:12px;margin-bottom:20px;align-items:center;background:white;padding:16px 20px;border-radius:8px;box-shadow:0 1px 4px #0000000d}.pagination-wrapper[data-v-ba0457b6]{margin-top:20px;display:flex;justify-content:flex-end;background:white;padding:14px 20px;border-radius:8px}[data-v-ba0457b6] .el-table{border-radius:8px;overflow:hidden;box-shadow:0 1px 4px #0000000d}.expand-content[data-v-ba0457b6]{padding:12px 20px;background:#fafafa}.expand-content[data-v-ba0457b6] .el-table{box-shadow:none}.no-data[data-v-ba0457b6]{text-align:center;color:#909399;padding:20px}.text-danger[data-v-ba0457b6]{color:#f56c6c}.detail-section[data-v-ba0457b6]{margin-top:20px}.detail-section h4[data-v-ba0457b6]{margin-bottom:12px;color:#303133}[data-v-ba0457b6] .el-dialog__body{padding-top:16px}.dialog-footer[data-v-ba0457b6]{display:flex;justify-content:flex-end;gap:12px}

File diff suppressed because one or more lines are too long

1
dist/assets/adminLayout.94cb81b4.css vendored Normal file
View File

@ -0,0 +1 @@
.layout[data-v-ab4faab8]{height:100%}.aside[data-v-ab4faab8]{background-color:#304156;transition:width .3s;overflow:hidden;display:flex;flex-direction:column}.logo[data-v-ab4faab8]{height:60px;line-height:60px;text-align:center;color:#fff;font-size:18px;font-weight:700;background-color:#1f2d3d;white-space:nowrap;overflow:hidden;flex-shrink:0}.logo-collapse[data-v-ab4faab8]{font-size:20px}.menu[data-v-ab4faab8]{border-right:none;flex:1;overflow-y:auto;overflow-x:hidden}.menu[data-v-ab4faab8]::-webkit-scrollbar{width:6px}.menu[data-v-ab4faab8]::-webkit-scrollbar-track{background:#304156}.menu[data-v-ab4faab8]::-webkit-scrollbar-thumb{background:#4a5568;border-radius:3px}.menu[data-v-ab4faab8]::-webkit-scrollbar-thumb:hover{background:#718096}.header[data-v-ab4faab8]{background-color:#fff;border-bottom:1px solid #e6e9f0;display:flex;align-items:center;justify-content:space-between;padding:0 20px}.header-left[data-v-ab4faab8]{display:flex;align-items:center}.collapse-icon[data-v-ab4faab8]{font-size:20px;cursor:pointer;color:#606266}.header-right[data-v-ab4faab8]{display:flex;align-items:center;gap:20px}.user-info[data-v-ab4faab8]{display:flex;align-items:center;gap:5px;cursor:pointer;color:#606266;font-size:14px;outline:none}.user-info[data-v-ab4faab8]:focus{outline:none}.points-badge[data-v-ab4faab8]{background-color:#f56c6c;color:#fff;padding:4px 8px;border-radius:4px;font-size:12px}.main[data-v-ab4faab8]{background-color:#f0f2f5;padding:10px}

1
dist/assets/adminLayout.b5059798.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
import{r as t}from"./request.3edc855e.js";const o=async r=>{const e=new FormData;return e.append("content",r),t.post("/barcode/generate",e)};export{o as g};
import{r as t}from"./request.92d2eb68.js";const o=async r=>{const e=new FormData;return e.append("content",r),t.post("/barcode/generate",e)};export{o as g};

View File

@ -1 +1 @@
import{r as a}from"./request.3edc855e.js";const r="/car",c=e=>{const t=e==null?void 0:e.data;return t?Array.isArray(t)?{list:t,total:t.length}:{list:Array.isArray(t.list)?t.list:[],total:typeof t.total=="number"?t.total:Array.isArray(t.list)?t.list.length:0}:{list:[],total:0}},l=async({keyword:e,page:t,pageSize:s})=>{const n={keyword:e||void 0,page:t,page_size:s},o=await a.get(`${r}/list`,{params:n});return c(o)},u=async e=>{const t=await a.get(`${r}/detail/${e}`);return(t==null?void 0:t.data)||null},d=async e=>a.post(`${r}/create`,e),p=async e=>a.put(`${r}/update`,e),y=async e=>{const t=new FormData;return t.append("id",String(e)),a.delete(`${r}/delete`,{data:t})},f=async e=>a.post(`${r}/shop/create`,e),h=async e=>a.post(`${r}/shop/delete`,e);export{u as a,h as b,d as c,y as d,f as e,l as f,p as u};
import{r as a}from"./request.92d2eb68.js";const r="/car",c=e=>{const t=e==null?void 0:e.data;return t?Array.isArray(t)?{list:t,total:t.length}:{list:Array.isArray(t.list)?t.list:[],total:typeof t.total=="number"?t.total:Array.isArray(t.list)?t.list.length:0}:{list:[],total:0}},l=async({keyword:e,page:t,pageSize:s})=>{const n={keyword:e||void 0,page:t,page_size:s},o=await a.get(`${r}/list`,{params:n});return c(o)},u=async e=>{const t=await a.get(`${r}/detail/${e}`);return(t==null?void 0:t.data)||null},d=async e=>a.post(`${r}/create`,e),p=async e=>a.put(`${r}/update`,e),y=async e=>{const t=new FormData;return t.append("id",String(e)),a.delete(`${r}/delete`,{data:t})},f=async e=>a.post(`${r}/shop/create`,e),h=async e=>a.post(`${r}/shop/delete`,e);export{u as a,h as b,d as c,y as d,f as e,l as f,p as u};

1
dist/assets/car.3e4d66eb.css vendored Normal file
View File

@ -0,0 +1 @@
.car-manager[data-v-5ab815e0]{padding:0}.card-header[data-v-5ab815e0]{font-size:16px;font-weight:600;color:#303133}.section-card[data-v-5ab815e0]{margin-bottom:16px}.filter-bar[data-v-5ab815e0]{display:flex;flex-wrap:wrap;gap:12px;margin-bottom:20px;align-items:center}.pagination-wrapper[data-v-5ab815e0]{margin-top:20px;display:flex;justify-content:flex-end}[data-v-5ab815e0] .el-table{border-radius:8px;overflow:hidden}[data-v-5ab815e0] .el-dialog__body{padding-top:16px}.dialog-footer[data-v-5ab815e0]{display:flex;justify-content:flex-end;gap:12px}.expand-content[data-v-5ab815e0]{padding:16px 24px}.expand-header[data-v-5ab815e0]{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.expand-title[data-v-5ab815e0]{font-size:14px;font-weight:600;color:#303133}.secret-text[data-v-5ab815e0]{color:#909399;font-family:monospace;letter-spacing:2px}.label-with-icon[data-v-5ab815e0]{display:inline-flex;align-items:center;gap:4px}

1
dist/assets/car.51b036d4.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
import{E as t}from"./index.a99ea9b6.js";const n=async(r,o="\u590D\u5236\u6210\u529F")=>{try{if(navigator.clipboard&&navigator.clipboard.writeText)return await navigator.clipboard.writeText(r),t.success(o),!0;const e=document.createElement("textarea");e.value=r,e.style.position="fixed",e.style.opacity="0",document.body.appendChild(e),e.select(),e.setSelectionRange(0,99999);const a=document.execCommand("copy");return document.body.removeChild(e),a?(t.success(o),!0):(t.error("\u590D\u5236\u5931\u8D25"),!1)}catch{return t.error("\u590D\u5236\u5931\u8D25"),!1}};export{n as c};

1
dist/assets/clipboard.3ccc9f0a.js vendored Normal file
View File

@ -0,0 +1 @@
import{E as s}from"./index.887c1a2d.js";const o=async(a,r="\u590D\u5236\u6210\u529F")=>{try{if(navigator.clipboard&&navigator.clipboard.writeText)return await navigator.clipboard.writeText(a),s.success({message:r,customClass:"scan-success-message"}),!0;const e=document.createElement("textarea");e.value=a,e.style.position="fixed",e.style.opacity="0",document.body.appendChild(e),e.select(),e.setSelectionRange(0,99999);const t=document.execCommand("copy");return document.body.removeChild(e),t?(s.success({message:r,customClass:"scan-success-message"}),!0):(s.error({message:"\u590D\u5236\u5931\u8D25",customClass:"scan-error-message"}),!1)}catch{return s.error({message:"\u590D\u5236\u5931\u8D25",customClass:"scan-error-message"}),!1}};export{o as c};

1
dist/assets/config.08364858.js vendored Normal file
View File

@ -0,0 +1 @@
import{a as i}from"./axios.e318b91e.js";const w=(t,n)=>{const e=new URLSearchParams;return e.append("isbn","0"),e.append("out_id","0"),e.append("quality","0"),e.append("query_index","1"),e.append("user_id","0"),i.post(`http://${t}:${n}/api/goods/query`,e.toString(),{headers:{"Content-Type":"application/x-www-form-urlencoded"},timeout:1e4}).then(p=>p.data)},$=(t,n,e,p,r,s,d)=>{const o=new URLSearchParams;return o.append("new_price",e),o.append("placeholder_down_price",p),o.append("min_shipping_fee",r),o.append("min_price",s),o.append("query_index",d),console.log(o.toString()),i.post(`http://${t}:${n}/api/config/price/set`,o.toString(),{headers:{"Content-Type":"application/x-www-form-urlencoded"},timeout:1e4}).then(c=>c.data)},y=(t,n,e,p)=>{const r=new FormData;return r.append("username",t),r.append("password",n),i.post(`http://${e}:${p}/api/kfz/login`,r,{timeout:15e3}).then(s=>s.data)},k=(t,n,e)=>i.post(`http://${n}:${e}/api/token/add`,t,{headers:{"Content-Type":"application/json"},timeout:1e4}).then(p=>p.data),S=(t,n)=>i.post(`http://${t}:${n}/api/token/list`,{},{headers:{"Content-Type":"application/json"},timeout:1e4}).then(e=>e.data),q=(t,n)=>i.post(`http://${t}:${n}/api/config/price/get`,{},{headers:{"Content-Type":"application/json"},timeout:1e4}).then(e=>e.data),T=({ip:t,port:n,isbn:e,bookName:p,author:r,publisher:s,isSuit:d,outId:o,quality:c,queryIndex:g,userId:l,placeholderDownPrice:u,minShippingFee:h,minPrice:m})=>{const a=new URLSearchParams;return d?(a.append("book_name",p||""),a.append("author",r||""),a.append("publishing",s||"")):a.append("isbn",e),a.append("out_id",String(o)),a.append("quality",String(c)),a.append("query_index",String(g)),a.append("user_id",String(l)),u&&a.append("placeholder_down_price",u),h&&a.append("min_shipping_fee",h),m&&a.append("min_price",m),i.post(`http://${t}:${n}/api/goods/query`,a.toString(),{headers:{"Content-Type":"application/x-www-form-urlencoded"},timeout:1e4}).then(f=>f.data)},x=(t,n,e)=>i.get(`http://${t}:${n}/api/token/delete`,{params:{id:e}}).then(p=>p.data);export{q as a,k as b,x as d,S as f,y as k,T as q,$ as s,w as t};

1
dist/assets/config.53d6eb34.css vendored Normal file
View File

@ -0,0 +1 @@
@charset "UTF-8";.config-page[data-v-9008af20]{padding:20px;max-width:860px;margin:0 auto}.config-card[data-v-9008af20]{border-radius:12px}.card-header[data-v-9008af20]{display:flex;align-items:center;justify-content:space-between}.header-left[data-v-9008af20]{display:flex;align-items:center;gap:8px;font-size:16px;font-weight:600}.config-row[data-v-9008af20]{display:flex;gap:16px}.config-row .flex-item[data-v-9008af20]{flex:1;min-width:0}.save-bar[data-v-9008af20]{display:flex}.result-box[data-v-9008af20]{margin-top:16px}.account-section .section-title[data-v-9008af20]{display:flex;align-items:center;gap:8px;font-size:15px;font-weight:600;color:#303133;margin-bottom:16px}.account-section .count-tag[data-v-9008af20]{margin-left:0}.account-form .account-row[data-v-9008af20]{margin-bottom:16px}.account-form .account-row .account-label[data-v-9008af20]{font-size:14px;color:#606266;margin-bottom:8px;font-weight:500}.account-form .account-row .account-inputs[data-v-9008af20]{display:flex;gap:8px;align-items:center}.add-btn[data-v-9008af20]{margin-top:4px}.bind-bar[data-v-9008af20],.append-bar[data-v-9008af20]{margin-top:16px}.append-form-actions[data-v-9008af20]{display:flex;gap:8px;margin-top:8px}.token-text[data-v-9008af20]{font-family:Courier New,monospace;font-size:12px}.fade-enter-active[data-v-9008af20],.fade-leave-active[data-v-9008af20]{transition:opacity .3s ease}.fade-enter-from[data-v-9008af20],.fade-leave-to[data-v-9008af20]{opacity:0}@media (max-width: 640px){.config-row[data-v-9008af20]{flex-direction:column;gap:0}.account-inputs[data-v-9008af20]{flex-wrap:wrap}}.label-with-icon[data-v-9008af20]{display:inline-flex;align-items:center;gap:4px}

File diff suppressed because one or more lines are too long

1
dist/assets/config.6d1e7ea1.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
@charset "UTF-8";.config-page[data-v-8f6b30a0]{padding:20px;max-width:860px;margin:0 auto}.config-card[data-v-8f6b30a0]{border-radius:12px}.card-header[data-v-8f6b30a0]{display:flex;align-items:center;justify-content:space-between}.header-left[data-v-8f6b30a0]{display:flex;align-items:center;gap:8px;font-size:16px;font-weight:600}.config-row[data-v-8f6b30a0]{display:flex;gap:16px}.config-row .flex-item[data-v-8f6b30a0]{flex:1;min-width:0}.save-bar[data-v-8f6b30a0]{display:flex}.result-box[data-v-8f6b30a0]{margin-top:16px}.account-section .section-title[data-v-8f6b30a0]{display:flex;align-items:center;gap:8px;font-size:15px;font-weight:600;color:#303133;margin-bottom:16px}.account-section .count-tag[data-v-8f6b30a0]{margin-left:0}.account-form .account-row[data-v-8f6b30a0]{margin-bottom:16px}.account-form .account-row .account-label[data-v-8f6b30a0]{font-size:14px;color:#606266;margin-bottom:8px;font-weight:500}.account-form .account-row .account-inputs[data-v-8f6b30a0]{display:flex;gap:8px;align-items:center}.add-btn[data-v-8f6b30a0]{margin-top:4px}.bind-bar[data-v-8f6b30a0],.append-bar[data-v-8f6b30a0]{margin-top:16px}.append-form-actions[data-v-8f6b30a0]{display:flex;gap:8px;margin-top:8px}.token-text[data-v-8f6b30a0]{font-family:Courier New,monospace;font-size:12px}.fade-enter-active[data-v-8f6b30a0],.fade-leave-active[data-v-8f6b30a0]{transition:opacity .3s ease}.fade-enter-from[data-v-8f6b30a0],.fade-leave-to[data-v-8f6b30a0]{opacity:0}@media (max-width: 640px){.config-row[data-v-8f6b30a0]{flex-direction:column;gap:0}.account-inputs[data-v-8f6b30a0]{flex-wrap:wrap}}.label-with-icon[data-v-8f6b30a0]{display:inline-flex;align-items:center;gap:4px}

1
dist/assets/config.af7eafe9.css vendored Normal file
View File

@ -0,0 +1 @@
@charset "UTF-8";.courier-config-page[data-v-c846cf04]{padding:20px;max-width:1000px;margin:0 auto}.config-card[data-v-c846cf04]{border-radius:12px}.card-header[data-v-c846cf04]{display:flex;align-items:center;justify-content:space-between}.header-left[data-v-c846cf04]{display:flex;align-items:center;gap:8px;font-size:16px;font-weight:600}.config-section[data-v-c846cf04]{margin-bottom:24px;padding-bottom:16px;border-bottom:1px solid #ebeef5}.config-section[data-v-c846cf04]:last-of-type{border-bottom:none;margin-bottom:16px}.section-title[data-v-c846cf04]{display:flex;align-items:center;gap:8px;font-size:15px;font-weight:600;color:#303133;margin-bottom:16px}.config-row[data-v-c846cf04]{display:flex;gap:16px;margin-bottom:8px}.config-row .flex-item[data-v-c846cf04],.config-row .flex-item-full[data-v-c846cf04]{flex:1;min-width:0}.switch-tip[data-v-c846cf04]{margin-left:12px;font-size:12px;color:#909399}.save-bar[data-v-c846cf04]{display:flex;gap:12px;margin-top:24px;padding-top:16px;border-top:1px solid #ebeef5}@media (max-width: 768px){.config-row[data-v-c846cf04]{flex-direction:column;gap:0}}

1
dist/assets/config.f83b38c3.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/assets/dashboard.84519081.css vendored Normal file
View File

@ -0,0 +1 @@
.dashboard[data-v-4b658a84]{padding:20px;background:#f5f7fa;min-height:calc(100vh - 120px)}.stats-row[data-v-4b658a84]{margin-bottom:20px}.stat-card[data-v-4b658a84]{border-radius:12px;transition:all .3s ease;border:none}.stat-card[data-v-4b658a84]:hover{transform:translateY(-4px);box-shadow:0 12px 24px #0000001a}.stat-card[data-v-4b658a84] .el-card__body{padding:20px}.stat-content[data-v-4b658a84]{display:flex;align-items:center}.stat-icon[data-v-4b658a84]{width:64px;height:64px;border-radius:16px;display:flex;align-items:center;justify-content:center;color:#fff;margin-right:20px;flex-shrink:0}.stat-info[data-v-4b658a84]{flex:1}.stat-title[data-v-4b658a84]{font-size:14px;color:#909399;margin-bottom:8px}.stat-value[data-v-4b658a84]{font-size:32px;font-weight:700;color:#303133;line-height:1.2;margin-bottom:4px}.stat-desc[data-v-4b658a84]{font-size:12px;color:#909399}.mt-20[data-v-4b658a84]{margin-top:20px}.table-card[data-v-4b658a84]{border-radius:12px;border:none}.table-card[data-v-4b658a84] .el-card__header{padding:16px 20px;border-bottom:1px solid #ebeef5}.table-card[data-v-4b658a84] .el-card__body{padding:0}.card-header[data-v-4b658a84]{display:flex;justify-content:space-between;align-items:center}.card-title[data-v-4b658a84]{font-size:16px;font-weight:600;color:#303133}.product-name[data-v-4b658a84]{color:#606266;font-weight:500}.price[data-v-4b658a84]{color:#f56c6c;font-weight:600}.total-count[data-v-4b658a84]{color:#409eff;font-weight:600;font-size:15px}[data-v-4b658a84] .el-table .el-table__header th{background:#fafafa;color:#606266;font-weight:600}[data-v-4b658a84] .el-table .el-table__body td{padding:12px 0}

1
dist/assets/dashboard.fd970744.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/assets/employee.896b22b2.js vendored Normal file
View File

@ -0,0 +1 @@
import{r as t}from"./request.92d2eb68.js";const p=async e=>t.get("/admin/employee/list",{params:e}),a=async e=>t.post("/admin/employee/add",e),n=async(e,o)=>t.post(`/admin/employee/topup/${e}`,o),m=async(e,o)=>t.post(`/admin/employee/deduct/${e}`,o);export{a,m as d,p as f,n as t};

1
dist/assets/employeeAdd.45227868.css vendored Normal file
View File

@ -0,0 +1 @@
.employee-add[data-v-6f7a1bc9]{padding:0}.card-header[data-v-6f7a1bc9]{display:flex;justify-content:space-between;align-items:center;font-size:16px;font-weight:600;color:#303133}.steps[data-v-6f7a1bc9]{margin:20px 0 40px}.step-content[data-v-6f7a1bc9]{min-height:300px;padding:20px 0}.add-form[data-v-6f7a1bc9]{width:500px;margin:0 auto}.preview-card[data-v-6f7a1bc9]{width:500px;margin:30px auto 0;background-color:#f8f9fa}.preview-content[data-v-6f7a1bc9]{padding:10px}.preview-item[data-v-6f7a1bc9]{margin-bottom:10px;display:flex;align-items:center}.preview-item .label[data-v-6f7a1bc9]{width:80px;color:#666;font-size:13px}.preview-item .value[data-v-6f7a1bc9]{color:#333;font-size:13px}.confirm-info[data-v-6f7a1bc9]{width:500px;margin:0 auto}.step-actions[data-v-6f7a1bc9]{margin-top:30px;text-align:center}.result[data-v-6f7a1bc9]{display:flex;justify-content:center}.result-info[data-v-6f7a1bc9]{margin:20px 0;text-align:left}.result-actions[data-v-6f7a1bc9]{margin-top:20px;display:flex;gap:10px;justify-content:center}[data-v-6f7a1bc9] .el-descriptions__label{width:120px}.title-center[data-v-6f7a1bc9]{text-align:center;font-weight:700;font-size:16px;margin-bottom:10px}

8
dist/assets/employeeAdd.53d2a71c.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/assets/employeeList.672857e3.css vendored Normal file
View File

@ -0,0 +1 @@
.employee-list[data-v-43c4e009]{padding:0}.card-header[data-v-43c4e009]{display:flex;justify-content:space-between;align-items:center;font-size:16px;font-weight:600;color:#303133}.search-form[data-v-43c4e009]{margin-bottom:20px;padding:20px;background-color:#f8f9fa;border-radius:4px}.stat-cards[data-v-43c4e009]{margin-bottom:20px}.stat-card[data-v-43c4e009]{text-align:center}.stat-item[data-v-43c4e009]{padding:10px}.stat-label[data-v-43c4e009]{font-size:14px;color:#909399;margin-bottom:8px}.stat-value[data-v-43c4e009]{font-size:24px;font-weight:700;color:#303133}.stat-value.success[data-v-43c4e009]{color:#67c23a}.stat-value.warning[data-v-43c4e009]{color:#e6a23c}.stat-value.info[data-v-43c4e009]{color:#409eff}.pagination[data-v-43c4e009]{margin-top:20px;display:flex;justify-content:flex-end}.points-warning[data-v-43c4e009]{color:#f56c6c;font-weight:700}.deduct-input[data-v-43c4e009] .el-input-number__decrease:hover,.deduct-input[data-v-43c4e009] .el-input-number__increase:hover{color:#fff;background-color:#f56c6c}.deduct-label[data-v-43c4e009] .el-form-item__label{color:#f56c6c;font-weight:700}

1
dist/assets/employeeList.70840dca.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
import{r as h,Z as E,b as s,$ as L,o as r,c as _,e as a,w as n,a0 as z,y as f,h as y,d as D,B as M,E as g,C as N}from"./index.a99ea9b6.js";import{r as $}from"./request.3edc855e.js";import"./axios.e318b91e.js";const q=c=>{const t=c==null?void 0:c.data;return t?Array.isArray(t)?{list:t,total:t.length}:{list:Array.isArray(t.list)?t.list:[],total:typeof t.total=="number"?t.total:Array.isArray(t.list)?t.list.length:0}:{list:[],total:0}},R=async()=>{const c=await $.get("/admin/user-type/list");return q(c)},S={key:1},Z={key:1},H={__name:"employeeType",setup(c){const t=h(!1),u=h([]),v=async()=>{t.value=!0;try{const o=await R();u.value=(o.list||[]).map(l=>({...l,_editing:!1}))}catch(o){console.error("\u83B7\u53D6\u5458\u5DE5\u7C7B\u578B\u5217\u8868\u5931\u8D25:",o)}finally{t.value=!1}},k=()=>{u.value.unshift({id:null,name:"",icon:"",check_status:!1,check_num:0,_editing:!0})},b=async o=>{o._editing=!1,o._backup=null,g.success("\u4FDD\u5B58\u6210\u529F")},V=(o,l)=>{if(!o.id){u.value.splice(l,1);return}N.confirm("\u786E\u8BA4\u5220\u9664\u8BE5\u5458\u5DE5\u7C7B\u578B\u5417\uFF1F","\u63D0\u793A",{type:"warning"}).then(()=>{u.value.splice(l,1),g.success("\u5220\u9664\u6210\u529F")}).catch(()=>{})};return E(()=>{v()}),(o,l)=>{const d=s("el-button"),w=s("el-image"),p=s("el-table-column"),C=s("el-input"),x=s("el-switch"),m=s("el-option"),T=s("el-select"),A=s("el-table"),B=L("loading");return r(),_("div",null,[a(d,{type:"primary",onClick:k,style:{"margin-bottom":"10px"}},{default:n(()=>[...l[0]||(l[0]=[y("\u65B0\u589E\u7C7B\u578B",-1)])]),_:1}),z((r(),f(A,{data:u.value,border:"",style:{width:"100%"}},{default:n(()=>[a(p,{prop:"icon",label:"\u56FE\u6807",width:"150",align:"center"},{default:n(({row:e})=>[e.icon?(r(),f(w,{key:0,src:e.icon,style:{width:"40px",height:"40px"}},{error:n(()=>[...l[1]||(l[1]=[D("span",null,"\u6682\u65E0\u56FE\u6807",-1)])]),_:1},8,["src"])):(r(),_("span",S,"\u6682\u65E0\u56FE\u6807"))]),_:1}),a(p,{prop:"name",label:"\u7C7B\u540D",width:"140",align:"center"},{default:n(({row:e})=>[e._editing?(r(),f(C,{key:0,modelValue:e.name,"onUpdate:modelValue":i=>e.name=i,placeholder:"\u8BF7\u8F93\u5165\u540D\u79F0"},null,8,["modelValue","onUpdate:modelValue"])):(r(),_("span",Z,M(e.name),1))]),_:1}),a(p,{prop:"check_status",label:"\u5F00\u542F\u8FC7\u671F\u65F6\u95F4\u6821\u9A8C","min-width":"160","show-overflow-tooltip":"",align:"center"},{default:n(({row:e})=>[a(x,{modelValue:e.check_status,"onUpdate:modelValue":i=>e.check_status=i,"active-color":"#13ce66","inactive-color":"#ff4949"},null,8,["modelValue","onUpdate:modelValue"])]),_:1}),a(p,{prop:"check_num",label:"\u673A\u68B0\u7801\u6821\u9A8C\u7B49\u7EA7","min-width":"200","show-overflow-tooltip":"",align:"center"},{default:n(({row:e})=>[a(T,{modelValue:e.check_num,"onUpdate:modelValue":i=>e.check_num=i,placeholder:"\u8BF7\u9009\u62E9\u673A\u68B0\u7801\u6821\u9A8C\u7B49\u7EA7"},{default:n(()=>[a(m,{label:"\u4E0D\u9650\u5236",value:0}),a(m,{label:"\u5355\u8BBE\u5907",value:1}),a(m,{label:"\u9501\u5B9A\u8BBE\u5907",value:2})]),_:1},8,["modelValue","onUpdate:modelValue"])]),_:1}),a(p,{label:"\u64CD\u4F5C",width:"200",align:"center"},{default:n(({row:e,$index:i})=>[a(d,{type:"success",size:"small",onClick:U=>b(e)},{default:n(()=>[...l[2]||(l[2]=[y("\u4FDD\u5B58",-1)])]),_:1},8,["onClick"]),a(d,{type:"danger",size:"small",onClick:U=>V(e,i)},{default:n(()=>[...l[3]||(l[3]=[y("\u5220\u9664",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[B,t.value]])])}}};export{H as default};

1
dist/assets/employeeType.c6cdd547.js vendored Normal file
View File

@ -0,0 +1 @@
import{r as g,$ as E,b as o,a0 as L,o as r,c as _,e as a,w as s,a1 as z,y as f,h as y,d as D,B as M,E as h,C as N}from"./index.887c1a2d.js";import{r as $}from"./request.92d2eb68.js";import"./axios.e318b91e.js";const q=i=>{const t=i==null?void 0:i.data;return t?Array.isArray(t)?{list:t,total:t.length}:{list:Array.isArray(t.list)?t.list:[],total:typeof t.total=="number"?t.total:Array.isArray(t.list)?t.list.length:0}:{list:[],total:0}},R=async()=>{const i=await $.get("/admin/user-type/list");return q(i)},S={key:1},j={key:1},I={__name:"employeeType",setup(i){const t=g(!1),u=g([]),v=async()=>{t.value=!0;try{const n=await R();u.value=(n.list||[]).map(l=>({...l,_editing:!1}))}catch(n){console.error("\u83B7\u53D6\u5458\u5DE5\u7C7B\u578B\u5217\u8868\u5931\u8D25:",n)}finally{t.value=!1}},k=()=>{u.value.unshift({id:null,name:"",icon:"",check_status:!1,check_num:0,_editing:!0})},b=async n=>{n._editing=!1,n._backup=null,h.success({message:"\u4FDD\u5B58\u6210\u529F",customClass:"scan-success-message"})},V=(n,l)=>{if(!n.id){u.value.splice(l,1);return}N.confirm("\u786E\u8BA4\u5220\u9664\u8BE5\u5458\u5DE5\u7C7B\u578B\u5417\uFF1F","\u63D0\u793A",{type:"warning"}).then(()=>{u.value.splice(l,1),h.success({message:"\u5220\u9664\u6210\u529F",customClass:"scan-success-message"})}).catch(()=>{})};return E(()=>{v()}),(n,l)=>{const p=o("el-button"),w=o("el-image"),m=o("el-table-column"),C=o("el-input"),x=o("el-switch"),d=o("el-option"),T=o("el-select"),A=o("el-table"),B=L("loading");return r(),_("div",null,[a(p,{type:"primary",onClick:k,style:{"margin-bottom":"10px"}},{default:s(()=>[...l[0]||(l[0]=[y("\u65B0\u589E\u7C7B\u578B",-1)])]),_:1}),z((r(),f(A,{data:u.value,border:"",style:{width:"100%"}},{default:s(()=>[a(m,{prop:"icon",label:"\u56FE\u6807",width:"150",align:"center"},{default:s(({row:e})=>[e.icon?(r(),f(w,{key:0,src:e.icon,style:{width:"40px",height:"40px"}},{error:s(()=>[...l[1]||(l[1]=[D("span",null,"\u6682\u65E0\u56FE\u6807",-1)])]),_:1},8,["src"])):(r(),_("span",S,"\u6682\u65E0\u56FE\u6807"))]),_:1}),a(m,{prop:"name",label:"\u7C7B\u540D",width:"140",align:"center"},{default:s(({row:e})=>[e._editing?(r(),f(C,{key:0,modelValue:e.name,"onUpdate:modelValue":c=>e.name=c,placeholder:"\u8BF7\u8F93\u5165\u540D\u79F0"},null,8,["modelValue","onUpdate:modelValue"])):(r(),_("span",j,M(e.name),1))]),_:1}),a(m,{prop:"check_status",label:"\u5F00\u542F\u8FC7\u671F\u65F6\u95F4\u6821\u9A8C","min-width":"160","show-overflow-tooltip":"",align:"center"},{default:s(({row:e})=>[a(x,{modelValue:e.check_status,"onUpdate:modelValue":c=>e.check_status=c,"active-color":"#13ce66","inactive-color":"#ff4949"},null,8,["modelValue","onUpdate:modelValue"])]),_:1}),a(m,{prop:"check_num",label:"\u673A\u68B0\u7801\u6821\u9A8C\u7B49\u7EA7","min-width":"200","show-overflow-tooltip":"",align:"center"},{default:s(({row:e})=>[a(T,{modelValue:e.check_num,"onUpdate:modelValue":c=>e.check_num=c,placeholder:"\u8BF7\u9009\u62E9\u673A\u68B0\u7801\u6821\u9A8C\u7B49\u7EA7"},{default:s(()=>[a(d,{label:"\u4E0D\u9650\u5236",value:0}),a(d,{label:"\u5355\u8BBE\u5907",value:1}),a(d,{label:"\u9501\u5B9A\u8BBE\u5907",value:2})]),_:1},8,["modelValue","onUpdate:modelValue"])]),_:1}),a(m,{label:"\u64CD\u4F5C",width:"200",align:"center"},{default:s(({row:e,$index:c})=>[a(p,{type:"success",size:"small",onClick:U=>b(e)},{default:s(()=>[...l[2]||(l[2]=[y("\u4FDD\u5B58",-1)])]),_:1},8,["onClick"]),a(p,{type:"danger",size:"small",onClick:U=>V(e,c)},{default:s(()=>[...l[3]||(l[3]=[y("\u5220\u9664",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[B,t.value]])])}}};export{I as default};

View File

@ -1 +0,0 @@
import{a7 as C,r as f,b as F,o as i,y as D,w as c,d as o,av as N,z as S,c as l,e as r,f as b,B as a,A as x,_ as $,am as P,aw as z,ax as I}from"./index.a99ea9b6.js";import{r as V}from"./request.3edc855e.js";const G={key:0,class:"popover-loading"},H={key:1,class:"popover-error"},M={key:2,class:"popover-content"},q={class:"popover-header"},T={class:"popover-isbn"},Y={key:0,class:"suit-badge"},j={class:"popover-body"},J={class:"popover-cover"},K=["src"],L={key:1,class:"cover-placeholder"},O={class:"popover-info"},Q={class:"info-row"},R={class:"info-value info-value-name"},U={class:"info-row"},W={class:"info-value"},X={class:"info-row"},Z={class:"info-value"},ss={class:"info-row"},os={class:"info-value"},es={class:"info-row"},ns={class:"info-value"},ts={class:"info-row"},as={class:"info-value info-value-price"},us={class:"info-row"},is={class:"info-value"},ls={class:"info-row"},rs={class:"info-value"},ds={key:3,class:"popover-empty"},cs=C({__name:"index",props:{isbn:{}},setup(_){const g=_,p=f(!1),d=f(null),n=f(null),v=new Map;function w(t){if(t==null||t==="")return"";const s=String(t);if(/^\d{4}-\d{2}-\d{2}$/.test(s))return s;if(/^\d{8}$/.test(s))return`${s.slice(0,4)}-${s.slice(4,6)}-${s.slice(6,8)}`;if(/^\d{4}-\d{2}$/.test(s))return`${s}-01`;const e=Number(t);if(!isNaN(e)&&e>1e4){const u=new Date(e*1e3);if(!isNaN(u.getTime()))return`${u.getFullYear()}-${String(u.getMonth()+1).padStart(2,"0")}-${String(u.getDate()).padStart(2,"0")}`}return s}function A(t){return t==null?"0.00":(t/100).toFixed(2)}async function k(t){if(v.has(t)){n.value=v.get(t);return}p.value=!0,d.value=null,n.value=null;try{const s=await V.get("/getBookInfo",{params:{isbn:t}}),e=s==null?void 0:s.data;if(!e){d.value="\u6570\u636E\u5E93\u4E2D\u6682\u65E0\u8BE5\u4E66\u6570\u636E";return}const u={bookName:e.book_name||"",author:e.author||"",publisher:e.publisher||"",publishDate:w(e.publication_time),binding:e.binding_layout||"",price:typeof e.fix_price=="number"?e.fix_price:0,pageCount:Number(e.page_count)||0,wordCount:Number(e.word_count)||0,book_pic:e.book_pic||void 0,isSuit:e.is_suit===1};v.set(t,u),n.value=u}catch(s){console.warn("[goodsPop] \u4E66\u7C4D\u4FE1\u606F\u67E5\u8BE2\u5931\u8D25:",s instanceof Error?s.message:String(s)),d.value="\u67E5\u8BE2\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5"}finally{p.value=!1}}function y(){!g.isbn||k(g.isbn)}function B(){}return(t,s)=>{const e=F("el-icon"),u=F("el-popover");return i(),D(u,{placement:"right-start",width:360,trigger:"hover","open-delay":500,"close-delay":100,disabled:!_.isbn,onShow:y,onHide:B},{reference:c(()=>[o("span",{class:S(["isbn-popover-trigger",{"is-loading":p.value}])},[N(t.$slots,"default",{},void 0,!0)],2)]),default:c(()=>{var h,m,E;return[p.value?(i(),l("div",G,[r(e,{class:"is-loading",size:24},{default:c(()=>[r(b(P))]),_:1}),s[0]||(s[0]=o("span",null,"\u6B63\u5728\u67E5\u8BE2\u4E66\u54C1\u4FE1\u606F...",-1))])):d.value?(i(),l("div",H,[r(e,{size:24,color:"#e6a23c"},{default:c(()=>[r(b(z))]),_:1}),o("span",null,a(d.value),1)])):n.value?(i(),l("div",M,[o("div",q,[o("span",T,a(_.isbn),1),n.value.isSuit?(i(),l("span",Y,"\u5957\u88C5\u4E66")):x("",!0)]),o("div",j,[o("div",J,[(h=n.value.book_pic)!=null&&h.pddPath?(i(),l("img",{key:0,src:n.value.book_pic.pddPath,alt:"\u5C01\u9762",class:"cover-image"},null,8,K)):(i(),l("div",L,[r(e,{size:32},{default:c(()=>[r(b(I))]),_:1})]))]),o("div",O,[o("div",Q,[s[1]||(s[1]=o("span",{class:"info-label"},"\u4E66\u540D\uFF1A",-1)),o("span",R,a(n.value.bookName||"\u672A\u77E5"),1)]),o("div",U,[s[2]||(s[2]=o("span",{class:"info-label"},"\u4F5C\u8005\uFF1A",-1)),o("span",W,a(n.value.author||"\u672A\u77E5"),1)]),o("div",X,[s[3]||(s[3]=o("span",{class:"info-label"},"\u51FA\u7248\u793E\uFF1A",-1)),o("span",Z,a(n.value.publisher||"\u672A\u77E5"),1)]),o("div",ss,[s[4]||(s[4]=o("span",{class:"info-label"},"\u51FA\u7248\u65F6\u95F4\uFF1A",-1)),o("span",os,a(n.value.publishDate||"\u672A\u77E5"),1)]),o("div",es,[s[5]||(s[5]=o("span",{class:"info-label"},"\u88C5\u5E27\uFF1A",-1)),o("span",ns,a(n.value.binding||"\u672A\u77E5"),1)]),o("div",ts,[s[6]||(s[6]=o("span",{class:"info-label"},"\u5B9A\u4EF7\uFF1A",-1)),o("span",as,"\xA5"+a(A(n.value.price)),1)]),o("div",us,[s[7]||(s[7]=o("span",{class:"info-label"},"\u9875\u6570\uFF1A",-1)),o("span",is,a((m=n.value.pageCount)!=null?m:"\u672A\u77E5"),1)]),o("div",ls,[s[8]||(s[8]=o("span",{class:"info-label"},"\u5B57\u6570\uFF1A",-1)),o("span",rs,a((E=n.value.wordCount)!=null?E:"\u672A\u77E5"),1)])])])])):(i(),l("div",ds,[...s[9]||(s[9]=[o("span",null,"\u6682\u65E0ISBN",-1)])]))]}),_:3},8,["disabled"])}}});var vs=$(cs,[["__scopeId","data-v-3c0e5793"]]);export{vs as G};

View File

@ -1 +0,0 @@
.logistics-manager[data-v-8c666032]{padding:16px}.filter-bar[data-v-8c666032]{display:flex;align-items:center;gap:12px;margin-bottom:16px;flex-wrap:wrap}.pagination-wrapper[data-v-8c666032]{display:flex;justify-content:flex-end;margin-top:16px}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
.isbn-popover-trigger[data-v-3c0e5793]{cursor:pointer;border-bottom:1px dashed #409eff;transition:all .2s}.isbn-popover-trigger[data-v-3c0e5793]:hover{color:#409eff}.isbn-popover-trigger.is-loading[data-v-3c0e5793]{opacity:.7}.popover-loading[data-v-3c0e5793]{display:flex;align-items:center;gap:8px;padding:20px;justify-content:center;color:#909399;font-size:13px}.popover-loading .is-loading[data-v-3c0e5793]{animation:rotating-3c0e5793 1.5s linear infinite}@keyframes rotating-3c0e5793{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.popover-error[data-v-3c0e5793]{display:flex;align-items:center;gap:8px;padding:20px;justify-content:center;color:#e6a23c;font-size:13px}.popover-empty[data-v-3c0e5793]{padding:20px;text-align:center;color:#c0c4cc;font-size:13px}.popover-content[data-v-3c0e5793]{padding:4px}.popover-header[data-v-3c0e5793]{display:flex;align-items:center;gap:8px;margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid #ebeef5}.popover-isbn[data-v-3c0e5793]{font-size:13px;font-weight:600;color:#303133;font-family:Courier New,monospace}.suit-badge[data-v-3c0e5793]{display:inline-block;padding:1px 8px;font-size:11px;color:#e6a23c;background:#fdf6ec;border:1px solid #f5dab1;border-radius:4px;line-height:1.6}.popover-body[data-v-3c0e5793]{display:flex;gap:14px}.popover-cover[data-v-3c0e5793]{flex-shrink:0;width:90px;height:120px;border-radius:4px;overflow:hidden;border:1px solid #ebeef5}.cover-image[data-v-3c0e5793]{width:100%;height:100%;object-fit:cover}.cover-placeholder[data-v-3c0e5793]{width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:#f5f7fa;color:#c0c4cc}.popover-info[data-v-3c0e5793]{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}.info-row[data-v-3c0e5793]{display:flex;font-size:12px;line-height:1.6}.info-label[data-v-3c0e5793]{flex-shrink:0;color:#909399;width:56px;text-align:right;margin-right:4px}.info-value[data-v-3c0e5793]{flex:1;color:#303133;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.info-value-name[data-v-3c0e5793]{font-weight:600;color:#409eff}.info-value-price[data-v-3c0e5793]{color:#f56c6c;font-weight:600}

File diff suppressed because one or more lines are too long

1
dist/assets/index.902e5a28.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/assets/index.fdfed103.css vendored Normal file
View File

@ -0,0 +1 @@
.isbn-popover-trigger[data-v-0c73fea4]{cursor:pointer;border-bottom:1px dashed #409eff;transition:all .2s}.isbn-popover-trigger[data-v-0c73fea4]:hover{color:#409eff}.isbn-popover-trigger.is-loading[data-v-0c73fea4]{opacity:.7}.popover-loading[data-v-0c73fea4]{display:flex;align-items:center;gap:8px;padding:20px;justify-content:center;color:#909399;font-size:13px}.popover-loading .is-loading[data-v-0c73fea4]{animation:rotating-0c73fea4 1.5s linear infinite}@keyframes rotating-0c73fea4{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.popover-error[data-v-0c73fea4]{display:flex;align-items:center;gap:8px;padding:20px;justify-content:center;color:#e6a23c;font-size:13px}.popover-empty[data-v-0c73fea4]{padding:20px;text-align:center;color:#c0c4cc;font-size:13px}.popover-content[data-v-0c73fea4]{padding:4px}.popover-header[data-v-0c73fea4]{display:flex;align-items:center;gap:8px;margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid #ebeef5}.popover-isbn[data-v-0c73fea4]{font-size:13px;font-weight:600;color:#303133;font-family:Courier New,monospace}.suit-badge[data-v-0c73fea4]{display:inline-block;padding:1px 8px;font-size:11px;color:#e6a23c;background:#fdf6ec;border:1px solid #f5dab1;border-radius:4px;line-height:1.6}.popover-body[data-v-0c73fea4]{display:flex;gap:14px}.popover-cover[data-v-0c73fea4]{flex-shrink:0;width:90px;height:120px;border-radius:4px;overflow:hidden;border:1px solid #ebeef5}.cover-image[data-v-0c73fea4]{width:100%;height:100%;object-fit:cover}.cover-placeholder[data-v-0c73fea4]{width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:#f5f7fa;color:#c0c4cc}.popover-info[data-v-0c73fea4]{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}.info-row[data-v-0c73fea4]{display:flex;font-size:12px;line-height:1.6}.info-label[data-v-0c73fea4]{flex-shrink:0;color:#909399;width:56px;text-align:right;margin-right:4px}.info-value[data-v-0c73fea4]{flex:1;color:#303133;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.info-value-name[data-v-0c73fea4]{font-weight:600;color:#409eff}.info-value-price[data-v-0c73fea4]{color:#f56c6c;font-weight:600}

1
dist/assets/inventory.4310c6c2.css vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/assets/inventory.46efe4da.js vendored Normal file
View File

@ -0,0 +1 @@
import{r as e}from"./request.92d2eb68.js";const n="/inventory",a=r=>{const t=r==null?void 0:r.data;return t?Array.isArray(t)?{list:t,total:t.length}:Array.isArray(t.list)?{list:t.list,total:typeof t.total=="number"?t.total:t.list.length}:typeof t=="object"?{list:[t],total:1}:{list:[],total:0}:{list:[],total:0}},c=async r=>{const t=await e.get(`${n}/list`,{params:r});return a(t)},y=async r=>{const t=await e.get(`${n}/detail`,{params:r}),s=t==null?void 0:t.data;return s?Array.isArray(s)?{list:s,total:s.length}:Array.isArray(s.list)?{list:s.list,total:typeof s.total=="number"?s.total:s.list.length}:{list:[],total:0}:{list:[],total:0}},u=async r=>{const t=await e.get(`${n}/log/list`,{params:r});return a(t)},f=async({keyword:r,page:t,pageSize:s}={})=>{const i={keyword:r||void 0,page:t,page_size:s},o=await e.get(`${n}/grouped-list`,{params:i});return a(o)},g=async()=>await e.get(`${n}/summary`);export{y as a,f as b,u as c,c as f,g as i};

1
dist/assets/inventory.57d7e7aa.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/assets/location.5e20e62a.js vendored Normal file
View File

@ -0,0 +1 @@
import{r as n}from"./request.92d2eb68.js";const r="/location",c=t=>{const e=t==null?void 0:t.data;return e?Array.isArray(e)?{list:e,total:e.length}:{list:Array.isArray(e.list)?e.list:[],total:typeof e.total=="number"?e.total:Array.isArray(e.list)?e.list.length:0}:{list:[],total:0}},f=async({keyword:t,warehouseId:e,type:a,status:s,page:i,pageSize:o})=>{const d={code:t||void 0,warehouse_id:e||void 0,type:a,status:s,page:i,page_size:o},u=await n.get(`${r}/list`,{params:d});return c(u)},l=async t=>{const e=await n.get(`${r}/detail/${t}`);return(e==null?void 0:e.data)||null},m=async t=>n.post(`${r}/create`,t),y=async t=>n.put(`${r}/update`,t),A=async t=>{const e=new FormData;return Array.isArray(t.ids)&&t.ids.forEach((a,s)=>{e.append(`ids[${s}]`,a)}),n.post(`${r}/delete`,e)},h=async t=>{const e=new FormData;return Array.isArray(t.ids)&&t.ids.forEach((a,s)=>{e.append(`ids[${s}]`,a)}),t.code!==void 0&&e.append("code",t.code),t.type!==void 0&&e.append("type",t.type),t.capacity!==void 0&&e.append("capacity",t.capacity),t.status!==void 0&&e.append("status",t.status),n.put(`${r}/update`,e)},$=async t=>n.post(`${r}/batch-generate`,t),L=async({user_id:t,warehouse_id:e,file:a})=>{const s=new FormData;return s.append("user_id",t),s.append("warehouse_id",e),s.append("file",a),n.post(`${r}/import-from-excel`,s)},g=async({page:t,pageSize:e,keyword:a}={})=>{const s={page:t,page_size:e,code:a||void 0},i=await n.get(`${r}/all-list`,{params:s});return c(i)};export{$ as a,h as b,m as c,A as d,f as e,l as f,g,L as i,y as u};

1
dist/assets/location.7eeab00a.css vendored Normal file
View File

@ -0,0 +1 @@
.location-manager[data-v-565b3e0e]{padding:20px;background-color:#f5f7fa;min-height:100vh}.filter-bar[data-v-565b3e0e]{display:flex;flex-wrap:wrap;gap:12px;margin-bottom:20px;align-items:center;background:white;padding:16px 20px;border-radius:8px;box-shadow:0 1px 4px #0000000d}.pagination-wrapper[data-v-565b3e0e]{margin-top:20px;display:flex;justify-content:flex-end;background:white;padding:14px 20px;border-radius:8px}[data-v-565b3e0e] .el-table{border-radius:8px;overflow:hidden;box-shadow:0 1px 4px #0000000d}[data-v-565b3e0e] .el-dialog__body{padding-top:16px}.dialog-footer[data-v-565b3e0e]{display:flex;justify-content:flex-end;gap:12px}

View File

@ -1 +0,0 @@
import{r as s}from"./request.3edc855e.js";const n="/location",d=t=>{const e=t==null?void 0:t.data;return e?Array.isArray(e)?{list:e,total:e.length}:{list:Array.isArray(e.list)?e.list:[],total:typeof e.total=="number"?e.total:Array.isArray(e.list)?e.list.length:0}:{list:[],total:0}},p=async({keyword:t,warehouseId:e,type:a,status:r,page:i,pageSize:c})=>{const o={code:t||void 0,warehouse_id:e||void 0,type:a,status:r,page:i,page_size:c},u=await s.get(`${n}/list`,{params:o});return d(u)},A=async t=>{const e=await s.get(`${n}/detail/${t}`);return(e==null?void 0:e.data)||null},y=async t=>s.post(`${n}/create`,t),h=async t=>s.put(`${n}/update`,t),$=async t=>{const e=new FormData;return Array.isArray(t.ids)&&t.ids.forEach((a,r)=>{e.append(`ids[${r}]`,a)}),s.post(`${n}/delete`,e)},l=async t=>{const e=new FormData;return Array.isArray(t.ids)&&t.ids.forEach((a,r)=>{e.append(`ids[${r}]`,a)}),t.code!==void 0&&e.append("code",t.code),t.type!==void 0&&e.append("type",t.type),t.capacity!==void 0&&e.append("capacity",t.capacity),t.status!==void 0&&e.append("status",t.status),s.put(`${n}/update`,e)},m=async t=>s.post(`${n}/batch-generate`,t);export{m as a,l as b,y as c,$ as d,p as e,A as f,h as u};

10
dist/assets/location.c9b9c86c.js vendored Normal file

File diff suppressed because one or more lines are too long

79
dist/assets/locationManager.3fdb9a7d.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.filter-bar[data-v-257183e8]{display:flex;flex-wrap:wrap;gap:12px;margin-bottom:20px;align-items:center;background:white;padding:16px 20px;border-radius:8px;box-shadow:0 1px 4px #0000000d}.pagination-wrapper[data-v-257183e8]{margin-top:20px;display:flex;justify-content:flex-end;background:white;padding:14px 20px;border-radius:8px}.location-empty[data-v-257183e8]{padding:40px 0;text-align:center;color:#999;background:#fafafa;border-radius:8px}[data-v-257183e8] .el-table{border-radius:8px;overflow:hidden;box-shadow:0 1px 4px #0000000d}[data-v-257183e8] .el-dialog__body{padding-top:16px}.dialog-footer[data-v-257183e8]{display:flex;justify-content:flex-end;gap:12px}.location-dialog[data-v-257183e8] .el-dialog__header{background:linear-gradient(90deg,#4b8afc,#6db6ff);border-bottom:none;color:#fff;border-radius:12px 12px 0 0}.location-dialog[data-v-257183e8] .el-dialog__title{color:#fff;font-size:18px;font-weight:600}.location-dialog[data-v-257183e8] .el-dialog__body{padding:24px 32px;background:#f6fbff}.location-dialog[data-v-257183e8] .el-form{background:#ffffff;border-radius:16px;padding:16px;box-shadow:inset 0 0 0 1px #3454d114}.location-dialog[data-v-257183e8] .el-form-item{margin-bottom:20px}.location-dialog[data-v-257183e8] .el-form-item__label{color:#4b5563;font-weight:520}.location-dialog[data-v-257183e8] .el-input,.location-dialog[data-v-257183e8] .el-input-number,.location-dialog[data-v-257183e8] .el-radio-group,.location-dialog[data-v-257183e8] .el-switch{border-radius:10px}.location-dialog[data-v-257183e8] .el-radio-group{gap:12px;display:flex;flex-wrap:wrap}.location-dialog[data-v-257183e8] .el-dialog__footer{padding:18px 32px 24px;background:#ffffff}.batch-code-range-header[data-v-257183e8]{display:flex;align-items:center;flex-wrap:wrap;gap:10px;margin-bottom:10px;border-radius:8px;color:#000000a6;font-size:12px;font-weight:600}.range-btn[data-v-257183e8]{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;padding:0;border-radius:50%;border:1px solid rgba(16,24,40,.08);background:#ffffff;color:#333;transition:transform .12s ease,box-shadow .12s ease,background .12s ease;box-shadow:0 1px 2px #10182814}.range-btn[data-v-257183e8]:hover{transform:translateY(-1px);box-shadow:0 4px 8px #1018281f}.range-btn-add[data-v-257183e8]{border-color:#1677ff29;color:#176cff}.range-btn-add[data-v-257183e8]:hover{background:#eff5ff}.range-btn-remove[data-v-257183e8]{border-color:#ff4d4f29;color:#ff4d4f}.range-btn-remove[data-v-257183e8]:hover{background:#fff2f2}.detail-loading[data-v-257183e8]{display:flex;align-items:center;justify-content:center;gap:8px;padding:40px 0;color:#409eff}.detail-content[data-v-257183e8]{padding:0 10px}.detail-section[data-v-257183e8]{margin-bottom:24px}.detail-row[data-v-257183e8]{display:flex;padding:8px 0;border-bottom:1px solid #f0f0f0}.detail-row[data-v-257183e8]:last-child{border-bottom:none}.detail-label[data-v-257183e8]{color:#909399;min-width:80px}.detail-code[data-v-257183e8]{font-weight:600;color:#409eff}.status-enabled[data-v-257183e8]{color:#67c23a}.status-disabled[data-v-257183e8]{color:#f56c6c}.barcode-section[data-v-257183e8]{margin-top:20px;padding-top:20px;border-top:1px solid #e4e7ed}.barcode-title[data-v-257183e8]{font-size:14px;font-weight:600;color:#303133;margin-bottom:16px;text-align:center}.barcode-loading[data-v-257183e8]{display:flex;align-items:center;justify-content:center;gap:8px;padding:30px 0;color:#409eff}.barcode-image-wrapper[data-v-257183e8]{display:flex;flex-direction:column;align-items:center;gap:8px}.barcode-image[data-v-257183e8]{max-width:100%;height:auto;border:1px solid #e4e7ed;border-radius:4px}.barcode-content-text[data-v-257183e8]{font-size:14px;font-weight:600;color:#606266;font-family:Courier New,monospace}.barcode-empty[data-v-257183e8]{text-align:center;color:#c0c4cc;padding:30px 0}.search-item[data-v-257183e8]{display:flex;align-items:center;gap:8px}.label-with-icon[data-v-257183e8]{display:inline-flex;align-items:center;gap:4px}

Some files were not shown because too many files have changed in this diff Show More