<script setup lang="tsx">
|
import dayjs from 'dayjs'
|
import { computed, onMounted, ref, watch } from 'vue'
|
import { useRoute, useRouter } from 'vue-router'
|
import {
|
NButton,
|
NCheckbox,
|
NCheckboxGroup,
|
NDataTable,
|
NDatePicker,
|
NForm,
|
NFormItem,
|
NInput,
|
NInputNumber,
|
NModal,
|
NRadio,
|
NRadioButton,
|
NRadioGroup,
|
NSelect,
|
NSpace,
|
NSwitch,
|
NTabPane,
|
NTabs,
|
NTimePicker,
|
NUpload,
|
useDialog,
|
useMessage,
|
} from 'naive-ui'
|
import type { DataTableColumns, FormInst, FormRules, UploadCustomRequestOptions, UploadFileInfo } from 'naive-ui'
|
import AiRichTextEditor from '@/components/custom/Editor/AiRichTextEditor/index.vue'
|
import CouponSelectModal from './components/CouponSelectModal.vue'
|
import UserSelectModal from './components/UserSelectModal.vue'
|
import Pagination from '@/components/common/Pagination.vue'
|
import { addCouponActivity, fetchCouponActivityDetail, fetchCouponDetail } from '@/service/api/coupon'
|
import { fetchWhitelistDetail } from '@/service/api/whitelist'
|
import { customUploadRequest } from '@/utils/upload'
|
import deleteIcon from '@/assets/pricingManagement/delete.svg'
|
|
const route = useRoute()
|
const router = useRouter()
|
const message = useMessage()
|
const dialog = useDialog()
|
const formRef = ref<FormInst | null>(null)
|
const fileList = ref<UploadFileInfo[]>([])
|
const saving = ref(false)
|
const detailLoading = ref(false)
|
const currentActivityId = ref<number | null>(null)
|
|
const showCouponModal = ref(false)
|
const currentSelectStrategyIndex = ref(-1)
|
const currentSelectCouponType = ref<'normal' | 'afterUse'>('normal')
|
const stackedCouponTypeSelectOpen = ref<Record<number, boolean>>({})
|
const currentSelectedCoupons = ref<any[]>([])
|
const currentSelectedUsers = ref<any[]>([])
|
|
const showUserModal = ref(false)
|
const currentSelectGroupIndex = ref(-1)
|
|
const showUserTypeHelpModal = ref(false)
|
const strategyList = ref<StrategyItem[]>([createStrategy()])
|
|
function handleOpenCouponModal(strategyIndex: number, type: 'normal' | 'afterUse') {
|
currentSelectStrategyIndex.value = strategyIndex
|
currentSelectCouponType.value = type
|
const strategy = strategyList.value[strategyIndex]
|
if (!strategy) {
|
currentSelectedCoupons.value = []
|
}
|
else if (type === 'normal') {
|
currentSelectedCoupons.value = [...(strategy.couponList || [])]
|
}
|
else {
|
currentSelectedCoupons.value = [...(strategy.distributionRulesAfterUse.couponList || [])]
|
}
|
showCouponModal.value = true
|
}
|
|
function handleSaveCoupons(rows: any[]) {
|
const strategyIndex = currentSelectStrategyIndex.value
|
if (strategyIndex < 0 || !strategyList.value[strategyIndex])
|
return
|
if (!rows.length) {
|
message.warning('请先选择优惠券')
|
return
|
}
|
|
if (currentSelectCouponType.value === 'normal') {
|
strategyList.value[strategyIndex].couponList = [...rows]
|
}
|
else {
|
strategyList.value[strategyIndex].distributionRulesAfterUse.couponList = [...rows]
|
}
|
message.success(`成功选择了 ${rows.length} 张优惠券`)
|
}
|
|
// 用户群相关
|
const userGroupList = ref<UserGroupItem[]>([
|
createUserGroup(1),
|
])
|
|
function handleOpenUserModal(groupIndex: number) {
|
currentSelectGroupIndex.value = groupIndex
|
currentSelectedUsers.value = [...(userGroupList.value[groupIndex]?.userList || [])]
|
showUserModal.value = true
|
}
|
|
interface UserListItem {
|
id: number | string
|
account: string
|
remark: string
|
}
|
|
interface UserGroupItem {
|
actitcityId: number | null
|
actitcityStrategeId: number | null
|
id: number | null
|
strategyName: string
|
userList: UserListItem[]
|
userType: 1 | 2 | 3 | 4 | 5
|
pagination: {
|
page: number
|
pageSize: number
|
total: number
|
}
|
}
|
|
function createUserGroup(userType: UserGroupItem['userType'] = 5): UserGroupItem {
|
return {
|
actitcityId: null,
|
actitcityStrategeId: null,
|
id: null,
|
strategyName: '',
|
userList: [],
|
userType,
|
pagination: { page: 1, pageSize: 10, total: 0 },
|
}
|
}
|
|
function handleSaveUsers(rows: any[]) {
|
const group = userGroupList.value[currentSelectGroupIndex.value]
|
if (!group)
|
return
|
|
const selectedUsers = rows.map(r => ({
|
id: r.id,
|
account: r.account,
|
remark: r.nickName || '-',
|
status: r.status,
|
}))
|
group.userList = selectedUsers
|
group.pagination.total = selectedUsers.length
|
message.success(`成功选择了 ${selectedUsers.length} 名用户`)
|
}
|
|
const activeTab = ref(1)
|
const isEditMode = computed(() => route.query.mode === 'edit' && !!route.query.id)
|
|
interface UsagePeriodItem {
|
startTime: string | null
|
endTime: string | null
|
}
|
|
interface DistributionRulesAfterUse {
|
ruleType: 1 | 2
|
couponList: any[]
|
}
|
|
interface StrategyItem {
|
actitcityId: number | null
|
id: number | null
|
name: string
|
distributionRules: 1 | 2 | 3
|
distributionTime: string | null
|
distributionRulesAfterUse: DistributionRulesAfterUse
|
effectiveRules: 1 | 2
|
effectiveDays: number | null
|
distributionsTotal: number | null
|
expireRules: 1 | 2 | 3
|
expireDays: number | null
|
expireDate: number | null
|
repPercent: number | null
|
couponType: number | null
|
limitNumber: number
|
stackedUsage: 0 | 1
|
startUsing: 0 | 1
|
usagePeriod: UsagePeriodItem[]
|
couponList: any[]
|
}
|
|
function createStrategy(): StrategyItem {
|
return {
|
actitcityId: null,
|
id: null,
|
name: '',
|
distributionRules: 1,
|
distributionTime: null,
|
distributionRulesAfterUse: {
|
ruleType: 1,
|
couponList: [],
|
},
|
effectiveRules: 1,
|
effectiveDays: null,
|
distributionsTotal: null,
|
expireRules: 1,
|
expireDays: null,
|
expireDate: null,
|
repPercent: null,
|
couponType: null,
|
limitNumber: 1,
|
stackedUsage: 0,
|
startUsing: 0,
|
usagePeriod: [{ startTime: null, endTime: null }],
|
couponList: [],
|
}
|
}
|
|
const strategyGroupOptions = computed(() => {
|
return strategyList.value.map((strategy, index) => ({
|
label: strategy.name.trim() || `策略${index + 1}`,
|
value: strategy.id ?? index + 1,
|
}))
|
})
|
|
const stackedCouponTypeOptions = [
|
{ label: '抵扣券', value: 1 },
|
{ label: '满减券', value: 2 },
|
{ label: '抹零券', value: 3 },
|
]
|
|
const strategyRules = {
|
name: [
|
{ required: true, message: '请输入策略名称', trigger: 'blur' },
|
{ max: 20, message: '最多20个字符', trigger: 'blur' },
|
],
|
// ... other rules
|
}
|
|
function handleAddStrategy() {
|
if (strategyList.value.length >= 10) {
|
message.warning('最多可添加10条策略')
|
return
|
}
|
strategyList.value.push(createStrategy())
|
}
|
|
function disablePastDate(timestamp: number) {
|
const todayStart = dayjs().startOf('day').valueOf()
|
return timestamp < todayStart
|
}
|
|
function disableExpireDate(timestamp: number, strategy: StrategyItem) {
|
if (disablePastDate(timestamp))
|
return true
|
|
// 当指定了生效日期时,失效日期必须大于生效日期,同一天也不可选
|
if (strategy.effectiveRules === 2 && strategy.effectiveDays) {
|
const effectiveDate = dayjs(strategy.effectiveDays).startOf('day').valueOf()
|
return timestamp <= effectiveDate
|
}
|
|
return false
|
}
|
|
function syncUserGroupStrategy(groupIndex: number, strategyId: number | null) {
|
const group = userGroupList.value[groupIndex]
|
if (!group)
|
return
|
|
group.actitcityStrategeId = strategyId
|
const matchedOption = strategyGroupOptions.value.find(item => item.value === strategyId)
|
group.strategyName = matchedOption?.label || ''
|
}
|
|
function handleRemoveStrategy(index: number) {
|
const removedStrategyId = strategyList.value[index]?.id ?? index + 1
|
strategyList.value.splice(index, 1)
|
|
userGroupList.value.forEach((group) => {
|
if (group.actitcityStrategeId === removedStrategyId) {
|
group.actitcityStrategeId = null
|
group.strategyName = ''
|
}
|
})
|
}
|
|
function handleAddTimeLimit(strategyIndex: number) {
|
if (strategyList.value[strategyIndex].usagePeriod.length < 10) {
|
strategyList.value[strategyIndex].usagePeriod.push({ startTime: null, endTime: null })
|
}
|
else {
|
message.warning('最多可配置10个时间段')
|
}
|
}
|
|
function handleRemoveTimeLimit(strategyIndex: number, timeLimitIndex: number) {
|
if (strategyList.value[strategyIndex].usagePeriod.length > 1) {
|
strategyList.value[strategyIndex].usagePeriod.splice(timeLimitIndex, 1)
|
}
|
}
|
|
function parseTimeToMinutes(time: string) {
|
const [hour, minute] = time.split(':').map(Number)
|
return hour * 60 + minute
|
}
|
|
function isUsageEndHourDisabled(hour: number, startTime: string | null) {
|
if (!startTime)
|
return false
|
const [startHour] = startTime.split(':').map(Number)
|
return hour < startHour
|
}
|
|
function isUsageEndMinuteDisabled(minute: number, hour: number | null, startTime: string | null) {
|
if (!startTime || hour === null)
|
return false
|
const [startHour, startMinute] = startTime.split(':').map(Number)
|
return hour === startHour && minute < startMinute
|
}
|
|
function ensureUsageEndTimeValid(timeLimit: UsagePeriodItem) {
|
if (!timeLimit.startTime || !timeLimit.endTime)
|
return
|
if (parseTimeToMinutes(timeLimit.endTime) < parseTimeToMinutes(timeLimit.startTime)) {
|
timeLimit.endTime = null
|
message.warning('结束时间不能小于开始时间')
|
}
|
}
|
|
function formatCouponType(couponType: any) {
|
if (couponType === 1)
|
return '抵扣券'
|
if (couponType === 2)
|
return '满减券'
|
if (couponType === 3)
|
return '抹零券'
|
return '-'
|
}
|
|
function formatCouponAmount(amount: any) {
|
if (amount === null || amount === undefined || amount === '')
|
return '-'
|
return `¥${amount}`
|
}
|
|
function handleReplaceCoupon(strategyIndex: number, type: 'normal' | 'afterUse') {
|
handleOpenCouponModal(strategyIndex, type)
|
}
|
|
function handleRemoveCoupon(strategyIndex: number, rowIndex: number, type: 'normal' | 'afterUse') {
|
const targetStrategy = strategyList.value[strategyIndex]
|
if (!targetStrategy)
|
return
|
|
if (type === 'normal')
|
targetStrategy.couponList.splice(rowIndex, 1)
|
else
|
targetStrategy.distributionRulesAfterUse.couponList.splice(rowIndex, 1)
|
|
message.success('移除成功')
|
}
|
|
function createCouponColumns(strategyIndex: number, type: 'normal' | 'afterUse'): DataTableColumns<any> {
|
return [
|
{ title: '添加时间', key: 'createTime', width: 220 },
|
{ title: '优惠券名称', key: 'couponName', ellipsis: true },
|
{
|
title: '优惠券类型',
|
key: 'couponType',
|
width: 100,
|
render: row => formatCouponType(row.couponType),
|
},
|
{
|
title: '条件金额',
|
key: 'conditionalAmount',
|
width: 100,
|
render: row => formatCouponAmount(row.conditionalAmount ?? row.conditionAmount),
|
},
|
{
|
title: '优惠金额',
|
key: 'amount',
|
width: 100,
|
render: row => formatCouponAmount(row.amount ?? row.discountAmount),
|
},
|
{ title: '优惠上限', key: 'discountLimit', width: 100, render: row => row.discountLimit ?? '-' },
|
{
|
title: '操作',
|
key: 'actions',
|
width: 100,
|
render: (_row, rowIndex) => {
|
return (
|
<div class="flex gap-12px text-[#0068FF] cursor-pointer">
|
<a onClick={() => handleReplaceCoupon(strategyIndex, type)}>更换</a>
|
<a onClick={() => handleRemoveCoupon(strategyIndex, rowIndex, type)}>移除</a>
|
</div>
|
)
|
},
|
},
|
]
|
}
|
|
const formModel = ref({
|
acticityName: '',
|
activityType: 1,
|
timeRange: null as any,
|
enableTotalLimit: false,
|
totalLimit: null as number | null,
|
img: '',
|
richText: '',
|
})
|
|
const rules: FormRules = {
|
acticityName: {
|
required: true,
|
message: '请输入活动名称',
|
trigger: 'blur',
|
},
|
activityType: {
|
required: true,
|
type: 'number',
|
message: '请选择活动类型',
|
trigger: 'change',
|
},
|
timeRange: {
|
required: true,
|
type: 'array',
|
message: '请选择活动时间',
|
trigger: 'change',
|
},
|
enableTotalLimit: {
|
required: true,
|
validator: (_rule: any, _value: any) => {
|
if (!formModel.value.enableTotalLimit)
|
return true
|
const value = formModel.value.totalLimit
|
if (value === null || value === undefined)
|
return new Error('请输入活动总额限额')
|
if (!Number.isInteger(value) || value <= 0 || value > 9999) {
|
return new Error('活动总额限额仅支持1-9999的正整数')
|
}
|
return true
|
},
|
trigger: ['change', 'blur'],
|
},
|
img: {
|
required: true,
|
message: '请上传活动封面图',
|
},
|
richText: {
|
required: true,
|
message: '请输入活动说明',
|
trigger: 'blur',
|
},
|
}
|
|
function handleTotalLimitUpdate(value: number | null) {
|
if (value === null || value === undefined) {
|
formModel.value.totalLimit = null
|
return
|
}
|
const digits = String(Math.trunc(Math.abs(value))).replace(/\D/g, '').slice(0, 4)
|
const normalized = digits ? Number(digits) : null
|
formModel.value.totalLimit = normalized && normalized > 0 ? normalized : null
|
}
|
|
type NotifyTimingValue = 1 | 2 | 4
|
|
interface NotifyFormModel {
|
activityId: number | null
|
id: number | null
|
noticeType: NotifyTimingValue[]
|
wechatTriggerTiming: NotifyTimingValue[]
|
wechatDaysBeforeExpiry: number | null
|
wechatMinutesBeforeEffective: number | null
|
smsTriggerTiming: NotifyTimingValue[]
|
smsDaysBeforeExpiry: number | null
|
smsMinutesBeforeEffective: number | null
|
blockTime: UsagePeriodItem[]
|
miniProgramTriggerTiming: NotifyTimingValue[]
|
miniProgramBeforeEffective: number | null
|
miniProgramDaysBeforeExpiry: number | null
|
toastBeforeEffective: string
|
toastBeforeExpiry: string
|
enableFloatingWindow: boolean
|
floatingWindowPriority: number | null
|
linkUrl: string
|
miniProgramPopupTitle: string
|
miniProgramPopupButtonText: string
|
}
|
|
// 通知方式
|
const notifyForm = ref<NotifyFormModel>({
|
activityId: null,
|
id: null,
|
noticeType: [1, 2, 4],
|
wechatTriggerTiming: [1, 2, 4],
|
wechatDaysBeforeExpiry: null,
|
wechatMinutesBeforeEffective: null,
|
smsTriggerTiming: [1, 2, 4],
|
smsDaysBeforeExpiry: null,
|
smsMinutesBeforeEffective: null,
|
blockTime: [{ startTime: null, endTime: null }],
|
miniProgramTriggerTiming: [1, 2, 4],
|
miniProgramBeforeEffective: null,
|
miniProgramDaysBeforeExpiry: null,
|
toastBeforeEffective: '',
|
toastBeforeExpiry: '',
|
enableFloatingWindow: true,
|
floatingWindowPriority: null,
|
linkUrl: '',
|
miniProgramPopupTitle: '',
|
miniProgramPopupButtonText: '',
|
})
|
|
// 其他表单与数据处理
|
|
function handleAddUserGroup() {
|
userGroupList.value.push(createUserGroup(1))
|
}
|
|
function handleRemoveUser(groupIndex: number, rowIndex: number) {
|
dialog.warning({
|
title: '确认移除该用户吗?',
|
content: '',
|
positiveText: '确定',
|
negativeText: '取消',
|
maskClosable: false,
|
autoFocus: false,
|
onPositiveClick: () => {
|
userGroupList.value[groupIndex].userList.splice(rowIndex, 1)
|
message.success('移除成功')
|
},
|
})
|
}
|
|
function createUserColumns(groupIndex: number): DataTableColumns<any> {
|
return [
|
{ title: '白名单账号', key: 'account' },
|
// { title: '备注说明', key: 'remark' },
|
{
|
title: '操作',
|
key: 'actions',
|
width: 100,
|
render: (_, rowIndex) => {
|
return (
|
<a
|
class="text-[#0068FF] cursor-pointer"
|
onClick={() => handleRemoveUser(groupIndex, rowIndex)}
|
>
|
移除
|
</a>
|
)
|
},
|
},
|
]
|
}
|
|
function handleUserPageChange(groupIndex: number, page: number, size: number) {
|
const group = userGroupList.value[groupIndex]
|
group.pagination.pageSize = size
|
group.pagination.page = page
|
// Call API...
|
}
|
|
function handleCancel() {
|
router.back()
|
}
|
|
function handleRemove() {
|
formModel.value.img = ''
|
fileList.value = []
|
}
|
|
// function getImageDimensions(file: File) {
|
// return new Promise<{ width: number, height: number }>((resolve, reject) => {
|
// const url = URL.createObjectURL(file)
|
// const img = new Image()
|
// img.onload = () => {
|
// resolve({ width: img.naturalWidth, height: img.naturalHeight })
|
// URL.revokeObjectURL(url)
|
// }
|
// img.onerror = () => {
|
// reject(new Error('load image failed'))
|
// URL.revokeObjectURL(url)
|
// }
|
// img.src = url
|
// })
|
// }
|
|
async function handleCustomUpload(options: UploadCustomRequestOptions) {
|
const rawFile = (options.file as any)?.file as File | undefined
|
if (rawFile) {
|
const allowedTypes = ['image/jpeg', 'image/png']
|
if (!allowedTypes.includes(rawFile.type)) {
|
message.error('仅支持 jpg、png 格式')
|
formModel.value.img = ''
|
fileList.value = []
|
options.onError?.()
|
return
|
}
|
|
// try {
|
// const { width, height } = await getImageDimensions(rawFile)
|
// }
|
// catch {
|
// message.error('图片读取失败,请重试')
|
// formModel.value.img = ''
|
// fileList.value = []
|
// options.onError?.()
|
// return
|
// }
|
}
|
|
const result = await customUploadRequest(options as any)
|
if (result) {
|
// 构建 JSON 数组字符串
|
const pictureData = [{
|
id: result.id,
|
name: result.name,
|
url: result.url,
|
}]
|
formModel.value.img = JSON.stringify(pictureData)
|
fileList.value = [{
|
id: result.id,
|
name: result.name,
|
status: 'finished',
|
url: result.url,
|
}]
|
formRef.value?.validate(undefined, (rule) => {
|
return rule?.key === 'bannerPictures'
|
})
|
}
|
}
|
|
function expandSelectedValues(value: unknown): NotifyTimingValue[] {
|
if (Array.isArray(value))
|
return value.map(item => Number(item)).filter((item): item is NotifyTimingValue => item === 1 || item === 2 || item === 4)
|
|
const numericValue = Number(value)
|
if (!numericValue)
|
return []
|
|
return [1, 2, 4].filter(item => (numericValue & item) === item) as NotifyTimingValue[]
|
}
|
|
function buildTimingValues(
|
receivedReminder: unknown,
|
aboutExpireReminder: unknown,
|
activityStartReminder: unknown,
|
): NotifyTimingValue[] {
|
const result: NotifyTimingValue[] = []
|
if (Number(receivedReminder) === 1)
|
result.push(1)
|
if (Number(aboutExpireReminder) === 1)
|
result.push(2)
|
if (Number(activityStartReminder) === 1)
|
result.push(4)
|
return result
|
}
|
|
function hasTiming(values: NotifyTimingValue[], timing: NotifyTimingValue) {
|
return values.includes(timing)
|
}
|
|
function toArray(value: unknown) {
|
if (Array.isArray(value))
|
return value
|
if (typeof value === 'string') {
|
try {
|
const parsed = JSON.parse(value)
|
return Array.isArray(parsed) ? parsed : []
|
}
|
catch {
|
return []
|
}
|
}
|
return []
|
}
|
|
function parseUsagePeriod(periods: unknown): UsagePeriodItem[] {
|
let sourceList: any[] = []
|
if (Array.isArray(periods)) {
|
sourceList = periods
|
}
|
else if (typeof periods === 'string' && periods.trim()) {
|
try {
|
const parsed = JSON.parse(periods)
|
sourceList = Array.isArray(parsed) ? parsed : periods.split(/[,,]/).map(item => item.trim()).filter(Boolean)
|
}
|
catch {
|
sourceList = periods.split(/[,,]/).map(item => item.trim()).filter(Boolean)
|
}
|
}
|
|
const parsed = sourceList
|
.map((item) => {
|
if (typeof item === 'object' && item !== null) {
|
const startTime = parseTimePoint((item as any).startTime)
|
const endTime = parseTimePoint((item as any).endTime)
|
if (startTime && endTime) {
|
return {
|
startTime,
|
endTime,
|
}
|
}
|
}
|
if (typeof item === 'string') {
|
const matched = item.match(/(\d{2}:\d{2})\s*[-_~至]\s*(\d{2}:\d{2})/)
|
if (matched?.[1] && matched?.[2]) {
|
return {
|
startTime: matched[1],
|
endTime: matched[2],
|
}
|
}
|
}
|
return null
|
})
|
.filter(Boolean) as UsagePeriodItem[]
|
|
return parsed.length ? parsed : [{ startTime: null, endTime: null }]
|
}
|
|
function parseTimePoint(value: unknown) {
|
if (!value)
|
return null
|
const source = String(value)
|
const matched = source.match(/(\d{2}:\d{2})/)
|
return matched?.[1] || null
|
}
|
|
function buildNotificationDateTime(time: string | null) {
|
if (!time)
|
return ''
|
// 新接口仅需要时分,如 10:00
|
return time
|
}
|
|
function parseSmsBlockTime(startTimeRaw: unknown, endTimeRaw: unknown): UsagePeriodItem[] {
|
if (!startTimeRaw || !endTimeRaw)
|
return [{ startTime: null, endTime: null }]
|
|
const startList = String(startTimeRaw).split(',').map(item => parseTimePoint(item.trim()))
|
const endList = String(endTimeRaw).split(',').map(item => parseTimePoint(item.trim()))
|
const pairCount = Math.max(startList.length, endList.length)
|
const parsedList: UsagePeriodItem[] = []
|
for (let index = 0; index < pairCount; index += 1) {
|
const startTime = startList[index] || null
|
const endTime = endList[index] || null
|
if (startTime && endTime) {
|
parsedList.push({
|
startTime,
|
endTime,
|
})
|
}
|
}
|
return parsedList.length ? parsedList : [{ startTime: null, endTime: null }]
|
}
|
|
function parseDistributionRulesAfterUse(value: unknown): DistributionRulesAfterUse {
|
const parsed = toArray(value)
|
if (!parsed.length) {
|
return {
|
ruleType: 1,
|
couponList: [],
|
}
|
}
|
|
return {
|
ruleType: 2,
|
couponList: parsed.map((id: number | string) => ({
|
id,
|
couponName: `优惠券${id}`,
|
})),
|
}
|
}
|
|
function parseUserList(value: unknown): UserListItem[] {
|
return toArray(value).map((id: number | string) => ({
|
id,
|
account: String(id),
|
remark: '-',
|
}))
|
}
|
|
async function fetchCouponDisplayMap(ids: Array<number | string>) {
|
const uniqueIds = Array.from(new Set(ids.filter(id => id !== null && id !== undefined && id !== '')))
|
const entries = await Promise.all(uniqueIds.map(async (id) => {
|
try {
|
const res = await fetchCouponDetail(id)
|
if (res.code === 200 && res.data) {
|
return [String(id), {
|
...res.data,
|
id,
|
couponName: res.data.couponName || `优惠券${id}`,
|
}] as const
|
}
|
}
|
catch {
|
// ignore single item failure and keep placeholder fallback
|
}
|
|
return [String(id), {
|
id,
|
couponName: `优惠券${id}`,
|
}] as const
|
}))
|
|
return new Map(entries)
|
}
|
|
async function fetchUserDisplayMap(ids: Array<number | string>) {
|
const uniqueIds = Array.from(new Set(ids.filter(id => id !== null && id !== undefined && id !== '')))
|
const entries = await Promise.all(uniqueIds.map(async (id) => {
|
try {
|
const res = await fetchWhitelistDetail(id)
|
if (res.code === 200 && res.data) {
|
return [String(id), {
|
id,
|
account: res.data.account || String(id),
|
remark: res.data.nickName || '-',
|
}] as const
|
}
|
}
|
catch {
|
// ignore single item failure and keep placeholder fallback
|
}
|
|
return [String(id), {
|
id,
|
account: String(id),
|
remark: '-',
|
}] as const
|
}))
|
|
return new Map(entries)
|
}
|
|
async function hydrateStrategyCouponDisplay(detail: Record<string, any>) {
|
const strategySource = toArray(detail.strategy)
|
const couponIds = strategySource.flatMap((item: any) => [
|
...toArray(item.couponIds),
|
...toArray(item.distributionRulesAfterUse ?? item.distributionAfterUse),
|
])
|
|
if (!couponIds.length)
|
return
|
|
const couponMap = await fetchCouponDisplayMap(couponIds)
|
strategyList.value = strategyList.value.map((strategy) => {
|
const couponList = strategy.couponList.map((item: any) => couponMap.get(String(item.id)) || item)
|
const distributionRulesAfterUse = {
|
...strategy.distributionRulesAfterUse,
|
couponList: strategy.distributionRulesAfterUse.couponList.map((item: any) => couponMap.get(String(item.id)) || item),
|
}
|
|
return {
|
...strategy,
|
couponList,
|
distributionRulesAfterUse,
|
}
|
})
|
}
|
|
async function hydrateUserDisplay(detail: Record<string, any>) {
|
const activityUsers = toArray(detail.activityUser)
|
const userIds = activityUsers.flatMap((item: any) => toArray(item.userList))
|
|
if (!userIds.length)
|
return
|
|
const userMap = await fetchUserDisplayMap(userIds)
|
userGroupList.value = userGroupList.value.map((group) => {
|
const userList = group.userList.map(item => userMap.get(String(item.id)) || item)
|
return {
|
...group,
|
userList,
|
pagination: {
|
...group.pagination,
|
total: userList.length,
|
},
|
}
|
})
|
}
|
|
function syncFileListFromImg(img: unknown) {
|
const parsed = toArray(img)
|
if (!parsed.length) {
|
fileList.value = []
|
return
|
}
|
|
fileList.value = parsed.map((item: any) => ({
|
id: item.id,
|
name: item.name,
|
status: 'finished',
|
url: item.url,
|
}))
|
}
|
|
function fillFormByDetail(detail: Record<string, any>) {
|
currentActivityId.value = Number(detail.id) || null
|
|
formModel.value = {
|
acticityName: detail.acticityName || '',
|
activityType: Number(detail.activityType) || 1,
|
timeRange: detail.startTime && detail.endTime
|
? [new Date(detail.startTime).getTime(), new Date(detail.endTime).getTime()]
|
: null,
|
enableTotalLimit: Boolean(detail.enableTotalLimit),
|
totalLimit: detail.totalLimit ?? null,
|
img: detail.img || '',
|
richText: detail.richText || '',
|
}
|
syncFileListFromImg(detail.img)
|
|
const strategySource = toArray(detail.strategy)
|
strategyList.value = strategySource.length
|
? strategySource.map((item: any) => ({
|
actitcityId: item.actitcityId ?? currentActivityId.value,
|
id: item.id ?? null,
|
name: item.name || '',
|
distributionRules: Number(item.distributionRules) as 1 | 2 | 3 || 1,
|
distributionTime: parseTimePoint(item.distributionTime),
|
distributionRulesAfterUse: parseDistributionRulesAfterUse(item.distributionRulesAfterUse ?? item.distributionAfterUse),
|
effectiveRules: Number(item.effectiveRules) as 1 | 2 || 1,
|
effectiveDays: item.effectiveDays ? new Date(item.effectiveDays).getTime() : null,
|
distributionsTotal: item.distributionsTotal ?? null,
|
expireRules: Number(item.expireRules) as 1 | 2 | 3 || 1,
|
expireDays: item.expireDays ?? null,
|
expireDate: item.expireDate ? new Date(item.expireDate).getTime() : null,
|
repPercent: item.repPercent ?? null,
|
couponType: item.couponType ?? item.couponTypeId ?? null,
|
limitNumber: item.limitNumber ?? 1,
|
stackedUsage: Number(item.stackedUsage) as 0 | 1 || 0,
|
startUsing: Number(item.startUsing) as 0 | 1 || 0,
|
usagePeriod: parseUsagePeriod(item.usagePeriod),
|
couponList: (() => {
|
const list = toArray(item.couponList)
|
if (list.length && typeof list[0] === 'object')
|
return list
|
return toArray(item.couponIds).map((id: number | string) => ({
|
id,
|
couponName: `优惠券${id}`,
|
}))
|
})(),
|
}))
|
: [createStrategy()]
|
|
const userSource = toArray(detail.activityUser)
|
userGroupList.value = userSource.length
|
? userSource.map((item: any) => ({
|
actitcityId: item.actitcityId ?? currentActivityId.value,
|
actitcityStrategeId: item.actitcityStrategeId ?? null,
|
id: item.id ?? null,
|
strategyName: item.strategyName || '',
|
userList: parseUserList(item.userList),
|
userType: Number(item.userType) as 1 | 2 | 3 | 4 | 5 || 1,
|
pagination: { page: 1, pageSize: 10, total: toArray(item.userList).length },
|
}))
|
: [createUserGroup()]
|
|
// 兼容老结构(notice)与新结构(字段拍平在详情根对象)
|
const notice = detail.notice || detail
|
const noticeType = detail.notice
|
? expandSelectedValues(notice.noticeType)
|
: [
|
Number(notice.wechatNotification) === 1 ? 1 : null,
|
Number(notice.smsNotification) === 1 ? 2 : null,
|
Number(notice.miniProgramNotification) === 1 ? 4 : null,
|
].filter(Boolean) as NotifyTimingValue[]
|
notifyForm.value = {
|
activityId: notice.activityId ?? currentActivityId.value,
|
id: notice.id ?? null,
|
noticeType,
|
wechatTriggerTiming: detail.notice
|
? expandSelectedValues(notice.wechatTriggerTiming)
|
: buildTimingValues(notice.wechatReceivedReminder, notice.wechatAboutExpireReminder, notice.wechatActicityStart),
|
wechatDaysBeforeExpiry: notice.wechatDaysBeforeExpiry ?? notice.wechatAboutExpireDays ?? null,
|
wechatMinutesBeforeEffective: notice.wechatMinutesBeforeEffective ?? notice.wechatActicityStartMinutes ?? null,
|
smsTriggerTiming: detail.notice
|
? expandSelectedValues(notice.smsTriggerTiming)
|
: buildTimingValues(notice.smsReceivedReminder, notice.smsAboutExpireReminder, notice.smsActicityStart),
|
smsDaysBeforeExpiry: notice.smsDaysBeforeExpiry ?? notice.smsAboutExpireDays ?? null,
|
smsMinutesBeforeEffective: notice.smsMinutesBeforeEffective ?? notice.smsActicityStartMinutes ?? null,
|
blockTime: detail.notice
|
? parseUsagePeriod(notice.blockTime)
|
: parseSmsBlockTime(notice.smsStartBlockingTime, notice.smsEndBlockingTime),
|
miniProgramTriggerTiming: detail.notice
|
? expandSelectedValues(notice.miniProgramTriggerTiming)
|
: buildTimingValues(notice.miniProgramReceivedReminder, notice.miniProgramAboutExpireReminder, 0),
|
miniProgramBeforeEffective: notice.miniProgramBeforeEffective ?? notice.miniProgramAboutExpireMinutes ?? null,
|
miniProgramDaysBeforeExpiry: notice.miniProgramDaysBeforeExpiry ?? notice.aboutExpireDays ?? null,
|
toastBeforeEffective: notice.toastBeforeEffective || notice.expireMinutesNotificationContent || '',
|
toastBeforeExpiry: notice.toastBeforeExpiry || notice.expireDaysNotificationContent || '',
|
enableFloatingWindow: detail.notice ? Boolean(notice.enableFloatingWindow) : Number(notice.enableFloatWindow) === 1,
|
floatingWindowPriority: notice.floatingWindowPriority ?? null,
|
linkUrl: notice.linkUrl || notice.floatWindowJumpUrl || '',
|
miniProgramPopupTitle: notice.miniProgramPopupTitle || '',
|
miniProgramPopupButtonText: notice.miniProgramPopupButtonText || notice.miniProgramBtnText || '',
|
}
|
}
|
|
async function getDetail() {
|
if (!isEditMode.value)
|
return
|
|
detailLoading.value = true
|
try {
|
const res = await fetchCouponActivityDetail(String(route.query.id))
|
if (res.code === 200 && res.data) {
|
fillFormByDetail(res.data)
|
await Promise.all([
|
hydrateStrategyCouponDisplay(res.data),
|
hydrateUserDisplay(res.data),
|
])
|
}
|
}
|
finally {
|
detailLoading.value = false
|
}
|
}
|
|
function formatDateTime(value: number | null, type: 'start' | 'end' = 'start') {
|
if (!value)
|
return ''
|
return dayjs(value).format(type === 'start' ? 'YYYY-MM-DD 00:00:00' : 'YYYY-MM-DD 23:59:59')
|
}
|
|
function formatDate(value: number | null) {
|
if (!value)
|
return ''
|
return dayjs(value).format('YYYY-MM-DD')
|
}
|
|
function buildUsagePeriod(usagePeriod: UsagePeriodItem[]) {
|
return usagePeriod
|
.filter(item => item.startTime && item.endTime)
|
.map(item => `${item.startTime}_${item.endTime}`)
|
}
|
|
function buildDistributionRulesAfterUse(strategy: StrategyItem) {
|
if (strategy.distributionRules !== 3 || strategy.distributionRulesAfterUse.ruleType === 1)
|
return []
|
|
return strategy.distributionRulesAfterUse.couponList
|
.map(item => item.id)
|
.filter((id: number | string | null | undefined) => id !== null && id !== undefined && id !== '')
|
}
|
|
function buildStrategyPayload(activityId: number | null = null) {
|
return strategyList.value.map(strategy => ({
|
actitcityId: activityId,
|
id: strategy.id,
|
name: strategy.name.trim(),
|
distributionRules: strategy.distributionRules,
|
distributionTime: strategy.distributionRules === 1 ? (strategy.distributionTime || '') : '',
|
// 指定优惠券使用后发放,接口真实字段为 distributionAfterUse。
|
distributionAfterUse: buildDistributionRulesAfterUse(strategy),
|
distributionsTotal: strategy.distributionsTotal,
|
effectiveRules: strategy.effectiveRules,
|
effectiveDays: strategy.effectiveRules === 2 ? formatDate(strategy.effectiveDays) : '',
|
expireRules: strategy.expireRules,
|
expireDays: strategy.expireRules === 2 ? strategy.expireDays : null,
|
expireDate: strategy.expireRules === 3 ? formatDate(strategy.expireDate) : '',
|
repPercent: strategy.stackedUsage === 1 ? strategy.repPercent : null,
|
couponType: strategy.stackedUsage === 1 ? strategy.couponType : null,
|
limitNumber: strategy.limitNumber,
|
stackedUsage: strategy.stackedUsage,
|
startUsing: strategy.startUsing,
|
usagePeriod: buildUsagePeriod(strategy.usagePeriod),
|
couponIds: strategy.couponList.map(item => item.id).filter((id: number | string | null | undefined) => id !== null && id !== undefined && id !== ''),
|
}))
|
}
|
|
function buildUserGroupPayload(activityId: number | null = null) {
|
return userGroupList.value.map(group => ({
|
actitcityId: activityId,
|
actitcityStrategeId: group.actitcityStrategeId,
|
id: group.id,
|
strategyName: group.strategyName,
|
userType: group.userType,
|
userList: group.userList.map(item => item.id).filter(id => id !== null && id !== undefined && id !== ''),
|
}))
|
}
|
|
function handleAddBlockTime() {
|
if (notifyForm.value.blockTime.length < 10)
|
notifyForm.value.blockTime.push({ startTime: null, endTime: null })
|
else
|
message.warning('最多可配置10个屏蔽时间段')
|
}
|
|
function handleRemoveBlockTime(index: number) {
|
if (notifyForm.value.blockTime.length > 1)
|
notifyForm.value.blockTime.splice(index, 1)
|
}
|
|
function buildNotifyPayload(activityId: number | null = null) {
|
const smsBlocks = notifyForm.value.blockTime.filter(item => item.startTime && item.endTime)
|
const smsStartBlockingTime = smsBlocks
|
.map(item => buildNotificationDateTime(item.startTime))
|
.filter(Boolean)
|
.join(',')
|
const smsEndBlockingTime = smsBlocks
|
.map(item => buildNotificationDateTime(item.endTime))
|
.filter(Boolean)
|
.join(',')
|
return {
|
id: notifyForm.value.id ?? activityId,
|
// 渠道总开关(0=关,1=开)
|
wechatNotification: notifyForm.value.noticeType.includes(1) ? 1 : 0,
|
smsNotification: notifyForm.value.noticeType.includes(2) ? 1 : 0,
|
miniProgramNotification: notifyForm.value.noticeType.includes(4) ? 1 : 0,
|
// 微信触达时机
|
wechatReceivedReminder: hasTiming(notifyForm.value.wechatTriggerTiming, 1) ? 1 : 0,
|
wechatAboutExpireReminder: hasTiming(notifyForm.value.wechatTriggerTiming, 2) ? 1 : 0,
|
wechatActicityStart: hasTiming(notifyForm.value.wechatTriggerTiming, 4) ? 1 : 0,
|
wechatAboutExpireDays: notifyForm.value.wechatDaysBeforeExpiry ?? 0,
|
wechatActicityStartMinutes: notifyForm.value.wechatMinutesBeforeEffective ?? 0,
|
// 短信触达时机
|
smsReceivedReminder: hasTiming(notifyForm.value.smsTriggerTiming, 1) ? 1 : 0,
|
smsAboutExpireReminder: hasTiming(notifyForm.value.smsTriggerTiming, 2) ? 1 : 0,
|
smsActicityStart: hasTiming(notifyForm.value.smsTriggerTiming, 4) ? 1 : 0,
|
smsAboutExpireDays: notifyForm.value.smsDaysBeforeExpiry ?? 0,
|
smsActicityStartMinutes: notifyForm.value.smsMinutesBeforeEffective ?? 0,
|
smsStartBlockingTime,
|
smsEndBlockingTime,
|
// 小程序通知
|
miniProgramReceivedReminder: hasTiming(notifyForm.value.miniProgramTriggerTiming, 1) ? 1 : 0,
|
miniProgramAboutExpireReminder: hasTiming(notifyForm.value.miniProgramTriggerTiming, 2) ? 1 : 0,
|
miniProgramAboutExpireMinutes: notifyForm.value.miniProgramBeforeEffective ?? 0,
|
aboutExpireDays: notifyForm.value.miniProgramDaysBeforeExpiry ?? 0,
|
expireMinutesNotificationContent: notifyForm.value.toastBeforeEffective || '',
|
expireDaysNotificationContent: notifyForm.value.toastBeforeExpiry || '',
|
miniProgramPopupTitle: notifyForm.value.miniProgramPopupTitle,
|
miniProgramBtnText: notifyForm.value.miniProgramPopupButtonText,
|
enableFloatWindow: notifyForm.value.enableFloatingWindow ? 1 : 0,
|
floatingWindowPriority: notifyForm.value.floatingWindowPriority,
|
floatWindowJumpUrl: notifyForm.value.linkUrl,
|
}
|
}
|
|
function validateStrategyList() {
|
for (let index = 0; index < strategyList.value.length; index += 1) {
|
const strategy = strategyList.value[index]
|
if (!strategy.name.trim()) {
|
activeTab.value = 2
|
message.warning(`请输入策略${index + 1}名称`)
|
return false
|
}
|
if (strategy.couponList.length === 0) {
|
activeTab.value = 2
|
message.warning(`请为策略${index + 1}选择优惠券`)
|
return false
|
}
|
if (strategy.distributionRules === 1 && !strategy.distributionTime) {
|
activeTab.value = 2
|
message.warning(`请为策略${index + 1}选择每日发放时间点`)
|
return false
|
}
|
if (strategy.distributionRules === 3 && strategy.distributionRulesAfterUse.ruleType === 2 && strategy.distributionRulesAfterUse.couponList.length === 0) {
|
activeTab.value = 2
|
message.warning(`请为策略${index + 1}选择使用后发放优惠券`)
|
return false
|
}
|
if (strategy.stackedUsage === 1 && (!strategy.repPercent || strategy.repPercent <= 0)) {
|
activeTab.value = 2
|
message.warning(`请为策略${index + 1}填写叠加使用百分比`)
|
return false
|
}
|
if (strategy.stackedUsage === 1 && !strategy.couponType) {
|
activeTab.value = 2
|
message.warning(`请为策略${index + 1}选择可叠加优惠券类型`)
|
return false
|
}
|
if (strategy.effectiveRules === 2 && !strategy.effectiveDays) {
|
activeTab.value = 2
|
message.warning(`请为策略${index + 1}选择生效日期`)
|
return false
|
}
|
if (strategy.expireRules === 2 && (!strategy.expireDays || strategy.expireDays <= 0)) {
|
activeTab.value = 2
|
message.warning(`请为策略${index + 1}填写失效天数`)
|
return false
|
}
|
if (strategy.expireRules === 3 && !strategy.expireDate) {
|
activeTab.value = 2
|
message.warning(`请为策略${index + 1}选择失效日期`)
|
return false
|
}
|
if (
|
strategy.effectiveRules === 2
|
&& strategy.effectiveDays
|
&& strategy.expireRules === 3
|
&& strategy.expireDate
|
&& dayjs(strategy.expireDate).startOf('day').valueOf() <= dayjs(strategy.effectiveDays).startOf('day').valueOf()
|
) {
|
activeTab.value = 2
|
message.warning(`策略${index + 1}的失效日期必须大于生效日期`)
|
return false
|
}
|
const invalidUsageTime = strategy.usagePeriod.find(item =>
|
item.startTime
|
&& item.endTime
|
&& parseTimeToMinutes(item.endTime) < parseTimeToMinutes(item.startTime),
|
)
|
if (invalidUsageTime) {
|
activeTab.value = 2
|
message.warning(`策略${index + 1}的使用限时结束时间不能小于开始时间`)
|
return false
|
}
|
}
|
return true
|
}
|
|
function validateUserGroupList() {
|
for (let index = 0; index < userGroupList.value.length; index += 1) {
|
const group = userGroupList.value[index]
|
if (!group.actitcityStrategeId) {
|
activeTab.value = 3
|
message.warning(`请为用户群选择绑定策略组`)
|
return false
|
}
|
if (group.userType === 5 && group.userList.length === 0) {
|
activeTab.value = 3
|
message.warning(`请为用户群导入指定用户`)
|
return false
|
}
|
}
|
return true
|
}
|
|
async function handleSubmit() {
|
try {
|
await formRef.value?.validate()
|
}
|
catch {
|
activeTab.value = 1
|
return
|
}
|
|
if (!validateStrategyList() || !validateUserGroupList())
|
return
|
|
const payload = {
|
// 已在顶层对象中存在 id 字段,此处不再重复赋值
|
acticityName: formModel.value.acticityName.trim(),
|
activityType: formModel.value.activityType,
|
startTime: Array.isArray(formModel.value.timeRange) ? formatDateTime(formModel.value.timeRange[0] as number, 'start') : '',
|
endTime: Array.isArray(formModel.value.timeRange) ? formatDateTime(formModel.value.timeRange[1] as number, 'end') : '',
|
enableTotalLimit: formModel.value.enableTotalLimit,
|
totalLimit: formModel.value.enableTotalLimit ? formModel.value.totalLimit : null,
|
img: formModel.value.img,
|
richText: formModel.value.richText,
|
// 新接口要求通知字段拍平到活动对象顶层
|
...buildNotifyPayload(currentActivityId.value),
|
strategy: buildStrategyPayload(currentActivityId.value),
|
activityUser: buildUserGroupPayload(currentActivityId.value),
|
}
|
|
saving.value = true
|
try {
|
const res = await addCouponActivity(payload)
|
if (res.code === 200) {
|
message.success('保存成功')
|
window.sessionStorage.setItem('activityManagementNeedRefresh', '1')
|
router.back()
|
}
|
}
|
finally {
|
saving.value = false
|
}
|
}
|
|
function handleNext() {
|
if (activeTab.value === 1) {
|
formRef.value?.validate().then(() => {
|
activeTab.value++
|
})
|
return
|
}
|
if (activeTab.value === 2) {
|
if (!validateStrategyList())
|
return
|
activeTab.value++
|
return
|
}
|
if (activeTab.value === 3) {
|
if (!validateUserGroupList())
|
return
|
activeTab.value++
|
return
|
}
|
if (activeTab.value === 4) {
|
handleSubmit()
|
}
|
}
|
|
function handlePrev() {
|
if (activeTab.value > 1)
|
activeTab.value--
|
}
|
|
function handleRemoveUserGroup(index: number) {
|
userGroupList.value.splice(index, 1)
|
}
|
|
function closeTimePicker() {
|
if (document.activeElement) {
|
(document.activeElement as HTMLElement).blur()
|
}
|
}
|
|
onMounted(() => {
|
getDetail()
|
})
|
|
watch(
|
() => strategyList.value.map(strategy => ({
|
effectiveRules: strategy.effectiveRules,
|
effectiveDays: strategy.effectiveDays,
|
expireRules: strategy.expireRules,
|
expireDate: strategy.expireDate,
|
})),
|
() => {
|
strategyList.value.forEach((strategy) => {
|
if (
|
strategy.effectiveRules === 2
|
&& strategy.effectiveDays
|
&& strategy.expireRules === 3
|
&& strategy.expireDate
|
&& dayjs(strategy.expireDate).startOf('day').valueOf() <= dayjs(strategy.effectiveDays).startOf('day').valueOf()
|
) {
|
strategy.expireDate = null
|
}
|
})
|
},
|
{ deep: true },
|
)
|
</script>
|
|
<template>
|
<div class=" w-full h-full relative content flex flex-col">
|
<div class="p-12px main flex flex-col flex-1 overflow-hidden">
|
<NTabs v-model:value="activeTab" type="line" class="custom-tabs" animated>
|
<NTabPane :name="1" tab="活动介绍内容" />
|
<NTabPane :name="2" tab="活动策略配置" />
|
<NTabPane :name="3" tab="发放用户" />
|
<NTabPane :name="4" tab="通知方式" />
|
</NTabs>
|
<div class="box flex-1">
|
<!-- 活动介绍内容表单 -->
|
<NForm
|
v-show="activeTab === 1" ref="formRef" :model="formModel" :rules="rules" label-placement="left"
|
require-mark-placement="left"
|
>
|
<div class="flex items-start gap-24px">
|
<NFormItem label="活动名称" path="acticityName">
|
<NInput v-model:value="formModel.acticityName" placeholder="请输入" class="w-300px!" />
|
</NFormItem>
|
<NFormItem label="活动类型" path="activityType" class="flex-1">
|
<NRadioGroup v-model:value="formModel.activityType" name="radiogroup">
|
<NRadioButton :value="1" label="新人活动" />
|
<NRadioButton :value="2" label="媒体邀约活动" />
|
<NRadioButton :value="3" label="日常提频" />
|
</NRadioGroup>
|
</NFormItem>
|
</div>
|
|
<NFormItem label="活动时间" path="timeRange" class="mt-8px">
|
<div class="flex items-center gap-12px">
|
<NDatePicker
|
v-model:value="formModel.timeRange" format="yyyy/MM/dd" type="daterange" clearable
|
start-placeholder="开始时间" end-placeholder="结束时间" close-on-select :actions="null"
|
style="width: 400px;"
|
/>
|
</div>
|
</NFormItem>
|
|
<NFormItem label="是否开启活动总限额" path="enableTotalLimit" class="mt-8px custom-form-item">
|
<div class="flex items-center gap-24px">
|
<NSwitch v-model:value="formModel.enableTotalLimit" />
|
<div v-if="formModel.enableTotalLimit" class="flex items-center gap-8px">
|
<span>当合计优惠金额达到</span>
|
<NInputNumber
|
:value="formModel.totalLimit"
|
placeholder="请输入"
|
:min="1"
|
:max="9999"
|
:precision="0"
|
:show-button="false"
|
style="width: 109px;"
|
@update:value="handleTotalLimitUpdate"
|
/>
|
<span>元时,结束活动(相关优惠券均失效)</span>
|
</div>
|
</div>
|
</NFormItem>
|
|
<NFormItem label="活动封面图" path="img" class="mt-8px" label-placement="top">
|
<div class="flex flex-col">
|
<NUpload
|
v-model:file-list="fileList"
|
:max="1"
|
list-type="image-card"
|
:custom-request="handleCustomUpload"
|
accept="image/png,image/jpeg"
|
@remove="handleRemove"
|
>
|
<img src="/src/assets/element-img/upload.png" style="width: 56px;height: 56px;">
|
</NUpload>
|
<div style="margin-top: 8px; color: #999; font-size: 12px;" class="flex items-center">
|
<img src="/src/assets/element-img/notice.png" style="width: 12px;height: 12px;margin-right: 6px;">
|
支持jpg/png ,大小≤5MB,最多上传1张
|
</div>
|
</div>
|
</NFormItem>
|
|
<NFormItem label="活动说明" path="richText" label-placement="top" class="mt-8px">
|
<div class="w-full ">
|
<AiRichTextEditor v-model="formModel.richText" height="300px" />
|
</div>
|
</NFormItem>
|
</NForm>
|
|
<!-- 活动策略配置 -->
|
<div v-show="activeTab === 2" class="flex flex-col">
|
<div
|
v-for="(strategy, strategyIndex) in strategyList" :key="strategyIndex"
|
class="mb-12px p-24px pb-12px border border-[#EEE9E9] last:mb-0 rounded-6px"
|
>
|
<NForm :model="strategy" :rules="strategyRules" label-placement="left" require-mark-placement="left">
|
<div class="flex items-center justify-between mb-8px">
|
<!-- 策略名称 -->
|
<NFormItem label="策略名称" path="name">
|
<NInput
|
v-model:value="strategy.name" placeholder="请输入" maxlength="20" clearable
|
style="width: 300px"
|
/>
|
</NFormItem>
|
<img v-if="strategyList.length > 1" :src="deleteIcon" alt="删除" style="width: 16px;height: 16px;cursor: pointer;" @click="handleRemoveStrategy(strategyIndex)">
|
</div>
|
|
<!-- 优惠券 -->
|
<NFormItem label="优惠券">
|
<div class="w-full flex justify-end">
|
<NButton type="primary" style="border: 1px solid #7ABDFF;" secondary class="w-97px" @click="handleOpenCouponModal(strategyIndex, 'normal')">
|
选择优惠券
|
</NButton>
|
</div>
|
</NFormItem>
|
<div class="mt-4px overflow-hidden">
|
<NDataTable
|
:columns="createCouponColumns(strategyIndex, 'normal')"
|
:data="strategy.couponList"
|
:bordered="false"
|
:style="strategy.couponList.length === 0 ? { minHeight: '263px' } : {}"
|
:flex-height="strategy.couponList.length === 0"
|
:pagination="false"
|
>
|
<template #empty>
|
<div class="flex-center">
|
<img src="/src/assets/element-img/no-data.png" style="width:123px;height:125px" @error="(e: any) => { e.target.src = 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png' }">
|
</div>
|
</template>
|
</NDataTable>
|
</div>
|
|
<!-- 发放规则 -->
|
<NFormItem label="发放规则" class="mt-24px">
|
<div class="w-full pl-8px">
|
<NRadioGroup v-model:value="strategy.distributionRules" class="h-32px flex items-center">
|
<NSpace>
|
<NRadio :value="1" label="每日循环发放" />
|
<NRadio :value="2" label="仅发放一次" />
|
<NRadio :value="3" label="使用后发放" />
|
</NSpace>
|
</NRadioGroup>
|
<div v-if="strategy.distributionRules === 1" class="flex items-center gap-16px mt-8px">
|
<NTimePicker
|
v-model:formatted-value="strategy.distributionTime"
|
format="HH:mm"
|
value-format="HH:mm"
|
:actions="['now']"
|
clearable
|
placeholder="请选择每日发放时间点"
|
style="width: 240px;"
|
@update:value="
|
() => {
|
closeTimePicker();
|
}
|
"
|
/>
|
</div>
|
|
<div
|
v-if="strategy.distributionRules === 3"
|
class="flex flex-col gap-16px w-full mt-8px"
|
>
|
<div class="flex items-center gap-16px">
|
<span>使用后发放规则</span>
|
<NRadioGroup v-model:value="strategy.distributionRulesAfterUse.ruleType">
|
<NSpace>
|
<NRadio :value="1" label="本券使用后循环发放" />
|
<NRadio :value="2" label="指定优惠券使用后发放" />
|
</NSpace>
|
</NRadioGroup>
|
</div>
|
<div v-if="strategy.distributionRulesAfterUse.ruleType === 2" class="flex flex-col gap-12px w-full">
|
<div class="flex items-center justify-between gap-16px">
|
<span>优惠券</span>
|
<NButton
|
type="primary" secondary style="border: 1px solid #7ABDFF;" class="w-97px"
|
@click="handleOpenCouponModal(strategyIndex, 'afterUse')"
|
>
|
选择优惠券
|
</NButton>
|
</div>
|
<div class="overflow-hidden w-full">
|
<NDataTable
|
:columns="createCouponColumns(strategyIndex, 'afterUse')"
|
:data="strategy.distributionRulesAfterUse.couponList"
|
:bordered="false"
|
:style="strategy.distributionRulesAfterUse.couponList.length === 0 ? { minHeight: '263px' } : {}"
|
:flex-height="strategy.distributionRulesAfterUse.couponList.length === 0"
|
:pagination="false"
|
>
|
<template #empty>
|
<div class="flex-center">
|
<img src="/src/assets/element-img/no-data.png" style="width:123px;height:125px" @error="(e: any) => { e.target.src = 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png' }">
|
</div>
|
</template>
|
</NDataTable>
|
</div>
|
</div>
|
</div>
|
</div>
|
</NFormItem>
|
|
<!-- 生效规则 -->
|
<NFormItem label="生效规则" class="mt-12px">
|
<div class="flex flex-col pl-8px gap-8px w-full">
|
<NRadioGroup v-model:value="strategy.effectiveRules" class="h-32px flex items-center">
|
<NSpace>
|
<NRadio :value="1" label="领取后生效" />
|
<NRadio :value="2" label="领取后指定日期生效" />
|
</NSpace>
|
</NRadioGroup>
|
|
<div v-if="strategy.effectiveRules === 2" class="flex items-center gap-16px">
|
<span>生效日期</span>
|
<NDatePicker
|
v-model:value="strategy.effectiveDays" type="date" format="yyyy/MM/dd" clearable
|
:actions="null"
|
:is-date-disabled="disablePastDate"
|
close-on-select
|
placeholder="请选择日期"
|
style="width: 240px;"
|
/>
|
</div>
|
</div>
|
</NFormItem>
|
|
<!-- 失效规则 -->
|
<NFormItem label="失效规则" class="mt-12px">
|
<div class="flex flex-col gap-8px w-full pl-8px">
|
<NRadioGroup v-model:value="strategy.expireRules" class="h-32px flex items-center">
|
<NSpace>
|
<NRadio :value="1" label="次日0点失效" />
|
<NRadio :value="2" label="指定有效天数" />
|
<NRadio :value="3" label="指定日期失效" />
|
</NSpace>
|
</NRadioGroup>
|
|
<div v-if="strategy.expireRules === 2" class="flex items-center gap-16px">
|
<span>失效天数</span>
|
<NInputNumber
|
v-model:value="strategy.expireDays" placeholder="请输入天数" :min="1" :show-button="false"
|
style="width: 240px;"
|
/>
|
</div>
|
<div v-if="strategy.expireRules === 3" class="flex items-center gap-16px">
|
<span>有效期至</span>
|
<NDatePicker
|
v-model:value="strategy.expireDate" type="date" format="yyyy/MM/dd" clearable
|
:actions="null"
|
:is-date-disabled="(timestamp: number) => disableExpireDate(timestamp, strategy)"
|
close-on-select
|
placeholder="请选择日期"
|
style="width: 240px;"
|
/>
|
</div>
|
</div>
|
</NFormItem>
|
|
<!-- 是否可叠加使用 -->
|
<NFormItem label="是否可叠加使用" class="mt-8px">
|
<div class="flex flex-col w-full pl-8px gap-12px">
|
<NRadioGroup v-model:value="strategy.stackedUsage" class="h-32px flex items-center">
|
<NSpace>
|
<NRadio :value="0" label="不可叠加" />
|
<NRadio :value="1" label="可叠加" />
|
</NSpace>
|
</NRadioGroup>
|
<div v-if="strategy.stackedUsage === 1" class="flex flex-col gap-12px">
|
<div class="flex items-center gap-8px">
|
<span>叠加使用规则</span>
|
<span class="pl-8px">1、叠加使用时,总优惠金额不可超过订单金额的</span>
|
<NInputNumber
|
v-model:value="strategy.repPercent"
|
placeholder="请输入百分比"
|
:min="1"
|
:max="100"
|
:show-button="false"
|
style="width: 109px;"
|
/>
|
<span>% ,若超出则按订单最高折扣计算</span>
|
</div>
|
<div class="flex items-center gap-8px pl-100px">
|
<span>2、可叠加的优惠券类型</span>
|
<NSelect
|
v-model:value="strategy.couponType"
|
:options="stackedCouponTypeOptions"
|
placeholder="请选择优惠券类型"
|
clearable
|
style="width: 277px;"
|
@update:show="(val) => stackedCouponTypeSelectOpen[strategyIndex] = val"
|
>
|
<template #arrow>
|
<img
|
src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e120f81c-9f15-4e1a-9938-22252898edf9.png"
|
alt="arrow"
|
:style="{
|
width: '16px',
|
height: '16px',
|
transition: 'transform 0.2s',
|
transform: stackedCouponTypeSelectOpen[strategyIndex] ? 'rotate(180deg)' : 'rotate(0deg)',
|
}"
|
>
|
</template>
|
<template #empty>
|
<div class="flex-center">
|
<img src="/src/assets/element-img/no-data.png" style="width: 50%" alt="no-data">
|
</div>
|
</template>
|
</NSelect>
|
</div>
|
</div>
|
</div>
|
</NFormItem>
|
|
<!-- 使用限时 -->
|
<NFormItem label="使用限时">
|
<div class="flex flex-col w-full">
|
<span class="text-[#171f26cc] text-13px h-32px line-height-32px">仅可在每天的以下时间段使用</span>
|
<div
|
v-for="(timeLimit, index) in strategy.usagePeriod" :key="index"
|
class="flex items-center gap-8px mt-8px"
|
>
|
<NTimePicker
|
v-model:formatted-value="timeLimit.startTime" format="HH:mm" value-format="HH:mm"
|
placeholder="请选择开始时间" style="width: 240px" :actions="['now']" @update:value="
|
() => {
|
closeTimePicker();
|
ensureUsageEndTimeValid(timeLimit);
|
}
|
"
|
/>
|
<span class="text-[#171f26cc]">至</span>
|
<NTimePicker
|
v-model:formatted-value="timeLimit.endTime" format="HH:mm" value-format="HH:mm"
|
placeholder="请选择结束时间"
|
style="width: 240px"
|
:actions="['now']"
|
:is-hour-disabled="(hour: number) => isUsageEndHourDisabled(hour, timeLimit.startTime)"
|
:is-minute-disabled="(minute: number, hour: number | null) => isUsageEndMinuteDisabled(minute, hour, timeLimit.startTime)"
|
@update:value="
|
() => {
|
closeTimePicker();
|
ensureUsageEndTimeValid(timeLimit);
|
}
|
"
|
/>
|
<img v-if="index > 0" :src="deleteIcon" alt="删除" style="width: 16px;height: 16px;cursor: pointer;" @click="handleRemoveTimeLimit(strategyIndex, index)">
|
</div>
|
<NButton
|
type="primary" secondary style="border: 1px solid #7ABDFF;" class="w-509px mt-16px"
|
@click="handleAddTimeLimit(strategyIndex)"
|
>
|
<template #icon>
|
<icon-park-outline-plus style="font-size: 32px;" />
|
</template>
|
新增时间段
|
</NButton>
|
</div>
|
</NFormItem>
|
|
<!-- 数量和自动使用 -->
|
<div class="flex items-start gap-24px mt-16px">
|
<NFormItem label="每人限领数量">
|
<NInputNumber
|
v-model:value="strategy.limitNumber" placeholder="最小1张,最大10张" :min="1" :max="10"
|
:show-button="false" style="width: 240px"
|
/>
|
</NFormItem>
|
<NFormItem label="发放总量">
|
<NInputNumber
|
v-model:value="strategy.distributionsTotal" placeholder="若不填则不限数量" :min="1"
|
:show-button="false" style="width: 240px"
|
/>
|
</NFormItem>
|
</div>
|
|
<NFormItem label="是否自动使用" class="mt-8px">
|
<NRadioGroup v-model:value="strategy.startUsing" class="h-32px flex items-center pl-8px">
|
<NSpace>
|
<NRadio :value="0" label="不开启" />
|
<NRadio :value="1" label="冒泡发单时自动使用" />
|
</NSpace>
|
</NRadioGroup>
|
</NFormItem>
|
</NForm>
|
</div>
|
|
<div class="flex">
|
<NButton type="primary" secondary style="width: 240px;border: 1px solid #7ABDFF;" @click="handleAddStrategy">
|
新增策略
|
</NButton>
|
</div>
|
</div>
|
|
<!-- 发放用户 -->
|
<div v-show="activeTab === 3" class="flex flex-col">
|
<div
|
v-for="(group, groupIndex) in userGroupList" :key="groupIndex"
|
class="flex flex-col gap-24px border-b border-[#EBEEF5] last:border-b-0 pb-24px last:pb-0 mb-24px"
|
>
|
<div class="flex items-center gap-24px">
|
<div class="flex items-center gap-16px">
|
<span class="text-[#606266]">用户类型</span>
|
<div class="flex items-center gap-4px">
|
<NRadioGroup v-model:value="group.userType">
|
<NRadioButton :value="1" label="全部" />
|
<NRadioButton :value="2" label="老用户" />
|
<NRadioButton :value="3" label="新用户" />
|
<NRadioButton :value="4" label="流失用户" />
|
<NRadioButton :value="5" label="指定用户" />
|
</NRadioGroup>
|
<icon-park-outline-help
|
color="#0256ff" class="cursor-pointer"
|
@click="showUserTypeHelpModal = true"
|
/>
|
</div>
|
</div>
|
<div class="flex items-center gap-16px">
|
<span class="text-[#606266]">绑定策略组</span>
|
<NSelect
|
:value="group.actitcityStrategeId"
|
:options="strategyGroupOptions"
|
placeholder="请选择"
|
style="width: 240px"
|
@update:value="(value) => syncUserGroupStrategy(groupIndex, value as number | null)"
|
/>
|
</div>
|
<img v-if="groupIndex > 0" :src="deleteIcon" alt="删除" style="width: 16px;height: 16px;cursor: pointer;margin-left: auto;" @click="handleRemoveUserGroup(groupIndex)">
|
</div>
|
|
<!-- 指定用户下的表格 -->
|
<div v-if="group.userType === 5" class="flex flex-col gap-12px w-full">
|
<div class="flex justify-end">
|
<NButton type="primary" secondary style="width: 84px;border: 1px solid #7ABDFF;" @click="handleOpenUserModal(groupIndex)">
|
导入用户
|
</NButton>
|
</div>
|
<div class="overflow-hidden">
|
<NDataTable
|
:columns="createUserColumns(groupIndex)"
|
:data="group.userList"
|
:bordered="false"
|
:style="group.userList.length === 0 ? { minHeight: '263px' } : {}"
|
:pagination="false"
|
>
|
<template #empty>
|
<div class="flex-center">
|
<img src="/src/assets/element-img/no-data.png" style="width:123px;height:125px" @error="(e: any) => { e.target.src = 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png' }">
|
</div>
|
</template>
|
</NDataTable>
|
</div>
|
<div v-if="group.pagination.total > 0" class="flex justify-end mt-8px">
|
<Pagination
|
:count="group.pagination.total" :page="group.pagination.page"
|
:page-size="group.pagination.pageSize"
|
@change="(page, size) => handleUserPageChange(groupIndex, page, size)"
|
/>
|
</div>
|
</div>
|
</div>
|
|
<div class="flex">
|
<NButton
|
style="width: 240px; border: 1px solid #7ABDFF;"
|
secondary
|
type="primary"
|
@click="handleAddUserGroup"
|
>
|
新增用户群
|
</NButton>
|
</div>
|
</div>
|
|
<!-- 通知方式 -->
|
<div v-show="activeTab === 4" class="flex flex-col gap-12px">
|
<div class="flex items-center gap-16px mb-8px">
|
<span>通知方式</span>
|
<NCheckboxGroup v-model:value="notifyForm.noticeType">
|
<NSpace item-style="display: flex; align-items: center;" :size="16">
|
<NCheckbox :value="1" label="微信消息通知" />
|
<NCheckbox :value="2" label="短信通知" />
|
<NCheckbox :value="4" label="小程序弹窗通知" />
|
</NSpace>
|
</NCheckboxGroup>
|
</div>
|
|
<div
|
v-if="notifyForm.noticeType.includes(1)"
|
class="border border-[#EEE9E9] border-solid rounded-6px p-24px flex flex-col gap-16px"
|
>
|
<div class=" font-bold text-14px">
|
微信消息配置
|
</div>
|
|
<div class="flex items-center gap-16px">
|
<span>触达时机</span>
|
<NCheckboxGroup v-model:value="notifyForm.wechatTriggerTiming">
|
<NSpace :size="16">
|
<NCheckbox :value="1" label="券到账" />
|
<NCheckbox :value="2" label="即将过期" />
|
<NCheckbox :value="4" label="活动开始" />
|
</NSpace>
|
</NCheckboxGroup>
|
</div>
|
|
<div class="flex items-center gap-8px ml-[70px]">
|
<span>临近到期时间前</span>
|
<NInputNumber
|
v-model:value="notifyForm.wechatDaysBeforeExpiry" placeholder="请输入天数" :show-button="false"
|
style="width: 109px;"
|
/>
|
<span>天,触达通知</span>
|
|
<span class="text-[#606266] ml-16px">临近券生效前</span>
|
<NInputNumber
|
v-model:value="notifyForm.wechatMinutesBeforeEffective" placeholder="请输入分钟数" :show-button="false"
|
style="width: 109px;"
|
/>
|
<span>分钟,触达通知</span>
|
</div>
|
</div>
|
|
<div
|
v-if="notifyForm.noticeType.includes(2)"
|
class="border border-[#EEE9E9] border-solid rounded-6px p-24px flex flex-col gap-16px"
|
>
|
<div class="font-bold text-14px">
|
短信通知配置
|
</div>
|
|
<div class="flex items-center gap-16px">
|
<span>触达时机</span>
|
<NCheckboxGroup v-model:value="notifyForm.smsTriggerTiming">
|
<NSpace :size="16">
|
<NCheckbox :value="1" label="券到账" />
|
<NCheckbox :value="2" label="即将过期" />
|
<NCheckbox :value="4" label="活动开始" />
|
</NSpace>
|
</NCheckboxGroup>
|
</div>
|
|
<div class="flex items-center gap-8px ml-[70px]">
|
<span>临近到期时间前</span>
|
<NInputNumber
|
v-model:value="notifyForm.smsDaysBeforeExpiry" placeholder="请输入天数" :show-button="false"
|
style="width: 109px;"
|
/>
|
<span>天,触达通知</span>
|
|
<span class="text-[#606266] ml-16px">临近券生效前</span>
|
<NInputNumber
|
v-model:value="notifyForm.smsMinutesBeforeEffective" placeholder="请输入分钟数" :show-button="false"
|
style="width: 109px;"
|
/>
|
<span>分钟,触达通知</span>
|
</div>
|
|
<div class="flex gap-8px">
|
<span class="lh-32px">短信通知屏蔽时间</span>
|
<div class="flex flex-col gap-8px">
|
<div
|
v-for="(timeLimit, index) in notifyForm.blockTime"
|
:key="index"
|
class="flex flex items-center gap-8px"
|
>
|
<NTimePicker
|
v-model:formatted-value="timeLimit.startTime"
|
format="HH:mm"
|
value-format="HH:mm"
|
placeholder="请选择开始时间"
|
style="width: 240px"
|
:actions="['now']" @update:value="
|
() => {
|
closeTimePicker();
|
}
|
"
|
/>
|
<span class="text-[#909399]">至</span>
|
<NTimePicker
|
v-model:formatted-value="timeLimit.endTime"
|
format="HH:mm"
|
value-format="HH:mm"
|
placeholder="请选择结束时间"
|
style="width: 240px"
|
:actions="['now']" @update:value="
|
() => {
|
closeTimePicker();
|
}
|
"
|
/>
|
<img v-if="index > 0" :src="deleteIcon" alt="删除" style="width: 16px;height: 16px;cursor: pointer;" @click="handleRemoveBlockTime(index)">
|
</div>
|
</div>
|
</div>
|
<NButton
|
style="width: 240px; border: 1px solid #7ABDFF;margin-left: 120px;"
|
secondary
|
type="primary"
|
@click="handleAddBlockTime"
|
>
|
新增时间段
|
</NButton>
|
</div>
|
|
<div
|
v-if="notifyForm.noticeType.includes(4)"
|
class="border border-[#EEE9E9] border-solid rounded-6px p-24px flex flex-col gap-16px"
|
>
|
<div class=" font-bold text-14px">
|
小程序弹窗配置
|
</div>
|
<div>
|
<div class="flex items-center gap-24px">
|
<div class="flex items-center gap-8px">
|
<span>弹窗标题</span>
|
<NInput
|
v-model:value="notifyForm.miniProgramPopupTitle"
|
placeholder="请输入(最多6个字)"
|
maxlength="6"
|
style="width: 300px;"
|
/>
|
</div>
|
<div class="flex items-center gap-8px">
|
<span>按钮文案</span>
|
<NInput
|
v-model:value="notifyForm.miniProgramPopupButtonText"
|
placeholder="请输入(最多6个字)"
|
maxlength="6"
|
style="width: 300px;"
|
/>
|
</div>
|
</div>
|
|
<div class="flex items-center gap-16px mt-16px">
|
<span>toast文案配置</span>
|
<NCheckboxGroup v-model:value="notifyForm.miniProgramTriggerTiming">
|
<NSpace :size="16">
|
<NCheckbox :value="1" label="券到账" />
|
<NCheckbox :value="2" label="即将过期" />
|
<!-- <NCheckbox :value="4" label="活动开始" /> -->
|
</NSpace>
|
</NCheckboxGroup>
|
</div>
|
|
<div class="flex items-center gap-8px mt-16px pl-[104px]">
|
<span>临近券生效前</span>
|
<NInputNumber
|
v-model:value="notifyForm.miniProgramBeforeEffective"
|
placeholder="请输入分钟数"
|
:show-button="false"
|
style="width: 109px;"
|
/>
|
<span>分钟,toast通知,通知内容为</span>
|
<NInput
|
v-model:value="notifyForm.toastBeforeEffective"
|
placeholder="请输入(最多10个字)"
|
maxlength="10"
|
style="width: 280px;"
|
/>
|
</div>
|
|
<div class="flex items-center gap-8px mt-16px pl-[104px]">
|
<span>临近到期时间前</span>
|
<NInputNumber
|
v-model:value="notifyForm.miniProgramDaysBeforeExpiry"
|
placeholder="请输入天数"
|
:show-button="false"
|
style="width: 109px;"
|
/>
|
<span>天,toast通知,通知内容为</span>
|
<NInput
|
v-model:value="notifyForm.toastBeforeExpiry"
|
placeholder="请输入(最多10个字)"
|
maxlength="10"
|
style="width: 300px;"
|
/>
|
</div>
|
|
<div class="flex items-center gap-24px mt-16px">
|
<div class="flex items-center gap-8px">
|
<span>是否开启浮窗</span>
|
<NSwitch v-model:value="notifyForm.enableFloatingWindow" />
|
</div>
|
<div v-if="notifyForm.enableFloatingWindow" class="flex items-center gap-8px">
|
<span>浮窗优先级权重</span>
|
<NInputNumber
|
v-model:value="notifyForm.floatingWindowPriority"
|
placeholder="请输入(1~100,权重越大越优先)"
|
:show-button="false"
|
:min="1"
|
:max="100"
|
style="width: 300px;"
|
/>
|
</div>
|
<div v-if="notifyForm.enableFloatingWindow" class="flex items-center gap-8px">
|
<span>跳转链接</span>
|
<NInput
|
v-model:value="notifyForm.linkUrl"
|
placeholder="请输入跳转链接"
|
style="width: 300px;"
|
/>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
<!-- 底部操作栏 -->
|
<div class="footer flex justify-end items-center shrink-0">
|
<NSpace>
|
<NButton v-if="activeTab === 1" class="w-84px" @click="handleCancel">
|
取消
|
</NButton>
|
<NButton v-if="activeTab > 1" class="w-84px" @click="handlePrev">
|
上一步
|
</NButton>
|
<NButton type="primary" class="w-84px" :loading="saving" @click="handleNext">
|
{{ activeTab === 4 ? '保存' : '下一步' }}
|
</NButton>
|
</NSpace>
|
</div>
|
|
<!-- 优惠券选择弹窗 -->
|
<CouponSelectModal
|
v-model:show="showCouponModal"
|
:selected-coupons="currentSelectedCoupons"
|
@save="handleSaveCoupons"
|
/>
|
|
<!-- 选择用户弹窗 -->
|
<UserSelectModal
|
v-model:show="showUserModal"
|
:selected-users="currentSelectedUsers"
|
@save="handleSaveUsers"
|
/>
|
|
<!-- 用户类型说明弹窗 -->
|
<NModal v-model:show="showUserTypeHelpModal" preset="card" style="width: 600px;" title="用户类型说明">
|
<div class="flex flex-col gap-24px text-[#303133] text-14px leading-relaxed p-12px">
|
<div>
|
<span class="font-bold">新用户:</span>未使用小程序下单的用户
|
</div>
|
<div>
|
<span class="font-bold">老用户:</span>已使用小程序下单并完成订单的用户(注:只要用户有订单生成,无论是否支付订单均划分为老用户)
|
</div>
|
<div>
|
<span class="font-bold">流失用户:</span>已使用小程序下单并且在行程结束后,在系统设定的阈值日期范围内,再未使用小程序下单的用户
|
</div>
|
</div>
|
</NModal>
|
</div>
|
</template>
|
|
<style scoped lang="scss">
|
.box {
|
padding: 24px;
|
background-color: #fff;
|
border-radius: 6px;
|
width: 100%;
|
box-sizing: border-box;
|
--n-label-text-color: rgba(23, 31, 38, 0.8);
|
|
:deep(.n-form-item-label) {
|
padding-right: 8px !important;
|
font-size: 14px;
|
font-weight: 500;
|
}
|
|
:deep(.n-radio-button:first-child) {
|
border-top-left-radius: 4px;
|
border-bottom-left-radius: 4px;
|
}
|
|
:deep(.n-radio-button:last-child) {
|
border-top-right-radius: 4px;
|
border-bottom-right-radius: 4px;
|
}
|
}
|
|
:deep(.n-upload-trigger--image-card) {
|
width: 56px !important;
|
height: 56px;
|
border: none !important;
|
}
|
|
:deep(.n-upload-file-list .n-upload-file.n-upload-file--image-card-type) {
|
width: 56px !important;
|
height: 56px;
|
}
|
|
:deep(.n-upload-dragger) {
|
border:none !important;
|
}
|
|
:deep(.n-upload-file-list.n-upload-file-list--grid) {
|
grid-template-columns: repeat(auto-fill, 56px);
|
}
|
|
.head {
|
font-family: PingFang SC;
|
font-size: 16px;
|
font-weight: 600;
|
line-height: 24px;
|
letter-spacing: normal;
|
color: #171f26;
|
position: relative;
|
text-indent: 11px;
|
margin-bottom: 16px;
|
|
&::before {
|
content: "";
|
position: absolute;
|
top: 50%;
|
left: 0;
|
transform: translateY(-50%);
|
width: 3px;
|
height: 14px;
|
z-index: 0;
|
border-radius: 3px;
|
background: #0068ff;
|
}
|
}
|
|
.footer {
|
position: sticky;
|
bottom: 0;
|
left: 0;
|
width: 100%;
|
height: 64px;
|
background-color: #fff;
|
box-shadow: 0px 0px 5px 0px rgba(23, 31, 38, 0.05);
|
padding: 16px 24px;
|
}
|
|
:deep(.n-tabs-tab-pad) {
|
width: 24px !important;
|
}
|
|
:deep(.n-tabs-pane-wrapper) {
|
height: 8px !important;
|
}
|
|
:deep(.n-tabs-nav-scroll-content) {
|
border: none !important;
|
}
|
|
:deep(.n-tabs-bar) {
|
height: 3px !important;
|
}
|
|
:deep(.n-data-table-empty) {
|
border-bottom: 1px solid #EBEEF5;
|
box-sizing: border-box;
|
}
|
|
.custom-form-item {
|
:deep(.n-form-item-feedback__line) {
|
padding-left: 195px;
|
}
|
}
|
</style>
|