feat: 完成会员消息触达模块页面与交互
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
import type {
|
||||
MemberMessageReachChannel,
|
||||
MemberMessageReachDetailDto,
|
||||
MemberMessageReachStatus,
|
||||
MemberMessageScheduleType,
|
||||
MemberMessageTemplateCategory,
|
||||
SaveMemberMessageReachPayload,
|
||||
SaveMemberMessageTemplatePayload,
|
||||
} from '#/api/member/message-reach';
|
||||
import type {
|
||||
MessageReachEditorForm,
|
||||
MessageReachFilterForm,
|
||||
MessageTemplateEditorForm,
|
||||
MessageTemplateFilterForm,
|
||||
} from '#/views/member/message-reach/types';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
/** 状态文案。 */
|
||||
export const MESSAGE_STATUS_TEXT_MAP: Record<MemberMessageReachStatus, string> =
|
||||
{
|
||||
draft: '草稿',
|
||||
pending: '待发送',
|
||||
sending: '发送中',
|
||||
sent: '已发送',
|
||||
failed: '发送失败',
|
||||
};
|
||||
|
||||
/** 状态标签色。 */
|
||||
export const MESSAGE_STATUS_COLOR_MAP: Record<
|
||||
MemberMessageReachStatus,
|
||||
string
|
||||
> = {
|
||||
draft: 'default',
|
||||
pending: 'processing',
|
||||
sending: 'warning',
|
||||
sent: 'success',
|
||||
failed: 'error',
|
||||
};
|
||||
|
||||
/** 渠道文案。 */
|
||||
export const MESSAGE_CHANNEL_TEXT_MAP: Record<
|
||||
MemberMessageReachChannel,
|
||||
string
|
||||
> = {
|
||||
inapp: '站内信',
|
||||
sms: '短信',
|
||||
'wechat-mini': '微信模板',
|
||||
};
|
||||
|
||||
/** 渠道标签色。 */
|
||||
export const MESSAGE_CHANNEL_COLOR_MAP: Record<
|
||||
MemberMessageReachChannel,
|
||||
string
|
||||
> = {
|
||||
inapp: 'blue',
|
||||
sms: 'green',
|
||||
'wechat-mini': 'orange',
|
||||
};
|
||||
|
||||
/** 模板分类文案。 */
|
||||
export const MESSAGE_TEMPLATE_CATEGORY_TEXT_MAP: Record<
|
||||
MemberMessageTemplateCategory,
|
||||
string
|
||||
> = {
|
||||
marketing: '营销',
|
||||
notice: '通知',
|
||||
recall: '召回',
|
||||
};
|
||||
|
||||
/** 模板分类颜色。 */
|
||||
export const MESSAGE_TEMPLATE_CATEGORY_COLOR_MAP: Record<
|
||||
MemberMessageTemplateCategory,
|
||||
string
|
||||
> = {
|
||||
marketing: 'magenta',
|
||||
notice: 'blue',
|
||||
recall: 'red',
|
||||
};
|
||||
|
||||
/** 收件状态文案。 */
|
||||
export const MESSAGE_RECIPIENT_STATUS_TEXT_MAP: Record<
|
||||
'failed' | 'pending' | 'sent',
|
||||
string
|
||||
> = {
|
||||
pending: '待发送',
|
||||
sent: '已发送',
|
||||
failed: '发送失败',
|
||||
};
|
||||
|
||||
/** 收件状态颜色。 */
|
||||
export const MESSAGE_RECIPIENT_STATUS_COLOR_MAP: Record<
|
||||
'failed' | 'pending' | 'sent',
|
||||
string
|
||||
> = {
|
||||
pending: 'processing',
|
||||
sent: 'success',
|
||||
failed: 'error',
|
||||
};
|
||||
|
||||
/** 格式化百分比。 */
|
||||
export function formatPercent(value: null | number | undefined) {
|
||||
const amount = Number(value ?? 0);
|
||||
if (!Number.isFinite(amount)) {
|
||||
return '0%';
|
||||
}
|
||||
|
||||
const digits = amount % 1 === 0 ? 0 : 1;
|
||||
return `${amount.toFixed(digits)}%`;
|
||||
}
|
||||
|
||||
/** 格式化数量。 */
|
||||
export function formatInteger(value: null | number | undefined) {
|
||||
const amount = Number(value ?? 0);
|
||||
if (!Number.isFinite(amount)) {
|
||||
return '0';
|
||||
}
|
||||
return Math.round(amount).toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
/** 格式化时间。 */
|
||||
export function formatDateTime(value?: string) {
|
||||
if (!value) {
|
||||
return '—';
|
||||
}
|
||||
|
||||
const parsed = dayjs(value);
|
||||
if (!parsed.isValid()) {
|
||||
return value;
|
||||
}
|
||||
return parsed.format('YYYY-MM-DD HH:mm');
|
||||
}
|
||||
|
||||
/** 解析消息发送时间。 */
|
||||
export function resolveMessageTime(
|
||||
sentAt?: string,
|
||||
scheduledAt?: string,
|
||||
): string {
|
||||
if (sentAt) {
|
||||
return formatDateTime(sentAt);
|
||||
}
|
||||
if (scheduledAt) {
|
||||
return formatDateTime(scheduledAt);
|
||||
}
|
||||
return '—';
|
||||
}
|
||||
|
||||
/** 将列表筛选映射为查询参数。 */
|
||||
export function mapMessageFilterToQuery(form: MessageReachFilterForm) {
|
||||
const keyword = form.keyword.trim();
|
||||
return {
|
||||
status: form.status || undefined,
|
||||
channel: form.channel || undefined,
|
||||
keyword: keyword || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/** 将模板筛选映射为查询参数。 */
|
||||
export function mapTemplateFilterToQuery(form: MessageTemplateFilterForm) {
|
||||
const keyword = form.keyword.trim();
|
||||
return {
|
||||
category: form.category || undefined,
|
||||
keyword: keyword || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/** 将详情映射为编辑表单。 */
|
||||
export function mapDetailToEditorForm(
|
||||
detail: MemberMessageReachDetailDto,
|
||||
form: MessageReachEditorForm,
|
||||
) {
|
||||
form.messageId = detail.messageId;
|
||||
form.templateId = detail.templateId;
|
||||
form.title = detail.title;
|
||||
form.content = detail.content;
|
||||
form.channels = [...detail.channels];
|
||||
form.audienceType = detail.audienceType;
|
||||
form.audienceTags = [...detail.audienceTags];
|
||||
form.scheduleType = detail.scheduleType;
|
||||
form.scheduledAt =
|
||||
detail.scheduleType === 'scheduled' && detail.scheduledAt
|
||||
? dayjs(detail.scheduledAt)
|
||||
: null;
|
||||
}
|
||||
|
||||
/** 重置消息编辑表单。 */
|
||||
export function resetMessageEditorForm(form: MessageReachEditorForm) {
|
||||
form.messageId = '';
|
||||
form.templateId = undefined;
|
||||
form.title = '';
|
||||
form.content = '';
|
||||
form.channels = ['inapp'];
|
||||
form.audienceType = 'all';
|
||||
form.audienceTags = [];
|
||||
form.scheduleType = 'immediate';
|
||||
form.scheduledAt = null;
|
||||
}
|
||||
|
||||
/** 重置模板编辑表单。 */
|
||||
export function resetTemplateEditorForm(form: MessageTemplateEditorForm) {
|
||||
form.templateId = '';
|
||||
form.name = '';
|
||||
form.category = 'notice';
|
||||
form.content = '';
|
||||
}
|
||||
|
||||
/** 消息编辑表单转保存请求。 */
|
||||
export function mapMessageEditorFormToSavePayload(
|
||||
form: MessageReachEditorForm,
|
||||
submitAction: 'draft' | 'send',
|
||||
): SaveMemberMessageReachPayload {
|
||||
const payload: SaveMemberMessageReachPayload = {
|
||||
messageId: form.messageId || undefined,
|
||||
templateId: form.templateId || undefined,
|
||||
title: form.title.trim(),
|
||||
content: form.content.trim(),
|
||||
channels: [...form.channels],
|
||||
audienceType: form.audienceType,
|
||||
audienceTags: [...form.audienceTags],
|
||||
scheduleType: form.scheduleType,
|
||||
scheduledAt:
|
||||
form.scheduleType === 'scheduled' && form.scheduledAt
|
||||
? form.scheduledAt.toISOString()
|
||||
: undefined,
|
||||
submitAction,
|
||||
};
|
||||
return payload;
|
||||
}
|
||||
|
||||
/** 模板编辑表单转保存请求。 */
|
||||
export function mapTemplateEditorFormToSavePayload(
|
||||
form: MessageTemplateEditorForm,
|
||||
): SaveMemberMessageTemplatePayload {
|
||||
return {
|
||||
templateId: form.templateId || undefined,
|
||||
name: form.name.trim(),
|
||||
category: form.category,
|
||||
content: form.content.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
/** 切换渠道。 */
|
||||
export function toggleChannel(
|
||||
channels: MemberMessageReachChannel[],
|
||||
channel: MemberMessageReachChannel,
|
||||
) {
|
||||
if (channels.includes(channel)) {
|
||||
return channels.filter((item) => item !== channel);
|
||||
}
|
||||
return [...channels, channel];
|
||||
}
|
||||
|
||||
/** 切换标签。 */
|
||||
export function toggleTag(tags: string[], tag: string) {
|
||||
if (tags.includes(tag)) {
|
||||
return tags.filter((item) => item !== tag);
|
||||
}
|
||||
return [...tags, tag];
|
||||
}
|
||||
|
||||
/** 解析时间类型。 */
|
||||
export function resolveScheduleType(value: string): MemberMessageScheduleType {
|
||||
return value === 'scheduled' ? 'scheduled' : 'immediate';
|
||||
}
|
||||
Reference in New Issue
Block a user