|
@@ -24,6 +24,35 @@
|
|
|
@text-change="(e: any) => $emit('update:modelValue', content)"
|
|
@text-change="(e: any) => $emit('update:modelValue', content)"
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <!-- 视频插入弹窗:网页链接 / 本地上传 二选一 -->
|
|
|
|
|
+ <el-dialog v-model="videoDialog.visible" title="插入视频" width="520px" append-to-body>
|
|
|
|
|
+ <el-radio-group v-model="videoDialog.mode">
|
|
|
|
|
+ <el-radio value="url">网页链接</el-radio>
|
|
|
|
|
+ <el-radio value="upload">本地上传</el-radio>
|
|
|
|
|
+ </el-radio-group>
|
|
|
|
|
+ <div v-if="videoDialog.mode === 'url'" style="margin-top: 12px">
|
|
|
|
|
+ <el-input v-model="videoDialog.url" placeholder="请输入视频地址(Embed URL)" clearable />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-else style="margin-top: 12px">
|
|
|
|
|
+ <el-upload
|
|
|
|
|
+ :action="upload.url"
|
|
|
|
|
+ :headers="upload.headers"
|
|
|
|
|
+ name="file"
|
|
|
|
|
+ accept="video/*"
|
|
|
|
|
+ :show-file-list="false"
|
|
|
|
|
+ :before-upload="handleVideoBeforeUpload"
|
|
|
|
|
+ :on-success="handleVideoUploadSuccess"
|
|
|
|
|
+ :on-error="handleUploadError"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-button type="primary">选择视频文件</el-button>
|
|
|
|
|
+ </el-upload>
|
|
|
|
|
+ <div v-if="videoDialog.uploadedUrl" style="margin-top: 8px; color: #67c23a">已上传:{{ videoDialog.uploadedName }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <template #footer>
|
|
|
|
|
+ <el-button @click="videoDialog.visible = false">取 消</el-button>
|
|
|
|
|
+ <el-button type="primary" @click="confirmInsertVideo">确 定</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-dialog>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
@@ -33,6 +62,144 @@ import { QuillEditor, Quill } from '@vueup/vue-quill';
|
|
|
import { propTypes } from '@/utils/propTypes';
|
|
import { propTypes } from '@/utils/propTypes';
|
|
|
import { globalHeaders } from '@/utils/request';
|
|
import { globalHeaders } from '@/utils/request';
|
|
|
|
|
|
|
|
|
|
+// 富文本统一输出内联样式:Quill默认输出class名(如ql-size-huge、ql-align-center),
|
|
|
|
|
+// 只有加载了Quill样式表的页面才能渲染,APP端渲染器不认识会导致字体大小/对齐丢失
|
|
|
|
|
+const SizeAttributor = Quill.import('attributors/style/size') as any;
|
|
|
|
|
+SizeAttributor.whitelist = ['10px', '12px', '14px', '16px', '18px', '20px', '24px', '32px'];
|
|
|
|
|
+Quill.register(SizeAttributor, true);
|
|
|
|
|
+Quill.register(Quill.import('attributors/style/align') as any, true);
|
|
|
|
|
+Quill.register(Quill.import('attributors/style/direction') as any, true);
|
|
|
|
|
+
|
|
|
|
|
+// 视频用 <video> 标签插入:Quill默认video格式是iframe嵌入,APP端渲染器对iframe兼容差;
|
|
|
|
|
+// video标签在WebView原生可播、flutter_html等组件渲染也可扩展支持,兼容性严格优于iframe
|
|
|
|
|
+const BlockEmbed = Quill.import('blots/block/embed') as any;
|
|
|
|
|
+class VideoTagBlot extends BlockEmbed {
|
|
|
|
|
+ static blotName = 'videoTag';
|
|
|
|
|
+ static tagName = 'video';
|
|
|
|
|
+ static create(value: string) {
|
|
|
|
|
+ const node = super.create(value);
|
|
|
|
|
+ node.setAttribute('src', value);
|
|
|
|
|
+ node.setAttribute('controls', 'controls');
|
|
|
|
|
+ node.setAttribute('preload', 'metadata');
|
|
|
|
|
+ node.setAttribute('playsinline', 'true');
|
|
|
|
|
+ node.setAttribute('style', 'max-width:100%;');
|
|
|
|
|
+ return node;
|
|
|
|
|
+ }
|
|
|
|
|
+ static value(node: HTMLElement) {
|
|
|
|
|
+ return node.getAttribute('src');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+Quill.register(VideoTagBlot, true);
|
|
|
|
|
+
|
|
|
|
|
+// 修复列表符号:Quill实际DOM为 ol > li[data-list=bullet/checked/unchecked],
|
|
|
|
|
+// 而vue-quill自带样式表是旧版规则(圆点只给 ul > li::before,ol li:before 统一走数字计数器),
|
|
|
|
|
+// 导致无序列表显示成数字编号。这里按 data-list 重写 li::before 的 content。
|
|
|
|
|
+// 注意:符号只能加在 li::before(旧样式表已为它配好宽度/边距/对齐),
|
|
|
|
|
+// 不能同时加到 li > .ql-ui::before,否则会出现两个圆点且后者紧贴文字
|
|
|
|
|
+// 用JS注入<style>而非SFC样式块:模块加载即生效,不依赖样式热更新通道
|
|
|
|
|
+const LIST_FIX_CSS = `
|
|
|
|
|
+.editor .ql-editor li[data-list='bullet']::before { content: '\\2022' !important; }
|
|
|
|
|
+.editor .ql-editor li[data-list='checked']::before { content: '\\2611' !important; color: #777; }
|
|
|
|
|
+.editor .ql-editor li[data-list='unchecked']::before { content: '\\2610' !important; color: #777; }
|
|
|
|
|
+.editor .ql-editor li[data-list='bullet'],
|
|
|
|
|
+.editor .ql-editor li[data-list='checked'],
|
|
|
|
|
+.editor .ql-editor li[data-list='unchecked'] { counter-increment: none !important; }
|
|
|
|
|
+`;
|
|
|
|
|
+const listFixStyle = document.getElementById('ql-list-fix-css') as HTMLStyleElement | null;
|
|
|
|
|
+if (listFixStyle) {
|
|
|
|
|
+ listFixStyle.textContent = LIST_FIX_CSS;
|
|
|
|
|
+} else {
|
|
|
|
|
+ const s = document.createElement('style');
|
|
|
|
|
+ s.id = 'ql-list-fix-css';
|
|
|
|
|
+ s.textContent = LIST_FIX_CSS;
|
|
|
|
|
+ document.head.appendChild(s);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 工具栏悬停提示:选择器 -> 提示文案(原生title,鼠标悬停停顿后显示)
|
|
|
|
|
+const TOOLBAR_TIPS: [string, string][] = [
|
|
|
|
|
+ ['button.ql-bold', '加粗:选中文字加粗'],
|
|
|
|
|
+ ['button.ql-italic', '斜体:选中文字变斜体'],
|
|
|
|
|
+ ['button.ql-underline', '下划线:选中文字加下划线'],
|
|
|
|
|
+ ['button.ql-strike', '删除线:选中文字加删除线'],
|
|
|
|
|
+ ['button.ql-blockquote', '引用:选中段落设为引用'],
|
|
|
|
|
+ ['button.ql-code-block', '代码块:选中内容设为代码块'],
|
|
|
|
|
+ ['button.ql-list[value="ordered"]', '有序列表:选中内容加数字编号'],
|
|
|
|
|
+ ['button.ql-list[value="bullet"]', '无序列表:选中内容加圆点符号'],
|
|
|
|
|
+ ['button.ql-indent[value="-1"]', '减少缩进:降低段落缩进层级'],
|
|
|
|
|
+ ['button.ql-indent[value="+1"]', '增加缩进:提高段落缩进层级'],
|
|
|
|
|
+ ['.ql-picker.ql-size .ql-picker-label', '字体大小:设置选中文字的字号'],
|
|
|
|
|
+ ['.ql-picker.ql-header .ql-picker-label', '标题:切换段落标题级别'],
|
|
|
|
|
+ ['.ql-picker.ql-color .ql-picker-label', '字体颜色:设置选中文字的颜色'],
|
|
|
|
|
+ ['.ql-picker.ql-background .ql-picker-label', '背景颜色:设置选中文字的背景色'],
|
|
|
|
|
+ ['.ql-picker.ql-align .ql-picker-label', '对齐方式:设置段落对齐'],
|
|
|
|
|
+ ['.ql-align .ql-picker-item:not([data-value])', '左对齐(默认)'],
|
|
|
|
|
+ ['.ql-align .ql-picker-item[data-value="center"]', '居中对齐:段落居中(居中请用此按钮)'],
|
|
|
|
|
+ ['.ql-align .ql-picker-item[data-value="right"]', '右对齐:段落靠右'],
|
|
|
|
|
+ ['.ql-align .ql-picker-item[data-value="justify"]', '两端对齐:段落两端对齐'],
|
|
|
|
|
+ ['button.ql-clean', '清除格式:移除选中内容的所有格式'],
|
|
|
|
|
+ ['button.ql-link', '链接:插入超链接'],
|
|
|
|
|
+ ['button.ql-image', '图片:上传并插入图片'],
|
|
|
|
|
+ ['button.ql-video', '视频:网页链接或本地上传插入视频']
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+/** 工具栏悬停功能提示:自定义气泡,不依赖原生title,也不依赖编辑器挂载时机 */
|
|
|
|
|
+const TIP_DELAY = 400; // 悬停停顿多久后显示(毫秒)
|
|
|
|
|
+let tipEl: HTMLDivElement | null = null;
|
|
|
|
|
+let tipTimer: any = null;
|
|
|
|
|
+
|
|
|
|
|
+function getTipEl() {
|
|
|
|
|
+ if (!tipEl) {
|
|
|
|
|
+ tipEl = document.createElement('div');
|
|
|
|
|
+ tipEl.style.cssText =
|
|
|
|
|
+ 'position:fixed;z-index:99999;padding:6px 10px;background:rgba(48,49,51,.95);color:#fff;' +
|
|
|
|
|
+ 'font-size:12px;line-height:1.5;border-radius:4px;pointer-events:none;white-space:nowrap;' +
|
|
|
|
|
+ 'transform:translate(-50%,-100%);display:none;';
|
|
|
|
|
+ document.body.appendChild(tipEl);
|
|
|
|
|
+ }
|
|
|
|
|
+ return tipEl;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function hideTip() {
|
|
|
|
|
+ if (tipTimer) {
|
|
|
|
|
+ clearTimeout(tipTimer);
|
|
|
|
|
+ tipTimer = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (tipEl) tipEl.style.display = 'none';
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function bindToolbarTips() {
|
|
|
|
|
+ // 挂window上防HMR/多实例重复绑定
|
|
|
|
|
+ if ((window as any).__qlToolbarTipsBound) return;
|
|
|
|
|
+ (window as any).__qlToolbarTipsBound = true;
|
|
|
|
|
+ document.addEventListener('mouseover', (e: MouseEvent) => {
|
|
|
|
|
+ const target = e.target as HTMLElement | null;
|
|
|
|
|
+ if (!target || !target.closest) return;
|
|
|
|
|
+ if (!target.closest('.ql-toolbar')) {
|
|
|
|
|
+ hideTip();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const hit = TOOLBAR_TIPS.find(([sel]) => target.closest(sel));
|
|
|
|
|
+ if (!hit) {
|
|
|
|
|
+ hideTip();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const anchor = target.closest(hit[0]) as HTMLElement;
|
|
|
|
|
+ if (tipTimer) clearTimeout(tipTimer);
|
|
|
|
|
+ tipTimer = setTimeout(() => {
|
|
|
|
|
+ const el = getTipEl();
|
|
|
|
|
+ el.textContent = hit[1];
|
|
|
|
|
+ const rect = anchor.getBoundingClientRect();
|
|
|
|
|
+ el.style.left = `${rect.left + rect.width / 2}px`;
|
|
|
|
|
+ el.style.top = `${rect.top - 6}px`;
|
|
|
|
|
+ el.style.display = 'block';
|
|
|
|
|
+ }, TIP_DELAY);
|
|
|
|
|
+ });
|
|
|
|
|
+ document.addEventListener('mouseout', hideTip);
|
|
|
|
|
+ document.addEventListener('mousedown', hideTip, true);
|
|
|
|
|
+ window.addEventListener('scroll', hideTip, true);
|
|
|
|
|
+}
|
|
|
|
|
+bindToolbarTips();
|
|
|
|
|
+
|
|
|
defineEmits(['update:modelValue']);
|
|
defineEmits(['update:modelValue']);
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
const props = defineProps({
|
|
@@ -58,6 +225,17 @@ const upload = reactive<UploadOption>({
|
|
|
});
|
|
});
|
|
|
const quillEditorRef = ref();
|
|
const quillEditorRef = ref();
|
|
|
const uploadRef = ref<HTMLDivElement>();
|
|
const uploadRef = ref<HTMLDivElement>();
|
|
|
|
|
+// 点击图片按钮时记录插入位置(上传弹窗关闭后光标可能丢失)
|
|
|
|
|
+let pendingImageIndex: number | null = null;
|
|
|
|
|
+// 点击视频按钮时记录插入位置,并维护视频弹窗状态
|
|
|
|
|
+let pendingVideoIndex: number | null = null;
|
|
|
|
|
+const videoDialog = reactive({
|
|
|
|
|
+ visible: false,
|
|
|
|
|
+ mode: 'url' as 'url' | 'upload',
|
|
|
|
|
+ url: '',
|
|
|
|
|
+ uploadedUrl: '',
|
|
|
|
|
+ uploadedName: ''
|
|
|
|
|
+});
|
|
|
|
|
|
|
|
const options = ref<any>({
|
|
const options = ref<any>({
|
|
|
theme: 'snow',
|
|
theme: 'snow',
|
|
@@ -71,7 +249,7 @@ const options = ref<any>({
|
|
|
['blockquote', 'code-block'], // 引用 代码块
|
|
['blockquote', 'code-block'], // 引用 代码块
|
|
|
[{ list: 'ordered' }, { list: 'bullet' }], // 有序、无序列表
|
|
[{ list: 'ordered' }, { list: 'bullet' }], // 有序、无序列表
|
|
|
[{ indent: '-1' }, { indent: '+1' }], // 缩进
|
|
[{ indent: '-1' }, { indent: '+1' }], // 缩进
|
|
|
- [{ size: ['small', false, 'large', 'huge'] }], // 字体大小
|
|
|
|
|
|
|
+ [{ size: ['10px', false, '18px', '32px'] }], // 字体大小(内联样式模式)
|
|
|
[{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题
|
|
[{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题
|
|
|
[{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
|
|
[{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
|
|
|
[{ align: [] }], // 对齐方式
|
|
[{ align: [] }], // 对齐方式
|
|
@@ -81,11 +259,29 @@ const options = ref<any>({
|
|
|
handlers: {
|
|
handlers: {
|
|
|
image: (value: boolean) => {
|
|
image: (value: boolean) => {
|
|
|
if (value) {
|
|
if (value) {
|
|
|
|
|
+ // 先记录插入位置,避免上传弹窗关闭后光标丢失
|
|
|
|
|
+ const quill = toRaw(quillEditorRef.value)?.getQuill();
|
|
|
|
|
+ const range = quill?.getSelection();
|
|
|
|
|
+ pendingImageIndex = range ? range.index : Math.max((quill?.getLength() ?? 1) - 1, 0);
|
|
|
// 调用element图片上传
|
|
// 调用element图片上传
|
|
|
uploadRef.value.click();
|
|
uploadRef.value.click();
|
|
|
} else {
|
|
} else {
|
|
|
Quill.format('image', true);
|
|
Quill.format('image', true);
|
|
|
}
|
|
}
|
|
|
|
|
+ },
|
|
|
|
|
+ // 视频弹窗:网页链接或本地上传。Quill自带tooltip是绝对定位,会飘出编辑器压住下面表单,
|
|
|
|
|
+ // 且编辑器未聚焦时第一次点击没反应
|
|
|
|
|
+ video: (value: boolean) => {
|
|
|
|
|
+ if (!value) return;
|
|
|
|
|
+ const quill = toRaw(quillEditorRef.value)?.getQuill();
|
|
|
|
|
+ if (!quill) return;
|
|
|
|
|
+ const range = quill.getSelection();
|
|
|
|
|
+ pendingVideoIndex = range ? range.index : Math.max(quill.getLength() - 1, 0);
|
|
|
|
|
+ videoDialog.url = '';
|
|
|
|
|
+ videoDialog.uploadedUrl = '';
|
|
|
|
|
+ videoDialog.uploadedName = '';
|
|
|
|
|
+ videoDialog.mode = 'url';
|
|
|
|
|
+ videoDialog.visible = true;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
@@ -126,12 +322,15 @@ const handleUploadSuccess = (res: any) => {
|
|
|
if (res.code === 200) {
|
|
if (res.code === 200) {
|
|
|
// 获取富文本实例
|
|
// 获取富文本实例
|
|
|
const quill = toRaw(quillEditorRef.value).getQuill();
|
|
const quill = toRaw(quillEditorRef.value).getQuill();
|
|
|
- // 获取光标位置
|
|
|
|
|
- const length = quill.selection.savedRange.index;
|
|
|
|
|
|
|
+ // 用点击图片按钮时记录的位置,为空则退回当前光标或末尾;
|
|
|
|
|
+ // 直接读 selection.savedRange 在编辑器未聚焦时是 null 会报错,导致第一次上传没反应
|
|
|
|
|
+ const range = quill.getSelection();
|
|
|
|
|
+ const index = pendingImageIndex ?? range?.index ?? Math.max(quill.getLength() - 1, 0);
|
|
|
|
|
+ pendingImageIndex = null;
|
|
|
// 插入图片,res为服务器返回的图片链接地址
|
|
// 插入图片,res为服务器返回的图片链接地址
|
|
|
- quill.insertEmbed(length, 'image', res.data.url);
|
|
|
|
|
- // 调整光标到最后
|
|
|
|
|
- quill.setSelection(length + 1);
|
|
|
|
|
|
|
+ quill.insertEmbed(index, 'image', res.data.url);
|
|
|
|
|
+ // 光标放到图片后
|
|
|
|
|
+ quill.setSelection(index + 1);
|
|
|
proxy?.$modal.closeLoading();
|
|
proxy?.$modal.closeLoading();
|
|
|
} else {
|
|
} else {
|
|
|
proxy?.$modal.msgError('图片插入失败');
|
|
proxy?.$modal.msgError('图片插入失败');
|
|
@@ -159,9 +358,69 @@ const handleBeforeUpload = (file: any) => {
|
|
|
proxy?.$modal.loading('正在上传文件,请稍候...');
|
|
proxy?.$modal.loading('正在上传文件,请稍候...');
|
|
|
return true;
|
|
return true;
|
|
|
};
|
|
};
|
|
|
-// 图片失败拦截
|
|
|
|
|
|
|
+// 图片/视频上传失败拦截
|
|
|
const handleUploadError = (err: any) => {
|
|
const handleUploadError = (err: any) => {
|
|
|
proxy?.$modal.msgError('上传文件失败');
|
|
proxy?.$modal.msgError('上传文件失败');
|
|
|
|
|
+ proxy?.$modal.closeLoading();
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 视频上传前拦截:类型 + 2GB上限(与后端multipart及视频内容页对齐)
|
|
|
|
|
+const handleVideoBeforeUpload = (file: any) => {
|
|
|
|
|
+ const isVideo = file.type?.startsWith('video/') || /\.(mp4|mov|m4v|webm|mkv|avi)$/i.test(file.name);
|
|
|
|
|
+ if (!isVideo) {
|
|
|
|
|
+ proxy?.$modal.msgError('请选择视频文件!');
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ const sizeMB = file.size / 1024 / 1024;
|
|
|
|
|
+ if (sizeMB >= 2048) {
|
|
|
|
|
+ proxy?.$modal.msgError('视频文件大小不能超过2GB');
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ proxy?.$modal.loading('正在上传视频,请稍候...');
|
|
|
|
|
+ return true;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 视频上传成功:先只记录地址,点确定时才插入正文
|
|
|
|
|
+const handleVideoUploadSuccess = (res: any, file: any) => {
|
|
|
|
|
+ proxy?.$modal.closeLoading();
|
|
|
|
|
+ if (res.code === 200) {
|
|
|
|
|
+ videoDialog.uploadedUrl = res.data.url;
|
|
|
|
|
+ videoDialog.uploadedName = file?.name || '视频';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ proxy?.$modal.msgError('视频上传失败');
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 确定插入视频:按所选方式取地址,插到点按钮时记录的位置
|
|
|
|
|
+const confirmInsertVideo = async () => {
|
|
|
|
|
+ const src = (videoDialog.mode === 'url' ? videoDialog.url : videoDialog.uploadedUrl).trim();
|
|
|
|
|
+ if (!src) {
|
|
|
|
|
+ proxy?.$modal.msgWarning(videoDialog.mode === 'url' ? '请输入视频地址' : '请先上传视频文件');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (videoDialog.mode === 'url') {
|
|
|
|
|
+ // <video>只认视频文件直链;抖音/哔哩等网页地址塞进去是黑屏,提前提醒
|
|
|
|
|
+ let pathname = src;
|
|
|
|
|
+ try {
|
|
|
|
|
+ pathname = new URL(src).pathname;
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ /* 不是标准URL就按原串校验 */
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!/\.(mp4|m4v|mov|webm|mkv|avi|flv)$/i.test(pathname)) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await proxy?.$modal.confirm('该链接不带视频文件扩展名(.mp4等)。网页链接(抖音、哔哩等)在正文中无法播放,建议下载后本地上传。仍要插入吗?');
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ const quill = toRaw(quillEditorRef.value)?.getQuill();
|
|
|
|
|
+ if (!quill) return;
|
|
|
|
|
+ const index = pendingVideoIndex ?? Math.max(quill.getLength() - 1, 0);
|
|
|
|
|
+ pendingVideoIndex = null;
|
|
|
|
|
+ quill.insertEmbed(index, 'videoTag', src);
|
|
|
|
|
+ quill.setSelection(index + 1, 0);
|
|
|
|
|
+ videoDialog.visible = false;
|
|
|
};
|
|
};
|
|
|
import { onMounted, nextTick } from 'vue';
|
|
import { onMounted, nextTick } from 'vue';
|
|
|
import Delta from 'quill-delta';
|
|
import Delta from 'quill-delta';
|
|
@@ -435,6 +694,18 @@ onMounted(() => {
|
|
|
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='huge']::before {
|
|
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='huge']::before {
|
|
|
content: '32px';
|
|
content: '32px';
|
|
|
}
|
|
}
|
|
|
|
|
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='10px']::before,
|
|
|
|
|
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='10px']::before {
|
|
|
|
|
+ content: '10px';
|
|
|
|
|
+}
|
|
|
|
|
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='18px']::before,
|
|
|
|
|
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='18px']::before {
|
|
|
|
|
+ content: '18px';
|
|
|
|
|
+}
|
|
|
|
|
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='32px']::before,
|
|
|
|
|
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='32px']::before {
|
|
|
|
|
+ content: '32px';
|
|
|
|
|
+}
|
|
|
.ql-snow .ql-picker.ql-header .ql-picker-label::before,
|
|
.ql-snow .ql-picker.ql-header .ql-picker-label::before,
|
|
|
.ql-snow .ql-picker.ql-header .ql-picker-item::before {
|
|
.ql-snow .ql-picker.ql-header .ql-picker-item::before {
|
|
|
content: '文本';
|
|
content: '文本';
|
|
@@ -480,4 +751,11 @@ onMounted(() => {
|
|
|
white-space: pre-wrap; /* 强制换行 */
|
|
white-space: pre-wrap; /* 强制换行 */
|
|
|
word-break: break-all; /* 防止长单词或URL溢出 */
|
|
word-break: break-all; /* 防止长单词或URL溢出 */
|
|
|
}
|
|
}
|
|
|
|
|
+.editor .ql-editor img,
|
|
|
|
|
+.editor .ql-editor video {
|
|
|
|
|
+ max-width: 100%; /* 自适应编辑器宽度,不溢出 */
|
|
|
|
|
+}
|
|
|
|
|
+.editor .ql-editor video {
|
|
|
|
|
+ margin: 8px 0;
|
|
|
|
|
+}
|
|
|
</style>
|
|
</style>
|