| package-lock.json | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| package.json | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/components/custom/Editor/AiRichTextEditor/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/assetManagement/vehicleManagement/operatingVehicle/components/PreparationDetailModal.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/businessOperations/geofenceManagement/detail.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/transaction/pricingManagement/rules.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/userContent/active/coupon/add.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/userContent/contentCenter/components/AgreementForm.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/userContent/contentCenter/components/OperationAppAgreementForm.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/userContent/contentCenter/components/SimpleRichTextForm.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/userContent/contentCenter/components/VehiclePrepareForm.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
package-lock.json
Diff too large package.json
@@ -18,6 +18,7 @@ }, "dependencies": { "@vueuse/core": "^13.6.0", "aieditor": "^1.4.2", "alova": "^3.3.4", "colord": "^2.9.3", "crypto-js": "^4.2.0", src/components/custom/Editor/AiRichTextEditor/index.vue
New file @@ -0,0 +1,381 @@ <script setup lang="ts"> import type { AiEditor as AiEditorInstance, AiEditorOptions, Uploader } from 'aieditor' import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { AiEditor } from 'aieditor' import 'aieditor/dist/style.css' import { uploadSingleFile } from '@/utils/upload' defineOptions({ name: 'AiRichTextEditor', }) interface Props { disabled?: boolean readOnly?: boolean value?: string placeholder?: string height?: string toolbarKeys?: AiEditorOptions['toolbarKeys'] } const props = withDefaults(defineProps<Props>(), { disabled: false, readOnly: false, value: '', placeholder: '请输入内容...', height: '360px', toolbarKeys: () => [ 'undo', 'redo', 'divider', 'heading', 'font-family', 'font-size', 'divider', 'bold', 'italic', 'underline', 'strike', 'link', 'divider', 'align', 'bullet-list', 'ordered-list', 'divider', 'image', 'video', 'attachment', 'quote', 'table', 'fullscreen', ], }) const emit = defineEmits<{ change: [value: string] input: [value: string] focus: [] blur: [] ready: [editor: AiEditorInstance] }>() const model = defineModel<string>() const editorRef = ref<HTMLElement | null>(null) const editor = ref<AiEditorInstance | null>(null) const lastHtml = ref('') const isUnmounted = ref(false) const isApplyingExternalContent = ref(false) let applyContentVersion = 0 const commonUploader: Uploader = async (file) => { const res = await uploadSingleFile(file) return { errorCode: 0, data: { src: res.url, href: res.url, fileName: res.name || file.name, alt: res.name || file.name, poster: '', }, } } function createUploadConfig() { return { uploadUrl: '/api/common/upload', uploadHeaders: {}, uploadFormName: 'file', uploader: commonUploader, } } function appendStyle(originStyle: string | null, styleText: string) { const style = (originStyle || '').trim() if (!style) return styleText return style.endsWith(';') ? `${style}${styleText}` : `${style};${styleText}` } function removeStyleProperties(originStyle: string | null, properties: string[]) { let style = (originStyle || '').trim() for (const property of properties) { const reg = new RegExp(`${property}\\s*:\\s*[^;]+;?`, 'gi') style = style.replace(reg, '') } return style.replace(/\s{2,}/g, ' ').trim() } function formatTableForMiniProgram(html: string) { if (!html || typeof document === 'undefined') return html || '' const container = document.createElement('div') container.innerHTML = html container.querySelectorAll('table').forEach((table) => { table.removeAttribute('width') table.setAttribute('border', '1') table.setAttribute('cellspacing', '0') table.setAttribute('cellpadding', '0') table.setAttribute( 'style', appendStyle( removeStyleProperties(table.getAttribute('style'), ['width', 'min-width', 'max-width']), 'width:100%;max-width:100%;border-collapse:collapse;table-layout:fixed;box-sizing:border-box;', ), ) table.querySelectorAll('colgroup, col').forEach((col) => { col.removeAttribute('width') const style = removeStyleProperties(col.getAttribute('style'), ['width', 'min-width', 'max-width']) if (style) col.setAttribute('style', style) else col.removeAttribute('style') }) table.querySelectorAll('th, td').forEach((cell) => { cell.removeAttribute('width') cell.setAttribute( 'style', appendStyle( removeStyleProperties(cell.getAttribute('style'), ['width', 'min-width', 'max-width']), 'width:auto;max-width:100%;border:1px solid #d9d9d9;padding:8px;vertical-align:top;word-break:break-all;overflow-wrap:anywhere;box-sizing:border-box;', ), ) }) table.querySelectorAll('th').forEach((headerCell) => { headerCell.setAttribute( 'style', appendStyle( headerCell.getAttribute('style'), 'font-weight:600;background:#f5f5f5;', ), ) }) }) return container.innerHTML } function syncModelValue(html: string) { const formattedHtml = formatTableForMiniProgram(html) if (isApplyingExternalContent.value) { lastHtml.value = formattedHtml return } if (formattedHtml === lastHtml.value) return lastHtml.value = formattedHtml if (model.value !== undefined) model.value = formattedHtml emit('input', formattedHtml) emit('change', formattedHtml) } function destroyEditor() { editor.value?.destroy() editor.value = null if (editorRef.value) editorRef.value.innerHTML = '' } function getIncomingContent() { return model.value ?? props.value ?? '' } function initEditor(content = getIncomingContent()) { if (!editorRef.value || editor.value || isUnmounted.value) return const initialContent = formatTableForMiniProgram(content || '') const instance = new AiEditor({ element: editorRef.value, placeholder: props.placeholder, content: initialContent, editable: !(props.disabled || props.readOnly), toolbarKeys: props.toolbarKeys, toolbarExcludeKeys: ['ai'], ai: false, draggable: false, image: createUploadConfig(), video: createUploadConfig(), attachment: createUploadConfig(), onChange: (currentEditor) => { syncModelValue(currentEditor.getHtml()) }, onFocus: () => { emit('focus') }, onBlur: () => { emit('blur') }, onCreated: (currentEditor) => { emit('ready', currentEditor) }, }) editor.value = instance lastHtml.value = initialContent } async function rebuildEditor(content: string, focus = false) { if (isUnmounted.value) return const formattedContent = formatTableForMiniProgram(content || '') if (formattedContent === lastHtml.value && editor.value) { lastHtml.value = formattedContent return } const version = ++applyContentVersion isApplyingExternalContent.value = true destroyEditor() await nextTick() if (isUnmounted.value || version !== applyContentVersion) return initEditor(formattedContent) if (focus) editor.value?.focus() requestAnimationFrame(() => { if (version === applyContentVersion) isApplyingExternalContent.value = false }) } async function setContent(content: string, focus = false) { await rebuildEditor(content, focus) } function clear() { destroyEditor() if (!isUnmounted.value && editorRef.value) initEditor('') lastHtml.value = '' syncModelValue('') } function focus() { editor.value?.focus() } function blur() { editor.value?.blur() } function getContent() { return editor.value?.getHtml() || '' } function getEditor() { return editor.value } watch( () => model.value, (value) => { if (value === undefined) return const html = value || '' if (html === lastHtml.value) return void rebuildEditor(html) }, { flush: 'post' }, ) watch( () => props.value, (value) => { if (model.value !== undefined) return const html = value || '' if (html === lastHtml.value) return void rebuildEditor(html) }, { flush: 'post' }, ) watch( () => [props.disabled, props.readOnly], (value) => { const [disabled, readOnly] = value editor.value?.setEditable(!(disabled || readOnly)) }, ) watch( () => props.placeholder, () => { const html = getIncomingContent() || lastHtml.value void rebuildEditor(html) }, ) onMounted(() => { isUnmounted.value = false void nextTick(() => { initEditor() }) }) onBeforeUnmount(() => { isUnmounted.value = true destroyEditor() }) defineExpose({ clear, blur, focus, setContent, getContent, getEditor, }) </script> <template> <div class="ai-editor-container"> <div ref="editorRef" class="ai-editor-host" :style="{ minHeight: height }" /> </div> </template> <style scoped> .ai-editor-container { width: 100%; border-radius: 4px; background: #fff; } .ai-editor-host { width: 100%; } :deep(aie-footer) { display: none !important; } :deep(.aie-container) { border-radius: 4px; } :deep(.aie-main) { min-height: v-bind(height); } </style> src/views/assetManagement/vehicleManagement/operatingVehicle/components/PreparationDetailModal.vue
@@ -78,7 +78,7 @@ <div class="value">{{ detailData.operator || "-" }}</div> </div> <div style="flex: 1"> <div class="label">送修时间</div> <div class="label">整备时间</div> <div class="value"> {{ detailData.operationTime @@ -127,7 +127,7 @@ </div> </div> </div> <div style="flex: 1"> <!-- <div style="flex: 1"> <div class="label">雾灯情况</div> <div class="flex gap-16px" @@ -148,9 +148,32 @@ /> </div> </div> </div> --> <div style="flex: 1"> <div class="label">转向灯情况</div> <div class="flex gap-16px" v-if=" detailData.turnSignalImages && detailData.turnSignalImages.length " > <div class="pic" v-for="(img, idx) in detailData.turnSignalImages" :key="idx" > <NImage :src="img" width="56" height="56" fallback-src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png" /> </div> </div> </div> </div> <div class="flex mt-12px"> <!-- <div class="flex mt-12px"> <div style="flex: 1"> <div class="label">转向灯情况</div> <div @@ -197,7 +220,7 @@ </div> </div> </div> </div> </div> --> <div class="flex mt-12px"> <div style="flex: 1"> src/views/businessOperations/geofenceManagement/detail.vue
@@ -55,6 +55,7 @@ const isDrawing = ref(false) // 是否正在绘制 const isEditing = ref(false) // 是否正在编辑 const isDisallowedAreaDrawing = ref(false) // 是否正在绘制禁行区围栏 const fenceEditorClosed = ref(false) // 围栏编辑器是否已关闭 // ==================== 电子围栏相关 ==================== // 电子围栏详情 @@ -230,8 +231,10 @@ // 添加地图点击事件监听器,用于处理点击运营区域时的编辑状态切换 map.on('click', (e: any) => { // layerOptions.isActive = false if (isDrawing.value) if (isDrawing.value) { stopPoint() return } // 检查是否点击在运营区域(主围栏)内或者点击在禁行区内 const point = [e.lnglat.lng, e.lnglat.lat] const isPointInFence = fence && (!isDrawing.value || modalType.value === 'detail') && isPointInPolygon(point, fence.getPath()) @@ -359,6 +362,19 @@ fence.setPath(path) } function stopPoint() { if (!isDisallowedAreaDrawing.value) return const fence = mouseTool?.polygonTemp?.obj const path = fence?.getPath() || [] if (path.length >= MAX_VERTICES) { message.error(`禁行区顶点数量不能超过${MAX_VERTICES}个`) path.splice(path.length - 2, 2) path.push(path[path.length - 1]) fence.setPath(path) } } function cancelDrawing() { const fence = mouseTool?.polygonTemp?.obj const path = fence.getPath() @@ -436,6 +452,7 @@ return } isDrawing.value = true isDisallowedAreaDrawing.value = true mouseTool = new AMap.MouseTool(map) // 绘制多边形 mouseTool.polygon({ @@ -453,6 +470,7 @@ mouseTool?.close() mouseTool = null isDrawing.value = false isDisallowedAreaDrawing.value = false // 获取围栏坐标 const path = e.obj.getPath().map((p: any) => [p.lng, p.lat]) if (path.length < 3) { @@ -1695,7 +1713,7 @@ fetchElectronicFenceAreaList().then((res) => { if (res.code === 200) { if (modalType.value !== 'add') { fenceAreaList.value = res.data.filter((item: any) => item.electronicId !== Number(routeId.value)) fenceAreaList.value = res.data.filter((item: any) => item.electronicId !== routeId.value && item.electronicId !== Number(routeId.value)) } else { fenceAreaList.value = res.data @@ -2153,7 +2171,7 @@ </div> </template> <div v-if="isActiceDisallowedArea && isDrawing" class="tips"> 请在地图上【蓝色运营区域】绘制多边形区域,注意禁行区顶点数量不能超过15个,双击闭合 请在地图上【蓝色运营区域】绘制多边形区域,双击闭合 </div> <!-- 抽屉 --> <div src/views/transaction/pricingManagement/rules.vue
@@ -41,10 +41,10 @@ <template> <div class="w-full h-full relative content"> <div class="p-12px h-full"> <div class="box py-16px! title mb-12px h-full"> <div class="box py-16px! mb-12px h-full"> <div class="head">计费规则说明</div> <div class="h-400px"> <RichTextEditor v-model="text" :disabled="false" /> <div class="rich-text-wrapper"> <AiRichTextEditor v-model="text" height="300px" /> </div> </div> </div> @@ -80,12 +80,6 @@ width: 100%; box-sizing: border-box; --n-label-text-color: rgba(23, 31, 38, 0.8); } .title { font-size: 20px; color: #171F26; font-weight: 600; } .head { @@ -125,4 +119,12 @@ padding: 16px 24px; } </style> .rich-text-wrapper :deep(.aie-container) { min-height: 300px; font-size: 14px; } .rich-text-wrapper :deep(.aie-main) { min-height: 300px; } </style> src/views/userContent/active/coupon/add.vue
@@ -3,7 +3,7 @@ import { useRoute, useRouter } from 'vue-router' import type { FormInst, FormRules } from 'naive-ui' import { useDialog, useMessage } from 'naive-ui' import RichTextEditor from '@/components/custom/Editor/RichTextEditor/index.vue' import AiRichTextEditor from '@/components/custom/Editor/AiRichTextEditor/index.vue' import { addCoupon, fetchCouponDetail } from '@/service/api/coupon' import { useTabStore } from '@/store' @@ -589,9 +589,9 @@ <div class="form-row full-width"> <n-form-item label="优惠券说明" path="description" :required="!isReadonly" label-placement="top"> <div class="w-full"> <div class="w-full rich-text-wrapper"> <template v-if="!isReadonly"> <RichTextEditor v-model="formModel.description" style="height: 300px" /> <AiRichTextEditor v-model="formModel.description" height="300px" /> </template> <div v-else class="rich-text-content" v-html="formModel.description" /> </div> @@ -771,4 +771,13 @@ .rich-text-content :deep(p:last-child) { margin-bottom: 0; } .rich-text-wrapper :deep(.aie-container) { min-height: 300px; font-size: 14px; } .rich-text-wrapper :deep(.aie-main) { min-height: 300px; } </style> src/views/userContent/contentCenter/components/AgreementForm.vue
@@ -1,7 +1,7 @@ <script setup lang="ts"> import { computed, ref } from 'vue' import type { FormInst, FormRules } from 'naive-ui' import RichTextEditor from '@/components/custom/Editor/RichTextEditor/index.vue' import AiRichTextEditor from '@/components/custom/Editor/AiRichTextEditor/index.vue' interface FormModel { privacyPolicy: string @@ -51,6 +51,14 @@ return r }) const currentContent = computed({ get: () => formModel.value[activeSubTab.value as keyof FormModel], set: (value: string) => { const key = activeSubTab.value as keyof FormModel formModel.value[key] = value }, }) async function validate() { return await formRef.value?.validate() } @@ -98,39 +106,9 @@ label-width="80" require-mark-placement="left" > <n-form-item v-show="activeSubTab === 'privacyPolicy'" label="" path="privacyPolicy" required> <n-form-item label="" :path="activeSubTab" required> <div class="rich-text-wrapper"> <RichTextEditor v-model="formModel.privacyPolicy" /> </div> </n-form-item> <n-form-item v-show="activeSubTab === 'personalInfoList'" label="" path="personalInfoList" required> <div class="rich-text-wrapper"> <RichTextEditor v-model="formModel.personalInfoList" /> </div> </n-form-item> <n-form-item v-show="activeSubTab === 'sensitiveInfoDesc'" label="" path="sensitiveInfoDesc" required> <div class="rich-text-wrapper"> <RichTextEditor v-model="formModel.sensitiveInfoDesc" /> </div> </n-form-item> <n-form-item v-show="activeSubTab === 'thirdPartyInfoDesc'" label="" path="thirdPartyInfoDesc" required> <div class="rich-text-wrapper"> <RichTextEditor v-model="formModel.thirdPartyInfoDesc" /> </div> </n-form-item> <n-form-item v-show="activeSubTab === 'rideNotice'" label="" path="rideNotice" required> <div class="rich-text-wrapper"> <RichTextEditor v-model="formModel.rideNotice" /> </div> </n-form-item> <n-form-item v-show="activeSubTab === 'permissionNotice'" label="" path="permissionNotice" required> <div class="rich-text-wrapper"> <RichTextEditor v-model="formModel.permissionNotice" /> <AiRichTextEditor v-model="currentContent" height="300px" /> </div> </n-form-item> </n-form> @@ -142,12 +120,12 @@ width: 100%; } .rich-text-wrapper :deep(.ql-container) { .rich-text-wrapper :deep(.aie-container) { min-height: 300px; font-size: 14px; } .rich-text-wrapper :deep(.ql-editor) { .rich-text-wrapper :deep(.aie-main) { min-height: 300px; } .tabs { src/views/userContent/contentCenter/components/OperationAppAgreementForm.vue
@@ -1,7 +1,7 @@ <script setup lang="ts"> import { computed, ref } from 'vue' import type { FormInst, FormRules } from 'naive-ui' import RichTextEditor from '@/components/custom/Editor/RichTextEditor/index.vue' import AiRichTextEditor from '@/components/custom/Editor/AiRichTextEditor/index.vue' interface FormModel { operationPrivacyPolicy: string @@ -41,6 +41,14 @@ r[key] = [{ required: true, message: map[key], trigger: 'blur' }] } return r }) const currentContent = computed({ get: () => formModel.value[activeSubTab.value as keyof FormModel], set: (value: string) => { const key = activeSubTab.value as keyof FormModel formModel.value[key] = value }, }) async function validate() { @@ -86,27 +94,9 @@ label-width="80" require-mark-placement="left" > <n-form-item v-show="activeSubTab === 'operationPrivacyPolicy'" label="" path="operationPrivacyPolicy" required> <n-form-item label="" :path="activeSubTab" required> <div class="rich-text-wrapper"> <RichTextEditor v-model="formModel.operationPrivacyPolicy" /> </div> </n-form-item> <n-form-item v-show="activeSubTab === 'thirdPartyShareList'" label="" path="thirdPartyShareList" required> <div class="rich-text-wrapper"> <RichTextEditor v-model="formModel.thirdPartyShareList" /> </div> </n-form-item> <n-form-item v-show="activeSubTab === 'personalInfoCollectList'" label="" path="personalInfoCollectList" required> <div class="rich-text-wrapper"> <RichTextEditor v-model="formModel.personalInfoCollectList" /> </div> </n-form-item> <n-form-item v-show="activeSubTab === 'permissionCall'" label="" path="permissionCall" required> <div class="rich-text-wrapper"> <RichTextEditor v-model="formModel.permissionCall" /> <AiRichTextEditor v-model="currentContent" height="300px" /> </div> </n-form-item> </n-form> @@ -118,12 +108,12 @@ width: 100%; } .rich-text-wrapper :deep(.ql-container) { .rich-text-wrapper :deep(.aie-container) { min-height: 300px; font-size: 14px; } .rich-text-wrapper :deep(.ql-editor) { .rich-text-wrapper :deep(.aie-main) { min-height: 300px; } .tabs { src/views/userContent/contentCenter/components/SimpleRichTextForm.vue
@@ -1,7 +1,7 @@ <script setup lang="ts"> import { ref } from 'vue' import type { FormInst, FormRules } from 'naive-ui' import RichTextEditor from '@/components/custom/Editor/RichTextEditor/index.vue' import AiRichTextEditor from '@/components/custom/Editor/AiRichTextEditor/index.vue' interface FormModel { content: string @@ -45,7 +45,7 @@ > <n-form-item label="" path="content" required> <div class="rich-text-wrapper"> <RichTextEditor v-model="formModel.content" /> <AiRichTextEditor v-model="formModel.content" height="300px" /> </div> </n-form-item> </n-form> @@ -57,12 +57,12 @@ width: 100%; } .rich-text-wrapper :deep(.ql-container) { .rich-text-wrapper :deep(.aie-container) { min-height: 300px; font-size: 14px; } .rich-text-wrapper :deep(.ql-editor) { .rich-text-wrapper :deep(.aie-main) { min-height: 300px; } </style> src/views/userContent/contentCenter/components/VehiclePrepareForm.vue
@@ -1,7 +1,7 @@ <script setup lang="ts"> import { computed, ref } from 'vue' import type { FormInst, FormRules } from 'naive-ui' import RichTextEditor from '@/components/custom/Editor/RichTextEditor/index.vue' import AiRichTextEditor from '@/components/custom/Editor/AiRichTextEditor/index.vue' interface FormModel { tirePressure: string @@ -45,6 +45,14 @@ r[key] = [{ required: true, message: map[key], trigger: 'blur' }] } return r }) const currentContent = computed({ get: () => formModel.value[activeSubTab.value as keyof FormModel], set: (value: string) => { const key = activeSubTab.value as keyof FormModel formModel.value[key] = value }, }) async function validate() { @@ -92,33 +100,9 @@ label-width="80" require-mark-placement="left" > <n-form-item v-show="activeSubTab === 'tirePressure'" label="" path="tirePressure" required> <n-form-item label="" :path="activeSubTab" required> <div class="rich-text-wrapper"> <RichTextEditor v-model="formModel.tirePressure" /> </div> </n-form-item> <n-form-item v-show="activeSubTab === 'lighting'" label="" path="lighting" required> <div class="rich-text-wrapper"> <RichTextEditor v-model="formModel.lighting" /> </div> </n-form-item> <n-form-item v-show="activeSubTab === 'bodyClean'" label="" path="bodyClean" required> <div class="rich-text-wrapper"> <RichTextEditor v-model="formModel.bodyClean" /> </div> </n-form-item> <n-form-item v-show="activeSubTab === 'cabinClean'" label="" path="cabinClean" required> <div class="rich-text-wrapper"> <RichTextEditor v-model="formModel.cabinClean" /> </div> </n-form-item> <n-form-item v-show="activeSubTab === 'lostItems'" label="" path="lostItems" required> <div class="rich-text-wrapper"> <RichTextEditor v-model="formModel.lostItems" /> <AiRichTextEditor v-model="currentContent" height="300px" /> </div> </n-form-item> </n-form> @@ -130,12 +114,12 @@ width: 100%; } .rich-text-wrapper :deep(.ql-container) { .rich-text-wrapper :deep(.aie-container) { min-height: 300px; font-size: 14px; } .rich-text-wrapper :deep(.ql-editor) { .rich-text-wrapper :deep(.aie-main) { min-height: 300px; } .tabs {