| package-lock.json | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| package.json | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/components/custom/Editor/AiRichTextEditor/index.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/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' @@ -414,9 +414,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> @@ -596,4 +596,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 {