front/src/views/plugin/AddPackage.vue

495 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>