feat(微信视频号): 新增类目关联与商品列表功能

- 在类目管理页面增加"关联分类"功能,支持将微信小店类目与平台类目进行映射
- 新增删除类目申请功能,允许删除审核中的申请
- 商品管理页面增加Tab切换,分别展示微信小店商品和可关联平台商品
- 类目申请页面优化资质信息显示逻辑,仅需资质的类目才显示JSON输入框
- 调整API导入顺序并新增相关接口:删除类目申请、更新类目映射、查询已关联类目、查询可关联商品
This commit is contained in:
pikachu1995@126.com
2026-05-06 17:15:23 +08:00
parent 118c136931
commit 6a17ffebe9
4 changed files with 512 additions and 111 deletions

View File

@@ -3,10 +3,10 @@ import {
getRequest,
postRequest,
putRequest,
putRequestWithNoForm,
deleteRequest,
importRequest,
getRequestWithNoToken,
putRequestWithNoForm,
postRequestWithNoTokenData,
postRequestWithNoForm,
managerUrl
@@ -388,6 +388,21 @@ export const applyWxChannelsCategory = (params) => {
return postRequestWithNoForm(`/wxchannels/category/apply`, params);
};
// 删除微信小店类目申请
export const deleteWxChannelsCategoryApply = (id) => {
return deleteRequest(`/wxchannels/category/${id}`);
};
// 编辑微信小店类目映射
export const updateWxChannelsCategoryMapping = (id, params) => {
return putRequestWithNoForm(`/wxchannels/category/${id}`, params);
};
// 查询已关联平台类目
export const getWxChannelsCategoryMapped = (params) => {
return getRequest(`/wxchannels/category/mapped`, params);
};
// 微信小店类目审核单详情
export const getWxChannelsCategoryFlowDetail = (params) => {
return getRequest(`/wxchannels/category/flow/detail`, params);
@@ -403,6 +418,11 @@ export const getWxChannelsGoodsPage = (params) => {
return getRequest(`/wxchannels/goods`, params);
};
// 微信视频号可关联平台商品分页
export const getWxChannelsLinkableGoodsPage = (params) => {
return getRequest(`/wxchannels/goods/linkable`, params);
};
// 微信视频号订单分页
export const getWxChannelsOrderPage = (params) => {
return getRequest(`/wxchannels/order`, params);

View File

@@ -42,7 +42,7 @@
<FormItem label="品牌ID列表">
<Input v-model="applyForm.brandIdsText" placeholder="可选逗号分隔1001,1002" />
</FormItem>
<FormItem label="证照组JSON">
<FormItem label="证照组JSON" v-if="needQualification">
<Input
type="textarea"
:rows="14"
@@ -50,6 +50,9 @@
placeholder='请输入 licenseGroupList 的 JSON 数组'
/>
</FormItem>
<FormItem v-else label="资质信息">
<span>当前类目无需提交资质信息</span>
</FormItem>
<FormItem>
<Button type="primary" :loading="submitting" @click="submitApply">提交申请</Button>
</FormItem>
@@ -64,6 +67,13 @@ import { getWxChannelsCategoryDetail, applyWxChannelsCategory } from "@/api/inde
export default {
name: "wxchannels-category-apply",
computed: {
needQualification() {
const list = this.detailData && this.detailData.productQuaList;
if (!Array.isArray(list) || list.length === 0) return false;
return list.some((item) => item && (item.mandatory === true || item.needToApply === true));
},
},
data() {
return {
loading: false,
@@ -125,15 +135,17 @@ export default {
},
async submitApply() {
let licenseGroupList = [];
try {
licenseGroupList = JSON.parse(this.applyForm.licenseGroupJson || "[]");
} catch (e) {
this.$Message.error("证照组JSON格式错误");
return;
}
if (!Array.isArray(licenseGroupList) || licenseGroupList.length === 0) {
this.$Message.warning("licenseGroupList不能为空");
return;
if (this.needQualification) {
try {
licenseGroupList = JSON.parse(this.applyForm.licenseGroupJson || "[]");
} catch (e) {
this.$Message.error("证照组JSON格式错误");
return;
}
if (!Array.isArray(licenseGroupList) || licenseGroupList.length === 0) {
this.$Message.warning("licenseGroupList不能为空");
return;
}
}
const brandIds = (this.applyForm.brandIdsText || "")
.split(",")
@@ -159,8 +171,10 @@ export default {
const payload = {
// 按一级->二级->三级顺序传递完整分类链
catsV2,
licenseGroupList,
};
if (this.needQualification) {
payload.licenseGroupList = licenseGroupList;
}
if (brandIds.length > 0) {
payload.brandIds = brandIds;
}

View File

@@ -1,25 +1,6 @@
<template>
<div class="wxchannels-category">
<Tabs v-model="innerTab">
<TabPane label="三级类目" name="third">
<div class="toolbar">
<Button type="warning" :loading="initLoading" @click="handleInitCategory">初始化分类</Button>
<Button type="primary" :loading="thirdLoading" style="margin-left: 10px" @click="loadThirdCategories">
刷新
</Button>
</div>
<Table
border
class="category-tree-table"
update-show-children
:loading="thirdLoading"
:columns="thirdColumns"
:data="thirdData"
:load-data="loadCategoryChildren"
row-key="catId"
></Table>
</TabPane>
<TabPane label="类目申请" name="apply">
<Form inline :label-width="70" class="search-form">
<FormItem label="状态">
@@ -48,6 +29,25 @@
></Page>
</Row>
</TabPane>
<TabPane label="三级类目" name="third">
<div class="toolbar">
<Button type="warning" :loading="initLoading" @click="handleInitCategory">初始化分类</Button>
<Button type="primary" :loading="thirdLoading" style="margin-left: 10px" @click="loadThirdCategories">
刷新
</Button>
</div>
<Table
border
class="category-tree-table"
update-show-children
:loading="thirdLoading"
:columns="thirdColumns"
:data="thirdData"
:load-data="loadCategoryChildren"
row-key="catId"
></Table>
</TabPane>
</Tabs>
<Modal v-model="detailModalVisible" :title="detailModalTitle" width="900" footer-hide>
@@ -97,6 +97,29 @@
</div>
</Modal>
<Modal
:title="bindModalTitle"
v-model="bindModalVisible"
:mask-closable="false"
:width="700"
>
<div style="position: relative; max-height: 520px; overflow: auto">
<Spin size="large" fix v-if="categoryTreeLoading"></Spin>
<Tree
:key="categoryTreeKey"
:data="categoryTreeData"
show-checkbox
@on-check-change="onBindCategoryCheckChange"
></Tree>
</div>
<div slot="footer">
<Button type="text" @click="bindModalVisible = false">取消</Button>
<Button type="primary" :loading="bindSubmitting" style="margin-left: 8px" @click="submitBindCategory">
确定
</Button>
</div>
</Modal>
</div>
</template>
@@ -107,19 +130,48 @@ import {
getWxChannelsCategoryFlowDetail,
getWxChannelsCategoryDetail,
initWxChannelsCategory,
deleteWxChannelsCategoryApply,
updateWxChannelsCategoryMapping,
getWxChannelsCategoryMapped,
} from "@/api/index";
import { getCategoryTree } from "@/api/goods";
const toStringArray = (arr) => {
if (!Array.isArray(arr)) return [];
return arr.map((x) => String(x)).filter((x) => x.length > 0);
};
const buildCategoryIdNameMap = (list, map) => {
if (!Array.isArray(list) || list.length === 0) return;
list.forEach((item) => {
if (!item) return;
const id = item.id !== undefined && item.id !== null ? String(item.id) : "";
if (id) map[id] = item.name;
buildCategoryIdNameMap(item.children || [], map);
});
};
export default {
name: "wxchannels-category",
data() {
return {
innerTab: "third",
innerTab: "apply",
thirdLoading: false,
initLoading: false,
applyLoading: false,
thirdData: [],
applyData: [],
applyTotal: 0,
bindModalVisible: false,
bindModalTitle: "关联分类",
bindSubmitting: false,
bindTargetRow: null,
selectedCategoryIds: [],
categoryTreeLoading: false,
categoryTreeData: [],
categoryTreeSource: [],
categoryIdNameMap: {},
categoryTreeKey: 0,
// 保存用户当前选择的完整类目链(一级 -> 二级 -> 三级)
fullCategoryPath: [],
detailModalVisible: false,
@@ -127,7 +179,7 @@ export default {
detailData: null,
detailRawJson: "",
applyQuery: {
status: "",
status: "APPROVED",
pageNumber: 1,
pageSize: 20,
},
@@ -169,7 +221,8 @@ export default {
align: "center",
render: (h, params) => {
const row = params.row || {};
if (Number(row.level) !== 3) {
// 仅最底层(无下级)展示申请按钮
if (row.hasChildren === true) {
return h("span", "-");
}
return h(
@@ -184,9 +237,33 @@ export default {
},
],
applyColumns: [
{ title: "审核单ID", key: "auditId", minWidth: 120, tooltip: true },
{ title: "类目ID", key: "catId", minWidth: 120, tooltip: true },
{ title: "类目名称", key: "catName", minWidth: 180, tooltip: true },
{
title: "类目ID",
key: "wxCategoryId",
minWidth: 120,
tooltip: true,
render: (h, params) => {
const row = params.row || {};
return h("span", row.wxCategoryId || row.catId || "-");
},
},
{
title: "类目名称",
key: "wxCategoryName",
minWidth: 180,
tooltip: true,
render: (h, params) => {
const row = params.row || {};
return h("span", row.wxCategoryName || row.catName || "-");
},
},
{
title: "已关联平台分类",
key: "mappedPlatformCategoryNames",
minWidth: 220,
tooltip: true,
render: (h, params) => h("span", this.formatMappedPlatformCategoryNames(params.row)),
},
{
title: "状态",
key: "status",
@@ -206,31 +283,31 @@ export default {
{
title: "操作",
key: "action",
width: 180,
width: 120,
render: (h, params) => {
const row = params.row || {};
const actions = [];
actions.push(
h(
if (row.status === "APPROVED") {
return h(
"Button",
{
props: { type: "text", size: "small" },
on: { click: () => this.showCategoryDetail(row.catId) },
on: { click: () => this.openBindCategoryModal(row) },
},
"类目详情"
)
);
actions.push(
h(
"关联分类"
);
}
if (row.status === "PENDING") {
return h(
"Button",
{
props: { type: "text", size: "small" },
on: { click: () => this.showFlowDetail(row.auditId) },
style: { color: "#ed4014" },
on: { click: () => this.handleDeleteApply(row) },
},
"审核单详情"
)
);
return h("div", actions);
"删除"
);
}
return h("span", "-");
},
},
],
@@ -257,11 +334,17 @@ export default {
innerTab(val) {
if (val === "apply" && this.applyData.length === 0) {
this.loadApplyPage();
} else if (val === "third" && this.thirdData.length === 0) {
this.loadThirdCategories();
}
},
},
mounted() {
this.loadThirdCategories();
if (this.innerTab === "apply") {
this.loadApplyPage();
} else {
this.loadThirdCategories();
}
},
methods: {
boolText(val) {
@@ -436,6 +519,156 @@ export default {
this.detailModalVisible = true;
}
},
buildCategoryTreeNodes(list, selectedSet) {
if (!Array.isArray(list) || list.length === 0) return [];
return list.map((item) => {
const children = this.buildCategoryTreeNodes(item.children || [], selectedSet);
return {
id: item.id,
title: item.name,
expand: true,
checked: selectedSet.has(String(item.id)),
children,
};
});
},
parsePlatformCategoryIds(row) {
if (!row) return [];
const raw =
row.platformCategoryIds ??
row.platform_category_ids ??
row.platformCategoryId ??
row.platform_category_id;
if (Array.isArray(raw)) return toStringArray(raw);
if (typeof raw === "string") {
const text = raw.trim();
if (!text) return [];
try {
const parsed = JSON.parse(text);
if (Array.isArray(parsed)) return toStringArray(parsed);
} catch (e) {
// ignore
}
return text
.split(",")
.map((x) => x.trim())
.filter((x) => x.length > 0);
}
if (raw !== undefined && raw !== null && String(raw)) return [String(raw)];
return [];
},
formatMappedPlatformCategoryNames(row) {
if (!row) return "-";
const raw = row.mappedPlatformCategoryNames ?? row.platformCategoryNames ?? row.platform_category_names;
if (Array.isArray(raw)) {
const names = raw.map((x) => String(x || "").trim()).filter((x) => x.length > 0);
return names.length > 0 ? names.join("") : "-";
}
if (typeof raw === "string") {
const text = raw.trim();
if (!text) return "-";
try {
const parsed = JSON.parse(text);
if (Array.isArray(parsed)) {
const names = parsed.map((x) => String(x || "").trim()).filter((x) => x.length > 0);
return names.length > 0 ? names.join("") : "-";
}
} catch (e) {
// ignore
}
return text;
}
return "-";
},
async fetchMappedCategoryIds(id, row) {
if (!id) return this.parsePlatformCategoryIds(row);
try {
const res = await getWxChannelsCategoryMapped({ id });
if (res && res.success) {
const list = Array.isArray(res.result) ? res.result : [];
return toStringArray(
list
.map((item) => item && (item.platformCategoryId || item.platform_category_id))
.filter((x) => x !== undefined && x !== null && String(x)),
);
}
} catch (e) {
// ignore and fallback
}
return this.parsePlatformCategoryIds(row);
},
async openBindCategoryModal(row) {
this.bindTargetRow = row || null;
this.bindModalVisible = true;
this.categoryTreeLoading = true;
this.categoryTreeKey += 1;
try {
const id = row && row.id;
this.selectedCategoryIds = await this.fetchMappedCategoryIds(id, row);
if (!Array.isArray(this.categoryTreeSource) || this.categoryTreeSource.length === 0) {
const res = await getCategoryTree();
this.categoryTreeSource = res && res.success ? res.result || [] : [];
const map = {};
buildCategoryIdNameMap(this.categoryTreeSource, map);
this.categoryIdNameMap = map;
}
const selectedSet = new Set(toStringArray(this.selectedCategoryIds));
this.categoryTreeData = this.buildCategoryTreeNodes(this.categoryTreeSource || [], selectedSet);
} finally {
this.categoryTreeLoading = false;
}
},
onBindCategoryCheckChange(checkedNodes) {
if (!Array.isArray(checkedNodes)) {
this.selectedCategoryIds = [];
return;
}
this.selectedCategoryIds = toStringArray(checkedNodes.map((node) => node && node.id).filter(Boolean));
},
async submitBindCategory() {
const row = this.bindTargetRow || {};
const id = row.id;
if (!id) {
this.$Message.warning("缺少申请ID");
return;
}
const platformCategoryIds = toStringArray(this.selectedCategoryIds);
const platformCategoryNames = platformCategoryIds
.map((x) => this.categoryIdNameMap[x] || "")
.filter((x) => x.length > 0);
this.bindSubmitting = true;
try {
const res = await updateWxChannelsCategoryMapping(id, {
platformCategoryIds,
platformCategoryNames,
});
if (res && res.success) {
this.$Message.success("关联分类成功");
this.bindModalVisible = false;
this.loadApplyPage();
}
} finally {
this.bindSubmitting = false;
}
},
handleDeleteApply(row) {
const id = row && row.id;
if (!id) {
this.$Message.warning("缺少申请ID");
return;
}
this.$Modal.confirm({
title: "确认删除",
content: "确认删除该类目申请吗?",
onOk: async () => {
const res = await deleteWxChannelsCategoryApply(id);
if (res && res.success) {
this.$Message.success("删除成功");
this.loadApplyPage();
}
},
});
},
},
};
</script>

View File

@@ -1,61 +1,114 @@
<template>
<div class="wxchannels-goods">
<Row>
<Form :model="searchForm" inline :label-width="70" class="search-form">
<FormItem label="商品名称" prop="goodsName">
<Input
v-model="searchForm.goodsName"
placeholder="请输入商品名称"
clearable
style="width: 220px"
/>
</FormItem>
<FormItem label="状态" prop="status">
<Select v-model="searchForm.status" clearable style="width: 180px" placeholder="全部">
<Option value="APPROVED">已通过</Option>
<Option value="PENDING">审核中</Option>
<Option value="REJECTED">拒绝</Option>
</Select>
</FormItem>
<Button @click="handleSearch" type="primary" icon="ios-search" class="search-btn" :loading="loading">查询</Button>
</Form>
</Row>
<Tabs v-model="activeTab" @on-click="handleTabChange">
<TabPane label="微信小店商品列表" name="channels">
<Row>
<Form :model="channelsSearchForm" inline :label-width="70" class="search-form">
<FormItem label="商品名称" prop="goodsName">
<Input
v-model="channelsSearchForm.goodsName"
placeholder="请输入商品名称"
clearable
style="width: 220px"
/>
</FormItem>
<FormItem label="状态" prop="status">
<Select v-model="channelsSearchForm.status" clearable style="width: 180px" placeholder="全部">
<Option value="APPROVED">通过</Option>
<Option value="PENDING">审核中</Option>
<Option value="REJECTED">已拒绝</Option>
</Select>
</FormItem>
<Button
@click="handleChannelsSearch"
type="primary"
icon="ios-search"
class="search-btn"
:loading="channelsLoading"
>
查询
</Button>
</Form>
</Row>
<Table :loading="loading" border :columns="columns" :data="data" class="mt_10"></Table>
<Row type="flex" justify="end" class="mt_10" style="margin-top: 10px">
<Page
:current="searchForm.pageNumber"
:total="total"
:page-size="searchForm.pageSize"
@on-change="changePage"
@on-page-size-change="changePageSize"
:page-size-opts="[20, 50, 100]"
size="small"
show-total
show-elevator
show-sizer
></Page>
</Row>
<Table :loading="channelsLoading" border :columns="channelsColumns" :data="channelsData" class="mt_10"></Table>
<Row type="flex" justify="end" class="mt_10" style="margin-top: 10px">
<Page
:current="channelsSearchForm.pageNumber"
:total="channelsTotal"
:page-size="channelsSearchForm.pageSize"
@on-change="changeChannelsPage"
@on-page-size-change="changeChannelsPageSize"
:page-size-opts="[20, 50, 100]"
size="small"
show-total
show-elevator
show-sizer
></Page>
</Row>
</TabPane>
<TabPane label="平台商品列表" name="platform">
<Row>
<Form :model="platformSearchForm" inline :label-width="70" class="search-form">
<FormItem label="商品名称" prop="goodsName">
<Input
v-model="platformSearchForm.goodsName"
placeholder="请输入商品名称"
clearable
style="width: 220px"
/>
</FormItem>
<Button
@click="handlePlatformSearch"
type="primary"
icon="ios-search"
class="search-btn"
:loading="platformLoading"
>
查询
</Button>
</Form>
</Row>
<Table :loading="platformLoading" border :columns="platformColumns" :data="platformData" class="mt_10"></Table>
<Row type="flex" justify="end" class="mt_10" style="margin-top: 10px">
<Page
:current="platformSearchForm.pageNumber"
:total="platformTotal"
:page-size="platformSearchForm.pageSize"
@on-change="changePlatformPage"
@on-page-size-change="changePlatformPageSize"
:page-size-opts="[20, 50, 100]"
size="small"
show-total
show-elevator
show-sizer
></Page>
</Row>
</TabPane>
</Tabs>
</div>
</template>
<script>
import { getWxChannelsGoodsPage } from "@/api/index";
import { getWxChannelsGoodsPage, getWxChannelsLinkableGoodsPage } from "@/api/index";
export default {
name: "wxchannels-goods",
data() {
return {
loading: false,
total: 0,
data: [],
searchForm: {
activeTab: "channels",
channelsLoading: false,
channelsTotal: 0,
channelsData: [],
channelsSearchForm: {
goodsName: "",
status: "",
pageNumber: 1,
pageSize: 20,
},
columns: [
channelsColumns: [
{
title: "商品图片",
key: "goodsImage",
@@ -97,40 +150,121 @@ export default {
},
},
],
platformInited: false,
platformLoading: false,
platformTotal: 0,
platformData: [],
platformSearchForm: {
goodsName: "",
pageNumber: 1,
pageSize: 20,
},
platformColumns: [
{
title: "商品主图",
key: "thumbnail",
width: 90,
align: "center",
render: (h, params) => {
return h("img", {
attrs: { src: params.row.thumbnail || "", alt: "加载图片失败" },
style: {
width: "50px",
height: "50px",
objectFit: "cover",
borderRadius: "4px",
},
});
},
},
{ title: "商品名称", key: "goodsName", minWidth: 220, tooltip: true },
{ title: "平台商品ID", key: "goodsId", minWidth: 130, tooltip: true },
{ title: "商品价格", key: "price", width: 100 },
{ title: "平台分类路径", key: "categoryPath", minWidth: 240, tooltip: true },
{
title: "发布状态",
key: "publishStatus",
width: 110,
render: (h, params) => {
const val = params.row.publishStatus;
const map = {
已发布: { label: "已发布", color: "green" },
未发布: { label: "未发布", color: "default" },
};
const item = map[val] || { label: val || "-", color: "default" };
return h("Tag", { props: { color: item.color } }, item.label);
},
},
],
};
},
mounted() {
this.loadPage();
this.loadChannelsPage();
},
methods: {
handleSearch() {
this.searchForm.pageNumber = 1;
this.loadPage();
handleTabChange(name) {
if (name === "platform" && !this.platformInited) {
this.loadPlatformPage();
}
},
changePage(pageNumber) {
this.searchForm.pageNumber = pageNumber;
this.loadPage();
handleChannelsSearch() {
this.channelsSearchForm.pageNumber = 1;
this.loadChannelsPage();
},
changePageSize(pageSize) {
this.searchForm.pageSize = pageSize;
this.searchForm.pageNumber = 1;
this.loadPage();
changeChannelsPage(pageNumber) {
this.channelsSearchForm.pageNumber = pageNumber;
this.loadChannelsPage();
},
loadPage() {
this.loading = true;
const params = { ...this.searchForm };
changeChannelsPageSize(pageSize) {
this.channelsSearchForm.pageSize = pageSize;
this.channelsSearchForm.pageNumber = 1;
this.loadChannelsPage();
},
loadChannelsPage() {
this.channelsLoading = true;
const params = { ...this.channelsSearchForm };
if (!params.goodsName) delete params.goodsName;
if (!params.status) delete params.status;
getWxChannelsGoodsPage(params)
.then((res) => {
if (res && res.success) {
const page = res.result || {};
this.data = Array.isArray(page.records) ? page.records : [];
this.total = Number(page.total || 0);
this.channelsData = Array.isArray(page.records) ? page.records : [];
this.channelsTotal = Number(page.total || 0);
}
})
.finally(() => {
this.loading = false;
this.channelsLoading = false;
});
},
handlePlatformSearch() {
this.platformSearchForm.pageNumber = 1;
this.loadPlatformPage();
},
changePlatformPage(pageNumber) {
this.platformSearchForm.pageNumber = pageNumber;
this.loadPlatformPage();
},
changePlatformPageSize(pageSize) {
this.platformSearchForm.pageSize = pageSize;
this.platformSearchForm.pageNumber = 1;
this.loadPlatformPage();
},
loadPlatformPage() {
this.platformLoading = true;
const params = { ...this.platformSearchForm };
if (!params.goodsName) delete params.goodsName;
getWxChannelsLinkableGoodsPage(params)
.then((res) => {
if (res && res.success) {
const page = res.result || {};
this.platformData = Array.isArray(page.records) ? page.records : [];
this.platformTotal = Number(page.total || 0);
this.platformInited = true;
}
})
.finally(() => {
this.platformLoading = false;
});
},
},