Merge pull request 'Release/test' (#9) from release/test into main

Reviewed-on: https://git.buggoglobal.com/html/client-web/pulls/9
This commit was merged in pull request #9.
This commit is contained in:
2026-09-21 11:35:42 +08:00
6 changed files with 69 additions and 15 deletions
+12
View File
@@ -142,3 +142,15 @@ test('matches the updated six-action icon and motion treatment', () => {
assert.match(dashboardView, /prefers-reduced-motion: reduce/)
assert.match(dashboardView, /\.client-dashboard a:focus-visible/)
})
test('opens all six dashboard businesses through their dedicated create pages', () => {
for (const route of [
'/orders/first-leg/create',
'/overseas-warehouse/return-relabel/create',
'/cross-border/domestic-trucking/create',
'/cross-border/pallet-shipping/create',
'/cross-border/procurement/create',
'/overseas-warehouse/labeling/create',
]) assert.match(dashboardView, new RegExp(`route: '${route.replaceAll('/', '\\/')}'`))
assert.match(dashboardView, /title: '退货换标'[\s\S]*?permission: 'merchant:overseas-warehouse:return-order:create'/)
})
@@ -68,9 +68,9 @@ export interface MerchantInboundFeeSnapshot {
chargeRuleId: string
ruleCode: string
chargeRuleVersion: number
carrierRateId: string
carrierRateId: string | null
rateCode: string
carrierRateVersion: number
carrierRateVersion: number | null
customerId: string
customerName: string
originWarehouseId: string
@@ -20,9 +20,11 @@ test('protects every overseas warehouse page with its independent list permissio
test('opens labeling and return relabeling on dedicated protected create pages', () => {
assert.match(routes, /path: 'overseas-warehouse\/labeling\/create'[\s\S]*?props: \{ service: 'labeling', createPage: true \}[\s\S]*?permission: 'merchant:overseas-warehouse:relabel:create'/)
assert.match(routes, /path: 'overseas-warehouse\/return-relabel\/create'[\s\S]*?props: \{ service: 'relabel', createPage: true \}[\s\S]*?permission: 'merchant:overseas-warehouse:return-order:create'/)
assert.match(routes, /path: 'overseas-warehouse\/return-relabel\/create'[\s\S]*?ReturnOrdersView\.vue'[\s\S]*?props: \{ createPage: true \}[\s\S]*?title: '新建退货预报'[\s\S]*?permission: 'merchant:overseas-warehouse:return-order:create'/)
assert.match(services, /props\.createPage \? 'create' : 'list'/)
assert.match(services, /function returnToList\(\)/)
assert.match(returns, /props\.createPage/)
assert.match(returns, /class="return-create-surface"/)
})
test('opens warehouse service and return receipt details as dedicated protected workspace pages', () => {
+2 -2
View File
@@ -24,8 +24,8 @@ export const overseasWarehouseModule = {
},
{
path: 'overseas-warehouse/return-relabel/create', name: 'overseas-return-relabel-create',
component: () => import('./views/WarehouseServiceView.vue'), props: { service: 'relabel', createPage: true }, release: 'active',
meta: { title: '新建退货换标单', workspaceTitle: '退货换标 · 新建', description: '填写并提交退货换标订单', permission: 'merchant:overseas-warehouse:return-order:create' },
component: () => import('./views/ReturnOrdersView.vue'), props: { createPage: true }, release: 'active',
meta: { title: '新建退货预报', workspaceTitle: '退货预报 · 新建', description: '填写并提交海外仓退货预报', permission: 'merchant:overseas-warehouse:return-order:create', keepAlive: false },
},
{
path: 'overseas-warehouse/return-relabel/:returnNo/detail', name: 'overseas-return-relabel-detail',
@@ -27,9 +27,13 @@ test('新建退货预报选择海外仓后带出联系人和电话', () => {
assert.match(view, /warehouseContactPhone: form\.warehouseContactPhone\.trim\(\)/)
})
test('新建退货预报使用加宽的双列表单弹窗', () => {
test('新建退货预报使用独立路由页面,编辑仍保留加宽弹框', () => {
assert.match(view, /defineProps<\{ createPage\?: boolean; detailPage\?: boolean/)
assert.match(view, /name: 'overseas-return-relabel-create'/)
assert.match(view, /v-if="props\.createPage"/)
assert.match(view, /class="return-create-surface"/)
assert.match(view, /<h2>新建退货预报<\/h2>/)
assert.match(view, /:title="editingReturnNo \? '编辑退货预报' : '新建退货预报'" width="1120px"/)
assert.match(view, /<el-form label-width="190px" class="return-create-form">/)
assert.match(view, /return-create-form \{ display:grid; grid-template-columns:repeat\(2,minmax\(0,1fr\)\)/)
})
@@ -10,7 +10,7 @@ import { useAuthStore } from '@/modules/auth/public'
import { getMerchantWarehouses, type MerchantWarehouse } from '@/modules/first-leg/public'
import { createReturnOrder, downloadReturnPhoto, getReturnOrder, listReturnOrders, updateReturnOrder, type ReturnOrderDetail, type ReturnOrderSummary, type ReturnReceipt } from '../api/returnOrders'
const props = withDefaults(defineProps<{ detailPage?: boolean; returnNo?: string }>(), { detailPage: false, returnNo: '' })
const props = withDefaults(defineProps<{ createPage?: boolean; detailPage?: boolean; returnNo?: string }>(), { createPage: false, detailPage: false, returnNo: '' })
const router = useRouter()
const route = useRoute()
const detailReturnNo = computed(() => props.returnNo || String(route.params.returnNo || ''))
@@ -110,10 +110,7 @@ async function loadOperationWarehouses() {
} finally { operationWarehousesLoading.value = false }
}
function openCreate() {
editingReturnNo.value = ''
resetForm()
showCreate.value = true
if (!operationWarehouses.value.length && !operationWarehousesLoading.value) void loadOperationWarehouses()
void router.push({ name: 'overseas-return-relabel-create' })
}
async function openEdit(returnNo: string) {
const detail = await getReturnOrder(returnNo)
@@ -176,6 +173,7 @@ function previewPhoto(fileId: string | number) {
}
function openDetail(returnNo: string) { void router.push({ name: 'overseas-return-relabel-detail', params: { returnNo } }) }
function returnToList() { void router.push({ name: 'overseas-return-relabel' }) }
function cancelCreate() { resetForm(); void router.push({ name: 'overseas-return-relabel' }) }
function canApplyRelabel(order: { status?: unknown; needRelabel?: unknown; relabelNo?: unknown }) { return order.status === 'COMPLETED' && order.needRelabel === true && !order.relabelNo }
function openRelabel(order: Pick<ReturnOrderSummary, 'returnNo' | 'overseasWarehouseId'>) {
void router.push({ name: 'overseas-labeling-create', query: { returnNo: order.returnNo, warehouseId: String(order.overseasWarehouseId) } })
@@ -218,19 +216,48 @@ function formatDateTime(value?: string | null) {
return new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).format(date)
}
onMounted(() => {
if (props.detailPage) void Promise.all([loadDetail(), loadOperationWarehouses()])
if (props.createPage) { resetForm(); void loadOperationWarehouses() }
else if (props.detailPage) void Promise.all([loadDetail(), loadOperationWarehouses()])
else void Promise.all([load(), loadOperationWarehouses()])
})
watch(() => [query.page, query.size] as const, ([nextPage, nextSize], [previousPage, previousSize]) => {
if (nextSize !== previousSize && nextPage !== 1) { query.page = 1; return }
if (!props.detailPage) void load()
if (!props.createPage && !props.detailPage) void load()
})
onBeforeUnmount(clearPhotoUrls)
</script>
<template>
<section class="return-orders-page">
<template v-if="!props.detailPage">
<template v-if="props.createPage">
<article class="return-create-surface">
<header class="return-create-header">
<div><h2>新建退货预报</h2><p>填写预计退回的 SKU 和数量,仓库签收后将自动更新实收结果。</p></div>
<el-button @click="returnToList"><ArrowLeft :size="16" />返回列表</el-button>
</header>
<div class="return-create-body">
<el-form label-width="150px" class="return-create-form">
<el-form-item label="海外仓" required class="return-form-wide"><el-select v-model="form.overseasWarehouseId" filterable clearable :loading="operationWarehousesLoading" :disabled="Boolean(operationWarehousesError)" placeholder="请选择海外仓" @change="selectOperationWarehouse"><el-option v-for="warehouse in operationWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="String(warehouse.id)" /></el-select><div v-if="operationWarehousesError" class="warehouse-error">{{ operationWarehousesError }}</div></el-form-item>
<el-form-item label="海外仓联系人" required><el-input v-model="form.warehouseContactName" :readonly="Boolean(selectedOperationWarehouse?.contactName)" :placeholder="selectedOperationWarehouse?.contactName ? '已从海外仓资料带出' : '该海外仓未维护,请手动填写'" /></el-form-item>
<el-form-item label="海外仓联系电话" required><el-input v-model="form.warehouseContactPhone" :readonly="Boolean(selectedOperationWarehouse?.contactPhone)" :placeholder="selectedOperationWarehouse?.contactPhone ? '已从海外仓资料带出' : '该海外仓未维护,请手动填写'" /></el-form-item>
<el-form-item label="客户参考号" class="return-form-wide"><el-input v-model="form.customerReferenceNo" placeholder="可填写客户侧参考号" /></el-form-item>
<el-form-item label="备注" class="return-form-wide"><el-input v-model="form.remark" type="textarea" :rows="3" placeholder="填写退货说明(选填)" /></el-form-item>
</el-form>
<div class="items-title"><span>SKU 预报明细</span><el-button text type="primary" @click="addItem"><Plus :size="15" />添加 SKU</el-button></div>
<div class="return-relabel-option"><span>换标</span><el-checkbox v-model="form.needRelabel">需要换标</el-checkbox></div>
<div class="items-hint">客户只需填写预计退回的 SKU 和数量;快递单号由仓库签收时按实际包裹扫描绑定。</div>
<el-table :data="form.items" row-key="id" size="small" border>
<el-table-column label="操作" width="80" fixed="left"><template #default="{ $index }"><el-button link type="danger" :disabled="form.items.length === 1" @click="removeItem($index)">删除</el-button></template></el-table-column>
<el-table-column label="SKU" min-width="240"><template #default="{ row }"><div><el-input v-model="row.skuCode" :class="{ 'duplicate-sku-input': duplicateSkuIds.has(row.id) }" placeholder="填写 SKU" @blur="row.skuCode = row.skuCode.trim()" /><div v-if="duplicateSkuIds.has(row.id)" class="duplicate-sku-error">该 SKU 已在明细中填写</div></div></template></el-table-column>
<el-table-column label="中文品名" min-width="320"><template #default="{ row }"><el-input v-model="row.productNameZh" placeholder="填写中文品名(选填)" /></template></el-table-column>
<el-table-column label="预报数量" width="180"><template #default="{ row }"><el-input-number v-model="row.expectedQuantity" :min="1" controls-position="right" placeholder="数量" /></template></el-table-column>
</el-table>
</div>
<footer class="return-create-actions"><el-button @click="cancelCreate">取消</el-button><el-button v-if="canCreate" type="primary" :loading="creating" @click="submit">提交预报</el-button></footer>
</article>
</template>
<template v-else-if="!props.detailPage">
<QueryToolbar v-model="queryModel" layout="grid" ariaLabel="退货签收筛选" :fields="queryFields" @search="search" />
<div class="return-table-card">
<DataTable v-model:page="query.page" v-model:page-size="query.size" :data="rows" :columns="tableColumns" :loading="loading" :total="total" :page-sizes="[15, 30, 50, 100]" :row-actions="rowActions" row-key="returnNo" empty-text="暂无退货预报">
@@ -330,6 +357,15 @@ onBeforeUnmount(clearPhotoUrls)
<style scoped>
.return-orders-page { display:grid; gap:12px; } .return-create-form { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); column-gap:20px; } .return-form-wide { grid-column:1 / -1; } .items-title { display:flex; justify-content:space-between; align-items:center; margin:16px 0 4px; font-weight:600; } .items-hint { margin-bottom:10px; color:var(--el-text-color-secondary); font-size:12px; } h3 { margin:24px 0 10px; font-size:16px; } h4 { margin:16px 0 8px; font-size:14px; } .remark { white-space:pre-wrap; } .warehouse-error, .duplicate-sku-error { margin-top:6px; color:var(--el-color-danger); font-size:12px; } :deep(.duplicate-sku-input .el-input__wrapper) { box-shadow:0 0 0 1px var(--el-color-danger) inset; } :deep(.el-dialog .el-select) { width:100%; }
.return-create-surface { overflow:hidden; border:1px solid var(--client-border); border-radius:var(--client-radius); background:#fff; }
.return-create-header { display:flex; min-height:68px; align-items:center; justify-content:space-between; gap:20px; padding:12px 20px; border-bottom:1px solid var(--client-border); }
.return-create-header h2 { margin:0; color:#1f2d45; font-size:20px; }
.return-create-header p { margin:4px 0 0; color:#7c899c; font-size:12px; }
.return-create-body { padding:18px 20px 14px; }
.return-create-body :deep(.el-select), .return-create-body :deep(.el-input-number) { width:100%; }
.return-create-body .items-title { margin-top:8px; }
.return-relabel-option { display:flex; align-items:center; gap:16px; margin:8px 0; color:#526176; }
.return-create-actions { display:flex; align-items:center; justify-content:flex-end; gap:10px; padding:12px 20px; border-top:1px solid var(--client-border); background:#fbfcfe; }
.return-table-card { overflow:hidden; border:1px solid var(--client-border); border-radius:var(--client-radius); background:#fff; box-shadow:none; }
.return-table-card :deep(.el-table) { width:100%; }
.return-status--pending-review { --el-tag-bg-color:#fff7e6; --el-tag-border-color:#f4c26b; --el-tag-text-color:#a86100; }