🎨 优化富文本编辑器,完善全屏功能
This commit is contained in:
@@ -76,6 +76,8 @@
|
|||||||
"sass": "^1.78.0",
|
"sass": "^1.78.0",
|
||||||
"terser": "^5.31.6",
|
"terser": "^5.31.6",
|
||||||
"unocss": "^66.4.2",
|
"unocss": "^66.4.2",
|
||||||
|
"unplugin-auto-import": "^0.17.8",
|
||||||
|
"unplugin-vue-components": "^0.27.5",
|
||||||
"vite": "^6.2.3",
|
"vite": "^6.2.3",
|
||||||
"vite-plugin-banner": "^0.8.0",
|
"vite-plugin-banner": "^0.8.0",
|
||||||
"vite-plugin-importer": "^0.2.5",
|
"vite-plugin-importer": "^0.2.5",
|
||||||
|
|||||||
88
src/auto-imports.d.ts
vendored
Normal file
88
src/auto-imports.d.ts
vendored
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* prettier-ignore */
|
||||||
|
// @ts-nocheck
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
// Generated by unplugin-auto-import
|
||||||
|
// biome-ignore lint: disable
|
||||||
|
export {}
|
||||||
|
declare global {
|
||||||
|
const EffectScope: typeof import('vue')['EffectScope']
|
||||||
|
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
|
||||||
|
const computed: typeof import('vue')['computed']
|
||||||
|
const createApp: typeof import('vue')['createApp']
|
||||||
|
const createPinia: typeof import('pinia')['createPinia']
|
||||||
|
const customRef: typeof import('vue')['customRef']
|
||||||
|
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||||
|
const defineComponent: typeof import('vue')['defineComponent']
|
||||||
|
const defineStore: typeof import('pinia')['defineStore']
|
||||||
|
const effectScope: typeof import('vue')['effectScope']
|
||||||
|
const getActivePinia: typeof import('pinia')['getActivePinia']
|
||||||
|
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||||
|
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||||
|
const h: typeof import('vue')['h']
|
||||||
|
const inject: typeof import('vue')['inject']
|
||||||
|
const isProxy: typeof import('vue')['isProxy']
|
||||||
|
const isReactive: typeof import('vue')['isReactive']
|
||||||
|
const isReadonly: typeof import('vue')['isReadonly']
|
||||||
|
const isRef: typeof import('vue')['isRef']
|
||||||
|
const mapActions: typeof import('pinia')['mapActions']
|
||||||
|
const mapGetters: typeof import('pinia')['mapGetters']
|
||||||
|
const mapState: typeof import('pinia')['mapState']
|
||||||
|
const mapStores: typeof import('pinia')['mapStores']
|
||||||
|
const mapWritableState: typeof import('pinia')['mapWritableState']
|
||||||
|
const markRaw: typeof import('vue')['markRaw']
|
||||||
|
const nextTick: typeof import('vue')['nextTick']
|
||||||
|
const onActivated: typeof import('vue')['onActivated']
|
||||||
|
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||||
|
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
||||||
|
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
|
||||||
|
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||||
|
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||||
|
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||||
|
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||||
|
const onMounted: typeof import('vue')['onMounted']
|
||||||
|
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||||
|
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||||
|
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||||
|
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||||
|
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||||
|
const onUpdated: typeof import('vue')['onUpdated']
|
||||||
|
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
||||||
|
const provide: typeof import('vue')['provide']
|
||||||
|
const reactive: typeof import('vue')['reactive']
|
||||||
|
const readonly: typeof import('vue')['readonly']
|
||||||
|
const ref: typeof import('vue')['ref']
|
||||||
|
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||||
|
const setActivePinia: typeof import('pinia')['setActivePinia']
|
||||||
|
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
|
||||||
|
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||||
|
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||||
|
const shallowRef: typeof import('vue')['shallowRef']
|
||||||
|
const storeToRefs: typeof import('pinia')['storeToRefs']
|
||||||
|
const toRaw: typeof import('vue')['toRaw']
|
||||||
|
const toRef: typeof import('vue')['toRef']
|
||||||
|
const toRefs: typeof import('vue')['toRefs']
|
||||||
|
const toValue: typeof import('vue')['toValue']
|
||||||
|
const triggerRef: typeof import('vue')['triggerRef']
|
||||||
|
const unref: typeof import('vue')['unref']
|
||||||
|
const useAttrs: typeof import('vue')['useAttrs']
|
||||||
|
const useCssModule: typeof import('vue')['useCssModule']
|
||||||
|
const useCssVars: typeof import('vue')['useCssVars']
|
||||||
|
const useId: typeof import('vue')['useId']
|
||||||
|
const useLink: typeof import('vue-router')['useLink']
|
||||||
|
const useModel: typeof import('vue')['useModel']
|
||||||
|
const useRoute: typeof import('vue-router')['useRoute']
|
||||||
|
const useRouter: typeof import('vue-router')['useRouter']
|
||||||
|
const useSlots: typeof import('vue')['useSlots']
|
||||||
|
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
||||||
|
const watch: typeof import('vue')['watch']
|
||||||
|
const watchEffect: typeof import('vue')['watchEffect']
|
||||||
|
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||||
|
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||||
|
}
|
||||||
|
// for type re-export
|
||||||
|
declare global {
|
||||||
|
// @ts-ignore
|
||||||
|
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||||
|
import('vue')
|
||||||
|
}
|
||||||
116
src/components.d.ts
vendored
Normal file
116
src/components.d.ts
vendored
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
// Generated by unplugin-vue-components
|
||||||
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
|
export {}
|
||||||
|
|
||||||
|
/* prettier-ignore */
|
||||||
|
declare module 'vue' {
|
||||||
|
export interface GlobalComponents {
|
||||||
|
Application: typeof import('./components/application/index.vue')['default']
|
||||||
|
ArrayCtrl: typeof import('./components/arrayCtrl/arrayCtrl.vue')['default']
|
||||||
|
BottomInfo: typeof import('./components/bottomInfo/bottomInfo.vue')['default']
|
||||||
|
Charts: typeof import('./components/charts/index.vue')['default']
|
||||||
|
ColumnItem: typeof import('./components/columnItem/ColumnItem.vue')['default']
|
||||||
|
CommandMenu: typeof import('./components/commandMenu/index.vue')['default']
|
||||||
|
Common: typeof import('./components/upload/common.vue')['default']
|
||||||
|
Content: typeof import('./components/content/index.vue')['default']
|
||||||
|
Cropper: typeof import('./components/upload/cropper.vue')['default']
|
||||||
|
CustomPic: typeof import('./components/customPic/index.vue')['default']
|
||||||
|
Docx: typeof import('./components/office/docx.vue')['default']
|
||||||
|
ElAlert: typeof import('element-plus/es')['ElAlert']
|
||||||
|
ElAside: typeof import('element-plus/es')['ElAside']
|
||||||
|
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||||
|
ElBacktop: typeof import('element-plus/es')['ElBacktop']
|
||||||
|
ElBadge: typeof import('element-plus/es')['ElBadge']
|
||||||
|
ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
|
||||||
|
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
|
||||||
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
|
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
|
||||||
|
ElCard: typeof import('element-plus/es')['ElCard']
|
||||||
|
ElCarousel: typeof import('element-plus/es')['ElCarousel']
|
||||||
|
ElCarouselItem: typeof import('element-plus/es')['ElCarouselItem']
|
||||||
|
ElCascader: typeof import('element-plus/es')['ElCascader']
|
||||||
|
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||||
|
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
|
||||||
|
ElCol: typeof import('element-plus/es')['ElCol']
|
||||||
|
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||||
|
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
||||||
|
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
||||||
|
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||||
|
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||||
|
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||||
|
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
|
||||||
|
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
|
||||||
|
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||||
|
ElDivider: typeof import('element-plus/es')['ElDivider']
|
||||||
|
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||||
|
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||||
|
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||||
|
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||||
|
ElEmpty: typeof import('element-plus/es')['ElEmpty']
|
||||||
|
ElForm: typeof import('element-plus/es')['ElForm']
|
||||||
|
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||||
|
ElHeader: typeof import('element-plus/es')['ElHeader']
|
||||||
|
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||||
|
ElImage: typeof import('element-plus/es')['ElImage']
|
||||||
|
ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
|
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||||
|
ElLink: typeof import('element-plus/es')['ElLink']
|
||||||
|
ElMain: typeof import('element-plus/es')['ElMain']
|
||||||
|
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||||
|
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||||
|
ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
|
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||||
|
ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
|
||||||
|
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||||
|
ElProgress: typeof import('element-plus/es')['ElProgress']
|
||||||
|
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||||
|
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||||
|
ElRow: typeof import('element-plus/es')['ElRow']
|
||||||
|
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||||
|
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
|
ElStatistic: typeof import('element-plus/es')['ElStatistic']
|
||||||
|
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||||
|
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||||
|
ElTable: typeof import('element-plus/es')['ElTable']
|
||||||
|
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||||
|
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||||
|
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||||
|
ElTag: typeof import('element-plus/es')['ElTag']
|
||||||
|
ElText: typeof import('element-plus/es')['ElText']
|
||||||
|
ElTimeline: typeof import('element-plus/es')['ElTimeline']
|
||||||
|
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
|
||||||
|
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||||
|
ElTransfer: typeof import('element-plus/es')['ElTransfer']
|
||||||
|
ElTree: typeof import('element-plus/es')['ElTree']
|
||||||
|
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
|
||||||
|
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||||
|
ElWatermark: typeof import('element-plus/es')['ElWatermark']
|
||||||
|
ErrorPreview: typeof import('./components/errorPreview/index.vue')['default']
|
||||||
|
Excel: typeof import('./components/office/excel.vue')['default']
|
||||||
|
ExportExcel: typeof import('./components/exportExcel/exportExcel.vue')['default']
|
||||||
|
ExportTemplate: typeof import('./components/exportExcel/exportTemplate.vue')['default']
|
||||||
|
Image: typeof import('./components/upload/image.vue')['default']
|
||||||
|
ImportExcel: typeof import('./components/exportExcel/importExcel.vue')['default']
|
||||||
|
MyTitle: typeof import('./components/MyTitle/MyTitle.vue')['default']
|
||||||
|
Office: typeof import('./components/office/index.vue')['default']
|
||||||
|
Pdf: typeof import('./components/office/pdf.vue')['default']
|
||||||
|
PmDialog: typeof import('./components/PmDialog/pm-dialog.vue')['default']
|
||||||
|
QRCode: typeof import('./components/upload/QR-code.vue')['default']
|
||||||
|
RichEdit: typeof import('./components/richtext/rich-edit.vue')['default']
|
||||||
|
RichView: typeof import('./components/richtext/rich-view.vue')['default']
|
||||||
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
SearchForm: typeof import('./components/searchForm/index.vue')['default']
|
||||||
|
SelectComponent: typeof import('./components/selectImage/selectComponent.vue')['default']
|
||||||
|
SelectFile: typeof import('./components/selectFile/selectFile.vue')['default']
|
||||||
|
SelectImage: typeof import('./components/selectImage/selectImage.vue')['default']
|
||||||
|
SvgIcon: typeof import('./components/svgIcon/svgIcon.vue')['default']
|
||||||
|
UserChoose: typeof import('./components/userChoose/index.vue')['default']
|
||||||
|
WarningBar: typeof import('./components/warningBar/warningBar.vue')['default']
|
||||||
|
}
|
||||||
|
export interface ComponentCustomProperties {
|
||||||
|
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="border border-solid border-gray-100 h-full">
|
<div ref="editorWrapper" class="rich-edit-wrapper" :class="{ 'is-fullscreen': isFullscreen }">
|
||||||
|
<!-- 独立的全屏按钮(浮在编辑器右上角) -->
|
||||||
|
<div class="fullscreen-toggle-btn" @click="toggleFullscreen" :title="isFullscreen ? '退出全屏 (ESC)' : '全屏编辑'">
|
||||||
|
<svg v-if="!isFullscreen" viewBox="0 0 1024 1024" width="18" height="18">
|
||||||
|
<path d="M290 236.4l43.9-43.9c4.7-4.7 4.7-12.3 0-17L231.4 73c-4.7-4.7-12.3-4.7-17 0L71 216.4c-4.7 4.7-4.7 12.3 0 17l43.9 43.9c4.7 4.7 12.3 4.7 17 0l100.7-100.7 57.4 57.4c4.7 4.7 12.3 4.7 17 0zM642.1 845.5L734 753.6c4.7-4.7 4.7-12.3 0-17l-43.9-43.9c-4.7-4.7-12.3-4.7-17 0l-100.7 100.7-57.4-57.4c-4.7-4.7-12.3-4.7-17 0l-43.9 43.9c-4.7 4.7-4.7 12.3 0 17L597 939.8c4.7 4.7 12.3 4.7 17 0l43.9-43.9c4.7-4.7 4.7-12.3 0-17l-57.4-57.4 41.6-42zM845.5 381.9L753.6 290c-4.7-4.7-12.3-4.7-17 0l-43.9 43.9c-4.7 4.7-4.7 12.3 0 17l100.7 100.7-57.4 57.4c-4.7 4.7-4.7 12.3 0 17l43.9 43.9c4.7 4.7 12.3 4.7 17 0L939.8 427c4.7-4.7 4.7-12.3 0-17l-43.9-43.9c-4.7-4.7-12.3-4.7-17 0l-57.4 57.4-42-41.6zM381.9 178.5L290 270.4c-4.7 4.7-4.7 12.3 0 17l43.9 43.9c4.7 4.7 12.3 4.7 17 0l100.7-100.7 57.4 57.4c4.7 4.7 12.3 4.7 17 0l43.9-43.9c4.7-4.7 4.7-12.3 0-17L427 84.2c-4.7-4.7-12.3-4.7-17 0l-43.9 43.9c-4.7 4.7-4.7 12.3 0 17l57.4 57.4-41.6 42z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
<svg v-else viewBox="0 0 1024 1024" width="18" height="18">
|
||||||
|
<path d="M204.8 819.2h204.8v204.8h-204.8v-204.8zM614.4 0h204.8v204.8h-204.8v-204.8zM0 614.4h204.8v204.8h-204.8v-204.8zM819.2 0h204.8v204.8h-204.8v-204.8z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Toolbar
|
<Toolbar
|
||||||
:editor="editorRef"
|
:editor="editorRef"
|
||||||
:default-config="toolbarConfig"
|
:default-config="toolbarConfig"
|
||||||
@@ -7,8 +17,8 @@
|
|||||||
/>
|
/>
|
||||||
<Editor
|
<Editor
|
||||||
v-model="valueHtml"
|
v-model="valueHtml"
|
||||||
class="overflow-y-hidden mt-0.5"
|
class="editor-content"
|
||||||
style="min-height: 25rem; height: 400px;"
|
:style="editorStyle"
|
||||||
:default-config="editorConfig"
|
:default-config="editorConfig"
|
||||||
mode="default"
|
mode="default"
|
||||||
@onCreated="handleCreated"
|
@onCreated="handleCreated"
|
||||||
@@ -18,352 +28,564 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import '@wangeditor/editor/dist/css/style.css' // 引入 css
|
import '@wangeditor/editor/dist/css/style.css'
|
||||||
|
|
||||||
const basePath = import.meta.env.VITE_BASE_API
|
const basePath = import.meta.env.VITE_BASE_API
|
||||||
|
|
||||||
import { onBeforeUnmount, ref, shallowRef, watch, computed } from 'vue'
|
import { onBeforeUnmount, ref, shallowRef, watch, computed, onMounted } from 'vue'
|
||||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||||
|
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { getUrl } from '@/utils/image'
|
import { getUrl } from '@/utils/image'
|
||||||
import botLogo from '@/assets/bot_logo.png'
|
import botLogo from '@/assets/bot_logo.png'
|
||||||
import { useUserStore } from '@/pinia/modules/user'
|
import { useUserStore } from '@/pinia/modules/user'
|
||||||
|
|
||||||
const emits = defineEmits(['change', 'update:modelValue'])
|
const emits = defineEmits(['change', 'update:modelValue'])
|
||||||
|
|
||||||
const change = (editor) => {
|
const change = (editor) => {
|
||||||
emits('change', editor)
|
emits('change', editor)
|
||||||
emits('update:modelValue', valueHtml.value)
|
emits('update:modelValue', valueHtml.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
useWatermark: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const props = defineProps({
|
const editorRef = shallowRef()
|
||||||
modelValue: {
|
const valueHtml = ref('')
|
||||||
type: String,
|
const userStore = useUserStore()
|
||||||
default: ''
|
const editorWrapper = ref(null)
|
||||||
|
const isFullscreen = ref(false)
|
||||||
|
const originalParent = ref(null)
|
||||||
|
const originalNextSibling = ref(null)
|
||||||
|
|
||||||
|
const editorStyle = computed(() => ({
|
||||||
|
minHeight: '25rem',
|
||||||
|
height: isFullscreen.value ? 'calc(100vh - 42px)' : '400px'
|
||||||
|
}))
|
||||||
|
|
||||||
|
const toolbarConfig = {}
|
||||||
|
|
||||||
|
// 创建自定义上传函数
|
||||||
|
const createCustomUpload = () => {
|
||||||
|
return async (file, insertFn) => {
|
||||||
|
const shouldUseWatermark = props.useWatermark
|
||||||
|
|
||||||
|
// 未开启水印则直接上传原图
|
||||||
|
if (!shouldUseWatermark) {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
const resp = await fetch(basePath + '/fileUploadAndDownload/upload?noSave=1', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'x-token': userStore.token,
|
||||||
|
'x-user-id': userStore.userInfo.ID
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const res = await resp.json()
|
||||||
|
if (res.code === 0) {
|
||||||
|
const urlPath = getUrl(res.data.file.url)
|
||||||
|
insertFn(urlPath, res.data.file.name)
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || '上传失败')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const watermarkedBlob = await addBottomWatermark(file, {
|
||||||
|
stripRatio: 0.18,
|
||||||
|
background: 'rgba(255,255,255,0.96)',
|
||||||
|
text: '好运助手',
|
||||||
|
textColor: '#333',
|
||||||
|
fontFamily: 'PingFang SC, Microsoft YaHei, Arial',
|
||||||
|
logo: botLogo
|
||||||
|
})
|
||||||
|
|
||||||
|
const newFile = new File([watermarkedBlob], file.name, { type: watermarkedBlob.type || file.type })
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', newFile)
|
||||||
|
const resp = await fetch(basePath + '/fileUploadAndDownload/upload?noSave=1', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'x-token': userStore.token,
|
||||||
|
'x-user-id': userStore.userInfo.ID
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const res = await resp.json()
|
||||||
|
if (res.code === 0) {
|
||||||
|
const urlPath = getUrl(res.data.file.url)
|
||||||
|
insertFn(urlPath, res.data.file.name)
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || '上传失败')
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('处理水印失败')
|
||||||
|
// 降级:直接走原图上传
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
const resp = await fetch(basePath + '/fileUploadAndDownload/upload?noSave=1', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'x-token': userStore.token,
|
||||||
|
'x-user-id': userStore.userInfo.ID
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const res = await resp.json()
|
||||||
|
if (res.code === 0) {
|
||||||
|
const urlPath = getUrl(res.data.file.url)
|
||||||
|
insertFn(urlPath, res.data.file.name)
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || '上传失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const editorConfig = computed(() => ({
|
||||||
|
placeholder: '请输入内容...',
|
||||||
|
MENU_CONF: {
|
||||||
|
uploadImage: {
|
||||||
|
server: basePath + '/fileUploadAndDownload/upload?noSave=1',
|
||||||
|
fieldName: 'file',
|
||||||
|
maxFileSize: 1024 * 1024 * 10,
|
||||||
|
maxNumberOfFiles: 1,
|
||||||
|
customUpload: createCustomUpload()
|
||||||
},
|
},
|
||||||
useWatermark: {
|
uploadVideo: {
|
||||||
type: Boolean,
|
server: basePath + '/fileUploadAndDownload/upload?noSave=1',
|
||||||
default: false
|
fieldName: 'file',
|
||||||
}
|
maxFileSize: 1024 * 1024 * 100,
|
||||||
})
|
maxNumberOfFiles: 1,
|
||||||
|
customInsert(res, insertFn) {
|
||||||
// 调试:监听 props 变化
|
|
||||||
watch(() => props.useWatermark, (newVal) => {
|
|
||||||
console.log('useWatermark prop changed to:', newVal)
|
|
||||||
}, { immediate: true })
|
|
||||||
|
|
||||||
const editorRef = shallowRef()
|
|
||||||
const valueHtml = ref('')
|
|
||||||
const userStore = useUserStore()
|
|
||||||
|
|
||||||
const toolbarConfig = {}
|
|
||||||
|
|
||||||
// 创建自定义上传函数,直接检查当前 props 值
|
|
||||||
const createCustomUpload = () => {
|
|
||||||
return async (file, insertFn) => {
|
|
||||||
// 直接获取当前的 props.useWatermark 值
|
|
||||||
const shouldUseWatermark = props.useWatermark
|
|
||||||
console.log('customUpload called, useWatermark:', shouldUseWatermark)
|
|
||||||
console.log('props.useWatermark:', props.useWatermark)
|
|
||||||
|
|
||||||
// 未开启水印则直接上传原图
|
|
||||||
if (!shouldUseWatermark) {
|
|
||||||
const formData = new FormData()
|
|
||||||
formData.append('file', file)
|
|
||||||
const resp = await fetch(basePath + '/fileUploadAndDownload/upload?noSave=1', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData,
|
|
||||||
headers: {
|
|
||||||
'x-token': userStore.token,
|
|
||||||
'x-user-id': userStore.userInfo.ID
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const res = await resp.json()
|
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
const urlPath = getUrl(res.data.file.url)
|
const urlPath = getUrl(res.data.file.url)
|
||||||
insertFn(urlPath, res.data.file.name)
|
insertFn(urlPath, res.data.file.name)
|
||||||
} else {
|
return
|
||||||
ElMessage.error(res.msg || '上传失败')
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
console.log('开始添加水印,文件:', file.name)
|
|
||||||
const watermarkedBlob = await addBottomWatermark(file, {
|
|
||||||
stripRatio: 0.18, // 水印条高度占原图高度比例
|
|
||||||
background: 'rgba(255,255,255,0.96)',
|
|
||||||
text: '好运助手',
|
|
||||||
textColor: '#333',
|
|
||||||
fontFamily: 'PingFang SC, Microsoft YaHei, Arial',
|
|
||||||
logo: botLogo
|
|
||||||
})
|
|
||||||
console.log('水印处理完成,新文件大小:', watermarkedBlob.size)
|
|
||||||
|
|
||||||
const newFile = new File([watermarkedBlob], file.name, { type: watermarkedBlob.type || file.type })
|
|
||||||
const formData = new FormData()
|
|
||||||
formData.append('file', newFile)
|
|
||||||
const resp = await fetch(basePath + '/fileUploadAndDownload/upload?noSave=1', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData,
|
|
||||||
headers: {
|
|
||||||
'x-token': userStore.token,
|
|
||||||
'x-user-id': userStore.userInfo.ID
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const res = await resp.json()
|
|
||||||
if (res.code === 0) {
|
|
||||||
const urlPath = getUrl(res.data.file.url)
|
|
||||||
insertFn(urlPath, res.data.file.name)
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.msg || '上传失败')
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
ElMessage.error('处理水印失败')
|
|
||||||
// 降级:直接走原图上传
|
|
||||||
const formData = new FormData()
|
|
||||||
formData.append('file', file)
|
|
||||||
const resp = await fetch(basePath + '/fileUploadAndDownload/upload?noSave=1', { method: 'POST', body: formData })
|
|
||||||
const res = await resp.json()
|
|
||||||
if (res.code === 0) {
|
|
||||||
const urlPath = getUrl(res.data.file.url)
|
|
||||||
insertFn(urlPath, res.data.file.name)
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.msg || '上传失败')
|
|
||||||
}
|
}
|
||||||
|
ElMessage.error(res.msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
// 将 editorConfig 改为计算属性,使其响应 props 变化
|
// 组件销毁时,也及时销毁编辑器
|
||||||
const editorConfig = computed(() => ({
|
onBeforeUnmount(() => {
|
||||||
placeholder: '请输入内容...',
|
const editor = editorRef.value
|
||||||
MENU_CONF: {
|
if (editor == null) return
|
||||||
uploadImage: {
|
|
||||||
server: basePath + '/fileUploadAndDownload/upload?noSave=1',
|
// 如果是全屏状态,先退出全屏
|
||||||
fieldName: 'file',
|
if (isFullscreen.value) {
|
||||||
maxFileSize: 1024 * 1024 * 10, // 限制图片大小为10MB
|
exitFullscreen()
|
||||||
maxNumberOfFiles: 1,
|
}
|
||||||
customUpload: createCustomUpload(),
|
|
||||||
customInsert(res, insertFn) {
|
// 移除键盘监听
|
||||||
if (res.code === 0) {
|
document.removeEventListener('keydown', handleEscKey)
|
||||||
const urlPath = getUrl(res.data.file.url)
|
|
||||||
insertFn(urlPath, res.data.file.name)
|
editor.destroy()
|
||||||
return
|
})
|
||||||
}
|
|
||||||
ElMessage.error(res.msg)
|
const handleCreated = (editor) => {
|
||||||
}
|
editorRef.value = editor
|
||||||
},
|
valueHtml.value = props.modelValue
|
||||||
uploadVideo: {
|
|
||||||
server: basePath + '/fileUploadAndDownload/upload?noSave=1',
|
// 动态更新上传配置
|
||||||
fieldName: 'file',
|
if (editor && editor.getConfig) {
|
||||||
maxFileSize: 1024 * 1024 * 100, // 限制视频大小为100MB
|
const config = editor.getConfig()
|
||||||
maxNumberOfFiles: 1,
|
if (config.MENU_CONF && config.MENU_CONF.uploadImage) {
|
||||||
customInsert(res, insertFn) {
|
config.MENU_CONF.uploadImage.customUpload = createCustomUpload()
|
||||||
if (res.code === 0) {
|
|
||||||
const urlPath = getUrl(res.data.file.url)
|
|
||||||
insertFn(urlPath, res.data.file.name)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ElMessage.error(res.msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}))
|
}
|
||||||
|
|
||||||
// 组件销毁时,也及时销毁编辑器
|
// 修复点击区域问题
|
||||||
onBeforeUnmount(() => {
|
setTimeout(() => {
|
||||||
const editor = editorRef.value
|
const editorContainer = editor.getEditableContainer()
|
||||||
if (editor == null) return
|
if (editorContainer) {
|
||||||
editor.destroy()
|
editorContainer.style.cursor = 'text'
|
||||||
})
|
editorContainer.style.minHeight = '300px'
|
||||||
|
|
||||||
const handleCreated = (editor) => {
|
editorContainer.addEventListener('click', (e) => {
|
||||||
editorRef.value = editor
|
if (e.target === editorContainer) {
|
||||||
|
editor.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 ESC 键退出全屏
|
||||||
|
function handleEscKey(e) {
|
||||||
|
if (e.key === 'Escape' && isFullscreen.value) {
|
||||||
|
exitFullscreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 监听 ESC 键
|
||||||
|
document.addEventListener('keydown', handleEscKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 切换全屏
|
||||||
|
function toggleFullscreen() {
|
||||||
|
if (isFullscreen.value) {
|
||||||
|
exitFullscreen()
|
||||||
|
} else {
|
||||||
|
enterFullscreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 进入全屏
|
||||||
|
function enterFullscreen() {
|
||||||
|
if (!editorWrapper.value) return
|
||||||
|
|
||||||
|
// 保存原始位置
|
||||||
|
originalParent.value = editorWrapper.value.parentElement
|
||||||
|
originalNextSibling.value = editorWrapper.value.nextElementSibling
|
||||||
|
|
||||||
|
isFullscreen.value = true
|
||||||
|
|
||||||
|
// 添加全屏样式类
|
||||||
|
document.body.classList.add('rich-editor-fullscreen')
|
||||||
|
|
||||||
|
// 移动到 body 下(避免受父元素样式影响)
|
||||||
|
document.body.appendChild(editorWrapper.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 退出全屏
|
||||||
|
function exitFullscreen() {
|
||||||
|
if (!editorWrapper.value || !originalParent.value) return
|
||||||
|
|
||||||
|
isFullscreen.value = false
|
||||||
|
|
||||||
|
// 移除全屏样式类
|
||||||
|
document.body.classList.remove('rich-editor-fullscreen')
|
||||||
|
|
||||||
|
// 恢复到原始位置
|
||||||
|
if (originalNextSibling.value) {
|
||||||
|
originalParent.value.insertBefore(editorWrapper.value, originalNextSibling.value)
|
||||||
|
} else {
|
||||||
|
originalParent.value.appendChild(editorWrapper.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空保存的引用
|
||||||
|
originalParent.value = null
|
||||||
|
originalNextSibling.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
() => {
|
||||||
valueHtml.value = props.modelValue
|
valueHtml.value = props.modelValue
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// 动态更新上传配置
|
// 监听 useWatermark 变化,动态更新编辑器配置
|
||||||
if (editor && editor.getConfig) {
|
watch(
|
||||||
const config = editor.getConfig()
|
() => props.useWatermark,
|
||||||
|
() => {
|
||||||
|
if (editorRef.value && editorRef.value.getConfig) {
|
||||||
|
const config = editorRef.value.getConfig()
|
||||||
if (config.MENU_CONF && config.MENU_CONF.uploadImage) {
|
if (config.MENU_CONF && config.MENU_CONF.uploadImage) {
|
||||||
config.MENU_CONF.uploadImage.customUpload = createCustomUpload()
|
config.MENU_CONF.uploadImage.customUpload = createCustomUpload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// 修复点击区域问题
|
// 水印相关函数
|
||||||
setTimeout(() => {
|
async function addBottomWatermark(file, options) {
|
||||||
const editorContainer = editor.getEditableContainer()
|
const { stripRatio = 0.18, background = 'rgba(255,255,255,0.96)', text = '', textColor = '#333', fontFamily = 'Arial', logo } = options || {}
|
||||||
if (editorContainer) {
|
|
||||||
// 确保整个编辑器区域都可以点击
|
|
||||||
editorContainer.style.cursor = 'text'
|
|
||||||
editorContainer.style.minHeight = '300px'
|
|
||||||
|
|
||||||
// 添加点击事件监听器
|
const img = await fileToImage(file)
|
||||||
editorContainer.addEventListener('click', (e) => {
|
const width = img.naturalWidth || img.width
|
||||||
if (e.target === editorContainer) {
|
const height = img.naturalHeight || img.height
|
||||||
// 如果点击的是容器本身,聚焦到编辑器
|
const stripHeight = Math.max(60, Math.floor(height * stripRatio))
|
||||||
editor.focus()
|
|
||||||
}
|
const canvas = document.createElement('canvas')
|
||||||
})
|
canvas.width = width
|
||||||
}
|
canvas.height = height + stripHeight
|
||||||
}, 100)
|
const ctx = canvas.getContext('2d')
|
||||||
|
|
||||||
|
// 原图
|
||||||
|
ctx.drawImage(img, 0, 0, width, height)
|
||||||
|
|
||||||
|
// 底部水印条背景
|
||||||
|
ctx.fillStyle = background
|
||||||
|
ctx.fillRect(0, height, width, stripHeight)
|
||||||
|
|
||||||
|
// 左侧 Logo
|
||||||
|
let logoSize = Math.floor(stripHeight * 0.6)
|
||||||
|
let logoPadding = Math.floor(stripHeight * 0.2)
|
||||||
|
if (logo) {
|
||||||
|
try {
|
||||||
|
const logoImg = await srcToImage(logo)
|
||||||
|
const ratio = logoImg.width / logoImg.height
|
||||||
|
const drawW = logoSize
|
||||||
|
const drawH = Math.floor(drawW / ratio)
|
||||||
|
const y = height + Math.floor((stripHeight - drawH) / 2)
|
||||||
|
ctx.drawImage(logoImg, logoPadding, y, drawW, drawH)
|
||||||
|
} catch { void 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
// 右侧文字
|
||||||
() => props.modelValue,
|
ctx.fillStyle = textColor
|
||||||
() => {
|
const fontSize = Math.floor(stripHeight * 0.38)
|
||||||
valueHtml.value = props.modelValue
|
ctx.font = `${fontSize}px ${fontFamily}`
|
||||||
}
|
ctx.textBaseline = 'middle'
|
||||||
)
|
ctx.textAlign = 'right'
|
||||||
|
const textPadding = Math.floor(stripHeight * 0.25)
|
||||||
|
ctx.fillText(text, width - textPadding, height + Math.floor(stripHeight / 2))
|
||||||
|
|
||||||
// 监听 useWatermark 变化,动态更新编辑器配置
|
const blob = await canvasToBlob(canvas, file.type || 'image/png')
|
||||||
watch(
|
return blob
|
||||||
() => props.useWatermark,
|
}
|
||||||
() => {
|
|
||||||
if (editorRef.value && editorRef.value.getConfig) {
|
|
||||||
const config = editorRef.value.getConfig()
|
|
||||||
if (config.MENU_CONF && config.MENU_CONF.uploadImage) {
|
|
||||||
config.MENU_CONF.uploadImage.customUpload = createCustomUpload()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
async function addBottomWatermark(file, options) {
|
function fileToImage(file) {
|
||||||
console.log('addBottomWatermark 开始处理,文件:', file.name, '大小:', file.size)
|
return new Promise((resolve, reject) => {
|
||||||
const { stripRatio = 0.18, background = 'rgba(255,255,255,0.96)', text = '', textColor = '#333', fontFamily = 'Arial', logo } = options || {}
|
const reader = new FileReader()
|
||||||
|
reader.onload = () => {
|
||||||
const img = await fileToImage(file)
|
|
||||||
const width = img.naturalWidth || img.width
|
|
||||||
const height = img.naturalHeight || img.height
|
|
||||||
const stripHeight = Math.max(60, Math.floor(height * stripRatio))
|
|
||||||
console.log('图片尺寸:', width, 'x', height, '水印条高度:', stripHeight)
|
|
||||||
|
|
||||||
const canvas = document.createElement('canvas')
|
|
||||||
canvas.width = width
|
|
||||||
canvas.height = height + stripHeight
|
|
||||||
const ctx = canvas.getContext('2d')
|
|
||||||
|
|
||||||
// 原图
|
|
||||||
ctx.drawImage(img, 0, 0, width, height)
|
|
||||||
|
|
||||||
// 底部水印条背景
|
|
||||||
ctx.fillStyle = background
|
|
||||||
ctx.fillRect(0, height, width, stripHeight)
|
|
||||||
|
|
||||||
// 左侧 Logo(可选)
|
|
||||||
let logoSize = Math.floor(stripHeight * 0.6)
|
|
||||||
let logoPadding = Math.floor(stripHeight * 0.2)
|
|
||||||
if (logo) {
|
|
||||||
try {
|
|
||||||
const logoImg = await srcToImage(logo)
|
|
||||||
const ratio = logoImg.width / logoImg.height
|
|
||||||
const drawW = logoSize
|
|
||||||
const drawH = Math.floor(drawW / ratio)
|
|
||||||
const y = height + Math.floor((stripHeight - drawH) / 2)
|
|
||||||
ctx.drawImage(logoImg, logoPadding, y, drawW, drawH)
|
|
||||||
} catch { void 0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 右侧文字
|
|
||||||
ctx.fillStyle = textColor
|
|
||||||
const fontSize = Math.floor(stripHeight * 0.38)
|
|
||||||
ctx.font = `${fontSize}px ${fontFamily}`
|
|
||||||
ctx.textBaseline = 'middle'
|
|
||||||
ctx.textAlign = 'right'
|
|
||||||
const textPadding = Math.floor(stripHeight * 0.25)
|
|
||||||
ctx.fillText(text, width - textPadding, height + Math.floor(stripHeight / 2))
|
|
||||||
|
|
||||||
const blob = await canvasToBlob(canvas, file.type || 'image/png')
|
|
||||||
return blob
|
|
||||||
}
|
|
||||||
|
|
||||||
function fileToImage(file) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.onload = () => {
|
|
||||||
const img = new Image()
|
|
||||||
img.onload = () => {
|
|
||||||
console.log('图片加载成功,尺寸:', img.naturalWidth, 'x', img.naturalHeight)
|
|
||||||
resolve(img)
|
|
||||||
}
|
|
||||||
img.onerror = (error) => {
|
|
||||||
console.error('图片加载失败:', error)
|
|
||||||
reject(error)
|
|
||||||
}
|
|
||||||
img.src = reader.result
|
|
||||||
}
|
|
||||||
reader.onerror = (error) => {
|
|
||||||
console.error('文件读取失败:', error)
|
|
||||||
reject(error)
|
|
||||||
}
|
|
||||||
reader.readAsDataURL(file)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function srcToImage(src) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const img = new Image()
|
const img = new Image()
|
||||||
img.crossOrigin = 'anonymous'
|
img.onload = () => resolve(img)
|
||||||
img.onload = () => {
|
img.onerror = reject
|
||||||
console.log('Logo 加载成功,尺寸:', img.naturalWidth, 'x', img.naturalHeight)
|
img.src = reader.result
|
||||||
resolve(img)
|
}
|
||||||
}
|
reader.onerror = reject
|
||||||
img.onerror = (error) => {
|
reader.readAsDataURL(file)
|
||||||
console.error('Logo 加载失败:', error, 'src:', src)
|
})
|
||||||
reject(error)
|
}
|
||||||
}
|
|
||||||
img.src = src
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function canvasToBlob(canvas, mime) {
|
function srcToImage(src) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (canvas.toBlob) {
|
const img = new Image()
|
||||||
canvas.toBlob((blob) => {
|
img.crossOrigin = 'anonymous'
|
||||||
console.log('Canvas 转 Blob 成功,大小:', blob.size, '类型:', blob.type)
|
img.onload = () => resolve(img)
|
||||||
resolve(blob)
|
img.onerror = reject
|
||||||
}, mime, 0.92)
|
img.src = src
|
||||||
} else {
|
})
|
||||||
// 兼容处理
|
}
|
||||||
const dataURL = canvas.toDataURL(mime)
|
|
||||||
const arr = dataURL.split(',')
|
function canvasToBlob(canvas, mime) {
|
||||||
const bstr = atob(arr[1])
|
return new Promise((resolve) => {
|
||||||
let n = bstr.length
|
if (canvas.toBlob) {
|
||||||
const u8arr = new Uint8Array(n)
|
canvas.toBlob((blob) => resolve(blob), mime, 0.92)
|
||||||
while (n--) u8arr[n] = bstr.charCodeAt(n)
|
} else {
|
||||||
const blob = new Blob([u8arr], { type: mime })
|
const dataURL = canvas.toDataURL(mime)
|
||||||
console.log('Canvas 转 Blob (兼容模式) 成功,大小:', blob.size, '类型:', blob.type)
|
const arr = dataURL.split(',')
|
||||||
resolve(blob)
|
const bstr = atob(arr[1])
|
||||||
}
|
let n = bstr.length
|
||||||
})
|
const u8arr = new Uint8Array(n)
|
||||||
}
|
while (n--) u8arr[n] = bstr.charCodeAt(n)
|
||||||
|
const blob = new Blob([u8arr], { type: mime })
|
||||||
|
resolve(blob)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
// 确保富文本编辑器的点击区域
|
.rich-edit-wrapper {
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fff;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.is-fullscreen {
|
||||||
|
position: fixed !important;
|
||||||
|
top: 0 !important;
|
||||||
|
left: 0 !important;
|
||||||
|
right: 0 !important;
|
||||||
|
bottom: 0 !important;
|
||||||
|
width: 100vw !important;
|
||||||
|
height: 100vh !important;
|
||||||
|
z-index: 99999 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
|
||||||
|
.editor-content {
|
||||||
|
flex: 1 !important;
|
||||||
|
overflow-y: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-toggle-btn {
|
||||||
|
top: 10px;
|
||||||
|
right: 20px;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.85);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content {
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-toggle-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: 10px;
|
||||||
|
z-index: 10;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
color: #666;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #e0e0e0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑器内容区域滚动条样式
|
||||||
:deep(.w-e-text-container) {
|
:deep(.w-e-text-container) {
|
||||||
min-height: 300px !important;
|
min-height: 300px !important;
|
||||||
cursor: text !important;
|
cursor: text !important;
|
||||||
|
overflow-y: auto !important;
|
||||||
|
overflow-x: hidden !important;
|
||||||
|
|
||||||
|
// 自定义滚动条样式
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: block !important;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: #c1c1c1;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #a8a8a8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Firefox 滚动条样式
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #c1c1c1 #f1f1f1;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.w-e-scroll) {
|
:deep(.w-e-scroll) {
|
||||||
min-height: 300px !important;
|
min-height: 300px !important;
|
||||||
cursor: text !important;
|
cursor: text !important;
|
||||||
|
overflow-y: auto !important;
|
||||||
|
overflow-x: hidden !important;
|
||||||
|
|
||||||
|
// 自定义滚动条样式
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: block !important;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: #c1c1c1;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #a8a8a8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Firefox 滚动条样式
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #c1c1c1 #f1f1f1;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.w-e-text-placeholder) {
|
:deep(.w-e-text-placeholder) {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保整个编辑器区域都可以点击
|
|
||||||
:deep(.w-e-text-container .w-e-scroll) {
|
:deep(.w-e-text-container .w-e-scroll) {
|
||||||
cursor: text !important;
|
cursor: text !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修复编辑器内容区域的点击问题
|
|
||||||
:deep(.w-e-text-container .w-e-scroll .w-e-text) {
|
:deep(.w-e-text-container .w-e-scroll .w-e-text) {
|
||||||
min-height: 300px !important;
|
min-height: 300px !important;
|
||||||
cursor: text !important;
|
cursor: text !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 编辑器外层容器滚动条样式
|
||||||
|
.editor-content {
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: block !important;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: #c1c1c1;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #a8a8a8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Firefox 滚动条样式
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #c1c1c1 #f1f1f1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
// 全局样式:全屏模式下隐藏其他元素
|
||||||
|
body.rich-editor-fullscreen {
|
||||||
|
overflow: hidden !important;
|
||||||
|
|
||||||
|
// 隐藏主要布局容器
|
||||||
|
> #app {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示全屏编辑器
|
||||||
|
> .rich-edit-wrapper.is-fullscreen {
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -62,6 +62,7 @@
|
|||||||
"/src/view/notice/notice/noticeForm.vue": "NoticeForm",
|
"/src/view/notice/notice/noticeForm.vue": "NoticeForm",
|
||||||
"/src/view/order/index.vue": "OrderManage",
|
"/src/view/order/index.vue": "OrderManage",
|
||||||
"/src/view/order/order/index.vue": "Index",
|
"/src/view/order/order/index.vue": "Index",
|
||||||
|
"/src/view/payConfig/index.vue": "PayConfig",
|
||||||
"/src/view/person/person.vue": "Person",
|
"/src/view/person/person.vue": "Person",
|
||||||
"/src/view/routerHolder.vue": "RouterHolder",
|
"/src/view/routerHolder.vue": "RouterHolder",
|
||||||
"/src/view/superAdmin/api/api.vue": "Api",
|
"/src/view/superAdmin/api/api.vue": "Api",
|
||||||
@@ -78,6 +79,7 @@
|
|||||||
"/src/view/superAdmin/operation/sysOperationRecord.vue": "SysOperationRecord",
|
"/src/view/superAdmin/operation/sysOperationRecord.vue": "SysOperationRecord",
|
||||||
"/src/view/superAdmin/params/sysParams.vue": "SysParams",
|
"/src/view/superAdmin/params/sysParams.vue": "SysParams",
|
||||||
"/src/view/superAdmin/user/user.vue": "User",
|
"/src/view/superAdmin/user/user.vue": "User",
|
||||||
|
"/src/view/system/ipConfig.vue": "IpConfig",
|
||||||
"/src/view/system/state.vue": "State",
|
"/src/view/system/state.vue": "State",
|
||||||
"/src/view/systemTools/autoCode/component/fieldDialog.vue": "FieldDialog",
|
"/src/view/systemTools/autoCode/component/fieldDialog.vue": "FieldDialog",
|
||||||
"/src/view/systemTools/autoCode/component/previewCodeDialog.vue": "PreviewCodeDialog",
|
"/src/view/systemTools/autoCode/component/previewCodeDialog.vue": "PreviewCodeDialog",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
:model="formData"
|
:model="formData"
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
label-width="auto"
|
label-width="auto"
|
||||||
style="margin-top: 0.5rem; "
|
style="margin-top: 0.5rem;"
|
||||||
>
|
>
|
||||||
<ColumnItem title="文章编辑">
|
<ColumnItem title="文章编辑">
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
@@ -77,6 +77,33 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 定时发布选项 -->
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="定时发布">
|
||||||
|
<el-switch
|
||||||
|
v-model="formData.isScheduled"
|
||||||
|
active-text="启用"
|
||||||
|
inactive-text="禁用"
|
||||||
|
@change="handleScheduledChange"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12" v-if="formData.isScheduled">
|
||||||
|
<el-form-item label="发布时间" prop="publishTime">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="formData.publishTime"
|
||||||
|
type="datetime"
|
||||||
|
placeholder="选择发布时间"
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
:disabled-date="disabledDate"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
</ColumnItem>
|
</ColumnItem>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div class="gva-table-box footer-box">
|
<div class="gva-table-box footer-box">
|
||||||
@@ -101,7 +128,7 @@
|
|||||||
import ColumnItem from '@/components/columnItem/ColumnItem.vue'
|
import ColumnItem from '@/components/columnItem/ColumnItem.vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import RichEdit from '@/components/richtext/rich-edit.vue'
|
import RichEdit from '@/components/richtext/rich-edit.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const userDialogRef = ref()
|
const userDialogRef = ref()
|
||||||
@@ -110,7 +137,9 @@
|
|||||||
|
|
||||||
const ruleFormRef = ref(null), formData = ref({
|
const ruleFormRef = ref(null), formData = ref({
|
||||||
isFree: 0, // 默认付费
|
isFree: 0, // 默认付费
|
||||||
price: 0
|
price: 0,
|
||||||
|
isScheduled: false, // 定时发布开关
|
||||||
|
publishTime: '' // 发布时间
|
||||||
}), rules = ref({
|
}), rules = ref({
|
||||||
title: [
|
title: [
|
||||||
{ required: true, message: '请输入文章标题', trigger: 'blur' }
|
{ required: true, message: '请输入文章标题', trigger: 'blur' }
|
||||||
@@ -125,9 +154,9 @@
|
|||||||
{ required: true, message: '请选择是否免费', trigger: 'change' }
|
{ required: true, message: '请选择是否免费', trigger: 'change' }
|
||||||
],
|
],
|
||||||
price: [
|
price: [
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: '请输入价格',
|
message: '请输入价格',
|
||||||
trigger: 'blur',
|
trigger: 'blur',
|
||||||
validator: (rule, value, callback) => {
|
validator: (rule, value, callback) => {
|
||||||
if (formData.value.isFree === 0 && (!value || value <= 0)) {
|
if (formData.value.isFree === 0 && (!value || value <= 0)) {
|
||||||
@@ -147,6 +176,20 @@
|
|||||||
],
|
],
|
||||||
content: [
|
content: [
|
||||||
{ required: true, message: '请输入文章内容', trigger: 'blur' }
|
{ required: true, message: '请输入文章内容', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
publishTime: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请选择发布时间',
|
||||||
|
trigger: 'change',
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
if (formData.value.isScheduled && (!value || value === '')) {
|
||||||
|
callback(new Error('启用定时发布时,请选择发布时间'))
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -188,22 +231,52 @@
|
|||||||
if (formData.value.isFree === undefined || formData.value.isFree === null) {
|
if (formData.value.isFree === undefined || formData.value.isFree === null) {
|
||||||
formData.value.isFree = 0
|
formData.value.isFree = 0
|
||||||
}
|
}
|
||||||
|
// 处理定时发布字段
|
||||||
|
if (formData.value.publishTime) {
|
||||||
|
formData.value.isScheduled = true
|
||||||
|
} else {
|
||||||
|
formData.value.isScheduled = false
|
||||||
|
formData.value.publishTime = ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStaffInfo(data) {
|
function getStaffInfo(data) {
|
||||||
console.log(formData)
|
console.log(formData)
|
||||||
formData.value.teacherId = data.id
|
formData.value.teacherId = data.id
|
||||||
formData.value.teacherName = data.nick_name
|
formData.value.teacherName = data.nick_name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理定时发布开关变化
|
||||||
|
function handleScheduledChange(value) {
|
||||||
|
if (!value) {
|
||||||
|
// 关闭定时发布时,清空发布时间
|
||||||
|
formData.value.publishTime = ''
|
||||||
|
} else {
|
||||||
|
// 开启定时发布时,设置默认发布时间为当前时间
|
||||||
|
const now = new Date()
|
||||||
|
const year = now.getFullYear()
|
||||||
|
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(now.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(now.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(now.getMinutes()).padStart(2, '0')
|
||||||
|
const seconds = String(now.getSeconds()).padStart(2, '0')
|
||||||
|
formData.value.publishTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 禁用过去的日期
|
||||||
|
function disabledDate(time) {
|
||||||
|
return time.getTime() < Date.now() - 8.64e7 // 禁用今天之前的日期
|
||||||
|
}
|
||||||
|
|
||||||
function submit(formRef) {
|
function submit(formRef) {
|
||||||
if(!formRef) return
|
if(!formRef) return
|
||||||
// 校验
|
// 校验
|
||||||
formRef.validate(async (valid) => {
|
formRef.validate(async (valid) => {
|
||||||
if(!valid) return
|
if(!valid) return
|
||||||
let fn = isEdit.value ? edit : add
|
let fn = isEdit.value ? edit : add
|
||||||
|
|
||||||
// 处理价格字段
|
// 处理价格字段
|
||||||
if (formData.value.isFree === 1) {
|
if (formData.value.isFree === 1) {
|
||||||
// 免费文章,价格设为0
|
// 免费文章,价格设为0
|
||||||
@@ -214,8 +287,16 @@
|
|||||||
formData.value.price = Math.round(Number(formData.value.price) * 100)
|
formData.value.price = Math.round(Number(formData.value.price) * 100)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fn(formData.value)
|
// 处理定时发布参数
|
||||||
|
const submitData = { ...formData.value }
|
||||||
|
if (!submitData.isScheduled) {
|
||||||
|
// 如果未启用定时发布,则不传递publishTime参数
|
||||||
|
delete submitData.publishTime
|
||||||
|
}
|
||||||
|
delete submitData.isScheduled // 移除前端使用的开关字段
|
||||||
|
|
||||||
|
const res = await fn(submitData)
|
||||||
console.log(res)
|
console.log(res)
|
||||||
if(res.code === 0) {
|
if(res.code === 0) {
|
||||||
ElMessage({
|
ElMessage({
|
||||||
|
|||||||
Reference in New Issue
Block a user