Revert "feat: 活动管理与优惠券页面优化 - 活动管理步骤条改为自定义组件,匹配设计稿样式 - 优惠券列表金额筛选改为区间搜索,移除添加时间筛选 - 优惠券名称列改为可点击链接跳转详情 - 调整列顺序与宽度,优化表格展示"

This reverts commit c5c48320963542a83e79c6ff97ed438cd561b1d8.
6个文件已修改
347 ■■■■■ 已修改文件
package-lock.json 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/active/activityManagement/add.vue 144 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/active/activityManagement/components/CouponSelectModal.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/active/coupon/add.vue 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/active/coupon/index.vue 140 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package-lock.json
@@ -1,12 +1,12 @@
{
  "name": "xp-robotaxi-management",
  "version": "1.2.5",
  "version": "1.2.11",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "xp-robotaxi-management",
      "version": "1.2.5",
      "version": "1.2.11",
      "license": "MIT",
      "dependencies": {
        "@vueuse/core": "^13.6.0",
package.json
@@ -55,6 +55,7 @@
    "simple-git-hooks": "^2.13.1",
    "typescript": "^5.8.3",
    "unocss": "^66.3.3",
    "@unocss/vite": "^66.3.3",
    "unplugin-auto-import": "^19.3.0",
    "unplugin-icons": "^22.2.0",
    "unplugin-vue-components": "^28.8.0",
src/views/userContent/active/activityManagement/add.vue
@@ -19,6 +19,8 @@
  NRadioGroup,
  NSelect,
  NSpace,
  NStep,
  NSteps,
  NSwitch,
  NTag,
  NTimePicker,
@@ -168,24 +170,6 @@
const activeTab = ref(1)
const isEditMode = computed(() => route.query.mode === 'edit' && !!route.query.id)
// 步骤条:naive-ui 的 NSteps 是「图标在左、标题在右」且圈内带序号,与设计稿的
// 「图标居中在上、标题在下」对不上,深度覆盖其内部结构不如自绘可控,故手写一层。
const stepList = [
  { value: 1, title: '活动介绍内容' },
  { value: 2, title: '活动策略' },
  { value: 3, title: '发放用户' },
  { value: 4, title: '通知方式' },
  { value: 5, title: '信息确认' },
]
function stepState(value: number) {
  if (activeTab.value > value)
    return 'finish'
  if (activeTab.value === value)
    return 'process'
  return 'wait'
}
interface UsagePeriodItem {
  startTime: string | null
@@ -1478,36 +1462,13 @@
<template>
  <div class=" w-full h-full relative content flex flex-col">
    <div class="pb-12px main flex flex-col flex-1 overflow-hidden">
      <div class="custom-steps">
        <div class="steps-inner">
          <template v-for="(item, index) in stepList" :key="item.value">
            <div class="step-item" :class="`step-item--${stepState(item.value)}`">
              <span class="step-icon">
                <svg v-if="stepState(item.value) === 'finish'" class="step-check" viewBox="0 0 14 10">
                  <path
                    d="M1 5L5 9L13 1" fill="none" stroke="currentColor" stroke-width="2"
                    stroke-linecap="round" stroke-linejoin="round"
                  />
                </svg>
                <svg v-else-if="stepState(item.value) === 'process'" class="step-clock" viewBox="0 0 16 16">
                  <circle cx="8" cy="8" r="8" fill="currentColor" />
                  <path
                    d="M8 4.2V8.3L10.6 9.8" fill="none" stroke="#fff" stroke-width="1.4"
                    stroke-linecap="round" stroke-linejoin="round"
                  />
                </svg>
                <span v-else class="step-dot" />
              </span>
              <span class="step-title">{{ item.title }}</span>
            </div>
            <span
              v-if="index < stepList.length - 1"
              class="step-line"
              :class="{ 'step-line--done': activeTab > item.value }"
            />
          </template>
        </div>
      </div>
      <NSteps :current="activeTab" size="small" class="custom-steps">
        <NStep title="活动介绍内容" />
        <NStep title="活动策略配置" />
        <NStep title="发放用户" />
        <NStep title="通知方式" />
        <NStep title="信息确认" />
      </NSteps>
      <div class="box flex-1 mx-12px">
        <!-- 活动介绍内容表单 -->
        <NForm
@@ -1977,7 +1938,7 @@
            <NCheckboxGroup v-model:value="notifyForm.noticeType">
              <NSpace item-style="display: flex; align-items: center;" :size="16">
                <NCheckbox :value="1" label="微信消息通知" />
                <NCheckbox v-if="false" :value="2" label="短信通知" />
                <NCheckbox :value="2" v-if="false" label="短信通知" />
                <NCheckbox :value="4" label="小程序弹窗通知" />
              </NSpace>
            </NCheckboxGroup>
@@ -2370,8 +2331,7 @@
  border-radius: 6px;
  width: 100%;
  box-sizing: border-box;
  /* 步骤条是 position: fixed 不占位,这里要手动让出它的 84px 高度再留 12px 间距 */
  margin-top: 96px;
  margin-top: 50px;
  --n-label-text-color: rgba(23, 31, 38, 0.8);
  :deep(.n-form-item-label) {
@@ -2394,82 +2354,10 @@
.custom-steps {
  position: fixed;
  width: 100%;
  height: 84px;
  background-color: #fff;
  background-color: rgb(244, 246, 248);
  z-index: 999;
  padding: 18px 12px 0 12px;
  padding: 12px 12px 0 12px;
  box-sizing: border-box;
}
.steps-inner {
  display: flex;
  align-items: flex-start;
  justify-content: center;
}
.step-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0 24px;
}
.step-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
}
.step-clock {
  width: 16px;
  height: 16px;
  color: #0068ff;
}
.step-check {
  width: 14px;
  height: 10px;
  color: #0068ff;
}
.step-dot {
  width: 16px;
  height: 16px;
  border: 2px solid #d9d9d9;
  border-radius: 50%;
  box-sizing: border-box;
}
.step-title {
  margin-top: 14px;
  font-size: 14px;
  line-height: 20px;
  color: #d9d9d9;
  white-space: nowrap;
}
.step-item--finish .step-title {
  color: #0068ff;
}
.step-item--process .step-title {
  color: #0068ff;
  font-weight: 500;
}
/* 连接线落在图标与标题之间的空隙里,已完成段只换色、不加粗 */
.step-line {
  flex: none;
  width: 80px;
  height: 2px;
  margin-top: 21px;
  background-color: #d9d9d9;
}
.step-line--done {
  background-color: #0068ff;
}
.confirm-block {
@@ -2527,6 +2415,12 @@
  word-break: break-all;
}
.custom-tabs {
  position: fixed;
  background-color: rgb(244, 246, 248);
  z-index: 999;
  padding: 12px 12px 0 12px;
}
:deep(.n-upload-trigger--image-card) {
  width: 56px !important;
  height: 56px;
src/views/userContent/active/activityManagement/components/CouponSelectModal.vue
@@ -86,13 +86,13 @@
  return amountText
}
// 使用期限:文案与优惠券列表保持一致
// 使用期限:按券自身的生效/失效规则展示
function formatValidityDisplay(row: any) {
  const days = Number(row.expireRulesDays)
  if (!days || Number.isNaN(days) || days <= 0)
    return '—'
  const effectiveText = Number(row.effectiveRules) === 2 ? '领取后次日0点生效' : '领取后生效'
  return `${effectiveText}-有效天数${days}天`
  const effectiveText = Number(row.effectiveRules) === 2 ? '领取次日0点生效' : '领取后生效'
  return `${effectiveText},生效后${days}天内有效`
}
function handleSearch() {
src/views/userContent/active/coupon/add.vue
@@ -53,12 +53,6 @@
  return Number(formModel.value.effectiveRules) === 2 ? '领取后次日0点生效' : '领取后生效'
})
const pageTitle = computed(() => {
  if (isReadonly.value)
    return '优惠券详情'
  return isEditMode.value ? '编辑优惠券' : '添加优惠券'
})
const couponTypeOptions = [
  { label: '折扣券', value: 1 },
  { label: '满减券', value: 2 },
@@ -526,9 +520,6 @@
  <div class="w-full h-full relative content">
    <div class="p-12px">
      <div class="box mb-12px" :class="{ 'full-height': isReadonly }">
        <div class="head">
          {{ pageTitle }}
        </div>
        <n-form
          ref="formRef"
          :model="formModel"
@@ -660,11 +651,11 @@
              </n-form-item>
            </div>
            <div class="form-row full-width">
              <n-form-item label="生效规则" path="effectiveRules" :required="!isReadonly">
            <div class="form-row">
              <n-form-item label="生效规则" path="effectiveRules" :required="!isReadonly" style="width: 400px;">
                <template v-if="!isReadonly">
                  <n-radio-group v-model:value="formModel.effectiveRules">
                    <n-space :size="48">
                    <n-space>
                      <n-radio v-for="item in effectiveRulesOptions" :key="item.value" :value="item.value">
                        {{ item.label }}
                      </n-radio>
@@ -673,36 +664,21 @@
                </template>
                <span v-else class="detail-text">{{ effectiveRulesLabel }}</span>
              </n-form-item>
            </div>
            <div class="form-row full-width">
              <n-form-item label="失效规则" :required="!isReadonly">
              <n-form-item label="失效规则" path="expireRulesDays" :required="!isReadonly" style="width: 400px;">
                <template v-if="!isReadonly">
                  <!-- 目前只有「指定有效天数」一种失效规则,单选项恒选中,不需要额外的表单字段承载 -->
                  <n-radio-group :value="1">
                    <n-radio :value="1">
                      指定有效天数
                    </n-radio>
                  </n-radio-group>
                </template>
                <span v-else class="detail-text">指定有效天数</span>
              </n-form-item>
            </div>
            <div class="form-row sub-form-row">
              <n-form-item label="有效天数" path="expireRulesDays" :required="!isReadonly">
                <template v-if="!isReadonly">
                  生效后
                  <n-input
                    :value="formModel.expireRulesDays"
                    placeholder="请输入天数"
                    clearable
                    style="width: 130px;"
                    style="width: 130px;margin: 0 8px;"
                    :input-props="{ inputmode: 'numeric' }"
                    @update:value="handleUpdateExpireRulesDays"
                  />
                  <span class="unit-text">天</span>
                  天失效
                </template>
                <span v-else class="detail-text">{{ formModel.expireRulesDays ? `${formModel.expireRulesDays} 天` : '—' }}</span>
                <span v-else class="detail-text">{{ formModel.expireRulesDays ? `生效后${formModel.expireRulesDays}天失效` : '—' }}</span>
              </n-form-item>
            </div>
@@ -710,7 +686,7 @@
              <n-form-item label="优惠券说明" path="description" :required="!isReadonly" label-placement="top">
                <div class="w-full rich-text-wrapper">
                  <template v-if="!isReadonly">
                    <AiRichTextEditor v-model="formModel.description" height="300px" :max-length="200" />
                    <AiRichTextEditor v-model="formModel.description" height="300px" />
                  </template>
                  <div v-else class="rich-text-content" v-html="formModel.description" />
                </div>
@@ -854,16 +830,6 @@
  .n-form-item {
    width: 100%;
  }
}
/* 「有效天数」是「失效规则」的下级配置项,缩进对齐到上一行单选项的位置 */
.form-row.sub-form-row {
  padding-left: 72px;
}
.unit-text {
  margin-left: 8px;
  color: rgba(23, 31, 38, 0.8);
}
:deep(.n-form-item-label--left-mark){
  width:auto !important;
src/views/userContent/active/coupon/index.vue
@@ -21,21 +21,10 @@
  name: '',
  couponType: null,
  status: null,
  conditionalAmountMin: '',
  conditionalAmountMax: '',
  amountMin: '',
  amountMax: '',
  conditionalAmount: '',
  amount: '',
  createTimeRange: null as [number, number] | null,
})
type AmountField = 'conditionalAmountMin' | 'conditionalAmountMax' | 'amountMin' | 'amountMax'
// 空值要转成 undefined 而不能落成 0,否则只填下限时上限会变成「≤ 0」把结果全部滤空
function toAmountParam(value: string) {
  if (value === '')
    return undefined
  const numberValue = Number(value)
  return Number.isNaN(numberValue) ? undefined : numberValue
}
const isCouponTypeOpen = ref(false)
const isStatusOpen = ref(false)
@@ -52,13 +41,13 @@
  3: '失效',
}
// 使用期限:按「生效规则-失效规则」拼接,术语与建券表单的「有效天数」保持一致;存量券未配置时展示占位符
// 使用期限:按券自身的失效规则展示,存量券未配置时展示占位符
function formatValidityDisplay(row: any) {
  const days = Number(row.expireRulesDays)
  if (!days || Number.isNaN(days) || days <= 0)
    return '—'
  const effectiveText = Number(row.effectiveRules) === 2 ? '领取后次日0点生效' : '领取后生效'
  return `${effectiveText}-有效天数${days}天`
  const effectiveText = Number(row.effectiveRules) === 2 ? '领取次日0点生效' : '领取后生效'
  return `${effectiveText},生效后${days}天内有效`
}
const loading = ref(false)
@@ -104,7 +93,7 @@
  return amountText
}
function handleAmountInput(field: AmountField, value: string) {
function handleAmountInput(field: 'conditionalAmount' | 'amount', value: string) {
  // 仅保留数字和一个小数点,且最多两位小数。
  const normalizedValue = value
    .replace(/[^\d.]/g, '')
@@ -115,28 +104,6 @@
    .replace('$#$', '.')
    .replace(/^(\d+)\.(\d{0,2}).*$/, '$1.$2')
  searchModel.value[field] = normalizedValue
}
// 区间筛选要塞进一个栅格列宽,两个输入框均分剩余宽度,分隔符和单位不参与伸缩
function renderAmountRange(minField: AmountField, maxField: AmountField) {
  return (
    <div class="flex items-center gap-8px w-full">
      <n-input
        class="flex-1 min-w-0"
        value={searchModel.value[minField]}
        placeholder=""
        onUpdateValue={(value: string) => handleAmountInput(minField, value)}
      />
      <span class="flex-none text-[rgba(23,31,38,0.6)]">~</span>
      <n-input
        class="flex-1 min-w-0"
        value={searchModel.value[maxField]}
        placeholder=""
        onUpdateValue={(value: string) => handleAmountInput(maxField, value)}
      />
      <span class="flex-none text-[rgba(23,31,38,0.6)]">元</span>
    </div>
  )
}
function handleSearch() {
@@ -150,10 +117,9 @@
    name: '',
    couponType: null,
    status: null,
    conditionalAmountMin: '',
    conditionalAmountMax: '',
    amountMin: '',
    amountMax: '',
    conditionalAmount: '',
    amount: '',
    createTimeRange: null,
  }
  currentPage.value = 1
  getList()
@@ -299,15 +265,18 @@
async function getList() {
  loading.value = true
  try {
    const [createStartTime, createEndTime] = searchModel.value.createTimeRange || []
    const { code, data } = await fetchCouponList({
      code: searchModel.value.code?.trim() || undefined,
      couponName: searchModel.value.name?.trim() || undefined,
      couponType: searchModel.value.couponType,
      status: searchModel.value.status,
      conditionalAmountMin: toAmountParam(searchModel.value.conditionalAmountMin),
      conditionalAmountMax: toAmountParam(searchModel.value.conditionalAmountMax),
      amountMin: toAmountParam(searchModel.value.amountMin),
      amountMax: toAmountParam(searchModel.value.amountMax),
      conditionalAmountMax: searchModel.value.conditionalAmount ? Number(searchModel.value.conditionalAmount) : undefined,
      conditionalAmountMin: searchModel.value.conditionalAmount ? Number(searchModel.value.conditionalAmount) : undefined,
      amountMin: searchModel.value.amount ? Number(searchModel.value.amount) : undefined,
      amountMax: searchModel.value.amount ? Number(searchModel.value.amount) : undefined,
      createTimeStart: createStartTime ? dayjs(createStartTime).startOf('day').format('YYYY-MM-DD HH:mm:ss') : undefined,
      createTimeEnd: createEndTime ? dayjs(createEndTime).endOf('day').format('YYYY-MM-DD HH:mm:ss') : undefined,
      pageNum: currentPage.value,
      pageSize: pageSize.value,
    })
@@ -363,16 +332,6 @@
    ),
  },
  {
    label: '条件金额',
    path: 'conditionalAmount',
    render: () => renderAmountRange('conditionalAmountMin', 'conditionalAmountMax'),
  },
  {
    label: '面额/折扣',
    path: 'amount',
    render: () => renderAmountRange('amountMin', 'amountMax'),
  },
  {
    label: '状态',
    path: 'status',
    render: () => (
@@ -400,25 +359,55 @@
      </NSelect>
    ),
  },
  {
    label: '条件金额',
    path: 'conditionalAmount',
    render: () => (
      <n-input
        value={searchModel.value.conditionalAmount}
        placeholder="请输入"
        clearable
        onUpdateValue={value => handleAmountInput('conditionalAmount', value)}
      />
    ),
  },
  {
    label: '面额/折扣',
    path: 'amount',
    render: () => (
      <n-input
        value={searchModel.value.amount}
        placeholder="请输入"
        clearable
        onUpdateValue={value => handleAmountInput('amount', value)}
      />
    ),
  },
  {
    label: '添加时间',
    path: 'createTimeRange',
    render: () => (
      <n-date-picker
        v-model:value={searchModel.value.createTimeRange}
        type="daterange"
        clearable
        format="yyyy/MM/dd"
        start-placeholder="开始时间"
        end-placeholder="结束时间"
        actions={[]}
        update-value-on-close
        close-on-select
      />
    ),
  },
])
const columns = computed<DataTableColumns<any>>(() => [
  { type: 'selection', width: 50, align: 'center', fixed: 'left' },
  { title: '序号', key: 'id', width: 60, align: 'left', resizable: true, render: (_, index) => (currentPage.value - 1) * pageSize.value + index + 1 },
  { title: '添加时间', key: 'createTime', align: 'left', width: 180, ellipsis: true, resizable: true, render: row => row.createTime ? dayjs(row.createTime).format('YYYY/MM/DD HH:mm') : '-' },
  { title: '优惠券编码', key: 'code', align: 'left', width: 200, ellipsis: true, resizable: true, render: row => row.id ? String(row.id) : '-' },
  {
    title: '优惠券名称',
    key: 'couponName',
    align: 'left',
    width: 260,
    ellipsis: true,
    resizable: true,
    // 名称做成入口链接,点击直达详情;.n-button 已在行点击忽略名单里,不会连带勾选当前行
    render: row => (
      <NButton text type="primary" onClick={() => handleDetail(row)}>
        {row.couponName || '-'}
      </NButton>
    ),
  },
  { title: '优惠券名称', key: 'couponName', align: 'left', width: 260, ellipsis: true, resizable: true },
  {
    title: '优惠券类型',
    key: 'couponType',
@@ -444,9 +433,9 @@
    render: row => <span>{formatCouponAmountDisplay(row)}</span>,
  },
  {
    title: '优惠券使用期限',
    title: '使用期限',
    key: 'expireRulesDays',
    width: 220,
    width: 200,
    align: 'left',
    ellipsis: true,
    resizable: true,
@@ -466,7 +455,6 @@
      )
    },
  },
  { title: '添加时间', key: 'createTime', align: 'left', width: 180, ellipsis: true, resizable: true, render: row => row.createTime ? dayjs(row.createTime).format('YYYY/MM/DD HH:mm') : '-' },
  {
    title: '操作',
    key: 'actions',
@@ -614,7 +602,7 @@
          :row-class-name="getRowClassName"
          :row-key="(row: any) => row.id"
          flex-height
          :scroll-x="1610"
          :scroll-x="1700"
          @update:checked-row-keys="handleCheck"
        >
          <template #empty>