feat(coupon): 重构优惠券类型为满减、满折、立减三种分类

- 新增立减优惠券类型,支持无门槛直接抵扣
- 优惠券列表页统一使用行对象判断类型并显示完整类型名称
- 发布页增加优惠券类型选择组件,切换时自动联动字段显隐
- 买家端购物车满减门槛为零时显示"无门槛使用"
- 卖家端发布页新增券面实时预览面板
- 卖家端发布页重构为分步布局并优化表单交互样式
- 面额与消费门槛校验增加大于零的数值约束
- 移除列表页冗余的标签颜色映射函数,简化展示逻辑
This commit is contained in:
pikachu1995@126.com
2026-11-05 22:36:21 +08:00
parent 29a6b00a21
commit 0f08807f00
12 changed files with 442 additions and 254 deletions

View File

@@ -162,19 +162,42 @@ lilishop-ui
## 8. 升级计划
> 更新日期:2026-11-04,依据各端近期提交及当前代码实现同步。
### ✅ 已完成特性
| 功能模块 | 功能模块 |
| :--- | :--- |
| ✅ 商品定时上下架 | ✅ 商品分组 / 会员分组 |
| ✅ 商品虚拟销量 | ✅ 专用发票(发票上传) |
| ✅ 置顶评论 | ✅ 客户等级体系 |
| ✅ 购物卡 | ✅ 腾讯云-云直播 |
| ✅ 卡密商品 | ✅ 二级分销 |
| ✅ 定时任务管理(数据库配置驱动) | ✅ 预约服务商品 |
| ✅ 主题色管理 | ✅ 立减优惠券 |
| ✅ 小程序订单管理(简单版) | ✅ 微信订单自动上报开关 |
按可核实的实现或完善时间倒序记录;日期表示代码完成或功能更新时间,不代表生产上线时间。
| 时间 | 功能模块 | 完成内容 |
| :--- | :--- | :--- |
| 2026-11-05 | ✅ 付费会员 | 单一付费会员、开通与续费、有效期、关联权益、商家与商品折扣、平台补贴及付费记录。 |
| 2026-11-04 | ✅ 客户等级体系 / 客户权益 | 等级配置、权益介绍、经验值任务与明细、权益发放及重试,提供默认等级与权益数据。 |
| 2026-11-04 | ✅ 小程序订单管理 | 增加小程序订单查询入口及微信订单自动上报开关。 |
| 2026-11-04 | ✅ 立减优惠券 | 新增立减券,完善优惠券校验、领取与订单优惠计算。 |
| 2026-11-04 | ✅ 商品定时上下架 | 完善定时上下架计划管理、独立列表及执行校验。 |
| 2026-11-04 | ✅ 微信小程序原生能力 | 接入微信地址导入、头像选择和昵称填写。 |
| 2026-11-02 | ✅ 消息模板 | 支持多渠道模板矩阵维护,统一站内信、短信、微信公众号及小程序订阅消息的场景配置和异步发送。 |
| 2026-10-30 | ✅ PC 页面装修 | 重构楼层组件,支持自适应高度、可视化热区绘制及配置交互优化。 |
| 2026-10-29 | ✅ 定时任务管理 | 重构为数据库配置驱动,支持运营后台维护任务。 |
| 2026-10-29 | ✅ 敏感词管理 | 支持按业务场景配置过滤开关,并优化词库加载。 |
| 2026-10-29 | ✅ PC 店铺搜索 | 新增店铺列表,支持商品与店铺双模式搜索。 |
| 2026-10-22 | ✅ 分类设置 | 支持四种分类页模板选择、商品列表展示字段开关及模板图片预览。 |
| 2026-10-20 | ✅ 移动端页面装修 | 完善可视化编辑器,新增首页组合组件及优惠券、店铺、文章等楼层。 |
| 2026-10-09 | ✅ 主题色管理 | 完善主题设置,支持自定义模式。 |
| 2026-10-02 | ✅ 预约服务商品 | 完善预约商品、预约交易及订单详情功能。 |
| 2026-08-05 | ✅ 二级分销 | 由一级分销扩展为二级分销,增加节点配置及分销员中心。 |
| 2026-08-03 | ✅ 卡密商品 | 完善电子卡券履约、订单状态及卡池管理。 |
| 2026-07-23 | ✅ 素材库管理 | 增加文件分类、目录管理及目录归属校验。 |
| 2026-07-20 | ✅ 经营统计报表 | 新增店铺业绩、商品同比环比、销售订单明细及商品销售汇总报表。 |
| 2026-07-13 | ✅ 腾讯云直播 | 接入并完善直播、聊天与直播商品相关功能。 |
| 2026-06-30 | ✅ 移动端技术升级 | 升级 Vue 3 与 uView Plus,适配组件和页面。 |
| 2026-06-29 | ✅ 电子面单发货 | 完善快递鸟电子面单,支持自动携带运单号发货。 |
| 2026-06-05 | ✅ 后端与管理端框架升级 | 5 月 25 日至 6 月 5 日完成 Spring Boot 4、管理端 Vue 3 与 Element Plus 升级。 |
| 2026-05-22 | ✅ 购物卡 | 新增礼品卡接口,以及选择、绑定、激活和列表管理。 |
| 2026-05-20 | ✅ 微信提现 | 完善转账状态查询、确认回调和重试处理,后续补充提现规则说明配置。 |
| 2026-04-16 | ✅ 置顶评论 | 支持评论置顶及商品评价管理。 |
| 2026-04-13 | ✅ 专用发票(发票上传) | 完善发票信息字段、发票详情及管理展示。 |
| 2026-04-01 | ✅ 商品虚拟销量 | 支持按商品规格设置虚拟销量,提供单个及批量设置入口。 |
| 2026-03-30 | ✅ 商品分组 | 新增商品分组管理及商品关联功能。 |
| 2026-03-24 | ✅ 会员分组 | 新增会员分组管理。 |
### 🔜 近期规划中
- 🔜 微信小店对接

View File

@@ -0,0 +1,6 @@
import { getRequest, putRequestWithNoForm } from '@/libs/axios';
export const getPaidConfig = () => getRequest('/member/paid/config');
export const savePaidConfig = data => putRequestWithNoForm('/member/paid/config', data);
export const getPaidOrders = params => getRequest('/member/paid/orders', params);
export const getPaidResources = (type, ids) => getRequest('/member/paid/resources', { type, ids: ids.join(',') });
export const getPaidDiscountSkus = params => getRequest('/member/paid/discount-skus', params);

View File

@@ -92,6 +92,9 @@
<span>{{ row.simpleSpecs || "默认规格" }}</span>
</template>
</el-table-column>
<el-table-column label="店铺名称" prop="storeName" min-width="160" show-overflow-tooltip>
<template #default="{ row }">{{ row.storeName || '—' }}</template>
</el-table-column>
<el-table-column label="销售价" width="120" align="right">
<template #default="{ row }">
<span class="goods-price font-semibold">
@@ -111,7 +114,7 @@
</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>
<span>{{ goodsTypeLabel(row.goodsType) }}</span>
</template>
</el-table-column>
</el-table>
@@ -136,11 +139,13 @@
</template>
<script setup>
import { nextTick, onMounted, reactive, ref, watch } from "vue";
import { nextTick, onMounted, onUnmounted, reactive, ref, watch } from "vue";
import * as API_Goods from "@/api/goods";
import { goodsTypeLabel } from "@/constants/goodsType";
const props = defineProps({
goodsLoader: { type: Function, default: null },
queryParams: { type: Object, default: () => ({}) },
selectedWay: {
type: Array,
default: () => [],
@@ -167,6 +172,8 @@ const category = ref([]);
const goodsData = ref([]);
const loading = ref(false);
const tableRef = ref(null);
let syncingSelection = false;
let querySequence = 0;
watch(category, (val) => {
goodsParams.categoryPath = val && val.length ? val[2] : "";
@@ -194,6 +201,8 @@ watch(
onMounted(() => {
init();
});
watch(() => props.queryParams, onSearchGoods, { deep: true });
onUnmounted(() => { querySequence++; });
function onSearchGoods() {
goodsData.value = [];
@@ -222,13 +231,14 @@ function onPageSizeChange(val) {
}
function getQueryGoodsList() {
const sequence = ++querySequence;
loading.value = true;
API_Goods.getGoodsSkuData(goodsParams)
(props.goodsLoader || API_Goods.getGoodsSkuData)({ ...goodsParams, ...props.queryParams })
.then((res) => {
initGoods(res);
if (sequence === querySequence) initGoods(res);
})
.finally(() => {
loading.value = false;
if (sequence === querySequence) loading.value = false;
});
}
@@ -245,16 +255,18 @@ function initGoods(res) {
function syncTableSelection() {
const table = tableRef.value;
if (!table) return;
if (!selectedList.value.length) {
// 将不在当前页的已选 SKU 也加入保留选择,避免翻页回显时丢失。
syncingSelection = true;
try {
table.clearSelection();
return;
selectedList.value.forEach((item) => {
const id = String(item.id || item.skuId);
const current = goodsData.value.find(row => String(row.id || row.skuId) === id);
table.toggleRowSelection(current || item, true);
});
} finally {
syncingSelection = false;
}
goodsData.value.forEach((item) => {
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) {
@@ -263,6 +275,7 @@ function emitSelected(list) {
}
function onSelectionChange(rows) {
if (syncingSelection) return;
if (type.value !== "multiple") {
if (!rows.length) {
emitSelected([]);

View File

@@ -16,6 +16,8 @@
v-if="flag"
ref="goodsDialogRef"
:selectedWay="goodsData"
:goods-loader="goodsLoader"
:query-params="queryParams"
@selected="
(val) => {
goodsData = val;
@@ -69,6 +71,7 @@ import goodsDialog from "./goods-dialog";
import linkDialog from "./link-dialog";
const emit = defineEmits(["closeFlag", "selectedGoodsData", "selectedLink"]);
defineProps({ goodsLoader: { type: Function, default: null }, queryParams: { type: Object, default: () => ({}) } });
const goodsFlag = ref(false); // 是否商品选择器
const goodsData = ref([]); // 选择的商品

View File

@@ -1,174 +1,10 @@
<template>
<div class="shop">
<div class="wap-content">
<div class="query-wrapper">
<div class="query-item">
<div>店铺名称</div>
<el-input
v-model="params.storeName"
placeholder="请输入店铺名称"
clearable
style="width: 150px"
@clear="resetSearch"
@keyup.enter="resetSearch"
/>
</div>
<div class="query-item">
<el-button type="primary" @click="resetSearch">搜索</el-button>
</div>
</div>
<div>
<el-table
ref="tableRef"
v-loading="loading"
class="shops-select-table"
:data="shopsData"
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="100" align="center">
<template #default="{ row }">
<img class="shop-logo" :src="row.storeLogo" alt="" />
</template>
</el-table-column>
<el-table-column
prop="storeName"
label="店铺名称"
min-width="240"
show-overflow-tooltip
/>
<el-table-column
prop="createTime"
label="创建时间"
width="180"
show-overflow-tooltip
/>
<el-table-column label="店铺类型" width="120">
<template #default="{ row }">
<span :class="{ theme_color: row.selfOperated }">
{{ row.selfOperated ? "自营" : "非自营" }}
</span>
</template>
</el-table-column>
<el-table-column label="店铺状态" width="120">
<template #default="{ row }">
<span :class="{ theme_color: row.storeDisable === 'OPEN' }">
{{ row.storeDisable === "OPEN" ? "开启中" : "未开启" }}
</span>
</template>
</el-table-column>
</el-table>
<el-pagination
class="pageration"
size="small"
layout="total, prev, pager, next, jumper"
:total="total"
:page-size="params.pageSize"
:current-page="params.pageNumber"
@current-change="changePageSize"
/>
</div>
</div>
</div>
<StoreSelector embedded :multiple="false" :query-params="shopQuery" @selected="selectShop" />
</template>
<script setup>
import { nextTick, reactive, ref } from "vue";
import { getShopListData } from "@/api/shops.js";
const emit = defineEmits(["selected"]);
const loading = ref(false);
const total = ref(0);
const params = reactive({
pageNumber: 1,
pageSize: 12,
storeDisable: "OPEN",
storeName: "",
});
const shopsData = ref([]);
const tableRef = ref(null);
const selectedShop = ref(null);
// 无 defineExpose:grep manager/src 全量确认无外部 ref/$refs/$parent 访问本组件成员
// (link-dialog.vue 的 $refs["lili-component"][0].type 写入指向首个 tab 的 goods-dialog,与本组件无关)
// created
init();
function resetSearch() {
shopsData.value = [];
params.pageNumber = 1;
init();
}
function changePageSize(v) {
params.pageNumber = v;
init();
}
function init() {
loading.value = true;
getShopListData(params)
.then((res) => {
if (res.success) {
total.value = res.result.total;
shopsData.value = res.result.records;
nextTick(syncTableSelection);
}
})
.finally(() => {
loading.value = false;
});
}
function syncTableSelection() {
const table = tableRef.value;
if (!table || !selectedShop.value) return;
shopsData.value.forEach((row) => {
table.toggleRowSelection(row, row.id === selectedShop.value.id);
});
}
function onSelectionChange(rows) {
// 选择链接只能保留一个店铺,勾选新店铺时自动取消上一个
if (!rows.length) {
selectedShop.value = null;
emit("selected", []);
return;
}
const row = rows[rows.length - 1];
if (rows.length > 1) {
tableRef.value?.clearSelection();
tableRef.value?.toggleRowSelection(row, true);
}
selectedShop.value = row;
emit("selected", [{ ...row, ___type: "shops" }]);
}
import StoreSelector from '@/components/store-selector/index.vue';
// Author: Bulbasaur; 2026-11-05. 装修链接复用统一店铺选择组件,保留单选及启用店铺范围。
const emit = defineEmits(['selected']);
const shopQuery = { storeDisable: 'OPEN' };
function selectShop(rows) { emit('selected', rows.map(row => ({ ...row, ___type: 'shops' }))); }
</script>
<style lang="scss" scoped>
@import "../style.scss";
.shop {
display: flex;
}
.shop-logo {
display: block;
width: 48px;
height: 48px;
margin: 0 auto;
border-radius: 4px;
object-fit: contain;
}
.pageration {
margin-top: 12px;
justify-content: flex-end;
}
</style>

View File

@@ -0,0 +1,25 @@
# 统一店铺选择器
作者:Bulbasaur;2026-11-05。
默认从右侧打开抽屉,支持搜索、分页、多选和已选回显。取消不提交,确认通过 `selected` 返回完整已选店铺数组。店铺类型、状态均为普通文字,无右侧预览区。
```vue
<StoreSelector ref="selector" @selected="onSelected" />
```
导入 `@/components/store-selector/index.vue`,通过 `selector.value.open(selectedRows)` 打开。`selectedRows` 为包含 `id` 的店铺对象数组;每次打开建立独立选择草稿。
| 参数 | 默认值 | 用途 |
| --- | --- | --- |
| multiple | true | false 时单选,返回数组最多一项 |
| embedded | false | true 时在父容器展示列表,并在选择时立即通知;适用于装修链接面板 |
| selectedWay | [] | 初始店铺数组;内嵌模式监听外部选择变化 |
| queryParams | {} | 店铺查询限制,例如 `{ storeDisable: 'OPEN' }` |
| title | 选择店铺 | 抽屉标题 |
| size | 1080px | 抽屉宽度,最大为视口宽度 |
| tableHeight | 自动 | 可选表格高度 |
已接入付费会员、装修链接单选、移动端装修店铺列表配置。查询复用 `getShopListData`(`/store/store`),不新增后端接口。
验证:在工作区根目录运行 `node scripts/test-store-selector.cjs`。

View File

@@ -0,0 +1,115 @@
<template>
<component :is="embedded ? 'div' : ElDrawer" v-bind="embedded ? {} : { modelValue: visible, title, size, direction: 'rtl', appendToBody: true, destroyOnClose: true }" :class="embedded ? 'store-selector-inline' : 'store-picker-drawer'" @update:model-value="visible = $event" @close="request++">
<el-form class="picker-search" inline @submit.prevent="search">
<el-form-item label="店铺名称"><el-input v-model="keyword" clearable placeholder="请输入店铺名称" /></el-form-item>
<el-form-item><el-button type="primary" @click="search">搜索</el-button><el-button @click="reset">重置</el-button></el-form-item>
</el-form>
<div class="picker-content">
<div class="picker-results">
<el-alert v-if="error" title="加载失败,请重试" type="error" :closable="false"><el-button link type="primary" @click="load">重新加载</el-button></el-alert>
<el-table ref="table" v-loading="loading" :data="rows" row-key="id" border :height="tableHeight || (embedded ? '340px' : 'max(300px, calc(100vh - 270px))')" :empty-text="error ? '加载失败' : '暂无匹配结果'" @select="selectStore" @select-all="selectPage" @row-click="toggleStore">
<el-table-column v-if="multiple" type="selection" :reserve-selection="false" width="48" align="center" />
<el-table-column v-else label="选择" width="60" align="center"><template #default="{row}"><el-radio :model-value="selectedStores[0]?.id" :value="row.id" :aria-label="'选择' + row.storeName" @click.stop @change="chooseSingle(row)" /></template></el-table-column>
<el-table-column label="店铺信息" min-width="265"><template #default="{row}"><div class="store-info"><el-avatar :size="40" shape="square" :src="row.storeLogo || ''">店</el-avatar><div class="store-copy"><div class="store-name" :title="row.storeName">{{ row.storeName || '未命名店铺' }}</div><div class="store-id">ID:{{ row.id }}</div></div></div></template></el-table-column>
<el-table-column label="店铺类型" width="96" align="center"><template #default="{row}">{{ selfOperated(row) ? '自营' : '非自营' }}</template></el-table-column>
<el-table-column label="状态" width="100" align="center"><template #default="{row}">{{ storeStatus(row.storeDisable) }}</template></el-table-column>
</el-table>
<el-pagination v-model:current-page="page" :total="total" :page-size="10" layout="total, prev, pager, next" @current-change="load" />
</div>
</div>
<template v-if="!embedded" #footer><div class="picker-footer"><span class="selected-count">已选择 <b>{{ selectedStores.length }}</b> 家店铺</span><div><el-button @click="visible = false">取消</el-button><el-button type="primary" :disabled="loading" @click="confirm">确定</el-button></div></div></template>
</component>
</template>
<script setup>
import { computed, nextTick, onMounted, ref, watch } from 'vue';
import { ElDrawer } from 'element-plus';
import { getShopListData } from '@/api/shops';
// Author: Bulbasaur; 2026-11-05.
const props = defineProps({
multiple: { type: Boolean, default: true },
embedded: { type: Boolean, default: false },
selectedWay: { type: Array, default: () => [] },
queryParams: { type: Object, default: () => ({}) },
title: { type: String, default: '选择店铺' },
size: { type: [String, Number], default: '1080px' },
tableHeight: { type: String, default: '' },
});
const emit = defineEmits(['selected']);
const visible = ref(false), keyword = ref(''), rows = ref([]), total = ref(0), page = ref(1), loading = ref(false), error = ref(false), table = ref();
const storeSelection = ref({});
const selectedStores = computed(() => Object.values(storeSelection.value));
let request = 0;
const selfOperated = row => [true, 1, 'true', '1'].includes(row.selfOperated);
const storeStatus = value => ({OPEN:'营业中',CLOSED:'已关闭',APPLY:'待审核',APPLYING:'待审核',REFUSED:'审核拒绝',EXPIRED:'已过期'}[value] || value || '—');
async function syncStores() {
await nextTick();
if (!table.value) return;
table.value.clearSelection();
rows.value.forEach(row => { if (storeSelection.value[String(row.id)]) table.value.toggleRowSelection(row, true); });
}
function notifyEmbedded() { if (props.embedded) emit('selected', selectedStores.value.map(row => ({...row}))); }
function chooseSingle(row) { storeSelection.value = { [String(row.id)]: {...row} }; notifyEmbedded(); }
function selectStore(selected, row) {
const key = String(row.id);
if (selected.some(item => String(item.id) === key)) storeSelection.value[key] = row;
else delete storeSelection.value[key];
notifyEmbedded();
}
function selectPage(selected) {
const ids = new Set(selected.map(row => String(row.id)));
rows.value.forEach(row => { if (ids.has(String(row.id))) storeSelection.value[String(row.id)] = row; else delete storeSelection.value[String(row.id)]; });
notifyEmbedded();
}
function toggleStore(row, column) {
if (column?.type === 'selection') return;
if (!props.multiple) { chooseSingle(row); return; }
const key = String(row.id);
if (storeSelection.value[key]) delete storeSelection.value[key]; else storeSelection.value[key] = row;
syncStores();
notifyEmbedded();
}
async function load() {
const id = ++request; loading.value = true; error.value = false;
try {
const res = await getShopListData({ ...props.queryParams, pageNumber: page.value, pageSize: 10, storeName: keyword.value.trim() });
if (id !== request || !visible.value) return;
if (!res.success) throw new Error('load');
rows.value = res.result.records || []; total.value = res.result.total || 0;
rows.value.forEach(row => { if (storeSelection.value[String(row.id)]) storeSelection.value[String(row.id)] = row; });
await syncStores();
} catch { if (id === request) { error.value = true; rows.value = []; total.value = 0; } }
finally { if (id === request) loading.value = false; }
}
function search() { page.value = 1; load(); }
function reset() { keyword.value = ''; search(); }
async function open(selected = props.selectedWay) {
request++; keyword.value = ''; rows.value = []; total.value = 0; page.value = 1; error.value = false;
setSelection(selected);
visible.value = true;
await nextTick(); table.value?.clearSelection();
await load();
}
function setSelection(selected) {
const valid = (Array.isArray(selected) ? selected : []).filter(row => row && row.id != null);
storeSelection.value = Object.fromEntries((props.multiple ? valid : valid.slice(0,1)).map(row => [String(row.id), {...row}]));
}
function confirm() { emit('selected', selectedStores.value.map(row => ({...row}))); visible.value = false; }
watch(() => props.selectedWay, selected => { if (props.embedded) { setSelection(selected); syncStores(); } }, { deep: true });
watch(() => props.queryParams, () => { if (visible.value) search(); }, { deep: true });
onMounted(() => { if (props.embedded) open(); });
defineExpose({ open });
</script>
<style scoped>
:global(.store-picker-drawer) { max-width: 100vw; }
:global(.store-picker-drawer .el-drawer__header) { margin-bottom: 0; padding: 20px; border-bottom: 1px solid #ebeef5; }
:global(.store-picker-drawer .el-drawer__body) { padding: 20px; }
:global(.store-picker-drawer .el-drawer__footer) { padding: 0 20px 20px; }
.picker-search { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; padding: 16px 18px 0; margin-bottom: 18px; background: #f7f8fa; border-radius: 6px; }
.picker-search .el-form-item { margin-right: 0; margin-bottom: 16px; }.picker-search .el-input { width: 240px; }
.picker-content { min-width: 0; }.picker-results { min-width: 0; }
.el-pagination { margin-top: 18px; }.store-info { display: flex; align-items: center; gap: 12px; min-height: 50px; }.el-avatar { flex-shrink: 0; background: #edf2f8; color: #7b8da6; }
.store-copy { min-width: 0; flex: 1; }.store-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; color: #303133; }.store-id { margin-top: 3px; font-size: 12px; color: #909399; overflow-wrap: anywhere; }
.selected-count b { color: var(--el-color-primary); }
.picker-footer { display: flex; justify-content: space-between; align-items: center; border-top: 1px solid #ebeef5; padding-top: 16px; }.selected-count { color: #909399; }
@media(max-width: 800px) { .picker-search .el-input { width: 180px; } }
</style>

View File

@@ -224,6 +224,14 @@ export const otherRouter = {
name: "member-benefit",
component: () => import("@/views/member/benefit/index.vue")
},
{
path: "paid-member-settings", title: "付费会员", name: "paid-member-settings",
component: () => import("@/views/member/paid/settings.vue")
},
{
path: "paid-member-records", title: "付费记录", name: "paid-member-records",
component: () => import("@/views/member/paid/records.vue")
},
{
path: "member-benefit-deliveries",
title: "权益发放明细",
@@ -242,7 +250,7 @@ export const otherRouter = {
name: "member-grade-experience-log",
component: () => import("@/views/member/grade/experience-log.vue")
},
{
path: "goods/goods-info/goodsDetail",

View File

@@ -111,7 +111,7 @@
<!-- 权益编码 -->
@@ -1585,3 +1585,6 @@ onMounted(() => {
}
</style>

View File

@@ -0,0 +1,47 @@
<template>
<div class="search">
<el-card shadow="never"><el-form inline :model="query" class="search-form" @submit.prevent @keyup.enter="search">
<el-form-item label="客户名称"><el-input v-model="query.keyword" placeholder="请输入客户昵称" clearable /></el-form-item><el-form-item label="会员ID"><el-input v-model="query.memberId" placeholder="请输入会员ID" clearable /></el-form-item>
<el-form-item label="支付状态"><el-select v-model="query.status" clearable placeholder="全部状态" style="width:160px"><el-option label="未支付" value="UNPAID" /><el-option label="已支付" value="PAID" /><el-option label="已取消" value="CANCELLED" /></el-select></el-form-item>
<el-form-item><el-button type="primary" @click="search">搜索</el-button><el-button @click="reset">重置</el-button></el-form-item>
</el-form></el-card>
<el-card shadow="never" class="mt_10"><div class="actions"><el-button :loading="exporting" @click="exportRecords">导出记录</el-button><el-button @click="router.push({name:'paid-member-settings'})">付费会员</el-button></div>
<el-table v-loading="loading" :data="rows" border>
<el-table-column prop="sn" label="会员费订单号" min-width="200" /><el-table-column prop="memberName" label="客户" width="130" /><el-table-column prop="memberId" label="会员ID" min-width="180" />
<el-table-column label="会员名称" min-width="140"><template #default="{row}">{{ name(row) }}</template></el-table-column>
<el-table-column prop="amount" label="付费金额(元)" width="115" /><el-table-column prop="months" label="有效月数" width="90" /><el-table-column label="支付状态" width="110"><template #default="{row}">{{ status(row) }}</template></el-table-column>
<el-table-column label="会员状态" width="110"><template #default="{row}">{{ membership(row) }}</template></el-table-column><el-table-column prop="payTime" label="支付时间" width="175" /><el-table-column prop="startTime" label="生效时间" width="175" /><el-table-column prop="endTime" label="到期时间" width="175" />
<el-table-column prop="paymentMethod" label="支付方式" width="115" /><el-table-column prop="createTime" label="创建时间" width="175" />
</el-table>
<el-pagination v-model:current-page="query.pageNumber" :page-size="20" :total="total" layout="total, prev, pager, next" @current-change="load" />
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import { getPaidOrders } from '@/api/paid-member';
defineOptions({name:'paidMemberRecords'});
const router=useRouter(), query=reactive({keyword:'',memberId:'',status:'',pageNumber:1,pageSize:20});
const rows=ref([]), total=ref(0), loading=ref(false), exporting=ref(false);
let request=0;
const time=value => value ? new Date(String(value).replace(/-/g,'/')).getTime() : 0;
function status(row) { return row.status === 'UNPAID' && time(row.payDeadline)<Date.now() ? '已超时' : ({UNPAID:'未支付',PAID:'已支付',CANCELLED:'已取消'}[row.status] || row.status); }
function membership(row) { return row.status !== 'PAID' ? '未开通' : time(row.startTime)>Date.now() ? '待生效' : time(row.endTime)<=Date.now() ? '已到期' : '有效'; }
function name(row) { try { return JSON.parse(row.configSnapshot).name; } catch { return '付费会员'; } }
async function load() { const id=++request;loading.value=true;try {const res=await getPaidOrders({...query});if(id===request && res.success){rows.value=res.result.records||[];total.value=res.result.total||0;}} finally {if(id===request)loading.value=false;} }
function search(){query.pageNumber=1;load();} function reset(){query.keyword='';query.memberId='';query.status='';search();}
async function exportRecords() {
exporting.value=true;
try {
const list=[], filters={...query}; let page=1;
while(true){const res=await getPaidOrders({...filters,pageNumber:page,pageSize:100});if(!res.success)return;const records=res.result.records||[];if(!records.length)break;list.push(...records);if(list.length>=res.result.total)break;if(list.length>=10000){ElMessage.warning('一次最多导出10000条,请缩小筛选范围');return;}page++;}
const quote=value=>{let text=String(value??'');if(/^[=+\-@]/.test(text))text="'"+text;return '"'+text.replace(/"/g,'""')+'"';};
const csv=[['会员费订单号','客户','会员ID','会员名称','付费金额','有效月数','支付状态','会员状态','支付时间','生效时间','到期时间','支付方式'],...list.map(r=>[r.sn,r.memberName,r.memberId,name(r),r.amount,r.months,status(r),membership(r),r.payTime,r.startTime,r.endTime,r.paymentMethod])].map(row=>row.map(quote).join(',')).join('\r\n');
const url=URL.createObjectURL(new Blob(['\uFEFF'+csv],{type:'text/csv;charset=utf-8;'}));const a=document.createElement('a');a.href=url;a.download='付费记录.csv';a.click();URL.revokeObjectURL(url);
} finally {exporting.value=false;}
}
onMounted(load);
</script>
<style scoped>.actions { display:flex; gap:12px; margin-bottom:18px; }.el-pagination { margin-top:20px;justify-content:flex-end; }</style>

View File

@@ -0,0 +1,159 @@
<template>
<div class="paid-settings" v-loading="loading">
<el-alert v-if="loadError" title="设置加载失败,请点击重试" type="error" :closable="false"><el-button link @click="loadSettings">重新加载</el-button></el-alert>
<el-form :model="form" label-width="150px" @submit.prevent>
<el-card shadow="never"><template #header>基本信息</template>
<el-form-item label="开放购买/续费"><el-switch v-model="form.enabled" /><span class="hint inline">关闭后,已支付会员仍有效至到期</span></el-form-item>
<el-form-item label="会员名称" required><el-input v-model="form.name" maxlength="50" style="width:360px" /></el-form-item>
<el-form-item label="会员图标"><UploadPicInput v-model="form.icon" /><div class="hint">推荐100 × 100像素,透明PNG</div></el-form-item>
<el-form-item label="会员背景"><el-radio-group v-model="form.backgroundType"><el-radio value="COLOR">背景色</el-radio><el-radio value="IMAGE">背景图</el-radio></el-radio-group><el-color-picker v-if="form.backgroundType === 'COLOR'" v-model="form.backgroundColor" class="inline" /></el-form-item>
<el-form-item v-if="form.backgroundType === 'IMAGE'" label="背景图"><UploadPicInput v-model="form.backgroundImage" /></el-form-item>
<el-form-item label="字体颜色"><el-color-picker v-model="form.fontColor" /></el-form-item>
<el-form-item label="开通价格" required><el-input-number v-model="form.price" :min="0.01" :max="1000000" :precision="2" :controls="false" /><span class="inline">元</span></el-form-item>
<el-form-item label="会员有效期" required><el-input-number v-model="form.months" :min="1" :max="120" :precision="0" :controls="false" /><span class="inline">个月</span><div class="hint">按自然月计算;未到期续费从原到期时间顺延。</div></el-form-item>
<el-form-item label="关联客户权益"><el-select v-model="form.benefitIds" multiple filterable style="width:480px;max-width:100%" placeholder="选择权益"><el-option v-for="b in benefitOptions" :key="b.id" :label="b.benefitName" :value="String(b.id)" /></el-select></el-form-item>
</el-card>
<el-card shadow="never"><template #header>商家设置</template>
<el-form-item label="商家范围"><el-radio-group v-model="form.storeScope"><el-radio value="SELF">自营商家</el-radio><el-radio value="CUSTOM">自定义选择</el-radio></el-radio-group></el-form-item>
<el-form-item v-if="form.storeScope === 'CUSTOM'" label="选择商家"><el-button @click="openPicker('stores')">添加商家</el-button><div class="picked-tags"><el-tag v-for="id in form.storeIds" :key="id" closable @close="remove('storeIds', id)">{{ stores[id]?.storeName || id }}</el-tag></div></el-form-item>
</el-card>
<el-card shadow="never"><template #header>商品折扣</template>
<el-form-item label="商品会员折扣"><el-radio-group v-model="form.discountMode"><el-radio value="ALL">所有适用商品统一折扣</el-radio><el-radio value="CUSTOM">自定义商品折扣</el-radio></el-radio-group></el-form-item>
<el-form-item v-if="form.discountMode === 'ALL'" label="统一折扣"><el-input-number v-model="form.discount" :min="0.01" :max="10" :precision="2" :controls="false" /><span class="inline">折</span></el-form-item>
<el-form-item v-else label="折扣商品"><div class="full"><el-button @click="openPicker('discount')">添加商品</el-button><el-table :data="filteredDiscountRules" border class="mt_10"><el-table-column label="商品" min-width="230"><template #default="{row}">{{ goods[row.skuId]?.goodsName || row.skuId }}</template></el-table-column><el-table-column label="SKU编码" width="180"><template #default="{row}">{{ goods[row.skuId]?.sn || row.skuId }}</template></el-table-column><el-table-column label="规格" min-width="160" show-overflow-tooltip><template #default="{row}">{{ goods[row.skuId]?.simpleSpecs?.trim() || '—' }}</template></el-table-column><el-table-column label="分类" min-width="180" show-overflow-tooltip><template #default="{row}">{{ goods[row.skuId]?.categoryName || '—' }}</template></el-table-column><el-table-column label="品牌" min-width="120" show-overflow-tooltip><template #default="{row}">{{ goods[row.skuId]?.brandName || '—' }}</template></el-table-column><el-table-column label="售价" width="120" align="right"><template #default="{row}">{{ goods[row.skuId]?.price == null ? '—' : '¥' + Number(goods[row.skuId].price).toFixed(2) }}</template></el-table-column><el-table-column label="折扣" width="190"><template #default="{row}"><el-input-number v-model="row.discount" :min="0.01" :max="10" :precision="2" style="width:140px" :controls="false" /> 折</template></el-table-column><el-table-column label="操作" width="80"><template #default="{row}"><el-button link type="danger" @click="form.goodsRules = form.goodsRules.filter(r => r.skuId !== row.skuId)">移除</el-button></template></el-table-column></el-table></div></el-form-item>
</el-card>
<el-card shadow="never"><template #header>会员中心推荐商品</template>
<el-button @click="openPicker('recommended')">添加商品</el-button>
<el-table :data="form.recommendedSkuIds.map(id => goods[id] || {id})" border class="mt_10"><el-table-column prop="sn" label="SKU编码" width="180" /><el-table-column label="商品名称" min-width="240"><template #default="{row}">{{ row.goodsName || row.id }}</template></el-table-column><el-table-column label="规格" min-width="160" show-overflow-tooltip><template #default="{row}">{{ row.simpleSpecs?.trim() || '—' }}</template></el-table-column><el-table-column label="分类" min-width="180" show-overflow-tooltip><template #default="{row}">{{ row.categoryName || '—' }}</template></el-table-column><el-table-column label="品牌" min-width="120" show-overflow-tooltip><template #default="{row}">{{ row.brandName || '—' }}</template></el-table-column><el-table-column label="售价" width="120" align="right"><template #default="{row}">{{ row.price == null ? '—' : '¥' + Number(row.price).toFixed(2) }}</template></el-table-column><el-table-column label="操作" width="90"><template #default="{row}"><el-button link type="danger" @click="remove('recommendedSkuIds', String(row.id))">移除</el-button></template></el-table-column></el-table>
</el-card>
<el-card shadow="never">
<template #header>展示设置</template>
<div class="entry-settings-grid">
<section class="entry-settings-block">
<h4>未开通会员入口</h4>
<el-form-item label="入口文案" required><el-input v-model="form.unopenedText" maxlength="50" show-word-limit class="entry-text-input" /></el-form-item>
<el-form-item label="字体颜色" required><el-color-picker v-model="form.unopenedFontColor" /><span class="entry-color-value">{{ form.unopenedFontColor }}</span></el-form-item>
<el-form-item label="背景图"><UploadPicInput v-model="form.unopenedBackground" /><div class="hint">用于个人中心未开通及已到期入口,推荐 750 × 160 像素,支持 PNG/JPG;不设置时使用默认渐变背景。</div></el-form-item>
<el-form-item label="效果预览"><div class="entry-preview" :style="{color: form.unopenedFontColor}"><img v-if="form.unopenedBackground" :src="form.unopenedBackground" alt="未开通入口背景" /><div class="entry-preview-content"><div><strong>{{ form.unopenedText || '开通付费会员,享专属权益' }}</strong><small>开通享商品优惠与专属权益</small></div><span>›</span></div></div></el-form-item>
</section>
<section class="entry-settings-block">
<h4>已开通会员入口</h4>
<el-form-item label="入口文案" required><el-input v-model="form.openedText" maxlength="50" show-word-limit class="entry-text-input" /></el-form-item>
<el-form-item label="字体颜色" required><el-color-picker v-model="form.openedFontColor" /><span class="entry-color-value">{{ form.openedFontColor }}</span></el-form-item>
<el-form-item label="背景图"><UploadPicInput v-model="form.openedBackground" /><div class="hint">用于个人中心有效会员入口,推荐 750 × 160 像素,支持 PNG/JPG;不设置时使用默认渐变背景。</div></el-form-item>
<el-form-item label="效果预览"><div class="entry-preview" :style="{color: form.openedFontColor}"><img v-if="form.openedBackground" :src="form.openedBackground" alt="已开通入口背景" /><div class="entry-preview-content"><div><strong>{{ form.openedText || '我的付费会员' }}</strong><small>有效期至 2027-11-05(示例)</small></div><span>›</span></div></div></el-form-item>
</section>
</div>
<el-form-item label="会员价格标签"><UploadPicInput v-model="form.priceLabel" /><div class="hint">展示在商品列表、商品详情的会员价前,推荐 48 × 48 像素、透明 PNG;不设置时只显示会员价文字。</div></el-form-item>
<el-form-item label="标签预览"><div class="price-label-preview"><img v-if="form.priceLabel" :src="form.priceLabel" alt="会员价格标签" /><span>会员价 ¥88.00</span></div></el-form-item>
</el-card>
<el-card shadow="never" class="document-card">
<template #header><div class="document-heading"><div><span>购买说明</span><div class="document-hint">展示在移动端付费会员页面,可填写权益介绍和使用说明。</div></div><el-button @click="fullscreenEditor(descriptionEditor)">全屏编辑</el-button></div></template>
<el-form-item label-width="0" class="document-field"><RichEditor ref="descriptionEditor" v-model="form.description" image-picker height="520px" class="full" /></el-form-item>
</el-card>
<el-card shadow="never" class="document-card">
<template #header><div class="document-heading"><div><span>会员协议</span><div class="document-hint">用户购买前阅读并同意的协议,开启购买前请完善内容。</div></div><el-button @click="fullscreenEditor(agreementEditor)">全屏编辑</el-button></div></template>
<el-form-item label-width="0" class="document-field"><RichEditor ref="agreementEditor" v-model="form.agreement" image-picker height="600px" class="full" /></el-form-item>
</el-card>
<div class="save-bar"><el-button type="primary" :loading="saving" :disabled="loading || !loaded" @click="save">保存设置</el-button></div>
</el-form>
<StoreSelector ref="picker" @selected="selected" />
<GoodsSelector ref="goodsPicker" :goods-loader="discountPicking ? getPaidDiscountSkus : undefined" :query-params="discountQuery" @selectedGoodsData="selected" />
</div>
</template>
<script setup>
import { reactive, ref, onMounted, watch, computed } from 'vue';
import { ElMessage } from 'element-plus';
import UploadPicInput from '@/components/lili/upload-pic-input.vue';
import RichEditor from '@/components/editor/index.vue';
import StoreSelector from '@/components/store-selector/index.vue';
import GoodsSelector from '@/components/lili-dialog/index.vue';
import { getPaidConfig, savePaidConfig, getPaidResources, getPaidDiscountSkus } from '@/api/paid-member';
import { getMemberBenefitByPage } from '@/api/member';
defineOptions({name:'paidMemberSettings'});
const form = reactive({ goodsRules: [], storeIds: [], recommendedSkuIds: [], benefitIds: [] });
const loading = ref(false), loaded = ref(false), loadError = ref(false), saving = ref(false), picker = ref(), benefitOptions = ref([]), stores = reactive({}), goods = reactive({});
const descriptionEditor = ref(), agreementEditor = ref(), goodsPicker = ref(), discountPicking = ref(false);
const discountQuery = computed(() => discountPicking.value ? { storeScope: form.storeScope, storeIds: form.storeIds.join(',') } : {});
const filteredDiscountRules = computed(() => form.goodsRules.filter(rule => storeAllowed(goods[rule.skuId])));
function fullscreenEditor(editor) { editor?.getEditor()?.execCommand('mceFullScreen'); }
let target = '';
function remember(rows, type) { rows.forEach(row => { const cache = type === 'STORE' ? stores : goods; cache[String(row.id)] = { ...cache[String(row.id)], ...row }; }); }
function remove(field, id) { form[field] = form[field].filter(value => value !== id); }
async function openPicker(value) {
target = value;
if (value === 'stores') {
picker.value.open(form.storeIds.map(id => ({ ...stores[id], id: String(id) })));
return;
}
discountPicking.value = value === 'discount';
if (discountPicking.value) {
if (form.storeScope === 'CUSTOM' && !form.storeIds.length) { ElMessage.warning('请先选择参与商家'); return; }
try { await reconcileDiscounts(); } catch { ElMessage.error('商品商家信息加载失败,请重试'); return; }
}
const ids = value === 'discount' ? form.goodsRules.map(rule => rule.skuId) : form.recommendedSkuIds;
goodsPicker.value.goodsData = ids.map(id => ({ ...goods[id], id: String(id) }));
goodsPicker.value.open('goods');
}
async function selected(rows) {
remember(rows, target === 'stores' ? 'STORE' : 'SKU');
if (target === 'stores') { form.storeIds = rows.map(row => String(row.id)); return; }
if (target === 'discount') {
try { await hydrate('SKU', rows.map(row => String(row.id))); } catch { ElMessage.error('商品商家信息加载失败,请重试'); return; }
const validRows = rows.filter(row => storeAllowed(goods[String(row.id)]));
if (validRows.length !== rows.length) ElMessage.warning('已排除不属于参与商家的商品');
const discounts = new Map(form.goodsRules.map(rule => [String(rule.skuId), rule.discount]));
form.goodsRules = validRows.map(row => ({ skuId: String(row.id), discount: discounts.get(String(row.id)) ?? form.discount ?? 9.5 }));
} else { form.recommendedSkuIds = rows.map(row => String(row.id)); }
const missing = rows.filter(row => !Object.hasOwn(row, 'categoryName') || !Object.hasOwn(row, 'brandName'));
if (missing.length) { try { await hydrate('SKU', missing.map(row => String(row.id))); } catch { ElMessage.warning('商品分类和品牌加载失败,可刷新页面重试'); } }
}
async function save() { saving.value = true; try { await reconcileDiscounts(); const res = await savePaidConfig({...form}); if (res.success) ElMessage.success('付费会员设置已保存'); } finally { saving.value = false; } }
function storeAllowed(sku) {
return !!sku && (form.storeScope === 'SELF' ? sku.storeSelfOperated === true : form.storeIds.map(String).includes(String(sku.storeId)));
}
async function reconcileDiscounts() {
const missing = form.goodsRules.filter(rule => !Object.hasOwn(goods[rule.skuId] || {}, 'storeSelfOperated'));
if (missing.length) await hydrate('SKU', missing.map(rule => rule.skuId));
const remaining = form.goodsRules.filter(rule => storeAllowed(goods[rule.skuId]));
if (remaining.length !== form.goodsRules.length) { form.goodsRules = remaining; ElMessage.warning('已移除不属于当前参与商家的折扣商品'); }
}
watch(() => [form.storeScope, ...form.storeIds], () => {
if (loading.value || !loaded.value) return;
reconcileDiscounts().catch(() => ElMessage.error('商品商家信息加载失败,请重试'));
}, { flush: 'sync' });
async function hydrate(type, ids) {
for (let i=0; i<ids.length; i+=100) { const res = await getPaidResources(type, ids.slice(i,i+100)); if (!res.success) throw new Error('resources'); remember(res.result, type); }
}
async function loadSettings() {
loading.value = true; loaded.value = false; loadError.value = false;
try {
const [config, rights] = await Promise.all([getPaidConfig(), getMemberBenefitByPage({pageNumber:1,pageSize:200})]);
if (!config.success) { loadError.value = true; return; }
Object.assign(form, config.result); loaded.value = true; benefitOptions.value = rights.result?.records || [];
const ids = [...new Set([...form.recommendedSkuIds, ...form.goodsRules.map(r => r.skuId)])];
await Promise.all([hydrate('SKU',ids), hydrate('STORE',form.storeIds)]);
} catch { loadError.value = true; } finally { loading.value = false; }
}
onMounted(loadSettings);
</script>
<style scoped>
.entry-settings-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; margin-bottom: 24px; }
.entry-settings-block { min-width: 0; border: 1px solid #ebeef5; border-radius: 6px; padding: 20px 16px 0; }
.entry-settings-block h4 { margin: 0 0 24px; color: #303133; font-size: 14px; }
.entry-text-input { width: 480px; max-width: 100%; }
.entry-color-value { margin-left: 12px; color: #909399; font-size: 12px; }
.entry-preview { position: relative; overflow: hidden; width: 375px; max-width: 100%; min-height: 80px; border-radius: 10px; background: linear-gradient(110deg, #423c36, #756149); }
.entry-preview > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.entry-preview-content { position: relative; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px; min-height: 80px; box-sizing: border-box; line-height: 1.5; }
.entry-preview-content strong { font-size: 14px; }.entry-preview-content small { display: block; margin-top: 4px; opacity: .75; font-size: 11px; }.entry-preview-content > span { font-size: 24px; }
.price-label-preview { display: flex; align-items: center; gap: 6px; color: #a77c3e; font-size: 13px; }.price-label-preview img { width: 24px; height: 24px; object-fit: contain; }
@media (max-width: 1400px) { .entry-settings-grid { grid-template-columns: minmax(0, 1fr); } }
.document-heading { display: flex; align-items: center; justify-content: space-between; gap: 20px; }
.document-heading > div { min-width: 0; }
.document-hint { margin-top: 8px; color: #909399; font-size: 12px; font-weight: normal; line-height: 20px; }
.document-field { margin-bottom: 0; }
.document-field :deep(.el-form-item__content) { min-width: 0; }
.document-card :deep(.tox-tinymce) { border-color: #dcdfe6; }
.paid-settings { padding-bottom: 70px; }.el-card { margin-top: 16px; }.hint { width: 100%; font-size: 12px; line-height: 20px; margin-top: 6px; color: #909399; }.inline { margin-left: 12px; }.hint.inline { width: auto; margin-top: 0; }.full { width: 100%; min-width: 0; }.picked-tags { width: 100%; display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }.save-bar { position: sticky; bottom: 0; padding: 16px; background: #fff; border-top: 1px solid #ebeef5; display: flex; justify-content: flex-end; z-index: 10; }
</style>

View File

@@ -80,7 +80,7 @@
<div class="ctrl-row">
<div class="ctrl-label">选择店铺</div>
<div class="ctrl-body">
<el-button plain type="primary" @click="shopDialogVisible = true">选择店铺</el-button>
<el-button plain type="primary" @click="openShops">选择店铺</el-button>
</div>
</div>
<div class="shop-chips" v-if="firstItem.shopList && firstItem.shopList.length">
@@ -176,38 +176,7 @@
/>
</el-dialog>
<el-dialog
v-model="shopDialogVisible"
title="选择店铺"
width="720px"
append-to-body
destroy-on-close
@open="loadShops"
>
<el-table
:data="shopOptions"
v-loading="shopLoading"
max-height="400"
@selection-change="onShopSelection"
>
<el-table-column type="selection" width="50" />
<el-table-column label="店铺" min-width="220">
<template #default="{ row }">
<div class="shop-cell">
<img :src="row.storeLogo" class="shop-cell-logo" alt="" />
<span>{{ row.storeName }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="类型" width="100">
<template #default="{ row }">{{ row.selfOperated ? "自营" : "旗舰店" }}</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="shopDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmShops">确定</el-button>
</template>
</el-dialog>
<StoreSelector ref="storeSelector" :query-params="{ storeDisable: 'OPEN' }" @selected="confirmShops" />
</div>
</template>
@@ -217,7 +186,7 @@ import ColorField from "../controls/ColorField.vue";
import SliderField from "../controls/SliderField.vue";
import ossManage from "@/views/sys/oss-manage/ossManage";
import ways from "@/components/lili-dialog/wap.js";
import { getShopListData } from "@/api/shops";
import StoreSelector from "@/components/store-selector/index.vue";
const DEFAULTS = {
bgColor1: "#FFFFFF",
@@ -247,7 +216,7 @@ const DEFAULT_ITEM = {
export default {
name: "PanelShop",
hideDecorateTitle: true,
components: { GradientField, ColorField, SliderField, ossManage },
components: { GradientField, ColorField, SliderField, ossManage, StoreSelector },
props: {
res: { type: Object, required: true },
},
@@ -260,10 +229,6 @@ export default {
{ key: "style", title: "样式" },
],
picModelFlag: false,
shopDialogVisible: false,
shopLoading: false,
shopOptions: [],
selectedShops: [],
};
},
computed: {
@@ -284,30 +249,15 @@ export default {
if (!Array.isArray(item.shopList)) item.shopList = [];
},
methods: {
// 加载店铺列表供选择
loadShops() {
this.shopLoading = true;
this.selectedShops = [];
getShopListData({ pageNumber: 1, pageSize: 100, status: "OPEN" })
.then((res) => {
this.shopOptions = (res && res.result && res.result.records) || [];
})
.finally(() => {
this.shopLoading = false;
});
},
onShopSelection(rows) {
this.selectedShops = rows;
},
confirmShops() {
this.firstItem.shopList = this.selectedShops.map((s) => ({
openShops() { this.$refs.storeSelector.open(this.firstItem.shopList); },
confirmShops(rows) {
this.firstItem.shopList = rows.map((s) => ({
id: s.id,
storeName: s.storeName,
storeLogo: s.storeLogo,
selfOperated: s.selfOperated,
storeBanner: s.storeBanner || "",
}));
this.shopDialogVisible = false;
},
removeShop(index) {
this.firstItem.shopList.splice(index, 1);