🎨 优化富文本编辑器,完善全屏功能
This commit is contained in:
@@ -76,6 +76,8 @@
|
||||
"sass": "^1.78.0",
|
||||
"terser": "^5.31.6",
|
||||
"unocss": "^66.4.2",
|
||||
"unplugin-auto-import": "^0.17.8",
|
||||
"unplugin-vue-components": "^0.27.5",
|
||||
"vite": "^6.2.3",
|
||||
"vite-plugin-banner": "^0.8.0",
|
||||
"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>
|
||||
<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
|
||||
:editor="editorRef"
|
||||
:default-config="toolbarConfig"
|
||||
@@ -7,8 +17,8 @@
|
||||
/>
|
||||
<Editor
|
||||
v-model="valueHtml"
|
||||
class="overflow-y-hidden mt-0.5"
|
||||
style="min-height: 25rem; height: 400px;"
|
||||
class="editor-content"
|
||||
:style="editorStyle"
|
||||
:default-config="editorConfig"
|
||||
mode="default"
|
||||
@onCreated="handleCreated"
|
||||
@@ -18,352 +28,564 @@
|
||||
</template>
|
||||
|
||||
<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 { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||
import { onBeforeUnmount, ref, shallowRef, watch, computed, onMounted } from 'vue'
|
||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getUrl } from '@/utils/image'
|
||||
import botLogo from '@/assets/bot_logo.png'
|
||||
import { useUserStore } from '@/pinia/modules/user'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getUrl } from '@/utils/image'
|
||||
import botLogo from '@/assets/bot_logo.png'
|
||||
import { useUserStore } from '@/pinia/modules/user'
|
||||
|
||||
const emits = defineEmits(['change', 'update:modelValue'])
|
||||
const emits = defineEmits(['change', 'update:modelValue'])
|
||||
|
||||
const change = (editor) => {
|
||||
emits('change', editor)
|
||||
emits('update:modelValue', valueHtml.value)
|
||||
const change = (editor) => {
|
||||
emits('change', editor)
|
||||
emits('update:modelValue', valueHtml.value)
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
useWatermark: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
const editorRef = shallowRef()
|
||||
const valueHtml = ref('')
|
||||
const userStore = useUserStore()
|
||||
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: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
// 调试:监听 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()
|
||||
uploadVideo: {
|
||||
server: basePath + '/fileUploadAndDownload/upload?noSave=1',
|
||||
fieldName: 'file',
|
||||
maxFileSize: 1024 * 1024 * 100,
|
||||
maxNumberOfFiles: 1,
|
||||
customInsert(res, insertFn) {
|
||||
if (res.code === 0) {
|
||||
const urlPath = getUrl(res.data.file.url)
|
||||
insertFn(urlPath, res.data.file.name)
|
||||
} else {
|
||||
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 || '上传失败')
|
||||
return
|
||||
}
|
||||
ElMessage.error(res.msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
// 将 editorConfig 改为计算属性,使其响应 props 变化
|
||||
const editorConfig = computed(() => ({
|
||||
placeholder: '请输入内容...',
|
||||
MENU_CONF: {
|
||||
uploadImage: {
|
||||
server: basePath + '/fileUploadAndDownload/upload?noSave=1',
|
||||
fieldName: 'file',
|
||||
maxFileSize: 1024 * 1024 * 10, // 限制图片大小为10MB
|
||||
maxNumberOfFiles: 1,
|
||||
customUpload: createCustomUpload(),
|
||||
customInsert(res, insertFn) {
|
||||
if (res.code === 0) {
|
||||
const urlPath = getUrl(res.data.file.url)
|
||||
insertFn(urlPath, res.data.file.name)
|
||||
return
|
||||
}
|
||||
ElMessage.error(res.msg)
|
||||
}
|
||||
},
|
||||
uploadVideo: {
|
||||
server: basePath + '/fileUploadAndDownload/upload?noSave=1',
|
||||
fieldName: 'file',
|
||||
maxFileSize: 1024 * 1024 * 100, // 限制视频大小为100MB
|
||||
maxNumberOfFiles: 1,
|
||||
customInsert(res, insertFn) {
|
||||
if (res.code === 0) {
|
||||
const urlPath = getUrl(res.data.file.url)
|
||||
insertFn(urlPath, res.data.file.name)
|
||||
return
|
||||
}
|
||||
ElMessage.error(res.msg)
|
||||
}
|
||||
}
|
||||
// 组件销毁时,也及时销毁编辑器
|
||||
onBeforeUnmount(() => {
|
||||
const editor = editorRef.value
|
||||
if (editor == null) return
|
||||
|
||||
// 如果是全屏状态,先退出全屏
|
||||
if (isFullscreen.value) {
|
||||
exitFullscreen()
|
||||
}
|
||||
|
||||
// 移除键盘监听
|
||||
document.removeEventListener('keydown', handleEscKey)
|
||||
|
||||
editor.destroy()
|
||||
})
|
||||
|
||||
const handleCreated = (editor) => {
|
||||
editorRef.value = editor
|
||||
valueHtml.value = props.modelValue
|
||||
|
||||
// 动态更新上传配置
|
||||
if (editor && editor.getConfig) {
|
||||
const config = editor.getConfig()
|
||||
if (config.MENU_CONF && config.MENU_CONF.uploadImage) {
|
||||
config.MENU_CONF.uploadImage.customUpload = createCustomUpload()
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// 组件销毁时,也及时销毁编辑器
|
||||
onBeforeUnmount(() => {
|
||||
const editor = editorRef.value
|
||||
if (editor == null) return
|
||||
editor.destroy()
|
||||
})
|
||||
// 修复点击区域问题
|
||||
setTimeout(() => {
|
||||
const editorContainer = editor.getEditableContainer()
|
||||
if (editorContainer) {
|
||||
editorContainer.style.cursor = 'text'
|
||||
editorContainer.style.minHeight = '300px'
|
||||
|
||||
const handleCreated = (editor) => {
|
||||
editorRef.value = editor
|
||||
editorContainer.addEventListener('click', (e) => {
|
||||
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
|
||||
}
|
||||
)
|
||||
|
||||
// 动态更新上传配置
|
||||
if (editor && editor.getConfig) {
|
||||
const config = editor.getConfig()
|
||||
// 监听 useWatermark 变化,动态更新编辑器配置
|
||||
watch(
|
||||
() => 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 修复点击区域问题
|
||||
setTimeout(() => {
|
||||
const editorContainer = editor.getEditableContainer()
|
||||
if (editorContainer) {
|
||||
// 确保整个编辑器区域都可以点击
|
||||
editorContainer.style.cursor = 'text'
|
||||
editorContainer.style.minHeight = '300px'
|
||||
// 水印相关函数
|
||||
async function addBottomWatermark(file, options) {
|
||||
const { stripRatio = 0.18, background = 'rgba(255,255,255,0.96)', text = '', textColor = '#333', fontFamily = 'Arial', logo } = options || {}
|
||||
|
||||
// 添加点击事件监听器
|
||||
editorContainer.addEventListener('click', (e) => {
|
||||
if (e.target === editorContainer) {
|
||||
// 如果点击的是容器本身,聚焦到编辑器
|
||||
editor.focus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}, 100)
|
||||
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))
|
||||
|
||||
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 }
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
valueHtml.value = props.modelValue
|
||||
}
|
||||
)
|
||||
// 右侧文字
|
||||
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))
|
||||
|
||||
// 监听 useWatermark 变化,动态更新编辑器配置
|
||||
watch(
|
||||
() => 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
const blob = await canvasToBlob(canvas, file.type || 'image/png')
|
||||
return blob
|
||||
}
|
||||
|
||||
async function addBottomWatermark(file, options) {
|
||||
console.log('addBottomWatermark 开始处理,文件:', file.name, '大小:', file.size)
|
||||
const { stripRatio = 0.18, background = 'rgba(255,255,255,0.96)', text = '', textColor = '#333', fontFamily = 'Arial', logo } = options || {}
|
||||
|
||||
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) => {
|
||||
function fileToImage(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
const img = new Image()
|
||||
img.crossOrigin = 'anonymous'
|
||||
img.onload = () => {
|
||||
console.log('Logo 加载成功,尺寸:', img.naturalWidth, 'x', img.naturalHeight)
|
||||
resolve(img)
|
||||
}
|
||||
img.onerror = (error) => {
|
||||
console.error('Logo 加载失败:', error, 'src:', src)
|
||||
reject(error)
|
||||
}
|
||||
img.src = src
|
||||
})
|
||||
}
|
||||
img.onload = () => resolve(img)
|
||||
img.onerror = reject
|
||||
img.src = reader.result
|
||||
}
|
||||
reader.onerror = reject
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
}
|
||||
|
||||
function canvasToBlob(canvas, mime) {
|
||||
return new Promise((resolve) => {
|
||||
if (canvas.toBlob) {
|
||||
canvas.toBlob((blob) => {
|
||||
console.log('Canvas 转 Blob 成功,大小:', blob.size, '类型:', blob.type)
|
||||
resolve(blob)
|
||||
}, mime, 0.92)
|
||||
} else {
|
||||
// 兼容处理
|
||||
const dataURL = canvas.toDataURL(mime)
|
||||
const arr = dataURL.split(',')
|
||||
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 })
|
||||
console.log('Canvas 转 Blob (兼容模式) 成功,大小:', blob.size, '类型:', blob.type)
|
||||
resolve(blob)
|
||||
}
|
||||
})
|
||||
}
|
||||
function srcToImage(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
img.crossOrigin = 'anonymous'
|
||||
img.onload = () => resolve(img)
|
||||
img.onerror = reject
|
||||
img.src = src
|
||||
})
|
||||
}
|
||||
|
||||
function canvasToBlob(canvas, mime) {
|
||||
return new Promise((resolve) => {
|
||||
if (canvas.toBlob) {
|
||||
canvas.toBlob((blob) => resolve(blob), mime, 0.92)
|
||||
} else {
|
||||
const dataURL = canvas.toDataURL(mime)
|
||||
const arr = dataURL.split(',')
|
||||
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>
|
||||
|
||||
<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) {
|
||||
min-height: 300px !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) {
|
||||
min-height: 300px !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) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// 确保整个编辑器区域都可以点击
|
||||
:deep(.w-e-text-container .w-e-scroll) {
|
||||
cursor: text !important;
|
||||
}
|
||||
|
||||
// 修复编辑器内容区域的点击问题
|
||||
:deep(.w-e-text-container .w-e-scroll .w-e-text) {
|
||||
min-height: 300px !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>
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"/src/view/notice/notice/noticeForm.vue": "NoticeForm",
|
||||
"/src/view/order/index.vue": "OrderManage",
|
||||
"/src/view/order/order/index.vue": "Index",
|
||||
"/src/view/payConfig/index.vue": "PayConfig",
|
||||
"/src/view/person/person.vue": "Person",
|
||||
"/src/view/routerHolder.vue": "RouterHolder",
|
||||
"/src/view/superAdmin/api/api.vue": "Api",
|
||||
@@ -78,6 +79,7 @@
|
||||
"/src/view/superAdmin/operation/sysOperationRecord.vue": "SysOperationRecord",
|
||||
"/src/view/superAdmin/params/sysParams.vue": "SysParams",
|
||||
"/src/view/superAdmin/user/user.vue": "User",
|
||||
"/src/view/system/ipConfig.vue": "IpConfig",
|
||||
"/src/view/system/state.vue": "State",
|
||||
"/src/view/systemTools/autoCode/component/fieldDialog.vue": "FieldDialog",
|
||||
"/src/view/systemTools/autoCode/component/previewCodeDialog.vue": "PreviewCodeDialog",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
label-width="auto"
|
||||
style="margin-top: 0.5rem; "
|
||||
style="margin-top: 0.5rem;"
|
||||
>
|
||||
<ColumnItem title="文章编辑">
|
||||
<el-row :gutter="20">
|
||||
@@ -77,6 +77,33 @@
|
||||
</el-col>
|
||||
</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>
|
||||
</el-form>
|
||||
<div class="gva-table-box footer-box">
|
||||
@@ -110,7 +137,9 @@
|
||||
|
||||
const ruleFormRef = ref(null), formData = ref({
|
||||
isFree: 0, // 默认付费
|
||||
price: 0
|
||||
price: 0,
|
||||
isScheduled: false, // 定时发布开关
|
||||
publishTime: '' // 发布时间
|
||||
}), rules = ref({
|
||||
title: [
|
||||
{ required: true, message: '请输入文章标题', trigger: 'blur' }
|
||||
@@ -147,6 +176,20 @@
|
||||
],
|
||||
content: [
|
||||
{ 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,6 +231,13 @@
|
||||
if (formData.value.isFree === undefined || formData.value.isFree === null) {
|
||||
formData.value.isFree = 0
|
||||
}
|
||||
// 处理定时发布字段
|
||||
if (formData.value.publishTime) {
|
||||
formData.value.isScheduled = true
|
||||
} else {
|
||||
formData.value.isScheduled = false
|
||||
formData.value.publishTime = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,6 +247,29 @@
|
||||
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) {
|
||||
if(!formRef) return
|
||||
// 校验
|
||||
@@ -215,7 +288,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if(res.code === 0) {
|
||||
ElMessage({
|
||||
|
||||
Reference in New Issue
Block a user