feat(内容中心): 拆分内容中心为乘客端和运维APP独立管理页面
- 新增乘客端内容管理页面,包含乘车指南、协议、常见问题等模块
- 新增运维APP内容管理页面,包含车辆整备、充电、检测、登录协议等模块
- 添加协议列表组件,支持拖拽排序、增删改查功能
- 更新路由配置和国际化文案,支持双端独立访问
| | |
| | | "whitelist": "Whitelist Management", |
| | | "whitelistLog": "Whitelist Log", |
| | | "contentCenter": "Content Center", |
| | | "contentCenterIndex": "Content Center", |
| | | "passengerContent": "Passenger App Content Management", |
| | | "opsAppContent": "Operations App Content Management", |
| | | "contentCenterActive": "Activity", |
| | | "contentCenterConpon": "Coupon Management", |
| | | "contentCenterConponAdd": "Add Coupon", |
| | |
| | | "whitelist": "白名单管理", |
| | | "whitelistLog": "白名单日志", |
| | | "contentCenter": "内容中心", |
| | | "contentCenterIndex": "内容中心", |
| | | "passengerContent": "乘客端内容管理", |
| | | "opsAppContent": "运维APP内容管理", |
| | | "contentCenterActive": "活动", |
| | | "contentCenterConpon": "优惠券管理", |
| | | "contentCenterConponAdd": "添加优惠券", |
| | |
| | | pid: 3, |
| | | }, |
| | | { |
| | | name: 'contentCenterIndex', |
| | | path: '/userContent/contentCenter/index', |
| | | title: '内容中心', |
| | | name: 'passengerContent', |
| | | path: '/userContent/contentCenter/passenger', |
| | | title: '乘客端内容管理', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:file-text', |
| | | menuType: 'page', |
| | | componentPath: '/userContent/contentCenter/index.vue', |
| | | componentPath: '/userContent/contentCenter/passenger.vue', |
| | | id: 3021, |
| | | keepAlive: false, |
| | | pid: 302, |
| | | fullScreen: true, |
| | | }, |
| | | { |
| | | name: 'opsAppContent', |
| | | path: '/userContent/contentCenter/opsApp', |
| | | title: '运维APP内容管理', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:file-text', |
| | | menuType: 'page', |
| | | componentPath: '/userContent/contentCenter/opsApp.vue', |
| | | id: 3024, |
| | | keepAlive: false, |
| | | pid: 302, |
| | | fullScreen: true, |
| | | }, |
| | | { |
| | | name: 'contentCenterActive', |
| | | path: '/userContent/active', |
| | | title: '活动', |
| | |
| | | |
| | | <template> |
| | | <div> |
| | | <div class="flex tabs mb-16px"> |
| | | <div class="flex tabs mb-16px "> |
| | | <div |
| | | v-for="tab in subTabs" |
| | | :key="tab.key" |
| New file |
| | |
| | | <script setup lang="ts"> |
| | | import { h, ref } from 'vue' |
| | | import { NButton, NCard, NDataTable, NForm, NFormItem, NInput, NModal, NPopconfirm, NSpace, useMessage } from 'naive-ui' |
| | | import RichTextEditor from '@/components/custom/Editor/RichTextEditor/index.vue' |
| | | |
| | | const props = defineProps<{ |
| | | type: 'passenger' | 'ops' |
| | | }>() |
| | | |
| | | const message = useMessage() |
| | | |
| | | const addBtnLabel = props.type === 'passenger' ? '新增协议' : '新增登录协议' |
| | | const titleLabel = '协议名称' |
| | | const modalTitleAdd = props.type === 'passenger' ? '新增协议' : '新增登录协议' |
| | | const modalTitleEdit = props.type === 'passenger' ? '编辑协议' : '编辑登录协议' |
| | | |
| | | interface AgreementItem { |
| | | id: string |
| | | title: string |
| | | content: string |
| | | creator: string |
| | | updateTime: string |
| | | } |
| | | |
| | | const tableData = ref<AgreementItem[]>([ |
| | | { |
| | | id: '1', |
| | | title: props.type === 'passenger' ? '乘车须知' : '小鹏Robotaxi录音录像及个人信息采集说明', |
| | | content: '', |
| | | creator: '张三', |
| | | updateTime: '2026-04-08 09:14:55', |
| | | }, |
| | | { |
| | | id: '2', |
| | | title: props.type === 'passenger' ? '隐私政策' : '权限说明', |
| | | content: '', |
| | | creator: '张三', |
| | | updateTime: '2026-04-08 09:14:58', |
| | | }, |
| | | ]) |
| | | |
| | | const columns = [ |
| | | { |
| | | title: '排序', |
| | | key: 'sort', |
| | | width: 80, |
| | | render: () => { |
| | | return h('div', { style: 'cursor: move; display: flex; align-items: center;' }, [ |
| | | h('img', { |
| | | src: '/src/assets/point/union.png', |
| | | alt: '拖动', |
| | | style: 'width: 15px; height: 15px;', |
| | | onError: (e: any) => { |
| | | e.target.src = 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png' |
| | | } |
| | | }) |
| | | ]) |
| | | }, |
| | | }, |
| | | { title: titleLabel, key: 'title' }, |
| | | { title: '创建人', key: 'creator' }, |
| | | { title: '创建/更新时间', key: 'updateTime' }, |
| | | { |
| | | title: '操作', |
| | | key: 'actions', |
| | | width: 120, |
| | | render: (row: AgreementItem) => { |
| | | return h(NSpace, { size: 'small', align: 'center' }, { |
| | | default: () => [ |
| | | h( |
| | | NButton, |
| | | { text: true, type: 'primary', onClick: () => handleEdit(row) }, |
| | | { default: () => '编辑' }, |
| | | ), |
| | | h( |
| | | NPopconfirm, |
| | | { onPositiveClick: () => handleDelete(row) }, |
| | | { |
| | | trigger: () => h(NButton, { text: true, type: 'primary' }, { default: () => '删除' }), |
| | | default: () => '确认删除该协议吗?', |
| | | }, |
| | | ), |
| | | ], |
| | | }) |
| | | }, |
| | | }, |
| | | ] |
| | | |
| | | const showModal = ref(false) |
| | | const isEdit = ref(false) |
| | | const formRef = ref<any>(null) |
| | | const formModel = ref({ |
| | | id: '', |
| | | title: '', |
| | | content: '', |
| | | }) |
| | | |
| | | let draggedIndex: number | null = null |
| | | |
| | | function handleDragStart(index: number, e: DragEvent) { |
| | | draggedIndex = index |
| | | if (e.target && (e.target as HTMLElement).classList) { |
| | | (e.target as HTMLElement).classList.add('dragging') |
| | | } |
| | | } |
| | | |
| | | function handleDragEnd(e: DragEvent) { |
| | | if (e.target && (e.target as HTMLElement).classList) { |
| | | (e.target as HTMLElement).classList.remove('dragging') |
| | | } |
| | | } |
| | | |
| | | function handleDragOver(e: DragEvent) { |
| | | e.preventDefault() |
| | | e.dataTransfer!.dropEffect = 'move' |
| | | } |
| | | |
| | | function handleDrop(index: number) { |
| | | if (draggedIndex !== null && draggedIndex !== index) { |
| | | const items = [...tableData.value] |
| | | const draggedItem = items[draggedIndex] |
| | | items.splice(draggedIndex, 1) |
| | | items.splice(index, 0, draggedItem) |
| | | tableData.value = items |
| | | } |
| | | draggedIndex = null |
| | | } |
| | | |
| | | const rowProps = (row: AgreementItem, index: number) => { |
| | | return { |
| | | draggable: true, |
| | | onDragstart: (e: DragEvent) => handleDragStart(index, e), |
| | | onDragend: handleDragEnd, |
| | | onDragover: handleDragOver, |
| | | onDrop: () => handleDrop(index), |
| | | class: 'draggable-row' |
| | | } |
| | | } |
| | | |
| | | const rules = { |
| | | title: { required: true, message: `请输入${titleLabel}`, trigger: 'blur' }, |
| | | content: { required: true, message: '请输入协议内容', trigger: 'blur' }, |
| | | } |
| | | |
| | | function handleAdd() { |
| | | isEdit.value = false |
| | | formModel.value = { |
| | | id: '', |
| | | title: '', |
| | | content: '', |
| | | } |
| | | showModal.value = true |
| | | } |
| | | |
| | | function handleEdit(row: AgreementItem) { |
| | | isEdit.value = true |
| | | formModel.value = { |
| | | id: row.id, |
| | | title: row.title, |
| | | content: row.content, |
| | | } |
| | | showModal.value = true |
| | | } |
| | | |
| | | function handleDelete(row: AgreementItem) { |
| | | tableData.value = tableData.value.filter(item => item.id !== row.id) |
| | | message.success('删除成功') |
| | | } |
| | | |
| | | function handleCancel() { |
| | | showModal.value = false |
| | | } |
| | | |
| | | async function handleSave() { |
| | | try { |
| | | await formRef.value?.validate() |
| | | if (isEdit.value) { |
| | | const index = tableData.value.findIndex(item => item.id === formModel.value.id) |
| | | if (index > -1) { |
| | | tableData.value[index].title = formModel.value.title |
| | | tableData.value[index].content = formModel.value.content |
| | | tableData.value[index].updateTime = new Date().toLocaleString().replace(/\//g, '-') |
| | | } |
| | | } |
| | | else { |
| | | tableData.value.unshift({ |
| | | id: String(Date.now()), |
| | | title: formModel.value.title, |
| | | content: formModel.value.content, |
| | | creator: '当前用户', |
| | | updateTime: new Date().toLocaleString().replace(/\//g, '-'), |
| | | }) |
| | | } |
| | | message.success('保存成功') |
| | | showModal.value = false |
| | | } |
| | | catch (error) { |
| | | // 验证失败 |
| | | } |
| | | } |
| | | |
| | | async function validate() { |
| | | return true |
| | | } |
| | | |
| | | function getData() { |
| | | return tableData.value |
| | | } |
| | | |
| | | function setData(data: any) { |
| | | // 暂时使用 mock 数据,不覆盖 |
| | | } |
| | | |
| | | defineExpose({ |
| | | validate, |
| | | getData, |
| | | setData, |
| | | }) |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="agreement-list-form roboTaxi"> |
| | | <div class="mb-16px"> |
| | | <n-button type="primary" secondary @click="handleAdd"> |
| | | {{ addBtnLabel }} |
| | | </n-button> |
| | | </div> |
| | | |
| | | <n-data-table |
| | | :columns="columns" |
| | | :data="tableData" |
| | | :bordered="false" |
| | | :bottom-bordered="true" |
| | | :row-key="row => row.id" |
| | | :row-props="rowProps" |
| | | class="custom-table" |
| | | /> |
| | | |
| | | <n-modal v-model:show="showModal" :mask-closable="false"> |
| | | <div class="modal-container"> |
| | | <div class="modal-header"> |
| | | <div class="modal-title"> |
| | | {{ isEdit ? modalTitleEdit : modalTitleAdd }} |
| | | </div> |
| | | </div> |
| | | <div class="modal-body"> |
| | | <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"> |
| | | <n-input v-model:value="formModel.title" placeholder="请输入" style="width: 400px" /> |
| | | </n-form-item> |
| | | <n-form-item label="协议内容" path="content" label-placement="top"> |
| | | <div class="rich-text-wrapper"> |
| | | <RichTextEditor v-model="formModel.content" /> |
| | | </div> |
| | | </n-form-item> |
| | | </n-form> |
| | | </div> |
| | | <div class="modal-footer"> |
| | | <n-button style="padding: 8px 24px; border-radius: 4px; font-size: 14px; margin-right: 12px;" @click="handleCancel"> |
| | | 取消 |
| | | </n-button> |
| | | <n-button type="primary" style="padding: 8px 24px; border-radius: 4px; font-size: 14px;" @click="handleSave"> |
| | | 保存 |
| | | </n-button> |
| | | </div> |
| | | </div> |
| | | </n-modal> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped lang="scss"> |
| | | .agreement-list-form { |
| | | width: 100%; |
| | | height: auto !important; |
| | | } |
| | | |
| | | .rich-text-wrapper { |
| | | width: 100%; |
| | | border: 1px solid #e5e6eb; |
| | | border-radius: 4px; |
| | | } |
| | | |
| | | .rich-text-wrapper :deep(.ql-container) { |
| | | min-height: 400px; |
| | | font-size: 14px; |
| | | border: none !important; |
| | | } |
| | | |
| | | .rich-text-wrapper :deep(.ql-toolbar) { |
| | | border: none !important; |
| | | border-bottom: 1px solid #e5e6eb !important; |
| | | background-color: #fafafa; |
| | | } |
| | | |
| | | .rich-text-wrapper :deep(.ql-editor) { |
| | | min-height: 400px; |
| | | } |
| | | |
| | | :deep(.n-data-table-th) { |
| | | background-color: #F7F8FA !important; |
| | | font-weight: 500; |
| | | color: #171F26; |
| | | } |
| | | |
| | | :deep(.draggable-row) { |
| | | cursor: move; |
| | | } |
| | | |
| | | :deep(.draggable-row.dragging) { |
| | | opacity: 0.5; |
| | | background-color: #f5f5f5; |
| | | } |
| | | |
| | | .modal-container { |
| | | width: 1000px; |
| | | background-color: #fff; |
| | | border-radius: 8px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .modal-header { |
| | | padding: 20px 24px; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | } |
| | | |
| | | .modal-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #171F26; |
| | | } |
| | | |
| | | .modal-body { |
| | | padding: 24px; |
| | | max-height: calc(100vh - 200px); |
| | | overflow-y: auto; |
| | | } |
| | | |
| | | .modal-footer { |
| | | padding: 16px 24px; |
| | | border-top: 1px solid #f0f0f0; |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | background-color: #fff; |
| | | } |
| | | </style> |
| New file |
| | |
| | | <script setup lang="ts"> |
| | | import { nextTick, ref, watch } from 'vue' |
| | | import { useMessage } from 'naive-ui' |
| | | import { |
| | | getAgreementContent, |
| | | getVehicleDescription, |
| | | saveAgreement, |
| | | saveVehicleDescription, |
| | | } from '@/service/api/contentCenter' |
| | | import VehiclePrepareForm from './components/VehiclePrepareForm.vue' |
| | | import SimpleRichTextForm from './components/SimpleRichTextForm.vue' |
| | | import AgreementListForm from './components/AgreementListForm.vue' |
| | | |
| | | const activeTab = ref('prepare') |
| | | const message = useMessage() |
| | | const loading = ref(false) |
| | | |
| | | const tabs = [ |
| | | { key: 'prepare', label: '车辆整备' }, |
| | | { key: 'charge', label: '车辆充电' }, |
| | | { key: 'inspect', label: '车辆检测' }, |
| | | { key: 'insurance', label: '保险说明' }, |
| | | { key: 'annual', label: '年检说明' }, |
| | | { key: 'maintenance', label: '车辆保养' }, |
| | | { key: 'qualification', label: '运营资质' }, |
| | | { key: 'opsAppNotice', label: '登录协议' }, |
| | | ] |
| | | |
| | | const contentFormRefs = ref<Record<string, any>>({}) |
| | | |
| | | async function handleSave() { |
| | | try { |
| | | const currentForm = contentFormRefs.value[activeTab.value] |
| | | if (currentForm) { |
| | | await currentForm.validate() |
| | | const data = currentForm.getData() |
| | | let saveRes: any = null |
| | | |
| | | if (activeTab.value === 'prepare') { |
| | | const map: Record<string, number> = { |
| | | tirePressure: 1, |
| | | lighting: 2, |
| | | bodyClean: 3, |
| | | cabinClean: 4, |
| | | lostItems: 5, |
| | | } |
| | | const activeKey = typeof currentForm.getActiveKey === 'function' ? currentForm.getActiveKey() : 'tirePressure' |
| | | const t = map[activeKey] ?? 1 |
| | | const content = data[activeKey] || '' |
| | | saveRes = await saveVehicleDescription({ descriptionType: t, descriptionContent: content }) |
| | | } |
| | | else if (['charge', 'inspect', 'insurance', 'annual', 'maintenance', 'qualification'].includes(activeTab.value)) { |
| | | const typeMap: Record<string, number> = { |
| | | charge: 6, |
| | | inspect: 7, |
| | | insurance: 8, |
| | | annual: 9, |
| | | maintenance: 10, |
| | | qualification: 11, |
| | | } |
| | | const t = typeMap[activeTab.value] |
| | | saveRes = await saveVehicleDescription({ descriptionType: t, descriptionContent: data.content || '' }) |
| | | } |
| | | else if (activeTab.value === 'opsAppNotice') { |
| | | const map: Record<string, number> = { |
| | | operationPrivacyPolicy: 8, |
| | | thirdPartyShareList: 9, |
| | | personalInfoCollectList: 10, |
| | | permissionCall: 11, |
| | | } |
| | | const activeKey = typeof currentForm.getActiveKey === 'function' ? currentForm.getActiveKey() : 'operationPrivacyPolicy' |
| | | const t = map[activeKey] ?? 8 |
| | | const content = data[activeKey] || '' |
| | | saveRes = await saveAgreement({ content, contentType: t }) |
| | | } |
| | | |
| | | if (saveRes && saveRes.code === 200) { |
| | | message.success('保存成功') |
| | | } |
| | | else if (saveRes) { |
| | | message.error('保存失败') |
| | | } |
| | | } |
| | | } |
| | | catch { |
| | | // message.error('验证失败') |
| | | } |
| | | } |
| | | |
| | | function handleCancel() { |
| | | |
| | | } |
| | | |
| | | async function loadContent(tabKey: string) { |
| | | try { |
| | | loading.value = true |
| | | await nextTick() |
| | | let currentForm = contentFormRefs.value[tabKey] |
| | | if (!currentForm || typeof currentForm.setData !== 'function') { |
| | | await nextTick() |
| | | currentForm = contentFormRefs.value[tabKey] |
| | | } |
| | | if (!currentForm || typeof currentForm.setData !== 'function') { |
| | | loading.value = false |
| | | return |
| | | } |
| | | |
| | | if (tabKey === 'prepare') { |
| | | const types = [1, 2, 3, 4, 5] |
| | | const res = await Promise.all(types.map(t => getVehicleDescription(t))) |
| | | if (res.every(r => r && r.code === 200)) { |
| | | const data: Record<string, string> = { |
| | | tirePressure: res[0].data?.descriptionContent || '', |
| | | lighting: res[1].data?.descriptionContent || '', |
| | | bodyClean: res[2].data?.descriptionContent || '', |
| | | cabinClean: res[3].data?.descriptionContent || '', |
| | | lostItems: res[4].data?.descriptionContent || '', |
| | | } |
| | | currentForm.setData(data) |
| | | } |
| | | } |
| | | else if (['charge', 'inspect', 'insurance', 'annual', 'maintenance', 'qualification'].includes(tabKey)) { |
| | | const typeMap: Record<string, number> = { |
| | | charge: 6, |
| | | inspect: 7, |
| | | insurance: 8, |
| | | annual: 9, |
| | | maintenance: 10, |
| | | qualification: 11, |
| | | } |
| | | const t = typeMap[tabKey] |
| | | const res = await getVehicleDescription(t) |
| | | if (res && res.code === 200) { |
| | | currentForm.setData({ content: res.data?.descriptionContent || '' }) |
| | | } |
| | | } |
| | | else if (tabKey === 'opsAppNotice') { |
| | | const types = [8, 9, 10, 11] |
| | | const res = await Promise.all(types.map(t => getAgreementContent(t))) |
| | | if (res.every(r => r && r.code === 200)) { |
| | | const data = { |
| | | operationPrivacyPolicy: res[0].data?.content || '', |
| | | thirdPartyShareList: res[1].data?.content || '', |
| | | personalInfoCollectList: res[2].data?.content || '', |
| | | permissionCall: res[3].data?.content || '', |
| | | } |
| | | currentForm.setData(data) |
| | | } |
| | | } |
| | | } |
| | | finally { |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | watch(activeTab, (v) => { |
| | | loadContent(v) |
| | | }, { immediate: true }) |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="h-full flex flex-col justify-between "> |
| | | <div class="p-12px content-page"> |
| | | <n-card> |
| | | <div> |
| | | <div class="head"> |
| | | 运维APP内容管理 |
| | | </div> |
| | | <n-tabs |
| | | v-model:value="activeTab" |
| | | type="line" |
| | | size="large" |
| | | :theme-overrides="{ |
| | | tabTextColor: '#666666', |
| | | tabTextColorActive: '#0068FF', |
| | | tabBorderColorActive: '#0068FF', |
| | | tabBorderRadius: '4px', |
| | | tabPaddingHorizontal: '16px', |
| | | tabPaddingVertical: '8px', |
| | | }" |
| | | 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;"> |
| | | <VehiclePrepareForm |
| | | v-if="tab.key === 'prepare'" |
| | | :ref="(el) => { if (el) contentFormRefs[tab.key] = el }" |
| | | /> |
| | | <SimpleRichTextForm |
| | | v-else-if="['charge', 'inspect', 'insurance', 'annual', 'maintenance', 'qualification'].includes(tab.key)" |
| | | :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 }" |
| | | /> |
| | | </div> |
| | | </n-tab-pane> |
| | | </n-tabs> |
| | | </div> |
| | | </n-card> |
| | | </div> |
| | | |
| | | <div v-permission="30211" class="footer flex justify-end " style="background-color: white; padding: 16px 24px; border-radius: 8px; box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);"> |
| | | <n-button style="padding: 8px 24px; border-radius: 4px; font-size: 14px;margin-right: 12px;" @click="handleCancel"> |
| | | 取消 |
| | | </n-button> |
| | | <n-button type="primary" style="padding: 8px 24px; border-radius: 4px; font-size: 14px;" @click="handleSave"> |
| | | 保存 |
| | | </n-button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped lang="scss"> |
| | | .content-page{ |
| | | height: calc(100vh - 302px); |
| | | } |
| | | .tab-container { |
| | | // border-bottom: 1px solid #e8e8e8; |
| | | } |
| | | :deep(.n-tabs-wrapper){ |
| | | padding: 0 10px; |
| | | } |
| | | |
| | | :deep(.n-tabs-nav--line .n-tabs-tab) { |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | :deep(.n-tabs-nav--line .n-tabs-tab:hover) { |
| | | color: #0068FF; |
| | | } |
| | | |
| | | :deep(.n-tabs-nav--line .n-tabs-tab--active) { |
| | | font-weight: 600; |
| | | } |
| | | |
| | | :deep(.n-tabs-nav--line .n-tabs-tab--active::after) { |
| | | height: 2px; |
| | | background-color: #0068FF; |
| | | border-radius: 2px; |
| | | } |
| | | |
| | | /* 修复富文本编辑器超链接弹窗被遮挡的问题 */ |
| | | .ql-tooltip { |
| | | z-index: 2000 !important; |
| | | left:-10px !important; |
| | | } |
| | | |
| | | /* 允许内容溢出,防止 tooltip 被裁剪 */ |
| | | .n-tabs .n-tab-pane, |
| | | .n-card > .n-card__content { |
| | | overflow: visible !important; |
| | | } |
| | | .n-tabs-tab__label{ |
| | | font-size: 14px; |
| | | } |
| | | .head { |
| | | font-family: PingFang SC; |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | line-height: 24px; |
| | | letter-spacing: normal; |
| | | color: #171F26; |
| | | position: relative; |
| | | text-indent: 11px; |
| | | margin-bottom: 16px; |
| | | |
| | | &::before { |
| | | content: ''; |
| | | position: absolute; |
| | | top: 50%; |
| | | left: 0; |
| | | transform: translateY(-50%); |
| | | width: 3px; |
| | | height: 14px; |
| | | z-index: 0; |
| | | border-radius: 3px; |
| | | background: #0068FF; |
| | | } |
| | | |
| | | } |
| | | .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; |
| | | |
| | | } |
| | | </style> |
| File was renamed from src/views/userContent/contentCenter/index.vue |
| | |
| | | getAgreementContent, |
| | | getFaqList, |
| | | getGuideList, |
| | | getVehicleDescription, |
| | | saveAgreement, |
| | | saveFaq, |
| | | saveGuide, |
| | | saveVehicleDescription, |
| | | } from '@/service/api/contentCenter' |
| | | import VehiclePrepareForm from './components/VehiclePrepareForm.vue' |
| | | import SimpleRichTextForm from './components/SimpleRichTextForm.vue' |
| | | import AgreementForm from './components/AgreementForm.vue' |
| | | import OperationAppAgreementForm from './components/OperationAppAgreementForm.vue' |
| | | import AgreementListForm from './components/AgreementListForm.vue' |
| | | import FaqForm from './components/FaqForm.vue' |
| | | import ContentForm from './components/ContentForm.vue' |
| | | |
| | |
| | | |
| | | const tabs = [ |
| | | { key: 'guide', label: '乘车指南' }, |
| | | { key: 'prepare', label: '车辆整备' }, |
| | | { key: 'charge', label: '车辆充电' }, |
| | | { key: 'inspect', label: '车辆检测' }, |
| | | { key: 'insurance', label: '保险说明' }, |
| | | { key: 'annual', label: '年检说明' }, |
| | | { key: 'maintenance', label: '车辆保养' }, |
| | | { key: 'qualification', label: '运营资质' }, |
| | | { key: 'notice', label: '协议' }, |
| | | { key: 'opsAppNotice', label: '运维APP' }, |
| | | { key: 'faq', label: '常见问题' }, |
| | | { key: 'prepaymentNotice', label: '预付款协议' }, |
| | | { key: 'paymentNotice', label: '付款协议' }, |
| | | ] |
| | | |
| | | const contentFormRefs = ref<Record<string, any>>({}) |
| | |
| | | await currentForm.validate() |
| | | const data = currentForm.getData() |
| | | let saveRes: any = null |
| | | if (activeTab.value === 'prepare') { |
| | | const map: Record<string, number> = { |
| | | tirePressure: 1, |
| | | lighting: 2, |
| | | bodyClean: 3, |
| | | cabinClean: 4, |
| | | lostItems: 5, |
| | | } |
| | | const activeKey = typeof currentForm.getActiveKey === 'function' ? currentForm.getActiveKey() : 'tirePressure' |
| | | const t = map[activeKey] ?? 1 |
| | | const content = data[activeKey] || '' |
| | | saveRes = await saveVehicleDescription({ descriptionType: t, descriptionContent: content }) |
| | | } |
| | | else if (['charge', 'inspect', 'insurance', 'annual', 'maintenance', 'qualification'].includes(activeTab.value)) { |
| | | const typeMap: Record<string, number> = { |
| | | charge: 6, |
| | | inspect: 7, |
| | | insurance: 8, |
| | | annual: 9, |
| | | maintenance: 10, |
| | | qualification: 11, |
| | | } |
| | | const t = typeMap[activeTab.value] |
| | | saveRes = await saveVehicleDescription({ descriptionType: t, descriptionContent: data.content || '' }) |
| | | } |
| | | else if (activeTab.value === 'notice') { |
| | | |
| | | if (activeTab.value === 'notice') { |
| | | const map: Record<string, number> = { |
| | | privacyPolicy: 1, |
| | | personalInfoList: 2, |
| | |
| | | const content = data[activeKey] || '' |
| | | saveRes = await saveAgreement({ content, contentType: t }) |
| | | } |
| | | else if (activeTab.value === 'opsAppNotice') { |
| | | const map: Record<string, number> = { |
| | | operationPrivacyPolicy: 8, |
| | | thirdPartyShareList: 9, |
| | | personalInfoCollectList: 10, |
| | | permissionCall: 11, |
| | | else if (['prepaymentNotice', 'paymentNotice'].includes(activeTab.value)) { |
| | | // TODO: 确认预付款协议和付款协议的 contentType,这里暂定为 12 和 13 |
| | | const typeMap: Record<string, number> = { |
| | | prepaymentNotice: 12, |
| | | paymentNotice: 13, |
| | | } |
| | | const activeKey = typeof currentForm.getActiveKey === 'function' ? currentForm.getActiveKey() : 'operationPrivacyPolicy' |
| | | const t = map[activeKey] ?? 8 |
| | | const content = data[activeKey] || '' |
| | | saveRes = await saveAgreement({ content, contentType: t }) |
| | | const t = typeMap[activeTab.value] |
| | | saveRes = await saveAgreement({ content: data.content || '', contentType: t }) |
| | | } |
| | | else if (activeTab.value === 'guide') { |
| | | if (data.contentType === 'steps') { |
| | |
| | | guideStep: idx + 1, |
| | | guideType: 1, |
| | | } |
| | | // if (s.recordId) obj.id = s.recordId |
| | | return obj |
| | | }) |
| | | saveRes = await saveGuide({ guideList }) |
| | |
| | | guideStep: 1, |
| | | guideType: 2, |
| | | } |
| | | // if (data.recordId) obj.id = data.recordId |
| | | const guideList = [obj] |
| | | saveRes = await saveGuide({ guideList }) |
| | | } |
| | |
| | | .filter((it: any) => it.questions || it.answer) |
| | | saveRes = await saveFaq({ frequentlyAskedQuestionsList }) |
| | | } |
| | | |
| | | if (saveRes && saveRes.code === 200) { |
| | | message.success('保存成功') |
| | | } |
| | |
| | | loading.value = false |
| | | return |
| | | } |
| | | if (tabKey === 'prepare') { |
| | | const types = [1, 2, 3, 4, 5] |
| | | const res = await Promise.all(types.map(t => getVehicleDescription(t))) |
| | | if (res.every(r => r && r.code === 200)) { |
| | | const data: Record<string, string> = { |
| | | tirePressure: res[0].data?.descriptionContent || '', |
| | | lighting: res[1].data?.descriptionContent || '', |
| | | bodyClean: res[2].data?.descriptionContent || '', |
| | | cabinClean: res[3].data?.descriptionContent || '', |
| | | lostItems: res[4].data?.descriptionContent || '', |
| | | } |
| | | currentForm.setData(data) |
| | | } |
| | | } |
| | | else if (['charge', 'inspect', 'insurance', 'annual', 'maintenance', 'qualification'].includes(tabKey)) { |
| | | const typeMap: Record<string, number> = { |
| | | charge: 6, |
| | | inspect: 7, |
| | | insurance: 8, |
| | | annual: 9, |
| | | maintenance: 10, |
| | | qualification: 11, |
| | | } |
| | | const t = typeMap[tabKey] |
| | | const res = await getVehicleDescription(t) |
| | | if (res && res.code === 200) { |
| | | currentForm.setData({ content: res.data?.descriptionContent || '' }) |
| | | } |
| | | } |
| | | else if (tabKey === 'notice') { |
| | | |
| | | if (tabKey === 'notice') { |
| | | const types = [1, 2, 3, 4, 5, 7] |
| | | const res = await Promise.all(types.map(t => getAgreementContent(t))) |
| | | if (res.every(r => r && r.code === 200)) { |
| | |
| | | currentForm.setData(data) |
| | | } |
| | | } |
| | | else if (tabKey === 'opsAppNotice') { |
| | | const types = [8, 9, 10, 11] |
| | | const res = await Promise.all(types.map(t => getAgreementContent(t))) |
| | | if (res.every(r => r && r.code === 200)) { |
| | | const data = { |
| | | operationPrivacyPolicy: res[0].data?.content || '', |
| | | thirdPartyShareList: res[1].data?.content || '', |
| | | personalInfoCollectList: res[2].data?.content || '', |
| | | permissionCall: res[3].data?.content || '', |
| | | } |
| | | currentForm.setData(data) |
| | | else if (['prepaymentNotice', 'paymentNotice'].includes(tabKey)) { |
| | | // TODO: 确认预付款协议和付款协议的 contentType,这里暂定为 12 和 13 |
| | | const typeMap: Record<string, number> = { |
| | | prepaymentNotice: 12, |
| | | paymentNotice: 13, |
| | | } |
| | | const t = typeMap[tabKey] |
| | | const res = await getAgreementContent(t) |
| | | if (res && res.code === 200) { |
| | | currentForm.setData({ content: res.data?.content || '' }) |
| | | } |
| | | } |
| | | else if (tabKey === 'faq') { |
| | |
| | | } |
| | | } |
| | | catch { |
| | | // 兼容老数据:直接就是一个 URL 字符串 |
| | | const url = String(img) |
| | | const name = url.split('/').pop() || '图片.jpg' |
| | | return [{ id: '1', name, url }] |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="h-full flex flex-col justify-between "> |
| | | <div class="h-full flex flex-col justify-between"> |
| | | <div class="p-12px content-page"> |
| | | <n-card> |
| | | <div> |
| | | <div class="head"> |
| | | 内容中心 |
| | | 乘客端内容管理 |
| | | </div> |
| | | <n-tabs |
| | | v-model:value="activeTab" |
| | |
| | | > |
| | | <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;"> |
| | | <VehiclePrepareForm |
| | | v-if="tab.key === 'prepare'" |
| | | :ref="(el) => { if (el) contentFormRefs[tab.key] = el }" |
| | | /> |
| | | <SimpleRichTextForm |
| | | v-else-if="tab.key === 'charge' || tab.key === 'inspect' || tab.key === 'insurance' || tab.key === 'annual' || tab.key === 'maintenance' || tab.key === 'qualification'" |
| | | :ref="(el) => { if (el) contentFormRefs[tab.key] = el }" |
| | | /> |
| | | <AgreementForm |
| | | v-else-if="tab.key === 'notice'" |
| | | :ref="(el) => { if (el) contentFormRefs[tab.key] = el }" |
| | | /> |
| | | <OperationAppAgreementForm |
| | | v-else-if="tab.key === 'opsAppNotice'" |
| | | <AgreementListForm |
| | | v-if="tab.key === 'notice'" |
| | | type="passenger" |
| | | :ref="(el) => { if (el) contentFormRefs[tab.key] = el }" |
| | | /> |
| | | <FaqForm |
| | | v-else-if="tab.key === 'faq'" |
| | | :ref="(el) => { if (el) contentFormRefs[tab.key] = el }" |
| | | /> |
| | | <SimpleRichTextForm |
| | | v-else-if="['prepaymentNotice', 'paymentNotice'].includes(tab.key)" |
| | | :ref="(el) => { if (el) contentFormRefs[tab.key] = el }" |
| | | /> |
| | | <ContentForm |
| | | v-else |
| | | :ref="(el) => { if (el) contentFormRefs[tab.key] = el }" |