src/service/api/operations.ts
@@ -1,5 +1,37 @@ import { request } from '@/service/http' /** 登录协议接口 */ export function saveLoginAgreement(data: { id?: number | string name: string content: string sort: number type: number }) { return request.Post<Service.ResponseResult<any>>('/operation/api/v1/agreement/saveLoginAgreement', data) } export function getLoginAgreementPageList(type: number) { return request.Get<Service.ResponseResult<Array<{ id: number content: string createBy: string createTime: string deleted: boolean name: string sort: number type: number updateBy: string updateTime: string version: string }>>>(`/operation/api/v1/agreement/getLoginAgreementPageList/${type}`) } export function deleteLoginAgreement(id: number | string) { return request.Delete<Service.ResponseResult<any>>(`/operation/api/v1/agreement/deleteLoginAgreement/${id}`) } /** 开通城市接口 */ // 获取开通城市列表 src/views/userContent/active/activityManagement/add.vue
@@ -1,5 +1,6 @@ <script setup lang="tsx"> import { ref } from 'vue' import dayjs from 'dayjs' import { computed, ref } from 'vue' import { useRouter } from 'vue-router' import { NButton, @@ -29,12 +30,14 @@ import CouponSelectModal from './components/CouponSelectModal.vue' import UserSelectModal from './components/UserSelectModal.vue' import Pagination from '@/components/common/Pagination.vue' import { addCouponActivity } from '@/service/api/coupon' import { customUploadRequest } from '@/utils/upload' const router = useRouter() const message = useMessage() const formRef = ref<FormInst | null>(null) const fileList = ref<UploadFileInfo[]>([]) const saving = ref(false) const showCouponModal = ref(false) const currentSelectStrategyIndex = ref(-1) @@ -64,7 +67,7 @@ strategyList.value[strategyIndex].couponList = [...rows] } else { strategyList.value[strategyIndex].afterUseCouponList = [...rows] strategyList.value[strategyIndex].distributionRulesAfterUse.couponList = [...rows] } message.success(`成功选择了 ${rows.length} 张优惠券`) } @@ -74,32 +77,61 @@ showUserModal.value = true } type UserListItem = { id: number | string account: string remark: string } type 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 }, } } // 用户群相关 const userGroupList = ref([ const userGroupList = ref<UserGroupItem[]>([ { userType: '指定用户', strategyGroup: null, ...createUserGroup(5), userList: [ { account: '15708179461', remark: '媒体' }, { account: '15708179461', remark: '-' }, { account: '15708179461', remark: '媒体' }, { account: '15708179461', remark: '-' }, { account: '15708179461', remark: '-' }, { id: 1, account: '15708179461', remark: '媒体' }, { id: 2, account: '15708179461', remark: '-' }, { id: 3, account: '15708179461', remark: '媒体' }, { id: 4, account: '15708179461', remark: '-' }, { id: 5, account: '15708179461', remark: '-' }, ], pagination: { page: 1, pageSize: 10, total: 1000 }, }, { userType: '老用户', strategyGroup: null, userList: [], pagination: { page: 1, pageSize: 10, total: 0 }, }, createUserGroup(2), ]) function handleSaveUsers(rows: any[]) { // In real app, append to userList const group = userGroupList.value[currentSelectGroupIndex.value] const newUsers = rows.map(r => ({ account: r.account, remark: '-' })) const newUsers = rows.map(r => ({ id: r.id, account: r.account, remark: r.nickName || '-', })) group.userList.push(...newUsers) group.pagination.total += newUsers.length message.success(`成功导入了 ${newUsers.length} 名用户`) @@ -107,28 +139,70 @@ const activeTab = ref(2) const strategyList = ref([ { strategyName: '', issueRule: '每日0点循环发放', afterUseRule: '本券使用后循环发放', type UsagePeriodItem = { startTime: string | null endTime: string | null } type DistributionRulesAfterUse = { ruleType: 1 | 2 couponList: any[] } type StrategyItem = { actitcityId: number | null id: number | null name: string distributionRules: 1 | 2 | 3 distributionRulesAfterUse: DistributionRulesAfterUse effectRule: '领取后生效' | '领取后指定日期生效' effectDate: number | null distributionsTotal: number | null effectiveDays: number | null effectiveRules: 1 | 2 | 3 endTime: 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, distributionRulesAfterUse: { ruleType: 1, couponList: [], }, effectRule: '领取后生效', effectDate: null, expireRule: '次日0点失效', expireDays: null, expireDate: null, canStack: false, timeLimits: [{ startTime: null, endTime: null }], perPersonLimit: 1, totalIssueAmount: null, autoUse: false, couponList: [] as any[], afterUseCouponList: [] as any[], }, ]) distributionsTotal: null, effectiveDays: null, effectiveRules: 1, endTime: null, limitNumber: 1, stackedUsage: 0, startUsing: 0, usagePeriod: [{ startTime: null, endTime: null }], couponList: [], } } const strategyList = ref<StrategyItem[]>([createStrategy()]) const strategyGroupOptions = computed(() => { return strategyList.value.map((strategy, index) => ({ label: strategy.name.trim() || `策略${index + 1}`, value: strategy.id ?? index + 1, })) }) const strategyRules = { strategyName: [ name: [ { required: true, message: '请输入策略名称', trigger: 'blur' }, { max: 20, message: '最多20个字符', trigger: 'blur' }, ], @@ -136,32 +210,34 @@ } function handleAddStrategy() { strategyList.value.push({ strategyName: '', issueRule: '每日0点循环发放', afterUseRule: '本券使用后循环发放', effectRule: '领取后生效', effectDate: null, expireRule: '次日0点失效', expireDays: null, expireDate: null, canStack: false, timeLimits: [{ startTime: null, endTime: null }], perPersonLimit: 1, totalIssueAmount: null, autoUse: false, couponList: [], afterUseCouponList: [], }) strategyList.value.push(createStrategy()) } 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].timeLimits.length < 10) { strategyList.value[strategyIndex].timeLimits.push({ startTime: null, endTime: null }) if (strategyList.value[strategyIndex].usagePeriod.length < 10) { strategyList.value[strategyIndex].usagePeriod.push({ startTime: null, endTime: null }) } else { message.warning('最多可配置10个时间段') @@ -169,8 +245,8 @@ } function handleRemoveTimeLimit(strategyIndex: number, timeLimitIndex: number) { if (strategyList.value[strategyIndex].timeLimits.length > 1) { strategyList.value[strategyIndex].timeLimits.splice(timeLimitIndex, 1) if (strategyList.value[strategyIndex].usagePeriod.length > 1) { strategyList.value[strategyIndex].usagePeriod.splice(timeLimitIndex, 1) } } @@ -202,14 +278,14 @@ if (type === 'normal') targetStrategy.couponList.splice(rowIndex, 1) else targetStrategy.afterUseCouponList.splice(rowIndex, 1) targetStrategy.distributionRulesAfterUse.couponList.splice(rowIndex, 1) message.success('移除成功') } function createCouponColumns(strategyIndex: number, type: 'normal' | 'afterUse'): DataTableColumns<any> { return [ { title: '添加时间', key: 'createTime', width: 140 }, { title: '添加时间', key: 'createTime', width: 220 }, { title: '优惠券名称', key: 'couponName', ellipsis: true }, { title: '优惠券类型', @@ -295,32 +371,59 @@ }, } type NotifyTimingValue = 1 | 2 | 4 type 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({ methods: ['微信消息通知', '短信通知', '小程序弹窗通知'], wxNotify: { triggers: ['券到账', '即将过期', '活动开始'], expireDays: null, expireMins: null, }, smsNotify: { triggers: ['券到账', '即将过期', '活动开始'], expireDays: null, expireMins: null, blockStartTime: null, blockEndTime: null, }, 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], miniProgramBeforeEffective: null, miniProgramDaysBeforeExpiry: null, toastBeforeEffective: '', toastBeforeExpiry: '', enableFloatingWindow: true, floatingWindowPriority: null, linkUrl: '', miniProgramPopupTitle: '', miniProgramPopupButtonText: '', }) // 其他表单与数据处理 function handleAddUserGroup() { userGroupList.value.push({ userType: '全部', strategyGroup: null, userList: [], pagination: { page: 1, pageSize: 10, total: 0 }, }) userGroupList.value.push(createUserGroup(1)) } function createUserColumns(groupIndex: number): DataTableColumns<any> { @@ -425,6 +528,178 @@ } } function sumSelectedValues(values: Array<number | string>) { return values.reduce((sum, value) => sum + Number(value || 0), 0) } 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 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, distributionRulesAfterUse: buildDistributionRulesAfterUse(strategy), distributionsTotal: strategy.distributionsTotal, effectiveDays: strategy.effectiveRules === 2 ? strategy.effectiveDays : null, effectiveRules: strategy.effectiveRules, endTime: strategy.effectiveRules === 3 ? formatDateTime(strategy.endTime, 'end') : '', 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: JSON.stringify(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 buildBlockTime(blockTime: UsagePeriodItem[]) { return blockTime .filter(item => item.startTime && item.endTime) .map(item => `${item.startTime}_${item.endTime}`) } function buildNotifyPayload(activityId: number | null = null) { return { activityId, id: notifyForm.value.id, noticeType: sumSelectedValues(notifyForm.value.noticeType), wechatTriggerTiming: sumSelectedValues(notifyForm.value.wechatTriggerTiming), wechatDaysBeforeExpiry: notifyForm.value.wechatDaysBeforeExpiry, wechatMinutesBeforeEffective: notifyForm.value.wechatMinutesBeforeEffective, smsTriggerTiming: sumSelectedValues(notifyForm.value.smsTriggerTiming), smsDaysBeforeExpiry: notifyForm.value.smsDaysBeforeExpiry, smsMinutesBeforeEffective: notifyForm.value.smsMinutesBeforeEffective, blockTime: buildBlockTime(notifyForm.value.blockTime), miniProgramTriggerTiming: sumSelectedValues(notifyForm.value.miniProgramTriggerTiming), miniProgramBeforeEffective: notifyForm.value.miniProgramBeforeEffective, miniProgramDaysBeforeExpiry: notifyForm.value.miniProgramDaysBeforeExpiry, toastBeforeEffective: notifyForm.value.toastBeforeEffective, toastBeforeExpiry: notifyForm.value.toastBeforeExpiry, enableFloatingWindow: notifyForm.value.enableFloatingWindow, floatingWindowPriority: notifyForm.value.floatingWindowPriority, linkUrl: notifyForm.value.linkUrl, miniProgramPopupTitle: notifyForm.value.miniProgramPopupTitle, miniProgramPopupButtonText: notifyForm.value.miniProgramPopupButtonText, } } 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 === 3 && strategy.distributionRulesAfterUse.ruleType === 2 && strategy.distributionRulesAfterUse.couponList.length === 0) { 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.userType === 5 && group.userList.length === 0) { activeTab.value = 3 message.warning(`请为用户群${index + 1}导入指定用户`) return false } } return true } async function handleSubmit() { try { await formRef.value?.validate() } catch { activeTab.value = 1 return } if (!validateStrategyList() || !validateUserGroupList()) return const payload = { 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, strategy: buildStrategyPayload(), activityUser: buildUserGroupPayload(), notice: buildNotifyPayload(), } 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(() => { @@ -432,8 +707,9 @@ }) return } if (activeTab.value === 2) { if (activeTab.value === 4) { handleSubmit() return } activeTab.value++ } @@ -562,28 +838,28 @@ <!-- 发放规则 --> <NFormItem label="发放规则:"> <div class="flex flex-col gap-16px w-full"> <NRadioGroup v-model:value="strategy.issueRule"> <NRadioGroup v-model:value="strategy.distributionRules"> <NSpace> <NRadio value="每日0点循环发放" label="每日0点循环发放" /> <NRadio value="仅发放一次" label="仅发放一次" /> <NRadio value="使用后发放" label="使用后发放" /> <NRadio :value="1" label="每日0点循环发放" /> <NRadio :value="2" label="仅发放一次" /> <NRadio :value="3" label="使用后发放" /> </NSpace> </NRadioGroup> <div v-if="strategy.issueRule === '使用后发放'" v-if="strategy.distributionRules === 3" class="bg-[#FAFAFA] p-16px rounded-4px border border-[#EBEEF5] flex flex-col gap-16px w-full" > <div class="flex items-center gap-16px"> <span class="text-[#606266]">使用后发放规则:</span> <NRadioGroup v-model:value="strategy.afterUseRule"> <NRadioGroup v-model:value="strategy.distributionRulesAfterUse.ruleType"> <NSpace> <NRadio value="本券使用后循环发放" label="本券使用后循环发放" /> <NRadio value="指定优惠券使用后发放" label="指定优惠券使用后发放" /> <NRadio :value="1" label="本券使用后循环发放" /> <NRadio :value="2" label="指定优惠券使用后发放" /> </NSpace> </NRadioGroup> </div> <div v-if="strategy.afterUseRule === '指定优惠券使用后发放'" class="flex flex-col gap-12px w-full"> <div v-if="strategy.distributionRulesAfterUse.ruleType === 2" class="flex flex-col gap-12px w-full"> <div class="flex items-center gap-16px"> <span class="text-[#606266]">优惠券:</span> <NButton @@ -596,7 +872,7 @@ <div class="border border-[#EBEEF5] rounded-4px overflow-hidden w-full"> <NDataTable :columns="createCouponColumns(strategyIndex, 'afterUse')" :data="strategy.afterUseCouponList" :data="strategy.distributionRulesAfterUse.couponList" :bordered="false" size="small" /> @@ -629,25 +905,25 @@ <!-- 失效规则 --> <NFormItem label="失效规则:"> <div class="flex flex-col gap-16px w-full"> <NRadioGroup v-model:value="strategy.expireRule"> <NRadioGroup v-model:value="strategy.effectiveRules"> <NSpace> <NRadio value="次日0点失效" label="次日0点失效" /> <NRadio value="指定有效天数" label="指定有效天数" /> <NRadio value="指定日期失效" label="指定日期失效" /> <NRadio :value="1" label="次日0点失效" /> <NRadio :value="2" label="指定有效天数" /> <NRadio :value="3" label="指定日期失效" /> </NSpace> </NRadioGroup> <div v-if="strategy.expireRule === '指定有效天数'" class="flex items-center gap-16px"> <div v-if="strategy.effectiveRules === 2" class="flex items-center gap-16px"> <span class="text-[#606266]">有效天数</span> <NInputNumber v-model:value="strategy.expireDays" placeholder="请输入天数" :min="1" :show-button="false" v-model:value="strategy.effectiveDays" placeholder="请输入天数" :min="1" :show-button="false" style="width: 200px;" /> </div> <div v-if="strategy.expireRule === '指定日期失效'" class="flex items-center gap-16px"> <div v-if="strategy.effectiveRules === 3" class="flex items-center gap-16px"> <span class="text-[#606266]">有效期至</span> <NDatePicker v-model:value="strategy.expireDate" type="date" format="yyyy/MM/dd" clearable v-model:value="strategy.endTime" type="date" format="yyyy/MM/dd" clearable :actions="null" close-on-select placeholder="请选择日期" style="width: 200px;" /> </div> @@ -656,10 +932,10 @@ <!-- 是否可叠加使用 --> <NFormItem label="是否可叠加使用:"> <NRadioGroup v-model:value="strategy.canStack"> <NRadioGroup v-model:value="strategy.stackedUsage"> <NSpace> <NRadio :value="false" label="不可叠加" /> <NRadio :value="true" label="可叠加" /> <NRadio :value="0" label="不可叠加" /> <NRadio :value="1" label="可叠加" /> </NSpace> </NRadioGroup> </NFormItem> @@ -669,7 +945,7 @@ <div class="flex flex-col gap-12px w-full"> <span class="text-[#606266] text-14px">仅可在每天的以下时间段使用</span> <div v-for="(timeLimit, index) in strategy.timeLimits" :key="index" v-for="(timeLimit, index) in strategy.usagePeriod" :key="index" class="flex items-center gap-12px" > <NTimePicker @@ -683,14 +959,14 @@ /> <div class="flex gap-8px ml-8px"> <div v-if="index === strategy.timeLimits.length - 1 && strategy.timeLimits.length < 10" v-if="index === strategy.usagePeriod.length - 1 && strategy.usagePeriod.length < 10" class="w-24px h-24px rounded-full border border-[#0068FF] text-[#0068FF] flex items-center justify-center cursor-pointer" @click="handleAddTimeLimit(strategyIndex)" > <span class="text-16px leading-none">+</span> </div> <div v-if="strategy.timeLimits.length > 1" v-if="strategy.usagePeriod.length > 1" class="w-24px h-24px rounded-full border border-[#C0C4CC] text-[#C0C4CC] flex items-center justify-center cursor-pointer" @click="handleRemoveTimeLimit(strategyIndex, index)" > @@ -705,23 +981,23 @@ <div class="flex items-start gap-40px"> <NFormItem label="每人限领数量:" class="flex-1"> <NInputNumber v-model:value="strategy.perPersonLimit" placeholder="最小1张,最大10张" :min="1" :max="10" v-model:value="strategy.limitNumber" placeholder="最小1张,最大10张" :min="1" :max="10" :show-button="false" style="width: 100%" /> </NFormItem> <NFormItem label="发放总量:" class="flex-1"> <NInputNumber v-model:value="strategy.totalIssueAmount" placeholder="若不填则不限数量" :min="1" v-model:value="strategy.distributionsTotal" placeholder="若不填则不限数量" :min="1" :show-button="false" style="width: 100%" /> </NFormItem> </div> <NFormItem label="是否自动使用:"> <NRadioGroup v-model:value="strategy.autoUse"> <NRadioGroup v-model:value="strategy.startUsing"> <NSpace> <NRadio :value="false" label="不开启" /> <NRadio :value="true" label="冒泡发单时自动使用" /> <NRadio :value="0" label="不开启" /> <NRadio :value="1" label="冒泡发单时自动使用" /> </NSpace> </NRadioGroup> </NFormItem> @@ -746,11 +1022,11 @@ <span class="text-[#606266]">用户类型</span> <div class="flex items-center gap-8px"> <NRadioGroup v-model:value="group.userType"> <NRadioButton value="全部" label="全部" /> <NRadioButton value="老用户" label="老用户" /> <NRadioButton value="新用户" label="新用户" /> <NRadioButton value="流失用户" label="流失用户" /> <NRadioButton value="指定用户" label="指定用户" /> <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 v-if="groupIndex === 0" color="#0256ff" class="cursor-pointer" @@ -760,12 +1036,18 @@ </div> <div class="flex items-center gap-16px"> <span class="text-[#606266]">绑定策略组</span> <NSelect v-model:value="group.strategyGroup" placeholder="请选择" style="width: 240px" /> <NSelect :value="group.actitcityStrategeId" :options="strategyGroupOptions" placeholder="请选择" style="width: 240px" @update:value="(value) => syncUserGroupStrategy(groupIndex, value as number | null)" /> </div> </div> <!-- 指定用户下的表格 --> <div v-if="group.userType === '指定用户'" class="flex flex-col gap-16px w-full"> <div v-if="group.userType === 5" class="flex flex-col gap-16px w-full"> <div> <NButton type="primary" style="width: 100px;" @click="handleOpenUserModal(groupIndex)"> 导入用户 @@ -801,17 +1083,17 @@ <div v-show="activeTab === 4" class="flex flex-col gap-24px"> <div class="flex items-center gap-24px"> <span class="text-[#606266] shrink-0">通知方式:</span> <NCheckboxGroup v-model:value="notifyForm.methods"> <NCheckboxGroup v-model:value="notifyForm.noticeType"> <NSpace item-style="display: flex; align-items: center;" :size="40"> <NCheckbox value="微信消息通知" label="微信消息通知" /> <NCheckbox value="短信通知" label="短信通知" /> <NCheckbox value="小程序弹窗通知" label="小程序弹窗通知" /> <NCheckbox :value="1" label="微信消息通知" /> <NCheckbox :value="2" label="短信通知" /> <NCheckbox :value="4" label="小程序弹窗通知" /> </NSpace> </NCheckboxGroup> </div> <div v-if="notifyForm.methods.includes('微信消息通知')" v-if="notifyForm.noticeType.includes(1)" class="bg-[#F8F9FA] rounded-8px p-24px flex flex-col gap-24px" > <div class="text-[#171f26] font-bold text-14px"> @@ -820,11 +1102,11 @@ <div class="flex items-center gap-24px pl-16px"> <span class="text-[#606266] shrink-0">触达时机:</span> <NCheckboxGroup v-model:value="notifyForm.wxNotify.triggers"> <NCheckboxGroup v-model:value="notifyForm.wechatTriggerTiming"> <NSpace :size="40"> <NCheckbox value="券到账" label="券到账" /> <NCheckbox value="即将过期" label="即将过期" /> <NCheckbox value="活动开始" label="活动开始" /> <NCheckbox :value="1" label="券到账" /> <NCheckbox :value="2" label="即将过期" /> <NCheckbox :value="4" label="活动开始" /> </NSpace> </NCheckboxGroup> </div> @@ -832,14 +1114,14 @@ <div class="flex items-center gap-12px pl-16px ml-[70px]"> <span class="text-[#606266]">临近到期时间前</span> <NInputNumber v-model:value="notifyForm.wxNotify.expireDays" placeholder="请输入天数" :show-button="false" v-model:value="notifyForm.wechatDaysBeforeExpiry" placeholder="请输入天数" :show-button="false" style="width: 160px;" /> <span class="text-[#606266]">天,触达通知</span> <span class="text-[#606266] ml-24px">临近券生效前</span> <NInputNumber v-model:value="notifyForm.wxNotify.expireMins" placeholder="请输入分钟数" :show-button="false" v-model:value="notifyForm.wechatMinutesBeforeEffective" placeholder="请输入分钟数" :show-button="false" style="width: 160px;" /> <span class="text-[#606266]">分钟,触达通知</span> @@ -847,7 +1129,7 @@ </div> <div v-if="notifyForm.methods.includes('短信通知')" v-if="notifyForm.noticeType.includes(2)" class="bg-[#F8F9FA] rounded-8px p-24px flex flex-col gap-24px" > <div class="text-[#171f26] font-bold text-14px"> @@ -856,11 +1138,11 @@ <div class="flex items-center gap-24px pl-16px"> <span class="text-[#606266] shrink-0">触达时机:</span> <NCheckboxGroup v-model:value="notifyForm.smsNotify.triggers"> <NCheckboxGroup v-model:value="notifyForm.smsTriggerTiming"> <NSpace :size="40"> <NCheckbox value="券到账" label="券到账" /> <NCheckbox value="即将过期" label="即将过期" /> <NCheckbox value="活动开始" label="活动开始" /> <NCheckbox :value="1" label="券到账" /> <NCheckbox :value="2" label="即将过期" /> <NCheckbox :value="4" label="活动开始" /> </NSpace> </NCheckboxGroup> </div> @@ -868,40 +1150,163 @@ <div class="flex items-center gap-12px pl-16px ml-[70px]"> <span class="text-[#606266]">临近到期时间前</span> <NInputNumber v-model:value="notifyForm.smsNotify.expireDays" placeholder="请输入天数" :show-button="false" v-model:value="notifyForm.smsDaysBeforeExpiry" placeholder="请输入天数" :show-button="false" style="width: 160px;" /> <span class="text-[#606266]">天,触达通知</span> <span class="text-[#606266] ml-24px">临近券生效前</span> <NInputNumber v-model:value="notifyForm.smsNotify.expireMins" placeholder="请输入分钟数" :show-button="false" v-model:value="notifyForm.smsMinutesBeforeEffective" placeholder="请输入分钟数" :show-button="false" style="width: 160px;" /> <span class="text-[#606266]">分钟,触达通知</span> </div> <div class="flex items-center gap-12px pl-16px"> <div class="flex flex-col gap-12px pl-16px"> <span class="text-[#606266] shrink-0">短信通知屏蔽时间:</span> <NTimePicker v-model:formatted-value="notifyForm.smsNotify.blockStartTime" format="HH:mm" value-format="HH:mm" placeholder="请选择开始时间" style="width: 200px" /> <NTimePicker v-model:formatted-value="notifyForm.smsNotify.blockEndTime" format="HH:mm" value-format="HH:mm" placeholder="请选择结束时间" style="width: 200px" /> <div v-for="(timeLimit, index) in notifyForm.blockTime" :key="index" class="flex items-center gap-12px" > <NTimePicker v-model:formatted-value="timeLimit.startTime" format="HH:mm" value-format="HH:mm" placeholder="请选择开始时间" style="width: 200px" /> <span class="text-[#909399]">至</span> <NTimePicker v-model:formatted-value="timeLimit.endTime" format="HH:mm" value-format="HH:mm" placeholder="请选择结束时间" style="width: 200px" /> <div class="flex gap-8px ml-8px"> <div v-if="index === notifyForm.blockTime.length - 1 && notifyForm.blockTime.length < 10" class="w-24px h-24px rounded-full border border-[#0068FF] text-[#0068FF] flex items-center justify-center cursor-pointer" @click="handleAddBlockTime" > <span class="text-16px leading-none">+</span> </div> <div v-if="notifyForm.blockTime.length > 1" class="w-24px h-24px rounded-full border border-[#C0C4CC] text-[#C0C4CC] flex items-center justify-center cursor-pointer" @click="handleRemoveBlockTime(index)" > <span class="text-12px leading-none mt-[-2px]">一</span> </div> </div> </div> </div> </div> <div v-if="notifyForm.methods.includes('小程序弹窗通知')" class="bg-[#F8F9FA] rounded-8px p-24px flex flex-col gap-24px" style="min-height: 200px;" v-if="notifyForm.noticeType.includes(4)" class="bg-[#F8F9FA] rounded-8px p-24px flex flex-col gap-24px" > <div class="text-[#171f26] font-bold text-14px"> 小程序弹窗配置 </div> <!-- Empty placeholder area matching the screenshot --> <div class="mini-popup-panel"> <div class="mini-popup-row"> <div class="mini-popup-field"> <span class="mini-popup-label">弹窗标题:</span> <NInput v-model:value="notifyForm.miniProgramPopupTitle" placeholder="请输入(最多6个字)" maxlength="6" /> </div> <div class="mini-popup-field"> <span class="mini-popup-label">按钮文案:</span> <NInput v-model:value="notifyForm.miniProgramPopupButtonText" placeholder="请输入(最多6个字)" maxlength="6" /> </div> </div> <div class="mini-popup-row mini-popup-row--top"> <div class="mini-popup-field mini-popup-field--full"> <span class="mini-popup-label">toast文案配置:</span> <NCheckboxGroup v-model:value="notifyForm.miniProgramTriggerTiming"> <NSpace :size="32"> <NCheckbox :value="1" label="券到账" /> <NCheckbox :value="2" label="即将过期" /> <NCheckbox :value="4" label="活动开始" /> </NSpace> </NCheckboxGroup> </div> </div> <div class="mini-popup-row mini-popup-row--compact"> <span class="mini-popup-spacer" /> <span class="mini-popup-text">临近券生效前</span> <NInputNumber v-model:value="notifyForm.miniProgramBeforeEffective" placeholder="请输入分钟数" :show-button="false" style="width: 180px;" /> <span class="mini-popup-text">分钟,toast通知,通知内容为:</span> <NInput v-model:value="notifyForm.toastBeforeEffective" placeholder="请输入(最多10个字)" maxlength="10" style="width: 280px;" /> </div> <div class="mini-popup-row mini-popup-row--compact"> <span class="mini-popup-spacer" /> <span class="mini-popup-text">临近到期时间前</span> <NInputNumber v-model:value="notifyForm.miniProgramDaysBeforeExpiry" placeholder="请输入天数" :show-button="false" style="width: 180px;" /> <span class="mini-popup-text">天,toast通知,通知内容为:</span> <NInput v-model:value="notifyForm.toastBeforeExpiry" placeholder="请输入(最多10个字)" maxlength="10" style="width: 280px;" /> </div> <div class="mini-popup-row mini-popup-row--bottom"> <div class="mini-popup-inline-field mini-popup-inline-field--switch"> <span class="mini-popup-label">是否开启浮窗:</span> <NSwitch v-model:value="notifyForm.enableFloatingWindow" /> </div> <div class="mini-popup-inline-field"> <span class="mini-popup-label">浮窗优先级权重:</span> <NInputNumber v-model:value="notifyForm.floatingWindowPriority" placeholder="请输入(1~100,权重越大越优先)" :show-button="false" :min="1" :max="100" style="width: 280px;" /> </div> <div class="mini-popup-inline-field"> <span class="mini-popup-label">跳转链接:</span> <NInput v-model:value="notifyForm.linkUrl" placeholder="请输入跳转链接" style="width: 260px;" /> </div> </div> </div> </div> </div> </div> @@ -912,7 +1317,7 @@ <NButton class="w-84px" @click="handleCancel"> 取消 </NButton> <NButton type="primary" class="w-84px" @click="handleNext"> <NButton type="primary" class="w-84px" :loading="saving" @click="handleNext"> {{ activeTab === 4 ? '保存' : '下一步' }} </NButton> </NSpace> @@ -1019,4 +1424,72 @@ box-shadow: 0px 0px 5px 0px rgba(23, 31, 38, 0.05); padding: 16px 24px; } .mini-popup-panel { display: flex; flex-direction: column; gap: 24px; padding: 8px 0 4px; } .mini-popup-row { display: flex; align-items: center; gap: 40px; flex-wrap: wrap; } .mini-popup-row--top { margin-top: -4px; } .mini-popup-row--compact { gap: 12px; flex-wrap: nowrap; } .mini-popup-row--bottom { justify-content: space-between; gap: 24px; } .mini-popup-field { display: flex; align-items: center; gap: 12px; min-width: 0; flex: 1; } .mini-popup-field--full { flex: none; } .mini-popup-inline-field { display: flex; align-items: center; gap: 12px; min-width: 0; } .mini-popup-inline-field--switch { min-width: 176px; } .mini-popup-label { color: #606266; white-space: nowrap; flex-shrink: 0; } .mini-popup-text { color: #606266; white-space: nowrap; flex-shrink: 0; } .mini-popup-spacer { width: 96px; flex-shrink: 0; } </style> src/views/userContent/active/activityManagement/components/ActivityInfo.vue
@@ -1,81 +1,177 @@ <script setup lang="tsx"> import { ref } from "vue"; import { NCard, NDataTable, NImage } from "naive-ui"; import { computed } from 'vue' import { NCard, NDataTable, NImage, NSpin } from 'naive-ui' import dayjs from 'dayjs' // Mock Data const basicInfo = { name: "活动名称活动名称活动名称活动名称", type: "新人活动", time: "2026/09/09-2026/10/10", hasLimit: "是", limitStrategy: "当合计优惠金额达到1000,000.00元时,结束活动,相关优惠券失效", desc: "AntV 是蚂蚁金服全新一代数据可视化解决方案,致力于提供一套简单方便、专业可靠、不限可能的数据可视化最佳实践。得益于丰富的业务场景和用户需求挑战,AntV 经历多年积累与不断打磨,已支撑整个阿里集团内外 20000+ 业务系统,通过了日均千万级 UV 产品的严苛考验,多余三行省略此字段单独一行行...", cover: "", }; interface Props { detailData?: Record<string, any> | null loading?: boolean } const strategyColumns1 = [ { title: "策略名称", key: "name", minWidth: 200 }, { title: "操作", key: "actions", width: 100, render(row: any) { return <a class="text-[#0068ff] cursor-pointer">详情</a>; }, }, ]; const strategyData1 = ref([ { id: 1, name: "策略名称内容策略名称内容策略名称内容策略名称内容" }, { id: 2, name: "策略名称内容策略名称内容策略名称内容策略名称内容" }, { id: 3, name: "策略名称内容策略名称内容策略名称内容策略名称内容" }, ]); const props = withDefaults(defineProps<Props>(), { detailData: null, loading: false, }) const strategyColumns2 = [ { title: "用户群", key: "group", width: 200 }, { title: "策略名称", key: "name", minWidth: 200 }, { title: "操作", key: "actions", width: 100, render(row: any) { return <a class="text-[#0068ff] cursor-pointer">详情</a>; }, }, ]; const strategyData2 = ref([ { id: 1, group: "新用户", name: "策略名称内容策略名称内容策略名称内容策略名称内容", }, { id: 2, group: "新用户", name: "策略名称内容策略名称内容策略名称内容策略名称内容", }, { id: 3, group: "流失用户", name: "策略名称内容策略名称内容策略名称内容策略名称内容", }, ]); const activityTypeMap: Record<number, string> = { 1: '新人活动', 2: '媒体邀约活动', 3: '日常提频', } const wechatNotify = { trigger: "券到账,即将到期,活动开始", expire: "临近到期时间前6天触达通知", effect: "临近券生效前88分钟触达通知", }; const userTypeMap: Record<number, string> = { 1: '全部用户', 2: '老用户', 3: '新用户', 4: '流失用户', 5: '指定用户', } const smsNotify = { trigger: "券到账,即将到期,活动开始", expire: "临近到期时间前6天触达通知", effect: "临近券生效前88分钟触达通知", blockTime: "2026/10/10 11:09:09-2026/10/10 11:09:09", }; const timingMap: Record<number, string> = { 1: '券到账', 2: '即将过期', 4: '活动开始', } 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 formatDateRange(startTime?: string, endTime?: string) { if (!startTime || !endTime) return '-' return `${dayjs(startTime).format('YYYY/MM/DD')} - ${dayjs(endTime).format('YYYY/MM/DD')}` } function formatLimitStrategy(enableTotalLimit?: boolean, totalLimit?: number | null) { if (!enableTotalLimit) return '未开启' if (totalLimit === null || totalLimit === undefined || totalLimit === '') return '已开启,不限额' return `当合计优惠金额达到${totalLimit}元时,结束活动,相关优惠券失效` } function formatNotifyTiming(value: unknown) { const source = Array.isArray(value) ? value : typeof value === 'number' ? Object.keys(timingMap).map(Number).filter(item => (value & item) === item) : [] return source.map(item => timingMap[Number(item)]).filter(Boolean).join(',') || '-' } function formatNotifyText(prefix: string, value?: number | null, unit = '天') { if (value === null || value === undefined || value === '') return '-' return `${prefix}${value}${unit}触达通知` } function formatBlockTime(value: unknown) { const list = toArray(value) return list.length ? list.join(',') : '-' } function getCoverUrl(value: unknown) { if (!value) return '' if (typeof value === 'string') { try { const parsed = JSON.parse(value) return parsed?.[0]?.url || '' } catch { return value } } if (Array.isArray(value)) return value?.[0]?.url || '' return '' } const activityInfo = computed(() => props.detailData || {}) const strategyList = computed(() => toArray(activityInfo.value.strategy)) const activityUserList = computed(() => toArray(activityInfo.value.activityUser)) const noticeInfo = computed(() => activityInfo.value.notice || {}) const basicInfo = computed(() => ({ name: activityInfo.value.acticityName || '-', type: activityTypeMap[Number(activityInfo.value.activityType)] || '-', time: formatDateRange(activityInfo.value.startTime, activityInfo.value.endTime), hasLimit: activityInfo.value.enableTotalLimit ? '是' : '否', limitStrategy: formatLimitStrategy(activityInfo.value.enableTotalLimit, activityInfo.value.totalLimit), desc: activityInfo.value.richText || '-', cover: getCoverUrl(activityInfo.value.img), })) const strategyColumns = [ { title: '策略名称', key: 'name', minWidth: 200 }, { title: '优惠券数量', key: 'couponCount', width: 120 }, ] const strategyData = computed(() => { return strategyList.value.map((item: any, index: number) => ({ id: item.id ?? index + 1, name: item.name || `策略${index + 1}`, couponCount: Array.isArray(item.couponIds) ? item.couponIds.length : 0, })) }) const userColumns = [ { title: '用户群', key: 'group', width: 200 }, { title: '策略名称', key: 'name', minWidth: 200 }, { title: '用户数量', key: 'userCount', width: 120 }, ] const userData = computed(() => { return activityUserList.value.map((item: any, index: number) => ({ id: item.id ?? index + 1, group: userTypeMap[Number(item.userType)] || '-', name: item.strategyName || '-', userCount: toArray(item.userList).length, })) }) const wechatNotify = computed(() => ({ trigger: formatNotifyTiming(noticeInfo.value.wechatTriggerTiming), expire: formatNotifyText('临近到期时间前', noticeInfo.value.wechatDaysBeforeExpiry, '天'), effect: formatNotifyText('临近券生效前', noticeInfo.value.wechatMinutesBeforeEffective, '分钟'), })) const smsNotify = computed(() => ({ trigger: formatNotifyTiming(noticeInfo.value.smsTriggerTiming), expire: formatNotifyText('临近到期时间前', noticeInfo.value.smsDaysBeforeExpiry, '天'), effect: formatNotifyText('临近券生效前', noticeInfo.value.smsMinutesBeforeEffective, '分钟'), blockTime: formatBlockTime(noticeInfo.value.blockTime), })) const miniProgramNotify = computed(() => ({ trigger: formatNotifyTiming(noticeInfo.value.miniProgramTriggerTiming), effect: formatNotifyText('临近券生效前', noticeInfo.value.miniProgramBeforeEffective, '分钟'), expire: formatNotifyText('临近到期时间前', noticeInfo.value.miniProgramDaysBeforeExpiry, '天'), effectToast: noticeInfo.value.toastBeforeEffective || '-', expireToast: noticeInfo.value.toastBeforeExpiry || '-', enableFloat: noticeInfo.value.enableFloatingWindow ? '开启' : '关闭', floatPriority: noticeInfo.value.floatingWindowPriority ?? '-', linkUrl: noticeInfo.value.linkUrl || '-', title: noticeInfo.value.miniProgramPopupTitle || '-', buttonText: noticeInfo.value.miniProgramPopupButtonText || '-', })) </script> <template> <div class="flex flex-col gap-12px"> <n-spin :show="loading"> <div class="flex flex-col gap-12px"> <!-- 基础信息 --> <n-card class="rounded-6px" :bordered="false" size="medium"> <div class="head">基础信息</div> @@ -124,8 +220,8 @@ <div class="head">活动策略</div> <div class="mt-16px"> <n-data-table :columns="strategyColumns1" :data="strategyData1" :columns="strategyColumns" :data="strategyData" :bordered="false" /> </div> @@ -136,8 +232,8 @@ <div class="head">发放用户</div> <div class="mt-16px"> <n-data-table :columns="strategyColumns2" :data="strategyData2" :columns="userColumns" :data="userData" :bordered="false" /> </div> @@ -182,9 +278,54 @@ <span class="value">{{ smsNotify.blockTime }}</span> </div> </div> <div class="sub-title mt-32px mb-12px">小程序弹窗通知</div> <div class="info-grid"> <div class="info-item"> <span class="label">触达时机:</span> <span class="value">{{ miniProgramNotify.trigger }}</span> </div> <div class="info-item"> <span class="label">弹窗标题:</span> <span class="value">{{ miniProgramNotify.title }}</span> </div> <div class="info-item"> <span class="label">按钮文案:</span> <span class="value">{{ miniProgramNotify.buttonText }}</span> </div> <div class="info-item"> <span class="label">券生效通知:</span> <span class="value">{{ miniProgramNotify.effect }}</span> </div> <div class="info-item"> <span class="label">生效toast文案:</span> <span class="value">{{ miniProgramNotify.effectToast }}</span> </div> <div class="info-item"> <span class="label">到期时间通知:</span> <span class="value">{{ miniProgramNotify.expire }}</span> </div> <div class="info-item"> <span class="label">到期toast文案:</span> <span class="value">{{ miniProgramNotify.expireToast }}</span> </div> <div class="info-item"> <span class="label">是否开启浮窗:</span> <span class="value">{{ miniProgramNotify.enableFloat }}</span> </div> <div class="info-item"> <span class="label">浮窗优先级:</span> <span class="value">{{ miniProgramNotify.floatPriority }}</span> </div> <div class="info-item col-span-3"> <span class="label">跳转链接:</span> <span class="value">{{ miniProgramNotify.linkUrl }}</span> </div> </div> </div> </n-card> </div> </div> </n-spin> </template> <style scoped lang="scss"> @@ -238,6 +379,18 @@ color: #171f26; word-break: break-all; } .col-span-2 { grid-column: span 2; } .col-span-3 { grid-column: span 3; } .items-start { align-items: flex-start; } .sub-title{ font-size: 14px; font-weight: 500; src/views/userContent/active/activityManagement/components/CouponSelectModal.vue
@@ -17,8 +17,6 @@ const props = defineProps<Props>() const emits = defineEmits<Emits>() const message = useMessage() const showModal = computed({ get: () => props.show, set: val => emits('update:show', val), @@ -32,10 +30,9 @@ }) const couponTypeOptions = [ { label: '满减券', value: 1 }, { label: '抵扣券', value: 2 }, { label: '免单券', value: 3 }, { label: '折扣券', value: 4 }, { label: '抵扣券', value: 1 }, { label: '满减券', value: 2 }, { label: '抹零券', value: 3 }, ] const isCouponTypeSelectOpen = ref(false) @@ -45,7 +42,20 @@ const currentPage = ref(1) const pageSize = ref(10) const total = ref(100) const total = ref(0) function formatMoneyWithUnit(value: unknown) { if (value === null || value === undefined || value === '') return '-' const numberValue = Number(value) if (Number.isNaN(numberValue)) return `¥${String(value)}` const formatted = new Intl.NumberFormat('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2, }).format(numberValue) return `¥${formatted}` } function handleSearch() { currentPage.value = 1 @@ -87,31 +97,19 @@ title: '条件金额', key: 'conditionalAmount', width: 100, render: row => row.conditionalAmount !== null && row.conditionalAmount !== undefined ? `¥${row.conditionalAmount}` : '-', render: row => formatMoneyWithUnit(row.conditionalAmount), }, { title: '优惠金额', key: 'amount', width: 100, render: row => row.amount !== null && row.amount !== undefined ? `¥${row.amount}` : '-', render: row => formatMoneyWithUnit(row.amount), }, { title: '状态', key: 'status', width: 100, render(row) { let color = '#909399' // 待发布 if (row.status === '已发布') color = '#52C41A' else if (row.status === '已暂停') color = '#FAAD14' else if (row.status === '已作废') color = '#FF4D4F' return ( <span style={{ color }}>{row.status}</span> ) }, render: () => '待发布', }, ] @@ -136,7 +134,13 @@ watch(() => props.show, (val) => { if (val) { searchModel.value = { couponName: '', couponType: null, } checkedRowKeys.value = [] currentPage.value = 1 pageSize.value = 10 getList() } }) src/views/userContent/active/activityManagement/detail.vue
@@ -1,17 +1,40 @@ <script setup lang="tsx"> import { ref } from "vue"; import { useRouter } from "vue-router"; import { NButton, NCard, NSpace, NTabPane, NTabs } from "naive-ui"; import ActivityInfo from "./components/ActivityInfo.vue"; import ReceiveRecord from "./components/ReceiveRecord.vue"; import ActivityOrderList from "./components/ActivityOrderList.vue"; import { onMounted, ref } from 'vue' import { useRoute, useRouter } from 'vue-router' import ActivityInfo from './components/ActivityInfo.vue' import ReceiveRecord from './components/ReceiveRecord.vue' import ActivityOrderList from './components/ActivityOrderList.vue' import { fetchCouponActivityDetail } from '@/service/api/coupon' const router = useRouter(); const activeTab = ref("info"); const route = useRoute() const router = useRouter() const activeTab = ref('info') const loading = ref(false) const detailData = ref<Record<string, any> | null>(null) async function getDetail() { const id = route.query.id if (!id) return loading.value = true try { const res = await fetchCouponActivityDetail(String(id)) if (res.code === 200) detailData.value = res.data || null } finally { loading.value = false } } function handleBack() { router.back(); router.back() } onMounted(() => { getDetail() }) </script> <template> @@ -43,7 +66,7 @@ </div> <div class="flex-1 overflow-y-auto bg-[#F2F3F5] pt-9px"> <ActivityInfo v-if="activeTab === 'info'" /> <ActivityInfo v-if="activeTab === 'info'" :detail-data="detailData" :loading="loading" /> <ReceiveRecord v-if="activeTab === 'record'" /> <ActivityOrderList v-if="activeTab === 'order'" /> </div> src/views/userContent/active/activityManagement/index.vue
@@ -1,5 +1,5 @@ <script setup lang="tsx"> import { computed, onMounted, reactive, ref } from 'vue' import { computed, onActivated, onMounted, reactive, ref } from 'vue' import { useRouter } from 'vue-router' import { NButton, NInput, NSelect, NSpace, NTag, useDialog, useMessage } from 'naive-ui' import type { DataTableColumns } from 'naive-ui' @@ -27,11 +27,18 @@ ] const statusOptions = [ { label: '待发布', value: 1 }, { label: '未开始', value: 1 }, { label: '进行中', value: 2 }, { label: '已暂停', value: 4 }, { label: '已结束', value: 3 }, { label: '暂停', value: 4 }, ] const statusLabelMap: Record<number, string> = { 1: '未开始', 2: '进行中', 3: '已结束', 4: '暂停', } const loading = ref(false) const list = ref<any[]>([]) @@ -191,23 +198,23 @@ width: 100, render: (row) => { let type: 'default' | 'success' | 'warning' | 'error' | 'info' | 'primary' = 'default' switch (row.status) { case '待发布': switch (Number(row.status)) { case 1: type = 'warning' break case '进行中': case 2: type = 'success' break case '已暂停': case 4: type = 'error' break case '已结束': case 3: type = 'default' break } return ( <NTag type={type} bordered={false}> {row.status} {statusLabelMap[Number(row.status)] || '-'} </NTag> ) }, @@ -219,21 +226,21 @@ fixed: 'right', render: (row) => { const btns = [] if (row.status === '待发布') { if (Number(row.status) === 1) { btns.push(<NButton text type="primary" onClick={() => handlePublish(row)}>发布</NButton>) btns.push(<NButton text type="primary" onClick={() => handleDetail(row)}>详情</NButton>) btns.push(<NButton text type="primary" onClick={() => handleEdit(row)}>编辑</NButton>) btns.push(<NButton text type="primary" onClick={() => handleDelete(row)}>删除</NButton>) } else if (row.status === '进行中') { else if (Number(row.status) === 2) { btns.push(<NButton text type="primary" onClick={() => handleDetail(row)}>详情</NButton>) btns.push(<NButton text type="primary" onClick={() => handlePause(row)}>暂停活动</NButton>) } else if (row.status === '已暂停') { else if (Number(row.status) === 4) { btns.push(<NButton text type="primary" onClick={() => handleDetail(row)}>详情</NButton>) btns.push(<NButton text type="primary" onClick={() => handleStop(row)}>终止活动</NButton>) } else if (row.status === '已结束') { else if (Number(row.status) === 3) { btns.push(<NButton text type="primary" onClick={() => handleDetail(row)}>详情</NButton>) btns.push(<NButton text type="primary" onClick={() => handleDelete(row)}>删除</NButton>) } @@ -248,6 +255,13 @@ onMounted(() => { getList() }) onActivated(() => { if (window.sessionStorage.getItem('activityManagementNeedRefresh') === '1') { window.sessionStorage.removeItem('activityManagementNeedRefresh') getList() } }) </script> <template> src/views/userContent/active/activityManagement/new.txt
New file @@ -0,0 +1,64 @@ activityId integer($int64) 活动id blockTime [...] createBy string 记录创建人,前端忽略 createTime string($date-time) 记录创建时间,前端忽略 deleted boolean 是否删除 0未删除 1已删除 enableFloatingWindow boolean 是否开启浮窗 floatingWindowPriority integer($int32) 浮窗优先级权重1-100 id integer($int64) linkUrl boolean 跳转链接 miniProgramBeforeEffective integer($int32) miniProgram临近生效分钟数 miniProgramDaysBeforeExpiry integer($int32) miniProgram临近到期天数 miniProgramTriggerTiming integer($int32) 触达时机,券到账=1,即将过期=2,活动开始=4,多选,设置选项的和 noticeType integer($int32) 通知方式,微信=1,短信=2,小程序=4,多选,设置选项的和 smsDaysBeforeExpiry integer($int32) sms临近到期天数 smsMinutesBeforeEffective integer($int32) sms临近生效分钟数 smsTriggerTiming integer($int32) 触达时机,券到账=1,即将过期=2,活动开始=4,多选,设置选项的和 toastBeforeEffective integer($int32) miniProgram临近生效toast toastBeforeExpiry integer($int32) miniProgram临近到期toast updateBy string 记录修改人,前端忽略 updateTime string($date-time) 记录修改时间,前端忽略 wechatDaysBeforeExpiry integer($int32) wx临近到期天数 wechatMinutesBeforeEffective integer($int32) wx临近生效分钟数 wechatTriggerTiming integer($int32) 触达时机,券到账=1,即将过期=2,活动开始=4,多选,设置选项的和 src/views/userContent/active/activityManagement/notice.txt
New file @@ -0,0 +1,53 @@ actitcityId integer($int32) 活动id createBy string 记录创建人,前端忽略 createTime string($date-time) 记录创建时间,前端忽略 deleted boolean 是否删除 0未删除 1已删除 distributionRules integer($int32) 发放规则(1=每日循环发放,2=仅发放一次,3=使用后发放) distributionRulesAfterUse string 使用后发放规则json distributionsTotal integer($int32) 发放总数 effectiveDays integer($int32) 有效天数 effectiveRules integer($int32) 生效规则(1=次日0点失效,2=指定有效天数,3=指定日期失效) endTime string($date-time) 有效结束时间 id integer($int32) 主键 limitNumber integer($int32) 每人限领数量 name string 策略名称 stackedUsage integer($int32) 是否叠加使用(0=否,1=是) startUsing integer($int32) 是否自动使用(0=不开启,1=冒泡发单时自动使用) updateBy string 记录修改人,前端忽略 updateTime string($date-time) 记录修改时间,前端忽略 usagePeriod string 使用时段json src/views/userContent/active/activityManagement/user.txt
New file @@ -0,0 +1,32 @@ actitcityId integer($int32) 活动id actitcityStrategeId integer($int32) 策略id createBy string 记录创建人,前端忽略 createTime string($date-time) 记录创建时间,前端忽略 deleted boolean 是否删除 0未删除 1已删除 id integer($int32) 主键 strategyName string 策略组名称 updateBy string 记录修改人,前端忽略 updateTime string($date-time) 记录修改时间,前端忽略 userList string 指定用户json数组 userType integer($int32) 用户类型(1=全部用户,2=老用户,3=新用户,4=流失用户,5=指定用户) src/views/userContent/contentCenter/api.txt
New file @@ -0,0 +1,34 @@ /api/v1/agreement/saveLoginAgreement 保存登录协议 post json { "content": "string", 协议内容 "id": 0, "name": "string", 协议名称 "sort": 0, 排序 "type": 0, 类型 1=乘客登录协议,2=运维端登录协议 } /api/v1/agreement/getLoginAgreementPageList/{type} 获取登录协议列表 get 类型 1=乘客登录协议,2=运维端登录协议 响应: { "code": 0, "data": [ { "content": "string", "createBy": "string", "createTime": "2026-04-24T16:13:14.634Z", "deleted": true, "id": 0, "name": "string", "sort": 0, "type": 0, "updateBy": "string", "updateTime": "2026-04-24T16:13:14.634Z", "version": "string" } ], "msg": "string" } /api/v1/agreement/deleteLoginAgreement/{id} 删除登录协议 delete src/views/userContent/contentCenter/components/AgreementListForm.vue
@@ -1,61 +1,56 @@ <script setup lang="ts"> import { h, ref } from 'vue' import { NButton, NCard, NDataTable, NForm, NFormItem, NInput, NModal, NPopconfirm, NSpace, useMessage } from 'naive-ui' import { computed, h, onMounted, ref, watch } from 'vue' import { NButton, NDataTable, NForm, NFormItem, NInput, NModal, NPopconfirm, NSpace, useMessage } from 'naive-ui' import type { FormInst } from 'naive-ui' import RichTextEditor from '@/components/custom/Editor/RichTextEditor/index.vue' import { deleteLoginAgreement, getLoginAgreementPageList, saveLoginAgreement } from '@/service/api/operations' const props = defineProps<{ type: 'passenger' | 'ops' }>() const message = useMessage() const addBtnLabel = props.type === 'passenger' ? '新增协议' : '新增登录协议' const titleLabel = '协议名称' const modalTitleAdd = props.type === 'passenger' ? '新增协议' : '新增登录协议' const modalTitleEdit = props.type === 'passenger' ? '编辑协议' : '编辑登录协议' interface AgreementItem { id: string id: number title: string content: string creator: string updateTime: string sort: number } const tableData = ref<AgreementItem[]>([ { id: '1', title: props.type === 'passenger' ? '乘车须知' : '小鹏Robotaxi录音录像及个人信息采集说明', content: '', creator: '张三', updateTime: '2026-04-08 09:14:55', }, { id: '2', title: props.type === 'passenger' ? '隐私政策' : '权限说明', content: '', creator: '张三', updateTime: '2026-04-08 09:14:58', }, ]) interface AgreementFormModel { id?: number title: string content: string sort: number } const message = useMessage() const tableLoading = ref(false) const saveLoading = ref(false) const deleteLoadingId = ref<number | null>(null) const showModal = ref(false) const isEdit = ref(false) const formRef = ref<FormInst | null>(null) const tableData = ref<AgreementItem[]>([]) const formModel = ref<AgreementFormModel>({ title: '', content: '', sort: 1, }) const agreementType = computed(() => props.type === 'passenger' ? 1 : 2) const addBtnLabel = computed(() => props.type === 'passenger' ? '新增协议' : '新增登录协议') const titleLabel = '协议名称' const modalTitleAdd = computed(() => props.type === 'passenger' ? '新增协议' : '新增登录协议') const modalTitleEdit = computed(() => props.type === 'passenger' ? '编辑协议' : '编辑登录协议') const columns = [ { title: '排序', key: 'sort', width: 80, render: () => { return h('div', { style: 'cursor: move; display: flex; align-items: center;' }, [ h('img', { src: '/src/assets/point/union.png', alt: '拖动', style: 'width: 15px; height: 15px;', onError: (e: any) => { e.target.src = 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png' } }) ]) }, render: (row: AgreementItem) => row.sort, }, { title: titleLabel, key: 'title' }, { title: '创建人', key: 'creator' }, @@ -76,7 +71,11 @@ NPopconfirm, { onPositiveClick: () => handleDelete(row) }, { trigger: () => h(NButton, { text: true, type: 'primary' }, { default: () => '删除' }), trigger: () => h(NButton, { text: true, type: 'primary', loading: deleteLoadingId.value === row.id, }, { default: () => '删除' }), default: () => '确认删除该协议吗?', }, ), @@ -86,68 +85,53 @@ }, ] const showModal = ref(false) const isEdit = ref(false) const formRef = ref<any>(null) const formModel = ref({ id: '', title: '', content: '', }) let draggedIndex: number | null = null function handleDragStart(index: number, e: DragEvent) { draggedIndex = index if (e.target && (e.target as HTMLElement).classList) { (e.target as HTMLElement).classList.add('dragging') } } function handleDragEnd(e: DragEvent) { if (e.target && (e.target as HTMLElement).classList) { (e.target as HTMLElement).classList.remove('dragging') } } function handleDragOver(e: DragEvent) { e.preventDefault() e.dataTransfer!.dropEffect = 'move' } function handleDrop(index: number) { if (draggedIndex !== null && draggedIndex !== index) { const items = [...tableData.value] const draggedItem = items[draggedIndex] items.splice(draggedIndex, 1) items.splice(index, 0, draggedItem) tableData.value = items } draggedIndex = null } const rowProps = (row: AgreementItem, index: number) => { return { draggable: true, onDragstart: (e: DragEvent) => handleDragStart(index, e), onDragend: handleDragEnd, onDragover: handleDragOver, onDrop: () => handleDrop(index), class: 'draggable-row' } } const rules = { title: { required: true, message: `请输入${titleLabel}`, trigger: 'blur' }, content: { required: true, message: '请输入协议内容', trigger: 'blur' }, } function normalizeList(data: Array<any>) { return [...data] .sort((a, b) => (a.sort || 0) - (b.sort || 0)) .map(item => ({ id: Number(item.id), title: item.name || '', content: item.content || '', creator: item.updateBy || item.createBy || '', updateTime: item.updateTime || item.createTime || '', sort: Number(item.sort || 0), })) } async function loadList() { tableLoading.value = true try { const res = await getLoginAgreementPageList(agreementType.value) if (res?.code === 200) { tableData.value = normalizeList(res.data || []) } else { tableData.value = [] message.error(res?.msg || '协议列表获取失败') } } finally { tableLoading.value = false } } function getNextSort() { if (tableData.value.length === 0) return 1 return Math.max(...tableData.value.map(item => item.sort || 0)) + 1 } function handleAdd() { isEdit.value = false formModel.value = { id: '', title: '', content: '', sort: getNextSort(), } showModal.value = true } @@ -158,13 +142,26 @@ id: row.id, title: row.title, content: row.content, sort: row.sort, } showModal.value = true } function handleDelete(row: AgreementItem) { tableData.value = tableData.value.filter(item => item.id !== row.id) message.success('删除成功') async function handleDelete(row: AgreementItem) { deleteLoadingId.value = row.id try { const res = await deleteLoginAgreement(row.id) if (res?.code === 200) { message.success('删除成功') await loadList() } else { message.error(res?.msg || '删除失败') } } finally { deleteLoadingId.value = null } } function handleCancel() { @@ -174,28 +171,28 @@ async function handleSave() { try { await formRef.value?.validate() if (isEdit.value) { const index = tableData.value.findIndex(item => item.id === formModel.value.id) if (index > -1) { tableData.value[index].title = formModel.value.title tableData.value[index].content = formModel.value.content tableData.value[index].updateTime = new Date().toLocaleString().replace(/\//g, '-') } saveLoading.value = true const res = await saveLoginAgreement({ id: formModel.value.id, name: formModel.value.title, content: formModel.value.content, sort: formModel.value.sort, type: agreementType.value, }) if (res?.code === 200) { message.success('保存成功') showModal.value = false await loadList() } else { tableData.value.unshift({ id: String(Date.now()), title: formModel.value.title, content: formModel.value.content, creator: '当前用户', updateTime: new Date().toLocaleString().replace(/\//g, '-'), }) message.error(res?.msg || '保存失败') } message.success('保存成功') showModal.value = false } catch (error) { // 验证失败 catch { // 表单校验未通过 } finally { saveLoading.value = false } } @@ -207,21 +204,30 @@ return tableData.value } function setData(data: any) { // 暂时使用 mock 数据,不覆盖 async function setData() { await loadList() } watch(() => props.type, async () => { await loadList() }) onMounted(async () => { await loadList() }) defineExpose({ validate, getData, setData, loadList, }) </script> <template> <div class="agreement-list-form roboTaxi"> <div class="mb-16px"> <n-button type="primary" secondary @click="handleAdd"> <n-button type="primary" secondary @click="handleAdd"> {{ addBtnLabel }} </n-button> </div> @@ -229,10 +235,10 @@ <n-data-table :columns="columns" :data="tableData" :loading="tableLoading" :bordered="false" :bottom-bordered="true" :row-key="row => row.id" :row-props="rowProps" class="custom-table" /> @@ -266,7 +272,7 @@ <n-button style="padding: 8px 24px; border-radius: 4px; font-size: 14px; margin-right: 12px;" @click="handleCancel"> 取消 </n-button> <n-button type="primary" style="padding: 8px 24px; border-radius: 4px; font-size: 14px;" @click="handleSave"> <n-button type="primary" :loading="saveLoading" style="padding: 8px 24px; border-radius: 4px; font-size: 14px;" @click="handleSave"> 保存 </n-button> </div> @@ -307,15 +313,6 @@ background-color: #F7F8FA !important; font-weight: 500; color: #171F26; } :deep(.draggable-row) { cursor: move; } :deep(.draggable-row.dragging) { opacity: 0.5; background-color: #f5f5f5; } .modal-container { src/views/userContent/contentCenter/opsApp.vue
@@ -2,9 +2,7 @@ import { nextTick, ref, watch } from 'vue' import { useMessage } from 'naive-ui' import { getAgreementContent, getVehicleDescription, saveAgreement, saveVehicleDescription, } from '@/service/api/contentCenter' import VehiclePrepareForm from './components/VehiclePrepareForm.vue' @@ -61,19 +59,6 @@ const t = typeMap[activeTab.value] saveRes = await saveVehicleDescription({ descriptionType: t, descriptionContent: data.content || '' }) } else if (activeTab.value === 'opsAppNotice') { const map: Record<string, number> = { operationPrivacyPolicy: 8, thirdPartyShareList: 9, personalInfoCollectList: 10, permissionCall: 11, } const activeKey = typeof currentForm.getActiveKey === 'function' ? currentForm.getActiveKey() : 'operationPrivacyPolicy' const t = map[activeKey] ?? 8 const content = data[activeKey] || '' saveRes = await saveAgreement({ content, contentType: t }) } if (saveRes && saveRes.code === 200) { message.success('保存成功') } @@ -134,19 +119,6 @@ currentForm.setData({ content: res.data?.descriptionContent || '' }) } } else if (tabKey === 'opsAppNotice') { const types = [8, 9, 10, 11] const res = await Promise.all(types.map(t => getAgreementContent(t))) if (res.every(r => r && r.code === 200)) { const data = { operationPrivacyPolicy: res[0].data?.content || '', thirdPartyShareList: res[1].data?.content || '', personalInfoCollectList: res[2].data?.content || '', permissionCall: res[3].data?.content || '', } currentForm.setData(data) } } } finally { loading.value = false @@ -202,7 +174,7 @@ </n-card> </div> <div v-permission="30211" class="footer flex justify-end " style="background-color: white; padding: 16px 24px; border-radius: 8px; box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);"> <div v-if="activeTab !== 'opsAppNotice'" v-permission="30211" class="footer flex justify-end " style="background-color: white; padding: 16px 24px; border-radius: 8px; box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);"> <n-button style="padding: 8px 24px; border-radius: 4px; font-size: 14px;margin-right: 12px;" @click="handleCancel"> 取消 </n-button> src/views/userContent/contentCenter/passenger.vue
@@ -36,21 +36,8 @@ const data = currentForm.getData() let saveRes: any = null if (activeTab.value === 'notice') { const map: Record<string, number> = { privacyPolicy: 1, personalInfoList: 2, sensitiveInfoDesc: 3, thirdPartyInfoDesc: 4, rideNotice: 5, permissionNotice: 7, } const activeKey = typeof currentForm.getActiveKey === 'function' ? currentForm.getActiveKey() : 'privacyPolicy' const t = map[activeKey] ?? 1 const content = data[activeKey] || '' saveRes = await saveAgreement({ content, contentType: t }) } else if (['prepaymentNotice', 'paymentNotice'].includes(activeTab.value)) { if (['prepaymentNotice', 'paymentNotice'].includes(activeTab.value)) { // 仍沿用旧协议内容接口 // TODO: 确认预付款协议和付款协议的 contentType,这里暂定为 12 和 13 const typeMap: Record<string, number> = { prepaymentNotice: 12, @@ -155,22 +142,8 @@ return } if (tabKey === 'notice') { const types = [1, 2, 3, 4, 5, 7] const res = await Promise.all(types.map(t => getAgreementContent(t))) if (res.every(r => r && r.code === 200)) { const data = { privacyPolicy: res[0].data?.content || '', personalInfoList: res[1].data?.content || '', sensitiveInfoDesc: res[2].data?.content || '', thirdPartyInfoDesc: res[3].data?.content || '', rideNotice: res[4].data?.content || '', permissionNotice: res[5].data?.content || '', } currentForm.setData(data) } } else if (['prepaymentNotice', 'paymentNotice'].includes(tabKey)) { if (['prepaymentNotice', 'paymentNotice'].includes(tabKey)) { // 仍沿用旧协议内容接口 // TODO: 确认预付款协议和付款协议的 contentType,这里暂定为 12 和 13 const typeMap: Record<string, number> = { prepaymentNotice: 12, @@ -306,7 +279,7 @@ </n-card> </div> <div v-permission="30211" class="footer flex justify-end " style="background-color: white; padding: 16px 24px; border-radius: 8px; box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);"> <div v-if="activeTab !== 'notice'" v-permission="30211" class="footer flex justify-end " style="background-color: white; padding: 16px 24px; border-radius: 8px; box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);"> <n-button style="padding: 8px 24px; border-radius: 4px; font-size: 14px;margin-right: 12px;" @click="handleCancel"> 取消 </n-button>