diff --git a/package.json b/package.json index 00477f9f..0613f286 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "preview": "vite preview --port 4173" }, "dependencies": { - "@chongchong/shared": "link:../../packages/shared", + "@chongchong/shared": "link:./shared", "@lucide/vue": "^1.27.0", "axios": "^1.18.1", "element-plus": "^2.14.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb5157c9..53a31cc7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@chongchong/shared': - specifier: link:../../packages/shared - version: link:../../packages/shared + specifier: link:./shared + version: link:./shared '@lucide/vue': specifier: ^1.27.0 version: 1.27.0(vue@3.5.40(typescript@5.9.3)) diff --git a/shared/AGENTS.md b/shared/AGENTS.md new file mode 100644 index 00000000..e25fc861 --- /dev/null +++ b/shared/AGENTS.md @@ -0,0 +1,28 @@ +# Shared Package Instructions + +## 项目定位 + +- 本包提供跨应用共享的 TypeScript 类型、常量、轻量工具和可复用资源。 +- 当前公共能力包括产品名称、用户模型、导航模型、API 响应模型和头程订单状态常量。 +- 只有具有明确跨项目语义,或已经被两个或以上项目稳定复用的能力才进入本包;单一应用的页面状态、UI 组件和临时业务逻辑留在对应项目。尚无消费者的契约型导出必须标记为暂定,并有对应 API 或产品决策依据。 + +## 公共 API 与依赖边界 + +- `src/index.ts` 是当前唯一公共入口。新增导出应有清晰命名和稳定语义,不让消费者依赖未导出的内部实现。 +- 公共类型保持平台无关,不依赖 DOM、Vue、微信、uni-app 或 Node 专属运行时,除非任务明确改变本包定位。 +- 不在共享类型中编码客户端鉴权结论;租户隔离、权限和数据合法性仍由 `services/api` 最终校验。 +- 避免以“复用”为由建立宽泛的万能类型。优先使用小而明确的模型,并区分请求、响应、领域状态和展示模型。 +- 新增依赖前先确认原生 TypeScript 能力或现有依赖无法满足需求;本包不应成为所有应用依赖的重量级入口。 + +## 兼容性与变更原则 + +- 修改导出名称、字段类型、枚举值或常量语义前,搜索所有消费者并评估兼容性。 +- 优先采用向后兼容的新增;删除或破坏性修改必须同步更新全部消费者,并在交付说明中明确迁移影响。 +- API 契约文档是服务端落地后的唯一权威来源;共享包只映射经确认的客户端模型,不反向定义服务端行为。API 尚未落地前,`ApiResponse` 和订单状态等无消费者导出视为暂定,不得当作已发布契约。 +- 状态码、权限码和业务常量应有唯一来源,不在多个应用中复制维护相同列表。 + +## 质量门禁 + +- 当前包未声明独立的安装、测试、类型检查或构建脚本,不得声称这些检查已在本包内通过。 +- 变更公共导出后,至少运行现有消费者的类型检查;当前直接消费者包括 `apps/admin-web` 和 `apps/client-web`。 +- 若新增本包自己的质量脚本,应同步更新 `package.json`、`README.md` 和本文件,并为公共工具及兼容性边界增加测试。 diff --git a/shared/README.md b/shared/README.md new file mode 100644 index 00000000..ec462282 --- /dev/null +++ b/shared/README.md @@ -0,0 +1,7 @@ +# 共享模块 + +用于存放跨应用共享的类型定义、常量、工具和可复用资源。 + +当前已提供产品名称、用户模型、导航模型、API 响应模型,以及经一期产品决策确认的头程订单状态与运输方式常量。 + +当前还提供平台收货仓模型、订单仓库快照模型和供 Admin、Client 静态原型共用的演示仓库数据。订单仓库快照在下单时复制仓库 ID、编码、名称及国内地址,避免仓库主数据后续改名或停用影响历史订单展示;真实仓库列表、状态、下单可用性和快照持久化最终由服务端 API 保证。 diff --git a/shared/package.json b/shared/package.json new file mode 100644 index 00000000..5e802c2d --- /dev/null +++ b/shared/package.json @@ -0,0 +1,11 @@ +{ + "name": "@chongchong/shared", + "version": "0.1.0", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "types": "./src/index.ts" +} + diff --git a/shared/src/index.ts b/shared/src/index.ts new file mode 100644 index 00000000..5bf0aef9 --- /dev/null +++ b/shared/src/index.ts @@ -0,0 +1,155 @@ +export const PRODUCT_NAME = '虫虫出海' + +const MAX_SAFE_INTEGER = BigInt(Number.MAX_SAFE_INTEGER) + +function quoteUnsafeIntegerTokens(source: string) { + let output = '' + let index = 0 + let inString = false + let escaped = false + + while (index < source.length) { + const character = source[index] + + if (inString) { + output += character + if (escaped) escaped = false + else if (character === '\\') escaped = true + else if (character === '"') inString = false + index += 1 + continue + } + + if (character === '"') { + inString = true + output += character + index += 1 + continue + } + + if (character === '-' || (character >= '0' && character <= '9')) { + const token = source.slice(index).match(/^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/)?.[0] + if (token) { + const isInteger = !token.includes('.') && !/[eE]/.test(token) + if (isInteger && (BigInt(token) > MAX_SAFE_INTEGER || BigInt(token) < -MAX_SAFE_INTEGER)) output += JSON.stringify(token) + else output += token + index += token.length + continue + } + } + + output += character + index += 1 + } + + return output +} + +export function parseJsonWithSafeIntegers(source: string) { + return JSON.parse(quoteUnsafeIntegerTokens(source)) as unknown +} + +export function parseApiJsonResponse(data: unknown) { + if (typeof data !== 'string') return data + const trimmed = data.trim() + if (!trimmed || (!trimmed.startsWith('{') && !trimmed.startsWith('['))) return data + try { + return parseJsonWithSafeIntegers(data) + } catch { + return data + } +} + +export type AppCode = 'admin' | 'client' + +export interface CurrentUser { + id: string + name: string + account: string + tenantName?: string + roles: string[] +} + +export interface NavigationItem { + path: string + title: string + permission?: string +} + +export interface ApiResponse { + code: number + message: string + data: T +} + +export const FIRST_LEG_ORDER_STATUSES = [ + { code: 'DRAFT', label: '草稿' }, + { code: 'PENDING_REVIEW', label: '待审核' }, + { code: 'ACCEPTED', label: '已受理' }, + { code: 'BOOKED', label: '已订舱' }, + { code: 'IN_TRANSIT', label: '运输中' }, + { code: 'COMPLETED', label: '已完成' }, + { code: 'CANCELLED', label: '已取消' }, +] as const + +export type FirstLegOrderStatus = (typeof FIRST_LEG_ORDER_STATUSES)[number]['code'] +export type FirstLegOrderStatusLabel = (typeof FIRST_LEG_ORDER_STATUSES)[number]['label'] + +export const FIRST_LEG_TRANSPORT_MODES = [ + { code: 'FCL', label: '海运整箱 FCL' }, + { code: 'LCL', label: '海运拼箱 LCL' }, + { code: 'AIR', label: '空运' }, + { code: 'ROAD', label: '陆运' }, +] as const + +export type FirstLegTransportMode = (typeof FIRST_LEG_TRANSPORT_MODES)[number]['code'] +export type FirstLegTransportModeLabel = (typeof FIRST_LEG_TRANSPORT_MODES)[number]['label'] + +export type PlatformWarehouseStatus = 'ACTIVE' | 'INACTIVE' + +export interface PlatformReceivingWarehouse { + id: string + code: string + name: string + province: string + city: string + address: string + contactName: string + contactPhone: string + businessHours: string + status: PlatformWarehouseStatus + remark: string + updatedAt: string +} + +export interface PlatformReceivingWarehouseSnapshot { + id: string + code: string + name: string + province: string + city: string + address: string +} + +export function createPlatformReceivingWarehouseSnapshot( + warehouse: PlatformReceivingWarehouse, +): PlatformReceivingWarehouseSnapshot { + return { + id: warehouse.id, + code: warehouse.code, + name: warehouse.name, + province: warehouse.province, + city: warehouse.city, + address: warehouse.address, + } +} + +// API 接入前供 Admin 与 Client 静态原型共用,不能作为服务端真实仓库数据。 +export const DEMO_PLATFORM_RECEIVING_WAREHOUSES: readonly PlatformReceivingWarehouse[] = [ + { id: 'WH001', code: 'CN-YW-01', name: '义乌仓', province: '浙江省', city: '义乌市', address: '北苑街道演示物流园 1 号库', contactName: '仓库客服', contactPhone: '0579-****001', businessHours: '周一至周六 08:30-18:00', status: 'ACTIVE', remark: '支持普通货物收货。', updatedAt: '2026-07-28 10:20' }, + { id: 'WH002', code: 'CN-SZ-01', name: '深圳仓', province: '广东省', city: '深圳市', address: '盐田区演示物流园 2 号库', contactName: '仓库客服', contactPhone: '0755-****002', businessHours: '周一至周六 08:30-18:00', status: 'ACTIVE', remark: '华南区域主要收货仓。', updatedAt: '2026-07-28 10:20' }, + { id: 'WH003', code: 'CN-QD-01', name: '青岛仓', province: '山东省', city: '青岛市', address: '黄岛区演示物流园 3 号库', contactName: '仓库客服', contactPhone: '0532-****003', businessHours: '周一至周五 09:00-17:30', status: 'ACTIVE', remark: '', updatedAt: '2026-07-28 10:20' }, + { id: 'WH004', code: 'CN-SH-01', name: '上海仓', province: '上海市', city: '上海市', address: '浦东新区演示物流园 4 号库', contactName: '仓库客服', contactPhone: '021-****004', businessHours: '周一至周六 08:30-18:00', status: 'ACTIVE', remark: '华东区域综合收货仓。', updatedAt: '2026-07-28 10:20' }, + { id: 'WH005', code: 'CN-NB-01', name: '宁波仓', province: '浙江省', city: '宁波市', address: '北仑区演示物流园 5 号库', contactName: '仓库客服', contactPhone: '0574-****005', businessHours: '周一至周六 08:30-18:00', status: 'ACTIVE', remark: '临近宁波港。', updatedAt: '2026-07-28 10:20' }, + { id: 'WH006', code: 'CN-XM-01', name: '厦门仓', province: '福建省', city: '厦门市', address: '湖里区演示物流园 6 号库', contactName: '仓库客服', contactPhone: '0592-****006', businessHours: '周一至周五 09:00-17:30', status: 'INACTIVE', remark: '静态演示:暂停接收新订单。', updatedAt: '2026-07-28 10:20' }, +]