diff --git a/apps/admin/src/components.d.ts b/apps/admin/src/components.d.ts index 1a61ff8..e39958f 100644 --- a/apps/admin/src/components.d.ts +++ b/apps/admin/src/components.d.ts @@ -13,22 +13,17 @@ declare module 'vue' { export interface GlobalComponents { ElAlert: typeof import('element-plus/es')['ElAlert'] ElAside: typeof import('element-plus/es')['ElAside'] - ElBadge: typeof import('element-plus/es')['ElBadge'] ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb'] ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem'] ElButton: typeof import('element-plus/es')['ElButton'] ElCascader: typeof import('element-plus/es')['ElCascader'] ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup'] - ElCol: typeof import('element-plus/es')['ElCol'] - ElCollapse: typeof import('element-plus/es')['ElCollapse'] - ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem'] ElColorPicker: typeof import('element-plus/es')['ElColorPicker'] ElContainer: typeof import('element-plus/es')['ElContainer'] ElDescriptions: typeof import('element-plus/es')['ElDescriptions'] ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem'] ElDialog: typeof import('element-plus/es')['ElDialog'] - ElDrawer: typeof import('element-plus/es')['ElDrawer'] ElDropdown: typeof import('element-plus/es')['ElDropdown'] ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] @@ -46,12 +41,10 @@ declare module 'vue' { ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] ElOption: typeof import('element-plus/es')['ElOption'] ElOptionGroup: typeof import('element-plus/es')['ElOptionGroup'] - ElPagination: typeof import('element-plus/es')['ElPagination'] ElPopover: typeof import('element-plus/es')['ElPopover'] ElRadio: typeof import('element-plus/es')['ElRadio'] ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] - ElRow: typeof import('element-plus/es')['ElRow'] ElSelect: typeof import('element-plus/es')['ElSelect'] ElSwitch: typeof import('element-plus/es')['ElSwitch'] ElTable: typeof import('element-plus/es')['ElTable'] diff --git a/apps/admin/src/views/goods/GoodsView.vue b/apps/admin/src/views/goods/GoodsView.vue index cbd76dd..8456469 100644 --- a/apps/admin/src/views/goods/GoodsView.vue +++ b/apps/admin/src/views/goods/GoodsView.vue @@ -4,7 +4,7 @@ import { useVirtualList } from '@vueuse/core' import { ElMessage, ElMessageBox } from 'element-plus' import { Plus, Edit, Delete, Search, Top, Refresh, - FolderAdd, Aim, ArrowDown, + FolderAdd, Aim, ArrowDown, QuestionFilled, } from '@element-plus/icons-vue' import type { CategoryTree, Country, Tag, TagGroup, Good, GoodDetail, @@ -257,13 +257,14 @@ const leftTreeData = computed(() => { }) function buildRightTree(tree: OriginGoodsTreeResponse) { - function mapOgs(ogs: any[], bucketId: string): any[] { - return ogs.map((og: any) => ({ + function mapCat(node: any): any { + const children = (node.children || []).map(mapCat) + const goods = (node.originGoods || []).map((og: any) => ({ id: 'og-' + og.id, label: og.goodName, isOG: true, rawId: og.id, - parentId: bucketId, + parentId: 'rc-' + node.categoryId, goodName: og.goodName, goodImage: og.goodImage, goodPrice: og.goodPrice, @@ -280,56 +281,12 @@ function buildRightTree(tree: OriginGoodsTreeResponse) { familyName: og.familyName ?? null, familyStale: og.familyStale ?? null, })) - } - - /** 分类内按族分桶:族节点为父、链接为子;无族链接进「未入族」桶 */ - function groupByFamily(ogs: any[], bucketPrefix: string): any[] { - const byFamily = new Map() - const ungrouped: any[] = [] - for (const og of ogs) { - if (og.familyId) { - const entry = byFamily.get(og.familyId) ?? { - key: og.familyId, code: og.familyCode ?? null, name: og.familyName ?? null, - stale: og.familyStale ?? null, ogs: [] as any[], - } - entry.ogs.push(og) - byFamily.set(og.familyId, entry) - } else { - ungrouped.push(og) - } - } - const nodes: any[] = [...byFamily.values()].map((f) => ({ - id: `fam-${f.key}`, - label: `${f.code ?? f.name ?? f.key}(${f.ogs.length})`, - isFamily: true, - familyId: f.key, - familyCode: f.code, - familyStale: f.stale, - children: mapOgs(f.ogs, `fam-${f.key}`), - })) - if (ungrouped.length) { - nodes.push({ - id: `fam-none-${bucketPrefix}`, - label: `未入族(${ungrouped.length})`, - isFamily: true, - familyId: null, - familyCode: null, - familyStale: null, - children: mapOgs(ungrouped, `fam-none-${bucketPrefix}`), - }) - } - return nodes - } - - function mapCat(node: any): any { - const children = (node.children || []).map(mapCat) - const familyNodes = groupByFamily(node.originGoods || [], node.categoryId) return { id: 'rc-' + node.categoryId, label: node.categoryName, configuredCount: node.configuredCount ?? 0, totalCount: node.totalCount ?? 0, - children: [...children, ...familyNodes], + children: [...children, ...goods], } } rightTreeData.value = tree.tree.map(mapCat) @@ -484,7 +441,8 @@ function onConfigCascaderChange(val: any) { configForm.value.categoryId = val.length ? val[val.length - 1] : '' } -/** 配置时把勾选的兄弟链接与主链接归入同一族(无族则自动建族);失败不阻断原配置流程 */ +/** 多对一合并的族支撑:勾选的兄弟链接与主链接静默归入同一族(无族则自动建族), + * 前端交互保持原「合并同名」流程不变;失败不阻断原配置流程 */ async function ensureFamilyMembership() { const primaryId = String(configPrimaryId.value || configOG.value.rawId) const checked = configChecked.value.filter((id) => id !== primaryId) @@ -675,34 +633,91 @@ function addCustomVariant() { }) } -// ─── Edit family (产品族) ─── +// ─── Edit family(编辑弹窗的族成员管理,替代旧主源/副源) ─── const editFamilyId = ref('') -const editFamilyTouched = ref(false) -const editFamilyOptions = ref>([]) -const editFamilyLoading = ref(false) +const editFamilyCode = ref(null) +const editFamilyMembers = ref>([]) +const familyMemberCount = ref(null) +const editFamilyLoaded = ref(false) +const editFamilyAddKw = ref('') +const editFamilyCandidates = ref>([]) -function initEditFamily(family: { familyId?: string; familyCode?: string | null; familyName?: string } | null) { +function initEditFamily(family: { familyId?: string; familyCode?: string | null } | null) { editFamilyId.value = family?.familyId ?? '' - editFamilyTouched.value = false - editFamilyOptions.value = family?.familyId - ? [{ id: family.familyId, familyCode: family.familyCode ?? null, familyName: family.familyName ?? '' }] - : [] + editFamilyCode.value = family?.familyCode ?? null + editFamilyMembers.value = [] + familyMemberCount.value = null + editFamilyLoaded.value = false + editFamilyAddKw.value = '' + editFamilyCandidates.value = [] + if (editFamilyId.value) loadEditFamily() } -async function searchEditFamilies(q: string) { - if (!q) return - editFamilyLoading.value = true +async function loadEditFamily() { + if (!editFamilyId.value) return try { - const res = await productFamiliesApi.list({ keyword: q, page: 1, pageSize: 30 }) - editFamilyOptions.value = res.items.map((f) => ({ id: f.id, familyCode: f.familyCode, familyName: f.familyName })) + const f = await productFamiliesApi.detail(editFamilyId.value) + editFamilyCode.value = f.familyCode + familyMemberCount.value = f._count.originGoods + const primaryId = f.primaryOriginGoodId ?? editGood.value?.originGoodId + editFamilyMembers.value = f.originGoods + .filter((m) => String(m.id) !== String(primaryId)) + .map((m) => ({ id: String(m.id), goodName: m.goodName, goodImage: m.goodImage })) } finally { - editFamilyLoading.value = false + editFamilyLoaded.value = true } } -function onEditFamilyChange(v: string | null | undefined) { - editFamilyTouched.value = true - editFamilyId.value = v ?? '' +async function refreshAfterFamilyChange() { + if (!editGood.value) return + const detail = await goodsApi.getGoodById(editGood.value.id) + editGood.value = detail + initEditFamily((detail.originGood as any)?.family ?? null) + refreshLeftTree() + refreshRightTree() +} + +async function searchFamilyCandidates(q: string) { + if (!q) { editFamilyCandidates.value = []; return } + try { + const res = await originGoodsApi.getOriginGoodsList({ keyword: q, page: 1, pageSize: 20 }) + editFamilyCandidates.value = res.items.map((o: any) => ({ id: String(o.id), goodName: o.goodName ?? o.sdsGoodId })) + } catch { editFamilyCandidates.value = [] } +} + +async function addFamilyMember(c: { id: string; goodName: string }) { + if (!editFamilyId.value) { ElMessage.warning('该商品尚未成族'); return } + try { + await productFamiliesApi.updateMembers(editFamilyId.value, { addOriginGoodIds: [c.id] }) + ElMessage.success('已加入族') + editFamilyAddKw.value = '' + await refreshAfterFamilyChange() + } catch (e: any) { + ElMessage.error(e?.response?.data?.message || '加入失败') + } +} + +async function removeFamilyMember(m: { id: string }) { + if (!editFamilyId.value) return + try { + await productFamiliesApi.updateMembers(editFamilyId.value, { removeOriginGoodIds: [m.id] }) + ElMessage.success('已移除出族') + await refreshAfterFamilyChange() + } catch (e: any) { + ElMessage.error(e?.response?.data?.message || '移除失败') + } +} + +async function promoteFamilyPrimary(m: { id: string }) { + if (!editFamilyId.value || !editGood.value) return + try { + await productFamiliesApi.patch(editFamilyId.value, { primaryOriginGoodId: m.id }) + await goodsApi.updateGood(editGood.value.id, { originGoodId: Number(m.id) } as any) + ElMessage.success('已切换主链接') + await refreshAfterFamilyChange() + } catch (e: any) { + ElMessage.error(e?.response?.data?.message || '切换失败') + } } async function openEdit(g: Good) { @@ -846,10 +861,6 @@ async function handleEditSubmit() { categoryId: Number(editForm.value.categoryId), tagIds: editForm.value.tagIds.map(Number), positionId: editForm.value.positionId ? Number(editForm.value.positionId) : null, - // 族变更:仅在用户改动过选择时提交(undefined = 不动,null = 脱离族) - ...(editFamilyTouched.value - ? { familyId: editFamilyId.value ? Number(editFamilyId.value) : null } - : {}), } as any) if (customPayload) await goodsApi.updateCustomGoodContent(editForm.value.id, customPayload) ElMessage.success('更新成功') @@ -1741,18 +1752,6 @@ onMounted(() => loadAll())
{{ data.label }} - {{ data.familyCode }} - 族待处理 loadAll()) @click.stop="openConfigFromRightTree(data)" >配置
-
- {{ data.isFamily ? '族' : '' }} {{ data.label }} - 待处理 +
+ {{ data.label }}