web-上传组件、富文本组件
This commit is contained in:
@@ -107,7 +107,7 @@ import Quill from 'quill';
|
||||
import { FileUpload } from '@/components/file';
|
||||
import type { SysFile } from '@/types';
|
||||
import { FILE_DOWNLOAD_URL } from '@/config';
|
||||
import { ImageResize } from '@/utils/quill-resize';
|
||||
import { registerImageResize } from '@/utils/quill-resize';
|
||||
// Quill 样式已在 main.ts 中全局引入
|
||||
|
||||
interface Props {
|
||||
@@ -204,7 +204,10 @@ function initQuill() {
|
||||
const node = super.create() as HTMLVideoElement;
|
||||
node.setAttribute('src', value);
|
||||
node.setAttribute('controls', 'true');
|
||||
node.setAttribute('style', 'max-width: 100%; display: block; margin: 12px auto;');
|
||||
node.setAttribute('class', 'custom-video');
|
||||
node.setAttribute('data-custom-video', 'true');
|
||||
// 视频默认居中显示
|
||||
node.setAttribute('style', 'max-width: 100%; display: block; margin: 0 auto;');
|
||||
return node;
|
||||
}
|
||||
|
||||
@@ -213,7 +216,33 @@ function initQuill() {
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义图片 Blot(与文字同行显示)
|
||||
const InlineEmbed: any = Quill.import('blots/embed');
|
||||
|
||||
class CustomImageBlot extends InlineEmbed {
|
||||
static blotName = 'customImage';
|
||||
static tagName = 'img';
|
||||
|
||||
static create(value: string) {
|
||||
const node = super.create() as HTMLImageElement;
|
||||
node.setAttribute('src', value);
|
||||
node.setAttribute('class', 'custom-image');
|
||||
node.setAttribute('data-custom-image', 'true');
|
||||
// 图片与文字同行显示
|
||||
node.setAttribute('style', 'max-width: 100%; display: inline-block; vertical-align: bottom;');
|
||||
return node;
|
||||
}
|
||||
|
||||
static value(node: HTMLImageElement) {
|
||||
return node.getAttribute('src');
|
||||
}
|
||||
}
|
||||
|
||||
Quill.register(VideoBlot);
|
||||
Quill.register(CustomImageBlot);
|
||||
|
||||
// 注册图片/视频拉伸模块
|
||||
registerImageResize();
|
||||
|
||||
// 配置选项
|
||||
const options = {
|
||||
@@ -249,7 +278,17 @@ function initQuill() {
|
||||
matchVisual: false
|
||||
},
|
||||
// 启用图片/视频缩放模块
|
||||
imageResize: ImageResize
|
||||
imageResize: {
|
||||
onResizeEnd: () => {
|
||||
console.log('🔄 图片/视频拉伸结束,强制更新内容');
|
||||
// 强制触发内容更新
|
||||
if (quillInstance) {
|
||||
const html = quillInstance.root.innerHTML;
|
||||
emit('update:modelValue', html);
|
||||
emit('change', html);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
placeholder: props.placeholder,
|
||||
readOnly: props.readOnly || props.disabled
|
||||
@@ -369,12 +408,12 @@ function handleUploadSuccess(files: SysFile[]) {
|
||||
|
||||
// 根据类型插入内容
|
||||
if (uploadType.value === 'image') {
|
||||
// 插入图片
|
||||
quillInstance!.insertEmbed(range.index, 'image', downloadUrl);
|
||||
// 插入自定义图片(与文字同行显示)
|
||||
quillInstance!.insertEmbed(range.index, 'customImage', downloadUrl);
|
||||
// 移动光标到图片后面
|
||||
quillInstance!.setSelection(range.index + 1);
|
||||
} else if (uploadType.value === 'video') {
|
||||
// 插入自定义视频(使用 customVideo 而不是默认的 video)
|
||||
// 插入自定义视频(单行居中显示)
|
||||
quillInstance!.insertEmbed(range.index, 'customVideo', downloadUrl);
|
||||
// 移动光标到视频后面
|
||||
quillInstance!.setSelection(range.index + 1);
|
||||
@@ -464,50 +503,93 @@ defineExpose({
|
||||
}
|
||||
}
|
||||
|
||||
// 图片样式 - 默认内联显示,底部对齐
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
// 视频容器样式
|
||||
// 视频容器样式 - 默认内联显示,底部对齐
|
||||
iframe, video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
// 自定义视频默认样式 - 单行居中显示
|
||||
.custom-video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 12px auto; // 默认居中
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
// 自定义图片默认样式 - 与文字同行显示
|
||||
.custom-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
// Quill 视频包装器
|
||||
.ql-video {
|
||||
display: block;
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
// 支持对齐方式
|
||||
// 支持对齐方式 - 图片和视频分别处理
|
||||
.ql-align-center {
|
||||
text-align: center;
|
||||
text-align: center !important;
|
||||
|
||||
iframe, video, .ql-video {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
// 视频始终居中显示
|
||||
video, .custom-video {
|
||||
display: block !important;
|
||||
margin-left: auto !important;
|
||||
margin-right: auto !important;
|
||||
}
|
||||
|
||||
// 图片跟随文字对齐
|
||||
img, .custom-image {
|
||||
display: inline-block !important;
|
||||
vertical-align: bottom !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ql-align-right {
|
||||
text-align: right;
|
||||
text-align: right !important;
|
||||
|
||||
iframe, video, .ql-video {
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
// 视频始终居中显示
|
||||
video, .custom-video {
|
||||
display: block !important;
|
||||
margin-left: auto !important;
|
||||
margin-right: auto !important;
|
||||
}
|
||||
|
||||
// 图片跟随文字对齐
|
||||
img, .custom-image {
|
||||
display: inline-block !important;
|
||||
vertical-align: bottom !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ql-align-left {
|
||||
text-align: left;
|
||||
text-align: left !important;
|
||||
|
||||
iframe, video, .ql-video {
|
||||
margin-left: 0;
|
||||
margin-right: auto;
|
||||
// 视频始终居中显示
|
||||
video, .custom-video {
|
||||
display: block !important;
|
||||
margin-left: auto !important;
|
||||
margin-right: auto !important;
|
||||
}
|
||||
|
||||
// 图片跟随文字对齐
|
||||
img, .custom-image {
|
||||
display: inline-block !important;
|
||||
vertical-align: bottom !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
<template>
|
||||
<div class="rich-text-example">
|
||||
<h2>富文本编辑器示例</h2>
|
||||
|
||||
<div class="example-section">
|
||||
<h3>基础使用</h3>
|
||||
<RichTextComponent
|
||||
v-model="content1"
|
||||
placeholder="请输入内容..."
|
||||
height="300px"
|
||||
/>
|
||||
|
||||
<div class="preview">
|
||||
<h4>预览:</h4>
|
||||
<div v-html="content1" class="preview-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="example-section">
|
||||
<h3>带字数统计</h3>
|
||||
<RichTextComponent
|
||||
v-model="content2"
|
||||
placeholder="最多输入500字..."
|
||||
height="200px"
|
||||
:max-length="500"
|
||||
show-word-count
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="example-section">
|
||||
<h3>只读模式</h3>
|
||||
<RichTextComponent
|
||||
v-model="content3"
|
||||
height="150px"
|
||||
read-only
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="example-section">
|
||||
<h3>禁用状态</h3>
|
||||
<RichTextComponent
|
||||
v-model="content4"
|
||||
height="150px"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="example-section">
|
||||
<h3>错误状态</h3>
|
||||
<RichTextComponent
|
||||
v-model="content5"
|
||||
height="150px"
|
||||
error
|
||||
error-message="内容不能为空"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="example-section">
|
||||
<h3>使用 ref 调用方法</h3>
|
||||
<RichTextComponent
|
||||
ref="editorRef"
|
||||
v-model="content6"
|
||||
height="200px"
|
||||
/>
|
||||
<div class="button-group">
|
||||
<el-button @click="getText">获取纯文本</el-button>
|
||||
<el-button @click="getHTML">获取HTML</el-button>
|
||||
<el-button @click="clearContent">清空内容</el-button>
|
||||
<el-button @click="setContent">设置内容</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { ElButton, ElMessage } from 'element-plus';
|
||||
import RichTextComponent from './RichTextComponent.vue';
|
||||
|
||||
const content1 = ref('<p>这是一段<strong>富文本</strong>内容</p>');
|
||||
const content2 = ref('');
|
||||
const content3 = ref('<p>这是只读内容,无法编辑</p>');
|
||||
const content4 = ref('<p>这是禁用状态</p>');
|
||||
const content5 = ref('');
|
||||
const content6 = ref('<p>测试内容</p>');
|
||||
|
||||
const editorRef = ref();
|
||||
|
||||
function getText() {
|
||||
const text = editorRef.value?.getText();
|
||||
ElMessage.success(`纯文本内容:${text}`);
|
||||
console.log('纯文本:', text);
|
||||
}
|
||||
|
||||
function getHTML() {
|
||||
const html = editorRef.value?.getHTML();
|
||||
ElMessage.success('HTML已输出到控制台');
|
||||
console.log('HTML:', html);
|
||||
}
|
||||
|
||||
function clearContent() {
|
||||
editorRef.value?.clear();
|
||||
ElMessage.success('内容已清空');
|
||||
}
|
||||
|
||||
function setContent() {
|
||||
const newContent = '<h2>新标题</h2><p>这是通过方法设置的内容</p><ul><li>列表项1</li><li>列表项2</li></ul>';
|
||||
editorRef.value?.setContent(newContent);
|
||||
ElMessage.success('内容已设置');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.rich-text-example {
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 30px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.example-section {
|
||||
margin-bottom: 40px;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 16px;
|
||||
color: #606266;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.preview {
|
||||
margin-top: 20px;
|
||||
padding: 16px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 12px 0;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
padding: 12px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
min-height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.button-group {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user