feat: 新增协议版本管理并优化多业务功能
新增协议版本管理相关API、路由与页面组件
重构用户内容中心协议列表页,改用独立版本管理页面替代内嵌弹窗
优化评价标签管理功能,对接后端分页接口并完善表单校验
修复定价管理页面特殊价格字段命名不统一问题
优化内容页面滚动样式与手机预览效果
新增评价弹窗状态更新接口
| | |
| | | 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内容管理', |
| | |
| | | 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: '活动', |
| | |
| | | import { request } from '../http' |
| | | import dayjs from 'dayjs' |
| | | |
| | | export type EvaluationDimensionKey = 'satisfied' | 'normal' | 'unsatisfied' |
| | |
| | | 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 |
| | | } |
| | | |
| | |
| | | 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), |
| | |
| | | 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), |
| | |
| | | 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), |
| | |
| | | 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) |
| | | } |
| | |
| | | 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) |
| | | } |
| | |
| | | }>>>(`/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}`) |
| | | } |
| | | |
| | | /** 开通城市接口 */ |
| | |
| | | |
| | | // 校验围栏运营策略缩短影响 |
| | | 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, |
| | | ) |
| | | } |
| | | |
| | |
| | | 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) |
| | | } |
| | |
| | | <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<{ |
| | |
| | | |
| | | 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() |
| | | } |
| | | |
| | |
| | | async function handleSubmit() { |
| | | await formRef.value?.validate() |
| | | emit('save', { |
| | | labelType: formModel.labelType, |
| | | labelContent: formModel.labelContent.trim(), |
| | | status: formModel.status, |
| | | }) |
| | | handleClose() |
| | | } |
| | | |
| | | watch(() => props.visible, (visible) => { |
| | |
| | | :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 |
| | |
| | | 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> |
| | | |
| | |
| | | <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> |
| | |
| | | <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 { |
| | |
| | | |
| | | 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) |
| | |
| | | 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(), |
| | |
| | | { 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 |
| | |
| | | |
| | | 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) { |
| | |
| | | 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 |
| | |
| | | return |
| | | configData.value = cloneValue(originalConfigData.value) |
| | | dirty.value = false |
| | | void loadTabList(activeTab.value) |
| | | } |
| | | |
| | | async function handleSave() { |
| | |
| | | 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 |
| | |
| | | } |
| | | catch (error) { |
| | | console.error('保存评价标签配置失败:', error) |
| | | message.error('配置保存失败,请检查网络后重试') |
| | | } |
| | | finally { |
| | | saving.value = false |
| | |
| | | |
| | | 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 |
| | |
| | | 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] |
| | |
| | | ...item, |
| | | sortOrder: index + 1, |
| | | })) |
| | | |
| | | markDirty() |
| | | } |
| | | |
| | | function handleDragStart(tagId: string) { |
| | |
| | | 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() { |
| | |
| | | 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> |
| | |
| | | </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> |
| | |
| | | v-model:visible="showTagModal" |
| | | :title="modalTitle" |
| | | :row-data="editingRowData" |
| | | :type-options="tagTypeOptions" |
| | | :saving="tagModalSaving" |
| | | @save="handleSaveTagForm" |
| | | /> |
| | | </div> |
| | |
| | | }, |
| | | // 基础定价-动态计费 |
| | | basicPricingJson1: { |
| | | specialPriceEnabled: 0, |
| | | isSpecialPrice: 0, |
| | | waitFeeEnabled: 0, |
| | | longDistanceFeeEnabled: 0, |
| | | nightFeeEnabled: 0, |
| | |
| | | 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) { |
| | |
| | | } |
| | | } |
| | | if (category.value === '1') { |
| | | if (submitBasicPricingJson1.specialPriceEnabled !== 1) { |
| | | if (submitBasicPricingJson1.isSpecialPrice !== 1) { |
| | | submitBasicPricingJson1.waitFeeEnabled = 0 |
| | | submitBasicPricingJson1.longDistanceFeeEnabled = 0 |
| | | submitBasicPricingJson1.nightFeeEnabled = 0 |
| | |
| | | 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']) |
| | | } |
| | | |
| | |
| | | </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"> |
| | | 是 |
| New file |
| | |
| | | <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> |
| | |
| | | <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' }, [ |
| | |
| | | ]) |
| | | }, |
| | | }, |
| | | { 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', |
| | |
| | | }, |
| | | ]) |
| | | |
| | | 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 || '', |
| | | })) |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | 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, |
| | |
| | | } |
| | | |
| | | 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() |
| | |
| | | finally { |
| | | deleteLoadingId.value = null |
| | | } |
| | | } |
| | | |
| | | function handleCancel() { |
| | | showModal.value = false |
| | | } |
| | | |
| | | function handleRowDragStart(row: AgreementItem) { |
| | |
| | | } |
| | | } |
| | | |
| | | 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 |
| | | } |
| | |
| | | <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" |
| | |
| | | <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> |
| | | |
| | |
| | | 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; |
| | |
| | | |
| | | :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> |
| New file |
| | |
| | | <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(/ /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> |
| | |
| | | <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> |
| | |
| | | .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 { |
| | |
| | | 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; |
| | |
| | | await currentForm.validate() |
| | | const data = currentForm.getData() |
| | | let saveRes: any = null |
| | | |
| | | |
| | | if (activeTab.value === 'prepare') { |
| | | const map: Record<string, number> = { |
| | | tirePressure: 1, |
| | |
| | | loading.value = false |
| | | return |
| | | } |
| | | |
| | | |
| | | if (tabKey === 'prepare') { |
| | | const types = [1, 2, 3, 4, 5] |
| | | const res = await Promise.all(types.map(t => getVehicleDescription(t))) |
| | |
| | | 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 }" |
| | |
| | | /> |
| | | <AgreementListForm |
| | | v-else-if="tab.key === 'opsAppNotice'" |
| | | type="ops" |
| | | :ref="(el) => { if (el) contentFormRefs[tab.key] = el }" |
| | | type="ops" |
| | | /> |
| | | </div> |
| | | </n-tab-pane> |
| | |
| | | .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; |
| | | } |
| | |
| | | |
| | | const tabs = [ |
| | | { key: 'guide', label: '乘车指南' }, |
| | | { key: 'notice', label: '协议' }, |
| | | { key: 'notice', label: '登录协议' }, |
| | | { key: 'faq', label: '常见问题' }, |
| | | { key: 'prepaymentNotice', label: '预付款协议' }, |
| | | { key: 'paymentNotice', label: '付款协议' }, |
| | |
| | | await currentForm.validate() |
| | | const data = currentForm.getData() |
| | | let saveRes: any = null |
| | | |
| | | |
| | | if (['prepaymentNotice', 'paymentNotice', 'rideNotice'].includes(activeTab.value)) { |
| | | // 仍沿用旧协议内容接口 |
| | | // 乘车须知使用协议内容接口,contentType 固定为 5 |
| | |
| | | .filter((it: any) => it.questions || it.answer) |
| | | saveRes = await saveFaq({ frequentlyAskedQuestionsList }) |
| | | } |
| | | |
| | | |
| | | if (saveRes && saveRes.code === 200) { |
| | | message.success('保存成功') |
| | | } |
| | |
| | | loading.value = false |
| | | return |
| | | } |
| | | |
| | | |
| | | if (['prepaymentNotice', 'paymentNotice', 'rideNotice'].includes(tabKey)) { |
| | | // 仍沿用旧协议内容接口 |
| | | // 乘车须知使用协议内容接口,contentType 固定为 5 |
| | |
| | | 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'" |
| | |
| | | .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; |
| | | } |