feat(manager): 优化列表操作列规范、会员分组统计、漏斗图重构与订单交互

1. 订单交互:移除 orderList 缓存机制修复默认带入订单号问题,支持 query 联动;

2. 会员模块:会员分组新增人数统计与联动,全量规范会员提现、充值、日志等操作列样式;

3. 商品与销量:虚拟销量优化首列缩略图与对齐方式,规范品牌与商品分组操作;

4. 数据统计:重构交易转化漏斗图为自适应 SVG 渐变图与指标卡片;

5. 系统与营销:统一菜单/部门操作列样式,优化 OSS 模块、营销活动与首页待办对齐。
This commit is contained in:
pikachu1995@126.com
2026-10-30 06:51:48 +08:00
parent 2563f122fc
commit c0cc8d8598
39 changed files with 16121 additions and 6705 deletions

View File

@@ -1,89 +1,239 @@
<template>
<div>
<el-card>
<el-form ref="formRef" :model="form" label-width="120px" :rules="formRule">
<div class="base-info-item">
<h4>积分商品信息</h4>
<div class="form-item-view">
<el-form-item label="商品名称">
<div>{{ form.goodsSku.goodsName }}</div>
</el-form-item>
<el-form-item label="SKU编码">
<div>{{ form.goodsSku.id }}</div>
</el-form-item>
<el-form-item label="店铺名称">
<div>{{ form.goodsSku.storeName }}</div>
</el-form-item>
<el-form-item label="商品价格">
<div>
<priceColorScheme :value="form.goodsSku.price" :color="$mainColor" />
</div>
</el-form-item>
<el-form-item label="库存">
<div>{{ form.goodsSku.quantity }}</div>
</el-form-item>
<el-form-item label="结算价格" prop="settlementPrice">
<el-input
type="number"
v-model="form.settlementPrice"
placeholder="请填写结算价格"
clearable
style="width: 260px"
/>
</el-form-item>
<el-form-item label="分类" prop="pointsGoodsCategoryId">
<el-select
v-model="form.pointsGoodsCategoryId"
placeholder="请选择分类"
style="width: 260px"
@change="changeCategory"
>
<el-option v-for="item in categoryList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="活动库存" prop="activeStock">
<el-input
type="number"
v-model="form.activeStock"
placeholder="请填写活动库存"
clearable
style="width: 260px"
/>
</el-form-item>
<el-form-item label="兑换积分" prop="points">
<el-input
type="number"
v-model="form.points"
placeholder="请填写兑换积分"
clearable
style="width: 260px"
/>
</el-form-item>
<el-form-item label="活动开始时间">
<el-date-picker
v-model="form.rangeTime"
type="datetimerange"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
start-placeholder="开始时间"
end-placeholder="结束时间"
:disabled-date="disabledDate"
style="width: 360px"
/>
</el-form-item>
</div>
<div class="footer">
<el-button @click="closeCurrentPage" style="margin-right: 5px">返回</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">保存</el-button>
<div class="points-goods-edit-page">
<!-- 顶部标题与返回栏 -->
<el-card shadow="never" class="header-card">
<div class="page-header-row">
<div class="header-left">
<div class="header-title">
<span>{{ onlyView ? "积分商品详情" : "编辑积分商品" }}</span>
<el-tag
size="small"
effect="plain"
:type="onlyView ? 'info' : 'warning'"
class="mode-tag"
>
{{ onlyView ? "查看模式" : "编辑模式" }}
</el-tag>
<el-tag
v-if="form.promotionStatus"
size="small"
effect="dark"
:type="promotionStatusTagType(form.promotionStatus)"
class="status-tag"
>
{{ promotionStatusText(form.promotionStatus) }}
</el-tag>
<span v-if="id" class="activity-id font-mono">ID: {{ id }}</span>
</div>
</div>
</el-form>
<div class="header-right">
<el-button @click="closeCurrentPage">返回积分商品列表</el-button>
</div>
</div>
</el-card>
<!-- 商品基本信息卡片 -->
<el-card shadow="never" class="info-card mt_10">
<div class="section-header">
<span class="section-indicator"></span>
<span class="section-title">商品基本信息</span>
</div>
<el-descriptions :column="2" border class="custom-descriptions">
<el-descriptions-item label="商品图片" width="120px">
<div class="goods-thumb-box">
<el-image
v-if="goodsThumbnail"
:src="goodsThumbnail"
fit="cover"
preview-teleported
:preview-src-list="[goodsThumbnail]"
class="goods-thumb-img"
>
<template #error>
<div class="thumb-slot">无图</div>
</template>
</el-image>
<div v-else class="thumb-slot">无图</div>
</div>
</el-descriptions-item>
<el-descriptions-item label="商品名称">
<div class="font-semibold text-dark">
{{ form.goodsName || form.goodsSku?.goodsName || "-" }}
</div>
<div v-if="form.goodsId" class="sub-id-text font-mono">
商品ID: {{ form.goodsId }}
</div>
</el-descriptions-item>
<el-descriptions-item label="SKU编码">
<span class="font-mono text-dark">
{{ form.skuId || form.goodsSku?.id || "-" }}
</span>
</el-descriptions-item>
<el-descriptions-item label="所属店铺">
<span class="text-secondary">
{{ form.goodsSku?.storeName || form.storeName || "-" }}
</span>
</el-descriptions-item>
<el-descriptions-item label="商品原价">
<span class="original-price-val font-mono">
{{ $filters.unitPrice(goodsPrice, "¥") }}
</span>
</el-descriptions-item>
<el-descriptions-item label="商品库存">
<span class="font-mono text-dark">
{{ form.goodsSku?.quantity != null ? `${form.goodsSku.quantity} 件` : "-" }}
</span>
</el-descriptions-item>
</el-descriptions>
</el-card>
<!-- 积分兑换配置区域 -->
<el-card shadow="never" class="info-card mt_10">
<div class="section-header">
<span class="section-indicator"></span>
<span class="section-title">积分兑换规则与参数</span>
</div>
<!-- 查看模式:以结构化描述列表呈现 -->
<template v-if="onlyView">
<el-descriptions :column="2" border class="custom-descriptions">
<el-descriptions-item label="兑换所需积分">
<span class="points-highlight font-mono font-semibold">
{{ form.points != null ? `${form.points} 积分` : "-" }}
</span>
</el-descriptions-item>
<el-descriptions-item label="结算价格">
<span class="font-mono font-semibold text-dark">
{{ form.settlementPrice != null ? $filters.unitPrice(form.settlementPrice, "¥") : "-" }}
</span>
</el-descriptions-item>
<el-descriptions-item label="积分商品分类">
<el-tag size="small" effect="plain" type="primary">
{{ currentCategoryName || "未设置分类" }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="活动库存">
<span class="font-mono text-dark font-semibold">
{{ form.activeStock != null ? `${form.activeStock} 件` : "-" }}
</span>
</el-descriptions-item>
<el-descriptions-item label="活动时间" :span="2">
<span class="font-mono text-dark">
{{ activityTimeRange }}
</span>
</el-descriptions-item>
</el-descriptions>
</template>
<!-- 编辑模式:表单输入 -->
<template v-else>
<el-form
ref="formRef"
:model="form"
label-width="120px"
:rules="formRule"
class="edit-form"
>
<el-form-item label="兑换积分" prop="points">
<el-input
v-model="form.points"
type="number"
placeholder="请填写所需兑换积分"
clearable
style="width: 280px"
>
<template #append>积分</template>
</el-input>
</el-form-item>
<el-form-item label="结算价格" prop="settlementPrice">
<el-input
v-model="form.settlementPrice"
type="number"
placeholder="请填写结算价格"
clearable
style="width: 280px"
>
<template #append>元</template>
</el-input>
</el-form-item>
<el-form-item label="积分分类" prop="pointsGoodsCategoryId">
<el-select
v-model="form.pointsGoodsCategoryId"
placeholder="请选择积分商品分类"
style="width: 280px"
@change="changeCategory"
>
<el-option
v-for="item in categoryList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="活动库存" prop="activeStock">
<el-input
v-model="form.activeStock"
type="number"
placeholder="请填写活动库存数量"
clearable
style="width: 280px"
>
<template #append>件</template>
</el-input>
</el-form-item>
<el-form-item label="活动时间" required class="points-activity-time-item">
<el-date-picker
v-model="form.rangeTime"
type="datetimerange"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
start-placeholder="开始时间"
end-placeholder="结束时间"
placeholder="请选择活动时间"
:disabled-date="disabledDate"
style="width: 380px"
/>
</el-form-item>
</el-form>
</template>
</el-card>
<!-- 底部操作栏 -->
<el-card shadow="never" class="footer-card mt_10">
<div class="footer-actions">
<el-button @click="closeCurrentPage">
{{ onlyView ? "返回" : "取消" }}
</el-button>
<el-button
v-if="!onlyView"
type="primary"
:loading="submitLoading"
@click="handleSubmit"
>
保存修改
</el-button>
</div>
</el-card>
</div>
</template>
<script setup>
import { onMounted, ref } from "vue";
import { onMounted, ref, computed } from "vue";
import { useRoute, useRouter } from "vue-router";
import {
updatePointsGoods,
@@ -99,6 +249,11 @@ const route = useRoute();
const router = useRouter();
const appStore = useAppStore();
const id = computed(() => route.query.id || "");
const onlyView = computed(() => {
return route.query.onlyView === "true" || route.query.onlyView === true;
});
const pointsGoodsCategoryId = ref("");
const pointsGoodsCategoryName = ref("");
const form = ref({
@@ -111,49 +266,106 @@ const form = ref({
promotionStatus: "NEW",
rangeTime: [],
});
const categoryList = ref([]);
const id = route.query.id;
const submitLoading = ref(false);
const formRule = {
settlementPrice: [{ required: true, message: "请填写结算价格" }],
pointsGoodsCategoryId: [{ required: true, message: "请选择积分商品分类" }],
points: [{ required: true, message: "请填写兑换积分" }],
activeStock: [{ required: true, message: "请填写库存" }],
settlementPrice: [{ required: true, message: "请填写结算价格", trigger: "blur" }],
pointsGoodsCategoryId: [
{ required: true, message: "请选择积分商品分类", trigger: "change" },
],
points: [{ required: true, message: "请填写兑换积分", trigger: "blur" }],
activeStock: [{ required: true, message: "请填写库存", trigger: "blur" }],
};
const formRef = ref(null);
onMounted(async () => {
await getCategory();
if (id) {
getData();
}
const goodsThumbnail = computed(() => {
return form.value.thumbnail || form.value.goodsSku?.thumbnail || "";
});
const goodsPrice = computed(() => {
if (form.value.goodsSku?.price != null) return form.value.goodsSku.price;
if (form.value.originalPrice != null) return form.value.originalPrice;
return 0;
});
const currentCategoryName = computed(() => {
if (pointsGoodsCategoryName.value) return pointsGoodsCategoryName.value;
if (form.value.pointsGoodsCategoryName) return form.value.pointsGoodsCategoryName;
const match = categoryList.value.find(
(c) => c.id === (pointsGoodsCategoryId.value || form.value.pointsGoodsCategoryId)
);
return match ? match.name : "";
});
const activityTimeRange = computed(() => {
const start = form.value.startTime || (form.value.rangeTime && form.value.rangeTime[0]);
const end = form.value.endTime || (form.value.rangeTime && form.value.rangeTime[1]);
if (start && end) {
return `${start} 至 ${end}`;
}
return start || end || "-";
});
function promotionStatusText(status) {
const map = {
NEW: "未开始",
START: "已开始",
END: "已结束",
CLOSE: "已关闭",
};
return map[status] || status;
}
function promotionStatusTagType(status) {
const map = {
NEW: "info",
START: "success",
END: "danger",
CLOSE: "danger",
};
return map[status] || "info";
}
function disabledDate(date) {
return date && date.getTime() < Date.now() - 86400000;
}
function changeCategory(id) {
const item = categoryList.value.find((c) => c.id === id);
pointsGoodsCategoryId.value = id;
function changeCategory(catId) {
const item = categoryList.value.find((c) => c.id === catId);
pointsGoodsCategoryId.value = catId;
pointsGoodsCategoryName.value = item ? item.name : "";
}
function closeCurrentPage() {
appStore.removeTag("edit-points-goods");
localStorage.pageOpenedList = JSON.stringify(appStore.pageOpenedList);
router.go(-1);
if (appStore?.removeTag) {
appStore.removeTag("edit-points-goods");
}
if (localStorage?.pageOpenedList && appStore?.pageOpenedList) {
localStorage.pageOpenedList = JSON.stringify(appStore.pageOpenedList);
}
if (window.history.length > 1) {
router.go(-1);
} else {
router.push({ name: "points-goods" });
}
}
function getData() {
getPointsGoodsById(id).then((res) => {
if (res.success) {
if (!id.value) return;
getPointsGoodsById(id.value).then((res) => {
if (res && res.success && res.result) {
const data = res.result;
form.value = data;
data.rangeTime = [];
form.value = {
...data,
goodsSku: data.goodsSku || {},
};
if (data.startTime && data.endTime) {
data.rangeTime = [data.startTime, data.endTime];
form.value.rangeTime = [data.startTime, data.endTime];
} else {
form.value.rangeTime = [];
}
if (data.pointsGoodsCategoryId) {
changeCategory(data.pointsGoodsCategoryId);
@@ -163,8 +375,14 @@ function getData() {
}
async function getCategory() {
const res = await getPointsGoodsCategoryList();
categoryList.value = res.result.records;
try {
const res = await getPointsGoodsCategoryList();
if (res && res.success && res.result) {
categoryList.value = res.result.records || [];
}
} catch (e) {
console.error("加载积分商品分类失败", e);
}
}
function handleSubmit() {
@@ -179,69 +397,203 @@ function handleSubmit() {
submitLoading.value = true;
const params = {
...form.value,
pointsGoodsCategoryId: pointsGoodsCategoryId.value || form.value.pointsGoodsCategoryId,
pointsGoodsCategoryId:
pointsGoodsCategoryId.value || form.value.pointsGoodsCategoryId,
pointsGoodsCategoryName:
pointsGoodsCategoryName.value || form.value.pointsGoodsCategoryName,
};
updatePointsGoods(params).then((res) => {
submitLoading.value = false;
if (res.success) {
Message.success("积分商品修改成功");
closeCurrentPage();
}
});
updatePointsGoods(params)
.then((res) => {
if (res && res.success) {
Message.success("积分商品修改成功");
closeCurrentPage();
}
})
.finally(() => {
submitLoading.value = false;
});
}
});
}
onMounted(async () => {
await getCategory();
if (id.value) {
getData();
}
});
</script>
<style lang="scss" scoped>
.el-form {
padding-bottom: 80px;
.points-goods-edit-page {
padding: 16px 20px;
background-color: #f0f2f5;
min-height: 100vh;
box-sizing: border-box;
.el-form-item {
width: 100%;
color: gray;
text-align: left;
}
}
div.base-info-item > div {
margin-left: 5%;
}
div.base-info-item {
margin-bottom: 10px;
h4 {
margin-bottom: 10px;
padding: 0 10px;
border: 1px solid #ddd;
background-color: #f8f8f8;
font-weight: bold;
color: #333;
font-size: 14px;
line-height: 40px;
text-align: left;
.mt_10 {
margin-top: 12px;
}
.form-item-view {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: space-between;
padding-left: 80px;
.font-mono {
font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
}
}
.footer {
width: 100%;
padding: 10px;
background-color: #ffc;
position: fixed;
bottom: 0;
right: 0;
text-align: center;
z-index: 9999;
.font-semibold {
font-weight: 600;
}
.text-dark {
color: #303133;
}
.text-secondary {
color: #606266;
}
.header-card {
border: none;
border-radius: 4px;
:deep(.el-card__body) {
padding: 16px 20px;
}
.page-header-row {
display: flex;
align-items: center;
justify-content: space-between;
.header-left {
.header-title {
display: flex;
align-items: center;
gap: 12px;
font-size: 18px;
font-weight: 600;
color: #1f2d3d;
.mode-tag,
.status-tag {
font-weight: normal;
}
.activity-id {
font-size: 13px;
color: #909399;
font-weight: normal;
}
}
}
}
}
.info-card {
border: none;
border-radius: 4px;
:deep(.el-card__body) {
padding: 20px;
}
.section-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
.section-indicator {
width: 4px;
height: 16px;
background-color: #409eff;
border-radius: 2px;
}
.section-title {
font-size: 15px;
font-weight: 600;
color: #303133;
}
}
.custom-descriptions {
:deep(.el-descriptions__label) {
width: 130px;
background-color: #f8f9fb;
color: #606266;
font-weight: 500;
}
:deep(.el-descriptions__content) {
color: #303133;
}
.goods-thumb-box {
width: 56px;
height: 56px;
border-radius: 4px;
overflow: hidden;
background-color: #f5f7fa;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #ebeef5;
.goods-thumb-img {
width: 100%;
height: 100%;
display: block;
}
.thumb-slot {
font-size: 12px;
color: #c0c4cc;
}
}
.sub-id-text {
font-size: 12px;
color: #909399;
margin-top: 4px;
}
.original-price-val {
color: #303133;
font-size: 14px;
}
.points-highlight {
color: #f56c6c;
font-size: 16px;
}
}
.edit-form {
padding: 8px 12px;
.points-activity-time-item {
:deep(.el-date-editor) {
width: 380px !important;
max-width: 380px;
flex-grow: 0;
}
}
}
}
.footer-card {
border: none;
border-radius: 4px;
:deep(.el-card__body) {
padding: 16px 20px;
}
.footer-actions {
display: flex;
justify-content: center;
gap: 16px;
}
}
}
</style>