mirror of
https://github.com/crmeb/CRMEB.git
synced 2026-10-28 13:31:04 +00:00
fix(优惠券选择): 契约组件支持 receiveType 筛选,新人礼仅可选系统赠送券
嵌入选择页按内容高度自适应,表格列宽收紧便于弹窗展示。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
519f6550d8
commit
1d87201680
@ -95,26 +95,34 @@ class FormBuilder extends Form
|
||||
}
|
||||
|
||||
/**
|
||||
* 优惠券标签选择器(form-create 自定义组件)
|
||||
* 优惠券标签选择器(form-create 自定义组件 · 契约组件)
|
||||
*
|
||||
* 前端 SchemaForm 已注册 type=SchemaCoupon 的自定义组件:
|
||||
* el-tag 标签展示已选优惠券(可删除),弹窗打开系统选择优惠券页回填
|
||||
* (/admin/marketing.Coupon/list.html,弹窗页脚确定/取消触发回填)。
|
||||
* 值为对象数组 [{id, title, coupon_price, use_min_price}],
|
||||
* 发券消费端(userFirstSubGiveCoupon)已兼容该格式。
|
||||
* ⚠️ 契约组件:改前端只能向后兼容扩展(新增可选 props、默认行为不变)。
|
||||
* 已支持 props:
|
||||
* - receiveType:筛选发送方式(1 用户领取 / 3 系统赠送),默认 '' 不限;
|
||||
* 新人礼等系统赠送场景应显式传 3。
|
||||
*
|
||||
* @param string $field 字段名
|
||||
* @param string $title 字段标题
|
||||
* @param array $value 已选优惠券对象数组
|
||||
* @param array $props 组件 props 覆盖:receiveType 等
|
||||
* @return \FormBuilder\Driver\CustomComponent
|
||||
*/
|
||||
public static function couponSelect($field, $title, $value = [])
|
||||
public static function couponSelect($field, $title, $value = [], array $props = [])
|
||||
{
|
||||
return self::createComponent('SchemaCoupon')
|
||||
->field($field)
|
||||
->title($title)
|
||||
->value((array)$value)
|
||||
->props(['field' => $field]);
|
||||
->props(array_merge([
|
||||
'field' => $field,
|
||||
'receiveType' => '',
|
||||
], $props));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -8,13 +8,23 @@
|
||||
<el-button type="primary" plain size="small" v-db-click @click="open">选择优惠券</el-button>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="visible" title="选择优惠券" width="950px" append-to-body destroy-on-close>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="选择优惠券"
|
||||
width="980px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
class="schema-coupon-dialog"
|
||||
align-center
|
||||
@closed="onClosed"
|
||||
>
|
||||
<iframe
|
||||
v-if="visible"
|
||||
ref="frameRef"
|
||||
:src="iframeSrc"
|
||||
frameborder="0"
|
||||
class="schema-coupon__frame"
|
||||
:style="{ height: `${frameHeight}px` }"
|
||||
@load="onFrameLoad"
|
||||
></iframe>
|
||||
<template #footer>
|
||||
@ -27,20 +37,30 @@
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 通用页面优惠券标签选择器(form-create 自定义组件)
|
||||
* 通用页面优惠券标签选择器(form-create 自定义组件 · 契约组件)
|
||||
*
|
||||
* el-tag 标签展示已选优惠券(可删除),弹窗以 iframe 打开系统选择优惠券页
|
||||
* (/admin/marketing.Coupon/list.html?fodder=<field>)。iframe 加载后向其
|
||||
* contentWindow 注入 form_create_helper(与 form-create frame 组件同协议),
|
||||
* 选择页确认时经 helper.set 回传选中行数据、弹窗页脚"确定"经 helper.onOk
|
||||
* 触发选择页的确认逻辑。值为对象数组 [{id, title, coupon_price, use_min_price}],
|
||||
* 后端经 Form::couponSelect() 生成规则。
|
||||
* (/admin/marketing.Coupon/list.html?fodder=<field>&embed=1&receive_type=<n>)。
|
||||
* iframe 加载后向其 contentWindow 注入 form_create_helper(与 form-create frame
|
||||
* 组件同协议),选择页确认时经 helper.set 回传选中行数据、弹窗页脚"确定"经
|
||||
* helper.onOk 触发选择页的确认逻辑。值为对象数组
|
||||
* [{id, title, coupon_price, use_min_price}],后端经 Form::couponSelect() 生成规则。
|
||||
*
|
||||
* 向后兼容扩展 props:
|
||||
* - receiveType:筛选发送方式(1 用户领取 / 3 系统赠送),默认 '' 不限
|
||||
*
|
||||
* 嵌入页通过 postMessage({ type: 'schema-coupon-resize', height }) 回传内容高度,
|
||||
* 弹窗 iframe 随数据行数收缩(表格最多 6 行后出滚动条)。
|
||||
*/
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import setting from '@/setting';
|
||||
|
||||
defineOptions({ name: 'SchemaCoupon' });
|
||||
|
||||
/** iframe 初始高度(搜索区 + 空表提示),随后由嵌入页 resize 消息校正 */
|
||||
const FRAME_HEIGHT_MIN = 160;
|
||||
const FRAME_HEIGHT_MAX = 520;
|
||||
|
||||
const props = defineProps({
|
||||
/** 已选优惠券对象数组 [{id, title, ...}](form-create field 值绑定) */
|
||||
modelValue: {
|
||||
@ -52,6 +72,14 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
/**
|
||||
* 发送方式筛选:1 用户领取 / 3 系统赠送;空字符串表示不限
|
||||
* (新人礼等系统赠送场景由 Form::couponSelect props.receiveType=3 传入)
|
||||
*/
|
||||
receiveType: {
|
||||
type: [Number, String],
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
@ -61,11 +89,21 @@ const visible = ref(false);
|
||||
/** 弹窗页脚"确定"触发的选择页回调列表(选择页经 helper.onOk 注册) */
|
||||
const okCallbacks = ref([]);
|
||||
const frameRef = ref(null);
|
||||
const frameHeight = ref(280);
|
||||
|
||||
/** 回填协议标识:与规则 field 一致 */
|
||||
const fodder = computed(() => props.field);
|
||||
/** 选择优惠券页地址(带 .html 后缀命中前端路由) */
|
||||
const iframeSrc = computed(() => `${setting.routePre}/marketing.Coupon/list.html?fodder=${fodder.value}`);
|
||||
/** 选择优惠券页地址(embed=1 隐藏页内重复页脚,由本弹窗页脚确认) */
|
||||
const iframeSrc = computed(() => {
|
||||
const query = new URLSearchParams({
|
||||
fodder: fodder.value || '',
|
||||
embed: '1',
|
||||
});
|
||||
if (props.receiveType !== '' && props.receiveType !== null && props.receiveType !== undefined) {
|
||||
query.set('receive_type', String(props.receiveType));
|
||||
}
|
||||
return `${setting.routePre}/marketing.Coupon/list.html?${query.toString()}`;
|
||||
});
|
||||
|
||||
/** 已选标签数据(兼容历史纯 id 格式,无券名时显示 ID 兜底) */
|
||||
const tags = computed(() =>
|
||||
@ -82,9 +120,15 @@ function remove(index) {
|
||||
/** 打开选择弹窗(destroy-on-close 会重建 iframe,历史回调一并失效清空) */
|
||||
function open() {
|
||||
okCallbacks.value = [];
|
||||
frameHeight.value = 280;
|
||||
visible.value = true;
|
||||
}
|
||||
|
||||
function onClosed() {
|
||||
okCallbacks.value = [];
|
||||
frameHeight.value = 280;
|
||||
}
|
||||
|
||||
/**
|
||||
* iframe 加载完成:向其 contentWindow 注入 form_create_helper
|
||||
* (与 form-create frame 组件相同的回填协议)
|
||||
@ -118,6 +162,23 @@ function onOk() {
|
||||
}
|
||||
okCallbacks.value.forEach((fn) => fn());
|
||||
}
|
||||
|
||||
/** 接收嵌入页高度回传,弹窗随列表行数收缩 */
|
||||
function onMessage(event) {
|
||||
const data = event?.data;
|
||||
if (!data || data.type !== 'schema-coupon-resize') return;
|
||||
const height = Number(data.height);
|
||||
if (!Number.isFinite(height) || height <= 0) return;
|
||||
frameHeight.value = Math.min(FRAME_HEIGHT_MAX, Math.max(FRAME_HEIGHT_MIN, Math.ceil(height)));
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('message', onMessage);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('message', onMessage);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@ -141,9 +202,20 @@ function onOk() {
|
||||
}
|
||||
|
||||
&__frame {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 540px;
|
||||
border: 0 none;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
/* append-to-body 弹窗样式需非 scoped */
|
||||
.schema-coupon-dialog {
|
||||
.el-dialog__body {
|
||||
padding: 8px 12px 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="coupon-select">
|
||||
<div ref="rootRef" class="coupon-select" :class="{ 'is-embed': isEmbed }">
|
||||
<el-form :inline="true" @submit.prevent>
|
||||
<el-form-item label="优惠券名称:">
|
||||
<el-input
|
||||
@ -17,23 +17,23 @@
|
||||
<el-table
|
||||
ref="couponTable"
|
||||
v-loading="loading"
|
||||
class="mt15"
|
||||
class="mt15 coupon-select__table"
|
||||
:data="couponList"
|
||||
:row-key="getRowKey"
|
||||
highlight-current-row
|
||||
no-userFrom-text="暂无数据"
|
||||
no-filtered-userFrom-text="暂无筛选结果"
|
||||
height="420"
|
||||
:max-height="tableMaxHeight"
|
||||
@selection-change="changeCheckbox"
|
||||
>
|
||||
<el-table-column type="selection" width="55" :selectable="checkSelectable" :reserve-selection="true" />
|
||||
<el-table-column label="ID" width="70" prop="id" />
|
||||
<el-table-column label="优惠券名称" min-width="140">
|
||||
<el-table-column type="selection" width="42" :selectable="checkSelectable" :reserve-selection="true" />
|
||||
<el-table-column label="ID" width="54" prop="id" />
|
||||
<el-table-column label="优惠券名称" min-width="96" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.title }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="优惠券类型" min-width="90">
|
||||
<el-table-column label="类型" width="64">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.type === 1">品类券</span>
|
||||
<span v-else-if="scope.row.type === 2">商品券</span>
|
||||
@ -41,36 +41,37 @@
|
||||
<span v-else>通用券</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="面值" min-width="80">
|
||||
<el-table-column label="面值" width="64">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.coupon_price }}元</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最低消费" min-width="90">
|
||||
<el-table-column label="门槛" width="72">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.use_min_price == '0.00' ? '无门槛' : `满${scope.row.use_min_price}元` }}</span>
|
||||
<span>{{ scope.row.use_min_price == '0.00' ? '无门槛' : `满${scope.row.use_min_price}` }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发布数量" min-width="130">
|
||||
<el-table-column label="数量" width="88">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.is_permanent">不限量</span>
|
||||
<div v-else>
|
||||
<span>发布:{{ scope.row.total_count }}</span>
|
||||
<span class="ml10">剩余:{{ scope.row.remain_count }}</span>
|
||||
</div>
|
||||
<span v-else class="cell-nowrap">{{ scope.row.total_count }}/{{ scope.row.remain_count }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="领取期限" min-width="150">
|
||||
<el-table-column label="领取期限" width="178">
|
||||
<template #default="scope">
|
||||
<div v-if="scope.row.start_time || scope.row.end_time">
|
||||
{{ $filters.formatDate(scope.row.start_time) }} ~ {{ $filters.formatDate(scope.row.end_time) }}
|
||||
</div>
|
||||
<span v-if="scope.row.start_time || scope.row.end_time" class="cell-nowrap">
|
||||
{{ formatDay(scope.row.start_time) }}~{{ formatDay(scope.row.end_time) }}
|
||||
</span>
|
||||
<span v-else>不限时</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="使用有效期" min-width="150">
|
||||
<el-table-column label="使用有效期" width="178">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.use_time }}</span>
|
||||
<!-- 有固定使用起止时间用日期(仅年月日);否则按领取后天数展示 -->
|
||||
<span v-if="scope.row.start_use_time" class="cell-nowrap">
|
||||
{{ formatDay(scope.row.start_use_time) }}~{{ formatDay(scope.row.end_use_time) }}
|
||||
</span>
|
||||
<span v-else>{{ Number(scope.row.coupon_time || 0) }}天</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@ -83,7 +84,8 @@
|
||||
@pagination="getList"
|
||||
/>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<!-- 嵌入 SchemaCoupon 弹窗时由外层页脚确认,避免重复按钮与高度浪费 -->
|
||||
<div v-if="!isEmbed" class="footer">
|
||||
<el-button v-db-click @click="cancel">取 消</el-button>
|
||||
<el-button type="primary" v-db-click @click="confirm"
|
||||
>确认选择{{ selectedRows.length ? `(${selectedRows.length})` : '' }}</el-button
|
||||
@ -93,7 +95,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { nextTick, onMounted, reactive, ref } from 'vue';
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { releasedListApi } from '@/api/marketing';
|
||||
@ -102,19 +104,56 @@ defineOptions({ name: 'CouponSelect' });
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
/** 是否嵌入 SchemaCoupon 弹窗(由 iframe query embed=1 标识) */
|
||||
const isEmbed = computed(() => String(route.query.embed || '') === '1');
|
||||
|
||||
/** 表头约 40px + 单行约 48px,最多展示 6 行后出滚动条 */
|
||||
const TABLE_HEADER_HEIGHT = 40;
|
||||
const TABLE_ROW_HEIGHT = 48;
|
||||
const TABLE_MAX_ROWS = 6;
|
||||
const tableMaxHeight = TABLE_HEADER_HEIGHT + TABLE_ROW_HEIGHT * TABLE_MAX_ROWS;
|
||||
|
||||
const loading = ref(false);
|
||||
const total = ref(0);
|
||||
const couponList = ref([]);
|
||||
const couponTable = ref(null);
|
||||
const rootRef = ref(null);
|
||||
const tableFrom = reactive({
|
||||
coupon_title: '',
|
||||
receive_type: '',
|
||||
// 支持 URL receive_type:新人礼传 3(系统赠送);缺省不限,保持历史行为
|
||||
receive_type: route.query.receive_type !== undefined && route.query.receive_type !== '' ? route.query.receive_type : '',
|
||||
page: 1,
|
||||
limit: 15,
|
||||
});
|
||||
/** 已选行完整数据([{id,title,coupon_price,use_min_price}],经 helper.set 回传展示券名) */
|
||||
const selectedRows = ref([]);
|
||||
|
||||
/**
|
||||
* 时间戳格式化为仅年月日(兼容秒/毫秒),用于期限列完整单行展示
|
||||
* @param {number|string} data
|
||||
* @returns {string}
|
||||
*/
|
||||
function formatDay(data) {
|
||||
if (data === null || data === undefined || data === '' || Number(data) === 0) {
|
||||
return '';
|
||||
}
|
||||
let ts = Number(data);
|
||||
if (!Number.isFinite(ts)) {
|
||||
return '';
|
||||
}
|
||||
if (ts > 0 && ts < 1e12) {
|
||||
ts *= 1000;
|
||||
}
|
||||
const date = new Date(ts);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return '';
|
||||
}
|
||||
const y = date.getFullYear();
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const d = String(date.getDate()).padStart(2, '0');
|
||||
return `${y}/${m}/${d}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 余量充足且在领取期内的券才允许勾选(与后端发券查询 getGiveCoupon 条件一致)
|
||||
*/
|
||||
@ -133,17 +172,32 @@ function getRowKey(row) {
|
||||
return row.id;
|
||||
}
|
||||
|
||||
/** 通知外层弹窗按内容高度收缩 iframe,避免大片空白 */
|
||||
function reportHeight() {
|
||||
if (!isEmbed.value || !window.parent || window.parent === window) return;
|
||||
nextTick(() => {
|
||||
const el = rootRef.value;
|
||||
if (!el) return;
|
||||
const height = Math.ceil(el.getBoundingClientRect().height);
|
||||
window.parent.postMessage({ type: 'schema-coupon-resize', height }, '*');
|
||||
});
|
||||
}
|
||||
|
||||
function getList() {
|
||||
loading.value = true;
|
||||
releasedListApi(tableFrom)
|
||||
.then((res) => {
|
||||
couponList.value = res.data.list;
|
||||
total.value = res.data.count;
|
||||
nextTick(() => setCheckedRows());
|
||||
nextTick(() => {
|
||||
setCheckedRows();
|
||||
reportHeight();
|
||||
});
|
||||
loading.value = false;
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
reportHeight();
|
||||
});
|
||||
}
|
||||
|
||||
@ -211,6 +265,8 @@ function waitHelper(timeout = 5000) {
|
||||
});
|
||||
}
|
||||
|
||||
watch(loading, () => reportHeight());
|
||||
|
||||
onMounted(async () => {
|
||||
const helper = await waitHelper();
|
||||
if (helper) {
|
||||
@ -225,6 +281,11 @@ onMounted(async () => {
|
||||
helper.onOk(confirm);
|
||||
}
|
||||
getList();
|
||||
window.addEventListener('resize', reportHeight);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', reportHeight);
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -232,6 +293,20 @@ onMounted(async () => {
|
||||
.coupon-select {
|
||||
padding: 15px 15px 0;
|
||||
background-color: var(--prev-bg-white);
|
||||
|
||||
&.is-embed {
|
||||
padding: 4px 4px 0;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
.coupon-select__table {
|
||||
width: 100%;
|
||||
}
|
||||
.cell-nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.page {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.footer {
|
||||
padding: 15px 0;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user