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:
@@ -170,6 +170,10 @@ export const deleteMemberGroup = (id) => {
|
||||
return deleteRequest(`/member/memberGroup/delete/${id}`);
|
||||
};
|
||||
|
||||
export const getMemberGroupUsers = (groupId, params) => {
|
||||
return getRequest(`/member/memberGroup/${groupId}/users`, params);
|
||||
};
|
||||
|
||||
export const addMemberGroupUsers = (groupId, memberIds) => {
|
||||
return postRequest(`/member/memberGroup/${groupId}/users`, {
|
||||
memberIds: Array.isArray(memberIds) ? memberIds.join(",") : memberIds,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,28 +1,56 @@
|
||||
<template>
|
||||
<div class="search">
|
||||
<el-card>
|
||||
<el-form inline>
|
||||
<el-form-item label="会员">
|
||||
<el-input v-model="searchForm.memberName" clearable style="width: 160px" />
|
||||
<div class="finance-wallet-log-page search">
|
||||
<el-card shadow="never" class="filter-card">
|
||||
<el-form inline class="search-form" @keyup.enter="handleSearch">
|
||||
<el-form-item label="客户ID">
|
||||
<el-input v-model="searchForm.memberId" placeholder="请输入客户ID" clearable style="width: 180px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="日期">
|
||||
<el-date-picker v-model="dateRange" type="daterange" value-format="YYYY-MM-DD" />
|
||||
<el-form-item label="会员名称">
|
||||
<el-input v-model="searchForm.memberName" placeholder="请输入会员名称" clearable style="width: 160px" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-form-item label="日期范围">
|
||||
<el-date-picker v-model="dateRange" type="daterange" value-format="YYYY-MM-DD" style="width: 240px" />
|
||||
</el-form-item>
|
||||
<el-form-item class="form-actions">
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
<el-button @click="handleExport">导出</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-card class="mt_10">
|
||||
<el-table v-loading="loading" border :data="data">
|
||||
<el-table-column prop="memberName" label="会员" width="140" />
|
||||
<el-table-column prop="money" label="金额" width="110" />
|
||||
<el-table-column prop="serviceType" label="业务类型" width="140" />
|
||||
<el-table-column prop="detail" label="明细" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="createTime" label="时间" width="170" />
|
||||
|
||||
<el-card shadow="never" class="table-card mt_10">
|
||||
<template #header>
|
||||
<div class="card-header-bar">
|
||||
<div class="header-title">
|
||||
<span>钱包流水明细</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button size="small" @click="getDataList">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" border stripe :data="data">
|
||||
<el-table-column prop="memberId" label="客户ID" min-width="190" show-overflow-tooltip />
|
||||
<el-table-column prop="memberName" label="会员名称" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="money" label="金额" width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" :class="row.money >= 0 ? 'money-increase' : 'money-decrease'">
|
||||
{{ row.money >= 0 ? '+' : '' }}¥ {{ Number(row.money || 0).toFixed(2) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="serviceType" label="业务类型" width="130" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row ? row.serviceType || "-" : "-" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="detail" label="明细" min-width="240" show-overflow-tooltip />
|
||||
<el-table-column prop="createTime" label="时间" width="170" align="center" />
|
||||
</el-table>
|
||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
||||
|
||||
<div class="pagination-wrap mt_10">
|
||||
<el-pagination
|
||||
v-model:current-page="searchForm.pageNumber"
|
||||
v-model:page-size="searchForm.pageSize"
|
||||
@@ -42,15 +70,16 @@ import { useTable } from "@/utils/useTable";
|
||||
import * as API_Finance from "@/api/finance";
|
||||
import { getRequest } from "@/libs/axios";
|
||||
import { downloadBlob } from "@/utils/downloadBlob";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
defineOptions({ name: "finance-wallet-log" });
|
||||
|
||||
const dateRange = ref([]);
|
||||
const { loading, data, total, searchForm, getDataList, handleSearch } = useTable(
|
||||
const { loading, data, total, searchForm, getDataList, handleSearch, resetSearch } = useTable(
|
||||
(p) => getRequest("/wallet/log", p),
|
||||
{
|
||||
pageSize: 20,
|
||||
form: { memberName: "" },
|
||||
form: { memberId: "", memberName: "" },
|
||||
beforeRequest: (p) => {
|
||||
const params = { ...p };
|
||||
if (dateRange.value?.length === 2) {
|
||||
@@ -62,8 +91,18 @@ const { loading, data, total, searchForm, getDataList, handleSearch } = useTable
|
||||
}
|
||||
);
|
||||
|
||||
function handleReset() {
|
||||
dateRange.value = [];
|
||||
searchForm.memberId = "";
|
||||
searchForm.memberName = "";
|
||||
resetSearch();
|
||||
}
|
||||
|
||||
function handleExport() {
|
||||
const params = { memberName: searchForm.memberName };
|
||||
const params = {
|
||||
memberId: searchForm.memberId || undefined,
|
||||
memberName: searchForm.memberName || undefined,
|
||||
};
|
||||
if (dateRange.value?.length === 2) {
|
||||
params.startDate = dateRange.value[0];
|
||||
params.endDate = dateRange.value[1];
|
||||
@@ -73,3 +112,78 @@ function handleExport() {
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.finance-wallet-log-page {
|
||||
padding: 16px 20px;
|
||||
background-color: #f6f8fa;
|
||||
min-height: calc(100vh - 110px);
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-dark {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.filter-card {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ebeef5;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px 16px 2px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 14px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-card {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ebeef5;
|
||||
|
||||
.card-header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.header-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.money-increase {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.money-decrease {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.pagination-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,47 +1,82 @@
|
||||
<template>
|
||||
<div class="search">
|
||||
<el-card>
|
||||
<div class="brand-manage-page search">
|
||||
<!-- 顶部筛选表单 -->
|
||||
<el-card shadow="never" class="filter-card">
|
||||
<el-form
|
||||
ref="searchFormRef"
|
||||
:model="searchForm"
|
||||
inline
|
||||
label-width="70px"
|
||||
label-width="76px"
|
||||
class="search-form"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<el-form-item label="品牌名称">
|
||||
<el-form-item label="品牌名称" prop="name">
|
||||
<el-input
|
||||
v-model="searchForm.name"
|
||||
placeholder="请输入品牌名称"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
style="width: 220px"
|
||||
@clear="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-form-item class="form-actions">
|
||||
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card class="mt_10">
|
||||
<div class="operation padding-row" style="margin-bottom: 10px;">
|
||||
<el-button type="primary" @click="add">添加</el-button>
|
||||
</div>
|
||||
<!-- 品牌数据表格卡片 -->
|
||||
<el-card shadow="never" class="table-card mt_10">
|
||||
<template #header>
|
||||
<div class="card-header-bar">
|
||||
<div class="header-title">
|
||||
<span>商品品牌列表</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button size="small" @click="getDataList">刷新</el-button>
|
||||
<el-button type="primary" size="small" @click="add">添加品牌</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table ref="table" v-loading="loading" border :data="data" style="width: 100%">
|
||||
<el-table-column prop="name" label="品牌名称" width="200" />
|
||||
<el-table-column label="品牌图标" align="left">
|
||||
<el-table
|
||||
ref="table"
|
||||
v-loading="loading"
|
||||
border
|
||||
stripe
|
||||
:data="data"
|
||||
style="width: 100%"
|
||||
class="custom-brand-table"
|
||||
>
|
||||
<!-- 品牌ID -->
|
||||
<el-table-column prop="id" label="品牌ID" min-width="190" show-overflow-tooltip />
|
||||
|
||||
<!-- 品牌图标 -->
|
||||
<el-table-column label="品牌图标" width="96" align="center">
|
||||
<template #default="{ row }">
|
||||
<img
|
||||
v-if="row"
|
||||
:src="row.logo || ''"
|
||||
alt="加载图片失败"
|
||||
style="cursor: pointer; width: 80px; height: 60px; margin: 10px 0; object-fit: contain"
|
||||
/>
|
||||
<div v-if="row && row.logo" class="logo-box">
|
||||
<el-image
|
||||
:src="row.logo"
|
||||
fit="contain"
|
||||
preview-teleported
|
||||
:preview-src-list="[row.logo]"
|
||||
class="brand-logo-img"
|
||||
>
|
||||
<template #error>
|
||||
<div class="logo-slot">无图</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" align="left" width="150">
|
||||
|
||||
<!-- 品牌名称 -->
|
||||
<el-table-column prop="name" label="品牌名称" min-width="180" show-overflow-tooltip />
|
||||
|
||||
<!-- 状态 -->
|
||||
<el-table-column label="状态" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-if="row"
|
||||
@@ -53,8 +88,12 @@
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="170" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="230" align="center" fixed="right">
|
||||
|
||||
<!-- 创建时间 -->
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center" show-overflow-tooltip />
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row">
|
||||
<a class="link-text" @click="edit(row)">编辑</a>
|
||||
@@ -67,7 +106,8 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
||||
<!-- 底部分页 -->
|
||||
<div class="pagination-wrap mt_10">
|
||||
<el-pagination
|
||||
v-model:current-page="searchForm.pageNumber"
|
||||
v-model:page-size="searchForm.pageSize"
|
||||
@@ -81,24 +121,31 @@
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 添加 / 编辑品牌弹窗 -->
|
||||
<el-dialog
|
||||
v-model="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="500px"
|
||||
width="520px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form ref="formRef" :model="form" label-width="100px" :rules="formValidate">
|
||||
<el-form ref="formRef" :model="form" label-width="90px" :rules="formValidate">
|
||||
<el-form-item v-if="modalType === 1 && form.id" label="品牌ID">
|
||||
<span class="font-mono text-dark">{{ form.id }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="品牌名称" prop="name">
|
||||
<el-input v-model="form.name" style="width: 100%" />
|
||||
<el-input v-model="form.name" placeholder="请输入品牌名称" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="品牌图标" prop="logo">
|
||||
<div style="display: flex; align-items: center; gap: 8px">
|
||||
<img
|
||||
:src="form.logo || defaultPic"
|
||||
alt=""
|
||||
style="width: 60px; height: 60px; object-fit: contain; border: 1px solid #dcdfe6; border-radius: 4px"
|
||||
/>
|
||||
<el-button type="primary" link @click="openLogoPicker">修改</el-button>
|
||||
<div class="dialog-logo-picker">
|
||||
<div class="logo-preview-box">
|
||||
<img
|
||||
:src="form.logo || defaultPic"
|
||||
alt="品牌图标"
|
||||
class="dialog-logo-img"
|
||||
/>
|
||||
</div>
|
||||
<el-button type="primary" link @click="openLogoPicker">上传/选择图标</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -108,7 +155,8 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="picModelFlag" width="1200px" :show-close="true">
|
||||
<!-- OSS 图片选择组件弹窗 -->
|
||||
<el-dialog v-model="picModelFlag" width="1200px" title="选择品牌图标" :show-close="true">
|
||||
<ossManage
|
||||
ref="ossManageRef"
|
||||
:is-component="true"
|
||||
@@ -117,13 +165,15 @@
|
||||
/>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 关联分类弹窗 -->
|
||||
<el-dialog
|
||||
v-model="categoryModalVisible"
|
||||
:title="categoryModalTitle"
|
||||
width="700px"
|
||||
width="680px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<div v-loading="categoryTreeLoading" style="position: relative; max-height: 520px; overflow: auto">
|
||||
<div v-loading="categoryTreeLoading" class="category-tree-wrap">
|
||||
<el-tree
|
||||
ref="categoryTree"
|
||||
:key="categoryTreeKey"
|
||||
@@ -139,7 +189,7 @@
|
||||
<template #footer>
|
||||
<el-button @click="categoryModalVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="categorySubmitLoading" @click="submitBrandCategory">
|
||||
提交
|
||||
提交关联
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@@ -299,7 +349,7 @@ function handleStatusSwitchChange(row, checked) {
|
||||
}
|
||||
|
||||
function delBrand(row) {
|
||||
ElMessageBox.confirm("您确认要删除 " + row.name + " ?", "确认删除", { type: "warning" }).then(() => {
|
||||
ElMessageBox.confirm("您确认要删除品牌 " + row.name + " ?", "确认删除", { type: "warning" }).then(() => {
|
||||
return delBrandApi(row.id).then((res) => {
|
||||
if (res.success) {
|
||||
ElMessage.success("品牌删除成功!");
|
||||
@@ -339,7 +389,7 @@ function handleSubmit() {
|
||||
|
||||
function add() {
|
||||
modalType.value = 0;
|
||||
modalTitle.value = "添加";
|
||||
modalTitle.value = "添加品牌";
|
||||
formRef.value?.resetFields();
|
||||
delete form.value.id;
|
||||
modalVisible.value = true;
|
||||
@@ -347,7 +397,7 @@ function add() {
|
||||
|
||||
function edit(v) {
|
||||
modalType.value = 1;
|
||||
modalTitle.value = "编辑";
|
||||
modalTitle.value = "编辑品牌";
|
||||
formRef.value?.resetFields();
|
||||
for (const attr in v) {
|
||||
if (v[attr] === null) {
|
||||
@@ -360,13 +410,129 @@ function edit(v) {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.link-text {
|
||||
color: #409eff;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
.brand-manage-page {
|
||||
.filter-card {
|
||||
.search-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 0;
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-dark {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.text-placeholder {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.time-text {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
.custom-brand-table {
|
||||
.logo-box {
|
||||
width: 52px;
|
||||
height: 40px;
|
||||
margin: 0 auto;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
background: #fafafa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
|
||||
.brand-logo-img {
|
||||
width: 48px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.logo-slot {
|
||||
font-size: 11px;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
.op-split {
|
||||
margin: 0 8px;
|
||||
color: #dcdee2;
|
||||
|
||||
.dialog-logo-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.logo-preview-box {
|
||||
width: 64px;
|
||||
height: 48px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
background: #fafafa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
|
||||
.dialog-logo-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.category-tree-wrap {
|
||||
position: relative;
|
||||
max-height: 520px;
|
||||
overflow: auto;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,27 +1,84 @@
|
||||
<template>
|
||||
<div class="search">
|
||||
<el-card>
|
||||
<div class="operation padding-row">
|
||||
<el-button type="primary" @click="openAdd">添加分组</el-button>
|
||||
<div class="goods-group-page search">
|
||||
<!-- 顶部筛选与操作栏 -->
|
||||
<el-card shadow="never" class="filter-card">
|
||||
<div class="card-toolbar">
|
||||
<el-form inline :model="searchForm" class="filter-form" @keyup.enter="handleSearch">
|
||||
<el-form-item label="分组名称">
|
||||
<el-input
|
||||
v-model="searchForm.groupName"
|
||||
placeholder="请输入分组名称"
|
||||
clearable
|
||||
style="width: 220px"
|
||||
@clear="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="toolbar-right">
|
||||
<el-button type="primary" @click="openAdd">添加分组</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-table ref="table" v-loading="loading" :data="data" border class="mt_10" style="width: 100%">
|
||||
<!-- 分组数据表格卡片 -->
|
||||
<el-card shadow="never" class="table-card mt_10">
|
||||
<template #header>
|
||||
<div class="card-header-bar">
|
||||
<div class="header-title">
|
||||
<span>商品分组列表</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button size="small" @click="getData">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table
|
||||
ref="table"
|
||||
v-loading="loading"
|
||||
:data="data"
|
||||
border
|
||||
stripe
|
||||
style="width: 100%"
|
||||
class="custom-group-table"
|
||||
>
|
||||
<!-- 分组ID -->
|
||||
<el-table-column prop="id" label="分组ID" min-width="190" show-overflow-tooltip />
|
||||
|
||||
<!-- 分组名称 -->
|
||||
<el-table-column prop="groupName" label="分组名称" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="description" label="分组描述" min-width="240" show-overflow-tooltip />
|
||||
<el-table-column prop="createTime" label="创建时间" width="180" />
|
||||
<el-table-column prop="updateTime" label="更新时间" width="180" />
|
||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||
|
||||
<!-- 分组描述 -->
|
||||
<el-table-column prop="description" label="分组描述" min-width="240" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<div v-if="row" class="ops" style="display: flex; justify-content: center">
|
||||
{{ row ? row.description || "-" : "-" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 创建时间 -->
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center" />
|
||||
|
||||
<!-- 更新时间 -->
|
||||
<el-table-column prop="updateTime" label="更新时间" min-width="170" align="center" />
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<el-table-column label="操作" width="140" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row">
|
||||
<a class="link-text" @click="openEdit(row)">编辑</a>
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text" @click="remove(row)">删除</a>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
||||
<!-- 底部分页 -->
|
||||
<div class="pagination-wrap mt_10">
|
||||
<el-pagination
|
||||
v-model:current-page="searchForm.pageNumber"
|
||||
v-model:page-size="searchForm.pageSize"
|
||||
@@ -35,13 +92,32 @@
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="addFlag" title="添加商品分组" width="500px" :close-on-click-modal="false" destroy-on-close>
|
||||
<!-- 添加商品分组弹窗 -->
|
||||
<el-dialog
|
||||
v-model="addFlag"
|
||||
title="添加商品分组"
|
||||
width="520px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form ref="addFormRef" :model="formAdd" :rules="rulesAdd" label-width="90px">
|
||||
<el-form-item label="分组名称" prop="groupName" style="width: 90%">
|
||||
<el-input v-model="formAdd.groupName" maxlength="30" placeholder="请输入分组名称" />
|
||||
<el-form-item label="分组名称" prop="groupName">
|
||||
<el-input
|
||||
v-model="formAdd.groupName"
|
||||
maxlength="30"
|
||||
show-word-limit
|
||||
placeholder="请输入分组名称"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="分组描述" prop="description" style="width: 90%">
|
||||
<el-input v-model="formAdd.description" maxlength="200" placeholder="请输入分组描述" />
|
||||
<el-form-item label="分组描述" prop="description">
|
||||
<el-input
|
||||
v-model="formAdd.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
placeholder="请输入分组描述信息"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -50,14 +126,35 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="editFlag" title="编辑商品分组" width="500px" :close-on-click-modal="false" destroy-on-close>
|
||||
<!-- 编辑商品分组弹窗 -->
|
||||
<el-dialog
|
||||
v-model="editFlag"
|
||||
title="编辑商品分组"
|
||||
width="520px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form ref="editFormRef" :model="formEdit" :rules="rulesEdit" label-width="90px">
|
||||
<el-input v-model="formEdit.id" style="display: none" />
|
||||
<el-form-item label="分组名称" prop="groupName" style="width: 90%">
|
||||
<el-input v-model="formEdit.groupName" maxlength="30" placeholder="请输入分组名称" />
|
||||
<el-form-item label="分组ID">
|
||||
<span>{{ formEdit.id }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="分组描述" prop="description" style="width: 90%">
|
||||
<el-input v-model="formEdit.description" maxlength="200" placeholder="请输入分组描述" />
|
||||
<el-form-item label="分组名称" prop="groupName">
|
||||
<el-input
|
||||
v-model="formEdit.groupName"
|
||||
maxlength="30"
|
||||
show-word-limit
|
||||
placeholder="请输入分组名称"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="分组描述" prop="description">
|
||||
<el-input
|
||||
v-model="formEdit.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
placeholder="请输入分组描述信息"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -79,6 +176,7 @@ const loading = ref(true);
|
||||
const searchForm = reactive({
|
||||
pageNumber: 1,
|
||||
pageSize: 20,
|
||||
groupName: "",
|
||||
});
|
||||
const data = ref([]);
|
||||
const total = ref(0);
|
||||
@@ -86,18 +184,22 @@ const addFlag = ref(false);
|
||||
const editFlag = ref(false);
|
||||
const submitAddLoading = ref(false);
|
||||
const submitEditLoading = ref(false);
|
||||
|
||||
const formAdd = ref({
|
||||
groupName: "",
|
||||
description: "",
|
||||
});
|
||||
|
||||
const formEdit = ref({
|
||||
id: "",
|
||||
groupName: "",
|
||||
description: "",
|
||||
});
|
||||
|
||||
const rulesAdd = reactive({
|
||||
groupName: [{ required: true, message: "请输入分组名称", trigger: "blur" }],
|
||||
});
|
||||
|
||||
const rulesEdit = reactive({
|
||||
groupName: [{ required: true, message: "请输入分组名称", trigger: "blur" }],
|
||||
});
|
||||
@@ -105,7 +207,14 @@ const rulesEdit = reactive({
|
||||
const addFormRef = ref(null);
|
||||
const editFormRef = ref(null);
|
||||
|
||||
function init() {
|
||||
function handleSearch() {
|
||||
searchForm.pageNumber = 1;
|
||||
getData();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.pageNumber = 1;
|
||||
searchForm.groupName = "";
|
||||
getData();
|
||||
}
|
||||
|
||||
@@ -216,36 +325,78 @@ function submitEdit() {
|
||||
}
|
||||
|
||||
function remove(row) {
|
||||
ElMessageBox.confirm("确定删除该分组?", "提示", { type: "warning" }).then(() => {
|
||||
return API_Goods.deleteGoodsGroup(row.id)
|
||||
.then((res) => {
|
||||
if (res && res.success) {
|
||||
ElMessage.success("删除成功");
|
||||
getData();
|
||||
} else if (res && res.message) {
|
||||
ElMessage.error(res.message);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.error("删除失败,请检查权限或后端接口");
|
||||
});
|
||||
}).catch(() => {});
|
||||
ElMessageBox.confirm(`确定删除分组 "${row.groupName || "该分组"}" 吗?`, "提示", {
|
||||
type: "warning",
|
||||
confirmButtonText: "确定删除",
|
||||
cancelButtonText: "取消",
|
||||
})
|
||||
.then(() => {
|
||||
return API_Goods.deleteGoodsGroup(row.id)
|
||||
.then((res) => {
|
||||
if (res && res.success) {
|
||||
ElMessage.success("删除成功");
|
||||
getData();
|
||||
} else if (res && res.message) {
|
||||
ElMessage.error(res.message);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.error("删除失败,请检查权限或后端接口");
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
getData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ops a {
|
||||
color: #2d8cf0;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
.ops span {
|
||||
display: inline-block;
|
||||
margin: 0 8px;
|
||||
color: #dcdee2;
|
||||
<style scoped lang="scss">
|
||||
.goods-group-page {
|
||||
.card-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
|
||||
.filter-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 0;
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.card-header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -51,7 +51,6 @@
|
||||
<el-card class="mt_10">
|
||||
<el-alert
|
||||
type="info"
|
||||
show-icon
|
||||
:closable="false"
|
||||
title="支持按规格逐条设置虚拟销量,列表总销量 = 真实销量 + 虚拟销量。"
|
||||
style="margin-bottom: 10px"
|
||||
@@ -71,55 +70,65 @@
|
||||
ref="tableRef"
|
||||
v-loading="loading"
|
||||
:data="data"
|
||||
border
|
||||
class="mt_10"
|
||||
style="width: 100%"
|
||||
@sort-change="changeSort"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column prop="id" label="SKU ID" width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="goodsId" label="SPU编码" width="180" show-overflow-tooltip />
|
||||
<el-table-column label="商品信息" min-width="360">
|
||||
<el-table-column label="商品信息" min-width="320">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row" class="goods-info">
|
||||
<img
|
||||
v-if="row.thumbnail"
|
||||
:src="row.thumbnail"
|
||||
class="goods-thumbnail"
|
||||
alt=""
|
||||
/>
|
||||
<div class="goods-thumb-box">
|
||||
<el-image
|
||||
v-if="row.thumbnail"
|
||||
:src="row.thumbnail"
|
||||
fit="cover"
|
||||
preview-teleported
|
||||
:preview-src-list="[row.thumbnail]"
|
||||
class="goods-thumbnail"
|
||||
>
|
||||
<template #error>
|
||||
<div class="thumb-slot">无图</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div v-else class="thumb-slot">无图</div>
|
||||
</div>
|
||||
<div class="goods-text">
|
||||
<div class="div-zoom">{{ row.goodsName }}</div>
|
||||
<div v-if="row.simpleSpecs" class="sub-title">
|
||||
<div class="goods-title" :title="row.goodsName">{{ row.goodsName || "-" }}</div>
|
||||
<div v-if="row.simpleSpecs" class="goods-specs">
|
||||
规格:{{ row.simpleSpecs.trim() || "-" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="真实销量" width="110">
|
||||
<el-table-column prop="goodsId" label="SPU编码" width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="id" label="SKU ID" width="160" show-overflow-tooltip />
|
||||
<el-table-column label="真实销量" width="110" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.buyCount || 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="虚拟销量" width="110">
|
||||
<el-table-column label="虚拟销量" width="110" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.virtualSalesInput || 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="总销量" width="110">
|
||||
<el-table-column label="总销量" width="110" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ getTotalSales(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row" :type="row.marketEnable === 'UPPER' ? 'success' : 'warning'">
|
||||
<span v-if="row">
|
||||
{{ row.marketEnable === "UPPER" ? "上架" : "下架" }}
|
||||
</el-tag>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" align="center" fixed="right">
|
||||
<el-table-column label="操作" width="90" align="center" fixed="right">
|
||||
<template #default="{ row, $index }">
|
||||
<a
|
||||
v-if="row"
|
||||
@@ -445,32 +454,63 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sub-title {
|
||||
margin-top: 6px;
|
||||
color: #808695;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.goods-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 5px 0;
|
||||
padding: 10px 0;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.goods-thumb-box {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
min-width: 44px;
|
||||
margin-right: 12px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #ebeef5;
|
||||
background-color: #f8f9fa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.goods-thumbnail {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
margin-right: 12px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.thumb-slot {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.goods-text {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.div-zoom {
|
||||
.goods-title {
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
word-break: break-all;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.goods-specs {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -493,11 +533,28 @@ onMounted(() => {
|
||||
|
||||
.sku-info-label {
|
||||
width: 90px;
|
||||
color: #808695;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mt_10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.link-text {
|
||||
color: #2d8cf0;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
display: inline-block;
|
||||
|
||||
&:hover {
|
||||
color: #57a3f3;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: #c5c8ce;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -77,9 +77,9 @@
|
||||
<div class="counts">{{ notices.appointmentWorkOrder || 0 }}</div>
|
||||
<div>未完成预约工单</div>
|
||||
</div>
|
||||
<div class="todo-item" @click="goProcurementAudit">
|
||||
<div class="counts">{{ notices.procurementAudit || 0 }}</div>
|
||||
<div>待审核采购单</div>
|
||||
<div class="todo-item" @click="goMemberWithdrawApply">
|
||||
<div class="counts">{{ notices.balanceCash || 0 }}</div>
|
||||
<div>待审核会员提现</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -321,10 +321,10 @@ function goAppointmentWorkOrders() {
|
||||
});
|
||||
}
|
||||
|
||||
function goProcurementAudit() {
|
||||
function goMemberWithdrawApply() {
|
||||
router.push({
|
||||
name: "manager-procurement-order-list",
|
||||
query: { status: "SUBMITTED" },
|
||||
name: "withdrawApply",
|
||||
query: { applyStatus: "APPLY" },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
</a>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu v-if="value != 0">
|
||||
<el-dropdown-item v-if="res.balanceCash" command="deposit">
|
||||
<el-badge :value="res.balanceCash">待处理预存款提现申请</el-badge>
|
||||
<el-dropdown-item v-if="res.balanceCash" command="withdrawApply">
|
||||
<el-badge :value="res.balanceCash">待处理会员提现申请</el-badge>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="res.complain" command="orderComplaint">
|
||||
<el-badge :value="res.complain">待处理投诉审核</el-badge>
|
||||
@@ -28,9 +28,6 @@
|
||||
<el-dropdown-item v-if="res.appointmentWorkOrder" command="manager-appointment-work-orders">
|
||||
<el-badge :value="res.appointmentWorkOrder">未完成预约工单</el-badge>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="res.procurementAudit" command="manager-procurement-order-list">
|
||||
<el-badge :value="res.procurementAudit">待审核采购单</el-badge>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
@@ -63,12 +60,12 @@ const value = computed(() => {
|
||||
});
|
||||
|
||||
function navigateTo(name) {
|
||||
if (name === "manager-appointment-work-orders") {
|
||||
router.push({ name, query: { serviceStatus: "OPEN" } });
|
||||
if (name === "withdrawApply") {
|
||||
router.push({ name: "withdrawApply", query: { applyStatus: "APPLY" } });
|
||||
return;
|
||||
}
|
||||
if (name === "manager-procurement-order-list") {
|
||||
router.push({ name, query: { status: "SUBMITTED" } });
|
||||
if (name === "manager-appointment-work-orders") {
|
||||
router.push({ name, query: { serviceStatus: "OPEN" } });
|
||||
return;
|
||||
}
|
||||
router.push({ name });
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
<template>
|
||||
<div class="search">
|
||||
<el-card>
|
||||
<div class="member-recharge-page search">
|
||||
<!-- 顶部筛选表单 -->
|
||||
<el-card shadow="never" class="filter-card">
|
||||
<el-form
|
||||
ref="searchFormRef"
|
||||
:model="searchForm"
|
||||
inline
|
||||
label-width="70px"
|
||||
label-width="76px"
|
||||
class="search-form"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<el-form-item label="客户ID" prop="memberId">
|
||||
<el-input
|
||||
v-model="searchForm.memberId"
|
||||
placeholder="请输入客户ID"
|
||||
clearable
|
||||
style="width: 180px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户名称" prop="memberName">
|
||||
<el-input
|
||||
v-model="searchForm.memberName"
|
||||
placeholder="请输入客户名称"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
style="width: 160px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="充值单号" prop="rechargeSn">
|
||||
@@ -22,59 +31,106 @@
|
||||
v-model="searchForm.rechargeSn"
|
||||
placeholder="请输入充值单号"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
style="width: 190px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="支付时间">
|
||||
<el-form-item label="充值时间">
|
||||
<el-date-picker
|
||||
v-model="selectDate"
|
||||
type="datetimerange"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
clearable
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
placeholder="选择起始时间"
|
||||
style="width: 360px"
|
||||
style="width: 320px"
|
||||
@change="selectDateRange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
|
||||
<el-form-item class="form-actions">
|
||||
<el-button type="primary" class="search-btn" @click="handleSearch">
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-card>
|
||||
|
||||
<!-- 充值数据表格 -->
|
||||
<el-card shadow="never" class="table-card mt_10">
|
||||
<template #header>
|
||||
<div class="card-header-bar">
|
||||
<div class="header-title">
|
||||
<span>会员充值记录</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button size="small" @click="getDataList">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table
|
||||
ref="table"
|
||||
v-loading="loading"
|
||||
border
|
||||
stripe
|
||||
:data="data"
|
||||
ref="table"
|
||||
class="mt_10"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="memberName" label="客户名称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="rechargeSn" label="订单号" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="充值金额" width="160" sortable>
|
||||
<!-- 客户ID -->
|
||||
<el-table-column prop="memberId" label="客户ID" min-width="170" show-overflow-tooltip />
|
||||
|
||||
<!-- 客户名称 -->
|
||||
<el-table-column prop="memberName" label="客户名称" min-width="140" show-overflow-tooltip />
|
||||
|
||||
<!-- 充值单号 -->
|
||||
<el-table-column prop="rechargeSn" label="充值单号" min-width="180" show-overflow-tooltip />
|
||||
|
||||
<!-- 充值金额 -->
|
||||
<el-table-column label="充值金额" width="140" align="right" sortable prop="rechargeMoney">
|
||||
<template #default="{ row }">
|
||||
<priceColorScheme v-if="row" :value="row.rechargeMoney" :color="$mainColor" unit="+" />
|
||||
<span v-if="row" class="recharge-money-text">
|
||||
+¥ {{ formatMoney(row.rechargeMoney) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="充值方式" width="120">
|
||||
|
||||
<!-- 充值方式 -->
|
||||
<el-table-column label="充值方式" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ rechargeWayText(row.rechargeWay) }}</span>
|
||||
{{ row ? rechargeWayText(row.rechargeWay) : "-" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="支付状态" width="120">
|
||||
|
||||
<!-- 支付状态 -->
|
||||
<el-table-column label="支付状态" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row" :type="row.payStatus === 'PAID' ? 'success' : 'danger'">
|
||||
{{ row.payStatus === "PAID" ? "已付款" : "未付款" }}
|
||||
</el-tag>
|
||||
{{ row ? (row.payStatus === "PAID" ? "已付款" : "未付款") : "-" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 第三方流水号 -->
|
||||
<el-table-column prop="receivableNo" label="第三方流水号" min-width="180" show-overflow-tooltip />
|
||||
|
||||
<!-- 充值时间 -->
|
||||
<el-table-column prop="createTime" label="充值时间" width="170" align="center" />
|
||||
|
||||
<!-- 支付时间 -->
|
||||
<el-table-column prop="payTime" label="支付时间" width="170" align="center" />
|
||||
|
||||
<!-- 操作 -->
|
||||
<el-table-column label="操作" width="110" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row">
|
||||
<a v-if="row.memberId" class="link-text" @click="viewMemberDetail(row)">客户详情</a>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="充值时间" width="190" />
|
||||
<el-table-column prop="payTime" label="支付时间" width="190" />
|
||||
</el-table>
|
||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
||||
|
||||
<!-- 底部分页 -->
|
||||
<div class="pagination-wrap mt_10">
|
||||
<el-pagination
|
||||
v-model:current-page="searchForm.pageNumber"
|
||||
v-model:page-size="searchForm.pageSize"
|
||||
@@ -91,49 +147,85 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useTable } from "@/utils/useTable";
|
||||
import { getUserRecharge } from "@/api/member";
|
||||
import { customRouterPush } from "@/utils/filters";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
defineOptions({ name: "recharge" });
|
||||
|
||||
// 模板 el-form 的 ref(原 ref="searchForm",与 data 同名会互相覆盖,改名为 searchFormRef)
|
||||
const searchFormRef = ref(null);
|
||||
const table = ref(null);
|
||||
|
||||
const selectDate = ref(null);
|
||||
const { loading, data, total, searchForm, getDataList } = useTable(
|
||||
|
||||
const { loading, data, total, searchForm, getDataList, handleSearch, resetSearch } = useTable(
|
||||
getUserRecharge,
|
||||
{
|
||||
pageSize: 20,
|
||||
immediate: false,
|
||||
form: { sort: "createTime", order: "desc", startDate: "", endDate: "", memberName: "" },
|
||||
form: {
|
||||
sort: "createTime",
|
||||
order: "desc",
|
||||
memberId: "",
|
||||
memberName: "",
|
||||
rechargeSn: "",
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
},
|
||||
beforeRequest: (p) => {
|
||||
const params = { ...p };
|
||||
if (selectDate.value && selectDate.value.length === 2) {
|
||||
params.startDate = selectDate.value[0];
|
||||
params.endDate = selectDate.value[1];
|
||||
} else {
|
||||
params.startDate = "";
|
||||
params.endDate = "";
|
||||
}
|
||||
return params;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
function rechargeWayText(way) {
|
||||
if (way === "ALIPAY") return "支付宝";
|
||||
if (way === "WECHAT") return "微信";
|
||||
if (way === "WECHAT") return "微信支付";
|
||||
if (way === "BANK_TRANSFER") return "线下转账";
|
||||
return "";
|
||||
return way || "-";
|
||||
}
|
||||
function init() {
|
||||
getDataList();
|
||||
|
||||
function formatMoney(val) {
|
||||
const num = Number(val || 0);
|
||||
return num.toFixed(2);
|
||||
}
|
||||
|
||||
function viewMemberDetail(row) {
|
||||
if (!row || !row.memberId) return;
|
||||
customRouterPush({ name: "member-detail", query: { id: row.memberId } });
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
selectDate.value = null;
|
||||
searchForm.memberId = "";
|
||||
searchForm.memberName = "";
|
||||
searchForm.rechargeSn = "";
|
||||
searchForm.startDate = "";
|
||||
searchForm.endDate = "";
|
||||
searchForm.pageNumber = 1;
|
||||
resetSearch();
|
||||
}
|
||||
|
||||
function changePage(v) {
|
||||
searchForm.pageNumber = v;
|
||||
getDataList();
|
||||
}
|
||||
|
||||
function changePageSize(v) {
|
||||
searchForm.pageNumber = 1;
|
||||
searchForm.pageSize = v;
|
||||
getDataList();
|
||||
}
|
||||
function handleSearch() {
|
||||
searchForm.pageNumber = 1;
|
||||
searchForm.pageSize = 20;
|
||||
getDataList();
|
||||
}
|
||||
|
||||
function selectDateRange(v) {
|
||||
if (v && v.length === 2) {
|
||||
searchForm.startDate = v[0];
|
||||
@@ -145,6 +237,84 @@ function selectDateRange(v) {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
getDataList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.member-recharge-page {
|
||||
padding: 16px 20px;
|
||||
background-color: #f6f8fa;
|
||||
min-height: calc(100vh - 110px);
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-dark {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
/* 筛选卡片 */
|
||||
.filter-card {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ebeef5;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px 16px 2px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 14px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 表格卡片 */
|
||||
.table-card {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ebeef5;
|
||||
|
||||
.card-header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.header-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.recharge-money-text {
|
||||
font-size: 14px;
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.pagination-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,75 +1,123 @@
|
||||
<template>
|
||||
<div class="search">
|
||||
<el-card>
|
||||
<div class="member-wallet-log-page search">
|
||||
<!-- 顶部筛选表单 -->
|
||||
<el-card shadow="never" class="filter-card">
|
||||
<el-form
|
||||
ref="searchFormRef"
|
||||
:model="searchForm"
|
||||
inline
|
||||
label-width="70px"
|
||||
label-width="76px"
|
||||
class="search-form"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<el-form-item label="客户ID" prop="memberId">
|
||||
<el-input
|
||||
v-model="searchForm.memberId"
|
||||
placeholder="请输入客户ID"
|
||||
clearable
|
||||
style="width: 180px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户名称" prop="memberName">
|
||||
<el-input
|
||||
v-model="searchForm.memberName"
|
||||
placeholder="请输入客户名称"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
style="width: 160px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="支付时间">
|
||||
<el-form-item label="变动时间">
|
||||
<el-date-picker
|
||||
v-model="selectDate"
|
||||
type="datetimerange"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
clearable
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
placeholder="选择起始时间"
|
||||
style="width: 360px"
|
||||
style="width: 320px"
|
||||
@change="selectDateRange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
|
||||
<el-form-item class="form-actions">
|
||||
<el-button type="primary" class="search-btn" @click="handleSearch">
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-card>
|
||||
|
||||
<!-- 变动记录表格 -->
|
||||
<el-card shadow="never" class="table-card mt_10">
|
||||
<template #header>
|
||||
<div class="card-header-bar">
|
||||
<div class="header-title">
|
||||
<span>预存款变动明细</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button size="small" @click="getDataList">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table
|
||||
ref="table"
|
||||
v-loading="loading"
|
||||
border
|
||||
stripe
|
||||
:data="data"
|
||||
ref="table"
|
||||
class="mt_10"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="memberName" label="客户名称" min-width="100" />
|
||||
<el-table-column label="变动金额" width="150">
|
||||
<!-- 客户ID -->
|
||||
<el-table-column prop="memberId" label="客户ID" min-width="190" show-overflow-tooltip />
|
||||
|
||||
<!-- 客户名称 -->
|
||||
<el-table-column prop="memberName" label="客户名称" min-width="140" show-overflow-tooltip />
|
||||
|
||||
<!-- 变动金额 -->
|
||||
<el-table-column label="变动金额" width="140" align="right" sortable prop="money">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row">
|
||||
<priceColorScheme
|
||||
v-if="row.money > 0"
|
||||
:value="row.money"
|
||||
color="green"
|
||||
/>
|
||||
<priceColorScheme
|
||||
v-else-if="row.money < 0"
|
||||
:value="row.money"
|
||||
:color="$mainColor"
|
||||
/>
|
||||
<span v-if="row.money > 0" class="money-increase">
|
||||
+¥ {{ formatMoney(row.money) }}
|
||||
</span>
|
||||
<span v-else-if="row.money < 0" class="money-decrease">
|
||||
-¥ {{ formatMoney(Math.abs(row.money)) }}
|
||||
</span>
|
||||
<span v-else class="money-zero">
|
||||
¥ 0.00
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="变更时间" width="200" />
|
||||
<el-table-column label="业务类型" width="200">
|
||||
|
||||
<!-- 业务类型 -->
|
||||
<el-table-column label="业务类型" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ serviceTypeText(row.serviceType) }}</span>
|
||||
{{ row ? serviceTypeText(row.serviceType) : "-" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 日志详细说明 -->
|
||||
<el-table-column prop="detail" label="变动说明" min-width="260" show-overflow-tooltip />
|
||||
|
||||
<!-- 变动时间 -->
|
||||
<el-table-column prop="createTime" label="变动时间" width="170" align="center" />
|
||||
|
||||
<!-- 操作 -->
|
||||
<el-table-column label="操作" width="110" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row">
|
||||
<a v-if="row.memberId" class="link-text" @click="viewMemberDetail(row)">客户详情</a>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="detail" label="详细" min-width="300" show-overflow-tooltip />
|
||||
</el-table>
|
||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
||||
|
||||
<!-- 底部分页 -->
|
||||
<div class="pagination-wrap mt_10">
|
||||
<el-pagination
|
||||
v-model:current-page="searchForm.pageNumber"
|
||||
v-model:page-size="searchForm.pageSize"
|
||||
@@ -86,23 +134,42 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useTable } from "@/utils/useTable";
|
||||
import { getUserWallet } from "@/api/member";
|
||||
import { customRouterPush } from "@/utils/filters";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
defineOptions({ name: "walletLog" });
|
||||
|
||||
// 模板 el-form 的 ref(原 ref="searchForm",与 data 同名会互相覆盖,改名为 searchFormRef)
|
||||
const searchFormRef = ref(null);
|
||||
const table = ref(null);
|
||||
|
||||
const selectDate = ref(null);
|
||||
const { loading, data, total, searchForm, getDataList } = useTable(
|
||||
|
||||
const { loading, data, total, searchForm, getDataList, handleSearch, resetSearch } = useTable(
|
||||
getUserWallet,
|
||||
{
|
||||
pageSize: 20,
|
||||
immediate: false,
|
||||
form: { sort: "createTime", order: "desc", startDate: "", endDate: "", memberName: "" },
|
||||
form: {
|
||||
sort: "createTime",
|
||||
order: "desc",
|
||||
memberId: "",
|
||||
memberName: "",
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
},
|
||||
beforeRequest: (p) => {
|
||||
const params = { ...p };
|
||||
if (selectDate.value && selectDate.value.length === 2) {
|
||||
params.startDate = selectDate.value[0];
|
||||
params.endDate = selectDate.value[1];
|
||||
} else {
|
||||
params.startDate = "";
|
||||
params.endDate = "";
|
||||
}
|
||||
return params;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -113,23 +180,46 @@ function serviceTypeText(type) {
|
||||
if (type === "WALLET_RECHARGE") return "余额充值";
|
||||
return "佣金提成";
|
||||
}
|
||||
function init() {
|
||||
getDataList();
|
||||
|
||||
function getServiceTagType(type) {
|
||||
if (type === "WALLET_RECHARGE") return "success";
|
||||
if (type === "WALLET_REFUND") return "primary";
|
||||
if (type === "WALLET_PAY") return "warning";
|
||||
if (type === "WALLET_WITHDRAWAL") return "danger";
|
||||
return "info";
|
||||
}
|
||||
|
||||
function formatMoney(val) {
|
||||
const num = Number(val || 0);
|
||||
return num.toFixed(2);
|
||||
}
|
||||
|
||||
function viewMemberDetail(row) {
|
||||
if (!row || !row.memberId) return;
|
||||
customRouterPush({ name: "member-detail", query: { id: row.memberId } });
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
selectDate.value = null;
|
||||
searchForm.memberId = "";
|
||||
searchForm.memberName = "";
|
||||
searchForm.startDate = "";
|
||||
searchForm.endDate = "";
|
||||
searchForm.pageNumber = 1;
|
||||
resetSearch();
|
||||
}
|
||||
|
||||
function changePage(v) {
|
||||
searchForm.pageNumber = v;
|
||||
getDataList();
|
||||
}
|
||||
|
||||
function changePageSize(v) {
|
||||
searchForm.pageNumber = 1;
|
||||
searchForm.pageSize = v;
|
||||
getDataList();
|
||||
}
|
||||
function handleSearch() {
|
||||
searchForm.pageNumber = 1;
|
||||
searchForm.pageSize = 20;
|
||||
getDataList();
|
||||
}
|
||||
|
||||
function selectDateRange(v) {
|
||||
if (v && v.length === 2) {
|
||||
searchForm.startDate = v[0];
|
||||
@@ -141,6 +231,93 @@ function selectDateRange(v) {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
getDataList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.member-wallet-log-page {
|
||||
padding: 16px 20px;
|
||||
background-color: #f6f8fa;
|
||||
min-height: calc(100vh - 110px);
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-dark {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
/* 筛选卡片 */
|
||||
.filter-card {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ebeef5;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px 16px 2px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 14px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 表格卡片 */
|
||||
.table-card {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ebeef5;
|
||||
|
||||
.card-header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.header-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.money-increase {
|
||||
font-size: 14px;
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.money-decrease {
|
||||
font-size: 14px;
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.money-zero {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.pagination-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,29 +1,52 @@
|
||||
<template>
|
||||
<div class="search">
|
||||
<el-card>
|
||||
<div class="member-withdraw-apply-page search">
|
||||
<!-- 顶部筛选表单 -->
|
||||
<el-card shadow="never" class="filter-card">
|
||||
<el-form
|
||||
ref="searchFormRef"
|
||||
:model="searchForm"
|
||||
inline
|
||||
label-width="70px"
|
||||
label-width="76px"
|
||||
class="search-form"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<el-form-item label="客户ID" prop="memberId">
|
||||
<el-input
|
||||
v-model="searchForm.memberId"
|
||||
placeholder="请输入客户ID"
|
||||
clearable
|
||||
style="width: 180px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户名称" prop="memberName">
|
||||
<el-input
|
||||
v-model="searchForm.memberName"
|
||||
placeholder="请输入客户名称"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
style="width: 160px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请编号" prop="sn">
|
||||
<el-input
|
||||
v-model="searchForm.sn"
|
||||
placeholder="请输入申请编号"
|
||||
clearable
|
||||
style="width: 180px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="提现状态" prop="applyStatus">
|
||||
<el-select v-model="searchForm.applyStatus" clearable style="width: 240px">
|
||||
<el-select
|
||||
v-model="searchForm.applyStatus"
|
||||
placeholder="全部状态"
|
||||
clearable
|
||||
style="width: 130px"
|
||||
>
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="申请中" value="APPLY" />
|
||||
<el-option label="审核通过" value="VIA_AUDITING" />
|
||||
<el-option label="提现成功" value="SUCCESS" />
|
||||
<el-option label="用户确认" value="WAIT_USER_CONFIRM" />
|
||||
<el-option label="审核拒绝" value="FAIL_AUDITING" />
|
||||
<el-option label="提现成功" value="SUCCESS" />
|
||||
<el-option label="提现失败" value="ERROR" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
@@ -33,55 +56,90 @@
|
||||
type="datetimerange"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
clearable
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
placeholder="选择起始时间"
|
||||
style="width: 360px"
|
||||
style="width: 320px"
|
||||
@change="selectDateRange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-form-item class="form-actions">
|
||||
<el-button type="primary" class="search-btn" @click="handleSearch">
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-card>
|
||||
|
||||
<!-- 提现申请数据表格 -->
|
||||
<el-card shadow="never" class="table-card mt_10">
|
||||
<template #header>
|
||||
<div class="card-header-bar">
|
||||
<div class="header-title">
|
||||
<span>会员提现申请</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button size="small" @click="getDataList">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table
|
||||
ref="table"
|
||||
v-loading="loading"
|
||||
border
|
||||
stripe
|
||||
:data="data"
|
||||
ref="table"
|
||||
class="mt_10"
|
||||
style="width: 100%"
|
||||
@sort-change="changeSort"
|
||||
@selection-change="changeSelect"
|
||||
>
|
||||
<el-table-column prop="sn" label="申请编号" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="memberName" label="客户名称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="申请金额" width="120">
|
||||
<!-- 客户ID -->
|
||||
<el-table-column prop="memberId" label="客户ID" min-width="170" show-overflow-tooltip />
|
||||
|
||||
<!-- 客户名称 -->
|
||||
<el-table-column prop="memberName" label="客户名称" min-width="140" show-overflow-tooltip />
|
||||
|
||||
<!-- 申请编号 -->
|
||||
<el-table-column prop="sn" label="申请编号" min-width="180" show-overflow-tooltip />
|
||||
|
||||
<!-- 申请提现金额 -->
|
||||
<el-table-column label="申请金额" width="130" align="right" sortable="custom" prop="applyMoney">
|
||||
<template #default="{ row }">
|
||||
<priceColorScheme v-if="row" :value="row.applyMoney" :color="$mainColor" />
|
||||
<span v-if="row">¥ {{ formatMoney(row.applyMoney) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="提现状态" width="120">
|
||||
|
||||
<!-- 提现状态 -->
|
||||
<el-table-column label="提现状态" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row" :type="applyStatusTagType(row.applyStatus)">
|
||||
{{ applyStatusText(row.applyStatus) }}
|
||||
</el-tag>
|
||||
{{ row ? applyStatusText(row.applyStatus) : "-" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="申请时间" width="170" sortable="custom" />
|
||||
<el-table-column prop="inspectTime" label="审核时间" width="170" />
|
||||
<el-table-column label="操作" width="120" align="center" fixed="right">
|
||||
|
||||
<!-- 申请时间 -->
|
||||
<el-table-column prop="createTime" label="申请时间" width="170" align="center" sortable="custom" />
|
||||
|
||||
<!-- 审核时间 -->
|
||||
<el-table-column prop="inspectTime" label="审核时间" width="170" align="center" />
|
||||
|
||||
<!-- 操作 -->
|
||||
<el-table-column label="操作" width="160" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row" class="ops">
|
||||
<a class="link-text" v-if="row.applyStatus === 'APPLY'" @click="openAudit(row)">审核</a>
|
||||
<a class="link-text" v-else @click="openQuery(row)">查看</a>
|
||||
</div>
|
||||
<template v-if="row">
|
||||
<a v-if="row.applyStatus === 'APPLY'" class="link-text" @click="openAudit(row)">审核</a>
|
||||
<a v-else class="link-text" @click="openQuery(row)">查看</a>
|
||||
<template v-if="row.memberId">
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text" @click="viewMemberDetail(row)">客户详情</a>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
||||
|
||||
<!-- 底部分页 -->
|
||||
<div class="pagination-wrap mt_10">
|
||||
<el-pagination
|
||||
v-model:current-page="searchForm.pageNumber"
|
||||
v-model:page-size="searchForm.pageSize"
|
||||
@@ -95,114 +153,170 @@
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 审核弹窗 -->
|
||||
<el-dialog
|
||||
v-model="roleModalVisible"
|
||||
:title="modalTitle"
|
||||
width="500px"
|
||||
title="提现申请审核"
|
||||
width="520px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form label-width="80px">
|
||||
<el-form-item label="申请编号">
|
||||
<span>{{ showList.sn }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="用户名称">
|
||||
<span>{{ showList.memberName }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请金额">
|
||||
<priceColorScheme :value="showList.applyMoney" :color="$mainColor" />
|
||||
</el-form-item>
|
||||
<el-form-item label="提现状态">
|
||||
<span>{{ paramTypeFilter(showList.applyStatus) }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请时间">
|
||||
<span>{{ showList.createTime }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="审核备注">
|
||||
<el-input v-model="audit" type="textarea" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="dialog-body">
|
||||
<el-descriptions :column="1" border size="small" class="custom-dialog-descriptions">
|
||||
<el-descriptions-item label="申请编号">
|
||||
<span class="font-mono">{{ showList.sn }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="客户ID">
|
||||
<span class="font-mono">{{ showList.memberId || "-" }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="客户名称">
|
||||
<span class="font-semibold">{{ showList.memberName }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="申请金额">
|
||||
<span class="money-text font-semibold">¥ {{ formatMoney(showList.applyMoney) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="申请时间">
|
||||
<span>{{ showList.createTime }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div class="remark-form-item" style="margin-top: 14px">
|
||||
<div class="remark-label" style="font-size: 13px; color: #606266; margin-bottom: 6px">
|
||||
审核备注说明(拒绝时必填):
|
||||
</div>
|
||||
<el-input
|
||||
v-model="audit"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
placeholder="请输入审核备注说明..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<template v-if="showList.applyStatus === 'APPLY'">
|
||||
<el-button @click="submitRole(false)">拒绝</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="submitRole(true)">通过</el-button>
|
||||
</template>
|
||||
<el-button @click="roleModalVisible = false">取消</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
:loading="submitLoading"
|
||||
@click="submitRole(false)"
|
||||
>
|
||||
拒绝
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="submitLoading"
|
||||
@click="submitRole(true)"
|
||||
>
|
||||
通过
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 查看详情弹窗 -->
|
||||
<el-dialog
|
||||
v-model="queryModalVisible"
|
||||
:title="modalTitle"
|
||||
width="500px"
|
||||
title="提现申请详情"
|
||||
width="520px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="申请编号:">
|
||||
<span>{{ showList.sn }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="用户名称:">
|
||||
<span>{{ showList.memberName }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请金额:">
|
||||
<priceColorScheme :value="showList.applyMoney" :color="$mainColor" />
|
||||
</el-form-item>
|
||||
<el-form-item label="提现状态:">
|
||||
<span>{{ paramTypeFilter(showList.applyStatus) }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请时间:">
|
||||
<span>{{ showList.createTime }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="审核时间:">
|
||||
<span>{{ showList.inspectTime }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="审核备注:">
|
||||
<span>{{ showList.inspectRemark || "暂无备注" }}</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="dialog-body">
|
||||
<el-descriptions :column="1" border size="small" class="custom-dialog-descriptions">
|
||||
<el-descriptions-item label="申请编号">
|
||||
<span class="font-mono">{{ showList.sn }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="客户ID">
|
||||
<span class="font-mono">{{ showList.memberId || "-" }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="客户名称">
|
||||
<span class="font-semibold">{{ showList.memberName }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="申请金额">
|
||||
<span class="money-text font-semibold">¥ {{ formatMoney(showList.applyMoney) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="提现状态">
|
||||
<el-tag :type="applyStatusTagType(showList.applyStatus)" size="small">
|
||||
{{ paramTypeFilter(showList.applyStatus) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="申请时间">
|
||||
<span>{{ showList.createTime }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="审核时间">
|
||||
<span>{{ showList.inspectTime || "-" }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="审核备注">
|
||||
<span>{{ showList.inspectRemark || "暂无备注" }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
<template #footer>
|
||||
<template v-if="showList.applyStatus === 'APPLY'">
|
||||
<el-button @click="submitRole(false)">拒绝</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="submitRole(true)">通过</el-button>
|
||||
</template>
|
||||
<el-button v-else @click="queryModalVisible = false">取消</el-button>
|
||||
<el-button @click="queryModalVisible = false">关 闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useTable } from "@/utils/useTable";
|
||||
import { getUserWithdrawApply, withdrawApply } from "@/api/member";
|
||||
import { customRouterPush } from "@/utils/filters";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
defineOptions({ name: "withdrawApply" });
|
||||
|
||||
// 模板 el-form 的 ref(原 ref="searchForm",与 data 同名会互相覆盖,改名为 searchFormRef)
|
||||
const route = useRoute();
|
||||
const searchFormRef = ref(null);
|
||||
// 模板 el-table 的 ref
|
||||
const table = ref(null);
|
||||
const selectDate = ref(null);
|
||||
|
||||
const modalTitle = ref("");
|
||||
const loading = ref(true);
|
||||
const audit = ref("");
|
||||
const roleModalVisible = ref(false);
|
||||
const queryModalVisible = ref(false);
|
||||
const searchForm = reactive({
|
||||
pageNumber: 1,
|
||||
pageSize: 20,
|
||||
sort: "createTime",
|
||||
order: "desc",
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
memberName: "",
|
||||
applyStatus: "",
|
||||
});
|
||||
const selectDate = ref(null);
|
||||
const submitLoading = ref(false);
|
||||
const selectList = ref([]);
|
||||
const selectCount = ref(0);
|
||||
const showList = ref({});
|
||||
const data = ref([]);
|
||||
const total = ref(0);
|
||||
|
||||
const { loading, data, total, searchForm, getDataList, handleSearch, resetSearch } = useTable(
|
||||
getUserWithdrawApply,
|
||||
{
|
||||
pageSize: 20,
|
||||
immediate: false,
|
||||
form: {
|
||||
sort: "createTime",
|
||||
order: "desc",
|
||||
memberId: "",
|
||||
memberName: "",
|
||||
sn: "",
|
||||
applyStatus: "",
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
},
|
||||
beforeRequest: (p) => {
|
||||
const params = { ...p };
|
||||
if (selectDate.value && selectDate.value.length === 2) {
|
||||
params.startDate = selectDate.value[0];
|
||||
params.endDate = selectDate.value[1];
|
||||
} else {
|
||||
params.startDate = "";
|
||||
params.endDate = "";
|
||||
}
|
||||
return params;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
function formatMoney(val) {
|
||||
const num = Number(val || 0);
|
||||
return num.toFixed(2);
|
||||
}
|
||||
|
||||
function viewMemberDetail(row) {
|
||||
if (!row || !row.memberId) return;
|
||||
customRouterPush({ name: "member-detail", query: { id: row.memberId } });
|
||||
}
|
||||
|
||||
function paramTypeFilter(val) {
|
||||
if (val === "APPLY") return "申请中";
|
||||
@@ -211,41 +325,47 @@ function paramTypeFilter(val) {
|
||||
if (val === "FAIL_AUDITING") return "审核拒绝";
|
||||
if (val === "SUCCESS") return "提现成功";
|
||||
if (val === "ERROR") return "提现失败";
|
||||
return "未知状态";
|
||||
return val || "未知状态";
|
||||
}
|
||||
|
||||
function applyStatusText(status) {
|
||||
if (status === "APPLY") return "申请中";
|
||||
if (status === "VIA_AUDITING") return "审核通过";
|
||||
if (status === "WAIT_USER_CONFIRM") return "用户确认";
|
||||
if (status === "SUCCESS") return "提现成功";
|
||||
if (status === "ERROR") return "提现失败";
|
||||
return "审核拒绝";
|
||||
if (status === "FAIL_AUDITING") return "审核拒绝";
|
||||
return status || "-";
|
||||
}
|
||||
|
||||
function applyStatusTagType(status) {
|
||||
if (status === "APPLY") return "warning";
|
||||
if (status === "VIA_AUDITING") return "success";
|
||||
if (status === "WAIT_USER_CONFIRM") return "warning";
|
||||
if (status === "SUCCESS") return "primary";
|
||||
if (status === "ERROR") return "primary";
|
||||
if (status === "ERROR") return "danger";
|
||||
return "danger";
|
||||
}
|
||||
|
||||
function openAudit(row) {
|
||||
showList.value = { ...row };
|
||||
audit.value = "";
|
||||
roleModalVisible.value = true;
|
||||
}
|
||||
|
||||
function openQuery(row) {
|
||||
showList.value = { ...row };
|
||||
modalTitle.value = "查看";
|
||||
queryModalVisible.value = true;
|
||||
}
|
||||
|
||||
function submitRole(res) {
|
||||
const params = {};
|
||||
params.applyId = showList.value.id;
|
||||
params.result = res;
|
||||
params.remark = audit.value;
|
||||
if (res === false && params.remark === "") {
|
||||
ElMessage.error("审核备注不能为空");
|
||||
const params = {
|
||||
applyId: showList.value.id,
|
||||
result: res,
|
||||
remark: audit.value ? audit.value.trim() : "",
|
||||
};
|
||||
if (res === false && !params.remark) {
|
||||
ElMessage.error("审核拒绝时备注说明不能为空");
|
||||
return;
|
||||
}
|
||||
submitLoading.value = true;
|
||||
@@ -265,50 +385,36 @@ function submitRole(res) {
|
||||
submitLoading.value = false;
|
||||
});
|
||||
}
|
||||
function init() {
|
||||
|
||||
function handleReset() {
|
||||
selectDate.value = null;
|
||||
searchForm.memberId = "";
|
||||
searchForm.memberName = "";
|
||||
searchForm.sn = "";
|
||||
searchForm.applyStatus = "";
|
||||
searchForm.startDate = "";
|
||||
searchForm.endDate = "";
|
||||
searchForm.pageNumber = 1;
|
||||
resetSearch();
|
||||
}
|
||||
|
||||
function changeSort(e) {
|
||||
searchForm.sort = e.prop || e.key || "createTime";
|
||||
searchForm.order = e.order === "ascending" ? "asc" : e.order === "descending" ? "desc" : "";
|
||||
getDataList();
|
||||
}
|
||||
|
||||
function changePage(v) {
|
||||
searchForm.pageNumber = v;
|
||||
getDataList();
|
||||
clearSelectAll();
|
||||
}
|
||||
|
||||
function changePageSize(v) {
|
||||
searchForm.pageNumber = 1;
|
||||
searchForm.pageSize = v;
|
||||
getDataList();
|
||||
}
|
||||
function handleSearch() {
|
||||
searchForm.pageNumber = 1;
|
||||
searchForm.pageSize = 20;
|
||||
getDataList();
|
||||
}
|
||||
function handleReset() {
|
||||
searchFormRef.value.resetFields();
|
||||
searchForm.pageNumber = 1;
|
||||
searchForm.pageSize = 20;
|
||||
selectDate.value = null;
|
||||
searchForm.startDate = "";
|
||||
searchForm.endDate = "";
|
||||
searchForm.memberName = "";
|
||||
getDataList();
|
||||
}
|
||||
function changeSort(e) {
|
||||
searchForm.sort = e.prop || e.key;
|
||||
searchForm.order = e.order === "ascending" ? "asc" : e.order === "descending" ? "desc" : "";
|
||||
if (!e.order) {
|
||||
searchForm.order = "";
|
||||
}
|
||||
getDataList();
|
||||
}
|
||||
function clearSelectAll() {
|
||||
if (table.value) {
|
||||
table.value.clearSelection();
|
||||
}
|
||||
}
|
||||
function changeSelect(e) {
|
||||
selectList.value = e;
|
||||
selectCount.value = e.length;
|
||||
}
|
||||
|
||||
function selectDateRange(v) {
|
||||
if (v && v.length === 2) {
|
||||
searchForm.startDate = v[0];
|
||||
@@ -318,30 +424,97 @@ function selectDateRange(v) {
|
||||
searchForm.endDate = "";
|
||||
}
|
||||
}
|
||||
function getDataList() {
|
||||
loading.value = true;
|
||||
getUserWithdrawApply(searchForm).then((res) => {
|
||||
loading.value = false;
|
||||
if (res.success) {
|
||||
data.value = res.result.records;
|
||||
total.value = res.result.total;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
if (route.query && route.query.applyStatus) {
|
||||
searchForm.applyStatus = route.query.applyStatus;
|
||||
}
|
||||
getDataList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ops a {
|
||||
color: #2d8cf0;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
.member-withdraw-apply-page {
|
||||
padding: 16px 20px;
|
||||
background-color: #f6f8fa;
|
||||
min-height: calc(100vh - 110px);
|
||||
}
|
||||
.ops span {
|
||||
display: inline-block;
|
||||
margin: 0 8px;
|
||||
color: #dcdee2;
|
||||
|
||||
.font-mono {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-dark {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
/* 筛选卡片 */
|
||||
.filter-card {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ebeef5;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px 16px 2px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 14px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 表格卡片 */
|
||||
.table-card {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ebeef5;
|
||||
|
||||
.card-header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.header-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-dialog-descriptions {
|
||||
:deep(.el-descriptions__label) {
|
||||
width: 100px;
|
||||
background-color: #fafafa;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.money-text {
|
||||
font-size: 15px;
|
||||
color: #f56c6c;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,24 +1,32 @@
|
||||
<template>
|
||||
<div class="search">
|
||||
<el-card>
|
||||
<div class="member-experience-log-page search">
|
||||
<!-- 顶部筛选表单 -->
|
||||
<el-card shadow="never" class="filter-card">
|
||||
<el-form
|
||||
ref="searchFormRef"
|
||||
:model="searchForm"
|
||||
inline
|
||||
label-width="90px"
|
||||
label-width="84px"
|
||||
class="search-form"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<el-form-item label="客户手机号" prop="mobile">
|
||||
<el-form-item label="客户手机号" prop="memberMobile">
|
||||
<el-input
|
||||
v-model="searchForm.memberMobile"
|
||||
clearable
|
||||
placeholder="请输入客户手机号"
|
||||
style="width: 220px"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="规则" prop="ruleKey">
|
||||
<el-select v-model="searchForm.ruleKey" clearable filterable style="width: 220px">
|
||||
<el-form-item label="规则类型" prop="ruleKey">
|
||||
<el-select
|
||||
v-model="searchForm.ruleKey"
|
||||
clearable
|
||||
filterable
|
||||
placeholder="全部规则"
|
||||
style="width: 180px"
|
||||
>
|
||||
<el-option label="全部规则" value="" />
|
||||
<el-option
|
||||
v-for="item in ruleOptions"
|
||||
:key="item.value"
|
||||
@@ -27,42 +35,103 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-form-item class="form-actions">
|
||||
<el-button type="primary" class="search-btn" @click="handleSearch">
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card>
|
||||
<el-table v-loading="loading" border :data="data" class="mt_10" style="width: 100%">
|
||||
<el-table-column label="客户手机号" width="140" show-overflow-tooltip>
|
||||
<!-- 经验值记录表格 -->
|
||||
<el-card shadow="never" class="table-card mt_10">
|
||||
<template #header>
|
||||
<div class="card-header-bar">
|
||||
<div class="header-title">
|
||||
<span>客户经验值流水</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button size="small" @click="getData">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
border
|
||||
stripe
|
||||
:data="data"
|
||||
style="width: 100%"
|
||||
>
|
||||
<!-- 客户ID -->
|
||||
<el-table-column prop="memberId" label="客户ID" min-width="190" show-overflow-tooltip />
|
||||
|
||||
<!-- 客户名称 -->
|
||||
<el-table-column prop="memberName" label="客户名称" min-width="130" show-overflow-tooltip />
|
||||
|
||||
<!-- 客户手机号 -->
|
||||
<el-table-column label="客户手机号" width="130" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.memberMobile || row.mobile || "-" }}</span>
|
||||
{{ row ? (row.memberMobile || row.mobile || "-") : "-" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="规则名称" width="150" show-overflow-tooltip>
|
||||
|
||||
<!-- 规则名称 -->
|
||||
<el-table-column label="规则名称" width="130" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.ruleName || findRuleName(row.ruleKey) || "-" }}</span>
|
||||
{{ row ? (row.ruleName || findRuleName(row.ruleKey) || "-") : "-" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="变化经验值" width="120">
|
||||
|
||||
<!-- 变动前经验值 -->
|
||||
<el-table-column label="变动前经验值" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ formatExperienceValue(row) }}</span>
|
||||
<span v-if="row">{{ row.beforeExperience != null ? formatNumber(row.beforeExperience) : "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="经验值限额" width="120">
|
||||
|
||||
<!-- 变动经验值 -->
|
||||
<el-table-column label="变动经验值" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ formatMaxValue(row) }}</span>
|
||||
<span v-if="row" class="experience-increase">
|
||||
+{{ formatNumber(row.variableExperience ?? row.value ?? 0) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 当前经验值 -->
|
||||
<el-table-column label="变动后经验值" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" class="experience-after">
|
||||
{{ formatNumber(row.experience ?? 0) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 操作说明 / 备注 -->
|
||||
<el-table-column label="操作说明" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.content || row.remark || row.description || "-" }}</span>
|
||||
<span>{{ row ? row.content || row.remark || row.description || "-" : "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 创建时间 -->
|
||||
<el-table-column prop="createTime" label="创建时间" width="170" align="center" />
|
||||
|
||||
<!-- 操作 -->
|
||||
<el-table-column label="操作" width="110" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row">
|
||||
<a v-if="row.memberId" class="link-text" @click="viewMemberDetail(row)">客户详情</a>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" width="170" />
|
||||
</el-table>
|
||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
||||
|
||||
<!-- 底部分页 -->
|
||||
<div class="pagination-wrap mt_10">
|
||||
<el-pagination
|
||||
v-model:current-page="searchForm.pageNumber"
|
||||
v-model:page-size="searchForm.pageSize"
|
||||
@@ -82,8 +151,10 @@
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useTable } from "@/utils/useTable";
|
||||
import * as API_Member from "@/api/member";
|
||||
import { customRouterPush } from "@/utils/filters";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
const RULE_OPTIONS = [
|
||||
const ruleOptions = [
|
||||
{ value: "CONSUME", label: "消费" },
|
||||
{ value: "REGISTER", label: "注册" },
|
||||
{ value: "SIGN_IN", label: "签到" },
|
||||
@@ -101,7 +172,7 @@ defineOptions({ name: "memberGradeExperienceLog" });
|
||||
|
||||
const searchFormRef = ref(null);
|
||||
|
||||
const { loading, data, total, searchForm, getDataList: getData } = useTable(
|
||||
const { loading, data, total, searchForm, getDataList: getData, resetSearch } = useTable(
|
||||
API_Member.getMemberExperienceByPage,
|
||||
{
|
||||
pageSize: 20,
|
||||
@@ -111,45 +182,138 @@ const { loading, data, total, searchForm, getDataList: getData } = useTable(
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
function formatNumber(val) {
|
||||
const num = Number(val || 0);
|
||||
return num.toLocaleString();
|
||||
}
|
||||
|
||||
function formatExperienceValue(row) {
|
||||
const v = row.value ?? row.variableExperience ?? row.experience ?? row.variableValue;
|
||||
return v == null ? "-" : v;
|
||||
}
|
||||
function formatMaxValue(row) {
|
||||
const v = row.maxValue ?? row.maxExperience ?? row.limitValue;
|
||||
return v == null ? "-" : v;
|
||||
}
|
||||
function findRuleName(ruleKey) {
|
||||
const hit = ruleOptions.find((item) => item.value === ruleKey);
|
||||
return hit ? hit.label : "";
|
||||
return hit ? hit.label : ruleKey || "";
|
||||
}
|
||||
function init() {
|
||||
getData();
|
||||
|
||||
function viewMemberDetail(row) {
|
||||
if (!row || !row.memberId) return;
|
||||
customRouterPush({ name: "member-detail", query: { id: row.memberId } });
|
||||
}
|
||||
|
||||
function buildParams(base = searchForm) {
|
||||
const params = {
|
||||
pageNumber: base.pageNumber,
|
||||
pageSize: base.pageSize,
|
||||
};
|
||||
if (base.memberMobile) params.mobile = base.memberMobile;
|
||||
if (base.memberMobile) params.mobile = base.memberMobile.trim();
|
||||
if (base.ruleKey) params.ruleKey = base.ruleKey;
|
||||
return params;
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
searchForm.pageNumber = 1;
|
||||
getData();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.memberMobile = "";
|
||||
searchForm.ruleKey = "";
|
||||
searchForm.pageNumber = 1;
|
||||
resetSearch();
|
||||
}
|
||||
|
||||
function changePage(v) {
|
||||
searchForm.pageNumber = v;
|
||||
getData();
|
||||
}
|
||||
|
||||
function changePageSize(v) {
|
||||
searchForm.pageNumber = 1;
|
||||
searchForm.pageSize = v;
|
||||
getData();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.member-experience-log-page {
|
||||
padding: 16px 20px;
|
||||
background-color: #f6f8fa;
|
||||
min-height: calc(100vh - 110px);
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-dark {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
/* 筛选卡片 */
|
||||
.filter-card {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ebeef5;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px 16px 2px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 14px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 表格卡片 */
|
||||
.table-card {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ebeef5;
|
||||
|
||||
.card-header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.header-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.experience-increase {
|
||||
font-size: 13px;
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.experience-after {
|
||||
font-size: 13px;
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
.pagination-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,29 +1,102 @@
|
||||
<template>
|
||||
<div class="search">
|
||||
<el-card>
|
||||
<div class="operation padding-row">
|
||||
<el-button type="primary" @click="openAdd">添加分组</el-button>
|
||||
<div class="member-group-page search">
|
||||
<!-- 顶部筛选与操作栏 -->
|
||||
<el-card shadow="never" class="filter-card">
|
||||
<div class="card-toolbar">
|
||||
<el-form inline :model="searchForm" class="filter-form" @keyup.enter="handleSearch">
|
||||
<el-form-item label="分组名称">
|
||||
<el-input
|
||||
v-model="searchForm.groupName"
|
||||
placeholder="请输入分组名称"
|
||||
clearable
|
||||
style="width: 220px"
|
||||
@clear="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="toolbar-right">
|
||||
<el-button type="primary" @click="openAdd">添加分组</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card>
|
||||
<el-table ref="table" v-loading="loading" :data="data" border class="mt_10" style="width: 100%">
|
||||
<!-- 分组数据表格 -->
|
||||
<el-card shadow="never" class="table-card mt_10">
|
||||
<template #header>
|
||||
<div class="card-header-bar">
|
||||
<div class="header-title">
|
||||
<span>会员分组列表</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button size="small" @click="getData">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table
|
||||
ref="table"
|
||||
v-loading="loading"
|
||||
:data="data"
|
||||
border
|
||||
stripe
|
||||
style="width: 100%"
|
||||
class="custom-group-table"
|
||||
>
|
||||
<!-- 分组ID -->
|
||||
<el-table-column prop="id" label="分组ID" min-width="190" />
|
||||
|
||||
<!-- 分组名称 -->
|
||||
<el-table-column prop="groupName" label="分组名称" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="description" label="分组描述" min-width="240" show-overflow-tooltip />
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="180" />
|
||||
<el-table-column prop="updateTime" label="更新时间" min-width="180" />
|
||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||
|
||||
<!-- 分组人数 -->
|
||||
<el-table-column prop="memberCount" label="分组人数" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row" class="ops" style="display: flex; justify-content: center">
|
||||
<template v-if="row">
|
||||
<a
|
||||
v-if="row.memberCount > 0"
|
||||
class="link-text"
|
||||
@click="viewGroupMembers(row)"
|
||||
>
|
||||
{{ row.memberCount }}
|
||||
</a>
|
||||
<span v-else>{{ row.memberCount ?? 0 }}</span>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 分组描述 -->
|
||||
<el-table-column prop="description" label="分组描述" min-width="240" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row ? row.description || "-" : "-" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 创建时间 -->
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center" />
|
||||
|
||||
<!-- 更新时间 -->
|
||||
<el-table-column prop="updateTime" label="更新时间" min-width="170" align="center" />
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<el-table-column label="操作" width="180" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row">
|
||||
<a class="link-text" @click="viewGroupMembers(row)">查看会员</a>
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text" @click="openEdit(row)">编辑</a>
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text" @click="remove(row)">删除</a>
|
||||
</div>
|
||||
<a class="link-text error-text" @click="remove(row)">删除</a>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
||||
<!-- 底部分页 -->
|
||||
<div class="pagination-wrap mt_10">
|
||||
<el-pagination
|
||||
v-model:current-page="searchForm.pageNumber"
|
||||
v-model:page-size="searchForm.pageSize"
|
||||
@@ -37,34 +110,78 @@
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="addFlag" title="添加分组" width="500px" :close-on-click-modal="false" destroy-on-close>
|
||||
<!-- 添加分组弹窗 -->
|
||||
<el-dialog
|
||||
v-model="addFlag"
|
||||
title="添加分组"
|
||||
width="520px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form ref="addForm" :model="formAdd" :rules="rulesAdd" label-width="90px">
|
||||
<el-form-item label="分组名称" prop="groupName" style="width: 90%">
|
||||
<el-input v-model="formAdd.groupName" maxlength="30" placeholder="请输入分组名称" />
|
||||
<el-form-item label="分组名称" prop="groupName">
|
||||
<el-input
|
||||
v-model="formAdd.groupName"
|
||||
maxlength="30"
|
||||
show-word-limit
|
||||
placeholder="请输入分组名称"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="分组描述" prop="description" style="width: 90%">
|
||||
<el-input v-model="formAdd.description" maxlength="200" placeholder="请输入分组描述" />
|
||||
<el-form-item label="分组描述" prop="description">
|
||||
<el-input
|
||||
v-model="formAdd.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
placeholder="请输入分组描述信息"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="addFlag = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitAddLoading" @click="submitAdd">确定</el-button>
|
||||
<el-button type="primary" :loading="submitAddLoading" @click="submitAdd">
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="editFlag" title="编辑分组" width="500px" :close-on-click-modal="false" destroy-on-close>
|
||||
<!-- 编辑分组弹窗 -->
|
||||
<el-dialog
|
||||
v-model="editFlag"
|
||||
title="编辑分组"
|
||||
width="520px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form ref="editForm" :model="formEdit" :rules="rulesEdit" label-width="90px">
|
||||
<el-input v-model="formEdit.id" style="display: none" />
|
||||
<el-form-item label="分组名称" prop="groupName" style="width: 90%">
|
||||
<el-input v-model="formEdit.groupName" maxlength="30" placeholder="请输入分组名称" />
|
||||
<el-form-item label="分组ID">
|
||||
<span>{{ formEdit.id }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="分组描述" prop="description" style="width: 90%">
|
||||
<el-input v-model="formEdit.description" maxlength="200" placeholder="请输入分组描述" />
|
||||
<el-form-item label="分组名称" prop="groupName">
|
||||
<el-input
|
||||
v-model="formEdit.groupName"
|
||||
maxlength="30"
|
||||
show-word-limit
|
||||
placeholder="请输入分组名称"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="分组描述" prop="description">
|
||||
<el-input
|
||||
v-model="formEdit.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
placeholder="请输入分组描述信息"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="editFlag = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitEditLoading" @click="submitEdit">确定</el-button>
|
||||
<el-button type="primary" :loading="submitEditLoading" @click="submitEdit">
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
@@ -72,11 +189,21 @@
|
||||
|
||||
<script setup>
|
||||
import { nextTick, onMounted, reactive, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import * as API_Member from "@/api/member.js";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
|
||||
defineOptions({ name: "memberGroup" });
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
function viewGroupMembers(row) {
|
||||
router.push({
|
||||
name: "memberList",
|
||||
query: { groupId: row.id },
|
||||
});
|
||||
}
|
||||
|
||||
const addForm = ref(null);
|
||||
const editForm = ref(null);
|
||||
|
||||
@@ -84,6 +211,7 @@ const loading = ref(true);
|
||||
const searchForm = reactive({
|
||||
pageNumber: 1,
|
||||
pageSize: 20,
|
||||
groupName: "",
|
||||
});
|
||||
const data = ref([]);
|
||||
const total = ref(0);
|
||||
@@ -91,44 +219,81 @@ const addFlag = ref(false);
|
||||
const editFlag = ref(false);
|
||||
const submitAddLoading = ref(false);
|
||||
const submitEditLoading = ref(false);
|
||||
|
||||
const formAdd = ref({
|
||||
groupName: "",
|
||||
description: "",
|
||||
});
|
||||
|
||||
const formEdit = ref({
|
||||
id: "",
|
||||
groupName: "",
|
||||
description: "",
|
||||
});
|
||||
|
||||
const rulesAdd = reactive({
|
||||
groupName: [{ required: true, message: "请输入分组名称", trigger: "blur" }],
|
||||
});
|
||||
|
||||
const rulesEdit = reactive({
|
||||
groupName: [{ required: true, message: "请输入分组名称", trigger: "blur" }],
|
||||
});
|
||||
|
||||
function init() {
|
||||
function handleSearch() {
|
||||
searchForm.pageNumber = 1;
|
||||
getData();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.pageNumber = 1;
|
||||
searchForm.groupName = "";
|
||||
getData();
|
||||
}
|
||||
|
||||
function changePage(v) {
|
||||
searchForm.pageNumber = v;
|
||||
getData();
|
||||
}
|
||||
|
||||
function changePageSize(v) {
|
||||
searchForm.pageNumber = 1;
|
||||
searchForm.pageSize = v;
|
||||
getData();
|
||||
}
|
||||
|
||||
function getData() {
|
||||
loading.value = true;
|
||||
API_Member.getMemberGroupByPage(searchForm).then((res) => {
|
||||
loading.value = false;
|
||||
if (res && res.success && res.result) {
|
||||
data.value = res.result.records || [];
|
||||
total.value = res.result.total || 0;
|
||||
}
|
||||
});
|
||||
API_Member.getMemberGroupByPage(searchForm)
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
if (res && res.success && res.result) {
|
||||
const records = res.result.records || [];
|
||||
data.value = records;
|
||||
total.value = res.result.total || 0;
|
||||
|
||||
// 异步获取各分组的会员总数
|
||||
records.forEach((row) => {
|
||||
if (row.memberCount == null) {
|
||||
API_Member.getMemberGroupUsers(row.id, { pageNumber: 1, pageSize: 1 })
|
||||
.then((userRes) => {
|
||||
if (userRes && userRes.success && userRes.result) {
|
||||
row.memberCount = userRes.result.total || 0;
|
||||
} else {
|
||||
row.memberCount = 0;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
row.memberCount = 0;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function openAdd() {
|
||||
addFlag.value = true;
|
||||
nextTick(() => {
|
||||
@@ -136,20 +301,26 @@ function openAdd() {
|
||||
formAdd.value = { groupName: "", description: "" };
|
||||
});
|
||||
}
|
||||
|
||||
function submitAdd() {
|
||||
addForm.value.validate((valid) => {
|
||||
if (!valid) return;
|
||||
submitAddLoading.value = true;
|
||||
API_Member.addMemberGroup(formAdd.value).then((res) => {
|
||||
submitAddLoading.value = false;
|
||||
if (res && res.success) {
|
||||
ElMessage.success("添加成功");
|
||||
addFlag.value = false;
|
||||
getData();
|
||||
}
|
||||
});
|
||||
API_Member.addMemberGroup(formAdd.value)
|
||||
.then((res) => {
|
||||
submitAddLoading.value = false;
|
||||
if (res && res.success) {
|
||||
ElMessage.success("添加成功");
|
||||
addFlag.value = false;
|
||||
getData();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
submitAddLoading.value = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
editFlag.value = true;
|
||||
submitEditLoading.value = false;
|
||||
@@ -169,48 +340,120 @@ function openEdit(row) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function submitEdit() {
|
||||
editForm.value.validate((valid) => {
|
||||
if (!valid) return;
|
||||
submitEditLoading.value = true;
|
||||
const { id, groupName, description } = formEdit.value;
|
||||
API_Member.updateMemberGroup(id, { groupName, description }).then((res) => {
|
||||
submitEditLoading.value = false;
|
||||
if (res && res.success) {
|
||||
ElMessage.success("修改成功");
|
||||
editFlag.value = false;
|
||||
getData();
|
||||
}
|
||||
});
|
||||
API_Member.updateMemberGroup(id, { groupName, description })
|
||||
.then((res) => {
|
||||
submitEditLoading.value = false;
|
||||
if (res && res.success) {
|
||||
ElMessage.success("修改成功");
|
||||
editFlag.value = false;
|
||||
getData();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
submitEditLoading.value = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function remove(row) {
|
||||
ElMessageBox.confirm("确定删除该分组?", "提示", { type: "warning" }).then(() => {
|
||||
API_Member.deleteMemberGroup(row.id).then((res) => {
|
||||
if (res && res.success) {
|
||||
ElMessage.success("删除成功");
|
||||
getData();
|
||||
} else if (res && res.message) {
|
||||
ElMessage.error(res.message);
|
||||
}
|
||||
});
|
||||
}).catch(() => {});
|
||||
ElMessageBox.confirm(`确定删除分组 "${row.groupName || "该分组"}" 吗?`, "删除确认", {
|
||||
type: "warning",
|
||||
confirmButtonText: "确定删除",
|
||||
cancelButtonText: "取消",
|
||||
})
|
||||
.then(() => {
|
||||
API_Member.deleteMemberGroup(row.id).then((res) => {
|
||||
if (res && res.success) {
|
||||
ElMessage.success("删除成功");
|
||||
getData();
|
||||
} else if (res && res.message) {
|
||||
ElMessage.error(res.message);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
getData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ops a {
|
||||
<style scoped lang="scss">
|
||||
.member-group-page {
|
||||
.card-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
|
||||
.filter-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 0;
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.card-header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.header-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.link-text {
|
||||
color: #2d8cf0;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
.ops span {
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
display: inline-block;
|
||||
|
||||
&:hover {
|
||||
color: #57a3f3;
|
||||
}
|
||||
|
||||
&.error-text {
|
||||
color: #ed4014;
|
||||
|
||||
&:hover {
|
||||
color: #f16643;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.op-split {
|
||||
margin: 0 8px;
|
||||
color: #dcdee2;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -308,6 +308,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, nextTick, onMounted } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { ArrowDown } from "@element-plus/icons-vue";
|
||||
import multipleMap from "@/components/map/multiple-map";
|
||||
import * as API_Member from "@/api/member.js";
|
||||
@@ -320,6 +321,8 @@ import defaultPic from "@/assets/default.png";
|
||||
|
||||
defineOptions({ name: "member" });
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const props = defineProps({
|
||||
selectedMember: {
|
||||
type: Boolean,
|
||||
@@ -511,7 +514,15 @@ function submitSetMemberGroup() {
|
||||
const groupId = memberGroupForm.value.groupId;
|
||||
memberGroupLoading.value = true;
|
||||
Promise.all(
|
||||
memberIds.map((memberId) => API_Member.setMemberUserGroups(memberId, [groupId]))
|
||||
memberIds.map((memberId) =>
|
||||
API_Member.setMemberUserGroups(memberId, [groupId]).then((res) => {
|
||||
if (!res || !res.success) {
|
||||
// 后端 updateUserGroups 若因原无分组返回失败,自动降级尝试 addMemberGroupUsers
|
||||
return API_Member.addMemberGroupUsers(groupId, [memberId]);
|
||||
}
|
||||
return res;
|
||||
})
|
||||
)
|
||||
)
|
||||
.then((results) => {
|
||||
memberGroupLoading.value = false;
|
||||
@@ -758,11 +769,25 @@ function handleSubmitModal() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (route.query && route.query.groupId) {
|
||||
searchForm.value.groupId = String(route.query.groupId);
|
||||
}
|
||||
getData();
|
||||
loadMemberGroupList();
|
||||
loadMemberGradeList();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => route.query.groupId,
|
||||
(val) => {
|
||||
if (val !== undefined) {
|
||||
searchForm.value.groupId = val ? String(val) : "";
|
||||
searchForm.value.pageNumber = 1;
|
||||
getData();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 外部通过 ref 调用(promotions/coupon-activity/coupon-publish.vue 的 memberLayoutRef.getSelection())
|
||||
defineExpose({ getSelection });
|
||||
</script>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<template>
|
||||
<div class="search">
|
||||
<el-card>
|
||||
<div class="member-recycle-page search">
|
||||
<!-- 顶部筛选条件表单 -->
|
||||
<el-card shadow="never" class="filter-card">
|
||||
<el-form
|
||||
ref="searchFormRef"
|
||||
:model="searchForm"
|
||||
inline
|
||||
label-width="70px"
|
||||
label-width="76px"
|
||||
class="search-form"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
@@ -14,7 +15,7 @@
|
||||
v-model="searchForm.id"
|
||||
placeholder="请输入客户ID"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户名称" prop="username">
|
||||
@@ -22,66 +23,94 @@
|
||||
v-model="searchForm.username"
|
||||
placeholder="请输入客户名称"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
style="width: 180px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="联系方式" prop="mobile">
|
||||
<el-input
|
||||
v-model="searchForm.mobile"
|
||||
placeholder="请输入会员联系方式"
|
||||
placeholder="请输入手机联系方式"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
style="width: 180px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-form-item class="form-actions">
|
||||
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card>
|
||||
<!-- 回收站会员表格卡片 -->
|
||||
<el-card shadow="never" class="table-card mt_10">
|
||||
<template #header>
|
||||
<div class="card-header-bar">
|
||||
<div class="header-title">
|
||||
<span>会员回收站列表</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button size="small" @click="getData">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table
|
||||
ref="table"
|
||||
v-loading="loading"
|
||||
border
|
||||
stripe
|
||||
:data="data"
|
||||
class="mt_10"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="id" label="客户ID" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="username" label="客户名称" min-width="150" align="left" show-overflow-tooltip />
|
||||
<el-table-column prop="nickName" label="昵称" min-width="120" align="left" show-overflow-tooltip />
|
||||
<el-table-column label="联系方式" min-width="130">
|
||||
<!-- 客户ID -->
|
||||
<el-table-column prop="id" label="客户ID" min-width="190" show-overflow-tooltip />
|
||||
|
||||
<!-- 客户名称 -->
|
||||
<el-table-column prop="username" label="客户名称" min-width="150" show-overflow-tooltip />
|
||||
|
||||
<!-- 昵称 -->
|
||||
<el-table-column prop="nickName" label="昵称" min-width="140" show-overflow-tooltip />
|
||||
|
||||
<!-- 联系方式 -->
|
||||
<el-table-column label="联系方式" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.mobile || "" }}</span>
|
||||
{{ row && row.mobile ? row.mobile : "-" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="注册时间" min-width="180" />
|
||||
<el-table-column label="积分数量" min-width="120" align="left">
|
||||
|
||||
<!-- 积分数量 -->
|
||||
<el-table-column label="积分数量" width="120">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.point == void 0 ? "0" : row.point }}</span>
|
||||
{{ row ? (row.point == void 0 ? "0" : row.point) : "0" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||
|
||||
<!-- 注册时间 -->
|
||||
<el-table-column prop="createTime" label="注册时间" min-width="180" align="center" />
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<el-table-column label="操作" width="180" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row" class="ops" style="display: flex; justify-content: center">
|
||||
<template v-if="row">
|
||||
<template v-if="selectedMember">
|
||||
<a class="link-text" @click="callback(row)">选择</a>
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text" @click="callback(row)">
|
||||
{{ row.___selected ? "已选择" : "选择" }}
|
||||
</a>
|
||||
</template>
|
||||
<a class="link-text" @click="detail(row)">查看</a>
|
||||
<template v-if="!selectedMember">
|
||||
<template v-else>
|
||||
<a class="link-text" @click="detail(row)">查看</a>
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text" @click="enable(row)">启用</a>
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text" @click="editPerm(row)">编辑</a>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
||||
<!-- 底部分页 -->
|
||||
<div class="pagination-wrap mt_10">
|
||||
<el-pagination
|
||||
v-model:current-page="searchForm.pageNumber"
|
||||
v-model:page-size="searchForm.pageSize"
|
||||
@@ -95,23 +124,38 @@
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="descFlag" :title="descTitle" width="500px" destroy-on-close>
|
||||
<el-form ref="formValidateRef" :model="formValidate" :rules="ruleValidate" label-width="80px">
|
||||
<!-- 编辑会员弹窗 -->
|
||||
<el-dialog
|
||||
v-model="descFlag"
|
||||
:title="descTitle"
|
||||
width="540px"
|
||||
destroy-on-close
|
||||
class="edit-member-dialog"
|
||||
>
|
||||
<el-form ref="formValidateRef" :model="formValidate" :rules="ruleValidate" label-width="90px">
|
||||
<el-form-item label="客户ID">
|
||||
<span>{{ formValidate.id }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="头像">
|
||||
<img alt="会员头像" :src="formValidate.face" class="face" />
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
class="upload"
|
||||
@click="openPicSelector"
|
||||
>修改</el-button>
|
||||
<input type="file" style="display: none" id="file" />
|
||||
<div class="avatar-edit-box">
|
||||
<el-avatar :size="50" :src="formValidate.face || undefined" class="dialog-avatar">
|
||||
{{ (formValidate.username || "用")[0] }}
|
||||
</el-avatar>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
class="upload-btn"
|
||||
@click="openPicSelector"
|
||||
>
|
||||
修改头像
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户名称" prop="name">
|
||||
<el-input v-model="formValidate.username" style="width: 200px" disabled />
|
||||
<el-input v-model="formValidate.username" style="width: 260px" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="用户昵称" prop="name">
|
||||
<el-input v-model="formValidate.nickName" style="width: 200px" />
|
||||
<el-input v-model="formValidate.nickName" placeholder="请输入用户昵称" style="width: 260px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="性别" prop="sex">
|
||||
<el-radio-group v-model="formValidate.sex">
|
||||
@@ -119,20 +163,29 @@
|
||||
<el-radio-button :value="0">女</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="修改密码" prop="password">
|
||||
<el-input v-model="formValidate.newPassword" type="password" show-password style="width: 220px" />
|
||||
<el-form-item label="重置密码" prop="password">
|
||||
<el-input
|
||||
v-model="formValidate.newPassword"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="留空则保持原密码"
|
||||
style="width: 260px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="生日" prop="birthday">
|
||||
<el-date-picker
|
||||
v-model="formValidate.birthday"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 220px"
|
||||
placeholder="请选择生日"
|
||||
style="width: 260px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="所在地" prop="mail">
|
||||
{{ formValidate.region || "暂无地址" }}
|
||||
<el-button style="margin-left: 10px" @click="$refs.map.open()">选择</el-button>
|
||||
<div class="region-box">
|
||||
<span class="region-text">{{ formValidate.region || "暂无地址" }}</span>
|
||||
<el-button size="small" @click="$refs.map.open()">选择地区</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -141,7 +194,7 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="picModelFlag" width="1200px">
|
||||
<el-dialog v-model="picModelFlag" width="1200px" title="选择头像图片">
|
||||
<ossManage
|
||||
ref="ossManageRef"
|
||||
@callback="callbackSelected"
|
||||
@@ -166,19 +219,16 @@ defineOptions({ name: "memberRecycle" });
|
||||
|
||||
const emit = defineEmits(["callback"]);
|
||||
|
||||
// 模板 el-form 的 ref(原 ref="searchForm",与 data 同名会互相覆盖,改名为 searchFormRef)
|
||||
const searchFormRef = ref(null);
|
||||
const table = ref(null);
|
||||
// 模板编辑弹窗 el-form 的 ref(原 ref="formValidate",与 data 同名会互相覆盖,改名为 formValidateRef)
|
||||
const formValidateRef = ref(null);
|
||||
// 模板 ossManage 组件的 ref(原 ref="ossManage",与组件 import 同名会冲突,改名为 ossManageRef)
|
||||
const ossManageRef = ref(null);
|
||||
|
||||
const selectedMember = ref(false);
|
||||
const descTitle = ref("");
|
||||
const descFlag = ref(false);
|
||||
const openSearch = ref(true);
|
||||
const loading = ref(true);
|
||||
|
||||
const searchForm = ref({
|
||||
pageNumber: 1,
|
||||
pageSize: 20,
|
||||
@@ -188,6 +238,7 @@ const searchForm = ref({
|
||||
mobile: "",
|
||||
disabled: "CLOSE",
|
||||
});
|
||||
|
||||
const picModelFlag = ref(false);
|
||||
const formValidate = ref({});
|
||||
const ruleValidate = ref({});
|
||||
@@ -197,26 +248,40 @@ const total = ref(0);
|
||||
function callback(val) {
|
||||
emit("callback", val);
|
||||
}
|
||||
|
||||
function init() {
|
||||
getData();
|
||||
}
|
||||
|
||||
function changePage() {
|
||||
getData();
|
||||
}
|
||||
|
||||
function changePageSize() {
|
||||
searchForm.value.pageNumber = 1;
|
||||
getData();
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
searchForm.value.pageNumber = 1;
|
||||
searchForm.value.pageSize = 20;
|
||||
getData();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.value.pageNumber = 1;
|
||||
searchForm.value.id = "";
|
||||
searchForm.value.username = "";
|
||||
searchForm.value.mobile = "";
|
||||
getData();
|
||||
}
|
||||
|
||||
function editPerm(val) {
|
||||
descTitle.value = `查看用户 ${val.username}`;
|
||||
descTitle.value = `编辑停用客户 - ${val.username || "客户"}`;
|
||||
descFlag.value = true;
|
||||
getMemberInfo(val.id);
|
||||
}
|
||||
|
||||
function getMemberInfo(id) {
|
||||
API_Member.getMemberInfoData(id).then((res) => {
|
||||
if (res.result) {
|
||||
@@ -224,16 +289,22 @@ function getMemberInfo(id) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getData() {
|
||||
loading.value = true;
|
||||
API_Member.getMemberListData(searchForm.value).then((res) => {
|
||||
loading.value = false;
|
||||
if (res.success) {
|
||||
data.value = res.result.records;
|
||||
total.value = res.result.total;
|
||||
}
|
||||
});
|
||||
API_Member.getMemberListData(searchForm.value)
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
if (res.success && res.result) {
|
||||
data.value = res.result.records || [];
|
||||
total.value = res.result.total || 0;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function openPicSelector() {
|
||||
picModelFlag.value = true;
|
||||
nextTick(() => {
|
||||
@@ -242,10 +313,12 @@ function openPicSelector() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function callbackSelected(val) {
|
||||
picModelFlag.value = false;
|
||||
formValidate.value.face = val.url;
|
||||
}
|
||||
|
||||
function selectedRegion(val) {
|
||||
if (val.type === "select") {
|
||||
const paths = val.data.map((item) => item.name).join(",");
|
||||
@@ -257,23 +330,32 @@ function selectedRegion(val) {
|
||||
formValidate.value.regionId = val.data.addrId;
|
||||
}
|
||||
}
|
||||
|
||||
function detail(row) {
|
||||
customRouterPush({ name: "member-detail", query: { id: row.id } });
|
||||
}
|
||||
|
||||
function enable(v) {
|
||||
let params = {
|
||||
memberIds: [v.id],
|
||||
disabled: true,
|
||||
};
|
||||
ElMessageBox.confirm("确定启用此会员?", "提示", { type: "warning" }).then(() => {
|
||||
API_Member.updateMemberStatus(params).then((res) => {
|
||||
if (res.success) {
|
||||
ElMessage.success("启用成功");
|
||||
getData();
|
||||
}
|
||||
});
|
||||
}).catch(() => {});
|
||||
ElMessageBox.confirm(`确定启用客户 "${v.username || v.nickName || "该客户"}" 吗?`, "提示", {
|
||||
type: "warning",
|
||||
confirmButtonText: "确定启用",
|
||||
cancelButtonText: "取消",
|
||||
})
|
||||
.then(() => {
|
||||
API_Member.updateMemberStatus(params).then((res) => {
|
||||
if (res.success) {
|
||||
ElMessage.success("启用成功");
|
||||
getData();
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function handleSubmitModal() {
|
||||
const { nickName, sex, username, face, newPassword, id, regionId, region } = formValidate.value;
|
||||
let time = new Date(formValidate.value.birthday);
|
||||
@@ -288,7 +370,6 @@ function handleSubmitModal() {
|
||||
id,
|
||||
};
|
||||
if (newPassword) {
|
||||
// 原 this.md5 为 main.js 注册的全局属性,改为具名导入(同 login.vue / userManage.vue)
|
||||
submit.password = md5(newPassword);
|
||||
}
|
||||
API_Member.updateMember(submit).then((res) => {
|
||||
@@ -304,20 +385,67 @@ onMounted(() => {
|
||||
init();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.face {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
.member-recycle-page {
|
||||
.filter-card {
|
||||
.search-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 0;
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.header-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
.ops a {
|
||||
color: #2d8cf0;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
|
||||
.avatar-edit-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.dialog-avatar {
|
||||
background-color: #f56c6c;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
.ops span {
|
||||
display: inline-block;
|
||||
margin: 0 8px;
|
||||
color: #dcdee2;
|
||||
|
||||
.region-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.region-text {
|
||||
color: #606266;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,37 +1,54 @@
|
||||
<template>
|
||||
<div class="search">
|
||||
<el-card class="points-statistics-card">
|
||||
<el-row type="flex" justify="space-around" align="middle" class="points-statistics">
|
||||
<div class="member-point-page search">
|
||||
<!-- 顶部数据统计概览看板 -->
|
||||
<el-card shadow="never" class="points-statistics-card">
|
||||
<el-row :gutter="24" class="points-statistics">
|
||||
<el-col :xs="24" :sm="12" class="points-statistics-item">
|
||||
<div class="points-statistics-title">已发放积分数</div>
|
||||
<div class="points-statistics-subtitle">历史累计发放积分数</div>
|
||||
<div class="points-statistics-value">{{ formatNumber(pointsStatistics.totalPoint) }}</div>
|
||||
<div class="stat-inner-box">
|
||||
<div class="points-statistics-title">已发放积分数</div>
|
||||
<div class="points-statistics-subtitle">平台历史累计发放积分总量</div>
|
||||
<div class="points-statistics-value">{{ formatNumber(pointsStatistics.totalPoint) }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" class="points-statistics-item">
|
||||
<div class="points-statistics-title">未使用积分数</div>
|
||||
<div class="points-statistics-subtitle">会员账户未使用积分数</div>
|
||||
<div class="points-statistics-value">{{ formatNumber(pointsStatistics.unUsedPoint) }}</div>
|
||||
<div class="stat-inner-box">
|
||||
<div class="points-statistics-title">未使用积分数</div>
|
||||
<div class="points-statistics-subtitle">会员账户当前剩余未使用积分总量</div>
|
||||
<div class="points-statistics-value text-warning">{{ formatNumber(pointsStatistics.unUsedPoint) }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<!-- 选项卡与数据主区域 -->
|
||||
<div class="point-tabs-wrap">
|
||||
<el-tabs v-model="activeTab" class="point-tabs">
|
||||
<el-tab-pane label="积分列表" name="pointList">
|
||||
<el-card class="point-content-card">
|
||||
<!-- 选项卡 1:会员积分列表 -->
|
||||
<el-tab-pane label="会员积分列表" name="pointList">
|
||||
<!-- 筛选条件 -->
|
||||
<el-card shadow="never" class="point-filter-card">
|
||||
<el-form
|
||||
ref="memberSearchFormRef"
|
||||
:model="memberSearchForm"
|
||||
inline
|
||||
label-width="70px"
|
||||
label-width="76px"
|
||||
class="search-form"
|
||||
@keyup.enter="handleMemberSearch"
|
||||
>
|
||||
<el-form-item label="客户ID" prop="id">
|
||||
<el-input
|
||||
v-model="memberSearchForm.id"
|
||||
placeholder="请输入客户ID"
|
||||
clearable
|
||||
style="width: 190px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户名称" prop="nickName">
|
||||
<el-input
|
||||
v-model="memberSearchForm.nickName"
|
||||
placeholder="请输入客户名称"
|
||||
clearable
|
||||
style="width: 180px"
|
||||
style="width: 160px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户账号" prop="username">
|
||||
@@ -39,82 +56,128 @@
|
||||
v-model="memberSearchForm.username"
|
||||
placeholder="请输入客户账号"
|
||||
clearable
|
||||
style="width: 180px"
|
||||
style="width: 160px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="账号状态" prop="disabled">
|
||||
<el-select
|
||||
v-model="memberSearchForm.disabled"
|
||||
clearable
|
||||
placeholder="请选择账号状态"
|
||||
style="width: 160px"
|
||||
placeholder="全部状态"
|
||||
style="width: 130px"
|
||||
>
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="启用" value="OPEN" />
|
||||
<el-option label="禁用" value="CLOSE" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="积分值">
|
||||
<el-form-item label="积分区间">
|
||||
<el-input-number
|
||||
v-model="memberSearchForm.minPoint"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
placeholder="最小积分值"
|
||||
style="width: 140px"
|
||||
placeholder="最小"
|
||||
controls-position="right"
|
||||
style="width: 120px"
|
||||
/>
|
||||
<span class="point-range-separator">-</span>
|
||||
<el-input-number
|
||||
v-model="memberSearchForm.maxPoint"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
placeholder="最大积分值"
|
||||
style="width: 140px"
|
||||
placeholder="最大"
|
||||
controls-position="right"
|
||||
style="width: 120px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button class="search-btn" type="primary" @click="handleMemberSearch">搜索</el-button>
|
||||
<el-form-item class="form-actions">
|
||||
<el-button class="search-btn" type="primary" @click="handleMemberSearch">
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleMemberReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-card class="point-content-card member-list-card">
|
||||
|
||||
<!-- 会员积分表格 -->
|
||||
<el-card shadow="never" class="point-content-card member-list-card mt_10">
|
||||
<template #header>
|
||||
<div class="card-title">用户列表</div>
|
||||
<div class="card-header-bar">
|
||||
<div class="header-title">
|
||||
<span>会员积分信息</span>
|
||||
<span class="total-badge">共 {{ memberTotal }} 位会员</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button size="small" @click="getMemberList">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table
|
||||
v-loading="memberLoading"
|
||||
border
|
||||
stripe
|
||||
:data="memberData"
|
||||
class="member-table"
|
||||
class="custom-point-table"
|
||||
style="width: 100%"
|
||||
>
|
||||
<!-- 客户ID -->
|
||||
<el-table-column label="客户ID" min-width="170">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" class="font-mono text-dark">{{ row.id || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 客户名称 -->
|
||||
<el-table-column label="客户名称" min-width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.nickName || "-" }}</span>
|
||||
<span v-if="row" class="font-semibold text-dark">{{ row.nickName || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="客户账号" min-width="260" show-overflow-tooltip>
|
||||
|
||||
<!-- 客户账号 -->
|
||||
<el-table-column label="客户账号" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.username || "-" }}</span>
|
||||
<span v-if="row" class="font-mono">{{ row.username || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="账号状态" min-width="120" align="center">
|
||||
|
||||
<!-- 账号状态 -->
|
||||
<el-table-column label="账号状态" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row" :type="row.disabled === true ? 'success' : 'info'">
|
||||
<el-tag
|
||||
v-if="row"
|
||||
:type="row.disabled === true ? 'success' : 'info'"
|
||||
size="small"
|
||||
>
|
||||
{{ row.disabled === true ? "启用" : "禁用" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="积分余额" min-width="120" align="center">
|
||||
|
||||
<!-- 积分余额 -->
|
||||
<el-table-column label="积分余额" width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.point == null ? 0 : row.point }}</span>
|
||||
<span v-if="row" class="point-num-text">
|
||||
{{ formatNumber(row.point == null ? 0 : row.point) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" align="center" fixed="right">
|
||||
|
||||
<!-- 操作 -->
|
||||
<el-table-column label="操作" width="180" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<a v-if="row" class="link-text" @click="detail(row)">详情</a>
|
||||
<template v-if="row">
|
||||
<a class="link-text" @click="detail(row)">客户详情</a>
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text" @click="viewMemberHistory(row)">变动明细</a>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-wrap mt_10">
|
||||
<el-pagination
|
||||
v-model:current-page="memberSearchForm.pageNumber"
|
||||
v-model:page-size="memberSearchForm.pageSize"
|
||||
@@ -128,63 +191,140 @@
|
||||
</div>
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 选项卡 2:积分增减记录 -->
|
||||
<el-tab-pane label="积分增减记录" name="pointChangeRecord">
|
||||
<el-card class="point-content-card">
|
||||
<!-- 筛选条件 -->
|
||||
<el-card shadow="never" class="point-filter-card">
|
||||
<el-form
|
||||
ref="searchFormRef"
|
||||
:model="searchForm"
|
||||
inline
|
||||
label-width="70px"
|
||||
label-width="76px"
|
||||
class="search-form"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<el-form-item label="会员名称" prop="username">
|
||||
<el-form-item label="客户ID" prop="memberId">
|
||||
<el-input
|
||||
v-model="searchForm.memberId"
|
||||
placeholder="请输入客户ID"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="会员名称" prop="memberName">
|
||||
<el-input
|
||||
v-model="searchForm.memberName"
|
||||
placeholder="请输入会员名称"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
style="width: 180px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button class="search-btn" type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-form-item class="form-actions">
|
||||
<el-button class="search-btn" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleHistoryReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-card class="point-content-card">
|
||||
|
||||
<!-- 积分记录表格 -->
|
||||
<el-card shadow="never" class="point-content-card mt_10">
|
||||
<template #header>
|
||||
<div class="card-header-bar">
|
||||
<div class="header-title">
|
||||
<span>积分流水记录</span>
|
||||
<span class="total-badge">共 {{ total }} 条明细</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button size="small" @click="getData">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
border
|
||||
stripe
|
||||
:data="data"
|
||||
ref="table"
|
||||
class="mt_10 point-table"
|
||||
class="custom-point-table"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="memberName" label="会员名称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="content" label="操作内容" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="beforePoint" label="之前积分" width="200" />
|
||||
<el-table-column label="变动积分" width="200">
|
||||
<!-- 客户ID -->
|
||||
<el-table-column label="客户ID" min-width="170">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" class="font-mono text-dark">{{ row.memberId || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 会员名称 -->
|
||||
<el-table-column prop="memberName" label="会员名称" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" class="font-semibold text-dark">{{ row.memberName || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 操作内容/原因 -->
|
||||
<el-table-column prop="content" label="操作内容" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span>{{ row ? row.content || "-" : "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 变动前积分 -->
|
||||
<el-table-column label="变动前积分" width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ formatNumber(row.beforePoint) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 变动积分 -->
|
||||
<el-table-column label="变动积分" width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row">
|
||||
<priceColorScheme
|
||||
<el-tag
|
||||
v-if="row.pointType === 'INCREASE'"
|
||||
:value="row.variablePoint"
|
||||
color="green"
|
||||
unit="+"
|
||||
/>
|
||||
<priceColorScheme
|
||||
type="success"
|
||||
size="small"
|
||||
effect="light"
|
||||
class="point-change-tag"
|
||||
>
|
||||
+{{ formatNumber(row.variablePoint) }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-else
|
||||
:value="row.variablePoint"
|
||||
:color="$mainColor"
|
||||
unit="-"
|
||||
/>
|
||||
type="danger"
|
||||
size="small"
|
||||
effect="light"
|
||||
class="point-change-tag"
|
||||
>
|
||||
-{{ formatNumber(row.variablePoint) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="point" label="当前积分" width="200" />
|
||||
<el-table-column prop="createTime" label="操作时间" width="200" />
|
||||
|
||||
<!-- 变动后积分 -->
|
||||
<el-table-column label="变动后积分" width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" class="point-num-text">
|
||||
{{ formatNumber(row.point) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 操作时间 -->
|
||||
<el-table-column prop="createTime" label="操作时间" width="170" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" class="time-text">{{ row.createTime }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-wrap mt_10">
|
||||
<el-pagination
|
||||
v-model:current-page="searchForm.pageNumber"
|
||||
v-model:page-size="searchForm.pageSize"
|
||||
@@ -207,12 +347,12 @@
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import * as API_Member from "@/api/member.js";
|
||||
import { customRouterPush } from "@/utils/filters";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
defineOptions({ name: "point" });
|
||||
|
||||
const emit = defineEmits(["callback"]);
|
||||
|
||||
// 模板 ref,script 内未使用
|
||||
const memberSearchFormRef = ref(null);
|
||||
const searchFormRef = ref(null);
|
||||
const table = ref(null);
|
||||
@@ -224,35 +364,60 @@ const pointsStatistics = ref({
|
||||
totalPoint: 0,
|
||||
unUsedPoint: 0,
|
||||
});
|
||||
|
||||
const searchForm = reactive({
|
||||
pageNumber: 1,
|
||||
pageSize: 20,
|
||||
memberId: "",
|
||||
memberName: "",
|
||||
});
|
||||
|
||||
const memberSearchForm = reactive({
|
||||
pageNumber: 1,
|
||||
pageSize: 20,
|
||||
id: "",
|
||||
nickName: "",
|
||||
username: "",
|
||||
disabled: "",
|
||||
minPoint: null,
|
||||
maxPoint: null,
|
||||
});
|
||||
|
||||
const data = ref([]);
|
||||
const memberData = ref([]);
|
||||
const memberTotal = ref(0);
|
||||
const total = ref(0);
|
||||
|
||||
function callback(val) {
|
||||
emit("callback", val);
|
||||
function copyText(text, label = "客户ID") {
|
||||
if (!text) return;
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard
|
||||
.writeText(String(text))
|
||||
.then(() => ElMessage.success(`已复制${label}到剪贴板`))
|
||||
.catch(() => ElMessage.info(String(text)));
|
||||
} else {
|
||||
ElMessage.info(String(text));
|
||||
}
|
||||
}
|
||||
|
||||
function detail(row) {
|
||||
customRouterPush({ name: "member-detail", query: { id: row.id } });
|
||||
}
|
||||
|
||||
function viewMemberHistory(row) {
|
||||
activeTab.value = "pointChangeRecord";
|
||||
searchForm.memberId = row.id;
|
||||
searchForm.memberName = row.nickName || row.username || "";
|
||||
searchForm.pageNumber = 1;
|
||||
getData();
|
||||
}
|
||||
|
||||
function init() {
|
||||
getStatistics();
|
||||
getMemberList();
|
||||
getData();
|
||||
}
|
||||
|
||||
function getStatistics() {
|
||||
API_Member.queryMemberPointsStatistics().then((res) => {
|
||||
if (res && res.success && res.result) {
|
||||
@@ -263,45 +428,71 @@ function getStatistics() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function formatNumber(value) {
|
||||
const numericValue = Number(value || 0);
|
||||
if (!Number.isFinite(numericValue)) return "0";
|
||||
return numericValue.toLocaleString();
|
||||
}
|
||||
|
||||
function changePage(v) {
|
||||
searchForm.pageNumber = v;
|
||||
getData();
|
||||
}
|
||||
|
||||
function changePageSize(v) {
|
||||
searchForm.pageNumber = 1;
|
||||
searchForm.pageSize = v;
|
||||
getData();
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
searchForm.pageNumber = 1;
|
||||
searchForm.pageSize = 20;
|
||||
getData();
|
||||
}
|
||||
|
||||
function handleHistoryReset() {
|
||||
searchForm.memberId = "";
|
||||
searchForm.memberName = "";
|
||||
searchForm.pageNumber = 1;
|
||||
getData();
|
||||
}
|
||||
|
||||
function handleMemberSearch() {
|
||||
memberSearchForm.pageNumber = 1;
|
||||
getMemberList();
|
||||
}
|
||||
|
||||
function handleMemberReset() {
|
||||
memberSearchForm.id = "";
|
||||
memberSearchForm.nickName = "";
|
||||
memberSearchForm.username = "";
|
||||
memberSearchForm.disabled = "";
|
||||
memberSearchForm.minPoint = null;
|
||||
memberSearchForm.maxPoint = null;
|
||||
memberSearchForm.pageNumber = 1;
|
||||
getMemberList();
|
||||
}
|
||||
|
||||
function changeMemberPage(v) {
|
||||
memberSearchForm.pageNumber = v;
|
||||
getMemberList();
|
||||
}
|
||||
|
||||
function changeMemberPageSize(v) {
|
||||
memberSearchForm.pageNumber = 1;
|
||||
memberSearchForm.pageSize = v;
|
||||
getMemberList();
|
||||
}
|
||||
|
||||
function getMemberList() {
|
||||
memberLoading.value = true;
|
||||
const params = {
|
||||
pageNumber: memberSearchForm.pageNumber,
|
||||
pageSize: memberSearchForm.pageSize,
|
||||
nickName: memberSearchForm.nickName || undefined,
|
||||
username: memberSearchForm.username || undefined,
|
||||
id: memberSearchForm.id ? memberSearchForm.id.trim() : undefined,
|
||||
nickName: memberSearchForm.nickName ? memberSearchForm.nickName.trim() : undefined,
|
||||
username: memberSearchForm.username ? memberSearchForm.username.trim() : undefined,
|
||||
disabled: memberSearchForm.disabled || undefined,
|
||||
minPoint: memberSearchForm.minPoint,
|
||||
maxPoint: memberSearchForm.maxPoint,
|
||||
@@ -318,31 +509,111 @@ function getMemberList() {
|
||||
memberLoading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function getData() {
|
||||
loading.value = true;
|
||||
API_Member.getHistoryPointData(searchForm).then((res) => {
|
||||
loading.value = false;
|
||||
if (res.success) {
|
||||
data.value = res.result.records;
|
||||
total.value = res.result.total;
|
||||
}
|
||||
});
|
||||
const params = {
|
||||
pageNumber: searchForm.pageNumber,
|
||||
pageSize: searchForm.pageSize,
|
||||
memberId: searchForm.memberId ? searchForm.memberId.trim() : undefined,
|
||||
memberName: searchForm.memberName ? searchForm.memberName.trim() : undefined,
|
||||
};
|
||||
API_Member.getHistoryPointData(params)
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
if (res && res.success && res.result) {
|
||||
data.value = res.result.records || [];
|
||||
total.value = res.result.total || 0;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.points-statistics-card {
|
||||
margin-bottom: 10px;
|
||||
.member-point-page {
|
||||
padding: 16px 20px;
|
||||
background-color: #f6f8fa;
|
||||
min-height: calc(100vh - 110px);
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-dark {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
/* 顶部统计卡片 */
|
||||
.points-statistics-card {
|
||||
margin-bottom: 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ebeef5;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.points-statistics {
|
||||
width: 100%;
|
||||
|
||||
.points-statistics-item {
|
||||
padding: 6px 0;
|
||||
|
||||
.stat-inner-box {
|
||||
text-align: center;
|
||||
padding: 12px 16px;
|
||||
background: #fbfbfc;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #f2f3f5;
|
||||
|
||||
.points-statistics-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.points-statistics-subtitle {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 18px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.points-statistics-value {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #f56c6c;
|
||||
line-height: 30px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 选项卡容器 */
|
||||
.point-tabs-wrap {
|
||||
padding: 12px 16px 16px;
|
||||
padding: 14px 18px 18px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(23, 35, 61, 0.04);
|
||||
border: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.point-tabs {
|
||||
@@ -352,107 +623,108 @@ onMounted(() => {
|
||||
|
||||
:deep(.el-tabs__item) {
|
||||
padding: 8px 18px;
|
||||
color: #515a6e;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item.is-active) {
|
||||
color: #409eff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__active-bar) {
|
||||
height: 2px;
|
||||
border-radius: 2px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.points-statistics {
|
||||
width: 100%;
|
||||
}
|
||||
/* 筛选表单 */
|
||||
.point-filter-card {
|
||||
border-radius: 6px;
|
||||
border: 1px solid #ebeef5;
|
||||
background-color: #fafbfe;
|
||||
|
||||
.points-statistics-item {
|
||||
padding: 10px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.points-statistics-title {
|
||||
font-size: 14px;
|
||||
color: #17233d;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.points-statistics-subtitle {
|
||||
font-size: 12px;
|
||||
color: #808695;
|
||||
line-height: 18px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.points-statistics-value {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #fa6419;
|
||||
line-height: 26px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.point-content-card {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.member-list-card {
|
||||
:deep(.el-card__header) {
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #17233d;
|
||||
}
|
||||
|
||||
.member-table {
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-table__header th.el-table__cell) {
|
||||
background: #fafafa;
|
||||
padding: 14px 0;
|
||||
font-size: 14px;
|
||||
color: #17233d;
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px 16px 2px;
|
||||
}
|
||||
|
||||
:deep(.el-table__header .cell) {
|
||||
padding: 0 16px;
|
||||
line-height: 22px;
|
||||
}
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
:deep(.el-table__body td.el-table__cell) {
|
||||
padding: 12px 0;
|
||||
}
|
||||
.el-form-item {
|
||||
margin-bottom: 14px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-table__body .cell) {
|
||||
padding: 0 16px;
|
||||
line-height: 22px;
|
||||
.form-actions {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.point-range-separator {
|
||||
display: inline-block;
|
||||
margin: 0 8px;
|
||||
color: #808695;
|
||||
margin: 0 6px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.link-text {
|
||||
color: #409eff;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
/* 表格区域卡片 */
|
||||
.point-content-card {
|
||||
border-radius: 6px;
|
||||
border: 1px solid #ebeef5;
|
||||
|
||||
.card-header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
|
||||
.total-badge {
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.face {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
/* 表格单元格 */
|
||||
.custom-point-table {
|
||||
.id-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.copy-btn {
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.point-num-text {
|
||||
font-weight: 600;
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.point-change-tag {
|
||||
font-weight: 600;
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
}
|
||||
|
||||
.time-text {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -150,12 +150,11 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import * as API_Order from "@/api/order";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { unitPrice, customRouterPush } from "@/utils/filters";
|
||||
|
||||
const QUERY_KEY = "manager-order-list-query";
|
||||
const PAGE_SIZES = [20, 50, 100];
|
||||
|
||||
const PROMOTION_OPTIONS = [
|
||||
@@ -201,37 +200,11 @@ function emptySearchForm(pageSize = 20) {
|
||||
};
|
||||
}
|
||||
|
||||
function readSavedQuery() {
|
||||
const raw = sessionStorage.getItem(QUERY_KEY);
|
||||
if (!raw) return null;
|
||||
try {
|
||||
const saved = JSON.parse(raw);
|
||||
const form = emptySearchForm();
|
||||
const src = saved && saved.searchForm ? saved.searchForm : {};
|
||||
Object.keys(form).forEach((key) => {
|
||||
if (src[key] !== undefined && src[key] !== null) {
|
||||
form[key] = src[key];
|
||||
}
|
||||
});
|
||||
form.pageNumber = Number(form.pageNumber) || 1;
|
||||
form.pageSize = PAGE_SIZES.includes(Number(form.pageSize)) ? Number(form.pageSize) : 20;
|
||||
const currentStatus = saved.currentStatus || form.orderStatus || "ALL";
|
||||
if (!currentStatus || currentStatus === "ALL") {
|
||||
form.orderStatus = "";
|
||||
}
|
||||
return {
|
||||
searchForm: form,
|
||||
selectDate: Array.isArray(saved.selectDate) ? saved.selectDate : null,
|
||||
currentStatus: currentStatus || "ALL",
|
||||
};
|
||||
} catch (e) {
|
||||
sessionStorage.removeItem(QUERY_KEY);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
defineOptions({ name: "orderList" });
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const searchFormRef = ref(null);
|
||||
const tableRef = ref(null);
|
||||
|
||||
@@ -248,6 +221,25 @@ const total = ref(0);
|
||||
const orderNumData = ref({});
|
||||
const currentStatus = ref("ALL");
|
||||
|
||||
function applyRouteQuery() {
|
||||
const q = route.query;
|
||||
if (!q) return;
|
||||
if (q.orderSn) searchForm.value.orderSn = String(q.orderSn);
|
||||
if (q.buyerName) searchForm.value.buyerName = String(q.buyerName);
|
||||
if (q.goodsName) searchForm.value.goodsName = String(q.goodsName);
|
||||
if (q.storeName) searchForm.value.storeName = String(q.storeName);
|
||||
if (q.orderType) searchForm.value.orderType = String(q.orderType);
|
||||
if (q.orderStatus) {
|
||||
searchForm.value.orderStatus = String(q.orderStatus);
|
||||
currentStatus.value = String(q.orderStatus);
|
||||
}
|
||||
if (q.startDate && q.endDate) {
|
||||
searchForm.value.startDate = String(q.startDate);
|
||||
searchForm.value.endDate = String(q.endDate);
|
||||
selectDate.value = [String(q.startDate), String(q.endDate)];
|
||||
}
|
||||
}
|
||||
|
||||
const orderStatusWithCount = computed(() => {
|
||||
const count = (value) => (value ? `(${value})` : "");
|
||||
return [
|
||||
@@ -340,23 +332,6 @@ function orderStatusTagType(v) {
|
||||
};
|
||||
return map[v] || "info";
|
||||
}
|
||||
function rememberQuery() {
|
||||
sessionStorage.setItem(
|
||||
QUERY_KEY,
|
||||
JSON.stringify({
|
||||
searchForm: searchForm.value,
|
||||
selectDate: selectDate.value,
|
||||
currentStatus: currentStatus.value,
|
||||
})
|
||||
);
|
||||
}
|
||||
function restoreQuery() {
|
||||
const saved = readSavedQuery();
|
||||
if (!saved) return;
|
||||
searchForm.value = saved.searchForm;
|
||||
selectDate.value = saved.selectDate;
|
||||
currentStatus.value = saved.currentStatus;
|
||||
}
|
||||
function init() {
|
||||
getDataList();
|
||||
getOrderNumData();
|
||||
@@ -380,7 +355,6 @@ function handleReset() {
|
||||
searchForm.value = emptySearchForm(pageSize);
|
||||
selectDate.value = null;
|
||||
currentStatus.value = "ALL";
|
||||
sessionStorage.removeItem(QUERY_KEY);
|
||||
init();
|
||||
}
|
||||
function selectDateRange(v) {
|
||||
@@ -403,7 +377,6 @@ function countParams() {
|
||||
}
|
||||
function getDataList() {
|
||||
loading.value = true;
|
||||
rememberQuery();
|
||||
API_Order.getOrderList(searchForm.value)
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
@@ -478,10 +451,24 @@ function getOrderNumData() {
|
||||
});
|
||||
}
|
||||
|
||||
// created
|
||||
restoreQuery();
|
||||
watch(
|
||||
() => route.query,
|
||||
(newQuery) => {
|
||||
if (newQuery && Object.keys(newQuery).length > 0) {
|
||||
applyRouteQuery();
|
||||
searchForm.value.pageNumber = 1;
|
||||
init();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
try {
|
||||
sessionStorage.removeItem("manager-order-list-query");
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
applyRouteQuery();
|
||||
init();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,76 +1,162 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="content-goods-publish">
|
||||
<el-form ref="form" label-width="70px">
|
||||
<div class="base-info-item">
|
||||
<h4>优惠券活动详情</h4>
|
||||
<div class="form-item-view">
|
||||
<el-form-item label="活动名称">
|
||||
<span>{{ couponActivity.promotionName }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="活动类型">
|
||||
<span v-if="couponActivity.couponActivityType === 'REGISTERED'">新人发券</span>
|
||||
<span v-else-if="couponActivity.couponActivityType === 'AUTO_COUPON'">自动发券</span>
|
||||
<span v-else>精确发券</span>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="couponActivity.couponFrequencyEnum != '' && couponActivity.couponFrequencyEnum != null && couponActivity.couponFrequencyEnum != undefined"
|
||||
label="领取频率"
|
||||
>
|
||||
<span v-if="couponActivity.couponFrequencyEnum === 'DAY'">每日</span>
|
||||
<span v-else-if="couponActivity.couponFrequencyEnum === 'WEEK'">每周一次</span>
|
||||
<span v-else>每月一次</span>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="couponActivity.couponActivityType === 'SPECIFY' || couponActivity.couponActivityType === 'AUTO_COUPON'"
|
||||
label="活动范围"
|
||||
>
|
||||
<span v-if="couponActivity.activityScope === 'ALL'">全部会员</span>
|
||||
<span v-else>指定会员</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="活动时间">
|
||||
<span>{{ couponActivity.startTime }}~{{ couponActivity.endTime }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="活动状态">
|
||||
<span v-if="couponActivity.promotionStatus === 'NEW'">未开始</span>
|
||||
<span v-if="couponActivity.promotionStatus === 'START'">已开始</span>
|
||||
<span v-if="couponActivity.promotionStatus === 'END'">已结束</span>
|
||||
<span v-if="couponActivity.promotionStatus === 'CLOSE'">已关闭</span>
|
||||
</el-form-item>
|
||||
<div class="coupon-activity-info-page">
|
||||
<!-- 顶部标题与返回栏 -->
|
||||
<el-card shadow="never" class="header-card">
|
||||
<div class="page-header-row">
|
||||
<div class="header-left">
|
||||
<div class="header-title">
|
||||
<span>优惠券活动详情</span>
|
||||
<el-tag :type="statusTagType(couponActivity.promotionStatus)" size="small" effect="plain" class="status-tag">
|
||||
{{ statusText(couponActivity.promotionStatus) }}
|
||||
</el-tag>
|
||||
<span v-if="id" class="activity-id font-mono">ID: {{ id }}</span>
|
||||
</div>
|
||||
<h4>优惠券列表</h4>
|
||||
<el-table :data="couponData" style="width: 100%">
|
||||
<el-table-column prop="couponName" label="优惠券名称" />
|
||||
<el-table-column label="优惠券金额">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row">
|
||||
<span v-if="row.couponType === 'DISCOUNT'">{{ row.couponDiscount }}折</span>
|
||||
<priceColorScheme v-else-if="row.couponType === 'PRICE'" :value="row.price" :color="$mainColor" />
|
||||
<span v-else>未知</span>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="优惠券类型">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ couponTypeText(row.couponType) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="num" label="赠送数量" />
|
||||
</el-table>
|
||||
<template v-if="couponActivity.activityScopeInfo && memberData.length > 0">
|
||||
<h4 class="mt_10">会员列表列表</h4>
|
||||
<el-table :data="memberData" style="width: 100%">
|
||||
<el-table-column prop="id" label="会员id" />
|
||||
<el-table-column prop="nickName" label="昵称" />
|
||||
</el-table>
|
||||
</template>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-button @click="back">返回活动列表</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<div class="footer">
|
||||
<el-button type="primary" @click="back">返回活动列表</el-button>
|
||||
</div>
|
||||
<!-- 活动基本信息卡片 -->
|
||||
<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="活动名称">
|
||||
<span class="font-semibold text-dark">{{ couponActivity.promotionName || "-" }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="活动类型">
|
||||
<el-tag size="small" effect="light" type="primary">
|
||||
{{ activityTypeText(couponActivity.couponActivityType) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item
|
||||
v-if="couponActivity.couponFrequencyEnum"
|
||||
label="领取频率"
|
||||
>
|
||||
<span>{{ frequencyText(couponActivity.couponFrequencyEnum) }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item
|
||||
v-if="couponActivity.couponActivityType === 'SPECIFY' || couponActivity.couponActivityType === 'AUTO_COUPON'"
|
||||
label="活动范围"
|
||||
>
|
||||
<el-tag size="small" effect="light" :type="couponActivity.activityScope === 'ALL' ? 'success' : 'warning'">
|
||||
{{ couponActivity.activityScope === "ALL" ? "全部会员" : "指定会员" }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="活动时间" :span="2">
|
||||
<span class="font-mono text-dark">
|
||||
{{ couponActivity.startTime || "-" }} 至 {{ couponActivity.endTime || "-" }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="活动状态" :span="2">
|
||||
<el-tag :type="statusTagType(couponActivity.promotionStatus)" size="small" effect="dark">
|
||||
{{ statusText(couponActivity.promotionStatus) }}
|
||||
</el-tag>
|
||||
</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>
|
||||
<span class="section-badge">共 {{ couponData.length }} 张优惠券</span>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
border
|
||||
stripe
|
||||
:data="couponData"
|
||||
style="width: 100%"
|
||||
empty-text="暂无关联优惠券"
|
||||
class="custom-table"
|
||||
>
|
||||
<el-table-column prop="couponName" label="优惠券名称" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span class="font-semibold text-dark">{{ row.couponName || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="优惠券类型" width="130" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row" size="small" effect="light" :type="row.couponType === 'DISCOUNT' ? 'warning' : 'danger'">
|
||||
{{ couponTypeText(row.couponType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="面额 / 折扣" width="140" align="right">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row">
|
||||
<span v-if="row.couponType === 'DISCOUNT'" class="price-val font-mono font-semibold">
|
||||
{{ row.couponDiscount }} 折
|
||||
</span>
|
||||
<span v-else-if="row.couponType === 'PRICE'" class="price-val font-mono font-semibold">
|
||||
¥ {{ Number(row.price || 0).toFixed(2) }}
|
||||
</span>
|
||||
<span v-else class="text-placeholder">-</span>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="num" label="赠送数量" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span class="font-mono text-dark">{{ row.num ?? "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 指定会员列表(若存在) -->
|
||||
<el-card
|
||||
v-if="memberData && memberData.length > 0"
|
||||
shadow="never"
|
||||
class="info-card mt_10"
|
||||
>
|
||||
<div class="section-header">
|
||||
<span class="section-indicator"></span>
|
||||
<span class="section-title">指定参与会员列表</span>
|
||||
<span class="section-badge">共 {{ memberData.length }} 位会员</span>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
border
|
||||
stripe
|
||||
:data="memberData"
|
||||
style="width: 100%"
|
||||
class="custom-table"
|
||||
>
|
||||
<el-table-column prop="id" label="会员ID" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<span class="font-mono text-dark">{{ row.id || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="nickName" label="会员昵称" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span class="font-semibold text-dark">{{ row.nickName || row.username || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<el-card shadow="never" class="footer-card mt_10">
|
||||
<div class="footer-actions">
|
||||
<el-button type="primary" @click="back">返回活动列表</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -96,65 +182,175 @@ onMounted(() => {
|
||||
});
|
||||
|
||||
function couponTypeText(v) {
|
||||
if (v === "DISCOUNT") return "打折";
|
||||
if (v === "PRICE") return "减免现金";
|
||||
return "未知";
|
||||
if (v === "DISCOUNT") return "打折券";
|
||||
if (v === "PRICE") return "减免现金券";
|
||||
return "未知类型";
|
||||
}
|
||||
|
||||
function activityTypeText(t) {
|
||||
if (t === "REGISTERED") return "新人发券";
|
||||
if (t === "AUTO_COUPON") return "自动发券";
|
||||
if (t === "SPECIFY") return "精确发券";
|
||||
return t || "-";
|
||||
}
|
||||
|
||||
function frequencyText(f) {
|
||||
if (f === "DAY") return "每日一次";
|
||||
if (f === "WEEK") return "每周一次";
|
||||
if (f === "MONTH") return "每月一次";
|
||||
return f || "-";
|
||||
}
|
||||
|
||||
function statusText(s) {
|
||||
if (s === "NEW") return "未开始";
|
||||
if (s === "START") return "已开始";
|
||||
if (s === "END") return "已结束";
|
||||
if (s === "CLOSE") return "已关闭";
|
||||
return s || "-";
|
||||
}
|
||||
|
||||
function statusTagType(s) {
|
||||
if (s === "START") return "success";
|
||||
if (s === "NEW") return "info";
|
||||
if (s === "END") return "danger";
|
||||
if (s === "CLOSE") return "warning";
|
||||
return "info";
|
||||
}
|
||||
|
||||
function getCouponActivity() {
|
||||
if (!id) return;
|
||||
apiGetCouponActivity(id).then((res) => {
|
||||
couponActivity.value = res.result;
|
||||
couponData.value = couponActivity.value.couponActivityItems;
|
||||
memberData.value = JSON.parse(couponActivity.value.activityScopeInfo);
|
||||
if (res && res.success && res.result) {
|
||||
couponActivity.value = res.result;
|
||||
couponData.value = Array.isArray(res.result.couponActivityItems)
|
||||
? res.result.couponActivityItems
|
||||
: [];
|
||||
if (res.result.activityScopeInfo) {
|
||||
try {
|
||||
const parsed = JSON.parse(res.result.activityScopeInfo);
|
||||
memberData.value = Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
memberData.value = [];
|
||||
}
|
||||
} else {
|
||||
memberData.value = [];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function back() {
|
||||
appStore.removeTag("coupon-activity-info");
|
||||
appStore.removeTag("coupon-activity");
|
||||
router.go(-1);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.content-goods-publish {
|
||||
padding: 15px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
border: 1px solid #ddd;
|
||||
background: none repeat 0 0 #fff;
|
||||
height: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.coupon-activity-info-page {
|
||||
padding-bottom: 20px;
|
||||
|
||||
div.base-info-item {
|
||||
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;
|
||||
.header-card {
|
||||
.page-header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.header-left {
|
||||
.header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
|
||||
.status-tag {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.activity-id {
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
color: #909399;
|
||||
background-color: #f4f4f5;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-item-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding-left: 40px;
|
||||
.info-card {
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.section-indicator {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
border-radius: 2px;
|
||||
background-color: #409eff;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.section-badge {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-descriptions {
|
||||
:deep(.el-descriptions__label) {
|
||||
width: 140px;
|
||||
background-color: #fafafa;
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.el-descriptions__content) {
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-table {
|
||||
.price-val {
|
||||
color: #f56c6c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-card {
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-dark {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.text-placeholder {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
}
|
||||
.footer {
|
||||
width: 88.7%;
|
||||
padding: 10px;
|
||||
background-color: #ffc;
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
left: 10%;
|
||||
text-align: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,34 +1,82 @@
|
||||
<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">
|
||||
<div class="coupon-publish-page">
|
||||
<!-- 顶部页面标题与操作栏 -->
|
||||
<el-card shadow="never" class="header-card">
|
||||
<div class="page-header-row">
|
||||
<div class="header-left">
|
||||
<div class="header-title">
|
||||
<span>{{ isOnlyView ? "平台优惠券详情" : (id ? "编辑平台优惠券" : "添加平台优惠券") }}</span>
|
||||
<el-tag v-if="isOnlyView" type="info" size="small" effect="plain" class="mode-tag">
|
||||
只读模式
|
||||
</el-tag>
|
||||
<el-tag v-else-if="id" type="warning" size="small" effect="plain" class="mode-tag">
|
||||
编辑模式
|
||||
</el-tag>
|
||||
<el-tag v-else type="success" size="small" effect="plain" class="mode-tag">
|
||||
新建模式
|
||||
</el-tag>
|
||||
<span v-if="id" class="coupon-id-tip font-mono">ID: {{ id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-button @click="closeCurrentPage">返回</el-button>
|
||||
<el-button
|
||||
v-if="!isOnlyView"
|
||||
type="primary"
|
||||
:loading="submitLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存提交
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 表单卡片 -->
|
||||
<el-card shadow="never" class="form-card mt_10">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
label-width="130px"
|
||||
:rules="formRule"
|
||||
class="custom-coupon-form"
|
||||
>
|
||||
<!-- 模块一:基本信息 -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<span class="section-indicator"></span>
|
||||
<span class="section-title">基本信息</span>
|
||||
<span class="section-subtitle">配置活动名称、券面额及发放规则</span>
|
||||
</div>
|
||||
|
||||
<div class="section-body">
|
||||
<el-form-item label="活动名称" prop="promotionName">
|
||||
<el-input
|
||||
:disabled="disabled"
|
||||
v-model="form.promotionName"
|
||||
placeholder="活动名称"
|
||||
placeholder="请输入活动名称"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
style="width: 360px"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="优惠券名称" prop="couponName">
|
||||
<el-input
|
||||
:disabled="disabled"
|
||||
v-model="form.couponName"
|
||||
placeholder="优惠券名称"
|
||||
placeholder="请输入优惠券名称"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
style="width: 360px"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="优惠券类型" prop="couponType">
|
||||
<el-select :disabled="disabled" v-model="form.couponType" style="width: 260px">
|
||||
<el-select :disabled="disabled" v-model="form.couponType" style="width: 240px">
|
||||
<el-option label="打折" value="DISCOUNT" />
|
||||
<el-option label="减免现金" value="PRICE" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
label="折扣"
|
||||
prop="couponDiscount"
|
||||
@@ -42,21 +90,25 @@
|
||||
:step="0.1"
|
||||
:precision="1"
|
||||
v-model="form.couponDiscount"
|
||||
style="width: 260px"
|
||||
style="width: 240px"
|
||||
/>
|
||||
<span class="describe">请输入0-10的数字,可有一位小数</span>
|
||||
<span class="form-describe">请输入 0.1 ~ 9.9 之间的数值(1位小数)</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="面额" prop="price" v-if="form.couponType == 'PRICE'">
|
||||
<el-input
|
||||
:disabled="disabled"
|
||||
v-model="form.price"
|
||||
placeholder="面额"
|
||||
placeholder="请输入减免面额"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
/>
|
||||
style="width: 240px"
|
||||
>
|
||||
<template #prefix>¥</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="活动类型" prop="getType">
|
||||
<el-select :disabled="disabled" v-model="form.getType" style="width: 260px">
|
||||
<el-select :disabled="disabled" v-model="form.getType" style="width: 240px">
|
||||
<el-option label="免费领取" value="FREE" />
|
||||
<el-option label="活动赠送" value="ACTIVITY" />
|
||||
</el-select>
|
||||
@@ -67,21 +119,23 @@
|
||||
:disabled="disabled"
|
||||
v-model="form.storeCommission"
|
||||
placeholder="店铺承担比例"
|
||||
style="width: 260px"
|
||||
style="width: 240px"
|
||||
>
|
||||
<template #append>%</template>
|
||||
</el-input>
|
||||
<span class="describe">店铺承担比例,输入0-100之间数值</span>
|
||||
<span class="form-describe">店铺承担的资金比例,范围 0-100</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="发放数量" prop="publishNum" v-if="form.getType === 'FREE'">
|
||||
<el-input
|
||||
:disabled="disabled"
|
||||
v-model="form.publishNum"
|
||||
placeholder="发放数量"
|
||||
style="width: 260px"
|
||||
placeholder="请输入发放数量"
|
||||
style="width: 240px"
|
||||
/>
|
||||
<div class="tips">如果发放数量为0时,则代表不限制发放数量</div>
|
||||
<span class="form-describe">若设置为 0,则代表不限制发放数量</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
label="领取数量限制"
|
||||
prop="couponLimitNum"
|
||||
@@ -90,43 +144,58 @@
|
||||
<el-input
|
||||
:disabled="disabled"
|
||||
v-model="form.couponLimitNum"
|
||||
placeholder="领取限制"
|
||||
placeholder="每人限领张数"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
style="width: 240px"
|
||||
/>
|
||||
<div class="tips">如果领取数量为0时,则代表不限制领取数量</div>
|
||||
<span class="form-describe">若设置为 0,则代表不限制每人领取数量</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="范围描述" prop="description">
|
||||
<el-input
|
||||
:disabled="disabled"
|
||||
v-model="form.description"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
:rows="3"
|
||||
maxlength="50"
|
||||
show-word-limit
|
||||
clearable
|
||||
style="width: 260px"
|
||||
placeholder="请输入优惠券适用范围或简要说明"
|
||||
style="width: 480px"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<h4>使用限制</h4>
|
||||
<div class="form-item-view">
|
||||
</div>
|
||||
|
||||
<!-- 模块二:使用限制 -->
|
||||
<div class="form-section mt_10">
|
||||
<div class="section-header">
|
||||
<span class="section-indicator"></span>
|
||||
<span class="section-title">使用限制</span>
|
||||
<span class="section-subtitle">配置消费门槛、有效时间以及指定适用商品范围</span>
|
||||
</div>
|
||||
|
||||
<div class="section-body">
|
||||
<el-form-item label="消费门槛" prop="consumeThreshold">
|
||||
<el-input
|
||||
:disabled="disabled"
|
||||
v-model="form.consumeThreshold"
|
||||
placeholder="消费门槛"
|
||||
placeholder="请输入消费门槛金额"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
/>
|
||||
style="width: 240px"
|
||||
>
|
||||
<template #prefix>¥</template>
|
||||
</el-input>
|
||||
<span class="form-describe">订单满此金额方可使用该优惠券</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="有效期" prop="rangeTime">
|
||||
<div v-if="form.getType == 'ACTIVITY'">
|
||||
<div v-if="form.getType == 'ACTIVITY'" class="mb_10">
|
||||
<el-radio-group v-model="rangeTimeType">
|
||||
<el-radio :disabled="disabled" :value="1" v-if="form.getType !== 'ACTIVITY'">起止时间</el-radio>
|
||||
<el-radio :disabled="disabled" :value="0">固定时间</el-radio>
|
||||
<el-radio :disabled="disabled" :value="0">固定有效天数</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<div v-if="rangeTimeType == 1">
|
||||
<el-date-picker
|
||||
:disabled="disabled"
|
||||
@@ -137,101 +206,159 @@
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
:disabled-date="options.disabledDate"
|
||||
style="width: 260px"
|
||||
style="width: 380px"
|
||||
/>
|
||||
</div>
|
||||
<div class="effectiveDays" v-if="rangeTimeType == 0">
|
||||
领取当天开始
|
||||
|
||||
<div class="effectiveDays-row" v-if="rangeTimeType == 0">
|
||||
<span class="days-label">领取当天开始</span>
|
||||
<el-input-number
|
||||
:disabled="disabled"
|
||||
v-model="form.effectiveDays"
|
||||
:min="1"
|
||||
style="width: 100px"
|
||||
:max="365"
|
||||
style="width: 130px"
|
||||
/>
|
||||
天内有效(1-365间的整数)
|
||||
<span class="days-unit">天内有效(1-365天的整数)</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="使用范围" prop="scopeType">
|
||||
<el-radio-group v-model="form.scopeType">
|
||||
<el-radio-button :disabled="disabled" value="ALL">全品类</el-radio-button>
|
||||
<el-radio-button :disabled="disabled" value="ALL">全品类通用</el-radio-button>
|
||||
<el-radio-button :disabled="disabled" value="PORTION_GOODS">指定商品</el-radio-button>
|
||||
<el-radio-button :disabled="disabled" value="PORTION_GOODS_CATEGORY">部分商品分类</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item style="width: 100%" v-if="form.scopeType == 'PORTION_GOODS'">
|
||||
<div style="display: flex; margin-bottom: 10px">
|
||||
<el-button :disabled="disabled" type="primary" @click="openSkuList">选择商品</el-button>
|
||||
<el-button
|
||||
:disabled="disabled"
|
||||
type="danger"
|
||||
plain
|
||||
style="margin-left: 10px"
|
||||
@click="delSelectGoods"
|
||||
>批量删除</el-button>
|
||||
<!-- 指定适用商品表格 -->
|
||||
<el-form-item
|
||||
v-if="form.scopeType == 'PORTION_GOODS'"
|
||||
label="指定商品"
|
||||
style="width: 100%"
|
||||
>
|
||||
<div class="portion-goods-wrapper">
|
||||
<div v-if="!isOnlyView" class="portion-goods-toolbar">
|
||||
<el-button type="primary" @click="openSkuList">选择商品</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
:disabled="selectedGoods.length === 0"
|
||||
@click="delSelectGoods"
|
||||
>
|
||||
批量删除 ({{ selectedGoods.length }})
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
border
|
||||
stripe
|
||||
:data="form.promotionGoodsList"
|
||||
style="width: 100%"
|
||||
empty-text="暂无指定商品数据"
|
||||
class="portion-goods-table"
|
||||
@selection-change="changeSelect"
|
||||
>
|
||||
<el-table-column
|
||||
v-if="!isOnlyView"
|
||||
type="selection"
|
||||
width="50"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column label="商品图片" width="76" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
v-if="row && row.thumbnail"
|
||||
:src="row.thumbnail"
|
||||
fit="cover"
|
||||
class="table-goods-thumb"
|
||||
preview-teleported
|
||||
:preview-src-list="[row.thumbnail]"
|
||||
>
|
||||
<template #error>
|
||||
<div class="thumb-slot">无图</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div v-else class="table-goods-thumb thumb-slot">无图</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="goodsName"
|
||||
label="商品名称"
|
||||
min-width="220"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="商品单价" width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" class="price-text font-mono font-semibold">
|
||||
{{ $filters.unitPrice(row.price, "¥") }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="库存" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" class="font-mono">{{ row.quantity ?? "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="!isOnlyView"
|
||||
label="操作"
|
||||
width="90"
|
||||
align="center"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="{ $index }">
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
link
|
||||
@click="delGoods($index)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<el-table
|
||||
border
|
||||
:data="form.promotionGoodsList"
|
||||
style="width: 100%"
|
||||
@selection-change="changeSelect"
|
||||
>
|
||||
<el-table-column type="selection" width="60" align="center" />
|
||||
<el-table-column prop="goodsName" label="商品名称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="商品价格" width="110">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" :style="{ color: $mainColor }">
|
||||
{{ $filters.unitPrice(row.price, "¥") }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="库存" width="90">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.quantity }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-button
|
||||
:disabled="disabled"
|
||||
type="danger"
|
||||
size="small"
|
||||
plain
|
||||
@click="delGoods($index)"
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="form.scopeType == 'PORTION_GOODS_CATEGORY'">
|
||||
<!-- 部分商品分类 -->
|
||||
<el-form-item v-if="form.scopeType == 'PORTION_GOODS_CATEGORY'" label="指定分类">
|
||||
<el-cascader
|
||||
:disabled="disabled"
|
||||
:options="goodsCategoryList"
|
||||
style="width: 260px"
|
||||
placeholder="请选择适用的商品分类"
|
||||
style="width: 360px"
|
||||
v-model="form.scopeIdGoods"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<div>
|
||||
<el-button :disabled="disabled" link @click="closeCurrentPage">返回</el-button>
|
||||
<el-button
|
||||
:disabled="disabled"
|
||||
type="primary"
|
||||
:loading="submitLoading"
|
||||
@click="handleSubmit"
|
||||
>提交</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 底部固定操作栏 -->
|
||||
<el-card shadow="never" class="footer-card mt_10">
|
||||
<div class="footer-actions">
|
||||
<el-button @click="closeCurrentPage">返回列表</el-button>
|
||||
<el-button
|
||||
v-if="!isOnlyView"
|
||||
type="primary"
|
||||
:loading="submitLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存提交
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<sku-select ref="skuSelectRef" @selectedGoodsData="selectedGoodsData"></sku-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref, watch } from "vue";
|
||||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import {
|
||||
savePlatformCoupon,
|
||||
@@ -277,7 +404,9 @@ const checkWeight = (rule, value, callback) => {
|
||||
}
|
||||
};
|
||||
|
||||
const disabled = ref(route.query.onlyView);
|
||||
const isOnlyView = computed(() => route.query.onlyView === "true" || route.query.onlyView === true);
|
||||
const disabled = computed(() => isOnlyView.value);
|
||||
|
||||
const rangeTimeType = ref(1);
|
||||
const modalType = ref(0);
|
||||
const form = ref({
|
||||
@@ -298,6 +427,7 @@ const id = ref(route.query.id);
|
||||
const submitLoading = ref(false);
|
||||
const selectedGoods = ref([]);
|
||||
const goodsCategoryList = ref([]);
|
||||
|
||||
const formRule = reactive({
|
||||
promotionName: [{ required: true, message: "活动名称不能为空" }],
|
||||
couponName: [{ required: true, message: "优惠券名称不能为空" }],
|
||||
@@ -328,6 +458,7 @@ const formRule = reactive({
|
||||
],
|
||||
description: [{ required: true, message: "请输入范围描述" }],
|
||||
});
|
||||
|
||||
const options = {
|
||||
disabledDate(date) {
|
||||
return date && date.valueOf() < Date.now() - 86400000;
|
||||
@@ -348,6 +479,7 @@ watch(
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => route.query,
|
||||
(e) => {
|
||||
@@ -355,7 +487,7 @@ watch(
|
||||
if (id.value) {
|
||||
getCoupon();
|
||||
} else {
|
||||
formRef.value.resetFields();
|
||||
formRef.value?.resetFields();
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -374,7 +506,7 @@ function getCoupon() {
|
||||
if (!data.promotionGoodsList) data.promotionGoodsList = [];
|
||||
rangeTimeType.value = data.rangeDayType === "DYNAMICTIME" ? 0 : 1;
|
||||
if (data.scopeType == "PORTION_GOODS_CATEGORY") {
|
||||
let prevCascader = data.scopeId.split(",");
|
||||
let prevCascader = data.scopeId ? data.scopeId.split(",") : [];
|
||||
function next(params, prev) {
|
||||
for (let i = 0; i < params.length; i++) {
|
||||
const item = params[i];
|
||||
@@ -407,6 +539,7 @@ function getCoupon() {
|
||||
form.value = data;
|
||||
});
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
@@ -465,7 +598,7 @@ function handleSubmit() {
|
||||
savePlatformCoupon(params).then((res) => {
|
||||
submitLoading.value = false;
|
||||
if (res.success) {
|
||||
Message.success("优惠券发送成功");
|
||||
Message.success("优惠券创建成功");
|
||||
closeCurrentPage();
|
||||
}
|
||||
});
|
||||
@@ -484,11 +617,14 @@ function handleSubmit() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeCurrentPage() {
|
||||
appStore.removeTag("edit-platform-coupon");
|
||||
appStore.removeTag("add-platform-coupon");
|
||||
localStorage.pageOpenedList = JSON.stringify(appStore.pageOpenedList);
|
||||
router.go(-1);
|
||||
}
|
||||
|
||||
function openSkuList() {
|
||||
skuSelectRef.value.open("goods");
|
||||
let data = JSON.parse(JSON.stringify(form.value.promotionGoodsList));
|
||||
@@ -497,9 +633,11 @@ function openSkuList() {
|
||||
});
|
||||
skuSelectRef.value.goodsData = data;
|
||||
}
|
||||
|
||||
function changeSelect(e) {
|
||||
selectedGoods.value = e;
|
||||
}
|
||||
|
||||
function delSelectGoods() {
|
||||
if (selectedGoods.value.length <= 0) {
|
||||
Message.warning("您还未选择要删除的数据");
|
||||
@@ -520,9 +658,11 @@ function delSelectGoods() {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function delGoods(index) {
|
||||
form.value.promotionGoodsList.splice(index, 1);
|
||||
}
|
||||
|
||||
function selectedGoodsData(item) {
|
||||
let list = [];
|
||||
item.forEach((e) => {
|
||||
@@ -535,7 +675,7 @@ function selectedGoodsData(item) {
|
||||
storeName: e.storeName,
|
||||
skuId: e.id,
|
||||
categoryPath: e.categoryPath,
|
||||
thumbnail: e.small,
|
||||
thumbnail: e.thumbnail || e.small,
|
||||
goodsType: e.goodsType,
|
||||
goodsId: e.goodsId,
|
||||
originPrice: e.price,
|
||||
@@ -543,6 +683,7 @@ function selectedGoodsData(item) {
|
||||
});
|
||||
form.value.promotionGoodsList = list;
|
||||
}
|
||||
|
||||
async function getCagetoryList() {
|
||||
let data = await getCategoryTree();
|
||||
goodsCategoryList.value = data.result;
|
||||
@@ -572,6 +713,7 @@ async function getCagetoryList() {
|
||||
return { value: item.id, label: item.name, children: item.children };
|
||||
});
|
||||
}
|
||||
|
||||
function filterCategoryId(list, idArr) {
|
||||
list.forEach((e) => {
|
||||
if (e instanceof Array) {
|
||||
@@ -584,36 +726,161 @@ function filterCategoryId(list, idArr) {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scpoed>
|
||||
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;
|
||||
}
|
||||
<style lang="scss" scoped>
|
||||
.coupon-publish-page {
|
||||
padding-bottom: 20px;
|
||||
|
||||
.describe {
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
color: #999;
|
||||
}
|
||||
.header-card {
|
||||
.page-header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.effectiveDays {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
.header-left {
|
||||
.header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
|
||||
> * {
|
||||
margin: 0 4px;
|
||||
.mode-tag {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.coupon-id-tip {
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
color: #909399;
|
||||
background-color: #f4f4f5;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-card {
|
||||
.custom-coupon-form {
|
||||
padding: 10px 10px 0;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 6px;
|
||||
padding: 18px 20px;
|
||||
background-color: #ffffff;
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-bottom: 14px;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
|
||||
.section-indicator {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
border-radius: 2px;
|
||||
background-color: #409eff;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.section-body {
|
||||
max-width: 900px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-describe {
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.effectiveDays-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.days-label,
|
||||
.days-unit {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
}
|
||||
|
||||
.portion-goods-wrapper {
|
||||
width: 100%;
|
||||
|
||||
.portion-goods-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.portion-goods-table {
|
||||
.table-goods-thumb {
|
||||
display: block;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
margin: 0 auto;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.thumb-slot {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 11px;
|
||||
color: #c0c4cc;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.price-text {
|
||||
color: #f56c6c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-card {
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
.tips {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-left: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,237 +1,223 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card>
|
||||
<el-form ref="formRef" :model="form" label-width="120px">
|
||||
<div class="base-info-item">
|
||||
<h4>基本信息</h4>
|
||||
<div class="form-item-view">
|
||||
<el-form-item label="活动名称" prop="promotionName">
|
||||
<el-input
|
||||
v-model="form.promotionName"
|
||||
disabled
|
||||
placeholder="活动名称"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="活动时间" prop="rangeTime">
|
||||
<el-date-picker
|
||||
type="datetimerange"
|
||||
v-model="form.rangeTime"
|
||||
disabled
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
placeholder="请选择"
|
||||
:disabled-date="options.disabledDate"
|
||||
style="width: 320px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="活动描述" prop="description">
|
||||
<el-input
|
||||
v-model="form.description"
|
||||
disabled
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<h4>优惠设置</h4>
|
||||
<div class="form-item-view">
|
||||
<el-form-item label="优惠门槛" prop="fullMoney">
|
||||
<el-input
|
||||
v-model="form.fullMoney"
|
||||
disabled
|
||||
placeholder="优惠门槛"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
/>
|
||||
<span class="describe">消费达到当前金额可以参与优惠</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="赠送优惠券">
|
||||
<el-radio-group v-model="form.discountType">
|
||||
<el-radio-button value="fullMinusFlag" disabled>减现金</el-radio-button>
|
||||
<el-radio-button value="fullRateFlag" disabled>打折</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="form.discountType == 'fullMinusFlag'"
|
||||
label="优惠金额"
|
||||
prop="fullMinus"
|
||||
>
|
||||
<el-input
|
||||
disabled
|
||||
v-model="form.fullMinus"
|
||||
placeholder="优惠金额"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="form.discountType == 'fullRateFlag'"
|
||||
label="优惠折扣"
|
||||
prop="fullRate"
|
||||
>
|
||||
<el-input-number
|
||||
placeholder="优惠折扣"
|
||||
:max="9.9"
|
||||
:min="0.1"
|
||||
:step="0.1"
|
||||
:precision="1"
|
||||
v-model="form.fullRate"
|
||||
style="width: 260px"
|
||||
/>
|
||||
<span class="describe">优惠折扣为0-10之间数字,可有一位小数</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="额外赠送">
|
||||
<el-checkbox v-model="form.freeFreightFlag" disabled>免邮费</el-checkbox>
|
||||
<el-checkbox v-model="form.couponFlag" disabled>送优惠券</el-checkbox>
|
||||
<el-checkbox v-model="form.giftFlag" disabled>送赠品</el-checkbox>
|
||||
<el-checkbox v-model="form.pointFlag" disabled>送积分</el-checkbox>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.couponFlag" label="赠送优惠券" prop="couponId">
|
||||
<el-select
|
||||
v-model="form.couponId"
|
||||
:disabled="form.promotionStatus != 'NEW'"
|
||||
filterable
|
||||
remote
|
||||
:remote-method="getCouponList"
|
||||
placeholder="输入优惠券名称搜索"
|
||||
:loading="couponLoading"
|
||||
style="width: 280px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in couponList"
|
||||
:value="item.id"
|
||||
:key="item.id"
|
||||
:label="item.couponName"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.giftFlag" label="赠品" prop="giftId">
|
||||
<el-select
|
||||
v-model="form.giftId"
|
||||
filterable
|
||||
remote
|
||||
:remote-method="getGiftList"
|
||||
placeholder="输入赠品名称搜索"
|
||||
disabled
|
||||
:loading="giftLoading"
|
||||
style="width: 260px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in giftList"
|
||||
:value="item.id"
|
||||
:key="item.id"
|
||||
:label="item.goodsName"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.pointFlag" label="赠积分" prop="point">
|
||||
<el-input
|
||||
v-model="form.point"
|
||||
type="number"
|
||||
disabled
|
||||
style="width: 260px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="使用范围" prop="scopeType">
|
||||
<el-radio-group v-model="form.scopeType">
|
||||
<el-radio-button value="ALL" disabled>全品类</el-radio-button>
|
||||
<el-radio-button value="PORTION_GOODS" disabled>指定商品</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
style="width: 100%"
|
||||
v-if="form.scopeType == 'PORTION_GOODS'"
|
||||
>
|
||||
<el-table border :data="form.promotionGoodsList" style="width: 100%">
|
||||
<el-table-column type="selection" width="60" align="center" />
|
||||
<el-table-column label="商品名称" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<template v-if="row">
|
||||
<a class="link-text mr_10" @click="linkTo(row.goodsId, row.skuId)">{{
|
||||
row.goodsName
|
||||
}}</a>
|
||||
<el-popover trigger="hover" title="扫码在手机中查看" placement="top" width="180">
|
||||
<template #reference>
|
||||
<img
|
||||
src="../../../assets/qrcode.svg"
|
||||
style="vertical-align: middle"
|
||||
class="hover-pointer"
|
||||
width="20"
|
||||
height="20"
|
||||
alt=""
|
||||
/>
|
||||
</template>
|
||||
<vue-qr
|
||||
:text="wapLinkTo(row.goodsId, row.skuId)"
|
||||
:margin="0"
|
||||
color-dark="#000"
|
||||
color-light="#fff"
|
||||
:size="150"
|
||||
/>
|
||||
</el-popover>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商品价格" width="110">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" :style="{ color: $mainColor }">
|
||||
{{ $filters.unitPrice(row.price, "¥") }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="库存" width="90">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.quantity }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-form-item>
|
||||
|
||||
<div>
|
||||
<el-button @click="$router.push({ name: 'promotions/full-discount' })">返回</el-button>
|
||||
</div>
|
||||
<div class="full-discount-detail-page">
|
||||
<!-- 顶部标题与返回栏 -->
|
||||
<el-card shadow="never" class="header-card">
|
||||
<div class="page-header-row">
|
||||
<div class="header-left">
|
||||
<div class="header-title">
|
||||
<span>满优惠活动详情</span>
|
||||
<el-tag type="info" size="small" effect="plain" class="mode-tag">
|
||||
只读模式
|
||||
</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="back">返回</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="活动名称">
|
||||
<span class="font-semibold text-dark">{{ form.promotionName || "-" }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="活动范围">
|
||||
<el-tag size="small" effect="light" :type="form.scopeType === 'ALL' ? 'success' : 'primary'">
|
||||
{{ form.scopeType === "ALL" ? "全品类通用" : "指定商品适用" }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="活动时间" :span="2">
|
||||
<span class="font-mono text-dark">
|
||||
{{ form.startTime || "-" }} 至 {{ form.endTime || "-" }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="活动描述" :span="2">
|
||||
<span class="text-secondary">{{ form.description || "暂无活动描述" }}</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>
|
||||
|
||||
<el-descriptions :column="2" border class="custom-descriptions">
|
||||
<el-descriptions-item label="优惠门槛">
|
||||
<span class="money-highlight font-mono font-semibold">
|
||||
满 ¥ {{ Number(form.fullMoney || 0).toFixed(2) }}
|
||||
</span>
|
||||
<span class="text-secondary ml_10" style="font-size: 12px">(达到当前金额可享优惠)</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="优惠形式">
|
||||
<template v-if="form.discountType === 'fullMinusFlag'">
|
||||
<el-tag size="small" effect="light" type="danger">减现金</el-tag>
|
||||
<span class="money-highlight font-mono font-semibold ml_10">
|
||||
减 ¥ {{ Number(form.fullMinus || 0).toFixed(2) }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="form.discountType === 'fullRateFlag'">
|
||||
<el-tag size="small" effect="light" type="warning">打折</el-tag>
|
||||
<span class="money-highlight font-mono font-semibold ml_10">
|
||||
打 {{ form.fullRate }} 折
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="text-secondary">无直接立减/打折</span>
|
||||
</template>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="附加赠送权益" :span="2">
|
||||
<div class="extra-rights-row">
|
||||
<el-tag :type="form.freeFreightFlag ? 'success' : 'info'" effect="plain" size="small">
|
||||
{{ form.freeFreightFlag ? "✓ 包邮(免运费)" : "不包邮" }}
|
||||
</el-tag>
|
||||
<el-tag :type="form.couponFlag ? 'success' : 'info'" effect="plain" size="small">
|
||||
{{ form.couponFlag ? "✓ 赠送优惠券" : "不送券" }}
|
||||
</el-tag>
|
||||
<el-tag :type="form.giftFlag ? 'success' : 'info'" effect="plain" size="small">
|
||||
{{ form.giftFlag ? "✓ 赠送赠品" : "不送赠品" }}
|
||||
</el-tag>
|
||||
<el-tag :type="form.pointFlag ? 'success' : 'info'" effect="plain" size="small">
|
||||
{{ form.pointFlag ? "✓ 赠送积分" : "不送积分" }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item v-if="form.couponFlag" label="赠送优惠券" :span="2">
|
||||
<span class="font-semibold text-dark">{{ couponNameText }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item v-if="form.giftFlag" label="赠品信息" :span="2">
|
||||
<span class="font-semibold text-dark">{{ giftNameText }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item v-if="form.pointFlag" label="赠送积分数" :span="2">
|
||||
<span class="font-mono font-semibold text-dark">{{ form.point ?? 0 }} 积分</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<!-- 指定参与商品列表(当指定商品时展示) -->
|
||||
<el-card
|
||||
v-if="form.scopeType === 'PORTION_GOODS'"
|
||||
shadow="never"
|
||||
class="info-card mt_10"
|
||||
>
|
||||
<div class="section-header">
|
||||
<span class="section-indicator"></span>
|
||||
<span class="section-title">指定适用商品列表</span>
|
||||
<span class="section-badge">共 {{ (form.promotionGoodsList || []).length }} 件商品</span>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
border
|
||||
stripe
|
||||
:data="form.promotionGoodsList || []"
|
||||
style="width: 100%"
|
||||
empty-text="暂无指定商品数据"
|
||||
class="custom-table"
|
||||
>
|
||||
<el-table-column label="商品图片" width="76" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
v-if="row && (row.thumbnail || row.small)"
|
||||
:src="row.thumbnail || row.small"
|
||||
fit="cover"
|
||||
class="table-goods-thumb"
|
||||
preview-teleported
|
||||
:preview-src-list="[row.thumbnail || row.small]"
|
||||
>
|
||||
<template #error>
|
||||
<div class="thumb-slot">无图</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div v-else class="table-goods-thumb thumb-slot">无图</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="商品名称" min-width="240" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<template v-if="row">
|
||||
<span class="goods-title font-semibold text-dark">{{ row.goodsName || "-" }}</span>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="商品单价" width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" class="price-val font-mono font-semibold">
|
||||
{{ $filters.unitPrice(row.price, "¥") }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="库存" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" class="font-mono text-dark">{{ row.quantity ?? "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<el-card shadow="never" class="footer-card mt_10">
|
||||
<div class="footer-actions">
|
||||
<el-button type="primary" @click="back">返回活动列表</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { getPlatformCouponList, getFullDiscountById } from "@/api/promotion";
|
||||
import { getGoodsSkuData } from "@/api/goods";
|
||||
import vueQr from "vue-qr";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
|
||||
defineOptions({ name: "add-full-discount" });
|
||||
defineOptions({ name: "full-discount-detail" });
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const formRef = ref(null);
|
||||
const id = ref(route.query.id);
|
||||
const form = ref({
|
||||
discountType: "fullMinusFlag",
|
||||
scopeType: "ALL",
|
||||
promotionGoodsList: [],
|
||||
});
|
||||
const id = ref(route.query.id);
|
||||
const couponList = ref([]);
|
||||
const giftList = ref([]);
|
||||
const giftLoading = ref(false);
|
||||
const couponLoading = ref(false);
|
||||
const options = {
|
||||
disabledDate(date) {
|
||||
return date && date.valueOf() < Date.now() - 86400000;
|
||||
},
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
const couponNameText = computed(() => {
|
||||
if (!form.value.couponId) return "未指定优惠券";
|
||||
const found = couponList.value.find((c) => String(c.id) === String(form.value.couponId));
|
||||
return found ? found.couponName : `优惠券 ID: ${form.value.couponId}`;
|
||||
});
|
||||
|
||||
const giftNameText = computed(() => {
|
||||
if (!form.value.giftId) return "未指定赠品";
|
||||
const found = giftList.value.find((g) => String(g.id) === String(form.value.giftId));
|
||||
return found ? found.goodsName : `赠品 ID: ${form.value.giftId}`;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (id.value) {
|
||||
getDetail();
|
||||
}
|
||||
@@ -241,79 +227,194 @@ onMounted(async () => {
|
||||
|
||||
function getDetail() {
|
||||
getFullDiscountById(id.value).then((res) => {
|
||||
let data = res.result;
|
||||
if (!data.scopeType === "ALL") {
|
||||
data.promotionGoodsList = [];
|
||||
if (res && res.success && res.result) {
|
||||
let data = res.result;
|
||||
if (data.scopeType !== "PORTION_GOODS") {
|
||||
data.promotionGoodsList = [];
|
||||
}
|
||||
if (data.fullRateFlag) {
|
||||
data.discountType = "fullRateFlag";
|
||||
} else {
|
||||
data.discountType = "fullMinusFlag";
|
||||
}
|
||||
form.value = data;
|
||||
}
|
||||
if (data.fullMinusFlag) {
|
||||
data.discountType = "fullMinusFlag";
|
||||
delete data.fullMinusFlag;
|
||||
} else {
|
||||
data.discountType = "fullMinusFlag";
|
||||
delete data.fullRateFlag;
|
||||
}
|
||||
data.rangeTime = [];
|
||||
data.rangeTime.push(new Date(data.startTime), new Date(data.endTime));
|
||||
|
||||
form.value = data;
|
||||
});
|
||||
}
|
||||
function getCouponList(query) {
|
||||
let params = {
|
||||
pageSize: 20,
|
||||
|
||||
function getCouponList() {
|
||||
const params = {
|
||||
pageSize: 50,
|
||||
pageNumber: 1,
|
||||
getType: "ACTIVITY",
|
||||
storeId: "",
|
||||
couponName: query,
|
||||
promotionStatus: "START",
|
||||
};
|
||||
couponLoading.value = true;
|
||||
getPlatformCouponList(params).then((res) => {
|
||||
couponLoading.value = false;
|
||||
if (res.success) {
|
||||
couponList.value = res.result.records;
|
||||
if (res && res.success && res.result) {
|
||||
couponList.value = res.result.records || [];
|
||||
}
|
||||
});
|
||||
}
|
||||
function getGiftList(query) {
|
||||
let params = {
|
||||
pageSize: 20,
|
||||
|
||||
function getGiftList() {
|
||||
const params = {
|
||||
pageSize: 50,
|
||||
pageNumber: 1,
|
||||
id: query === form.value.giftId ? form.value.giftId : null,
|
||||
goodsName: query === form.value.giftId ? null : query,
|
||||
marketEnable: "UPPER",
|
||||
authFlag: "PASS"
|
||||
authFlag: "PASS",
|
||||
};
|
||||
giftLoading.value = true;
|
||||
getGoodsSkuData(params).then((res) => {
|
||||
giftLoading.value = false;
|
||||
if (res.success) {
|
||||
giftList.value = res.result.records;
|
||||
if (res && res.success && res.result) {
|
||||
giftList.value = res.result.records || [];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function back() {
|
||||
appStore.removeTag("full-discount-detail");
|
||||
router.go(-1);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
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;
|
||||
}
|
||||
.describe {
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
color: #999;
|
||||
}
|
||||
.link-text {
|
||||
color: #409eff;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
.full-discount-detail-page {
|
||||
padding-bottom: 20px;
|
||||
|
||||
.header-card {
|
||||
.page-header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.header-left {
|
||||
.header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
|
||||
.mode-tag {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.activity-id {
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
color: #909399;
|
||||
background-color: #f4f4f5;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-card {
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.section-indicator {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
border-radius: 2px;
|
||||
background-color: #409eff;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.section-badge {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-descriptions {
|
||||
:deep(.el-descriptions__label) {
|
||||
width: 140px;
|
||||
background-color: #fafafa;
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.el-descriptions__content) {
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.money-highlight {
|
||||
color: #f56c6c;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.extra-rights-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.custom-table {
|
||||
.table-goods-thumb {
|
||||
display: block;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
margin: 0 auto;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.thumb-slot {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 11px;
|
||||
color: #c0c4cc;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.price-val {
|
||||
color: #f56c6c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-card {
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-dark {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.ml_10 {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,103 +1,196 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card v-loading="detailLoading">
|
||||
<el-form ref="formRef" :model="form" label-width="130px" :rules="onlyView ? {} : formRule">
|
||||
<div class="base-info-item">
|
||||
<h4>{{ pageTitle }}</h4>
|
||||
<div class="form-item-view gift-card-activity-form">
|
||||
<el-form-item label="礼品卡名称" prop="giftCardName">
|
||||
<el-input
|
||||
v-model="form.giftCardName"
|
||||
type="text"
|
||||
placeholder="请输入礼品卡名称"
|
||||
clearable
|
||||
maxlength="64"
|
||||
show-word-limit
|
||||
style="width: 360px"
|
||||
:disabled="onlyView"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="礼品卡类型" prop="cardType" class="gift-card-type-form-item">
|
||||
<el-radio-group v-model="form.cardType" class="gift-card-type-group">
|
||||
<el-radio value="CASH" :disabled="onlyView">现金卡</el-radio>
|
||||
<el-radio value="PICKUP" disabled>提货卡</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="面值" prop="faceValue">
|
||||
<el-input
|
||||
v-model="form.faceValue"
|
||||
type="text"
|
||||
placeholder="面值"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
:disabled="onlyView"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="总库存" prop="stockTotal">
|
||||
<el-input-number
|
||||
v-model="form.stockTotal"
|
||||
:min="1"
|
||||
:precision="0"
|
||||
placeholder="最多可制发凭证张数"
|
||||
style="width: 200px"
|
||||
:disabled="onlyView"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="validityType" :label-width="0" class="gift-card-validity-form-item">
|
||||
<div class="gift-card-validity-outer">
|
||||
<div class="gift-card-validity-label-col">
|
||||
<span v-if="!onlyView" class="gift-card-validity-required">*</span>
|
||||
<span>有效期</span>
|
||||
</div>
|
||||
<div class="gift-card-validity-radios">
|
||||
<el-radio-group v-model="form.validityType" class="gift-card-validity-group">
|
||||
<el-radio value="LONG_TERM" class="gift-card-validity-row" :disabled="onlyView"
|
||||
>长期有效</el-radio
|
||||
>
|
||||
<el-radio value="AFTER_ACTIVATE_MONTHS" class="gift-card-validity-row" :disabled="onlyView">
|
||||
<span>自激活后</span>
|
||||
<el-input-number
|
||||
v-model="form.validMonthsAfterActivate"
|
||||
:min="1"
|
||||
:max="99"
|
||||
:precision="0"
|
||||
placeholder="请输入1-99的整数"
|
||||
:disabled="onlyView || form.validityType !== 'AFTER_ACTIVATE_MONTHS'"
|
||||
class="gift-card-validity-input-gap"
|
||||
/>
|
||||
<span>月内有效</span>
|
||||
</el-radio>
|
||||
<el-radio value="FIXED_UNTIL" class="gift-card-validity-row" :disabled="onlyView">
|
||||
<span>指定有效期至</span>
|
||||
<el-date-picker
|
||||
v-model="form.validEndTime"
|
||||
type="datetime"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="选择结束时间"
|
||||
clearable
|
||||
:disabled="onlyView || form.validityType !== 'FIXED_UNTIL'"
|
||||
class="gift-card-validity-date-gap"
|
||||
style="width: 220px"
|
||||
/>
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="foot-btn">
|
||||
<el-button style="margin-right: 8px" @click="closeCurrentPage">返回</el-button>
|
||||
<el-button
|
||||
v-if="!onlyView"
|
||||
type="primary"
|
||||
:loading="submitLoading"
|
||||
@click="handleSubmit"
|
||||
>提交</el-button
|
||||
<div class="gift-card-activity-page">
|
||||
<!-- 顶部标题与返回栏 -->
|
||||
<el-card shadow="never" class="header-card">
|
||||
<div class="page-header-row">
|
||||
<div class="header-left">
|
||||
<div class="header-title">
|
||||
<span>{{ pageTitle }}</span>
|
||||
<el-tag
|
||||
size="small"
|
||||
effect="plain"
|
||||
:type="onlyView ? 'info' : (isEditMode ? 'warning' : 'primary')"
|
||||
class="mode-tag"
|
||||
>
|
||||
{{ onlyView ? "查看模式" : (isEditMode ? "编辑模式" : "新建模式") }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-if="form.cardType"
|
||||
size="small"
|
||||
effect="dark"
|
||||
type="primary"
|
||||
class="type-tag"
|
||||
>
|
||||
{{ form.cardType === "CASH" ? "现金卡" : "提货卡" }}
|
||||
</el-tag>
|
||||
<span v-if="route.query.id" class="activity-id font-mono">
|
||||
ID: {{ route.query.id }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
<div class="header-right">
|
||||
<el-button @click="closeCurrentPage">返回礼品卡列表</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 礼品卡基础信息卡片 -->
|
||||
<el-card shadow="never" v-loading="detailLoading" 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="font-semibold text-dark">{{ form.giftCardName || "-" }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="礼品卡类型">
|
||||
<el-tag size="small" effect="plain" type="primary">
|
||||
{{ form.cardType === "CASH" ? "现金卡" : "提货卡" }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="礼品卡面值">
|
||||
<span class="price-highlight font-mono font-semibold">
|
||||
{{ form.faceValue ? $filters.unitPrice(form.faceValue, "¥") : "-" }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="总库存">
|
||||
<span class="font-mono text-dark font-semibold">
|
||||
{{ form.stockTotal != null ? `${form.stockTotal} 张` : "-" }}
|
||||
</span>
|
||||
<span class="hint-inline">(最多可制发凭证张数)</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="有效期限" :span="2">
|
||||
<span class="font-mono text-dark">{{ validityDisplayText }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item v-if="activityDetail.createTime" label="创建时间">
|
||||
<span class="font-mono text-secondary">{{ activityDetail.createTime }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item v-if="route.query.id" label="活动ID">
|
||||
<span class="font-mono text-secondary">{{ route.query.id }}</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="giftCardName">
|
||||
<el-input
|
||||
v-model="form.giftCardName"
|
||||
type="text"
|
||||
placeholder="请输入礼品卡名称"
|
||||
clearable
|
||||
maxlength="64"
|
||||
show-word-limit
|
||||
style="width: 380px"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="礼品卡类型" prop="cardType">
|
||||
<el-radio-group v-model="form.cardType">
|
||||
<el-radio value="CASH">现金卡</el-radio>
|
||||
<el-radio value="PICKUP" disabled>提货卡(暂未开放)</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="面值" prop="faceValue">
|
||||
<el-input
|
||||
v-model="form.faceValue"
|
||||
type="number"
|
||||
placeholder="请填写面值金额"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
>
|
||||
<template #append>元</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="总库存" prop="stockTotal">
|
||||
<el-input-number
|
||||
v-model="form.stockTotal"
|
||||
:min="1"
|
||||
:precision="0"
|
||||
placeholder="最多可制发凭证张数"
|
||||
style="width: 220px"
|
||||
/>
|
||||
<span class="input-tip-text">张(最多可制发凭证张数)</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="有效期限" required class="validity-form-item">
|
||||
<el-radio-group v-model="form.validityType" class="validity-radio-group">
|
||||
<div class="validity-row">
|
||||
<el-radio value="LONG_TERM">长期有效</el-radio>
|
||||
</div>
|
||||
|
||||
<div class="validity-row">
|
||||
<el-radio value="AFTER_ACTIVATE_MONTHS">
|
||||
<span class="radio-inline-text">自激活后</span>
|
||||
<el-input-number
|
||||
v-model="form.validMonthsAfterActivate"
|
||||
:min="1"
|
||||
:max="99"
|
||||
:precision="0"
|
||||
placeholder="1-99"
|
||||
:disabled="form.validityType !== 'AFTER_ACTIVATE_MONTHS'"
|
||||
class="validity-input-num"
|
||||
/>
|
||||
<span class="radio-inline-text">个月内有效</span>
|
||||
</el-radio>
|
||||
</div>
|
||||
|
||||
<div class="validity-row">
|
||||
<el-radio value="FIXED_UNTIL">
|
||||
<span class="radio-inline-text">指定有效期至</span>
|
||||
<el-date-picker
|
||||
v-model="form.validEndTime"
|
||||
type="datetime"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="选择有效截止时间"
|
||||
clearable
|
||||
:disabled="form.validityType !== 'FIXED_UNTIL'"
|
||||
class="validity-date-picker"
|
||||
style="width: 220px"
|
||||
/>
|
||||
</el-radio>
|
||||
</div>
|
||||
</el-radio-group>
|
||||
</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"
|
||||
>
|
||||
{{ isEditMode ? "保存修改" : "提交创建" }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -121,6 +214,56 @@ const route = useRoute();
|
||||
const router = useRouter();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const detailLoading = ref(false);
|
||||
const submitLoading = ref(false);
|
||||
const activityDetail = ref({});
|
||||
|
||||
const form = reactive({
|
||||
giftCardName: "",
|
||||
cardType: "CASH",
|
||||
faceValue: "",
|
||||
stockTotal: null,
|
||||
validityType: "LONG_TERM",
|
||||
validEndTime: null,
|
||||
validMonthsAfterActivate: null,
|
||||
});
|
||||
|
||||
const formRef = ref(null);
|
||||
|
||||
const onlyView = computed(() => {
|
||||
const q = route.query.onlyView;
|
||||
return q === "true" || q === true;
|
||||
});
|
||||
|
||||
const isEditMode = computed(() => {
|
||||
return !!route.query.id && !onlyView.value;
|
||||
});
|
||||
|
||||
const pageTitle = computed(() => {
|
||||
if (onlyView.value) {
|
||||
return "查看礼品卡";
|
||||
}
|
||||
if (isEditMode.value) {
|
||||
return "编辑礼品卡";
|
||||
}
|
||||
return "添加礼品卡";
|
||||
});
|
||||
|
||||
const validityDisplayText = computed(() => {
|
||||
const type = String(form.validityType || "").toUpperCase();
|
||||
if (type === "LONG_TERM") {
|
||||
return "长期有效";
|
||||
}
|
||||
if (type === "AFTER_ACTIVATE_MONTHS") {
|
||||
const m = form.validMonthsAfterActivate;
|
||||
return m != null && m !== "" ? `自激活后 ${m} 个月内有效` : "自激活后指定月数有效";
|
||||
}
|
||||
if (type === "FIXED_UNTIL") {
|
||||
return form.validEndTime ? `指定有效期至 ${form.validEndTime}` : "指定时间有效";
|
||||
}
|
||||
return "-";
|
||||
});
|
||||
|
||||
const checkFaceValue = (rule, value, callback) => {
|
||||
if (!value && value !== 0) {
|
||||
return callback(new Error("面额不能为空"));
|
||||
@@ -135,6 +278,7 @@ const checkFaceValue = (rule, value, callback) => {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
const validateStock = (rule, value, callback) => {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
callback(new Error("请填写总库存"));
|
||||
@@ -147,17 +291,6 @@ const validateStock = (rule, value, callback) => {
|
||||
callback();
|
||||
};
|
||||
|
||||
const detailLoading = ref(false);
|
||||
const submitLoading = ref(false);
|
||||
const form = reactive({
|
||||
giftCardName: "",
|
||||
cardType: "CASH",
|
||||
faceValue: "",
|
||||
stockTotal: null,
|
||||
validityType: "LONG_TERM",
|
||||
validEndTime: null,
|
||||
validMonthsAfterActivate: null,
|
||||
});
|
||||
const formRule = {
|
||||
giftCardName: [
|
||||
{ required: true, message: "活动名称不能为空", trigger: "blur" },
|
||||
@@ -167,51 +300,6 @@ const formRule = {
|
||||
stockTotal: [{ required: true, validator: validateStock, trigger: "blur" }],
|
||||
};
|
||||
|
||||
const formRef = ref(null);
|
||||
|
||||
const onlyView = computed(() => {
|
||||
const q = route.query.onlyView;
|
||||
return q === "true" || q === true;
|
||||
});
|
||||
const isEditMode = computed(() => {
|
||||
return !!route.query.id && !onlyView.value;
|
||||
});
|
||||
const pageTitle = computed(() => {
|
||||
if (onlyView.value) {
|
||||
return "查看礼品卡";
|
||||
}
|
||||
if (isEditMode.value) {
|
||||
return "编辑礼品卡";
|
||||
}
|
||||
return "添加礼品卡";
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (route.query.id) {
|
||||
loadDetail(route.query.id);
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => form.validityType,
|
||||
(v) => {
|
||||
if (v !== "FIXED_UNTIL") {
|
||||
form.validEndTime = null;
|
||||
}
|
||||
if (v !== "AFTER_ACTIVATE_MONTHS") {
|
||||
form.validMonthsAfterActivate = null;
|
||||
}
|
||||
}
|
||||
);
|
||||
watch(
|
||||
() => route.query.id,
|
||||
(id) => {
|
||||
if (id) {
|
||||
loadDetail(id);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const parseDetailDate = (val) => {
|
||||
if (!val) {
|
||||
return null;
|
||||
@@ -222,14 +310,15 @@ const parseDetailDate = (val) => {
|
||||
const d = new Date(val);
|
||||
return Number.isNaN(d.getTime()) ? null : d;
|
||||
};
|
||||
|
||||
const loadDetail = (id) => {
|
||||
detailLoading.value = true;
|
||||
getGiftCardCashActivity(id)
|
||||
.then((res) => {
|
||||
detailLoading.value = false;
|
||||
if (!res.success || !res.result) {
|
||||
return;
|
||||
}
|
||||
activityDetail.value = res.result;
|
||||
const d = res.result;
|
||||
form.giftCardName = d.giftCardName || "";
|
||||
form.cardType = d.cardType || "CASH";
|
||||
@@ -247,18 +336,26 @@ const loadDetail = (id) => {
|
||||
? d.validMonthsAfterActivate
|
||||
: null;
|
||||
})
|
||||
.catch(() => {
|
||||
.finally(() => {
|
||||
detailLoading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const closeCurrentPage = () => {
|
||||
const name = route.name;
|
||||
if (name) {
|
||||
if (name && appStore?.removeTag) {
|
||||
appStore.removeTag(name);
|
||||
}
|
||||
localStorage.pageOpenedList = JSON.stringify(appStore.pageOpenedList);
|
||||
router.go(-1);
|
||||
if (localStorage?.pageOpenedList && appStore?.pageOpenedList) {
|
||||
localStorage.pageOpenedList = JSON.stringify(appStore.pageOpenedList);
|
||||
}
|
||||
if (window.history.length > 1) {
|
||||
router.go(-1);
|
||||
} else {
|
||||
router.push({ name: "manager-gift-card-cash" });
|
||||
}
|
||||
};
|
||||
|
||||
const validateByValidityType = () => {
|
||||
const t = form.validityType;
|
||||
if (t === "FIXED_UNTIL") {
|
||||
@@ -276,6 +373,7 @@ const validateByValidityType = () => {
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const buildPayload = () => {
|
||||
const payload = {
|
||||
giftCardName: (form.giftCardName || "").trim(),
|
||||
@@ -295,6 +393,7 @@ const buildPayload = () => {
|
||||
}
|
||||
return payload;
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (onlyView.value) {
|
||||
return;
|
||||
@@ -312,97 +411,222 @@ const handleSubmit = () => {
|
||||
: saveGiftCardCashActivity(buildPayload());
|
||||
req
|
||||
.then((res) => {
|
||||
submitLoading.value = false;
|
||||
if (res.success) {
|
||||
Message.success(isEditMode.value ? "礼品卡活动已更新" : "礼品卡活动创建成功");
|
||||
closeCurrentPage();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
.finally(() => {
|
||||
submitLoading.value = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => form.validityType,
|
||||
(v) => {
|
||||
if (v !== "FIXED_UNTIL") {
|
||||
form.validEndTime = null;
|
||||
}
|
||||
if (v !== "AFTER_ACTIVATE_MONTHS") {
|
||||
form.validMonthsAfterActivate = null;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => route.query.id,
|
||||
(id) => {
|
||||
if (id) {
|
||||
loadDetail(id);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
if (route.query.id) {
|
||||
loadDetail(route.query.id);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.gift-card-activity-form :deep(.el-form-item) {
|
||||
margin-bottom: 26px;
|
||||
}
|
||||
.gift-card-type-form-item :deep(.el-form-item__label) {
|
||||
line-height: 32px;
|
||||
padding-top: 1px;
|
||||
}
|
||||
.gift-card-type-form-item :deep(.el-form-item__content) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 32px;
|
||||
}
|
||||
.gift-card-type-group {
|
||||
gap: 20px;
|
||||
}
|
||||
.gift-card-validity-form-item :deep(.el-form-item__content) {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
.gift-card-validity-outer {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
.gift-card-validity-label-col {
|
||||
flex: 0 0 130px;
|
||||
width: 130px;
|
||||
padding-right: 12px;
|
||||
<style lang="scss" scoped>
|
||||
.gift-card-activity-page {
|
||||
padding: 16px 20px;
|
||||
background-color: #f0f2f5;
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
text-align: right;
|
||||
line-height: 32px;
|
||||
min-height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-top: 1px;
|
||||
}
|
||||
.gift-card-validity-required {
|
||||
color: #ed4014;
|
||||
margin-right: 4px;
|
||||
font-family: SimSun, serif;
|
||||
}
|
||||
.gift-card-validity-radios {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-top: 1px;
|
||||
}
|
||||
.gift-card-validity-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.gift-card-validity-group :deep(.el-radio) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-right: 0;
|
||||
margin-bottom: 10px;
|
||||
line-height: 32px;
|
||||
min-height: 32px;
|
||||
height: auto;
|
||||
}
|
||||
.gift-card-validity-group :deep(.el-radio:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.gift-card-validity-group :deep(.el-radio__label) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
line-height: 32px;
|
||||
}
|
||||
.gift-card-validity-input-gap {
|
||||
width: 168px;
|
||||
margin: 0 10px;
|
||||
}
|
||||
.gift-card-validity-date-gap {
|
||||
margin-left: 10px;
|
||||
|
||||
.mt_10 {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
|
||||
}
|
||||
|
||||
.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,
|
||||
.type-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;
|
||||
}
|
||||
|
||||
.price-highlight {
|
||||
color: #f56c6c;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.hint-inline {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-form {
|
||||
padding: 8px 12px;
|
||||
|
||||
.input-tip-text {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.validity-radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
|
||||
.validity-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
:deep(.el-radio) {
|
||||
margin-right: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.el-radio__label) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.radio-inline-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.validity-input-num {
|
||||
width: 130px;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.validity-date-picker {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-card {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,109 +1,260 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card>
|
||||
<el-form ref="formRef" :model="form" label-width="120px" :rules="formRule">
|
||||
<div class="base-info-item">
|
||||
<div class="form-item-view">
|
||||
<h4>商品信息</h4>
|
||||
|
||||
<el-form-item label="选择商品" prop="promotionGoodsList">
|
||||
<el-button type="primary" @click="openSkuList">选择商品</el-button>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item style="width: 100%">
|
||||
<el-table
|
||||
border
|
||||
class="kanjia-goods-table"
|
||||
:data="form.promotionGoodsList"
|
||||
style="width: 100%"
|
||||
@selection-change="changeSelect"
|
||||
>
|
||||
<el-table-column type="selection" width="60" align="center" />
|
||||
<el-table-column prop="goodsName" label="商品名称" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column label="商品价格" width="150">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" :style="{ color: $mainColor }">
|
||||
{{ $filters.unitPrice(row.price, "¥") }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="库存" width="120">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.quantity }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结算价格" width="160">
|
||||
<template #default="{ $index }">
|
||||
<el-input-number
|
||||
:min="0"
|
||||
v-model="form.promotionGoodsList[$index].settlementPrice"
|
||||
controls-position="right"
|
||||
class="kanjia-goods-input"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最低砍" width="160">
|
||||
<template #default="{ $index }">
|
||||
<el-input-number
|
||||
:min="0"
|
||||
v-model="form.promotionGoodsList[$index].lowestPrice"
|
||||
controls-position="right"
|
||||
class="kanjia-goods-input"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最高砍" width="160">
|
||||
<template #default="{ $index }">
|
||||
<el-input-number
|
||||
:min="0"
|
||||
v-model="form.promotionGoodsList[$index].highestPrice"
|
||||
controls-position="right"
|
||||
class="kanjia-goods-input"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="活动库存" width="160">
|
||||
<template #default="{ $index }">
|
||||
<el-input-number
|
||||
:min="0"
|
||||
v-model="form.promotionGoodsList[$index].stock"
|
||||
controls-position="right"
|
||||
class="kanjia-goods-input"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-button type="danger" size="small" plain @click="delGoods($index)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-form-item>
|
||||
<el-form-item label="活动时间" prop="rangeTime" class="kanjia-activity-time-form-item">
|
||||
<el-date-picker
|
||||
type="datetimerange"
|
||||
v-model="form.rangeTime"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
placeholder="请选择"
|
||||
:disabled-date="options.disabledDate"
|
||||
/>
|
||||
</el-form-item>
|
||||
<div>
|
||||
<el-button link @click="closeCurrentPage">返回</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">提交</el-button>
|
||||
</div>
|
||||
<div class="kanjia-add-page">
|
||||
<!-- 顶部标题与返回栏 -->
|
||||
<el-card shadow="never" class="header-card">
|
||||
<div class="page-header-row">
|
||||
<div class="header-left">
|
||||
<div class="header-title">
|
||||
<span>添加砍价活动商品</span>
|
||||
<el-tag size="small" effect="plain" type="primary" class="mode-tag">
|
||||
新增活动
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
<div class="header-right">
|
||||
<el-button @click="closeCurrentPage">返回砍价列表</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
label-width="110px"
|
||||
:rules="formRule"
|
||||
class="main-form"
|
||||
>
|
||||
<!-- 活动时间设置卡片 -->
|
||||
<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-form-item
|
||||
label="活动时间"
|
||||
prop="rangeTime"
|
||||
class="kanjia-activity-time-form-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="options.disabledDate"
|
||||
style="width: 380px"
|
||||
/>
|
||||
<span class="time-tip-text">请设置砍价活动开展的时间段</span>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
|
||||
<!-- 砍价商品列表卡片 -->
|
||||
<el-card shadow="never" class="info-card mt_10">
|
||||
<div class="card-header-bar">
|
||||
<div class="header-title-group">
|
||||
<span class="section-indicator"></span>
|
||||
<span class="section-title">砍价活动商品</span>
|
||||
<span class="total-badge">已选 {{ form.promotionGoodsList.length }} 件商品</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button
|
||||
v-if="selectedGoods.length"
|
||||
type="danger"
|
||||
plain
|
||||
size="small"
|
||||
@click="delBatch"
|
||||
>
|
||||
批量删除 ({{ selectedGoods.length }})
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="form.promotionGoodsList.length"
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="clearAllGoods"
|
||||
>
|
||||
清空商品
|
||||
</el-button>
|
||||
<el-button type="primary" size="small" @click="openSkuList">
|
||||
选择商品
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form-item prop="promotionGoodsList" :label-width="0" class="table-form-item">
|
||||
<el-table
|
||||
border
|
||||
stripe
|
||||
class="kanjia-goods-table"
|
||||
:data="form.promotionGoodsList"
|
||||
style="width: 100%"
|
||||
empty-text="暂未添加商品,请点击右上角【选择商品】按钮添加"
|
||||
@selection-change="changeSelect"
|
||||
>
|
||||
<el-table-column type="selection" width="50" align="center" />
|
||||
|
||||
<!-- 商品图片 -->
|
||||
<el-table-column label="商品图片" width="76" align="center">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row && row.thumbnail" class="goods-thumb-box">
|
||||
<el-image
|
||||
:src="row.thumbnail"
|
||||
fit="cover"
|
||||
preview-teleported
|
||||
:preview-src-list="[row.thumbnail]"
|
||||
class="goods-thumb-img"
|
||||
>
|
||||
<template #error>
|
||||
<div class="thumb-slot">无图</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
<div v-else class="goods-thumb-box thumb-slot">无图</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 商品名称与SKU -->
|
||||
<el-table-column
|
||||
prop="goodsName"
|
||||
label="商品名称"
|
||||
min-width="180"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div v-if="row">
|
||||
<div class="font-semibold text-dark">{{ row.goodsName || "-" }}</div>
|
||||
<div v-if="row.skuId" class="sku-sub-text font-mono">
|
||||
SKU: {{ row.skuId }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 所属商家 -->
|
||||
<el-table-column
|
||||
prop="storeName"
|
||||
label="所属商家"
|
||||
min-width="120"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span class="text-secondary">{{ (row && row.storeName) || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 商品原价 -->
|
||||
<el-table-column label="商品原价" width="110" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" class="font-mono text-dark">
|
||||
{{ $filters.unitPrice(row.price || row.originalPrice, "¥") }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 当前库存 -->
|
||||
<el-table-column label="可用库存" width="90" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" class="font-mono text-dark">{{ row.quantity ?? "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 结算价格 -->
|
||||
<el-table-column label="结算价格(元)" width="140" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-input-number
|
||||
v-model="form.promotionGoodsList[$index].settlementPrice"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
controls-position="right"
|
||||
class="kanjia-goods-input"
|
||||
placeholder="结算价"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 最低砍 -->
|
||||
<el-table-column label="最低可砍(元)" width="140" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-input-number
|
||||
v-model="form.promotionGoodsList[$index].lowestPrice"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
controls-position="right"
|
||||
class="kanjia-goods-input"
|
||||
placeholder="最低可砍"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 最高砍 -->
|
||||
<el-table-column label="最高可砍(元)" width="140" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-input-number
|
||||
v-model="form.promotionGoodsList[$index].highestPrice"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
controls-position="right"
|
||||
class="kanjia-goods-input"
|
||||
placeholder="最高可砍"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 活动库存 -->
|
||||
<el-table-column label="活动库存(件)" width="130" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-input-number
|
||||
v-model="form.promotionGoodsList[$index].stock"
|
||||
:min="1"
|
||||
:precision="0"
|
||||
controls-position="right"
|
||||
class="kanjia-goods-input"
|
||||
placeholder="活动库存"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 操作 -->
|
||||
<el-table-column label="操作" width="80" align="center" fixed="right">
|
||||
<template #default="{ $index }">
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
@click="delGoods($index)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<el-card shadow="never" class="footer-card mt_10">
|
||||
<div class="footer-actions">
|
||||
<el-button @click="closeCurrentPage">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="submitLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
提交创建
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-form>
|
||||
|
||||
<sku-select ref="skuSelectRef" @selectedGoodsData="selectedGoodsData"></sku-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { nextTick, onMounted, reactive, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { nextTick, reactive, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { saveKanJiaActivityGoods } from "@/api/promotion";
|
||||
import { regular } from "@/utils";
|
||||
import skuSelect from "@/components/lili-dialog";
|
||||
@@ -114,19 +265,17 @@ import { useAppStore } from "@/stores/app";
|
||||
|
||||
defineOptions({ name: "add-kanjia-activity-goods" });
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const modalType = ref(0);
|
||||
const form = reactive({
|
||||
promotionGoodsList: [],
|
||||
rangeTime: [],
|
||||
});
|
||||
const id = ref(route.query.id);
|
||||
|
||||
const submitLoading = ref(false);
|
||||
const selectedGoods = ref([]);
|
||||
const promotionGoodsList = ref([]);
|
||||
|
||||
const formRule = {
|
||||
promotionGoodsList: [
|
||||
{
|
||||
@@ -146,6 +295,7 @@ const formRule = {
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const options = {
|
||||
disabledDate(date) {
|
||||
return date && date.valueOf() < Date.now() - 86400000;
|
||||
@@ -155,14 +305,6 @@ const options = {
|
||||
const formRef = ref(null);
|
||||
const skuSelectRef = ref(null);
|
||||
|
||||
onMounted(async () => {
|
||||
if (id.value) {
|
||||
// 历史遗留:原代码调用了未定义的 getCoupon 方法,此处原样保留
|
||||
getCoupon();
|
||||
modalType.value = 1;
|
||||
}
|
||||
});
|
||||
|
||||
function handleSubmit() {
|
||||
formRef.value.validate((valid) => {
|
||||
if (!valid) {
|
||||
@@ -182,18 +324,19 @@ function handleSubmit() {
|
||||
delete params.rangeTime;
|
||||
|
||||
let checkResult = true;
|
||||
form.promotionGoodsList.forEach((res) => {
|
||||
for (let i = 0; i < form.promotionGoodsList.length; i++) {
|
||||
const res = form.promotionGoodsList[i];
|
||||
const eCoupon = isECoupon(res.goodsType);
|
||||
if (res.stock <= 0 || res.stock > res.quantity) {
|
||||
checkResult = false;
|
||||
ElMessage.error("活动库存不能为0且不能超过商品库存");
|
||||
return;
|
||||
ElMessage.error(`商品【${res.goodsName}】活动库存不能为0且不能超过可用库存`);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!regular.money.test(res.settlementPrice)) {
|
||||
checkResult = false;
|
||||
ElMessage.error("结算价格金额格式不正确");
|
||||
return;
|
||||
ElMessage.error(`商品【${res.goodsName}】结算价格金额格式不正确`);
|
||||
break;
|
||||
}
|
||||
if (
|
||||
(eCoupon ? res.settlementPrice < 0 : res.settlementPrice <= 0) ||
|
||||
@@ -202,63 +345,78 @@ function handleSubmit() {
|
||||
checkResult = false;
|
||||
ElMessage.error(
|
||||
eCoupon
|
||||
? "结算价格不能小于0且不能超过商品价格"
|
||||
: "结算价格金额不能为0且不能超过商品价格"
|
||||
? `商品【${res.goodsName}】结算价格不能小于0且不能超过商品原价`
|
||||
: `商品【${res.goodsName}】结算价格不能为0且不能超过商品原价`
|
||||
);
|
||||
return;
|
||||
break;
|
||||
}
|
||||
if (!regular.money.test(res.highestPrice)) {
|
||||
checkResult = false;
|
||||
ElMessage.error("最高可砍金额格式错误");
|
||||
return;
|
||||
ElMessage.error(`商品【${res.goodsName}】最高可砍金额格式错误`);
|
||||
break;
|
||||
}
|
||||
if (res.highestPrice <= 0 || res.highestPrice > res.price) {
|
||||
checkResult = false;
|
||||
ElMessage.error("最高可砍金额不能为0且不能超过商品价格");
|
||||
return;
|
||||
ElMessage.error(`商品【${res.goodsName}】最高可砍金额不能为0且不能超过商品原价`);
|
||||
break;
|
||||
}
|
||||
if (!regular.money.test(res.lowestPrice)) {
|
||||
checkResult = false;
|
||||
ElMessage.error("最低可砍金额格式错误");
|
||||
return;
|
||||
ElMessage.error(`商品【${res.goodsName}】最低可砍金额格式错误`);
|
||||
break;
|
||||
}
|
||||
if (res.lowestPrice < 0 || (!eCoupon && res.lowestPrice <= 0) || res.lowestPrice > res.price) {
|
||||
if (
|
||||
res.lowestPrice < 0 ||
|
||||
(!eCoupon && res.lowestPrice <= 0) ||
|
||||
res.lowestPrice > res.price
|
||||
) {
|
||||
checkResult = false;
|
||||
ElMessage.error(eCoupon ? "最低可砍金额不能小于0" : "最低可砍金额不能为0");
|
||||
return;
|
||||
ElMessage.error(
|
||||
eCoupon
|
||||
? `商品【${res.goodsName}】最低可砍金额不能小于0`
|
||||
: `商品【${res.goodsName}】最低可砍金额不能为0`
|
||||
);
|
||||
break;
|
||||
}
|
||||
if (parseInt(res.lowestPrice) > parseInt(res.highestPrice)) {
|
||||
if (parseFloat(res.lowestPrice) > parseFloat(res.highestPrice)) {
|
||||
checkResult = false;
|
||||
ElMessage.error("最低砍价金额不能大于最高砍价金额");
|
||||
return;
|
||||
ElMessage.error(`商品【${res.goodsName}】最低砍价金额不能大于最高砍价金额`);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!checkResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
submitLoading.value = true;
|
||||
saveKanJiaActivityGoods(params).then((res) => {
|
||||
submitLoading.value = false;
|
||||
if (res.success) {
|
||||
ElMessage.success(
|
||||
modalType.value === 1 ? "砍价活动修改成功" : "砍价活动添加成功"
|
||||
);
|
||||
closeCurrentPage();
|
||||
}
|
||||
});
|
||||
saveKanJiaActivityGoods(params)
|
||||
.then((res) => {
|
||||
if (res && res.success) {
|
||||
ElMessage.success("砍价活动添加成功");
|
||||
closeCurrentPage();
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
submitLoading.value = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function closeCurrentPage() {
|
||||
appStore.removeTag("add-kan-jia-goods");
|
||||
localStorage.pageOpenedList = JSON.stringify(appStore.pageOpenedList);
|
||||
if (appStore?.removeTag) {
|
||||
appStore.removeTag("add-kanjia-activity-goods");
|
||||
appStore.removeTag("add-kan-jia-goods");
|
||||
}
|
||||
if (localStorage?.pageOpenedList && appStore?.pageOpenedList) {
|
||||
localStorage.pageOpenedList = JSON.stringify(appStore.pageOpenedList);
|
||||
}
|
||||
router.go(-1);
|
||||
}
|
||||
|
||||
function openSkuList() {
|
||||
skuSelectRef.value.open("goods");
|
||||
let data = JSON.parse(JSON.stringify(form.promotionGoodsList));
|
||||
const data = JSON.parse(JSON.stringify(form.promotionGoodsList));
|
||||
data.forEach((e) => {
|
||||
e.id = e.skuId;
|
||||
});
|
||||
@@ -266,7 +424,7 @@ function openSkuList() {
|
||||
}
|
||||
|
||||
function changeSelect(e) {
|
||||
selectedGoods.value = e;
|
||||
selectedGoods.value = e || [];
|
||||
}
|
||||
|
||||
function delGoods(index) {
|
||||
@@ -274,17 +432,33 @@ function delGoods(index) {
|
||||
touchPromotionGoodsValidate();
|
||||
}
|
||||
|
||||
function delBatch() {
|
||||
if (!selectedGoods.value.length) return;
|
||||
const skuIds = new Set(selectedGoods.value.map((g) => g.skuId || g.id));
|
||||
form.promotionGoodsList = form.promotionGoodsList.filter(
|
||||
(g) => !skuIds.has(g.skuId || g.id)
|
||||
);
|
||||
selectedGoods.value = [];
|
||||
touchPromotionGoodsValidate();
|
||||
}
|
||||
|
||||
function clearAllGoods() {
|
||||
form.promotionGoodsList = [];
|
||||
selectedGoods.value = [];
|
||||
touchPromotionGoodsValidate();
|
||||
}
|
||||
|
||||
function touchPromotionGoodsValidate() {
|
||||
nextTick(() => {
|
||||
const form = formRef.value;
|
||||
if (form && typeof form.validateField === "function") {
|
||||
form.validateField("promotionGoodsList", () => {});
|
||||
const f = formRef.value;
|
||||
if (f && typeof f.validateField === "function") {
|
||||
f.validateField("promotionGoodsList", () => {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function selectedGoodsData(item) {
|
||||
let list = [];
|
||||
const list = [];
|
||||
item.forEach((e) => {
|
||||
list.push({
|
||||
settlementPrice: e.settlementPrice || 0,
|
||||
@@ -310,55 +484,203 @@ function selectedGoodsData(item) {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.kanjia-goods-table {
|
||||
:deep(.el-table__body .el-table__cell .cell) {
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
white-space: normal;
|
||||
.kanjia-add-page {
|
||||
padding: 16px 20px;
|
||||
background-color: #f0f2f5;
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
|
||||
.mt_10 {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.kanjia-goods-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.kanjia-activity-time-form-item {
|
||||
:deep(.el-date-editor) {
|
||||
width: 360px !important;
|
||||
max-width: 360px;
|
||||
flex-grow: 0;
|
||||
.font-mono {
|
||||
font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
.describe {
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.effectiveDays {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
|
||||
> * {
|
||||
margin: 0 4px;
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.tips {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
.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 {
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-form {
|
||||
.table-form-item {
|
||||
margin-bottom: 0;
|
||||
|
||||
:deep(.el-form-item__content) {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.time-tip-text {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.card-header-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.header-title-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.section-indicator {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background-color: #409eff;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.total-badge {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.kanjia-goods-table {
|
||||
:deep(th.el-table__cell) {
|
||||
background-color: #fafafa;
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.goods-thumb-box {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background-color: #f5f7fa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #ebeef5;
|
||||
margin: 0 auto;
|
||||
|
||||
.goods-thumb-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.thumb-slot {
|
||||
font-size: 11px;
|
||||
color: #c0c4cc;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sku-sub-text {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.kanjia-goods-input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-card {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,94 +1,238 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card>
|
||||
<el-form ref="formRef" :model="form" label-width="120px" :rules="formRule">
|
||||
<div class="base-info-item">
|
||||
<div class="form-item-view">
|
||||
<h4>商品信息</h4>
|
||||
<el-form-item label="商品名称">
|
||||
<div>{{ form.goodsName }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="SKU编码">
|
||||
<div>{{ form.skuId }}</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
|
||||
:disabled="onlyView"
|
||||
type="number"
|
||||
v-model="form.settlementPrice"
|
||||
placeholder="请填写结算价格"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="最低可砍" prop="lowestPrice">
|
||||
<el-input
|
||||
:disabled="onlyView"
|
||||
type="number"
|
||||
v-model="form.lowestPrice"
|
||||
placeholder="请填写最低可砍金额"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="最高可砍" prop="highestPrice">
|
||||
<el-input
|
||||
:disabled="onlyView"
|
||||
type="number"
|
||||
v-model="form.highestPrice"
|
||||
placeholder="请填写最高可砍金额"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="活动库存" prop="stock">
|
||||
<el-input
|
||||
:disabled="onlyView"
|
||||
type="number"
|
||||
v-model="form.stock"
|
||||
placeholder="请填写活动库存"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="活动时间" prop="rangeTime" class="kanjia-activity-time-form-item">
|
||||
<el-date-picker
|
||||
:disabled="onlyView"
|
||||
v-model="form.rangeTime"
|
||||
type="datetimerange"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
placeholder="请选择"
|
||||
:disabled-date="options.disabledDate"
|
||||
/>
|
||||
</el-form-item>
|
||||
<div>
|
||||
<el-button link @click="closeCurrentPage">返回</el-button>
|
||||
<el-button v-if="!onlyView" type="primary" :loading="submitLoading" @click="handleSubmit">
|
||||
提交
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="kanjia-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 || "-" }}</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 || "-" }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="所属店铺">
|
||||
<span class="text-secondary">{{ goodsStoreName }}</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">{{ goodsStock }} 件</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="price-highlight font-mono font-semibold">
|
||||
{{ $filters.unitPrice(form.settlementPrice, "¥") }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="活动库存">
|
||||
<span class="font-mono text-dark font-semibold">
|
||||
{{ form.stock != null ? `${form.stock} 件` : "-" }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="最低可砍金额">
|
||||
<span class="font-mono text-dark">
|
||||
{{ $filters.unitPrice(form.lowestPrice, "¥") }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="最高可砍金额">
|
||||
<span class="font-mono text-dark">
|
||||
{{ $filters.unitPrice(form.highestPrice, "¥") }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item
|
||||
v-if="form.purchasePrice != null"
|
||||
label="最低购买金额"
|
||||
>
|
||||
<span class="font-mono text-dark">
|
||||
{{ $filters.unitPrice(form.purchasePrice, "¥") }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="活动时间" :span="form.purchasePrice != null ? 1 : 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="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="lowestPrice">
|
||||
<el-input
|
||||
v-model="form.lowestPrice"
|
||||
type="number"
|
||||
placeholder="请填写最低可砍金额"
|
||||
clearable
|
||||
style="width: 280px"
|
||||
>
|
||||
<template #append>元</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="最高可砍" prop="highestPrice">
|
||||
<el-input
|
||||
v-model="form.highestPrice"
|
||||
type="number"
|
||||
placeholder="请填写最高可砍金额"
|
||||
clearable
|
||||
style="width: 280px"
|
||||
>
|
||||
<template #append>元</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="活动库存" prop="stock">
|
||||
<el-input
|
||||
v-model="form.stock"
|
||||
type="number"
|
||||
placeholder="请填写活动库存"
|
||||
clearable
|
||||
style="width: 280px"
|
||||
>
|
||||
<template #append>件</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
label="活动时间"
|
||||
prop="rangeTime"
|
||||
class="kanjia-activity-time-form-item"
|
||||
>
|
||||
<el-date-picker
|
||||
v-model="form.rangeTime"
|
||||
type="datetimerange"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
placeholder="请选择活动时间"
|
||||
:disabled-date="options.disabledDate"
|
||||
/>
|
||||
</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 { ref, onMounted } from "vue";
|
||||
import { ref, onMounted, computed } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { getKanJiaActivityGoodsById, editKanJiaActivityGoods } from "@/api/promotion";
|
||||
import { regular } from "@/utils";
|
||||
@@ -103,6 +247,66 @@ 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 submitLoading = ref(false);
|
||||
const form = ref({
|
||||
purchasePrice: 0,
|
||||
goodsSku: {},
|
||||
rangeTime: [],
|
||||
});
|
||||
|
||||
const goodsThumbnail = computed(() => {
|
||||
return form.value.thumbnail || form.value.goodsSku?.thumbnail || "";
|
||||
});
|
||||
|
||||
const goodsStoreName = computed(() => {
|
||||
return form.value.goodsSku?.storeName || form.value.storeName || "-";
|
||||
});
|
||||
|
||||
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 goodsStock = computed(() => {
|
||||
if (form.value.goodsSku?.quantity != null) return form.value.goodsSku.quantity;
|
||||
return "-";
|
||||
});
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
const checkSettlementPrice = (rule, value, callback) => {
|
||||
if (!value && value !== 0) {
|
||||
return callback(new Error("结算金额不能为空"));
|
||||
@@ -115,6 +319,7 @@ const checkSettlementPrice = (rule, value, callback) => {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
const checkLowestPrice = (rule, value, callback) => {
|
||||
if (!value && value !== 0) {
|
||||
return callback(new Error("最低可砍金额不能为空"));
|
||||
@@ -127,6 +332,7 @@ const checkLowestPrice = (rule, value, callback) => {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
const checkHighestPrice = (rule, value, callback) => {
|
||||
if (!value && value !== 0) {
|
||||
return callback(new Error("最高可砍金额不能为空"));
|
||||
@@ -140,15 +346,6 @@ const checkHighestPrice = (rule, value, callback) => {
|
||||
}
|
||||
};
|
||||
|
||||
const modalType = ref(0);
|
||||
const form = ref({
|
||||
purchasePrice: 0,
|
||||
goodsSku: {},
|
||||
rangeTime: [],
|
||||
});
|
||||
const id = route.query.id;
|
||||
const onlyView = route.query.onlyView;
|
||||
const submitLoading = ref(false);
|
||||
const formRule = {
|
||||
settlementPrice: [
|
||||
{ required: true, message: "请输入结算金额" },
|
||||
@@ -172,6 +369,7 @@ const formRule = {
|
||||
],
|
||||
stock: [{ required: true, message: "请输入活动库存" }],
|
||||
};
|
||||
|
||||
const options = {
|
||||
disabledDate(date) {
|
||||
return date && date.valueOf() < Date.now() - 86400000;
|
||||
@@ -180,17 +378,21 @@ const options = {
|
||||
|
||||
const formRef = ref(null);
|
||||
|
||||
onMounted(async () => {
|
||||
if (id) {
|
||||
onMounted(() => {
|
||||
if (id.value) {
|
||||
getKanJiaActivityGoods();
|
||||
modalType.value = 1;
|
||||
}
|
||||
});
|
||||
|
||||
function getKanJiaActivityGoods() {
|
||||
getKanJiaActivityGoodsById(id).then((res) => {
|
||||
form.value = res.result;
|
||||
form.value.rangeTime = [res.result.startTime, res.result.endTime];
|
||||
getKanJiaActivityGoodsById(id.value).then((res) => {
|
||||
if (res && res.success && res.result) {
|
||||
form.value = {
|
||||
...res.result,
|
||||
goodsSku: res.result.goodsSku || {},
|
||||
rangeTime: [res.result.startTime, res.result.endTime],
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -213,7 +415,7 @@ function handleSubmit() {
|
||||
|
||||
const eCoupon = isECoupon(params.goodsType || params.goodsSku?.goodsType);
|
||||
|
||||
if (params.stock <= 0 || params.stock > params.goodsSku.quantity) {
|
||||
if (params.stock <= 0 || (params.goodsSku?.quantity != null && params.stock > params.goodsSku.quantity)) {
|
||||
ElMessage.error("活动库存不能为0且不能超过商品库存");
|
||||
return;
|
||||
}
|
||||
@@ -223,7 +425,7 @@ function handleSubmit() {
|
||||
}
|
||||
if (
|
||||
(eCoupon ? params.settlementPrice < 0 : params.settlementPrice <= 0) ||
|
||||
params.settlementPrice > params.price
|
||||
params.settlementPrice > (params.price || goodsPrice.value)
|
||||
) {
|
||||
ElMessage.error(
|
||||
eCoupon
|
||||
@@ -236,7 +438,7 @@ function handleSubmit() {
|
||||
ElMessage.error("最高可砍金额格式错误");
|
||||
return;
|
||||
}
|
||||
if (params.highestPrice <= 0 || params.highestPrice > params.price) {
|
||||
if (params.highestPrice <= 0 || params.highestPrice > (params.price || goodsPrice.value)) {
|
||||
ElMessage.error("最高可砍金额不能为0且不能超过商品价格");
|
||||
return;
|
||||
}
|
||||
@@ -247,52 +449,212 @@ function handleSubmit() {
|
||||
if (
|
||||
params.lowestPrice < 0 ||
|
||||
(!eCoupon && params.lowestPrice <= 0) ||
|
||||
params.lowestPrice > params.price
|
||||
params.lowestPrice > (params.price || goodsPrice.value)
|
||||
) {
|
||||
ElMessage.error(eCoupon ? "最低可砍金额不能小于0" : "最低可砍金额不能为0");
|
||||
return;
|
||||
}
|
||||
if (params.lowestPrice > params.highestPrice) {
|
||||
if (parseFloat(params.lowestPrice) > parseFloat(params.highestPrice)) {
|
||||
ElMessage.error("最低砍价金额不能大于最高砍价金额");
|
||||
return;
|
||||
}
|
||||
|
||||
submitLoading.value = true;
|
||||
editKanJiaActivityGoods(params).then((res) => {
|
||||
submitLoading.value = false;
|
||||
if (res.success) {
|
||||
ElMessage.success("砍价活动修改成功");
|
||||
closeCurrentPage();
|
||||
}
|
||||
});
|
||||
editKanJiaActivityGoods(params)
|
||||
.then((res) => {
|
||||
if (res && res.success) {
|
||||
ElMessage.success("砍价活动修改成功");
|
||||
closeCurrentPage();
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
submitLoading.value = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function closeCurrentPage() {
|
||||
appStore.removeTag("add-kan-jia-goods");
|
||||
localStorage.pageOpenedList = JSON.stringify(appStore.pageOpenedList);
|
||||
if (appStore?.removeTag) {
|
||||
appStore.removeTag("add-kan-jia-goods");
|
||||
appStore.removeTag("edit-kanJia-activity-goods");
|
||||
}
|
||||
if (localStorage?.pageOpenedList && appStore?.pageOpenedList) {
|
||||
localStorage.pageOpenedList = JSON.stringify(appStore.pageOpenedList);
|
||||
}
|
||||
router.go(-1);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
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;
|
||||
}
|
||||
.kanjia-edit-page {
|
||||
padding: 16px 20px;
|
||||
background-color: #f0f2f5;
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
|
||||
.kanjia-activity-time-form-item {
|
||||
:deep(.el-date-editor) {
|
||||
width: 360px !important;
|
||||
max-width: 360px;
|
||||
flex-grow: 0;
|
||||
.mt_10 {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.price-highlight {
|
||||
color: #f56c6c;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-form {
|
||||
padding: 8px 12px;
|
||||
|
||||
.kanjia-activity-time-form-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>
|
||||
|
||||
@@ -1,98 +1,290 @@
|
||||
<template>
|
||||
<div class="pintuan-goods">
|
||||
<el-card>
|
||||
<h4>活动详情</h4>
|
||||
<el-table border :data="data" style="width: 100%; margin: 10px 0">
|
||||
<el-table-column prop="promotionName" label="活动名称" min-width="120" />
|
||||
<el-table-column prop="startTime" label="活动开始时间" min-width="120" />
|
||||
<el-table-column prop="endTime" label="活动结束时间" min-width="120" />
|
||||
<el-table-column prop="requiredNum" label="成团人数" min-width="90" />
|
||||
<el-table-column prop="limitNum" label="限购数量" min-width="90" />
|
||||
<el-table-column label="状态" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row" :type="promotionStatusTagType(row.promotionStatus)">
|
||||
{{ promotionStatusText(row.promotionStatus) }}
|
||||
<div class="pintuan-goods-page">
|
||||
<!-- 顶部标题与返回栏 -->
|
||||
<el-card shadow="never" class="header-card">
|
||||
<div class="page-header-row">
|
||||
<div class="header-left">
|
||||
<div class="header-title">
|
||||
<span>拼团活动商品管理</span>
|
||||
<el-tag
|
||||
v-if="pintuanInfo.promotionStatus"
|
||||
:type="promotionStatusTagType(pintuanInfo.promotionStatus)"
|
||||
size="small"
|
||||
effect="plain"
|
||||
class="status-tag"
|
||||
>
|
||||
{{ promotionStatusText(pintuanInfo.promotionStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<span v-if="pintuanId" class="activity-id font-mono">ID: {{ pintuanId }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-button @click="back">返回拼团列表</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="3" border class="custom-descriptions">
|
||||
<el-descriptions-item label="活动名称">
|
||||
<span class="font-semibold text-dark">{{ pintuanInfo.promotionName || "-" }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="活动状态">
|
||||
<el-tag
|
||||
v-if="pintuanInfo.promotionStatus"
|
||||
:type="promotionStatusTagType(pintuanInfo.promotionStatus)"
|
||||
size="small"
|
||||
effect="dark"
|
||||
>
|
||||
{{ promotionStatusText(pintuanInfo.promotionStatus) }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="所属店铺">
|
||||
<span class="text-secondary">{{ pintuanInfo.storeName || "全平台 / 自营" }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="活动开始时间">
|
||||
<span class="font-mono text-dark">{{ pintuanInfo.startTime || "-" }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="活动结束时间">
|
||||
<span class="font-mono text-dark">{{ pintuanInfo.endTime || "-" }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="成团人数">
|
||||
<span class="font-mono text-dark">
|
||||
{{ pintuanInfo.requiredNum ? `${pintuanInfo.requiredNum} 人` : "-" }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="限购数量">
|
||||
<span class="font-mono text-dark">
|
||||
{{ pintuanInfo.limitNum > 0 ? `${pintuanInfo.limitNum} 件` : "不限购" }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="虚拟成团">
|
||||
<el-tag
|
||||
v-if="pintuanInfo.fictitious != null"
|
||||
:type="pintuanInfo.fictitious ? 'success' : 'info'"
|
||||
size="small"
|
||||
effect="plain"
|
||||
>
|
||||
{{ pintuanInfo.fictitious ? "已开启" : "未开启" }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="活动ID">
|
||||
<span class="font-mono text-secondary">{{ pintuanId || "-" }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item
|
||||
v-if="pintuanInfo.pintuanRule"
|
||||
label="规则说明"
|
||||
:span="3"
|
||||
>
|
||||
<span class="text-secondary rule-desc">{{ pintuanInfo.pintuanRule }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<!-- 拼团商品列表卡片 -->
|
||||
<el-card shadow="never" class="info-card mt_10">
|
||||
<div class="card-header-bar">
|
||||
<div class="header-title-group">
|
||||
<span class="section-indicator"></span>
|
||||
<span class="section-title">已报名拼团商品</span>
|
||||
<span class="total-badge">共 {{ total }} 件商品</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button size="small" @click="getDataList">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4>商品信息</h4>
|
||||
<el-table
|
||||
ref="table"
|
||||
v-loading="loading"
|
||||
border
|
||||
class="operation"
|
||||
stripe
|
||||
:data="goodsData"
|
||||
style="width: 100%"
|
||||
empty-text="暂无报名的拼团商品"
|
||||
class="custom-table"
|
||||
>
|
||||
<el-table-column label="商品名称" min-width="120">
|
||||
<!-- 商品图片 -->
|
||||
<el-table-column label="商品图片" width="76" align="center">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row && (row.thumbnail || row.small || row.goodsImage)" class="goods-thumb-box">
|
||||
<el-image
|
||||
:src="row.thumbnail || row.small || row.goodsImage"
|
||||
fit="cover"
|
||||
preview-teleported
|
||||
:preview-src-list="[row.thumbnail || row.small || row.goodsImage]"
|
||||
class="goods-thumb-img"
|
||||
>
|
||||
<template #error>
|
||||
<div class="thumb-slot">无图</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
<div v-else class="goods-thumb-box thumb-slot">无图</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 商品名称 -->
|
||||
<el-table-column prop="goodsName" label="商品名称" min-width="220" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<div v-if="row">
|
||||
<a
|
||||
class="goods-link"
|
||||
@click="handleLinkTo(row.goodsId, row.skuId)"
|
||||
>
|
||||
{{ row.goodsName || "-" }}
|
||||
</a>
|
||||
<div v-if="row.skuId" class="sku-sub-text font-mono">
|
||||
SKU: {{ row.skuId }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 所属商家 -->
|
||||
<el-table-column prop="storeName" label="所属商家" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span class="text-secondary">{{ (row && row.storeName) || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 商品原价 -->
|
||||
<el-table-column label="商品原价" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" class="original-price font-mono">
|
||||
{{ $filters.unitPrice(row.originalPrice, "¥") }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 拼团价格 -->
|
||||
<el-table-column label="拼团价格" width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" class="pintuan-price font-mono font-semibold">
|
||||
{{ $filters.unitPrice(row.price, "¥") }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 活动库存 -->
|
||||
<el-table-column label="活动库存" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" class="font-mono text-dark">{{ row.quantity ?? "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 限购数量 -->
|
||||
<el-table-column label="限购数量" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" class="font-mono text-dark">
|
||||
{{ row.limitNum > 0 ? row.limitNum : "不限" }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 操作 -->
|
||||
<el-table-column label="查看" width="140" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row">
|
||||
<a class="link-text mr_10" @click="linkTo(row.goodsId, row.skuId)">{{ row.goodsName }}</a>
|
||||
<el-popover trigger="hover" title="扫码在手机中查看" placement="top" width="180">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="handleLinkTo(row.goodsId, row.skuId)"
|
||||
>
|
||||
PC查看
|
||||
</el-button>
|
||||
|
||||
<el-popover
|
||||
trigger="hover"
|
||||
placement="top"
|
||||
width="180"
|
||||
>
|
||||
<template #reference>
|
||||
<img
|
||||
src="../../../assets/qrcode.svg"
|
||||
style="vertical-align: middle"
|
||||
class="hover-pointer"
|
||||
width="20"
|
||||
height="20"
|
||||
alt="qrcode"
|
||||
/>
|
||||
<el-button link type="primary" class="ml_8">
|
||||
手机扫码
|
||||
</el-button>
|
||||
</template>
|
||||
<vue-qr
|
||||
:text="wapLinkTo(row.goodsId, row.skuId)"
|
||||
:margin="0"
|
||||
color-dark="#000"
|
||||
color-light="#fff"
|
||||
:size="150"
|
||||
/>
|
||||
<div class="qr-popover-content">
|
||||
<div class="qr-popover-title">扫码在手机中查看</div>
|
||||
<vue-qr
|
||||
:text="handleWapLinkTo(row.goodsId, row.skuId)"
|
||||
:margin="4"
|
||||
color-dark="#000"
|
||||
color-light="#fff"
|
||||
:size="140"
|
||||
/>
|
||||
</div>
|
||||
</el-popover>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="quantity" label="库存" min-width="80" />
|
||||
<el-table-column label="拼团价格" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" :style="{ color: $mainColor }">
|
||||
{{ $filters.unitPrice(row.price, "¥") }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="page operation mt_10" style="display: flex; justify-content: flex-end">
|
||||
<!-- 分页区域 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="searchForm.pageNumber"
|
||||
v-model:page-size="searchForm.pageSize"
|
||||
:page-sizes="[20, 50, 100]"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
background
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
size="small"
|
||||
size="default"
|
||||
@current-change="changePage"
|
||||
@size-change="changePageSize"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 底部返回卡片 -->
|
||||
<el-card shadow="never" class="footer-card mt_10">
|
||||
<div class="footer-actions">
|
||||
<el-button @click="back">返回拼团列表</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { onMounted, reactive, ref, computed, getCurrentInstance } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { getPintuanGoodsList, getPintuanDetail } from "@/api/promotion.js";
|
||||
import vueQr from "vue-qr";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { proxy } = getCurrentInstance() || {};
|
||||
|
||||
const loading = ref(false);
|
||||
const pintuanInfo = ref({});
|
||||
const goodsData = ref([]);
|
||||
const total = ref(0);
|
||||
|
||||
const searchForm = reactive({
|
||||
pageNumber: 1,
|
||||
pageSize: 20,
|
||||
});
|
||||
const data = ref([]);
|
||||
const total = ref(0);
|
||||
const goodsData = ref([]);
|
||||
|
||||
const pintuanId = computed(() => {
|
||||
return route.query.id || pintuanInfo.value.id || "";
|
||||
});
|
||||
|
||||
function promotionStatusText(status) {
|
||||
const map = {
|
||||
@@ -103,6 +295,7 @@ function promotionStatusText(status) {
|
||||
};
|
||||
return map[status] || "未知";
|
||||
}
|
||||
|
||||
function promotionStatusTagType(status) {
|
||||
const map = {
|
||||
NEW: "info",
|
||||
@@ -110,34 +303,77 @@ function promotionStatusTagType(status) {
|
||||
END: "danger",
|
||||
CLOSE: "danger",
|
||||
};
|
||||
return map[status] || "danger";
|
||||
return map[status] || "info";
|
||||
}
|
||||
function init() {
|
||||
getDataList();
|
||||
getPintuanMsg();
|
||||
|
||||
function handleLinkTo(goodsId, skuId) {
|
||||
if (proxy && typeof proxy.linkTo === "function") {
|
||||
proxy.linkTo(goodsId, skuId);
|
||||
} else {
|
||||
const pcUrl = window.BASE?.PC_URL || "";
|
||||
const url = skuId
|
||||
? `${pcUrl}/goodsDetail?skuId=${skuId}&goodsId=${goodsId}`
|
||||
: `${pcUrl}/goodsDetail?goodsId=${goodsId}`;
|
||||
window.open(url, "_blank");
|
||||
}
|
||||
}
|
||||
|
||||
function handleWapLinkTo(goodsId, skuId) {
|
||||
if (proxy && typeof proxy.wapLinkTo === "function") {
|
||||
return proxy.wapLinkTo(goodsId, skuId);
|
||||
}
|
||||
const wapUrl = window.BASE?.WAP_URL || "";
|
||||
if (skuId) {
|
||||
return `${wapUrl}/pages/product/goods?id=${skuId}&goodsId=${goodsId}`;
|
||||
}
|
||||
return `${wapUrl}/pages/product/goods?goodsId=${goodsId}`;
|
||||
}
|
||||
|
||||
function back() {
|
||||
if (window.history.length > 1) {
|
||||
router.go(-1);
|
||||
} else {
|
||||
router.push({ name: "manager-pintuan" });
|
||||
}
|
||||
}
|
||||
|
||||
function changePage() {
|
||||
getDataList();
|
||||
}
|
||||
|
||||
function changePageSize() {
|
||||
searchForm.pageNumber = 1;
|
||||
getDataList();
|
||||
}
|
||||
|
||||
function getDataList() {
|
||||
if (!pintuanId.value) return;
|
||||
loading.value = true;
|
||||
searchForm.pintuanId = route.query.id;
|
||||
getPintuanGoodsList(searchForm).then((res) => {
|
||||
loading.value = false;
|
||||
if (res.success) {
|
||||
goodsData.value = res.result.records;
|
||||
total.value = res.result.total;
|
||||
searchForm.pintuanId = pintuanId.value;
|
||||
getPintuanGoodsList(searchForm)
|
||||
.then((res) => {
|
||||
if (res && res.success && res.result) {
|
||||
goodsData.value = res.result.records || [];
|
||||
total.value = res.result.total || 0;
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function getPintuanMsg() {
|
||||
if (!pintuanId.value) return;
|
||||
getPintuanDetail(pintuanId.value).then((res) => {
|
||||
if (res && res.success && res.result) {
|
||||
pintuanInfo.value = res.result;
|
||||
}
|
||||
});
|
||||
}
|
||||
function getPintuanMsg() {
|
||||
getPintuanDetail(route.query.id).then((res) => {
|
||||
if (res.success) data.value.push(res.result);
|
||||
});
|
||||
|
||||
function init() {
|
||||
getPintuanMsg();
|
||||
getDataList();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -146,31 +382,246 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
h4 {
|
||||
margin: 20px 0;
|
||||
padding: 0 10px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
border-left: 3px solid red;
|
||||
.pintuan-goods-page {
|
||||
padding: 16px 20px;
|
||||
background-color: #f0f2f5;
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
|
||||
.mt_10 {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.ml_8 {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
|
||||
}
|
||||
|
||||
.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;
|
||||
|
||||
.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: 120px;
|
||||
background-color: #f8f9fb;
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.el-descriptions__content) {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.rule-desc {
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
.card-header-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.header-title-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.section-indicator {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background-color: #409eff;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.total-badge {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-table {
|
||||
:deep(th.el-table__cell) {
|
||||
background-color: #fafafa;
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.goods-thumb-box {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background-color: #f5f7fa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #ebeef5;
|
||||
margin: 0 auto;
|
||||
|
||||
.goods-thumb-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.thumb-slot {
|
||||
font-size: 11px;
|
||||
color: #c0c4cc;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.goods-link {
|
||||
color: #303133;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.sku-sub-text {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.original-price {
|
||||
color: #909399;
|
||||
text-decoration: line-through;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.pintuan-price {
|
||||
color: #f56c6c;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-card {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-text {
|
||||
color: #409eff;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
.qr-popover-content {
|
||||
text-align: center;
|
||||
padding: 6px 0;
|
||||
|
||||
.mr_10 {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.hover-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mt_10 {
|
||||
margin-top: 10px;
|
||||
.qr-popover-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,70 +1,192 @@
|
||||
<template>
|
||||
<div class="seckill-goods">
|
||||
<el-card>
|
||||
<el-table border :data="data" style="width: 100%">
|
||||
<el-table-column prop="promotionName" label="活动名称" min-width="120" />
|
||||
<el-table-column prop="startTime" label="活动开始时间" min-width="160" />
|
||||
<el-table-column label="报名截止时间" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ unixDate(row.applyEndTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="时间场次" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row">
|
||||
<el-tag v-for="item in unixHours(row.hours)" :key="item" class="hour-tag">{{ item }}</el-tag>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="活动状态" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row" :type="seckillStatusTagType(row.promotionStatus)">
|
||||
{{ seckillStatusText(row.promotionStatus) }}
|
||||
<div class="seckill-goods-page">
|
||||
<!-- 顶部标题与返回栏 -->
|
||||
<el-card shadow="never" class="header-card">
|
||||
<div class="page-header-row">
|
||||
<div class="header-left">
|
||||
<div class="header-title">
|
||||
<span>秒杀活动商品管理</span>
|
||||
<el-tag
|
||||
v-if="seckillInfo.promotionStatus"
|
||||
:type="seckillStatusTagType(seckillInfo.promotionStatus)"
|
||||
size="small"
|
||||
effect="plain"
|
||||
class="status-tag"
|
||||
>
|
||||
{{ seckillStatusText(seckillInfo.promotionStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<span v-if="seckillId" class="activity-id font-mono">ID: {{ seckillId }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-button @click="back">返回秒杀列表</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="活动名称">
|
||||
<span class="font-semibold text-dark">{{ seckillInfo.promotionName || "-" }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="活动状态">
|
||||
<el-tag
|
||||
v-if="seckillInfo.promotionStatus"
|
||||
:type="seckillStatusTagType(seckillInfo.promotionStatus)"
|
||||
size="small"
|
||||
effect="dark"
|
||||
>
|
||||
{{ seckillStatusText(seckillInfo.promotionStatus) }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="活动开始时间">
|
||||
<span class="font-mono text-dark">{{ seckillInfo.startTime || "-" }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="报名截止时间">
|
||||
<span class="font-mono text-dark">{{ unixDate(seckillInfo.applyEndTime) }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="时间场次" :span="2">
|
||||
<div class="hours-tags-row">
|
||||
<template v-if="seckillInfo.hours">
|
||||
<el-tag
|
||||
v-for="item in unixHours(seckillInfo.hours)"
|
||||
:key="item"
|
||||
size="small"
|
||||
effect="light"
|
||||
class="hour-tag"
|
||||
>
|
||||
{{ item }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<span v-else class="text-placeholder">未设置场次</span>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item v-if="seckillInfo.seckillRule" label="规则说明" :span="2">
|
||||
<span class="text-secondary">{{ seckillInfo.seckillRule }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<!-- 秒杀商品列表卡片 -->
|
||||
<el-card shadow="never" class="info-card mt_10">
|
||||
<div class="card-header-bar">
|
||||
<div class="header-title-group">
|
||||
<span class="section-indicator"></span>
|
||||
<span class="section-title">已报名秒杀商品</span>
|
||||
<span class="total-badge">共 {{ total }} 件商品</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button size="small" @click="getDataList">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
ref="table"
|
||||
v-loading="loading"
|
||||
border
|
||||
class="operation"
|
||||
stripe
|
||||
:data="goodsList"
|
||||
style="width: 100%"
|
||||
empty-text="暂无报名的秒杀商品"
|
||||
class="custom-table"
|
||||
>
|
||||
<el-table-column prop="goodsName" label="商品名称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="商品价格" width="110">
|
||||
<!-- 商品图片 -->
|
||||
<el-table-column label="商品图片" width="76" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" :style="{ color: $mainColor }">
|
||||
{{ $filters.unitPrice(row.originalPrice, "¥") }}</span>
|
||||
<div v-if="row && (row.thumbnail || row.small || row.goodsImage)" class="goods-thumb-box">
|
||||
<el-image
|
||||
:src="row.thumbnail || row.small || row.goodsImage"
|
||||
fit="cover"
|
||||
preview-teleported
|
||||
:preview-src-list="[row.thumbnail || row.small || row.goodsImage]"
|
||||
class="goods-thumb-img"
|
||||
>
|
||||
<template #error>
|
||||
<div class="thumb-slot">无图</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
<div v-else class="goods-thumb-box thumb-slot">无图</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="库存" width="90">
|
||||
|
||||
<!-- 商品名称 -->
|
||||
<el-table-column prop="goodsName" label="商品名称" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.quantity }}</span>
|
||||
<span class="font-semibold text-dark">{{ row.goodsName || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="活动价格" width="100">
|
||||
|
||||
<!-- 商家名称 -->
|
||||
<el-table-column prop="storeName" label="所属商家" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" :style="{ color: $mainColor }">
|
||||
{{ $filters.unitPrice(row.price, "¥") }}</span>
|
||||
<span class="text-secondary">{{ row.storeName || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="storeName" label="商家名称" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column label="活动场次" width="100">
|
||||
|
||||
<!-- 活动场次 -->
|
||||
<el-table-column label="活动场次" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row">{{ row.timeLine + ":00" }}</el-tag>
|
||||
<el-tag v-if="row && row.timeLine != null" size="small" effect="plain" type="warning">
|
||||
{{ row.timeLine }}:00 场
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" align="center">
|
||||
|
||||
<!-- 原价 -->
|
||||
<el-table-column label="商品原价" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" class="original-price font-mono">
|
||||
{{ $filters.unitPrice(row.originalPrice, "¥") }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 秒杀价格 -->
|
||||
<el-table-column label="秒杀价格" width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" class="seckill-price font-mono font-semibold">
|
||||
{{ $filters.unitPrice(row.price, "¥") }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 库存 -->
|
||||
<el-table-column label="活动库存" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" class="font-mono text-dark">{{ row.quantity ?? "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 操作 -->
|
||||
<el-table-column label="操作" width="110" align="center" fixed="right">
|
||||
<template #default="{ row, $index }">
|
||||
<a v-if="row" class="link-text" @click="delGoods($index, row)">删除</a>
|
||||
<el-button
|
||||
v-if="row"
|
||||
link
|
||||
type="danger"
|
||||
@click="delGoods($index, row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
||||
<!-- 底部分页 -->
|
||||
<div class="pagination-wrap mt_10">
|
||||
<el-pagination
|
||||
v-model:current-page="searchForm.pageNumber"
|
||||
v-model:page-size="searchForm.pageSize"
|
||||
@@ -77,108 +199,142 @@
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<el-card shadow="never" class="footer-card mt_10">
|
||||
<div class="footer-actions">
|
||||
<el-button type="primary" @click="back">返回秒杀列表</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import {
|
||||
seckillGoodsList,
|
||||
seckillDetail,
|
||||
delSeckillGoods,
|
||||
} from "@/api/promotion.js";
|
||||
import { Message, Modal } from "@/utils/message";
|
||||
import { unixToDate } from "@/utils/filters";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
|
||||
defineOptions({ name: "seckill-goods" });
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const promotionStatus = ref("");
|
||||
const seckillId = route.query.id;
|
||||
const seckillInfo = ref({});
|
||||
const loading = ref(false);
|
||||
const searchForm = reactive({
|
||||
pageNumber: 1,
|
||||
pageSize: 20,
|
||||
});
|
||||
const total = ref(0);
|
||||
const data = ref([]);
|
||||
const goodsList = ref([]);
|
||||
|
||||
function seckillStatusText(status) {
|
||||
const map = {
|
||||
NEW: "新建",
|
||||
START: "开始",
|
||||
END: "结束",
|
||||
CLOSE: "废弃",
|
||||
NEW: "未开始",
|
||||
START: "进行中",
|
||||
END: "已结束",
|
||||
CLOSE: "已关闭",
|
||||
};
|
||||
return map[status] || status || "-";
|
||||
}
|
||||
|
||||
function seckillStatusTagType(status) {
|
||||
const map = {
|
||||
NEW: "danger",
|
||||
NEW: "info",
|
||||
START: "success",
|
||||
END: "danger",
|
||||
CLOSE: "danger",
|
||||
CLOSE: "warning",
|
||||
};
|
||||
return map[status] || "danger";
|
||||
return map[status] || "info";
|
||||
}
|
||||
|
||||
function init() {
|
||||
getSeckillMsg();
|
||||
}
|
||||
|
||||
function changePage() {
|
||||
getDataList();
|
||||
}
|
||||
|
||||
function changePageSize() {
|
||||
searchForm.pageNumber = 1;
|
||||
getDataList();
|
||||
}
|
||||
|
||||
function getDataList() {
|
||||
if (!seckillId) return;
|
||||
loading.value = true;
|
||||
searchForm.seckillId = route.query.id;
|
||||
seckillGoodsList(searchForm).then((res) => {
|
||||
loading.value = false;
|
||||
if (res.success && res.result) {
|
||||
goodsList.value = res.result.records;
|
||||
total.value = res.result.total;
|
||||
}
|
||||
});
|
||||
searchForm.seckillId = seckillId;
|
||||
seckillGoodsList(searchForm)
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
if (res && res.success && res.result) {
|
||||
goodsList.value = res.result.records || [];
|
||||
total.value = res.result.total || 0;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function getSeckillMsg() {
|
||||
seckillDetail(route.query.id).then((res) => {
|
||||
if (res.success && res.result) {
|
||||
data.value = [];
|
||||
data.value.push(res.result);
|
||||
promotionStatus.value = res.result.promotionStatus;
|
||||
if (!seckillId) return;
|
||||
seckillDetail(seckillId).then((res) => {
|
||||
if (res && res.success && res.result) {
|
||||
seckillInfo.value = res.result;
|
||||
getDataList();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function delGoods(index, row) {
|
||||
Modal.confirm({
|
||||
title: "确认删除",
|
||||
content: "您确认要删除该商品吗?删除后不可恢复",
|
||||
onOk: () => {
|
||||
ElMessageBox.confirm(`确认要删除商品 "${row.goodsName || "该商品"}" 吗?删除后不可恢复。`, "确认删除", {
|
||||
type: "warning",
|
||||
confirmButtonText: "确定删除",
|
||||
cancelButtonText: "取消",
|
||||
})
|
||||
.then(() => {
|
||||
const params = {
|
||||
seckillId: row.seckillId,
|
||||
id: row.id,
|
||||
};
|
||||
delSeckillGoods(params).then((res) => {
|
||||
if (res.success) {
|
||||
if (res && res.success) {
|
||||
goodsList.value.splice(index, 1);
|
||||
Message.success("删除成功!");
|
||||
total.value = Math.max(0, total.value - 1);
|
||||
ElMessage.success("删除成功!");
|
||||
} else if (res && res.message) {
|
||||
ElMessage.error(res.message);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function unixDate(time) {
|
||||
if (!time) return "-";
|
||||
return unixToDate(new Date(time) / 1000);
|
||||
}
|
||||
|
||||
function unixHours(item) {
|
||||
const hourArr = item.split(",");
|
||||
for (let i = 0; i < hourArr.length; i++) {
|
||||
hourArr[i] += ":00";
|
||||
}
|
||||
return hourArr;
|
||||
if (!item) return [];
|
||||
const hourArr = String(item).split(",");
|
||||
return hourArr.map((h) => `${h.trim()}:00`);
|
||||
}
|
||||
|
||||
function back() {
|
||||
appStore.removeTag("seckill-goods");
|
||||
router.go(-1);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -187,22 +343,193 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.operation {
|
||||
margin: 10px 0;
|
||||
}
|
||||
.seckill-goods-page {
|
||||
padding-bottom: 20px;
|
||||
|
||||
.hour-tag {
|
||||
margin-right: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.header-card {
|
||||
.page-header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.link-text {
|
||||
color: #409eff;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
.header-left {
|
||||
.header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
|
||||
.mt_10 {
|
||||
margin-top: 10px;
|
||||
.status-tag {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.activity-id {
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
color: #909399;
|
||||
background-color: #f4f4f5;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-card {
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.section-indicator {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
border-radius: 2px;
|
||||
background-color: #409eff;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.card-header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
|
||||
.header-title-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.section-indicator {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
border-radius: 2px;
|
||||
background-color: #409eff;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.total-badge {
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
color: #909399;
|
||||
background-color: #f4f4f5;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-descriptions {
|
||||
:deep(.el-descriptions__label) {
|
||||
width: 140px;
|
||||
background-color: #fafafa;
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.el-descriptions__content) {
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.hours-tags-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
|
||||
.hour-tag {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-table {
|
||||
.goods-thumb-box {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
margin: 0 auto;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
background-color: #fafafa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
|
||||
.goods-thumb-img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.thumb-slot {
|
||||
font-size: 11px;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.original-price {
|
||||
color: #909399;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.seckill-price {
|
||||
color: #f56c6c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-card {
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-dark {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.text-placeholder {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.pagination-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<el-card class="card">
|
||||
<div>
|
||||
<h4>交易概况</h4>
|
||||
<div class="flex">
|
||||
<div class="flex" style="align-items: flex-start;">
|
||||
<div class="transactionList">
|
||||
<div
|
||||
class="transaction-item"
|
||||
@@ -98,45 +98,159 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="shap">
|
||||
<div id="overViewChart">
|
||||
<!-- 上 -->
|
||||
<div class="block">
|
||||
<div class="box">
|
||||
<span>访客数</span>
|
||||
<span>{{ overViewList.uvNum || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 中 -->
|
||||
<div class="block">
|
||||
<div class="box">
|
||||
<span>下单笔数</span>
|
||||
<span>{{ overViewList.orderNum || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 下 -->
|
||||
<div class="block">
|
||||
<div class="box">
|
||||
<span>付款笔数</span>
|
||||
<span>{{ overViewList.paymentOrderNum || 0 }}</span>
|
||||
</div>
|
||||
<div class="funnel-card">
|
||||
<div class="funnel-header">
|
||||
<h4>交易转化漏斗</h4>
|
||||
<span class="funnel-sub">全链路转化分析</span>
|
||||
</div>
|
||||
<div class="funnel-body">
|
||||
<svg
|
||||
class="funnel-svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 460 236"
|
||||
width="460"
|
||||
height="236"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="gradTier1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#FF5C58" />
|
||||
<stop offset="100%" stop-color="#E83D39" />
|
||||
</linearGradient>
|
||||
<linearGradient id="gradTier2" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#FF8259" />
|
||||
<stop offset="100%" stop-color="#F25B30" />
|
||||
</linearGradient>
|
||||
<linearGradient id="gradTier3" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#FFA454" />
|
||||
<stop offset="100%" stop-color="#F28528" />
|
||||
</linearGradient>
|
||||
|
||||
<!-- 线 -->
|
||||
<div class="rightBorder"></div>
|
||||
<div class="leftTopBorder"></div>
|
||||
<div class="leftBottomBorder"></div>
|
||||
<!--数据 -->
|
||||
<div class="leftTopTips">
|
||||
<div>下单转化率</div>
|
||||
<div>{{ overViewList.orderConversionRate || "0%" }}</div>
|
||||
<filter id="funnelShadow" x="-10%" y="-10%" width="120%" height="130%">
|
||||
<feDropShadow dx="0" dy="2" stdDeviation="2.5" flood-opacity="0.12" />
|
||||
</filter>
|
||||
<filter id="badgeShadow" x="-10%" y="-10%" width="120%" height="130%">
|
||||
<feDropShadow dx="0" dy="1" stdDeviation="2" flood-opacity="0.06" />
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- 连线:左侧 访客 -> 下单 -->
|
||||
<path
|
||||
class="bracket-line"
|
||||
d="M 138 42 L 98 42 Q 90 42 90 50 L 90 102 Q 90 110 98 110 L 161 110"
|
||||
/>
|
||||
|
||||
<!-- 连线:左侧 下单 -> 付款 -->
|
||||
<path
|
||||
class="bracket-line"
|
||||
d="M 161 110 L 98 110 Q 90 110 90 118 L 90 170 Q 90 178 98 178 L 182 178"
|
||||
/>
|
||||
|
||||
<!-- 连线:右侧 全店转化率 -->
|
||||
<path
|
||||
class="bracket-line"
|
||||
d="M 322 42 L 362 42 Q 370 42 370 50 L 370 170 Q 370 178 362 178 L 278 178"
|
||||
/>
|
||||
|
||||
<!-- 漏斗层级 1:访客数 -->
|
||||
<g class="funnel-tier">
|
||||
<polygon
|
||||
points="128,14 332,14 312,70 148,70"
|
||||
fill="url(#gradTier1)"
|
||||
stroke="url(#gradTier1)"
|
||||
stroke-width="6"
|
||||
stroke-linejoin="round"
|
||||
filter="url(#funnelShadow)"
|
||||
/>
|
||||
<text x="230" y="37" text-anchor="middle" class="funnel-label">访客数 UV</text>
|
||||
<text x="230" y="59" text-anchor="middle" class="funnel-value">
|
||||
{{ overViewList.uvNum || 0 }}
|
||||
</text>
|
||||
</g>
|
||||
|
||||
<!-- 漏斗层级 2:下单笔数 -->
|
||||
<g class="funnel-tier">
|
||||
<polygon
|
||||
points="152,82 308,82 290,138 170,138"
|
||||
fill="url(#gradTier2)"
|
||||
stroke="url(#gradTier2)"
|
||||
stroke-width="6"
|
||||
stroke-linejoin="round"
|
||||
filter="url(#funnelShadow)"
|
||||
/>
|
||||
<text x="230" y="105" text-anchor="middle" class="funnel-label">下单笔数</text>
|
||||
<text x="230" y="127" text-anchor="middle" class="funnel-value">
|
||||
{{ overViewList.orderNum || 0 }}
|
||||
</text>
|
||||
</g>
|
||||
|
||||
<!-- 漏斗层级 3:付款笔数 -->
|
||||
<g class="funnel-tier">
|
||||
<polygon
|
||||
points="174,150 286,150 270,206 190,206"
|
||||
fill="url(#gradTier3)"
|
||||
stroke="url(#gradTier3)"
|
||||
stroke-width="6"
|
||||
stroke-linejoin="round"
|
||||
filter="url(#funnelShadow)"
|
||||
/>
|
||||
<text x="230" y="173" text-anchor="middle" class="funnel-label">付款笔数</text>
|
||||
<text x="230" y="195" text-anchor="middle" class="funnel-value">
|
||||
{{ overViewList.paymentOrderNum || 0 }}
|
||||
</text>
|
||||
</g>
|
||||
|
||||
<!-- 左侧卡片 1:下单转化率 -->
|
||||
<g transform="translate(6, 57)" filter="url(#badgeShadow)">
|
||||
<rect width="80" height="38" class="badge-bg" />
|
||||
<text x="40" y="16" text-anchor="middle" class="badge-title">下单转化率</text>
|
||||
<text x="40" y="31" text-anchor="middle" class="badge-value">
|
||||
{{ overViewList.orderConversionRate || "0%" }}
|
||||
</text>
|
||||
</g>
|
||||
|
||||
<!-- 左侧卡片 2:付款转化率 -->
|
||||
<g transform="translate(6, 125)" filter="url(#badgeShadow)">
|
||||
<rect width="80" height="38" class="badge-bg" />
|
||||
<text x="40" y="16" text-anchor="middle" class="badge-title">付款转化率</text>
|
||||
<text x="40" y="31" text-anchor="middle" class="badge-value">
|
||||
{{ overViewList.paymentsConversionRate || "0%" }}
|
||||
</text>
|
||||
</g>
|
||||
|
||||
<!-- 右侧卡片:全店转化率 -->
|
||||
<g transform="translate(374, 91)" filter="url(#badgeShadow)">
|
||||
<rect width="80" height="38" class="badge-bg-highlight" />
|
||||
<text x="40" y="16" text-anchor="middle" class="badge-title">全店转化率</text>
|
||||
<text x="40" y="31" text-anchor="middle" class="badge-value-highlight">
|
||||
{{ overViewList.overallConversionRate || "0%" }}
|
||||
</text>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="leftBottomTips">
|
||||
<div>付款转化率</div>
|
||||
<div>{{ overViewList.paymentsConversionRate || "0%" }}</div>
|
||||
</div>
|
||||
<div class="rightTips">
|
||||
<div>整体转换率</div>
|
||||
<div>{{ overViewList.overallConversionRate || "0%" }}</div>
|
||||
<div class="funnel-summary">
|
||||
<div class="summary-col">
|
||||
<div class="dot dot-tier1"></div>
|
||||
<div class="summary-text">
|
||||
<span class="label">访客UV</span>
|
||||
<span class="val">{{ overViewList.uvNum || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-arrow">➔</div>
|
||||
<div class="summary-col">
|
||||
<div class="dot dot-tier2"></div>
|
||||
<div class="summary-text">
|
||||
<span class="label">下单笔数</span>
|
||||
<span class="val">{{ overViewList.orderNum || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-arrow">➔</div>
|
||||
<div class="summary-col">
|
||||
<div class="dot dot-tier3"></div>
|
||||
<div class="summary-text">
|
||||
<span class="label">付款笔数</span>
|
||||
<span class="val">{{ overViewList.paymentOrderNum || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -553,128 +667,168 @@ onMounted(() => {
|
||||
text-align: right;
|
||||
margin: 20px 0;
|
||||
}
|
||||
#overViewChart {
|
||||
.shap {
|
||||
width: 480px;
|
||||
flex-shrink: 0;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.funnel-card {
|
||||
background: #fbfcfe;
|
||||
border: 1px solid #ebedf0;
|
||||
border-radius: 8px;
|
||||
padding: 16px 14px 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.funnel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
padding: 0 6px;
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.funnel-sub {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.funnel-body {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.funnel-svg {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
|
||||
.funnel-label {
|
||||
font-size: 13px;
|
||||
fill: rgba(255, 255, 255, 0.94);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.funnel-value {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
fill: #ffffff;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.bracket-line {
|
||||
fill: none;
|
||||
stroke: #cbd5e1;
|
||||
stroke-width: 1.5;
|
||||
stroke-dasharray: 4, 3;
|
||||
}
|
||||
|
||||
.badge-bg {
|
||||
fill: #ffffff;
|
||||
stroke: #e2e8f0;
|
||||
stroke-width: 1;
|
||||
rx: 6px;
|
||||
}
|
||||
|
||||
.badge-bg-highlight {
|
||||
fill: #fff7f6;
|
||||
stroke: #ffccc7;
|
||||
stroke-width: 1;
|
||||
rx: 6px;
|
||||
}
|
||||
|
||||
.badge-title {
|
||||
font-size: 12px;
|
||||
fill: #64748b;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.badge-value {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
fill: #1e293b;
|
||||
}
|
||||
|
||||
.badge-value-highlight {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
fill: #e83d39;
|
||||
}
|
||||
}
|
||||
|
||||
.funnel-tier {
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, filter 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
filter: brightness(1.04);
|
||||
}
|
||||
}
|
||||
|
||||
.funnel-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
margin-left: 20px;
|
||||
> .leftTopTips {
|
||||
position: absolute;
|
||||
top: 68px;
|
||||
left: -2px;
|
||||
width: 85px;
|
||||
text-align: center;
|
||||
background: rgb(255, 255, 255);
|
||||
z-index: 1;
|
||||
padding: 5px 0px;
|
||||
}
|
||||
> .leftBottomTips {
|
||||
position: absolute;
|
||||
bottom: 100px;
|
||||
left: -2px;
|
||||
width: 85px;
|
||||
text-align: center;
|
||||
background: rgb(255, 255, 255);
|
||||
z-index: 1;
|
||||
padding: 5px 0px;
|
||||
}
|
||||
> .rightTips {
|
||||
position: absolute;
|
||||
bottom: 240px;
|
||||
right: 0px;
|
||||
width: 85px;
|
||||
text-align: center;
|
||||
background: rgb(255, 255, 255);
|
||||
z-index: 1;
|
||||
padding: 5px 0px;
|
||||
}
|
||||
> .rightBorder {
|
||||
width: 110px;
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
right: 40px;
|
||||
border: 2px solid #d9d9d9;
|
||||
border-left: 0;
|
||||
height: 280px;
|
||||
}
|
||||
> .leftTopBorder {
|
||||
border: 2px solid #d9d9d9;
|
||||
height: 118px;
|
||||
width: 56px;
|
||||
position: absolute;
|
||||
left: 40px;
|
||||
top: 30px;
|
||||
border-right: 0;
|
||||
}
|
||||
> .leftBottomBorder {
|
||||
width: 108px;
|
||||
border: 2px solid #d9d9d9;
|
||||
height: 150px;
|
||||
position: absolute;
|
||||
bottom: 45px;
|
||||
left: 40px;
|
||||
border-right: 0;
|
||||
}
|
||||
> .block {
|
||||
height: 0px;
|
||||
border-left: 30px solid transparent;
|
||||
border-right: 30px solid transparent;
|
||||
position: relative;
|
||||
background: rgb(255, 255, 255);
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
}
|
||||
> .block:nth-of-type(1) {
|
||||
width: 240px;
|
||||
border-top: 90px solid #ff4646;
|
||||
> .box {
|
||||
left: -30px;
|
||||
top: -90px;
|
||||
width: 240px;
|
||||
height: 90px;
|
||||
}
|
||||
}
|
||||
> .block:nth-of-type(2) {
|
||||
width: 172px;
|
||||
border-top: 100px solid #ff8585;
|
||||
margin-top: 10px;
|
||||
> .box {
|
||||
left: -29px;
|
||||
top: -100px;
|
||||
width: 172px;
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
> .block:nth-of-type(3) {
|
||||
width: 100px;
|
||||
margin-top: 10px;
|
||||
border-top: 150px solid #ffb396;
|
||||
border-left: 50px solid transparent;
|
||||
border-right: 50px solid transparent;
|
||||
justify-content: space-around;
|
||||
margin-top: 12px;
|
||||
padding: 10px 14px;
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #f0f2f5;
|
||||
|
||||
> .box {
|
||||
left: -50px;
|
||||
top: -150px;
|
||||
width: 100px;
|
||||
height: 120px;
|
||||
z-index: 2;
|
||||
.summary-col {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.dot-tier1 {
|
||||
background: #ff5c58;
|
||||
}
|
||||
&.dot-tier2 {
|
||||
background: #ff8259;
|
||||
}
|
||||
&.dot-tier3 {
|
||||
background: #ffa454;
|
||||
}
|
||||
}
|
||||
:deep(.box){
|
||||
color: #fff;
|
||||
position: absolute;
|
||||
|
||||
z-index: 2;
|
||||
.summary-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
> span {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
|
||||
.label {
|
||||
font-size: 11px;
|
||||
color: #909399;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.val {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
line-height: 1.3;
|
||||
}
|
||||
}
|
||||
|
||||
.summary-arrow {
|
||||
color: #dcdfe6;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -685,13 +839,9 @@ h4 {
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
.transactionList {
|
||||
flex: 7;
|
||||
padding: 0 20px;
|
||||
}
|
||||
.shap {
|
||||
width: 400px;
|
||||
margin-left: 20px;
|
||||
margin-top: 50px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.transaction-card {
|
||||
@@ -702,7 +852,7 @@ h4 {
|
||||
}
|
||||
.card-item-label {
|
||||
font-weight: bold;
|
||||
font-size: #666;
|
||||
color: #666;
|
||||
font-size: 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@@ -1,113 +1,138 @@
|
||||
<template>
|
||||
<div class="search">
|
||||
<el-card>
|
||||
<el-row class="operation">
|
||||
<el-switch
|
||||
v-model="strict"
|
||||
active-text="级联"
|
||||
inactive-text="单选"
|
||||
style="margin-right: 5px"
|
||||
/>
|
||||
<el-button @click="addRoot">添加部门</el-button>
|
||||
<el-button type="primary" @click="add">添加子部门</el-button>
|
||||
<el-button @click="delAll">批量删除</el-button>
|
||||
<el-button @click="getParentList">刷新</el-button>
|
||||
</el-row>
|
||||
<el-row type="flex" justify="start">
|
||||
<el-col :md="8" :lg="8" :xl="6">
|
||||
<el-alert type="info" show-icon :closable="false">
|
||||
当前选择编辑:
|
||||
<span class="select-title">{{ editTitle }}</span>
|
||||
<a v-if="form.id" class="select-clear" @click="cancelSelect">取消选择</a>
|
||||
</el-alert>
|
||||
<div v-loading="loading" class="tree-bar" :style="{ maxHeight: maxHeight }">
|
||||
<el-tree
|
||||
ref="treeRef"
|
||||
:data="data"
|
||||
:props="treeProps"
|
||||
node-key="id"
|
||||
show-checkbox
|
||||
highlight-current
|
||||
default-expand-all
|
||||
:check-strictly="!strict"
|
||||
:current-node-key="currentNodeKey"
|
||||
@node-click="onNodeClick"
|
||||
@check="onCheckChange"
|
||||
/>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :md="15" :lg="13" :xl="9" style="margin-left: 10px">
|
||||
<el-form ref="formRef" :model="form" label-width="100px" :rules="formValidate">
|
||||
<el-form-item label="部门名称" prop="title">
|
||||
<el-input v-model="form.title" />
|
||||
</el-form-item>
|
||||
<el-form-item label="选择角色">
|
||||
<el-select
|
||||
v-model="selectedRole"
|
||||
multiple
|
||||
:loading="userLoading"
|
||||
placeholder="请选择角色"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in users"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序值" prop="sortOrder">
|
||||
<el-tooltip placement="right" content="值越小越靠前,支持小数">
|
||||
<el-input-number v-model="form.sortOrder" :max="1000" :min="0" />
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
style="margin-right: 5px"
|
||||
:loading="submitLoading"
|
||||
@click="submitEdit"
|
||||
>
|
||||
修改并保存
|
||||
</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div class="department-manage">
|
||||
<el-card shadow="never" class="manage-card">
|
||||
<!-- 顶部操作工具栏 -->
|
||||
<div class="table-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<el-button type="primary" @click="addRoot">添加一级部门</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
:disabled="selectCount === 0"
|
||||
@click="delAll"
|
||||
>
|
||||
批量删除 {{ selectCount > 0 ? `(${selectCount})` : "" }}
|
||||
</el-button>
|
||||
<el-button @click="toggleExpandAll">
|
||||
{{ isAllExpanded ? "折叠全部" : "展开全部" }}
|
||||
</el-button>
|
||||
<el-button @click="getParentList">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-right">
|
||||
<el-input
|
||||
v-model="searchQuery"
|
||||
placeholder="按部门名称筛选..."
|
||||
clearable
|
||||
:prefix-icon="Search"
|
||||
class="search-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 纯表格展示:树形表格 -->
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
v-loading="loading"
|
||||
:data="filteredData"
|
||||
row-key="id"
|
||||
default-expand-all
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
border
|
||||
stripe
|
||||
class="dept-table"
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="50" align="center" />
|
||||
|
||||
<el-table-column prop="title" label="部门名称" min-width="260" />
|
||||
|
||||
<el-table-column prop="sortOrder" label="排序值" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.sortOrder ?? 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="createTime" label="创建时间" width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.createTime || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<a class="link-text" @click="addChild(row)">添加子部门</a>
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text" @click="editRow(row)">编辑</a>
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text error-text" @click="deleteRow(row)">删除</a>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 部门新建 / 编辑弹窗 -->
|
||||
<el-dialog
|
||||
v-model="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="500px"
|
||||
width="540px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form ref="formAddRef" :model="formAdd" label-width="85px" :rules="formValidate">
|
||||
<div v-if="showParent">
|
||||
<el-form-item label="上级部门:">{{ form.title }}</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="部门名称" prop="title">
|
||||
<el-input v-model="formAdd.title" />
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
label-width="95px"
|
||||
:rules="formValidate"
|
||||
>
|
||||
<el-form-item v-if="form.parentTitle" label="上级部门">
|
||||
<el-input :model-value="form.parentTitle" disabled />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="部门名称" prop="title">
|
||||
<el-input v-model="form.title" placeholder="请输入部门名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="选择角色">
|
||||
<el-select
|
||||
v-model="selectedRole"
|
||||
multiple
|
||||
:loading="userLoading"
|
||||
placeholder="请选择部门关联角色"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in users"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="排序值" prop="sortOrder">
|
||||
<el-tooltip placement="right" content="值越小越靠前,支持小数">
|
||||
<el-input-number v-model="formAdd.sortOrder" :max="1000" :min="0" />
|
||||
<el-tooltip placement="right" content="值越小越靠前">
|
||||
<el-input-number v-model="form.sortOrder" :min="0" :max="1000" />
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="cancelAdd">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="submitAdd">提交</el-button>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="modalVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="submitForm">
|
||||
确定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { ref, reactive, computed, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Search } from "@element-plus/icons-vue";
|
||||
import {
|
||||
initDepartment,
|
||||
addDepartment,
|
||||
@@ -121,35 +146,34 @@ import {
|
||||
defineOptions({ name: "department-manage" });
|
||||
|
||||
const loading = ref(true);
|
||||
const maxHeight = ref("500px");
|
||||
const strict = ref(true);
|
||||
const submitLoading = ref(false);
|
||||
const userLoading = ref(false);
|
||||
const loadingEdit = ref(false);
|
||||
const modalVisible = ref(false);
|
||||
const modalTitle = ref("");
|
||||
const isEditMode = ref(false);
|
||||
|
||||
const data = ref([]);
|
||||
const users = ref([]);
|
||||
const selectedRole = ref([]);
|
||||
const selectList = ref([]);
|
||||
const selectCount = ref(0);
|
||||
const showParent = ref(false);
|
||||
const modalTitle = ref("");
|
||||
const editTitle = ref("");
|
||||
const selectedRole = ref([]);
|
||||
const searchKey = ref("");
|
||||
const currentNodeKey = ref(null);
|
||||
const treeProps = reactive({
|
||||
label: "title",
|
||||
children: "children",
|
||||
isLeaf: "isLeaf",
|
||||
});
|
||||
const searchQuery = ref("");
|
||||
const isAllExpanded = ref(true);
|
||||
|
||||
const tableRef = ref(null);
|
||||
const formRef = ref(null);
|
||||
|
||||
const form = ref({
|
||||
id: "",
|
||||
title: "",
|
||||
parentId: "",
|
||||
parentId: "0",
|
||||
parentTitle: "",
|
||||
sortOrder: 0,
|
||||
status: 0,
|
||||
});
|
||||
const formAdd = ref({});
|
||||
|
||||
const formValidate = reactive({
|
||||
title: [{ required: true, message: "名称不能为空", trigger: "blur" }],
|
||||
title: [{ required: true, message: "部门名称不能为空", trigger: "blur" }],
|
||||
sortOrder: [
|
||||
{
|
||||
required: true,
|
||||
@@ -159,26 +183,58 @@ const formValidate = reactive({
|
||||
},
|
||||
],
|
||||
});
|
||||
const submitLoading = ref(false);
|
||||
const data = ref([]);
|
||||
const dataEdit = ref([]);
|
||||
const users = ref([]);
|
||||
|
||||
const treeRef = ref(null);
|
||||
const formRef = ref(null);
|
||||
const formAddRef = ref(null);
|
||||
// 前端树形过滤
|
||||
const filteredData = computed(() => {
|
||||
const query = searchQuery.value.trim().toLowerCase();
|
||||
if (!query) {
|
||||
return data.value;
|
||||
}
|
||||
const filterTree = (nodes) => {
|
||||
return nodes
|
||||
.map((node) => {
|
||||
const matches = node.title && node.title.toLowerCase().includes(query);
|
||||
const filteredChildren = node.children ? filterTree(node.children) : [];
|
||||
if (matches || filteredChildren.length > 0) {
|
||||
return {
|
||||
...node,
|
||||
children: filteredChildren.length > 0 ? filteredChildren : undefined,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
};
|
||||
return filterTree(data.value);
|
||||
});
|
||||
|
||||
function init() {
|
||||
getParentList();
|
||||
loadRoles();
|
||||
}
|
||||
|
||||
function getParentList() {
|
||||
loading.value = true;
|
||||
initDepartment().then((res) => {
|
||||
loading.value = false;
|
||||
initDepartment()
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
if (res.success) {
|
||||
const list = Array.isArray(res.result) ? res.result : [];
|
||||
data.value = bubbleSort(normalizeDepartmentTree(list));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function loadRoles() {
|
||||
getRoleList({
|
||||
pageNumber: 1,
|
||||
pageSize: 10000,
|
||||
}).then((res) => {
|
||||
if (res.success) {
|
||||
const list = Array.isArray(res.result) ? res.result : [];
|
||||
data.value = bubbleSort(normalizeDepartmentTree(list));
|
||||
users.value = res.result.records || [];
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -195,47 +251,6 @@ function normalizeDepartmentTree(list) {
|
||||
});
|
||||
}
|
||||
|
||||
function onNodeClick(data) {
|
||||
if (!data) {
|
||||
cancelSelect();
|
||||
return;
|
||||
}
|
||||
const row = JSON.parse(JSON.stringify(data));
|
||||
for (const attr in row) {
|
||||
if (row[attr] == null) {
|
||||
row[attr] = "";
|
||||
}
|
||||
}
|
||||
editTitle.value = row.title;
|
||||
currentNodeKey.value = row.id;
|
||||
userLoading.value = true;
|
||||
|
||||
getUserByDepartmentId(row.id).then((res) => {
|
||||
const way = [];
|
||||
res.result &&
|
||||
res.result.forEach((item) => {
|
||||
way.push(item.roleId);
|
||||
});
|
||||
selectedRole.value = way;
|
||||
});
|
||||
|
||||
getRoleList({
|
||||
pageNumber: 1,
|
||||
pageSize: 10000,
|
||||
}).then((res) => {
|
||||
userLoading.value = false;
|
||||
if (res.success) {
|
||||
users.value = res.result.records;
|
||||
form.value = row;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onCheckChange(_data, { checkedNodes }) {
|
||||
selectList.value = checkedNodes;
|
||||
selectCount.value = checkedNodes.length;
|
||||
}
|
||||
|
||||
function bubbleSort(array) {
|
||||
if (!Array.isArray(array)) return [];
|
||||
const len = array.length;
|
||||
@@ -252,139 +267,263 @@ function bubbleSort(array) {
|
||||
return array;
|
||||
}
|
||||
|
||||
function cancelSelect() {
|
||||
treeRef.value?.setCurrentKey(null);
|
||||
currentNodeKey.value = null;
|
||||
formRef.value.resetFields();
|
||||
delete form.value.id;
|
||||
editTitle.value = "";
|
||||
selectedRole.value = [];
|
||||
function onSelectionChange(selection) {
|
||||
selectList.value = selection;
|
||||
selectCount.value = selection.length;
|
||||
}
|
||||
|
||||
function selectTreeEdit(v) {
|
||||
if (v.length > 0) {
|
||||
for (const attr in v[0]) {
|
||||
if (v[0][attr] == null) {
|
||||
v[0][attr] = "";
|
||||
function toggleExpandAll() {
|
||||
isAllExpanded.value = !isAllExpanded.value;
|
||||
const toggleRows = (rows) => {
|
||||
rows.forEach((row) => {
|
||||
tableRef.value?.toggleRowExpansion(row, isAllExpanded.value);
|
||||
if (row.children && row.children.length) {
|
||||
toggleRows(row.children);
|
||||
}
|
||||
}
|
||||
const data = JSON.parse(JSON.stringify(v[0]));
|
||||
form.value.parentId = data.id;
|
||||
form.value.parentTitle = data.title;
|
||||
}
|
||||
});
|
||||
};
|
||||
toggleRows(data.value);
|
||||
}
|
||||
|
||||
function cancelAdd() {
|
||||
modalVisible.value = false;
|
||||
// 添加一级部门
|
||||
function addRoot() {
|
||||
isEditMode.value = false;
|
||||
modalTitle.value = "添加一级部门";
|
||||
form.value = {
|
||||
id: "",
|
||||
title: "",
|
||||
parentId: 0,
|
||||
parentTitle: "",
|
||||
sortOrder: 0,
|
||||
status: 0,
|
||||
};
|
||||
selectedRole.value = [];
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
formRef.value.resetFields();
|
||||
form.value.status = 0;
|
||||
// 添加子部门
|
||||
function addChild(row) {
|
||||
isEditMode.value = false;
|
||||
modalTitle.value = `添加子部门 (上级: ${row.title})`;
|
||||
form.value = {
|
||||
id: "",
|
||||
title: "",
|
||||
parentId: row.id,
|
||||
parentTitle: row.title,
|
||||
sortOrder: 0,
|
||||
status: 0,
|
||||
};
|
||||
selectedRole.value = [];
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function submitEdit() {
|
||||
// 编辑当前部门
|
||||
function editRow(row) {
|
||||
isEditMode.value = true;
|
||||
modalTitle.value = `编辑部门 - ${row.title}`;
|
||||
form.value = {
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
parentId: row.parentId,
|
||||
parentTitle: "",
|
||||
sortOrder: row.sortOrder ?? 0,
|
||||
status: row.status ?? 0,
|
||||
};
|
||||
selectedRole.value = [];
|
||||
userLoading.value = true;
|
||||
modalVisible.value = true;
|
||||
|
||||
getUserByDepartmentId(row.id)
|
||||
.then((res) => {
|
||||
userLoading.value = false;
|
||||
if (res && res.result) {
|
||||
selectedRole.value = res.result.map((item) => item.roleId);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
userLoading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
// 提交表单(统一处理新增与修改)
|
||||
function submitForm() {
|
||||
formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
if (!form.value.id) {
|
||||
ElMessage.warning("请先点击选择要修改的部门");
|
||||
return;
|
||||
}
|
||||
const roleWay = [];
|
||||
selectedRole.value.forEach((item) => {
|
||||
roleWay.push({
|
||||
departmentId: form.value.id,
|
||||
roleId: item,
|
||||
});
|
||||
});
|
||||
|
||||
Promise.all([
|
||||
editDepartment(form.value.id, form.value),
|
||||
updateDepartmentRole(form.value.id, roleWay),
|
||||
]).then((res) => {
|
||||
submitLoading.value = false;
|
||||
if (res[0].success) {
|
||||
ElMessage.success("编辑成功");
|
||||
init();
|
||||
modalVisible.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function submitAdd() {
|
||||
formAddRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true;
|
||||
addDepartment(formAdd.value).then((res) => {
|
||||
submitLoading.value = false;
|
||||
if (res.success) {
|
||||
ElMessage.success("添加成功");
|
||||
init();
|
||||
modalVisible.value = false;
|
||||
}
|
||||
});
|
||||
if (isEditMode.value) {
|
||||
// 编辑模式
|
||||
const roleWay = selectedRole.value.map((roleId) => ({
|
||||
departmentId: form.value.id,
|
||||
roleId,
|
||||
}));
|
||||
Promise.all([
|
||||
editDepartment(form.value.id, form.value),
|
||||
updateDepartmentRole(form.value.id, roleWay),
|
||||
])
|
||||
.then((res) => {
|
||||
submitLoading.value = false;
|
||||
if (res[0].success) {
|
||||
ElMessage.success("编辑部门成功");
|
||||
modalVisible.value = false;
|
||||
getParentList();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
submitLoading.value = false;
|
||||
});
|
||||
} else {
|
||||
// 新增模式
|
||||
addDepartment(form.value)
|
||||
.then((res) => {
|
||||
submitLoading.value = false;
|
||||
if (res.success) {
|
||||
ElMessage.success("添加部门成功");
|
||||
modalVisible.value = false;
|
||||
getParentList();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
submitLoading.value = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function add() {
|
||||
if (form.value.id == "" || form.value.id == null) {
|
||||
ElMessage.warning("请先点击选择一个部门");
|
||||
return;
|
||||
}
|
||||
modalTitle.value = "添加子部门";
|
||||
showParent.value = true;
|
||||
formAdd.value = {
|
||||
parentId: form.value.id,
|
||||
sortOrder: 0,
|
||||
status: 0,
|
||||
};
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function addRoot() {
|
||||
modalTitle.value = "添加一级部门";
|
||||
showParent.value = false;
|
||||
formAdd.value = {
|
||||
parentId: 0,
|
||||
sortOrder: 0,
|
||||
status: 0,
|
||||
};
|
||||
modalVisible.value = true;
|
||||
// 单个删除
|
||||
function deleteRow(row) {
|
||||
ElMessageBox.confirm(
|
||||
`您确认要删除部门 "${row.title}" 及其下级所有数据?`,
|
||||
"确认删除",
|
||||
{
|
||||
type: "warning",
|
||||
confirmButtonText: "确定删除",
|
||||
cancelButtonText: "取消",
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
deleteDepartment(row.id).then((res) => {
|
||||
if (res.success) {
|
||||
ElMessage.success("删除成功");
|
||||
getParentList();
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
function delAll() {
|
||||
if (selectCount.value <= 0) {
|
||||
ElMessage.warning("您还未勾选要删除的数据");
|
||||
return;
|
||||
}
|
||||
ElMessageBox.confirm("您确认要删除所选的 " + selectCount.value + " 条数据及其下级所有数据?", "确认删除", { type: "warning" }).then(() => {
|
||||
let ids = "";
|
||||
selectList.value.forEach(function (e) {
|
||||
ids += e.id + ",";
|
||||
});
|
||||
ids = ids.substring(0, ids.length - 1);
|
||||
ElMessageBox.confirm(
|
||||
`您确认要删除所选的 ${selectCount.value} 条部门数据及其下级所有数据?`,
|
||||
"确认删除",
|
||||
{
|
||||
type: "warning",
|
||||
confirmButtonText: "确定删除",
|
||||
cancelButtonText: "取消",
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
const ids = selectList.value.map((e) => e.id).join(",");
|
||||
deleteDepartment(ids).then((res) => {
|
||||
if (res.success) {
|
||||
ElMessage.success("删除成功");
|
||||
selectList.value = [];
|
||||
selectCount.value = 0;
|
||||
cancelSelect();
|
||||
init();
|
||||
getParentList();
|
||||
}
|
||||
});
|
||||
}).catch(() => {});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const height = document.documentElement.clientHeight;
|
||||
maxHeight.value = Number(height - 287) + "px";
|
||||
init();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/tree-common.scss";
|
||||
.department-manage {
|
||||
width: 100%;
|
||||
|
||||
.manage-card {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ebeef5;
|
||||
background: #fff;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 18px 20px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.search-input {
|
||||
width: 240px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dept-table {
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-table__row) {
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.link-text {
|
||||
color: #2d8cf0;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
display: inline-block;
|
||||
|
||||
&:hover {
|
||||
color: #57a3f3;
|
||||
}
|
||||
|
||||
&.error-text {
|
||||
color: #ed4014;
|
||||
|
||||
&:hover {
|
||||
color: #f16643;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.op-split {
|
||||
margin: 0 8px;
|
||||
color: #dcdee2;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,245 +1,215 @@
|
||||
<template>
|
||||
<div class="search">
|
||||
<el-card>
|
||||
<el-row class="operation">
|
||||
<el-switch
|
||||
v-model="strict"
|
||||
class="selectModel"
|
||||
active-text="级联"
|
||||
inactive-text="单选"
|
||||
style="margin-right: 5px"
|
||||
/>
|
||||
<el-button @click="addRootMenu">添加顶级菜单</el-button>
|
||||
<el-button type="primary" @click="addMenu">添加子菜单</el-button>
|
||||
<el-button @click="delAll">批量删除</el-button>
|
||||
<el-dropdown @command="handleDropdown">
|
||||
<el-button>
|
||||
更多操作
|
||||
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
|
||||
<div class="menu-manage-container">
|
||||
<el-card shadow="never" class="manage-card">
|
||||
<!-- 顶部操作工具栏 -->
|
||||
<div class="table-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<el-button type="primary" @click="addRootMenu">添加顶级菜单</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
:disabled="selectCount === 0"
|
||||
@click="delAll"
|
||||
>
|
||||
批量删除 {{ selectCount > 0 ? `(${selectCount})` : "" }}
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="refresh">刷新</el-dropdown-item>
|
||||
<el-dropdown-item command="expandOne">收合所有</el-dropdown-item>
|
||||
<el-dropdown-item command="expandTwo">展开一级</el-dropdown-item>
|
||||
<el-dropdown-item command="expandThree">展开两级</el-dropdown-item>
|
||||
<el-dropdown-item command="expandAll">展开所有</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-row>
|
||||
<el-row type="flex" justify="start">
|
||||
<el-col :md="8" :lg="8" :xl="6">
|
||||
<el-alert type="info" show-icon :closable="false">
|
||||
当前选择编辑:
|
||||
<span class="select-title">{{ editTitle }}</span>
|
||||
<a class="select-clear" v-if="form.id" @click="cancelEdit">取消选择</a>
|
||||
</el-alert>
|
||||
<el-button @click="toggleExpandAll">
|
||||
{{ isAllExpanded ? "折叠全部" : "展开全部" }}
|
||||
</el-button>
|
||||
<el-button @click="getAllList">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-right">
|
||||
<el-input
|
||||
v-model="searchKey"
|
||||
v-model="searchQuery"
|
||||
placeholder="按菜单名称筛选..."
|
||||
clearable
|
||||
placeholder="输入菜单名搜索"
|
||||
@input="search"
|
||||
@clear="search"
|
||||
:prefix-icon="Search"
|
||||
class="search-input"
|
||||
/>
|
||||
<div v-loading="loading" class="tree-bar" :style="{ maxHeight: maxHeight }">
|
||||
<el-tree
|
||||
:key="treeKey"
|
||||
ref="treeRef"
|
||||
:data="data"
|
||||
:props="treeProps"
|
||||
node-key="id"
|
||||
show-checkbox
|
||||
highlight-current
|
||||
:check-strictly="!strict"
|
||||
:default-expanded-keys="expandedKeys"
|
||||
:current-node-key="currentNodeKey"
|
||||
@node-click="onNodeClick"
|
||||
@check="onCheckChange"
|
||||
>
|
||||
<template #default="{ data: nodeData }">
|
||||
<span class="tree-node-label">
|
||||
<el-icon v-if="nodeData.level == 0" class="tree-node-icon"><Menu /></el-icon>
|
||||
<el-icon v-else class="tree-node-icon"><List /></el-icon>
|
||||
<span>{{ nodeData.title }}</span>
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :md="15" :lg="13" :xl="9" style="margin-left: 10px">
|
||||
<el-form ref="formRef" :model="form" label-width="110px" :rules="formValidate">
|
||||
<el-form-item label="类型" prop="type">
|
||||
<div v-show="form.level == 0">
|
||||
<el-icon :size="16" style="margin-right: 5px"><Menu /></el-icon>
|
||||
<span>顶级菜单</span>
|
||||
</div>
|
||||
<div v-show="form.level == 1 || form.level == 2">
|
||||
<el-icon :size="16" style="margin-right: 5px"><List /></el-icon>
|
||||
<span>页面菜单</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="菜单名称" prop="title">
|
||||
<el-input class="menu-input" v-model="form.title" />
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="form.level != 0"
|
||||
label="路由地址"
|
||||
prop="path"
|
||||
class="block-tool"
|
||||
>
|
||||
<el-tooltip placement="right" content="路由地址,英文唯一,跳转页面,路径展示用 ">
|
||||
<el-input class="menu-input" v-model="form.path" />
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item label="路由名称" prop="name" class="block-tool">
|
||||
<el-tooltip placement="right" content="路由name,需英文唯一,跳转页面用">
|
||||
<el-input class="menu-input" v-model="form.name" />
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.level != 0" label="文件路径" prop="frontRoute">
|
||||
<el-input class="menu-input" v-model="form.frontRoute" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.level != 0" label="权限url" class="block-tool">
|
||||
<el-tooltip placement="right" content="*号模糊匹配,逗号分割">
|
||||
<el-input
|
||||
class="menu-input"
|
||||
v-model="form.permission"
|
||||
type="textarea"
|
||||
maxlength="1000"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序值" prop="sortOrder">
|
||||
<el-tooltip placement="right" content="值越小越靠前,支持小数">
|
||||
<el-input-number v-model="form.sortOrder" :max="1000" :min="0" />
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
style="margin-right: 5px"
|
||||
:loading="submitLoading"
|
||||
@click="submitEdit"
|
||||
>
|
||||
保存
|
||||
</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 纯列表树形表格 -->
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
v-loading="loading"
|
||||
:data="filteredData"
|
||||
row-key="id"
|
||||
default-expand-all
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
border
|
||||
stripe
|
||||
class="menu-table"
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="50" align="center" />
|
||||
|
||||
<el-table-column prop="title" label="菜单名称" min-width="220" />
|
||||
|
||||
<el-table-column prop="level" label="类型" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.level === 0 ? "顶级菜单" : row.level === 1 ? "一级页面" : "二级页面" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="path" label="路由地址" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.path || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="name" label="路由名称" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.name || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="frontRoute" label="前端组件" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.frontRoute || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="sortOrder" label="排序值" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.sortOrder ?? 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.level < 2">
|
||||
<a class="link-text" @click="addChildMenu(row)">添加子菜单</a>
|
||||
<span class="op-split">|</span>
|
||||
</template>
|
||||
<a class="link-text" @click="editRow(row)">编辑</a>
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text error-text" @click="deleteRow(row)">删除</a>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 菜单新建 / 编辑弹窗 -->
|
||||
<el-dialog
|
||||
v-model="menuModalVisible"
|
||||
v-model="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="500px"
|
||||
align-center
|
||||
append-to-body
|
||||
width="560px"
|
||||
:close-on-click-modal="false"
|
||||
class="menu-manage-dialog"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form ref="formAddRef" :model="formAdd" label-width="100px" :rules="formValidate">
|
||||
<div v-if="showParent">
|
||||
<el-form-item label="上级节点:">{{ parentTitle }}</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="类型" prop="type">
|
||||
<div v-show="formAdd.level == 0">
|
||||
<el-icon :size="16" style="margin-right: 5px"><Menu /></el-icon>
|
||||
<span>顶级菜单</span>
|
||||
</div>
|
||||
<div v-show="formAdd.level != 0">
|
||||
<el-icon :size="16" style="margin-right: 5px"><List /></el-icon>
|
||||
<span>页面菜单</span>
|
||||
</div>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
label-width="100px"
|
||||
:rules="formValidate"
|
||||
>
|
||||
<el-form-item v-if="parentTitle" label="上级菜单">
|
||||
<el-input :model-value="parentTitle" disabled />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="菜单类型">
|
||||
<el-tag v-if="form.level === 0" type="primary">顶级菜单</el-tag>
|
||||
<el-tag v-else type="success">页面菜单 (Level {{ form.level }})</el-tag>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="菜单名称" prop="title">
|
||||
<el-input class="menu-input" v-model="formAdd.title" />
|
||||
<el-input v-model="form.title" placeholder="请输入菜单名称" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="formAdd.level != 0" label="路由地址" prop="path">
|
||||
<el-input v-model="formAdd.path" />
|
||||
|
||||
<el-form-item v-if="form.level != 0" label="路由地址" prop="path">
|
||||
<el-input v-model="form.path" placeholder="英文唯一,如 /goods" />
|
||||
</el-form-item>
|
||||
<el-form-item label="路由名称" prop="name" class="block-tool">
|
||||
<el-tooltip placement="right" content="路由name,需英文唯一,跳转页面用">
|
||||
<el-input v-model="formAdd.name" />
|
||||
|
||||
<el-form-item label="路由名称" prop="name">
|
||||
<el-tooltip placement="right" content="英文唯一,用于路由跳转">
|
||||
<el-input v-model="form.name" placeholder="请输入路由名称" />
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="formAdd.level != 0" label="文件路径" prop="frontRoute">
|
||||
<el-input v-model="formAdd.frontRoute" />
|
||||
|
||||
<el-form-item v-if="form.level != 0" label="前端组件" prop="frontRoute">
|
||||
<el-input v-model="form.frontRoute" placeholder="例如 Main 或 views/goods/index" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="formAdd.level != 0" label="权限url">
|
||||
<el-input v-model="formAdd.permission" type="textarea" />
|
||||
<div class="desc">*号模糊匹配,逗号分割</div>
|
||||
|
||||
<el-form-item v-if="form.level != 0" label="权限URL">
|
||||
<el-input
|
||||
v-model="form.permission"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="支持 * 号模糊匹配,多个用英文逗号分割"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="排序值" prop="sortOrder">
|
||||
<el-tooltip placement="right" content="值越小越靠前,支持小数">
|
||||
<el-input-number v-model="formAdd.sortOrder" :max="1000" :min="0" />
|
||||
<el-tooltip placement="right" content="值越小越靠前">
|
||||
<el-input-number v-model="form.sortOrder" :min="0" :max="1000" />
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="menuModalVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="submitAdd">提交</el-button>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="modalVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="submitForm">
|
||||
确定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { ref, reactive, computed, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Menu, List, ArrowDown } from "@element-plus/icons-vue";
|
||||
import { Search } from "@element-plus/icons-vue";
|
||||
import {
|
||||
getAllPermissionList,
|
||||
addPermission,
|
||||
editPermission,
|
||||
deletePermission,
|
||||
searchPermission,
|
||||
} from "@/api/index";
|
||||
import util from "@/libs/util.js";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
|
||||
defineOptions({ name: "menu-manage" });
|
||||
defineOptions({ name: "manager-menu-manage" });
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
const loading = ref(true);
|
||||
const strict = ref(true);
|
||||
const maxHeight = ref("500px");
|
||||
const expandLevel = ref(1);
|
||||
const menuModalVisible = ref(false);
|
||||
const submitLoading = ref(false);
|
||||
const modalVisible = ref(false);
|
||||
const modalTitle = ref("");
|
||||
const isEditMode = ref(false);
|
||||
const parentTitle = ref("");
|
||||
|
||||
const data = ref([]);
|
||||
const selectList = ref([]);
|
||||
const selectCount = ref(0);
|
||||
const showParent = ref(false);
|
||||
const searchKey = ref("");
|
||||
const parentTitle = ref("");
|
||||
const editTitle = ref("");
|
||||
const modalTitle = ref("");
|
||||
const expandedKeys = ref([]);
|
||||
const currentNodeKey = ref(null);
|
||||
const treeKey = ref(0);
|
||||
const treeProps = reactive({
|
||||
label: "title",
|
||||
children: "children",
|
||||
});
|
||||
const searchQuery = ref("");
|
||||
const isAllExpanded = ref(true);
|
||||
|
||||
const tableRef = ref(null);
|
||||
const formRef = ref(null);
|
||||
|
||||
const form = ref({
|
||||
id: "",
|
||||
title: "",
|
||||
name: "",
|
||||
path: "",
|
||||
frontRoute: "",
|
||||
parentId: "",
|
||||
parentId: "0",
|
||||
sortOrder: 0,
|
||||
level: 0,
|
||||
permission: "",
|
||||
});
|
||||
const formAdd = ref({});
|
||||
|
||||
const formValidate = reactive({
|
||||
title: [{ required: true, message: "菜单名称名称不能为空", trigger: "blur" }],
|
||||
title: [{ required: true, message: "菜单名称不能为空", trigger: "blur" }],
|
||||
name: [{ required: true, message: "路由名称不能为空", trigger: "blur" }],
|
||||
path: [{ required: true, message: "路由地址不能为空", trigger: "blur" }],
|
||||
frontRoute: [{ required: true, message: "文件地址不能为空", trigger: "blur" }],
|
||||
frontRoute: [{ required: true, message: "前端组件路径不能为空", trigger: "blur" }],
|
||||
sortOrder: [
|
||||
{
|
||||
required: true,
|
||||
@@ -249,280 +219,217 @@ const formValidate = reactive({
|
||||
},
|
||||
],
|
||||
});
|
||||
const submitLoading = ref(false);
|
||||
const data = ref([]);
|
||||
|
||||
const treeRef = ref(null);
|
||||
const formRef = ref(null);
|
||||
const formAddRef = ref(null);
|
||||
// 前端树形过滤
|
||||
const filteredData = computed(() => {
|
||||
const query = searchQuery.value.trim().toLowerCase();
|
||||
if (!query) {
|
||||
return data.value;
|
||||
}
|
||||
const filterTree = (nodes) => {
|
||||
return nodes
|
||||
.map((node) => {
|
||||
const matches =
|
||||
(node.title && node.title.toLowerCase().includes(query)) ||
|
||||
(node.name && node.name.toLowerCase().includes(query)) ||
|
||||
(node.path && node.path.toLowerCase().includes(query));
|
||||
const filteredChildren = node.children ? filterTree(node.children) : [];
|
||||
if (matches || filteredChildren.length > 0) {
|
||||
return {
|
||||
...node,
|
||||
children: filteredChildren.length > 0 ? filteredChildren : undefined,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
};
|
||||
return filterTree(data.value);
|
||||
});
|
||||
|
||||
function init() {
|
||||
getAllList();
|
||||
}
|
||||
|
||||
function collectExpandedKeys(nodes, keys = []) {
|
||||
if (!nodes) return keys;
|
||||
nodes.forEach((node) => {
|
||||
if (node.expand === true) {
|
||||
keys.push(node.id);
|
||||
}
|
||||
if (node.children && node.children.length > 0) {
|
||||
collectExpandedKeys(node.children, keys);
|
||||
}
|
||||
});
|
||||
return keys;
|
||||
}
|
||||
|
||||
function handleDropdown(name) {
|
||||
if (name == "expandOne") {
|
||||
expandLevel.value = 1;
|
||||
getAllList();
|
||||
} else if (name == "expandTwo") {
|
||||
expandLevel.value = 2;
|
||||
getAllList();
|
||||
} else if (name == "expandThree") {
|
||||
expandLevel.value = 3;
|
||||
getAllList();
|
||||
}
|
||||
if (name == "expandAll") {
|
||||
expandLevel.value = 4;
|
||||
getAllList();
|
||||
} else if (name == "refresh") {
|
||||
getAllList();
|
||||
}
|
||||
}
|
||||
|
||||
function getAllList() {
|
||||
loading.value = true;
|
||||
getAllPermissionList().then((res) => {
|
||||
loading.value = false;
|
||||
if (res.success) {
|
||||
const level = expandLevel.value;
|
||||
res.result.forEach(function (e) {
|
||||
if (level == 1) {
|
||||
if (e.level == 0) {
|
||||
e.expand = false;
|
||||
}
|
||||
if (e.children && e.children.length > 0) {
|
||||
e.children.forEach(function (c) {
|
||||
if (c.level == 1) {
|
||||
c.expand = false;
|
||||
}
|
||||
if (c.children && c.children.length > 0) {
|
||||
c.children.forEach(function (b) {
|
||||
if (b.level == 2) {
|
||||
b.expand = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (level == 2) {
|
||||
if (e.level == 0) {
|
||||
e.expand = true;
|
||||
}
|
||||
if (e.children && e.children.length > 0) {
|
||||
e.children.forEach(function (c) {
|
||||
if (c.level == 1) {
|
||||
c.expand = false;
|
||||
}
|
||||
if (c.children && c.children.length > 0) {
|
||||
c.children.forEach(function (b) {
|
||||
if (b.level == 2) {
|
||||
b.expand = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (level == 3) {
|
||||
if (e.level == 0) {
|
||||
e.expand = true;
|
||||
}
|
||||
if (e.children && e.children.length > 0) {
|
||||
e.children.forEach(function (c) {
|
||||
if (c.level == 1) {
|
||||
c.expand = true;
|
||||
}
|
||||
if (c.children && c.children.length > 0) {
|
||||
c.children.forEach(function (b) {
|
||||
if (b.level == 2) {
|
||||
b.expand = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (level == 4) {
|
||||
if (e.level == 0) {
|
||||
e.expand = true;
|
||||
}
|
||||
if (e.children && e.children.length > 0) {
|
||||
e.children.forEach(function (c) {
|
||||
if (c.level == 1) {
|
||||
c.expand = true;
|
||||
}
|
||||
if (c.children && c.children.length > 0) {
|
||||
c.children.forEach(function (b) {
|
||||
if (b.level == 2) {
|
||||
b.expand = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
data.value = res.result;
|
||||
expandedKeys.value = collectExpandedKeys(res.result);
|
||||
treeKey.value += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onNodeClick(nodeData) {
|
||||
if (nodeData) {
|
||||
const menu = JSON.parse(JSON.stringify(nodeData));
|
||||
form.value = menu;
|
||||
editTitle.value = menu.title;
|
||||
currentNodeKey.value = menu.id;
|
||||
} else {
|
||||
cancelEdit();
|
||||
}
|
||||
}
|
||||
|
||||
function onCheckChange(_data, { checkedNodes }) {
|
||||
selectList.value = checkedNodes;
|
||||
selectCount.value = checkedNodes.length;
|
||||
}
|
||||
|
||||
function search() {
|
||||
if (searchKey.value) {
|
||||
loading.value = true;
|
||||
searchPermission({ title: searchKey.value }).then((res) => {
|
||||
getAllPermissionList()
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
if (res.success) {
|
||||
data.value = res.result;
|
||||
expandedKeys.value = [];
|
||||
treeKey.value += 1;
|
||||
data.value = Array.isArray(res.result) ? res.result : [];
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function onSelectionChange(selection) {
|
||||
selectList.value = selection;
|
||||
selectCount.value = selection.length;
|
||||
}
|
||||
|
||||
function toggleExpandAll() {
|
||||
isAllExpanded.value = !isAllExpanded.value;
|
||||
const toggleRows = (rows) => {
|
||||
rows.forEach((row) => {
|
||||
tableRef.value?.toggleRowExpansion(row, isAllExpanded.value);
|
||||
if (row.children && row.children.length) {
|
||||
toggleRows(row.children);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
getAllList();
|
||||
}
|
||||
};
|
||||
toggleRows(data.value);
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
treeRef.value?.setCurrentKey(null);
|
||||
currentNodeKey.value = null;
|
||||
formRef.value.resetFields();
|
||||
form.value.id = "";
|
||||
editTitle.value = "";
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
formRef.value.resetFields();
|
||||
form.value.frontRoute = "";
|
||||
}
|
||||
|
||||
function submitEdit() {
|
||||
formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
if (!form.value.id) {
|
||||
ElMessage.warning("请先点击选择要修改的菜单节点");
|
||||
return;
|
||||
}
|
||||
submitLoading.value = true;
|
||||
if (form.value.sortOrder == null) {
|
||||
form.value.sortOrder = 0;
|
||||
}
|
||||
delete form.value.icon;
|
||||
delete form.value.frontComponent;
|
||||
delete form.value.buttonType;
|
||||
delete form.value.updateTime;
|
||||
delete form.value.selected;
|
||||
delete form.value.description;
|
||||
delete form.value.children;
|
||||
|
||||
editPermission(form.value).then((res) => {
|
||||
submitLoading.value = false;
|
||||
if (res.success) {
|
||||
ElMessage.success("编辑成功");
|
||||
appStore.setAdded(false);
|
||||
util.initRouter();
|
||||
init();
|
||||
menuModalVisible.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function submitAdd() {
|
||||
formAddRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true;
|
||||
|
||||
addPermission(formAdd.value).then((res) => {
|
||||
submitLoading.value = false;
|
||||
if (res.success) {
|
||||
ElMessage.success("添加成功");
|
||||
appStore.setAdded(false);
|
||||
util.initRouter();
|
||||
init();
|
||||
menuModalVisible.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addMenu() {
|
||||
if (!form.value.id) {
|
||||
ElMessage.warning("请先点击选择一个菜单权限节点");
|
||||
return;
|
||||
}
|
||||
parentTitle.value = form.value.title;
|
||||
modalTitle.value = "添加子节点";
|
||||
showParent.value = true;
|
||||
if (form.value.level == 2) {
|
||||
ElMessageBox.alert("仅支持2级菜单目录", "抱歉,不能添加啦", { type: "warning" });
|
||||
return;
|
||||
}
|
||||
formAdd.value = {
|
||||
parentId: form.value.id,
|
||||
level: Number(form.value.level) + 1,
|
||||
// 添加顶级菜单
|
||||
function addRootMenu() {
|
||||
isEditMode.value = false;
|
||||
parentTitle.value = "";
|
||||
modalTitle.value = "添加顶级菜单";
|
||||
form.value = {
|
||||
id: "",
|
||||
title: "",
|
||||
name: "",
|
||||
path: "/",
|
||||
frontRoute: "Main",
|
||||
parentId: 0,
|
||||
level: 0,
|
||||
sortOrder: 0,
|
||||
permission: "",
|
||||
};
|
||||
if (form.value.level == 0) {
|
||||
formAdd.value.path = "/";
|
||||
formAdd.value.frontRoute = "Main";
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
// 添加子菜单
|
||||
function addChildMenu(row) {
|
||||
if (row.level >= 2) {
|
||||
ElMessageBox.alert("仅支持2级菜单目录,不能再添加子菜单", "提示", {
|
||||
type: "warning",
|
||||
});
|
||||
return;
|
||||
}
|
||||
menuModalVisible.value = true;
|
||||
}
|
||||
|
||||
function addRootMenu() {
|
||||
modalTitle.value = "添加顶级菜单";
|
||||
showParent.value = false;
|
||||
formAdd.value = {
|
||||
level: 0,
|
||||
isEditMode.value = false;
|
||||
parentTitle.value = row.title;
|
||||
modalTitle.value = `添加子菜单 (上级: ${row.title})`;
|
||||
form.value = {
|
||||
id: "",
|
||||
title: "",
|
||||
name: "",
|
||||
path: "",
|
||||
frontRoute: "",
|
||||
parentId: row.id,
|
||||
level: Number(row.level) + 1,
|
||||
sortOrder: 0,
|
||||
permission: "",
|
||||
};
|
||||
menuModalVisible.value = true;
|
||||
if (row.level === 0) {
|
||||
form.value.path = "/";
|
||||
form.value.frontRoute = "Main";
|
||||
}
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
// 编辑当前菜单
|
||||
function editRow(row) {
|
||||
isEditMode.value = true;
|
||||
parentTitle.value = "";
|
||||
modalTitle.value = `编辑菜单 - ${row.title}`;
|
||||
form.value = {
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
name: row.name,
|
||||
path: row.path,
|
||||
frontRoute: row.frontRoute,
|
||||
parentId: row.parentId,
|
||||
level: row.level,
|
||||
sortOrder: row.sortOrder ?? 0,
|
||||
permission: row.permission || "",
|
||||
};
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
// 提交保存
|
||||
function submitForm() {
|
||||
formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true;
|
||||
const payload = { ...form.value };
|
||||
if (payload.sortOrder == null) {
|
||||
payload.sortOrder = 0;
|
||||
}
|
||||
delete payload.children;
|
||||
delete payload.icon;
|
||||
delete payload.frontComponent;
|
||||
delete payload.buttonType;
|
||||
delete payload.updateTime;
|
||||
delete payload.selected;
|
||||
delete payload.description;
|
||||
|
||||
const request = isEditMode.value
|
||||
? editPermission(payload)
|
||||
: addPermission(payload);
|
||||
|
||||
request
|
||||
.then((res) => {
|
||||
submitLoading.value = false;
|
||||
if (res.success) {
|
||||
ElMessage.success(isEditMode.value ? "编辑成功" : "添加成功");
|
||||
appStore.setAdded(false);
|
||||
util.initRouter();
|
||||
modalVisible.value = false;
|
||||
getAllList();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
submitLoading.value = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 删除单个
|
||||
function deleteRow(row) {
|
||||
ElMessageBox.confirm(
|
||||
`您确认要删除菜单 "${row.title}" 及其下级所有数据?`,
|
||||
"确认删除",
|
||||
{
|
||||
type: "warning",
|
||||
confirmButtonText: "确定删除",
|
||||
cancelButtonText: "取消",
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
deletePermission(row.id).then((res) => {
|
||||
if (res.success) {
|
||||
ElMessage.success("删除成功");
|
||||
appStore.setAdded(false);
|
||||
util.initRouter();
|
||||
getAllList();
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
function delAll() {
|
||||
if (selectCount.value <= 0) {
|
||||
ElMessage.warning("您还未勾选要删除的数据");
|
||||
return;
|
||||
}
|
||||
ElMessageBox.confirm("您确认要删除所选的 " + selectCount.value + " 条数据?", "确认删除", { type: "warning" }).then(() => {
|
||||
let ids = "";
|
||||
selectList.value.forEach(function (e) {
|
||||
ids += e.id + ",";
|
||||
});
|
||||
ids = ids.substring(0, ids.length - 1);
|
||||
ElMessageBox.confirm(
|
||||
`您确认要删除所选的 ${selectCount.value} 条数据?`,
|
||||
"确认删除",
|
||||
{
|
||||
type: "warning",
|
||||
confirmButtonText: "确定删除",
|
||||
cancelButtonText: "取消",
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
const ids = selectList.value.map((e) => e.id).join(",");
|
||||
deletePermission(ids).then((res) => {
|
||||
if (res.success) {
|
||||
ElMessage.success("删除成功");
|
||||
@@ -530,42 +437,96 @@ function delAll() {
|
||||
util.initRouter();
|
||||
selectList.value = [];
|
||||
selectCount.value = 0;
|
||||
cancelEdit();
|
||||
init();
|
||||
getAllList();
|
||||
}
|
||||
});
|
||||
}).catch(() => {});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const height = document.documentElement.clientHeight;
|
||||
maxHeight.value = Number(height - 287) + "px";
|
||||
init();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/tree-common.scss";
|
||||
.desc {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
.menu-input {
|
||||
width: 362px;
|
||||
}
|
||||
.tree-node-icon {
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.tree-node-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
.menu-manage-container {
|
||||
width: 100%;
|
||||
|
||||
<style lang="scss">
|
||||
.menu-manage-dialog .el-dialog__body {
|
||||
max-height: calc(100vh - 220px);
|
||||
overflow-y: auto;
|
||||
.manage-card {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ebeef5;
|
||||
background: #fff;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 18px 20px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.search-input {
|
||||
width: 240px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-table {
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-table__row) {
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.link-text {
|
||||
color: #2d8cf0;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
display: inline-block;
|
||||
|
||||
&:hover {
|
||||
color: #57a3f3;
|
||||
}
|
||||
|
||||
&.error-text {
|
||||
color: #ed4014;
|
||||
|
||||
&:hover {
|
||||
color: #f16643;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.op-split {
|
||||
margin: 0 8px;
|
||||
color: #dcdee2;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,201 +1,174 @@
|
||||
<template>
|
||||
<div class="search">
|
||||
<el-card>
|
||||
<el-row class="operation">
|
||||
<el-switch
|
||||
v-model="strict"
|
||||
class="selectModel"
|
||||
active-text="级联"
|
||||
inactive-text="单选"
|
||||
style="margin-right: 5px"
|
||||
/>
|
||||
<el-button @click="addRootMenu">添加顶级菜单</el-button>
|
||||
<el-button type="primary" @click="addMenu">添加子菜单</el-button>
|
||||
<el-button @click="delAll">批量删除</el-button>
|
||||
<el-dropdown @command="handleDropdown">
|
||||
<el-button>
|
||||
更多操作
|
||||
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
|
||||
<div class="menu-manage-container">
|
||||
<el-card shadow="never" class="manage-card">
|
||||
<!-- 顶部操作工具栏 -->
|
||||
<div class="table-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<el-button type="primary" @click="addRootMenu">添加顶级菜单</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
:disabled="selectCount === 0"
|
||||
@click="delAll"
|
||||
>
|
||||
批量删除 {{ selectCount > 0 ? `(${selectCount})` : "" }}
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="refresh">刷新</el-dropdown-item>
|
||||
<el-dropdown-item command="expandOne">收合所有</el-dropdown-item>
|
||||
<el-dropdown-item command="expandTwo">展开一级</el-dropdown-item>
|
||||
<el-dropdown-item command="expandThree">展开两级</el-dropdown-item>
|
||||
<el-dropdown-item command="expandAll">展开所有</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-row>
|
||||
<el-row type="flex" justify="start">
|
||||
<el-col :md="8" :lg="8" :xl="6">
|
||||
<el-alert type="info" show-icon :closable="false">
|
||||
当前选择编辑:
|
||||
<span class="select-title">{{ editTitle }}</span>
|
||||
<a class="select-clear" v-if="form.id" @click="cancelEdit">取消选择</a>
|
||||
</el-alert>
|
||||
<el-button @click="toggleExpandAll">
|
||||
{{ isAllExpanded ? "折叠全部" : "展开全部" }}
|
||||
</el-button>
|
||||
<el-button @click="getAllList">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-right">
|
||||
<el-input
|
||||
v-model="searchKey"
|
||||
v-model="searchQuery"
|
||||
placeholder="按菜单名称筛选..."
|
||||
clearable
|
||||
placeholder="输入菜单名搜索"
|
||||
@input="search"
|
||||
@clear="search"
|
||||
:prefix-icon="Search"
|
||||
class="search-input"
|
||||
/>
|
||||
<div v-loading="loading" class="tree-bar" :style="{ maxHeight: maxHeight }">
|
||||
<el-tree
|
||||
:key="treeKey"
|
||||
ref="treeRef"
|
||||
:data="data"
|
||||
:props="treeProps"
|
||||
node-key="id"
|
||||
show-checkbox
|
||||
highlight-current
|
||||
:check-strictly="!strict"
|
||||
:default-expanded-keys="expandedKeys"
|
||||
:current-node-key="currentNodeKey"
|
||||
@node-click="onNodeClick"
|
||||
@check="onCheckChange"
|
||||
>
|
||||
<template #default="{ data: nodeData }">
|
||||
<span class="tree-node-label">
|
||||
<el-icon v-if="nodeData.level == 0" class="tree-node-icon"><Menu /></el-icon>
|
||||
<el-icon v-else class="tree-node-icon"><List /></el-icon>
|
||||
<span>{{ nodeData.title }}</span>
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :md="15" :lg="13" :xl="9" style="margin-left: 10px">
|
||||
<el-form ref="formRef" :model="form" label-width="110px" :rules="formValidate">
|
||||
<el-form-item label="类型" prop="type">
|
||||
<div v-show="form.level == 0">
|
||||
<el-icon :size="16" style="margin-right: 5px"><Menu /></el-icon>
|
||||
<span>顶级菜单</span>
|
||||
</div>
|
||||
<div v-show="form.level == 1 || form.level == 2">
|
||||
<el-icon :size="16" style="margin-right: 5px"><List /></el-icon>
|
||||
<span>页面菜单</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="菜单名称" prop="title">
|
||||
<el-input class="menu-input" v-model="form.title" />
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="form.level != 0"
|
||||
label="路由地址"
|
||||
prop="path"
|
||||
class="block-tool"
|
||||
>
|
||||
<el-tooltip placement="right" content="路由地址,英文唯一,跳转页面,路径展示用 ">
|
||||
<el-input class="menu-input" v-model="form.path" />
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item label="路由名称" prop="name" class="block-tool">
|
||||
<el-tooltip placement="right" content="路由name,需英文唯一,跳转页面用">
|
||||
<el-input class="menu-input" v-model="form.name" />
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.level != 0" label="文件路径" prop="frontRoute">
|
||||
<el-input class="menu-input" v-model="form.frontRoute" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.level != 0" label="权限url" class="block-tool">
|
||||
<el-tooltip placement="right" content="*号模糊匹配,逗号分割">
|
||||
<el-input
|
||||
class="menu-input"
|
||||
v-model="form.permission"
|
||||
type="textarea"
|
||||
maxlength="1000"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序值" prop="sortOrder">
|
||||
<el-tooltip placement="right" content="值越小越靠前,支持小数">
|
||||
<el-input-number v-model="form.sortOrder" :max="1000" :min="0" />
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
style="margin-right: 5px"
|
||||
:loading="submitLoading"
|
||||
@click="submitEdit"
|
||||
>
|
||||
保存
|
||||
</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 纯列表树形表格 -->
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
v-loading="loading"
|
||||
:data="filteredData"
|
||||
row-key="id"
|
||||
default-expand-all
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
border
|
||||
stripe
|
||||
class="menu-table"
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="50" align="center" />
|
||||
|
||||
<el-table-column prop="title" label="菜单名称" min-width="220" />
|
||||
|
||||
<el-table-column prop="level" label="类型" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.level === 0 ? "顶级菜单" : row.level === 1 ? "一级页面" : "二级页面" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="path" label="路由地址" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.path || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="name" label="路由名称" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.name || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="frontRoute" label="前端组件" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.frontRoute || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="sortOrder" label="排序值" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.sortOrder ?? 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.level < 2">
|
||||
<a class="link-text" @click="addChildMenu(row)">添加子菜单</a>
|
||||
<span class="op-split">|</span>
|
||||
</template>
|
||||
<a class="link-text" @click="editRow(row)">编辑</a>
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text error-text" @click="deleteRow(row)">删除</a>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 菜单新建 / 编辑弹窗 -->
|
||||
<el-dialog
|
||||
v-model="menuModalVisible"
|
||||
v-model="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="500px"
|
||||
align-center
|
||||
append-to-body
|
||||
width="560px"
|
||||
:close-on-click-modal="false"
|
||||
class="menu-manage-dialog"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form ref="formAddRef" :model="formAdd" label-width="100px" :rules="formValidate">
|
||||
<div v-if="showParent">
|
||||
<el-form-item label="上级节点:">{{ parentTitle }}</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="类型" prop="type">
|
||||
<div v-show="formAdd.level == 0">
|
||||
<el-icon :size="16" style="margin-right: 5px"><Menu /></el-icon>
|
||||
<span>顶级菜单</span>
|
||||
</div>
|
||||
<div v-show="formAdd.level != 0">
|
||||
<el-icon :size="16" style="margin-right: 5px"><List /></el-icon>
|
||||
<span>页面菜单</span>
|
||||
</div>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
label-width="100px"
|
||||
:rules="formValidate"
|
||||
>
|
||||
<el-form-item v-if="parentTitle" label="上级菜单">
|
||||
<el-input :model-value="parentTitle" disabled />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="菜单类型">
|
||||
<el-tag v-if="form.level === 0" type="primary">顶级菜单</el-tag>
|
||||
<el-tag v-else type="success">页面菜单 (Level {{ form.level }})</el-tag>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="菜单名称" prop="title">
|
||||
<el-input class="menu-input" v-model="formAdd.title" />
|
||||
<el-input v-model="form.title" placeholder="请输入菜单名称" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="formAdd.level != 0" label="路由地址" prop="path">
|
||||
<el-input v-model="formAdd.path" />
|
||||
|
||||
<el-form-item v-if="form.level != 0" label="路由地址" prop="path">
|
||||
<el-input v-model="form.path" placeholder="英文唯一,如 /goods" />
|
||||
</el-form-item>
|
||||
<el-form-item label="路由名称" prop="name" class="block-tool">
|
||||
<el-tooltip placement="right" content="路由name,需英文唯一,跳转页面用">
|
||||
<el-input v-model="formAdd.name" />
|
||||
|
||||
<el-form-item label="路由名称" prop="name">
|
||||
<el-tooltip placement="right" content="英文唯一,用于路由跳转">
|
||||
<el-input v-model="form.name" placeholder="请输入路由名称" />
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="formAdd.level != 0" label="文件路径" prop="frontRoute">
|
||||
<el-input v-model="formAdd.frontRoute" />
|
||||
|
||||
<el-form-item v-if="form.level != 0" label="前端组件" prop="frontRoute">
|
||||
<el-input v-model="form.frontRoute" placeholder="例如 Main 或 views/goods/index" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="formAdd.level != 0" label="权限url">
|
||||
<el-input v-model="formAdd.permission" type="textarea" />
|
||||
<div class="desc">*号模糊匹配,逗号分割</div>
|
||||
|
||||
<el-form-item v-if="form.level != 0" label="权限URL">
|
||||
<el-input
|
||||
v-model="form.permission"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="支持 * 号模糊匹配,多个用英文逗号分割"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="排序值" prop="sortOrder">
|
||||
<el-tooltip placement="right" content="值越小越靠前,支持小数">
|
||||
<el-input-number v-model="formAdd.sortOrder" :max="1000" :min="0" />
|
||||
<el-tooltip placement="right" content="值越小越靠前">
|
||||
<el-input-number v-model="form.sortOrder" :min="0" :max="1000" />
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="menuModalVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="submitAdd">提交</el-button>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="modalVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="submitForm">
|
||||
确定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { ref, reactive, computed, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Menu, List, ArrowDown } from "@element-plus/icons-vue";
|
||||
import { Search } from "@element-plus/icons-vue";
|
||||
import {
|
||||
getStoreAllPermissionList,
|
||||
addStorePermission,
|
||||
editStorePermission,
|
||||
deleteStorePermission,
|
||||
searchStorePermission,
|
||||
} from "@/api/index";
|
||||
import util from "@/libs/util.js";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
@@ -205,41 +178,38 @@ defineOptions({ name: "store-menu-manage" });
|
||||
const appStore = useAppStore();
|
||||
|
||||
const loading = ref(true);
|
||||
const strict = ref(true);
|
||||
const maxHeight = ref("500px");
|
||||
const expandLevel = ref(1);
|
||||
const menuModalVisible = ref(false);
|
||||
const submitLoading = ref(false);
|
||||
const modalVisible = ref(false);
|
||||
const modalTitle = ref("");
|
||||
const isEditMode = ref(false);
|
||||
const parentTitle = ref("");
|
||||
|
||||
const data = ref([]);
|
||||
const selectList = ref([]);
|
||||
const selectCount = ref(0);
|
||||
const showParent = ref(false);
|
||||
const searchKey = ref("");
|
||||
const parentTitle = ref("");
|
||||
const editTitle = ref("");
|
||||
const modalTitle = ref("");
|
||||
const expandedKeys = ref([]);
|
||||
const currentNodeKey = ref(null);
|
||||
const treeKey = ref(0);
|
||||
const treeProps = reactive({
|
||||
label: "title",
|
||||
children: "children",
|
||||
});
|
||||
const searchQuery = ref("");
|
||||
const isAllExpanded = ref(true);
|
||||
|
||||
const tableRef = ref(null);
|
||||
const formRef = ref(null);
|
||||
|
||||
const form = ref({
|
||||
id: "",
|
||||
title: "",
|
||||
name: "",
|
||||
path: "",
|
||||
frontRoute: "",
|
||||
parentId: "",
|
||||
parentId: "0",
|
||||
sortOrder: 0,
|
||||
level: 0,
|
||||
permission: "",
|
||||
});
|
||||
const formAdd = ref({});
|
||||
|
||||
const formValidate = reactive({
|
||||
title: [{ required: true, message: "菜单名称名称不能为空", trigger: "blur" }],
|
||||
title: [{ required: true, message: "菜单名称不能为空", trigger: "blur" }],
|
||||
name: [{ required: true, message: "路由名称不能为空", trigger: "blur" }],
|
||||
path: [{ required: true, message: "路由地址不能为空", trigger: "blur" }],
|
||||
frontRoute: [{ required: true, message: "文件地址不能为空", trigger: "blur" }],
|
||||
frontRoute: [{ required: true, message: "前端组件路径不能为空", trigger: "blur" }],
|
||||
sortOrder: [
|
||||
{
|
||||
required: true,
|
||||
@@ -249,267 +219,217 @@ const formValidate = reactive({
|
||||
},
|
||||
],
|
||||
});
|
||||
const submitLoading = ref(false);
|
||||
const data = ref([]);
|
||||
|
||||
const treeRef = ref(null);
|
||||
const formRef = ref(null);
|
||||
const formAddRef = ref(null);
|
||||
// 前端树形过滤
|
||||
const filteredData = computed(() => {
|
||||
const query = searchQuery.value.trim().toLowerCase();
|
||||
if (!query) {
|
||||
return data.value;
|
||||
}
|
||||
const filterTree = (nodes) => {
|
||||
return nodes
|
||||
.map((node) => {
|
||||
const matches =
|
||||
(node.title && node.title.toLowerCase().includes(query)) ||
|
||||
(node.name && node.name.toLowerCase().includes(query)) ||
|
||||
(node.path && node.path.toLowerCase().includes(query));
|
||||
const filteredChildren = node.children ? filterTree(node.children) : [];
|
||||
if (matches || filteredChildren.length > 0) {
|
||||
return {
|
||||
...node,
|
||||
children: filteredChildren.length > 0 ? filteredChildren : undefined,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
};
|
||||
return filterTree(data.value);
|
||||
});
|
||||
|
||||
function init() {
|
||||
getAllList();
|
||||
}
|
||||
function collectExpandedKeys(nodes, keys = []) {
|
||||
if (!nodes) return keys;
|
||||
nodes.forEach((node) => {
|
||||
if (node.expand === true) {
|
||||
keys.push(node.id);
|
||||
}
|
||||
if (node.children && node.children.length > 0) {
|
||||
collectExpandedKeys(node.children, keys);
|
||||
}
|
||||
});
|
||||
return keys;
|
||||
}
|
||||
function handleDropdown(name) {
|
||||
if (name == "expandOne") {
|
||||
expandLevel.value = 1;
|
||||
getAllList();
|
||||
} else if (name == "expandTwo") {
|
||||
expandLevel.value = 2;
|
||||
getAllList();
|
||||
} else if (name == "expandThree") {
|
||||
expandLevel.value = 3;
|
||||
getAllList();
|
||||
}
|
||||
if (name == "expandAll") {
|
||||
expandLevel.value = 4;
|
||||
getAllList();
|
||||
} else if (name == "refresh") {
|
||||
getAllList();
|
||||
}
|
||||
}
|
||||
|
||||
function getAllList() {
|
||||
loading.value = true;
|
||||
getStoreAllPermissionList().then((res) => {
|
||||
loading.value = false;
|
||||
if (res.success) {
|
||||
const level = expandLevel.value;
|
||||
res.result.forEach(function (e) {
|
||||
if (level == 1) {
|
||||
if (e.level == 0) {
|
||||
e.expand = false;
|
||||
}
|
||||
if (e.children && e.children.length > 0) {
|
||||
e.children.forEach(function (c) {
|
||||
if (c.level == 1) {
|
||||
c.expand = false;
|
||||
}
|
||||
if (c.children && c.children.length > 0) {
|
||||
c.children.forEach(function (b) {
|
||||
if (b.level == 2) {
|
||||
b.expand = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (level == 2) {
|
||||
if (e.level == 0) {
|
||||
e.expand = true;
|
||||
}
|
||||
if (e.children && e.children.length > 0) {
|
||||
e.children.forEach(function (c) {
|
||||
if (c.level == 1) {
|
||||
c.expand = false;
|
||||
}
|
||||
if (c.children && c.children.length > 0) {
|
||||
c.children.forEach(function (b) {
|
||||
if (b.level == 2) {
|
||||
b.expand = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (level == 3) {
|
||||
if (e.level == 0) {
|
||||
e.expand = true;
|
||||
}
|
||||
if (e.children && e.children.length > 0) {
|
||||
e.children.forEach(function (c) {
|
||||
if (c.level == 1) {
|
||||
c.expand = true;
|
||||
}
|
||||
if (c.children && c.children.length > 0) {
|
||||
c.children.forEach(function (b) {
|
||||
if (b.level == 2) {
|
||||
b.expand = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (level == 4) {
|
||||
if (e.level == 0) {
|
||||
e.expand = true;
|
||||
}
|
||||
if (e.children && e.children.length > 0) {
|
||||
e.children.forEach(function (c) {
|
||||
if (c.level == 1) {
|
||||
c.expand = true;
|
||||
}
|
||||
if (c.children && c.children.length > 0) {
|
||||
c.children.forEach(function (b) {
|
||||
if (b.level == 2) {
|
||||
b.expand = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
data.value = res.result;
|
||||
expandedKeys.value = collectExpandedKeys(res.result);
|
||||
treeKey.value += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
function onNodeClick(nodeData) {
|
||||
if (nodeData) {
|
||||
const menu = JSON.parse(JSON.stringify(nodeData));
|
||||
form.value = menu;
|
||||
editTitle.value = menu.title;
|
||||
currentNodeKey.value = menu.id;
|
||||
} else {
|
||||
cancelEdit();
|
||||
}
|
||||
}
|
||||
function onCheckChange(_data, { checkedNodes }) {
|
||||
selectList.value = checkedNodes;
|
||||
selectCount.value = checkedNodes.length;
|
||||
}
|
||||
function search() {
|
||||
if (searchKey.value) {
|
||||
loading.value = true;
|
||||
searchStorePermission({ title: searchKey.value }).then((res) => {
|
||||
getStoreAllPermissionList()
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
if (res.success) {
|
||||
data.value = res.result;
|
||||
expandedKeys.value = [];
|
||||
treeKey.value += 1;
|
||||
data.value = Array.isArray(res.result) ? res.result : [];
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function onSelectionChange(selection) {
|
||||
selectList.value = selection;
|
||||
selectCount.value = selection.length;
|
||||
}
|
||||
|
||||
function toggleExpandAll() {
|
||||
isAllExpanded.value = !isAllExpanded.value;
|
||||
const toggleRows = (rows) => {
|
||||
rows.forEach((row) => {
|
||||
tableRef.value?.toggleRowExpansion(row, isAllExpanded.value);
|
||||
if (row.children && row.children.length) {
|
||||
toggleRows(row.children);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
getAllList();
|
||||
}
|
||||
};
|
||||
toggleRows(data.value);
|
||||
}
|
||||
function cancelEdit() {
|
||||
treeRef.value?.setCurrentKey(null);
|
||||
currentNodeKey.value = null;
|
||||
formRef.value.resetFields();
|
||||
form.value.id = "";
|
||||
editTitle.value = "";
|
||||
}
|
||||
function handleReset() {
|
||||
formRef.value.resetFields();
|
||||
form.value.frontRoute = "";
|
||||
}
|
||||
function submitEdit() {
|
||||
formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
if (!form.value.id) {
|
||||
ElMessage.warning("请先点击选择要修改的菜单节点");
|
||||
return;
|
||||
}
|
||||
submitLoading.value = true;
|
||||
if (form.value.sortOrder == null) {
|
||||
form.value.sortOrder = 0;
|
||||
}
|
||||
delete form.value.icon;
|
||||
delete form.value.frontComponent;
|
||||
delete form.value.buttonType;
|
||||
delete form.value.updateTime;
|
||||
delete form.value.selected;
|
||||
delete form.value.description;
|
||||
delete form.value.children;
|
||||
|
||||
editStorePermission(form.value).then((res) => {
|
||||
submitLoading.value = false;
|
||||
if (res.success) {
|
||||
ElMessage.success("编辑成功");
|
||||
appStore.setAdded(false);
|
||||
util.initRouter();
|
||||
init();
|
||||
menuModalVisible.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
function submitAdd() {
|
||||
formAddRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true;
|
||||
|
||||
addStorePermission(formAdd.value).then((res) => {
|
||||
submitLoading.value = false;
|
||||
if (res.success) {
|
||||
ElMessage.success("添加成功");
|
||||
appStore.setAdded(false);
|
||||
util.initRouter();
|
||||
init();
|
||||
menuModalVisible.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
function addMenu() {
|
||||
if (!form.value.id) {
|
||||
ElMessage.warning("请先点击选择一个菜单权限节点");
|
||||
return;
|
||||
}
|
||||
parentTitle.value = form.value.title;
|
||||
modalTitle.value = "添加子节点";
|
||||
showParent.value = true;
|
||||
if (form.value.level == 2) {
|
||||
ElMessageBox.alert("仅支持2级菜单目录", "抱歉,不能添加啦", { type: "warning" });
|
||||
return;
|
||||
}
|
||||
formAdd.value = {
|
||||
parentId: form.value.id,
|
||||
level: Number(form.value.level) + 1,
|
||||
// 添加顶级菜单
|
||||
function addRootMenu() {
|
||||
isEditMode.value = false;
|
||||
parentTitle.value = "";
|
||||
modalTitle.value = "添加顶级菜单";
|
||||
form.value = {
|
||||
id: "",
|
||||
title: "",
|
||||
name: "",
|
||||
path: "/",
|
||||
frontRoute: "Main",
|
||||
parentId: 0,
|
||||
level: 0,
|
||||
sortOrder: 0,
|
||||
permission: "",
|
||||
};
|
||||
if (form.value.level == 0) {
|
||||
formAdd.value.path = "/";
|
||||
formAdd.value.frontRoute = "Main";
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
// 添加子菜单
|
||||
function addChildMenu(row) {
|
||||
if (row.level >= 2) {
|
||||
ElMessageBox.alert("仅支持2级菜单目录,不能再添加子菜单", "提示", {
|
||||
type: "warning",
|
||||
});
|
||||
return;
|
||||
}
|
||||
menuModalVisible.value = true;
|
||||
}
|
||||
function addRootMenu() {
|
||||
modalTitle.value = "添加顶级菜单";
|
||||
showParent.value = false;
|
||||
formAdd.value = {
|
||||
level: 0,
|
||||
isEditMode.value = false;
|
||||
parentTitle.value = row.title;
|
||||
modalTitle.value = `添加子菜单 (上级: ${row.title})`;
|
||||
form.value = {
|
||||
id: "",
|
||||
title: "",
|
||||
name: "",
|
||||
path: "",
|
||||
frontRoute: "",
|
||||
parentId: row.id,
|
||||
level: Number(row.level) + 1,
|
||||
sortOrder: 0,
|
||||
permission: "",
|
||||
};
|
||||
menuModalVisible.value = true;
|
||||
if (row.level === 0) {
|
||||
form.value.path = "/";
|
||||
form.value.frontRoute = "Main";
|
||||
}
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
// 编辑当前菜单
|
||||
function editRow(row) {
|
||||
isEditMode.value = true;
|
||||
parentTitle.value = "";
|
||||
modalTitle.value = `编辑菜单 - ${row.title}`;
|
||||
form.value = {
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
name: row.name,
|
||||
path: row.path,
|
||||
frontRoute: row.frontRoute,
|
||||
parentId: row.parentId,
|
||||
level: row.level,
|
||||
sortOrder: row.sortOrder ?? 0,
|
||||
permission: row.permission || "",
|
||||
};
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
// 提交保存
|
||||
function submitForm() {
|
||||
formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true;
|
||||
const payload = { ...form.value };
|
||||
if (payload.sortOrder == null) {
|
||||
payload.sortOrder = 0;
|
||||
}
|
||||
delete payload.children;
|
||||
delete payload.icon;
|
||||
delete payload.frontComponent;
|
||||
delete payload.buttonType;
|
||||
delete payload.updateTime;
|
||||
delete payload.selected;
|
||||
delete payload.description;
|
||||
|
||||
const request = isEditMode.value
|
||||
? editStorePermission(payload)
|
||||
: addStorePermission(payload);
|
||||
|
||||
request
|
||||
.then((res) => {
|
||||
submitLoading.value = false;
|
||||
if (res.success) {
|
||||
ElMessage.success(isEditMode.value ? "编辑成功" : "添加成功");
|
||||
appStore.setAdded(false);
|
||||
util.initRouter();
|
||||
modalVisible.value = false;
|
||||
getAllList();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
submitLoading.value = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 删除单个
|
||||
function deleteRow(row) {
|
||||
ElMessageBox.confirm(
|
||||
`您确认要删除菜单 "${row.title}" 及其下级所有数据?`,
|
||||
"确认删除",
|
||||
{
|
||||
type: "warning",
|
||||
confirmButtonText: "确定删除",
|
||||
cancelButtonText: "取消",
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
deleteStorePermission(row.id).then((res) => {
|
||||
if (res.success) {
|
||||
ElMessage.success("删除成功");
|
||||
appStore.setAdded(false);
|
||||
util.initRouter();
|
||||
getAllList();
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
function delAll() {
|
||||
if (selectCount.value <= 0) {
|
||||
ElMessage.warning("您还未勾选要删除的数据");
|
||||
return;
|
||||
}
|
||||
ElMessageBox.confirm("您确认要删除所选的 " + selectCount.value + " 条数据?", "确认删除", { type: "warning" }).then(() => {
|
||||
let ids = "";
|
||||
selectList.value.forEach(function (e) {
|
||||
ids += e.id + ",";
|
||||
});
|
||||
ids = ids.substring(0, ids.length - 1);
|
||||
ElMessageBox.confirm(
|
||||
`您确认要删除所选的 ${selectCount.value} 条数据?`,
|
||||
"确认删除",
|
||||
{
|
||||
type: "warning",
|
||||
confirmButtonText: "确定删除",
|
||||
cancelButtonText: "取消",
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
const ids = selectList.value.map((e) => e.id).join(",");
|
||||
deleteStorePermission(ids).then((res) => {
|
||||
if (res.success) {
|
||||
ElMessage.success("删除成功");
|
||||
@@ -517,42 +437,96 @@ function delAll() {
|
||||
util.initRouter();
|
||||
selectList.value = [];
|
||||
selectCount.value = 0;
|
||||
cancelEdit();
|
||||
init();
|
||||
getAllList();
|
||||
}
|
||||
});
|
||||
}).catch(() => {});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const height = document.documentElement.clientHeight;
|
||||
maxHeight.value = Number(height - 287) + "px";
|
||||
init();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/tree-common.scss";
|
||||
.desc {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
.menu-input {
|
||||
width: 362px;
|
||||
}
|
||||
.tree-node-icon {
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.tree-node-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
.menu-manage-container {
|
||||
width: 100%;
|
||||
|
||||
<style lang="scss">
|
||||
.menu-manage-dialog .el-dialog__body {
|
||||
max-height: calc(100vh - 220px);
|
||||
overflow-y: auto;
|
||||
.manage-card {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ebeef5;
|
||||
background: #fff;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 18px 20px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.search-input {
|
||||
width: 240px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-table {
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-table__row) {
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.link-text {
|
||||
color: #2d8cf0;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
display: inline-block;
|
||||
|
||||
&:hover {
|
||||
color: #57a3f3;
|
||||
}
|
||||
|
||||
&.error-text {
|
||||
color: #ed4014;
|
||||
|
||||
&:hover {
|
||||
color: #f16643;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.op-split {
|
||||
margin: 0 8px;
|
||||
color: #dcdee2;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,45 +1,149 @@
|
||||
.search {
|
||||
.oss-operation {
|
||||
margin-bottom: 2vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
width: 100%;
|
||||
|
||||
button {
|
||||
margin-right: 5px;
|
||||
.oss-header-card {
|
||||
margin-bottom: 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ebeef5;
|
||||
background: #fff;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 14px 20px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.divider {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background: #e2e8f0;
|
||||
}
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.search-input {
|
||||
width: 240px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.none {
|
||||
display: none;
|
||||
.oss-main-card {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ebeef5;
|
||||
background: #fff;
|
||||
min-height: 600px;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.oss-manage-box {
|
||||
display: flex;
|
||||
|
||||
.file-list {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
border-right: 1px solid #e5e6eb;
|
||||
padding: 20px 12px 28px;
|
||||
border-right: 1px solid #f0f2f5;
|
||||
padding: 0 16px 0 0;
|
||||
|
||||
.group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 4px 6px 12px;
|
||||
margin-bottom: 4px;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
|
||||
.group-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.add-group-btn {
|
||||
padding: 4px 8px;
|
||||
height: auto;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pic-list {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 0 0 24px 0;
|
||||
padding: 0 0 0 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
|
||||
.oss-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 2px 0 14px;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
gap: 12px;
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
|
||||
.select-all-checkbox {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.selection-badge {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
|
||||
b {
|
||||
color: #ff6a00;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.img-box {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
gap: 16px 14px;
|
||||
margin-top: 16px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.img-item {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -47,10 +151,11 @@
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
max-width: 220px;
|
||||
aspect-ratio: 1 / 1;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #eef2f6;
|
||||
border: 1.5px solid #edf2f7;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
@@ -58,130 +163,343 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px;
|
||||
transition: all 0.2s ease;
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
cursor: pointer;
|
||||
|
||||
img,
|
||||
.card-media {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
margin: auto;
|
||||
&:hover {
|
||||
border-color: #ffaa66;
|
||||
box-shadow: 0 6px 16px -3px rgba(255, 106, 0, 0.16);
|
||||
|
||||
.card-actions {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.card-checkbox {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.card-file {
|
||||
&.custom-checkbox-card-checked {
|
||||
border-color: #ff6a00 !important;
|
||||
background: #fffaf5 !important;
|
||||
box-shadow: 0 0 0 1px #ff6a00, 0 4px 12px -2px rgba(255, 106, 0, 0.25);
|
||||
|
||||
.card-checkbox {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.card-preview-media {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f5f7fa;
|
||||
color: #909399;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 6px;
|
||||
box-sizing: border-box;
|
||||
|
||||
img,
|
||||
.card-media {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.video-play-overlay {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
backdrop-filter: blur(2px);
|
||||
transition: all 0.2s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:hover .video-play-overlay {
|
||||
transform: translate(-50%, -50%) scale(1.1);
|
||||
background: rgba(255, 106, 0, 0.85);
|
||||
}
|
||||
}
|
||||
|
||||
.card-file-generic {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #edf2f7 100%);
|
||||
gap: 6px;
|
||||
|
||||
.file-badge {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
&.pdf .file-badge { background: #fee2e2; color: #dc2626; }
|
||||
&.doc .file-badge { background: #dbeafe; color: #2563eb; }
|
||||
&.xls .file-badge { background: #dcfce7; color: #16a34a; }
|
||||
&.zip .file-badge { background: #ffedd5; color: #ea580c; }
|
||||
&.code .file-badge { background: #f3e8ff; color: #9333ea; }
|
||||
}
|
||||
|
||||
.card-checkbox {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
z-index: 1000;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
z-index: 10;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
:deep(.el-checkbox__inner) {
|
||||
border-radius: 3px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 1.5px solid #cbd5e1;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(4px);
|
||||
|
||||
&::after {
|
||||
top: 1.5px;
|
||||
left: 4.5px;
|
||||
width: 3.5px;
|
||||
height: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.is-checked .el-checkbox__inner) {
|
||||
background-color: #ff6a00;
|
||||
border-color: #ff6a00;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__label) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: #ff6a00;
|
||||
}
|
||||
.card-actions {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 15;
|
||||
padding: 6px 8px;
|
||||
background: linear-gradient(to top, rgba(15, 23, 42, 0.88) 0%, rgba(15, 23, 42, 0.65) 85%, transparent 100%);
|
||||
backdrop-filter: blur(3px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
opacity: 0;
|
||||
transform: translateY(100%);
|
||||
transition: all 0.22s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
.custom-checkbox-card-checked {
|
||||
border: 2px solid #ff6a00 !important;
|
||||
background: #fffaf5 !important;
|
||||
}
|
||||
.action-btn {
|
||||
color: #f1f5f9;
|
||||
padding: 3px 5px;
|
||||
border-radius: 3px;
|
||||
transition: all 0.15s ease;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.oss-card-selectable {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: #ff6a00;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
&.btn-delete:hover {
|
||||
color: #f87171;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
color: #252931;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.sub-info {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
max-width: 220px;
|
||||
height: 18px;
|
||||
color: #94a3b8;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
color: #1e293b;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
margin-top: 2px;
|
||||
margin-top: 6px;
|
||||
transition: color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: #ff6a00;
|
||||
}
|
||||
}
|
||||
|
||||
.sub-info {
|
||||
width: 100%;
|
||||
max-width: 220px;
|
||||
height: 16px;
|
||||
color: #94a3b8;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
margin-top: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-box {
|
||||
padding: 60px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.page-box {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.pagination-box {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.demo-tree-render .el-tree-node__label {
|
||||
width: 94%;
|
||||
margin-top: 24px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid #f0f2f5;
|
||||
}
|
||||
}
|
||||
|
||||
.oss-group-tree {
|
||||
padding: 4px 0 0;
|
||||
|
||||
.pc-image-picker-dialog {
|
||||
margin-top: 16px !important;
|
||||
margin-bottom: 16px !important;
|
||||
max-height: calc(100vh - 32px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.el-dialog__header {
|
||||
padding: 12px 20px 8px;
|
||||
flex-shrink: 0;
|
||||
:deep(.el-tree-node__expand-icon) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 6px 16px 12px;
|
||||
overflow: hidden !important;
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
flex: 1;
|
||||
:deep(.el-tree-node) {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none !important; /* Chrome, Safari, Edge */
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
:deep(.el-tree-node__content) {
|
||||
padding: 0 10px !important;
|
||||
height: 38px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
line-height: 38px;
|
||||
color: #334155;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
:deep(.el-tree-node__content:hover) {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
:deep(.el-tree-node.is-current > .el-tree-node__content) {
|
||||
background: #fff7ed;
|
||||
color: #ff6a00;
|
||||
font-weight: 600;
|
||||
|
||||
.oss-tree-icon {
|
||||
color: #ff6a00;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.oss-tree-node {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding-right: 4px;
|
||||
font-size: 13px;
|
||||
|
||||
.node-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
.oss-tree-icon {
|
||||
font-size: 16px;
|
||||
color: #94a3b8;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.node-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.oss-tree-more {
|
||||
cursor: pointer;
|
||||
color: #94a3b8;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
opacity: 0;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: #e2e8f0;
|
||||
color: #1e293b;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .oss-tree-more {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.file-preview-fallback {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px 16px;
|
||||
color: #606266;
|
||||
|
||||
.file-preview-name {
|
||||
margin: 16px 0 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
word-break: break-all;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.file-preview-tip {
|
||||
margin: 0 0 20px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
// 弹窗组件调用时的兼容样式
|
||||
.is-component-search {
|
||||
.is-component-card {
|
||||
border: none !important;
|
||||
@@ -196,22 +514,32 @@
|
||||
|
||||
.oss-manage-box {
|
||||
.file-list {
|
||||
padding: 0 10px 10px;
|
||||
padding: 0 10px 10px 0;
|
||||
}
|
||||
.pic-list {
|
||||
padding: 0;
|
||||
padding: 0 0 0 10px;
|
||||
}
|
||||
.oss-operation {
|
||||
margin-bottom: 6px;
|
||||
.oss-toolbar {
|
||||
padding: 0 0 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.img-box {
|
||||
margin-top: 6px;
|
||||
margin-top: 8px;
|
||||
gap: 10px 12px;
|
||||
|
||||
.img-item {
|
||||
.card {
|
||||
max-width: 128px;
|
||||
max-width: 130px;
|
||||
margin: 0 auto;
|
||||
|
||||
// 在选图组件模式下,不需要hover遮罩把选择搞复杂
|
||||
.card-actions {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.card-checkbox {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
.text {
|
||||
height: 18px;
|
||||
@@ -228,12 +556,17 @@
|
||||
}
|
||||
}
|
||||
.page-box {
|
||||
margin-top: 8px;
|
||||
margin-top: 10px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.oss-select-footer {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
margin-top: 12px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #ededed;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,13 +574,13 @@
|
||||
padding: 0;
|
||||
|
||||
.el-tree-node {
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.el-tree-node__content {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
font-size: 14px;
|
||||
height: 34px;
|
||||
line-height: 34px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user