feat(@vben/web-antd): implement new customer gift page and drawers
This commit is contained in:
@@ -185,4 +185,5 @@ export async function deleteMarketingCouponApi(data: DeleteMarketingCouponDto) {
|
||||
|
||||
export * from './flash-sale';
|
||||
export * from './full-reduction';
|
||||
export * from './new-customer';
|
||||
export * from './seckill';
|
||||
|
||||
213
apps/web-antd/src/api/marketing/new-customer.ts
Normal file
213
apps/web-antd/src/api/marketing/new-customer.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* 文件职责:营销中心新客有礼 API 与 DTO 定义。
|
||||
* 1. 维护新客有礼详情、配置保存、邀请记录分页与写入契约。
|
||||
*/
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/** 礼包类型。 */
|
||||
export type MarketingNewCustomerGiftType = 'coupon' | 'direct';
|
||||
|
||||
/** 券场景。 */
|
||||
export type MarketingNewCustomerCouponScene = 'invitee' | 'inviter' | 'welcome';
|
||||
|
||||
/** 券类型。 */
|
||||
export type MarketingNewCustomerCouponType =
|
||||
| 'amount_off'
|
||||
| 'discount'
|
||||
| 'free_shipping';
|
||||
|
||||
/** 分享渠道。 */
|
||||
export type MarketingNewCustomerShareChannel =
|
||||
| 'moments'
|
||||
| 'sms'
|
||||
| 'wechat_friend';
|
||||
|
||||
/** 邀请订单状态。 */
|
||||
export type MarketingNewCustomerInviteOrderStatus = 'ordered' | 'pending_order';
|
||||
|
||||
/** 邀请奖励状态。 */
|
||||
export type MarketingNewCustomerInviteRewardStatus = 'issued' | 'pending';
|
||||
|
||||
/** 优惠券规则。 */
|
||||
export interface MarketingNewCustomerCouponRuleDto {
|
||||
couponType: MarketingNewCustomerCouponType;
|
||||
id: string;
|
||||
minimumSpend: null | number;
|
||||
scene: MarketingNewCustomerCouponScene;
|
||||
sortOrder: number;
|
||||
validDays: number;
|
||||
value: null | number;
|
||||
}
|
||||
|
||||
/** 新客有礼设置。 */
|
||||
export interface MarketingNewCustomerSettingsDto {
|
||||
directMinimumSpend: null | number;
|
||||
directReduceAmount: null | number;
|
||||
giftEnabled: boolean;
|
||||
giftType: MarketingNewCustomerGiftType;
|
||||
inviteEnabled: boolean;
|
||||
inviteeCoupons: MarketingNewCustomerCouponRuleDto[];
|
||||
inviterCoupons: MarketingNewCustomerCouponRuleDto[];
|
||||
shareChannels: MarketingNewCustomerShareChannel[];
|
||||
storeId: string;
|
||||
updatedAt: string;
|
||||
welcomeCoupons: MarketingNewCustomerCouponRuleDto[];
|
||||
}
|
||||
|
||||
/** 统计数据。 */
|
||||
export interface MarketingNewCustomerStatsDto {
|
||||
firstOrderConversionRate: number;
|
||||
firstOrderedCount: number;
|
||||
giftClaimRate: number;
|
||||
giftClaimedCount: number;
|
||||
monthlyGrowthCount: number;
|
||||
monthlyGrowthRatePercent: number;
|
||||
monthlyNewCustomers: number;
|
||||
}
|
||||
|
||||
/** 邀请记录。 */
|
||||
export interface MarketingNewCustomerInviteRecordDto {
|
||||
id: string;
|
||||
inviteeName: string;
|
||||
inviteTime: string;
|
||||
inviterName: string;
|
||||
orderStatus: MarketingNewCustomerInviteOrderStatus;
|
||||
rewardIssuedAt: null | string;
|
||||
rewardStatus: MarketingNewCustomerInviteRewardStatus;
|
||||
sourceChannel?: string;
|
||||
}
|
||||
|
||||
/** 邀请记录分页。 */
|
||||
export interface MarketingNewCustomerInviteRecordListResultDto {
|
||||
items: MarketingNewCustomerInviteRecordDto[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
/** 新客有礼详情。 */
|
||||
export interface MarketingNewCustomerDetailDto {
|
||||
inviteRecords: MarketingNewCustomerInviteRecordListResultDto;
|
||||
settings: MarketingNewCustomerSettingsDto;
|
||||
stats: MarketingNewCustomerStatsDto;
|
||||
}
|
||||
|
||||
/** 新客有礼详情查询。 */
|
||||
export interface MarketingNewCustomerDetailQuery {
|
||||
recordPage?: number;
|
||||
recordPageSize?: number;
|
||||
storeId: string;
|
||||
}
|
||||
|
||||
/** 邀请记录查询。 */
|
||||
export interface MarketingNewCustomerInviteRecordListQuery {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
storeId: string;
|
||||
}
|
||||
|
||||
/** 保存券规则项。 */
|
||||
export interface SaveMarketingNewCustomerCouponRuleDto {
|
||||
couponType: MarketingNewCustomerCouponType;
|
||||
minimumSpend: null | number;
|
||||
validDays: number;
|
||||
value: null | number;
|
||||
}
|
||||
|
||||
/** 保存新客有礼配置请求。 */
|
||||
export interface SaveMarketingNewCustomerSettingsDto {
|
||||
directMinimumSpend: null | number;
|
||||
directReduceAmount: null | number;
|
||||
giftEnabled: boolean;
|
||||
giftType: MarketingNewCustomerGiftType;
|
||||
inviteEnabled: boolean;
|
||||
inviteeCoupons: SaveMarketingNewCustomerCouponRuleDto[];
|
||||
inviterCoupons: SaveMarketingNewCustomerCouponRuleDto[];
|
||||
shareChannels: MarketingNewCustomerShareChannel[];
|
||||
storeId: string;
|
||||
welcomeCoupons: SaveMarketingNewCustomerCouponRuleDto[];
|
||||
}
|
||||
|
||||
/** 写入邀请记录请求。 */
|
||||
export interface WriteMarketingNewCustomerInviteRecordDto {
|
||||
inviteeName: string;
|
||||
inviteTime: string;
|
||||
inviterName: string;
|
||||
orderStatus: MarketingNewCustomerInviteOrderStatus;
|
||||
rewardIssuedAt: null | string;
|
||||
rewardStatus: MarketingNewCustomerInviteRewardStatus;
|
||||
sourceChannel?: string;
|
||||
storeId: string;
|
||||
}
|
||||
|
||||
/** 新客成长记录。 */
|
||||
export interface MarketingNewCustomerGrowthRecordDto {
|
||||
customerKey: string;
|
||||
customerName?: string;
|
||||
firstOrderAt: null | string;
|
||||
giftClaimedAt: null | string;
|
||||
id: string;
|
||||
registeredAt: string;
|
||||
sourceChannel?: string;
|
||||
}
|
||||
|
||||
/** 写入成长记录请求。 */
|
||||
export interface WriteMarketingNewCustomerGrowthRecordDto {
|
||||
customerKey: string;
|
||||
customerName?: string;
|
||||
firstOrderAt: null | string;
|
||||
giftClaimedAt: null | string;
|
||||
registeredAt: string;
|
||||
sourceChannel?: string;
|
||||
storeId: string;
|
||||
}
|
||||
|
||||
/** 获取新客有礼详情。 */
|
||||
export async function getMarketingNewCustomerDetailApi(
|
||||
params: MarketingNewCustomerDetailQuery,
|
||||
) {
|
||||
return requestClient.get<MarketingNewCustomerDetailDto>(
|
||||
'/marketing/new-customer/detail',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 保存新客有礼配置。 */
|
||||
export async function saveMarketingNewCustomerSettingsApi(
|
||||
data: SaveMarketingNewCustomerSettingsDto,
|
||||
) {
|
||||
return requestClient.post<MarketingNewCustomerSettingsDto>(
|
||||
'/marketing/new-customer/save',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 获取邀请记录分页。 */
|
||||
export async function getMarketingNewCustomerInviteRecordListApi(
|
||||
params: MarketingNewCustomerInviteRecordListQuery,
|
||||
) {
|
||||
return requestClient.get<MarketingNewCustomerInviteRecordListResultDto>(
|
||||
'/marketing/new-customer/invite-record/list',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 写入邀请记录。 */
|
||||
export async function writeMarketingNewCustomerInviteRecordApi(
|
||||
data: WriteMarketingNewCustomerInviteRecordDto,
|
||||
) {
|
||||
return requestClient.post<MarketingNewCustomerInviteRecordDto>(
|
||||
'/marketing/new-customer/invite-record/write',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 写入成长记录。 */
|
||||
export async function writeMarketingNewCustomerGrowthRecordApi(
|
||||
data: WriteMarketingNewCustomerGrowthRecordDto,
|
||||
) {
|
||||
return requestClient.post<MarketingNewCustomerGrowthRecordDto>(
|
||||
'/marketing/new-customer/growth-record/write',
|
||||
data,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 文件职责:新客有礼券添加抽屉。
|
||||
* 1. 提供券类型、门槛、有效期编辑能力。
|
||||
*/
|
||||
import type { NewCustomerCouponDrawerForm } from '../types';
|
||||
|
||||
import type {
|
||||
MarketingNewCustomerCouponScene,
|
||||
MarketingNewCustomerCouponType,
|
||||
} from '#/api/marketing';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { Button, Drawer, Form, InputNumber, Radio } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
NEW_CUSTOMER_COUPON_SCENE_TITLE_MAP,
|
||||
NEW_CUSTOMER_COUPON_TYPE_OPTIONS,
|
||||
} from '../composables/new-customer-page/constants';
|
||||
|
||||
defineProps<{
|
||||
form: NewCustomerCouponDrawerForm;
|
||||
open: boolean;
|
||||
scene: MarketingNewCustomerCouponScene;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'close'): void;
|
||||
(event: 'setCouponType', value: MarketingNewCustomerCouponType): void;
|
||||
(event: 'setMinimumSpend', value: null | number): void;
|
||||
(event: 'setValidDays', value: null | number): void;
|
||||
(event: 'setValue', value: null | number): void;
|
||||
(event: 'submit'): void;
|
||||
}>();
|
||||
|
||||
function parseNullableNumber(value: null | number | string) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return null;
|
||||
}
|
||||
const parsed = Number(value);
|
||||
return Number.isNaN(parsed) ? null : parsed;
|
||||
}
|
||||
|
||||
function onCouponTypeChange(value: unknown) {
|
||||
if (
|
||||
value === 'amount_off' ||
|
||||
value === 'discount' ||
|
||||
value === 'free_shipping'
|
||||
) {
|
||||
emit('setCouponType', value);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Drawer
|
||||
:open="open"
|
||||
:title="`添加优惠券 · ${NEW_CUSTOMER_COUPON_SCENE_TITLE_MAP[scene]}`"
|
||||
width="480"
|
||||
class="mnc-coupon-drawer"
|
||||
@close="emit('close')"
|
||||
>
|
||||
<Form layout="vertical" class="mnc-coupon-drawer-form">
|
||||
<Form.Item label="券类型" required>
|
||||
<Radio.Group
|
||||
:value="form.couponType"
|
||||
button-style="solid"
|
||||
@update:value="onCouponTypeChange"
|
||||
>
|
||||
<Radio.Button
|
||||
v-for="item in NEW_CUSTOMER_COUPON_TYPE_OPTIONS"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
v-if="form.couponType === 'amount_off'"
|
||||
label="减免金额"
|
||||
required
|
||||
>
|
||||
<div class="mnc-inline-fields">
|
||||
<span>¥</span>
|
||||
<InputNumber
|
||||
:value="form.value ?? undefined"
|
||||
:min="0.01"
|
||||
:precision="2"
|
||||
placeholder="如:5"
|
||||
@update:value="
|
||||
(value) => emit('setValue', parseNullableNumber(value))
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item v-if="form.couponType === 'discount'" label="折扣" required>
|
||||
<div class="mnc-inline-fields">
|
||||
<InputNumber
|
||||
:value="form.value ?? undefined"
|
||||
:min="0.1"
|
||||
:max="9.99"
|
||||
:precision="2"
|
||||
placeholder="如:8"
|
||||
@update:value="
|
||||
(value) => emit('setValue', parseNullableNumber(value))
|
||||
"
|
||||
/>
|
||||
<span>折</span>
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item v-if="form.couponType === 'free_shipping'" label="券说明">
|
||||
<div class="mnc-shipping-tip">
|
||||
<IconifyIcon icon="lucide:truck" />
|
||||
免配送费券无需设置面额
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="使用门槛">
|
||||
<div class="mnc-inline-fields">
|
||||
<span>订单满</span>
|
||||
<InputNumber
|
||||
:value="form.minimumSpend ?? undefined"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
placeholder="如:30"
|
||||
@update:value="
|
||||
(value) => emit('setMinimumSpend', parseNullableNumber(value))
|
||||
"
|
||||
/>
|
||||
<span>元可用</span>
|
||||
</div>
|
||||
<div class="mnc-field-hint">留空或填 0 表示无门槛</div>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="有效期" required>
|
||||
<div class="mnc-inline-fields">
|
||||
<span>领取后</span>
|
||||
<InputNumber
|
||||
:value="form.validDays"
|
||||
:min="1"
|
||||
:max="365"
|
||||
:precision="0"
|
||||
placeholder="如:7"
|
||||
@update:value="
|
||||
(value) => emit('setValidDays', parseNullableNumber(value))
|
||||
"
|
||||
/>
|
||||
<span>天</span>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<template #footer>
|
||||
<div class="mnc-coupon-drawer-footer">
|
||||
<Button @click="emit('close')">取消</Button>
|
||||
<Button type="primary" @click="emit('submit')">添加</Button>
|
||||
</div>
|
||||
</template>
|
||||
</Drawer>
|
||||
</template>
|
||||
@@ -0,0 +1,80 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 文件职责:新客有礼券组渲染组件。
|
||||
* 1. 展示券列表、删除与添加入口。
|
||||
*/
|
||||
import type { NewCustomerCouponFormItem } from '../types';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { Button, Empty } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
formatCouponDescription,
|
||||
formatCouponName,
|
||||
} from '../composables/new-customer-page/helpers';
|
||||
|
||||
defineProps<{
|
||||
addText: string;
|
||||
canManage: boolean;
|
||||
coupons: NewCustomerCouponFormItem[];
|
||||
description?: string;
|
||||
title: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'add'): void;
|
||||
(event: 'remove', index: number): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mnc-coupon-group">
|
||||
<div class="mnc-coupon-group-title">
|
||||
{{ title }}
|
||||
<span v-if="description" class="mnc-coupon-group-desc">{{
|
||||
description
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="coupons.length > 0" class="mnc-coupon-list">
|
||||
<div
|
||||
v-for="(item, index) in coupons"
|
||||
:key="`${item.id}-${index}`"
|
||||
class="mnc-coupon-item"
|
||||
>
|
||||
<div class="mnc-coupon-info">
|
||||
<div class="mnc-coupon-name">{{ formatCouponName(item) }}</div>
|
||||
<div class="mnc-coupon-desc">{{ formatCouponDescription(item) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mnc-coupon-validity">
|
||||
<IconifyIcon icon="lucide:clock-3" />
|
||||
领取后{{ item.validDays }}天
|
||||
</div>
|
||||
|
||||
<Button
|
||||
v-if="canManage"
|
||||
type="text"
|
||||
danger
|
||||
class="mnc-coupon-remove"
|
||||
@click="emit('remove', index)"
|
||||
>
|
||||
<IconifyIcon icon="lucide:trash-2" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Empty v-else class="mnc-coupon-empty" description="暂未添加优惠券" />
|
||||
|
||||
<button
|
||||
v-if="canManage"
|
||||
type="button"
|
||||
class="mnc-add-coupon-btn"
|
||||
@click="emit('add')"
|
||||
>
|
||||
<IconifyIcon icon="lucide:plus" />
|
||||
{{ addText }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,114 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 文件职责:新客有礼邀请记录表格。
|
||||
* 1. 渲染邀请记录状态与分页。
|
||||
*/
|
||||
import type { NewCustomerInviteRecordViewModel } from '../types';
|
||||
|
||||
import { Table } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
NEW_CUSTOMER_ORDER_STATUS_TEXT_MAP,
|
||||
NEW_CUSTOMER_REWARD_STATUS_TEXT_MAP,
|
||||
} from '../composables/new-customer-page/constants';
|
||||
|
||||
defineProps<{
|
||||
loading: boolean;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
records: NewCustomerInviteRecordViewModel[];
|
||||
total: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'pageChange', page: number, pageSize: number): void;
|
||||
}>();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '邀请人',
|
||||
dataIndex: 'inviterName',
|
||||
key: 'inviterName',
|
||||
},
|
||||
{
|
||||
title: '被邀请人',
|
||||
dataIndex: 'inviteeName',
|
||||
key: 'inviteeName',
|
||||
},
|
||||
{
|
||||
title: '邀请时间',
|
||||
dataIndex: 'inviteTime',
|
||||
key: 'inviteTime',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'orderStatus',
|
||||
key: 'orderStatus',
|
||||
},
|
||||
{
|
||||
title: '奖励发放',
|
||||
dataIndex: 'rewardStatus',
|
||||
key: 'rewardStatus',
|
||||
},
|
||||
];
|
||||
|
||||
function handleTableChange(pagination: {
|
||||
current?: number;
|
||||
pageSize?: number;
|
||||
}) {
|
||||
emit('pageChange', pagination.current ?? 1, pagination.pageSize ?? 10);
|
||||
}
|
||||
|
||||
function resolveOrderStatusText(status: unknown) {
|
||||
if (status === 'ordered' || status === 'pending_order') {
|
||||
return NEW_CUSTOMER_ORDER_STATUS_TEXT_MAP[status];
|
||||
}
|
||||
return NEW_CUSTOMER_ORDER_STATUS_TEXT_MAP.pending_order;
|
||||
}
|
||||
|
||||
function resolveRewardStatusText(status: unknown) {
|
||||
if (status === 'issued' || status === 'pending') {
|
||||
return NEW_CUSTOMER_REWARD_STATUS_TEXT_MAP[status];
|
||||
}
|
||||
return NEW_CUSTOMER_REWARD_STATUS_TEXT_MAP.pending;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Table
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data-source="records"
|
||||
:pagination="{
|
||||
current: page,
|
||||
pageSize,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '20', '50'],
|
||||
showTotal: (value) => `共 ${value} 条`,
|
||||
}"
|
||||
row-key="id"
|
||||
size="middle"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'orderStatus'">
|
||||
<span
|
||||
class="mnc-status-tag"
|
||||
:class="record.orderStatus === 'ordered' ? 'is-green' : 'is-orange'"
|
||||
>
|
||||
{{ resolveOrderStatusText(record.orderStatus) }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'rewardStatus'">
|
||||
<span
|
||||
class="mnc-status-tag"
|
||||
:class="record.rewardStatus === 'issued' ? 'is-green' : 'is-gray'"
|
||||
>
|
||||
{{ resolveRewardStatusText(record.rewardStatus) }}
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</template>
|
||||
@@ -0,0 +1,80 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 文件职责:新客有礼统计卡片。
|
||||
* 1. 展示本月新客、礼包领取率、新客转化率三项指标。
|
||||
*/
|
||||
import type { NewCustomerStatsViewModel } from '../types';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import {
|
||||
formatInteger,
|
||||
formatPercent,
|
||||
} from '../composables/new-customer-page/helpers';
|
||||
|
||||
const props = defineProps<{
|
||||
stats: NewCustomerStatsViewModel;
|
||||
}>();
|
||||
|
||||
const growthIsUp = computed(() => props.stats.monthlyGrowthCount >= 0);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mnc-stats">
|
||||
<div class="mnc-stat-card">
|
||||
<div class="mnc-stat-label">
|
||||
<IconifyIcon icon="lucide:user-plus" />
|
||||
本月新客
|
||||
</div>
|
||||
<div class="mnc-stat-value">
|
||||
{{ formatInteger(stats.monthlyNewCustomers) }}
|
||||
<span class="mnc-stat-unit">人</span>
|
||||
<span
|
||||
class="mnc-stat-trend"
|
||||
:class="{ down: !growthIsUp, up: growthIsUp }"
|
||||
>
|
||||
<IconifyIcon
|
||||
:icon="growthIsUp ? 'lucide:trending-up' : 'lucide:trending-down'"
|
||||
/>
|
||||
{{ growthIsUp ? '+' : ''
|
||||
}}{{ formatPercent(stats.monthlyGrowthRatePercent) }}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="mnc-stat-sub">
|
||||
较上月{{ stats.monthlyGrowthCount >= 0 ? '增长' : '减少' }}
|
||||
{{ formatInteger(Math.abs(stats.monthlyGrowthCount)) }} 人
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mnc-stat-card">
|
||||
<div class="mnc-stat-label">
|
||||
<IconifyIcon icon="lucide:ticket" />
|
||||
礼包领取率
|
||||
</div>
|
||||
<div class="mnc-stat-value">
|
||||
{{ formatPercent(stats.giftClaimRate) }}
|
||||
<span class="mnc-stat-unit">%</span>
|
||||
</div>
|
||||
<div class="mnc-stat-sub">
|
||||
{{ formatInteger(stats.monthlyNewCustomers) }} 人中
|
||||
{{ formatInteger(stats.giftClaimedCount) }} 人已领取
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mnc-stat-card">
|
||||
<div class="mnc-stat-label">
|
||||
<IconifyIcon icon="lucide:shopping-bag" />
|
||||
新客转化率
|
||||
</div>
|
||||
<div class="mnc-stat-value">
|
||||
{{ formatPercent(stats.firstOrderConversionRate) }}
|
||||
<span class="mnc-stat-unit">%</span>
|
||||
</div>
|
||||
<div class="mnc-stat-sub">
|
||||
首单完成率,{{ formatInteger(stats.firstOrderedCount) }} 人已下单
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,131 @@
|
||||
import type {
|
||||
MarketingNewCustomerCouponScene,
|
||||
MarketingNewCustomerCouponType,
|
||||
MarketingNewCustomerGiftType,
|
||||
MarketingNewCustomerInviteOrderStatus,
|
||||
MarketingNewCustomerInviteRewardStatus,
|
||||
MarketingNewCustomerShareChannel,
|
||||
} from '#/api/marketing';
|
||||
import type {
|
||||
NewCustomerCouponDrawerForm,
|
||||
NewCustomerInviteRecordPager,
|
||||
NewCustomerPageForm,
|
||||
NewCustomerStatsViewModel,
|
||||
} from '#/views/marketing/new-customer/types';
|
||||
|
||||
/**
|
||||
* 文件职责:新客有礼页面常量与默认值。
|
||||
*/
|
||||
|
||||
/** 查看权限码。 */
|
||||
export const NEW_CUSTOMER_VIEW_PERMISSION =
|
||||
'tenant:marketing:new-customer:view';
|
||||
|
||||
/** 管理权限码。 */
|
||||
export const NEW_CUSTOMER_MANAGE_PERMISSION =
|
||||
'tenant:marketing:new-customer:manage';
|
||||
|
||||
/** 礼包类型选项。 */
|
||||
export const NEW_CUSTOMER_GIFT_TYPE_OPTIONS: Array<{
|
||||
label: string;
|
||||
value: MarketingNewCustomerGiftType;
|
||||
}> = [
|
||||
{ label: '优惠券包', value: 'coupon' },
|
||||
{ label: '首单直减', value: 'direct' },
|
||||
];
|
||||
|
||||
/** 分享渠道选项。 */
|
||||
export const NEW_CUSTOMER_SHARE_CHANNEL_OPTIONS: Array<{
|
||||
label: string;
|
||||
value: MarketingNewCustomerShareChannel;
|
||||
}> = [
|
||||
{ label: '微信好友', value: 'wechat_friend' },
|
||||
{ label: '朋友圈', value: 'moments' },
|
||||
{ label: '短信', value: 'sms' },
|
||||
];
|
||||
|
||||
/** 券类型选项。 */
|
||||
export const NEW_CUSTOMER_COUPON_TYPE_OPTIONS: Array<{
|
||||
label: string;
|
||||
value: MarketingNewCustomerCouponType;
|
||||
}> = [
|
||||
{ label: '满减券', value: 'amount_off' },
|
||||
{ label: '折扣券', value: 'discount' },
|
||||
{ label: '免配送费', value: 'free_shipping' },
|
||||
];
|
||||
|
||||
/** 券场景标题。 */
|
||||
export const NEW_CUSTOMER_COUPON_SCENE_TITLE_MAP: Record<
|
||||
MarketingNewCustomerCouponScene,
|
||||
string
|
||||
> = {
|
||||
welcome: '新客礼包券',
|
||||
inviter: '邀请人奖励券',
|
||||
invitee: '被邀请人奖励券',
|
||||
};
|
||||
|
||||
/** 邀请记录订单状态文案。 */
|
||||
export const NEW_CUSTOMER_ORDER_STATUS_TEXT_MAP: Record<
|
||||
MarketingNewCustomerInviteOrderStatus,
|
||||
string
|
||||
> = {
|
||||
pending_order: '待下单',
|
||||
ordered: '已下单',
|
||||
};
|
||||
|
||||
/** 邀请记录奖励状态文案。 */
|
||||
export const NEW_CUSTOMER_REWARD_STATUS_TEXT_MAP: Record<
|
||||
MarketingNewCustomerInviteRewardStatus,
|
||||
string
|
||||
> = {
|
||||
pending: '待触发',
|
||||
issued: '已发放',
|
||||
};
|
||||
|
||||
/** 创建默认页面表单。 */
|
||||
export function createDefaultNewCustomerPageForm(): NewCustomerPageForm {
|
||||
return {
|
||||
giftEnabled: true,
|
||||
giftType: 'coupon',
|
||||
directReduceAmount: 10,
|
||||
directMinimumSpend: 30,
|
||||
inviteEnabled: true,
|
||||
shareChannels: ['wechat_friend', 'moments'],
|
||||
welcomeCoupons: [],
|
||||
inviterCoupons: [],
|
||||
inviteeCoupons: [],
|
||||
};
|
||||
}
|
||||
|
||||
/** 创建默认统计。 */
|
||||
export function createDefaultNewCustomerStats(): NewCustomerStatsViewModel {
|
||||
return {
|
||||
monthlyNewCustomers: 0,
|
||||
monthlyGrowthCount: 0,
|
||||
monthlyGrowthRatePercent: 0,
|
||||
giftClaimRate: 0,
|
||||
giftClaimedCount: 0,
|
||||
firstOrderConversionRate: 0,
|
||||
firstOrderedCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/** 创建默认邀请记录分页。 */
|
||||
export function createDefaultNewCustomerInvitePager(): NewCustomerInviteRecordPager {
|
||||
return {
|
||||
items: [],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
totalCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/** 创建默认券抽屉表单。 */
|
||||
export function createDefaultNewCustomerCouponDrawerForm(): NewCustomerCouponDrawerForm {
|
||||
return {
|
||||
couponType: 'amount_off',
|
||||
value: null,
|
||||
minimumSpend: null,
|
||||
validDays: 7,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import type { StoreListItemDto } from '#/api/store';
|
||||
import type {
|
||||
NewCustomerInviteRecordPager,
|
||||
NewCustomerPageForm,
|
||||
NewCustomerStatsViewModel,
|
||||
} from '#/views/marketing/new-customer/types';
|
||||
|
||||
/**
|
||||
* 文件职责:新客有礼页面数据加载动作。
|
||||
* 1. 加载门店、详情与邀请记录分页。
|
||||
*/
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
getMarketingNewCustomerDetailApi,
|
||||
getMarketingNewCustomerInviteRecordListApi,
|
||||
} from '#/api/marketing';
|
||||
import { getStoreListApi } from '#/api/store';
|
||||
|
||||
import {
|
||||
createDefaultNewCustomerInvitePager,
|
||||
createDefaultNewCustomerStats,
|
||||
} from './constants';
|
||||
import { mapSettingsToPageForm } from './helpers';
|
||||
|
||||
interface CreateDataActionsOptions {
|
||||
form: NewCustomerPageForm;
|
||||
invitePager: Ref<NewCustomerInviteRecordPager>;
|
||||
isDetailLoading: Ref<boolean>;
|
||||
isInviteLoading: Ref<boolean>;
|
||||
isStoreLoading: Ref<boolean>;
|
||||
selectedStoreId: Ref<string>;
|
||||
stats: Ref<NewCustomerStatsViewModel>;
|
||||
stores: Ref<StoreListItemDto[]>;
|
||||
updatedAt: Ref<string>;
|
||||
}
|
||||
|
||||
export function createDataActions(options: CreateDataActionsOptions) {
|
||||
function applyForm(next: NewCustomerPageForm) {
|
||||
options.form.giftEnabled = next.giftEnabled;
|
||||
options.form.giftType = next.giftType;
|
||||
options.form.directReduceAmount = next.directReduceAmount;
|
||||
options.form.directMinimumSpend = next.directMinimumSpend;
|
||||
options.form.inviteEnabled = next.inviteEnabled;
|
||||
options.form.shareChannels = [...next.shareChannels];
|
||||
options.form.welcomeCoupons = [...next.welcomeCoupons];
|
||||
options.form.inviterCoupons = [...next.inviterCoupons];
|
||||
options.form.inviteeCoupons = [...next.inviteeCoupons];
|
||||
}
|
||||
|
||||
async function loadStores() {
|
||||
options.isStoreLoading.value = true;
|
||||
try {
|
||||
const result = await getStoreListApi({
|
||||
page: 1,
|
||||
pageSize: 200,
|
||||
});
|
||||
options.stores.value = result.items ?? [];
|
||||
|
||||
if (options.stores.value.length === 0) {
|
||||
options.selectedStoreId.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const exists = options.stores.value.some(
|
||||
(item) => item.id === options.selectedStoreId.value,
|
||||
);
|
||||
if (!exists) {
|
||||
options.selectedStoreId.value = options.stores.value[0]?.id ?? '';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
message.error('加载门店失败');
|
||||
} finally {
|
||||
options.isStoreLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDetail(resetForm: boolean) {
|
||||
if (!options.selectedStoreId.value) {
|
||||
options.stats.value = createDefaultNewCustomerStats();
|
||||
options.invitePager.value = createDefaultNewCustomerInvitePager();
|
||||
options.updatedAt.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
options.isDetailLoading.value = true;
|
||||
try {
|
||||
const detail = await getMarketingNewCustomerDetailApi({
|
||||
storeId: options.selectedStoreId.value,
|
||||
recordPage: options.invitePager.value.page,
|
||||
recordPageSize: options.invitePager.value.pageSize,
|
||||
});
|
||||
|
||||
options.stats.value = detail.stats;
|
||||
options.updatedAt.value = detail.settings.updatedAt;
|
||||
|
||||
if (resetForm) {
|
||||
applyForm(mapSettingsToPageForm(detail.settings));
|
||||
}
|
||||
|
||||
options.invitePager.value = {
|
||||
items: detail.inviteRecords.items ?? [],
|
||||
page: detail.inviteRecords.page,
|
||||
pageSize: detail.inviteRecords.pageSize,
|
||||
totalCount: detail.inviteRecords.totalCount,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
options.stats.value = createDefaultNewCustomerStats();
|
||||
options.invitePager.value = createDefaultNewCustomerInvitePager();
|
||||
message.error('加载新客有礼失败');
|
||||
} finally {
|
||||
options.isDetailLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadInviteRecords() {
|
||||
if (!options.selectedStoreId.value) {
|
||||
options.invitePager.value = createDefaultNewCustomerInvitePager();
|
||||
return;
|
||||
}
|
||||
|
||||
options.isInviteLoading.value = true;
|
||||
try {
|
||||
const result = await getMarketingNewCustomerInviteRecordListApi({
|
||||
storeId: options.selectedStoreId.value,
|
||||
page: options.invitePager.value.page,
|
||||
pageSize: options.invitePager.value.pageSize,
|
||||
});
|
||||
|
||||
options.invitePager.value = {
|
||||
items: result.items ?? [],
|
||||
page: result.page,
|
||||
pageSize: result.pageSize,
|
||||
totalCount: result.totalCount,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
options.invitePager.value = {
|
||||
...options.invitePager.value,
|
||||
items: [],
|
||||
totalCount: 0,
|
||||
};
|
||||
message.error('加载邀请记录失败');
|
||||
} finally {
|
||||
options.isInviteLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loadDetail,
|
||||
loadInviteRecords,
|
||||
loadStores,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import type { ComputedRef } from 'vue';
|
||||
|
||||
import type { MarketingNewCustomerCouponScene } from '#/api/marketing';
|
||||
import type {
|
||||
NewCustomerCouponDrawerForm,
|
||||
NewCustomerPageForm,
|
||||
} from '#/views/marketing/new-customer/types';
|
||||
|
||||
/**
|
||||
* 文件职责:新客有礼券抽屉动作。
|
||||
* 1. 管理抽屉开关、字段更新与添加券行为。
|
||||
*/
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { createDefaultNewCustomerCouponDrawerForm } from './constants';
|
||||
import { normalizeCouponSort, toNullableNumber } from './helpers';
|
||||
|
||||
interface CreateDrawerActionsOptions {
|
||||
canManage: ComputedRef<boolean>;
|
||||
form: NewCustomerPageForm;
|
||||
}
|
||||
|
||||
export function createDrawerActions(options: CreateDrawerActionsOptions) {
|
||||
const isCouponDrawerOpen = ref(false);
|
||||
const couponDrawerScene = ref<MarketingNewCustomerCouponScene>('welcome');
|
||||
const couponDrawerForm = reactive<NewCustomerCouponDrawerForm>(
|
||||
createDefaultNewCustomerCouponDrawerForm(),
|
||||
);
|
||||
|
||||
function setCouponDrawerOpen(value: boolean) {
|
||||
isCouponDrawerOpen.value = value;
|
||||
}
|
||||
|
||||
function setCouponDrawerType(
|
||||
value: NewCustomerCouponDrawerForm['couponType'],
|
||||
) {
|
||||
couponDrawerForm.couponType = value;
|
||||
if (value === 'free_shipping') {
|
||||
couponDrawerForm.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
function setCouponDrawerValue(value: null | number) {
|
||||
couponDrawerForm.value = value;
|
||||
}
|
||||
|
||||
function setCouponDrawerMinimumSpend(value: null | number) {
|
||||
couponDrawerForm.minimumSpend = value;
|
||||
}
|
||||
|
||||
function setCouponDrawerValidDays(value: null | number) {
|
||||
couponDrawerForm.validDays = Math.max(1, Math.min(365, Number(value || 1)));
|
||||
}
|
||||
|
||||
function openCouponDrawer(scene: MarketingNewCustomerCouponScene) {
|
||||
if (!options.canManage.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
resetCouponDrawerForm();
|
||||
couponDrawerScene.value = scene;
|
||||
isCouponDrawerOpen.value = true;
|
||||
}
|
||||
|
||||
function closeCouponDrawer() {
|
||||
isCouponDrawerOpen.value = false;
|
||||
}
|
||||
|
||||
function submitCouponDrawer() {
|
||||
if (!validateBeforeSubmit()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const next = {
|
||||
id: `draft-${Date.now()}-${Math.round(Math.random() * 1000)}`,
|
||||
scene: couponDrawerScene.value,
|
||||
couponType: couponDrawerForm.couponType,
|
||||
value:
|
||||
couponDrawerForm.couponType === 'free_shipping'
|
||||
? null
|
||||
: toNullableNumber(couponDrawerForm.value),
|
||||
minimumSpend: toNullableNumber(couponDrawerForm.minimumSpend),
|
||||
validDays: Math.max(
|
||||
1,
|
||||
Math.min(365, Math.floor(couponDrawerForm.validDays)),
|
||||
),
|
||||
sortOrder: 0,
|
||||
};
|
||||
|
||||
if (couponDrawerScene.value === 'welcome') {
|
||||
options.form.welcomeCoupons = normalizeCouponSort('welcome', [
|
||||
...options.form.welcomeCoupons,
|
||||
next,
|
||||
]);
|
||||
} else if (couponDrawerScene.value === 'inviter') {
|
||||
options.form.inviterCoupons = normalizeCouponSort('inviter', [
|
||||
...options.form.inviterCoupons,
|
||||
next,
|
||||
]);
|
||||
} else {
|
||||
options.form.inviteeCoupons = normalizeCouponSort('invitee', [
|
||||
...options.form.inviteeCoupons,
|
||||
next,
|
||||
]);
|
||||
}
|
||||
|
||||
isCouponDrawerOpen.value = false;
|
||||
}
|
||||
|
||||
function validateBeforeSubmit() {
|
||||
const minimumSpend = toNullableNumber(couponDrawerForm.minimumSpend) ?? 0;
|
||||
if (minimumSpend < 0) {
|
||||
message.warning('使用门槛不能小于 0');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (couponDrawerForm.validDays < 1 || couponDrawerForm.validDays > 365) {
|
||||
message.warning('有效期必须在 1-365 天之间');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (couponDrawerForm.couponType === 'free_shipping') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const value = toNullableNumber(couponDrawerForm.value);
|
||||
if (value === null || value <= 0) {
|
||||
message.warning('请填写正确的券面额');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (couponDrawerForm.couponType === 'discount' && value >= 10) {
|
||||
message.warning('折扣券面额必须小于 10');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
couponDrawerForm.couponType === 'amount_off' &&
|
||||
minimumSpend > 0 &&
|
||||
value >= minimumSpend
|
||||
) {
|
||||
message.warning('满减券减免金额必须小于使用门槛');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function resetCouponDrawerForm() {
|
||||
const next = createDefaultNewCustomerCouponDrawerForm();
|
||||
couponDrawerForm.couponType = next.couponType;
|
||||
couponDrawerForm.value = next.value;
|
||||
couponDrawerForm.minimumSpend = next.minimumSpend;
|
||||
couponDrawerForm.validDays = next.validDays;
|
||||
}
|
||||
|
||||
return {
|
||||
closeCouponDrawer,
|
||||
couponDrawerForm,
|
||||
couponDrawerScene,
|
||||
isCouponDrawerOpen,
|
||||
openCouponDrawer,
|
||||
setCouponDrawerMinimumSpend,
|
||||
setCouponDrawerOpen,
|
||||
setCouponDrawerType,
|
||||
setCouponDrawerValidDays,
|
||||
setCouponDrawerValue,
|
||||
submitCouponDrawer,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import type { ComputedRef, Ref } from 'vue';
|
||||
|
||||
import type { MarketingNewCustomerCouponScene } from '#/api/marketing';
|
||||
import type { NewCustomerPageForm } from '#/views/marketing/new-customer/types';
|
||||
|
||||
/**
|
||||
* 文件职责:新客有礼页面编辑与保存动作。
|
||||
* 1. 管理主表单字段更新。
|
||||
* 2. 负责保存与重置。
|
||||
*/
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { saveMarketingNewCustomerSettingsApi } from '#/api/marketing';
|
||||
|
||||
import { buildSaveSettingsPayload, normalizeCouponSort } from './helpers';
|
||||
|
||||
interface CreateEditorActionsOptions {
|
||||
canManage: ComputedRef<boolean>;
|
||||
form: NewCustomerPageForm;
|
||||
isSaving: Ref<boolean>;
|
||||
loadDetail: (resetForm: boolean) => Promise<void>;
|
||||
selectedStoreId: Ref<string>;
|
||||
}
|
||||
|
||||
export function createEditorActions(options: CreateEditorActionsOptions) {
|
||||
function setGiftEnabled(value: boolean) {
|
||||
options.form.giftEnabled = value;
|
||||
}
|
||||
|
||||
function setGiftType(value: NewCustomerPageForm['giftType']) {
|
||||
options.form.giftType = value;
|
||||
}
|
||||
|
||||
function setDirectReduceAmount(value: null | number) {
|
||||
options.form.directReduceAmount = value;
|
||||
}
|
||||
|
||||
function setDirectMinimumSpend(value: null | number) {
|
||||
options.form.directMinimumSpend = value;
|
||||
}
|
||||
|
||||
function setInviteEnabled(value: boolean) {
|
||||
options.form.inviteEnabled = value;
|
||||
}
|
||||
|
||||
function toggleShareChannel(
|
||||
channel: NewCustomerPageForm['shareChannels'][number],
|
||||
) {
|
||||
if (options.form.shareChannels.includes(channel)) {
|
||||
options.form.shareChannels = options.form.shareChannels.filter(
|
||||
(item) => item !== channel,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
options.form.shareChannels = [...options.form.shareChannels, channel];
|
||||
}
|
||||
|
||||
function removeCoupon(scene: MarketingNewCustomerCouponScene, index: number) {
|
||||
const list = resolveCouponList(scene).filter((_, idx) => idx !== index);
|
||||
applyCouponList(scene, list);
|
||||
}
|
||||
|
||||
async function resetSettings() {
|
||||
await options.loadDetail(true);
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
if (!options.canManage.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!options.selectedStoreId.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateBeforeSubmit()) {
|
||||
return;
|
||||
}
|
||||
|
||||
options.isSaving.value = true;
|
||||
try {
|
||||
await saveMarketingNewCustomerSettingsApi(
|
||||
buildSaveSettingsPayload(options.form, options.selectedStoreId.value),
|
||||
);
|
||||
message.success('新客有礼设置已保存');
|
||||
await options.loadDetail(true);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
options.isSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function validateBeforeSubmit() {
|
||||
if (options.form.shareChannels.length === 0) {
|
||||
message.warning('请至少选择一个分享渠道');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
options.form.giftType === 'coupon' &&
|
||||
options.form.welcomeCoupons.length === 0
|
||||
) {
|
||||
message.warning('新客礼包至少需要添加一张优惠券');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (options.form.giftType === 'direct') {
|
||||
if (
|
||||
options.form.directReduceAmount === null ||
|
||||
options.form.directReduceAmount <= 0
|
||||
) {
|
||||
message.warning('首单直减金额必须大于 0');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
options.form.directMinimumSpend === null ||
|
||||
options.form.directMinimumSpend < 0
|
||||
) {
|
||||
message.warning('首单直减使用门槛不能小于 0');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
options.form.inviteEnabled &&
|
||||
(options.form.inviterCoupons.length === 0 ||
|
||||
options.form.inviteeCoupons.length === 0)
|
||||
) {
|
||||
message.warning('开启老带新后必须配置邀请人和被邀请人奖励券');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function resolveCouponList(scene: MarketingNewCustomerCouponScene) {
|
||||
if (scene === 'welcome') {
|
||||
return options.form.welcomeCoupons;
|
||||
}
|
||||
if (scene === 'inviter') {
|
||||
return options.form.inviterCoupons;
|
||||
}
|
||||
return options.form.inviteeCoupons;
|
||||
}
|
||||
|
||||
function applyCouponList(
|
||||
scene: MarketingNewCustomerCouponScene,
|
||||
list: NewCustomerPageForm['welcomeCoupons'],
|
||||
) {
|
||||
const normalized = normalizeCouponSort(scene, list);
|
||||
if (scene === 'welcome') {
|
||||
options.form.welcomeCoupons = normalized;
|
||||
return;
|
||||
}
|
||||
if (scene === 'inviter') {
|
||||
options.form.inviterCoupons = normalized;
|
||||
return;
|
||||
}
|
||||
options.form.inviteeCoupons = normalized;
|
||||
}
|
||||
|
||||
return {
|
||||
removeCoupon,
|
||||
resetSettings,
|
||||
saveSettings,
|
||||
setDirectMinimumSpend,
|
||||
setDirectReduceAmount,
|
||||
setGiftEnabled,
|
||||
setGiftType,
|
||||
setInviteEnabled,
|
||||
toggleShareChannel,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import type {
|
||||
MarketingNewCustomerCouponScene,
|
||||
MarketingNewCustomerSettingsDto,
|
||||
SaveMarketingNewCustomerCouponRuleDto,
|
||||
SaveMarketingNewCustomerSettingsDto,
|
||||
} from '#/api/marketing';
|
||||
import type {
|
||||
NewCustomerCouponFormItem,
|
||||
NewCustomerPageForm,
|
||||
} from '#/views/marketing/new-customer/types';
|
||||
|
||||
import { toCouponFormItem } from '#/views/marketing/new-customer/types';
|
||||
|
||||
/**
|
||||
* 文件职责:新客有礼页面纯函数工具。
|
||||
*/
|
||||
|
||||
/** 格式化百分比。 */
|
||||
export function formatPercent(value: number) {
|
||||
return Number(value || 0).toFixed(1);
|
||||
}
|
||||
|
||||
/** 格式化整数。 */
|
||||
export function formatInteger(value: number) {
|
||||
return Math.trunc(Number(value || 0)).toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
/** 格式化金额。 */
|
||||
export function formatMoney(value: null | number) {
|
||||
if (value === null || value === undefined) {
|
||||
return '0';
|
||||
}
|
||||
return Number(value).toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
}
|
||||
|
||||
/** 生成券标题。 */
|
||||
export function formatCouponName(item: NewCustomerCouponFormItem) {
|
||||
if (item.couponType === 'free_shipping') {
|
||||
return '免配送费';
|
||||
}
|
||||
|
||||
if (item.couponType === 'discount') {
|
||||
return `满${formatMoney(item.minimumSpend)}享${formatMoney(item.value)}折`;
|
||||
}
|
||||
|
||||
return `满${formatMoney(item.minimumSpend)}减${formatMoney(item.value)}`;
|
||||
}
|
||||
|
||||
/** 生成券描述。 */
|
||||
export function formatCouponDescription(item: NewCustomerCouponFormItem) {
|
||||
if (item.couponType === 'free_shipping') {
|
||||
return '无门槛,减免配送费';
|
||||
}
|
||||
|
||||
if (!item.minimumSpend || item.minimumSpend <= 0) {
|
||||
return '无门槛可用';
|
||||
}
|
||||
|
||||
return `订单满 ${formatMoney(item.minimumSpend)} 元可用`;
|
||||
}
|
||||
|
||||
/** 重建排序。 */
|
||||
export function normalizeCouponSort(
|
||||
scene: MarketingNewCustomerCouponScene,
|
||||
items: NewCustomerCouponFormItem[],
|
||||
) {
|
||||
return items.map((item, index) => ({
|
||||
...item,
|
||||
scene,
|
||||
sortOrder: index + 1,
|
||||
}));
|
||||
}
|
||||
|
||||
/** 拷贝券列表。 */
|
||||
export function cloneCouponItems(items: NewCustomerCouponFormItem[]) {
|
||||
return items.map((item) => ({ ...item }));
|
||||
}
|
||||
|
||||
/** 应用后端设置到页面表单。 */
|
||||
export function mapSettingsToPageForm(
|
||||
settings: MarketingNewCustomerSettingsDto,
|
||||
): NewCustomerPageForm {
|
||||
return {
|
||||
giftEnabled: settings.giftEnabled,
|
||||
giftType: settings.giftType,
|
||||
directReduceAmount: settings.directReduceAmount,
|
||||
directMinimumSpend: settings.directMinimumSpend,
|
||||
inviteEnabled: settings.inviteEnabled,
|
||||
shareChannels: [...settings.shareChannels],
|
||||
welcomeCoupons: normalizeCouponSort(
|
||||
'welcome',
|
||||
settings.welcomeCoupons.map((item) => toCouponFormItem(item)),
|
||||
),
|
||||
inviterCoupons: normalizeCouponSort(
|
||||
'inviter',
|
||||
settings.inviterCoupons.map((item) => toCouponFormItem(item)),
|
||||
),
|
||||
inviteeCoupons: normalizeCouponSort(
|
||||
'invitee',
|
||||
settings.inviteeCoupons.map((item) => toCouponFormItem(item)),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/** 构建保存请求。 */
|
||||
export function buildSaveSettingsPayload(
|
||||
form: NewCustomerPageForm,
|
||||
storeId: string,
|
||||
): SaveMarketingNewCustomerSettingsDto {
|
||||
return {
|
||||
storeId,
|
||||
giftEnabled: form.giftEnabled,
|
||||
giftType: form.giftType,
|
||||
directReduceAmount:
|
||||
form.giftType === 'direct'
|
||||
? toNullableNumber(form.directReduceAmount)
|
||||
: null,
|
||||
directMinimumSpend:
|
||||
form.giftType === 'direct'
|
||||
? toNullableNumber(form.directMinimumSpend)
|
||||
: null,
|
||||
inviteEnabled: form.inviteEnabled,
|
||||
shareChannels: [...form.shareChannels],
|
||||
welcomeCoupons: buildSaveCouponRules(form.welcomeCoupons),
|
||||
inviterCoupons: buildSaveCouponRules(form.inviterCoupons),
|
||||
inviteeCoupons: buildSaveCouponRules(form.inviteeCoupons),
|
||||
};
|
||||
}
|
||||
|
||||
/** 构建保存券列表。 */
|
||||
export function buildSaveCouponRules(
|
||||
items: NewCustomerCouponFormItem[],
|
||||
): SaveMarketingNewCustomerCouponRuleDto[] {
|
||||
return items.map((item) => ({
|
||||
couponType: item.couponType,
|
||||
value:
|
||||
item.couponType === 'free_shipping' ? null : toNullableNumber(item.value),
|
||||
minimumSpend: toNullableNumber(item.minimumSpend),
|
||||
validDays: Math.max(1, Math.min(365, Math.floor(item.validDays))),
|
||||
}));
|
||||
}
|
||||
|
||||
/** 解析可空数字。 */
|
||||
export function toNullableNumber(value: null | number | string) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return null;
|
||||
}
|
||||
const parsed = Number(value);
|
||||
if (Number.isNaN(parsed)) {
|
||||
return null;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import type { StoreListItemDto } from '#/api/store';
|
||||
|
||||
/**
|
||||
* 文件职责:新客有礼页面状态与行为编排。
|
||||
* 1. 管理门店、统计、配置表单与邀请记录分页状态。
|
||||
* 2. 编排保存动作与优惠券抽屉动作。
|
||||
*/
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
import {
|
||||
createDefaultNewCustomerInvitePager,
|
||||
createDefaultNewCustomerPageForm,
|
||||
createDefaultNewCustomerStats,
|
||||
NEW_CUSTOMER_MANAGE_PERMISSION,
|
||||
} from './new-customer-page/constants';
|
||||
import { createDataActions } from './new-customer-page/data-actions';
|
||||
import { createDrawerActions } from './new-customer-page/drawer-actions';
|
||||
import { createEditorActions } from './new-customer-page/editor-actions';
|
||||
|
||||
export function useMarketingNewCustomerPage() {
|
||||
const accessStore = useAccessStore();
|
||||
|
||||
const stores = ref<StoreListItemDto[]>([]);
|
||||
const selectedStoreId = ref('');
|
||||
const isStoreLoading = ref(false);
|
||||
|
||||
const isDetailLoading = ref(false);
|
||||
const isInviteLoading = ref(false);
|
||||
const isSaving = ref(false);
|
||||
|
||||
const form = reactive(createDefaultNewCustomerPageForm());
|
||||
const stats = ref(createDefaultNewCustomerStats());
|
||||
const invitePager = ref(createDefaultNewCustomerInvitePager());
|
||||
const updatedAt = ref('');
|
||||
|
||||
const storeOptions = computed(() =>
|
||||
stores.value.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
})),
|
||||
);
|
||||
const hasStore = computed(() => stores.value.length > 0);
|
||||
|
||||
const accessCodeSet = computed(
|
||||
() => new Set((accessStore.accessCodes ?? []).map(String)),
|
||||
);
|
||||
const canManage = computed(() =>
|
||||
accessCodeSet.value.has(NEW_CUSTOMER_MANAGE_PERMISSION),
|
||||
);
|
||||
|
||||
function setSelectedStoreId(value: string) {
|
||||
selectedStoreId.value = value;
|
||||
}
|
||||
|
||||
function setInvitePagerPage(page: number) {
|
||||
invitePager.value = {
|
||||
...invitePager.value,
|
||||
page,
|
||||
};
|
||||
}
|
||||
|
||||
function setInvitePagerPageSize(pageSize: number) {
|
||||
invitePager.value = {
|
||||
...invitePager.value,
|
||||
pageSize,
|
||||
};
|
||||
}
|
||||
|
||||
const { loadStores, loadDetail, loadInviteRecords } = createDataActions({
|
||||
stores,
|
||||
selectedStoreId,
|
||||
isStoreLoading,
|
||||
isDetailLoading,
|
||||
isInviteLoading,
|
||||
form,
|
||||
stats,
|
||||
invitePager,
|
||||
updatedAt,
|
||||
});
|
||||
|
||||
const {
|
||||
removeCoupon,
|
||||
resetSettings,
|
||||
saveSettings,
|
||||
setDirectMinimumSpend,
|
||||
setDirectReduceAmount,
|
||||
setGiftEnabled,
|
||||
setGiftType,
|
||||
setInviteEnabled,
|
||||
toggleShareChannel,
|
||||
} = createEditorActions({
|
||||
canManage,
|
||||
form,
|
||||
isSaving,
|
||||
loadDetail,
|
||||
selectedStoreId,
|
||||
});
|
||||
|
||||
const {
|
||||
closeCouponDrawer,
|
||||
couponDrawerForm,
|
||||
couponDrawerScene,
|
||||
isCouponDrawerOpen,
|
||||
openCouponDrawer,
|
||||
setCouponDrawerMinimumSpend,
|
||||
setCouponDrawerType,
|
||||
setCouponDrawerValidDays,
|
||||
setCouponDrawerValue,
|
||||
submitCouponDrawer,
|
||||
} = createDrawerActions({
|
||||
canManage,
|
||||
form,
|
||||
});
|
||||
|
||||
async function handleInvitePageChange(page: number, pageSize: number) {
|
||||
setInvitePagerPage(page);
|
||||
setInvitePagerPageSize(pageSize);
|
||||
await loadInviteRecords();
|
||||
}
|
||||
|
||||
watch(selectedStoreId, async () => {
|
||||
invitePager.value = {
|
||||
...invitePager.value,
|
||||
page: 1,
|
||||
};
|
||||
await loadDetail(true);
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await loadStores();
|
||||
});
|
||||
|
||||
return {
|
||||
canManage,
|
||||
closeCouponDrawer,
|
||||
couponDrawerForm,
|
||||
couponDrawerScene,
|
||||
form,
|
||||
handleInvitePageChange,
|
||||
hasStore,
|
||||
invitePager,
|
||||
isCouponDrawerOpen,
|
||||
isDetailLoading,
|
||||
isInviteLoading,
|
||||
isSaving,
|
||||
isStoreLoading,
|
||||
openCouponDrawer,
|
||||
removeCoupon,
|
||||
resetSettings,
|
||||
saveSettings,
|
||||
selectedStoreId,
|
||||
setCouponDrawerMinimumSpend,
|
||||
setCouponDrawerType,
|
||||
setCouponDrawerValidDays,
|
||||
setCouponDrawerValue,
|
||||
setDirectMinimumSpend,
|
||||
setDirectReduceAmount,
|
||||
setGiftEnabled,
|
||||
setGiftType,
|
||||
setInviteEnabled,
|
||||
setSelectedStoreId,
|
||||
stats,
|
||||
storeOptions,
|
||||
submitCouponDrawer,
|
||||
toggleShareChannel,
|
||||
updatedAt,
|
||||
};
|
||||
}
|
||||
316
apps/web-antd/src/views/marketing/new-customer/index.vue
Normal file
316
apps/web-antd/src/views/marketing/new-customer/index.vue
Normal file
@@ -0,0 +1,316 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 文件职责:营销中心-新客有礼页面主视图。
|
||||
* 1. 还原原型布局(工具栏、横幅、统计、配置区、邀请区、底部保存栏)。
|
||||
* 2. 编排新客券二级抽屉与页面保存行为。
|
||||
*/
|
||||
import type { MarketingNewCustomerCouponScene } from '#/api/marketing';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
InputNumber,
|
||||
Select,
|
||||
Spin,
|
||||
Switch,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import NewCustomerCouponDrawer from './components/NewCustomerCouponDrawer.vue';
|
||||
import NewCustomerCouponGroup from './components/NewCustomerCouponGroup.vue';
|
||||
import NewCustomerInviteRecordTable from './components/NewCustomerInviteRecordTable.vue';
|
||||
import NewCustomerStatsCards from './components/NewCustomerStatsCards.vue';
|
||||
import {
|
||||
NEW_CUSTOMER_GIFT_TYPE_OPTIONS,
|
||||
NEW_CUSTOMER_SHARE_CHANNEL_OPTIONS,
|
||||
} from './composables/new-customer-page/constants';
|
||||
import { useMarketingNewCustomerPage } from './composables/useMarketingNewCustomerPage';
|
||||
|
||||
const {
|
||||
canManage,
|
||||
closeCouponDrawer,
|
||||
couponDrawerForm,
|
||||
couponDrawerScene,
|
||||
form,
|
||||
handleInvitePageChange,
|
||||
hasStore,
|
||||
invitePager,
|
||||
isCouponDrawerOpen,
|
||||
isDetailLoading,
|
||||
isInviteLoading,
|
||||
isSaving,
|
||||
isStoreLoading,
|
||||
openCouponDrawer,
|
||||
removeCoupon,
|
||||
resetSettings,
|
||||
saveSettings,
|
||||
selectedStoreId,
|
||||
setCouponDrawerMinimumSpend,
|
||||
setCouponDrawerType,
|
||||
setCouponDrawerValidDays,
|
||||
setCouponDrawerValue,
|
||||
setDirectMinimumSpend,
|
||||
setDirectReduceAmount,
|
||||
setGiftEnabled,
|
||||
setGiftType,
|
||||
setInviteEnabled,
|
||||
setSelectedStoreId,
|
||||
stats,
|
||||
storeOptions,
|
||||
submitCouponDrawer,
|
||||
toggleShareChannel,
|
||||
updatedAt,
|
||||
} = useMarketingNewCustomerPage();
|
||||
|
||||
function parseNullableNumber(value: null | number | string) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return null;
|
||||
}
|
||||
const parsed = Number(value);
|
||||
return Number.isNaN(parsed) ? null : parsed;
|
||||
}
|
||||
|
||||
function openSceneCouponDrawer(scene: MarketingNewCustomerCouponScene) {
|
||||
openCouponDrawer(scene);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="新客有礼" content-class="page-marketing-new-customer">
|
||||
<div class="mnc-page">
|
||||
<div class="mnc-toolbar">
|
||||
<Select
|
||||
class="mnc-store-select"
|
||||
:value="selectedStoreId"
|
||||
:options="storeOptions"
|
||||
:loading="isStoreLoading"
|
||||
placeholder="请选择门店"
|
||||
@update:value="(value) => setSelectedStoreId(String(value ?? ''))"
|
||||
/>
|
||||
|
||||
<span v-if="!canManage" class="mnc-readonly-tip">当前为只读权限</span>
|
||||
</div>
|
||||
|
||||
<div class="mnc-banner">
|
||||
<span class="mnc-banner-icon">
|
||||
<IconifyIcon icon="lucide:gift" />
|
||||
</span>
|
||||
<div class="mnc-banner-text">
|
||||
<strong>新客有礼</strong>
|
||||
新用户首次下单自动发放优惠礼包,提升新客转化率。开启后,每位新注册用户将自动收到您配置的欢迎礼包。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NewCustomerStatsCards v-if="hasStore" :stats="stats" />
|
||||
|
||||
<div v-if="!hasStore" class="mnc-empty">暂无门店,请先创建门店</div>
|
||||
|
||||
<Spin v-else :spinning="isDetailLoading">
|
||||
<Card class="mnc-card" :bordered="false">
|
||||
<div class="mnc-section-title">新客礼包配置</div>
|
||||
|
||||
<div class="mnc-row">
|
||||
<div class="mnc-row-label">启用状态</div>
|
||||
<div class="mnc-row-control">
|
||||
<Switch
|
||||
:checked="form.giftEnabled"
|
||||
:disabled="!canManage"
|
||||
@update:checked="(value) => setGiftEnabled(Boolean(value))"
|
||||
/>
|
||||
<span class="mnc-row-hint">开启后新用户注册即发放礼包</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mnc-row">
|
||||
<div class="mnc-row-label">礼包类型</div>
|
||||
<div class="mnc-row-control mnc-inline">
|
||||
<button
|
||||
v-for="item in NEW_CUSTOMER_GIFT_TYPE_OPTIONS"
|
||||
:key="item.value"
|
||||
type="button"
|
||||
class="mnc-pill"
|
||||
:class="{ checked: form.giftType === item.value }"
|
||||
:disabled="!canManage"
|
||||
@click="setGiftType(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="form.giftType === 'coupon'">
|
||||
<div class="mnc-row mnc-row-block">
|
||||
<NewCustomerCouponGroup
|
||||
title="券包内容"
|
||||
add-text="添加优惠券"
|
||||
:coupons="form.welcomeCoupons"
|
||||
:can-manage="canManage"
|
||||
@add="openSceneCouponDrawer('welcome')"
|
||||
@remove="(index) => removeCoupon('welcome', index)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="mnc-row">
|
||||
<div class="mnc-row-label">直减金额</div>
|
||||
<div class="mnc-row-control">
|
||||
<div class="mnc-inline">
|
||||
<span>¥</span>
|
||||
<InputNumber
|
||||
:value="form.directReduceAmount ?? undefined"
|
||||
:min="0.01"
|
||||
:precision="2"
|
||||
:disabled="!canManage"
|
||||
placeholder="如:10"
|
||||
@update:value="
|
||||
(value) =>
|
||||
setDirectReduceAmount(parseNullableNumber(value))
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="mnc-row-hint">新用户首单自动减免的金额</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mnc-row">
|
||||
<div class="mnc-row-label">使用门槛</div>
|
||||
<div class="mnc-row-control">
|
||||
<div class="mnc-inline">
|
||||
<span>订单满</span>
|
||||
<InputNumber
|
||||
:value="form.directMinimumSpend ?? undefined"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:disabled="!canManage"
|
||||
placeholder="如:30"
|
||||
@update:value="
|
||||
(value) =>
|
||||
setDirectMinimumSpend(parseNullableNumber(value))
|
||||
"
|
||||
/>
|
||||
<span>元可用</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<Card class="mnc-card" :bordered="false">
|
||||
<div class="mnc-section-title">老带新分享</div>
|
||||
|
||||
<div class="mnc-row">
|
||||
<div class="mnc-row-label">启用状态</div>
|
||||
<div class="mnc-row-control">
|
||||
<Switch
|
||||
:checked="form.inviteEnabled"
|
||||
:disabled="!canManage"
|
||||
@update:checked="(value) => setInviteEnabled(Boolean(value))"
|
||||
/>
|
||||
<span class="mnc-row-hint">
|
||||
开启后顾客可在小程序内分享邀请好友
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mnc-row mnc-row-block">
|
||||
<NewCustomerCouponGroup
|
||||
title="邀请人奖励"
|
||||
description="成功邀请新用户下单后发放"
|
||||
add-text="添加奖励券"
|
||||
:coupons="form.inviterCoupons"
|
||||
:can-manage="canManage"
|
||||
@add="openSceneCouponDrawer('inviter')"
|
||||
@remove="(index) => removeCoupon('inviter', index)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mnc-row mnc-row-block">
|
||||
<NewCustomerCouponGroup
|
||||
title="被邀请人奖励"
|
||||
description="新用户通过邀请注册后额外获得,叠加新客礼包"
|
||||
add-text="添加奖励券"
|
||||
:coupons="form.inviteeCoupons"
|
||||
:can-manage="canManage"
|
||||
@add="openSceneCouponDrawer('invitee')"
|
||||
@remove="(index) => removeCoupon('invitee', index)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mnc-row">
|
||||
<div class="mnc-row-label">分享渠道</div>
|
||||
<div class="mnc-row-control">
|
||||
<div class="mnc-inline">
|
||||
<button
|
||||
v-for="item in NEW_CUSTOMER_SHARE_CHANNEL_OPTIONS"
|
||||
:key="item.value"
|
||||
type="button"
|
||||
class="mnc-pill"
|
||||
:class="{ checked: form.shareChannels.includes(item.value) }"
|
||||
:disabled="!canManage"
|
||||
@click="toggleShareChannel(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="mnc-row-hint">
|
||||
顾客在小程序内可通过以上渠道分享邀请
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mnc-row mnc-row-block">
|
||||
<div class="mnc-row-block-title">邀请记录(近期)</div>
|
||||
<div class="mnc-invite-table">
|
||||
<NewCustomerInviteRecordTable
|
||||
:records="invitePager.items"
|
||||
:page="invitePager.page"
|
||||
:page-size="invitePager.pageSize"
|
||||
:total="invitePager.totalCount"
|
||||
:loading="isInviteLoading"
|
||||
@page-change="handleInvitePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class="mnc-save-card" :bordered="false">
|
||||
<div class="mnc-save-bar">
|
||||
<span v-if="updatedAt" class="mnc-updated-at">
|
||||
最近更新:{{ updatedAt }}
|
||||
</span>
|
||||
<Button :disabled="!canManage || isSaving" @click="resetSettings">
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
:disabled="!canManage"
|
||||
:loading="isSaving"
|
||||
@click="saveSettings"
|
||||
>
|
||||
保存设置
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</Spin>
|
||||
</div>
|
||||
|
||||
<NewCustomerCouponDrawer
|
||||
:open="isCouponDrawerOpen"
|
||||
:form="couponDrawerForm"
|
||||
:scene="couponDrawerScene"
|
||||
@close="closeCouponDrawer"
|
||||
@set-coupon-type="setCouponDrawerType"
|
||||
@set-value="setCouponDrawerValue"
|
||||
@set-minimum-spend="setCouponDrawerMinimumSpend"
|
||||
@set-valid-days="setCouponDrawerValidDays"
|
||||
@submit="submitCouponDrawer"
|
||||
/>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style lang="less">
|
||||
@import './styles/index.less';
|
||||
</style>
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 文件职责:新客有礼页面基础变量样式。
|
||||
*/
|
||||
.page-marketing-new-customer {
|
||||
--mnc-transition: 220ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--mnc-shadow-sm: 0 1px 2px rgb(0 0 0 / 4%);
|
||||
--mnc-shadow-md: 0 8px 20px rgb(0 0 0 / 8%), 0 2px 4px rgb(0 0 0 / 6%);
|
||||
--mnc-border: #e7eaf0;
|
||||
--mnc-text: #1f2937;
|
||||
--mnc-subtext: #6b7280;
|
||||
--mnc-muted: #9ca3af;
|
||||
--mnc-primary: #1677ff;
|
||||
}
|
||||
168
apps/web-antd/src/views/marketing/new-customer/styles/card.less
Normal file
168
apps/web-antd/src/views/marketing/new-customer/styles/card.less
Normal file
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* 文件职责:新客有礼券列表与交互控件样式。
|
||||
*/
|
||||
.page-marketing-new-customer {
|
||||
.mnc-pill {
|
||||
height: 30px;
|
||||
padding: 0 14px;
|
||||
font-size: 12px;
|
||||
line-height: 28px;
|
||||
color: #4b5563;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
transition: all var(--mnc-transition);
|
||||
}
|
||||
|
||||
.mnc-pill:hover {
|
||||
color: var(--mnc-primary);
|
||||
border-color: #91caff;
|
||||
}
|
||||
|
||||
.mnc-pill.checked {
|
||||
color: var(--mnc-primary);
|
||||
background: #e8f3ff;
|
||||
border-color: #91caff;
|
||||
}
|
||||
|
||||
.mnc-pill:disabled {
|
||||
color: #b7bcc7;
|
||||
cursor: not-allowed;
|
||||
background: #fafafa;
|
||||
border-color: #e5e7eb;
|
||||
}
|
||||
|
||||
.mnc-coupon-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.mnc-coupon-group-title {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: baseline;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--mnc-text);
|
||||
}
|
||||
|
||||
.mnc-coupon-group-desc {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--mnc-muted);
|
||||
}
|
||||
|
||||
.mnc-coupon-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.mnc-coupon-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 12px 14px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--mnc-border);
|
||||
border-left: 3px solid var(--mnc-primary);
|
||||
border-radius: 8px;
|
||||
transition: box-shadow var(--mnc-transition);
|
||||
}
|
||||
|
||||
.mnc-coupon-item:nth-child(2) {
|
||||
border-left-color: #16a34a;
|
||||
}
|
||||
|
||||
.mnc-coupon-item:nth-child(3) {
|
||||
border-left-color: #d97706;
|
||||
}
|
||||
|
||||
.mnc-coupon-item:hover {
|
||||
box-shadow: var(--mnc-shadow-sm);
|
||||
}
|
||||
|
||||
.mnc-coupon-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mnc-coupon-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--mnc-text);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mnc-coupon-desc {
|
||||
margin-top: 2px;
|
||||
font-size: 11px;
|
||||
color: var(--mnc-muted);
|
||||
}
|
||||
|
||||
.mnc-coupon-validity {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: var(--mnc-subtext);
|
||||
}
|
||||
|
||||
.mnc-coupon-validity .iconify {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.mnc-coupon-remove {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.mnc-coupon-remove .iconify {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.mnc-coupon-empty .ant-empty-image {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.mnc-coupon-empty .ant-empty-description {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.mnc-add-coupon-btn {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
color: var(--mnc-subtext);
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: 2px dashed #d9d9d9;
|
||||
border-radius: 8px;
|
||||
transition: all var(--mnc-transition);
|
||||
}
|
||||
|
||||
.mnc-add-coupon-btn:hover {
|
||||
color: var(--mnc-primary);
|
||||
background: #f5f9ff;
|
||||
border-color: var(--mnc-primary);
|
||||
}
|
||||
|
||||
.mnc-add-coupon-btn .iconify {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* 文件职责:新客有礼券抽屉样式。
|
||||
*/
|
||||
.mnc-coupon-drawer {
|
||||
.ant-drawer-header {
|
||||
min-height: 54px;
|
||||
padding: 0 18px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.ant-drawer-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.ant-drawer-body {
|
||||
padding: 14px 16px 12px;
|
||||
}
|
||||
|
||||
.ant-drawer-footer {
|
||||
padding: 10px 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.mnc-coupon-drawer-form {
|
||||
.ant-form-item {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.ant-form-item-label {
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.ant-form-item-label > label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-input-number {
|
||||
border-color: #e5e7eb !important;
|
||||
border-radius: 6px !important;
|
||||
}
|
||||
|
||||
.ant-input-number-input {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.ant-input-number:focus-within,
|
||||
.ant-radio-group
|
||||
.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled) {
|
||||
border-color: #1677ff !important;
|
||||
box-shadow: 0 0 0 2px rgb(22 119 255 / 10%) !important;
|
||||
}
|
||||
|
||||
.mnc-inline-fields {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.mnc-inline-fields .ant-input-number {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.mnc-field-hint {
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.mnc-shipping-tip {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
background: #f8f9fb;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.mnc-shipping-tip .iconify {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.mnc-coupon-drawer-footer {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
@import './base.less';
|
||||
@import './layout.less';
|
||||
@import './section.less';
|
||||
@import './card.less';
|
||||
@import './drawer.less';
|
||||
@import './table.less';
|
||||
@import './responsive.less';
|
||||
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* 文件职责:新客有礼页面布局样式。
|
||||
*/
|
||||
.page-marketing-new-customer {
|
||||
.mnc-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
max-width: 960px;
|
||||
}
|
||||
|
||||
.mnc-toolbar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 10px 14px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--mnc-border);
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--mnc-shadow-sm);
|
||||
}
|
||||
|
||||
.mnc-store-select {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.mnc-store-select .ant-select-selector {
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
|
||||
.mnc-readonly-tip {
|
||||
padding-inline: 10px;
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
line-height: 26px;
|
||||
color: #d97706;
|
||||
background: #fff7e6;
|
||||
border: 1px solid #ffd591;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.mnc-banner {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 14px 18px;
|
||||
background: linear-gradient(135deg, #eef6ff, #f8fbff);
|
||||
border: 1px solid #d5e6ff;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.mnc-banner-icon {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: var(--mnc-primary);
|
||||
background: #deecff;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.mnc-banner-icon .iconify {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.mnc-banner-text {
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: var(--mnc-subtext);
|
||||
}
|
||||
|
||||
.mnc-banner-text strong {
|
||||
margin-right: 6px;
|
||||
font-weight: 600;
|
||||
color: var(--mnc-text);
|
||||
}
|
||||
|
||||
.mnc-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.mnc-stat-card {
|
||||
padding: 18px 20px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--mnc-border);
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--mnc-shadow-sm);
|
||||
transition: box-shadow var(--mnc-transition);
|
||||
}
|
||||
|
||||
.mnc-stat-card:hover {
|
||||
box-shadow: var(--mnc-shadow-md);
|
||||
}
|
||||
|
||||
.mnc-stat-label {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--mnc-muted);
|
||||
}
|
||||
|
||||
.mnc-stat-label .iconify {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: var(--mnc-primary);
|
||||
}
|
||||
|
||||
.mnc-stat-value {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: baseline;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
color: var(--mnc-text);
|
||||
}
|
||||
|
||||
.mnc-stat-unit {
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
color: var(--mnc-muted);
|
||||
}
|
||||
|
||||
.mnc-stat-trend {
|
||||
display: inline-flex;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.mnc-stat-trend.up {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.mnc-stat-trend.down {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.mnc-stat-trend .iconify {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.mnc-stat-sub {
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
color: var(--mnc-muted);
|
||||
}
|
||||
|
||||
.mnc-empty {
|
||||
padding: 28px 14px;
|
||||
font-size: 13px;
|
||||
color: var(--mnc-muted);
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
border: 1px solid var(--mnc-border);
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--mnc-shadow-sm);
|
||||
}
|
||||
|
||||
.mnc-card,
|
||||
.mnc-save-card {
|
||||
border: 1px solid var(--mnc-border);
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--mnc-shadow-sm);
|
||||
}
|
||||
|
||||
.mnc-card .ant-card-body {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.mnc-save-card .ant-card-body {
|
||||
padding: 14px 18px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 文件职责:新客有礼页面响应式样式。
|
||||
*/
|
||||
.page-marketing-new-customer {
|
||||
@media (width <= 992px) {
|
||||
.mnc-page {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.mnc-stats {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.mnc-row {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mnc-row-label {
|
||||
width: auto;
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 768px) {
|
||||
.mnc-toolbar {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.mnc-readonly-tip {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.mnc-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.mnc-stat-value {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.mnc-coupon-item {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.mnc-coupon-validity {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.mnc-save-bar {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.mnc-updated-at {
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 文件职责:新客有礼页面区块与行结构样式。
|
||||
*/
|
||||
.page-marketing-new-customer {
|
||||
.mnc-section-title {
|
||||
padding-left: 10px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--mnc-text);
|
||||
border-left: 3px solid var(--mnc-primary);
|
||||
}
|
||||
|
||||
.mnc-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.mnc-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.mnc-row-block {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.mnc-row-label {
|
||||
flex-shrink: 0;
|
||||
width: 120px;
|
||||
padding-top: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
color: var(--mnc-text);
|
||||
}
|
||||
|
||||
.mnc-row-control {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mnc-row-control .ant-switch {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.mnc-row-hint {
|
||||
font-size: 11px;
|
||||
color: var(--mnc-muted);
|
||||
}
|
||||
|
||||
.mnc-inline {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mnc-inline .ant-input-number {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.mnc-inline .ant-input-number-input {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.mnc-row-block-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--mnc-text);
|
||||
}
|
||||
|
||||
.mnc-invite-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mnc-save-bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.mnc-updated-at {
|
||||
margin-right: auto;
|
||||
font-size: 12px;
|
||||
color: var(--mnc-muted);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 文件职责:新客有礼邀请记录表格样式。
|
||||
*/
|
||||
.page-marketing-new-customer {
|
||||
.mnc-invite-table {
|
||||
.ant-table-wrapper .ant-table {
|
||||
overflow: hidden;
|
||||
border: 1px solid #f1f3f8;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.ant-table-wrapper .ant-table-thead > tr > th {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
background: #fafcff;
|
||||
}
|
||||
}
|
||||
|
||||
.mnc-status-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 22px;
|
||||
padding: 0 9px;
|
||||
font-size: 12px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.mnc-status-tag.is-green {
|
||||
color: #166534;
|
||||
background: #dcfce7;
|
||||
}
|
||||
|
||||
.mnc-status-tag.is-orange {
|
||||
color: #c2410c;
|
||||
background: #ffedd5;
|
||||
}
|
||||
|
||||
.mnc-status-tag.is-gray {
|
||||
color: #475569;
|
||||
background: #e2e8f0;
|
||||
}
|
||||
}
|
||||
75
apps/web-antd/src/views/marketing/new-customer/types.ts
Normal file
75
apps/web-antd/src/views/marketing/new-customer/types.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import type {
|
||||
MarketingNewCustomerCouponRuleDto,
|
||||
MarketingNewCustomerCouponScene,
|
||||
MarketingNewCustomerCouponType,
|
||||
MarketingNewCustomerGiftType,
|
||||
MarketingNewCustomerInviteRecordDto,
|
||||
MarketingNewCustomerShareChannel,
|
||||
MarketingNewCustomerStatsDto,
|
||||
} from '#/api/marketing';
|
||||
|
||||
/**
|
||||
* 文件职责:新客有礼页面类型定义。
|
||||
*/
|
||||
|
||||
/** 页面可编辑优惠券项。 */
|
||||
export interface NewCustomerCouponFormItem {
|
||||
couponType: MarketingNewCustomerCouponType;
|
||||
id: string;
|
||||
minimumSpend: null | number;
|
||||
scene: MarketingNewCustomerCouponScene;
|
||||
sortOrder: number;
|
||||
validDays: number;
|
||||
value: null | number;
|
||||
}
|
||||
|
||||
/** 页面可编辑表单。 */
|
||||
export interface NewCustomerPageForm {
|
||||
directMinimumSpend: null | number;
|
||||
directReduceAmount: null | number;
|
||||
giftEnabled: boolean;
|
||||
giftType: MarketingNewCustomerGiftType;
|
||||
inviteEnabled: boolean;
|
||||
inviteeCoupons: NewCustomerCouponFormItem[];
|
||||
inviterCoupons: NewCustomerCouponFormItem[];
|
||||
shareChannels: MarketingNewCustomerShareChannel[];
|
||||
welcomeCoupons: NewCustomerCouponFormItem[];
|
||||
}
|
||||
|
||||
/** 券抽屉表单。 */
|
||||
export interface NewCustomerCouponDrawerForm {
|
||||
couponType: MarketingNewCustomerCouponType;
|
||||
minimumSpend: null | number;
|
||||
validDays: number;
|
||||
value: null | number;
|
||||
}
|
||||
|
||||
/** 邀请记录分页模型。 */
|
||||
export interface NewCustomerInviteRecordPager {
|
||||
items: MarketingNewCustomerInviteRecordDto[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
/** 统计视图模型。 */
|
||||
export type NewCustomerStatsViewModel = MarketingNewCustomerStatsDto;
|
||||
|
||||
/** 邀请记录视图模型。 */
|
||||
export type NewCustomerInviteRecordViewModel =
|
||||
MarketingNewCustomerInviteRecordDto;
|
||||
|
||||
/** 映射为页面可编辑券项。 */
|
||||
export function toCouponFormItem(
|
||||
source: MarketingNewCustomerCouponRuleDto,
|
||||
): NewCustomerCouponFormItem {
|
||||
return {
|
||||
id: source.id,
|
||||
scene: source.scene,
|
||||
couponType: source.couponType,
|
||||
value: source.value,
|
||||
minimumSpend: source.minimumSpend,
|
||||
validDays: source.validDays,
|
||||
sortOrder: source.sortOrder,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user