feat(#40): add shopee product admin page
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function listShopeeProducts(params) {
|
||||
return request({ url: '/api/admin/v1/shopee-products', method: 'get', params })
|
||||
}
|
||||
|
||||
export function createShopeeProduct(data) {
|
||||
return request({ url: '/api/admin/v1/shopee-products', method: 'post', data })
|
||||
}
|
||||
|
||||
export function getShopeeProduct(productId) {
|
||||
return request({ url: `/api/admin/v1/shopee-products/${productId}`, method: 'get' })
|
||||
}
|
||||
|
||||
export function updateShopeeProduct(productId, data) {
|
||||
return request({ url: `/api/admin/v1/shopee-products/${productId}`, method: 'patch', data })
|
||||
}
|
||||
|
||||
export function linkShopeeProductPdd(productId, data) {
|
||||
return request({ url: `/api/admin/v1/shopee-products/${productId}/link-pdd`, method: 'post', data })
|
||||
}
|
||||
|
||||
export function restoreShopeeProduct(productId, data) {
|
||||
return request({ url: `/api/admin/v1/shopee-products/${productId}/restore`, method: 'post', data })
|
||||
}
|
||||
|
||||
export function addShopeeSpecValue(productId, data) {
|
||||
return request({ url: `/api/admin/v1/shopee-products/${productId}/specs/values`, method: 'post', data })
|
||||
}
|
||||
|
||||
export function removeShopeeSpecValue(productId, data) {
|
||||
return request({ url: `/api/admin/v1/shopee-products/${productId}/specs/values`, method: 'delete', data })
|
||||
}
|
||||
|
||||
export function setShopeeSpecMapping(productId, data) {
|
||||
return request({ url: `/api/admin/v1/shopee-products/${productId}/specs/mapping`, method: 'put', data })
|
||||
}
|
||||
|
||||
export function clearShopeeSpecMapping(productId, data) {
|
||||
return request({ url: `/api/admin/v1/shopee-products/${productId}/specs/mapping`, method: 'delete', data })
|
||||
}
|
||||
|
||||
export function confirmShopeeSpecMapping(productId, data) {
|
||||
return request({ url: `/api/admin/v1/shopee-products/${productId}/specs/mapping/confirm`, method: 'post', data })
|
||||
}
|
||||
|
||||
export function confirmShopeeExactMatches(productId, data) {
|
||||
return request({ url: `/api/admin/v1/shopee-products/${productId}/specs/mapping/confirm-exact-matches`, method: 'post', data })
|
||||
}
|
||||
|
||||
export function batchDeleteShopeeProducts(data) {
|
||||
return request({ url: '/api/admin/v1/shopee-products/batch-delete', method: 'post', data })
|
||||
}
|
||||
@@ -106,6 +106,19 @@ export const constantRoutes = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/shopee-products',
|
||||
component: Layout,
|
||||
redirect: '/shopee-products/index',
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
component: () => import('@/views/goauto/shopee-products/index'),
|
||||
name: 'GoAutoShopeeProducts',
|
||||
meta: { title: '虾皮商品', icon: 'goods' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/collection-rules',
|
||||
component: Layout,
|
||||
|
||||
@@ -0,0 +1,378 @@
|
||||
<template>
|
||||
<BasicLayout>
|
||||
<template #wrapper>
|
||||
<el-card class="page-card" shadow="never">
|
||||
<div class="page-heading">
|
||||
<div><h1>虾皮商品</h1><p>维护虾皮(Shopee)商品档案、关联 PDD 商品与规格映射;本页不包含采购任务、订单和物流字段。</p></div>
|
||||
<div class="heading-actions">
|
||||
<el-button :icon="Delete" type="danger" plain :disabled="selectedProducts.length === 0" @click="openBatchDelete">删除{{ selectedProducts.length ? ` (${selectedProducts.length})` : '' }}</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="openCreate">添加虾皮商品</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-form :model="query" :inline="true" class="search-form" @submit.prevent="search">
|
||||
<el-form-item label="搜索"><el-input v-model="query.keyword" placeholder="虾皮商品ID、标题或店铺" clearable @keyup.enter="search" /></el-form-item>
|
||||
<el-form-item label="范围"><el-select v-model="query.status" style="width: 150px"><el-option label="全部(不含已删除)" value="" /><el-option label="已删除" value="deleted" /></el-select></el-form-item>
|
||||
<el-form-item><el-button type="primary" :icon="Search" @click="search">查询</el-button><el-button :icon="RefreshLeft" @click="reset">重置</el-button></el-form-item>
|
||||
</el-form>
|
||||
<el-alert v-if="query.status === 'deleted'" title="当前显示已删除商品,可逐条恢复。恢复后原有 PDD 关联与规格映射保持不变。" type="warning" :closable="false" show-icon class="notice" />
|
||||
<el-table ref="productTable" v-loading="loading" :data="products" row-key="id" border stripe empty-text="暂无虾皮商品" @selection-change="handleSelectionChange">
|
||||
<el-table-column v-if="query.status !== 'deleted'" type="selection" width="48" />
|
||||
<el-table-column label="参考图" width="76"><template #default="{ row }"><el-image v-if="row.imageUrl" :src="row.imageUrl" fit="cover" class="thumb" :preview-src-list="[row.imageUrl]" preview-teleported /><div v-else class="thumb placeholder">无图</div></template></el-table-column>
|
||||
<el-table-column label="虾皮商品ID" prop="shopeeItemId" min-width="140" />
|
||||
<el-table-column label="标题 / 店铺" min-width="220"><template #default="{ row }"><div class="primary">{{ row.title || '资料待完善' }}</div><div class="muted">{{ row.shopName || '尚未填写店铺' }}</div></template></el-table-column>
|
||||
<el-table-column label="售价" width="120"><template #default="{ row }">{{ priceText(row) }}</template></el-table-column>
|
||||
<el-table-column label="PDD 商品" min-width="160"><template #default="{ row }"><a v-if="row.pddProductId" class="link" href="javascript:void(0)" @click="openPddDetail(row.pddProductId)">PDD-{{ row.pddProductId }} ↗</a><span v-else class="muted">未关联</span></template></el-table-column>
|
||||
<el-table-column label="映射状态" min-width="180"><template #default="{ row }"><el-tag :type="mappingMeta(row).type">{{ mappingMeta(row).label }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right"><template #default="{ row }"><el-button v-if="row.deleted" type="primary" link @click="restore(row)">恢复</el-button><el-button v-else type="primary" link @click="openDetail(row.id)">详情</el-button></template></el-table-column>
|
||||
</el-table>
|
||||
<pagination v-show="total > 0" v-model:current-page="query.page" v-model:page-size="query.pageSize" :total="total" @pagination="load" />
|
||||
<p class="scope-note">本页不支持按虾皮订单号搜索——商品档案不含订单数据;订单相关字段在 SYB 商品模块与采购任务模块查看。</p>
|
||||
</el-card>
|
||||
|
||||
<!-- 创建 -->
|
||||
<el-dialog v-model="createDialog.open" title="新建虾皮商品" width="640px" :close-on-click-modal="false">
|
||||
<el-form ref="createForm" :model="createData" :rules="createRules" label-position="top">
|
||||
<el-form-item label="虾皮商品ID" prop="shopeeItemId"><el-input v-model="createData.shopeeItemId" maxlength="64" placeholder="全局唯一,创建后不可修改" /></el-form-item>
|
||||
<div class="form-grid"><el-form-item label="商品标题"><el-input v-model="createData.title" maxlength="500" /></el-form-item><el-form-item label="店铺"><el-input v-model="createData.shopName" maxlength="255" /></el-form-item></div>
|
||||
<el-form-item label="规格值(可选)"><div class="spec-quick-add"><span class="muted">只录虾皮侧的颜色和尺码名称,不涉及 PDD;留空也可以保存,之后在详情页补充。</span>
|
||||
<div class="quick-row"><span class="quick-label">颜色</span><el-input v-model="quickColor" size="small" placeholder="例如 浅蓝色" @keyup.enter="addQuickValue('color')" /><el-button size="small" @click="addQuickValue('color')">+ 添加</el-button></div>
|
||||
<div class="chips"><el-tag v-for="(v, i) in colorValues" :key="v" closable size="small" @close="colorValues.splice(i, 1)">{{ v }}</el-tag></div>
|
||||
<div class="quick-row"><span class="quick-label">尺码</span><el-input v-model="quickSize" size="small" placeholder="例如 XL" @keyup.enter="addQuickValue('size')" /><el-button size="small" @click="addQuickValue('size')">+ 添加</el-button></div>
|
||||
<div class="chips"><el-tag v-for="(v, i) in sizeValues" :key="v" closable size="small" @close="sizeValues.splice(i, 1)">{{ v }}</el-tag></div>
|
||||
</div></el-form-item>
|
||||
<el-form-item label="PDD 商品(选填)"><el-button @click="openPddPicker('create')">{{ createData.pddProductId ? `已选择 PDD-${createData.pddProductId}` : '搜索并选择' }}</el-button><span v-if="createData.pddProductId" class="muted" style="margin-left:8px">选择后仍需完成规格映射才能采购</span></el-form-item>
|
||||
</el-form>
|
||||
<template #footer><el-button @click="createDialog.open = false">取消</el-button><el-button type="primary" :loading="createDialog.saving" @click="submitCreate">保存</el-button></template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- PDD 选择器 -->
|
||||
<el-dialog v-model="pddPicker.open" title="搜索并选择 PDD 商品" width="720px" :close-on-click-modal="false">
|
||||
<el-input v-model="pddPicker.keyword" placeholder="goods_id、标题或店铺" clearable @keyup.enter="searchPddPicker" />
|
||||
<el-table v-loading="pddPicker.loading" :data="pddPicker.items" border size="small" style="margin-top:12px" max-height="360" empty-text="请输入关键词查询">
|
||||
<el-table-column label="goods_id" prop="goodsId" width="150" />
|
||||
<el-table-column label="标题 / 店铺" min-width="200"><template #default="{ row }"><div>{{ row.title || '资料待完善' }}</div><div class="muted">{{ row.shopName }}</div></template></el-table-column>
|
||||
<el-table-column label="状态" width="100"><template #default="{ row }"><el-tag :type="row.status === 'disabled' ? 'info' : 'success'">{{ row.status === 'disabled' ? '已停用' : '可用' }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="90"><template #default="{ row }"><el-button type="primary" link :disabled="row.status === 'disabled'" @click="pickPdd(row)">选择</el-button></template></el-table-column>
|
||||
</el-table>
|
||||
<template #footer><el-button @click="pddPicker.open = false">取消</el-button></template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 批量删除确认 -->
|
||||
<el-dialog v-model="batchDelete.open" :title="batchDelete.step === 'confirm' ? '删除虾皮商品?' : '删除结果'" width="640px" :close-on-click-modal="false" @closed="batchDelete = emptyBatchDelete()">
|
||||
<template v-if="batchDelete.step === 'confirm'">
|
||||
<el-alert title="软删除:记录保留在数据库中,不会物理删除。删除后不出现在默认列表,不能被关联和映射;可在筛选器选择「已删除」查看并恢复。" type="warning" :closable="false" show-icon class="notice" />
|
||||
<p>将对选中的 {{ batchDelete.products.length }} 条虾皮商品执行删除;被 SYB 明细或采购任务引用的商品会被服务端跳过,不影响其他条目。</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-alert :title="`已删除 ${batchDelete.deletedCount} 条,跳过 ${batchDelete.skippedCount} 条`" :type="batchDelete.skippedCount ? 'warning' : 'success'" :closable="false" show-icon class="notice" />
|
||||
<el-table :data="batchDelete.results" border size="small" max-height="320">
|
||||
<el-table-column label="ID" prop="id" width="90" />
|
||||
<el-table-column label="结果" width="100"><template #default="{ row }"><el-tag :type="row.status === 'deleted' ? 'success' : 'info'">{{ row.status === 'deleted' ? '已删除' : '已跳过' }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="说明" min-width="200"><template #default="{ row }">{{ row.reason || '—' }}</template></el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
<template #footer>
|
||||
<template v-if="batchDelete.step === 'confirm'"><el-button @click="batchDelete.open = false">取消</el-button><el-button type="danger" :loading="batchDelete.saving" @click="submitBatchDelete">确认删除 {{ batchDelete.products.length }} 条</el-button></template>
|
||||
<el-button v-else type="primary" @click="batchDelete.open = false; load()">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 详情 -->
|
||||
<el-drawer v-model="detail.open" title="虾皮商品详情" size="880px">
|
||||
<div v-loading="detail.loading" class="drawer-body">
|
||||
<template v-if="detail.product">
|
||||
<div class="drawer-actions"><el-tag :type="mappingMeta(detail.product).type">{{ mappingMeta(detail.product).label }}</el-tag><el-button type="primary" @click="openEdit">编辑档案</el-button></div>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="虾皮商品ID">{{ detail.product.shopeeItemId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="店铺">{{ detail.product.shopName || '未填写' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="标题" :span="2">{{ detail.product.title || '未填写' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="售价">{{ priceText(detail.product) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="关联 PDD 商品"><a v-if="detail.product.pddProductId" class="link" href="javascript:void(0)" @click="openPddDetail(detail.product.pddProductId)">PDD-{{ detail.product.pddProductId }} ↗</a><span v-else class="muted">未关联</span> <el-button link type="primary" size="small" @click="openPddPicker('link')">{{ detail.product.pddProductId ? '更换' : '关联' }}</el-button></el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<p v-if="detail.product.pddProductId && detail.product.sharedByPddCount > 1" class="muted shared-note">该 PDD 商品当前被 {{ detail.product.sharedByPddCount }} 个虾皮商品共用,允许共用。</p>
|
||||
|
||||
<div class="section-heading"><h3>规格映射</h3>
|
||||
<el-button size="small" :disabled="!hasPendingExactMatch" @click="confirmAllExact">一键确认全部精确匹配</el-button>
|
||||
</div>
|
||||
<el-alert title="映射由人工确认,系统只给候选。名称完全相等时自动预填并置于「待确认」;名称相同不代表实物尺寸相同(如台湾码与大陆码),确认前请核对。" type="info" :closable="false" show-icon class="notice" />
|
||||
<el-empty v-if="!detail.product.specs.length" description="尚未添加任何规格值" />
|
||||
<div v-for="dimension in detail.product.specs" :key="dimension.name" class="spec-block">
|
||||
<div class="spec-title">{{ dimension.name }} <el-tag size="small" effect="plain">{{ roleLabel(dimension.role) }}</el-tag></div>
|
||||
<el-table :data="dimension.values" border size="small">
|
||||
<el-table-column label="虾皮值" min-width="120"><template #default="{ row }">{{ row.name }}<div class="muted">{{ row.source === 'manual' ? '人工添加' : 'SYB 导入' }}</div></template></el-table-column>
|
||||
<el-table-column label="映射到 PDD 值" min-width="150"><template #default="{ row }"><el-input v-model="row.pddValueDraft" size="small" placeholder="填写 PDD 规格值" /></template></el-table-column>
|
||||
<el-table-column label="来源 / 状态" width="150"><template #default="{ row }"><span v-if="row.mapping"><el-tag size="small" :type="mappingSourceType(row.mapping.source)">{{ mappingSourceLabel(row.mapping.source) }}</el-tag><div class="muted">{{ row.mapping.status === 'confirmed' ? '已确认' : '待确认' }}</div></span><span v-else class="muted">未映射</span></template></el-table-column>
|
||||
<el-table-column label="操作" width="200"><template #default="{ row }">
|
||||
<el-button v-if="row.mapping && row.mapping.status === 'pending'" size="small" @click="confirmMapping(dimension.name, row)">确认</el-button>
|
||||
<el-button size="small" @click="saveMapping(dimension.name, row)">{{ row.mapping ? '更新' : '保存' }}</el-button>
|
||||
<el-button v-if="row.mapping" size="small" @click="clearMapping(dimension.name, row)">清除</el-button>
|
||||
<el-button v-if="row.source === 'manual'" size="small" type="danger" link @click="removeValue(dimension.name, row)">删除</el-button>
|
||||
</template></el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="section-heading"><h3>添加规格值</h3></div>
|
||||
<div class="quick-row"><span class="quick-label">维度</span><el-select v-model="addValueForm.role" size="small" style="width:110px"><el-option label="颜色" value="color" /><el-option label="尺码" value="size" /><el-option label="其他" value="other" /></el-select><el-input v-model="addValueForm.dimension" size="small" placeholder="维度名称,如 颜色 / 尺码" style="width:160px" /><el-input v-model="addValueForm.name" size="small" placeholder="规格值名称" style="width:160px" /><el-button size="small" type="primary" @click="submitAddValue">+ 添加规格值</el-button></div>
|
||||
</template>
|
||||
</div>
|
||||
</el-drawer>
|
||||
|
||||
<!-- 编辑档案 -->
|
||||
<el-drawer v-model="edit.open" title="编辑虾皮商品档案" size="560px" :close-on-click-modal="false">
|
||||
<div v-loading="edit.loading" class="drawer-body">
|
||||
<el-form ref="editForm" :model="editData" label-position="top">
|
||||
<el-form-item label="商品标题"><el-input v-model="editData.title" maxlength="500" /></el-form-item>
|
||||
<el-form-item label="店铺"><el-input v-model="editData.shopName" maxlength="255" /></el-form-item>
|
||||
<el-form-item label="参考图 URL"><el-input v-model="editData.imageUrl" maxlength="2048" placeholder="SYB 提供的图片 URL" /></el-form-item>
|
||||
<el-form-item label="售价(参考,分)"><el-input-number v-model="editData.salePriceCent" :min="0" :precision="0" /><span class="muted" style="margin-left:8px">币种由系统配置统一设置,不逐商品选择</span></el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer><el-button @click="edit.open = false">取消</el-button><el-button type="primary" :loading="edit.saving" @click="saveEdit">保存修改</el-button></template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
</BasicLayout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Plus, RefreshLeft, Search, Delete } from '@element-plus/icons-vue'
|
||||
import {
|
||||
listShopeeProducts, createShopeeProduct, getShopeeProduct, updateShopeeProduct,
|
||||
linkShopeeProductPdd, restoreShopeeProduct, addShopeeSpecValue, removeShopeeSpecValue,
|
||||
setShopeeSpecMapping, clearShopeeSpecMapping, confirmShopeeSpecMapping, confirmShopeeExactMatches,
|
||||
batchDeleteShopeeProducts
|
||||
} from '@/api/goauto/shopee-products'
|
||||
import { listPddProducts } from '@/api/goauto/pdd-products'
|
||||
|
||||
export default {
|
||||
name: 'GoAutoShopeeProducts',
|
||||
setup() { return { Plus, RefreshLeft, Search, Delete } },
|
||||
data() {
|
||||
return {
|
||||
loading: false, products: [], selectedProducts: [], total: 0,
|
||||
query: { page: 1, pageSize: 20, keyword: '', status: '' },
|
||||
createDialog: { open: false, saving: false },
|
||||
createData: this.emptyCreate(),
|
||||
createRules: { shopeeItemId: [{ required: true, message: '请输入虾皮商品ID', trigger: 'blur' }, { max: 64, message: '不能超过 64 个字符', trigger: 'blur' }] },
|
||||
quickColor: '', quickSize: '', colorValues: [], sizeValues: [],
|
||||
pddPicker: { open: false, loading: false, keyword: '', items: [], target: 'create' },
|
||||
batchDelete: this.emptyBatchDelete(),
|
||||
detail: { open: false, loading: false, product: null },
|
||||
edit: { open: false, loading: false, saving: false, productId: null },
|
||||
editData: { title: '', shopName: '', imageUrl: '', salePriceCent: null },
|
||||
addValueForm: { dimension: '', role: 'other', name: '' }
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasPendingExactMatch() {
|
||||
if (!this.detail.product) return false
|
||||
return this.detail.product.specs.some(d => d.values.some(v => v.mapping && v.mapping.source === 'exact_match' && v.mapping.status === 'pending'))
|
||||
}
|
||||
},
|
||||
created() { this.load() },
|
||||
methods: {
|
||||
emptyCreate() { return { shopeeItemId: '', title: '', shopName: '', pddProductId: null } },
|
||||
emptyBatchDelete() { return { open: false, saving: false, step: 'confirm', products: [], results: [], deletedCount: 0, skippedCount: 0 } },
|
||||
async load() {
|
||||
this.loading = true
|
||||
this.selectedProducts = []
|
||||
this.$refs.productTable?.clearSelection()
|
||||
try {
|
||||
const r = await listShopeeProducts(this.query)
|
||||
this.products = r.data.items
|
||||
this.total = r.data.total
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
search() { this.query.page = 1; this.load() },
|
||||
reset() { this.query = { page: 1, pageSize: 20, keyword: '', status: '' }; this.load() },
|
||||
handleSelectionChange(rows) { this.selectedProducts = rows },
|
||||
priceText(row) { if (row.salePriceCent === null || row.salePriceCent === undefined) return '—'; return `${row.currency || ''} ${(row.salePriceCent / 100).toFixed(2)}` },
|
||||
roleLabel(role) { return { color: '颜色', size: '尺码', other: '其他' }[role] || role },
|
||||
mappingSourceLabel(source) { return { manual: '人工', exact_match: '精确匹配', ai_match: 'AI 建议' }[source] || source },
|
||||
mappingSourceType(source) { return { manual: 'success', exact_match: 'warning', ai_match: '' }[source] || 'info' },
|
||||
mappingMeta(product) {
|
||||
if (!product) return { label: '-', type: 'info' }
|
||||
if (!product.pddProductId) return { label: '未关联 PDD 商品', type: 'info' }
|
||||
const values = (product.specs || []).flatMap(d => d.values)
|
||||
if (!values.length) return { label: '待完善', type: 'warning' }
|
||||
const confirmed = values.filter(v => v.mapping && v.mapping.status === 'confirmed').length
|
||||
return confirmed === values.length ? { label: '可采购', type: 'success' } : { label: `待完善 · ${confirmed}/${values.length} 已确认`, type: 'warning' }
|
||||
},
|
||||
|
||||
// ---------------- 创建 ----------------
|
||||
openCreate() {
|
||||
this.createDialog = { open: true, saving: false }
|
||||
this.createData = this.emptyCreate()
|
||||
this.colorValues = []; this.sizeValues = []; this.quickColor = ''; this.quickSize = ''
|
||||
this.$nextTick(() => this.$refs.createForm?.clearValidate())
|
||||
},
|
||||
addQuickValue(role) {
|
||||
if (role === 'color') { const v = this.quickColor.trim(); if (v && !this.colorValues.includes(v)) this.colorValues.push(v); this.quickColor = '' } else { const v = this.quickSize.trim(); if (v && !this.sizeValues.includes(v)) this.sizeValues.push(v); this.quickSize = '' }
|
||||
},
|
||||
async submitCreate() {
|
||||
if (!await this.$refs.createForm.validate().catch(() => false)) return
|
||||
this.createDialog.saving = true
|
||||
try {
|
||||
const specs = []
|
||||
if (this.colorValues.length) specs.push({ name: '颜色', role: 'color', values: this.colorValues.map(name => ({ name, source: 'manual' })) })
|
||||
if (this.sizeValues.length) specs.push({ name: '尺码', role: 'size', values: this.sizeValues.map(name => ({ name, source: 'manual' })) })
|
||||
const r = await createShopeeProduct({
|
||||
requestId: crypto.randomUUID(), shopeeItemId: this.createData.shopeeItemId.trim(),
|
||||
title: this.createData.title.trim(), shopName: this.createData.shopName.trim(),
|
||||
pddProductId: this.createData.pddProductId || undefined, specs
|
||||
})
|
||||
ElMessage.success('虾皮商品已添加')
|
||||
this.createDialog.open = false
|
||||
await this.load()
|
||||
await this.openDetail(r.data.product.id)
|
||||
} finally {
|
||||
this.createDialog.saving = false
|
||||
}
|
||||
},
|
||||
|
||||
// ---------------- PDD 选择 ----------------
|
||||
openPddPicker(target) { this.pddPicker = { open: true, loading: false, keyword: '', items: [], target } },
|
||||
async searchPddPicker() {
|
||||
this.pddPicker.loading = true
|
||||
try {
|
||||
const r = await listPddProducts({ page: 1, pageSize: 20, keyword: this.pddPicker.keyword })
|
||||
this.pddPicker.items = r.data.items
|
||||
} finally {
|
||||
this.pddPicker.loading = false
|
||||
}
|
||||
},
|
||||
async pickPdd(row) {
|
||||
if (this.pddPicker.target === 'create') {
|
||||
this.createData.pddProductId = row.id
|
||||
this.pddPicker.open = false
|
||||
return
|
||||
}
|
||||
try {
|
||||
const r = await linkShopeeProductPdd(this.detail.product.id, { requestId: crypto.randomUUID(), pddProductId: row.id })
|
||||
this.detail.product = r.data.product
|
||||
ElMessage.success('已关联 PDD 商品')
|
||||
} finally {
|
||||
this.pddPicker.open = false
|
||||
}
|
||||
},
|
||||
openPddDetail(pddProductId) {
|
||||
const route = this.$router.resolve({ path: '/pdd-products/index', query: { productId: pddProductId }})
|
||||
window.open(route.href, '_blank', 'noopener')
|
||||
},
|
||||
|
||||
// ---------------- 详情 / 映射 ----------------
|
||||
async openDetail(id) {
|
||||
this.detail = { open: true, loading: true, product: null }
|
||||
try {
|
||||
const r = await getShopeeProduct(id)
|
||||
this.detail.product = this.withDraftMappings(r.data.product)
|
||||
} finally {
|
||||
this.detail.loading = false
|
||||
}
|
||||
},
|
||||
withDraftMappings(product) {
|
||||
product.specs = (product.specs || []).map(d => ({ ...d, values: d.values.map(v => ({ ...v, pddValueDraft: v.mapping ? v.mapping.pddValue : '' })) }))
|
||||
return product
|
||||
},
|
||||
async refreshDetail() {
|
||||
const r = await getShopeeProduct(this.detail.product.id)
|
||||
this.detail.product = this.withDraftMappings(r.data.product)
|
||||
await this.load()
|
||||
},
|
||||
async submitAddValue() {
|
||||
const dimension = this.addValueForm.dimension.trim()
|
||||
const name = this.addValueForm.name.trim()
|
||||
if (!dimension || !name) { ElMessage.warning('请填写维度名称和规格值'); return }
|
||||
await addShopeeSpecValue(this.detail.product.id, { requestId: crypto.randomUUID(), dimension, role: this.addValueForm.role, name })
|
||||
this.addValueForm = { dimension: '', role: 'other', name: '' }
|
||||
await this.refreshDetail()
|
||||
},
|
||||
async removeValue(dimension, row) {
|
||||
await removeShopeeSpecValue(this.detail.product.id, { requestId: crypto.randomUUID(), dimension, name: row.name })
|
||||
await this.refreshDetail()
|
||||
},
|
||||
async saveMapping(dimension, row) {
|
||||
const pddValue = (row.pddValueDraft || '').trim()
|
||||
if (!pddValue) { ElMessage.warning('请填写映射到的 PDD 规格值'); return }
|
||||
const source = pddValue === row.name ? 'exact_match' : 'manual'
|
||||
await setShopeeSpecMapping(this.detail.product.id, { requestId: crypto.randomUUID(), dimension, valueName: row.name, pddValue, source })
|
||||
await this.refreshDetail()
|
||||
},
|
||||
async clearMapping(dimension, row) {
|
||||
await clearShopeeSpecMapping(this.detail.product.id, { requestId: crypto.randomUUID(), dimension, valueName: row.name })
|
||||
await this.refreshDetail()
|
||||
},
|
||||
async confirmMapping(dimension, row) {
|
||||
await confirmShopeeSpecMapping(this.detail.product.id, { requestId: crypto.randomUUID(), dimension, valueName: row.name })
|
||||
await this.refreshDetail()
|
||||
},
|
||||
async confirmAllExact() {
|
||||
await confirmShopeeExactMatches(this.detail.product.id, { requestId: crypto.randomUUID() })
|
||||
ElMessage.success('已确认全部精确匹配')
|
||||
await this.refreshDetail()
|
||||
},
|
||||
|
||||
// ---------------- 编辑档案 ----------------
|
||||
openEdit() {
|
||||
const p = this.detail.product
|
||||
this.edit = { open: true, loading: false, saving: false, productId: p.id }
|
||||
this.editData = { title: p.title || '', shopName: p.shopName || '', imageUrl: p.imageUrl || '', salePriceCent: p.salePriceCent }
|
||||
},
|
||||
async saveEdit() {
|
||||
this.edit.saving = true
|
||||
try {
|
||||
await updateShopeeProduct(this.edit.productId, { requestId: crypto.randomUUID(), ...this.editData })
|
||||
ElMessage.success('虾皮商品资料已保存')
|
||||
this.edit.open = false
|
||||
await this.refreshDetail()
|
||||
} finally {
|
||||
this.edit.saving = false
|
||||
}
|
||||
},
|
||||
|
||||
// ---------------- 删除 / 恢复 ----------------
|
||||
openBatchDelete() { this.batchDelete = { ...this.emptyBatchDelete(), open: true, products: this.selectedProducts.map(item => ({ ...item })) } },
|
||||
async submitBatchDelete() {
|
||||
this.batchDelete.saving = true
|
||||
try {
|
||||
const r = await batchDeleteShopeeProducts({ requestId: crypto.randomUUID(), ids: this.batchDelete.products.map(item => item.id) })
|
||||
this.batchDelete.results = r.data.results
|
||||
this.batchDelete.deletedCount = r.data.results.filter(x => x.status === 'deleted').length
|
||||
this.batchDelete.skippedCount = r.data.results.filter(x => x.status === 'skipped').length
|
||||
this.batchDelete.step = 'result'
|
||||
} finally {
|
||||
this.batchDelete.saving = false
|
||||
}
|
||||
},
|
||||
async restore(row) {
|
||||
await restoreShopeeProduct(row.id, { requestId: crypto.randomUUID() })
|
||||
ElMessage.success('已恢复')
|
||||
await this.load()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-card { min-height: calc(100vh - 124px); }
|
||||
.page-heading { display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:24px;h1{margin:0 0 6px;font-size:24px;color:#1f2937}p{margin:0;color:#606266;line-height:1.5} }
|
||||
.heading-actions { display:flex;gap:12px;flex-shrink:0 }
|
||||
.search-form{padding:16px 16px 0;margin-bottom:16px;border:1px solid #e5e7eb;border-radius:8px;background:#f8fafc}
|
||||
.primary{font-weight:600;color:#1f2937}.muted{font-size:12px;color:#909399}
|
||||
.thumb{width:48px;height:48px;border-radius:4px;object-fit:cover}.thumb.placeholder{display:flex;align-items:center;justify-content:center;background:#f1f5f9;color:#909399;font-size:11px}
|
||||
.link{color:#1677ff;cursor:pointer}
|
||||
.scope-note{margin-top:12px;font-size:12px;color:#b91c1c}
|
||||
.notice{margin-bottom:16px}
|
||||
.drawer-body{padding:0 4px 24px}
|
||||
.drawer-actions{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:16px}
|
||||
.shared-note{margin:8px 0 0}
|
||||
.section-heading{display:flex;align-items:center;justify-content:space-between;margin:24px 0 12px;h3{margin:0}}
|
||||
.spec-block{margin-bottom:20px}.spec-title{display:flex;align-items:center;gap:8px;margin-bottom:8px;font-weight:600}
|
||||
.spec-quick-add{display:flex;flex-direction:column;gap:8px;width:100%}
|
||||
.quick-row{display:flex;align-items:center;gap:8px;margin:6px 0}
|
||||
.quick-label{font-size:12px;color:#606266;width:32px}
|
||||
.chips{display:flex;flex-wrap:wrap;gap:6px}
|
||||
.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:0 20px}
|
||||
@media(max-width:768px){.page-heading{align-items:stretch;flex-direction:column}.form-grid{grid-template-columns:1fr}}
|
||||
</style>
|
||||
Reference in New Issue
Block a user