微信小店相关内容

This commit is contained in:
pikachu1995@126.com
2026-05-04 16:56:48 +08:00
parent fe4e0ce75c
commit 118c136931
13 changed files with 2527 additions and 1 deletions

View File

@@ -373,6 +373,31 @@ export const getWxChannelsThirdCategory = (params) => {
return getRequest(`/wxchannels/category/third`, params);
};
// 微信小店类目申请分页
export const getWxChannelsCategoryPage = (params) => {
return getRequest(`/wxchannels/category`, params);
};
// 初始化微信小店类目
export const initWxChannelsCategory = () => {
return postRequestWithNoForm(`/wxchannels/category/init`, {});
};
// 申请微信小店类目
export const applyWxChannelsCategory = (params) => {
return postRequestWithNoForm(`/wxchannels/category/apply`, params);
};
// 微信小店类目审核单详情
export const getWxChannelsCategoryFlowDetail = (params) => {
return getRequest(`/wxchannels/category/flow/detail`, params);
};
// 微信小店类目详情
export const getWxChannelsCategoryDetail = (params) => {
return getRequest(`/wxchannels/category/detail`, params);
};
// 微信视频号商品分页
export const getWxChannelsGoodsPage = (params) => {
return getRequest(`/wxchannels/goods`, params);
@@ -388,11 +413,71 @@ export const getWxChannelsOverviewSummary = (params) => {
return getRequest(`/wxchannels/overview/summary`, params);
};
// 微信视频号概况-日报
export const getWxChannelsOverviewDaily = (params) => {
return getRequest(`/wxchannels/overview/daily`, params);
};
// 微信视频号概况-报表导出
export const exportWxChannelsOverview = (params) => {
return getRequest(`/wxchannels/overview/export`, params, "blob");
};
// 微信视频号退单分页
export const getWxChannelsRefundPage = (params) => {
return getRequest(`/wxchannels/refund`, params);
};
// 微信小店资金-账户余额
export const getWxChannelsFundsBalance = () => {
return getRequest(`/wxchannels/funds/balance`);
};
// 微信小店资金-结算账户信息
export const getWxChannelsFundsBankAcct = () => {
return getRequest(`/wxchannels/funds/bankacct`);
};
// 微信小店资金-流水列表
export const getWxChannelsFundsFlows = (params) => {
return getRequest(`/wxchannels/funds/flows`, params);
};
// 微信小店资金-流水详情
export const getWxChannelsFundsFlowDetail = (params) => {
return getRequest(`/wxchannels/funds/flows/detail`, params);
};
// 微信小店资金-订单流水列表
export const getWxChannelsFundsOrderFlow = (params) => {
return getRequest(`/wxchannels/funds/orderflow`, params);
};
// 微信小店配置
export const getWxChannelsSetting = () => {
return getRequest(`/wxchannels/setting`);
};
// 保存微信小店配置
export const saveWxChannelsSetting = (params) => {
return postRequestWithNoForm(`/wxchannels/setting`, params);
};
// 微信小店品牌库列表
export const getWxChannelsBrandLibrary = (params) => {
return getRequest(`/wxchannels/brand/library`, params);
};
// 微信小店品牌资质申请列表
export const getWxChannelsBrandList = (params) => {
return getRequest(`/wxchannels/brand/list`, params);
};
// 微信小店生效中的品牌资质列表
export const getWxChannelsValidBrandList = (params) => {
return getRequest(`/wxchannels/brand/valid/list`, params);
};
// 分页查询敏感词
export const getSensitiveWordsPage = (params) => {

View File

@@ -48,7 +48,9 @@ router.beforeEach((to, from, next) => {
});
router.afterEach((to) => {
Util.openNewPage(router.app, to.name, to.params, to.query);
if (!(to.meta && to.meta.noTag)) {
Util.openNewPage(router.app, to.name, to.params, to.query);
}
ViewUI.LoadingBar.finish();
window.scrollTo(0, 0);
});

View File

@@ -181,6 +181,21 @@ export const otherRouter = {
component: () =>
import("@/views/promotions/points-goods/points-goods-edit.vue")
},
{
path: "promotions/wxchannels",
title: "微信小店",
name: "promotions-wxchannels",
component: () =>
import("@/views/promotions/wxchannels/index.vue")
},
{
path: "promotions/wxchannels/category-apply",
title: "申请类目",
name: "promotions-wxchannels-category-apply",
meta: { noTag: true },
component: () =>
import("@/views/promotions/wxchannels/category-apply.vue")
},
{
path: "promotions/manager-points-goods-category",
title: "积分商品分类",

View File

@@ -0,0 +1,211 @@
<template>
<div class="wxchannels-brand">
<Form inline :label-width="90" class="search-form">
<FormItem label="列表类型">
<Select v-model="queryForm.type" style="width: 200px" @on-change="handleTypeChange">
<Option value="library">品牌库列表</Option>
<Option value="list">资质申请列表</Option>
<Option value="valid">生效资质列表</Option>
</Select>
</FormItem>
<FormItem label="审核状态" v-if="queryForm.type === 'list'">
<Select v-model="queryForm.status" clearable style="width: 180px" placeholder="全部">
<Option v-for="item in statusOptions" :key="item.value" :value="item.value">{{ item.label }}</Option>
</Select>
</FormItem>
<FormItem label="每页数量">
<InputNumber :min="1" :max="200" v-model="queryForm.pageSize" style="width: 140px" />
</FormItem>
<Button type="primary" icon="ios-search" :loading="loading" @click="handleSearch">查询</Button>
</Form>
<Table border :loading="loading" :columns="columns" :data="tableData" class="mt_10"></Table>
<div class="pager-bar">
<span class="pager-text"> {{ queryForm.pageNumber }} 当前条数{{ tableData.length }}</span>
<Button :disabled="loading || queryForm.pageNumber <= 1" @click="handlePrevPage">上一页</Button>
<Button type="primary" :disabled="loading || !nextKey" @click="handleNextPage">下一页</Button>
</div>
<Modal v-model="rawModalVisible" title="原始数据" width="760" footer-hide>
<pre class="raw-json">{{ rawJson }}</pre>
</Modal>
</div>
</template>
<script>
import {
getWxChannelsBrandLibrary,
getWxChannelsBrandList,
getWxChannelsValidBrandList,
} from "@/api/index";
export default {
name: "wxchannels-brand",
data() {
return {
loading: false,
tableData: [],
nextKey: "",
rawModalVisible: false,
rawJson: "",
queryForm: {
type: "library",
status: "",
pageNumber: 1,
pageSize: 20,
},
statusOptions: [
{ value: 0, label: "待审核" },
{ value: 1, label: "审核通过" },
{ value: 2, label: "审核驳回" },
{ value: 3, label: "已撤回" },
],
columns: [
{ title: "品牌ID", key: "brandId", minWidth: 120, tooltip: true },
{ title: "中文名称", key: "chName", minWidth: 220, tooltip: true },
{ title: "英文名称", key: "enName", minWidth: 220, tooltip: true },
{ title: "创建时间", key: "createTime", minWidth: 170, tooltip: true },
{ title: "更新时间", key: "updateTime", minWidth: 170, tooltip: true },
{
title: "操作",
key: "action",
width: 100,
align: "center",
render: (h, params) => {
return h(
"Button",
{
props: {
type: "text",
size: "small",
},
on: {
click: () => this.showRaw(params.row.__raw || {}),
},
},
"查看"
);
},
},
],
};
},
mounted() {
this.loadPage();
},
methods: {
handleTypeChange() {
this.queryForm.status = "";
this.queryForm.pageNumber = 1;
this.loadPage();
},
handleSearch() {
this.queryForm.pageNumber = 1;
this.loadPage();
},
handleNextPage() {
if (!this.nextKey) return;
this.queryForm.pageNumber += 1;
this.loadPage();
},
handlePrevPage() {
if (this.queryForm.pageNumber <= 1) return;
this.queryForm.pageNumber -= 1;
this.loadPage();
},
async loadPage() {
this.loading = true;
try {
const params = {
pageNumber: Number(this.queryForm.pageNumber) || 1,
pageSize: Number(this.queryForm.pageSize) || 20,
};
if (this.queryForm.type === "list" && this.queryForm.status !== "" && this.queryForm.status !== null) {
params.status = this.queryForm.status;
}
const res = await this.fetchByType(params);
if (res && res.success) {
const result = res.result || {};
const sourceList = this.extractList(result);
this.tableData = sourceList.map((item) => this.normalizeRow(item));
this.nextKey = result.next_key || result.nextKey || "";
} else {
this.tableData = [];
this.nextKey = "";
}
} finally {
this.loading = false;
}
},
fetchByType(params) {
if (this.queryForm.type === "list") {
return getWxChannelsBrandList(params);
}
if (this.queryForm.type === "valid") {
return getWxChannelsValidBrandList(params);
}
return getWxChannelsBrandLibrary(params);
},
extractList(result) {
if (!result || typeof result !== "object") return [];
const keys = ["brands", "brand_list", "list", "records", "items", "data"];
for (let i = 0; i < keys.length; i += 1) {
const value = result[keys[i]];
if (Array.isArray(value)) return value;
}
if (Array.isArray(result)) return result;
return [];
},
normalizeRow(item) {
const raw = item || {};
const brandId = raw.brand_id || raw.brandId || raw.id || "-";
const chName = raw.ch_name || raw.chName || raw.brand_wording || raw.brandName || raw.name || "-";
const enName = raw.en_name || raw.enName || "-";
const createTime = raw.create_time || raw.createTime || "-";
const updateTime = raw.update_time || raw.updateTime || "-";
return {
brandId,
chName,
enName,
createTime,
updateTime,
__raw: raw,
};
},
showRaw(raw) {
this.rawJson = JSON.stringify(raw, null, 2);
this.rawModalVisible = true;
},
},
};
</script>
<style scoped lang="scss">
.wxchannels-brand {
min-height: 360px;
.pager-bar {
display: flex;
justify-content: flex-end;
align-items: center;
margin-top: 12px;
gap: 8px;
}
.pager-text {
color: #666;
margin-right: 8px;
}
.raw-json {
max-height: 420px;
overflow: auto;
margin: 0;
background: #f7f7f7;
border: 1px solid #eee;
padding: 10px;
border-radius: 4px;
}
}
</style>

View File

@@ -0,0 +1,210 @@
<template>
<div class="wxchannels-category-apply">
<Card>
<div class="toolbar">
<Button @click="$router.back()">返回</Button>
</div>
<div v-if="loading" class="loading-wrap">
<Spin size="large"></Spin>
</div>
<div v-else>
<Row :gutter="12">
<Col :span="12">
<Card dis-hover>
<p class="detail-title">类目ID</p>
<p class="detail-value">{{ detailData.catId || "-" }}</p>
</Card>
</Col>
<Col :span="12">
<Card dis-hover>
<p class="detail-title">类目名称</p>
<p class="detail-value">{{ detailData.name || "-" }}</p>
</Card>
</Col>
</Row>
<Card dis-hover class="mt_10">
<p class="detail-title">属性信息attr</p>
<div class="attr-grid">
<span>支持虚拟发货{{ boolText(detailData.attr && detailData.attr.shopNoShipment) }}</span>
<span>定向准入{{ boolText(detailData.attr && detailData.attr.accessPermitRequired) }}</span>
<span>支持预售{{ boolText(detailData.attr && detailData.attr.preSale) }}</span>
<span>7天无理由退货{{ boolText(detailData.attr && detailData.attr.sevenDayReturn) }}</span>
<span>品牌定向准入{{ boolText(detailData.attr && detailData.attr.isLimitBrand) }}</span>
<span>保证金(){{ (detailData.attr && detailData.attr.deposit) || "-" }}</span>
<span>价格下限(){{ (detailData.attr && detailData.attr.floorPrice) || "-" }}</span>
</div>
</Card>
<Form class="mt_10" :label-width="120">
<FormItem label="品牌ID列表">
<Input v-model="applyForm.brandIdsText" placeholder="可选逗号分隔1001,1002" />
</FormItem>
<FormItem label="证照组JSON">
<Input
type="textarea"
:rows="14"
v-model="applyForm.licenseGroupJson"
placeholder='请输入 licenseGroupList 的 JSON 数组'
/>
</FormItem>
<FormItem>
<Button type="primary" :loading="submitting" @click="submitApply">提交申请</Button>
</FormItem>
</Form>
</div>
</Card>
</div>
</template>
<script>
import { getWxChannelsCategoryDetail, applyWxChannelsCategory } from "@/api/index";
export default {
name: "wxchannels-category-apply",
data() {
return {
loading: false,
submitting: false,
catId: "",
detailData: {},
fullCategoryPath: [],
applyForm: {
brandIdsText: "",
licenseGroupJson:
'[\n {\n "licenseGroupId": 1,\n "license": {\n "licenseId": 1,\n "fileIdList": ["file_id_1"],\n "licenseFieldList": [\n { "key": "field_key", "value": "field_value" }\n ]\n }\n }\n]',
},
};
},
mounted() {
this.catId = this.$route.query.catId;
this.parseFullCategoryPath();
if (!this.catId) {
this.$Message.warning("缺少类目ID");
this.$router.back();
return;
}
this.loadDetail();
},
methods: {
parseFullCategoryPath() {
const raw = this.$route.query.fullCategoryPath;
if (!raw) {
this.fullCategoryPath = [];
return;
}
try {
const parsed = JSON.parse(raw);
this.fullCategoryPath = Array.isArray(parsed)
? parsed.map((item) => ({
catId: item && (item.catId || item.id),
catName: (item && (item.catName || item.name)) || "",
}))
: [];
} catch (e) {
this.fullCategoryPath = [];
}
},
boolText(val) {
if (val === true) return "是";
if (val === false) return "否";
return "-";
},
async loadDetail() {
this.loading = true;
try {
const res = await getWxChannelsCategoryDetail({ catId: this.catId });
if (res && res.success) {
this.detailData = res.result || {};
}
} finally {
this.loading = false;
}
},
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;
}
const brandIds = (this.applyForm.brandIdsText || "")
.split(",")
.map((v) => Number(String(v).trim()))
.filter((v) => Number.isFinite(v) && v > 0);
const categoryPath = Array.isArray(this.fullCategoryPath) ? this.fullCategoryPath : [];
const catsV2 = categoryPath
.filter((item) => item && item.catId)
.map((item) => Number(item.catId))
.filter((id) => Number.isFinite(id) && id > 0);
if (catsV2.length === 0 && this.catId) {
const fallbackId = Number(this.catId);
if (Number.isFinite(fallbackId) && fallbackId > 0) {
catsV2.push(fallbackId);
}
}
if (catsV2.length === 0) {
this.$Message.warning("缺少完整类目链");
return;
}
const payload = {
// 按一级->二级->三级顺序传递完整分类链
catsV2,
licenseGroupList,
};
if (brandIds.length > 0) {
payload.brandIds = brandIds;
}
this.submitting = true;
try {
const res = await applyWxChannelsCategory(payload);
if (res && res.success) {
this.$Message.success(`申请成功审核单ID${res.result || "-"}`);
this.$router.back();
}
} finally {
this.submitting = false;
}
},
},
};
</script>
<style scoped lang="scss">
.wxchannels-category-apply {
.toolbar {
margin-bottom: 12px;
}
}
.loading-wrap {
text-align: center;
padding: 30px 0;
}
.detail-title {
color: #666;
margin-bottom: 8px;
}
.detail-value {
font-size: 16px;
color: #17233d;
}
.attr-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px 16px;
color: #515a6e;
}
</style>

View File

@@ -0,0 +1,490 @@
<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="状态">
<Select v-model="applyQuery.status" clearable placeholder="全部" style="width: 180px">
<Option value="PENDING">审核中</Option>
<Option value="APPROVED">已通过</Option>
<Option value="REJECTED">已拒绝</Option>
</Select>
</FormItem>
<Button type="primary" icon="ios-search" :loading="applyLoading" @click="handleApplySearch">查询</Button>
</Form>
<Table border class="mt_10" :loading="applyLoading" :columns="applyColumns" :data="applyData"></Table>
<Row type="flex" justify="end" class="mt_10" style="margin-top: 10px">
<Page
:current="applyQuery.pageNumber"
:total="applyTotal"
:page-size="applyQuery.pageSize"
@on-change="changeApplyPage"
@on-page-size-change="changeApplyPageSize"
:page-size-opts="[20, 50, 100]"
size="small"
show-total
show-elevator
show-sizer
></Page>
</Row>
</TabPane>
</Tabs>
<Modal v-model="detailModalVisible" :title="detailModalTitle" width="900" footer-hide>
<div v-if="detailData">
<Row :gutter="12">
<Col :span="12">
<Card dis-hover>
<p class="detail-title">类目ID</p>
<p class="detail-value">{{ detailData.catId || "-" }}</p>
</Card>
</Col>
<Col :span="12">
<Card dis-hover>
<p class="detail-title">类目名称</p>
<p class="detail-value">{{ detailData.name || "-" }}</p>
</Card>
</Col>
</Row>
<Card dis-hover class="mt_10">
<p class="detail-title">属性信息attr</p>
<div class="attr-grid">
<span>支持虚拟发货{{ boolText(detailData.attr && detailData.attr.shopNoShipment) }}</span>
<span>定向准入{{ boolText(detailData.attr && detailData.attr.accessPermitRequired) }}</span>
<span>支持预售{{ boolText(detailData.attr && detailData.attr.preSale) }}</span>
<span>7天无理由退货{{ boolText(detailData.attr && detailData.attr.sevenDayReturn) }}</span>
<span>品牌定向准入{{ boolText(detailData.attr && detailData.attr.isLimitBrand) }}</span>
<span>保证金(){{ (detailData.attr && detailData.attr.deposit) || "-" }}</span>
<span>价格下限(){{ (detailData.attr && detailData.attr.floorPrice) || "-" }}</span>
</div>
</Card>
<Card dis-hover class="mt_10">
<p class="detail-title">资质信息productQuaList</p>
<Table
border
size="small"
:columns="productQuaColumns"
:data="Array.isArray(detailData.productQuaList) ? detailData.productQuaList : []"
></Table>
</Card>
<Card dis-hover class="mt_10">
<p class="detail-title">原始JSON</p>
<pre class="raw-json">{{ detailRawJson }}</pre>
</Card>
</div>
</Modal>
</div>
</template>
<script>
import {
getWxChannelsThirdCategory,
getWxChannelsCategoryPage,
getWxChannelsCategoryFlowDetail,
getWxChannelsCategoryDetail,
initWxChannelsCategory,
} from "@/api/index";
export default {
name: "wxchannels-category",
data() {
return {
innerTab: "third",
thirdLoading: false,
initLoading: false,
applyLoading: false,
thirdData: [],
applyData: [],
applyTotal: 0,
// 保存用户当前选择的完整类目链(一级 -> 二级 -> 三级)
fullCategoryPath: [],
detailModalVisible: false,
detailModalTitle: "",
detailData: null,
detailRawJson: "",
applyQuery: {
status: "",
pageNumber: 1,
pageSize: 20,
},
thirdColumns: [
{
title: "",
key: "_treeExpand",
width: 70,
tree: true,
render: (h) => h("span", { class: "tree-expand-placeholder" }, ""),
},
{ title: "类目名称", key: "catName", minWidth: 180, tooltip: true },
{ title: "类目ID", key: "catId", width: 140 },
{ title: "层级", key: "level", width: 90 },
{
title: "状态",
key: "status",
width: 120,
render: (h, params) => {
const row = params.row || {};
if (Number(row.level) !== 3) {
return h("span", "-");
}
const val = row.status;
const map = {
PENDING: { label: "审核中", color: "orange" },
APPROVED: { label: "已通过", color: "green" },
REJECTED: { label: "已拒绝", color: "red" },
UNSUBMITTED: { label: "未申请", color: "default" },
};
const item = map[val] || { label: val || "-", color: "default" };
return h("Tag", { props: { color: item.color } }, item.label);
},
},
{
title: "操作",
key: "action",
width: 90,
align: "center",
render: (h, params) => {
const row = params.row || {};
if (Number(row.level) !== 3) {
return h("span", "-");
}
return h(
"Button",
{
props: { type: "text", size: "small" },
on: { click: () => this.goApplyPage(row) },
},
"申请"
);
},
},
],
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: "状态",
key: "status",
minWidth: 110,
render: (h, params) => {
const val = params.row.status;
const map = {
PENDING: { label: "审核中", color: "orange" },
APPROVED: { label: "已通过", color: "green" },
REJECTED: { label: "已拒绝", color: "red" },
};
const target = map[val] || { label: val || "-", color: "default" };
return h("Tag", { props: { color: target.color } }, target.label);
},
},
{ title: "创建时间", key: "createTime", minWidth: 170, tooltip: true },
{
title: "操作",
key: "action",
width: 180,
render: (h, params) => {
const row = params.row || {};
const actions = [];
actions.push(
h(
"Button",
{
props: { type: "text", size: "small" },
on: { click: () => this.showCategoryDetail(row.catId) },
},
"类目详情"
)
);
actions.push(
h(
"Button",
{
props: { type: "text", size: "small" },
on: { click: () => this.showFlowDetail(row.auditId) },
},
"审核单详情"
)
);
return h("div", actions);
},
},
],
productQuaColumns: [
{ title: "资质ID", key: "quaId", minWidth: 100, tooltip: true },
{ title: "资质名称", key: "name", minWidth: 180, tooltip: true },
{
title: "是否需申请",
key: "needToApply",
width: 100,
render: (h, params) => h("span", params.row.needToApply ? "是" : "否"),
},
{
title: "是否必填",
key: "mandatory",
width: 90,
render: (h, params) => h("span", params.row.mandatory ? "是" : "否"),
},
{ title: "描述", key: "tips", minWidth: 240, tooltip: true },
],
};
},
watch: {
innerTab(val) {
if (val === "apply" && this.applyData.length === 0) {
this.loadApplyPage();
}
},
},
mounted() {
this.loadThirdCategories();
},
methods: {
boolText(val) {
if (val === true) return "是";
if (val === false) return "否";
return "-";
},
async handleInitCategory() {
this.initLoading = true;
try {
const res = await initWxChannelsCategory();
if (res && res.success) {
this.$Message.success("初始化分类成功");
await this.loadThirdCategories();
}
} finally {
this.initLoading = false;
}
},
goApplyPage(row) {
const catId = row && row.catId;
if (!catId) {
this.$Message.warning("缺少类目ID");
return;
}
const path = this.findCategoryPathById(this.thirdData, catId) || [];
this.fullCategoryPath = path.map((item) => ({
catId: item.catId,
catName: item.catName,
}));
// 兜底:如果树中未命中,至少带上当前三级类目
if (this.fullCategoryPath.length === 0) {
this.fullCategoryPath = [
{
catId,
catName: (row && row.catName) || "",
},
];
}
this.$router.push({
name: "promotions-wxchannels-category-apply",
query: {
catId,
fullCategoryPath: JSON.stringify(this.fullCategoryPath),
},
});
},
findCategoryPathById(list, targetCatId, parentPath = []) {
if (!Array.isArray(list) || list.length === 0) return [];
for (let i = 0; i < list.length; i += 1) {
const item = list[i] || {};
const current = {
catId: item.catId || item.cat_id,
catName: item.catName || item.cat_name || "",
level: item.level,
};
const nextPath = parentPath.concat(current);
if (String(current.catId) === String(targetCatId)) {
return nextPath;
}
if (Array.isArray(item.children) && item.children.length > 0) {
const found = this.findCategoryPathById(item.children, targetCatId, nextPath);
if (found.length > 0) {
return found;
}
}
}
return [];
},
async loadThirdCategories() {
this.thirdLoading = true;
try {
// 首次仅请求顶级类目,不传 parentCatId
const res = await getWxChannelsThirdCategory({});
const list = res && res.success && Array.isArray(res.result) ? res.result : [];
this.thirdData = list.map((item) => this.normalizeCategoryNode(item));
} finally {
this.thirdLoading = false;
}
},
normalizeCategoryNode(item) {
const level = Number((item && item.level) || 0);
const hasChildren = !!(item && (item.hasChildren ?? item.has_children));
const node = {
...(item || {}),
catId: (item && (item.catId || item.cat_id)) || "",
catName: (item && (item.catName || item.cat_name)) || "-",
level,
hasChildren,
status: (item && item.status) || "",
qualification: (item && item.qualification) || "-",
qualificationType: item ? (item.qualificationType ?? item.qualification_type ?? 0) : 0,
productQualification: (item && (item.productQualification || item.product_qualification)) || "-",
productQualificationType: item ? (item.productQualificationType ?? item.product_qualification_type ?? 0) : 0,
};
// 仅在 hasChildren=true 时展示展开标识
if (hasChildren) {
node._loading = false;
node.children = [];
}
return node;
},
loadCategoryChildren(row, callback) {
if (!row || !row.catId || !row.hasChildren) {
callback([]);
return;
}
getWxChannelsThirdCategory({ parentCatId: row.catId })
.then((res) => {
if (res && res.success) {
const list = Array.isArray(res.result) ? res.result : [];
callback(list.map((item) => this.normalizeCategoryNode(item)));
return;
}
callback([]);
})
.catch(() => callback([]));
},
handleApplySearch() {
this.applyQuery.pageNumber = 1;
this.loadApplyPage();
},
changeApplyPage(pageNumber) {
this.applyQuery.pageNumber = pageNumber;
this.loadApplyPage();
},
changeApplyPageSize(pageSize) {
this.applyQuery.pageSize = pageSize;
this.applyQuery.pageNumber = 1;
this.loadApplyPage();
},
loadApplyPage() {
this.applyLoading = true;
const params = { ...this.applyQuery };
if (!params.status) {
delete params.status;
}
getWxChannelsCategoryPage(params)
.then((res) => {
if (res && res.success) {
const page = res.result || {};
this.applyData = Array.isArray(page.records) ? page.records : [];
this.applyTotal = Number(page.total || 0);
}
})
.finally(() => {
this.applyLoading = false;
});
},
async showCategoryDetail(catId) {
if (!catId) {
this.$Message.warning("缺少类目ID");
return;
}
const res = await getWxChannelsCategoryDetail({ catId });
if (res && res.success) {
this.detailModalTitle = "类目详情";
this.detailData = res.result || {};
this.detailRawJson = JSON.stringify(this.detailData, null, 2);
this.detailModalVisible = true;
}
},
async showFlowDetail(auditId) {
if (!auditId) {
this.$Message.warning("缺少审核单ID");
return;
}
const res = await getWxChannelsCategoryFlowDetail({ auditId });
if (res && res.success) {
this.detailModalTitle = "审核单详情";
this.detailRawJson = JSON.stringify(res.result || {}, null, 2);
this.detailModalVisible = true;
}
},
},
};
</script>
<style scoped lang="scss">
.wxchannels-category {
min-height: 360px;
}
.toolbar {
display: flex;
align-items: center;
margin-bottom: 12px;
}
::v-deep .category-tree-table .tree-expand-placeholder {
display: inline-block;
width: 1px;
}
::v-deep .category-tree-table .ivu-table-row td:first-child .ivu-table-cell {
padding-left: 10px;
padding-right: 8px;
}
.raw-json {
max-height: 420px;
overflow: auto;
margin: 0;
background: #f7f7f7;
border: 1px solid #eee;
padding: 10px;
border-radius: 4px;
}
.detail-title {
color: #666;
margin-bottom: 8px;
}
.detail-value {
font-size: 16px;
color: #17233d;
}
.attr-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px 16px;
color: #515a6e;
}
</style>

View File

@@ -0,0 +1,567 @@
<template>
<div class="wxchannels-funds">
<Row :gutter="12">
<Col :span="12">
<Card dis-hover>
<div slot="title">账户余额</div>
<Button size="small" :loading="balanceLoading" @click="loadBalance">刷新</Button>
<div class="mt_10 balance-grid">
<div>可提现余额(){{ balanceInfo.availableAmount }}</div>
<div>待结算余额(){{ balanceInfo.pendingAmount }}</div>
<div>二级商户号{{ balanceInfo.subMchid }}</div>
<div>错误码{{ balanceInfo.errcode }}</div>
</div>
<pre class="raw-json">{{ balanceJson }}</pre>
</Card>
</Col>
<Col :span="12">
<Card dis-hover>
<div slot="title">结算账户</div>
<Button size="small" :loading="bankLoading" @click="loadBankAcct">刷新</Button>
<div class="mt_10 balance-grid">
<div>错误码{{ bankInfo.errcode }}</div>
<div>账户类型{{ bankInfo.bankAccountType }}</div>
<div>开户银行{{ bankInfo.accountBank }}</div>
<div>开户银行省市编码{{ bankInfo.bankAddressCode }}</div>
<div>开户银行联行号{{ bankInfo.bankBranchId }}</div>
<div>开户银行全称{{ bankInfo.bankName }}</div>
<div>银行账号{{ bankInfo.accountNumber }}</div>
<div>账户名称{{ bankInfo.accountName }}</div>
</div>
<pre class="raw-json">{{ bankJson }}</pre>
</Card>
</Col>
</Row>
<Tabs v-model="innerTab" class="mt_10">
<TabPane label="资金流水" name="flows">
<Form inline :label-width="80" class="search-form">
<FormItem label="支付单号">
<Input v-model="flowQuery.transactionId" clearable placeholder="transactionId" style="width: 220px" />
</FormItem>
<FormItem label="每页数量">
<InputNumber :min="1" :max="100" v-model="flowQuery.pageSize" style="width: 120px" />
</FormItem>
<FormItem label="时间范围">
<DatePicker
type="daterange"
v-model="flowDateRange"
format="yyyy-MM-dd"
clearable
@on-change="handleFlowDateChange"
style="width: 240px"
></DatePicker>
</FormItem>
<Button type="primary" icon="ios-search" :loading="flowLoading" @click="handleFlowSearch">查询</Button>
</Form>
<Table border :loading="flowLoading" :columns="flowColumns" :data="flowData" class="mt_10"></Table>
<div class="pager-bar">
<span class="pager-text">当前条数{{ flowData.length }}</span>
<Button :disabled="flowLoading || !hasFlowPrev" @click="handleFlowPrev">上一页</Button>
<Button type="primary" :disabled="flowLoading || !flowNextKey" @click="handleFlowNext">下一页</Button>
</div>
</TabPane>
<TabPane label="订单流水" name="orderFlow">
<Form inline :label-width="90" class="search-form">
<FormItem label="结算状态">
<Select v-model="orderFlowQuery.orderSettleState" style="width: 140px">
<Option :value="0">未结算</Option>
<Option :value="1">已结算</Option>
</Select>
</FormItem>
<FormItem label="订单状态">
<InputNumber :min="0" v-model="orderFlowQuery.orderState" style="width: 120px" />
</FormItem>
<FormItem label="支付方式">
<InputNumber :min="0" v-model="orderFlowQuery.orderPayMethod" style="width: 120px" />
</FormItem>
<FormItem label="订单ID">
<Input v-model="orderFlowQuery.orderId" clearable style="width: 180px" />
</FormItem>
<FormItem label="页大小">
<InputNumber :min="1" :max="100" v-model="orderFlowQuery.limit" style="width: 120px" />
</FormItem>
<FormItem label="偏移量">
<InputNumber :min="0" v-model="orderFlowQuery.offset" style="width: 120px" />
</FormItem>
<FormItem label="使用上下文">
<i-switch v-model="orderFlowQuery.usePageCtx">
<span slot="open"></span>
<span slot="close"></span>
</i-switch>
</FormItem>
<FormItem label="分页上下文" v-if="orderFlowQuery.usePageCtx">
<Input v-model="orderFlowQuery.pageCtx" clearable style="width: 220px" placeholder="可为空,自动使用上次返回" />
</FormItem>
<FormItem label="时间范围">
<DatePicker
type="daterange"
v-model="orderFlowDateRange"
format="yyyy-MM-dd"
clearable
@on-change="handleOrderFlowDateChange"
style="width: 240px"
></DatePicker>
</FormItem>
<Button type="primary" icon="ios-search" :loading="orderFlowLoading" @click="handleOrderFlowSearch">查询</Button>
</Form>
<Table border :loading="orderFlowLoading" :columns="orderFlowColumns" :data="orderFlowData" class="mt_10"></Table>
<div class="pager-bar">
<span class="pager-text">总数量{{ orderFlowTotalCount }}pageCtx{{ orderFlowRespPageCtx || "-" }}</span>
</div>
</TabPane>
</Tabs>
<Modal v-model="detailModalVisible" :title="detailModalTitle" width="900" footer-hide>
<div v-if="detailType === 'flowDetail' && flowDetailData">
<Row :gutter="12">
<Col :span="8"><p class="detail-item">流水ID{{ flowDetailData.flowId || "-" }}</p></Col>
<Col :span="8"><p class="detail-item">资金类型{{ flowDetailData.fundsTypeDesc || "-" }}</p></Col>
<Col :span="8"><p class="detail-item">流水类型{{ flowTypeText(flowDetailData.flowType) }}</p></Col>
</Row>
<Row :gutter="12">
<Col :span="8"><p class="detail-item">金额(){{ flowDetailData.amount || "-" }}</p></Col>
<Col :span="8"><p class="detail-item">余额(){{ flowDetailData.balance || "-" }}</p></Col>
<Col :span="8"><p class="detail-item">记账时间{{ flowDetailData.bookkeepingTime || "-" }}</p></Col>
</Row>
<p class="detail-item">备注{{ flowDetailData.remark || "-" }}</p>
<Table
border
size="small"
class="mt_10"
:columns="flowRelatedColumns"
:data="Array.isArray(flowDetailData.relatedInfoList) ? flowDetailData.relatedInfoList : []"
></Table>
<pre class="raw-json mt_10">{{ detailRawJson }}</pre>
</div>
<pre v-else class="raw-json">{{ detailRawJson }}</pre>
</Modal>
</div>
</template>
<script>
import {
getWxChannelsFundsBalance,
getWxChannelsFundsBankAcct,
getWxChannelsFundsFlows,
getWxChannelsFundsFlowDetail,
getWxChannelsFundsOrderFlow,
} from "@/api/index";
export default {
name: "wxchannels-funds",
data() {
return {
innerTab: "flows",
balanceLoading: false,
bankLoading: false,
flowLoading: false,
orderFlowLoading: false,
balanceInfo: {
errcode: "-",
errmsg: "-",
availableAmount: "-",
pendingAmount: "-",
subMchid: "-",
},
bankInfo: {
errcode: "-",
bankAccountType: "-",
accountBank: "-",
bankAddressCode: "-",
bankBranchId: "-",
bankName: "-",
accountNumber: "-",
accountName: "-",
},
balanceJson: "{}",
bankJson: "{}",
flowData: [],
orderFlowData: [],
orderFlowTotalCount: 0,
orderFlowRespPageCtx: "",
flowDateRange: null,
orderFlowDateRange: null,
flowNextKey: "",
flowCursorStack: [],
flowCurrentCursor: "",
detailModalVisible: false,
detailModalTitle: "",
detailType: "raw",
flowDetailData: null,
detailRawJson: "",
flowQuery: {
page: 1,
pageSize: 10,
transactionId: "",
startTime: null,
endTime: null,
},
orderFlowQuery: {
orderSettleState: 1,
orderState: null,
orderPayMethod: null,
orderId: "",
limit: 10,
offset: 0,
usePageCtx: false,
pageCtx: "",
begin: null,
end: null,
},
flowColumns: [
{ title: "流水ID", key: "flowId", minWidth: 140, tooltip: true },
{ title: "资金类型", key: "fundsTypeDesc", minWidth: 140, tooltip: true },
{ title: "流水类型", key: "flowTypeText", minWidth: 100, tooltip: true },
{ title: "流水金额(分)", key: "amount", minWidth: 120, tooltip: true },
{ title: "余额(分)", key: "balance", minWidth: 120, tooltip: true },
{ title: "支付单号", key: "transactionId", minWidth: 180, tooltip: true },
{ title: "记账时间", key: "bookkeepingTime", minWidth: 170, tooltip: true },
{ title: "备注", key: "remark", minWidth: 200, tooltip: true },
{
title: "操作",
key: "action",
width: 100,
render: (h, params) => {
return h(
"Button",
{
props: { type: "text", size: "small" },
on: { click: () => this.showFlowDetail(params.row.flowId) },
},
"详情"
);
},
},
],
orderFlowColumns: [
{ title: "订单ID", key: "orderId", minWidth: 180, tooltip: true },
{ title: "结算状态", key: "orderSettleState", minWidth: 100, tooltip: true },
{ title: "订单状态", key: "orderState", minWidth: 100, tooltip: true },
{ title: "支付方式", key: "orderPayMethod", minWidth: 100, tooltip: true },
{ title: "创建时间(秒)", key: "orderCreateTime", minWidth: 120, tooltip: true },
{ title: "支付时间(秒)", key: "orderPaidTime", minWidth: 120, tooltip: true },
{ title: "商户实收(分)", key: "mchReceivedAmount", minWidth: 120, tooltip: true },
{ title: "支出金额(分)", key: "expenseAmount", minWidth: 120, tooltip: true },
{ title: "预计结算(分)", key: "mchSettleAmount", minWidth: 120, tooltip: true },
{ title: "用户实付(分)", key: "buyerPaidAmount", minWidth: 120, tooltip: true },
{
title: "原始数据",
key: "raw",
width: 100,
render: (h, params) => {
return h(
"Button",
{
props: { type: "text", size: "small" },
on: { click: () => this.showRaw("订单流水详情", params.row.__raw || {}) },
},
"查看"
);
},
},
],
flowRelatedColumns: [
{ title: "关联类型", key: "relatedType", minWidth: 100, tooltip: true },
{ title: "关联订单号", key: "orderId", minWidth: 180, tooltip: true },
{ title: "关联售后单号", key: "aftersaleId", minWidth: 180, tooltip: true },
{ title: "关联提现单号", key: "withdrawId", minWidth: 180, tooltip: true },
{ title: "关联支付单号", key: "transactionId", minWidth: 180, tooltip: true },
{ title: "记账时间", key: "bookkeepingTime", minWidth: 170, tooltip: true },
],
};
},
computed: {
hasFlowPrev() {
return this.flowCursorStack.length > 0;
},
},
mounted() {
this.loadBalance();
this.loadBankAcct();
this.loadFlows();
},
methods: {
showRaw(title, raw) {
this.detailModalTitle = title;
this.detailType = "raw";
this.flowDetailData = null;
this.detailRawJson = JSON.stringify(raw || {}, null, 2);
this.detailModalVisible = true;
},
flowTypeText(val) {
if (Number(val) === 1) return "收入";
if (Number(val) === 2) return "支出";
return val || "-";
},
loadBalance() {
this.balanceLoading = true;
getWxChannelsFundsBalance()
.then((res) => {
if (res && res.success) {
const result = res.result || {};
this.balanceInfo = {
errcode: result.errcode ?? "-",
errmsg: result.errmsg || "-",
availableAmount: result.availableAmount ?? result.available_amount ?? "-",
pendingAmount: result.pendingAmount ?? result.pending_amount ?? "-",
subMchid: result.subMchid || result.sub_mchid || "-",
};
this.balanceJson = JSON.stringify(result, null, 2);
}
})
.finally(() => {
this.balanceLoading = false;
});
},
loadBankAcct() {
this.bankLoading = true;
getWxChannelsFundsBankAcct()
.then((res) => {
if (res && res.success) {
const result = res.result || {};
const accountInfo = result.accountInfo || result.account_info || {};
this.bankInfo = {
errcode: result.errcode ?? "-",
bankAccountType: accountInfo.bankAccountType || accountInfo.bank_account_type || "-",
accountBank: accountInfo.accountBank || accountInfo.account_bank || "-",
bankAddressCode: accountInfo.bankAddressCode || accountInfo.bank_address_code || "-",
bankBranchId: accountInfo.bankBranchId || accountInfo.bank_branch_id || "-",
bankName: accountInfo.bankName || accountInfo.bank_name || "-",
accountNumber: accountInfo.accountNumber || accountInfo.account_number || "-",
accountName: accountInfo.accountName || accountInfo.account_name || "-",
};
this.bankJson = JSON.stringify(result, null, 2);
}
})
.finally(() => {
this.bankLoading = false;
});
},
handleFlowDateChange(v) {
if (!v || v.length !== 2) {
this.flowQuery.startTime = null;
this.flowQuery.endTime = null;
return;
}
const begin = v[0] ? Math.floor(new Date(`${v[0]}T00:00:00`).getTime() / 1000) : null;
const end = v[1] ? Math.floor(new Date(`${v[1]}T23:59:59`).getTime() / 1000) : null;
this.flowQuery.startTime = Number.isFinite(begin) ? begin : null;
this.flowQuery.endTime = Number.isFinite(end) ? end : null;
},
handleFlowSearch() {
this.flowQuery.page = 1;
this.flowCurrentCursor = "";
this.flowNextKey = "";
this.flowCursorStack = [];
this.loadFlows("");
},
handleFlowNext() {
if (!this.flowNextKey) return;
this.flowCursorStack.push(this.flowCurrentCursor || "");
this.flowQuery.page += 1;
this.loadFlows(this.flowNextKey);
},
handleFlowPrev() {
if (!this.hasFlowPrev) return;
const prev = this.flowCursorStack.pop() || "";
this.flowQuery.page = Math.max(1, this.flowQuery.page - 1);
this.loadFlows(prev);
},
loadFlows(nextKey = "") {
this.flowLoading = true;
this.flowCurrentCursor = nextKey || "";
const params = {
page: this.flowQuery.page,
pageSize: this.flowQuery.pageSize,
nextKey: nextKey || undefined,
startTime: this.flowQuery.startTime,
endTime: this.flowQuery.endTime,
transactionId: this.flowQuery.transactionId || undefined,
};
Object.keys(params).forEach((k) => {
if (params[k] === null || params[k] === "" || params[k] === undefined) delete params[k];
});
getWxChannelsFundsFlows(params)
.then((res) => {
if (res && res.success) {
const result = res.result || {};
const list = this.extractList(result);
this.flowData = list.map((item) => this.normalizeFlow(item));
this.flowNextKey = result.next_key || result.nextKey || "";
}
})
.finally(() => {
this.flowLoading = false;
});
},
async showFlowDetail(flowId) {
if (!flowId) {
this.$Message.warning("缺少流水ID");
return;
}
const res = await getWxChannelsFundsFlowDetail({ flowId });
if (res && res.success) {
const result = res.result || {};
this.detailModalTitle = "资金流水详情";
this.detailType = "flowDetail";
this.flowDetailData = result.fundsFlow || result.funds_flow || null;
this.detailRawJson = JSON.stringify(result, null, 2);
this.detailModalVisible = true;
}
},
handleOrderFlowDateChange(v) {
if (!v || v.length !== 2) {
this.orderFlowQuery.begin = null;
this.orderFlowQuery.end = null;
return;
}
const begin = v[0] ? Math.floor(new Date(`${v[0]}T00:00:00`).getTime() / 1000) : null;
const end = v[1] ? Math.floor(new Date(`${v[1]}T23:59:59`).getTime() / 1000) : null;
this.orderFlowQuery.begin = Number.isFinite(begin) ? begin : null;
this.orderFlowQuery.end = Number.isFinite(end) ? end : null;
},
handleOrderFlowSearch() {
if (this.orderFlowQuery.orderSettleState === null || this.orderFlowQuery.orderSettleState === undefined) {
this.$Message.warning("请填写订单结算状态");
return;
}
if (!this.orderFlowQuery.limit) {
this.$Message.warning("请填写页大小");
return;
}
this.orderFlowLoading = true;
const params = {
...this.orderFlowQuery,
};
if (params.usePageCtx && !params.pageCtx && this.orderFlowRespPageCtx) {
params.pageCtx = this.orderFlowRespPageCtx;
}
Object.keys(params).forEach((k) => {
if (params[k] === null || params[k] === "" || params[k] === undefined) delete params[k];
});
getWxChannelsFundsOrderFlow(params)
.then((res) => {
if (res && res.success) {
const result = res.result || {};
this.orderFlowTotalCount = Number(result.totalCount ?? result.total_count ?? 0);
this.orderFlowRespPageCtx = result.pageCtx || result.page_ctx || "";
const list = Array.isArray(result.dataList || result.data_list) ? result.dataList || result.data_list : [];
this.orderFlowData = list.map((item) => this.normalizeOrderFlow(item));
}
})
.finally(() => {
this.orderFlowLoading = false;
});
},
extractList(result) {
if (!result || typeof result !== "object") return [];
const keys = [
"fundsFlowList",
"funds_flow_list",
"list",
"records",
"items",
"data",
"flows",
"order_flows",
"orderFlowList",
"flow_list",
];
for (let i = 0; i < keys.length; i += 1) {
const value = result[keys[i]];
if (Array.isArray(value)) return value;
}
if (Array.isArray(result)) return result;
return [];
},
normalizeFlow(item) {
const raw = item || {};
const relatedList = Array.isArray(raw.relatedInfoList || raw.related_info_list)
? raw.relatedInfoList || raw.related_info_list
: [];
const firstRelated = relatedList[0] || {};
return {
flowId: raw.flow_id || raw.flowId || raw.id || "-",
fundsTypeDesc: raw.fundsTypeDesc || raw.funds_type_desc || "-",
flowTypeText: this.flowTypeText(raw.flowType ?? raw.flow_type),
transactionId:
firstRelated.transactionId ||
firstRelated.transaction_id ||
raw.transaction_id ||
raw.transactionId ||
raw.pay_transaction_id ||
"-",
amount: raw.amount ?? "-",
balance: raw.balance ?? "-",
bookkeepingTime: raw.bookkeepingTime || raw.bookkeeping_time || raw.create_time || raw.createTime || "-",
remark: raw.remark || "-",
__raw: raw,
};
},
normalizeOrderFlow(item) {
const raw = item || {};
return {
orderId: raw.order_id || raw.orderId || "-",
orderSettleState: raw.order_settle_state ?? raw.orderSettleState ?? "-",
orderState: raw.order_state ?? raw.orderState ?? "-",
orderPayMethod: raw.order_pay_method ?? raw.orderPayMethod ?? "-",
orderCreateTime: raw.order_create_time ?? raw.orderCreateTime ?? "-",
orderPaidTime: raw.order_paid_time ?? raw.orderPaidTime ?? "-",
mchReceivedAmount: raw.mch_received_amount ?? raw.mchReceivedAmount ?? "-",
expenseAmount: raw.expense_amount ?? raw.expenseAmount ?? "-",
mchSettleAmount: raw.mch_settle_amount ?? raw.mchSettleAmount ?? "-",
buyerPaidAmount: raw.buyer_paid_amount ?? raw.buyerPaidAmount ?? "-",
__raw: raw,
};
},
},
};
</script>
<style scoped lang="scss">
.wxchannels-funds {
min-height: 360px;
}
.pager-bar {
display: flex;
justify-content: flex-end;
align-items: center;
margin-top: 12px;
gap: 8px;
}
.pager-text {
color: #666;
margin-right: 8px;
}
.raw-json {
max-height: 240px;
overflow: auto;
margin-top: 10px;
margin-bottom: 0;
background: #f7f7f7;
border: 1px solid #eee;
padding: 10px;
border-radius: 4px;
}
.balance-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px 16px;
color: #515a6e;
}
.detail-item {
color: #515a6e;
margin-bottom: 10px;
}
</style>

View File

@@ -0,0 +1,138 @@
<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>
<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>
</div>
</template>
<script>
import { getWxChannelsGoodsPage } from "@/api/index";
export default {
name: "wxchannels-goods",
data() {
return {
loading: false,
total: 0,
data: [],
searchForm: {
goodsName: "",
status: "",
pageNumber: 1,
pageSize: 20,
},
columns: [
{
title: "商品图片",
key: "goodsImage",
width: 90,
align: "center",
render: (h, params) => {
return h("img", {
attrs: { src: params.row.goodsImage || "", 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: "平台SKU ID", key: "skuId", minWidth: 130, tooltip: true },
{ title: "店铺", key: "storeName", minWidth: 160, tooltip: true },
{ title: "分类", key: "categoryName", minWidth: 160, tooltip: true },
{ title: "销售价", key: "costPrice", width: 100 },
{ title: "微信小店价", key: "channelPrice", width: 110 },
{ title: "库存", key: "stock", width: 90 },
{
title: "状态",
key: "status",
width: 110,
render: (h, params) => {
const val = params.row.status;
const map = {
APPROVED: { label: "已通过", color: "green" },
PENDING: { label: "审核中", color: "orange" },
REJECTED: { label: "已拒绝", color: "red" },
};
const item = map[val] || { label: val || "-", color: "default" };
return h("Tag", { props: { color: item.color } }, item.label);
},
},
],
};
},
mounted() {
this.loadPage();
},
methods: {
handleSearch() {
this.searchForm.pageNumber = 1;
this.loadPage();
},
changePage(pageNumber) {
this.searchForm.pageNumber = pageNumber;
this.loadPage();
},
changePageSize(pageSize) {
this.searchForm.pageSize = pageSize;
this.searchForm.pageNumber = 1;
this.loadPage();
},
loadPage() {
this.loading = true;
const params = { ...this.searchForm };
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);
}
})
.finally(() => {
this.loading = false;
});
},
},
};
</script>

View File

@@ -0,0 +1,60 @@
<template>
<Card>
<Tabs v-model="activeTab">
<TabPane label="微信小店设置" name="setting">
<WxChannelsSetting v-if="activeTab === 'setting'" />
</TabPane>
<TabPane label="品牌列表" name="brand">
<WxChannelsBrand v-if="activeTab === 'brand'" />
</TabPane>
<TabPane label="微信小店类目" name="category">
<WxChannelsCategory v-if="activeTab === 'category'" />
</TabPane>
<TabPane label="微信小店商品" name="goods">
<WxChannelsGoods v-if="activeTab === 'goods'" />
</TabPane>
<TabPane label="微信小店订单" name="order">
<WxChannelsOrder v-if="activeTab === 'order'" />
</TabPane>
<TabPane label="微信小店退单" name="refund">
<WxChannelsRefund v-if="activeTab === 'refund'" />
</TabPane>
<TabPane label="微信小店资金" name="funds">
<WxChannelsFunds v-if="activeTab === 'funds'" />
</TabPane>
<TabPane label="微信小店概况" name="overview">
<WxChannelsOverview v-if="activeTab === 'overview'" />
</TabPane>
</Tabs>
</Card>
</template>
<script>
import WxChannelsSetting from "./setting.vue";
import WxChannelsBrand from "./brand.vue";
import WxChannelsCategory from "./category.vue";
import WxChannelsGoods from "./goods.vue";
import WxChannelsOrder from "./order.vue";
import WxChannelsRefund from "./refund.vue";
import WxChannelsFunds from "./funds.vue";
import WxChannelsOverview from "./overview.vue";
export default {
name: "promotions-wxchannels",
components: {
WxChannelsSetting,
WxChannelsBrand,
WxChannelsCategory,
WxChannelsGoods,
WxChannelsOrder,
WxChannelsRefund,
WxChannelsFunds,
WxChannelsOverview,
},
data() {
return {
activeTab: "setting",
};
},
};
</script>

View File

@@ -0,0 +1,178 @@
<template>
<div class="wxchannels-order">
<Row>
<Form :model="searchForm" inline :label-width="70" class="search-form">
<FormItem label="订单编号" prop="channelOrderSn">
<Input
v-model="searchForm.channelOrderSn"
placeholder="微信小店订单编号"
clearable
style="width: 220px"
/>
</FormItem>
<FormItem label="会员昵称" prop="memberNickName">
<Input
v-model="searchForm.memberNickName"
placeholder="请输入会员昵称"
clearable
style="width: 180px"
/>
</FormItem>
<FormItem label="商品名称" prop="goodsName">
<Input
v-model="searchForm.goodsName"
placeholder="请输入商品名称"
clearable
style="width: 200px"
/>
</FormItem>
<FormItem label="状态" prop="status">
<Input v-model="searchForm.status" placeholder="订单状态" clearable style="width: 140px" />
</FormItem>
<FormItem label="场景" prop="scene">
<Select v-model="searchForm.scene" clearable style="width: 140px" placeholder="全部">
<Option value="LIVE">直播</Option>
<Option value="WINDOW">橱窗</Option>
</Select>
</FormItem>
<FormItem label="下单时间">
<DatePicker
type="daterange"
v-model="selectDate"
format="yyyy-MM-dd"
clearable
@on-change="selectDateRange"
placeholder="选择起始时间"
style="width: 240px"
></DatePicker>
</FormItem>
<Button @click="handleSearch" type="primary" icon="ios-search" class="search-btn" :loading="loading">查询</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>
</div>
</template>
<script>
import { getWxChannelsOrderPage } from "@/api/index";
export default {
name: "wxchannels-order",
data() {
return {
loading: false,
total: 0,
data: [],
selectDate: null,
searchForm: {
channelOrderSn: "",
memberNickName: "",
goodsName: "",
status: "",
scene: "",
startTime: null,
endTime: null,
pageNumber: 1,
pageSize: 20,
},
columns: [
{ title: "微信小店订单编号", key: "channelOrderSn", minWidth: 200, tooltip: true },
{ title: "平台订单编号", key: "orderSn", minWidth: 200, tooltip: true },
{ title: "会员ID", key: "memberId", minWidth: 120, tooltip: true },
{ title: "会员昵称", key: "memberNickName", minWidth: 140, tooltip: true },
{ title: "订单金额", key: "amount", width: 110 },
{
title: "订单状态",
key: "status",
width: 130,
render: (h, params) => {
const val = params.row.status;
if (!val) return h("span", "-");
return h("Tag", { props: { color: "blue" } }, val);
},
},
{ title: "带货微信小店名称", key: "channelName", minWidth: 160, tooltip: true },
{
title: "下单场景",
key: "scene",
width: 110,
render: (h, params) => {
const val = params.row.scene;
const map = {
LIVE: { label: "直播", color: "orange" },
WINDOW: { label: "橱窗", color: "purple" },
};
const item = map[val] || { label: val || "-", color: "default" };
return h("Tag", { props: { color: item.color } }, item.label);
},
},
{ title: "创建时间", key: "createTime", minWidth: 160, tooltip: true },
],
};
},
mounted() {
this.loadPage();
},
methods: {
selectDateRange(v) {
if (!v || v.length !== 2) {
this.searchForm.startTime = null;
this.searchForm.endTime = null;
return;
}
const startStr = v[0];
const endStr = v[1];
const start = startStr ? new Date(`${startStr}T00:00:00`).getTime() : null;
const end = endStr ? new Date(`${endStr}T23:59:59`).getTime() : null;
this.searchForm.startTime = Number.isFinite(start) ? start : null;
this.searchForm.endTime = Number.isFinite(end) ? end : null;
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.loadPage();
},
changePage(pageNumber) {
this.searchForm.pageNumber = pageNumber;
this.loadPage();
},
changePageSize(pageSize) {
this.searchForm.pageSize = pageSize;
this.searchForm.pageNumber = 1;
this.loadPage();
},
loadPage() {
this.loading = true;
const params = { ...this.searchForm };
Object.keys(params).forEach((k) => {
if (params[k] === null || params[k] === "" || params[k] === undefined) delete params[k];
});
getWxChannelsOrderPage(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);
}
})
.finally(() => {
this.loading = false;
});
},
},
};
</script>

View File

@@ -0,0 +1,202 @@
<template>
<div class="wxchannels-overview">
<Row>
<Form inline :label-width="70" class="search-form">
<FormItem label="时间范围">
<DatePicker
type="daterange"
v-model="dateRange"
format="yyyy-MM-dd"
clearable
@on-change="handleDateChange"
style="width: 260px"
></DatePicker>
</FormItem>
<Button type="primary" icon="ios-search" :loading="loading" @click="handleSearch">查询</Button>
<Button style="margin-left: 8px" :loading="exportLoading" @click="handleExport">导出报表</Button>
</Form>
</Row>
<Row :gutter="12" class="mt_10">
<Col :span="8">
<Card dis-hover>
<p class="title">总销售额</p>
<p class="value">{{ formatAmount(summary.totalSales) }}</p>
</Card>
</Col>
<Col :span="8">
<Card dis-hover>
<p class="title">直播间销售额</p>
<p class="value">{{ formatAmount(summary.liveSales) }}</p>
</Card>
</Col>
<Col :span="8">
<Card dis-hover>
<p class="title">橱窗销售额</p>
<p class="value">{{ formatAmount(summary.windowSales) }}</p>
</Card>
</Col>
</Row>
<Row :gutter="12" class="mt_10">
<Col :span="8">
<Card dis-hover>
<p class="title">总退款金额</p>
<p class="value refund">{{ formatAmount(summary.totalRefund) }}</p>
</Card>
</Col>
<Col :span="8">
<Card dis-hover>
<p class="title">直播间退款金额</p>
<p class="value refund">{{ formatAmount(summary.liveRefund) }}</p>
</Card>
</Col>
<Col :span="8">
<Card dis-hover>
<p class="title">橱窗退款金额</p>
<p class="value refund">{{ formatAmount(summary.windowRefund) }}</p>
</Card>
</Col>
</Row>
<Table border :loading="loading" :columns="dailyColumns" :data="dailyData" class="mt_10"></Table>
</div>
</template>
<script>
import {
getWxChannelsOverviewSummary,
getWxChannelsOverviewDaily,
exportWxChannelsOverview,
} from "@/api/index";
export default {
name: "wxchannels-overview",
data() {
return {
loading: false,
exportLoading: false,
dateRange: null,
query: {
startTime: null,
endTime: null,
},
summary: {
totalSales: 0,
liveSales: 0,
windowSales: 0,
totalRefund: 0,
liveRefund: 0,
windowRefund: 0,
},
dailyData: [],
dailyColumns: [
{ title: "日期", key: "date", minWidth: 120, tooltip: true },
{ title: "总销售额", key: "totalSales", minWidth: 120 },
{ title: "直播间销售额", key: "liveSales", minWidth: 120 },
{ title: "橱窗销售额", key: "windowSales", minWidth: 120 },
{ title: "总退款金额", key: "totalRefund", minWidth: 120 },
{ title: "直播间退款金额", key: "liveRefund", minWidth: 120 },
{ title: "橱窗退款金额", key: "windowRefund", minWidth: 120 },
],
};
},
mounted() {
this.handleSearch();
},
methods: {
formatAmount(val) {
const n = Number(val || 0);
return Number.isFinite(n) ? n.toFixed(2) : "0.00";
},
handleDateChange(v) {
if (!v || v.length !== 2) {
this.query.startTime = null;
this.query.endTime = null;
return;
}
const start = v[0] ? new Date(`${v[0]}T00:00:00`).getTime() : null;
const end = v[1] ? new Date(`${v[1]}T23:59:59`).getTime() : null;
this.query.startTime = Number.isFinite(start) ? start : null;
this.query.endTime = Number.isFinite(end) ? end : null;
},
buildParams() {
const params = {
startTime: this.query.startTime,
endTime: this.query.endTime,
};
Object.keys(params).forEach((k) => {
if (params[k] === null || params[k] === undefined || params[k] === "") delete params[k];
});
return params;
},
async handleSearch() {
this.loading = true;
try {
const params = this.buildParams();
const [summaryRes, dailyRes] = await Promise.all([
getWxChannelsOverviewSummary(params),
getWxChannelsOverviewDaily(params),
]);
if (summaryRes && summaryRes.success) {
this.summary = {
...this.summary,
...(summaryRes.result || {}),
};
}
if (dailyRes && dailyRes.success) {
this.dailyData = Array.isArray(dailyRes.result) ? dailyRes.result : [];
}
} finally {
this.loading = false;
}
},
handleExport() {
this.exportLoading = true;
const params = this.buildParams();
exportWxChannelsOverview(params)
.then((res) => {
const blob = new Blob([res], {
type: "application/vnd.ms-excel;charset=utf-8",
});
if ("download" in document.createElement("a")) {
const link = document.createElement("a");
link.download = "微信小店概况报表.xls";
link.style.display = "none";
link.href = URL.createObjectURL(blob);
document.body.appendChild(link);
link.click();
URL.revokeObjectURL(link.href);
document.body.removeChild(link);
} else {
navigator.msSaveBlob(blob, "微信小店概况报表.xls");
}
})
.finally(() => {
this.exportLoading = false;
});
},
},
};
</script>
<style scoped lang="scss">
.wxchannels-overview {
min-height: 360px;
}
.title {
color: #666;
margin-bottom: 6px;
}
.value {
font-size: 22px;
font-weight: 600;
color: #2d8cf0;
}
.value.refund {
color: #ed4014;
}
</style>

View File

@@ -0,0 +1,186 @@
<template>
<div class="wxchannels-refund">
<Row>
<Form :model="searchForm" inline :label-width="70" class="search-form">
<FormItem label="退单编号" prop="channelRefundSn">
<Input
v-model="searchForm.channelRefundSn"
placeholder="微信小店退单编号"
clearable
style="width: 220px"
/>
</FormItem>
<FormItem label="订单编号" prop="channelOrderSn">
<Input
v-model="searchForm.channelOrderSn"
placeholder="微信小店订单编号"
clearable
style="width: 220px"
/>
</FormItem>
<FormItem label="会员昵称" prop="memberNickName">
<Input
v-model="searchForm.memberNickName"
placeholder="请输入会员昵称"
clearable
style="width: 180px"
/>
</FormItem>
<FormItem label="商品名称" prop="goodsName">
<Input
v-model="searchForm.goodsName"
placeholder="请输入商品名称"
clearable
style="width: 200px"
/>
</FormItem>
<FormItem label="状态" prop="status">
<Input v-model="searchForm.status" placeholder="退单状态" clearable style="width: 140px" />
</FormItem>
<FormItem label="场景" prop="scene">
<Select v-model="searchForm.scene" clearable style="width: 140px" placeholder="全部">
<Option value="LIVE">直播</Option>
<Option value="WINDOW">橱窗</Option>
</Select>
</FormItem>
<FormItem label="下单时间">
<DatePicker
type="daterange"
v-model="selectDate"
format="yyyy-MM-dd"
clearable
@on-change="selectDateRange"
placeholder="选择起始时间"
style="width: 240px"
></DatePicker>
</FormItem>
<Button @click="handleSearch" type="primary" icon="ios-search" class="search-btn" :loading="loading">查询</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>
</div>
</template>
<script>
import { getWxChannelsRefundPage } from "@/api/index";
export default {
name: "wxchannels-refund",
data() {
return {
loading: false,
total: 0,
data: [],
selectDate: null,
searchForm: {
channelRefundSn: "",
channelOrderSn: "",
memberNickName: "",
goodsName: "",
status: "",
scene: "",
startTime: null,
endTime: null,
pageNumber: 1,
pageSize: 20,
},
columns: [
{ title: "微信小店退单编号", key: "channelRefundSn", minWidth: 200, tooltip: true },
{ title: "微信小店订单编号", key: "channelOrderSn", minWidth: 200, tooltip: true },
{ title: "会员ID", key: "memberId", minWidth: 120, tooltip: true },
{ title: "会员昵称", key: "memberNickName", minWidth: 140, tooltip: true },
{ title: "退款金额", key: "amount", width: 110 },
{
title: "退单状态",
key: "status",
width: 130,
render: (h, params) => {
const val = params.row.status;
if (!val) return h("span", "-");
return h("Tag", { props: { color: "blue" } }, val);
},
},
{
title: "下单场景",
key: "scene",
width: 110,
render: (h, params) => {
const val = params.row.scene;
const map = {
LIVE: { label: "直播", color: "orange" },
WINDOW: { label: "橱窗", color: "purple" },
};
const item = map[val] || { label: val || "-", color: "default" };
return h("Tag", { props: { color: item.color } }, item.label);
},
},
{ title: "创建时间", key: "createTime", minWidth: 160, tooltip: true },
],
};
},
mounted() {
this.loadPage();
},
methods: {
selectDateRange(v) {
if (!v || v.length !== 2) {
this.searchForm.startTime = null;
this.searchForm.endTime = null;
return;
}
const startStr = v[0];
const endStr = v[1];
const start = startStr ? new Date(`${startStr}T00:00:00`).getTime() : null;
const end = endStr ? new Date(`${endStr}T23:59:59`).getTime() : null;
this.searchForm.startTime = Number.isFinite(start) ? start : null;
this.searchForm.endTime = Number.isFinite(end) ? end : null;
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.loadPage();
},
changePage(pageNumber) {
this.searchForm.pageNumber = pageNumber;
this.loadPage();
},
changePageSize(pageSize) {
this.searchForm.pageSize = pageSize;
this.searchForm.pageNumber = 1;
this.loadPage();
},
loadPage() {
this.loading = true;
const params = { ...this.searchForm };
Object.keys(params).forEach((k) => {
if (params[k] === null || params[k] === "" || params[k] === undefined) delete params[k];
});
getWxChannelsRefundPage(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);
}
})
.finally(() => {
this.loading = false;
});
},
},
};
</script>

View File

@@ -0,0 +1,182 @@
<template>
<div class="wxchannels-setting">
<Form
ref="settingForm"
:model="settingForm"
:rules="formValidate"
:label-width="170"
label-position="right"
>
<FormItem label="微信小店 AppId" prop="appId">
<Input
v-model="settingForm.appId"
clearable
placeholder="请输入微信小店 AppId"
style="width: 420px"
/>
</FormItem>
<FormItem label="微信小店 AppSecret" prop="appSecret">
<Input
v-model="settingForm.appSecret"
clearable
placeholder="请输入微信小店 AppSecret"
style="width: 420px"
/>
</FormItem>
<FormItem label="接口基础地址" prop="apiBase">
<Input
v-model="settingForm.apiBase"
clearable
placeholder="https://api.weixin.qq.com/minishop"
style="width: 420px"
/>
</FormItem>
<FormItem label="获取 access_token 地址" prop="tokenUrl">
<Input
v-model="settingForm.tokenUrl"
clearable
placeholder="https://api.weixin.qq.com/cgi-bin/token"
style="width: 420px"
/>
</FormItem>
<FormItem label="消息回调 Token" prop="callbackToken">
<Input
v-model="settingForm.callbackToken"
clearable
placeholder="请输入消息回调 Token"
style="width: 420px"
/>
</FormItem>
<FormItem label="消息回调 EncodingAESKey" prop="encodingAesKey">
<Input
v-model="settingForm.encodingAesKey"
clearable
placeholder="请输入消息回调 EncodingAESKey"
style="width: 420px"
/>
</FormItem>
<FormItem>
<Button type="primary" :loading="submitLoading" @click="handleSubmit">
保存设置
</Button>
<Button style="margin-left: 8px" @click="handleReset">重置</Button>
</FormItem>
</Form>
<Spin size="large" fix v-if="loading"></Spin>
</div>
</template>
<script>
import { getWxChannelsSetting, saveWxChannelsSetting } from "@/api/index";
const defaultSettingForm = () => ({
appId: "",
appSecret: "",
apiBase: "",
tokenUrl: "",
callbackToken: "",
encodingAesKey: "",
});
export default {
name: "wxchannels-setting",
data() {
const validateUrl = (rule, value, callback) => {
if (!value) {
callback(new Error(rule.message));
return;
}
const urlPattern = /^https?:\/\/.+/;
if (!urlPattern.test(value)) {
callback(new Error("请输入正确的 URL 地址"));
return;
}
callback();
};
return {
loading: false,
settingForm: defaultSettingForm(),
originalForm: defaultSettingForm(),
submitLoading: false,
formValidate: {
appId: [
{ required: true, message: "请输入微信小店 AppId", trigger: "blur" }
],
appSecret: [
{ required: true, message: "请输入微信小店 AppSecret", trigger: "blur" }
],
apiBase: [
{
required: true,
validator: validateUrl,
message: "请输入接口基础地址",
trigger: "blur",
}
],
tokenUrl: [
{
required: true,
validator: validateUrl,
message: "请输入获取 access_token 地址",
trigger: "blur",
}
],
},
};
},
methods: {
async loadData() {
this.loading = true;
try {
const res = await getWxChannelsSetting();
if (res && res.success) {
const nextForm = {
...defaultSettingForm(),
...(res.result || {}),
};
this.settingForm = nextForm;
this.originalForm = { ...nextForm };
}
} finally {
this.loading = false;
}
},
handleSubmit() {
this.$refs.settingForm.validate(async (valid) => {
if (!valid) {
return;
}
this.submitLoading = true;
try {
const payload = { ...this.settingForm };
const res = await saveWxChannelsSetting(payload);
if (res && res.success) {
this.originalForm = { ...payload };
this.$Message.success("保存成功");
}
} finally {
this.submitLoading = false;
}
});
},
handleReset() {
this.settingForm = { ...this.originalForm };
this.$nextTick(() => {
this.$refs.settingForm.clearValidate();
});
},
},
mounted() {
this.loadData().catch(() => {
this.$Message.error("获取设置失败");
});
},
};
</script>
<style scoped lang="scss">
.wxchannels-setting {
position: relative;
min-height: 320px;
}
</style>