mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-ui.git
synced 2026-11-07 01:18:54 +08:00
feat(manager): 优化列表操作列规范、会员分组统计、漏斗图重构与订单交互
1. 订单交互:移除 orderList 缓存机制修复默认带入订单号问题,支持 query 联动; 2. 会员模块:会员分组新增人数统计与联动,全量规范会员提现、充值、日志等操作列样式; 3. 商品与销量:虚拟销量优化首列缩略图与对齐方式,规范品牌与商品分组操作; 4. 数据统计:重构交易转化漏斗图为自适应 SVG 渐变图与指标卡片; 5. 系统与营销:统一菜单/部门操作列样式,优化 OSS 模块、营销活动与首页待办对齐。
This commit is contained in:
@@ -1,100 +1,138 @@
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<div class="wap-content">
|
||||
<div class="query-wrapper">
|
||||
<div class="query-item">
|
||||
<div>搜索范围</div>
|
||||
<el-input
|
||||
v-model="goodsParams.goodsName"
|
||||
placeholder="商品名称"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
@clear="onSearchGoods"
|
||||
@keyup.enter="onSearchGoods"
|
||||
/>
|
||||
</div>
|
||||
<div class="query-item">
|
||||
<el-cascader
|
||||
v-model="category"
|
||||
:options="skuList"
|
||||
placeholder="请选择商品分类"
|
||||
popper-class="goods-dialog-cascader-popper"
|
||||
style="width: 250px"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div class="query-item">
|
||||
<el-button type="primary" @click="onSearchGoods">搜索</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
v-loading="loading"
|
||||
class="goods-select-table"
|
||||
:data="goodsData"
|
||||
row-key="id"
|
||||
height="340"
|
||||
empty-text="暂无商品信息"
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<el-table-column
|
||||
type="selection"
|
||||
label="选择"
|
||||
width="60"
|
||||
align="center"
|
||||
reserve-selection
|
||||
/>
|
||||
<el-table-column label="商品图片" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<img class="goods-thumb" :src="row.thumbnail" alt="" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="goodsName"
|
||||
label="商品名称"
|
||||
min-width="200"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="商品规格" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.simpleSpecs || "默认规格" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="价格" width="110">
|
||||
<template #default="{ row }">
|
||||
<span class="goods-price">¥{{ $filters.unitPrice(row.price) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="库存" width="90">
|
||||
<template #default="{ row }">
|
||||
{{ row.quantity != null ? row.quantity : "—" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="销量" width="90">
|
||||
<template #default="{ row }">
|
||||
{{ row.buyCount != null ? row.buyCount : 0 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商品类型" width="110">
|
||||
<template #default="{ row }">
|
||||
{{ goodsTypeLabel(row.goodsType) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-model:current-page="goodsParams.pageNumber"
|
||||
class="pageration"
|
||||
:total="total"
|
||||
:page-size="goodsParams.pageSize"
|
||||
layout="total, prev, pager, next"
|
||||
size="small"
|
||||
@current-change="changePageSize"
|
||||
<div class="goods-dialog-wrapper">
|
||||
<!-- 顶部搜索筛选栏 -->
|
||||
<div class="goods-query-bar">
|
||||
<div class="query-item">
|
||||
<span class="query-label">商品名称:</span>
|
||||
<el-input
|
||||
v-model="goodsParams.goodsName"
|
||||
placeholder="请输入商品名称"
|
||||
clearable
|
||||
style="width: 220px"
|
||||
@clear="onSearchGoods"
|
||||
@keyup.enter="onSearchGoods"
|
||||
/>
|
||||
</div>
|
||||
<div class="query-item">
|
||||
<span class="query-label">商品编号:</span>
|
||||
<el-input
|
||||
v-model="goodsParams.sn"
|
||||
placeholder="请输入商品编号/货号"
|
||||
clearable
|
||||
style="width: 190px"
|
||||
@clear="onSearchGoods"
|
||||
@keyup.enter="onSearchGoods"
|
||||
/>
|
||||
</div>
|
||||
<div class="query-item">
|
||||
<span class="query-label">商品分类:</span>
|
||||
<el-cascader
|
||||
v-model="category"
|
||||
:options="skuList"
|
||||
placeholder="请选择商品分类"
|
||||
popper-class="goods-dialog-cascader-popper"
|
||||
style="width: 240px"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div class="query-item query-actions">
|
||||
<el-button type="primary" @click="onSearchGoods">搜索</el-button>
|
||||
<el-button @click="onResetGoods">重置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格展示区域 (自适应填充剩余高度) -->
|
||||
<div class="goods-table-wrap">
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
v-loading="loading"
|
||||
class="goods-select-table"
|
||||
:data="goodsData"
|
||||
row-key="id"
|
||||
height="100%"
|
||||
stripe
|
||||
border
|
||||
empty-text="暂无匹配商品信息"
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<el-table-column
|
||||
type="selection"
|
||||
width="50"
|
||||
align="center"
|
||||
reserve-selection
|
||||
/>
|
||||
<el-table-column label="商品图片" width="76" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
class="goods-thumb"
|
||||
:src="row.thumbnail"
|
||||
fit="cover"
|
||||
preview-teleported
|
||||
:preview-src-list="row.thumbnail ? [row.thumbnail] : []"
|
||||
>
|
||||
<template #error>
|
||||
<div class="thumb-slot">无图</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="goodsName"
|
||||
label="商品名称"
|
||||
min-width="260"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="goods-title-line">{{ row.goodsName || "-" }}</div>
|
||||
<div v-if="row.sn" class="goods-sn-line font-mono">货号: {{ row.sn }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商品规格" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.simpleSpecs || "默认规格" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="销售价" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span class="goods-price font-semibold">
|
||||
¥ {{ Number(row.price || 0).toFixed(2) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="可用库存" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.quantity != null ? row.quantity : "—" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="累计销量" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.buyCount != null ? row.buyCount : 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商品类型" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" type="info">{{ goodsTypeLabel(row.goodsType) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 底部翻页栏 -->
|
||||
<div class="goods-pagination-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="goodsParams.pageNumber"
|
||||
v-model:page-size="goodsParams.pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[15, 30, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
size="small"
|
||||
@current-change="changePage"
|
||||
@size-change="onPageSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { nextTick, onMounted, reactive, ref, watch } from "vue";
|
||||
import * as API_Goods from "@/api/goods";
|
||||
@@ -131,6 +169,7 @@ const tableRef = ref(null);
|
||||
watch(category, (val) => {
|
||||
goodsParams.categoryPath = val && val.length ? val[2] : "";
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.selectedWay,
|
||||
(val) => {
|
||||
@@ -139,6 +178,7 @@ watch(
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => goodsParams.categoryPath,
|
||||
() => {
|
||||
@@ -158,10 +198,27 @@ function onSearchGoods() {
|
||||
goodsParams.pageNumber = 1;
|
||||
getQueryGoodsList();
|
||||
}
|
||||
function changePageSize(v) {
|
||||
|
||||
function onResetGoods() {
|
||||
goodsParams.goodsName = "";
|
||||
goodsParams.sn = "";
|
||||
category.value = [];
|
||||
goodsParams.categoryPath = "";
|
||||
goodsParams.pageNumber = 1;
|
||||
getQueryGoodsList();
|
||||
}
|
||||
|
||||
function changePage(v) {
|
||||
goodsParams.pageNumber = v;
|
||||
getQueryGoodsList();
|
||||
}
|
||||
|
||||
function onPageSizeChange(val) {
|
||||
goodsParams.pageSize = val;
|
||||
goodsParams.pageNumber = 1;
|
||||
getQueryGoodsList();
|
||||
}
|
||||
|
||||
function getQueryGoodsList() {
|
||||
loading.value = true;
|
||||
API_Goods.getGoodsSkuData(goodsParams)
|
||||
@@ -172,6 +229,7 @@ function getQueryGoodsList() {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function initGoods(res) {
|
||||
const records = res?.result?.records || [];
|
||||
records.forEach((item) => {
|
||||
@@ -181,6 +239,7 @@ function initGoods(res) {
|
||||
goodsData.value = records;
|
||||
nextTick(syncTableSelection);
|
||||
}
|
||||
|
||||
function syncTableSelection() {
|
||||
const table = tableRef.value;
|
||||
if (!table) return;
|
||||
@@ -189,16 +248,19 @@ function syncTableSelection() {
|
||||
return;
|
||||
}
|
||||
goodsData.value.forEach((item) => {
|
||||
const selected = selectedList.value.some((e) => (e.id || e.skuId) && String(e.id || e.skuId) === String(item.id || item.skuId));
|
||||
const selected = selectedList.value.some(
|
||||
(e) => (e.id || e.skuId) && String(e.id || e.skuId) === String(item.id || item.skuId)
|
||||
);
|
||||
table.toggleRowSelection(item, selected);
|
||||
});
|
||||
}
|
||||
|
||||
function emitSelected(list) {
|
||||
selectedList.value = list;
|
||||
emit("selected", selectedList.value);
|
||||
}
|
||||
|
||||
function onSelectionChange(rows) {
|
||||
// 选择链接时为单选:保留最后勾选的一项,多选框交互不变
|
||||
if (type.value !== "multiple") {
|
||||
if (!rows.length) {
|
||||
emitSelected([]);
|
||||
@@ -214,17 +276,18 @@ function onSelectionChange(rows) {
|
||||
}
|
||||
emitSelected(rows);
|
||||
}
|
||||
|
||||
function init() {
|
||||
API_Goods.getGoodsSkuData(goodsParams).then((res) => {
|
||||
initGoods(res);
|
||||
});
|
||||
getQueryGoodsList();
|
||||
API_Goods.getCategoryTree({ deleteFlag: false }).then((res) => {
|
||||
if (res.success) {
|
||||
if (res.success && res.result) {
|
||||
deepGroup(res.result);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deepGroup(val) {
|
||||
skuList.value = [];
|
||||
val.forEach((item) => {
|
||||
let childWay = [];
|
||||
if (item.children) {
|
||||
@@ -252,34 +315,102 @@ function deepGroup(val) {
|
||||
});
|
||||
}
|
||||
|
||||
// 父组件 lili-dialog/index.vue 通过 $refs.goodsDialog.type = "single" 切换单选;
|
||||
// expose 的 ref 外部读写等价于原 data 字段
|
||||
defineExpose({ type });
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./style.scss";
|
||||
.wap-content {
|
||||
.goods-dialog-wrapper {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 顶部搜索栏 */
|
||||
.goods-query-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 0 0 12px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.query-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.query-label {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
white-space: nowrap;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.query-actions {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 表格容器 */
|
||||
.goods-table-wrap {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.goods-thumb {
|
||||
display: block;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin: 0 auto;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
border: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.thumb-slot {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 12px;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.goods-title-line {
|
||||
font-size: 13px;
|
||||
color: #303133;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.goods-sn-line {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.goods-price {
|
||||
color: $theme_color;
|
||||
color: #f56c6c;
|
||||
}
|
||||
.pageration {
|
||||
|
||||
/* 底部翻页栏 */
|
||||
.goods-pagination-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
padding-right: 8px;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
padding-top: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
<el-dialog
|
||||
v-if="goodsFlag"
|
||||
v-model="flag"
|
||||
width="1160px"
|
||||
top="120px"
|
||||
title="选择商品"
|
||||
width="85%"
|
||||
top="5vh"
|
||||
:z-index="10000"
|
||||
:close-on-click-modal="false"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
class="goods-selector-dialog"
|
||||
@close="clickClose"
|
||||
>
|
||||
<goodsDialog
|
||||
@@ -21,8 +23,15 @@
|
||||
"
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button @click="clickClose">取消</el-button>
|
||||
<el-button type="primary" @click="clickOK">确定</el-button>
|
||||
<div class="goods-dialog-footer">
|
||||
<span class="selected-count-tip">
|
||||
已选择 <strong class="text-primary">{{ goodsData.length }}</strong> 件商品
|
||||
</span>
|
||||
<div class="footer-actions">
|
||||
<el-button @click="clickClose">取消</el-button>
|
||||
<el-button type="primary" @click="clickOK">确定</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@@ -53,6 +62,7 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import goodsDialog from "./goods-dialog";
|
||||
@@ -61,8 +71,8 @@ import linkDialog from "./link-dialog";
|
||||
const emit = defineEmits(["closeFlag", "selectedGoodsData", "selectedLink"]);
|
||||
|
||||
const goodsFlag = ref(false); // 是否商品选择器
|
||||
const goodsData = ref([]); //选择的商品
|
||||
const linkData = ref(""); //选择的链接
|
||||
const goodsData = ref([]); // 选择的商品
|
||||
const linkData = ref(""); // 选择的链接
|
||||
const flag = ref(false); // modal显隐
|
||||
|
||||
// 模板 ref:原 ref="goodsDialog" 与 import 的组件同名,改名避免互相覆盖
|
||||
@@ -112,14 +122,7 @@ function close() {
|
||||
flag.value = false;
|
||||
}
|
||||
|
||||
// 外部通过 ref 访问的成员(grep manager/src 全量确认):
|
||||
// - open:page-decoration 各 modelList、modelForm/modelFormItem、hotzone/Zone、wap/advertising、alertAdvertising 等打开弹窗(goods/link)
|
||||
// - singleGoods:modelList/forYour.vue、mixModel.vue 直接调用
|
||||
// - clearGoodsSelected:wap/decorate.vue、modelList/goodsAndType.vue 调用
|
||||
// - goodsData:modelList/newGoodsSort.vue、notEnough.vue 回写已选商品
|
||||
// - goodsFlag:wap/decorate.vue 直接写入
|
||||
// - flag:wap/decorate.vue 直接写入 true 打开弹窗
|
||||
// - close:未被 grep 到外部调用,原 Options API 实例天然暴露,保守保留
|
||||
// 外部通过 ref 访问的成员:
|
||||
defineExpose({
|
||||
open,
|
||||
close,
|
||||
@@ -130,14 +133,73 @@ defineExpose({
|
||||
flag,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.el-dialog__body) {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.goods-dialog-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.selected-count-tip {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
|
||||
.text-primary {
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
margin: 0 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
/* 商品选择器弹窗全局尺寸与布局增强 */
|
||||
.goods-selector-dialog {
|
||||
max-width: 1360px;
|
||||
min-width: 900px;
|
||||
border-radius: 8px;
|
||||
|
||||
.el-dialog__header {
|
||||
padding: 16px 20px;
|
||||
margin-right: 0;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
|
||||
.el-dialog__title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
width: 100% !important;
|
||||
height: calc(84vh - 120px) !important;
|
||||
min-height: 520px !important;
|
||||
max-height: 760px !important;
|
||||
padding: 16px 20px !important;
|
||||
box-sizing: border-box !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.el-dialog__footer {
|
||||
padding: 12px 20px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
}
|
||||
|
||||
// 抽屉 append-to-body 后不在当前组件 DOM 内,不能使用 scoped 样式
|
||||
.link-selector-drawer {
|
||||
max-width: 96vw;
|
||||
|
||||
Reference in New Issue
Block a user