zjk
2026-04-25 28776e605a707041036fe828eb99ecb5281fd1ae
Merge branch 'deploy-1.0' of http://120.76.84.145:10101/gitblit/r/H5/xp-robotaxi-web into deploy-1.0
4个文件已添加
9个文件已修改
1732 ■■■■■ 已修改文件
src/service/api/operations.ts 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/active/activityManagement/add.vue 769 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/active/activityManagement/components/ActivityInfo.vue 301 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/active/activityManagement/components/CouponSelectModal.vue 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/active/activityManagement/detail.vue 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/active/activityManagement/index.vue 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/active/activityManagement/new.txt 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/active/activityManagement/notice.txt 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/active/activityManagement/user.txt 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/contentCenter/api.txt 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/contentCenter/components/AgreementListForm.vue 249 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/contentCenter/opsApp.vue 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/contentCenter/passenger.vue 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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>