495 lines
14 KiB
Vue
495 lines
14 KiB
Vue
<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>
|