优化
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
# 四期海外仓前端待补契约
|
||||
|
||||
第一批正式页面只冻结路由、权限、字段与操作承载方式。以下后端/基础设施契约缺失前,页面不得用演示数据或本地成功提示替代真实调用:
|
||||
|
||||
- 海外区域 API Base、海外 Access Token 的存储/刷新/选择机制,以及与现有国内 `http` 客户端的统一错误处理。
|
||||
- 尾程订单分页查询、详情、导入模板、批量导入、异步导出、批量转仓和入库计划接口;分页查询需明确“全部 / 待分配”页签的请求映射,以及出库单号、派送单号只读关联字段。
|
||||
- 入库单分页查询、详情、创建、编辑、预约、智能排车接口,以及状态机、幂等键和操作权限。
|
||||
- 库存汇总、分页查询、详情、库位库存、托盘箱号和进销存记录接口。
|
||||
- 换标单分页、详情、创建、编辑/审核、文件上传与费用明细接口,以及状态机和幂等约定。
|
||||
- 托盘分页、详情、创建、编辑接口,以及海外仓/FBA 仓候选、编码唯一性与占用状态约束。
|
||||
- 箱号分页、详情、创建、编辑接口,以及产品/托盘/入库单关联校验;“已出库”只作为只读库存事实,本模块不承接出库动作。
|
||||
- 产品分页、详情、创建、编辑接口,以及 SKU 唯一性、品类和包装尺寸精度约定。
|
||||
- 四期权限资源在 IAM 初始化数据中的正式登记。前端暂按本模块 `permissions.ts` 的稳定编码装配。
|
||||
- 所有分页响应、错误码、`requestId`/`traceId`、时间/金额精度和长整型 ID 字符串传输约定。
|
||||
|
||||
范围排除:海外仓出库、退货出库、海外派送和其他库存离仓接口。
|
||||
|
||||
当前 `types.ts` 只冻结原型派生的页面展示字段,不是海外 API DTO;不得据此推导分页包络、URL、请求参数、时间/金额精度或写接口。待上述契约冻结后再建立实际 API 适配层,并逐页替换空态数据源。
|
||||
@@ -0,0 +1,57 @@
|
||||
# 四期海外仓原型核对矩阵
|
||||
|
||||
原型来源:`docs/悦/货代SAAS平台后台/prototype-src/modules/03-overseas-warehouse.html`。
|
||||
|
||||
| 页面 | 操作 | 原型证据 | 原型承载方式 | 当前实现(开工前) | 冻结结论 |
|
||||
|---|---|---|---|---|---|
|
||||
| 尾程订单 | 列表/筛选 | `page-last-mile-orders`,868–897 行;`lastMileSearch()`、`lastMileReset()` | 独立列表页,状态标签、查询区、横向宽表 | `/prototype/overseas-warehouse-orders/*` 占位 | 建立正式列表路由;操作列第一列,保留原型字段顺序,接口未接时显示明确契约态 |
|
||||
| 尾程订单 | 详情 | `page-last-mile-order-detail`,899–909 行;`openLastMileOrderDetail()`、`renderLastMileOrderDetail()` | 独立详情页,紧凑页头;订单信息、订单明细、计划信息、关联单据、货物信息、操作记录两列分区 | 正式参数路由与 `LastMileOrderDetailView.vue`;接口待接入时保留字段、空表与明确契约态 | 保持独立页面,不改为抽屉或弹框;原型中的出库计划和派送字段因本期边界排除,不形成操作入口 |
|
||||
| 尾程订单 | 导入 | `modal-lmo-import`,933 行 | 居中弹框,约 980px | 无正式实现 | 后端导入契约未定义,本批不提供可点击成功入口 |
|
||||
| 尾程订单 | 导出字段 | `modal-lmo-field-selector`,935 行 | 居中弹框,约 760px | 无正式实现 | 后端异步导出契约未定义,本批不模拟导出 |
|
||||
| 尾程订单 | 批量转仓 | `modal-lmo-batch-transfer`,921 行 | 居中弹框,约 720px | 无正式实现 | 属库内操作,但写接口/幂等契约未定义;暂不提供写入口 |
|
||||
| 尾程订单 | 入库计划 | `modal-lmo-inbound-plan`,912 行 | 居中弹框,约 720px | 无正式实现 | 属本期入仓链路;待写接口与状态机冻结后按原型实现 |
|
||||
| 尾程订单 | 出库计划 | `modal-lmo-outbound-plan`,927 行 | 居中弹框,约 760px | 无正式实现 | 明确属于库存离仓,排除,不实现 |
|
||||
| 入库单 | 列表/筛选 | `page-fba-inbound-order`,491–525 行;`fbioSearch()`、`fbioReset()` | 独立列表页,查询区、批量预约/排车、横向宽表 | `/prototype/fba-returns/inbound-orders` 占位 | 建立正式列表路由;保留筛选与列契约,接口未接时不填充演示数据 |
|
||||
| 入库单 | 详情 | `page-fba-inbound-order-detail`,684–691 行;`openFbaInboundOrderDetail()` 8567–8575 行 | 独立详情页;单据信息、仓储信息、预约与排车三组字段,应收费用明细位于正文 | 正式参数路由与 `InboundOrderDetailView.vue`;字段和费用空态已落位 | 保持独立页面,不改成抽屉;真实查询与费用数据等待海外区域接口 |
|
||||
| 入库单 | 编辑 | `modal-fbio-edit`,527 行 | 居中弹框,约 880px | 无正式实现 | 待海外区域写接口与状态机冻结后实现 |
|
||||
| 入库单 | 预约 | `modal-fbio-appointment`,526 行 | 居中弹框,约 720px | 无正式实现 | 待预约/排车接口与幂等契约冻结后实现 |
|
||||
| 入库单 | 新增 | 页面按钮存在但原型未定义对应可达表单 | 原型未定义 | 无正式实现 | 标记待确认,不自行选弹框/抽屉/独立页 |
|
||||
| 库存管理 | 列表/筛选 | `page-inventory-overview`,51–89 行;`unifiedInventorySearch()`、`unifiedInventoryReset()` | 独立列表页,六项紧凑汇总、查询区、横向宽表 | 无正式页面 | 建立正式列表路由;汇总与列表在无接口时保持零/空且明确说明未接契约 |
|
||||
| 库存管理 | 详情 | `page-warehouse-stock-detail`,938–983 行;`renderWarehouseStockDetail()` 6624–6646 行 | 独立详情页;商品/库存汇总左右并列,库位库存、托盘箱号、进销存明细连续分区 | 正式参数路由与 `InventoryDetailView.vue`;字段和三类明细空态已落位 | 保持独立页面;进销存仅为只读库存事实,不提供出库或派送操作 |
|
||||
| 库存管理 | 导出 | 列表页“导出Excel”按钮 | 页面工具栏动作,具体浮层未定义 | 无正式实现 | 待 File 导出类型与海外 API 契约补齐,不模拟成功 |
|
||||
|
||||
第一批明确不实现:`overseas-outbound`、`fba-outbound-order*`、`fba-return-outbound`、尾程订单的出库计划及任何海外派送/库存离仓操作。
|
||||
|
||||
尾程列表按 `page-last-mile-orders` 保留“全部 / 待分配”状态页签,并保留入库单号、出库单号、派送单号、换标单号的原型列顺序;其中出库单号和派送单号仅为只读关联事实,不提供链接、详情跳转或操作入口。箱号列表遵循 `renderFbaCarton()`:仅“在库”记录显示编辑入口,“已出库”记录只读。
|
||||
|
||||
第一批桌面布局按 `1920×1080` 验收:列表使用紧凑查询区与横向宽表;详情使用连续白底、双列或三列紧凑字段分区,长表保持横向滚动。当前缺少人工页面截图,视觉结果待总控按真实页面截图确认。
|
||||
|
||||
## 第二批:换标单、托盘、箱号、产品库
|
||||
|
||||
| 页面 | 操作 | 原型证据 | 原型承载方式与尺寸 | 实施结论 |
|
||||
|---|---|---|---|---|
|
||||
| 换标单 | 列表/筛选 | `page-fba-relabel-order`,531–555 行;`renderFbaRelabelOrder()` 8042–8058 行 | 独立列表页;SKU、状态筛选;换标单号、SKU、中文品名、箱数、预算总价、贴标要求、文件、状态 | 建立正式列表;操作列按仓库规则移到第一列;无查询接口时保持空表和契约态 |
|
||||
| 换标单 | 详情 | `page-fba-relabel-order-detail`,696–699 行;`openFbaRelabelOrderDetail()` 8605–8613 行 | 独立详情页;单据信息、商品信息、作业要求、费用明细 | 建立正式参数路由,详情复用 `list` 权限;不改为弹框或抽屉 |
|
||||
| 换标单 | 新增 | `modal-fba-relabel-form`,556–566 行;`openFbaRelabelCreate()` 7994–8001 行 | 居中弹框,最大宽度约 760px;订单信息与作业要求分组 | 按原型建立 `create` 权限入口与弹框;写接口未冻结,保存禁用且不模拟成功 |
|
||||
| 换标单 | 编辑/审核 | 同一 `modal-fba-relabel-form`;`openFbaRelabelEdit()` 8003–8022 行 | 居中弹框,约 760px;显示状态和预算报价,提交文案为“提交审核” | 使用独立 `edit` 权限;仅保留承载和字段契约,端点/状态机未冻结前不提交 |
|
||||
| 托盘管理 | 列表/筛选 | `page-fba-pallet`,626–652 行;`renderFbaPallet()` 8319–8335 行 | 独立列表页;编码、类型、仓库、状态筛选;横向表格 | 建立正式列表;操作列第一列;仓库选项等待海外基础资料接口 |
|
||||
| 托盘管理 | 详情 | `page-fba-pallet-detail`,702–705 行;`openFbaPalletDetail()` 8615–8621 行 | 独立详情页;基础信息、使用信息 | 建立正式参数路由,详情复用 `list` 权限 |
|
||||
| 托盘管理 | 新增/编辑 | `modal-fba-pallet-form`,654 行;`openFbaPalletForm()` 8358–8375 行 | 同一居中弹框,最大宽度约 760px;两列字段 | 分别使用 `create`、`edit` 权限;保存禁用,不发明 URL 或本地写入 |
|
||||
| 箱号管理 | 列表/筛选 | `page-fba-carton`,657–681 行;`renderFbaCarton()` 8428–8448 行 | 独立列表页;箱号、FBA 仓、状态筛选 | 建立正式列表;操作列第一列;“已出库”仅作为只读库存事实,不提供离仓动作 |
|
||||
| 箱号管理 | 详情 | `page-fba-carton-detail`,708–711 行;`openFbaCartonDetail()` 8623–8630 行 | 独立详情页;基础信息、商品信息 | 建立正式参数路由,详情复用 `list` 权限;关联出库单只读展示不链接出库流程 |
|
||||
| 箱号管理 | 新增/编辑 | `overlays.html` 的 `modal-fba-carton-form`,1528–1541 行;`openFbaCartonForm()` 8460–8481 行 | 同一居中弹框,最大宽度约 860px;两列字段;已出库不可编辑 | 分别使用 `create`、`edit` 权限;不实现出库状态变更,保存禁用 |
|
||||
| 产品库 | 列表/筛选 | `page-fba-product`,720–745 行;`renderFbaProduct()` 8695–8711 行 | 独立列表页;SKU、中文品名、品类筛选 | 建立正式列表;操作列第一列 |
|
||||
| 产品库 | 详情 | `page-fba-product-detail`,714–717 行;`openFbaProductDetail()` 8683–8690 行 | 独立详情页;基础信息、包装信息、其他信息 | 建立正式参数路由,详情复用 `list` 权限 |
|
||||
| 产品库 | 新增/编辑 | `modal-fbap-add`,748–771 行;`openFbapModal()`、`openFbapEditModal()` 8753–8767 行 | 同一居中弹框,最大宽度约 560px;单列标签/控件布局 | 分别使用 `create`、`edit` 权限;保存禁用,不在前端生成产品数据 |
|
||||
|
||||
第二批所有新增/编辑弹框按原型尺寸和字段顺序承载;接口未冻结时只用于展示已确认的交互容器,不提供可成功提交路径。导出接口同样未定义,本批不显示可用导出动作。
|
||||
|
||||
## 第三批:库存只读支撑页
|
||||
|
||||
| 页面 | 操作 | 原型证据 | 原型承载方式与布局 | 实施结论 |
|
||||
|---|---|---|---|---|
|
||||
| 进销存记录 | 列表/筛选 | `page-inventory-transactions`,986–1007 行;`renderInventoryTransactions()` 6573–6582 行;`inventoryTransactionSearch()`、`inventoryTransactionReset()` 6584–6599 行 | 从库存管理进入的独立列表页;日期、客户/货主、海外仓、SKU、业务类型及业务/入库/出库单号筛选;约 2050px 横向表格 | 建立无独立菜单的正式路由,复用库存 `list` 权限;出库单号与出库数量仅作为只读库存流水事实,不提供出库详情跳转或离仓操作 |
|
||||
| 进销存记录 | 返回 | `backToInventoryManagement()` 6606–6609 行 | 返回库存管理列表 | 页面工具栏提供明确返回按钮 |
|
||||
| 进销存记录 | 导出 | 页面 989 行“导出Excel”按钮 | 工具栏动作,原型未定义导出任务载体 | File 导出类型与海外接口未冻结,本批不显示可成功导出入口 |
|
||||
|
||||
`overseas-inbound`、`overseas-relabel` 等原型库存视图用于联查入仓/换标库存事实,不新增重复菜单页;当前正式入库单、换标单、库存列表与详情已经分别承载对应入口。`purchase-stock*` 和 `fba-return-stock` 是否作为独立正式页面尚无本轮导航与接口契约,不在本批自行扩展。
|
||||
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
|
||||
withDefaults(defineProps<{
|
||||
title: string
|
||||
width: string
|
||||
domain: string
|
||||
confirmText?: string
|
||||
}>(), {
|
||||
confirmText: '保存',
|
||||
})
|
||||
|
||||
const open = defineModel<boolean>({ default: false })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppDialog v-model="open" :title="title" :width="width" :confirm-text="confirmText" confirm-disabled>
|
||||
<el-alert type="warning" :closable="false" show-icon :title="`${domain}写接口待接入`">
|
||||
<p>弹框按原型保留字段与尺寸;端点、状态机和幂等契约冻结前不可提交,也不会在本地模拟保存成功。</p>
|
||||
</el-alert>
|
||||
<div class="pending-form"><slot /></div>
|
||||
</AppDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pending-form { margin-top: 16px; }
|
||||
.pending-form :deep(.el-select), .pending-form :deep(.el-input-number) { width: 100%; }
|
||||
.pending-form :deep(.el-alert p) { margin: 0; }
|
||||
</style>
|
||||
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{ domain: string }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-alert class="contract-pending" type="warning" :closable="false" show-icon>
|
||||
<template #title>{{ domain }}海外区域接口待接入</template>
|
||||
<p>页面已按四期原型冻结字段与布局;在海外 API、Token 与权限资源补齐前,不展示演示数据,也不会模拟查询或写操作成功。</p>
|
||||
</el-alert>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.contract-pending p { margin: 5px 0 0; color: var(--el-text-color-regular); font-size: 13px; line-height: 1.55; }
|
||||
</style>
|
||||
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowLeft } from '@lucide/vue'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import ContractPendingNotice from './ContractPendingNotice.vue'
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
referenceLabel: string
|
||||
referenceValue: string
|
||||
backLabel: string
|
||||
domain: string
|
||||
}>()
|
||||
|
||||
defineEmits<{ back: [] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminPage as="div" class="overseas-detail-page">
|
||||
<header class="overseas-detail-head">
|
||||
<el-button :aria-label="backLabel" @click="$emit('back')"><ArrowLeft :size="16" />返回</el-button>
|
||||
<div class="overseas-detail-title">
|
||||
<h1>{{ title }}</h1>
|
||||
<p>{{ referenceLabel }} <strong>{{ referenceValue }}</strong></p>
|
||||
</div>
|
||||
<span class="overseas-detail-status" role="status">接口待接入</span>
|
||||
</header>
|
||||
|
||||
<ContractPendingNotice :domain="domain" />
|
||||
<slot />
|
||||
</AdminPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.overseas-detail-page { min-width: 0; }
|
||||
.overseas-detail-head { display: flex; min-height: 44px; align-items: center; gap: 12px; }
|
||||
.overseas-detail-title { min-width: 0; flex: 1; }
|
||||
.overseas-detail-title h1 { margin: 0; color: var(--cc-text); font-size: 20px; line-height: 1.35; }
|
||||
.overseas-detail-title p { margin: 3px 0 0; color: var(--cc-muted); font-size: 12px; line-height: 1.5; }
|
||||
.overseas-detail-title strong { color: var(--cc-text); overflow-wrap: anywhere; }
|
||||
.overseas-detail-status { display: inline-flex; min-height: 26px; align-items: center; padding: 3px 10px; border: 1px solid #f3d19e; border-radius: 999px; background: #fdf6ec; color: #b88230; font-size: 12px; font-weight: 600; white-space: nowrap; }
|
||||
</style>
|
||||
@@ -0,0 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
import OverseasDetailShell from './OverseasDetailShell.vue'
|
||||
|
||||
interface DetailGroup {
|
||||
title: string
|
||||
fields: string[]
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
referenceLabel: string
|
||||
referenceValue: string
|
||||
backLabel: string
|
||||
domain: string
|
||||
groups: DetailGroup[]
|
||||
}>()
|
||||
|
||||
defineEmits<{ back: [] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OverseasDetailShell
|
||||
:title="title"
|
||||
:reference-label="referenceLabel"
|
||||
:reference-value="referenceValue"
|
||||
:back-label="backLabel"
|
||||
:domain="domain"
|
||||
@back="$emit('back')"
|
||||
>
|
||||
<section class="read-only-detail" :aria-label="title">
|
||||
<section v-for="group in groups" :key="group.title" class="detail-group" :aria-label="group.title">
|
||||
<h2>{{ group.title }}</h2>
|
||||
<dl class="detail-fields"><div v-for="field in group.fields" :key="field"><dt>{{ field }}</dt><dd>—</dd></div></dl>
|
||||
</section>
|
||||
</section>
|
||||
</OverseasDetailShell>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.read-only-detail { display: grid; min-width: 0; grid-template-columns: repeat(2, minmax(0, 1fr)); border: 1px solid var(--cc-border); background: #fff; }
|
||||
.detail-group { min-width: 0; padding: 16px 18px 18px; border-right: 1px solid var(--cc-border); border-bottom: 1px solid var(--cc-border); }
|
||||
.detail-group:nth-child(even) { border-right: 0; }
|
||||
.detail-group h2 { margin: 0 0 12px; color: var(--cc-text); font-size: 15px; }
|
||||
.detail-fields { margin: 0; border-top: 1px solid var(--cc-border); border-left: 1px solid var(--cc-border); }
|
||||
.detail-fields > div { display: grid; min-width: 0; min-height: 46px; grid-template-columns: 126px minmax(0, 1fr); border-right: 1px solid var(--cc-border); border-bottom: 1px solid var(--cc-border); }
|
||||
.detail-fields dt, .detail-fields dd { display: flex; min-width: 0; align-items: center; padding: 8px 10px; }
|
||||
.detail-fields dt { background: #f8fafc; color: var(--cc-muted); font-size: 12px; }
|
||||
.detail-fields dd { margin: 0; color: var(--cc-text); overflow-wrap: anywhere; }
|
||||
</style>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import OverseasReadOnlyDetail from '../components/OverseasReadOnlyDetail.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const cartonId = computed(() => String(route.params.cartonId || '—'))
|
||||
const groups = [
|
||||
{ title: '基础信息', fields: ['箱号', '所属FBA仓', '状态', '关联托盘', '关联入库单', '关联出库单'] },
|
||||
{ title: '商品信息', fields: ['商品SKU', '中文品名', '数量', '重量(kg)', '创建时间', '更新时间', '备注'] },
|
||||
]
|
||||
function back() { void router.push({ name: 'overseas-cartons' }) }
|
||||
</script>
|
||||
|
||||
<template><OverseasReadOnlyDetail title="箱号详情" reference-label="箱号" :reference-value="cartonId" back-label="返回箱号管理" domain="箱号详情" :groups="groups" @back="back" /></template>
|
||||
@@ -0,0 +1,69 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { Eye, Pencil, Plus } from '@lucide/vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import ContractPendingNotice from '../components/ContractPendingNotice.vue'
|
||||
import ContractPendingFormDialog from '../components/ContractPendingFormDialog.vue'
|
||||
import { overseasWarehousePermissions as permission } from '../permissions'
|
||||
import type { CartonListItem } from '../types'
|
||||
|
||||
const access = useAccessPreviewStore()
|
||||
const router = useRouter()
|
||||
const rows = ref<CartonListItem[]>([])
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const filters = ref<QueryModel>({ cartonNo: '', fbaWarehouse: '', status: '' })
|
||||
const fields: QueryField[] = [
|
||||
{ key: 'cartonNo', type: 'input', label: '箱号', placeholder: '请输入箱号', width: 190 },
|
||||
{ key: 'fbaWarehouse', type: 'select', label: '所属FBA仓', placeholder: '全部FBA仓', width: 180, options: [] },
|
||||
{ key: 'status', type: 'select', label: '状态', placeholder: '全部状态', width: 150, options: ['在库', '已出库'].map((value) => ({ label: value, value })) },
|
||||
]
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 180, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'cartonNo', label: '箱号', minWidth: 180 }, { key: 'fbaWarehouse', label: '所属FBA仓', minWidth: 150 },
|
||||
{ key: 'sku', label: '商品SKU', minWidth: 150 }, { key: 'productName', label: '中文品名', minWidth: 180 },
|
||||
{ key: 'quantity', label: '数量', width: 100, align: 'right' }, { key: 'weight', label: '重量(kg)', width: 110, align: 'right' },
|
||||
{ key: 'status', label: '状态', width: 100 },
|
||||
]
|
||||
const dialogOpen = ref(false)
|
||||
const dialogMode = ref<'create' | 'edit'>('create')
|
||||
const form = reactive({ cartonNo: '', fbaWarehouse: '', sku: '', productName: '', quantity: undefined as number | undefined, weight: undefined as number | undefined, palletNo: '', inboundOrderNo: '', outboundOrderNo: '', status: '在库', remark: '' })
|
||||
|
||||
function optionalNumber(value: unknown) { const number = Number(value); return value === '' || value == null || !Number.isFinite(number) ? undefined : number }
|
||||
function openForm(mode: 'create' | 'edit', source?: object) { const row = source as CartonListItem | undefined; dialogMode.value = mode; Object.assign(form, { cartonNo: '', fbaWarehouse: '', sku: '', productName: '', quantity: undefined, weight: undefined, palletNo: '', inboundOrderNo: '', outboundOrderNo: '', status: '在库', remark: '' }); if (mode === 'edit' && row) Object.assign(form, { cartonNo: String(row.cartonNo || ''), fbaWarehouse: String(row.fbaWarehouse || ''), sku: String(row.sku || ''), productName: String(row.productName || ''), quantity: optionalNumber(row.quantity), weight: optionalNumber(row.weight), palletNo: String(row.palletNo || ''), inboundOrderNo: String(row.inboundOrderNo || ''), outboundOrderNo: String(row.outboundOrderNo || ''), status: String(row.status || '在库'), remark: String(row.remark || '') }); dialogOpen.value = true }
|
||||
function canEditCarton(source: object) { return String((source as CartonListItem).status) === '在库' }
|
||||
function openDetail(source: object) { const row = source as CartonListItem; void router.push({ name: 'overseas-carton-detail', params: { cartonId: String(row.cartonNo) } }) }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminPage><ListPageLayout aria-label="箱号管理列表">
|
||||
<template #toolbar><QueryToolbar v-model="filters" class="surface-panel" :fields="fields" ariaLabel="箱号筛选" preference-key="overseas-cartons-query" @search="page = 1" /></template>
|
||||
<template #state><ContractPendingNotice domain="箱号管理" /></template>
|
||||
<section class="surface-panel table-panel"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :total="0" preference-key="overseas-cartons" row-key="cartonNo" empty-text="海外区域箱号查询契约待接入">
|
||||
<template #toolbar-actions><el-button v-if="access.can(permission.cartonCreate)" type="primary" @click="openForm('create')"><Plus :size="16" />新增箱号</el-button></template>
|
||||
<template #actions="{ row }"><el-button size="small" plain type="primary" @click="openDetail(row)"><Eye :size="15" />查看</el-button><el-button v-if="access.can(permission.cartonEdit) && canEditCarton(row)" size="small" plain @click="openForm('edit', row)"><Pencil :size="15" />编辑</el-button></template>
|
||||
<template #pagination-left>共 0 条箱号记录</template>
|
||||
</DataTable></section>
|
||||
</ListPageLayout>
|
||||
<ContractPendingFormDialog v-model="dialogOpen" :title="dialogMode === 'create' ? '新增箱号' : '编辑箱号'" width="min(860px, calc(100vw - 28px))" domain="箱号">
|
||||
<el-form label-position="top"><el-row :gutter="16">
|
||||
<el-col :span="12"><el-form-item label="箱号" required><el-input v-model="form.cartonNo" :readonly="dialogMode === 'edit'" placeholder="请输入箱号" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="所属 FBA 仓" required><el-select v-model="form.fbaWarehouse" placeholder="FBA仓候选接口待接入" disabled /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="商品 SKU" required><el-input v-model="form.sku" placeholder="请输入商品 SKU" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="中文品名" required><el-input v-model="form.productName" placeholder="请输入中文品名" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="数量" required><el-input-number v-model="form.quantity" :min="1" :precision="0" controls-position="right" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="重量(kg)" required><el-input-number v-model="form.weight" :min="0.01" :precision="2" controls-position="right" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="关联托盘"><el-input v-model="form.palletNo" placeholder="请输入关联托盘号" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="关联入库单"><el-input v-model="form.inboundOrderNo" placeholder="请输入关联入库单号" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="关联出库单"><el-input v-model="form.outboundOrderNo" readonly /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="状态"><el-input v-model="form.status" readonly /></el-form-item></el-col>
|
||||
<el-col :span="24"><el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="3" maxlength="500" show-word-limit /></el-form-item></el-col>
|
||||
</el-row></el-form>
|
||||
</ContractPendingFormDialog>
|
||||
</AdminPage>
|
||||
</template>
|
||||
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import OverseasDetailShell from '../components/OverseasDetailShell.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const orderId = computed(() => String(route.params.orderId || '—'))
|
||||
|
||||
const groups = [
|
||||
{ title: '单据信息', fields: ['入库单号', '入仓码', '类型', '货主', '关联订单'] },
|
||||
{ title: '仓储信息', fields: ['海外仓', 'FBA仓库', '总箱数', '计费体积(m³)', '总托盘数'] },
|
||||
{ title: '预约与排车', fields: ['状态', '预约状态', '预约送仓时间', '预计装车时间', '排车状态', '创建时间', '更新时间', '备注'] },
|
||||
]
|
||||
const feeColumns: DataTableColumn[] = [
|
||||
{ key: 'index', label: '序号', width: 70, align: 'center' },
|
||||
{ key: 'name', label: '费用项目', minWidth: 180 },
|
||||
{ key: 'currency', label: '币种', width: 100 },
|
||||
{ key: 'amount', label: '金额', width: 140, align: 'right' },
|
||||
{ key: 'remark', label: '备注', minWidth: 200 },
|
||||
]
|
||||
|
||||
function back() {
|
||||
void router.push({ name: 'overseas-inbound-orders' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OverseasDetailShell title="入库单详情" reference-label="入库单号" :reference-value="orderId" back-label="返回入库单" domain="入库单详情" @back="back">
|
||||
<section class="detail-section" aria-label="入库单基本信息">
|
||||
<div class="detail-groups">
|
||||
<section v-for="group in groups" :key="group.title" class="detail-group" :aria-label="group.title">
|
||||
<h2>{{ group.title }}</h2>
|
||||
<dl class="detail-fields"><div v-for="label in group.fields" :key="label"><dt>{{ label }}</dt><dd>—</dd></div></dl>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<section class="detail-section fee-section" aria-labelledby="inbound-fees-heading">
|
||||
<h2 id="inbound-fees-heading">应收费用明细</h2>
|
||||
<DataTable :data="[]" :columns="feeColumns" column-settings-mode="hide" empty-text="海外区域应收费用契约待接入" />
|
||||
<footer class="fee-total"><span>应收费用合计(USD)</span><strong>—</strong></footer>
|
||||
</section>
|
||||
</OverseasDetailShell>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.detail-section { min-width: 0; border: 1px solid var(--cc-border); background: #fff; }
|
||||
.detail-groups { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.detail-group { min-width: 0; padding: 16px 18px 18px; border-right: 1px solid var(--cc-border); }
|
||||
.detail-group:last-child { border-right: 0; }
|
||||
.detail-group h2, .fee-section h2 { margin: 0 0 12px; color: var(--cc-text); font-size: 15px; }
|
||||
.detail-fields { margin: 0; border-top: 1px solid var(--cc-border); border-left: 1px solid var(--cc-border); }
|
||||
.detail-fields > div { display: grid; min-width: 0; min-height: 46px; grid-template-columns: 120px minmax(0, 1fr); border-right: 1px solid var(--cc-border); border-bottom: 1px solid var(--cc-border); }
|
||||
.detail-fields dt, .detail-fields dd { display: flex; min-width: 0; align-items: center; padding: 8px 10px; }
|
||||
.detail-fields dt { background: #f8fafc; color: var(--cc-muted); font-size: 12px; }
|
||||
.detail-fields dd { margin: 0; color: var(--cc-text); overflow-wrap: anywhere; }
|
||||
.fee-section { overflow: hidden; padding-top: 16px; }
|
||||
.fee-section h2 { margin-right: 18px; margin-left: 18px; }
|
||||
.fee-total { display: flex; min-height: 48px; align-items: center; justify-content: flex-end; gap: 40px; padding: 10px 18px; border-top: 1px solid var(--cc-border); color: var(--cc-text); }
|
||||
.fee-total strong { min-width: 120px; font-size: 16px; }
|
||||
</style>
|
||||
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { Eye } from '@lucide/vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import ContractPendingNotice from '../components/ContractPendingNotice.vue'
|
||||
import type { InboundOrderListItem } from '../types'
|
||||
|
||||
const rows = ref<InboundOrderListItem[]>([])
|
||||
const router = useRouter()
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const filters = ref<QueryModel>({ orderNo: '', inboundCode: '', status: '', warehouse: '', fbaWarehouse: '', appointmentStatus: '', dispatchStatus: '', inboundDate: [] })
|
||||
const fields: QueryField[] = [
|
||||
{ key: 'orderNo', type: 'input', label: '入库单号', placeholder: '请输入入库单号' },
|
||||
{ key: 'inboundCode', type: 'input', label: '入仓码', placeholder: '请输入入仓码' },
|
||||
{ key: 'status', type: 'select', label: '状态', placeholder: '全部状态', options: ['待审核', '已审核', '已入库'].map((value) => ({ label: value, value })) },
|
||||
{ key: 'warehouse', type: 'select', label: '海外仓', placeholder: '全部海外仓', options: [] },
|
||||
{ key: 'fbaWarehouse', type: 'select', label: 'FBA仓库', placeholder: '全部FBA仓库', options: [] },
|
||||
{ key: 'appointmentStatus', type: 'select', label: '预约状态', placeholder: '全部预约状态', options: ['未预约', '预约中', '预约成功'].map((value) => ({ label: value, value })) },
|
||||
{ key: 'dispatchStatus', type: 'select', label: '排车状态', placeholder: '全部排车状态', options: ['未排车', '已排车'].map((value) => ({ label: value, value })) },
|
||||
{ key: 'inboundDate', type: 'daterange', label: '入库时间' },
|
||||
]
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 120, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'orderNo', prop: 'orderNo', label: '入库单号', minWidth: 170 },
|
||||
{ key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 140 },
|
||||
{ key: 'type', prop: 'type', label: '类型', width: 100 },
|
||||
{ key: 'warehouse', prop: 'warehouse', label: '海外仓', minWidth: 170 },
|
||||
{ key: 'fbaWarehouse', prop: 'fbaWarehouse', label: 'FBA仓库', minWidth: 130 },
|
||||
{ key: 'owner', prop: 'owner', label: '货主', minWidth: 150 },
|
||||
{ key: 'cartons', prop: 'cartons', label: '总箱数', width: 90, align: 'right' },
|
||||
{ key: 'volume', prop: 'volume', label: '计费体积(m³)', width: 125, align: 'right' },
|
||||
{ key: 'pallets', prop: 'pallets', label: '总托盘数', width: 100, align: 'right' },
|
||||
{ key: 'status', prop: 'status', label: '状态', width: 100 },
|
||||
{ key: 'appointmentStatus', prop: 'appointmentStatus', label: 'FBA预约', width: 110 },
|
||||
{ key: 'dispatchStatus', prop: 'dispatchStatus', label: '排车状态', width: 110 },
|
||||
]
|
||||
|
||||
function openDetail(source: object) {
|
||||
const row = source as InboundOrderListItem
|
||||
void router.push({ name: 'overseas-inbound-order-detail', params: { orderId: row.orderNo } })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminPage><ListPageLayout aria-label="海外仓入库单列表">
|
||||
<template #toolbar><QueryToolbar v-model="filters" class="surface-panel" :fields="fields" ariaLabel="入库单筛选" preference-key="overseas-inbound-orders-query" @search="page = 1" /></template>
|
||||
<template #state><ContractPendingNotice domain="入库单" /></template>
|
||||
<section class="surface-panel table-panel"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :total="0" preference-key="overseas-inbound-orders" row-key="orderNo" empty-text="海外区域入库单查询契约待接入">
|
||||
<template #actions="{ row }"><el-button size="small" plain type="primary" @click="openDetail(row)"><Eye :size="15" />查看</el-button></template>
|
||||
<template #pagination-left>共 0 条入库单</template>
|
||||
</DataTable></section>
|
||||
</ListPageLayout></AdminPage>
|
||||
</template>
|
||||
@@ -0,0 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import OverseasDetailShell from '../components/OverseasDetailShell.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const inventoryId = computed(() => String(route.params.inventoryId || '—'))
|
||||
|
||||
const productFields = ['客户', '海外仓', 'SKU编码', '中文品名', '英文品名', '更新时间']
|
||||
const stockFields = ['在库总量', '可用库存', '暂扣库存', '美本土散版', '在途库存', '采购库存', '预计总量']
|
||||
const locationColumns: DataTableColumn[] = [
|
||||
{ key: 'location', label: '库位', minWidth: 130 },
|
||||
{ key: 'status', label: '库存状态', width: 120 },
|
||||
{ key: 'palletNo', label: '托盘号', minWidth: 150 },
|
||||
{ key: 'cartons', label: '箱数', width: 90, align: 'right' },
|
||||
{ key: 'quantity', label: '库存数量', width: 110, align: 'right' },
|
||||
{ key: 'available', label: '可用数量', width: 110, align: 'right' },
|
||||
{ key: 'inboundDate', label: '入库日期', width: 130 },
|
||||
{ key: 'ageDays', label: '库龄(天)', width: 100, align: 'right' },
|
||||
]
|
||||
const palletColumns: DataTableColumn[] = [
|
||||
{ key: 'palletNo', label: '托盘号', minWidth: 150 },
|
||||
{ key: 'cartons', label: '关联箱数', width: 110, align: 'right' },
|
||||
{ key: 'cartonRange', label: '箱号范围', minWidth: 200 },
|
||||
{ key: 'location', label: '库位', minWidth: 130 },
|
||||
{ key: 'quantity', label: '库存数量', width: 110, align: 'right' },
|
||||
]
|
||||
const ledgerColumns: DataTableColumn[] = [
|
||||
{ key: 'occurredAt', label: '发生时间', minWidth: 160 },
|
||||
{ key: 'businessType', label: '业务类型', width: 120 },
|
||||
{ key: 'businessOrderNo', label: '业务订单号', minWidth: 170 },
|
||||
{ key: 'inboundOrderNo', label: '入库单号', minWidth: 160 },
|
||||
{ key: 'outboundOrderNo', label: '出库单号', minWidth: 160 },
|
||||
{ key: 'location', label: '库位', minWidth: 120 },
|
||||
{ key: 'inboundQuantity', label: '入库数量', width: 110, align: 'right' },
|
||||
{ key: 'outboundQuantity', label: '出库数量', width: 110, align: 'right' },
|
||||
{ key: 'balance', label: '结存', width: 100, align: 'right' },
|
||||
{ key: 'operator', label: '操作人', minWidth: 120 },
|
||||
{ key: 'remark', label: '备注', minWidth: 180 },
|
||||
]
|
||||
|
||||
function back() {
|
||||
void router.push({ name: 'overseas-inventory' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OverseasDetailShell title="库存详情" reference-label="库存记录" :reference-value="inventoryId" back-label="返回库存管理" domain="库存详情" @back="back">
|
||||
<section class="overview-panel" aria-label="库存概览">
|
||||
<section aria-labelledby="inventory-product-heading">
|
||||
<h2 id="inventory-product-heading">商品信息</h2>
|
||||
<dl class="detail-fields"><div v-for="label in productFields" :key="label"><dt>{{ label }}</dt><dd>—</dd></div></dl>
|
||||
</section>
|
||||
<section aria-labelledby="inventory-summary-heading">
|
||||
<h2 id="inventory-summary-heading">库存状态汇总</h2>
|
||||
<dl class="detail-fields"><div v-for="label in stockFields" :key="label"><dt>{{ label }}</dt><dd>—</dd></div></dl>
|
||||
</section>
|
||||
</section>
|
||||
<section class="ledger-section" aria-labelledby="inventory-location-heading">
|
||||
<h2 id="inventory-location-heading">库位库存</h2>
|
||||
<DataTable :data="[]" :columns="locationColumns" column-settings-mode="hide" empty-text="海外区域库位库存契约待接入" />
|
||||
</section>
|
||||
<section class="ledger-section" aria-labelledby="inventory-pallet-heading">
|
||||
<h2 id="inventory-pallet-heading">托盘箱号信息</h2>
|
||||
<DataTable :data="[]" :columns="palletColumns" column-settings-mode="hide" empty-text="海外区域托盘箱号契约待接入" />
|
||||
</section>
|
||||
<section class="ledger-section" aria-labelledby="inventory-ledger-heading">
|
||||
<h2 id="inventory-ledger-heading">进销存单据明细</h2>
|
||||
<DataTable :data="[]" :columns="ledgerColumns" column-settings-mode="hide" empty-text="海外区域进销存契约待接入" />
|
||||
</section>
|
||||
</OverseasDetailShell>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.overview-panel { display: grid; min-width: 0; grid-template-columns: repeat(2, minmax(0, 1fr)); border: 1px solid var(--cc-border); background: #fff; }
|
||||
.overview-panel > section { min-width: 0; padding: 16px 18px 18px; border-right: 1px solid var(--cc-border); }
|
||||
.overview-panel > section:last-child { border-right: 0; }
|
||||
.overview-panel h2, .ledger-section h2 { margin: 0 0 12px; color: var(--cc-text); font-size: 15px; }
|
||||
.detail-fields { margin: 0; border-top: 1px solid var(--cc-border); border-left: 1px solid var(--cc-border); }
|
||||
.detail-fields > div { display: grid; min-width: 0; min-height: 46px; grid-template-columns: 120px minmax(0, 1fr); border-right: 1px solid var(--cc-border); border-bottom: 1px solid var(--cc-border); }
|
||||
.detail-fields dt, .detail-fields dd { display: flex; min-width: 0; align-items: center; padding: 8px 10px; }
|
||||
.detail-fields dt { background: #f8fafc; color: var(--cc-muted); font-size: 12px; }
|
||||
.detail-fields dd { margin: 0; color: var(--cc-text); }
|
||||
.ledger-section { min-width: 0; overflow: hidden; padding-top: 16px; border: 1px solid var(--cc-border); background: #fff; }
|
||||
.ledger-section h2 { margin-right: 18px; margin-left: 18px; }
|
||||
</style>
|
||||
@@ -0,0 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ArrowLeft } from '@lucide/vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import ContractPendingNotice from '../components/ContractPendingNotice.vue'
|
||||
import type { InventoryTransactionListItem } from '../types'
|
||||
|
||||
const router = useRouter()
|
||||
const rows = ref<InventoryTransactionListItem[]>([])
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const filters = ref<QueryModel>({ occurredDate: [], owner: '', warehouse: '', sku: '', businessType: '', businessOrderNo: '', inboundOrderNo: '', outboundOrderNo: '' })
|
||||
const fields: QueryField[] = [
|
||||
{ key: 'occurredDate', type: 'daterange', label: '发生日期', width: 260 },
|
||||
{ key: 'owner', type: 'input', label: '客户/货主', placeholder: '请输入客户或货主', width: 190 },
|
||||
{ key: 'warehouse', type: 'select', label: '海外仓', placeholder: '全部海外仓', width: 190, options: [] },
|
||||
{ key: 'sku', type: 'input', label: 'SKU编码', placeholder: '请输入SKU编码', width: 170 },
|
||||
{ key: 'businessType', type: 'select', label: '业务类型', placeholder: '全部业务类型', width: 170, options: ['采购库存', '头程入库', '尾程入库', '退货入库', '暂扣库存', '一件代发', '头程出库', '派送出库', '调拨转仓', '库存调整'].map((value) => ({ label: value, value })) },
|
||||
{ key: 'businessOrderNo', type: 'input', label: '业务订单号', placeholder: '请输入业务订单号', width: 180 },
|
||||
{ key: 'inboundOrderNo', type: 'input', label: '入库单号', placeholder: '请输入入库单号', width: 180 },
|
||||
{ key: 'outboundOrderNo', type: 'input', label: '出库单号', placeholder: '请输入出库单号', width: 180 },
|
||||
]
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'occurredAt', label: '发生时间', minWidth: 160 },
|
||||
{ key: 'owner', label: '客户', minWidth: 160 },
|
||||
{ key: 'warehouse', label: '海外仓', minWidth: 180 },
|
||||
{ key: 'sku', label: 'SKU编码', minWidth: 150 },
|
||||
{ key: 'productName', label: '中文品名', minWidth: 180 },
|
||||
{ key: 'businessType', label: '业务类型', width: 120 },
|
||||
{ key: 'businessOrderNo', label: '业务订单号', minWidth: 170 },
|
||||
{ key: 'inboundOrderNo', label: '入库单号', minWidth: 160 },
|
||||
{ key: 'outboundOrderNo', label: '出库单号', minWidth: 160 },
|
||||
{ key: 'inboundQuantity', label: '入库数量', width: 110, align: 'right' },
|
||||
{ key: 'outboundQuantity', label: '出库数量', width: 110, align: 'right' },
|
||||
{ key: 'balance', label: '结存', width: 100, align: 'right' },
|
||||
{ key: 'operator', label: '操作人', minWidth: 120 },
|
||||
{ key: 'remark', label: '备注', minWidth: 180 },
|
||||
]
|
||||
|
||||
function back() { void router.push({ name: 'overseas-inventory' }) }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminPage><ListPageLayout aria-label="进销存记录列表">
|
||||
<template #toolbar><QueryToolbar v-model="filters" class="surface-panel" :fields="fields" ariaLabel="进销存记录筛选" preference-key="overseas-inventory-transactions-query" @search="page = 1" /></template>
|
||||
<template #state><ContractPendingNotice domain="进销存记录" /><p class="read-only-boundary">出库相关字段仅展示既有库存流水事实;本页面不提供出库单跳转、库存扣减或其他离仓操作。</p></template>
|
||||
<section class="surface-panel table-panel"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :total="0" preference-key="overseas-inventory-transactions" row-key="id" empty-text="海外区域进销存查询契约待接入">
|
||||
<template #toolbar-actions><el-button @click="back"><ArrowLeft :size="16" />返回库存管理</el-button></template>
|
||||
<template #pagination-left>共 0 条进销存记录</template>
|
||||
</DataTable></section>
|
||||
</ListPageLayout></AdminPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.read-only-boundary { margin: 8px 2px 0; color: var(--cc-muted); font-size: 12px; line-height: 1.5; }
|
||||
</style>
|
||||
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { BookOpen, Eye } from '@lucide/vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import ContractPendingNotice from '../components/ContractPendingNotice.vue'
|
||||
import type { InventoryListItem } from '../types'
|
||||
|
||||
const rows = ref<InventoryListItem[]>([])
|
||||
const router = useRouter()
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const filters = ref<QueryModel>({ warehouse: '', owner: '', sku: '', productName: '', batchNo: '', location: '', status: '' })
|
||||
const fields: QueryField[] = [
|
||||
{ key: 'warehouse', type: 'select', label: '海外仓', placeholder: '全部海外仓', options: [] },
|
||||
{ key: 'owner', type: 'input', label: '客户/货主', placeholder: '请输入客户或货主' },
|
||||
{ key: 'sku', type: 'input', label: 'SKU编码', placeholder: '请输入SKU编码' },
|
||||
{ key: 'productName', type: 'input', label: '中文品名', placeholder: '请输入中文品名' },
|
||||
{ key: 'batchNo', type: 'input', label: '批次号', placeholder: '请输入批次号' },
|
||||
{ key: 'location', type: 'input', label: '库位', placeholder: '请输入库位' },
|
||||
{ key: 'status', type: 'select', label: '库存状态', placeholder: '全部库存状态', options: ['正常', '库存不足', '无可用库存', '存在暂扣', '存在美本土散版'].map((value) => ({ label: value, value })) },
|
||||
]
|
||||
const metrics = [
|
||||
['在库总量', 0, 'primary'], ['可用库存', 0, 'success'], ['暂扣库存', 0, 'danger'],
|
||||
['美本土散版', 0, 'neutral'], ['在途库存', 0, 'warning'], ['采购库存', 0, 'neutral'],
|
||||
] as const
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 110, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'owner', prop: 'owner', label: '客户', minWidth: 160 },
|
||||
{ key: 'warehouse', prop: 'warehouse', label: '海外仓', minWidth: 170 },
|
||||
{ key: 'sku', prop: 'sku', label: 'SKU编码', minWidth: 150 },
|
||||
{ key: 'productName', prop: 'productName', label: '中文品名', minWidth: 180 },
|
||||
{ key: 'onHand', prop: 'onHand', label: '在库总量', width: 105, align: 'right' },
|
||||
{ key: 'available', prop: 'available', label: '可用库存', width: 105, align: 'right' },
|
||||
{ key: 'held', prop: 'held', label: '暂扣库存', width: 105, align: 'right' },
|
||||
{ key: 'looseboard', prop: 'looseboard', label: '美本土散版', width: 115, align: 'right' },
|
||||
{ key: 'inTransit', prop: 'inTransit', label: '在途库存', width: 105, align: 'right' },
|
||||
{ key: 'purchase', prop: 'purchase', label: '采购库存', width: 105, align: 'right' },
|
||||
{ key: 'status', prop: 'status', label: '库存状态', width: 110 },
|
||||
{ key: 'updatedAt', prop: 'updatedAt', label: '更新时间', minWidth: 150 },
|
||||
]
|
||||
|
||||
function openDetail(source: object) {
|
||||
const row = source as InventoryListItem
|
||||
void router.push({ name: 'overseas-inventory-detail', params: { inventoryId: row.id } })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminPage><ListPageLayout aria-label="海外仓库存管理">
|
||||
<template #toolbar><QueryToolbar v-model="filters" class="surface-panel" :fields="fields" ariaLabel="库存筛选" preference-key="overseas-inventory-query" @search="page = 1" /></template>
|
||||
<template #metrics><section class="inventory-metrics" aria-label="库存汇总"><div v-for="metric in metrics" :key="metric[0]" class="inventory-metric" :class="`is-${metric[2]}`"><span>{{ metric[0] }}</span><strong>{{ metric[1] }}</strong></div></section></template>
|
||||
<template #state><ContractPendingNotice domain="库存管理" /></template>
|
||||
<section class="surface-panel table-panel"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :total="0" preference-key="overseas-inventory" row-key="id" empty-text="海外区域库存查询契约待接入">
|
||||
<template #toolbar-actions><el-button @click="router.push({ name: 'overseas-inventory-transactions' })"><BookOpen :size="16" />进销存记录</el-button></template>
|
||||
<template #actions="{ row }"><el-button size="small" plain type="primary" @click="openDetail(row)"><Eye :size="15" />查看</el-button></template>
|
||||
<template #pagination-left>共 0 条库存记录</template>
|
||||
</DataTable></section>
|
||||
</ListPageLayout></AdminPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.inventory-metrics { display: grid; grid-template-columns: repeat(6, minmax(130px, 1fr)); overflow: hidden; border: 1px solid var(--cc-border); border-radius: 8px; background: #fff; }
|
||||
.inventory-metric { display: flex; min-width: 0; flex-direction: column; gap: 5px; padding: 13px 16px; border-right: 1px solid var(--cc-border); }
|
||||
.inventory-metric:last-child { border-right: 0; }
|
||||
.inventory-metric span { color: #64748b; font-size: 12px; }
|
||||
.inventory-metric strong { color: #334155; font-size: 22px; line-height: 1; }
|
||||
.inventory-metric.is-primary strong { color: var(--cc-primary); }
|
||||
.inventory-metric.is-success strong { color: #16a34a; }
|
||||
.inventory-metric.is-danger strong { color: #dc2626; }
|
||||
.inventory-metric.is-warning strong { color: #d97706; }
|
||||
</style>
|
||||
@@ -0,0 +1,84 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import OverseasDetailShell from '../components/OverseasDetailShell.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const orderId = computed(() => String(route.params.orderId || '—'))
|
||||
|
||||
const orderFields = ['订单类型', '拒收时间', '实际入库时间', '头程单号', '货主', '海外仓', '转仓目的仓', 'FBA仓库', '集装箱号', '货柜异常', '异常原因', '运输方式', '预计到仓', '清关状态']
|
||||
const detailFields = ['订单类型', '入仓码', '目的仓', '托数', '箱数', '数量', '体积重量', '预计到港时间', '实际到港时间', '预计到仓', '是否暂扣', '备注(仅自己)', '备注(客户)', '备注(仓库)']
|
||||
const planFields = ['入仓码', '计划入库仓', '合托批次', '合托后托数', 'FBA预约号', '预约状态', '换标状态']
|
||||
const documentColumns: DataTableColumn[] = [
|
||||
{ key: 'type', label: '单据类型', minWidth: 120 },
|
||||
{ key: 'number', label: '单据编号', minWidth: 180 },
|
||||
{ key: 'createdAt', label: '生成时间', minWidth: 160 },
|
||||
{ key: 'quote', label: '报价', minWidth: 140 },
|
||||
]
|
||||
const cargoColumns: DataTableColumn[] = [
|
||||
{ key: 'sku', label: 'SKU', minWidth: 150 },
|
||||
{ key: 'productName', label: '中文品名', minWidth: 180 },
|
||||
{ key: 'pallets', label: '托数', width: 90, align: 'right' },
|
||||
{ key: 'cartons', label: '箱数', width: 90, align: 'right' },
|
||||
{ key: 'quantity', label: '件数', width: 90, align: 'right' },
|
||||
{ key: 'weight', label: '重量(kg)', width: 110, align: 'right' },
|
||||
{ key: 'volume', label: '体积(m³)', width: 110, align: 'right' },
|
||||
]
|
||||
const logColumns: DataTableColumn[] = [
|
||||
{ key: 'occurredAt', label: '时间', minWidth: 160 },
|
||||
{ key: 'node', label: '操作节点', minWidth: 140 },
|
||||
{ key: 'operator', label: '操作人', minWidth: 130 },
|
||||
{ key: 'result', label: '处理结果', minWidth: 220 },
|
||||
]
|
||||
|
||||
function back() {
|
||||
void router.push({ name: 'overseas-last-mile-orders' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OverseasDetailShell title="尾程订单详情" reference-label="尾程单号" :reference-value="orderId" back-label="返回尾程订单" domain="尾程订单详情" @back="back">
|
||||
<div class="detail-grid">
|
||||
<section class="detail-section" aria-labelledby="last-mile-order-heading">
|
||||
<h2 id="last-mile-order-heading">订单信息</h2>
|
||||
<dl class="detail-fields"><div v-for="label in orderFields" :key="label"><dt>{{ label }}</dt><dd>—</dd></div></dl>
|
||||
</section>
|
||||
<section class="detail-section" aria-labelledby="last-mile-lines-heading">
|
||||
<h2 id="last-mile-lines-heading">订单明细</h2>
|
||||
<dl class="detail-fields"><div v-for="label in detailFields" :key="label"><dt>{{ label }}</dt><dd>—</dd></div></dl>
|
||||
</section>
|
||||
<section class="detail-section" aria-labelledby="last-mile-plan-heading">
|
||||
<h2 id="last-mile-plan-heading">计划信息</h2>
|
||||
<dl class="detail-fields"><div v-for="label in planFields" :key="label"><dt>{{ label }}</dt><dd>—</dd></div></dl>
|
||||
</section>
|
||||
<section class="detail-section table-section" aria-labelledby="last-mile-documents-heading">
|
||||
<h2 id="last-mile-documents-heading">关联单据</h2>
|
||||
<DataTable :data="[]" :columns="documentColumns" column-settings-mode="hide" empty-text="海外区域关联单据契约待接入" />
|
||||
</section>
|
||||
<section class="detail-section table-section is-wide" aria-labelledby="last-mile-cargo-heading">
|
||||
<h2 id="last-mile-cargo-heading">货物信息</h2>
|
||||
<DataTable :data="[]" :columns="cargoColumns" column-settings-mode="hide" empty-text="海外区域货物明细契约待接入" />
|
||||
</section>
|
||||
<section class="detail-section table-section" aria-labelledby="last-mile-logs-heading">
|
||||
<h2 id="last-mile-logs-heading">操作记录</h2>
|
||||
<DataTable :data="[]" :columns="logColumns" column-settings-mode="hide" empty-text="海外区域操作记录契约待接入" />
|
||||
</section>
|
||||
</div>
|
||||
</OverseasDetailShell>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.detail-grid { display: grid; min-width: 0; grid-template-columns: repeat(2, minmax(0, 1fr)); border: 1px solid var(--cc-border); background: #fff; }
|
||||
.detail-section { min-width: 0; padding: 16px 18px 18px; border-right: 1px solid var(--cc-border); border-bottom: 1px solid var(--cc-border); }
|
||||
.detail-section:nth-child(even) { border-right: 0; }
|
||||
.detail-section.is-wide { grid-column: 1 / -1; border-right: 0; }
|
||||
.detail-section h2 { margin: 0 0 12px; color: var(--cc-text); font-size: 15px; }
|
||||
.detail-fields { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); margin: 0; border-top: 1px solid var(--cc-border); border-left: 1px solid var(--cc-border); }
|
||||
.detail-fields > div { min-width: 0; min-height: 58px; padding: 9px 11px; border-right: 1px solid var(--cc-border); border-bottom: 1px solid var(--cc-border); }
|
||||
.detail-fields dt { color: var(--cc-muted); font-size: 12px; }
|
||||
.detail-fields dd { margin: 4px 0 0; color: var(--cc-text); overflow-wrap: anywhere; }
|
||||
.table-section { padding-right: 0; padding-bottom: 0; padding-left: 0; overflow: hidden; }
|
||||
.table-section h2 { margin-right: 18px; margin-left: 18px; }
|
||||
</style>
|
||||
@@ -0,0 +1,93 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { Eye } from '@lucide/vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import ContractPendingNotice from '../components/ContractPendingNotice.vue'
|
||||
import type { LastMileOrderListItem } from '../types'
|
||||
|
||||
const rows = ref<LastMileOrderListItem[]>([])
|
||||
const router = useRouter()
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const statusTab = ref<'全部' | '待分配'>('全部')
|
||||
const filters = ref<QueryModel>({ orderNo: '', inboundCode: '', warehouse: '', fbaWarehouse: '', orderType: '', owner: '', containerNo: '', status: '', consolidated: '', portEta: [] })
|
||||
const fields: QueryField[] = [
|
||||
{ key: 'orderNo', type: 'input', label: '尾程单号', placeholder: '请输入尾程单号' },
|
||||
{ key: 'inboundCode', type: 'input', label: '入仓码', placeholder: '请输入入仓码' },
|
||||
{ key: 'warehouse', type: 'select', label: '海外仓', placeholder: '全部海外仓', options: [] },
|
||||
{ key: 'fbaWarehouse', type: 'select', label: 'FBA仓库', placeholder: '全部FBA仓库', options: [] },
|
||||
{ key: 'orderType', type: 'select', label: '订单类型', placeholder: '全部订单类型', options: ['一件代发', '头程', '拒收订单', '快递订单'].map((value) => ({ label: value, value })) },
|
||||
{ key: 'owner', type: 'input', label: '货主', placeholder: '请输入货主' },
|
||||
{ key: 'containerNo', type: 'input', label: '集装箱号', placeholder: '请输入集装箱号' },
|
||||
{ key: 'status', type: 'select', label: '当前状态', placeholder: '全部状态', options: ['待入库计划', '待清关', '清关完成', '已合托', '待预约', '预约中', '预约成功'].map((value) => ({ label: value, value })) },
|
||||
{ key: 'consolidated', type: 'select', label: '是否合托', placeholder: '全部', options: [{ label: '是', value: 'yes' }, { label: '否', value: 'no' }] },
|
||||
{ key: 'portEta', type: 'daterange', label: '预计到港时间' },
|
||||
]
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 110, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'orderNo', prop: 'orderNo', label: '尾程单号', minWidth: 170 },
|
||||
{ key: 'warning', prop: 'warning', label: '预警', width: 90 },
|
||||
{ key: 'orderType', prop: 'orderType', label: '订单类型', minWidth: 110 },
|
||||
{ key: 'rejectedAt', prop: 'rejectedAt', label: '拒收时间', minWidth: 150 },
|
||||
{ key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 140 },
|
||||
{ key: 'owner', prop: 'owner', label: '货主', minWidth: 150 },
|
||||
{ key: 'warehouse', prop: 'warehouse', label: '海外仓', minWidth: 170 },
|
||||
{ key: 'transferWarehouse', prop: 'transferWarehouse', label: '转仓', minWidth: 140 },
|
||||
{ key: 'fbaWarehouse', prop: 'fbaWarehouse', label: 'FBA仓库', minWidth: 130 },
|
||||
{ key: 'containerNo', prop: 'containerNo', label: '集装箱号', minWidth: 150 },
|
||||
{ key: 'containerStatus', prop: 'containerStatus', label: '货柜状态', width: 110 },
|
||||
{ key: 'abnormal', prop: 'abnormal', label: '是否异常', width: 100 },
|
||||
{ key: 'held', prop: 'held', label: '是否暂扣', width: 100 },
|
||||
{ key: 'pallets', prop: 'pallets', label: '托数', width: 80, align: 'right' },
|
||||
{ key: 'cartons', prop: 'cartons', label: '箱数', width: 80, align: 'right' },
|
||||
{ key: 'volume', prop: 'volume', label: '体积(m³)', width: 100, align: 'right' },
|
||||
{ key: 'quantity', prop: 'quantity', label: '数量', width: 80, align: 'right' },
|
||||
{ key: 'volumeWeight', prop: 'volumeWeight', label: '体积重量', width: 100, align: 'right' },
|
||||
{ key: 'portEta', prop: 'portEta', label: '预计到港时间', minWidth: 150 },
|
||||
{ key: 'portAta', prop: 'portAta', label: '实际到港时间', minWidth: 150 },
|
||||
{ key: 'warehouseEta', prop: 'warehouseEta', label: '预计到仓', minWidth: 140 },
|
||||
{ key: 'inboundOrderNo', prop: 'inboundOrderNo', label: '入库单号', minWidth: 150 },
|
||||
{ key: 'outboundOrderNo', prop: 'outboundOrderNo', label: '出库单号', minWidth: 150 },
|
||||
{ key: 'dispatchOrderNo', prop: 'dispatchOrderNo', label: '派送单号', minWidth: 150 },
|
||||
{ key: 'relabelOrderNo', prop: 'relabelOrderNo', label: '换标单号', minWidth: 150 },
|
||||
{ key: 'relabelStatus', prop: 'relabelStatus', label: '换标状态', width: 110 },
|
||||
{ key: 'consolidated', prop: 'consolidated', label: '合托标记', width: 100 },
|
||||
{ key: 'status', prop: 'status', label: '当前状态', width: 110 },
|
||||
{ key: 'internalRemark', prop: 'internalRemark', label: '备注(仅自己)', minWidth: 160 },
|
||||
{ key: 'customerRemark', prop: 'customerRemark', label: '备注(客户)', minWidth: 160 },
|
||||
{ key: 'warehouseRemark', prop: 'warehouseRemark', label: '备注(仓库)', minWidth: 160 },
|
||||
]
|
||||
|
||||
function openDetail(source: object) {
|
||||
const row = source as LastMileOrderListItem
|
||||
void router.push({ name: 'overseas-last-mile-order-detail', params: { orderId: row.orderNo } })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminPage><ListPageLayout aria-label="尾程订单列表">
|
||||
<template #toolbar>
|
||||
<section class="surface-panel last-mile-toolbar" aria-label="尾程订单状态与筛选">
|
||||
<el-radio-group v-model="statusTab" aria-label="尾程订单状态" @change="page = 1">
|
||||
<el-radio-button value="全部">全部</el-radio-button>
|
||||
<el-radio-button value="待分配">待分配</el-radio-button>
|
||||
</el-radio-group>
|
||||
<QueryToolbar v-model="filters" :fields="fields" ariaLabel="尾程订单筛选" preference-key="overseas-last-mile-orders-query" @search="page = 1" />
|
||||
</section>
|
||||
</template>
|
||||
<template #state><ContractPendingNotice domain="尾程订单" /></template>
|
||||
<section class="surface-panel table-panel"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :total="0" preference-key="overseas-last-mile-orders" row-key="orderNo" empty-text="海外区域尾程订单查询契约待接入">
|
||||
<template #actions="{ row }"><el-button size="small" plain type="primary" @click="openDetail(row)"><Eye :size="15" />查看</el-button></template>
|
||||
<template #pagination-left>共 0 条尾程订单</template>
|
||||
</DataTable></section>
|
||||
</ListPageLayout></AdminPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.last-mile-toolbar { padding: 14px 16px 0; }
|
||||
.last-mile-toolbar :deep(.query-toolbar) { margin-top: 12px; padding: 12px 0 16px; border: 0; border-top: 1px solid var(--cc-border); border-radius: 0; }
|
||||
</style>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import OverseasReadOnlyDetail from '../components/OverseasReadOnlyDetail.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const palletId = computed(() => String(route.params.palletId || '—'))
|
||||
const groups = [
|
||||
{ title: '基础信息', fields: ['托盘编码', '托盘类型', '仓库', '容积(m³)', '采购价格', '所属FBA仓', '状态'] },
|
||||
{ title: '使用信息', fields: ['关联入库单', '关联箱数', '创建时间', '更新时间', '备注'] },
|
||||
]
|
||||
function back() { void router.push({ name: 'overseas-pallets' }) }
|
||||
</script>
|
||||
|
||||
<template><OverseasReadOnlyDetail title="托盘详情" reference-label="托盘编码" :reference-value="palletId" back-label="返回托盘管理" domain="托盘详情" :groups="groups" @back="back" /></template>
|
||||
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { Eye, Pencil, Plus } from '@lucide/vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import ContractPendingNotice from '../components/ContractPendingNotice.vue'
|
||||
import ContractPendingFormDialog from '../components/ContractPendingFormDialog.vue'
|
||||
import { overseasWarehousePermissions as permission } from '../permissions'
|
||||
import type { PalletListItem } from '../types'
|
||||
|
||||
const access = useAccessPreviewStore()
|
||||
const router = useRouter()
|
||||
const rows = ref<PalletListItem[]>([])
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const filters = ref<QueryModel>({ code: '', type: '', warehouse: '', status: '' })
|
||||
const fields: QueryField[] = [
|
||||
{ key: 'code', type: 'input', label: '托盘编码', placeholder: '请输入托盘编码', width: 180 },
|
||||
{ key: 'type', type: 'select', label: '托盘类型', placeholder: '全部类型', width: 160, options: ['木托盘', '塑料托盘', '金属托盘', '纸托盘', '免熏蒸'].map((value) => ({ label: value, value })) },
|
||||
{ key: 'warehouse', type: 'select', label: '仓库', placeholder: '全部仓库', width: 190, options: [] },
|
||||
{ key: 'status', type: 'select', label: '状态', placeholder: '全部状态', width: 140, options: ['闲置', '占用'].map((value) => ({ label: value, value })) },
|
||||
]
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 180, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'code', label: '托盘编码', minWidth: 160 }, { key: 'type', label: '托盘类型', minWidth: 130 },
|
||||
{ key: 'warehouse', label: '仓库', minWidth: 180 }, { key: 'volume', label: '容积(m³)', width: 110, align: 'right' },
|
||||
{ key: 'purchasePrice', label: '采购价格', width: 120, align: 'right' }, { key: 'fbaWarehouse', label: '所属FBA仓', minWidth: 140 },
|
||||
{ key: 'status', label: '状态', width: 100 }, { key: 'remark', label: '备注', minWidth: 180 },
|
||||
]
|
||||
const dialogOpen = ref(false)
|
||||
const dialogMode = ref<'create' | 'edit'>('create')
|
||||
const form = reactive({ code: '', type: '木托盘', volume: undefined as number | undefined, purchasePrice: undefined as number | undefined, warehouse: '', fbaWarehouse: '', status: '闲置', remark: '' })
|
||||
|
||||
function optionalNumber(value: unknown) { const number = Number(value); return value === '' || value == null || !Number.isFinite(number) ? undefined : number }
|
||||
function openForm(mode: 'create' | 'edit', source?: object) { const row = source as PalletListItem | undefined; dialogMode.value = mode; Object.assign(form, { code: '', type: '木托盘', volume: undefined, purchasePrice: undefined, warehouse: '', fbaWarehouse: '', status: '闲置', remark: '' }); if (mode === 'edit' && row) Object.assign(form, { code: String(row.code || ''), type: String(row.type || '木托盘'), volume: optionalNumber(row.volume), purchasePrice: optionalNumber(row.purchasePrice), warehouse: String(row.warehouse || ''), fbaWarehouse: String(row.fbaWarehouse || ''), status: String(row.status || '闲置'), remark: String(row.remark || '') }); dialogOpen.value = true }
|
||||
function openDetail(source: object) { const row = source as PalletListItem; void router.push({ name: 'overseas-pallet-detail', params: { palletId: String(row.code) } }) }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminPage><ListPageLayout aria-label="托盘管理列表">
|
||||
<template #toolbar><QueryToolbar v-model="filters" class="surface-panel" :fields="fields" ariaLabel="托盘筛选" preference-key="overseas-pallets-query" @search="page = 1" /></template>
|
||||
<template #state><ContractPendingNotice domain="托盘管理" /></template>
|
||||
<section class="surface-panel table-panel"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :total="0" preference-key="overseas-pallets" row-key="code" empty-text="海外区域托盘查询契约待接入">
|
||||
<template #toolbar-actions><el-button v-if="access.can(permission.palletCreate)" type="primary" @click="openForm('create')"><Plus :size="16" />新增托盘</el-button></template>
|
||||
<template #actions="{ row }"><el-button size="small" plain type="primary" @click="openDetail(row)"><Eye :size="15" />查看</el-button><el-button v-if="access.can(permission.palletEdit)" size="small" plain @click="openForm('edit', row)"><Pencil :size="15" />编辑</el-button></template>
|
||||
<template #pagination-left>共 0 条托盘</template>
|
||||
</DataTable></section>
|
||||
</ListPageLayout>
|
||||
<ContractPendingFormDialog v-model="dialogOpen" :title="dialogMode === 'create' ? '新增托盘' : '编辑托盘'" width="min(760px, calc(100vw - 28px))" domain="托盘">
|
||||
<el-form label-position="top"><el-row :gutter="16">
|
||||
<el-col :span="12"><el-form-item label="托盘编码" required><el-input v-model="form.code" :readonly="dialogMode === 'edit'" placeholder="请输入托盘编码" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="托盘类型" required><el-select v-model="form.type"><el-option v-for="value in ['木托盘', '塑料托盘', '金属托盘', '纸托盘', '免熏蒸']" :key="value" :label="value" :value="value" /></el-select></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="容积(m³)" required><el-input-number v-model="form.volume" :min="0.01" :precision="2" controls-position="right" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="采购价格(USD)" required><el-input-number v-model="form.purchasePrice" :min="0" :precision="2" controls-position="right" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="仓库" required><el-select v-model="form.warehouse" placeholder="海外仓候选接口待接入" disabled /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="所属 FBA 仓" required><el-select v-model="form.fbaWarehouse" placeholder="FBA仓候选接口待接入" disabled /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="状态" required><el-select v-model="form.status"><el-option label="闲置" value="闲置" /><el-option label="占用" value="占用" /></el-select></el-form-item></el-col>
|
||||
<el-col :span="24"><el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="3" /></el-form-item></el-col>
|
||||
</el-row></el-form>
|
||||
</ContractPendingFormDialog>
|
||||
</AdminPage>
|
||||
</template>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import OverseasReadOnlyDetail from '../components/OverseasReadOnlyDetail.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const productId = computed(() => String(route.params.productId || '—'))
|
||||
const groups = [
|
||||
{ title: '基础信息', fields: ['商品SKU', '中文品名', '英文品名', '产品分类'] },
|
||||
{ title: '包装信息', fields: ['长(cm)', '宽(cm)', '高(cm)', '重量(kg)', '包装体积(m³)'] },
|
||||
{ title: '其他信息', fields: ['创建时间', '更新时间', '备注'] },
|
||||
]
|
||||
function back() { void router.push({ name: 'overseas-products' }) }
|
||||
</script>
|
||||
|
||||
<template><OverseasReadOnlyDetail title="产品详情" reference-label="商品SKU" :reference-value="productId" back-label="返回产品库" domain="产品详情" :groups="groups" @back="back" /></template>
|
||||
@@ -0,0 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { Eye, Pencil, Plus } from '@lucide/vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import ContractPendingNotice from '../components/ContractPendingNotice.vue'
|
||||
import ContractPendingFormDialog from '../components/ContractPendingFormDialog.vue'
|
||||
import { overseasWarehousePermissions as permission } from '../permissions'
|
||||
import type { ProductListItem } from '../types'
|
||||
|
||||
const access = useAccessPreviewStore()
|
||||
const router = useRouter()
|
||||
const rows = ref<ProductListItem[]>([])
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const filters = ref<QueryModel>({ sku: '', productName: '', category: '' })
|
||||
const fields: QueryField[] = [
|
||||
{ key: 'sku', type: 'input', label: '商品SKU', placeholder: '请输入商品SKU', width: 180 },
|
||||
{ key: 'productName', type: 'input', label: '中文品名', placeholder: '请输入中文品名', width: 190 },
|
||||
{ key: 'category', type: 'select', label: '品类', placeholder: '全部品类', width: 150, options: ['电子', '家居', '服饰', '户外'].map((value) => ({ label: value, value })) },
|
||||
]
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 180, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'sku', label: '商品SKU', minWidth: 150 }, { key: 'productName', label: '中文品名', minWidth: 180 },
|
||||
{ key: 'englishName', label: '英文品名', minWidth: 180 }, { key: 'category', label: '品类', width: 120 },
|
||||
{ key: 'length', label: '长(cm)', width: 100, align: 'right' }, { key: 'width', label: '宽(cm)', width: 100, align: 'right' },
|
||||
{ key: 'height', label: '高(cm)', width: 100, align: 'right' }, { key: 'weight', label: '重量(kg)', width: 110, align: 'right' },
|
||||
]
|
||||
const dialogOpen = ref(false)
|
||||
const dialogMode = ref<'create' | 'edit'>('create')
|
||||
const form = reactive({ sku: '', productName: '', englishName: '', category: '', length: undefined as number | undefined, width: undefined as number | undefined, height: undefined as number | undefined, weight: undefined as number | undefined })
|
||||
|
||||
function optionalNumber(value: unknown) { const number = Number(value); return value === '' || value == null || !Number.isFinite(number) ? undefined : number }
|
||||
function openForm(mode: 'create' | 'edit', source?: object) { const row = source as ProductListItem | undefined; dialogMode.value = mode; Object.assign(form, { sku: '', productName: '', englishName: '', category: '', length: undefined, width: undefined, height: undefined, weight: undefined }); if (mode === 'edit' && row) Object.assign(form, { sku: String(row.sku || ''), productName: String(row.productName || ''), englishName: String(row.englishName || ''), category: String(row.category || ''), length: optionalNumber(row.length), width: optionalNumber(row.width), height: optionalNumber(row.height), weight: optionalNumber(row.weight) }); dialogOpen.value = true }
|
||||
function openDetail(source: object) { const row = source as ProductListItem; void router.push({ name: 'overseas-product-detail', params: { productId: String(row.sku) } }) }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminPage><ListPageLayout aria-label="产品库列表">
|
||||
<template #toolbar><QueryToolbar v-model="filters" class="surface-panel" :fields="fields" ariaLabel="产品筛选" preference-key="overseas-products-query" @search="page = 1" /></template>
|
||||
<template #state><ContractPendingNotice domain="产品库" /></template>
|
||||
<section class="surface-panel table-panel"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :total="0" preference-key="overseas-products" row-key="sku" empty-text="海外区域产品查询契约待接入">
|
||||
<template #toolbar-actions><el-button v-if="access.can(permission.productCreate)" type="primary" @click="openForm('create')"><Plus :size="16" />新增产品</el-button></template>
|
||||
<template #actions="{ row }"><el-button size="small" plain type="primary" @click="openDetail(row)"><Eye :size="15" />查看</el-button><el-button v-if="access.can(permission.productEdit)" size="small" plain @click="openForm('edit', row)"><Pencil :size="15" />编辑</el-button></template>
|
||||
<template #pagination-left>共 0 条产品</template>
|
||||
</DataTable></section>
|
||||
</ListPageLayout>
|
||||
<ContractPendingFormDialog v-model="dialogOpen" :title="dialogMode === 'create' ? '新增产品' : '编辑产品'" width="min(560px, calc(100vw - 28px))" domain="产品" confirm-text="确定">
|
||||
<el-form label-width="110px" label-position="right">
|
||||
<el-form-item label="商品SKU" required><el-input v-model="form.sku" placeholder="如 SKU-A1001" /></el-form-item>
|
||||
<el-form-item label="中文品名" required><el-input v-model="form.productName" placeholder="请输入中文品名" /></el-form-item>
|
||||
<el-form-item label="英文品名"><el-input v-model="form.englishName" placeholder="请输入英文品名" /></el-form-item>
|
||||
<el-form-item label="品类" required><el-select v-model="form.category" placeholder="请选择品类"><el-option v-for="value in ['电子', '家居', '服饰', '户外']" :key="value" :label="value" :value="value" /></el-select></el-form-item>
|
||||
<el-form-item label="长(cm)"><el-input-number v-model="form.length" :min="0" :precision="2" controls-position="right" /></el-form-item>
|
||||
<el-form-item label="宽(cm)"><el-input-number v-model="form.width" :min="0" :precision="2" controls-position="right" /></el-form-item>
|
||||
<el-form-item label="高(cm)"><el-input-number v-model="form.height" :min="0" :precision="2" controls-position="right" /></el-form-item>
|
||||
<el-form-item label="重量(kg)"><el-input-number v-model="form.weight" :min="0" :precision="3" controls-position="right" /></el-form-item>
|
||||
</el-form>
|
||||
</ContractPendingFormDialog>
|
||||
</AdminPage>
|
||||
</template>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import OverseasReadOnlyDetail from '../components/OverseasReadOnlyDetail.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const orderId = computed(() => String(route.params.orderId || '—'))
|
||||
const groups = [
|
||||
{ title: '单据信息', fields: ['换标单号', '关联订单', '状态', '创建时间', '更新时间'] },
|
||||
{ title: '商品信息', fields: ['商品SKU', '中文品名', '箱数', '预算总价'] },
|
||||
{ title: '作业要求', fields: ['贴标要求', '上传文件', '备注'] },
|
||||
{ title: '费用明细', fields: ['开箱费', '挑货费', '换标费', '换箱唛标费', '换标费总计'] },
|
||||
]
|
||||
function back() { void router.push({ name: 'overseas-relabel-orders' }) }
|
||||
</script>
|
||||
|
||||
<template><OverseasReadOnlyDetail title="换标单详情" reference-label="换标单号" :reference-value="orderId" back-label="返回换标单" domain="换标单详情" :groups="groups" @back="back" /></template>
|
||||
@@ -0,0 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { Eye, Pencil, Plus, Upload } from '@lucide/vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import ContractPendingNotice from '../components/ContractPendingNotice.vue'
|
||||
import ContractPendingFormDialog from '../components/ContractPendingFormDialog.vue'
|
||||
import { overseasWarehousePermissions as permission } from '../permissions'
|
||||
import type { RelabelOrderListItem } from '../types'
|
||||
|
||||
const access = useAccessPreviewStore()
|
||||
const router = useRouter()
|
||||
const rows = ref<RelabelOrderListItem[]>([])
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const filters = ref<QueryModel>({ sku: '', status: '' })
|
||||
const fields: QueryField[] = [
|
||||
{ key: 'sku', type: 'input', label: '商品SKU', placeholder: '请输入商品SKU', width: 180 },
|
||||
{ key: 'status', type: 'select', label: '状态', placeholder: '全部状态', width: 160, options: ['待处理', '处理中', '已完成'].map((value) => ({ label: value, value })) },
|
||||
]
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 190, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'orderNo', label: '换标单号', minWidth: 170 },
|
||||
{ key: 'sku', label: '商品SKU', minWidth: 150 },
|
||||
{ key: 'productName', label: '中文品名', minWidth: 180 },
|
||||
{ key: 'cartons', label: '箱数', width: 90, align: 'right' },
|
||||
{ key: 'budgetTotal', label: '预算总价', width: 120, align: 'right' },
|
||||
{ key: 'labelRequirement', label: '贴标要求', minWidth: 150 },
|
||||
{ key: 'fileName', label: '上传文件', minWidth: 160 },
|
||||
{ key: 'status', label: '状态', width: 110 },
|
||||
]
|
||||
const dialogOpen = ref(false)
|
||||
const dialogMode = ref<'create' | 'edit'>('create')
|
||||
const form = reactive({ orderNo: '', relatedOrderNo: '', sku: '', productName: '', cartons: undefined as number | undefined, budgetTotal: undefined as number | undefined, status: '待处理', labelRequirement: '', relabelCost: undefined as number | undefined, remark: '' })
|
||||
|
||||
function optionalNumber(value: unknown) { const number = Number(value); return value === '' || value == null || !Number.isFinite(number) ? undefined : number }
|
||||
function openForm(mode: 'create' | 'edit', source?: object) {
|
||||
const row = source as RelabelOrderListItem | undefined
|
||||
dialogMode.value = mode
|
||||
Object.assign(form, { orderNo: '', relatedOrderNo: '', sku: '', productName: '', cartons: undefined, budgetTotal: undefined, status: '待处理', labelRequirement: '', relabelCost: undefined, remark: '' })
|
||||
if (mode === 'edit' && row) Object.assign(form, { orderNo: String(row.orderNo || ''), relatedOrderNo: String(row.relatedOrderNo || ''), sku: String(row.sku || ''), productName: String(row.productName || ''), cartons: optionalNumber(row.cartons), budgetTotal: optionalNumber(row.budgetTotal), status: String(row.status || '待处理'), labelRequirement: String(row.labelRequirement || ''), relabelCost: optionalNumber(row.relabelCost), remark: String(row.remark || '') })
|
||||
dialogOpen.value = true
|
||||
}
|
||||
|
||||
function openDetail(source: object) {
|
||||
const row = source as RelabelOrderListItem
|
||||
void router.push({ name: 'overseas-relabel-order-detail', params: { orderId: String(row.orderNo) } })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminPage><ListPageLayout aria-label="换标单列表">
|
||||
<template #toolbar><QueryToolbar v-model="filters" class="surface-panel" :fields="fields" ariaLabel="换标单筛选" preference-key="overseas-relabel-orders-query" @search="page = 1" /></template>
|
||||
<template #state><ContractPendingNotice domain="换标单" /></template>
|
||||
<section class="surface-panel table-panel"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :total="0" preference-key="overseas-relabel-orders" row-key="orderNo" empty-text="海外区域换标单查询契约待接入">
|
||||
<template #toolbar-actions><el-button v-if="access.can(permission.relabelOrderCreate)" type="primary" @click="openForm('create')"><Plus :size="16" />新增换标单</el-button></template>
|
||||
<template #actions="{ row }"><el-button size="small" plain type="primary" @click="openDetail(row)"><Eye :size="15" />查看</el-button><el-button v-if="access.can(permission.relabelOrderEdit)" size="small" plain @click="openForm('edit', row)"><Pencil :size="15" />审核</el-button></template>
|
||||
<template #pagination-left>共 0 条换标单</template>
|
||||
</DataTable></section>
|
||||
</ListPageLayout>
|
||||
<ContractPendingFormDialog v-model="dialogOpen" :title="dialogMode === 'create' ? '新增换标单' : '审核换标单'" width="min(760px, calc(100vw - 28px))" domain="换标单" :confirm-text="dialogMode === 'create' ? '保存换标单' : '提交审核'">
|
||||
<el-form label-position="top"><h3>订单信息</h3><el-row :gutter="16">
|
||||
<el-col :span="12"><el-form-item label="换标单号"><el-input :model-value="dialogMode === 'create' ? '系统自动生成' : form.orderNo || '—'" readonly /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="关联订单" required><el-input v-model="form.relatedOrderNo" placeholder="请输入关联订单号" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="商品SKU" required><el-input v-model="form.sku" placeholder="请输入商品SKU" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="中文品名" required><el-input v-model="form.productName" placeholder="请输入中文品名" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="箱数" required><el-input-number v-model="form.cartons" :min="1" :precision="0" controls-position="right" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="预算总价(USD)" required><el-input-number v-model="form.budgetTotal" :min="0" :precision="2" controls-position="right" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="状态"><el-select v-model="form.status" :disabled="dialogMode === 'create'"><el-option v-for="value in ['待处理', '处理中', '已完成']" :key="value" :label="value" :value="value" /></el-select></el-form-item></el-col>
|
||||
</el-row><h3>作业要求</h3><el-row :gutter="16">
|
||||
<el-col :span="12"><el-form-item label="贴标要求" required><el-select v-model="form.labelRequirement" placeholder="请选择贴标要求"><el-option v-for="value in ['全覆盖换标', '加贴新标', '移除原标+换标']" :key="value" :label="value" :value="value" /></el-select></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="上传文件"><el-button disabled><Upload :size="16" />选择文件</el-button></el-form-item></el-col>
|
||||
<el-col v-if="dialogMode === 'edit'" :span="12"><el-form-item label="预算报价(USD)"><el-input-number v-model="form.relabelCost" :min="0" :precision="2" controls-position="right" /></el-form-item></el-col>
|
||||
<el-col :span="24"><el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="3" maxlength="500" show-word-limit /></el-form-item></el-col>
|
||||
</el-row></el-form>
|
||||
</ContractPendingFormDialog>
|
||||
</AdminPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
h3 { margin: 0 0 12px; color: var(--cc-text); font-size: 14px; }
|
||||
h3:not(:first-child) { margin-top: 4px; padding-top: 14px; border-top: 1px solid var(--cc-border); }
|
||||
</style>
|
||||
@@ -0,0 +1,209 @@
|
||||
import { readFileSync, readdirSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
function source(fileName: string) {
|
||||
return readFileSync(resolve(process.cwd(), 'src/modules/overseas-warehouse/pages', fileName), 'utf8')
|
||||
}
|
||||
|
||||
function columnKeys(fileName: string) {
|
||||
const value = source(fileName)
|
||||
const start = value.indexOf('const columns: DataTableColumn[]')
|
||||
const end = value.indexOf('\n]', start)
|
||||
return [...value.slice(start, end).matchAll(/\{ key: '([^']+)'/g)].map((match) => match[1])
|
||||
}
|
||||
|
||||
describe('overseas warehouse first-batch prototype contract', () => {
|
||||
it('gives every formal list query toolbar a unique stable preference key', () => {
|
||||
const expected = new Map([
|
||||
['LastMileOrdersView.vue', 'overseas-last-mile-orders-query'],
|
||||
['InboundOrdersView.vue', 'overseas-inbound-orders-query'],
|
||||
['InventoryView.vue', 'overseas-inventory-query'],
|
||||
['InventoryTransactionsView.vue', 'overseas-inventory-transactions-query'],
|
||||
['RelabelOrdersView.vue', 'overseas-relabel-orders-query'],
|
||||
['PalletsView.vue', 'overseas-pallets-query'],
|
||||
['CartonsView.vue', 'overseas-cartons-query'],
|
||||
['ProductsView.vue', 'overseas-products-query'],
|
||||
])
|
||||
const keys = [...expected.entries()].map(([fileName, preferenceKey]) => {
|
||||
const value = source(fileName)
|
||||
const toolbar = value.match(/<QueryToolbar\b[^>]*\/>/)?.[0]
|
||||
expect(toolbar, `${fileName} should render a QueryToolbar`).toBeDefined()
|
||||
expect(toolbar).toContain(`preference-key="${preferenceKey}"`)
|
||||
return preferenceKey
|
||||
})
|
||||
const queryToolbarPages = readdirSync(resolve(process.cwd(), 'src/modules/overseas-warehouse/pages'))
|
||||
.filter((fileName) => fileName.endsWith('.vue') && source(fileName).includes('<QueryToolbar'))
|
||||
|
||||
expect(queryToolbarPages.sort()).toEqual([...expected.keys()].sort())
|
||||
expect(new Set(keys).size).toBe(expected.size)
|
||||
})
|
||||
|
||||
it('keeps the operation column first on every business list', () => {
|
||||
for (const fileName of ['LastMileOrdersView.vue', 'InboundOrdersView.vue', 'InventoryView.vue', 'RelabelOrdersView.vue', 'PalletsView.vue', 'CartonsView.vue', 'ProductsView.vue']) {
|
||||
const value = source(fileName)
|
||||
const columns = value.slice(value.indexOf('const columns: DataTableColumn[]'))
|
||||
expect(columns.match(/\{\s*key: '([^']+)'/)?.[1]).toBe('actions')
|
||||
}
|
||||
})
|
||||
|
||||
it('preserves the prototype dialog carriers and desktop widths', () => {
|
||||
const expected = [
|
||||
['RelabelOrdersView.vue', '760px', '新增换标单', '审核换标单'],
|
||||
['PalletsView.vue', '760px', '新增托盘', '编辑托盘'],
|
||||
['CartonsView.vue', '860px', '新增箱号', '编辑箱号'],
|
||||
['ProductsView.vue', '560px', '新增产品', '编辑产品'],
|
||||
]
|
||||
for (const [fileName, width, createTitle, editTitle] of expected) {
|
||||
const value = source(fileName)
|
||||
expect(value).toContain('<ContractPendingFormDialog')
|
||||
expect(value).toContain(width)
|
||||
expect(value).toContain(createTitle)
|
||||
expect(value).toContain(editTitle)
|
||||
expect(value).not.toContain('AppDrawer')
|
||||
}
|
||||
})
|
||||
|
||||
it('implements all second-batch details as formal parameterized pages', () => {
|
||||
for (const fileName of ['RelabelOrderDetailView.vue', 'PalletDetailView.vue', 'CartonDetailView.vue', 'ProductDetailView.vue']) {
|
||||
const value = source(fileName)
|
||||
expect(value).toContain('<OverseasReadOnlyDetail')
|
||||
expect(value).toContain('@back="back"')
|
||||
expect(value).not.toContain('AppDialog')
|
||||
expect(value).not.toContain('AppDrawer')
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps second-batch forms non-submitting until overseas write contracts exist', () => {
|
||||
const dialog = readFileSync(resolve(process.cwd(), 'src/modules/overseas-warehouse/components/ContractPendingFormDialog.vue'), 'utf8')
|
||||
expect(dialog).toContain('confirm-disabled')
|
||||
expect(dialog).toContain('不可提交')
|
||||
expect(dialog).not.toContain('@confirm=')
|
||||
for (const fileName of ['RelabelOrdersView.vue', 'PalletsView.vue', 'CartonsView.vue', 'ProductsView.vue']) {
|
||||
const value = source(fileName)
|
||||
expect(value).not.toContain('ElMessage.success')
|
||||
expect(value).not.toMatch(/https?:\/\//)
|
||||
}
|
||||
})
|
||||
|
||||
it('implements three prototype-defined details as formal pages', () => {
|
||||
for (const fileName of ['LastMileOrderDetailView.vue', 'InboundOrderDetailView.vue', 'InventoryDetailView.vue']) {
|
||||
const value = source(fileName)
|
||||
expect(value).toContain('<OverseasDetailShell')
|
||||
expect(value).toContain('@back="back"')
|
||||
expect(value).not.toContain('AppDrawer')
|
||||
expect(value).not.toContain('AppDialog')
|
||||
}
|
||||
})
|
||||
|
||||
it('does not expose outbound planning or delivery actions from the last-mile detail', () => {
|
||||
const value = source('LastMileOrderDetailView.vue')
|
||||
expect(value).not.toContain('计划出库日期')
|
||||
expect(value).not.toContain('派送方式')
|
||||
expect(value).not.toContain('保存并生成出库单')
|
||||
expect(value).not.toContain('海外派送')
|
||||
})
|
||||
|
||||
it('shows contract-pending empty states instead of demo data or fake success actions', () => {
|
||||
for (const fileName of ['LastMileOrderDetailView.vue', 'InboundOrderDetailView.vue', 'InventoryDetailView.vue']) {
|
||||
const value = source(fileName)
|
||||
expect(value).toContain(':data="[]"')
|
||||
expect(value).toContain('契约待接入')
|
||||
expect(value).not.toContain('ElMessage.success')
|
||||
}
|
||||
})
|
||||
|
||||
it('adds the prototype inventory transaction page as a read-only inventory route', () => {
|
||||
const inventory = source('InventoryView.vue')
|
||||
const transactions = source('InventoryTransactionsView.vue')
|
||||
expect(inventory).toContain("name: 'overseas-inventory-transactions'")
|
||||
expect(transactions).toContain('进销存记录筛选')
|
||||
expect(transactions).toContain('业务订单号')
|
||||
expect(transactions).toContain('入库单号')
|
||||
expect(transactions).toContain('出库单号')
|
||||
expect(transactions).toContain('出库相关字段仅展示既有库存流水事实')
|
||||
expect(transactions).toContain("router.push({ name: 'overseas-inventory' })")
|
||||
expect(transactions).not.toContain('保存')
|
||||
expect(transactions).not.toContain('ElMessage.success')
|
||||
expect(transactions).not.toMatch(/router\.push\([^)]*outbound/)
|
||||
})
|
||||
|
||||
it('uses module presentation models instead of untyped records', () => {
|
||||
for (const fileName of ['LastMileOrdersView.vue', 'InboundOrdersView.vue', 'InventoryView.vue', 'RelabelOrdersView.vue', 'PalletsView.vue', 'CartonsView.vue', 'ProductsView.vue', 'InventoryTransactionsView.vue']) {
|
||||
const value = source(fileName)
|
||||
expect(value).toContain("from '../types'")
|
||||
expect(value).not.toContain('ref<Record<string, unknown>[]>')
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps every formal list connected to its read-only detail route', () => {
|
||||
const expected = [
|
||||
['LastMileOrdersView.vue', 'overseas-last-mile-order-detail'],
|
||||
['InboundOrdersView.vue', 'overseas-inbound-order-detail'],
|
||||
['InventoryView.vue', 'overseas-inventory-detail'],
|
||||
['RelabelOrdersView.vue', 'overseas-relabel-order-detail'],
|
||||
['PalletsView.vue', 'overseas-pallet-detail'],
|
||||
['CartonsView.vue', 'overseas-carton-detail'],
|
||||
['ProductsView.vue', 'overseas-product-detail'],
|
||||
]
|
||||
for (const [fileName, routeName] of expected) {
|
||||
const value = source(fileName)
|
||||
expect(value).toContain(routeName)
|
||||
expect(value).toContain('<Eye :size="15" />查看')
|
||||
}
|
||||
})
|
||||
|
||||
it('preserves last-mile status tabs and read-only outbound association columns without links', () => {
|
||||
const value = source('LastMileOrdersView.vue')
|
||||
expect(value).toContain('aria-label="尾程订单状态"')
|
||||
expect(value).toContain('<el-radio-button value="全部">全部</el-radio-button>')
|
||||
expect(value).toContain('<el-radio-button value="待分配">待分配</el-radio-button>')
|
||||
expect(value.indexOf("label: '入库单号'")).toBeLessThan(value.indexOf("label: '出库单号'"))
|
||||
expect(value.indexOf("label: '出库单号'")).toBeLessThan(value.indexOf("label: '派送单号'"))
|
||||
expect(value.indexOf("label: '派送单号'")).toBeLessThan(value.indexOf("label: '换标单号'"))
|
||||
expect(value).not.toMatch(/router\.push\([^)]*(outbound|dispatch)/)
|
||||
})
|
||||
|
||||
it('keeps outbound cartons read-only and uses the prototype product confirmation copy', () => {
|
||||
const cartons = source('CartonsView.vue')
|
||||
const products = source('ProductsView.vue')
|
||||
expect(cartons).toContain("function canEditCarton(source: object)")
|
||||
expect(cartons).toContain("=== '在库'")
|
||||
expect(cartons).toContain('access.can(permission.cartonEdit) && canEditCarton(row)')
|
||||
expect(products).toContain('confirm-text="确定"')
|
||||
})
|
||||
|
||||
it('keeps every prototype list column in its frozen order', () => {
|
||||
expect(columnKeys('LastMileOrdersView.vue')).toEqual([
|
||||
'actions', 'orderNo', 'warning', 'orderType', 'rejectedAt', 'inboundCode', 'owner', 'warehouse',
|
||||
'transferWarehouse', 'fbaWarehouse', 'containerNo', 'containerStatus', 'abnormal', 'held', 'pallets',
|
||||
'cartons', 'volume', 'quantity', 'volumeWeight', 'portEta', 'portAta', 'warehouseEta', 'inboundOrderNo',
|
||||
'outboundOrderNo', 'dispatchOrderNo', 'relabelOrderNo', 'relabelStatus', 'consolidated', 'status',
|
||||
'internalRemark', 'customerRemark', 'warehouseRemark',
|
||||
])
|
||||
expect(columnKeys('InboundOrdersView.vue')).toEqual([
|
||||
'actions', 'orderNo', 'inboundCode', 'type', 'warehouse', 'fbaWarehouse', 'owner', 'cartons', 'volume',
|
||||
'pallets', 'status', 'appointmentStatus', 'dispatchStatus',
|
||||
])
|
||||
expect(columnKeys('InventoryView.vue')).toEqual([
|
||||
'actions', 'owner', 'warehouse', 'sku', 'productName', 'onHand', 'available', 'held', 'looseboard',
|
||||
'inTransit', 'purchase', 'status', 'updatedAt',
|
||||
])
|
||||
expect(columnKeys('RelabelOrdersView.vue')).toEqual([
|
||||
'actions', 'orderNo', 'sku', 'productName', 'cartons', 'budgetTotal', 'labelRequirement', 'fileName', 'status',
|
||||
])
|
||||
expect(columnKeys('PalletsView.vue')).toEqual([
|
||||
'actions', 'code', 'type', 'warehouse', 'volume', 'purchasePrice', 'fbaWarehouse', 'status', 'remark',
|
||||
])
|
||||
expect(columnKeys('CartonsView.vue')).toEqual([
|
||||
'actions', 'cartonNo', 'fbaWarehouse', 'sku', 'productName', 'quantity', 'weight', 'status',
|
||||
])
|
||||
expect(columnKeys('ProductsView.vue')).toEqual([
|
||||
'actions', 'sku', 'productName', 'englishName', 'category', 'length', 'width', 'height', 'weight',
|
||||
])
|
||||
expect(columnKeys('InventoryTransactionsView.vue')).toEqual([
|
||||
'occurredAt', 'owner', 'warehouse', 'sku', 'productName', 'businessType', 'businessOrderNo',
|
||||
'inboundOrderNo', 'outboundOrderNo', 'inboundQuantity', 'outboundQuantity', 'balance', 'operator', 'remark',
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,17 @@
|
||||
export const overseasWarehousePermissions = {
|
||||
lastMileOrderList: 'platform:overseas-warehouse:last-mile-order:list',
|
||||
inboundOrderList: 'platform:overseas-warehouse:inbound-order:list',
|
||||
inventoryList: 'platform:overseas-warehouse:inventory:list',
|
||||
relabelOrderList: 'platform:overseas-warehouse:relabel-order:list',
|
||||
relabelOrderCreate: 'platform:overseas-warehouse:relabel-order:create',
|
||||
relabelOrderEdit: 'platform:overseas-warehouse:relabel-order:edit',
|
||||
palletList: 'platform:overseas-warehouse:pallet:list',
|
||||
palletCreate: 'platform:overseas-warehouse:pallet:create',
|
||||
palletEdit: 'platform:overseas-warehouse:pallet:edit',
|
||||
cartonList: 'platform:overseas-warehouse:carton:list',
|
||||
cartonCreate: 'platform:overseas-warehouse:carton:create',
|
||||
cartonEdit: 'platform:overseas-warehouse:carton:edit',
|
||||
productList: 'platform:overseas-warehouse:product:list',
|
||||
productCreate: 'platform:overseas-warehouse:product:create',
|
||||
productEdit: 'platform:overseas-warehouse:product:edit',
|
||||
} as const
|
||||
@@ -0,0 +1,40 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { overseasWarehouseRoutes } from './routes'
|
||||
import { overseasWarehousePermissions as permission } from './permissions'
|
||||
|
||||
describe('overseas warehouse routes', () => {
|
||||
it('uses list permission for each list and read-only detail route', () => {
|
||||
expect(overseasWarehouseRoutes.map((route) => [route.path, route.meta.permission])).toEqual([
|
||||
['overseas-warehouse/last-mile-orders', permission.lastMileOrderList],
|
||||
['overseas-warehouse/last-mile-orders/:orderId', permission.lastMileOrderList],
|
||||
['overseas-warehouse/inbound-orders', permission.inboundOrderList],
|
||||
['overseas-warehouse/inbound-orders/:orderId', permission.inboundOrderList],
|
||||
['overseas-warehouse/inventory', permission.inventoryList],
|
||||
['overseas-warehouse/inventory-transactions', permission.inventoryList],
|
||||
['overseas-warehouse/inventory/:inventoryId', permission.inventoryList],
|
||||
['overseas-warehouse/relabel-orders', permission.relabelOrderList],
|
||||
['overseas-warehouse/relabel-orders/:orderId', permission.relabelOrderList],
|
||||
['overseas-warehouse/pallets', permission.palletList],
|
||||
['overseas-warehouse/pallets/:palletId', permission.palletList],
|
||||
['overseas-warehouse/cartons', permission.cartonList],
|
||||
['overseas-warehouse/cartons/:cartonId', permission.cartonList],
|
||||
['overseas-warehouse/products', permission.productList],
|
||||
['overseas-warehouse/products/:productId', permission.productList],
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps all details as formal parameterized pages', () => {
|
||||
const details = overseasWarehouseRoutes.filter((route) => route.path.includes('/:'))
|
||||
expect(details).toHaveLength(7)
|
||||
expect(details.every((route) => route.meta.showPageHeading === false)).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps create and edit permissions independent for second-batch resources', () => {
|
||||
expect(new Set([
|
||||
permission.relabelOrderCreate, permission.relabelOrderEdit,
|
||||
permission.palletCreate, permission.palletEdit,
|
||||
permission.cartonCreate, permission.cartonEdit,
|
||||
permission.productCreate, permission.productEdit,
|
||||
]).size).toBe(8)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,95 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { overseasWarehousePermissions as permission } from './permissions'
|
||||
|
||||
export const overseasWarehouseRoutes = [
|
||||
{
|
||||
path: 'overseas-warehouse/last-mile-orders',
|
||||
name: 'overseas-last-mile-orders',
|
||||
component: () => import('./pages/LastMileOrdersView.vue'),
|
||||
meta: { title: '尾程订单', description: '管理海外接柜后的尾程订单与入仓衔接', permission: permission.lastMileOrderList },
|
||||
},
|
||||
{
|
||||
path: 'overseas-warehouse/last-mile-orders/:orderId',
|
||||
name: 'overseas-last-mile-order-detail',
|
||||
component: () => import('./pages/LastMileOrderDetailView.vue'),
|
||||
meta: { title: '尾程订单详情', description: '查看尾程订单、计划、货物和操作记录', permission: permission.lastMileOrderList, showPageHeading: false },
|
||||
},
|
||||
{
|
||||
path: 'overseas-warehouse/inbound-orders',
|
||||
name: 'overseas-inbound-orders',
|
||||
component: () => import('./pages/InboundOrdersView.vue'),
|
||||
meta: { title: '入库单', description: '管理海外合作仓入库、预约与排车衔接', permission: permission.inboundOrderList },
|
||||
},
|
||||
{
|
||||
path: 'overseas-warehouse/inbound-orders/:orderId',
|
||||
name: 'overseas-inbound-order-detail',
|
||||
component: () => import('./pages/InboundOrderDetailView.vue'),
|
||||
meta: { title: '入库单详情', description: '查看海外仓入库单与应收费用', permission: permission.inboundOrderList, showPageHeading: false },
|
||||
},
|
||||
{
|
||||
path: 'overseas-warehouse/inventory',
|
||||
name: 'overseas-inventory',
|
||||
component: () => import('./pages/InventoryView.vue'),
|
||||
meta: { title: '库存管理', description: '查看海外仓库存汇总、可用量和库内分布', permission: permission.inventoryList },
|
||||
},
|
||||
{
|
||||
path: 'overseas-warehouse/inventory-transactions',
|
||||
name: 'overseas-inventory-transactions',
|
||||
component: () => import('./pages/InventoryTransactionsView.vue'),
|
||||
meta: { title: '进销存记录', description: '查看海外仓库存的只读变动流水', permission: permission.inventoryList },
|
||||
},
|
||||
{
|
||||
path: 'overseas-warehouse/inventory/:inventoryId',
|
||||
name: 'overseas-inventory-detail',
|
||||
component: () => import('./pages/InventoryDetailView.vue'),
|
||||
meta: { title: '库存详情', description: '查看商品、库位、托盘箱号和进销存明细', permission: permission.inventoryList, showPageHeading: false },
|
||||
},
|
||||
{
|
||||
path: 'overseas-warehouse/relabel-orders',
|
||||
name: 'overseas-relabel-orders',
|
||||
component: () => import('./pages/RelabelOrdersView.vue'),
|
||||
meta: { title: '换标单', description: '管理海外仓换标作业要求与费用信息', permission: permission.relabelOrderList },
|
||||
},
|
||||
{
|
||||
path: 'overseas-warehouse/relabel-orders/:orderId',
|
||||
name: 'overseas-relabel-order-detail',
|
||||
component: () => import('./pages/RelabelOrderDetailView.vue'),
|
||||
meta: { title: '换标单详情', description: '查看换标单、作业要求与费用明细', permission: permission.relabelOrderList, showPageHeading: false },
|
||||
},
|
||||
{
|
||||
path: 'overseas-warehouse/pallets',
|
||||
name: 'overseas-pallets',
|
||||
component: () => import('./pages/PalletsView.vue'),
|
||||
meta: { title: '托盘管理', description: '管理海外仓托盘主数据与使用状态', permission: permission.palletList },
|
||||
},
|
||||
{
|
||||
path: 'overseas-warehouse/pallets/:palletId',
|
||||
name: 'overseas-pallet-detail',
|
||||
component: () => import('./pages/PalletDetailView.vue'),
|
||||
meta: { title: '托盘详情', description: '查看托盘基础信息与使用信息', permission: permission.palletList, showPageHeading: false },
|
||||
},
|
||||
{
|
||||
path: 'overseas-warehouse/cartons',
|
||||
name: 'overseas-cartons',
|
||||
component: () => import('./pages/CartonsView.vue'),
|
||||
meta: { title: '箱号管理', description: '管理海外仓箱号、商品与托盘关联', permission: permission.cartonList },
|
||||
},
|
||||
{
|
||||
path: 'overseas-warehouse/cartons/:cartonId',
|
||||
name: 'overseas-carton-detail',
|
||||
component: () => import('./pages/CartonDetailView.vue'),
|
||||
meta: { title: '箱号详情', description: '查看箱号基础信息与商品信息', permission: permission.cartonList, showPageHeading: false },
|
||||
},
|
||||
{
|
||||
path: 'overseas-warehouse/products',
|
||||
name: 'overseas-products',
|
||||
component: () => import('./pages/ProductsView.vue'),
|
||||
meta: { title: '产品库', description: '管理海外仓商品与包装信息', permission: permission.productList },
|
||||
},
|
||||
{
|
||||
path: 'overseas-warehouse/products/:productId',
|
||||
name: 'overseas-product-detail',
|
||||
component: () => import('./pages/ProductDetailView.vue'),
|
||||
meta: { title: '产品详情', description: '查看商品基础信息与包装信息', permission: permission.productList, showPageHeading: false },
|
||||
},
|
||||
] satisfies RouteRecordRaw[]
|
||||
@@ -0,0 +1,25 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const source = readFileSync(resolve(process.cwd(), 'src/modules/overseas-warehouse/types.ts'), 'utf8')
|
||||
|
||||
describe('overseas warehouse presentation contracts', () => {
|
||||
it('covers every formal list without freezing an API transport', () => {
|
||||
for (const name of [
|
||||
'LastMileOrderListItem', 'InboundOrderListItem', 'InventoryListItem',
|
||||
'RelabelOrderListItem', 'PalletListItem', 'CartonListItem',
|
||||
'ProductListItem', 'InventoryTransactionListItem',
|
||||
]) expect(source).toContain(`interface ${name}`)
|
||||
expect(source).not.toContain('axios')
|
||||
expect(source).not.toContain("from '@/shared/api/http'")
|
||||
expect(source).not.toMatch(/https?:\/\//)
|
||||
expect(source).not.toContain('tenantId')
|
||||
})
|
||||
|
||||
it('marks outbound references as read-only inventory facts', () => {
|
||||
expect(source.match(/Read-only inventory fact/g)).toHaveLength(2)
|
||||
expect(source).not.toContain('createOutbound')
|
||||
expect(source).not.toContain('updateOutbound')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Prototype-derived presentation models for the Admin pages in this module.
|
||||
* These types intentionally do not freeze overseas API URLs, pagination envelopes,
|
||||
* identifiers, timestamps, money precision, or mutation request contracts.
|
||||
*/
|
||||
export interface LastMileOrderListItem {
|
||||
orderNo: string
|
||||
warning: string
|
||||
orderType: string
|
||||
rejectedAt: string
|
||||
inboundCode: string
|
||||
owner: string
|
||||
warehouse: string
|
||||
transferWarehouse: string
|
||||
fbaWarehouse: string
|
||||
containerNo: string
|
||||
containerStatus: string
|
||||
abnormal: string
|
||||
held: string
|
||||
pallets: number
|
||||
cartons: number
|
||||
volume: number
|
||||
quantity: number
|
||||
volumeWeight: number
|
||||
portEta: string
|
||||
portAta: string
|
||||
warehouseEta: string
|
||||
inboundOrderNo: string
|
||||
/** Read-only association fact; this module does not implement the outbound workflow. */
|
||||
outboundOrderNo: string
|
||||
/** Read-only association fact; this module does not implement overseas delivery. */
|
||||
dispatchOrderNo: string
|
||||
relabelOrderNo: string
|
||||
relabelStatus: string
|
||||
consolidated: string
|
||||
status: string
|
||||
internalRemark: string
|
||||
customerRemark: string
|
||||
warehouseRemark: string
|
||||
}
|
||||
|
||||
export interface InboundOrderListItem {
|
||||
orderNo: string
|
||||
inboundCode: string
|
||||
type: string
|
||||
warehouse: string
|
||||
fbaWarehouse: string
|
||||
owner: string
|
||||
cartons: number
|
||||
volume: number
|
||||
pallets: number
|
||||
status: string
|
||||
appointmentStatus: string
|
||||
dispatchStatus: string
|
||||
}
|
||||
|
||||
export interface InventoryListItem {
|
||||
id: string
|
||||
owner: string
|
||||
warehouse: string
|
||||
sku: string
|
||||
productName: string
|
||||
onHand: number
|
||||
available: number
|
||||
held: number
|
||||
looseboard: number
|
||||
inTransit: number
|
||||
purchase: number
|
||||
status: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface RelabelOrderListItem {
|
||||
orderNo: string
|
||||
relatedOrderNo: string
|
||||
sku: string
|
||||
productName: string
|
||||
cartons: number
|
||||
budgetTotal: number
|
||||
labelRequirement: string
|
||||
relabelCost?: number
|
||||
fileName: string
|
||||
status: string
|
||||
remark: string
|
||||
}
|
||||
|
||||
export interface PalletListItem {
|
||||
code: string
|
||||
type: string
|
||||
warehouse: string
|
||||
volume: number
|
||||
purchasePrice: number
|
||||
fbaWarehouse: string
|
||||
status: string
|
||||
remark: string
|
||||
}
|
||||
|
||||
export interface CartonListItem {
|
||||
cartonNo: string
|
||||
fbaWarehouse: string
|
||||
sku: string
|
||||
productName: string
|
||||
quantity: number
|
||||
weight: number
|
||||
status: string
|
||||
palletNo: string
|
||||
inboundOrderNo: string
|
||||
/** Read-only inventory fact; this module does not implement the outbound workflow. */
|
||||
outboundOrderNo: string
|
||||
remark: string
|
||||
}
|
||||
|
||||
export interface ProductListItem {
|
||||
sku: string
|
||||
productName: string
|
||||
englishName: string
|
||||
category: string
|
||||
length: number
|
||||
width: number
|
||||
height: number
|
||||
weight: number
|
||||
}
|
||||
|
||||
export interface InventoryTransactionListItem {
|
||||
id: string
|
||||
occurredAt: string
|
||||
owner: string
|
||||
warehouse: string
|
||||
sku: string
|
||||
productName: string
|
||||
businessType: string
|
||||
businessOrderNo: string
|
||||
inboundOrderNo: string
|
||||
/** Read-only inventory fact; it is not an outbound-workflow link. */
|
||||
outboundOrderNo: string
|
||||
inboundQuantity: number
|
||||
outboundQuantity: number
|
||||
balance: number
|
||||
operator: string
|
||||
remark: string
|
||||
}
|
||||
Reference in New Issue
Block a user