zjk
2026-06-09 2daad1263b7c67444e033fedb233f43a0c2ca3e3
feat: 新增协议版本管理并优化多业务功能

新增协议版本管理相关API、路由与页面组件
重构用户内容中心协议列表页,改用独立版本管理页面替代内嵌弹窗
优化评价标签管理功能,对接后端分页接口并完善表单校验
修复定价管理页面特殊价格字段命名不统一问题
优化内容页面滚动样式与手机预览效果
新增评价弹窗状态更新接口
2个文件已添加
11个文件已修改
1946 ■■■■■ 已修改文件
src/router/routes.static.ts 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/service/api/evaluationTag.ts 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/service/api/operations.ts 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/service/api/system.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/evaluationTag/components/TagFormModal.vue 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/evaluationTag/index.vue 341 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/transaction/pricingManagement/detail.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/contentCenter/agreementVersion.vue 798 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/contentCenter/components/AgreementListForm.vue 226 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/contentCenter/components/AgreementVersionModal.vue 167 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/contentCenter/components/ContentForm.vue 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/contentCenter/opsApp.vue 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/contentCenter/passenger.vue 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/routes.static.ts
@@ -497,6 +497,21 @@
    fullScreen: true,
  },
  {
    name: 'passengerContentAgreementVersion',
    path: '/userContent/contentCenter/passenger/agreementVersion',
    title: '协议版本管理',
    requiresAuth: true,
    icon: 'icon-park-outline:file-text',
    menuType: 'page',
    componentPath: '/userContent/contentCenter/agreementVersion.vue',
    id: 2026429021,
    keepAlive: false,
    pid: 302,
    hide: true,
    activeMenu: '/userContent/contentCenter/passenger',
    fullScreen: true,
  },
  {
    name: 'opsAppContent',
    path: '/userContent/contentCenter/opsApp',
    title: '运维APP内容管理',
@@ -510,6 +525,21 @@
    fullScreen: true,
  },
  {
    name: 'opsAppContentAgreementVersion',
    path: '/userContent/contentCenter/opsApp/agreementVersion',
    title: '协议版本管理',
    requiresAuth: true,
    icon: 'icon-park-outline:file-text',
    menuType: 'page',
    componentPath: '/userContent/contentCenter/agreementVersion.vue',
    id: 2026429041,
    keepAlive: false,
    pid: 302,
    hide: true,
    activeMenu: '/userContent/contentCenter/opsApp',
    fullScreen: true,
  },
  {
    name: 'contentCenterActive',
    path: '/userContent/active',
    title: '活动',
src/service/api/evaluationTag.ts
@@ -1,3 +1,4 @@
import { request } from '../http'
import dayjs from 'dayjs'
export type EvaluationDimensionKey = 'satisfied' | 'normal' | 'unsatisfied'
@@ -9,6 +10,31 @@
  createTime: string
  updateTime: string
  status: 0 | 1
  sortOrder: number
}
export interface EvaluationTagPageQuery {
  evaluateDimension: 1 | 2 | 3
  pageNum: number
  pageSize: number
  status?: 0 | 1
  tagContent?: string
}
export interface SaveOrUpdateEvaluationTagData {
  evaluateDimension: 1 | 2 | 3
  id?: number | string
  status: 0 | 1
  tagContent: string
}
export interface UpdateEvaluationTagStatusData {
  id: number | string
  status: 0 | 1
}
export interface UpdateEvaluationTagSortItem {
  id: number | string
  sortOrder: number
}
@@ -53,7 +79,7 @@
    satisfied: {
      key: 'satisfied',
      label: '满意',
      maxEnabledCount: 5,
      maxEnabledCount: 8,
      tags: [
        createTag('sat_1', '车内干净整洁', '刘凌婷', '2021-07-29 10:59', 1, 1),
        createTag('sat_2', '乘车体验非常舒适', '黄文雯', '2021-07-29 10:59', 1, 2),
@@ -64,7 +90,7 @@
    normal: {
      key: 'normal',
      label: '一般',
      maxEnabledCount: 5,
      maxEnabledCount: 8,
      tags: [
        createTag('nor_1', '等待时间略长', '刘凌婷', '2021-07-29 10:59', 1, 1),
        createTag('nor_2', '车内温度一般', '黄文雯', '2021-07-29 10:59', 1, 2),
@@ -74,7 +100,7 @@
    unsatisfied: {
      key: 'unsatisfied',
      label: '不满意',
      maxEnabledCount: 5,
      maxEnabledCount: 8,
      tags: [
        createTag('uns_1', '车内有异味', '刘凌婷', '2021-07-29 10:59', 1, 1),
        createTag('uns_2', '等待时间过长', '黄文雯', '2021-07-29 10:59', 1, 2),
@@ -119,3 +145,41 @@
    data: cloneConfig(mockEvaluationTagConfig),
  } as Service.ResponseResult<EvaluationTagConfig>
}
export function fetchEvaluationTagPageList(data: EvaluationTagPageQuery) {
  return request.Post<Service.ResponseResult<{
    current: number
    hasNextPage: boolean
    hasPrevPage: boolean
    pages: number
    records: Array<{
      createBy: string
      createTime: string
      deleted: boolean
      evaluateDimension: number
      evaluateDimensionName: string
      id: number | string
      sortOrder: number
      status: 0 | 1
      statusName: string
      tagContent: string
      updateBy: string
      updateTime: string
    }>
    size: number
    startIndex: number
    total: number
  }>>('/operation/api/v1/evaluateTag/pageList', data)
}
export function saveOrUpdateEvaluationTag(data: SaveOrUpdateEvaluationTagData) {
  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/evaluateTag/saveOrUpdate', data)
}
export function updateEvaluationTagStatus(data: UpdateEvaluationTagStatusData) {
  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/evaluateTag/updateStatus', data)
}
export function updateEvaluationTagSort(data: UpdateEvaluationTagSortItem[]) {
  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/evaluateTag/updateSort', data)
}
src/service/api/operations.ts
@@ -6,8 +6,10 @@
  id?: number | string
  name: string
  content: string
  sort: number
  type: number
  version: string
  status: number
  sort?: number
}) {
  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/agreement/saveLoginAgreement', data)
}
@@ -28,8 +30,73 @@
  }>>>(`/operation/api/v1/agreement/getLoginAgreementPageList/${type}`)
}
export function getLoginAgreementVersionPageList(
  nameId: number | string,
  params: {
    pageNum: number
    pageSize: number
    type: number
  },
) {
  return request.Get<Service.ResponseResult<{
    current: number
    pages: number
    records: Array<{
      id: number
      content: string
      createBy: string
      createTime: string
      name: string
      sort: number
      status: number
      type: number
      updateBy: string
      updateTime: string
      version: string
    }>
    size: number
    total: number
  }>>(`/operation/api/v1/agreement/getLoginAgreementVersionPageList/${nameId}`, { params })
}
export function getLoginAgreementLatestVersion(nameId: number | string) {
  return request.Get<Service.ResponseResult<string>>(`/operation/api/v1/agreement/loginAgreement/latestVersion/${nameId}`)
}
export function getLoginAgreementDetail(id: number | string) {
  return request.Get<Service.ResponseResult<{
    id: number
    content: string
    createBy: string
    createTime: string
    name: string
    sort: number
    status: number
    type: number
    updateBy: string
    updateTime: string
    version: string
  }>>(`/operation/api/v1/agreement/loginAgreement/detail/${id}`)
}
export function deleteLoginAgreement(id: number | string) {
  return request.Delete<Service.ResponseResult<any>>(`/operation/api/v1/agreement/deleteLoginAgreement/${id}`)
}
export function enableLoginAgreement(id: number | string) {
  return request.Post<Service.ResponseResult<any>>(`/operation/api/v1/agreement/enableLoginAgreement/${id}`)
}
export function disableLoginAgreement(id: number | string) {
  return request.Post<Service.ResponseResult<any>>(`/operation/api/v1/agreement/disableLoginAgreement/${id}`)
}
export function deleteLoginAgreementVersion(id: number | string) {
  return request.Post<Service.ResponseResult<any>>(`/operation/api/v1/agreement/deleteLoginAgreementVersion/${id}`)
}
export function copyLoginAgreement(id: number | string) {
  return request.Post<Service.ResponseResult<any>>(`/operation/api/v1/agreement/copyLoginAgreement/${id}`)
}
/** 开通城市接口 */
@@ -104,12 +171,12 @@
// 校验围栏运营策略缩短影响
export function validateFenceOperationStrategyShorten(params: {
  id: number | string;
  operationStrategy: string;
  id: number | string
  operationStrategy: string
}) {
  return request.Post<Service.ResponseResult<any>>(
    '/map/api/v1/electronicFence/validateOperationStrategyShorten',
    params
    params,
  )
}
src/service/api/system.ts
@@ -34,3 +34,8 @@
export function saveSystemSetting(data: any) {
  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/systemSetting/save', data)
}
/** 修改评价弹窗开关 */
export function updateEvaluatePopupStatus(data: { evaluatePopupStatus: 0 | 1 }) {
  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/systemSetting/updateEvaluatePopupStatus', data)
}
src/views/system/evaluationTag/components/TagFormModal.vue
@@ -1,18 +1,32 @@
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue'
import type { FormInst, FormRules } from 'naive-ui'
import { NButton, NForm, NFormItem, NInput, NModal, NSpace } from 'naive-ui'
import { NButton, NForm, NFormItem, NInput, NModal, NSelect, NSpace, NSwitch } from 'naive-ui'
import type { EvaluationDimensionKey } from '@/service/api/evaluationTag'
interface TagFormValue {
export interface TagFormValue {
  labelType: EvaluationDimensionKey | null
  labelContent: string
  status: 0 | 1
}
interface TagTypeOption {
  label: string
  value: EvaluationDimensionKey
  maxEnabledCount: number
}
const props = withDefaults(defineProps<{
  visible: boolean
  title?: string
  rowData?: Partial<TagFormValue> | null
  typeOptions?: TagTypeOption[]
  saving?: boolean
}>(), {
  title: '新增评价标签',
  title: '新增标签',
  rowData: null,
  typeOptions: () => [],
  saving: false,
})
const emit = defineEmits<{
@@ -22,18 +36,29 @@
const formRef = ref<FormInst | null>(null)
const formModel = reactive<TagFormValue>({
  labelType: null,
  labelContent: '',
  status: 1,
})
const currentTypeMaxEnabledCount = computed(() => {
  return props.typeOptions.find(item => item.value === formModel.labelType)?.maxEnabledCount || 0
})
const rules: FormRules = {
  labelType: [
    { required: true, message: '请选择标签类型', trigger: ['change', 'blur'] },
  ],
  labelContent: [
    { required: true, message: '请输入标签内容', trigger: ['blur', 'input'] },
    { max: 20, message: '标签内容最多20个字符', trigger: ['blur', 'input'] },
    { max: 10, message: '标签内容最多10个字符', trigger: ['blur', 'input'] },
  ],
}
function resetForm() {
  formModel.labelType = props.rowData?.labelType || props.typeOptions[0]?.value || null
  formModel.labelContent = props.rowData?.labelContent || ''
  formModel.status = Number(props.rowData?.status) === 0 ? 0 : 1
  formRef.value?.restoreValidation()
}
@@ -44,9 +69,10 @@
async function handleSubmit() {
  await formRef.value?.validate()
  emit('save', {
    labelType: formModel.labelType,
    labelContent: formModel.labelContent.trim(),
    status: formModel.status,
  })
  handleClose()
}
watch(() => props.visible, (visible) => {
@@ -63,7 +89,7 @@
    :title="title"
    style="width: 400px"
    :segmented="{ content: true, action: true }"
    :content-style="{ padding: '16px 24px 0 24px' }"
    :content-style="{ padding: '24px 36px 0 36px' }"
    @close="handleClose"
  >
    <NForm
@@ -73,13 +99,41 @@
      label-placement="top"
      require-mark-placement="left"
    >
      <NFormItem label="标签内容" path="labelContent" required>
      <NFormItem label="标签类型" path="labelType" required class="mb-8px">
        <NSelect
          v-model:value="formModel.labelType"
          :options="typeOptions"
          label-field="label"
          value-field="value"
          placeholder="请选择"
        />
      </NFormItem>
      <NFormItem label="标签内容" path="labelContent" required class="mb-8px">
        <NInput
          v-model:value="formModel.labelContent"
          placeholder="请输入"
          maxlength="20"
          maxlength="10"
          show-count
          clearable
        />
      </NFormItem>
      <NFormItem path="status" required label-placement="left">
        <template #label>
          <div class="status-label">
            <span>标签状态</span>
            <span class="status-limit-text">(最多可同时启用{{ currentTypeMaxEnabledCount }}个标签)</span>
          </div>
        </template>
        <NSwitch v-model:value="formModel.status" :checked-value="1" :unchecked-value="0">
          <template #checked>
            启用
          </template>
          <template #unchecked>
            停用
          </template>
        </NSwitch>
      </NFormItem>
    </NForm>
@@ -88,10 +142,26 @@
        <NButton class="w-84px" @click="handleClose">
          取消
        </NButton>
        <NButton type="primary" class="w-84px" @click="handleSubmit">
          保存
        <NButton type="primary" class="w-84px" :loading="saving" @click="handleSubmit">
          确定
        </NButton>
      </NSpace>
    </template>
  </NModal>
</template>
<style scoped lang="scss">
.status-label {
  display: inline-flex;
  align-items: center;
}
.status-limit-text {
  color: rgba(23, 31, 38, 0.6);
  font-size: 12px;
}
:deep(.n-switch) {
  margin-left: auto;
}
</style>
src/views/system/evaluationTag/index.vue
@@ -1,13 +1,16 @@
<script setup lang="tsx">
import type { DataTableColumns } from 'naive-ui'
import { computed, h, onMounted, reactive, ref } from 'vue'
import { computed, h, onMounted, reactive, ref, watch } from 'vue'
import { NButton, NTag, NTooltip, useMessage } from 'naive-ui'
import dayjs from 'dayjs'
import dragIcon from '@/assets/point/union.png'
import { createIcon } from '@/utils/icon'
import TagFormModal from './components/TagFormModal.vue'
import type { TagFormValue } from './components/TagFormModal.vue'
import type { EvaluationDimensionConfig, EvaluationDimensionKey, EvaluationTagConfig, EvaluationTagItem } from '@/service/api/evaluationTag'
import { fetchEvaluationTagConfig, saveEvaluationTagConfig } from '@/service/api/evaluationTag'
import { fetchEvaluationTagPageList, saveEvaluationTagConfig, saveOrUpdateEvaluationTag, updateEvaluationTagSort, updateEvaluationTagStatus } from '@/service/api/evaluationTag'
import { fetchSystemSetting, updateEvaluatePopupStatus } from '@/service/api/system'
import Pagination from '@/components/common/Pagination.vue'
import SearchPanel from '@/components/custom/SearchPanel.vue'
interface FilterState {
@@ -32,6 +35,7 @@
const loading = ref(false)
const saving = ref(false)
const tagModalSaving = ref(false)
const dirty = ref(false)
const activeTab = ref<EvaluationDimensionKey>('satisfied')
const configData = ref<EvaluationTagConfig | null>(null)
@@ -43,7 +47,23 @@
const draggingTagId = ref<string | null>(null)
const dragOverTagId = ref<string | null>(null)
const editingRowData = ref<Partial<EvaluationTagItem> | null>(null)
type TagFormRowData = Partial<TagFormValue>
const editingRowData = ref<TagFormRowData | null>(null)
const loadedTabs = reactive<Record<EvaluationDimensionKey, boolean>>({
  satisfied: false,
  normal: false,
  unsatisfied: false,
})
const pageStateMap = reactive<Record<EvaluationDimensionKey, {
  pageNum: number
  pageSize: number
  total: number
}>>({
  satisfied: { pageNum: 1, pageSize: 10, total: 0 },
  normal: { pageNum: 1, pageSize: 10, total: 0 },
  unsatisfied: { pageNum: 1, pageSize: 10, total: 0 },
})
const inputFilters = reactive<Record<EvaluationDimensionKey, FilterState>>({
  satisfied: createFilterState(),
@@ -68,8 +88,22 @@
  { label: '不满意', value: 'unsatisfied' },
]
const tagTypeOptions = computed(() => {
  if (!configData.value) {
    return tabOptions.map(item => ({
      ...item,
      maxEnabledCount: 0,
    }))
  }
  return tabOptions.map(item => ({
    ...item,
    maxEnabledCount: configData.value?.dimensions[item.value]?.maxEnabledCount || 0,
  }))
})
const currentInputFilter = computed(() => inputFilters[activeTab.value])
const currentAppliedFilter = computed(() => appliedFilters[activeTab.value])
const currentPageState = computed(() => pageStateMap[activeTab.value])
const currentDimensionConfig = computed<EvaluationDimensionConfig | null>(() => {
  return configData.value?.dimensions?.[activeTab.value] || null
@@ -114,11 +148,13 @@
function handleSearch() {
  applyCurrentFilter()
  void loadTabList(activeTab.value, true)
}
function handleResetSearch() {
  resetFilterState(inputFilters[activeTab.value])
  resetFilterState(appliedFilters[activeTab.value])
  void loadTabList(activeTab.value, true)
}
function normalizeConfig(config: EvaluationTagConfig) {
@@ -133,22 +169,105 @@
  return config
}
function createEmptyConfig(): EvaluationTagConfig {
  return {
    popupEnabled: 0,
    dimensions: {
      satisfied: {
        key: 'satisfied',
        label: '满意',
        maxEnabledCount: 8,
        tags: [],
      },
      normal: {
        key: 'normal',
        label: '一般',
        maxEnabledCount: 8,
        tags: [],
      },
      unsatisfied: {
        key: 'unsatisfied',
        label: '不满意',
        maxEnabledCount: 8,
        tags: [],
      },
    },
  }
}
function getEvaluateDimensionValue(dimensionKey: EvaluationDimensionKey): 1 | 2 | 3 {
  if (dimensionKey === 'satisfied')
    return 1
  if (dimensionKey === 'normal')
    return 2
  return 3
}
function isSuccessCode(code?: number) {
  return code === 0 || code === 200
}
function formatDateTime(value?: string) {
  if (!value)
    return ''
  const parsedValue = dayjs(value)
  return parsedValue.isValid() ? parsedValue.format('YYYY-MM-DD HH:mm:ss') : value
}
function mapPageRecord(item: any): EvaluationTagItem {
  return {
    id: String(item.id),
    labelContent: item.tagContent || '',
    createBy: item.createBy || item.updateBy || '',
    createTime: formatDateTime(item.createTime),
    updateTime: formatDateTime(item.updateTime),
    status: Number(item.status) === 1 ? 1 : 0,
    sortOrder: Number(item.sortOrder || 0),
  }
}
async function loadTabList(dimensionKey = activeTab.value, resetToFirstPage = false) {
  if (!configData.value)
    return
  const pageState = pageStateMap[dimensionKey]
  if (resetToFirstPage)
    pageState.pageNum = 1
  const filterState = appliedFilters[dimensionKey]
  const res = await fetchEvaluationTagPageList({
    evaluateDimension: getEvaluateDimensionValue(dimensionKey),
    pageNum: pageState.pageNum,
    pageSize: pageState.pageSize,
    status: filterState.status === null ? undefined : filterState.status,
    tagContent: filterState.labelContent.trim() || undefined,
  })
  if (!isSuccessCode(res?.code) || !res.data) {
    return
  }
  configData.value.dimensions[dimensionKey].tags = (res.data.records || []).map(mapPageRecord)
  pageState.pageNum = Number(res.data.current || pageState.pageNum || 1)
  pageState.pageSize = Number(res.data.size || pageState.pageSize || 10)
  pageState.total = Number(res.data.total || 0)
  if (originalConfigData.value && !dirty.value) {
    originalConfigData.value.dimensions[dimensionKey].tags = cloneValue(configData.value.dimensions[dimensionKey].tags)
  }
  loadedTabs[dimensionKey] = true
}
async function loadConfig() {
  loading.value = true
  try {
    const res = await fetchEvaluationTagConfig()
    if (res.code !== 200 || !res.data) {
      message.error('标签数据加载失败,请刷新页面')
      return
    const systemRes = await fetchSystemSetting()
    const nextConfig = normalizeConfig(createEmptyConfig())
    if (systemRes?.code === 200 && systemRes.data) {
      nextConfig.popupEnabled = Number(systemRes.data.evaluatePopupStatus) === 1 ? 1 : 0
    }
    const nextConfig = normalizeConfig(cloneValue(res.data))
    configData.value = nextConfig
    originalConfigData.value = cloneValue(nextConfig)
    await loadTabList(activeTab.value, true)
    originalConfigData.value = cloneValue(configData.value)
    dirty.value = false
  }
  catch (error) {
    console.error('加载评价标签失败:', error)
    message.error('标签数据加载失败,请刷新页面')
  }
  finally {
    loading.value = false
@@ -160,6 +279,7 @@
    return
  configData.value = cloneValue(originalConfigData.value)
  dirty.value = false
  void loadTabList(activeTab.value)
}
async function handleSave() {
@@ -167,12 +287,20 @@
    return
  saving.value = true
  try {
    const res = await saveEvaluationTagConfig(cloneValue(configData.value))
    if (res.code !== 200 || !res.data) {
      message.error('配置保存失败,请检查网络后重试')
    const [tagRes, popupRes] = await Promise.all([
      saveEvaluationTagConfig(cloneValue(configData.value)),
      updateEvaluatePopupStatus({
        evaluatePopupStatus: Number(configData.value.popupEnabled) === 1 ? 1 : 0,
      }),
    ])
    if (tagRes.code !== 200 || !tagRes.data || popupRes.code !== 200) {
      return
    }
    const nextConfig = normalizeConfig(cloneValue(res.data))
    const nextConfig = normalizeConfig(cloneValue(tagRes.data))
    const latestSystemRes = await fetchSystemSetting()
    if (latestSystemRes?.code === 200 && latestSystemRes.data) {
      nextConfig.popupEnabled = Number(latestSystemRes.data.evaluatePopupStatus) === 1 ? 1 : 0
    }
    configData.value = nextConfig
    originalConfigData.value = cloneValue(nextConfig)
    dirty.value = false
@@ -180,7 +308,6 @@
  }
  catch (error) {
    console.error('保存评价标签配置失败:', error)
    message.error('配置保存失败,请检查网络后重试')
  }
  finally {
    saving.value = false
@@ -189,54 +316,94 @@
function handleAddTag() {
  editingTagId.value = null
  editingRowData.value = null
  modalTitle.value = '新增评价标签'
  editingRowData.value = {
    labelType: activeTab.value,
    labelContent: '',
    status: 1,
  }
  modalTitle.value = '新增标签'
  showTagModal.value = true
}
function handleEditTag(row: EvaluationTagItem) {
  editingTagId.value = row.id
  editingRowData.value = cloneValue(row)
  modalTitle.value = '编辑评价标签'
  editingRowData.value = {
    labelType: activeTab.value,
    labelContent: row.labelContent,
    status: row.status,
  }
  modalTitle.value = '编辑标签'
  showTagModal.value = true
}
function handleSaveTagForm(payload: { labelContent: string }) {
  const currentDimension = currentDimensionConfig.value
  if (!currentDimension)
function normalizeDimensionSort(dimensionKey: EvaluationDimensionKey) {
  const dimension = configData.value?.dimensions[dimensionKey]
  if (!dimension)
    return
  const now = dayjs().format('YYYY-MM-DD HH:mm')
  if (editingTagId.value) {
    currentDimension.tags = currentDimension.tags.map((item) => {
      if (item.id !== editingTagId.value)
        return item
      return {
        ...item,
        labelContent: payload.labelContent,
        updateTime: now,
      }
    })
  }
  else {
    currentDimension.tags.push({
      id: `${activeTab.value}_${Date.now()}`,
      labelContent: payload.labelContent,
      createBy: '当前用户',
      createTime: now,
      updateTime: now,
      status: 0,
      sortOrder: currentDimension.tags.length + 1,
    })
  }
  currentDimension.tags = currentDimension.tags.map((item, index) => ({
  dimension.tags = dimension.tags.map((item, index) => ({
    ...item,
    sortOrder: index + 1,
  }))
  markDirty()
}
function handleToggleStatus(row: EvaluationTagItem) {
function validateEnabledLimit(dimensionKey: EvaluationDimensionKey, status: 0 | 1, currentTagId?: string | null) {
  if (!configData.value || Number(status) !== 1)
    return true
  const targetDimension = configData.value.dimensions[dimensionKey]
  const enabledCount = targetDimension.tags.filter(item => Number(item.status) === 1 && item.id !== currentTagId).length
  if (enabledCount >= targetDimension.maxEnabledCount) {
    message.warning(`该评价维度下最多可同时启用 ${targetDimension.maxEnabledCount} 个标签,请先停用其他标签`)
    return false
  }
  return true
}
async function handleSaveTagForm(payload: TagFormValue) {
  if (!configData.value || !payload.labelType)
    return
  if (!validateEnabledLimit(payload.labelType, payload.status, editingTagId.value))
    return
  const sourceDimensionKey = editingRowData.value?.labelType || activeTab.value
  tagModalSaving.value = true
  try {
    const res = await saveOrUpdateEvaluationTag({
      evaluateDimension: getEvaluateDimensionValue(payload.labelType),
      id: editingTagId.value || undefined,
      status: payload.status,
      tagContent: payload.labelContent,
    })
    if (!isSuccessCode(res?.code)) {
      message.error(res?.msg || '保存失败')
      return
    }
    if (sourceDimensionKey !== payload.labelType)
      loadedTabs[sourceDimensionKey] = false
    activeTab.value = payload.labelType
    await loadTabList(payload.labelType, true)
    if (sourceDimensionKey !== payload.labelType) {
      await loadTabList(sourceDimensionKey, true)
      loadedTabs[sourceDimensionKey] = true
    }
    editingTagId.value = null
    editingRowData.value = null
    showTagModal.value = false
    message.success('保存成功')
  }
  catch (error) {
    console.error('保存评价标签失败:', error)
  }
  finally {
    tagModalSaving.value = false
  }
}
async function handleToggleStatus(row: EvaluationTagItem) {
  const currentDimension = currentDimensionConfig.value
  if (!currentDimension)
    return
@@ -244,44 +411,49 @@
  const nextStatus = Number(row.status) === 1 ? 0 : 1
  const enabledTotal = currentDimension.tags.filter(item => Number(item.status) === 1).length
  if (nextStatus === 1 && enabledTotal >= currentDimension.maxEnabledCount) {
    message.warning(`每个评价维度最多可同时启用 ${currentDimension.maxEnabledCount} 个标签,请先停用其他标签`)
    message.warning(`该评价维度下最多可同时启用 ${currentDimension.maxEnabledCount} 个标签,请先停用其他标签`)
    return
  }
  currentDimension.tags = currentDimension.tags.map((item) => {
    if (item.id !== row.id)
      return item
    return {
      ...item,
  try {
    const res = await updateEvaluationTagStatus({
      id: row.id,
      status: nextStatus as 0 | 1,
      updateTime: dayjs().format('YYYY-MM-DD HH:mm'),
    })
    if (!isSuccessCode(res?.code)) {
      message.error(res?.msg || '状态更新失败')
      return
    }
  })
  markDirty()
    await loadTabList(activeTab.value)
    message.success(nextStatus === 1 ? '启用成功' : '停用成功')
  }
  catch (error) {
    console.error('更新评价标签状态失败:', error)
  }
}
function reorderCurrentTabList(sourceId: string, targetId: string) {
function getReorderedTagList(sourceId: string, targetId: string) {
  const currentDimension = currentDimensionConfig.value
  if (!currentDimension || sourceId === targetId)
    return
    return null
  const sourceList = currentDimension.tags
  const visibleList = filteredTagList.value
  const visibleIds = visibleList.map(item => item.id)
  if (!visibleIds.includes(sourceId) || !visibleIds.includes(targetId))
    return
    return null
  const reorderedVisibleList = [...visibleList]
  const sourceIndex = reorderedVisibleList.findIndex(item => item.id === sourceId)
  const targetIndex = reorderedVisibleList.findIndex(item => item.id === targetId)
  if (sourceIndex === -1 || targetIndex === -1)
    return
    return null
  const [movedItem] = reorderedVisibleList.splice(sourceIndex, 1)
  reorderedVisibleList.splice(targetIndex, 0, movedItem)
  let visibleCursor = 0
  currentDimension.tags = sourceList.map((item) => {
  return sourceList.map((item) => {
    if (!visibleIds.includes(item.id))
      return item
    const nextItem = reorderedVisibleList[visibleCursor]
@@ -291,8 +463,6 @@
    ...item,
    sortOrder: index + 1,
  }))
  markDirty()
}
function handleDragStart(tagId: string) {
@@ -300,12 +470,30 @@
  dragOverTagId.value = null
}
function handleRowDrop(targetId: string) {
async function handleRowDrop(targetId: string) {
  if (!draggingTagId.value)
    return
  reorderCurrentTabList(draggingTagId.value, targetId)
  const sourceId = draggingTagId.value
  const reorderedList = getReorderedTagList(sourceId, targetId)
  draggingTagId.value = null
  dragOverTagId.value = null
  if (!reorderedList)
    return
  try {
    const res = await updateEvaluationTagSort(reorderedList.map(item => ({
      id: item.id,
      sortOrder: item.sortOrder,
    })))
    if (!isSuccessCode(res?.code)) {
      message.error(res?.msg || '排序更新失败')
      return
    }
    await loadTabList(activeTab.value)
    message.success('排序更新成功')
  }
  catch (error) {
    console.error('更新评价标签排序失败:', error)
  }
}
function handleRowDragEnd() {
@@ -439,6 +627,16 @@
onMounted(() => {
  loadConfig()
})
watch(activeTab, async (tab) => {
  if (!loadedTabs[tab])
    await loadTabList(tab)
})
function handlePageChange(page: number) {
  pageStateMap[activeTab.value].pageNum = page
  void loadTabList(activeTab.value)
}
</script>
<template>
@@ -548,6 +746,13 @@
                    </div>
                  </template>
                </n-data-table>
                <Pagination
                  :count="currentPageState.total"
                  :page="currentPageState.pageNum"
                  :page-size="currentPageState.pageSize"
                  class="robotaxi-pagination mt-12px"
                  @change="handlePageChange"
                />
              </div>
            </n-card>
          </div>
@@ -571,6 +776,8 @@
      v-model:visible="showTagModal"
      :title="modalTitle"
      :row-data="editingRowData"
      :type-options="tagTypeOptions"
      :saving="tagModalSaving"
      @save="handleSaveTagForm"
    />
  </div>
src/views/transaction/pricingManagement/detail.vue
@@ -32,7 +32,7 @@
  },
  // 基础定价-动态计费
  basicPricingJson1: {
    specialPriceEnabled: 0,
    isSpecialPrice: 0,
    waitFeeEnabled: 0,
    longDistanceFeeEnabled: 0,
    nightFeeEnabled: 0,
@@ -226,22 +226,23 @@
    nextConfig[section.enabledKey] = hasAnyDisplayValue(nextConfig, section.requiredKeys) ? 1 : 0
  })
  const currentSpecialPriceEnabled = nextConfig.specialPriceEnabled
  const currentSpecialPriceEnabled = nextConfig.isSpecialPrice ?? nextConfig.specialPriceEnabled
  if (currentSpecialPriceEnabled === 1 || currentSpecialPriceEnabled === '1' || currentSpecialPriceEnabled === true) {
    nextConfig.specialPriceEnabled = 1
    nextConfig.isSpecialPrice = 1
  }
  else if (currentSpecialPriceEnabled === 0 || currentSpecialPriceEnabled === '0' || currentSpecialPriceEnabled === false) {
    nextConfig.specialPriceEnabled = 0
    nextConfig.isSpecialPrice = 0
  }
  else {
    nextConfig.specialPriceEnabled = sectionConfigList.some(section => Number(nextConfig[section.enabledKey]) === 1) ? 1 : 0
    nextConfig.isSpecialPrice = sectionConfigList.some(section => Number(nextConfig[section.enabledKey]) === 1) ? 1 : 0
  }
  delete nextConfig.specialPriceEnabled
  return nextConfig
}
function isSpecialPriceEnabled() {
  return Number(model.value.basicPricingJson1.specialPriceEnabled) === 1
  return Number(model.value.basicPricingJson1.isSpecialPrice) === 1
}
function isSpecialPriceSectionEnabled(sectionKey: keyof typeof SPECIAL_PRICE_SECTION_CONFIG) {
@@ -468,7 +469,7 @@
      }
    }
    if (category.value === '1') {
      if (submitBasicPricingJson1.specialPriceEnabled !== 1) {
      if (submitBasicPricingJson1.isSpecialPrice !== 1) {
        submitBasicPricingJson1.waitFeeEnabled = 0
        submitBasicPricingJson1.longDistanceFeeEnabled = 0
        submitBasicPricingJson1.nightFeeEnabled = 0
@@ -483,16 +484,16 @@
      else if (submitBasicPricingJson1.waitFeeEnabled !== 1) {
        clearSpecialPriceSectionFields(submitBasicPricingJson1, ['num4', 'num5'])
      }
      if (submitBasicPricingJson1.specialPriceEnabled === 1 && submitBasicPricingJson1.longDistanceFeeEnabled !== 1) {
      if (submitBasicPricingJson1.isSpecialPrice === 1 && submitBasicPricingJson1.longDistanceFeeEnabled !== 1) {
        clearSpecialPriceSectionFields(submitBasicPricingJson1, ['num7', 'num8', 'num9', 'num10', 'num11', 'num12', 'num13'])
      }
      if (submitBasicPricingJson1.specialPriceEnabled === 1 && submitBasicPricingJson1.nightFeeEnabled !== 1) {
      if (submitBasicPricingJson1.isSpecialPrice === 1 && submitBasicPricingJson1.nightFeeEnabled !== 1) {
        clearSpecialPriceSectionFields(submitBasicPricingJson1, ['num14', 'num141', 'num15', 'num16', 'num17', 'num18', 'num19', 'num20'])
      }
      if (submitBasicPricingJson1.specialPriceEnabled === 1 && submitBasicPricingJson1.peakFeeEnabled !== 1) {
      if (submitBasicPricingJson1.isSpecialPrice === 1 && submitBasicPricingJson1.peakFeeEnabled !== 1) {
        clearSpecialPriceSectionFields(submitBasicPricingJson1, ['num21', 'num211', 'num22', 'num221', 'num23', 'num24', 'num25', 'num26', 'num27', 'num28'])
      }
      if (submitBasicPricingJson1.specialPriceEnabled === 1 && submitBasicPricingJson1.holidayFeeEnabled !== 1) {
      if (submitBasicPricingJson1.isSpecialPrice === 1 && submitBasicPricingJson1.holidayFeeEnabled !== 1) {
        clearSpecialPriceSectionFields(submitBasicPricingJson1, ['num29', 'num30', 'num31', 'num32', 'num33', 'num34'])
      }
@@ -923,7 +924,7 @@
                </div>
                <div class="mb-24px">
                  <n-form-item label="是否开启特殊价格" required label-style="font-weight: 600;">
                    <n-radio-group v-model:value="model.basicPricingJson1.specialPriceEnabled" name="specialPriceEnabled" class="ml-2">
                    <n-radio-group v-model:value="model.basicPricingJson1.isSpecialPrice" name="isSpecialPrice" class="ml-2">
                      <NSpace>
                        <n-radio :value="1">
                          是
src/views/userContent/contentCenter/agreementVersion.vue
New file
@@ -0,0 +1,798 @@
<script setup lang="tsx">
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import type { DataTableColumns } from 'naive-ui'
import { NButton, NInput, NTag, NTooltip, useDialog, useMessage } from 'naive-ui'
import Pagination from '@/components/common/Pagination.vue'
import AgreementVersionModal from './components/AgreementVersionModal.vue'
import type { AgreementVersionFormValue } from './components/AgreementVersionModal.vue'
import {
  copyLoginAgreement,
  deleteLoginAgreementVersion,
  disableLoginAgreement,
  enableLoginAgreement,
  getLoginAgreementDetail,
  getLoginAgreementLatestVersion,
  getLoginAgreementVersionPageList,
  saveLoginAgreement,
} from '@/service/api/operations'
interface AgreementVersionRow {
  id?: number
  version: string
  content: string
  creator: string
  createTime: string
  sort: number
  isNew?: boolean
  status?: 0 | 1 | 2
}
const route = useRoute()
const router = useRouter()
const dialog = useDialog()
const message = useMessage()
const loading = ref(false)
const saving = ref(false)
const versionModalSaving = ref(false)
const agreementName = ref('')
const versionList = ref<AgreementVersionRow[]>([])
const deletedIds = ref<number[]>([])
const total = ref(0)
const showVersionModal = ref(false)
const modalTitle = ref('新增版本')
const modalReadonly = ref(false)
const editingVersionId = ref<number | null>(null)
const editingRowData = ref<AgreementVersionFormValue | null>(null)
const actionLoadingKey = ref('')
const pageParams = ref({
  pageNum: 1,
  pageSize: 10,
})
const pageMode = computed<'add' | 'edit'>(() => route.query.mode === 'edit' ? 'edit' : 'add')
const agreementType = computed(() => route.name === 'opsAppContentAgreementVersion' ? 2 : 1)
function compareVersion(a: string, b: string) {
  const aParts = a.split('.').map(item => Number(item) || 0)
  const bParts = b.split('.').map(item => Number(item) || 0)
  const maxLength = Math.max(aParts.length, bParts.length)
  for (let index = 0; index < maxLength; index += 1) {
    const diff = (aParts[index] || 0) - (bParts[index] || 0)
    if (diff !== 0)
      return diff
  }
  return 0
}
function sortVersionList(list: AgreementVersionRow[]) {
  return [...list].sort((a, b) => compareVersion(b.version, a.version))
}
const sortedVersionList = computed(() => {
  return sortVersionList(versionList.value)
})
const pagedVersionList = computed(() => sortedVersionList.value)
function getStatusConfig(row: AgreementVersionRow, rowIndex: number) {
  if (Number(row.status) === 0) {
    return {
      label: '待生效',
      type: 'warning' as const,
      style: {
        color: '#fa8c16',
        backgroundColor: '#fff7e8',
      },
    }
  }
  if (Number(row.status) === 1) {
    return {
      label: '启用中',
      type: 'success' as const,
      style: {
        color: '#0bb52d',
        backgroundColor: '#dcf5dd',
      },
    }
  }
  return {
    label: '已失效',
    type: 'default' as const,
    style: {
      color: '#666666',
      backgroundColor: 'rgba(23, 31, 38, 0.05)',
    },
  }
}
function getStatusValue(row: AgreementVersionRow, rowIndex: number): 0 | 1 | 2 {
  if ([0, 1, 2].includes(Number(row.status)))
    return Number(row.status) as 0 | 1 | 2
  if (rowIndex === 0 && row.isNew)
    return 0
  if (rowIndex === 0)
    return 1
  return 2
}
function getRowStatus(row: AgreementVersionRow, rowIndex: number) {
  return getStatusValue(row, rowIndex)
}
function isPendingStatus(row: AgreementVersionRow, rowIndex: number) {
  return getRowStatus(row, rowIndex) === 0
}
function isEnabledStatus(row: AgreementVersionRow, rowIndex: number) {
  return getRowStatus(row, rowIndex) === 1
}
function isInvalidStatus(row: AgreementVersionRow, rowIndex: number) {
  return getRowStatus(row, rowIndex) === 2
}
function getActionLoadingValue(row: AgreementVersionRow, action: string) {
  return actionLoadingKey.value === `${action}_${row.id || row.version}`
}
function getDisableConfirmText(row: AgreementVersionRow) {
  const hasOtherPendingVersion = versionList.value.some(item => Number(item.status) === 0 && item.id !== row.id)
  if (!hasOtherPendingVersion)
    return '停用后乘客端将无协议展示,影响用户登录流程,是否确认停用?'
  return '停用后该版本将失效,是否确认停用?'
}
async function fetchVersionDetail(row: AgreementVersionRow) {
  if (!row.id) {
    return {
      id: row.id,
      version: row.version,
      content: row.content,
    }
  }
  const res = await getLoginAgreementDetail(row.id)
  if (res?.code !== 200 || !res.data) {
    message.error(res?.msg || '协议详情获取失败')
    return null
  }
  return res.data
}
function openVersionModal(title: string, rowData: AgreementVersionFormValue, readonly = false) {
  modalTitle.value = title
  modalReadonly.value = readonly
  editingRowData.value = rowData
  showVersionModal.value = true
}
const columns = computed<DataTableColumns<AgreementVersionRow>>(() => [
  {
    title: '版本号',
    key: 'version',
    width: 180,
    resizable: true,
  },
  {
    title: '创建人',
    key: 'creator',
    width: 180,
    resizable: true,
    ellipsis: { tooltip: true },
  },
  {
    title: '创建时间',
    key: 'createTime',
    width: 200,
    resizable: true,
  },
  {
    title: '状态',
    key: 'status',
    width: 140,
    resizable: true,
    render: (row, index) => {
      const statusConfig = getStatusConfig(row, index)
      return (
        <NTag bordered={false} type={statusConfig.type} style={statusConfig.style}>
          {statusConfig.label}
        </NTag>
      )
    },
  },
  {
    title: '操作',
    key: 'actions',
    width: 120,
    render: (row, index) => (
      <div class="flex items-center">
        {isPendingStatus(row, index)
          ? (
              <NButton text type="primary" loading={getActionLoadingValue(row, 'enable')} onClick={() => confirmEnableVersion(row)}>
                启用
              </NButton>
            )
          : null}
        {isPendingStatus(row, index)
          ? (
              <NButton text type="primary" class="ml-12px" onClick={() => handleEditVersion(row, index)}>
                编辑
              </NButton>
            )
          : null}
        <NButton text type="primary" class="ml-12px" onClick={() => handleViewVersion(row)}>
          详情
        </NButton>
        {(isEnabledStatus(row, index) || isInvalidStatus(row, index))
          ? (
              <NButton
                text
                type="primary"
                class="ml-12px"
                loading={getActionLoadingValue(row, 'copy')}
                onClick={() => handleCopyVersion(row)}
              >
                复制
              </NButton>
            )
          : null}
        {isEnabledStatus(row, index)
          ? (
              <NButton
                text
                type="primary"
                class="ml-12px"
                loading={getActionLoadingValue(row, 'disable')}
                onClick={() => confirmDisableVersion(row)}
              >
                停用
              </NButton>
            )
          : null}
      </div>
    ),
  },
])
function getNextVersionFromLocal() {
  const latestVersion = sortedVersionList.value[0]?.version
  if (!latestVersion)
    return '1.0.0'
  const versionParts = latestVersion.split('.').map(item => Number(item) || 0)
  while (versionParts.length < 3) {
    versionParts.push(0)
  }
  versionParts[2] += 1
  return versionParts.join('.')
}
async function getNextVersion() {
  const nameId = getAgreementNameId()
  if (!nameId)
    return getNextVersionFromLocal()
  const res = await getLoginAgreementLatestVersion(nameId)
  if (res?.code !== 200 || !res.data) {
    message.error(res?.msg || '最新版本号获取失败')
    return ''
  }
  return String(res.data)
}
function resetPagination() {
  const maxPage = Math.max(1, Math.ceil(total.value / pageParams.value.pageSize))
  if (pageParams.value.pageNum > maxPage)
    pageParams.value.pageNum = maxPage
}
function normalizeRemoteList(list: Array<any>) {
  return list
    .map((item, index) => ({
      id: Number(item.id),
      version: item.version || `${index + 1}.0.0`,
      content: item.content || '',
      creator: item.updateBy || item.createBy || '',
      createTime: item.createTime || item.updateTime || '',
      sort: Number(item.sort || index + 1),
      isNew: false,
      status: [0, 1, 2].includes(Number(item.status)) ? Number(item.status) as 0 | 1 | 2 : undefined,
    }))
}
function getAgreementNameId() {
  const queryId = route.query.nameId
  if (typeof queryId === 'string' && queryId)
    return queryId
  if (typeof queryId === 'number')
    return String(queryId)
  return ''
}
function extractAgreementNameId(data: any) {
  if (data === null || data === undefined)
    return ''
  if (typeof data === 'string' || typeof data === 'number')
    return String(data)
  if (typeof data === 'object') {
    if (data.nameId !== null && data.nameId !== undefined)
      return String(data.nameId)
    if (data.id !== null && data.id !== undefined)
      return String(data.id)
  }
  return ''
}
async function loadVersionList() {
  const nameId = getAgreementNameId()
  if (pageMode.value !== 'edit' || !nameId) {
    versionList.value = []
    deletedIds.value = []
    total.value = 0
    if (pageMode.value !== 'edit')
      agreementName.value = ''
    return
  }
  loading.value = true
  try {
    const res = await getLoginAgreementVersionPageList(nameId, {
      pageNum: pageParams.value.pageNum,
      pageSize: pageParams.value.pageSize,
      type: agreementType.value,
    })
    if (res?.code !== 200 || !res.data) {
      message.error(res?.msg || '协议版本列表获取失败')
      return
    }
    const records = res.data.records || []
    agreementName.value = records[0]?.name || String(route.query.title || agreementName.value || '')
    versionList.value = normalizeRemoteList(records)
    total.value = Number(res.data.total || 0)
    deletedIds.value = []
    resetPagination()
  }
  finally {
    loading.value = false
  }
}
async function handleAddVersion() {
  const version = await getNextVersion()
  if (!version)
    return
  editingVersionId.value = null
  openVersionModal('新增版本', {
    version,
    content: '',
  })
}
async function handleEditVersion(row: AgreementVersionRow, rowIndex: number) {
  if (!isPendingStatus(row, rowIndex)) {
    message.warning('仅待生效版本支持编辑')
    return
  }
  const detail = await fetchVersionDetail(row)
  if (!detail)
    return
  editingVersionId.value = row.id || null
  openVersionModal('编辑版本', {
    id: Number(detail.id),
    version: detail.version || row.version,
    content: detail.content || '',
  })
}
function handleDeleteVersion(row: AgreementVersionRow) {
  if (row.id)
    deletedIds.value.push(row.id)
  versionList.value = versionList.value.filter(item => item.version !== row.version)
  total.value = Math.max(0, total.value - 1)
  resetPagination()
}
async function handleViewVersion(row: AgreementVersionRow) {
  const detail = await fetchVersionDetail(row)
  if (!detail)
    return
  editingVersionId.value = row.id || null
  openVersionModal('协议详情', {
    id: Number(detail.id),
    version: detail.version || row.version,
    content: detail.content || '',
  }, true)
}
async function handleCopyVersion(row: AgreementVersionRow) {
  if (!row.id) {
    message.warning('当前版本缺少ID,无法复制')
    return
  }
  actionLoadingKey.value = `copy_${row.id || row.version}`
  try {
    const res = await copyLoginAgreement(row.id)
    if (res?.code !== 200) {
      message.error(res?.msg || '复制失败')
      return
    }
    message.success('复制成功')
    pageParams.value.pageNum = 1
    await loadVersionList()
  }
  finally {
    actionLoadingKey.value = ''
  }
}
async function updateVersionStatus(row: AgreementVersionRow, loadingType: 'enable' | 'disable') {
  if (!row.id) {
    message.warning('当前版本缺少ID,无法操作')
    return
  }
  actionLoadingKey.value = `${loadingType}_${row.id || row.version}`
  try {
    const res = loadingType === 'enable'
      ? await enableLoginAgreement(row.id)
      : await disableLoginAgreement(row.id)
    if (res?.code !== 200) {
      message.error(res?.msg || `${loadingType === 'enable' ? '启用' : '停用'}失败`)
      return
    }
    message.success(loadingType === 'enable' ? '启用成功' : '停用成功')
    await loadVersionList()
  }
  finally {
    actionLoadingKey.value = ''
  }
}
function handleEnableVersion(row: AgreementVersionRow) {
  return updateVersionStatus(row, 'enable')
}
function handleDisableVersion(row: AgreementVersionRow) {
  return updateVersionStatus(row, 'disable')
}
function confirmEnableVersion(row: AgreementVersionRow) {
  dialog.warning({
    title: '提示',
    content: '启用新版本后,旧版本将失效,新用户将看到新版本协议,是否确认?',
    positiveText: '确认',
    negativeText: '取消',
    onPositiveClick: () => handleEnableVersion(row),
  })
}
function confirmDisableVersion(row: AgreementVersionRow) {
  dialog.warning({
    title: '提示',
    content: getDisableConfirmText(row),
    positiveText: '确认',
    negativeText: '取消',
    onPositiveClick: () => handleDisableVersion(row),
  })
}
async function syncPageModeAfterVersionSaved(nameId?: string) {
  if (pageMode.value === 'edit')
    return
  await router.replace({
    name: route.name as string,
    query: {
      ...route.query,
      mode: 'edit',
      nameId: nameId || getAgreementNameId(),
      title: agreementName.value.trim(),
    },
  })
}
async function handleSaveVersion(payload: AgreementVersionFormValue) {
  if (!agreementName.value.trim()) {
    message.warning('请输入协议名称')
    return
  }
  versionModalSaving.value = true
  try {
    const res = await saveLoginAgreement({
      id: payload.id,
      name: agreementName.value.trim(),
      content: payload.content,
      type: agreementType.value,
      version: payload.version,
      status: 0,
    })
    if (res?.code !== 200) {
      message.error(res?.msg || '保存失败')
      return
    }
    const nameId = extractAgreementNameId(res.data)
    message.success('保存成功')
    showVersionModal.value = false
    await syncPageModeAfterVersionSaved(nameId)
    await loadVersionList()
  }
  finally {
    versionModalSaving.value = false
  }
}
function handlePageChange(page: number) {
  pageParams.value.pageNum = page
  void loadVersionList()
}
function handleOperationLog() {
  message.info('操作日志接口待接入')
}
function handleBack() {
  router.back()
}
function getSaveRows() {
  return [...versionList.value]
    .sort((a, b) => compareVersion(a.version, b.version))
    .map((item, index, list) => {
      const rowIndex = list.length - index - 1
      return {
        ...item,
        sort: index + 1,
        status: getStatusValue(item, rowIndex),
      }
    })
}
async function handleSave() {
  if (!agreementName.value.trim()) {
    message.warning('请输入协议名称')
    return
  }
  if (versionList.value.length === 0) {
    message.warning('请先新增版本')
    return
  }
  saving.value = true
  try {
    for (const id of deletedIds.value) {
      const deleteRes = await deleteLoginAgreementVersion(id)
      if (deleteRes?.code !== 200) {
        message.error(deleteRes?.msg || '删除版本失败')
        return
      }
    }
    const saveRows = getSaveRows()
    for (const row of saveRows) {
      const saveRes = await saveLoginAgreement({
        id: row.id,
        name: agreementName.value.trim(),
        content: row.content,
        type: agreementType.value,
        version: row.version,
        status: row.status || 0,
        sort: row.sort,
      })
      if (saveRes?.code !== 200) {
        message.error(saveRes?.msg || '保存失败')
        return
      }
    }
    message.success('保存成功')
    router.back()
  }
  finally {
    saving.value = false
  }
}
onMounted(() => {
  loadVersionList()
})
</script>
<template>
  <div class="content roboTaxi">
    <div class="page-body">
      <div class="query-box">
        <n-form label-placement="left" require-mark-placement="left">
          <div class="form-row">
            <n-form-item label="协议名称" required class="agreement-name-item">
              <NInput v-model:value="agreementName" placeholder="请输入" maxlength="20" clearable class="w-236px" />
            </n-form-item>
          </div>
        </n-form>
      </div>
      <div class="table-box">
        <div class="table-toolbar">
          <div class="table-toolbar-left">
            <NButton @click="handleOperationLog">
              操作日志
            </NButton>
          </div>
          <div class="table-toolbar-right">
            <div class="flex items-center justify-end">
              <NButton type="primary" secondary @click="handleAddVersion">
                新增版本号
              </NButton>
            </div>
          </div>
        </div>
        <div class="table-content">
          <div class="flex-1 flex flex-col h-full">
            <n-data-table
              class="flex-1"
              :columns="columns"
              :data="pagedVersionList"
              :loading="loading"
              :bordered="false"
              :row-key="(row: AgreementVersionRow) => row.id || row.version"
              flex-height
              :scroll-x="900"
            >
              <template #empty>
                <div class="flex-center">
                  <img src="/src/assets/element-img/no-data.png" style="width:123px;height:125px" @error="(e: any) => { e.target.src = 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png' }">
                </div>
              </template>
            </n-data-table>
            <Pagination
              :count="total"
              :page="pageParams.pageNum"
              :page-size="pageParams.pageSize"
              class="robotaxi-pagination mt-12px"
              @change="handlePageChange"
            />
          </div>
        </div>
      </div>
      <div class="footer-placeholder" />
    </div>
    <div class="footer">
      <div class="flex justify-end gap-3">
        <NButton class="w-84px" @click="handleBack">
          取消
        </NButton>
        <NButton type="primary" class="w-84px" :loading="saving" @click="handleSave">
          保存
        </NButton>
      </div>
    </div>
    <AgreementVersionModal
      v-model:visible="showVersionModal"
      :title="modalTitle"
      :row-data="editingRowData"
      :loading="versionModalSaving"
      :readonly="modalReadonly"
      @save="handleSaveVersion"
    />
  </div>
</template>
<style scoped lang="scss">
.content {
  position: relative;
  height: calc(100vh - 76px);
  width: 100%;
  background: #f5f7fa;
}
.page-body {
  height: calc(100vh - 76px);
  overflow-y: auto;
  padding: 12px;
  scrollbar-width: none;
}
.page-body::-webkit-scrollbar {
  display: none;
}
.query-box,
.table-box {
  background-color: #fff;
  border-radius: 6px;
  width: 100%;
  box-sizing: border-box;
}
.query-box {
  padding: 12px;
}
.table-box {
  margin-top: 12px;
  padding: 0 12px 12px;
  min-height: calc(100vh - 76px - 64px - 140px);
  display: flex;
  flex-direction: column;
}
.table-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 0;
}
.table-toolbar-left,
.table-toolbar-right {
  display: flex;
  align-items: center;
}
.table-content {
  flex: 1;
  display: flex;
  flex-direction: column;
}
.agreement-name-item {
  width: 360px;
}
.agreement-name-item :deep(.n-form-item-feedback-wrapper) {
  display: none;
}
.head {
  font-size: 16px;
  font-weight: 600;
  line-height: 24px;
  color: #171f26;
  position: relative;
  text-indent: 11px;
  margin-bottom: 16px;
}
.form-row {
  display: flex;
  gap: 24px;
}
.row-item {
  min-width: 340px;
  flex: 1;
}
.footer-placeholder {
  height: 64px;
}
.footer {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 64px;
  background-color: #fff;
  box-shadow: 0px 0px 5px 0px rgba(23, 31, 38, 0.05);
  padding: 16px 24px;
  box-sizing: border-box;
}
:deep(.n-data-table-th) {
  background-color: #f7f8fa !important;
  font-weight: 500;
  color: #171f26;
}
:deep(.n-card) {
  box-shadow: none;
}
:deep(.robotaxi-pagination) {
  padding-right: 8px;
}
</style>
src/views/userContent/contentCenter/components/AgreementListForm.vue
@@ -1,56 +1,43 @@
<script setup lang="ts">
import { computed, h, onMounted, ref, watch } from 'vue'
import { NButton, NDataTable, NForm, NFormItem, NInput, NModal, NPopconfirm, NSpace, useMessage } from 'naive-ui'
import type { DataTableColumns, FormInst, FormRules } from 'naive-ui'
import AiRichTextEditor from '@/components/custom/Editor/AiRichTextEditor/index.vue'
import { useRouter } from 'vue-router'
import { NButton, NDataTable, NPopconfirm, NSpace, useMessage } from 'naive-ui'
import type { DataTableColumns } from 'naive-ui'
import dragIcon from '@/assets/point/union.png'
import { deleteLoginAgreement, getLoginAgreementPageList, saveLoginAgreement } from '@/service/api/operations'
import { deleteLoginAgreement, getLoginAgreementDetail, getLoginAgreementPageList, saveLoginAgreement } from '@/service/api/operations'
const props = defineProps<{
  type: 'passenger' | 'ops'
}>()
interface AgreementItem {
  id: number
  id?: number
  nameId: number
  title: string
  content: string
  creator: string
  updateTime: string
  sort: number
  version: string
}
interface AgreementFormModel {
  id?: number
  title: string
  content: string
  sort: number
}
const router = useRouter()
const message = useMessage()
const tableLoading = ref(false)
const saveLoading = ref(false)
const deleteLoadingId = ref<number | null>(null)
const draggedAgreementId = 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 agreementVersionRouteName = computed(() => props.type === 'passenger' ? 'passengerContentAgreementVersion' : 'opsAppContentAgreementVersion')
const columns = computed<DataTableColumns<AgreementItem>>(() => [
  {
    title: '排序',
    key: 'sort',
    resizable: true,
    width: 100,
    render: () => {
      return h('div', { class: 'sort-cell' }, [
@@ -62,9 +49,10 @@
      ])
    },
  },
  { title: titleLabel, key: 'title' },
  { title: '创建人', key: 'creator' },
  { title: '创建/更新时间', key: 'updateTime' },
  { title: titleLabel, resizable: true, key: 'title' },
  { title: '启用中版本号', resizable: true, key: 'version', width: 160 },
  { title: '创建人', resizable: true, key: 'creator', width: 160 },
  { title: '创建/更新时间', resizable: true, key: 'updateTime', width: 160 },
  {
    title: '操作',
    key: 'actions',
@@ -95,35 +83,17 @@
  },
])
const rules: FormRules = {
  title: [
    {
      required: true,
      validator: (_rule, value: string) => {
        if (!value || !value.trim())
          return new Error(`请输入${titleLabel}`)
        if (value.length > 20)
          return new Error(`${titleLabel}最多输入20个字符`)
        return true
      },
      trigger: ['blur', 'input'],
    },
  ],
  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),
      nameId: Number(item.nameId),
      title: item.name || '',
      content: item.content || '',
      creator: item.updateBy || item.createBy || '',
      updateTime: item.updateTime || item.createTime || '',
      sort: Number(item.sort || 0),
      version: item.version || '',
    }))
}
@@ -144,12 +114,6 @@
  }
}
function getNextSort() {
  if (tableData.value.length === 0)
    return 1
  return Math.max(...tableData.value.map(item => item.sort || 0)) + 1
}
function normalizeSort(list: AgreementItem[]) {
  return list.map((item, index) => ({
    ...item,
@@ -158,30 +122,33 @@
}
function handleAdd() {
  isEdit.value = false
  formModel.value = {
    title: '',
    content: '',
    sort: getNextSort(),
  }
  showModal.value = true
  router.push({
    name: agreementVersionRouteName.value,
    query: {
      mode: 'add',
    },
  })
}
function handleEdit(row: AgreementItem) {
  isEdit.value = true
  formModel.value = {
    id: row.id,
    title: row.title,
    content: row.content,
    sort: row.sort,
async function handleEdit(row: AgreementItem) {
  const res = await getLoginAgreementDetail(row.nameId)
  if (res?.code !== 200 || !res.data) {
    message.error(res?.msg || '协议详情获取失败')
    return
  }
  showModal.value = true
  router.push({
    name: agreementVersionRouteName.value,
    query: {
      mode: 'edit',
      nameId: String(row.nameId),
    },
  })
}
async function handleDelete(row: AgreementItem) {
  deleteLoadingId.value = row.id
  deleteLoadingId.value = row.nameId
  try {
    const res = await deleteLoginAgreement(row.id)
    const res = await deleteLoginAgreement(row.nameId)
    if (res?.code === 200) {
      message.success('删除成功')
      await loadList()
@@ -193,10 +160,6 @@
  finally {
    deleteLoadingId.value = null
  }
}
function handleCancel() {
  showModal.value = false
}
function handleRowDragStart(row: AgreementItem) {
@@ -282,34 +245,6 @@
  }
}
async function handleSave() {
  try {
    await formRef.value?.validate()
    saveLoading.value = true
    const res = await saveLoginAgreement({
      id: formModel.value.id,
      name: formModel.value.title.trim(),
      content: formModel.value.content,
      sort: formModel.value.sort,
      type: agreementType.value,
    })
    if (res?.code === 200) {
      message.success('保存成功')
      showModal.value = false
      await loadList()
    }
    else {
      message.error(res?.msg || '保存失败')
    }
  }
  catch {
    // 表单校验未通过
  }
  finally {
    saveLoading.value = false
  }
}
async function validate() {
  return true
}
@@ -341,12 +276,12 @@
<template>
  <div class="agreement-list-form roboTaxi">
    <div class="mb-16px agreement-toolbar agreement-toolbar-right">
      <n-button type="primary" secondary @click="handleAdd">
      <NButton type="primary" secondary @click="handleAdd">
        {{ addBtnLabel }}
      </n-button>
      </NButton>
    </div>
    <n-data-table
    <NDataTable
      :columns="columns"
      :data="tableData"
      :loading="tableLoading"
@@ -361,60 +296,7 @@
          <img src="/src/assets/element-img/no-data.png" style="width:123px;height:125px" @error="(e: any) => { e.target.src = 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png' }">
        </div>
      </template>
    </n-data-table>
    <n-modal
      v-model:show="showModal"
      :mask-closable="false"
      preset="card"
      :title="isEdit ? modalTitleEdit : modalTitleAdd"
      class="agreement-modal"
      :style="{ width: '1000px' }"
      :segmented="{
        content: true,
        action: true,
      }"
      :content-style="{
        padding: '24px',
        maxHeight: 'calc(100vh - 200px)',
        overflowY: 'auto',
      }"
    >
      <n-form
        ref="formRef"
        :model="formModel"
        :rules="rules"
        label-placement="left"
        label-width="auto"
        require-mark-placement="left"
      >
        <n-form-item :label="titleLabel" path="title" class="mb-8px" label-placement="top">
          <n-input
            v-model:value="formModel.title"
            placeholder="请输入"
            :maxlength="20"
            show-count
            style="width: 100%"
          />
        </n-form-item>
        <n-form-item label="协议内容" path="content" label-placement="top">
          <div class="rich-text-wrapper">
            <AiRichTextEditor v-model="formModel.content" height="400px" />
          </div>
        </n-form-item>
      </n-form>
      <template #action>
        <n-space justify="end" :size="8">
          <n-button style="padding: 8px 24px; border-radius: 4px; font-size: 14px;" @click="handleCancel">
            取消
          </n-button>
          <n-button type="primary" :loading="saveLoading" style="padding: 8px 24px; border-radius: 4px; font-size: 14px;" @click="handleSave">
            保存
          </n-button>
        </n-space>
      </template>
    </n-modal>
    </NDataTable>
  </div>
</template>
@@ -444,19 +326,6 @@
  display: block;
}
.rich-text-wrapper {
  width: 100%;
}
.rich-text-wrapper :deep(.aie-container) {
  min-height: 400px;
  font-size: 14px;
}
.rich-text-wrapper :deep(.aie-main) {
  min-height: 400px;
}
:deep(.n-data-table-th) {
  background-color: #F7F8FA !important;
  font-weight: 500;
@@ -465,20 +334,5 @@
:deep(.agreement-row.dragging td) {
  background-color: #F2F3F5;
}
:deep(.agreement-modal .n-card-header) {
  padding: 20px 24px;
}
:deep(.agreement-modal .n-card-header__main) {
  font-size: 16px;
  font-weight: 600;
  color: #171F26;
}
:deep(.agreement-modal .n-card__action) {
  background-color: transparent !important;
  padding: 16px 24px;
}
</style>
src/views/userContent/contentCenter/components/AgreementVersionModal.vue
New file
@@ -0,0 +1,167 @@
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue'
import type { FormInst, FormRules } from 'naive-ui'
import { NButton, NForm, NFormItem, NInput, NModal, NSpace } from 'naive-ui'
import AiRichTextEditor from '@/components/custom/Editor/AiRichTextEditor/index.vue'
export interface AgreementVersionFormValue {
  id?: number
  version: string
  content: string
}
const props = withDefaults(defineProps<{
  visible: boolean
  title?: string
  rowData?: AgreementVersionFormValue | null
  loading?: boolean
  readonly?: boolean
}>(), {
  title: '新增版本',
  rowData: null,
  loading: false,
  readonly: false,
})
const emit = defineEmits<{
  'update:visible': [value: boolean]
  'save': [value: AgreementVersionFormValue]
}>()
const formRef = ref<FormInst | null>(null)
const formModel = reactive<AgreementVersionFormValue>({
  version: '',
  content: '',
})
const plainTextLength = computed(() => {
  return formModel.content
    .replace(/<[^>]+>/g, '')
    .replace(/&nbsp;/g, ' ')
    .trim()
    .length
})
const rules: FormRules = {
  content: [
    {
      required: true,
      validator: () => {
        if (!plainTextLength.value)
          return new Error('请输入协议内容')
        if (plainTextLength.value > 200)
          return new Error('协议内容最多输入200个字符')
        return true
      },
      trigger: ['blur', 'change', 'input'],
    },
  ],
}
function resetForm() {
  formModel.id = props.rowData?.id
  formModel.version = props.rowData?.version || ''
  formModel.content = props.rowData?.content || ''
  formRef.value?.restoreValidation()
}
function handleClose() {
  emit('update:visible', false)
}
async function handleSave() {
  if (props.readonly)
    return
  await formRef.value?.validate()
  emit('save', {
    id: formModel.id,
    version: formModel.version,
    content: formModel.content,
  })
}
watch(() => props.visible, (visible) => {
  if (visible)
    resetForm()
})
</script>
<template>
  <NModal
    :show="visible"
    :mask-closable="false"
    preset="card"
    :title="title"
    class="agreement-version-modal"
    :style="{ width: '1000px' }"
    :segmented="{ content: true, action: true }"
    :content-style="{ padding: '24px 24px 0 24px' }"
    @close="handleClose"
  >
    <NForm
      ref="formRef"
      :model="formModel"
      :rules="rules"
      label-placement="top"
      require-mark-placement="left"
    >
      <NFormItem label="版本号" required>
        <NInput v-model:value="formModel.version" disabled placeholder="系统自动生成" />
      </NFormItem>
      <NFormItem label="协议内容" path="content" required>
        <div class="rich-text-wrapper">
          <AiRichTextEditor v-model="formModel.content" :read-only="readonly" height="360px" />
        </div>
      </NFormItem>
    </NForm>
    <template #action>
      <NSpace justify="end" :size="8">
        <NButton class="w-84px" @click="handleClose">
          {{ readonly ? '关闭' : '取消' }}
        </NButton>
        <NButton v-if="!readonly" type="primary" class="w-84px" :loading="loading" @click="handleSave">
          保存
        </NButton>
      </NSpace>
    </template>
  </NModal>
</template>
<style scoped lang="scss">
.rich-text-wrapper {
  width: 100%;
}
.rich-text-wrapper :deep(.aie-container) {
  min-height: 360px;
  font-size: 14px;
}
.rich-text-wrapper :deep(.aie-main) {
  min-height: 360px;
}
.content-count {
  width: 100%;
  margin-top: 8px;
  text-align: right;
  font-size: 12px;
  color: rgba(23, 31, 38, 0.45);
}
:deep(.agreement-version-modal .n-card-header) {
  padding: 20px 24px;
}
:deep(.agreement-version-modal .n-card-header__main) {
  font-size: 16px;
  font-weight: 600;
  color: #171f26;
}
:deep(.agreement-version-modal .n-card__action) {
  background-color: transparent !important;
  padding: 16px 24px;
}
</style>
src/views/userContent/contentCenter/components/ContentForm.vue
@@ -248,13 +248,18 @@
      <div class="phone-preview">
        <img
          src="@/assets/service/phone.png" alt="手机预览"
          style="width: 198px; height: 400px; position: relative; z-index: 1;"
          style="width: 198px; position: relative; z-index: 1; pointer-events: none;"
        >
        <div
          v-if="contentType === 'image' && formModel.images.length > 0 && formModel.images[0].url"
          class="phone-screen"
        >
          <img :src="formModel.images[0].url" alt="预览内容" @error="(e: any) => { e.target.src = 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png' }">
          <div class="head">
            乘车指南
          </div>
          <div class="content">
            <img :src="formModel.images[0].url" alt="预览内容" @error="(e: any) => { e.target.src = 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png' }">
          </div>
        </div>
      </div>
    </div>
@@ -308,10 +313,14 @@
.phone-preview {
  position: relative;
  width: 198px;
  height: 400px;
  // height: 400px;
  display: flex;
  justify-content: center;
  align-items: center;
  img {
    height: auto;
  object-fit: contain;
  }
}
.phone-screen {
@@ -319,21 +328,43 @@
  top: 0;
  left: 10px;
  z-index: 0;
  width: 182px;
  height: 400px;
  width: 179px;
  height: calc(100% - 9px);
  overflow: hidden;
  border-radius: 24px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: flex-start;
  background-color: #f5f5f5;
  .head {
    width: 100%;
    height: 48px;
    display: flex;
    align-items: end;
    justify-content: center;
    font-size: 10px;
    // background-color: red;
  }
  .content {
    flex: 1;
    overflow: auto;
    min-height: 0;
    scrollbar-width: none;
    -ms-overflow-style: none;
  }
  .content::-webkit-scrollbar {
    display: none;
  }
}
.phone-screen img {
  width: 100%;
  height: 100%;
  min-height: 100%;
  object-fit: cover;
  height: auto;
  object-fit: contain;
}
:deep(.n-form-item-blank) {
  width: 300px;
src/views/userContent/contentCenter/opsApp.vue
@@ -33,7 +33,7 @@
      await currentForm.validate()
      const data = currentForm.getData()
      let saveRes: any = null
      if (activeTab.value === 'prepare') {
        const map: Record<string, number> = {
          tirePressure: 1,
@@ -89,7 +89,7 @@
      loading.value = false
      return
    }
    if (tabKey === 'prepare') {
      const types = [1, 2, 3, 4, 5]
      const res = await Promise.all(types.map(t => getVehicleDescription(t)))
@@ -153,7 +153,7 @@
            class="tab-container"
          >
            <n-tab-pane v-for="tab in tabs" :key="tab.key" :name="tab.key" :tab="tab.label">
              <div style="height: calc(100vh - 304px); overflow-y: auto; padding-bottom: 40px;">
              <div class="tab-scroll-area">
                <VehiclePrepareForm
                  v-if="tab.key === 'prepare'"
                  :ref="(el) => { if (el) contentFormRefs[tab.key] = el }"
@@ -164,8 +164,8 @@
                />
                <AgreementListForm
                  v-else-if="tab.key === 'opsAppNotice'"
                  type="ops"
                  :ref="(el) => { if (el) contentFormRefs[tab.key] = el }"
                  type="ops"
                />
              </div>
            </n-tab-pane>
@@ -189,6 +189,32 @@
.content-page{
  height: calc(100vh - 302px);
}
.tab-scroll-area {
  height: calc(100vh - 304px);
  overflow-y: auto;
  padding-bottom: 40px;
  scrollbar-width: thin;
  scrollbar-color: rgba(23, 31, 38, 0.2) transparent;
}
.tab-scroll-area::-webkit-scrollbar {
  width: 6px;
}
.tab-scroll-area::-webkit-scrollbar-thumb {
  border-radius: 999px;
  background: rgba(23, 31, 38, 0.2);
}
.tab-scroll-area::-webkit-scrollbar-track {
  background: transparent;
}
.tab-scroll-area::-webkit-scrollbar-thumb:hover {
  background: rgba(23, 31, 38, 0.3);
}
.tab-container {
  // border-bottom: 1px solid #e8e8e8;
}
src/views/userContent/contentCenter/passenger.vue
@@ -20,7 +20,7 @@
const tabs = [
  { key: 'guide', label: '乘车指南' },
  { key: 'notice', label: '协议' },
  { key: 'notice', label: '登录协议' },
  { key: 'faq', label: '常见问题' },
  { key: 'prepaymentNotice', label: '预付款协议' },
  { key: 'paymentNotice', label: '付款协议' },
@@ -36,7 +36,7 @@
      await currentForm.validate()
      const data = currentForm.getData()
      let saveRes: any = null
      if (['prepaymentNotice', 'paymentNotice', 'rideNotice'].includes(activeTab.value)) {
        // 仍沿用旧协议内容接口
        // 乘车须知使用协议内容接口,contentType 固定为 5
@@ -113,7 +113,7 @@
          .filter((it: any) => it.questions || it.answer)
        saveRes = await saveFaq({ frequentlyAskedQuestionsList })
      }
      if (saveRes && saveRes.code === 200) {
        message.success('保存成功')
      }
@@ -144,7 +144,7 @@
      loading.value = false
      return
    }
    if (['prepaymentNotice', 'paymentNotice', 'rideNotice'].includes(tabKey)) {
      // 仍沿用旧协议内容接口
      // 乘车须知使用协议内容接口,contentType 固定为 5
@@ -259,11 +259,11 @@
            class="tab-container"
          >
            <n-tab-pane v-for="tab in tabs" :key="tab.key" :name="tab.key" :tab="tab.label">
              <div style="height: calc(100vh - 304px); overflow-y: auto; padding-bottom: 40px;">
              <div class="tab-scroll-area">
                <AgreementListForm
                  v-if="tab.key === 'notice'"
                  type="passenger"
                  :ref="(el) => { if (el) contentFormRefs[tab.key] = el }"
                  type="passenger"
                />
                <FaqForm
                  v-else-if="tab.key === 'faq'"
@@ -299,6 +299,32 @@
.content-page{
  height: calc(100vh - 302px);
}
.tab-scroll-area {
  height: calc(100vh - 304px);
  overflow-y: auto;
  padding-bottom: 40px;
  scrollbar-width: thin;
  scrollbar-color: rgba(23, 31, 38, 0.2) transparent;
}
.tab-scroll-area::-webkit-scrollbar {
  width: 6px;
}
.tab-scroll-area::-webkit-scrollbar-thumb {
  border-radius: 999px;
  background: rgba(23, 31, 38, 0.2);
}
.tab-scroll-area::-webkit-scrollbar-track {
  background: transparent;
}
.tab-scroll-area::-webkit-scrollbar-thumb:hover {
  background: rgba(23, 31, 38, 0.3);
}
.tab-container {
  // border-bottom: 1px solid #e8e8e8;
}