优化包裹多商品列表与明细侧窗,修复状态Tab筛选,并补充到仓页与插件确认页。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
f5011edde6
commit
8e6f0d45ea
@ -163,6 +163,7 @@
|
||||
"useCssVar": true,
|
||||
"useCssVars": true,
|
||||
"useCurrentElement": true,
|
||||
"useCurrentWarehouseStore": true,
|
||||
"useCycleList": true,
|
||||
"useDark": true,
|
||||
"useDateFormat": true,
|
||||
@ -294,6 +295,7 @@
|
||||
"useVibrate": true,
|
||||
"useVirtualList": true,
|
||||
"useWakeLock": true,
|
||||
"useWarehouseAdminCustomer": true,
|
||||
"useWarehouseList": true,
|
||||
"useWebNotification": true,
|
||||
"useWebSocket": true,
|
||||
|
||||
@ -102,6 +102,9 @@ server {
|
||||
location ^~ /operation/printLabel {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /operation/parcelArrival {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
|
||||
# ============ 订单前端路由(^~ 须比下方 /order/ API 正则代理更优先)============
|
||||
location ^~ /order/priceCheck {
|
||||
@ -144,6 +147,11 @@ server {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
|
||||
# 采集插件确认页(/Plugin/AddPackage);API 走 /plugin/browser/*
|
||||
location ^~ /Plugin/ {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
|
||||
# ============ 系统管理前端路由(^~ 须比下方 /system/ API 正则代理更优先)============
|
||||
# 页面路径为 camelCase;API 为 kebab(如 /system/dict/list、/system/batch-email/…),勿用宽泛前缀匹配 dict
|
||||
location = /system/dict {
|
||||
@ -222,7 +230,7 @@ server {
|
||||
}
|
||||
|
||||
# ============ Admin API 同源反代(/rbac /order /xhs … → api.nanafa.com/admin)============
|
||||
location ~ ^/(rbac|order|monitor|customer|recharge|channel|system|warehouse)/ {
|
||||
location ~ ^/(rbac|order|monitor|customer|recharge|channel|system|warehouse|plugin)/ {
|
||||
rewrite ^/(.*)$ /admin/$1 break;
|
||||
proxy_pass http://api.nanafa.com;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
@ -39,6 +39,7 @@ server {
|
||||
location ^~ /operation/inboundScan { try_files /index.html =200; }
|
||||
location ^~ /operation/outboundScan { try_files /index.html =200; }
|
||||
location ^~ /operation/printLabel { try_files /index.html =200; }
|
||||
location ^~ /operation/parcelArrival { try_files /index.html =200; }
|
||||
|
||||
# 订单前端路由(^~ 须比下方 /order/ API 正则代理更优先)
|
||||
location ^~ /order/priceCheck { try_files /index.html =200; }
|
||||
@ -54,6 +55,7 @@ server {
|
||||
location = /warehouse/sku { try_files /index.html =200; }
|
||||
location = /warehouse/binLocation { try_files /index.html =200; }
|
||||
location ^~ /warehouse/inboundNotice { try_files /index.html =200; }
|
||||
location ^~ /Plugin/ { try_files /index.html =200; }
|
||||
|
||||
location ^~ /channel/salesProducts { try_files /index.html =200; }
|
||||
location ^~ /channel/serviceChannels { try_files /index.html =200; }
|
||||
@ -109,7 +111,7 @@ server {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location ~ ^/(rbac|order|monitor|customer|recharge|channel|system|warehouse)/ {
|
||||
location ~ ^/(rbac|order|monitor|customer|recharge|channel|system|warehouse|plugin)/ {
|
||||
rewrite ^/(.*)$ /admin/$1 break;
|
||||
proxy_pass http://api.nanafa.com;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
24
src/api/plugin/index.ts
Normal file
24
src/api/plugin/index.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { axiosInstance } from '@/api/rbac'
|
||||
import { assertBusinessOk, omsUrl } from '@/api/oms/common'
|
||||
|
||||
function unwrapData<T = any>(body: any): T {
|
||||
assertBusinessOk(body)
|
||||
// 兼容 {code,data} 包裹,以及直接返回 JSON 字符串/对象
|
||||
if (body != null && typeof body === 'object' && ('data' in body || 'Data' in body)) {
|
||||
const nested = body.data ?? body.Data
|
||||
if (nested !== undefined) return nested as T
|
||||
}
|
||||
return body as T
|
||||
}
|
||||
|
||||
/** 读取采集草稿(key 来自插件 add) */
|
||||
export async function getPluginPackage(key: string) {
|
||||
const res = await axiosInstance.get(omsUrl('/plugin/browser/getPackage'), { params: { key } })
|
||||
return unwrapData<string>(res.data)
|
||||
}
|
||||
|
||||
/** 插件确认页默认仓库 */
|
||||
export async function getPluginWhseInfo() {
|
||||
const res = await axiosInstance.get(omsUrl('/plugin/browser/getPluginWhseInfo'))
|
||||
return unwrapData<{ id: number; name: string } | null>(res.data)
|
||||
}
|
||||
@ -25,6 +25,18 @@ export const deleteWarehouseSku = (id: number) => postOk('/warehouse/sku/delete'
|
||||
export const bindWarehouseSkuMapping = (body: Record<string, unknown>) =>
|
||||
postOk('/warehouse/sku/bind-mapping', body)
|
||||
|
||||
/** 上传 SKU 图片,返回相对路径 url */
|
||||
export async function uploadWarehouseSkuImage(file: File): Promise<string> {
|
||||
const form = new FormData()
|
||||
form.append('file', file)
|
||||
const res = await axiosInstance.post(omsUrl('/warehouse/sku/upload-image'), form)
|
||||
assertBusinessOk(res.data)
|
||||
const data = unwrapData<any>(res.data)
|
||||
const url = data?.url ?? data?.fileUrl ?? data?.Url ?? data?.FileUrl
|
||||
if (!url) throw new Error('上传成功但未返回图片地址')
|
||||
return String(url)
|
||||
}
|
||||
|
||||
// Bin location
|
||||
export const getWarehouseBinLocationList = (body: Record<string, unknown>) =>
|
||||
postList('/warehouse/bin-location/list', body).then((r) => ({ items: r.items, total: r.items.length }))
|
||||
@ -48,6 +60,14 @@ export const saveWarehouseParcel = (body: Record<string, unknown>) => postOk('/w
|
||||
export const deleteWarehouseParcel = (id: number) => postOk('/warehouse/parcel/delete', { id })
|
||||
export const putawayWarehouseParcel = (body: Record<string, unknown>) => postOk('/warehouse/parcel/putaway', body)
|
||||
|
||||
// Parcel arrival(快递到仓)
|
||||
export const getWarehouseParcelArrivalList = (body: Record<string, unknown>) =>
|
||||
postList('/warehouse/parcel-arrival/list', body)
|
||||
export const createWarehouseParcelArrival = (body: Record<string, unknown>) =>
|
||||
postOk('/warehouse/parcel-arrival/create', body)
|
||||
export const deleteWarehouseParcelArrival = (id: number) =>
|
||||
postOk('/warehouse/parcel-arrival/delete', { id })
|
||||
|
||||
// Inventory
|
||||
export const getWarehouseInventoryList = (body: Record<string, unknown>) =>
|
||||
postList('/warehouse/inventory/list', body)
|
||||
|
||||
4
src/auto-imports.d.ts
vendored
4
src/auto-imports.d.ts
vendored
@ -151,6 +151,7 @@ declare global {
|
||||
const useCssVar: typeof import('@vueuse/core')['useCssVar']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
|
||||
const useCurrentWarehouseStore: typeof import('./store/index')['useCurrentWarehouseStore']
|
||||
const useCycleList: typeof import('@vueuse/core')['useCycleList']
|
||||
const useDark: typeof import('@vueuse/core')['useDark']
|
||||
const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
|
||||
@ -282,6 +283,7 @@ declare global {
|
||||
const useVibrate: typeof import('@vueuse/core')['useVibrate']
|
||||
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
|
||||
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
|
||||
const useWarehouseAdminCustomer: typeof import('./composables/useWarehouseAdminCustomer')['useWarehouseAdminCustomer']
|
||||
const useWarehouseList: typeof import('./composables/useWarehouseList')['useWarehouseList']
|
||||
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
|
||||
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
|
||||
@ -469,6 +471,7 @@ declare module 'vue' {
|
||||
readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
|
||||
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
|
||||
readonly useCurrentElement: UnwrapRef<typeof import('@vueuse/core')['useCurrentElement']>
|
||||
readonly useCurrentWarehouseStore: UnwrapRef<typeof import('./store/index')['useCurrentWarehouseStore']>
|
||||
readonly useCycleList: UnwrapRef<typeof import('@vueuse/core')['useCycleList']>
|
||||
readonly useDark: UnwrapRef<typeof import('@vueuse/core')['useDark']>
|
||||
readonly useDateFormat: UnwrapRef<typeof import('@vueuse/core')['useDateFormat']>
|
||||
@ -600,6 +603,7 @@ declare module 'vue' {
|
||||
readonly useVibrate: UnwrapRef<typeof import('@vueuse/core')['useVibrate']>
|
||||
readonly useVirtualList: UnwrapRef<typeof import('@vueuse/core')['useVirtualList']>
|
||||
readonly useWakeLock: UnwrapRef<typeof import('@vueuse/core')['useWakeLock']>
|
||||
readonly useWarehouseAdminCustomer: UnwrapRef<typeof import('./composables/useWarehouseAdminCustomer')['useWarehouseAdminCustomer']>
|
||||
readonly useWarehouseList: UnwrapRef<typeof import('./composables/useWarehouseList')['useWarehouseList']>
|
||||
readonly useWebNotification: UnwrapRef<typeof import('@vueuse/core')['useWebNotification']>
|
||||
readonly useWebSocket: UnwrapRef<typeof import('@vueuse/core')['useWebSocket']>
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { getWarehouseList } from '@/api/system/warehouse'
|
||||
import { useCurrentWarehouseStore } from '@/store'
|
||||
|
||||
const props = withDefaults(
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
variant?: 'navbar' | 'login'
|
||||
}>(),
|
||||
@ -13,32 +14,24 @@ const props = withDefaults(
|
||||
)
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const serviceWarehouseListLoading = ref(false)
|
||||
const serviceWarehouseSelectData = ref<Array<{ label: string; value: number | string }>>([])
|
||||
const serviceWarehouseSelect = ref<number | string | undefined>(undefined)
|
||||
const warehouseStore = useCurrentWarehouseStore()
|
||||
|
||||
async function loadServiceDataList() {
|
||||
serviceWarehouseListLoading.value = true
|
||||
try {
|
||||
const { items } = await getWarehouseList({
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
})
|
||||
const list = items ?? []
|
||||
serviceWarehouseSelectData.value = list
|
||||
const list = (items ?? [])
|
||||
.filter((row) => row.id != null)
|
||||
.map((row) => ({
|
||||
label: row.warehouseName || String(row.id),
|
||||
value: row.id as number | string,
|
||||
value: Number(row.id),
|
||||
}))
|
||||
serviceWarehouseSelect.value = serviceWarehouseSelectData.value[0]?.value
|
||||
warehouseStore.setOptions(list)
|
||||
} catch (error) {
|
||||
console.error('Failed to load service warehouse list', error)
|
||||
serviceWarehouseSelectData.value = []
|
||||
serviceWarehouseSelect.value = undefined
|
||||
} finally {
|
||||
serviceWarehouseListLoading.value = false
|
||||
warehouseStore.setOptions([])
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,15 +43,15 @@ onMounted(() => {
|
||||
<template>
|
||||
<div class="contact-service" :class="`contact-service--${variant}`">
|
||||
<a-select
|
||||
v-model="serviceWarehouseSelect"
|
||||
:model-value="warehouseStore.warehouseId"
|
||||
class="contact-service__select"
|
||||
:loading="serviceWarehouseListLoading"
|
||||
:placeholder="t('nav.contactService.selectWarehouse')"
|
||||
popup-container="body"
|
||||
allow-search
|
||||
@change="(v: any) => warehouseStore.setWarehouseId(v)"
|
||||
>
|
||||
<a-option
|
||||
v-for="opt in serviceWarehouseSelectData"
|
||||
v-for="opt in warehouseStore.options"
|
||||
:key="String(opt.value)"
|
||||
:value="opt.value"
|
||||
>
|
||||
|
||||
@ -140,6 +140,8 @@
|
||||
const emit = defineEmits<{
|
||||
'update:selectedRows': [rows: any[]];
|
||||
'action-click': [row: any, rowIndex: number, action?: string];
|
||||
/** 数据列单击(非操作/复选框列) */
|
||||
'cell-click': [payload: { row: any; rowIndex: number; columnKey: string; event: MouseEvent }];
|
||||
'search': [];
|
||||
'selection-change': [rows: any[]];
|
||||
'checkbox-change': [row: any, checked: boolean, rowIndex: number];
|
||||
@ -869,6 +871,8 @@
|
||||
// 列定义中的渲染器
|
||||
if (colDef.renderer) {
|
||||
td.innerHTML = colDef.renderer(rowData, value);
|
||||
// 允许单元格内角标(如「多」)不被 td overflow 裁切
|
||||
td.style.overflow = 'visible';
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -985,13 +989,55 @@
|
||||
rowIndex < props.data.length &&
|
||||
colIndex !== checkboxColIndex &&
|
||||
colIndex !== actionColStartIndex &&
|
||||
colIndex !== actionColEndIndex
|
||||
colIndex !== actionColEndIndex &&
|
||||
colIndex !== serialColIndex
|
||||
) {
|
||||
const row = props.data[rowIndex];
|
||||
if (row) {
|
||||
emit('action-click', row, rowIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// 数据列单击 → cell-click(供业务页打开明细浮层等)
|
||||
if (
|
||||
!isDoubleClick &&
|
||||
rowIndex != null &&
|
||||
rowIndex >= 0 &&
|
||||
rowIndex < props.data.length &&
|
||||
colIndex != null &&
|
||||
colIndex !== checkboxColIndex &&
|
||||
colIndex !== actionColStartIndex &&
|
||||
colIndex !== actionColEndIndex &&
|
||||
colIndex !== serialColIndex
|
||||
) {
|
||||
const target = event?.target as HTMLElement | null;
|
||||
if (
|
||||
target &&
|
||||
(target.closest('input,button,a,select,textarea') ||
|
||||
target.closest('.is-finish-switch-btn') ||
|
||||
target.closest('.customer-status-switch'))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let dataColIndex = colIndex - colOffset;
|
||||
if (props.actionColumnPosition === 'beforeLast' && actionColEndIndex >= 0) {
|
||||
if (colIndex > actionColEndIndex) {
|
||||
dataColIndex = colIndex - colOffset - 1;
|
||||
}
|
||||
}
|
||||
if (dataColIndex >= 0 && dataColIndex < effectiveColumns.value.length) {
|
||||
const colDef = effectiveColumns.value[dataColIndex];
|
||||
const row = props.data[rowIndex];
|
||||
if (colDef?.key && row) {
|
||||
emit('cell-click', {
|
||||
row,
|
||||
rowIndex,
|
||||
columnKey: colDef.key,
|
||||
event: event as MouseEvent,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
afterRender: function(isForced: boolean) {
|
||||
if (!hotInstance) return;
|
||||
|
||||
@ -452,6 +452,7 @@ const menuNameToLocaleKeyMap: Record<string, string> = {
|
||||
'入库扫描': 'menu.operation.inboundScan',
|
||||
'出库扫描': 'menu.operation.outboundScan',
|
||||
'打印标签': 'menu.operation.printLabel',
|
||||
'快递到仓': 'menu.operation.parcelArrival',
|
||||
'API': 'menu.api',
|
||||
'上游接口': 'menu.api.upstreamInterface',
|
||||
'下游接口': 'menu.api.downstreamInterface',
|
||||
|
||||
@ -261,6 +261,10 @@ export const PAGE_BUTTON_PARENT_ALIASES: Record<string, string> = {
|
||||
'Warehouse.Parcel.Query': 'Warehouse.Parcel',
|
||||
'Warehouse.InboundNotice.Query': 'Warehouse.InboundNotice',
|
||||
'Warehouse.BinLocation.Query': 'Warehouse.BinLocation',
|
||||
'Operation.InboundScan.Query': 'Operation.InboundScan',
|
||||
'Operation.OutboundScan.Query': 'Operation.OutboundScan',
|
||||
'Operation.PrintLabel.Query': 'Operation.PrintLabel',
|
||||
'Operation.ParcelArrival.Query': 'Operation.ParcelArrival',
|
||||
};
|
||||
|
||||
type BtnInput = { code: string; name: string };
|
||||
@ -642,6 +646,11 @@ const CORE_GROUPS: PageButtonGroup[] = [
|
||||
{ code: 'Reset', name: '重置' },
|
||||
{ code: 'Print', name: '打印' },
|
||||
]),
|
||||
B('Operation.ParcelArrival', [
|
||||
{ code: 'Query', name: '查询' },
|
||||
{ code: 'Save', name: '到仓' },
|
||||
{ code: 'Delete', name: '删除' },
|
||||
]),
|
||||
|
||||
// ── 客户 / 用户 ──
|
||||
B('User.Recharge', [
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch } from 'vue';
|
||||
import { computed, onMounted, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useAppStore } from '@/store';
|
||||
import TopNav from '@/components/top-nav/index.vue';
|
||||
@ -24,12 +24,17 @@
|
||||
import usePermission from '@/hooks/permission';
|
||||
import useResponsive from '@/hooks/responsive';
|
||||
import PageLayout from './page-layout.vue';
|
||||
import { setRapidPackageHostCookie } from '@/utils/rapidPackagePluginHost';
|
||||
|
||||
const appStore = useAppStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const permission = usePermission();
|
||||
useResponsive(true);
|
||||
|
||||
onMounted(() => {
|
||||
setRapidPackageHostCookie();
|
||||
});
|
||||
|
||||
const footer = computed(() => appStore.footer);
|
||||
|
||||
|
||||
@ -117,7 +117,15 @@ export default {
|
||||
'warehouse.sku.form.skuName': 'SKU Name',
|
||||
'warehouse.sku.form.weightG': 'Weight(g)',
|
||||
'warehouse.sku.form.remark': 'Remark',
|
||||
'warehouse.sku.form.imageUrl': 'Image URL',
|
||||
'warehouse.sku.form.imageUrl': 'SKU image',
|
||||
'warehouse.sku.form.uploadImage': 'Upload image',
|
||||
'warehouse.sku.form.reuploadImage': 'Replace image',
|
||||
'warehouse.sku.form.clearImage': 'Clear image',
|
||||
'warehouse.sku.message.uploadSuccess': 'Image uploaded',
|
||||
'warehouse.sku.message.uploadFailed': 'Image upload failed',
|
||||
'warehouse.sku.message.imageTooLarge': 'Image must be under 5MB',
|
||||
'warehouse.sku.form.currentWarehouse': 'Current warehouse',
|
||||
'warehouse.common.noCurrentWarehouse': 'Please select a warehouse in the top-right first',
|
||||
'warehouse.sku.form.dimensions': 'Dimensions',
|
||||
'warehouse.sku.form.length': 'L',
|
||||
'warehouse.sku.form.width': 'W',
|
||||
@ -196,6 +204,14 @@ export default {
|
||||
'warehouse.parcel.status.stored': 'Stored',
|
||||
'warehouse.parcel.table.warehouse': 'Warehouse',
|
||||
'warehouse.parcel.table.items': 'Items',
|
||||
'warehouse.parcel.table.attribute': 'Attributes',
|
||||
'warehouse.parcel.table.quantity': 'Qty',
|
||||
'warehouse.parcel.badge.multiTip': 'Multi-item parcel. Click Items / Attributes / Qty for details.',
|
||||
'warehouse.parcel.drawer.itemsTitle': 'Item details',
|
||||
'warehouse.parcel.drawer.totalQty': 'Total qty',
|
||||
'warehouse.parcel.drawer.itemCount': '{n} line(s)',
|
||||
'warehouse.parcel.drawer.edit': 'Edit parcel',
|
||||
'warehouse.parcel.drawer.empty': 'No items',
|
||||
'warehouse.parcel.table.arrivalTime': 'Arrival Time',
|
||||
'warehouse.parcel.table.createTime': 'Created At',
|
||||
'warehouse.parcel.table.trackingNo': 'Tracking No',
|
||||
@ -207,6 +223,7 @@ export default {
|
||||
'warehouse.parcel.form.trackingNo': 'Tracking No',
|
||||
'warehouse.parcel.form.carrierName': 'Carrier',
|
||||
'warehouse.parcel.form.productName': 'Product',
|
||||
'warehouse.parcel.form.attribute': 'Attributes',
|
||||
'warehouse.parcel.form.quantity': 'Qty',
|
||||
'warehouse.parcel.form.remark': 'Remark',
|
||||
'warehouse.parcel.form.itemRemark': 'Item remark',
|
||||
@ -220,6 +237,21 @@ export default {
|
||||
'warehouse.parcel.message.deleteSuccess': 'Deleted',
|
||||
'warehouse.parcel.message.required': 'Warehouse and tracking no required',
|
||||
'warehouse.parcel.message.itemsRequired': 'Add at least one product',
|
||||
'warehouse.parcelArrival.form.trackingNo': 'Domestic tracking No.',
|
||||
'warehouse.parcelArrival.form.placeholder.scanOrInput': 'Scan or enter domestic tracking number',
|
||||
'warehouse.parcelArrival.search.trackingNo': 'Domestic tracking No.',
|
||||
'warehouse.parcelArrival.table.trackingNo': 'Domestic tracking No.',
|
||||
'warehouse.parcelArrival.table.warehouse': 'Warehouse',
|
||||
'warehouse.parcelArrival.table.createTime': 'Created at',
|
||||
'warehouse.parcelArrival.table.createBy': 'Created by',
|
||||
'warehouse.parcelArrival.actions.reset': 'Reset',
|
||||
'warehouse.parcelArrival.actions.arrive': 'Confirm arrival',
|
||||
'warehouse.parcelArrival.actions.delete': 'Delete arrival record',
|
||||
'warehouse.parcelArrival.message.loadFailed': 'Failed to load arrival records',
|
||||
'warehouse.parcelArrival.message.trackingRequired': 'Please scan or enter the tracking number',
|
||||
'warehouse.parcelArrival.message.arriveSuccess': 'Arrived. Parcel status updated to arrived',
|
||||
'warehouse.parcelArrival.message.arriveFailed': 'Failed to mark arrival',
|
||||
'warehouse.parcelArrival.message.deleteSuccess': 'Deleted',
|
||||
'warehouse.inbound.status.all': 'All',
|
||||
'warehouse.inbound.status.forecasted': 'Forecasted',
|
||||
'warehouse.inbound.status.completed': 'Completed',
|
||||
@ -391,6 +423,7 @@ export default {
|
||||
'menu.operation.inboundScan': 'Inbound Scan',
|
||||
'menu.operation.outboundScan': 'Outbound Scan',
|
||||
'menu.operation.printLabel': 'Print Label',
|
||||
'menu.operation.parcelArrival': 'Parcel Arrival',
|
||||
'menu.api': 'API',
|
||||
'menu.api.upstreamInterface': 'Upstream Interface',
|
||||
'menu.api.downstreamInterface': 'Downstream Interface',
|
||||
|
||||
@ -117,7 +117,15 @@ export default {
|
||||
'warehouse.sku.form.skuName': 'SKU 名称',
|
||||
'warehouse.sku.form.weightG': '重量(g)',
|
||||
'warehouse.sku.form.remark': '备注',
|
||||
'warehouse.sku.form.imageUrl': '图片地址',
|
||||
'warehouse.sku.form.imageUrl': 'SKU 图片',
|
||||
'warehouse.sku.form.uploadImage': '上传图片',
|
||||
'warehouse.sku.form.reuploadImage': '重新上传',
|
||||
'warehouse.sku.form.clearImage': '清除图片',
|
||||
'warehouse.sku.message.uploadSuccess': '图片上传成功',
|
||||
'warehouse.sku.message.uploadFailed': '图片上传失败',
|
||||
'warehouse.sku.message.imageTooLarge': '图片不能超过 5MB',
|
||||
'warehouse.sku.form.currentWarehouse': '当前仓库',
|
||||
'warehouse.common.noCurrentWarehouse': '请先在右上角选择仓库',
|
||||
'warehouse.sku.form.dimensions': '尺寸',
|
||||
'warehouse.sku.form.length': '长',
|
||||
'warehouse.sku.form.width': '宽',
|
||||
@ -196,6 +204,14 @@ export default {
|
||||
'warehouse.parcel.status.stored': '已上架',
|
||||
'warehouse.parcel.table.warehouse': '仓库',
|
||||
'warehouse.parcel.table.items': '商品信息',
|
||||
'warehouse.parcel.table.attribute': '属性',
|
||||
'warehouse.parcel.table.quantity': '数量',
|
||||
'warehouse.parcel.badge.multiTip': '多商品包裹,点击商品/属性/数量可查看明细',
|
||||
'warehouse.parcel.drawer.itemsTitle': '商品明细',
|
||||
'warehouse.parcel.drawer.totalQty': '合计数量',
|
||||
'warehouse.parcel.drawer.itemCount': '共 {n} 项',
|
||||
'warehouse.parcel.drawer.edit': '编辑包裹',
|
||||
'warehouse.parcel.drawer.empty': '暂无商品明细',
|
||||
'warehouse.parcel.table.arrivalTime': '到仓时间',
|
||||
'warehouse.parcel.table.createTime': '创建时间',
|
||||
'warehouse.parcel.table.trackingNo': '快递单号',
|
||||
@ -207,6 +223,7 @@ export default {
|
||||
'warehouse.parcel.form.trackingNo': '快递单号',
|
||||
'warehouse.parcel.form.carrierName': '快递公司',
|
||||
'warehouse.parcel.form.productName': '商品名称',
|
||||
'warehouse.parcel.form.attribute': '属性',
|
||||
'warehouse.parcel.form.quantity': '数量',
|
||||
'warehouse.parcel.form.remark': '备注',
|
||||
'warehouse.parcel.form.itemRemark': '商品备注',
|
||||
@ -220,6 +237,21 @@ export default {
|
||||
'warehouse.parcel.message.deleteSuccess': '删除成功',
|
||||
'warehouse.parcel.message.required': '请填写仓库和快递单号',
|
||||
'warehouse.parcel.message.itemsRequired': '请至少填写一条商品信息',
|
||||
'warehouse.parcelArrival.form.trackingNo': '国内快递单号',
|
||||
'warehouse.parcelArrival.form.placeholder.scanOrInput': '扫描或输入国内快递单号',
|
||||
'warehouse.parcelArrival.search.trackingNo': '国内快递单号',
|
||||
'warehouse.parcelArrival.table.trackingNo': '国内快递单号',
|
||||
'warehouse.parcelArrival.table.warehouse': '仓库',
|
||||
'warehouse.parcelArrival.table.createTime': '创建时间',
|
||||
'warehouse.parcelArrival.table.createBy': '创建人',
|
||||
'warehouse.parcelArrival.actions.reset': '重置',
|
||||
'warehouse.parcelArrival.actions.arrive': '确认到仓',
|
||||
'warehouse.parcelArrival.actions.delete': '删除到仓记录',
|
||||
'warehouse.parcelArrival.message.loadFailed': '加载到仓记录失败',
|
||||
'warehouse.parcelArrival.message.trackingRequired': '请扫描或输入国内快递单号',
|
||||
'warehouse.parcelArrival.message.arriveSuccess': '到仓成功,包裹状态已更新为已到仓',
|
||||
'warehouse.parcelArrival.message.arriveFailed': '到仓失败',
|
||||
'warehouse.parcelArrival.message.deleteSuccess': '删除成功',
|
||||
'warehouse.inbound.status.all': '全部',
|
||||
'warehouse.inbound.status.forecasted': '预报',
|
||||
'warehouse.inbound.status.completed': '已完成',
|
||||
@ -391,6 +423,7 @@ export default {
|
||||
'menu.operation.inboundScan': '入库扫描',
|
||||
'menu.operation.outboundScan': '出库扫描',
|
||||
'menu.operation.printLabel': '打印标签',
|
||||
'menu.operation.parcelArrival': '快递到仓',
|
||||
'menu.api': 'API',
|
||||
'menu.api.upstreamInterface': '上游接口',
|
||||
'menu.api.downstreamInterface': '下游接口',
|
||||
|
||||
@ -78,6 +78,15 @@ const router = createRouter({
|
||||
hideInMenu: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/Plugin/AddPackage',
|
||||
name: 'PluginAddPackage',
|
||||
component: () => import('@/views/plugin/AddPackage.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
hideInMenu: true,
|
||||
},
|
||||
},
|
||||
...appRoutes,
|
||||
REDIRECT_MAIN,
|
||||
NOT_FOUND_ROUTE,
|
||||
|
||||
@ -19,6 +19,8 @@ const OPERATION: AppRouteRecordRaw = {
|
||||
meta: {
|
||||
locale: 'menu.operation.inboundScan',
|
||||
requiresAuth: true,
|
||||
permission: 'Operation.InboundScan.Query',
|
||||
order: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -28,6 +30,8 @@ const OPERATION: AppRouteRecordRaw = {
|
||||
meta: {
|
||||
locale: 'menu.operation.outboundScan',
|
||||
requiresAuth: true,
|
||||
permission: 'Operation.OutboundScan.Query',
|
||||
order: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -37,6 +41,19 @@ const OPERATION: AppRouteRecordRaw = {
|
||||
meta: {
|
||||
locale: 'menu.operation.printLabel',
|
||||
requiresAuth: true,
|
||||
permission: 'Operation.PrintLabel.Query',
|
||||
order: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'parcelArrival',
|
||||
name: 'OperationParcelArrival',
|
||||
component: () => import('@/views/warehouse/parcelArrival/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.operation.parcelArrival',
|
||||
requiresAuth: true,
|
||||
permission: 'Operation.ParcelArrival.Query',
|
||||
order: 4,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@ -4,8 +4,16 @@ import useUserStore from './modules/user';
|
||||
import useTabBarStore from './modules/tab-bar';
|
||||
import { useMessageStore } from './modules/message';
|
||||
import { useAuthStore } from './modules/auth';
|
||||
import useCurrentWarehouseStore from './modules/current-warehouse';
|
||||
|
||||
const pinia = createPinia();
|
||||
|
||||
export { useAppStore, useUserStore, useTabBarStore, useMessageStore, useAuthStore };
|
||||
export {
|
||||
useAppStore,
|
||||
useUserStore,
|
||||
useTabBarStore,
|
||||
useMessageStore,
|
||||
useAuthStore,
|
||||
useCurrentWarehouseStore,
|
||||
};
|
||||
export default pinia;
|
||||
|
||||
92
src/store/modules/current-warehouse.ts
Normal file
92
src/store/modules/current-warehouse.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { setRapidPackageWarehouseCookie } from '@/utils/rapidPackagePluginHost'
|
||||
|
||||
const STORAGE_KEY = 'OMS_CURRENT_WAREHOUSE'
|
||||
|
||||
export type CurrentWarehouse = {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
function readStored(): CurrentWarehouse | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY)
|
||||
if (!raw) return null
|
||||
const parsed = JSON.parse(raw)
|
||||
const id = Number(parsed?.id)
|
||||
if (!id) return null
|
||||
return { id, name: String(parsed?.name || id) }
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 顶栏右上角当前仓库(联系客服旁的选择器)。
|
||||
* 仓储新增表单统一从此处带入 warehouseId。
|
||||
*/
|
||||
const useCurrentWarehouseStore = defineStore('currentWarehouse', () => {
|
||||
const current = ref<CurrentWarehouse | null>(readStored())
|
||||
const options = ref<Array<{ label: string; value: number }>>([])
|
||||
|
||||
const warehouseId = computed(() => current.value?.id)
|
||||
const warehouseName = computed(() => current.value?.name || '')
|
||||
|
||||
function persist(wh: CurrentWarehouse | null) {
|
||||
current.value = wh
|
||||
try {
|
||||
if (wh) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(wh))
|
||||
} else {
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
setRapidPackageWarehouseCookie(wh)
|
||||
}
|
||||
|
||||
function setOptions(list: Array<{ label: string; value: number }>) {
|
||||
options.value = list
|
||||
if (current.value?.id) {
|
||||
const hit = list.find((o) => o.value === current.value!.id)
|
||||
if (hit && hit.label !== current.value.name) {
|
||||
persist({ id: hit.value, name: hit.label })
|
||||
} else if (!hit && list[0]) {
|
||||
persist({ id: list[0].value, name: list[0].label })
|
||||
}
|
||||
return
|
||||
}
|
||||
if (list[0]) {
|
||||
persist({ id: list[0].value, name: list[0].label })
|
||||
}
|
||||
}
|
||||
|
||||
function setWarehouseId(id: number | string | undefined | null) {
|
||||
const num = Number(id)
|
||||
if (!num) {
|
||||
persist(null)
|
||||
return
|
||||
}
|
||||
const hit = options.value.find((o) => o.value === num)
|
||||
persist({ id: num, name: hit?.label || String(num) })
|
||||
}
|
||||
|
||||
/** 新增表单默认仓库:顶栏当前仓 → 否则列表筛选项 */
|
||||
function resolveDefaultWarehouseId(fallback?: number | null) {
|
||||
return warehouseId.value || fallback || undefined
|
||||
}
|
||||
|
||||
return {
|
||||
current,
|
||||
options,
|
||||
warehouseId,
|
||||
warehouseName,
|
||||
setOptions,
|
||||
setWarehouseId,
|
||||
resolveDefaultWarehouseId,
|
||||
}
|
||||
})
|
||||
|
||||
export default useCurrentWarehouseStore
|
||||
@ -52,6 +52,12 @@ export const SUB_MENU_DEFAULT_ICONS: Record<string, string> = {
|
||||
'menu.operation.inboundScan': 'IconImport',
|
||||
'menu.operation.outboundScan': 'IconExport',
|
||||
'menu.operation.printLabel': 'IconPrinter',
|
||||
'menu.operation.parcelArrival': 'IconImport',
|
||||
'menu.warehouse.parcel': 'IconGift',
|
||||
'menu.warehouse.inventory': 'IconStorage',
|
||||
'menu.warehouse.sku': 'IconTags',
|
||||
'menu.warehouse.inboundNotice': 'IconFile',
|
||||
'menu.warehouse.binLocation': 'IconLocation',
|
||||
'menu.api.upstreamInterface': 'IconCloud',
|
||||
'menu.api.downstreamInterface': 'IconCloudDownload',
|
||||
'menu.api.createInterface': 'IconPlus',
|
||||
|
||||
110
src/utils/rapidPackagePluginHost.ts
Normal file
110
src/utils/rapidPackagePluginHost.ts
Normal file
@ -0,0 +1,110 @@
|
||||
/** 采集插件读取的站点根地址(与哪哪发 / WMS 约定一致) */
|
||||
export const RAPID_PACKAGE_HOST_COOKIE = 'RAPID_PACKAGE_HOST'
|
||||
/** 插件确认页默认仓库:JSON `{ id, name }` */
|
||||
export const RAPID_PACKAGE_WAREHOUSE_COOKIE = 'RAPID_PACKAGE_WAREHOUSE'
|
||||
/** 管理端代采客户:JSON `{ id, name }` */
|
||||
export const RAPID_PACKAGE_CUSTOMER_COOKIE = 'RAPID_PACKAGE_CUSTOMER'
|
||||
|
||||
function writeCookie(name: string, value: string): void {
|
||||
if (typeof document === 'undefined') return
|
||||
const protocol = window.location.protocol
|
||||
const isHttps = protocol === 'https:'
|
||||
const parts = [
|
||||
`${encodeURIComponent(name)}=${encodeURIComponent(value)}`,
|
||||
'path=/',
|
||||
`expires=${new Date(Date.now() + 999 * 864e5).toUTCString()}`,
|
||||
]
|
||||
if (isHttps) {
|
||||
parts.push('Secure', 'SameSite=None')
|
||||
}
|
||||
document.cookie = parts.join('; ')
|
||||
}
|
||||
|
||||
function readJsonCookie(name: string): { id: number; name: string } | null {
|
||||
if (typeof window === 'undefined') return null
|
||||
try {
|
||||
const fromLs = window.localStorage?.getItem(name)
|
||||
const raw =
|
||||
fromLs ||
|
||||
document.cookie
|
||||
.split(';')
|
||||
.map((s) => s.trim())
|
||||
.find((s) => s.startsWith(`${name}=`))
|
||||
?.slice(name.length + 1)
|
||||
if (!raw) return null
|
||||
const parsed = JSON.parse(decodeURIComponent(raw))
|
||||
const id = Number(parsed?.id)
|
||||
if (!id) return null
|
||||
return { id, name: String(parsed?.name || id) }
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入 RAPID_PACKAGE_HOST,供浏览器扩展识别当前 OMS 域名。
|
||||
*/
|
||||
export function setRapidPackageHostCookie(): void {
|
||||
if (typeof window === 'undefined') return
|
||||
const url = `${window.location.protocol}//${window.location.host}`
|
||||
try {
|
||||
writeCookie(RAPID_PACKAGE_HOST_COOKIE, url)
|
||||
} catch (e) {
|
||||
console.warn('[rapidPackagePluginHost] cookie set failed', e)
|
||||
}
|
||||
try {
|
||||
window.localStorage?.setItem(RAPID_PACKAGE_HOST_COOKIE, url)
|
||||
} catch {
|
||||
/* 隐私模式等 */
|
||||
}
|
||||
}
|
||||
|
||||
/** 写入当前选用的仓库,供确认页读取 */
|
||||
export function setRapidPackageWarehouseCookie(warehouse: { id: number; name: string } | null): void {
|
||||
if (typeof window === 'undefined') return
|
||||
if (!warehouse?.id) {
|
||||
try {
|
||||
writeCookie(RAPID_PACKAGE_WAREHOUSE_COOKIE, '')
|
||||
window.localStorage?.removeItem(RAPID_PACKAGE_WAREHOUSE_COOKIE)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return
|
||||
}
|
||||
const raw = JSON.stringify({ id: warehouse.id, name: warehouse.name || String(warehouse.id) })
|
||||
try {
|
||||
writeCookie(RAPID_PACKAGE_WAREHOUSE_COOKIE, raw)
|
||||
window.localStorage?.setItem(RAPID_PACKAGE_WAREHOUSE_COOKIE, raw)
|
||||
} catch (e) {
|
||||
console.warn('[rapidPackagePluginHost] warehouse cookie set failed', e)
|
||||
}
|
||||
}
|
||||
|
||||
export function readRapidPackageWarehouseCookie(): { id: number; name: string } | null {
|
||||
return readJsonCookie(RAPID_PACKAGE_WAREHOUSE_COOKIE)
|
||||
}
|
||||
|
||||
/** 管理端写入当前代采客户,供插件草稿/查重携带 customerId */
|
||||
export function setRapidPackageCustomerCookie(customer: { id: number; name: string } | null): void {
|
||||
if (typeof window === 'undefined') return
|
||||
if (!customer?.id) {
|
||||
try {
|
||||
writeCookie(RAPID_PACKAGE_CUSTOMER_COOKIE, '')
|
||||
window.localStorage?.removeItem(RAPID_PACKAGE_CUSTOMER_COOKIE)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return
|
||||
}
|
||||
const raw = JSON.stringify({ id: customer.id, name: customer.name || String(customer.id) })
|
||||
try {
|
||||
writeCookie(RAPID_PACKAGE_CUSTOMER_COOKIE, raw)
|
||||
window.localStorage?.setItem(RAPID_PACKAGE_CUSTOMER_COOKIE, raw)
|
||||
} catch (e) {
|
||||
console.warn('[rapidPackagePluginHost] customer cookie set failed', e)
|
||||
}
|
||||
}
|
||||
|
||||
export function readRapidPackageCustomerCookie(): { id: number; name: string } | null {
|
||||
return readJsonCookie(RAPID_PACKAGE_CUSTOMER_COOKIE)
|
||||
}
|
||||
@ -40,18 +40,67 @@ export function resolveWarehouseLabel(
|
||||
return map.get(warehouseId) || `#${warehouseId}`
|
||||
}
|
||||
|
||||
/** 商品信息:首个品名;多项时后缀 +N(其余条数) */
|
||||
export function summarizeParcelItems(items: unknown): string {
|
||||
const list = Array.isArray(items) ? items : []
|
||||
if (!list.length) return '-'
|
||||
const first = list[0] as Row
|
||||
const name = pickField<string>(first, 'productName', 'ProductName') || '-'
|
||||
const qty = Number(pickField(first, 'quantity', 'Quantity') ?? 0)
|
||||
if (list.length === 1) return `${name} ×${qty}`
|
||||
const totalQty = list.reduce(
|
||||
(sum, item) => sum + Number(pickField(item as Row, 'quantity', 'Quantity') ?? 0),
|
||||
0,
|
||||
)
|
||||
return `${name} 等${list.length}项 / ${totalQty}件`
|
||||
if (list.length === 1) return name
|
||||
return `${name} +${list.length - 1}`
|
||||
}
|
||||
|
||||
/** 属性:只取第一条明细的属性(不加 +N) */
|
||||
export function summarizeParcelAttributes(items: unknown): string {
|
||||
const list = Array.isArray(items) ? items : []
|
||||
if (!list.length) return '-'
|
||||
const first = list[0] as Row
|
||||
const text = String(pickField(first, 'attributeText', 'AttributeText') ?? '').trim()
|
||||
return text || '-'
|
||||
}
|
||||
|
||||
/** 数量:合计件数 */
|
||||
export function summarizeParcelQuantity(items: unknown): string {
|
||||
const list = Array.isArray(items) ? items : []
|
||||
if (!list.length) return '-'
|
||||
const total = list.reduce((sum, item) => {
|
||||
const q = Number(pickField(item as Row, 'quantity', 'Quantity') ?? 0)
|
||||
return sum + (q > 0 ? q : 1)
|
||||
}, 0)
|
||||
return String(total)
|
||||
}
|
||||
|
||||
export type ParcelItemLine = {
|
||||
productName: string
|
||||
attributeText: string
|
||||
quantity: number
|
||||
remark: string
|
||||
}
|
||||
|
||||
/** 规范化包裹明细行(列表浮层 / 编辑共用) */
|
||||
export function normalizeParcelItemLines(items: unknown): ParcelItemLine[] {
|
||||
const list = Array.isArray(items) ? items : []
|
||||
return list.map((item) => {
|
||||
const row = item as Row
|
||||
const q = Number(pickField(row, 'quantity', 'Quantity') ?? 1)
|
||||
return {
|
||||
productName: String(pickField(row, 'productName', 'ProductName') ?? '').trim() || '-',
|
||||
attributeText: String(pickField(row, 'attributeText', 'AttributeText') ?? '').trim() || '-',
|
||||
quantity: q > 0 ? q : 1,
|
||||
remark: String(pickField(row, 'remark', 'Remark') ?? '').trim() || '-',
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 悬停提示:品名 ×数量,最多 maxLines 行 */
|
||||
export function summarizeParcelItemsTooltip(items: unknown, maxLines = 5): string {
|
||||
const lines = normalizeParcelItemLines(items)
|
||||
if (!lines.length) return ''
|
||||
const shown = lines.slice(0, maxLines).map((l) => `${l.productName} ×${l.quantity}`)
|
||||
if (lines.length > maxLines) {
|
||||
shown.push(`共 ${lines.length} 项`)
|
||||
}
|
||||
return shown.join('\n')
|
||||
}
|
||||
|
||||
export function formatMoney(amount: unknown, currency?: string | null): string {
|
||||
|
||||
@ -32,7 +32,9 @@ defineOptions({ name: 'PlatformStoreOrder' })
|
||||
const { t } = useI18n()
|
||||
const { tableContainerRef, tableHeight } = useTableContainerHeight()
|
||||
|
||||
const fulfillStatus = ref<string>()
|
||||
/** Tab active-key;全部用 'all',请求时再转成不传 fulfillStatus */
|
||||
const STATUS_TAB_ALL = 'all'
|
||||
const fulfillStatus = ref<string>(STATUS_TAB_ALL)
|
||||
const keyword = ref('')
|
||||
const summary = ref<Record<string, number>>({})
|
||||
const warehouseOptions = ref<Array<{ label: string; value: number }>>([])
|
||||
@ -45,7 +47,7 @@ const pageSize = ref(30)
|
||||
const totalCount = ref(0)
|
||||
|
||||
const statusTabs = [
|
||||
{ value: undefined, labelKey: 'order.platformStoreOrder.status.all', countKey: null },
|
||||
{ value: STATUS_TAB_ALL, labelKey: 'order.platformStoreOrder.status.all', countKey: null },
|
||||
{ value: 'pending', labelKey: 'order.platformStoreOrder.status.pending', countKey: 'pending' as const },
|
||||
{ value: 'toShip', labelKey: 'order.platformStoreOrder.status.toShip', countKey: 'toShip' as const },
|
||||
{ value: 'shipped', labelKey: 'order.platformStoreOrder.status.shipped', countKey: 'shipped' as const },
|
||||
@ -134,13 +136,19 @@ async function loadSummary() {
|
||||
}
|
||||
}
|
||||
|
||||
function resolveFulfillStatusFilter(): string | undefined {
|
||||
const status = fulfillStatus.value
|
||||
if (!status || status === STATUS_TAB_ALL) return undefined
|
||||
return status
|
||||
}
|
||||
|
||||
async function loadList() {
|
||||
listLoading.value = true
|
||||
try {
|
||||
const { items, total } = await getPlatformStoreOrderList({
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
fulfillStatus: fulfillStatus.value,
|
||||
fulfillStatus: resolveFulfillStatusFilter(),
|
||||
platformOrderNo: keyword.value || undefined,
|
||||
})
|
||||
tableRows.value = (items || []).map((item: Record<string, unknown>) => mapRow(item))
|
||||
@ -152,6 +160,12 @@ async function loadList() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleStatusTabChange(key: string | number) {
|
||||
fulfillStatus.value = String(key)
|
||||
currentPage.value = 1
|
||||
loadList()
|
||||
}
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
currentPage.value = page
|
||||
loadList()
|
||||
@ -316,9 +330,10 @@ onMounted(async () => {
|
||||
<a-tabs
|
||||
v-model:active-key="fulfillStatus"
|
||||
class="status-tabs"
|
||||
@change="() => { currentPage = 1; loadList() }"
|
||||
hide-content
|
||||
@change="handleStatusTabChange"
|
||||
>
|
||||
<a-tab-pane v-for="tab in statusTabs" :key="tab.value ?? 'all'">
|
||||
<a-tab-pane v-for="tab in statusTabs" :key="tab.value">
|
||||
<template #title>
|
||||
{{ t(tab.labelKey) }}
|
||||
<span v-if="tab.countKey" class="tab-count">({{ summary[tab.countKey] ?? 0 }})</span>
|
||||
|
||||
494
src/views/plugin/AddPackage.vue
Normal file
494
src/views/plugin/AddPackage.vue
Normal file
@ -0,0 +1,494 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { getPluginPackage, getPluginWhseInfo } from '@/api/plugin'
|
||||
import { saveWarehouseParcel } from '@/api/warehouse'
|
||||
import { getWarehouseList } from '@/api/system/warehouse'
|
||||
import {
|
||||
readRapidPackageCustomerCookie,
|
||||
readRapidPackageWarehouseCookie,
|
||||
setRapidPackageCustomerCookie,
|
||||
setRapidPackageHostCookie,
|
||||
setRapidPackageWarehouseCookie,
|
||||
} from '@/utils/rapidPackagePluginHost'
|
||||
import { isLogin } from '@/utils/auth'
|
||||
import { useWarehouseAdminCustomer } from '@/composables/useWarehouseAdminCustomer'
|
||||
|
||||
defineOptions({ name: 'PluginAddPackage' })
|
||||
|
||||
interface LineRow {
|
||||
productName: string
|
||||
attributeText: string
|
||||
quantity: number
|
||||
remark: string
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const spinning = ref(true)
|
||||
const saving = ref(false)
|
||||
const {
|
||||
isAdmin,
|
||||
customerId: adminCustomerId,
|
||||
customerOptions,
|
||||
withCustomerId,
|
||||
ensureMutationCustomerId,
|
||||
} = useWarehouseAdminCustomer()
|
||||
const form = reactive({
|
||||
trackingNo: '',
|
||||
orderNo: '',
|
||||
carrierName: '',
|
||||
remark: '',
|
||||
warehouseId: undefined as number | undefined,
|
||||
})
|
||||
const warehouseOptions = ref<Array<{ label: string; value: number }>>([])
|
||||
const itemRows = ref<LineRow[]>([{ productName: '', attributeText: '', quantity: 1, remark: '' }])
|
||||
|
||||
const canSave = computed(
|
||||
() =>
|
||||
!!form.warehouseId &&
|
||||
!!form.trackingNo.trim() &&
|
||||
itemRows.value.some((r) => r.productName.trim()) &&
|
||||
(!isAdmin.value || !!Number(adminCustomerId.value)),
|
||||
)
|
||||
|
||||
function ensureTrailingEmptyRow() {
|
||||
if (!itemRows.value.some((r) => !r.productName.trim())) {
|
||||
itemRows.value = [...itemRows.value, { productName: '', attributeText: '', quantity: 1, remark: '' }]
|
||||
}
|
||||
}
|
||||
|
||||
/** 解析草稿商品列表:兼容数组、JSON 字符串、URI 编码字符串 */
|
||||
function parseDraftItems(raw: unknown): any[] {
|
||||
let cur: unknown = raw
|
||||
if (cur == null || cur === '') return []
|
||||
if (Array.isArray(cur)) return cur
|
||||
if (typeof cur === 'string') {
|
||||
const s = cur.trim()
|
||||
if (!s) return []
|
||||
const candidates = [s]
|
||||
try {
|
||||
candidates.push(decodeURIComponent(s))
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
try {
|
||||
candidates.push(decodeURI(s))
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
for (const part of candidates) {
|
||||
try {
|
||||
const parsed = JSON.parse(part)
|
||||
if (Array.isArray(parsed)) return parsed
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
const nested = (parsed as any).items ?? (parsed as any).Items ?? (parsed as any).files
|
||||
if (Array.isArray(nested)) return nested
|
||||
return [parsed]
|
||||
}
|
||||
} catch {
|
||||
/* try next */
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
if (typeof cur === 'object') {
|
||||
const obj = cur as any
|
||||
if (Array.isArray(obj.items)) return obj.items
|
||||
if (Array.isArray(obj.Items)) return obj.Items
|
||||
if (Array.isArray(obj.files)) return obj.files
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
function applyDraft(raw: unknown) {
|
||||
let data: any = raw
|
||||
if (typeof raw === 'string') {
|
||||
try {
|
||||
data = JSON.parse(raw)
|
||||
} catch {
|
||||
try {
|
||||
data = JSON.parse(decodeURIComponent(raw))
|
||||
} catch {
|
||||
Message.warning('草稿解析失败')
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
// 偶发双层包裹:{ data: {...} } / { Data: "..." }
|
||||
if (data && typeof data === 'object' && (data.data != null || data.Data != null)) {
|
||||
const nested = data.data ?? data.Data
|
||||
if (typeof nested === 'string') {
|
||||
try {
|
||||
data = JSON.parse(nested)
|
||||
} catch {
|
||||
data = nested
|
||||
}
|
||||
} else if (nested && typeof nested === 'object') {
|
||||
data = nested
|
||||
}
|
||||
}
|
||||
form.trackingNo = String(data?.track_no || data?.trackingNo || '').trim()
|
||||
form.orderNo = String(data?.order_no || data?.orderNo || '').trim()
|
||||
let carrierRaw = String(data?.express_name || data?.carrierName || '').trim()
|
||||
if (carrierRaw.includes('%')) {
|
||||
try {
|
||||
carrierRaw = decodeURIComponent(carrierRaw)
|
||||
} catch {
|
||||
/* keep original */
|
||||
}
|
||||
}
|
||||
form.carrierName = carrierRaw
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
.replace(/(快递|速递)$/g, '')
|
||||
.trim()
|
||||
// 备注只填订单号
|
||||
if (!form.remark && form.orderNo) {
|
||||
form.remark = form.orderNo
|
||||
}
|
||||
const items = parseDraftItems(data?.items ?? data?.Items ?? data?.files)
|
||||
itemRows.value = items.length
|
||||
? items.map((it: any) => ({
|
||||
productName: String(it?.name || it?.productName || it?.title || '').trim(),
|
||||
attributeText: [it?.attributes, it?.spec, it?.attributeText]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.trim(),
|
||||
quantity: Number(it?.quantity ?? it?.productNumber) > 0
|
||||
? Number(it?.quantity ?? it?.productNumber)
|
||||
: 1,
|
||||
remark: String(it?.remark || '').trim(),
|
||||
}))
|
||||
: [{ productName: '', attributeText: '', quantity: 1, remark: '' }]
|
||||
ensureTrailingEmptyRow()
|
||||
}
|
||||
|
||||
async function loadWarehouses() {
|
||||
try {
|
||||
const res = await getWarehouseList({ page: 1, pageSize: 100 })
|
||||
const items = (res as any)?.items ?? (res as any)?.Items ?? []
|
||||
warehouseOptions.value = items.map((w: any) => ({
|
||||
label: String(w.warehouseName ?? w.WarehouseName ?? w.name ?? w.Id),
|
||||
value: Number(w.id ?? w.Id),
|
||||
}))
|
||||
} catch {
|
||||
warehouseOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveWarehouse() {
|
||||
const cookieWh = readRapidPackageWarehouseCookie()
|
||||
if (cookieWh?.id) {
|
||||
form.warehouseId = cookieWh.id
|
||||
if (!warehouseOptions.value.some((o) => o.value === cookieWh.id)) {
|
||||
warehouseOptions.value = [
|
||||
{ label: cookieWh.name, value: cookieWh.id },
|
||||
...warehouseOptions.value,
|
||||
]
|
||||
}
|
||||
return
|
||||
}
|
||||
try {
|
||||
const wh = await getPluginWhseInfo()
|
||||
if (wh?.id) {
|
||||
form.warehouseId = wh.id
|
||||
setRapidPackageWarehouseCookie({ id: wh.id, name: wh.name })
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
if (!form.warehouseId && warehouseOptions.value[0]) {
|
||||
form.warehouseId = warehouseOptions.value[0].value
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDraftByKey(key: string) {
|
||||
const k = String(key || '').trim()
|
||||
if (!k) return
|
||||
const data = await getPluginPackage(k)
|
||||
applyDraft(data)
|
||||
}
|
||||
|
||||
function handleMessage(event: MessageEvent) {
|
||||
const payload = event.data?.data ?? event.data
|
||||
if (!payload || typeof payload !== 'object') return
|
||||
if (payload.code === 'addPackage' && payload.package) {
|
||||
spinning.value = true
|
||||
loadDraftByKey(String(payload.package))
|
||||
.catch((e: any) => Message.error(e?.message || '加载草稿失败'))
|
||||
.finally(() => {
|
||||
spinning.value = false
|
||||
})
|
||||
}
|
||||
if (payload.code === 'clearPackage') {
|
||||
form.trackingNo = ''
|
||||
form.orderNo = ''
|
||||
form.carrierName = ''
|
||||
form.remark = ''
|
||||
itemRows.value = [{ productName: '', attributeText: '', quantity: 1, remark: '' }]
|
||||
}
|
||||
}
|
||||
|
||||
function notifyParentClosed(saved: boolean) {
|
||||
const msg = {
|
||||
action: saved ? 'closed' : 'closedWin',
|
||||
code: saved ? 'packageSaved' : 'packageClosed',
|
||||
}
|
||||
try {
|
||||
if (window.opener && !window.opener.closed) {
|
||||
window.opener.postMessage(msg, '*')
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
try {
|
||||
// 页面内弹层:iframe → 购物页父窗口
|
||||
if (window.parent && window.parent !== window) {
|
||||
window.parent.postMessage(msg, '*')
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
try {
|
||||
window.close()
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveCustomer() {
|
||||
if (!isAdmin.value) return
|
||||
const cookieCu = readRapidPackageCustomerCookie()
|
||||
if (cookieCu?.id) {
|
||||
adminCustomerId.value = cookieCu.id
|
||||
if (!customerOptions.value.some((o) => o.value === cookieCu.id)) {
|
||||
customerOptions.value = [
|
||||
{ label: cookieCu.name, value: cookieCu.id },
|
||||
...customerOptions.value,
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!isLogin()) {
|
||||
Message.warning('请先登录 OMS')
|
||||
window.location.href = '/'
|
||||
return
|
||||
}
|
||||
if (isAdmin.value && ensureMutationCustomerId() == null) {
|
||||
Message.warning('管理端请先选择客户')
|
||||
return
|
||||
}
|
||||
if (!canSave.value) {
|
||||
Message.warning('请填写快递单号、仓库与至少一个商品')
|
||||
return
|
||||
}
|
||||
const items = itemRows.value
|
||||
.filter((r) => r.productName.trim())
|
||||
.map((r) => ({
|
||||
productName: r.productName.trim(),
|
||||
attributeText: r.attributeText.trim() || undefined,
|
||||
quantity: Number(r.quantity) || 1,
|
||||
remark: r.remark || undefined,
|
||||
}))
|
||||
saving.value = true
|
||||
try {
|
||||
const selected = warehouseOptions.value.find((o) => o.value === form.warehouseId)
|
||||
if (selected) {
|
||||
setRapidPackageWarehouseCookie({ id: selected.value, name: selected.label })
|
||||
}
|
||||
if (isAdmin.value && adminCustomerId.value) {
|
||||
const cu = customerOptions.value.find((o) => o.value === Number(adminCustomerId.value))
|
||||
setRapidPackageCustomerCookie({
|
||||
id: Number(adminCustomerId.value),
|
||||
name: cu?.label || String(adminCustomerId.value),
|
||||
})
|
||||
}
|
||||
await saveWarehouseParcel(
|
||||
withCustomerId({
|
||||
id: 0,
|
||||
warehouseId: form.warehouseId!,
|
||||
trackingNo: form.trackingNo.trim(),
|
||||
carrierName: form.carrierName.trim() || undefined,
|
||||
lifecycleStatus: 'inTransit',
|
||||
remark: form.remark.trim() || undefined,
|
||||
items,
|
||||
}),
|
||||
)
|
||||
Message.success('包裹已保存')
|
||||
setTimeout(() => notifyParentClosed(true), 400)
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || '保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
setRapidPackageHostCookie()
|
||||
window.addEventListener('message', handleMessage)
|
||||
if (!isLogin()) {
|
||||
Message.warning('未登录:请先打开本站登录后再使用采集插件')
|
||||
spinning.value = false
|
||||
return
|
||||
}
|
||||
try {
|
||||
await loadWarehouses()
|
||||
await resolveWarehouse()
|
||||
await resolveCustomer()
|
||||
const key = String(route.query.key || '')
|
||||
if (key) {
|
||||
await loadDraftByKey(key)
|
||||
}
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || '初始化失败')
|
||||
} finally {
|
||||
spinning.value = false
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('message', handleMessage)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="plugin-add-package">
|
||||
<a-spin :loading="spinning" tip="加载草稿…">
|
||||
<header class="plugin-add-package__header">
|
||||
<h1>添加包裹</h1>
|
||||
<p>来自淘宝 / 1688 / 拼多多的采集草稿,确认后写入「我的包裹」</p>
|
||||
</header>
|
||||
|
||||
<a-form layout="vertical" class="plugin-add-package__form">
|
||||
<a-card title="基本信息" :bordered="true" size="small">
|
||||
<a-row :gutter="16">
|
||||
<a-col v-if="isAdmin" :span="24">
|
||||
<a-form-item label="客户" required>
|
||||
<a-select
|
||||
v-model="adminCustomerId"
|
||||
allow-search
|
||||
:options="customerOptions"
|
||||
placeholder="管理端请选择客户后再保存"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="快递单号" required>
|
||||
<a-input v-model="form.trackingNo" allow-clear placeholder="快递单号" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="仓库" required>
|
||||
<a-select
|
||||
v-model="form.warehouseId"
|
||||
allow-search
|
||||
:options="warehouseOptions"
|
||||
placeholder="选择仓库"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="快递公司">
|
||||
<a-input v-model="form.carrierName" allow-clear placeholder="可选" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="平台订单号">
|
||||
<a-input :model-value="form.orderNo" disabled placeholder="—" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="备注">
|
||||
<a-input v-model="form.remark" allow-clear :max-length="500" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
|
||||
<a-card title="商品明细" :bordered="true" size="small" style="margin-top: 12px">
|
||||
<div v-for="(row, idx) in itemRows" :key="idx" class="plugin-add-package__line">
|
||||
<a-input
|
||||
v-model="row.productName"
|
||||
placeholder="品名"
|
||||
style="flex: 1"
|
||||
@input="ensureTrailingEmptyRow"
|
||||
/>
|
||||
<a-input v-model="row.attributeText" placeholder="属性" style="width: 180px" />
|
||||
<a-input-number v-model="row.quantity" :min="1" style="width: 100px" />
|
||||
<a-input v-model="row.remark" placeholder="备注" style="width: 140px" />
|
||||
<a-button
|
||||
v-if="row.productName"
|
||||
type="text"
|
||||
status="danger"
|
||||
@click="itemRows.splice(idx, 1)"
|
||||
>
|
||||
删除
|
||||
</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-form>
|
||||
|
||||
<footer class="plugin-add-package__footer">
|
||||
<a-space>
|
||||
<a-button @click="notifyParentClosed(false)">关闭</a-button>
|
||||
<a-button type="primary" :loading="saving" :disabled="!canSave" @click="handleSave">
|
||||
保存包裹
|
||||
</a-button>
|
||||
</a-space>
|
||||
</footer>
|
||||
</a-spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.plugin-add-package {
|
||||
min-height: 100vh;
|
||||
padding: 20px 24px 80px;
|
||||
background: #f0f3f4;
|
||||
box-sizing: border-box;
|
||||
|
||||
&__header {
|
||||
margin-bottom: 16px;
|
||||
h1 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 20px;
|
||||
color: #1d2129;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
color: #86909c;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
&__form {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
&__line {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 12px 24px;
|
||||
background: #fff;
|
||||
border-top: 1px solid #e5e6eb;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -29,6 +29,7 @@ import {
|
||||
} from '@/utils/warehouseListMapper'
|
||||
import { renderWarehouseActionBtn, wrapWarehouseActions } from '@/utils/warehouseActionRenderer'
|
||||
import { useWarehouseAdminCustomer } from '@/composables/useWarehouseAdminCustomer'
|
||||
import { useCurrentWarehouseStore } from '@/store'
|
||||
|
||||
defineOptions({ name: 'WarehouseInboundNotice' })
|
||||
|
||||
@ -74,8 +75,11 @@ const {
|
||||
ensureMutationCustomerId,
|
||||
resolveSelectedCustomerId,
|
||||
} = useWarehouseAdminCustomer()
|
||||
const currentWarehouseStore = useCurrentWarehouseStore()
|
||||
|
||||
const receiptStatus = ref<string>()
|
||||
/** Tab active-key;全部用 'all',请求时再转成不传 receiptStatus */
|
||||
const STATUS_TAB_ALL = 'all'
|
||||
const receiptStatus = ref<string>(STATUS_TAB_ALL)
|
||||
const summary = ref({ forecasted: 0, received: 0, completed: 0 })
|
||||
const keyword = ref('')
|
||||
const warehouseId = ref<number>()
|
||||
@ -91,7 +95,7 @@ const selectedBoxKeys = ref<string[]>([])
|
||||
const activeBoxKey = ref('')
|
||||
|
||||
const statusTabs = [
|
||||
{ value: undefined, labelKey: 'warehouse.inbound.status.all', countKey: null },
|
||||
{ value: STATUS_TAB_ALL, labelKey: 'warehouse.inbound.status.all', countKey: null },
|
||||
{ value: 'forecasted', labelKey: 'warehouse.inbound.status.forecasted', countKey: 'forecasted' as const },
|
||||
{ value: 'received', labelKey: 'warehouse.inbound.status.received', countKey: 'received' as const },
|
||||
{ value: 'completed', labelKey: 'warehouse.inbound.status.completed', countKey: 'completed' as const },
|
||||
@ -267,13 +271,19 @@ async function loadSummary() {
|
||||
}
|
||||
}
|
||||
|
||||
function resolveReceiptStatusFilter(): string | undefined {
|
||||
const status = receiptStatus.value
|
||||
if (!status || status === STATUS_TAB_ALL) return undefined
|
||||
return status
|
||||
}
|
||||
|
||||
async function loadList() {
|
||||
listLoading.value = true
|
||||
try {
|
||||
const { items, total } = await getWarehouseInboundList(withCustomerId({
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
receiptStatus: receiptStatus.value,
|
||||
receiptStatus: resolveReceiptStatusFilter(),
|
||||
keyword: keyword.value || undefined,
|
||||
warehouseId: warehouseId.value,
|
||||
}))
|
||||
@ -286,6 +296,12 @@ async function loadList() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleStatusTabChange(key: string | number) {
|
||||
receiptStatus.value = String(key)
|
||||
currentPage.value = 1
|
||||
loadList()
|
||||
}
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
currentPage.value = page
|
||||
loadList()
|
||||
@ -428,11 +444,15 @@ function handleAdd() {
|
||||
if (!hasPermission('Warehouse.InboundNotice.Save')) return
|
||||
if (isAdmin.value && ensureMutationCustomerId() == null) return
|
||||
formModel.id = 0
|
||||
formModel.warehouseId = warehouseId.value
|
||||
formModel.warehouseId = currentWarehouseStore.resolveDefaultWarehouseId(warehouseId.value)
|
||||
formModel.markReference = ''
|
||||
formModel.remark = ''
|
||||
formModel.inboundType = 'normal'
|
||||
resetBoxes()
|
||||
if (!formModel.warehouseId) {
|
||||
$baseMessage(t('warehouse.common.noCurrentWarehouse'), 'warning')
|
||||
return
|
||||
}
|
||||
openModal(true)
|
||||
}
|
||||
|
||||
@ -536,9 +556,10 @@ onMounted(async () => {
|
||||
<a-tabs
|
||||
v-model:active-key="receiptStatus"
|
||||
class="status-tabs"
|
||||
@change="() => { currentPage = 1; loadList() }"
|
||||
hide-content
|
||||
@change="handleStatusTabChange"
|
||||
>
|
||||
<a-tab-pane v-for="tab in statusTabs" :key="tab.value ?? 'all'">
|
||||
<a-tab-pane v-for="tab in statusTabs" :key="tab.value">
|
||||
<template #title>
|
||||
{{ t(tab.labelKey) }}
|
||||
<span v-if="tab.countKey" class="tab-count">({{ summary[tab.countKey] }})</span>
|
||||
@ -692,8 +713,16 @@ onMounted(async () => {
|
||||
:label-col-props="{ span: 6 }"
|
||||
:wrapper-col-props="{ span: 16 }"
|
||||
>
|
||||
<a-form-item :label="t('warehouse.common.warehouse')" required>
|
||||
<a-select v-model="formModel.warehouseId" :options="warehouseOptions" allow-search />
|
||||
<a-form-item :label="t('warehouse.parcel.form.currentWarehouse')" required>
|
||||
<span class="wh-form-readonly">
|
||||
{{
|
||||
modalWarehouseName
|
||||
|| warehouseOptions.find((o) => o.value === formModel.warehouseId)?.label
|
||||
|| currentWarehouseStore.warehouseName
|
||||
|| formModel.warehouseId
|
||||
|| '—'
|
||||
}}
|
||||
</span>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('warehouse.inbound.form.markReference')">
|
||||
<a-input v-model="formModel.markReference" :placeholder="t('warehouse.inbound.form.markPlaceholder')" allow-clear />
|
||||
@ -791,15 +820,6 @@ onMounted(async () => {
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
|
||||
<a-alert
|
||||
v-if="formModel.warehouseId && modalWarehouseName"
|
||||
type="info"
|
||||
:show-icon="true"
|
||||
class="wh-form-divider"
|
||||
>
|
||||
{{ t('warehouse.common.warehouse') }}:{{ modalWarehouseName }}
|
||||
</a-alert>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
|
||||
@ -15,17 +15,25 @@ import {
|
||||
buildWarehouseNameMap,
|
||||
formatDateTime,
|
||||
normalizeListRow,
|
||||
normalizeParcelItemLines,
|
||||
pickField,
|
||||
resolveWarehouseLabel,
|
||||
summarizeParcelAttributes,
|
||||
summarizeParcelItems,
|
||||
summarizeParcelItemsTooltip,
|
||||
summarizeParcelQuantity,
|
||||
type ParcelItemLine,
|
||||
} from '@/utils/warehouseListMapper'
|
||||
import { renderWarehouseActionBtn, wrapWarehouseActions } from '@/utils/warehouseActionRenderer'
|
||||
import { useWarehouseAdminCustomer } from '@/composables/useWarehouseAdminCustomer'
|
||||
import { setRapidPackageHostCookie, setRapidPackageWarehouseCookie, setRapidPackageCustomerCookie } from '@/utils/rapidPackagePluginHost'
|
||||
import { useCurrentWarehouseStore } from '@/store'
|
||||
|
||||
defineOptions({ name: 'WarehouseParcel' })
|
||||
|
||||
interface ParcelItemRow {
|
||||
productName: string
|
||||
attributeText: string
|
||||
quantity: number
|
||||
remark: string
|
||||
}
|
||||
@ -39,8 +47,11 @@ const {
|
||||
withCustomerId,
|
||||
ensureMutationCustomerId,
|
||||
} = useWarehouseAdminCustomer()
|
||||
const currentWarehouseStore = useCurrentWarehouseStore()
|
||||
|
||||
const lifecycleStatus = ref<string>()
|
||||
/** Tab active-key;全部用 'all',请求时再转成不传 lifecycleStatus */
|
||||
const STATUS_TAB_ALL = 'all'
|
||||
const lifecycleStatus = ref<string>(STATUS_TAB_ALL)
|
||||
const keyword = ref('')
|
||||
const warehouseId = ref<number>()
|
||||
const warehouseOptions = ref<Array<{ label: string; value: number }>>([])
|
||||
@ -52,7 +63,7 @@ const pageSize = ref(30)
|
||||
const totalCount = ref(0)
|
||||
|
||||
const statusTabs = [
|
||||
{ value: undefined, labelKey: 'warehouse.parcel.status.all' },
|
||||
{ value: STATUS_TAB_ALL, labelKey: 'warehouse.parcel.status.all' },
|
||||
{ value: 'inTransit', labelKey: 'warehouse.parcel.status.inTransit' },
|
||||
{ value: 'arrived', labelKey: 'warehouse.parcel.status.arrived' },
|
||||
{ value: 'stored', labelKey: 'warehouse.parcel.status.stored' },
|
||||
@ -68,34 +79,142 @@ const formModel = reactive({
|
||||
lifecycleStatus: 'inTransit',
|
||||
remark: '',
|
||||
})
|
||||
const itemRows = ref<ParcelItemRow[]>([{ productName: '', quantity: 1, remark: '' }])
|
||||
const itemRows = ref<ParcelItemRow[]>([{ productName: '', attributeText: '', quantity: 1, remark: '' }])
|
||||
|
||||
const filledItemCount = computed(() => itemRows.value.filter((r) => r.productName.trim()).length)
|
||||
|
||||
const DETAIL_COLUMN_KEYS = new Set(['itemsSummary', 'attributeSummary', 'quantitySummary'])
|
||||
const detailVisible = ref(false)
|
||||
const detailRow = ref<any>(null)
|
||||
const detailItems = computed<ParcelItemLine[]>(() => {
|
||||
const raw = detailRow.value?._raw || detailRow.value
|
||||
return normalizeParcelItemLines(pickField(raw || {}, 'items', 'Items'))
|
||||
})
|
||||
const detailTrackingNo = computed(() => {
|
||||
const raw = detailRow.value?._raw || detailRow.value
|
||||
return String(pickField(raw || {}, 'trackingNo', 'TrackingNo') ?? detailRow.value?.trackingNo ?? '-')
|
||||
})
|
||||
const detailQuantityTotal = computed(() =>
|
||||
detailItems.value.reduce((sum, item) => sum + (Number(item.quantity) || 0), 0),
|
||||
)
|
||||
|
||||
function escapeHtml(s: string) {
|
||||
return String(s)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
function getRowItemCount(row: any): number {
|
||||
if (typeof row?._itemCount === 'number') return row._itemCount
|
||||
const items = pickField(row?._raw || {}, 'items', 'Items')
|
||||
return Array.isArray(items) ? items.length : 0
|
||||
}
|
||||
|
||||
function isMultiItemRow(row: any): boolean {
|
||||
return getRowItemCount(row) > 1
|
||||
}
|
||||
|
||||
/** 快递单号:多商品时右上角红色「多」标识 */
|
||||
function renderTrackingNoCell(row: any, value: any) {
|
||||
const text = value == null || value === '' ? '-' : String(value)
|
||||
if (!isMultiItemRow(row)) {
|
||||
return escapeHtml(text)
|
||||
}
|
||||
const tip = t('warehouse.parcel.badge.multiTip')
|
||||
return (
|
||||
`<div class="parcel-tracking-cell" style="position:relative;padding:2px 14px 2px 0;min-height:18px;box-sizing:border-box;">` +
|
||||
`<span>${escapeHtml(text)}</span>` +
|
||||
`<span class="parcel-multi-badge" title="${escapeHtml(tip)}" ` +
|
||||
`style="position:absolute;top:0;right:0;z-index:1;display:inline-flex;align-items:center;justify-content:center;` +
|
||||
`min-width:16px;height:16px;padding:0 3px;border-radius:3px;background:#f53f3f;color:#fff;` +
|
||||
`font-size:10px;font-weight:700;line-height:1;letter-spacing:0;">多</span>` +
|
||||
`</div>`
|
||||
)
|
||||
}
|
||||
|
||||
/** 商品/属性/数量:仅多商品可点、主色;单商品普通文本 */
|
||||
function renderDetailCell(row: any, value: any) {
|
||||
const text = value == null || value === '' ? '-' : String(value)
|
||||
if (!isMultiItemRow(row)) {
|
||||
return escapeHtml(text)
|
||||
}
|
||||
const tip = summarizeParcelItemsTooltip(pickField(row?._raw || {}, 'items', 'Items'))
|
||||
const titleAttr = tip ? ` title="${escapeHtml(tip)}"` : ''
|
||||
return `<span class="parcel-detail-cell"${titleAttr} style="cursor:pointer;color:var(--app-primary-color);">${escapeHtml(text)}</span>`
|
||||
}
|
||||
|
||||
const hotColumns = computed(() => [
|
||||
{ key: 'trackingNo', label: t('warehouse.parcel.table.trackingNo'), width: 160 },
|
||||
{
|
||||
key: 'trackingNo',
|
||||
label: t('warehouse.parcel.table.trackingNo'),
|
||||
width: 168,
|
||||
renderer: renderTrackingNoCell,
|
||||
},
|
||||
{ key: 'carrierName', label: t('warehouse.parcel.table.carrierName'), width: 120 },
|
||||
{ key: 'lifecycleStatus', label: t('warehouse.parcel.table.lifecycleStatus'), width: 100 },
|
||||
{ key: 'warehouseName', label: t('warehouse.parcel.table.warehouse'), width: 140 },
|
||||
{ key: 'itemsSummary', label: t('warehouse.parcel.table.items'), width: 200 },
|
||||
{
|
||||
key: 'itemsSummary',
|
||||
label: t('warehouse.parcel.table.items'),
|
||||
width: 200,
|
||||
renderer: renderDetailCell,
|
||||
},
|
||||
{
|
||||
key: 'attributeSummary',
|
||||
label: t('warehouse.parcel.table.attribute'),
|
||||
width: 180,
|
||||
renderer: renderDetailCell,
|
||||
},
|
||||
{
|
||||
key: 'quantitySummary',
|
||||
label: t('warehouse.parcel.table.quantity'),
|
||||
width: 100,
|
||||
renderer: renderDetailCell,
|
||||
},
|
||||
{ key: 'remark', label: t('warehouse.parcel.table.remark'), width: 140 },
|
||||
{ key: 'arrivalTime', label: t('warehouse.parcel.table.arrivalTime'), width: 160 },
|
||||
{ key: 'createTime', label: t('warehouse.parcel.table.createTime'), width: 160 },
|
||||
{ key: 'remark', label: t('warehouse.parcel.table.remark'), width: 140 },
|
||||
])
|
||||
|
||||
function openItemDetail(row: any) {
|
||||
if (!isMultiItemRow(row)) return
|
||||
detailRow.value = row
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
function closeItemDetail() {
|
||||
detailVisible.value = false
|
||||
detailRow.value = null
|
||||
}
|
||||
|
||||
function handleCellClick(payload: { row: any; rowIndex: number; columnKey: string }) {
|
||||
if (!DETAIL_COLUMN_KEYS.has(payload.columnKey)) return
|
||||
if (!isMultiItemRow(payload.row)) return
|
||||
openItemDetail(payload.row)
|
||||
}
|
||||
|
||||
function handleEditFromDetail() {
|
||||
const row = detailRow.value
|
||||
if (!row) return
|
||||
closeItemDetail()
|
||||
handleEdit(row)
|
||||
}
|
||||
|
||||
watch(
|
||||
itemRows,
|
||||
(rows) => {
|
||||
const last = rows[rows.length - 1]
|
||||
if (last && last.productName.trim()) {
|
||||
itemRows.value = [...rows, { productName: '', quantity: 1, remark: '' }]
|
||||
itemRows.value = [...rows, { productName: '', attributeText: '', quantity: 1, remark: '' }]
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
function resetItems() {
|
||||
itemRows.value = [{ productName: '', quantity: 1, remark: '' }]
|
||||
itemRows.value = [{ productName: '', attributeText: '', quantity: 1, remark: '' }]
|
||||
}
|
||||
|
||||
function removeItemRow(index: number) {
|
||||
@ -107,18 +226,23 @@ function mapRow(raw: Record<string, unknown>) {
|
||||
const row = normalizeListRow(raw)
|
||||
const status = String(pickField(row, 'lifecycleStatus', 'LifecycleStatus') ?? '')
|
||||
const whId = pickField<number>(row, 'warehouseId', 'WarehouseId')
|
||||
const items = pickField(row, 'items', 'Items')
|
||||
const itemCount = Array.isArray(items) ? items.length : 0
|
||||
return {
|
||||
id: pickField<number>(row, 'id', 'Id'),
|
||||
trackingNo: pickField(row, 'trackingNo', 'TrackingNo') ?? '-',
|
||||
carrierName: pickField(row, 'carrierName', 'CarrierName') ?? '-',
|
||||
lifecycleStatus: t(`warehouse.parcel.status.${status}`, status),
|
||||
warehouseName: resolveWarehouseLabel(whId, warehouseNameMap.value),
|
||||
itemsSummary: summarizeParcelItems(pickField(row, 'items', 'Items')),
|
||||
itemsSummary: summarizeParcelItems(items),
|
||||
attributeSummary: summarizeParcelAttributes(items),
|
||||
quantitySummary: summarizeParcelQuantity(items),
|
||||
arrivalTime: formatDateTime(pickField(row, 'arrivalTime', 'ArrivalTime')),
|
||||
createTime: formatDateTime(pickField(row, 'createTime', 'CreateTime')),
|
||||
remark: pickField(row, 'remark', 'Remark') ?? '',
|
||||
_raw: row,
|
||||
_status: status,
|
||||
_itemCount: itemCount,
|
||||
}
|
||||
}
|
||||
|
||||
@ -134,13 +258,38 @@ async function loadWarehouses() {
|
||||
}
|
||||
}
|
||||
|
||||
watch(warehouseId, (id) => {
|
||||
const opt = warehouseOptions.value.find((o) => o.value === id)
|
||||
setRapidPackageWarehouseCookie(opt ? { id: opt.value, name: opt.label } : null)
|
||||
})
|
||||
|
||||
watch(adminCustomerId, (id) => {
|
||||
if (!isAdmin.value) {
|
||||
setRapidPackageCustomerCookie(null)
|
||||
return
|
||||
}
|
||||
const num = Number(id)
|
||||
if (!num) {
|
||||
setRapidPackageCustomerCookie(null)
|
||||
return
|
||||
}
|
||||
const opt = customerOptions.value.find((o) => o.value === num)
|
||||
setRapidPackageCustomerCookie({ id: num, name: opt?.label || String(num) })
|
||||
})
|
||||
|
||||
function resolveLifecycleStatusFilter(): string | undefined {
|
||||
const status = lifecycleStatus.value
|
||||
if (!status || status === STATUS_TAB_ALL) return undefined
|
||||
return status
|
||||
}
|
||||
|
||||
async function loadList() {
|
||||
listLoading.value = true
|
||||
try {
|
||||
const { items, total } = await getWarehouseParcelList(withCustomerId({
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
lifecycleStatus: lifecycleStatus.value,
|
||||
lifecycleStatus: resolveLifecycleStatusFilter(),
|
||||
keyword: keyword.value || undefined,
|
||||
warehouseId: warehouseId.value,
|
||||
}))
|
||||
@ -155,6 +304,12 @@ async function loadList() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleStatusTabChange(key: string | number) {
|
||||
lifecycleStatus.value = String(key)
|
||||
currentPage.value = 1
|
||||
loadList()
|
||||
}
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
currentPage.value = page
|
||||
loadList()
|
||||
@ -175,12 +330,16 @@ function handleAdd() {
|
||||
if (!hasPermission('Warehouse.Parcel.Save')) return
|
||||
if (isAdmin.value && ensureMutationCustomerId() == null) return
|
||||
formModel.id = 0
|
||||
formModel.warehouseId = warehouseId.value
|
||||
formModel.warehouseId = currentWarehouseStore.resolveDefaultWarehouseId(warehouseId.value)
|
||||
formModel.trackingNo = ''
|
||||
formModel.carrierName = ''
|
||||
formModel.lifecycleStatus = 'inTransit'
|
||||
formModel.remark = ''
|
||||
resetItems()
|
||||
if (!formModel.warehouseId) {
|
||||
$baseMessage(t('warehouse.common.noCurrentWarehouse'), 'warning')
|
||||
return
|
||||
}
|
||||
openModal(true)
|
||||
}
|
||||
|
||||
@ -198,11 +357,12 @@ function handleEdit(row: any) {
|
||||
if (Array.isArray(items) && items.length) {
|
||||
itemRows.value = items.map((item: any) => ({
|
||||
productName: String(pickField(item, 'productName', 'ProductName') ?? ''),
|
||||
attributeText: String(pickField(item, 'attributeText', 'AttributeText') ?? ''),
|
||||
quantity: Number(pickField(item, 'quantity', 'Quantity') ?? 1) || 1,
|
||||
remark: String(pickField(item, 'remark', 'Remark') ?? ''),
|
||||
}))
|
||||
if (!itemRows.value.some((r) => !r.productName.trim())) {
|
||||
itemRows.value = [...itemRows.value, { productName: '', quantity: 1, remark: '' }]
|
||||
itemRows.value = [...itemRows.value, { productName: '', attributeText: '', quantity: 1, remark: '' }]
|
||||
}
|
||||
} else {
|
||||
resetItems()
|
||||
@ -220,6 +380,7 @@ async function handleSave() {
|
||||
.filter((row) => row.productName.trim())
|
||||
.map((row) => ({
|
||||
productName: row.productName.trim(),
|
||||
attributeText: row.attributeText.trim() || undefined,
|
||||
quantity: Number(row.quantity) || 1,
|
||||
remark: row.remark || undefined,
|
||||
}))
|
||||
@ -271,6 +432,7 @@ function handleActionClick(row: any, _i: number, action?: string) {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
setRapidPackageHostCookie()
|
||||
await loadWarehouses()
|
||||
await loadList()
|
||||
})
|
||||
@ -282,9 +444,10 @@ onMounted(async () => {
|
||||
<a-tabs
|
||||
v-model:active-key="lifecycleStatus"
|
||||
class="status-tabs"
|
||||
@change="() => { currentPage = 1; loadList() }"
|
||||
hide-content
|
||||
@change="handleStatusTabChange"
|
||||
>
|
||||
<a-tab-pane v-for="tab in statusTabs" :key="tab.value ?? 'all'" :title="t(tab.labelKey)" />
|
||||
<a-tab-pane v-for="tab in statusTabs" :key="tab.value" :title="t(tab.labelKey)" />
|
||||
</a-tabs>
|
||||
<div class="table-toolbar">
|
||||
<div class="toolbar-left">
|
||||
@ -341,10 +504,71 @@ onMounted(async () => {
|
||||
:action-column-width="90"
|
||||
:action-renderer="actionRenderer"
|
||||
@action-click="handleActionClick"
|
||||
@cell-click="handleCellClick"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-drawer
|
||||
v-model:visible="detailVisible"
|
||||
:title="t('warehouse.parcel.drawer.itemsTitle')"
|
||||
:width="840"
|
||||
unmount-on-close
|
||||
@cancel="closeItemDetail"
|
||||
>
|
||||
<div class="parcel-detail-drawer">
|
||||
<div class="parcel-detail-drawer__meta">
|
||||
<span>{{ t('warehouse.parcel.table.trackingNo') }}:{{ detailTrackingNo }}</span>
|
||||
<span>
|
||||
{{ t('warehouse.parcel.drawer.totalQty') }}:{{ detailQuantityTotal }}
|
||||
· {{ t('warehouse.parcel.drawer.itemCount', { n: detailItems.length }) }}
|
||||
</span>
|
||||
</div>
|
||||
<a-table
|
||||
:data="detailItems"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
:bordered="{ cell: true }"
|
||||
:scroll="{ x: '100%' }"
|
||||
>
|
||||
<template #columns>
|
||||
<a-table-column :title="'#'" :width="48">
|
||||
<template #cell="{ rowIndex }">{{ rowIndex + 1 }}</template>
|
||||
</a-table-column>
|
||||
<a-table-column :title="t('warehouse.parcel.form.productName')" data-index="productName" />
|
||||
<a-table-column
|
||||
:title="t('warehouse.parcel.form.attribute')"
|
||||
data-index="attributeText"
|
||||
:width="140"
|
||||
/>
|
||||
<a-table-column
|
||||
:title="t('warehouse.parcel.form.quantity')"
|
||||
data-index="quantity"
|
||||
:width="80"
|
||||
/>
|
||||
<a-table-column
|
||||
:title="t('warehouse.parcel.form.itemRemark')"
|
||||
data-index="remark"
|
||||
:width="120"
|
||||
/>
|
||||
</template>
|
||||
</a-table>
|
||||
<a-empty v-if="!detailItems.length" :description="t('warehouse.parcel.drawer.empty')" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<a-space :size="8">
|
||||
<a-button @click="closeItemDetail">{{ t('warehouse.common.cancel') }}</a-button>
|
||||
<a-button
|
||||
v-if="hasPermission('Warehouse.Parcel.Save') && detailRow?._status !== 'stored'"
|
||||
type="primary"
|
||||
@click="handleEditFromDetail"
|
||||
>
|
||||
{{ t('warehouse.parcel.drawer.edit') }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-drawer>
|
||||
|
||||
<ModalPro
|
||||
:title="formModel.id ? t('warehouse.common.edit') : t('warehouse.parcel.modal.add')"
|
||||
:width="720"
|
||||
@ -360,7 +584,17 @@ onMounted(async () => {
|
||||
:label-col-props="{ span: 6 }"
|
||||
:wrapper-col-props="{ span: 16 }"
|
||||
>
|
||||
<a-form-item :label="t('warehouse.common.warehouse')" required>
|
||||
<a-form-item v-if="!formModel.id" :label="t('warehouse.parcel.form.currentWarehouse')" required>
|
||||
<span class="wh-form-readonly">
|
||||
{{
|
||||
warehouseOptions.find((o) => o.value === formModel.warehouseId)?.label
|
||||
|| currentWarehouseStore.warehouseName
|
||||
|| formModel.warehouseId
|
||||
|| '—'
|
||||
}}
|
||||
</span>
|
||||
</a-form-item>
|
||||
<a-form-item v-else :label="t('warehouse.common.warehouse')" required>
|
||||
<a-select v-model="formModel.warehouseId" :options="warehouseOptions" allow-search />
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('warehouse.parcel.form.trackingNo')" required>
|
||||
@ -396,6 +630,11 @@ onMounted(async () => {
|
||||
<a-input v-model="record.productName" allow-clear />
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column :title="t('warehouse.parcel.form.attribute')" :width="160">
|
||||
<template #cell="{ record }">
|
||||
<a-input v-model="record.attributeText" allow-clear />
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column :title="t('warehouse.parcel.form.quantity')" :width="110">
|
||||
<template #cell="{ record }">
|
||||
<a-input-number v-model="record.quantity" :min="1" style="width: 100%" />
|
||||
@ -440,4 +679,15 @@ onMounted(async () => {
|
||||
<style scoped lang="less">
|
||||
@import '@/styles/warehouse-list-page.less';
|
||||
@import '@/styles/warehouse-form-modal.less';
|
||||
|
||||
.parcel-detail-drawer {
|
||||
&__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 16px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
color: #4e5969;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
497
src/views/warehouse/parcelArrival/index.vue
Normal file
497
src/views/warehouse/parcelArrival/index.vue
Normal file
@ -0,0 +1,497 @@
|
||||
<template>
|
||||
<div class="waybill-container">
|
||||
<div class="waybill-layout">
|
||||
<div class="form-section">
|
||||
<div class="form-section-toolbar">
|
||||
<a-checkbox v-model="keepScanFocus" size="small">
|
||||
{{ $t('operation.common.keepScanFocus') }}
|
||||
</a-checkbox>
|
||||
<a-checkbox v-model="voiceEnabled" size="small">
|
||||
{{ $t('operation.common.voiceBroadcast') }}
|
||||
</a-checkbox>
|
||||
</div>
|
||||
<a-form :model="formData" layout="vertical" class="waybill-form">
|
||||
<a-form-item
|
||||
v-if="isAdmin"
|
||||
:label="t('warehouse.common.customer')"
|
||||
class="form-item-large"
|
||||
>
|
||||
<a-select
|
||||
v-model="adminCustomerId"
|
||||
allow-clear
|
||||
allow-search
|
||||
:placeholder="t('warehouse.common.selectCustomer')"
|
||||
:options="customerOptions"
|
||||
size="large"
|
||||
class="large-input"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('warehouse.parcelArrival.form.trackingNo')" class="form-item-large">
|
||||
<a-input
|
||||
ref="scanInputRef"
|
||||
v-model="formData.trackingNo"
|
||||
:placeholder="t('warehouse.parcelArrival.form.placeholder.scanOrInput')"
|
||||
size="large"
|
||||
class="large-input"
|
||||
@press-enter="() => void handleSubmit()"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('warehouse.common.warehouse')" class="form-item-large">
|
||||
<a-input
|
||||
:model-value="currentWarehouseLabel"
|
||||
readonly
|
||||
class="large-input readonly-field"
|
||||
:placeholder="t('warehouse.common.noCurrentWarehouse')"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<div class="form-actions">
|
||||
<a-button class="reset-btn" @click="handleReset">
|
||||
{{ t('warehouse.parcelArrival.actions.reset') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="hasPermission('Operation.ParcelArrival.Save')"
|
||||
type="primary"
|
||||
class="submit-btn"
|
||||
:loading="submitting"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ t('warehouse.parcelArrival.actions.arrive') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<div class="recent-orders-section">
|
||||
<div class="list-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<a-space wrap :size="8">
|
||||
<a-input
|
||||
v-model="keyword"
|
||||
allow-clear
|
||||
:placeholder="t('warehouse.parcelArrival.search.trackingNo')"
|
||||
style="width: 200px"
|
||||
@press-enter="handleSearch"
|
||||
/>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
{{ t('warehouse.common.search') }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
<p
|
||||
v-if="feedbackMessage"
|
||||
class="scan-feedback-text"
|
||||
:class="`is-${feedbackType}`"
|
||||
>
|
||||
{{ feedbackMessage }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<div class="pagination-controls">
|
||||
<PaginationPro
|
||||
:total-count="totalCount"
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:page-sizes="[10, 30, 50, 100]"
|
||||
@page-change="handlePageChange"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="tableContainerRef" class="list-wrap">
|
||||
<HandsontablePro
|
||||
:columns="hotColumns"
|
||||
:data="tableRows"
|
||||
:total="totalCount"
|
||||
:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:height="tableHeight"
|
||||
:loading="listLoading"
|
||||
:read-only="true"
|
||||
:stretch-columns="true"
|
||||
show-action-column
|
||||
:action-column-width="72"
|
||||
:action-renderer="actionRenderer"
|
||||
@action-click="handleActionClick"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Modal } from '@arco-design/web-vue'
|
||||
import HandsontablePro from '@/components/HandsontablePro/index.vue'
|
||||
import PaginationPro from '@/components/PaginationPro/index.vue'
|
||||
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
|
||||
import { useOperationScanFocus } from '@/composables/useOperationScanFocus'
|
||||
import { useOperationScanFeedback } from '@/composables/useOperationScanFeedback'
|
||||
import { useWarehouseAdminCustomer } from '@/composables/useWarehouseAdminCustomer'
|
||||
import {
|
||||
createWarehouseParcelArrival,
|
||||
deleteWarehouseParcelArrival,
|
||||
getWarehouseParcelArrivalList,
|
||||
} from '@/api/warehouse'
|
||||
import { useCurrentWarehouseStore } from '@/store'
|
||||
import { hasPermission } from '@/utils/rbac-permission'
|
||||
import { formatDateTime, normalizeListRow, pickField } from '@/utils/warehouseListMapper'
|
||||
import { renderWarehouseActionBtn, wrapWarehouseActions } from '@/utils/warehouseActionRenderer'
|
||||
|
||||
defineOptions({ name: 'WarehouseParcelArrival' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const { tableContainerRef, tableHeight } = useTableContainerHeight()
|
||||
const scanInputRef = ref<{ focus?: () => void } | null>(null)
|
||||
const { keepScanFocus, maybeRefocusScan } = useOperationScanFocus('parcelArrival', scanInputRef)
|
||||
const {
|
||||
voiceEnabled,
|
||||
feedbackType,
|
||||
feedbackMessage,
|
||||
clearFeedback,
|
||||
showError,
|
||||
showSuccess,
|
||||
showWarning,
|
||||
} = useOperationScanFeedback('parcelArrival')
|
||||
const {
|
||||
isAdmin,
|
||||
customerId: adminCustomerId,
|
||||
customerOptions,
|
||||
withCustomerId,
|
||||
ensureMutationCustomerId,
|
||||
} = useWarehouseAdminCustomer()
|
||||
const currentWarehouseStore = useCurrentWarehouseStore()
|
||||
|
||||
const submitting = ref(false)
|
||||
const listLoading = ref(false)
|
||||
const keyword = ref('')
|
||||
const tableRows = ref<any[]>([])
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(30)
|
||||
const totalCount = ref(0)
|
||||
|
||||
const formData = reactive({
|
||||
trackingNo: '',
|
||||
})
|
||||
|
||||
const currentWarehouseLabel = computed(() => {
|
||||
const id = currentWarehouseStore.warehouseId
|
||||
if (!id) return ''
|
||||
return currentWarehouseStore.warehouseName || String(id)
|
||||
})
|
||||
|
||||
const hotColumns = computed(() => [
|
||||
{ key: 'trackingNo', label: t('warehouse.parcelArrival.table.trackingNo'), width: 200 },
|
||||
{ key: 'warehouseName', label: t('warehouse.parcelArrival.table.warehouse'), width: 160 },
|
||||
{ key: 'createTime', label: t('warehouse.parcelArrival.table.createTime'), width: 180 },
|
||||
{ key: 'createBy', label: t('warehouse.parcelArrival.table.createBy'), width: 120 },
|
||||
])
|
||||
|
||||
function mapRow(raw: Record<string, unknown>) {
|
||||
const row = normalizeListRow(raw)
|
||||
return {
|
||||
id: pickField<number>(row, 'id', 'Id'),
|
||||
trackingNo: pickField(row, 'trackingNo', 'TrackingNo') ?? '-',
|
||||
warehouseName: pickField(row, 'warehouseName', 'WarehouseName') ?? '-',
|
||||
createTime: formatDateTime(pickField(row, 'createTime', 'CreateTime')),
|
||||
createBy: pickField(row, 'createBy', 'CreateBy') ?? '-',
|
||||
_raw: row,
|
||||
}
|
||||
}
|
||||
|
||||
async function loadList() {
|
||||
listLoading.value = true
|
||||
try {
|
||||
const { items, total } = await getWarehouseParcelArrivalList(
|
||||
withCustomerId({
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
keyword: keyword.value || undefined,
|
||||
warehouseId: currentWarehouseStore.warehouseId,
|
||||
}),
|
||||
)
|
||||
tableRows.value = (items || []).map((item: Record<string, unknown>) => mapRow(item))
|
||||
totalCount.value = Number(total || 0)
|
||||
} catch (e: any) {
|
||||
showError(e?.message || t('warehouse.parcelArrival.message.loadFailed'))
|
||||
tableRows.value = []
|
||||
totalCount.value = 0
|
||||
} finally {
|
||||
listLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
currentPage.value = page
|
||||
loadList()
|
||||
}
|
||||
|
||||
function handlePageSizeChange(size: number) {
|
||||
pageSize.value = size
|
||||
currentPage.value = 1
|
||||
loadList()
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
currentPage.value = 1
|
||||
loadList()
|
||||
}
|
||||
|
||||
function handleReset(clearStatus = true) {
|
||||
formData.trackingNo = ''
|
||||
if (clearStatus) clearFeedback()
|
||||
maybeRefocusScan()
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!hasPermission('Operation.ParcelArrival.Save')) return
|
||||
const trackingNo = formData.trackingNo?.trim()
|
||||
if (!trackingNo) {
|
||||
showWarning(t('warehouse.parcelArrival.message.trackingRequired'))
|
||||
maybeRefocusScan()
|
||||
return
|
||||
}
|
||||
const warehouseId = currentWarehouseStore.warehouseId
|
||||
if (!warehouseId) {
|
||||
showWarning(t('warehouse.common.noCurrentWarehouse'))
|
||||
maybeRefocusScan()
|
||||
return
|
||||
}
|
||||
if (isAdmin.value && ensureMutationCustomerId() == null) {
|
||||
showWarning(t('warehouse.common.selectCustomerRequired'))
|
||||
maybeRefocusScan()
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
await createWarehouseParcelArrival(
|
||||
withCustomerId({
|
||||
warehouseId,
|
||||
trackingNo,
|
||||
}),
|
||||
)
|
||||
showSuccess(t('warehouse.parcelArrival.message.arriveSuccess'))
|
||||
handleReset(false)
|
||||
currentPage.value = 1
|
||||
await loadList()
|
||||
maybeRefocusScan()
|
||||
} catch (e: any) {
|
||||
showError(e?.message || t('warehouse.parcelArrival.message.arriveFailed'))
|
||||
maybeRefocusScan()
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleDelete(row: any) {
|
||||
if (!hasPermission('Operation.ParcelArrival.Delete')) return
|
||||
Modal.confirm({
|
||||
title: t('warehouse.parcelArrival.actions.delete'),
|
||||
content: row.trackingNo,
|
||||
onOk: async () => {
|
||||
await deleteWarehouseParcelArrival(row.id)
|
||||
showSuccess(t('warehouse.parcelArrival.message.deleteSuccess'))
|
||||
await loadList()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function actionRenderer(row: any) {
|
||||
if (!hasPermission('Operation.ParcelArrival.Delete') || !row?.id) return ''
|
||||
return wrapWarehouseActions([
|
||||
renderWarehouseActionBtn('delete', t('warehouse.common.delete')),
|
||||
])
|
||||
}
|
||||
|
||||
function handleActionClick(row: any, _i: number, action?: string) {
|
||||
if (action === 'delete') handleDelete(row)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => currentWarehouseStore.warehouseId,
|
||||
() => {
|
||||
currentPage.value = 1
|
||||
loadList()
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
loadList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.waybill-container {
|
||||
padding: 16px;
|
||||
background-color: #f0f3f4;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.waybill-layout {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
gap: 16px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 24px;
|
||||
margin-bottom: 0;
|
||||
flex: 3 1 0;
|
||||
min-width: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.form-section-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin: -4px 0 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.waybill-form {
|
||||
.form-item-large {
|
||||
:deep(.arco-form-item-label) {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
}
|
||||
|
||||
.large-input {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.readonly-field {
|
||||
:deep(.arco-input) {
|
||||
background-color: #f7f8fa;
|
||||
color: #4e5969;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
|
||||
.reset-btn,
|
||||
.submit-btn {
|
||||
min-width: 100px;
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.recent-orders-section {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 0;
|
||||
flex: 7 1 0;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.list-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: 16px 16px 0;
|
||||
margin-bottom: 16px;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: nowrap;
|
||||
gap: 12px;
|
||||
|
||||
.toolbar-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.scan-feedback-text {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
|
||||
&.is-error {
|
||||
color: #f53f3f;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&.is-success {
|
||||
color: #00b42a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&.is-warning {
|
||||
color: #d46b08;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-right .pagination-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.list-wrap {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 0 16px 16px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
overscroll-behavior: contain;
|
||||
touch-action: pan-x pan-y;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.waybill-layout {
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
flex: 0 0 auto;
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.recent-orders-section {
|
||||
flex: 1;
|
||||
min-height: 360px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -7,8 +7,14 @@ import PaginationPro from '@/components/PaginationPro/index.vue'
|
||||
import ModalPro from '@/components/ModalPro/index.vue'
|
||||
import useModal from '@/components/ModalPro/useModal'
|
||||
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
|
||||
import { deleteWarehouseSku, getWarehouseSkuList, saveWarehouseSku } from '@/api/warehouse'
|
||||
import {
|
||||
deleteWarehouseSku,
|
||||
getWarehouseSkuList,
|
||||
saveWarehouseSku,
|
||||
uploadWarehouseSkuImage,
|
||||
} from '@/api/warehouse'
|
||||
import { getWarehouseList } from '@/api/system/warehouse'
|
||||
import { resolveOmsAssetUrl } from '@/api/oms/common'
|
||||
import { $baseMessage } from '@/utils'
|
||||
import { hasPermission } from '@/utils/rbac-permission'
|
||||
import {
|
||||
@ -19,6 +25,7 @@ import {
|
||||
} from '@/utils/warehouseListMapper'
|
||||
import { renderWarehouseActionBtn, wrapWarehouseActions } from '@/utils/warehouseActionRenderer'
|
||||
import { useWarehouseAdminCustomer } from '@/composables/useWarehouseAdminCustomer'
|
||||
import { useCurrentWarehouseStore } from '@/store'
|
||||
|
||||
defineOptions({ name: 'WarehouseSku' })
|
||||
|
||||
@ -31,6 +38,7 @@ const {
|
||||
withCustomerId,
|
||||
ensureMutationCustomerId,
|
||||
} = useWarehouseAdminCustomer()
|
||||
const currentWarehouseStore = useCurrentWarehouseStore()
|
||||
|
||||
const listLoading = ref(false)
|
||||
const tableRows = ref<any[]>([])
|
||||
@ -40,6 +48,8 @@ const totalCount = ref(0)
|
||||
const keyword = ref('')
|
||||
const warehouseOptions = ref<Array<{ label: string; value: number }>>([])
|
||||
const selectedWarehouseId = ref<number>()
|
||||
const imageUploading = ref(false)
|
||||
const skuImageFileList = ref<any[]>([])
|
||||
|
||||
const [registerFormModal, { openModal, closeModal }] = useModal() as any
|
||||
const saving = ref(false)
|
||||
@ -67,6 +77,17 @@ const skuVolumeText = computed(() => {
|
||||
return `${(l * w * h).toFixed(2)} cm³`
|
||||
})
|
||||
|
||||
const skuImagePreview = computed(() => resolveOmsAssetUrl(formModel.skuImageUrl))
|
||||
|
||||
const currentWarehouseLabel = computed(() => {
|
||||
const id = currentWarehouseStore.resolveDefaultWarehouseId(selectedWarehouseId.value)
|
||||
return (
|
||||
warehouseOptions.value.find((o) => o.value === id)?.label
|
||||
|| currentWarehouseStore.warehouseName
|
||||
|| (id ? String(id) : '')
|
||||
)
|
||||
})
|
||||
|
||||
const hotColumns = computed(() => [
|
||||
{ key: 'skuCode', label: t('warehouse.sku.table.skuCode'), width: 140 },
|
||||
{ key: 'skuName', label: t('warehouse.sku.table.skuName'), width: 180 },
|
||||
@ -87,6 +108,7 @@ function resetForm() {
|
||||
formModel.widthCm = undefined
|
||||
formModel.heightCm = undefined
|
||||
formModel.remark = ''
|
||||
skuImageFileList.value = []
|
||||
}
|
||||
|
||||
function changeSkuCode() {
|
||||
@ -99,6 +121,51 @@ function clearSkuSize() {
|
||||
formModel.heightCm = undefined
|
||||
}
|
||||
|
||||
function clearSkuImage() {
|
||||
formModel.skuImageUrl = ''
|
||||
skuImageFileList.value = []
|
||||
}
|
||||
|
||||
function syncImageFileList() {
|
||||
if (!formModel.skuImageUrl) {
|
||||
skuImageFileList.value = []
|
||||
return
|
||||
}
|
||||
skuImageFileList.value = [
|
||||
{
|
||||
uid: '-1',
|
||||
name: 'sku-image',
|
||||
url: skuImagePreview.value,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
async function handleSkuImageUpload(options: any) {
|
||||
const file = options?.fileItem?.file ?? options?.file
|
||||
if (!file) {
|
||||
options?.onError?.(new Error('no file'))
|
||||
return
|
||||
}
|
||||
if ((file as File).size > 5 * 1024 * 1024) {
|
||||
$baseMessage(t('warehouse.sku.message.imageTooLarge'), 'warning')
|
||||
options?.onError?.(new Error('too large'))
|
||||
return
|
||||
}
|
||||
imageUploading.value = true
|
||||
try {
|
||||
const url = await uploadWarehouseSkuImage(file as File)
|
||||
formModel.skuImageUrl = url
|
||||
syncImageFileList()
|
||||
options?.onSuccess?.(url)
|
||||
$baseMessage(t('warehouse.sku.message.uploadSuccess'), 'success')
|
||||
} catch (e: any) {
|
||||
options?.onError?.(e)
|
||||
$baseMessage(e?.message || t('warehouse.sku.message.uploadFailed'), 'error')
|
||||
} finally {
|
||||
imageUploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function mapRow(raw: Record<string, unknown>) {
|
||||
const row = normalizeListRow(raw)
|
||||
return {
|
||||
@ -164,6 +231,10 @@ function handleSearch() {
|
||||
function handleAdd() {
|
||||
if (!hasPermission('Warehouse.Sku.Save')) return
|
||||
if (isAdmin.value && ensureMutationCustomerId() == null) return
|
||||
if (!currentWarehouseStore.resolveDefaultWarehouseId(selectedWarehouseId.value)) {
|
||||
$baseMessage(t('warehouse.common.noCurrentWarehouse'), 'warning')
|
||||
return
|
||||
}
|
||||
resetForm()
|
||||
openModal(true)
|
||||
}
|
||||
@ -181,6 +252,7 @@ function handleEdit(row: any) {
|
||||
formModel.widthCm = pickField<number>(raw, 'widthCm', 'WidthCm')
|
||||
formModel.heightCm = pickField<number>(raw, 'heightCm', 'HeightCm')
|
||||
formModel.remark = String(pickField(raw, 'remark', 'Remark') ?? '')
|
||||
syncImageFileList()
|
||||
openModal(true)
|
||||
}
|
||||
|
||||
@ -322,6 +394,9 @@ onMounted(async () => {
|
||||
:label-col-props="{ span: 6 }"
|
||||
:wrapper-col-props="{ span: 16 }"
|
||||
>
|
||||
<a-form-item :label="t('warehouse.sku.form.currentWarehouse')">
|
||||
<span class="wh-form-readonly">{{ currentWarehouseLabel || '—' }}</span>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('warehouse.sku.form.skuCode')" required>
|
||||
<a-input
|
||||
v-if="!formModel.id"
|
||||
@ -337,11 +412,33 @@ onMounted(async () => {
|
||||
<a-form-item :label="t('warehouse.sku.form.imageUrl')">
|
||||
<div class="wh-form-sku-preview">
|
||||
<div class="wh-form-sku-preview__image">
|
||||
<img v-if="formModel.skuImageUrl" :src="formModel.skuImageUrl" alt="sku" />
|
||||
<img v-if="skuImagePreview" :src="skuImagePreview" alt="sku" />
|
||||
<span v-else class="wh-form-hint">—</span>
|
||||
</div>
|
||||
<div class="wh-form-sku-preview__fields">
|
||||
<a-input v-model="formModel.skuImageUrl" allow-clear />
|
||||
<a-upload
|
||||
:file-list="skuImageFileList"
|
||||
:custom-request="handleSkuImageUpload"
|
||||
:show-file-list="false"
|
||||
accept="image/jpeg,image/png,image/webp,image/gif,image/bmp"
|
||||
:disabled="imageUploading"
|
||||
>
|
||||
<a-button type="outline" :loading="imageUploading">
|
||||
{{
|
||||
formModel.skuImageUrl
|
||||
? t('warehouse.sku.form.reuploadImage')
|
||||
: t('warehouse.sku.form.uploadImage')
|
||||
}}
|
||||
</a-button>
|
||||
</a-upload>
|
||||
<a-button
|
||||
v-if="formModel.skuImageUrl"
|
||||
type="text"
|
||||
status="danger"
|
||||
@click="clearSkuImage"
|
||||
>
|
||||
{{ t('warehouse.sku.form.clearImage') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
@ -367,13 +464,28 @@ onMounted(async () => {
|
||||
<a-form-item :label="t('warehouse.sku.form.dimensions')">
|
||||
<div>
|
||||
<div class="wh-form-inline-size">
|
||||
<a-input-number v-model="formModel.lengthCm" :min="0" style="width: 110px" :placeholder="t('warehouse.sku.form.length')">
|
||||
<a-input-number
|
||||
v-model="formModel.lengthCm"
|
||||
:min="0"
|
||||
style="width: 110px"
|
||||
:placeholder="t('warehouse.sku.form.length')"
|
||||
>
|
||||
<template #suffix>cm</template>
|
||||
</a-input-number>
|
||||
<a-input-number v-model="formModel.widthCm" :min="0" style="width: 110px" :placeholder="t('warehouse.sku.form.width')">
|
||||
<a-input-number
|
||||
v-model="formModel.widthCm"
|
||||
:min="0"
|
||||
style="width: 110px"
|
||||
:placeholder="t('warehouse.sku.form.width')"
|
||||
>
|
||||
<template #suffix>cm</template>
|
||||
</a-input-number>
|
||||
<a-input-number v-model="formModel.heightCm" :min="0" style="width: 110px" :placeholder="t('warehouse.sku.form.height')">
|
||||
<a-input-number
|
||||
v-model="formModel.heightCm"
|
||||
:min="0"
|
||||
style="width: 110px"
|
||||
:placeholder="t('warehouse.sku.form.height')"
|
||||
>
|
||||
<template #suffix>cm</template>
|
||||
</a-input-number>
|
||||
<a-button type="text" @click="clearSkuSize">{{ t('warehouse.sku.form.clearSize') }}</a-button>
|
||||
|
||||
@ -77,7 +77,30 @@ export default defineConfig(({ mode }) => {
|
||||
if (warehouseSpaExact.some((p) => path === p || path === `${p}/`)) {
|
||||
return '/index.html';
|
||||
}
|
||||
if (path === '/warehouse/inboundNotice' || path.startsWith('/warehouse/inboundNotice/')) {
|
||||
// inboundNotice 页面无更深 API 子路径冲突;入库 API 为 /warehouse/inbound/*
|
||||
if (path === '/warehouse/inboundNotice' || path === '/warehouse/inboundNotice/') {
|
||||
return '/index.html';
|
||||
}
|
||||
},
|
||||
},
|
||||
'/plugin': {
|
||||
target: 'http://api.nanafa.com/admin',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
bypass(req) {
|
||||
const path = req.url?.split('?')[0] ?? '';
|
||||
if (path === '/Plugin/AddPackage' || path.startsWith('/Plugin/AddPackage/')) {
|
||||
return '/index.html';
|
||||
}
|
||||
},
|
||||
},
|
||||
'/Plugin': {
|
||||
target: 'http://api.nanafa.com/admin',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
bypass(req) {
|
||||
const path = req.url?.split('?')[0] ?? '';
|
||||
if (path === '/Plugin/AddPackage' || path.startsWith('/Plugin/AddPackage/')) {
|
||||
return '/index.html';
|
||||
}
|
||||
},
|
||||
@ -129,6 +152,7 @@ export default defineConfig(({ mode }) => {
|
||||
'/operation/inboundScan',
|
||||
'/operation/outboundScan',
|
||||
'/operation/printLabel',
|
||||
'/operation/parcelArrival',
|
||||
];
|
||||
const isSpaPage = operationSpaPrefixes.some(
|
||||
(p) => path === p || path.startsWith(`${p}/`),
|
||||
@ -267,8 +291,19 @@ export default defineConfig(({ mode }) => {
|
||||
{
|
||||
name: 'exclude-frontend-routes-from-proxy',
|
||||
configureServer(server) {
|
||||
// 仅精确匹配的 SPA 路径(页面 URL 与后端 API 前缀相同,不能用「前缀匹配」,否则 /system/dict/list 会返回 index.html)
|
||||
const frontendRoutesExact = ['/system/dict', '/system/warehouseAddress', '/system/announcement'];
|
||||
// 仅精确匹配的 SPA 路径(页面 URL 与后端 API 前缀相同,不能用「前缀匹配」,否则 /warehouse/parcel/list 会返回 index.html)
|
||||
const frontendRoutesExact = [
|
||||
'/system/dict',
|
||||
'/system/warehouseAddress',
|
||||
'/system/announcement',
|
||||
// 仓储页面路径与 API 同段,必须精确匹配(/warehouse/parcel/list 走代理)
|
||||
'/warehouse/parcel',
|
||||
'/warehouse/inventory',
|
||||
'/warehouse/sku',
|
||||
'/warehouse/inboundNotice',
|
||||
'/warehouse/binLocation',
|
||||
'/Plugin/AddPackage',
|
||||
];
|
||||
|
||||
// 前端路由路径列表(这些路径不应该被代理,应该走前端路由)
|
||||
// 所有前端路由都使用驼峰命名,以这些前缀开头的路径都是前端路由
|
||||
@ -304,12 +339,6 @@ export default defineConfig(({ mode }) => {
|
||||
'/order/batchJobs',
|
||||
'/order/platformStore',
|
||||
'/order/platformStoreOrder',
|
||||
// 仓储前端路由(页面路径与 API 子路径相同时用精确匹配,见 /warehouse 代理 bypass)
|
||||
'/warehouse/parcel',
|
||||
'/warehouse/inventory',
|
||||
'/warehouse/sku',
|
||||
'/warehouse/inboundNotice',
|
||||
'/warehouse/binLocation',
|
||||
// Xhs 相关前端路由(勿用 /xhs 前缀,否则 API 会被误判为 SPA)
|
||||
'/xhs/startService',
|
||||
'/xhs/analysis',
|
||||
@ -333,6 +362,7 @@ export default defineConfig(({ mode }) => {
|
||||
'/operation/inboundScan',
|
||||
'/operation/outboundScan',
|
||||
'/operation/printLabel',
|
||||
'/operation/parcelArrival',
|
||||
// System 相关前端路由(与 router system-management 子路径一致,避免刷新被 /system 代理到后端 404)
|
||||
'/system/users',
|
||||
'/system/roles',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user