390 lines
32 KiB
TypeScript
390 lines
32 KiB
TypeScript
import { computed, ref } from 'vue'
|
|
import { defineStore } from 'pinia'
|
|
import {
|
|
createPlatformReceivingWarehouseSnapshot,
|
|
DEMO_PLATFORM_RECEIVING_WAREHOUSES,
|
|
} from '@chongchong/shared'
|
|
import type {
|
|
Customer,
|
|
Counterparty,
|
|
ExpressCompany,
|
|
FirstLegOrder,
|
|
InternalUser,
|
|
OrderStatus,
|
|
Permission,
|
|
Role,
|
|
Warehouse,
|
|
} from '@/types/admin'
|
|
|
|
const initialExpressCompanies: ExpressCompany[] = [
|
|
{ id: 'E001', name: 'DHL', code: 'DHL', website: 'www.dhl.com', phone: '95380', status: '合作中', remark: '国际快递渠道。', updatedAt: '2026-07-28 09:10' },
|
|
{ id: 'E002', name: 'FedEx', code: 'FDX', website: 'www.fedex.com', phone: '400-886-1888', status: '合作中', remark: '国际快递渠道。', updatedAt: '2026-07-28 09:12' },
|
|
{ id: 'E003', name: 'UPS', code: 'UPS', website: 'www.ups.com', phone: '400-820-8388', status: '合作中', remark: '', updatedAt: '2026-07-28 09:15' },
|
|
{ id: 'E004', name: '顺丰速运', code: 'SF', website: 'www.sf-express.com', phone: '95338', status: '合作中', remark: '国内收货常用渠道。', updatedAt: '2026-07-28 09:20' },
|
|
{ id: 'E005', name: '中通快递', code: 'ZTO', website: 'www.zto.com', phone: '95311', status: '合作中', remark: '', updatedAt: '2026-07-28 09:22' },
|
|
{ id: 'E006', name: '圆通速递', code: 'YTO', website: 'www.yto.net.cn', phone: '95554', status: '已终止', remark: '暂停新增引用,历史订单保留。', updatedAt: '2026-07-28 09:25' },
|
|
{ id: 'E007', name: 'EMS', code: 'EMS', website: 'www.ems.com.cn', phone: '11183', status: '合作中', remark: '', updatedAt: '2026-07-28 09:28' },
|
|
{ id: 'E008', name: '德邦快递', code: 'DPK', website: 'www.deppon.com', phone: '95353', status: '合作中', remark: '大件货物备选渠道。', updatedAt: '2026-07-28 09:30' },
|
|
]
|
|
|
|
const initialCounterparties: Counterparty[] = [
|
|
{ id: 'CP001', code: 'CP-HY-001', name: '华远贸易有限公司', types: ['客户', '发货单位'], contactName: '张伟', contactPhone: '138****6789', email: 'zhangwei@huayuan.cn', country: '中国', province: '浙江省', city: '杭州市', postalCode: '310000', address: '滨江区江南大道 588 号', status: '启用', remark: '美线核心客户。', updatedAt: '2026-07-28 10:10' },
|
|
{ id: 'CP002', code: 'CP-SD-002', name: '深圳速达供应链有限公司', types: ['供应商', '承运商', '发货单位'], contactName: '李明', contactPhone: '139****8901', email: 'liming@suda.cn', country: '中国', province: '广东省', city: '深圳市', postalCode: '518000', address: '盐田区深盐路 2028 号', status: '启用', remark: '欧洲线拼箱合作方。', updatedAt: '2026-07-28 10:15' },
|
|
{ id: 'CP003', code: 'CP-CUS-003', name: 'HY GLOBAL LLC', types: ['客户', '收货单位'], contactName: 'Alice Chen', contactPhone: '+1 213 *** 8899', email: 'ops@hyglobal.example', country: '美国', province: 'California', city: 'Los Angeles', postalCode: '90001', address: '1200 S Alameda St', status: '启用', remark: '华远贸易美国收货方。', updatedAt: '2026-07-28 10:20' },
|
|
{ id: 'CP004', code: 'CP-CB-004', name: '宁波海诚报关有限公司', types: ['供应商', '报关行'], contactName: '周海', contactPhone: '0574-****2211', email: 'service@haicheng.example', country: '中国', province: '浙江省', city: '宁波市', postalCode: '315800', address: '北仑区港兴路 36 号', status: '启用', remark: '宁波口岸报关服务。', updatedAt: '2026-07-28 10:25' },
|
|
{ id: 'CP005', code: 'CP-OLD-005', name: '厦门远港运输有限公司', types: ['承运商'], contactName: '刘洋', contactPhone: '0592-****9812', email: 'dispatch@xm-old.example', country: '中国', province: '福建省', city: '厦门市', postalCode: '361000', address: '湖里区东港北路 31 号', status: '停用', remark: '合同到期,停止新增引用。', updatedAt: '2026-07-28 10:30' },
|
|
]
|
|
|
|
const initialCustomers: Customer[] = [
|
|
{ id: 'T001', name: '华远贸易有限公司', creditCode: '91330100MA15XXXX91', contact: '张伟', phone: '138****6789', email: 'zhangwei@huayuan.cn', sales: '王建国', packageName: '企业版', period: 30, status: '正常', orders: 286, joinedAt: '2025-03-15', address: '浙江省杭州市滨江区江南大道 588 号', remark: '美线核心客户,优先处理订舱需求。' },
|
|
{ id: 'T002', name: '深圳速达供应链有限公司', creditCode: '91440300MA95XXXX13', contact: '李明', phone: '139****8901', email: 'liming@suda.cn', sales: '李秀英', packageName: '专业版', period: 45, status: '正常', orders: 152, joinedAt: '2025-06-20', address: '广东省深圳市盐田区深盐路 2028 号', remark: '欧洲线拼箱客户。' },
|
|
{ id: 'T003', name: '广州恒通物流有限公司', creditCode: '91440100MA32XXXX45', contact: '王芳', phone: '137****2345', email: 'wangfang@hengtong.cn', sales: '张志强', packageName: '专业版', period: 60, status: '正常', orders: 98, joinedAt: '2025-09-01', address: '广东省广州市白云区机场路 1688 号', remark: '空运及敏感货渠道客户。' },
|
|
{ id: 'T004', name: '宁波远洋货运代理有限公司', creditCode: '91330200MA18XXXX38', contact: '陈强', phone: '136****4567', email: 'chenqiang@ocean-nb.cn', sales: '刘德海', packageName: '基础版', period: 90, status: '正常', orders: 45, joinedAt: '2026-01-10', address: '浙江省宁波市北仑区港兴路 88 号', remark: '' },
|
|
{ id: 'T005', name: '北京国贸跨境供应链有限公司', creditCode: '91110100MA87XXXX23', contact: '赵雪', phone: '135****7890', email: 'zhaoxue@bjgmt.cn', sales: '陈美玲', packageName: '基础版', period: 30, status: '待审核', orders: 0, joinedAt: '2026-07-26', address: '北京市朝阳区建国路 93 号', remark: '营业执照已上传,等待审核。' },
|
|
{ id: 'T006', name: '厦门港务货运有限公司', creditCode: '91350200MA12XXXX79', contact: '刘洋', phone: '133****0123', email: 'liuyang@xmgw.cn', sales: '赵晓东', packageName: '企业版', period: 45, status: '停用', orders: 73, joinedAt: '2024-11-01', address: '福建省厦门市湖里区东港北路 31 号', remark: '合同到期,暂时停用。' },
|
|
]
|
|
|
|
const initialUsers: InternalUser[] = [
|
|
{ id: 'U001', account: 'admin', name: '系统管理员', department: '平台管理', phone: '13800000001', email: 'admin@ccglobal.cn', roleIds: ['R001'], status: '正常', lastLogin: '2026-07-27 17:56', createdAt: '2026-01-01' },
|
|
{ id: 'U002', account: 'zhangwei', name: '张伟', department: '运营部', phone: '13800000002', email: 'zhangwei@ccglobal.cn', roleIds: ['R002'], status: '正常', lastLogin: '2026-07-27 16:30', createdAt: '2026-02-08' },
|
|
{ id: 'U003', account: 'liming', name: '李明', department: '操作部', phone: '13800000003', email: 'liming@ccglobal.cn', roleIds: ['R003'], status: '正常', lastLogin: '2026-07-27 15:42', createdAt: '2026-02-18' },
|
|
{ id: 'U004', account: 'wangfang', name: '王芳', department: '财务部', phone: '13800000004', email: 'wangfang@ccglobal.cn', roleIds: ['R004'], status: '正常', lastLogin: '2026-07-27 14:18', createdAt: '2026-03-02' },
|
|
{ id: 'U005', account: 'chenqiang', name: '陈强', department: '销售部', phone: '13800000005', email: 'chenqiang@ccglobal.cn', roleIds: ['R005'], status: '正常', lastLogin: '2026-07-26 18:02', createdAt: '2026-04-15' },
|
|
{ id: 'U006', account: 'zhaoting', name: '赵婷', department: '操作部', phone: '13800000006', email: 'zhaoting@ccglobal.cn', roleIds: ['R003'], status: '停用', lastLogin: '2026-07-18 10:10', createdAt: '2026-05-10' },
|
|
]
|
|
|
|
const initialPermissions: Permission[] = [
|
|
{ id: 'P010', parentId: null, name: '业务管理', code: 'business', type: '目录', path: '', method: '-', status: '启用', sort: 20 },
|
|
{ id: 'P100', parentId: 'P010', name: '客户管理', code: 'platform:customer:list', type: '菜单', path: '/customers', method: '-', status: '启用', sort: 10 },
|
|
{ id: 'P101', parentId: 'P010', name: '企业审核', code: 'platform:customer:review', type: '菜单', path: '/customers/reviews', method: '-', status: '启用', sort: 15 },
|
|
{ id: 'P102', parentId: 'P100', name: '新增客户', code: 'customer:create', type: '按钮', path: '', method: '-', status: '启用', sort: 10 },
|
|
{ id: 'P103', parentId: 'P101', name: '审核通过', code: 'platform:customer:approve', type: '按钮', path: '', method: '-', status: '启用', sort: 10 },
|
|
{ id: 'P104', parentId: 'P101', name: '审核驳回', code: 'platform:customer:reject', type: '按钮', path: '', method: '-', status: '启用', sort: 20 },
|
|
{ id: 'P150', parentId: 'P010', name: '仓库管理', code: 'platform:warehouse:list', type: '菜单', path: '/warehouses', method: '-', status: '启用', sort: 20 },
|
|
{ id: 'P152', parentId: 'P150', name: '维护仓库', code: 'warehouse:write', type: '按钮', path: '', method: '-', status: '启用', sort: 10 },
|
|
{ id: 'P200', parentId: 'P010', name: '头程订单', code: 'platform:order:list', type: '菜单', path: '/orders/first-leg', method: '-', status: '启用', sort: 30 },
|
|
{ id: 'P202', parentId: 'P200', name: '创建订单', code: 'first-leg:create', type: '按钮', path: '', method: '-', status: '启用', sort: 10 },
|
|
{ id: 'P203', parentId: 'P200', name: '更新状态', code: 'first-leg:status', type: '按钮', path: '', method: '-', status: '启用', sort: 20 },
|
|
{ id: 'P204', parentId: 'P200', name: '维护履约快递', code: 'first-leg:delivery', type: '按钮', path: '', method: '-', status: '启用', sort: 30 },
|
|
{ id: 'P015', parentId: null, name: '基础资料', code: 'master-data', type: '目录', path: '', method: '-', status: '启用', sort: 25 },
|
|
{ id: 'P160', parentId: 'P015', name: '快递公司', code: 'platform:express:list', type: '菜单', path: '/master-data/express-companies', method: '-', status: '启用', sort: 10 },
|
|
{ id: 'P161', parentId: 'P160', name: '维护快递公司', code: 'express-company:write', type: '按钮', path: '', method: '-', status: '启用', sort: 10 },
|
|
{ id: 'P162', parentId: 'P160', name: '导入快递公司', code: 'express-company:import', type: '按钮', path: '', method: '-', status: '启用', sort: 20 },
|
|
{ id: 'P165', parentId: 'P015', name: '往来单位', code: 'platform:counterparty:list', type: '菜单', path: '/master-data/counterparties', method: '-', status: '启用', sort: 20 },
|
|
{ id: 'P166', parentId: 'P165', name: '维护往来单位', code: 'counterparty:write', type: '按钮', path: '', method: '-', status: '启用', sort: 10 },
|
|
{ id: 'P167', parentId: 'P165', name: '导入往来单位', code: 'counterparty:import', type: '按钮', path: '', method: '-', status: '启用', sort: 20 },
|
|
{ id: 'P020', parentId: null, name: '系统管理', code: 'system', type: '目录', path: '', method: '-', status: '启用', sort: 30 },
|
|
{ id: 'P300', parentId: 'P020', name: '内部用户', code: 'platform:user:list', type: '菜单', path: '/system/users', method: '-', status: '启用', sort: 10 },
|
|
{ id: 'P302', parentId: 'P300', name: '维护用户', code: 'system:user:write', type: '按钮', path: '', method: '-', status: '启用', sort: 10 },
|
|
{ id: 'P400', parentId: 'P020', name: '角色管理', code: 'platform:role:list', type: '菜单', path: '/system/roles', method: '-', status: '启用', sort: 20 },
|
|
{ id: 'P402', parentId: 'P400', name: '角色授权', code: 'system:role:grant', type: '按钮', path: '', method: '-', status: '启用', sort: 10 },
|
|
{ id: 'P500', parentId: 'P020', name: '菜单权限', code: 'platform:permission:list', type: '菜单', path: '/system/permissions', method: '-', status: '启用', sort: 30 },
|
|
{ id: 'P502', parentId: 'P500', name: '维护权限', code: 'system:permission:write', type: '按钮', path: '', method: '-', status: '启用', sort: 10 },
|
|
]
|
|
|
|
const allPermissionIds = initialPermissions.map((item) => item.id)
|
|
const initialRoles: Role[] = [
|
|
{ id: 'R001', name: '超级管理员', code: 'SUPER_ADMIN', description: '平台最高权限,拥有全部菜单和数据权限。', userCount: 1, dataScope: '全部数据', status: '启用', permissionIds: allPermissionIds, builtIn: true },
|
|
{ id: 'R002', name: '运营主管', code: 'OPS_MANAGER', description: '负责客户审核、仓库维护、基础资料、订单分派和运营监控。', userCount: 1, dataScope: '全部数据', status: '启用', permissionIds: ['P010', 'P100', 'P103', 'P150', 'P152', 'P200', 'P203', 'P204', 'P015', 'P160', 'P161', 'P162', 'P165', 'P166', 'P167'] },
|
|
{ id: 'R003', name: '操作员', code: 'OPERATOR', description: '查看收货仓并处理头程订单和物流节点更新。', userCount: 2, dataScope: '本部门数据', status: '启用', permissionIds: ['P010', 'P150', 'P200', 'P202', 'P203', 'P204'] },
|
|
{ id: 'R004', name: '财务', code: 'FINANCE', description: '查看订单应收并处理对账结算。', userCount: 1, dataScope: '本部门数据', status: '启用', permissionIds: ['P010', 'P200'] },
|
|
{ id: 'R005', name: '销售', code: 'SALES', description: '维护客户、查看收货仓并跟进本人负责的订单。', userCount: 1, dataScope: '本人数据', status: '启用', permissionIds: ['P010', 'P100', 'P102', 'P150', 'P200'] },
|
|
]
|
|
|
|
function demoWarehouseSnapshot(id: string) {
|
|
const warehouse = DEMO_PLATFORM_RECEIVING_WAREHOUSES.find((item) => item.id === id)
|
|
if (!warehouse) throw new Error(`演示仓库不存在:${id}`)
|
|
return createPlatformReceivingWarehouseSnapshot(warehouse)
|
|
}
|
|
|
|
const initialOrders: FirstLegOrder[] = [
|
|
{ id: 'O001', no: 'BY20260727001', customerId: 'T001', customerName: '华远贸易有限公司', referenceNo: 'HY-0727-01', transportMode: '海运整箱 FCL', businessType: '出口', origin: '宁波', destination: '洛杉矶', receivingWarehouseId: 'WH005', receivingWarehouseSnapshot: demoWarehouseSnapshot('WH005'), container: '40HQ x 2', etd: '2026-08-02', eta: '2026-08-18', amount: 48500, currency: 'CNY', status: '已受理', priority: '紧急', operator: '张伟', shipper: '华远贸易有限公司', consignee: 'HY GLOBAL LLC', cargoName: '家居用品', pieces: 1260, weight: 27800, volume: 62.5, createdAt: '2026-07-27 09:18', remark: '需要在截关日前完成报关资料确认。', timeline: [{ time: '2026-07-27 09:18', title: '客户提交订单', description: '客户完成货物与平台收货仓信息提交。', operator: '客户管理员' }, { time: '2026-07-27 10:02', title: '订单已受理', description: '运营审核资料完整,进入订舱准备。', operator: '张伟' }] },
|
|
{ id: 'O002', no: 'BY20260726008', customerId: 'T002', customerName: '深圳速达供应链有限公司', referenceNo: 'SD-EU-558', transportMode: '海运拼箱 LCL', businessType: '出口', origin: '深圳', destination: '汉堡', receivingWarehouseId: 'WH002', receivingWarehouseSnapshot: demoWarehouseSnapshot('WH002'), container: 'LCL 8.6 CBM', etd: '2026-08-05', eta: '2026-09-06', amount: 12800, currency: 'CNY', status: '已订舱', priority: '普通', operator: '李明', shipper: '深圳速达供应链有限公司', consignee: 'SUDA EUROPE GMBH', cargoName: '电子配件', pieces: 286, weight: 5200, volume: 8.6, createdAt: '2026-07-26 15:42', remark: '', timeline: [{ time: '2026-07-26 15:42', title: '客户提交订单', description: '订单进入平台审核。', operator: '客户管理员' }, { time: '2026-07-27 08:55', title: '订舱完成', description: '已获得船公司舱位确认。', operator: '李明' }] },
|
|
{ id: 'O003', no: 'BY20260726005', customerId: 'T003', customerName: '广州恒通物流有限公司', referenceNo: 'HT-AIR-0719', transportMode: '空运', businessType: '出口', origin: '广州', destination: '纽约', receivingWarehouseId: 'WH002', receivingWarehouseSnapshot: demoWarehouseSnapshot('WH002'), container: '500 KG', etd: '2026-07-30', eta: '2026-08-01', amount: 35600, currency: 'CNY', status: '运输中', priority: '加急', operator: '赵婷', shipper: '广州恒通物流有限公司', consignee: 'HENGTONG US INC.', cargoName: '服装样品', pieces: 36, weight: 500, volume: 2.8, createdAt: '2026-07-26 10:20', remark: '航班起飞后第一时间同步轨迹。', timeline: [{ time: '2026-07-26 10:20', title: '客户提交订单', description: '加急空运订单。', operator: '客户管理员' }, { time: '2026-07-27 16:30', title: '货物已起飞', description: '航班 CZ699 离港。', operator: '赵婷' }] },
|
|
{ id: 'O004', no: 'BY20260725012', customerId: 'T004', customerName: '宁波远洋货运代理有限公司', referenceNo: 'NB-ROAD-201', transportMode: '陆运', businessType: '出口', origin: '宁波', destination: '霍尔果斯', receivingWarehouseId: 'WH005', receivingWarehouseSnapshot: demoWarehouseSnapshot('WH005'), container: '整车 x 3', etd: '2026-07-28', eta: '2026-08-08', amount: 18200, currency: 'CNY', status: '待审核', priority: '普通', operator: '未分配', shipper: '宁波远洋货运代理有限公司', consignee: 'ALATAU TRADING LLP', cargoName: '机械配件', pieces: 480, weight: 18500, volume: 46, createdAt: '2026-07-25 14:08', remark: '', timeline: [{ time: '2026-07-25 14:08', title: '客户提交订单', description: '等待平台审核与人员分派。', operator: '客户管理员' }] },
|
|
{ id: 'O005', no: 'BY20260724009', customerId: 'T001', customerName: '华远贸易有限公司', referenceNo: 'HY-0724-06', transportMode: '海运整箱 FCL', businessType: '出口', origin: '上海', destination: '长滩', receivingWarehouseId: 'WH004', receivingWarehouseSnapshot: demoWarehouseSnapshot('WH004'), container: '20GP x 5', etd: '2026-07-28', eta: '2026-08-12', amount: 56000, currency: 'CNY', status: '已完成', priority: '普通', operator: '张伟', shipper: '华远贸易有限公司', consignee: 'HY GLOBAL LLC', cargoName: '户外用品', pieces: 3200, weight: 55000, volume: 135, createdAt: '2026-07-24 08:45', remark: '', timeline: [{ time: '2026-07-24 08:45', title: '客户提交订单', description: '订单资料提交。', operator: '客户管理员' }, { time: '2026-07-27 12:20', title: '订单已完成', description: '全部业务节点已确认。', operator: '张伟' }] },
|
|
{ id: 'O006', no: 'BY20260723007', customerId: 'T002', customerName: '深圳速达供应链有限公司', referenceNo: 'SD-EU-552', transportMode: '海运拼箱 LCL', businessType: '出口', origin: '青岛', destination: '鹿特丹', receivingWarehouseId: 'WH003', receivingWarehouseSnapshot: demoWarehouseSnapshot('WH003'), container: 'LCL 12 CBM', etd: '2026-08-10', eta: '2026-09-15', amount: 15300, currency: 'CNY', status: '草稿', priority: '普通', operator: '未分配', shipper: '深圳速达供应链有限公司', consignee: 'SUDA EUROPE GMBH', cargoName: '厨房用品', pieces: 520, weight: 8500, volume: 12, createdAt: '2026-07-23 17:10', remark: '等待客户补充收货人税号。', timeline: [{ time: '2026-07-23 17:10', title: '保存草稿', description: '订单尚未提交审核。', operator: '客户管理员' }] },
|
|
]
|
|
|
|
export const useAdminDataStore = defineStore('admin-data', () => {
|
|
const customers = ref<Customer[]>(structuredClone(initialCustomers))
|
|
const users = ref<InternalUser[]>(structuredClone(initialUsers))
|
|
const roles = ref<Role[]>(structuredClone(initialRoles))
|
|
const permissions = ref<Permission[]>(structuredClone(initialPermissions))
|
|
const warehouses = ref<Warehouse[]>(DEMO_PLATFORM_RECEIVING_WAREHOUSES.map((item) => ({ ...item })))
|
|
const expressCompanies = ref<ExpressCompany[]>(structuredClone(initialExpressCompanies))
|
|
const counterparties = ref<Counterparty[]>(structuredClone(initialCounterparties))
|
|
const orders = ref<FirstLegOrder[]>(structuredClone(initialOrders))
|
|
|
|
const activeUsers = computed(() => users.value.filter((item) => item.status === '正常').length)
|
|
const pendingCustomers = computed(() => customers.value.filter((item) => item.status === '待审核').length)
|
|
const activeWarehouses = computed(() => warehouses.value.filter((item) => item.status === 'ACTIVE').length)
|
|
const activeExpressCompanies = computed(() => expressCompanies.value.filter((item) => item.status === '合作中').length)
|
|
const activeCounterparties = computed(() => counterparties.value.filter((item) => item.status === '启用').length)
|
|
const pendingOrders = computed(() => orders.value.filter((item) => ['待审核', '已受理'].includes(item.status)).length)
|
|
|
|
function nextId(prefix: string, rows: Array<{ id: string }>) {
|
|
const max = rows.reduce((value, item) => Math.max(value, Number(item.id.replace(/\D/g, '')) || 0), 0)
|
|
return `${prefix}${String(max + 1).padStart(3, '0')}`
|
|
}
|
|
|
|
function saveCustomer(payload: Omit<Customer, 'id' | 'orders' | 'joinedAt'> & { id?: string }) {
|
|
if (payload.id) {
|
|
const index = customers.value.findIndex((item) => item.id === payload.id)
|
|
if (index >= 0) customers.value[index] = { ...customers.value[index], ...payload } as Customer
|
|
return payload.id
|
|
}
|
|
const id = nextId('T', customers.value)
|
|
customers.value.unshift({ ...payload, id, orders: 0, joinedAt: new Date().toISOString().slice(0, 10) })
|
|
return id
|
|
}
|
|
|
|
function setCustomerStatus(id: string, status: Customer['status']) {
|
|
const customer = customers.value.find((item) => item.id === id)
|
|
if (customer) customer.status = status
|
|
}
|
|
|
|
function saveWarehouse(payload: Omit<Warehouse, 'id' | 'updatedAt'> & { id?: string }) {
|
|
const duplicate = warehouses.value.find((item) => item.code.toLowerCase() === payload.code.trim().toLowerCase() && item.id !== payload.id)
|
|
if (duplicate) throw new Error('仓库编码已存在')
|
|
const updatedAt = new Date().toISOString().slice(0, 16).replace('T', ' ')
|
|
if (payload.id) {
|
|
const index = warehouses.value.findIndex((item) => item.id === payload.id)
|
|
if (index >= 0) warehouses.value[index] = { ...warehouses.value[index], ...payload, code: payload.code.trim().toUpperCase(), updatedAt } as Warehouse
|
|
return payload.id
|
|
}
|
|
const id = nextId('WH', warehouses.value)
|
|
warehouses.value.unshift({ ...payload, id, code: payload.code.trim().toUpperCase(), updatedAt })
|
|
return id
|
|
}
|
|
|
|
function setWarehouseStatus(id: string, status: Warehouse['status']) {
|
|
const warehouse = warehouses.value.find((item) => item.id === id)
|
|
if (warehouse) {
|
|
warehouse.status = status
|
|
warehouse.updatedAt = new Date().toISOString().slice(0, 16).replace('T', ' ')
|
|
}
|
|
}
|
|
|
|
function saveExpressCompany(payload: Omit<ExpressCompany, 'id' | 'updatedAt'> & { id?: string }) {
|
|
const normalizedCode = payload.code.trim().toUpperCase()
|
|
const duplicateCode = expressCompanies.value.find((item) => item.code.toLowerCase() === normalizedCode.toLowerCase() && item.id !== payload.id)
|
|
if (duplicateCode) throw new Error('快递公司编码已存在')
|
|
const duplicateName = expressCompanies.value.find((item) => item.name.trim().toLowerCase() === payload.name.trim().toLowerCase() && item.id !== payload.id)
|
|
if (duplicateName) throw new Error('快递公司名称已存在')
|
|
const updatedAt = new Date().toISOString().slice(0, 16).replace('T', ' ')
|
|
if (payload.id) {
|
|
const index = expressCompanies.value.findIndex((item) => item.id === payload.id)
|
|
if (index >= 0) expressCompanies.value[index] = { ...expressCompanies.value[index], ...payload, code: normalizedCode, updatedAt } as ExpressCompany
|
|
return payload.id
|
|
}
|
|
const id = nextId('E', expressCompanies.value)
|
|
expressCompanies.value.unshift({ ...payload, id, code: normalizedCode, updatedAt })
|
|
return id
|
|
}
|
|
|
|
function setExpressCompanyStatus(id: string, status: ExpressCompany['status']) {
|
|
const company = expressCompanies.value.find((item) => item.id === id)
|
|
if (!company) return
|
|
company.status = status
|
|
company.updatedAt = new Date().toISOString().slice(0, 16).replace('T', ' ')
|
|
}
|
|
|
|
function saveCounterparty(payload: Omit<Counterparty, 'id' | 'updatedAt'> & { id?: string }) {
|
|
const normalizedCode = payload.code.trim().toUpperCase()
|
|
const duplicate = counterparties.value.find((item) => item.code.toLowerCase() === normalizedCode.toLowerCase() && item.id !== payload.id)
|
|
if (duplicate) throw new Error('往来单位编码已存在')
|
|
const updatedAt = new Date().toISOString().slice(0, 16).replace('T', ' ')
|
|
if (payload.id) {
|
|
const index = counterparties.value.findIndex((item) => item.id === payload.id)
|
|
if (index >= 0) counterparties.value[index] = { ...counterparties.value[index], ...payload, code: normalizedCode, types: [...payload.types], updatedAt } as Counterparty
|
|
return payload.id
|
|
}
|
|
const id = nextId('CP', counterparties.value)
|
|
counterparties.value.unshift({ ...payload, id, code: normalizedCode, types: [...payload.types], updatedAt })
|
|
return id
|
|
}
|
|
|
|
function setCounterpartyStatus(id: string, status: Counterparty['status']) {
|
|
const counterparty = counterparties.value.find((item) => item.id === id)
|
|
if (!counterparty) return
|
|
counterparty.status = status
|
|
counterparty.updatedAt = new Date().toISOString().slice(0, 16).replace('T', ' ')
|
|
}
|
|
|
|
function saveUser(payload: Omit<InternalUser, 'id' | 'lastLogin' | 'createdAt'> & { id?: string }) {
|
|
if (payload.id) {
|
|
const index = users.value.findIndex((item) => item.id === payload.id)
|
|
if (index >= 0) users.value[index] = { ...users.value[index], ...payload } as InternalUser
|
|
return payload.id
|
|
}
|
|
const id = nextId('U', users.value)
|
|
users.value.unshift({ ...payload, id, lastLogin: '-', createdAt: new Date().toISOString().slice(0, 10) })
|
|
syncRoleCounts()
|
|
return id
|
|
}
|
|
|
|
function setUserStatus(id: string, status: InternalUser['status']) {
|
|
const user = users.value.find((item) => item.id === id)
|
|
if (user && user.account !== 'admin') user.status = status
|
|
}
|
|
|
|
function saveRole(payload: Omit<Role, 'id' | 'userCount'> & { id?: string }) {
|
|
if (payload.id) {
|
|
const index = roles.value.findIndex((item) => item.id === payload.id)
|
|
if (index >= 0) roles.value[index] = { ...roles.value[index], ...payload } as Role
|
|
syncRoleCounts()
|
|
return payload.id
|
|
}
|
|
const id = nextId('R', roles.value)
|
|
roles.value.push({ ...payload, id, userCount: 0 })
|
|
return id
|
|
}
|
|
|
|
function removeRole(id: string) {
|
|
const role = roles.value.find((item) => item.id === id)
|
|
if (!role || role.builtIn || users.value.some((item) => item.roleIds.includes(id))) return false
|
|
roles.value = roles.value.filter((item) => item.id !== id)
|
|
return true
|
|
}
|
|
|
|
function syncRoleCounts() {
|
|
roles.value.forEach((role) => {
|
|
role.userCount = users.value.filter((user) => user.roleIds.includes(role.id)).length
|
|
})
|
|
}
|
|
|
|
function savePermission(payload: Omit<Permission, 'id'> & { id?: string }) {
|
|
if (payload.id) {
|
|
const index = permissions.value.findIndex((item) => item.id === payload.id)
|
|
if (index >= 0) permissions.value[index] = { ...permissions.value[index], ...payload } as Permission
|
|
return payload.id
|
|
}
|
|
const id = nextId('P', permissions.value)
|
|
permissions.value.push({ ...payload, id })
|
|
return id
|
|
}
|
|
|
|
function saveOrder(payload: Omit<FirstLegOrder, 'id' | 'no' | 'createdAt' | 'timeline' | 'receivingWarehouseSnapshot'> & { id?: string }) {
|
|
const existing = payload.id ? orders.value.find((item) => item.id === payload.id) : undefined
|
|
const warehouse = warehouses.value.find((item) => item.id === payload.receivingWarehouseId)
|
|
const keepsExistingWarehouse = existing?.receivingWarehouseId === payload.receivingWarehouseId
|
|
if (!warehouse || (warehouse.status !== 'ACTIVE' && !keepsExistingWarehouse)) throw new Error('所选收货仓当前不可用于新订单')
|
|
const receivingWarehouseSnapshot = keepsExistingWarehouse && existing?.receivingWarehouseSnapshot
|
|
? existing.receivingWarehouseSnapshot
|
|
: createPlatformReceivingWarehouseSnapshot(warehouse)
|
|
if (payload.id) {
|
|
const index = orders.value.findIndex((item) => item.id === payload.id)
|
|
if (index >= 0) {
|
|
const previous = orders.value[index]
|
|
const keepsPendingReview = previous.status === '待审核'
|
|
const time = new Date().toISOString().slice(0, 16).replace('T', ' ')
|
|
const timeline = [...previous.timeline]
|
|
if (keepsPendingReview) timeline.push({
|
|
time,
|
|
title: '后台修改待审核订单',
|
|
description: '平台已修正订单资料,订单继续保持待审核;本次修改已记录审计。',
|
|
operator: payload.operator === '未分配' ? '平台管理员' : payload.operator,
|
|
})
|
|
orders.value[index] = {
|
|
...previous,
|
|
...payload,
|
|
status: keepsPendingReview ? '待审核' : payload.status,
|
|
receivingWarehouseSnapshot,
|
|
timeline,
|
|
} as FirstLegOrder
|
|
}
|
|
return payload.id
|
|
}
|
|
const id = nextId('O', orders.value)
|
|
const no = `BY${new Date().toISOString().slice(0, 10).replace(/-/g, '')}${String(orders.value.length + 1).padStart(3, '0')}`
|
|
orders.value.unshift({
|
|
...payload,
|
|
receivingWarehouseSnapshot,
|
|
id,
|
|
no,
|
|
createdAt: new Date().toISOString().slice(0, 16).replace('T', ' '),
|
|
timeline: [{ time: new Date().toISOString().slice(0, 16).replace('T', ' '), title: '后台创建订单', description: '平台工作人员创建头程订单。', operator: '平台管理员' }],
|
|
})
|
|
return id
|
|
}
|
|
|
|
function updateOrderStatus(id: string, status: OrderStatus, operator: string, note: string) {
|
|
const order = orders.value.find((item) => item.id === id)
|
|
if (!order) return
|
|
const transitions: Record<OrderStatus, OrderStatus[]> = { 草稿: ['待审核', '已取消'], 待审核: ['已受理', '已取消'], 已受理: ['已订舱', '已取消'], 已订舱: ['运输中', '已取消'], 运输中: ['已完成', '已取消'], 已完成: [], 已取消: [] }
|
|
if (status !== order.status && !transitions[order.status].includes(status)) throw new Error(`订单不能从“${order.status}”直接变更为“${status}”`)
|
|
order.status = status
|
|
order.operator = operator || order.operator
|
|
order.timeline.push({
|
|
time: new Date().toISOString().slice(0, 16).replace('T', ' '),
|
|
title: `状态更新为${status}`,
|
|
description: note || '平台更新订单处理状态。',
|
|
operator: operator || '平台管理员',
|
|
})
|
|
}
|
|
|
|
function recordCustomerModificationContact(id: string, operator: string) {
|
|
const order = orders.value.find((item) => item.id === id)
|
|
if (!order || order.status !== '待审核') throw new Error('仅待审核订单可记录联系客户修改')
|
|
order.timeline.push({ time: new Date().toISOString().slice(0, 16).replace('T', ' '), title: '联系客户修改订单', description: '已联系客户补充或修正订单资料,订单继续保持待审核。', operator })
|
|
}
|
|
|
|
function saveOrderDeliveryInfo(id: string, expressCompanyId: string, trackingNo: string, operator: string) {
|
|
const order = orders.value.find((item) => item.id === id)
|
|
const company = expressCompanies.value.find((item) => item.id === expressCompanyId)
|
|
if (!order) throw new Error('订单不存在')
|
|
if (!company) throw new Error('快递公司不存在')
|
|
if (company.status !== '合作中' && order.deliveryInfo?.expressCompanyId !== company.id) throw new Error('停用快递公司不能新增到履约信息')
|
|
const time = new Date().toISOString().slice(0, 16).replace('T', ' ')
|
|
order.deliveryInfo = {
|
|
expressCompanyId: company.id,
|
|
expressCompanyCode: company.code,
|
|
expressCompanyName: company.name,
|
|
trackingNo: trackingNo.trim(),
|
|
updatedAt: time,
|
|
operator,
|
|
}
|
|
order.timeline.push({
|
|
time,
|
|
title: '更新履约快递信息',
|
|
description: `${company.name} · ${trackingNo.trim()}`,
|
|
operator,
|
|
})
|
|
}
|
|
|
|
return {
|
|
customers,
|
|
users,
|
|
roles,
|
|
permissions,
|
|
warehouses,
|
|
expressCompanies,
|
|
counterparties,
|
|
orders,
|
|
activeUsers,
|
|
pendingCustomers,
|
|
activeWarehouses,
|
|
activeExpressCompanies,
|
|
activeCounterparties,
|
|
pendingOrders,
|
|
saveCustomer,
|
|
setCustomerStatus,
|
|
saveWarehouse,
|
|
setWarehouseStatus,
|
|
saveExpressCompany,
|
|
setExpressCompanyStatus,
|
|
saveCounterparty,
|
|
setCounterpartyStatus,
|
|
saveUser,
|
|
setUserStatus,
|
|
saveRole,
|
|
removeRole,
|
|
savePermission,
|
|
saveOrder,
|
|
updateOrderStatus,
|
|
recordCustomerModificationContact,
|
|
saveOrderDeliveryInfo,
|
|
}
|
|
})
|