feat(goods): add editable custom products
This commit is contained in:
@@ -8,6 +8,8 @@ import type {
|
||||
BatchPriorityRequest,
|
||||
GoodsFilter,
|
||||
PaginatedResult,
|
||||
CreateCustomGoodRequest,
|
||||
UpdateCustomGoodContentRequest,
|
||||
} from '@/types'
|
||||
|
||||
export const goodsApi = {
|
||||
@@ -26,6 +28,14 @@ export const goodsApi = {
|
||||
return request.post<any, Good>('/goods', data)
|
||||
},
|
||||
|
||||
createCustomGood: (data: CreateCustomGoodRequest) => {
|
||||
return request.post<any, GoodDetail>('/goods/custom', data)
|
||||
},
|
||||
|
||||
updateCustomGoodContent: (id: string, data: UpdateCustomGoodContentRequest) => {
|
||||
return request.patch<any, GoodDetail>(`/goods/${id}/custom-content`, data)
|
||||
},
|
||||
|
||||
// Update good
|
||||
updateGood: (id: string, data: UpdateGoodRequest) => {
|
||||
return request.patch<any, Good>(`/goods/${id}`, data)
|
||||
|
||||
Vendored
+3
@@ -11,6 +11,7 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
ElAlert: typeof import('element-plus/es')['ElAlert']
|
||||
ElAside: typeof import('element-plus/es')['ElAside']
|
||||
ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
|
||||
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
|
||||
@@ -33,6 +34,7 @@ declare module 'vue' {
|
||||
ElImage: typeof import('element-plus/es')['ElImage']
|
||||
ElImageViewer: typeof import('element-plus/es')['ElImageViewer']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||
ElMain: typeof import('element-plus/es')['ElMain']
|
||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
@@ -42,6 +44,7 @@ declare module 'vue' {
|
||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||
|
||||
@@ -68,6 +68,49 @@ export interface OriginGoodVariant {
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface CustomGoodVariantRequest {
|
||||
sku: string
|
||||
sizeId?: string | null
|
||||
sizeName?: string | null
|
||||
colorId?: string | null
|
||||
colorName?: string | null
|
||||
colorHex?: string | null
|
||||
imageUrl?: string | null
|
||||
price?: string | null
|
||||
originalPrice?: string | null
|
||||
weightG?: string | null
|
||||
boxLengthCm?: string | null
|
||||
boxWidthCm?: string | null
|
||||
boxHeightCm?: string | null
|
||||
designData?: Record<string, unknown> | null
|
||||
enabled?: boolean
|
||||
sortOrder?: number
|
||||
}
|
||||
|
||||
export interface CustomGoodDetailRequest {
|
||||
productCode?: string | null
|
||||
englishName?: string | null
|
||||
productionCycleHours?: number | null
|
||||
minWeightG?: string | null
|
||||
productionProcess?: string | null
|
||||
materialDescription?: string | null
|
||||
blankDesignUrl?: string | null
|
||||
detailsPageVideoUrl?: string | null
|
||||
textureName?: string | null
|
||||
reminder?: string | null
|
||||
productPerformance?: string | null
|
||||
applicableScenarios?: string | null
|
||||
washingInstructions?: string | null
|
||||
specialDescription?: string | null
|
||||
designExplanation?: string | null
|
||||
designArea?: string | null
|
||||
pictureRequest?: string | null
|
||||
sizeChart?: Record<string, unknown> | null
|
||||
packageSpecs?: Record<string, unknown> | null
|
||||
options?: Record<string, unknown> | null
|
||||
media?: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
export interface GoodDetail extends Good {
|
||||
originDetail: OriginGoodDetail | null
|
||||
variants: OriginGoodVariant[]
|
||||
@@ -84,6 +127,27 @@ export interface CreateGoodRequest {
|
||||
goodPriority?: number
|
||||
}
|
||||
|
||||
export interface CreateCustomGoodRequest {
|
||||
goodName: string
|
||||
goodImage?: string
|
||||
goodPrice?: string | null
|
||||
countryId: number
|
||||
categoryId: number
|
||||
tagIds?: number[]
|
||||
positionId?: number
|
||||
goodPriority?: number
|
||||
detail?: CustomGoodDetailRequest
|
||||
variants?: CustomGoodVariantRequest[]
|
||||
}
|
||||
|
||||
export interface UpdateCustomGoodContentRequest {
|
||||
goodName?: string
|
||||
goodImage?: string | null
|
||||
goodPrice?: string | null
|
||||
detail?: CustomGoodDetailRequest
|
||||
variants?: CustomGoodVariantRequest[]
|
||||
}
|
||||
|
||||
export interface UpdateGoodRequest {
|
||||
goodName?: string
|
||||
goodImage?: string | null
|
||||
@@ -271,6 +335,8 @@ export interface OriginGood {
|
||||
goodImage: string | null
|
||||
goodPrice: string | null
|
||||
sdsGoodId: string
|
||||
source: 'SDS' | 'CUSTOM'
|
||||
isCustom: boolean
|
||||
sdsCategoryId: string | null
|
||||
delisted?: boolean
|
||||
createdAt: string
|
||||
|
||||
@@ -170,7 +170,8 @@ function goodToNode(g: Good): any {
|
||||
originGoodImage: g.originGood?.goodImage || null,
|
||||
originGoodPrice: g.originGood?.goodPrice || null,
|
||||
sdsGoodId: g.originGood?.sdsGoodId || null,
|
||||
originDelisted: !!g.originGoodId && !activeOriginGoodIds.value.has(String(g.originGoodId)),
|
||||
isCustom: g.originGood?.isCustom === true,
|
||||
originDelisted: g.originGood?.source === 'SDS' && !activeOriginGoodIds.value.has(String(g.originGoodId)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,6 +431,67 @@ async function handleConfigSubmit() {
|
||||
} finally { configLoading.value = false }
|
||||
}
|
||||
|
||||
// ─── Custom Good ───
|
||||
const customVisible = ref(false)
|
||||
const customLoading = ref(false)
|
||||
const customPositions = ref<Position[]>([])
|
||||
const customForm = ref({
|
||||
goodName: '', goodImage: '', goodPrice: '', countryId: '',
|
||||
cascaderCategory: [] as string[], categoryId: '', tagIds: [] as string[],
|
||||
positionId: '', goodPriority: 0,
|
||||
})
|
||||
|
||||
function openCustomCreate() {
|
||||
customForm.value = {
|
||||
goodName: '', goodImage: '', goodPrice: '', countryId: '',
|
||||
cascaderCategory: [], categoryId: '', tagIds: [], positionId: '', goodPriority: 0,
|
||||
}
|
||||
customPositions.value = []
|
||||
customVisible.value = true
|
||||
}
|
||||
|
||||
async function loadCustomPositions() {
|
||||
const params: any = { page: 1, pageSize: 200 }
|
||||
if (customForm.value.countryId) params.countryId = customForm.value.countryId
|
||||
if (customForm.value.categoryId) params.categoryId = customForm.value.categoryId
|
||||
try {
|
||||
const res = await positionsApi.getPositionsList(params) as any
|
||||
customPositions.value = Array.isArray(res) ? res : (res.items ?? [])
|
||||
} catch { customPositions.value = [] }
|
||||
}
|
||||
|
||||
function onCustomCascaderChange(val: any) {
|
||||
const path = Array.isArray(val) ? val : []
|
||||
customForm.value.categoryId = path.length ? String(path[path.length - 1]) : ''
|
||||
loadCustomPositions()
|
||||
}
|
||||
|
||||
async function handleCustomCreate() {
|
||||
if (!customForm.value.goodName.trim()) { ElMessage.warning('请输入商品名称'); return }
|
||||
if (!customForm.value.countryId) { ElMessage.warning('请选择国家'); return }
|
||||
if (!customForm.value.categoryId) { ElMessage.warning('请选择分类'); return }
|
||||
customLoading.value = true
|
||||
try {
|
||||
const created = await goodsApi.createCustomGood({
|
||||
goodName: customForm.value.goodName.trim(),
|
||||
goodImage: customForm.value.goodImage || undefined,
|
||||
goodPrice: customForm.value.goodPrice || null,
|
||||
countryId: Number(customForm.value.countryId),
|
||||
categoryId: Number(customForm.value.categoryId),
|
||||
tagIds: customForm.value.tagIds.map(Number),
|
||||
positionId: customForm.value.positionId ? Number(customForm.value.positionId) : undefined,
|
||||
goodPriority: customForm.value.goodPriority,
|
||||
detail: {},
|
||||
})
|
||||
ElMessage.success('自定义商品已创建,可继续完善详情、尺码、包装和 SKU')
|
||||
customVisible.value = false
|
||||
await refreshLeftTree()
|
||||
await openEdit(created)
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.response?.data?.message || '自定义商品创建失败')
|
||||
} finally { customLoading.value = false }
|
||||
}
|
||||
|
||||
// ─── Edit Good (replaces detail — click opens edit directly) ───
|
||||
const editVisible = ref(false)
|
||||
const editLoading = ref(false)
|
||||
@@ -455,6 +517,75 @@ const editSizeRows = computed(() => {
|
||||
}))
|
||||
})
|
||||
const editPackageRows = computed(() => editOriginDetail.value?.packageSpecs?.rows ?? [])
|
||||
const editIsCustom = computed(() => editGood.value?.originGood?.isCustom === true)
|
||||
const customContentForm = ref({
|
||||
goodPrice: '', productCode: '', englishName: '', productionCycleHours: undefined as number | undefined,
|
||||
minWeightG: '', productionProcess: '', materialDescription: '',
|
||||
blankDesignUrl: '', detailsPageVideoUrl: '', textureName: '', reminder: '',
|
||||
productPerformance: '', applicableScenarios: '', washingInstructions: '', specialDescription: '',
|
||||
designExplanation: '', designArea: '', pictureRequest: '',
|
||||
sizeChartJson: '{\n "columns": [],\n "rows": []\n}',
|
||||
packageSpecsJson: '{\n "rows": []\n}',
|
||||
optionsJson: '{}',
|
||||
mediaJson: '{}',
|
||||
variants: [] as Array<{
|
||||
sku: string; sizeId: string; sizeName: string; colorId: string; colorName: string; colorHex: string; imageUrl: string;
|
||||
price: string; originalPrice: string; weightG: string; boxLengthCm: string;
|
||||
boxWidthCm: string; boxHeightCm: string; designDataJson: string; enabled: boolean
|
||||
}>,
|
||||
})
|
||||
|
||||
function fillCustomContent(g: GoodDetail) {
|
||||
const detail = g.originDetail ?? {}
|
||||
customContentForm.value = {
|
||||
goodPrice: g.originGood?.goodPrice ?? '',
|
||||
productCode: String(detail.productCode ?? ''),
|
||||
englishName: String(detail.englishName ?? ''),
|
||||
productionCycleHours: detail.productionCycleHours == null ? undefined : Number(detail.productionCycleHours),
|
||||
minWeightG: String(detail.minWeightG ?? ''),
|
||||
productionProcess: String(detail.productionProcess ?? ''),
|
||||
materialDescription: String(detail.materialDescription ?? ''),
|
||||
blankDesignUrl: String(detail.blankDesignUrl ?? ''),
|
||||
detailsPageVideoUrl: String(detail.detailsPageVideoUrl ?? ''),
|
||||
textureName: String(detail.textureName ?? ''),
|
||||
reminder: String(detail.reminder ?? ''),
|
||||
productPerformance: String(detail.productPerformance ?? ''),
|
||||
applicableScenarios: String(detail.applicableScenarios ?? ''),
|
||||
washingInstructions: String(detail.washingInstructions ?? ''),
|
||||
specialDescription: String(detail.specialDescription ?? ''),
|
||||
designExplanation: String(detail.designExplanation ?? ''),
|
||||
designArea: String(detail.designArea ?? ''),
|
||||
pictureRequest: String(detail.pictureRequest ?? ''),
|
||||
sizeChartJson: JSON.stringify(detail.sizeChart ?? { columns: [], rows: [] }, null, 2),
|
||||
packageSpecsJson: JSON.stringify(detail.packageSpecs ?? { rows: [] }, null, 2),
|
||||
optionsJson: JSON.stringify(detail.options ?? {}, null, 2),
|
||||
mediaJson: JSON.stringify(detail.media ?? {}, null, 2),
|
||||
variants: g.variants.map((variant) => ({
|
||||
sku: variant.sku,
|
||||
sizeId: String(variant.sizeId ?? ''),
|
||||
sizeName: String(variant.sizeName ?? ''),
|
||||
colorId: String(variant.colorId ?? ''),
|
||||
colorName: String(variant.colorName ?? ''),
|
||||
colorHex: String(variant.colorHex ?? ''),
|
||||
imageUrl: String(variant.imageUrl ?? ''),
|
||||
price: String(variant.price ?? ''),
|
||||
originalPrice: String(variant.originalPrice ?? ''),
|
||||
weightG: String(variant.weightG ?? ''),
|
||||
boxLengthCm: String(variant.boxLengthCm ?? ''),
|
||||
boxWidthCm: String(variant.boxWidthCm ?? ''),
|
||||
boxHeightCm: String(variant.boxHeightCm ?? ''),
|
||||
designDataJson: JSON.stringify(variant.designData ?? {}, null, 2),
|
||||
enabled: variant.enabled,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
function addCustomVariant() {
|
||||
customContentForm.value.variants.push({
|
||||
sku: '', sizeId: '', sizeName: '', colorId: '', colorName: '', colorHex: '', imageUrl: '', price: '',
|
||||
originalPrice: '', weightG: '', boxLengthCm: '', boxWidthCm: '', boxHeightCm: '', designDataJson: '{}', enabled: true,
|
||||
})
|
||||
}
|
||||
|
||||
async function openEdit(g: Good) {
|
||||
editGood.value = g
|
||||
@@ -468,9 +599,12 @@ async function openEdit(g: Good) {
|
||||
positionId: g.positionId || '',
|
||||
}
|
||||
editVisible.value = true
|
||||
loadEditPositions()
|
||||
editDetailLoading.value = true
|
||||
try {
|
||||
editGood.value = await goodsApi.getGoodById(g.id)
|
||||
const detail = await goodsApi.getGoodById(g.id)
|
||||
editGood.value = detail
|
||||
if (detail.originGood?.isCustom) fillCustomContent(detail)
|
||||
} catch {
|
||||
ElMessage.warning('商品详情加载失败,当前显示列表数据')
|
||||
} finally {
|
||||
@@ -495,6 +629,7 @@ async function handleSyncOriginDetail(data: any) {
|
||||
}
|
||||
|
||||
async function handleSyncOneDetail() {
|
||||
if (editIsCustom.value) return
|
||||
const goodId = editGood.value?.originGood?.sdsGoodId
|
||||
if (!goodId) return
|
||||
detailSyncing.value = true
|
||||
@@ -511,6 +646,73 @@ async function handleSyncOneDetail() {
|
||||
}
|
||||
|
||||
async function handleEditSubmit() {
|
||||
let customPayload: any = null
|
||||
if (editIsCustom.value) {
|
||||
let sizeChart: Record<string, unknown>
|
||||
let packageSpecs: Record<string, unknown>
|
||||
let options: Record<string, unknown>
|
||||
let media: Record<string, unknown>
|
||||
try {
|
||||
sizeChart = JSON.parse(customContentForm.value.sizeChartJson)
|
||||
packageSpecs = JSON.parse(customContentForm.value.packageSpecsJson)
|
||||
options = JSON.parse(customContentForm.value.optionsJson)
|
||||
media = JSON.parse(customContentForm.value.mediaJson)
|
||||
for (const variant of customContentForm.value.variants) JSON.parse(variant.designDataJson)
|
||||
} catch {
|
||||
ElMessage.error('尺码表、包装规格、选项、媒体或 SKU 设计数据不是有效 JSON')
|
||||
return
|
||||
}
|
||||
if (customContentForm.value.variants.some((variant) => !variant.sku.trim())) {
|
||||
ElMessage.error('SKU 不能为空')
|
||||
return
|
||||
}
|
||||
customPayload = {
|
||||
goodName: editForm.value.goodName,
|
||||
goodImage: editForm.value.goodImage || null,
|
||||
goodPrice: customContentForm.value.goodPrice || null,
|
||||
detail: {
|
||||
productCode: customContentForm.value.productCode || null,
|
||||
englishName: customContentForm.value.englishName || null,
|
||||
productionCycleHours: customContentForm.value.productionCycleHours ?? null,
|
||||
minWeightG: customContentForm.value.minWeightG || null,
|
||||
productionProcess: customContentForm.value.productionProcess || null,
|
||||
materialDescription: customContentForm.value.materialDescription || null,
|
||||
blankDesignUrl: customContentForm.value.blankDesignUrl || null,
|
||||
detailsPageVideoUrl: customContentForm.value.detailsPageVideoUrl || null,
|
||||
textureName: customContentForm.value.textureName || null,
|
||||
reminder: customContentForm.value.reminder || null,
|
||||
productPerformance: customContentForm.value.productPerformance || null,
|
||||
applicableScenarios: customContentForm.value.applicableScenarios || null,
|
||||
washingInstructions: customContentForm.value.washingInstructions || null,
|
||||
specialDescription: customContentForm.value.specialDescription || null,
|
||||
designExplanation: customContentForm.value.designExplanation || null,
|
||||
designArea: customContentForm.value.designArea || null,
|
||||
pictureRequest: customContentForm.value.pictureRequest || null,
|
||||
sizeChart,
|
||||
packageSpecs,
|
||||
options,
|
||||
media,
|
||||
},
|
||||
variants: customContentForm.value.variants.map((variant: any, index: number) => ({
|
||||
sku: variant.sku.trim(),
|
||||
sizeId: variant.sizeId || null,
|
||||
sizeName: variant.sizeName || null,
|
||||
colorId: variant.colorId || null,
|
||||
colorName: variant.colorName || null,
|
||||
colorHex: variant.colorHex || null,
|
||||
imageUrl: variant.imageUrl || null,
|
||||
price: variant.price || null,
|
||||
originalPrice: variant.originalPrice || null,
|
||||
weightG: variant.weightG || null,
|
||||
boxLengthCm: variant.boxLengthCm || null,
|
||||
boxWidthCm: variant.boxWidthCm || null,
|
||||
boxHeightCm: variant.boxHeightCm || null,
|
||||
designData: JSON.parse(variant.designDataJson),
|
||||
enabled: variant.enabled,
|
||||
sortOrder: index,
|
||||
})),
|
||||
}
|
||||
}
|
||||
editLoading.value = true
|
||||
try {
|
||||
await goodsApi.updateGood(editForm.value.id, {
|
||||
@@ -521,6 +723,7 @@ async function handleEditSubmit() {
|
||||
tagIds: editForm.value.tagIds.map(Number),
|
||||
positionId: editForm.value.positionId ? Number(editForm.value.positionId) : null,
|
||||
} as any)
|
||||
if (customPayload) await goodsApi.updateCustomGoodContent(editForm.value.id, customPayload)
|
||||
ElMessage.success('更新成功')
|
||||
editVisible.value = false
|
||||
refreshLeftTree()
|
||||
@@ -530,8 +733,20 @@ async function handleEditSubmit() {
|
||||
} finally { editLoading.value = false }
|
||||
}
|
||||
|
||||
function onEditCascaderChange(val: string[]) {
|
||||
editForm.value.categoryId = val.length ? val[val.length - 1] : ''
|
||||
function onEditCascaderChange(val: any) {
|
||||
const path = Array.isArray(val) ? val : []
|
||||
editForm.value.categoryId = path.length ? String(path[path.length - 1]) : ''
|
||||
loadEditPositions()
|
||||
}
|
||||
|
||||
async function loadEditPositions() {
|
||||
const params: any = { page: 1, pageSize: 200 }
|
||||
if (editForm.value.countryId) params.countryId = editForm.value.countryId
|
||||
if (editForm.value.categoryId) params.categoryId = editForm.value.categoryId
|
||||
try {
|
||||
const res = await positionsApi.getPositionsList(params) as any
|
||||
configPositions.value = Array.isArray(res) ? res : (res.items ?? [])
|
||||
} catch { configPositions.value = [] }
|
||||
}
|
||||
|
||||
async function handleDeleteGood(g: Good) {
|
||||
@@ -1264,6 +1479,7 @@ onMounted(() => loadAll())
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
<el-button size="small" type="primary" :icon="Search" @click="onSearch">搜索</el-button>
|
||||
<el-button size="small" type="success" :icon="Plus" @click="openCustomCreate">新增自定义商品</el-button>
|
||||
|
||||
<div class="gv-filter-spacer" />
|
||||
<el-radio-group v-model="mode" size="small" @change="onModeChange">
|
||||
@@ -1343,7 +1559,7 @@ onMounted(() => loadAll())
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="data.originGoodName" class="gt-row">
|
||||
<div class="gt-label">原产品</div>
|
||||
<div class="gt-label">{{ data.isCustom ? '来源' : '原产品' }}</div>
|
||||
<div class="gt-val gt-val-ellipsis">{{ data.originGoodName }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1353,13 +1569,14 @@ onMounted(() => loadAll())
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<span class="good-actions" @click.stop>
|
||||
<el-button size="small" link :icon="Aim" title="定位原产品" @click="locateInRightTree(data.originGoodId)" />
|
||||
<el-button v-if="!data.isCustom" size="small" link :icon="Aim" title="定位原产品" @click="locateInRightTree(data.originGoodId)" />
|
||||
<el-button size="small" link :icon="Edit" @click="openEdit(data.raw)" />
|
||||
<el-button size="small" link type="danger" :icon="Delete" @click="handleDeleteGood(data.raw)" />
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="data.country || data.tags?.length || data.originDelisted" class="good-meta">
|
||||
<span v-if="data.originDelisted" class="good-delisted-badge">下架</span>
|
||||
<span v-if="data.isCustom" class="good-tag">自定义</span>
|
||||
<span v-if="data.country" class="good-country">{{ data.country }}</span>
|
||||
<span
|
||||
v-for="t in (data.tags || []).slice(0, 3)" :key="t.id"
|
||||
@@ -1579,15 +1796,55 @@ onMounted(() => loadAll())
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- Custom Good Modal -->
|
||||
<el-dialog v-model="customVisible" title="新增自定义商品" width="620px" destroy-on-close>
|
||||
<el-alert
|
||||
title="自定义商品不关联 SDS 原产品,名称、图片、价格、详情、尺码、包装和 SKU 均可维护。"
|
||||
type="info"
|
||||
:closable="false"
|
||||
style="margin-bottom:16px"
|
||||
/>
|
||||
<el-form label-width="90px">
|
||||
<el-form-item label="商品名称" required><el-input v-model="customForm.goodName" /></el-form-item>
|
||||
<el-form-item label="商品图片"><ImageUpload v-model="customForm.goodImage" label="上传图片" /></el-form-item>
|
||||
<el-form-item label="基础价格"><el-input v-model="customForm.goodPrice" placeholder="例如 28.00" /></el-form-item>
|
||||
<el-form-item label="国家" required>
|
||||
<el-select v-model="customForm.countryId" filterable style="width:100%" @change="loadCustomPositions">
|
||||
<el-option v-for="c in allCountries" :key="c.id" :label="c.countryName" :value="c.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="分类" required>
|
||||
<el-cascader v-model="customForm.cascaderCategory" :options="categoryCascader as any" :props="{ checkStrictly: true }" style="width:100%" @change="onCustomCascaderChange" />
|
||||
</el-form-item>
|
||||
<el-form-item label="标签">
|
||||
<el-select v-model="customForm.tagIds" multiple filterable style="width:100%">
|
||||
<el-option-group v-for="g in groupedTagOptions" :key="g.id" :label="g.label">
|
||||
<el-option v-for="t in g.tags" :key="t.id" :label="t.tagName" :value="t.id" />
|
||||
</el-option-group>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="运营位">
|
||||
<el-select v-model="customForm.positionId" clearable style="width:100%">
|
||||
<el-option v-for="p in customPositions" :key="p.id" :label="`#${p.indexVal}`" :value="p.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="优先级"><el-input-number v-model="customForm.goodPriority" :min="0" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="customVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="customLoading" @click="handleCustomCreate">创建并完善详情</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- Edit Good Modal (direct edit — no separate detail step) -->
|
||||
<el-dialog v-model="editVisible" :title="editGood?.goodName || '编辑商品'" width="900px" destroy-on-close>
|
||||
<!-- Origin product reference -->
|
||||
<div v-if="editGood?.originGood" class="edit-og-ref">
|
||||
<el-image v-if="editGood.originGood.goodImage" :src="editGood.originGood.goodImage" fit="cover" class="edit-og-img" />
|
||||
<div class="edit-og-meta">
|
||||
<div class="edit-og-label">关联原产品</div>
|
||||
<div class="edit-og-label">{{ editIsCustom ? '自定义商品' : '关联原产品' }}</div>
|
||||
<div class="edit-og-name">{{ editGood.originGood.goodName }}</div>
|
||||
<div class="edit-og-sub">SDS ID: {{ editGood.originGood.sdsGoodId }}<template v-if="editGood.originGood.goodPrice"> · ¥{{ editGood.originGood.goodPrice }}</template></div>
|
||||
<div class="edit-og-sub">{{ editIsCustom ? '自定义 ID' : 'SDS ID' }}: {{ editGood.originGood.sdsGoodId }}<template v-if="editGood.originGood.goodPrice"> · ¥{{ editGood.originGood.goodPrice }}</template></div>
|
||||
<div class="edit-og-status">
|
||||
<el-tag :type="editGood.originGood.hasDetail ? 'success' : 'warning'" size="small">
|
||||
{{ editGood.originGood.hasDetail ? '详情已同步' : '详情未同步' }}
|
||||
@@ -1601,6 +1858,7 @@ onMounted(() => loadAll())
|
||||
</div>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="!editIsCustom"
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
@@ -1616,14 +1874,14 @@ onMounted(() => loadAll())
|
||||
</el-form-item>
|
||||
<el-form-item label="国家">
|
||||
<div class="select-inline">
|
||||
<el-select v-model="editForm.countryId" filterable>
|
||||
<el-select v-model="editForm.countryId" filterable @change="loadEditPositions">
|
||||
<el-option v-for="c in allCountries" :key="c.id" :label="c.countryName" :value="c.id" />
|
||||
</el-select>
|
||||
<el-button text :icon="Plus" @click="quickCreateCountry(() => { const latest = allCountries.value[allCountries.value.length - 1]; if (latest) editForm.value.countryId = latest.id })" />
|
||||
<el-button text :icon="Plus" @click="quickCreateCountry(() => { const latest = allCountries[allCountries.length - 1]; if (latest) editForm.countryId = latest.id })" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="分类">
|
||||
<el-cascader v-model="editForm.cascaderCategory" :options="categoryCascader" :props="{ checkStrictly: true }" @change="onEditCascaderChange" />
|
||||
<el-cascader v-model="editForm.cascaderCategory" :options="categoryCascader as any" :props="{ checkStrictly: true }" @change="onEditCascaderChange" />
|
||||
</el-form-item>
|
||||
<el-form-item label="标签">
|
||||
<div class="select-inline">
|
||||
@@ -1632,14 +1890,43 @@ onMounted(() => loadAll())
|
||||
<el-option v-for="t in g.tags" :key="t.id" :label="t.tagName" :value="t.id" />
|
||||
</el-option-group>
|
||||
</el-select>
|
||||
<el-button text :icon="Plus" @click="quickCreateTag(() => { const latest = allTags.value[allTags.value.length - 1]; if (latest) editForm.value.tagIds.push(latest.id) })" />
|
||||
<el-button text :icon="Plus" @click="quickCreateTag(() => { const latest = allTags[allTags.length - 1]; if (latest) editForm.tagIds.push(latest.id) })" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="运营位">
|
||||
<el-select v-model="editForm.positionId" clearable placeholder="可选">
|
||||
<el-option v-for="p in configPositions" :key="p.id" :label="`#${p.indexVal}`" :value="p.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="editIsCustom" label="基础价格">
|
||||
<el-input v-model="customContentForm.goodPrice" placeholder="例如 28.00" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-tabs v-if="editOriginDetail" class="detail-tabs">
|
||||
<el-tab-pane label="商品详情">
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-form v-if="editIsCustom" label-width="100px" class="custom-detail-form">
|
||||
<el-form-item label="商品编码"><el-input v-model="customContentForm.productCode" /></el-form-item>
|
||||
<el-form-item label="英文名称"><el-input v-model="customContentForm.englishName" /></el-form-item>
|
||||
<el-form-item label="生产周期"><el-input-number v-model="customContentForm.productionCycleHours" :min="0" /><span style="margin-left:8px">小时</span></el-form-item>
|
||||
<el-form-item label="净重"><el-input v-model="customContentForm.minWeightG"><template #append>g</template></el-input></el-form-item>
|
||||
<el-form-item label="生产工艺"><el-input v-model="customContentForm.productionProcess" type="textarea" /></el-form-item>
|
||||
<el-form-item label="材质"><el-input v-model="customContentForm.materialDescription" type="textarea" /></el-form-item>
|
||||
<el-form-item label="空白设计图"><el-input v-model="customContentForm.blankDesignUrl" /></el-form-item>
|
||||
<el-form-item label="详情视频"><el-input v-model="customContentForm.detailsPageVideoUrl" /></el-form-item>
|
||||
<el-form-item label="面料名称"><el-input v-model="customContentForm.textureName" /></el-form-item>
|
||||
<el-form-item label="温馨提示"><el-input v-model="customContentForm.reminder" type="textarea" /></el-form-item>
|
||||
<el-form-item label="产品性能"><el-input v-model="customContentForm.productPerformance" type="textarea" /></el-form-item>
|
||||
<el-form-item label="适用场景"><el-input v-model="customContentForm.applicableScenarios" type="textarea" /></el-form-item>
|
||||
<el-form-item label="洗涤说明"><el-input v-model="customContentForm.washingInstructions" type="textarea" /></el-form-item>
|
||||
<el-form-item label="特殊说明"><el-input v-model="customContentForm.specialDescription" type="textarea" /></el-form-item>
|
||||
<el-form-item label="设计说明"><el-input v-model="customContentForm.designExplanation" type="textarea" /></el-form-item>
|
||||
<el-form-item label="设计区域"><el-input v-model="customContentForm.designArea" /></el-form-item>
|
||||
<el-form-item label="图片要求"><el-input v-model="customContentForm.pictureRequest" type="textarea" /></el-form-item>
|
||||
<el-form-item label="商品选项 JSON"><el-input v-model="customContentForm.optionsJson" type="textarea" :rows="6" /></el-form-item>
|
||||
<el-form-item label="媒体数据 JSON"><el-input v-model="customContentForm.mediaJson" type="textarea" :rows="6" /></el-form-item>
|
||||
</el-form>
|
||||
<el-descriptions v-else :column="2" border size="small">
|
||||
<el-descriptions-item label="商品编码">{{ editOriginDetail.productCode || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="英文名称">{{ editOriginDetail.englishName || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="生产周期">{{ editOriginDetail.productionCycleHours ?? '-' }} 小时</el-descriptions-item>
|
||||
@@ -1649,7 +1936,8 @@ onMounted(() => loadAll())
|
||||
</el-descriptions>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="`尺码表 (${editSizeRows.length})`">
|
||||
<el-table :data="editSizeRows" border max-height="320">
|
||||
<el-input v-if="editIsCustom" v-model="customContentForm.sizeChartJson" type="textarea" :rows="14" placeholder="尺码表 JSON" />
|
||||
<el-table v-else :data="editSizeRows" border max-height="320">
|
||||
<el-table-column prop="sizeName" label="尺码" width="100" fixed />
|
||||
<el-table-column
|
||||
v-for="column in editSizeColumns"
|
||||
@@ -1661,7 +1949,8 @@ onMounted(() => loadAll())
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="`包装规格 (${editPackageRows.length})`">
|
||||
<el-table :data="editPackageRows" border max-height="320">
|
||||
<el-input v-if="editIsCustom" v-model="customContentForm.packageSpecsJson" type="textarea" :rows="14" placeholder="包装规格 JSON" />
|
||||
<el-table v-else :data="editPackageRows" border max-height="320">
|
||||
<el-table-column prop="sizeName" label="尺码" width="90" fixed />
|
||||
<el-table-column label="包装尺寸 (cm)" min-width="160">
|
||||
<template #default="{ row }">
|
||||
@@ -1673,7 +1962,28 @@ onMounted(() => loadAll())
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="`SKU (${editVariants.length})`">
|
||||
<el-table :data="editVariants" border max-height="320">
|
||||
<template v-if="editIsCustom">
|
||||
<div style="display:flex;justify-content:flex-end;margin-bottom:8px"><el-button size="small" :icon="Plus" @click="addCustomVariant">添加 SKU</el-button></div>
|
||||
<el-table :data="customContentForm.variants" border max-height="320">
|
||||
<el-table-column label="SKU" min-width="160"><template #default="{ row }"><el-input v-model="row.sku" /></template></el-table-column>
|
||||
<el-table-column label="尺码 ID" width="120"><template #default="{ row }"><el-input v-model="row.sizeId" /></template></el-table-column>
|
||||
<el-table-column label="尺码" width="120"><template #default="{ row }"><el-input v-model="row.sizeName" /></template></el-table-column>
|
||||
<el-table-column label="颜色 ID" width="120"><template #default="{ row }"><el-input v-model="row.colorId" /></template></el-table-column>
|
||||
<el-table-column label="颜色" width="120"><template #default="{ row }"><el-input v-model="row.colorName" /></template></el-table-column>
|
||||
<el-table-column label="色值" width="120"><template #default="{ row }"><el-input v-model="row.colorHex" placeholder="#FFFFFF" /></template></el-table-column>
|
||||
<el-table-column label="图片" min-width="180"><template #default="{ row }"><el-input v-model="row.imageUrl" /></template></el-table-column>
|
||||
<el-table-column label="价格" width="120"><template #default="{ row }"><el-input v-model="row.price" /></template></el-table-column>
|
||||
<el-table-column label="原价" width="120"><template #default="{ row }"><el-input v-model="row.originalPrice" /></template></el-table-column>
|
||||
<el-table-column label="重量(g)" width="120"><template #default="{ row }"><el-input v-model="row.weightG" /></template></el-table-column>
|
||||
<el-table-column label="包装长(cm)" width="130"><template #default="{ row }"><el-input v-model="row.boxLengthCm" /></template></el-table-column>
|
||||
<el-table-column label="包装宽(cm)" width="130"><template #default="{ row }"><el-input v-model="row.boxWidthCm" /></template></el-table-column>
|
||||
<el-table-column label="包装高(cm)" width="130"><template #default="{ row }"><el-input v-model="row.boxHeightCm" /></template></el-table-column>
|
||||
<el-table-column label="设计数据 JSON" min-width="220"><template #default="{ row }"><el-input v-model="row.designDataJson" type="textarea" :rows="2" /></template></el-table-column>
|
||||
<el-table-column label="启用" width="80"><template #default="{ row }"><el-switch v-model="row.enabled" /></template></el-table-column>
|
||||
<el-table-column label="操作" width="70"><template #default="{ $index }"><el-button link type="danger" @click="customContentForm.variants.splice($index, 1)">删除</el-button></template></el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
<el-table v-else :data="editVariants" border max-height="320">
|
||||
<el-table-column prop="sku" label="SKU" min-width="170" fixed />
|
||||
<el-table-column prop="sizeName" label="尺码" width="90" />
|
||||
<el-table-column prop="colorName" label="颜色" width="100" />
|
||||
|
||||
@@ -136,7 +136,7 @@ onUnmounted(() => {
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="sync-action-title">数据同步</h2>
|
||||
<p class="sync-action-desc">从上游 SDS 系统拉取最新分类和产品数据并同步到本地数据库</p>
|
||||
<p class="sync-action-desc">分类和产品每小时自动同步;全部 SDS 原产品详情每天 03:30 自动同步,也可手动执行</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sync-action-buttons">
|
||||
|
||||
Reference in New Issue
Block a user