更新项目管理模块中合同管理、项目安装、项目验收、回访记录等功能
This commit is contained in:
1 parent
307e9cf1b9
commit
b3cef22a54
32 files changed
+5920
-2426
No files matched your search
@@ -0,0 +1,31 @@
|
||||
# Checklist
|
||||
|
||||
## 后端:安装派单
|
||||
- [x] `install_order` entity 字段与 db_design.html D6 完全一致,含注释与索引
|
||||
- [x] `POST /api/install-orders` 创建成功并自动生成唯一 install_no,返回统一结构
|
||||
- [x] `GET /api/install-orders` 支持关键字/客户/状态/日期范围筛选与分页
|
||||
- [x] `PUT /api/install-orders/:id/complete` 仅允许 0→1 流转,重复调用被拒绝
|
||||
- [x] 非法入参(缺必填/超长/非法枚举)返回 422
|
||||
|
||||
## 后端:安全
|
||||
- [x] 六个项目管理控制器全部端点:无 JWT 返回 401,无权限点返回 403
|
||||
- [x] `sys_menu` 存在 `project:install` 权限点且 RBAC 匹配生效
|
||||
- [x] 六个 service 无字符串拼接 SQL,模糊搜索全部绑定参数,排序字段白名单
|
||||
- [x] 六个 DTO 对全部外部入参有 class-validator 校验
|
||||
- [x] 创建/更新/删除/指派/流转/取消/复核/催办动作均写入 sys_operation_log
|
||||
|
||||
## 前端:页面
|
||||
- [x] 勘察台账页含统计卡、状态Tab、高级筛选、台账列、批量指派/催办入口,数据与接口联通
|
||||
- [x] 勘察派单页含报价联动、优先级、到场窗口、草稿/派发,提交与接口联通
|
||||
- [x] 方案页含设备明细编辑,合同页含金额/支付周期/文件,验收页含关联合同,均为拜访台账式交互
|
||||
- [x] 安装派单页 CRUD + 标记完成可用,路由 `/project/install` 与菜单项生效
|
||||
- [x] 回访记录页列表/新建/编辑可用
|
||||
|
||||
## 前端:安全与兼容
|
||||
- [x] `views/project/**` 无 `v-html` 渲染后端文本
|
||||
- [x] 401 自动跳登录,403/422 有统一提示
|
||||
- [x] 所有列表接口响应为 `{ code, message, data: { list, total, page, pageSize } }`
|
||||
|
||||
## 构建
|
||||
- [x] `cd client && npx vue-tsc --noEmit` 通过(vue-tsc 无类型错误)
|
||||
- [x] `cd server && npx tsc --noEmit` 通过(tsc 无类型错误)
|
||||
@@ -0,0 +1,70 @@
|
||||
# 项目管理模块全栈重建 Spec
|
||||
|
||||
## Why
|
||||
|
||||
项目管理模块(勘察派单/方案/合同/安装/验收/回访)后端代码已存在但前端页面未按系统设计稿实现,安装派单(install_order)全栈缺失;同时需要系统性补齐接口鉴权、SQL 注入防护、前端 XSS 防护与操作日志,并保证同一套 API 可被 APP 直接复用。
|
||||
|
||||
## What Changes
|
||||
|
||||
- **前端重建**:按设计稿重建勘察台账页(参考 `系统设计/UI/项目勘察台帐/code.html`)与勘察派单页(参考 `系统设计/UI/项目勘察派单/code.html`);方案/合同/验收/安装/回访页按客户拜访管理台账(`client/src/views/customer/visit/index.vue`)的交互与样式体系重建。
|
||||
- **安装派单全栈新增**:install_order 的 entity / dto / service / controller / 前端台账页 / API / 路由,功能深度为台账式完整 CRUD + 状态流转(0已派发 → 1已完成)。
|
||||
- **安全加固**:
|
||||
- 接口鉴权补全:项目管理所有接口统一走 JWT 认证 + RBAC 权限点(复用现有 `RbacMiddleware`,按 path 匹配 permission code)。
|
||||
- SQL 注入防护审查:全部查询走 Sequelize 参数化,禁止字符串拼接 SQL;DTO 层用 class-validator 全量校验入参。
|
||||
- 前端 XSS 防护:用户输入的备注/说明/意见等文本字段渲染时禁止 `v-html`,统一文本插值;富文本场景如需 HTML 必须经白名单消毒。
|
||||
- 操作日志埋点:创建/更新/删除/指派/状态流转/取消/复核等关键动作写入 `sys_operation_log`。
|
||||
- **APP 兼容**:不新增命名空间,Web 与 APP 共用现有 `/api/project-*` 系列接口;统一响应结构 `{ code, message, data }`,分页结构统一为 `{ list, total, page, pageSize }`;字段命名与时间格式(ISO 8601)保持稳定。
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: 项目管理(新建)、系统安全(加固)
|
||||
- Affected code:
|
||||
- server:`src/entity/install_order.entity.ts`(新增)、`src/dto/install_order.dto.ts`(新增)、`src/service/install_order.service.ts`(新增)、`src/controller/install_order.controller.ts`(新增)、既有 project_plan / contract / survey_order / acceptance / visit_record 的 controller/service(安全审查与日志埋点)
|
||||
- client:`src/views/project/**`(全部重建)、`src/api/installOrder.ts`(新增)、`src/router/index.ts`(补安装派单路由)、`src/layouts/index.vue`(菜单补项)
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 安装派单管理(install_order 全栈)
|
||||
系统 SHALL 提供安装派单的台账式完整 CRUD 与状态流转:列表分页筛选(关键字/客户/状态/派单日期范围)、新建、编辑、详情、删除、标记完成。单号自动生成(格式与勘察单号规则一致,如 `WH-INST-YYYY-NNNN`)。
|
||||
|
||||
#### Scenario: 创建安装派单
|
||||
- **WHEN** 授权用户提交合法派单表单(customerId、planId、installer、dispatchDate 必填)
|
||||
- **THEN** 系统生成唯一 install_no,状态为 0(已派发),写入操作日志,返回统一成功响应
|
||||
|
||||
#### Scenario: 状态流转
|
||||
- **WHEN** 授权用户对已派发工单执行「标记完成」
|
||||
- **THEN** 状态从 0 流转为 1,记录完成时间,写入操作日志;已完成工单禁止再次流转
|
||||
|
||||
### Requirement: 勘察台账页按设计稿重建
|
||||
系统 SHALL 按 `系统设计/UI/项目勘察台帐/code.html` 重建勘察台账页:统计卡片(待指派/勘察中等待复核/已归档)、状态筛选 Tab、高级筛选(优先级/业务员/电工/时间窗口)、列表列(工单编号+优先级、关联客户与项目、空间位置、测绘回路规格、责任业务员、指派电工、期望时效/SLA、预估劳务费、状态、操作)、批量指派、催办、智能派单入口。
|
||||
|
||||
#### Scenario: 台账筛选
|
||||
- **WHEN** 用户组合使用状态 Tab 与高级筛选条件
|
||||
- **THEN** 列表与统计卡片按筛选结果刷新,分页保持统一响应结构
|
||||
|
||||
### Requirement: 勘察派单页按设计稿重建
|
||||
系统 SHALL 按 `系统设计/UI/项目勘察派单/code.html` 重建派单表单页:关联客户选择、任务标题、空间位置、回路规格/数量、优先级(特急P1/加急P2/常规P3)、计划到场窗口、SLA、预估劳务费自动报价(复用 `/api/survey-orders/quote`)、指派电工、草稿保存与正式派发。
|
||||
|
||||
### Requirement: 方案/合同/验收/回访页重建
|
||||
系统 SHALL 按客户拜访台账的交互体系(筛选区 + 表格 + 分页 + 新建/编辑弹窗或表单页 + 状态标签)重建项目方案(含设备明细)、合同(含金额/支付周期/合同文件)、项目验收(含验收意见/文件)、回访记录四个台账页。
|
||||
|
||||
### Requirement: 接口鉴权补全
|
||||
系统 SHALL 保证 `/api/survey-orders`、`/api/project-plans`、`/api/contracts`、`/api/install-orders`、`/api/acceptances`、`/api/visit-records` 全部端点强制 JWT 认证并校验对应 RBAC 权限点(`project:survey`、`project:plan`、`project:contract`、`project:install`、`project:acceptance`、`project:visit`)。
|
||||
|
||||
#### Scenario: 未认证访问被拒
|
||||
- **WHEN** 请求未携带有效 JWT
|
||||
- **THEN** 返回 401 统一错误结构;无权限点返回 403
|
||||
|
||||
### Requirement: SQL 注入防护审查
|
||||
系统 SHALL 保证项目管理模块所有数据库访问经由 Sequelize 参数化;关键字模糊搜索使用绑定参数的 `Op.like`,排序字段白名单校验,禁止任何字符串拼接 SQL。
|
||||
|
||||
### Requirement: 前端 XSS 防护
|
||||
系统 SHALL 禁止对后端返回的用户输入文本使用 `v-html`;表格/详情中的备注、说明、意见字段使用文本插值渲染。
|
||||
|
||||
### Requirement: 操作日志埋点
|
||||
系统 SHALL 在创建/更新/删除/指派/批量指派/状态流转/取消/复核/催办动作中写入 `sys_operation_log`(操作人、动作、目标单号、摘要)。
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 统一响应结构(APP 兼容)
|
||||
项目管理全部接口 SHALL 返回 `{ code, message, data }`;列表 data 统一为 `{ list, total, page, pageSize }`;时间字段序列化为 ISO 8601 字符串;错误码语义稳定(401 未认证 / 403 无权限 / 422 参数校验失败 / 500 服务器错误),供 APP 端直接消费。
|
||||
@@ -0,0 +1,37 @@
|
||||
# Tasks
|
||||
|
||||
## 后端:安装派单(新增)
|
||||
- [x] Task 1: 新增 install_order 后端全栈
|
||||
- [x] 1.1 创建 `src/entity/install_order.entity.ts`(字段对齐 db_design.html D6:install_no/customer_id/installer/worker_id/dispatch_date/start_date/end_date/plan_id/remark/status/created_at)
|
||||
- [x] 1.2 创建 `src/dto/install_order.dto.ts`(class-validator:QueryDTO/CreateDTO/UpdateDTO,必填校验、长度限制、状态枚举校验)
|
||||
- [x] 1.3 创建 `src/service/install_order.service.ts`(list 分页+筛选、detail、create 生成单号、update、delete、complete 状态流转;全程参数化查询)
|
||||
- [x] 1.4 创建 `src/controller/install_order.controller.ts`(`/api/install-orders`:GET /、GET /:id、POST /、PUT /:id、PUT /:id/complete、DELETE /:id)
|
||||
|
||||
## 后端:安全加固(可并行)
|
||||
- [x] Task 2: 接口鉴权补全
|
||||
- [x] 2.1 核对六个项目管理控制器的 JWT + RbacMiddleware 挂载,确认 path→permission code 映射覆盖(project:survey/plan/contract/install/acceptance/visit)
|
||||
- [x] 2.2 在 `sys_menu` 种子/迁移中补齐 `project:install` 权限点
|
||||
- [x] Task 3: SQL 注入防护审查
|
||||
- [x] 3.1 审查六个 service,确认无字符串拼接 SQL、模糊搜索均为绑定参数 `Op.like`、排序字段白名单
|
||||
- [x] 3.2 审查六个 DTO,确认所有外部入参均有 class-validator 校验(类型/长度/枚举/范围)
|
||||
- [x] Task 4: 操作日志埋点
|
||||
- [x] 4.1 封装/复用操作日志写入方法(写 sys_operation_log:操作人、模块、动作、目标单号、摘要)
|
||||
- [x] 4.2 在六个 service 的 create/update/delete/assign/batchAssign/状态流转/cancel/review/urge/complete 处埋点
|
||||
|
||||
## 前端:页面重建
|
||||
- [x] Task 5: 重建勘察台账页 `views/project/survey/index.vue`(对齐 `系统设计/UI/项目勘察台帐/code.html`:统计卡、状态Tab、高级筛选、台账列、批量指派/催办/智能派单入口)
|
||||
- [x] Task 6: 重建勘察派单页 `views/project/survey/form.vue`(对齐 `系统设计/UI/项目勘察派单/code.html`:客户选择、回路规格/数量、优先级、到场窗口、SLA、劳务费报价联动、草稿/派发)
|
||||
- [x] Task 7: 重建方案管理页 `views/project/plan/index.vue`(拜访台账式:筛选+表格+分页+新建/编辑含设备明细+状态标签)
|
||||
- [x] Task 8: 重建合同管理页 `views/project/contract/index.vue`(筛选+表格+新建/编辑含金额/支付周期/合同文件上传+状态标签)
|
||||
- [x] Task 9: 重建项目验收页 `views/project/acceptance/index.vue`(筛选+表格+新建/编辑含验收意见/文件+关联合同)
|
||||
- [x] Task 10: 新增安装派单页 `views/project/install/index.vue` + `src/api/installOrder.ts` + 路由 `/project/install` + 布局菜单项(台账式 CRUD + 标记完成)
|
||||
- [x] Task 11: 重建回访记录页 `views/project/visit/index.vue` + 补齐 `src/api/visitRecord.ts`(如缺失)
|
||||
- [x] Task 12: 前端 XSS 防护审查
|
||||
- [x] 12.1 全量检查 `views/project/**` 无 `v-html` 渲染后端文本;备注/说明/意见统一文本插值
|
||||
- [x] 12.2 确认 axios 拦截器对 401 跳登录、403/422 统一提示,响应结构与 APP 兼容约定一致
|
||||
|
||||
## Task Dependencies
|
||||
- Task 5、6 依赖后端 survey_order 既有接口(不阻塞,可与 Task 1-4 并行)
|
||||
- Task 10 依赖 Task 1(安装派单后端先行)
|
||||
- Task 12 依赖 Task 5-11(在页面重建完成后统一审查)
|
||||
- Task 4 依赖 Task 1(install service 一并埋点)
|
||||
@@ -18,6 +18,8 @@ export interface AcceptanceQuery {
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
customerId?: number;
|
||||
acceptStartDate?: string;
|
||||
acceptEndDate?: string;
|
||||
}
|
||||
|
||||
export interface AcceptanceListResult {
|
||||
|
||||
@@ -2,11 +2,13 @@ import request from '@/utils/request';
|
||||
|
||||
export interface ContractInfo {
|
||||
id: number;
|
||||
contractNo: string;
|
||||
customerId: number;
|
||||
xtContractNo?: string;
|
||||
contractAmount?: number;
|
||||
payCycle?: string;
|
||||
salesmanId?: number;
|
||||
salesmanName?: string;
|
||||
signDate?: string;
|
||||
contractDesc?: string;
|
||||
startDate?: string;
|
||||
@@ -24,6 +26,8 @@ export interface ContractQuery {
|
||||
customerId?: number;
|
||||
salesmanId?: number;
|
||||
status?: number;
|
||||
signStartDate?: string;
|
||||
signEndDate?: string;
|
||||
}
|
||||
|
||||
export interface ContractListResult {
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
// 安装派单状态:0已派发 1已完成
|
||||
export const INSTALL_STATUS_OPTIONS = [
|
||||
{ value: 0, label: '已派发' },
|
||||
{ value: 1, label: '已完成' }
|
||||
];
|
||||
|
||||
export const INSTALL_STATUS = {
|
||||
DISPATCHED: 0,
|
||||
COMPLETED: 1
|
||||
};
|
||||
|
||||
// 关联客户精简字段(列表 decorate 附带)
|
||||
export interface InstallOrderCustomer {
|
||||
id: number;
|
||||
customerNo?: string;
|
||||
customerName?: string;
|
||||
contactPerson?: string;
|
||||
contactPhone?: string;
|
||||
address?: string;
|
||||
}
|
||||
|
||||
// 共享电工精简字段
|
||||
export interface InstallOrderWorker {
|
||||
id: number;
|
||||
workerName?: string;
|
||||
phone?: string;
|
||||
workerTitle?: string;
|
||||
}
|
||||
|
||||
// 关联方案精简字段
|
||||
export interface InstallOrderPlan {
|
||||
id: number;
|
||||
planNo?: string;
|
||||
planDesc?: string;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export interface InstallOrderInfo {
|
||||
id: number;
|
||||
installNo: string;
|
||||
customerId: number;
|
||||
installer?: string;
|
||||
workerId?: number;
|
||||
dispatchDate?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
planId: number;
|
||||
remark?: string;
|
||||
status: number;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
// 关联信息(列表/详情 decorate 附带)
|
||||
customer?: InstallOrderCustomer;
|
||||
worker?: InstallOrderWorker;
|
||||
plan?: InstallOrderPlan;
|
||||
customerName?: string;
|
||||
workerName?: string;
|
||||
planNo?: string;
|
||||
}
|
||||
|
||||
export interface InstallOrderQuery {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
customerId?: number;
|
||||
status?: number;
|
||||
// 派单日期范围,逗号分隔字符串 [startDate,endDate],后端 split 解析
|
||||
dateRange?: string;
|
||||
}
|
||||
|
||||
export interface InstallOrderListResult {
|
||||
list: InstallOrderInfo[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface InstallOrderCreateParams {
|
||||
customerId: number;
|
||||
installer?: string;
|
||||
workerId?: number;
|
||||
dispatchDate?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
planId: number;
|
||||
remark?: string;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export type InstallOrderUpdateParams = Partial<InstallOrderCreateParams>;
|
||||
|
||||
export const installOrderApi = {
|
||||
getList: (params: InstallOrderQuery) =>
|
||||
request.get<InstallOrderListResult>('/install-orders', { params }),
|
||||
|
||||
getDetail: (id: number) =>
|
||||
request.get<InstallOrderInfo>(`/install-orders/${id}`),
|
||||
|
||||
create: (data: InstallOrderCreateParams) =>
|
||||
request.post<InstallOrderInfo>('/install-orders', data),
|
||||
|
||||
update: (id: number, data: InstallOrderUpdateParams) =>
|
||||
request.put<InstallOrderInfo>(`/install-orders/${id}`, data),
|
||||
|
||||
// 标记完成:0已派发 → 1已完成
|
||||
complete: (id: number) =>
|
||||
request.put<InstallOrderInfo>(`/install-orders/${id}/complete`),
|
||||
|
||||
delete: (id: number) => request.delete(`/install-orders/${id}`)
|
||||
};
|
||||
@@ -29,6 +29,8 @@ export interface ProjectPlanQuery {
|
||||
keyword?: string;
|
||||
customerId?: number;
|
||||
status?: number;
|
||||
registerStartDate?: string;
|
||||
registerEndDate?: string;
|
||||
}
|
||||
|
||||
export interface ProjectPlanListResult {
|
||||
|
||||
@@ -9,6 +9,13 @@ export interface UploadImageResult {
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface UploadFileResult {
|
||||
key: string;
|
||||
url: string;
|
||||
name: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
// 上传现场照片到对象存储(RustFS),返回对象 key 与预签名 URL
|
||||
export const uploadApi = {
|
||||
uploadImage: (file: File) => {
|
||||
@@ -19,5 +26,15 @@ export const uploadApi = {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
timeout: 60000
|
||||
});
|
||||
},
|
||||
|
||||
// 上传方案/合同/验收等业务文件到对象存储,返回对象 key 与预签名 URL
|
||||
uploadFile: (file: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append('files', file);
|
||||
return request.post<UploadFileResult>('/upload/file', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
timeout: 60000
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import request from '@/utils/request';
|
||||
|
||||
export interface VisitRecordInfo {
|
||||
id: number;
|
||||
visitNo?: string;
|
||||
customerId: number;
|
||||
contactPerson?: string;
|
||||
contactPhone?: string;
|
||||
@@ -17,6 +18,8 @@ export interface VisitRecordQuery {
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
customerId?: number;
|
||||
visitStartDate?: string;
|
||||
visitEndDate?: string;
|
||||
}
|
||||
|
||||
export interface VisitRecordListResult {
|
||||
|
||||
@@ -145,6 +145,12 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import("@/views/project/visit/index.vue"),
|
||||
meta: { requiresAuth: true, title: "回访记录", permission: "project:visit" }
|
||||
},
|
||||
{
|
||||
path: "project/install",
|
||||
name: "ProjectInstall",
|
||||
component: () => import("@/views/project/install/index.vue"),
|
||||
meta: { requiresAuth: true, title: "安装派单", permission: "project:install" }
|
||||
},
|
||||
{
|
||||
path: "dispatch",
|
||||
name: "Dispatch",
|
||||
|
||||
@@ -1,25 +1,68 @@
|
||||
<template>
|
||||
<div class="page-management">
|
||||
<a-card title="项目验收管理">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<div class="acceptance-page">
|
||||
<!-- 顶部标题 + 台账操作 -->
|
||||
<div class="page-header">
|
||||
<div class="breadcrumb">
|
||||
<span>运营管控体系</span>
|
||||
<span class="sep">/</span>
|
||||
<span>项目管理</span>
|
||||
<span class="sep">/</span>
|
||||
<span class="cur">项目验收台账</span>
|
||||
</div>
|
||||
<div class="header-main">
|
||||
<div class="header-left">
|
||||
<div class="header-row">
|
||||
<span class="title-bar"></span>
|
||||
<span class="main-title">项目验收台账</span>
|
||||
<span class="sub-title">ACCEPTANCE LEDGER</span>
|
||||
</div>
|
||||
<p class="desc">登记项目验收意见与验收文件,关联施工合同,跟踪验收人、安装人与验收结论全流程。</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增验收
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-bar">
|
||||
<!-- 主体卡片 -->
|
||||
<div class="content-card">
|
||||
<!-- 筛选区 -->
|
||||
<div class="filter-card">
|
||||
<div class="filter-grid">
|
||||
<div class="filter-item filter-item-search">
|
||||
<label class="filter-label">模糊搜索</label>
|
||||
<a-input
|
||||
v-model:value="searchForm.keyword"
|
||||
placeholder="验收单号/验收人"
|
||||
placeholder="输入验收单号、验收人、安装人..."
|
||||
allow-clear
|
||||
style="width: 220px"
|
||||
@pressEnter="handleSearch"
|
||||
>
|
||||
<template #prefix><SearchOutlined /></template>
|
||||
</a-input>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label class="filter-label">所属客户</label>
|
||||
<a-select
|
||||
v-model:value="searchForm.customerId"
|
||||
placeholder="全部客户"
|
||||
allow-clear
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
:options="customerOptions"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-item filter-item-date">
|
||||
<label class="filter-label">验收日期范围</label>
|
||||
<a-range-picker
|
||||
v-model:value="acceptRange"
|
||||
style="width: 100%"
|
||||
:placeholder="['开始日期', '结束日期']"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-item filter-item-actions">
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
查询
|
||||
@@ -29,37 +72,79 @@
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 列表区 -->
|
||||
<div class="list-section">
|
||||
<div class="list-header">
|
||||
<div class="list-title">
|
||||
<span class="dot"></span>
|
||||
<span class="t">验收数据台账</span>
|
||||
<span class="divider">|</span>
|
||||
<span class="range-text">
|
||||
显示第 <b>{{ rangeStart }}</b> 到 <b>{{ rangeEnd }}</b> 笔,共
|
||||
<b class="strong">{{ total }}</b> 笔验收记录
|
||||
</span>
|
||||
</div>
|
||||
<div class="list-extra">
|
||||
<a-button type="text" size="small" title="刷新列表" @click="fetchList">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper" ref="wrapperRef">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
:scroll="{ x: 'max-content', y: tableHeight }"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'customerName'">
|
||||
{{ getCustomerName(record.customerId) }}
|
||||
<template v-if="column.key === 'acceptNo'">
|
||||
<span class="mono">{{ record.acceptNo }}</span>
|
||||
</template>
|
||||
<template v-if="column.key === 'acceptDate'">
|
||||
{{ record.acceptDate || '-' }}
|
||||
|
||||
<template v-else-if="column.key === 'customerName'">
|
||||
<div class="cust-name">{{ getCustomerName(record.customerId) }}</div>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
<template #icon><EditOutlined /></template>
|
||||
编辑
|
||||
</a-button>
|
||||
<a-popconfirm title="确定要删除该验收记录吗?" ok-text="确定" cancel-text="取消" @confirm="handleDelete(record.id)">
|
||||
<a-button type="link" size="small" danger>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
|
||||
<template v-else-if="column.key === 'acceptor'">
|
||||
{{ record.acceptor || '—' }}
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'acceptDate'">
|
||||
<div class="plan-time">{{ record.acceptDate || '—' }}</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'installer'">
|
||||
{{ record.installer || '—' }}
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'acceptOpinion'">
|
||||
<div v-if="record.acceptOpinion" class="sub-line">{{ record.acceptOpinion }}</div>
|
||||
<div v-else>—</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'contractName'">
|
||||
<span class="mono-sub">{{ getContractNo(record.contractId) }}</span>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<div class="row-actions">
|
||||
<a-button type="primary" size="small" @click="handleEdit(record)">编辑</a-button>
|
||||
<a-dropdown trigger="click">
|
||||
<a-button type="link" size="small">更多 <DownOutlined /></a-button>
|
||||
<template #overlay>
|
||||
<a-menu @click="({ key }) => handleMore(key as string, record)">
|
||||
<a-menu-item key="delete" danger>删除</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -68,13 +153,32 @@
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<!-- 表尾分页 -->
|
||||
<div class="list-footer">
|
||||
<span class="range-text">
|
||||
共 <b class="strong">{{ total }}</b> 条
|
||||
</span>
|
||||
<a-pagination
|
||||
v-model:current="searchForm.page"
|
||||
v-model:page-size="searchForm.pageSize"
|
||||
:total="total"
|
||||
:show-size-changer="true"
|
||||
:show-quick-jumper="true"
|
||||
:page-size-options="['10', '20', '50']"
|
||||
:show-total="(t: number) => `共 ${t} 条`"
|
||||
@change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新建/编辑弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
:confirm-loading="modalLoading"
|
||||
width="620px"
|
||||
width="660px"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
@@ -103,9 +207,37 @@
|
||||
<a-form-item label="安装人" name="installer">
|
||||
<a-input v-model:value="formData.installer" placeholder="请输入安装人" maxlength="64" />
|
||||
</a-form-item>
|
||||
<a-form-item label="关联合同" name="contractId">
|
||||
<a-select
|
||||
v-model:value="formData.contractId"
|
||||
placeholder="请选择关联合同"
|
||||
:options="contractOptions"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="验收意见" name="acceptOpinion">
|
||||
<a-textarea v-model:value="formData.acceptOpinion" :rows="3" placeholder="请输入验收意见" maxlength="2000" />
|
||||
</a-form-item>
|
||||
<a-form-item label="验收文件" name="acceptFileUrl">
|
||||
<a-upload
|
||||
:show-upload-list="false"
|
||||
:custom-request="handleFileUpload"
|
||||
:before-upload="beforeFileUpload"
|
||||
:disabled="uploading"
|
||||
>
|
||||
<a-button :loading="uploading">
|
||||
<template #icon><UploadOutlined /></template>
|
||||
{{ uploading ? '上传中...' : '点击上传验收文件' }}
|
||||
</a-button>
|
||||
</a-upload>
|
||||
<div v-if="formData.acceptFileUrl" class="file-link">
|
||||
<PaperClipOutlined />
|
||||
<a :href="formData.acceptFileUrl" target="_blank">{{ fileName }}</a>
|
||||
<a-button type="link" size="small" danger @click="clearFile">移除</a-button>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
@@ -113,94 +245,125 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { SearchOutlined, ReloadOutlined, PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
PlusOutlined,
|
||||
DownOutlined,
|
||||
UploadOutlined,
|
||||
PaperClipOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { acceptanceApi, type AcceptanceInfo, type AcceptanceQuery } from '@/api/acceptance';
|
||||
import { customerApi } from '@/api/customer';
|
||||
import { contractApi, type ContractInfo } from '@/api/contract';
|
||||
import { customerApi, type CustomerInfo } from '@/api/customer';
|
||||
import { uploadApi } from '@/api/upload';
|
||||
import { useTableScroll } from '@/composables/useTableScroll';
|
||||
|
||||
const { wrapperRef, tableHeight } = useTableScroll();
|
||||
|
||||
// ==================== 列表数据 ====================
|
||||
const loading = ref(false);
|
||||
const dataSource = ref<AcceptanceInfo[]>([]);
|
||||
const total = ref(0);
|
||||
const searchForm = reactive<AcceptanceQuery>({ page: 1, pageSize: 10, keyword: '' });
|
||||
const modalVisible = ref(false);
|
||||
const modalTitle = ref('');
|
||||
const modalLoading = ref(false);
|
||||
const formRef = ref();
|
||||
const isEdit = ref(false);
|
||||
const editId = ref<number | null>(null);
|
||||
const customerOptions = ref<{ label: string; value: number }[]>([]);
|
||||
|
||||
const formData = reactive({
|
||||
const searchForm = reactive<AcceptanceQuery>({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
keyword: '',
|
||||
customerId: undefined as number | undefined,
|
||||
acceptor: '',
|
||||
acceptDate: undefined as any,
|
||||
installer: '',
|
||||
acceptOpinion: '',
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
customerId: [{ required: true, message: '请选择所属客户', trigger: 'change' }],
|
||||
};
|
||||
const acceptRange = ref<any>(undefined);
|
||||
|
||||
const columns = [
|
||||
{ title: '验收单号', dataIndex: 'acceptNo', key: 'acceptNo', ellipsis: true, width: 170 },
|
||||
{ title: '所属客户', key: 'customerName', ellipsis: true },
|
||||
{ title: '验收人', dataIndex: 'acceptor', key: 'acceptor', ellipsis: true },
|
||||
{ title: '验收时间', key: 'acceptDate', width: 110 },
|
||||
{ title: '安装人', dataIndex: 'installer', key: 'installer', ellipsis: true },
|
||||
{ title: '验收意见', dataIndex: 'acceptOpinion', key: 'acceptOpinion', ellipsis: true },
|
||||
{ title: '操作', key: 'action', width: 140, fixed: 'right' as const },
|
||||
{ title: '验收单号', key: 'acceptNo', width: 170 },
|
||||
{ title: '客户', key: 'customerName', width: 180 },
|
||||
{ title: '验收人', key: 'acceptor', width: 110 },
|
||||
{ title: '验收日期', key: 'acceptDate', width: 120 },
|
||||
{ title: '安装人', key: 'installer', width: 110 },
|
||||
{ title: '验收意见', key: 'acceptOpinion', ellipsis: true },
|
||||
{ title: '关联合同', key: 'contractName', width: 150 },
|
||||
{ title: '操作', key: 'action', align: 'center' as const, width: 170, fixed: 'right' as const },
|
||||
];
|
||||
|
||||
const pagination = computed(() => ({
|
||||
current: searchForm.page,
|
||||
pageSize: searchForm.pageSize,
|
||||
total: total.value,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (t: number) => `共 ${t} 条数据`,
|
||||
}));
|
||||
const rangeStart = computed(() => (total.value === 0 ? 0 : (searchForm.page! - 1) * searchForm.pageSize! + 1));
|
||||
const rangeEnd = computed(() => Math.min(searchForm.page! * searchForm.pageSize!, total.value));
|
||||
|
||||
onMounted(() => {
|
||||
fetchList();
|
||||
fetchCustomerOptions();
|
||||
});
|
||||
// ==================== 下拉选项 ====================
|
||||
const customerOptions = ref<{ label: string; value: number }[]>([]);
|
||||
const contractOptions = ref<{ label: string; value: number }[]>([]);
|
||||
|
||||
async function fetchCustomerOptions() {
|
||||
try {
|
||||
const res = await customerApi.getCustomerOptions();
|
||||
customerOptions.value = (res.data || []).map((item: CustomerInfo) => ({
|
||||
label: item.customerName,
|
||||
value: item.id,
|
||||
}));
|
||||
} catch {
|
||||
customerOptions.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchContractOptions() {
|
||||
try {
|
||||
const res = await contractApi.getList({ page: 1, pageSize: 999, status: 1 });
|
||||
contractOptions.value = (res.data.list || []).map((item: ContractInfo) => ({
|
||||
label: item.contractNo,
|
||||
value: item.id,
|
||||
}));
|
||||
} catch {
|
||||
contractOptions.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
function getCustomerName(customerId?: number): string {
|
||||
if (!customerId) return '—';
|
||||
const found = customerOptions.value.find((c) => c.value === customerId);
|
||||
return found ? found.label : `#${customerId}`;
|
||||
}
|
||||
|
||||
function getContractNo(contractId?: number): string {
|
||||
if (!contractId) return '—';
|
||||
const found = contractOptions.value.find((c) => c.value === contractId);
|
||||
return found ? found.label : `#${contractId}`;
|
||||
}
|
||||
|
||||
// ==================== 数据请求 ====================
|
||||
function buildQuery(): AcceptanceQuery {
|
||||
const range = acceptRange.value as any[] | undefined;
|
||||
return {
|
||||
page: searchForm.page,
|
||||
pageSize: searchForm.pageSize,
|
||||
keyword: searchForm.keyword || undefined,
|
||||
customerId: searchForm.customerId,
|
||||
acceptStartDate: range?.[0] ? range[0].format('YYYY-MM-DD') : undefined,
|
||||
acceptEndDate: range?.[1] ? range[1].format('YYYY-MM-DD') : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res: any = await acceptanceApi.getList(searchForm);
|
||||
if (res.code === 200) {
|
||||
dataSource.value = res.data.list;
|
||||
total.value = res.data.total;
|
||||
}
|
||||
const res = await acceptanceApi.getList(buildQuery());
|
||||
dataSource.value = res.data.list || [];
|
||||
total.value = res.data.total || 0;
|
||||
} catch {
|
||||
message.error('获取验收列表失败');
|
||||
dataSource.value = [];
|
||||
total.value = 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCustomerOptions() {
|
||||
try {
|
||||
const res: any = await customerApi.getCustomerOptions();
|
||||
if (res.code === 200) {
|
||||
customerOptions.value = res.data.map((item: any) => ({ label: item.customerName, value: item.id }));
|
||||
}
|
||||
} catch {
|
||||
console.error('获取客户选项失败');
|
||||
}
|
||||
}
|
||||
|
||||
function getCustomerName(customerId: number): string {
|
||||
const found = customerOptions.value.find((c) => c.value === customerId);
|
||||
return found ? found.label : `#${customerId}`;
|
||||
}
|
||||
onMounted(() => {
|
||||
fetchList();
|
||||
fetchCustomerOptions();
|
||||
fetchContractOptions();
|
||||
});
|
||||
|
||||
// ==================== 筛选与分页 ====================
|
||||
function handleSearch() {
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
@@ -208,16 +371,56 @@ function handleSearch() {
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = '';
|
||||
searchForm.customerId = undefined;
|
||||
acceptRange.value = undefined;
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleTableChange(pag: any) {
|
||||
searchForm.page = pag.current;
|
||||
searchForm.pageSize = pag.pageSize;
|
||||
function handlePageChange(page: number, pageSize: number) {
|
||||
searchForm.page = page;
|
||||
searchForm.pageSize = pageSize;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
// ==================== 弹窗表单 ====================
|
||||
const modalVisible = ref(false);
|
||||
const modalTitle = ref('');
|
||||
const modalLoading = ref(false);
|
||||
const formRef = ref();
|
||||
const isEdit = ref(false);
|
||||
const editId = ref<number | null>(null);
|
||||
const uploading = ref(false);
|
||||
const fileName = ref('');
|
||||
|
||||
const formData = reactive({
|
||||
customerId: undefined as number | undefined,
|
||||
acceptor: '',
|
||||
acceptDate: undefined as any,
|
||||
installer: '',
|
||||
contractId: undefined as number | undefined,
|
||||
acceptOpinion: '',
|
||||
acceptFileUrl: '',
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
customerId: [{ required: true, message: '请选择所属客户', trigger: 'change' }],
|
||||
};
|
||||
|
||||
function resetForm() {
|
||||
formData.customerId = undefined;
|
||||
formData.acceptor = '';
|
||||
formData.acceptDate = undefined;
|
||||
formData.installer = '';
|
||||
formData.contractId = undefined;
|
||||
formData.acceptOpinion = '';
|
||||
formData.acceptFileUrl = '';
|
||||
fileName.value = '';
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields();
|
||||
}
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false;
|
||||
editId.value = null;
|
||||
@@ -232,24 +435,58 @@ function handleEdit(record: AcceptanceInfo) {
|
||||
modalTitle.value = '编辑验收';
|
||||
resetForm();
|
||||
formData.customerId = record.customerId;
|
||||
formData.acceptor = record.acceptor;
|
||||
formData.acceptor = record.acceptor || '';
|
||||
formData.acceptDate = record.acceptDate ? dayjs(record.acceptDate) : undefined;
|
||||
formData.installer = record.installer;
|
||||
formData.acceptOpinion = record.acceptOpinion;
|
||||
formData.installer = record.installer || '';
|
||||
formData.contractId = record.contractId;
|
||||
formData.acceptOpinion = record.acceptOpinion || '';
|
||||
formData.acceptFileUrl = record.acceptFileUrl || '';
|
||||
fileName.value = deriveFileName(record.acceptFileUrl);
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.customerId = undefined;
|
||||
formData.acceptor = '';
|
||||
formData.acceptDate = undefined;
|
||||
formData.installer = '';
|
||||
formData.acceptOpinion = '';
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields();
|
||||
// ==================== 文件上传 ====================
|
||||
function beforeFileUpload(file: File) {
|
||||
const isLt20M = file.size / 1024 / 1024 < 20;
|
||||
if (!isLt20M) {
|
||||
message.error('文件大小不能超过 20MB');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function handleFileUpload(options: any) {
|
||||
const { file, onSuccess, onError } = options;
|
||||
uploading.value = true;
|
||||
try {
|
||||
const res = await uploadApi.uploadFile(file as File);
|
||||
formData.acceptFileUrl = res.data.url;
|
||||
fileName.value = res.data.name || (file as File).name;
|
||||
onSuccess?.(res.data);
|
||||
message.success('上传成功');
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '文件上传失败');
|
||||
onError?.(e);
|
||||
} finally {
|
||||
uploading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function clearFile() {
|
||||
formData.acceptFileUrl = '';
|
||||
fileName.value = '';
|
||||
}
|
||||
|
||||
function deriveFileName(url?: string): string {
|
||||
if (!url) return '';
|
||||
try {
|
||||
return decodeURIComponent(url.split('/').pop() || url);
|
||||
} catch {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 提交 ====================
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
@@ -259,30 +496,27 @@ async function handleSubmit() {
|
||||
|
||||
modalLoading.value = true;
|
||||
try {
|
||||
const submitData: any = {
|
||||
const submitData = {
|
||||
customerId: formData.customerId,
|
||||
acceptor: formData.acceptor,
|
||||
acceptor: formData.acceptor || undefined,
|
||||
acceptDate: formData.acceptDate ? formData.acceptDate.format('YYYY-MM-DD') : undefined,
|
||||
installer: formData.installer,
|
||||
acceptOpinion: formData.acceptOpinion,
|
||||
installer: formData.installer || undefined,
|
||||
contractId: formData.contractId || undefined,
|
||||
acceptOpinion: formData.acceptOpinion || undefined,
|
||||
acceptFileUrl: formData.acceptFileUrl || undefined,
|
||||
};
|
||||
|
||||
let res: any;
|
||||
if (isEdit.value && editId.value) {
|
||||
res = await acceptanceApi.update(editId.value, submitData);
|
||||
await acceptanceApi.update(editId.value, submitData);
|
||||
message.success('更新成功');
|
||||
} else {
|
||||
res = await acceptanceApi.create(submitData);
|
||||
await acceptanceApi.create(submitData);
|
||||
message.success('创建成功');
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
modalVisible.value = false;
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '操作失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('操作失败');
|
||||
// 拦截器已提示
|
||||
} finally {
|
||||
modalLoading.value = false;
|
||||
}
|
||||
@@ -293,61 +527,338 @@ function handleCancel() {
|
||||
resetForm();
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
// ==================== 行内操作 ====================
|
||||
function handleMore(key: string, record: AcceptanceInfo) {
|
||||
if (key === 'delete') return confirmDelete(record);
|
||||
}
|
||||
|
||||
function confirmDelete(record: AcceptanceInfo) {
|
||||
Modal.confirm({
|
||||
title: '确认删除该验收记录?',
|
||||
content: `验收单「${record.acceptNo}」将被删除,此操作不可恢复。`,
|
||||
okText: '确认删除',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
async onOk() {
|
||||
try {
|
||||
const res: any = await acceptanceApi.delete(id);
|
||||
if (res.code === 200) {
|
||||
await acceptanceApi.delete(record.id);
|
||||
message.success('删除成功');
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
// 拦截器已提示
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-management {
|
||||
.acceptance-page {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 20px;
|
||||
padding: 20px 24px;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(180deg, #eef3f8 0%, #e8eefb 100%);
|
||||
}
|
||||
:deep(.ant-card) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
:deep(.ant-card-body) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
.search-bar {
|
||||
|
||||
.page-header {
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: #5b6b7e;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.breadcrumb .sep {
|
||||
color: #c2c6d8;
|
||||
}
|
||||
|
||||
.breadcrumb .cur {
|
||||
color: #0054cd;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.header-main {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.title-bar {
|
||||
width: 5px;
|
||||
height: 22px;
|
||||
background: #0054cd;
|
||||
border-radius: 3px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #0d1c2c;
|
||||
line-height: 1.4;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #0054cd;
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin: 6px 0 0;
|
||||
font-size: 13px;
|
||||
color: #5b6b7e;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.content-card {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #dbe4ee;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.07);
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.filter-card {
|
||||
flex-shrink: 0;
|
||||
background: #f8faff;
|
||||
border: 1px solid #dbe4ee;
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.filter-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 2fr) 1fr 1.6fr auto;
|
||||
gap: 12px 16px;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.filter-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.filter-item-actions {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #5b6b7e;
|
||||
}
|
||||
|
||||
.list-section {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 2px 4px 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.list-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.list-title .dot {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background: #0054cd;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.list-title .t {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #0d1c2c;
|
||||
}
|
||||
|
||||
.list-title .divider {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.range-text {
|
||||
font-size: 12px;
|
||||
color: #5b6b7e;
|
||||
}
|
||||
|
||||
.range-text b {
|
||||
font-family: Consolas, Menlo, monospace;
|
||||
color: #0d1c2c;
|
||||
}
|
||||
|
||||
.range-text .strong {
|
||||
color: #0054cd;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.list-extra {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
color: #5b6b7e;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-footer {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
padding-top: 12px;
|
||||
margin-top: 8px;
|
||||
border-top: 1px solid #eef1f6;
|
||||
}
|
||||
|
||||
.cust-name {
|
||||
font-weight: 600;
|
||||
color: #0d1c2c;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: Consolas, Menlo, monospace;
|
||||
font-weight: 700;
|
||||
color: #0054cd;
|
||||
}
|
||||
|
||||
.mono-sub {
|
||||
font-family: Consolas, Menlo, monospace;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.plan-time {
|
||||
font-family: Consolas, Menlo, monospace;
|
||||
font-size: 12.5px;
|
||||
color: #0d1c2c;
|
||||
}
|
||||
|
||||
.sub-line {
|
||||
font-size: 12.5px;
|
||||
color: #5b6b7e;
|
||||
line-height: 1.5;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.row-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.file-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
color: #0054cd;
|
||||
}
|
||||
|
||||
.file-link a {
|
||||
color: #0054cd;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
:deep(.ant-table) {
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.ant-table-thead > tr > th) {
|
||||
background: #f4f7ff;
|
||||
color: #5b6b7e;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
:deep(.ant-btn-link) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
:deep(.ant-btn-primary) {
|
||||
background: #0054cd;
|
||||
border-color: #0054cd;
|
||||
}
|
||||
|
||||
:deep(.ant-btn-primary:not(:disabled):not(.ant-btn-disabled):hover) {
|
||||
background: #0947ab;
|
||||
border-color: #0947ab;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,873 @@
|
||||
<template>
|
||||
<div class="install-page">
|
||||
<!-- 顶部标题 + 台账操作 -->
|
||||
<div class="page-header">
|
||||
<div class="breadcrumb">
|
||||
<span>运营管控体系</span>
|
||||
<span class="sep">/</span>
|
||||
<span>项目管理</span>
|
||||
<span class="sep">/</span>
|
||||
<span class="cur">安装派单台账</span>
|
||||
</div>
|
||||
<div class="header-main">
|
||||
<div class="header-left">
|
||||
<div class="header-row">
|
||||
<span class="title-bar"></span>
|
||||
<span class="main-title">安装派单台账</span>
|
||||
<span class="sub-title">INSTALL DISPATCH LEDGER</span>
|
||||
</div>
|
||||
<p class="desc">统筹项目方案落地施工派单,跟踪派单、施工起止与完工状态全流程。</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增安装派单
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主体卡片 -->
|
||||
<div class="content-card">
|
||||
<!-- 筛选区 -->
|
||||
<div class="filter-card">
|
||||
<div class="filter-grid">
|
||||
<div class="filter-item filter-item-search">
|
||||
<label class="filter-label">模糊搜索</label>
|
||||
<a-input
|
||||
v-model:value="searchForm.keyword"
|
||||
placeholder="输入安装单号、安装人、备注..."
|
||||
allow-clear
|
||||
@pressEnter="handleSearch"
|
||||
>
|
||||
<template #prefix><SearchOutlined /></template>
|
||||
</a-input>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label class="filter-label">关联客户</label>
|
||||
<a-select
|
||||
v-model:value="searchForm.customerId"
|
||||
placeholder="全部客户"
|
||||
allow-clear
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
:options="customerOptions"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label class="filter-label">当前状态</label>
|
||||
<a-select
|
||||
v-model:value="searchForm.status"
|
||||
placeholder="全部状态"
|
||||
allow-clear
|
||||
:options="statusOptions"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-item filter-item-date">
|
||||
<label class="filter-label">派单日期范围</label>
|
||||
<a-range-picker
|
||||
v-model:value="searchForm.dateRange"
|
||||
style="width: 100%"
|
||||
:placeholder="['开始日期', '结束日期']"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-item filter-item-actions">
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 列表区 -->
|
||||
<div class="list-section">
|
||||
<div class="list-header">
|
||||
<div class="list-title">
|
||||
<span class="dot"></span>
|
||||
<span class="t">安装派单数据台账</span>
|
||||
<span class="divider">|</span>
|
||||
<span class="range-text">
|
||||
显示第 <b>{{ rangeStart }}</b> 到 <b>{{ rangeEnd }}</b> 笔,共
|
||||
<b class="strong">{{ total }}</b> 笔派单记录
|
||||
</span>
|
||||
</div>
|
||||
<div class="list-extra">
|
||||
<a-button type="text" size="small" title="刷新列表" @click="fetchList">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper" ref="wrapperRef">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
:scroll="{ x: 'max-content', y: tableHeight }"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'installNo'">
|
||||
<span class="install-no">{{ record.installNo }}</span>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'customer'">
|
||||
<div class="cust-name">{{ record.customer?.customerName || '-' }}</div>
|
||||
<div class="sub-line">{{ record.customer?.contactPhone || '—' }}</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'plan'">
|
||||
<div class="plan-no">{{ record.plan?.planNo || '-' }}</div>
|
||||
<div class="sub-line">{{ record.plan?.planDesc || '—' }}</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'installer'">
|
||||
<div class="type-main">{{ record.installer || record.workerName || '-' }}</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'dispatchDate'">
|
||||
<div class="plan-time">{{ formatDate(record.dispatchDate) }}</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'period'">
|
||||
<div class="plan-time">
|
||||
{{ formatDate(record.startDate) }} ~ {{ formatDate(record.endDate) }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<span class="status-pill" :style="pillStyle(statusMeta(record.status))">
|
||||
<i class="dot" :style="{ background: statusMeta(record.status).color }"></i>
|
||||
{{ statusMeta(record.status).label }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<div class="row-actions">
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
|
||||
<a-button
|
||||
v-if="record.status === INSTALL_STATUS.DISPATCHED"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleComplete(record)"
|
||||
>标记完成</a-button>
|
||||
<a-button type="link" size="small" danger @click="handleDelete(record)">删除</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template #emptyText>
|
||||
<a-empty description="暂无安装派单数据" />
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<!-- 表尾:分页 -->
|
||||
<div class="list-footer">
|
||||
<a-pagination
|
||||
v-model:current="searchForm.page"
|
||||
v-model:page-size="searchForm.pageSize"
|
||||
:total="total"
|
||||
:show-size-changer="true"
|
||||
:show-quick-jumper="true"
|
||||
:page-size-options="['10', '20', '50']"
|
||||
:show-total="(t: number) => `共 ${t} 条`"
|
||||
@change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新建/编辑弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="formVisible"
|
||||
:title="formMode === 'create' ? '新增安装派单' : '编辑安装派单'"
|
||||
:confirm-loading="formLoading"
|
||||
width="640px"
|
||||
@ok="handleSubmit"
|
||||
>
|
||||
<a-form ref="formRef" :model="form" :rules="formRules" :label-col="{ span: 6 }" :wrapper-col="{ span: 17 }">
|
||||
<a-form-item label="安装单号" v-if="formMode === 'edit'">
|
||||
<span class="mono">{{ form.installNo }}</span>
|
||||
</a-form-item>
|
||||
<a-form-item label="关联客户" name="customerId">
|
||||
<a-select
|
||||
v-model:value="form.customerId"
|
||||
placeholder="请选择客户"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
:options="customerOptions"
|
||||
:disabled="formMode === 'edit'"
|
||||
@change="onCustomerChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="关联方案" name="planId">
|
||||
<a-select
|
||||
v-model:value="form.planId"
|
||||
placeholder="请选择方案"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
:options="planOptions"
|
||||
:not-found-content="form.customerId ? '该客户暂无方案' : '请先选择客户'"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="安装人" name="installer">
|
||||
<a-input v-model:value="form.installer" placeholder="请输入安装施工负责人" maxlength="64" />
|
||||
</a-form-item>
|
||||
<a-form-item label="派单日期" name="dispatchDate">
|
||||
<a-date-picker
|
||||
v-model:value="form.dispatchDate"
|
||||
style="width: 100%"
|
||||
placeholder="请选择派单日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="施工起止" name="period">
|
||||
<a-range-picker
|
||||
v-model:value="form.period"
|
||||
style="width: 100%"
|
||||
:placeholder="['开始日期', '结束日期']"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea v-model:value="form.remark" placeholder="选填,施工说明、注意事项等" :rows="3" maxlength="500" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
PlusOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
installOrderApi,
|
||||
INSTALL_STATUS_OPTIONS,
|
||||
INSTALL_STATUS,
|
||||
type InstallOrderInfo,
|
||||
type InstallOrderQuery,
|
||||
} from '@/api/installOrder';
|
||||
import { customerApi, type CustomerInfo } from '@/api/customer';
|
||||
import { projectPlanApi, type ProjectPlanInfo } from '@/api/projectPlan';
|
||||
import { useTableScroll } from '@/composables/useTableScroll';
|
||||
|
||||
const { wrapperRef, tableHeight } = useTableScroll();
|
||||
|
||||
// ==================== 元数据 ====================
|
||||
const statusOptions = INSTALL_STATUS_OPTIONS;
|
||||
|
||||
// 状态配色:0已派发(琥珀) 1已完成(绿)
|
||||
const STATUS_META: Record<number, { label: string; color: string; bg: string; border: string }> = {
|
||||
0: { label: '已派发', color: '#d97706', bg: '#fef3c7', border: '#fde68a' },
|
||||
1: { label: '已完成', color: '#059669', bg: '#d1fae5', border: '#a7f3d0' },
|
||||
};
|
||||
|
||||
const statusMeta = (v: number) => STATUS_META[v] || STATUS_META[0];
|
||||
const pillStyle = (m: { color: string; bg: string; border: string }) => ({
|
||||
background: m.bg,
|
||||
color: m.color,
|
||||
borderColor: m.border,
|
||||
});
|
||||
|
||||
// ==================== 列表数据 ====================
|
||||
const loading = ref(false);
|
||||
const dataSource = ref<InstallOrderInfo[]>([]);
|
||||
const total = ref(0);
|
||||
|
||||
const searchForm = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
keyword: '',
|
||||
customerId: undefined as number | undefined,
|
||||
status: undefined as number | undefined,
|
||||
dateRange: undefined as any,
|
||||
});
|
||||
|
||||
const columns = [
|
||||
{ title: '安装单号', key: 'installNo', width: 170 },
|
||||
{ title: '关联客户', key: 'customer', width: 200 },
|
||||
{ title: '关联方案', key: 'plan', width: 180 },
|
||||
{ title: '安装人', key: 'installer', width: 130 },
|
||||
{ title: '派单日期', key: 'dispatchDate', width: 130, align: 'center' as const },
|
||||
{ title: '施工起止', key: 'period', width: 220 },
|
||||
{ title: '状态', key: 'status', width: 120, align: 'center' as const },
|
||||
{ title: '操作', key: 'action', width: 200, align: 'center' as const, fixed: 'right' as const },
|
||||
];
|
||||
|
||||
const rangeStart = computed(() => (total.value === 0 ? 0 : (searchForm.page - 1) * searchForm.pageSize + 1));
|
||||
const rangeEnd = computed(() => Math.min(searchForm.page * searchForm.pageSize, total.value));
|
||||
|
||||
// ==================== 下拉选项 ====================
|
||||
const customerOptions = ref<{ label: string; value: number }[]>([]);
|
||||
const planOptions = ref<{ label: string; value: number }[]>([]);
|
||||
|
||||
async function fetchCustomerOptions() {
|
||||
try {
|
||||
const res = await customerApi.getCustomerOptions();
|
||||
customerOptions.value = (res.data || []).map((item: CustomerInfo) => ({
|
||||
label: item.customerName,
|
||||
value: item.id,
|
||||
}));
|
||||
} catch {
|
||||
customerOptions.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 按客户拉取方案下拉(方案与客户一致性校验在后端)
|
||||
async function fetchPlanOptions(customerId?: number) {
|
||||
if (!customerId) {
|
||||
planOptions.value = [];
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await projectPlanApi.getList({ page: 1, pageSize: 999, customerId });
|
||||
planOptions.value = (res.data?.list || []).map((item: ProjectPlanInfo) => ({
|
||||
label: item.planDesc ? `${item.planNo} · ${item.planDesc}` : item.planNo,
|
||||
value: item.id,
|
||||
}));
|
||||
} catch {
|
||||
planOptions.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 数据请求 ====================
|
||||
function buildQuery(): InstallOrderQuery {
|
||||
const range = searchForm.dateRange as any[] | undefined;
|
||||
let dateRange: string | undefined;
|
||||
if (range && range.length === 2 && (range[0] || range[1])) {
|
||||
const s = range[0] ? dayjs(range[0]).format('YYYY-MM-DD') : '';
|
||||
const e = range[1] ? dayjs(range[1]).format('YYYY-MM-DD') : '';
|
||||
if (s || e) dateRange = `${s},${e}`;
|
||||
}
|
||||
return {
|
||||
page: searchForm.page,
|
||||
pageSize: searchForm.pageSize,
|
||||
keyword: searchForm.keyword || undefined,
|
||||
customerId: searchForm.customerId,
|
||||
status: searchForm.status,
|
||||
dateRange,
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await installOrderApi.getList(buildQuery());
|
||||
dataSource.value = res.data.list || [];
|
||||
total.value = res.data.total || 0;
|
||||
} catch {
|
||||
dataSource.value = [];
|
||||
total.value = 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchList();
|
||||
fetchCustomerOptions();
|
||||
});
|
||||
|
||||
// ==================== 筛选与分页 ====================
|
||||
function handleSearch() {
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = '';
|
||||
searchForm.customerId = undefined;
|
||||
searchForm.status = undefined;
|
||||
searchForm.dateRange = undefined;
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handlePageChange(page: number, pageSize: number) {
|
||||
searchForm.page = page;
|
||||
searchForm.pageSize = pageSize;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
// ==================== 新建/编辑弹窗 ====================
|
||||
const formVisible = ref(false);
|
||||
const formLoading = ref(false);
|
||||
const formMode = ref<'create' | 'edit'>('create');
|
||||
const formRef = ref();
|
||||
const editingId = ref<number | null>(null);
|
||||
|
||||
const form = reactive({
|
||||
installNo: '',
|
||||
customerId: undefined as number | undefined,
|
||||
planId: undefined as number | undefined,
|
||||
installer: '',
|
||||
dispatchDate: undefined as any,
|
||||
period: undefined as any,
|
||||
remark: '',
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
customerId: [{ required: true, message: '请选择关联客户', trigger: 'change' }],
|
||||
planId: [{ required: true, message: '请选择关联方案', trigger: 'change' }],
|
||||
};
|
||||
|
||||
function resetForm() {
|
||||
form.installNo = '';
|
||||
form.customerId = undefined;
|
||||
form.planId = undefined;
|
||||
form.installer = '';
|
||||
form.dispatchDate = undefined;
|
||||
form.period = undefined;
|
||||
form.remark = '';
|
||||
planOptions.value = [];
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
formMode.value = 'create';
|
||||
editingId.value = null;
|
||||
resetForm();
|
||||
formVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(record: InstallOrderInfo) {
|
||||
formMode.value = 'edit';
|
||||
editingId.value = record.id;
|
||||
resetForm();
|
||||
form.installNo = record.installNo;
|
||||
form.customerId = record.customerId;
|
||||
form.planId = record.planId;
|
||||
form.installer = record.installer || '';
|
||||
form.dispatchDate = record.dispatchDate ? dayjs(record.dispatchDate) : undefined;
|
||||
form.period = (record.startDate || record.endDate)
|
||||
? [record.startDate ? dayjs(record.startDate) : undefined, record.endDate ? dayjs(record.endDate) : undefined]
|
||||
: undefined;
|
||||
form.remark = record.remark || '';
|
||||
fetchPlanOptions(record.customerId);
|
||||
formVisible.value = true;
|
||||
}
|
||||
|
||||
function onCustomerChange() {
|
||||
form.planId = undefined;
|
||||
fetchPlanOptions(form.customerId);
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const period = form.period as any[] | undefined;
|
||||
const payload = {
|
||||
customerId: form.customerId!,
|
||||
planId: form.planId!,
|
||||
installer: form.installer || undefined,
|
||||
dispatchDate: form.dispatchDate ? dayjs(form.dispatchDate).format('YYYY-MM-DD') : undefined,
|
||||
startDate: period && period[0] ? dayjs(period[0]).format('YYYY-MM-DD') : undefined,
|
||||
endDate: period && period[1] ? dayjs(period[1]).format('YYYY-MM-DD') : undefined,
|
||||
remark: form.remark || undefined,
|
||||
};
|
||||
formLoading.value = true;
|
||||
try {
|
||||
if (formMode.value === 'create') {
|
||||
const res = await installOrderApi.create(payload);
|
||||
message.success(res.message || '创建成功');
|
||||
} else if (editingId.value !== null) {
|
||||
const res = await installOrderApi.update(editingId.value, payload);
|
||||
message.success(res.message || '更新成功');
|
||||
}
|
||||
formVisible.value = false;
|
||||
fetchList();
|
||||
} catch {
|
||||
// 拦截器已提示
|
||||
} finally {
|
||||
formLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 行内操作 ====================
|
||||
function handleComplete(record: InstallOrderInfo) {
|
||||
Modal.confirm({
|
||||
title: '确认标记完成?',
|
||||
content: `安装单 ${record.installNo} 将变更为「已完成」状态,完成后不可再修改。`,
|
||||
okText: '确认完成',
|
||||
cancelText: '取消',
|
||||
async onOk() {
|
||||
try {
|
||||
const res = await installOrderApi.complete(record.id);
|
||||
message.success(res.message || '已标记完成');
|
||||
fetchList();
|
||||
} catch {
|
||||
// 拦截器已提示
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleDelete(record: InstallOrderInfo) {
|
||||
Modal.confirm({
|
||||
title: '确认删除该安装派单?',
|
||||
content: `安装单 ${record.installNo} 删除后不可恢复,请谨慎操作。`,
|
||||
okText: '确认删除',
|
||||
okType: 'danger',
|
||||
cancelText: '返回',
|
||||
async onOk() {
|
||||
try {
|
||||
const res = await installOrderApi.delete(record.id);
|
||||
message.success(res.message || '删除成功');
|
||||
fetchList();
|
||||
} catch {
|
||||
// 拦截器已提示
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 展示辅助 ====================
|
||||
function formatDate(value?: string) {
|
||||
if (!value) return '—';
|
||||
return dayjs(value).format('YYYY-MM-DD');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.install-page {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 20px 24px;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(180deg, #eef3f8 0%, #e8eefb 100%);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: #5b6b7e;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.breadcrumb .sep {
|
||||
color: #c2c6d8;
|
||||
}
|
||||
|
||||
.breadcrumb .cur {
|
||||
color: #0054cd;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.header-main {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.title-bar {
|
||||
width: 5px;
|
||||
height: 22px;
|
||||
background: #0054cd;
|
||||
border-radius: 3px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #0d1c2c;
|
||||
line-height: 1.4;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #0054cd;
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin: 6px 0 0;
|
||||
font-size: 13px;
|
||||
color: #5b6b7e;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.content-card {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #dbe4ee;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.07);
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.filter-card {
|
||||
flex-shrink: 0;
|
||||
background: #f8faff;
|
||||
border: 1px solid #dbe4ee;
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.filter-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 2fr) 1fr 1fr 1.6fr auto;
|
||||
gap: 12px 16px;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.filter-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.filter-item-actions {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #5b6b7e;
|
||||
}
|
||||
|
||||
.list-section {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 2px 4px 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.list-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.list-title .dot {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background: #0054cd;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.list-title .t {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #0d1c2c;
|
||||
}
|
||||
|
||||
.list-title .divider {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.range-text {
|
||||
font-size: 12px;
|
||||
color: #5b6b7e;
|
||||
}
|
||||
|
||||
.range-text b {
|
||||
font-family: Consolas, Menlo, monospace;
|
||||
color: #0d1c2c;
|
||||
}
|
||||
|
||||
.range-text .strong {
|
||||
color: #0054cd;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.list-extra {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
color: #5b6b7e;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-footer {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
padding-top: 12px;
|
||||
margin-top: 8px;
|
||||
border-top: 1px solid #eef1f6;
|
||||
}
|
||||
|
||||
.install-no {
|
||||
font-family: Consolas, Menlo, monospace;
|
||||
font-weight: 700;
|
||||
color: #0054cd;
|
||||
}
|
||||
|
||||
.cust-name {
|
||||
font-weight: 600;
|
||||
color: #0d1c2c;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.type-main {
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.plan-no {
|
||||
font-family: Consolas, Menlo, monospace;
|
||||
font-size: 12.5px;
|
||||
color: #0d1c2c;
|
||||
}
|
||||
|
||||
.sub-line {
|
||||
margin-top: 3px;
|
||||
font-size: 11.5px;
|
||||
color: #8a97a8;
|
||||
line-height: 1.45;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.plan-time {
|
||||
font-family: Consolas, Menlo, monospace;
|
||||
font-size: 12.5px;
|
||||
color: #0d1c2c;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 2px 10px;
|
||||
border-radius: 9999px;
|
||||
border: 1px solid transparent;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-pill .dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.row-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: Consolas, Menlo, monospace;
|
||||
color: #0054cd;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.ant-table) {
|
||||
font-size: 13px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.ant-table-thead > tr > th) {
|
||||
background: #f4f7ff;
|
||||
color: #5b6b7e;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
:deep(.ant-btn-link) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
:deep(.ant-btn-primary) {
|
||||
background: #0054cd;
|
||||
border-color: #0054cd;
|
||||
}
|
||||
|
||||
:deep(.ant-btn-primary:not(:disabled):not(.ant-btn-disabled):hover) {
|
||||
background: #0947ab;
|
||||
border-color: #0947ab;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -208,6 +208,18 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field span-2">
|
||||
<label class="field-label"><span>备注</span></label>
|
||||
<a-textarea
|
||||
v-model:value="formState.remark"
|
||||
:rows="3"
|
||||
:maxlength="500"
|
||||
show-count
|
||||
placeholder="派单备注说明,如现场特殊要求、安全交底等"
|
||||
:disabled="isReadonly"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,25 +1,68 @@
|
||||
<template>
|
||||
<div class="page-management">
|
||||
<a-card title="回访记录管理">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<div class="visit-page">
|
||||
<!-- 顶部标题 + 台账操作 -->
|
||||
<div class="page-header">
|
||||
<div class="breadcrumb">
|
||||
<span>运营管控体系</span>
|
||||
<span class="sep">/</span>
|
||||
<span>项目管理</span>
|
||||
<span class="sep">/</span>
|
||||
<span class="cur">回访记录台账</span>
|
||||
</div>
|
||||
<div class="header-main">
|
||||
<div class="header-left">
|
||||
<div class="header-row">
|
||||
<span class="title-bar"></span>
|
||||
<span class="main-title">回访记录台账</span>
|
||||
<span class="sub-title">VISIT RECORD LEDGER</span>
|
||||
</div>
|
||||
<p class="desc">登记项目交付后的客户回访记录,跟踪回访人、回访时间与客户意见反馈全流程。</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增回访
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-bar">
|
||||
<!-- 主体卡片 -->
|
||||
<div class="content-card">
|
||||
<!-- 筛选区 -->
|
||||
<div class="filter-card">
|
||||
<div class="filter-grid">
|
||||
<div class="filter-item filter-item-search">
|
||||
<label class="filter-label">模糊搜索</label>
|
||||
<a-input
|
||||
v-model:value="searchForm.keyword"
|
||||
placeholder="联系人/回访人"
|
||||
placeholder="输入客户名称、回访人、联系人..."
|
||||
allow-clear
|
||||
style="width: 220px"
|
||||
@pressEnter="handleSearch"
|
||||
>
|
||||
<template #prefix><SearchOutlined /></template>
|
||||
</a-input>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label class="filter-label">所属客户</label>
|
||||
<a-select
|
||||
v-model:value="searchForm.customerId"
|
||||
placeholder="全部客户"
|
||||
allow-clear
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
:options="customerOptions"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-item filter-item-date">
|
||||
<label class="filter-label">回访日期范围</label>
|
||||
<a-range-picker
|
||||
v-model:value="visitRange"
|
||||
style="width: 100%"
|
||||
:placeholder="['开始日期', '结束日期']"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-item filter-item-actions">
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
查询
|
||||
@@ -29,37 +72,68 @@
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 列表区 -->
|
||||
<div class="list-section">
|
||||
<div class="list-header">
|
||||
<div class="list-title">
|
||||
<span class="dot"></span>
|
||||
<span class="t">回访数据台账</span>
|
||||
<span class="divider">|</span>
|
||||
<span class="range-text">
|
||||
显示第 <b>{{ rangeStart }}</b> 到 <b>{{ rangeEnd }}</b> 笔,共
|
||||
<b class="strong">{{ total }}</b> 笔回访记录
|
||||
</span>
|
||||
</div>
|
||||
<div class="list-extra">
|
||||
<a-button type="text" size="small" title="刷新列表" @click="fetchList">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper" ref="wrapperRef">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
:scroll="{ x: 'max-content', y: tableHeight }"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'customerName'">
|
||||
{{ getCustomerName(record.customerId) }}
|
||||
<div class="cust-name">{{ getCustomerName(record.customerId) }}</div>
|
||||
<div class="sub-line">{{ record.contactPerson || '—' }}{{ record.contactPhone ? ' · ' + record.contactPhone : '' }}</div>
|
||||
</template>
|
||||
<template v-if="column.key === 'visitTime'">
|
||||
{{ record.visitTime || '-' }}
|
||||
|
||||
<template v-else-if="column.key === 'visitor'">
|
||||
{{ record.visitor || '—' }}
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
<template #icon><EditOutlined /></template>
|
||||
编辑
|
||||
</a-button>
|
||||
<a-popconfirm title="确定要删除该回访记录吗?" ok-text="确定" cancel-text="取消" @confirm="handleDelete(record.id)">
|
||||
<a-button type="link" size="small" danger>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
|
||||
<template v-else-if="column.key === 'visitTime'">
|
||||
<div class="plan-time">{{ record.visitTime || '—' }}</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'customerOpinion'">
|
||||
<div v-if="record.customerOpinion" class="sub-line">{{ record.customerOpinion }}</div>
|
||||
<div v-else>—</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<div class="row-actions">
|
||||
<a-button type="primary" size="small" @click="handleEdit(record)">编辑</a-button>
|
||||
<a-dropdown trigger="click">
|
||||
<a-button type="link" size="small">更多 <DownOutlined /></a-button>
|
||||
<template #overlay>
|
||||
<a-menu @click="({ key }) => handleMore(key as string, record)">
|
||||
<a-menu-item key="delete" danger>删除</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -68,8 +142,27 @@
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<!-- 表尾分页 -->
|
||||
<div class="list-footer">
|
||||
<span class="range-text">
|
||||
共 <b class="strong">{{ total }}</b> 条
|
||||
</span>
|
||||
<a-pagination
|
||||
v-model:current="searchForm.page"
|
||||
v-model:page-size="searchForm.pageSize"
|
||||
:total="total"
|
||||
:show-size-changer="true"
|
||||
:show-quick-jumper="true"
|
||||
:page-size-options="['10', '20', '50']"
|
||||
:show-total="(t: number) => `共 ${t} 条`"
|
||||
@change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新建/编辑弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
@@ -107,7 +200,7 @@
|
||||
<a-date-picker v-model:value="formData.visitTime" style="width: 100%" placeholder="请选择回访时间" />
|
||||
</a-form-item>
|
||||
<a-form-item label="客户意见" name="customerOpinion">
|
||||
<a-textarea v-model:value="formData.customerOpinion" :rows="3" placeholder="请输入客户意见" maxlength="2000" />
|
||||
<a-textarea v-model:value="formData.customerOpinion" :rows="3" placeholder="请输入客户意见反馈" maxlength="2000" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
@@ -116,26 +209,119 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { SearchOutlined, ReloadOutlined, PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { SearchOutlined, ReloadOutlined, PlusOutlined, DownOutlined } from '@ant-design/icons-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { visitRecordApi, type VisitRecordInfo, type VisitRecordQuery } from '@/api/visitRecord';
|
||||
import { customerApi } from '@/api/customer';
|
||||
import { customerApi, type CustomerInfo } from '@/api/customer';
|
||||
import { useTableScroll } from '@/composables/useTableScroll';
|
||||
|
||||
const { wrapperRef, tableHeight } = useTableScroll();
|
||||
|
||||
// ==================== 列表数据 ====================
|
||||
const loading = ref(false);
|
||||
const dataSource = ref<VisitRecordInfo[]>([]);
|
||||
const total = ref(0);
|
||||
const searchForm = reactive<VisitRecordQuery>({ page: 1, pageSize: 10, keyword: '' });
|
||||
|
||||
const searchForm = reactive<VisitRecordQuery>({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
keyword: '',
|
||||
customerId: undefined as number | undefined,
|
||||
});
|
||||
const visitRange = ref<any>(undefined);
|
||||
|
||||
const columns = [
|
||||
{ title: '客户', key: 'customerName', width: 220 },
|
||||
{ title: '回访人', key: 'visitor', width: 120 },
|
||||
{ title: '回访时间', key: 'visitTime', width: 160 },
|
||||
{ title: '回访内容摘要', key: 'customerOpinion', ellipsis: true },
|
||||
{ title: '操作', key: 'action', align: 'center' as const, width: 170, fixed: 'right' as const },
|
||||
];
|
||||
|
||||
const rangeStart = computed(() => (total.value === 0 ? 0 : (searchForm.page! - 1) * searchForm.pageSize! + 1));
|
||||
const rangeEnd = computed(() => Math.min(searchForm.page! * searchForm.pageSize!, total.value));
|
||||
|
||||
// ==================== 下拉选项 ====================
|
||||
const customerOptions = ref<{ label: string; value: number }[]>([]);
|
||||
|
||||
async function fetchCustomerOptions() {
|
||||
try {
|
||||
const res = await customerApi.getCustomerOptions();
|
||||
customerOptions.value = (res.data || []).map((item: CustomerInfo) => ({
|
||||
label: item.customerName,
|
||||
value: item.id,
|
||||
}));
|
||||
} catch {
|
||||
customerOptions.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
function getCustomerName(customerId?: number): string {
|
||||
if (!customerId) return '—';
|
||||
const found = customerOptions.value.find((c) => c.value === customerId);
|
||||
return found ? found.label : `#${customerId}`;
|
||||
}
|
||||
|
||||
// ==================== 数据请求 ====================
|
||||
function buildQuery(): VisitRecordQuery {
|
||||
const range = visitRange.value as any[] | undefined;
|
||||
return {
|
||||
page: searchForm.page,
|
||||
pageSize: searchForm.pageSize,
|
||||
keyword: searchForm.keyword || undefined,
|
||||
customerId: searchForm.customerId,
|
||||
visitStartDate: range?.[0] ? range[0].format('YYYY-MM-DD') : undefined,
|
||||
visitEndDate: range?.[1] ? range[1].format('YYYY-MM-DD') : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await visitRecordApi.getList(buildQuery());
|
||||
dataSource.value = res.data.list || [];
|
||||
total.value = res.data.total || 0;
|
||||
} catch {
|
||||
dataSource.value = [];
|
||||
total.value = 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchList();
|
||||
fetchCustomerOptions();
|
||||
});
|
||||
|
||||
// ==================== 筛选与分页 ====================
|
||||
function handleSearch() {
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = '';
|
||||
searchForm.customerId = undefined;
|
||||
visitRange.value = undefined;
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handlePageChange(page: number, pageSize: number) {
|
||||
searchForm.page = page;
|
||||
searchForm.pageSize = pageSize;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
// ==================== 弹窗表单 ====================
|
||||
const modalVisible = ref(false);
|
||||
const modalTitle = ref('');
|
||||
const modalLoading = ref(false);
|
||||
const formRef = ref();
|
||||
const isEdit = ref(false);
|
||||
const editId = ref<number | null>(null);
|
||||
const customerOptions = ref<{ label: string; value: number }[]>([]);
|
||||
|
||||
const formData = reactive({
|
||||
customerId: undefined as number | undefined,
|
||||
@@ -151,76 +337,16 @@ const formRules = {
|
||||
contactPhone: [{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }],
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '所属客户', key: 'customerName', ellipsis: true },
|
||||
{ title: '联系人', dataIndex: 'contactPerson', key: 'contactPerson', ellipsis: true },
|
||||
{ title: '联系电话', dataIndex: 'contactPhone', key: 'contactPhone', ellipsis: true },
|
||||
{ title: '回访人', dataIndex: 'visitor', key: 'visitor', ellipsis: true },
|
||||
{ title: '回访时间', key: 'visitTime', width: 110 },
|
||||
{ title: '客户意见', dataIndex: 'customerOpinion', key: 'customerOpinion', ellipsis: true },
|
||||
{ title: '操作', key: 'action', width: 140, fixed: 'right' as const },
|
||||
];
|
||||
|
||||
const pagination = computed(() => ({
|
||||
current: searchForm.page,
|
||||
pageSize: searchForm.pageSize,
|
||||
total: total.value,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (t: number) => `共 ${t} 条数据`,
|
||||
}));
|
||||
|
||||
onMounted(() => {
|
||||
fetchList();
|
||||
fetchCustomerOptions();
|
||||
});
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res: any = await visitRecordApi.getList(searchForm);
|
||||
if (res.code === 200) {
|
||||
dataSource.value = res.data.list;
|
||||
total.value = res.data.total;
|
||||
function resetForm() {
|
||||
formData.customerId = undefined;
|
||||
formData.contactPerson = '';
|
||||
formData.contactPhone = '';
|
||||
formData.visitor = '';
|
||||
formData.visitTime = undefined;
|
||||
formData.customerOpinion = '';
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields();
|
||||
}
|
||||
} catch {
|
||||
message.error('获取回访记录失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCustomerOptions() {
|
||||
try {
|
||||
const res: any = await customerApi.getCustomerOptions();
|
||||
if (res.code === 200) {
|
||||
customerOptions.value = res.data.map((item: any) => ({ label: item.customerName, value: item.id }));
|
||||
}
|
||||
} catch {
|
||||
console.error('获取客户选项失败');
|
||||
}
|
||||
}
|
||||
|
||||
function getCustomerName(customerId: number): string {
|
||||
const found = customerOptions.value.find((c) => c.value === customerId);
|
||||
return found ? found.label : `#${customerId}`;
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = '';
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleTableChange(pag: any) {
|
||||
searchForm.page = pag.current;
|
||||
searchForm.pageSize = pag.pageSize;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
@@ -237,26 +363,15 @@ function handleEdit(record: VisitRecordInfo) {
|
||||
modalTitle.value = '编辑回访';
|
||||
resetForm();
|
||||
formData.customerId = record.customerId;
|
||||
formData.contactPerson = record.contactPerson;
|
||||
formData.contactPhone = record.contactPhone;
|
||||
formData.visitor = record.visitor;
|
||||
formData.contactPerson = record.contactPerson || '';
|
||||
formData.contactPhone = record.contactPhone || '';
|
||||
formData.visitor = record.visitor || '';
|
||||
formData.visitTime = record.visitTime ? dayjs(record.visitTime) : undefined;
|
||||
formData.customerOpinion = record.customerOpinion;
|
||||
formData.customerOpinion = record.customerOpinion || '';
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.customerId = undefined;
|
||||
formData.contactPerson = '';
|
||||
formData.contactPhone = '';
|
||||
formData.visitor = '';
|
||||
formData.visitTime = undefined;
|
||||
formData.customerOpinion = '';
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 提交 ====================
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
@@ -266,31 +381,26 @@ async function handleSubmit() {
|
||||
|
||||
modalLoading.value = true;
|
||||
try {
|
||||
const submitData: any = {
|
||||
const submitData = {
|
||||
customerId: formData.customerId,
|
||||
contactPerson: formData.contactPerson,
|
||||
contactPhone: formData.contactPhone,
|
||||
visitor: formData.visitor,
|
||||
contactPerson: formData.contactPerson || undefined,
|
||||
contactPhone: formData.contactPhone || undefined,
|
||||
visitor: formData.visitor || undefined,
|
||||
visitTime: formData.visitTime ? formData.visitTime.format('YYYY-MM-DD') : undefined,
|
||||
customerOpinion: formData.customerOpinion,
|
||||
customerOpinion: formData.customerOpinion || undefined,
|
||||
};
|
||||
|
||||
let res: any;
|
||||
if (isEdit.value && editId.value) {
|
||||
res = await visitRecordApi.update(editId.value, submitData);
|
||||
await visitRecordApi.update(editId.value, submitData);
|
||||
message.success('更新成功');
|
||||
} else {
|
||||
res = await visitRecordApi.create(submitData);
|
||||
await visitRecordApi.create(submitData);
|
||||
message.success('创建成功');
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
modalVisible.value = false;
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '操作失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('操作失败');
|
||||
// 拦截器已提示
|
||||
} finally {
|
||||
modalLoading.value = false;
|
||||
}
|
||||
@@ -301,61 +411,314 @@ function handleCancel() {
|
||||
resetForm();
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
// ==================== 行内操作 ====================
|
||||
function handleMore(key: string, record: VisitRecordInfo) {
|
||||
if (key === 'delete') return confirmDelete(record);
|
||||
}
|
||||
|
||||
function confirmDelete(record: VisitRecordInfo) {
|
||||
Modal.confirm({
|
||||
title: '确认删除该回访记录?',
|
||||
content: `客户「${getCustomerName(record.customerId)}」的回访记录将被删除,此操作不可恢复。`,
|
||||
okText: '确认删除',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
async onOk() {
|
||||
try {
|
||||
const res: any = await visitRecordApi.delete(id);
|
||||
if (res.code === 200) {
|
||||
await visitRecordApi.delete(record.id);
|
||||
message.success('删除成功');
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
// 拦截器已提示
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-management {
|
||||
.visit-page {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 20px;
|
||||
padding: 20px 24px;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(180deg, #eef3f8 0%, #e8eefb 100%);
|
||||
}
|
||||
:deep(.ant-card) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
:deep(.ant-card-body) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
.search-bar {
|
||||
|
||||
.page-header {
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: #5b6b7e;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.breadcrumb .sep {
|
||||
color: #c2c6d8;
|
||||
}
|
||||
|
||||
.breadcrumb .cur {
|
||||
color: #0054cd;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.header-main {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.title-bar {
|
||||
width: 5px;
|
||||
height: 22px;
|
||||
background: #0054cd;
|
||||
border-radius: 3px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #0d1c2c;
|
||||
line-height: 1.4;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #0054cd;
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin: 6px 0 0;
|
||||
font-size: 13px;
|
||||
color: #5b6b7e;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.content-card {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #dbe4ee;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.07);
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.filter-card {
|
||||
flex-shrink: 0;
|
||||
background: #f8faff;
|
||||
border: 1px solid #dbe4ee;
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.filter-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 2fr) 1fr 1.6fr auto;
|
||||
gap: 12px 16px;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.filter-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.filter-item-actions {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #5b6b7e;
|
||||
}
|
||||
|
||||
.list-section {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 2px 4px 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.list-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.list-title .dot {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background: #0054cd;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.list-title .t {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #0d1c2c;
|
||||
}
|
||||
|
||||
.list-title .divider {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.range-text {
|
||||
font-size: 12px;
|
||||
color: #5b6b7e;
|
||||
}
|
||||
|
||||
.range-text b {
|
||||
font-family: Consolas, Menlo, monospace;
|
||||
color: #0d1c2c;
|
||||
}
|
||||
|
||||
.range-text .strong {
|
||||
color: #0054cd;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.list-extra {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
color: #5b6b7e;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-footer {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
padding-top: 12px;
|
||||
margin-top: 8px;
|
||||
border-top: 1px solid #eef1f6;
|
||||
}
|
||||
|
||||
.cust-name {
|
||||
font-weight: 600;
|
||||
color: #0d1c2c;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.plan-time {
|
||||
font-family: Consolas, Menlo, monospace;
|
||||
font-size: 12.5px;
|
||||
color: #0d1c2c;
|
||||
}
|
||||
|
||||
.sub-line {
|
||||
margin-top: 3px;
|
||||
font-size: 11.5px;
|
||||
color: #8a97a8;
|
||||
line-height: 1.5;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.row-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
:deep(.ant-table) {
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.ant-table-thead > tr > th) {
|
||||
background: #f4f7ff;
|
||||
color: #5b6b7e;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
:deep(.ant-btn-link) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
:deep(.ant-btn-primary) {
|
||||
background: #0054cd;
|
||||
border-color: #0054cd;
|
||||
}
|
||||
|
||||
:deep(.ant-btn-primary:not(:disabled):not(.ant-btn-disabled):hover) {
|
||||
background: #0947ab;
|
||||
border-color: #0947ab;
|
||||
}
|
||||
</style>
|
||||
+266
-43
@@ -41,7 +41,7 @@
|
||||
resolved "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.7.tgz"
|
||||
integrity sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==
|
||||
|
||||
"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.23.0":
|
||||
"@babel/core@^7.23.0":
|
||||
version "7.29.7"
|
||||
resolved "https://registry.npmmirror.com/@babel/core/-/core-7.29.7.tgz"
|
||||
integrity sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==
|
||||
@@ -275,14 +275,6 @@
|
||||
"@babel/types" "^7.29.7"
|
||||
debug "^4.3.1"
|
||||
|
||||
"@babel/types@^7.28.2", "@babel/types@^7.29.7":
|
||||
version "7.29.7"
|
||||
resolved "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz"
|
||||
integrity sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==
|
||||
dependencies:
|
||||
"@babel/helper-string-parser" "^7.29.7"
|
||||
"@babel/helper-validator-identifier" "^7.29.7"
|
||||
|
||||
"@babel/types@7.23.0":
|
||||
version "7.23.0"
|
||||
resolved "https://registry.npmmirror.com/@babel/types/-/types-7.23.0.tgz"
|
||||
@@ -292,6 +284,14 @@
|
||||
"@babel/helper-validator-identifier" "^7.22.20"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@babel/types@^7.28.2", "@babel/types@^7.29.7":
|
||||
version "7.29.7"
|
||||
resolved "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz"
|
||||
integrity sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==
|
||||
dependencies:
|
||||
"@babel/helper-string-parser" "^7.29.7"
|
||||
"@babel/helper-validator-identifier" "^7.29.7"
|
||||
|
||||
"@ctrl/tinycolor@^3.4.0", "@ctrl/tinycolor@^3.5.0":
|
||||
version "3.6.1"
|
||||
resolved "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz"
|
||||
@@ -307,6 +307,116 @@
|
||||
resolved "https://registry.npmmirror.com/@emotion/unitless/-/unitless-0.8.1.tgz"
|
||||
integrity sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==
|
||||
|
||||
"@esbuild/aix-ppc64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f"
|
||||
integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==
|
||||
|
||||
"@esbuild/android-arm64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052"
|
||||
integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==
|
||||
|
||||
"@esbuild/android-arm@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28"
|
||||
integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==
|
||||
|
||||
"@esbuild/android-x64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e"
|
||||
integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==
|
||||
|
||||
"@esbuild/darwin-arm64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a"
|
||||
integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==
|
||||
|
||||
"@esbuild/darwin-x64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22"
|
||||
integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==
|
||||
|
||||
"@esbuild/freebsd-arm64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e"
|
||||
integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==
|
||||
|
||||
"@esbuild/freebsd-x64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261"
|
||||
integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==
|
||||
|
||||
"@esbuild/linux-arm64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b"
|
||||
integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==
|
||||
|
||||
"@esbuild/linux-arm@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9"
|
||||
integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==
|
||||
|
||||
"@esbuild/linux-ia32@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2"
|
||||
integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==
|
||||
|
||||
"@esbuild/linux-loong64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df"
|
||||
integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==
|
||||
|
||||
"@esbuild/linux-mips64el@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe"
|
||||
integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==
|
||||
|
||||
"@esbuild/linux-ppc64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4"
|
||||
integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==
|
||||
|
||||
"@esbuild/linux-riscv64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc"
|
||||
integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==
|
||||
|
||||
"@esbuild/linux-s390x@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de"
|
||||
integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==
|
||||
|
||||
"@esbuild/linux-x64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0"
|
||||
integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==
|
||||
|
||||
"@esbuild/netbsd-x64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047"
|
||||
integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==
|
||||
|
||||
"@esbuild/openbsd-x64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70"
|
||||
integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==
|
||||
|
||||
"@esbuild/sunos-x64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b"
|
||||
integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==
|
||||
|
||||
"@esbuild/win32-arm64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d"
|
||||
integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==
|
||||
|
||||
"@esbuild/win32-ia32@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b"
|
||||
integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==
|
||||
|
||||
"@esbuild/win32-x64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz"
|
||||
@@ -405,7 +515,7 @@
|
||||
"@nodelib/fs.stat" "2.0.5"
|
||||
run-parallel "^1.1.9"
|
||||
|
||||
"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5":
|
||||
"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
|
||||
version "2.0.5"
|
||||
resolved "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
|
||||
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
|
||||
@@ -418,6 +528,121 @@
|
||||
"@nodelib/fs.scandir" "2.1.5"
|
||||
fastq "^1.6.0"
|
||||
|
||||
"@rollup/rollup-android-arm-eabi@4.60.4":
|
||||
version "4.60.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz#3a04f01e9f01392bbef5920b94aa3b88794be7ab"
|
||||
integrity sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==
|
||||
|
||||
"@rollup/rollup-android-arm64@4.60.4":
|
||||
version "4.60.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz#e371b653ceabc900790ae73f5548a0fd7cd63a70"
|
||||
integrity sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==
|
||||
|
||||
"@rollup/rollup-darwin-arm64@4.60.4":
|
||||
version "4.60.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz#2a5aa70432e39816d666d79287a7324cfc3b4e72"
|
||||
integrity sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==
|
||||
|
||||
"@rollup/rollup-darwin-x64@4.60.4":
|
||||
version "4.60.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz#c3b5b49629379cd9cdc5d841bf00ed44ebf393dd"
|
||||
integrity sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==
|
||||
|
||||
"@rollup/rollup-freebsd-arm64@4.60.4":
|
||||
version "4.60.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz#f929d8e0462fae6602fc960beeabd7287d859283"
|
||||
integrity sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==
|
||||
|
||||
"@rollup/rollup-freebsd-x64@4.60.4":
|
||||
version "4.60.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz#c01cb58031226f95d0900b1ec847f4fb32c6e809"
|
||||
integrity sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf@4.60.4":
|
||||
version "4.60.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz#f29d890c4858c8e0d3be01677eef4f6a359eed9d"
|
||||
integrity sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf@4.60.4":
|
||||
version "4.60.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz#1ebfc8eb9f66136ed2faae5f44995add5ca3c964"
|
||||
integrity sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu@4.60.4":
|
||||
version "4.60.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz#c1fa823c2c4ce46ba7f61de1a4c3fdadd4fb4e7b"
|
||||
integrity sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl@4.60.4":
|
||||
version "4.60.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz#a7f18854d0471b78bda8ea38f0891a4e059b571d"
|
||||
integrity sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu@4.60.4":
|
||||
version "4.60.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz#83658a9a4576bcce8cef85b2c78b9b649d2200c4"
|
||||
integrity sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==
|
||||
|
||||
"@rollup/rollup-linux-loong64-musl@4.60.4":
|
||||
version "4.60.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz#fd2af677ae3417bb58d57ae37dd0d84686e40244"
|
||||
integrity sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu@4.60.4":
|
||||
version "4.60.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz#6481647181c4cf8f1ddbd99f62c84cfc56c1a94a"
|
||||
integrity sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==
|
||||
|
||||
"@rollup/rollup-linux-ppc64-musl@4.60.4":
|
||||
version "4.60.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz#18610a1a1550e28a5042ca916f898419540f17f4"
|
||||
integrity sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu@4.60.4":
|
||||
version "4.60.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz#597bb80465a2621dbe0de0a41c66394a8a7e9a6e"
|
||||
integrity sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl@4.60.4":
|
||||
version "4.60.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz#a2a919a9f927ef7f24a60af77e3cb55f1ad59e4d"
|
||||
integrity sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu@4.60.4":
|
||||
version "4.60.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz#3166f6ceae7df9bbfddf9f36be1937231e13e3c6"
|
||||
integrity sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu@4.60.4":
|
||||
version "4.60.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz#23c9bf79771d804fb87415eb0767569f273261e5"
|
||||
integrity sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==
|
||||
|
||||
"@rollup/rollup-linux-x64-musl@4.60.4":
|
||||
version "4.60.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz#97941c6b94d67fe25cde0f027c10a19f2d1fdd39"
|
||||
integrity sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==
|
||||
|
||||
"@rollup/rollup-openbsd-x64@4.60.4":
|
||||
version "4.60.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz#7aeb7d92e2cd1d399f56daf75c39040b777b6c77"
|
||||
integrity sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==
|
||||
|
||||
"@rollup/rollup-openharmony-arm64@4.60.4":
|
||||
version "4.60.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz#925de61ae83bf99aa636e8acea87432e8c0ffaab"
|
||||
integrity sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc@4.60.4":
|
||||
version "4.60.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz#888ab83842721491044c46a7407e1f38f3235bb4"
|
||||
integrity sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc@4.60.4":
|
||||
version "4.60.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz#fa30ac24e3f0232139d2a47500560a28695764d4"
|
||||
integrity sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu@4.60.4":
|
||||
version "4.60.4"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz"
|
||||
@@ -441,7 +666,7 @@
|
||||
resolved "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz"
|
||||
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
|
||||
|
||||
"@types/node@^18.0.0 || >=20.0.0", "@types/node@^22.15.30":
|
||||
"@types/node@^22.15.30":
|
||||
version "22.19.19"
|
||||
resolved "https://registry.npmmirror.com/@types/node/-/node-22.19.19.tgz"
|
||||
integrity sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==
|
||||
@@ -463,7 +688,7 @@
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^1.3.0"
|
||||
|
||||
"@typescript-eslint/parser@^7.0.0", "@typescript-eslint/parser@^7.0.1":
|
||||
"@typescript-eslint/parser@^7.0.1":
|
||||
version "7.18.0"
|
||||
resolved "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-7.18.0.tgz"
|
||||
integrity sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==
|
||||
@@ -539,14 +764,14 @@
|
||||
resolved "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz"
|
||||
integrity sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==
|
||||
|
||||
"@volar/language-core@~1.11.1", "@volar/language-core@1.11.1":
|
||||
"@volar/language-core@1.11.1", "@volar/language-core@~1.11.1":
|
||||
version "1.11.1"
|
||||
resolved "https://registry.npmmirror.com/@volar/language-core/-/language-core-1.11.1.tgz"
|
||||
integrity sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==
|
||||
dependencies:
|
||||
"@volar/source-map" "1.11.1"
|
||||
|
||||
"@volar/source-map@~1.11.1", "@volar/source-map@1.11.1":
|
||||
"@volar/source-map@1.11.1", "@volar/source-map@~1.11.1":
|
||||
version "1.11.1"
|
||||
resolved "https://registry.npmmirror.com/@volar/source-map/-/source-map-1.11.1.tgz"
|
||||
integrity sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==
|
||||
@@ -603,7 +828,7 @@
|
||||
estree-walker "^2.0.2"
|
||||
source-map-js "^1.2.1"
|
||||
|
||||
"@vue/compiler-dom@^3.3.0", "@vue/compiler-dom@^3.3.4", "@vue/compiler-dom@3.5.34":
|
||||
"@vue/compiler-dom@3.5.34", "@vue/compiler-dom@^3.3.0", "@vue/compiler-dom@^3.3.4":
|
||||
version "3.5.34"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.34.tgz"
|
||||
integrity sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==
|
||||
@@ -611,7 +836,7 @@
|
||||
"@vue/compiler-core" "3.5.34"
|
||||
"@vue/shared" "3.5.34"
|
||||
|
||||
"@vue/compiler-sfc@^3.5.18", "@vue/compiler-sfc@3.5.34":
|
||||
"@vue/compiler-sfc@3.5.34", "@vue/compiler-sfc@^3.5.18":
|
||||
version "3.5.34"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.34.tgz"
|
||||
integrity sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==
|
||||
@@ -696,7 +921,7 @@
|
||||
"@vue/reactivity" "3.5.34"
|
||||
"@vue/shared" "3.5.34"
|
||||
|
||||
"@vue/runtime-dom@^3.4.15", "@vue/runtime-dom@3.5.34":
|
||||
"@vue/runtime-dom@3.5.34", "@vue/runtime-dom@^3.4.15":
|
||||
version "3.5.34"
|
||||
resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.34.tgz"
|
||||
integrity sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==
|
||||
@@ -714,7 +939,7 @@
|
||||
"@vue/compiler-ssr" "3.5.34"
|
||||
"@vue/shared" "3.5.34"
|
||||
|
||||
"@vue/shared@^3.3.0", "@vue/shared@^3.5.18", "@vue/shared@3.5.34":
|
||||
"@vue/shared@3.5.34", "@vue/shared@^3.3.0", "@vue/shared@^3.5.18":
|
||||
version "3.5.34"
|
||||
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.34.tgz"
|
||||
integrity sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==
|
||||
@@ -729,7 +954,7 @@ acorn-jsx@^5.3.2:
|
||||
resolved "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
|
||||
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
|
||||
|
||||
"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.14.0, acorn@^8.9.0:
|
||||
acorn@^8.14.0, acorn@^8.9.0:
|
||||
version "8.16.0"
|
||||
resolved "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz"
|
||||
integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==
|
||||
@@ -902,7 +1127,7 @@ braces@^3.0.3, braces@~3.0.2:
|
||||
dependencies:
|
||||
fill-range "^7.1.1"
|
||||
|
||||
browserslist@^4.24.0, browserslist@^4.28.2, "browserslist@>= 4.21.0":
|
||||
browserslist@^4.24.0, browserslist@^4.28.2:
|
||||
version "4.28.2"
|
||||
resolved "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.2.tgz"
|
||||
integrity sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==
|
||||
@@ -1056,7 +1281,7 @@ de-indent@^1.0.2:
|
||||
resolved "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz"
|
||||
integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==
|
||||
|
||||
debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@4:
|
||||
debug@4, debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4:
|
||||
version "4.4.3"
|
||||
resolved "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz"
|
||||
integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==
|
||||
@@ -1116,7 +1341,7 @@ dunder-proto@^1.0.1:
|
||||
es-errors "^1.3.0"
|
||||
gopd "^1.2.0"
|
||||
|
||||
echarts@^6.0.0, echarts@^6.1.0:
|
||||
echarts@^6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.npmmirror.com/echarts/-/echarts-6.1.0.tgz"
|
||||
integrity sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==
|
||||
@@ -1234,7 +1459,7 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4
|
||||
resolved "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz"
|
||||
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
|
||||
|
||||
"eslint@^6.0.0 || ^7.0.0 || >=8.0.0", "eslint@^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", eslint@^8.56.0, eslint@>=6.0.0:
|
||||
eslint@^8.56.0:
|
||||
version "8.57.1"
|
||||
resolved "https://registry.npmmirror.com/eslint/-/eslint-8.57.1.tgz"
|
||||
integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==
|
||||
@@ -1416,6 +1641,11 @@ fs.realpath@^1.0.0:
|
||||
resolved "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz"
|
||||
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
|
||||
|
||||
fsevents@~2.3.2, fsevents@~2.3.3:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
|
||||
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
|
||||
|
||||
function-bind@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz"
|
||||
@@ -1651,7 +1881,7 @@ isexe@^2.0.0:
|
||||
resolved "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz"
|
||||
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
|
||||
|
||||
jiti@^1.21.7, jiti@>=1.21.0:
|
||||
jiti@^1.21.7:
|
||||
version "1.21.7"
|
||||
resolved "https://registry.npmmirror.com/jiti/-/jiti-1.21.7.tgz"
|
||||
integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==
|
||||
@@ -1705,7 +1935,7 @@ kolorist@^1.8.0:
|
||||
resolved "https://registry.npmmirror.com/kolorist/-/kolorist-1.8.0.tgz"
|
||||
integrity sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==
|
||||
|
||||
less@*, less@^4.6.4:
|
||||
less@^4.6.4:
|
||||
version "4.6.4"
|
||||
resolved "https://registry.npmmirror.com/less/-/less-4.6.4.tgz"
|
||||
integrity sha512-OJmO5+HxZLLw0RLzkqaNHzcgEAQG7C0y3aMbwtCzIUFZsLMNNq/1IdAdHEycQ58CwUO3jPTHmoN+tE5I7FQxNg==
|
||||
@@ -1837,14 +2067,7 @@ minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimatch@^9.0.3:
|
||||
version "9.0.9"
|
||||
resolved "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz"
|
||||
integrity sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==
|
||||
dependencies:
|
||||
brace-expansion "^2.0.2"
|
||||
|
||||
minimatch@^9.0.4:
|
||||
minimatch@^9.0.3, minimatch@^9.0.4:
|
||||
version "9.0.9"
|
||||
resolved "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz"
|
||||
integrity sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==
|
||||
@@ -2015,7 +2238,7 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
|
||||
resolved "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz"
|
||||
integrity sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==
|
||||
|
||||
"picomatch@^3 || ^4", picomatch@^4.0.4:
|
||||
picomatch@^4.0.4:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz"
|
||||
integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==
|
||||
@@ -2085,7 +2308,7 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0:
|
||||
resolved "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz"
|
||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
||||
|
||||
postcss@^8.0.0, postcss@^8.1.0, postcss@^8.2.14, postcss@^8.4.21, postcss@^8.4.35, postcss@^8.4.43, postcss@^8.4.47, postcss@^8.5.14, postcss@>=8.0.9:
|
||||
postcss@^8.4.35, postcss@^8.4.43, postcss@^8.4.47, postcss@^8.5.14:
|
||||
version "8.5.15"
|
||||
resolved "https://registry.npmmirror.com/postcss/-/postcss-8.5.15.tgz"
|
||||
integrity sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==
|
||||
@@ -2416,16 +2639,16 @@ ts-interface-checker@^0.1.9:
|
||||
resolved "https://registry.npmmirror.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz"
|
||||
integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==
|
||||
|
||||
tslib@^2.8.1:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz"
|
||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||
|
||||
tslib@2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz"
|
||||
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
|
||||
|
||||
tslib@^2.8.1:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz"
|
||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||
|
||||
type-check@^0.4.0, type-check@~0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz"
|
||||
@@ -2438,7 +2661,7 @@ type-fest@^0.20.2:
|
||||
resolved "https://registry.npmmirror.com/type-fest/-/type-fest-0.20.2.tgz"
|
||||
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
|
||||
|
||||
typescript@*, typescript@>=4.2.0, typescript@>=4.5.0, typescript@~5.3.3, typescript@5.x:
|
||||
typescript@~5.3.3:
|
||||
version "5.3.3"
|
||||
resolved "https://registry.npmmirror.com/typescript/-/typescript-5.3.3.tgz"
|
||||
integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==
|
||||
@@ -2508,7 +2731,7 @@ vite-plugin-vue-dev-locator@1.0.3:
|
||||
kolorist "^1.8.0"
|
||||
magic-string "^0.30.4"
|
||||
|
||||
"vite@^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0", "vite@^5.0.0 || ^6.0.0", vite@^5.0.12, vite@>=4.0.0:
|
||||
vite@^5.0.12:
|
||||
version "5.4.21"
|
||||
resolved "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz"
|
||||
integrity sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==
|
||||
@@ -2568,7 +2791,7 @@ vue-types@^3.0.0:
|
||||
dependencies:
|
||||
is-plain-object "3.0.1"
|
||||
|
||||
vue@^3.0.0, vue@^3.2.25, vue@^3.3.0, vue@^3.4.0, vue@^3.4.15, vue@^3.5.0, vue@^3.5.11, vue@>=3.0.1, vue@>=3.0.3, vue@>=3.2.0, vue@3.5.34:
|
||||
vue@^3.4.15:
|
||||
version "3.5.34"
|
||||
resolved "https://registry.npmmirror.com/vue/-/vue-3.5.34.tgz"
|
||||
integrity sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==
|
||||
|
||||
+10
-8
@@ -106,9 +106,10 @@ ON CONFLICT (id) DO UPDATE SET
|
||||
permission = EXCLUDED.permission,
|
||||
visible = EXCLUDED.visible;
|
||||
|
||||
-- 初始菜单(项目域子菜单 + 客户扩展子菜单,id:24-31 + 空间拓扑 32)
|
||||
-- 初始菜单(项目域子菜单 + 客户扩展子菜单,id:24-31 + 空间拓扑 32 + 安装派单 33)
|
||||
-- 项目域:客户拜访(customer:visit) / 项目勘察(project:survey) / 项目方案(project:plan)
|
||||
-- 合同管理(project:contract) / 项目验收(project:acceptance) / 回访记录(project:visit)
|
||||
-- 合同管理(project:contract) / 安装派单(project:install)
|
||||
-- 项目验收(project:acceptance) / 回访记录(project:visit)
|
||||
-- 客户域:空间拓扑与用电架构(customer:topology)
|
||||
-- 注:客户拜访归属“项目管理”,但仍沿用 /customer/visit 路径与 customer:visit 权限码
|
||||
INSERT INTO sys_menu (id, parent_id, menu_name, menu_path, component, icon, sort_order, menu_type, permission, visible) VALUES
|
||||
@@ -116,8 +117,9 @@ INSERT INTO sys_menu (id, parent_id, menu_name, menu_path, component, icon, sort
|
||||
(27, 2, '项目勘察', '/project/survey', 'project/survey/index', 'xt-xiangmukancha', 2, 2, 'project:survey', 1),
|
||||
(28, 2, '项目方案', '/project/plan', 'project/plan/index', 'xt-xiangmufangan', 3, 2, 'project:plan', 1),
|
||||
(29, 2, '合同管理', '/project/contract', 'project/contract/index', 'FileDoneOutlined', 4, 2, 'project:contract', 1),
|
||||
(30, 2, '项目验收', '/project/acceptance', 'project/acceptance/index', 'ThunderboltOutlined', 5, 2, 'project:acceptance',1),
|
||||
(31, 2, '回访记录', '/project/visit', 'project/visit/index', 'AuditOutlined', 6, 2, 'project:visit', 1),
|
||||
(33, 2, '项目安装', '/project/install', 'project/install/index', 'ToolOutlined', 5, 2, 'project:install', 1),
|
||||
(30, 2, '项目验收', '/project/acceptance', 'project/acceptance/index', 'ThunderboltOutlined', 6, 2, 'project:acceptance',1),
|
||||
(31, 2, '回访记录', '/project/visit', 'project/visit/index', 'AuditOutlined', 7, 2, 'project:visit', 1),
|
||||
(32, 1, '空间拓扑与用电架构', '/customer/topology', 'customer/topology/index', 'xt-topology', 5, 2, 'customer:topology',1)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
parent_id = EXCLUDED.parent_id,
|
||||
@@ -131,7 +133,7 @@ ON CONFLICT (id) DO UPDATE SET
|
||||
visible = EXCLUDED.visible;
|
||||
|
||||
-- 重置 sys_menu 自增序列
|
||||
SELECT setval('sys_menu_id_seq', 32, true);
|
||||
SELECT setval('sys_menu_id_seq', 33, true);
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 3. 初始字典(sys_dict 无业务唯一键,清空重建以支持幂等执行)
|
||||
@@ -186,16 +188,16 @@ INSERT INTO sys_user_role (user_id, role_id) VALUES (1, 1)
|
||||
ON CONFLICT (user_id, role_id) DO NOTHING;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 6. 角色-菜单绑定:ADMIN 拥有全部 31 条菜单,SALESMAN 拥有客户域+项目+统计报表
|
||||
-- 6. 角色-菜单绑定:ADMIN 拥有全部 32 条菜单,SALESMAN 拥有客户域+项目+统计报表
|
||||
-- WORKER、CUSTOMER 不绑定任何菜单
|
||||
-- ------------------------------------------------------------
|
||||
INSERT INTO sys_role_menu (role_id, menu_id) VALUES
|
||||
(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9), (1, 10), (1, 11),
|
||||
(1, 12), (1, 13), (1, 14), (1, 15), (1, 16), (1, 17), (1, 18), (1, 19), (1, 20), (1, 21), (1, 22), (1, 23),
|
||||
(1, 24), (1, 27), (1, 28), (1, 29), (1, 30), (1, 31), (1, 32),
|
||||
(1, 24), (1, 27), (1, 28), (1, 29), (1, 30), (1, 31), (1, 32), (1, 33),
|
||||
(2, 1), (2, 2), (2, 5),
|
||||
(2, 12), (2, 13), (2, 14), (2, 15), (2, 16), (2, 17), (2, 18), (2, 19), (2, 20), (2, 21), (2, 22), (2, 23),
|
||||
(2, 24), (2, 27), (2, 28), (2, 29), (2, 30), (2, 31), (2, 32)
|
||||
(2, 24), (2, 27), (2, 28), (2, 29), (2, 30), (2, 31), (2, 32), (2, 33)
|
||||
ON CONFLICT (role_id, menu_id) DO NOTHING;
|
||||
|
||||
-- 移除已废弃的菜单(客户建筑/客户房间,id=25/26)及其角色绑定(幂等清理,确保每次执行都收敛)
|
||||
|
||||
Generated
+648
-3
@@ -8,11 +8,14 @@
|
||||
"name": "smart-grid-iot-server",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.1131.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.1131.0",
|
||||
"@midwayjs/cache-manager": "^3.19.0",
|
||||
"@midwayjs/core": "^3.19.0",
|
||||
"@midwayjs/koa": "^3.19.0",
|
||||
"@midwayjs/logger": "^4.0.1",
|
||||
"@midwayjs/sequelize": "^3.19.0",
|
||||
"@midwayjs/upload": "^3.20.0",
|
||||
"@midwayjs/validate": "^3.19.0",
|
||||
"@types/validator": "^13.15.10",
|
||||
"bcryptjs": "^2.4.3",
|
||||
@@ -48,6 +51,331 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/checksums": {
|
||||
"version": "3.1001.1",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1001.1.tgz",
|
||||
"integrity": "sha512-x12Q17KYlJAd3nKf8LV5LV0vt8sh8/6YfQLGPtrGnQf/tW4jqxPGq5GPpuVitpQYM3eUR4XB7CbxZf751NMbLw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.978.1",
|
||||
"@aws-sdk/types": "^3.974.6",
|
||||
"@smithy/core": "^3.35.0",
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/client-s3": {
|
||||
"version": "3.1141.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1141.0.tgz",
|
||||
"integrity": "sha512-uOVH37xGLenAdJkCPCin/JJG2PgWrFcSsDnQ9+C9Zq8N9Oalo5ol4xmn5fG28iWAlA/b/9boQZgHbMh+UsIhcg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/checksums": "^3.1001.1",
|
||||
"@aws-sdk/core": "^3.978.1",
|
||||
"@aws-sdk/credential-provider-node": "^3.972.84",
|
||||
"@aws-sdk/middleware-sdk-s3": "^3.972.77",
|
||||
"@aws-sdk/signature-v4-multi-region": "^3.996.47",
|
||||
"@aws-sdk/types": "^3.974.6",
|
||||
"@smithy/core": "^3.35.0",
|
||||
"@smithy/fetch-http-handler": "^5.8.0",
|
||||
"@smithy/node-http-handler": "^4.12.1",
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/core": {
|
||||
"version": "3.978.1",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.978.1.tgz",
|
||||
"integrity": "sha512-LbY9aGsEiznDWmUc30Nwv3aIX/+dbwTx8KfS0yOC3NPYMO+O91e6jkT1azf34FwjOndq8/Q+RcVVZz5xnerwdg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/types": "^3.974.6",
|
||||
"@aws-sdk/xml-builder": "^3.972.41",
|
||||
"@aws/lambda-invoke-store": "^0.3.0",
|
||||
"@smithy/core": "^3.35.0",
|
||||
"@smithy/signature-v4": "^5.7.3",
|
||||
"@smithy/types": "^4.19.0",
|
||||
"bowser": "^2.11.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-env": {
|
||||
"version": "3.972.72",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.72.tgz",
|
||||
"integrity": "sha512-xTKO/FWJPozTIXbozVnVGoNBhaGba8TBcx+KyUjRVeOlXE+dUc7GTR1cLvu0uTdIdmemzaFbqqCshXeZA1fZew==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.978.1",
|
||||
"@aws-sdk/types": "^3.974.6",
|
||||
"@smithy/core": "^3.35.0",
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-http": {
|
||||
"version": "3.972.74",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.74.tgz",
|
||||
"integrity": "sha512-u91E/hT8f4d1xy0Jl7VG4nVKJ3lxbrZkoBTeSVoJdWBiSEUMwMS/9+e0H/aJVQV//Lt5wuzP+E69v4aRSsNTmw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.978.1",
|
||||
"@aws-sdk/types": "^3.974.6",
|
||||
"@smithy/core": "^3.35.0",
|
||||
"@smithy/fetch-http-handler": "^5.8.0",
|
||||
"@smithy/node-http-handler": "^4.12.1",
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-ini": {
|
||||
"version": "3.973.17",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.17.tgz",
|
||||
"integrity": "sha512-ged4KXdBkvIC81bLvNHHuQKdKak/VXhQTR1NWYTTqW0474nlmsxy9O/vlgTIohDDWH3xpBdtVMZRyjb+DnocDA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.978.1",
|
||||
"@aws-sdk/credential-provider-env": "^3.972.72",
|
||||
"@aws-sdk/credential-provider-http": "^3.972.74",
|
||||
"@aws-sdk/credential-provider-login": "^3.972.79",
|
||||
"@aws-sdk/credential-provider-process": "^3.972.72",
|
||||
"@aws-sdk/credential-provider-sso": "^3.973.16",
|
||||
"@aws-sdk/credential-provider-web-identity": "^3.972.78",
|
||||
"@aws-sdk/nested-clients": "^3.997.46",
|
||||
"@aws-sdk/types": "^3.974.6",
|
||||
"@smithy/core": "^3.35.0",
|
||||
"@smithy/credential-provider-imds": "^4.5.2",
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-login": {
|
||||
"version": "3.972.79",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.79.tgz",
|
||||
"integrity": "sha512-L+Z85anONJd8MaiuraO4wRxATCdEejBZ3K3eymzWI5JPXa9sOS9CkIm72PBKqXKX+Z9p9NGMX5AIMXm0LEflgw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.978.1",
|
||||
"@aws-sdk/nested-clients": "^3.997.46",
|
||||
"@aws-sdk/types": "^3.974.6",
|
||||
"@smithy/core": "^3.35.0",
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-node": {
|
||||
"version": "3.972.84",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.84.tgz",
|
||||
"integrity": "sha512-oHt854odINVwzwsh+c5x69j0ajm4DbqqqVJ+O1ECsCIZeMDAbzFpXItaqP7UZstJj/ATdTk/KFSH0LaNAgV+kA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-provider-env": "^3.972.72",
|
||||
"@aws-sdk/credential-provider-http": "^3.972.74",
|
||||
"@aws-sdk/credential-provider-ini": "^3.973.17",
|
||||
"@aws-sdk/credential-provider-process": "^3.972.72",
|
||||
"@aws-sdk/credential-provider-sso": "^3.973.16",
|
||||
"@aws-sdk/credential-provider-web-identity": "^3.972.78",
|
||||
"@aws-sdk/types": "^3.974.6",
|
||||
"@smithy/core": "^3.35.0",
|
||||
"@smithy/credential-provider-imds": "^4.5.2",
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-process": {
|
||||
"version": "3.972.72",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.72.tgz",
|
||||
"integrity": "sha512-rLIp2xbMjX/k9/od7APpqq1ZgXXnV0pOL1Th3ZsL8Wu0TRtBsDTVS8iPqcfRFcHakFxPvR04OSTv2ka2qOb/2A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.978.1",
|
||||
"@aws-sdk/types": "^3.974.6",
|
||||
"@smithy/core": "^3.35.0",
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-sso": {
|
||||
"version": "3.973.16",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.16.tgz",
|
||||
"integrity": "sha512-IGihaJfFZYacJJr/odqILCoK7W/mvrZ7cuK7ECn3sAu4vLC6u0V8bS7mCGbdugJ8Aum2tnvqmx0F2MRFp2rn9g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.978.1",
|
||||
"@aws-sdk/nested-clients": "^3.997.46",
|
||||
"@aws-sdk/token-providers": "3.1138.0",
|
||||
"@aws-sdk/types": "^3.974.6",
|
||||
"@smithy/core": "^3.35.0",
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-web-identity": {
|
||||
"version": "3.972.78",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.78.tgz",
|
||||
"integrity": "sha512-/y9WvNtlcPBGLR0qc1a+9J/xtYZfVczvLUOuXaVWylzttH7ewsxwHtjmiJSolNrVSDorIxHGHMU61CbonRkmwA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.978.1",
|
||||
"@aws-sdk/nested-clients": "^3.997.46",
|
||||
"@aws-sdk/types": "^3.974.6",
|
||||
"@smithy/core": "^3.35.0",
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/middleware-sdk-s3": {
|
||||
"version": "3.972.77",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.77.tgz",
|
||||
"integrity": "sha512-E7W2UOeUoc+lg3uIfR/dM7ZwusHwhBQrKMnlkRv4EXRR+C0YtV1pg25xC7GdZIhXH+NAMgZPCbE7o5to2cjFiw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.978.1",
|
||||
"@aws-sdk/signature-v4-multi-region": "^3.996.47",
|
||||
"@aws-sdk/types": "^3.974.6",
|
||||
"@smithy/core": "^3.35.0",
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/nested-clients": {
|
||||
"version": "3.997.46",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.46.tgz",
|
||||
"integrity": "sha512-oRxtBcka/JGHGs9l9p9IVajGoTP8vTPmoAzdHGy4Qcy9P5vPnDf6nhIeM/COQNY9k/OahImTRaLkHftoXvfcmQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.978.1",
|
||||
"@aws-sdk/signature-v4-multi-region": "^3.996.47",
|
||||
"@aws-sdk/types": "^3.974.6",
|
||||
"@smithy/core": "^3.35.0",
|
||||
"@smithy/fetch-http-handler": "^5.8.0",
|
||||
"@smithy/node-http-handler": "^4.12.1",
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/s3-request-presigner": {
|
||||
"version": "3.1141.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1141.0.tgz",
|
||||
"integrity": "sha512-HHHcP3gvLqeY9GPptpxLvA6VwDu+MWKCoaGGm0UtSUBh1hC7xJ9nNHoybgFfstC4lrj+k8oD6a1rWbGBWKYdag==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.978.1",
|
||||
"@aws-sdk/signature-v4-multi-region": "^3.996.47",
|
||||
"@aws-sdk/types": "^3.974.6",
|
||||
"@smithy/core": "^3.35.0",
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/signature-v4-multi-region": {
|
||||
"version": "3.996.47",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.47.tgz",
|
||||
"integrity": "sha512-Zk08macMvQTHzQJCLJVkOlviVoqwYMrpXv4lmLN7b7sAbiMoOK7Go0NYdR5UeF+MW8LIbRmwrNy9u/5VvX1U5g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/types": "^3.974.6",
|
||||
"@smithy/signature-v4": "^5.7.3",
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/token-providers": {
|
||||
"version": "3.1138.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1138.0.tgz",
|
||||
"integrity": "sha512-GpyAr0DD63YOEmYFM6Df+gJuIgC92MMTiBK4FTKfxii5MJ9ge20epR7LyroulscYlG89J+ZB2ivFDPjvfQhzdw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.978.1",
|
||||
"@aws-sdk/nested-clients": "^3.997.46",
|
||||
"@aws-sdk/types": "^3.974.6",
|
||||
"@smithy/core": "^3.35.0",
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/types": {
|
||||
"version": "3.974.6",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.6.tgz",
|
||||
"integrity": "sha512-v/clNZzZnDxGyvpHMOGpJKVXFAExJzUNAAjaWGdcx8QAcXLGwTaOkw33p5SHAi0YAioK32xB3hWwOekRVfmfKg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/xml-builder": {
|
||||
"version": "3.972.41",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.41.tgz",
|
||||
"integrity": "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws/lambda-invoke-store": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz",
|
||||
"integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
||||
@@ -1623,6 +1951,19 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@midwayjs/upload": {
|
||||
"version": "3.20.24",
|
||||
"resolved": "https://registry.npmjs.org/@midwayjs/upload/-/upload-3.20.24.tgz",
|
||||
"integrity": "sha512-DOjmQ7sCHvzkyebZjWAEq6fp7LvyQXsfNp3zQs5CA8a2d64OQpJbgso6iXeZVB2OwkLV592Wuq0UfHXXhvgXsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"file-type": "16.5.4",
|
||||
"raw-body": "2.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@midwayjs/validate": {
|
||||
"version": "3.20.24",
|
||||
"resolved": "https://registry.npmmirror.com/@midwayjs/validate/-/validate-3.20.24.tgz",
|
||||
@@ -1752,6 +2093,93 @@
|
||||
"@sinonjs/commons": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/core": {
|
||||
"version": "3.35.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.35.0.tgz",
|
||||
"integrity": "sha512-zRMhfkByhT2snNdr1si24vJitU6Cr9ix2MikUfWmkAgp4jrNP0GcKSP5YvwQ+TlI8AZXER5QOGJn3JsVtSD9/A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/credential-provider-imds": {
|
||||
"version": "4.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.5.2.tgz",
|
||||
"integrity": "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/core": "^3.33.2",
|
||||
"@smithy/types": "^4.17.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/fetch-http-handler": {
|
||||
"version": "5.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.8.0.tgz",
|
||||
"integrity": "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/core": "^3.33.3",
|
||||
"@smithy/types": "^4.18.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/node-http-handler": {
|
||||
"version": "4.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.12.1.tgz",
|
||||
"integrity": "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/core": "^3.33.3",
|
||||
"@smithy/types": "^4.18.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/signature-v4": {
|
||||
"version": "5.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.7.4.tgz",
|
||||
"integrity": "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/core": "^3.35.0",
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/types": {
|
||||
"version": "4.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.19.0.tgz",
|
||||
"integrity": "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tokenizer/token": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
|
||||
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmmirror.com/@tsconfig/node10/-/node10-1.0.12.tgz",
|
||||
@@ -2490,6 +2918,18 @@
|
||||
"ncc": "dist/ncc/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"event-target-shim": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.5"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
|
||||
@@ -2795,6 +3235,26 @@
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.10.32",
|
||||
"resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz",
|
||||
@@ -2827,6 +3287,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/bowser": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz",
|
||||
"integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.15",
|
||||
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.15.tgz",
|
||||
@@ -2907,6 +3373,30 @@
|
||||
"node-int64": "^0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-equal-constant-time": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
@@ -3835,6 +4325,24 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/event-target-shim": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/execa": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/execa/-/execa-5.1.1.tgz",
|
||||
@@ -3936,6 +4444,23 @@
|
||||
"bser": "2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/file-type": {
|
||||
"version": "16.5.4",
|
||||
"resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz",
|
||||
"integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readable-web-to-node-stream": "^3.0.0",
|
||||
"strtok3": "^6.2.4",
|
||||
"token-types": "^4.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/file-type?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/filelist": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/filelist/-/filelist-1.0.6.tgz",
|
||||
@@ -4390,6 +4915,26 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz",
|
||||
@@ -6323,6 +6868,19 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/peek-readable": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz",
|
||||
"integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Borewit"
|
||||
}
|
||||
},
|
||||
"node_modules/pg": {
|
||||
"version": "8.23.0",
|
||||
"resolved": "https://registry.npmmirror.com/pg/-/pg-8.23.0.tgz",
|
||||
@@ -6533,6 +7091,15 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prompts": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmmirror.com/prompts/-/prompts-2.4.2.tgz",
|
||||
@@ -6614,7 +7181,6 @@
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.2.tgz",
|
||||
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
@@ -6630,7 +7196,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz",
|
||||
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "2.0.0",
|
||||
@@ -6647,7 +7212,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
@@ -6660,6 +7224,38 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
|
||||
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"abort-controller": "^3.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"events": "^3.3.0",
|
||||
"process": "^0.11.10",
|
||||
"string_decoder": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-web-to-node-stream": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz",
|
||||
"integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readable-stream": "^4.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Borewit"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz",
|
||||
@@ -7190,6 +7786,15 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-length": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/string-length/-/string-length-4.0.2.tgz",
|
||||
@@ -7265,6 +7870,23 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/strtok3": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz",
|
||||
"integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tokenizer/token": "^0.3.0",
|
||||
"peek-readable": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Borewit"
|
||||
}
|
||||
},
|
||||
"node_modules/superagent": {
|
||||
"version": "8.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/superagent/-/superagent-8.1.2.tgz",
|
||||
@@ -7371,6 +7993,23 @@
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/token-types": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz",
|
||||
"integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tokenizer/token": "^0.3.0",
|
||||
"ieee754": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Borewit"
|
||||
}
|
||||
},
|
||||
"node_modules/toposort-class": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/toposort-class/-/toposort-class-1.0.1.tgz",
|
||||
@@ -7507,6 +8146,12 @@
|
||||
"json5": "lib/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsscmp": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/tsscmp/-/tsscmp-1.0.6.tgz",
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Controller, Get, Post, Put, Del, Inject, Query, Body, Param } from "@midwayjs/core";
|
||||
import { Context } from "@midwayjs/koa";
|
||||
import { InstallOrderService } from "../service/install_order.service";
|
||||
|
||||
@Controller("/api/install-orders")
|
||||
export class InstallOrderController {
|
||||
@Inject() ctx!: Context;
|
||||
@Inject() installOrderService!: InstallOrderService;
|
||||
|
||||
@Get("/") async list(@Query() query: any) { return this.installOrderService.list(query, this.ctx); }
|
||||
@Get("/:id") async detail(@Param("id") id: string) { return this.installOrderService.detail(Number(id), this.ctx); }
|
||||
@Post("/") async create(@Body() body: any) { return this.installOrderService.create(body, this.ctx); }
|
||||
@Put("/:id") async update(@Param("id") id: string, @Body() body: any) { return this.installOrderService.update(Number(id), body, this.ctx); }
|
||||
@Put("/:id/complete") async complete(@Param("id") id: string) { return this.installOrderService.complete(Number(id), this.ctx); }
|
||||
@Del("/:id") async delete(@Param("id") id: string) { return this.installOrderService.delete(Number(id), this.ctx); }
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsInt,
|
||||
IsArray,
|
||||
IsDateString,
|
||||
IsIn,
|
||||
ArrayNotEmpty,
|
||||
Matches,
|
||||
Min,
|
||||
Max,
|
||||
MaxLength,
|
||||
ArrayMaxSize,
|
||||
ArrayMinSize
|
||||
} from "class-validator";
|
||||
|
||||
export class InstallOrderQueryDTO {
|
||||
@IsOptional() @IsInt() @Min(1) page?: number = 1;
|
||||
@IsOptional() @IsInt() @Min(1) @Max(200) pageSize?: number = 10;
|
||||
@IsOptional() @IsString() @MaxLength(64) keyword?: string;
|
||||
@IsOptional() @IsInt() customerId?: number;
|
||||
@IsOptional() @IsInt() @IsIn([0, 1]) status?: number;
|
||||
// 派单日期范围 [startDate, endDate],YYYY-MM-DD
|
||||
@IsOptional() @IsArray() @ArrayMinSize(2) @ArrayMaxSize(2) @IsDateString({}, { each: true }) dateRange?: string[];
|
||||
}
|
||||
|
||||
export class InstallOrderCreateDTO {
|
||||
@IsNotEmpty({ message: "所属客户不能为空" }) @IsInt() customerId!: number;
|
||||
@IsNotEmpty({ message: "关联方案不能为空" }) @IsInt() planId!: number;
|
||||
@IsNotEmpty({ message: "安装人不能为空" }) @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) installer!: string;
|
||||
@IsNotEmpty({ message: "派单日期不能为空" }) @IsDateString() dispatchDate!: string;
|
||||
|
||||
@IsOptional() @IsInt() workerId?: number;
|
||||
@IsOptional() @IsDateString() startDate?: string;
|
||||
@IsOptional() @IsDateString() endDate?: string;
|
||||
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) remark?: string;
|
||||
@IsOptional() @IsInt() @IsIn([0, 1]) status?: number;
|
||||
}
|
||||
|
||||
export class InstallOrderUpdateDTO {
|
||||
@IsOptional() @IsInt() customerId?: number;
|
||||
@IsOptional() @IsInt() planId?: number;
|
||||
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) installer?: string;
|
||||
@IsOptional() @IsDateString() dispatchDate?: string;
|
||||
@IsOptional() @IsInt() workerId?: number;
|
||||
@IsOptional() @IsDateString() startDate?: string;
|
||||
@IsOptional() @IsDateString() endDate?: string;
|
||||
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) remark?: string;
|
||||
@IsOptional() @IsInt() @IsIn([0, 1]) status?: number;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Column, Model, Table, DataType, CreatedAt, ForeignKey, BelongsTo } from "sequelize-typescript";
|
||||
import Customer from "./customer.entity";
|
||||
import Worker from "./worker.entity";
|
||||
import ProjectPlan from "./project_plan.entity";
|
||||
|
||||
@Table({
|
||||
tableName: "install_order",
|
||||
comment: "安装派单信息表",
|
||||
timestamps: false,
|
||||
indexes: [
|
||||
{ name: "idx_install_customer", fields: ["customer_id"] },
|
||||
{ name: "idx_install_worker", fields: ["worker_id"] },
|
||||
{ name: "idx_install_plan", fields: ["plan_id"] },
|
||||
{ name: "idx_install_status", fields: ["status"] },
|
||||
{ name: "idx_install_dispatch_date", fields: ["dispatch_date"] }
|
||||
]
|
||||
})
|
||||
export default class InstallOrder extends Model {
|
||||
@Column({ type: DataType.BIGINT, autoIncrement: true, primaryKey: true })
|
||||
id!: number;
|
||||
|
||||
@Column({ type: DataType.STRING(64), allowNull: false, unique: true, field: "install_no", comment: "安装单号(唯一)" })
|
||||
installNo!: string;
|
||||
|
||||
@ForeignKey(() => Customer)
|
||||
@Column({ type: DataType.BIGINT, allowNull: false, field: "customer_id", comment: "客户ID" })
|
||||
customerId!: number;
|
||||
|
||||
@Column({ type: DataType.STRING(64), comment: "安装人" })
|
||||
installer!: string;
|
||||
|
||||
@ForeignKey(() => Worker)
|
||||
@Column({ type: DataType.BIGINT, field: "worker_id", comment: "共享电工ID" })
|
||||
workerId!: number;
|
||||
|
||||
@Column({ type: DataType.DATEONLY, field: "dispatch_date", comment: "派单日期" })
|
||||
dispatchDate!: string;
|
||||
|
||||
@Column({ type: DataType.DATEONLY, field: "start_date", comment: "开始日期" })
|
||||
startDate!: string;
|
||||
|
||||
@Column({ type: DataType.DATEONLY, field: "end_date", comment: "结束日期" })
|
||||
endDate!: string;
|
||||
|
||||
@ForeignKey(() => ProjectPlan)
|
||||
@Column({ type: DataType.BIGINT, allowNull: false, field: "plan_id", comment: "方案ID" })
|
||||
planId!: number;
|
||||
|
||||
@Column({ type: DataType.TEXT, comment: "备注" })
|
||||
remark!: string;
|
||||
|
||||
@Column({ type: DataType.SMALLINT, allowNull: false, defaultValue: 0, comment: "状态:0已派发 1已完成" })
|
||||
status!: number;
|
||||
|
||||
@BelongsTo(() => Customer, { foreignKey: "customerId", as: "customer" })
|
||||
customer!: Customer;
|
||||
|
||||
@BelongsTo(() => Worker, { foreignKey: "workerId", as: "worker" })
|
||||
worker!: Worker;
|
||||
|
||||
@BelongsTo(() => ProjectPlan, { foreignKey: "planId", as: "plan" })
|
||||
plan!: ProjectPlan;
|
||||
|
||||
@CreatedAt
|
||||
created_at!: Date;
|
||||
}
|
||||
@@ -23,6 +23,7 @@ const PERMISSION_MAP: Record<string, string> = {
|
||||
'/api/survey-orders': 'project:survey',
|
||||
'/api/project-plans': 'project:plan',
|
||||
'/api/contracts': 'project:contract',
|
||||
'/api/install-orders': 'project:install',
|
||||
'/api/acceptances': 'project:acceptance',
|
||||
'/api/visit-records': 'project:visit',
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Provide } from "@midwayjs/core";
|
||||
import { Provide, Inject } from "@midwayjs/core";
|
||||
import { InjectRepository } from "@midwayjs/sequelize";
|
||||
import { Repository } from "sequelize-typescript";
|
||||
import { Op, UniqueConstraintError } from "sequelize";
|
||||
@@ -7,6 +7,7 @@ import Acceptance from "../entity/acceptance.entity";
|
||||
import Customer from "../entity/customer.entity";
|
||||
import SysUserRole from "../entity/sysUserRole.entity";
|
||||
import SysRole from "../entity/sysRole.entity";
|
||||
import { LogService } from "./log.service";
|
||||
|
||||
@Provide()
|
||||
export class AcceptanceService {
|
||||
@@ -22,6 +23,9 @@ export class AcceptanceService {
|
||||
@InjectRepository(SysRole)
|
||||
roleModel!: Repository<SysRole>;
|
||||
|
||||
@Inject()
|
||||
logService!: LogService;
|
||||
|
||||
private async isAdmin(userId: number): Promise<boolean> {
|
||||
const userRoles = await this.userRoleModel.findAll({ where: { userId }, include: [this.roleModel] });
|
||||
return userRoles.some((ur) => (ur as any).role?.roleCode === "ADMIN");
|
||||
@@ -57,9 +61,18 @@ export class AcceptanceService {
|
||||
const where: any = {};
|
||||
|
||||
if (query.keyword) {
|
||||
where[Op.or] = [{ acceptor: { [Op.like]: `%${query.keyword}%` } }, { acceptOpinion: { [Op.like]: `%${query.keyword}%` } }];
|
||||
where[Op.or] = [
|
||||
{ acceptNo: { [Op.like]: `%${query.keyword}%` } },
|
||||
{ acceptor: { [Op.like]: `%${query.keyword}%` } },
|
||||
{ acceptOpinion: { [Op.like]: `%${query.keyword}%` } }
|
||||
];
|
||||
}
|
||||
if (query.customerId !== undefined) where.customerId = query.customerId;
|
||||
if (query.acceptStartDate || query.acceptEndDate) {
|
||||
where.acceptDate = {};
|
||||
if (query.acceptStartDate) where.acceptDate[Op.gte] = query.acceptStartDate;
|
||||
if (query.acceptEndDate) where.acceptDate[Op.lte] = query.acceptEndDate;
|
||||
}
|
||||
|
||||
if (!(await this.isAdmin(userId))) {
|
||||
const myCustomers = await this.customerModel.findAll({ where: { salesmanId: userId }, attributes: ["id"] });
|
||||
@@ -88,6 +101,7 @@ export class AcceptanceService {
|
||||
try {
|
||||
data.acceptNo = await this.generateAcceptNo();
|
||||
const created = await this.acceptModel.create(data);
|
||||
await this.logService.logAction(ctx, "项目验收", "创建", data.acceptNo, `创建项目验收 ${data.acceptNo}`);
|
||||
return { code: 200, message: "创建项目验收成功", data: created.toJSON() };
|
||||
} catch (err: any) {
|
||||
if (err instanceof UniqueConstraintError) {
|
||||
@@ -108,6 +122,7 @@ export class AcceptanceService {
|
||||
}
|
||||
delete data.acceptNo;
|
||||
await record.update(data);
|
||||
await this.logService.logAction(ctx, "项目验收", "更新", record.acceptNo, `更新项目验收 ${record.acceptNo}`);
|
||||
return { code: 200, message: "更新项目验收成功", data: record.toJSON() };
|
||||
}
|
||||
|
||||
@@ -115,7 +130,9 @@ export class AcceptanceService {
|
||||
const record = await this.acceptModel.findByPk(id);
|
||||
if (!record) return { code: 404, message: "项目验收不存在", data: null };
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限删除该验收", data: null };
|
||||
const acceptNo = record.acceptNo;
|
||||
await record.destroy();
|
||||
await this.logService.logAction(ctx, "项目验收", "删除", acceptNo, `删除项目验收 ${acceptNo}`);
|
||||
return { code: 200, message: "删除项目验收成功", data: null };
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Provide } from "@midwayjs/core";
|
||||
import { Provide, Inject } from "@midwayjs/core";
|
||||
import { InjectRepository } from "@midwayjs/sequelize";
|
||||
import { Repository } from "sequelize-typescript";
|
||||
import { Op, UniqueConstraintError } from "sequelize";
|
||||
@@ -7,6 +7,7 @@ import Contract from "../entity/contract.entity";
|
||||
import Customer from "../entity/customer.entity";
|
||||
import SysUserRole from "../entity/sysUserRole.entity";
|
||||
import SysRole from "../entity/sysRole.entity";
|
||||
import { LogService } from "./log.service";
|
||||
|
||||
@Provide()
|
||||
export class ContractService {
|
||||
@@ -22,6 +23,9 @@ export class ContractService {
|
||||
@InjectRepository(SysRole)
|
||||
roleModel!: Repository<SysRole>;
|
||||
|
||||
@Inject()
|
||||
logService!: LogService;
|
||||
|
||||
private async isAdmin(userId: number): Promise<boolean> {
|
||||
const userRoles = await this.userRoleModel.findAll({ where: { userId }, include: [this.roleModel] });
|
||||
return userRoles.some((ur) => (ur as any).role?.roleCode === "ADMIN");
|
||||
@@ -72,11 +76,20 @@ export class ContractService {
|
||||
const where: any = {};
|
||||
|
||||
if (query.keyword) {
|
||||
where[Op.or] = [{ xtContractNo: { [Op.like]: `%${query.keyword}%` } }, { contractDesc: { [Op.like]: `%${query.keyword}%` } }];
|
||||
where[Op.or] = [
|
||||
{ contractNo: { [Op.like]: `%${query.keyword}%` } },
|
||||
{ xtContractNo: { [Op.like]: `%${query.keyword}%` } },
|
||||
{ contractDesc: { [Op.like]: `%${query.keyword}%` } }
|
||||
];
|
||||
}
|
||||
if (query.customerId !== undefined) where.customerId = query.customerId;
|
||||
if (query.salesmanId) where.salesmanId = query.salesmanId;
|
||||
if (query.status !== undefined) where.status = query.status;
|
||||
if (query.signStartDate || query.signEndDate) {
|
||||
where.signDate = {};
|
||||
if (query.signStartDate) where.signDate[Op.gte] = query.signStartDate;
|
||||
if (query.signEndDate) where.signDate[Op.lte] = query.signEndDate;
|
||||
}
|
||||
|
||||
if (!(await this.isAdmin(userId))) {
|
||||
const myCustomers = await this.customerModel.findAll({ where: { salesmanId: userId }, attributes: ["id"] });
|
||||
@@ -109,6 +122,7 @@ export class ContractService {
|
||||
data.contractNo = await this.generateContractNo();
|
||||
if (!data.xtContractNo) data.xtContractNo = await this.generateXtContractNo();
|
||||
const created = await this.contractModel.create(data);
|
||||
await this.logService.logAction(ctx, "项目合同", "创建", data.contractNo, `创建项目合同 ${data.contractNo}`);
|
||||
return { code: 200, message: "创建项目合同成功", data: created.toJSON() };
|
||||
} catch (err: any) {
|
||||
if (err instanceof UniqueConstraintError) {
|
||||
@@ -131,6 +145,7 @@ export class ContractService {
|
||||
}
|
||||
delete data.contractNo;
|
||||
await record.update(data);
|
||||
await this.logService.logAction(ctx, "项目合同", "更新", record.contractNo, `更新项目合同 ${record.contractNo}`);
|
||||
return { code: 200, message: "更新项目合同成功", data: record.toJSON() };
|
||||
}
|
||||
|
||||
@@ -138,7 +153,9 @@ export class ContractService {
|
||||
const record = await this.contractModel.findByPk(id);
|
||||
if (!record) return { code: 404, message: "项目合同不存在", data: null };
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限删除该合同", data: null };
|
||||
const contractNo = record.contractNo;
|
||||
await record.destroy();
|
||||
await this.logService.logAction(ctx, "项目合同", "删除", contractNo, `删除项目合同 ${contractNo}`);
|
||||
return { code: 200, message: "删除项目合同成功", data: null };
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
import { Provide, Inject } from "@midwayjs/core";
|
||||
import { InjectRepository } from "@midwayjs/sequelize";
|
||||
import { Repository } from "sequelize-typescript";
|
||||
import { Op, UniqueConstraintError } from "sequelize";
|
||||
import { Context } from "@midwayjs/koa";
|
||||
import InstallOrder from "../entity/install_order.entity";
|
||||
import Customer from "../entity/customer.entity";
|
||||
import Worker from "../entity/worker.entity";
|
||||
import ProjectPlan from "../entity/project_plan.entity";
|
||||
import SysUserRole from "../entity/sysUserRole.entity";
|
||||
import SysRole from "../entity/sysRole.entity";
|
||||
import { LogService } from "./log.service";
|
||||
|
||||
// 安装派单状态:0已派发 1已完成
|
||||
const STATUS = { DISPATCHED: 0, COMPLETED: 1 };
|
||||
|
||||
@Provide()
|
||||
export class InstallOrderService {
|
||||
@InjectRepository(InstallOrder)
|
||||
installModel!: Repository<InstallOrder>;
|
||||
|
||||
@InjectRepository(Customer)
|
||||
customerModel!: Repository<Customer>;
|
||||
|
||||
@InjectRepository(Worker)
|
||||
workerModel!: Repository<Worker>;
|
||||
|
||||
@InjectRepository(ProjectPlan)
|
||||
planModel!: Repository<ProjectPlan>;
|
||||
|
||||
@InjectRepository(SysUserRole)
|
||||
userRoleModel!: Repository<SysUserRole>;
|
||||
|
||||
@InjectRepository(SysRole)
|
||||
roleModel!: Repository<SysRole>;
|
||||
|
||||
@Inject()
|
||||
logService!: LogService;
|
||||
|
||||
private async isAdmin(userId: number): Promise<boolean> {
|
||||
const userRoles = await this.userRoleModel.findAll({ where: { userId }, include: [this.roleModel] });
|
||||
return userRoles.some((ur) => (ur as any).role?.roleCode === "ADMIN");
|
||||
}
|
||||
|
||||
private async isCustomerOwner(userId: number, customerId: number): Promise<boolean> {
|
||||
if (await this.isAdmin(userId)) return true;
|
||||
const customer = await this.customerModel.findByPk(customerId);
|
||||
return !!customer && customer.salesmanId === userId;
|
||||
}
|
||||
|
||||
// 工单可见性:管理员 / 客户归属业务员
|
||||
private async isOrderVisible(userId: number, order: InstallOrder): Promise<boolean> {
|
||||
if (await this.isAdmin(userId)) return true;
|
||||
return this.isCustomerOwner(userId, order.customerId);
|
||||
}
|
||||
|
||||
// 单号规则:WH-INST-YYYY-NNNN(年内累计序号),冲突重试 1 次
|
||||
private async generateInstallNo(retry: number = 0): Promise<string> {
|
||||
const now = new Date();
|
||||
const d = new Date(now.getTime() + 8 * 60 * 60 * 1000);
|
||||
const year = d.getUTCFullYear();
|
||||
const prefix = `WH-INST-${year}-`;
|
||||
const last = await this.installModel.findOne({ where: { installNo: { [Op.like]: `${prefix}%` } }, order: [["install_no", "DESC"]] });
|
||||
let seq = 1;
|
||||
if (last) seq = (parseInt(last.installNo.slice(-4), 10) || 0) + 1;
|
||||
const result = `${prefix}${String(seq).padStart(4, "0")}`;
|
||||
const exists = await this.installModel.findOne({ where: { installNo: result } });
|
||||
if (exists) {
|
||||
if (retry < 1) return this.generateInstallNo(retry + 1);
|
||||
throw new Error("安装单号生成冲突,请重试");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// query 参数 → Sequelize where(关键字/客户/状态/派单日期范围)
|
||||
private buildWhere(query: any): any {
|
||||
const where: any = {};
|
||||
if (query.keyword) {
|
||||
where[Op.or] = [
|
||||
{ installNo: { [Op.like]: `%${query.keyword}%` } },
|
||||
{ installer: { [Op.like]: `%${query.keyword}%` } },
|
||||
{ remark: { [Op.like]: `%${query.keyword}%` } }
|
||||
];
|
||||
}
|
||||
if (query.customerId !== undefined && query.customerId !== "") where.customerId = Number(query.customerId);
|
||||
if (query.status !== undefined && query.status !== "") where.status = Number(query.status);
|
||||
// dateRange: [startDate, endDate],作用于 dispatch_date
|
||||
const range = Array.isArray(query.dateRange) ? query.dateRange : (typeof query.dateRange === "string" ? query.dateRange.split(",") : null);
|
||||
if (range && range.length === 2 && (range[0] || range[1])) {
|
||||
const cond: any = {};
|
||||
if (range[0]) cond[Op.gte] = range[0];
|
||||
if (range[1]) cond[Op.lte] = range[1];
|
||||
where.dispatchDate = cond;
|
||||
}
|
||||
return where;
|
||||
}
|
||||
|
||||
// 行级隔离:非管理员仅可见「客户归属自己」的工单
|
||||
private async scopeWhere(baseWhere: any, ctx: Context): Promise<any> {
|
||||
const userId = ctx.state.user.userId;
|
||||
const where: any = { ...baseWhere };
|
||||
if (await this.isAdmin(userId)) return where;
|
||||
|
||||
const myCustomers = await this.customerModel.findAll({ where: { salesmanId: userId }, attributes: ["id"] });
|
||||
const ids = myCustomers.map((c) => c.id);
|
||||
where.customerId = ids.length ? { [Op.in]: ids } : -1;
|
||||
return where;
|
||||
}
|
||||
|
||||
// 批量补充客户/电工/方案信息,避免 N+1
|
||||
private async decorate(rows: any[]): Promise<any[]> {
|
||||
if (rows.length === 0) return rows;
|
||||
const customerIds = [...new Set(rows.map((r) => r.customerId).filter(Boolean))];
|
||||
const workerIds = [...new Set(rows.map((r) => r.workerId).filter(Boolean))];
|
||||
const planIds = [...new Set(rows.map((r) => r.planId).filter(Boolean))];
|
||||
|
||||
const [customers, workers, plans] = await Promise.all([
|
||||
customerIds.length
|
||||
? this.customerModel.findAll({
|
||||
where: { id: { [Op.in]: customerIds } },
|
||||
attributes: ["id", "customerNo", "customerName", "contactPerson", "contactPhone", "address"]
|
||||
})
|
||||
: Promise.resolve([]),
|
||||
workerIds.length
|
||||
? this.workerModel.findAll({
|
||||
where: { id: { [Op.in]: workerIds } },
|
||||
attributes: ["id", "workerName", "phone", "workerTitle"]
|
||||
})
|
||||
: Promise.resolve([]),
|
||||
planIds.length
|
||||
? this.planModel.findAll({
|
||||
where: { id: { [Op.in]: planIds } },
|
||||
attributes: ["id", "planNo", "planDesc", "status"]
|
||||
})
|
||||
: Promise.resolve([])
|
||||
]);
|
||||
|
||||
const customerMap = new Map(customers.map((c) => [c.id, c]));
|
||||
const workerMap = new Map(workers.map((w) => [w.id, w]));
|
||||
const planMap = new Map(plans.map((p) => [p.id, p]));
|
||||
|
||||
for (const row of rows) {
|
||||
const customer: any = customerMap.get(row.customerId);
|
||||
const worker: any = workerMap.get(row.workerId);
|
||||
const plan: any = planMap.get(row.planId);
|
||||
row.customer = customer ? customer.toJSON() : null;
|
||||
row.customerName = customer?.customerName || "";
|
||||
row.worker = worker ? worker.toJSON() : null;
|
||||
row.workerName = worker?.workerName || "";
|
||||
row.plan = plan ? plan.toJSON() : null;
|
||||
row.planNo = plan?.planNo || "";
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
// 统一清洗入参:空串转 null
|
||||
private normalizePayload(data: any): any {
|
||||
const payload: any = { ...data };
|
||||
for (const key of Object.keys(payload)) {
|
||||
if (payload[key] === "") payload[key] = null;
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
async list(query: any, ctx: Context): Promise<any> {
|
||||
const page = Number(query.page) || 1;
|
||||
const pageSize = Number(query.pageSize) || 10;
|
||||
|
||||
const where = await this.scopeWhere(this.buildWhere(query), ctx);
|
||||
const { count, rows } = await this.installModel.findAndCountAll({
|
||||
where,
|
||||
limit: pageSize,
|
||||
offset: (page - 1) * pageSize,
|
||||
order: [["created_at", "DESC"]],
|
||||
distinct: true
|
||||
});
|
||||
|
||||
const list = await this.decorate(rows.map((r) => r.toJSON()));
|
||||
return { code: 200, message: "获取安装派单列表成功", data: { list, total: count, page, pageSize } };
|
||||
}
|
||||
|
||||
async detail(id: number, ctx: Context): Promise<any> {
|
||||
const record = await this.installModel.findByPk(id);
|
||||
if (!record) return { code: 404, message: "安装派单不存在", data: null };
|
||||
if (!(await this.isOrderVisible(ctx.state.user.userId, record))) return { code: 403, message: "无权限访问该安装派单", data: null };
|
||||
|
||||
const vo: any = record.toJSON();
|
||||
await this.decorate([vo]);
|
||||
return { code: 200, message: "获取安装派单详情成功", data: vo };
|
||||
}
|
||||
|
||||
async create(data: any, ctx: Context): Promise<any> {
|
||||
const userId = ctx.state.user.userId;
|
||||
if (!(await this.isCustomerOwner(userId, data.customerId))) return { code: 403, message: "无权限为该客户创建安装派单", data: null };
|
||||
|
||||
// 方案与客户一致性校验,防止串客户派单
|
||||
const plan = await this.planModel.findByPk(Number(data.planId));
|
||||
if (!plan) return { code: 400, message: "关联方案不存在", data: null };
|
||||
if (Number(plan.customerId) !== Number(data.customerId)) return { code: 400, message: "方案与客户不匹配", data: null };
|
||||
|
||||
if (data.workerId) {
|
||||
const worker = await this.workerModel.findByPk(Number(data.workerId));
|
||||
if (!worker) return { code: 400, message: "共享电工不存在", data: null };
|
||||
}
|
||||
|
||||
const payload = this.normalizePayload(data);
|
||||
payload.installNo = await this.generateInstallNo();
|
||||
if (payload.status === undefined || payload.status === null) payload.status = STATUS.DISPATCHED;
|
||||
|
||||
let created: InstallOrder;
|
||||
try {
|
||||
created = await this.installModel.create(payload);
|
||||
} catch (err: any) {
|
||||
if (err instanceof UniqueConstraintError) {
|
||||
payload.installNo = await this.generateInstallNo();
|
||||
created = await this.installModel.create(payload);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const vo: any = created.toJSON();
|
||||
await this.decorate([vo]);
|
||||
await this.logService.logAction(ctx, "安装派单", "创建", created.installNo, `创建安装派单 ${created.installNo}`);
|
||||
return { code: 200, message: "创建安装派单成功", data: vo };
|
||||
}
|
||||
|
||||
async update(id: number, data: any, ctx: Context): Promise<any> {
|
||||
const record = await this.installModel.findByPk(id);
|
||||
if (!record) return { code: 404, message: "安装派单不存在", data: null };
|
||||
if (!(await this.isOrderVisible(ctx.state.user.userId, record))) return { code: 403, message: "无权操作该安装派单", data: null };
|
||||
if (record.status === STATUS.COMPLETED) return { code: 400, message: "已完成的派单不可修改", data: null };
|
||||
|
||||
delete data.installNo;
|
||||
if (data.customerId !== undefined && Number(data.customerId) !== Number(record.customerId)) {
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, Number(data.customerId)))) {
|
||||
return { code: 403, message: "无权限转移到该客户", data: null };
|
||||
}
|
||||
}
|
||||
if (data.planId !== undefined) {
|
||||
const plan = await this.planModel.findByPk(Number(data.planId));
|
||||
if (!plan) return { code: 400, message: "关联方案不存在", data: null };
|
||||
const targetCustomerId = data.customerId !== undefined ? Number(data.customerId) : Number(record.customerId);
|
||||
if (Number(plan.customerId) !== targetCustomerId) return { code: 400, message: "方案与客户不匹配", data: null };
|
||||
}
|
||||
|
||||
const payload = this.normalizePayload(data);
|
||||
await record.update(payload);
|
||||
const fresh = await this.installModel.findByPk(id);
|
||||
const vo: any = fresh!.toJSON();
|
||||
await this.decorate([vo]);
|
||||
await this.logService.logAction(ctx, "安装派单", "更新", record.installNo, `更新安装派单 ${record.installNo}`);
|
||||
return { code: 200, message: "更新安装派单成功", data: vo };
|
||||
}
|
||||
|
||||
// 完成派单:仅允许 0已派发 → 1已完成
|
||||
async complete(id: number, ctx: Context): Promise<any> {
|
||||
const record = await this.installModel.findByPk(id);
|
||||
if (!record) return { code: 404, message: "安装派单不存在", data: null };
|
||||
if (!(await this.isOrderVisible(ctx.state.user.userId, record))) return { code: 403, message: "无权操作该安装派单", data: null };
|
||||
if (record.status !== STATUS.DISPATCHED) return { code: 400, message: "仅已派发状态的派单可标记完成", data: null };
|
||||
|
||||
await record.update({ status: STATUS.COMPLETED });
|
||||
const vo: any = record.toJSON();
|
||||
await this.decorate([vo]);
|
||||
await this.logService.logAction(ctx, "安装派单", "完成", record.installNo, `完成安装派单 ${record.installNo}`);
|
||||
return { code: 200, message: "安装派单已完成", data: vo };
|
||||
}
|
||||
|
||||
async delete(id: number, ctx: Context): Promise<any> {
|
||||
const record = await this.installModel.findByPk(id);
|
||||
if (!record) return { code: 404, message: "安装派单不存在", data: null };
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限删除该安装派单", data: null };
|
||||
const installNo = record.installNo;
|
||||
await record.destroy();
|
||||
await this.logService.logAction(ctx, "安装派单", "删除", installNo, `删除安装派单 ${installNo}`);
|
||||
return { code: 200, message: "删除安装派单成功", data: null };
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Provide, Inject } from "@midwayjs/core";
|
||||
import { InjectRepository } from "@midwayjs/sequelize";
|
||||
import { Context } from "@midwayjs/koa";
|
||||
import SysOperationLog from "../entity/sysOperationLog.entity";
|
||||
import { Op } from "sequelize";
|
||||
import dayjs from "dayjs";
|
||||
@@ -9,6 +10,24 @@ export class LogService {
|
||||
@InjectRepository(SysOperationLog)
|
||||
logModel!: typeof SysOperationLog;
|
||||
|
||||
// 写入操作日志:操作人userId/模块/动作/目标单号/摘要
|
||||
// method 与 ip 从 ctx 提取,targetNo+摘要 落 params(JSON) 字段
|
||||
async logAction(ctx: Context, module: string, action: string, targetNo: string, summary: string): Promise<void> {
|
||||
try {
|
||||
const userId = (ctx.state as any)?.user?.userId ?? 0;
|
||||
await this.logModel.create({
|
||||
userId,
|
||||
module,
|
||||
action,
|
||||
method: ctx.method || "",
|
||||
params: JSON.stringify({ targetNo: targetNo || "", summary: summary || "" }),
|
||||
ip: ctx.ip || ""
|
||||
} as any);
|
||||
} catch {
|
||||
// 日志写入失败不影响主业务流程
|
||||
}
|
||||
}
|
||||
|
||||
async list(query: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Provide } from "@midwayjs/core";
|
||||
import { Provide, Inject } from "@midwayjs/core";
|
||||
import { InjectRepository } from "@midwayjs/sequelize";
|
||||
import { Repository } from "sequelize-typescript";
|
||||
import { Op, UniqueConstraintError } from "sequelize";
|
||||
@@ -8,6 +8,7 @@ import ProjectPlanDetail from "../entity/project_plan_detail.entity";
|
||||
import Customer from "../entity/customer.entity";
|
||||
import SysUserRole from "../entity/sysUserRole.entity";
|
||||
import SysRole from "../entity/sysRole.entity";
|
||||
import { LogService } from "./log.service";
|
||||
|
||||
@Provide()
|
||||
export class ProjectPlanService {
|
||||
@@ -26,6 +27,9 @@ export class ProjectPlanService {
|
||||
@InjectRepository(SysRole)
|
||||
roleModel!: Repository<SysRole>;
|
||||
|
||||
@Inject()
|
||||
logService!: LogService;
|
||||
|
||||
private async isAdmin(userId: number): Promise<boolean> {
|
||||
const userRoles = await this.userRoleModel.findAll({ where: { userId }, include: [this.roleModel] });
|
||||
return userRoles.some((ur) => (ur as any).role?.roleCode === "ADMIN");
|
||||
@@ -61,10 +65,19 @@ export class ProjectPlanService {
|
||||
const where: any = {};
|
||||
|
||||
if (query.keyword) {
|
||||
where[Op.or] = [{ registrar: { [Op.like]: `%${query.keyword}%` } }, { planDesc: { [Op.like]: `%${query.keyword}%` } }];
|
||||
where[Op.or] = [
|
||||
{ planNo: { [Op.like]: `%${query.keyword}%` } },
|
||||
{ registrar: { [Op.like]: `%${query.keyword}%` } },
|
||||
{ planDesc: { [Op.like]: `%${query.keyword}%` } }
|
||||
];
|
||||
}
|
||||
if (query.customerId !== undefined) where.customerId = query.customerId;
|
||||
if (query.status !== undefined) where.status = query.status;
|
||||
if (query.registerStartDate || query.registerEndDate) {
|
||||
where.registerTime = {};
|
||||
if (query.registerStartDate) where.registerTime[Op.gte] = query.registerStartDate;
|
||||
if (query.registerEndDate) where.registerTime[Op.lte] = query.registerEndDate;
|
||||
}
|
||||
|
||||
if (!(await this.isAdmin(userId))) {
|
||||
const myCustomers = await this.customerModel.findAll({ where: { salesmanId: userId }, attributes: ["id"] });
|
||||
@@ -99,6 +112,7 @@ export class ProjectPlanService {
|
||||
await this.detailModel.bulkCreate(details.map((d: any) => ({ ...d, planId: created.id, customerId: created.customerId })));
|
||||
}
|
||||
const result = await this.planModel.findByPk(created.id, { include: [this.detailModel] });
|
||||
await this.logService.logAction(ctx, "项目方案", "创建", data.planNo, `创建项目方案 ${data.planNo}`);
|
||||
return { code: 200, message: "创建项目方案成功", data: result?.toJSON() };
|
||||
} catch (err: any) {
|
||||
if (err instanceof UniqueConstraintError) {
|
||||
@@ -133,6 +147,7 @@ export class ProjectPlanService {
|
||||
}
|
||||
}
|
||||
const result = await this.planModel.findByPk(id, { include: [this.detailModel] });
|
||||
await this.logService.logAction(ctx, "项目方案", "更新", record.planNo, `更新项目方案 ${record.planNo}`);
|
||||
return { code: 200, message: "更新项目方案成功", data: result?.toJSON() };
|
||||
}
|
||||
|
||||
@@ -140,8 +155,10 @@ export class ProjectPlanService {
|
||||
const record = await this.planModel.findByPk(id);
|
||||
if (!record) return { code: 404, message: "项目方案不存在", data: null };
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限删除该方案", data: null };
|
||||
const planNo = record.planNo;
|
||||
await this.detailModel.destroy({ where: { planId: id } });
|
||||
await record.destroy();
|
||||
await this.logService.logAction(ctx, "项目方案", "删除", planNo, `删除项目方案 ${planNo}`);
|
||||
return { code: 200, message: "删除项目方案成功", data: null };
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import SysUser from "../entity/sysUser.entity";
|
||||
import SysUserRole from "../entity/sysUserRole.entity";
|
||||
import SysRole from "../entity/sysRole.entity";
|
||||
import { UploadService } from "./upload.service";
|
||||
import { LogService } from "./log.service";
|
||||
|
||||
// 勘察工单状态:0待指派 1现场勘察中 2待技术复核 3已归档 4预留 5已取消
|
||||
const STATUS = { PENDING_ASSIGN: 0, SURVEYING: 1, PENDING_REVIEW: 2, ARCHIVED: 3, RESERVED: 4, CANCELED: 5 };
|
||||
@@ -57,6 +58,9 @@ export class SurveyOrderService {
|
||||
@Inject()
|
||||
uploadService!: UploadService;
|
||||
|
||||
@Inject()
|
||||
logService!: LogService;
|
||||
|
||||
private async isAdmin(userId: number): Promise<boolean> {
|
||||
const userRoles = await this.userRoleModel.findAll({ where: { userId }, include: [this.roleModel] });
|
||||
return userRoles.some((ur) => (ur as any).role?.roleCode === "ADMIN");
|
||||
@@ -479,6 +483,7 @@ export class SurveyOrderService {
|
||||
await this.createSopResults(created.id, templateCode);
|
||||
const vo = this.toVO(created);
|
||||
await this.decorate([vo]);
|
||||
await this.logService.logAction(ctx, "勘察派单", "创建", created.surveyNo, `创建勘察派单 ${created.surveyNo}`);
|
||||
return { code: 200, message: "创建勘察派单成功", data: vo };
|
||||
}
|
||||
|
||||
@@ -501,6 +506,7 @@ export class SurveyOrderService {
|
||||
const fresh = await this.surveyModel.findByPk(id);
|
||||
const vo = this.toVO(fresh!);
|
||||
await this.decorate([vo]);
|
||||
await this.logService.logAction(ctx, "勘察派单", "更新", record.surveyNo, `更新勘察派单 ${record.surveyNo}`);
|
||||
return { code: 200, message: "更新勘察派单成功", data: vo };
|
||||
}
|
||||
|
||||
@@ -535,6 +541,7 @@ export class SurveyOrderService {
|
||||
|
||||
const result = await this.assignCore(id, worker, dto.estimatedFee);
|
||||
if (!result.ok) return { code: result.code, message: result.message, data: null };
|
||||
await this.logService.logAction(ctx, "勘察派单", "指派", record.surveyNo, `指派勘察派单 ${record.surveyNo} 至 ${worker.workerName}`);
|
||||
return { code: 200, message: "派单成功", data: result.data };
|
||||
}
|
||||
|
||||
@@ -555,6 +562,7 @@ export class SurveyOrderService {
|
||||
failIds.push(id);
|
||||
}
|
||||
}
|
||||
await this.logService.logAction(ctx, "勘察派单", "批量指派", "", `批量指派 ${successCount} 条勘察派单至 ${worker.workerName},失败 ${failIds.length} 条`);
|
||||
return {
|
||||
code: 200,
|
||||
message: `批量派单完成,成功 ${successCount} 条`,
|
||||
@@ -580,6 +588,7 @@ export class SurveyOrderService {
|
||||
await record.update(patch);
|
||||
const vo = this.toVO(record);
|
||||
await this.decorate([vo]);
|
||||
await this.logService.logAction(ctx, "勘察派单", "复核", record.surveyNo, `复核勘察派单 ${record.surveyNo}: ${dto.result === "pass" ? "通过归档" : "退回勘察"}`);
|
||||
return { code: 200, message: dto.result === "pass" ? "复核通过,工单已归档" : "已退回现场勘察中", data: vo };
|
||||
}
|
||||
|
||||
@@ -594,6 +603,7 @@ export class SurveyOrderService {
|
||||
const urgeCount = (record.urgeCount || 0) + 1;
|
||||
const lastUrgedAt = new Date();
|
||||
await record.update({ urgeCount, lastUrgedAt });
|
||||
await this.logService.logAction(ctx, "勘察派单", "催办", record.surveyNo, `催办勘察派单 ${record.surveyNo}(第 ${urgeCount} 次)`);
|
||||
return { code: 200, message: "催办成功", data: { urgeCount, lastUrgedAt } };
|
||||
}
|
||||
|
||||
@@ -606,6 +616,7 @@ export class SurveyOrderService {
|
||||
}
|
||||
|
||||
await record.update({ status: STATUS.CANCELED, cancelReason: dto.reason || null });
|
||||
await this.logService.logAction(ctx, "勘察派单", "取消", record.surveyNo, `取消勘察派单 ${record.surveyNo}`);
|
||||
return { code: 200, message: "取消工单成功", data: this.toVO(record) };
|
||||
}
|
||||
|
||||
@@ -613,9 +624,11 @@ export class SurveyOrderService {
|
||||
const record = await this.surveyModel.findByPk(id);
|
||||
if (!record) return { code: 404, message: "勘察派单不存在", data: null };
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限删除该勘察派单", data: null };
|
||||
const surveyNo = record.surveyNo;
|
||||
// 级联清理回执后删除工单
|
||||
await this.sopResultModel.destroy({ where: { surveyOrderId: id } });
|
||||
await record.destroy();
|
||||
await this.logService.logAction(ctx, "勘察派单", "删除", surveyNo, `删除勘察派单 ${surveyNo}`);
|
||||
return { code: 200, message: "删除勘察派单成功", data: null };
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Provide } from "@midwayjs/core";
|
||||
import { Provide, Inject } from "@midwayjs/core";
|
||||
import { InjectRepository } from "@midwayjs/sequelize";
|
||||
import { Repository } from "sequelize-typescript";
|
||||
import { Op, UniqueConstraintError } from "sequelize";
|
||||
@@ -7,6 +7,7 @@ import VisitRecord from "../entity/visit_record.entity";
|
||||
import Customer from "../entity/customer.entity";
|
||||
import SysUserRole from "../entity/sysUserRole.entity";
|
||||
import SysRole from "../entity/sysRole.entity";
|
||||
import { LogService } from "./log.service";
|
||||
|
||||
@Provide()
|
||||
export class VisitRecordService {
|
||||
@@ -22,6 +23,9 @@ export class VisitRecordService {
|
||||
@InjectRepository(SysRole)
|
||||
roleModel!: Repository<SysRole>;
|
||||
|
||||
@Inject()
|
||||
logService!: LogService;
|
||||
|
||||
private async isAdmin(userId: number): Promise<boolean> {
|
||||
const userRoles = await this.userRoleModel.findAll({ where: { userId }, include: [this.roleModel] });
|
||||
return userRoles.some((ur) => (ur as any).role?.roleCode === "ADMIN");
|
||||
@@ -65,6 +69,11 @@ export class VisitRecordService {
|
||||
];
|
||||
}
|
||||
if (query.customerId !== undefined) where.customerId = query.customerId;
|
||||
if (query.visitStartDate || query.visitEndDate) {
|
||||
where.visitTime = {};
|
||||
if (query.visitStartDate) where.visitTime[Op.gte] = query.visitStartDate;
|
||||
if (query.visitEndDate) where.visitTime[Op.lte] = query.visitEndDate;
|
||||
}
|
||||
|
||||
if (!(await this.isAdmin(userId))) {
|
||||
const myCustomers = await this.customerModel.findAll({ where: { salesmanId: userId }, attributes: ["id"] });
|
||||
@@ -93,6 +102,7 @@ export class VisitRecordService {
|
||||
try {
|
||||
data.visitNo = await this.generateVisitNo();
|
||||
const created = await this.visitModel.create(data);
|
||||
await this.logService.logAction(ctx, "客户回访", "创建", data.visitNo, `创建客户回访 ${data.visitNo}`);
|
||||
return { code: 200, message: "创建客户回访成功", data: created.toJSON() };
|
||||
} catch (err: any) {
|
||||
if (err instanceof UniqueConstraintError) {
|
||||
@@ -113,6 +123,7 @@ export class VisitRecordService {
|
||||
}
|
||||
delete data.visitNo;
|
||||
await record.update(data);
|
||||
await this.logService.logAction(ctx, "客户回访", "更新", record.visitNo, `更新客户回访 ${record.visitNo}`);
|
||||
return { code: 200, message: "更新客户回访成功", data: record.toJSON() };
|
||||
}
|
||||
|
||||
@@ -120,7 +131,9 @@ export class VisitRecordService {
|
||||
const record = await this.visitModel.findByPk(id);
|
||||
if (!record) return { code: 404, message: "客户回访不存在", data: null };
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限删除该回访记录", data: null };
|
||||
const visitNo = record.visitNo;
|
||||
await record.destroy();
|
||||
await this.logService.logAction(ctx, "客户回访", "删除", visitNo, `删除客户回访 ${visitNo}`);
|
||||
return { code: 200, message: "删除客户回访成功", data: null };
|
||||
}
|
||||
}
|
||||
+79
-1421
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user