refactor(chat): 优化聊天消息编辑组件代码结构

- 移除不必要的代码注释和冗余逻辑
- 简化表单验证函数参数传递
- 删除重复的类型定义和无用变量
- 统一事件处理函数命名风格
- 清理未使用的插件引用和样式文件
- 优化条件判断语句提高可读性
This commit is contained in:
2025-11-21 14:40:55 +08:00
parent e5e54b6aa6
commit b23788f428
2 changed files with 1486 additions and 1208 deletions

View File

@@ -12,7 +12,9 @@
@ok="save" @ok="save"
> >
<template #extra> <template #extra>
<a-button type="primary" style="margin-right: 8px" @click="save">保存</a-button> <a-button type="primary" style="margin-right: 8px" @click="save"
>保存</a-button
>
</template> </template>
<a-form <a-form
ref="formRef" ref="formRef"
@@ -60,6 +62,13 @@
/> />
</div> </div>
</a-form-item> </a-form-item>
<a-form-item label="关键词" name="tags">
<a-select
v-model:value="form.tags"
mode="tags"
placeholder="按回车分隔"
/>
</a-form-item>
<a-form-item label="内容详情" name="content"> <a-form-item label="内容详情" name="content">
<!-- 富文本编辑器 --> <!-- 富文本编辑器 -->
<div v-if="editor == 1"> <div v-if="editor == 1">
@@ -72,7 +81,8 @@
placeholder="支持直接粘贴或拖拽图片,也可点击工具栏图片按钮从文件库选择" placeholder="支持直接粘贴或拖拽图片,也可点击工具栏图片按钮从文件库选择"
/> />
<div class="file-selector-tip"> <div class="file-selector-tip">
💡 提示工具栏"图片"按钮从图片库选择"上传"按钮快速上传图片"视频"按钮从视频库选择"上传视频"按钮快速上传视频"一键排版"按钮自动优化文章格式"首行缩进"按钮切换段落缩进 💡
提示工具栏"图片"按钮从图片库选择"上传"按钮快速上传图片"视频"按钮从视频库选择"上传视频"按钮快速上传视频"一键排版"按钮自动优化文章格式"首行缩进"按钮切换段落缩进
</div> </div>
</div> </div>
@@ -84,7 +94,7 @@
type="primary" type="primary"
size="small" size="small"
@click="openMarkdownImageSelector" @click="openMarkdownImageSelector"
style="margin-right: 8px;" style="margin-right: 8px"
> >
📷 从图片库选择 📷 从图片库选择
</a-button> </a-button>
@@ -92,7 +102,7 @@
type="default" type="default"
size="small" size="small"
@click="openMarkdownVideoSelector" @click="openMarkdownVideoSelector"
style="margin-right: 8px;" style="margin-right: 8px"
> >
🎬 从视频库选择 🎬 从视频库选择
</a-button> </a-button>
@@ -107,7 +117,8 @@
:onUploadImg="onMarkdownUploadImg" :onUploadImg="onMarkdownUploadImg"
/> />
<div class="file-selector-tip"> <div class="file-selector-tip">
💡 提示支持Markdown语法可以使用工具栏按钮从文件库选择图片/视频也可以直接拖拽上传文件 💡
提示支持Markdown语法可以使用工具栏按钮从文件库选择图片/视频也可以直接拖拽上传文件
</div> </div>
</div> </div>
@@ -143,34 +154,61 @@
</a-radio-group> </a-radio-group>
</a-form-item> </a-form-item>
<a-divider class="py-4 mb-3" style="height: 20px" /> <a-divider class="py-4 mb-3" style="height: 20px" />
<a-form-item label="关键词" name="tags"> <a-form-item label="上传视频" name="video">
<a-select <div class="video-upload-container">
v-model:value="form.tags" <!-- 视频预览 -->
mode="tags" <div v-if="form.video" class="video-preview">
placeholder="按回车分隔" <video
></a-select> :src="form.video"
</a-form-item> controls
<a-form-item label="描述" name="overview"> style="max-width: 100%; max-height: 300px; border-radius: 8px"
<a-textarea ></video>
:rows="3" <a-button
show-count type="link"
placeholder="请输入描述" danger
v-model:value="form.overview" @click="removeVideo"
style="margin-top: 8px"
>
删除视频
</a-button>
</div>
<!-- 上传按钮 -->
<div v-else>
<a-upload
:before-upload="beforeVideoUpload"
:custom-request="handleVideoUpload"
:show-upload-list="false"
accept="video/*"
>
<a-button type="primary">
<upload-outlined /> 选择视频文件
</a-button>
</a-upload>
<div class="upload-tip"> 支持上传视频文件大小不超过200MB </div>
</div>
<!-- 上传进度 -->
<div v-if="videoUploading" class="upload-progress">
<a-progress
:percent="uploadProgress"
:status="uploadProgress === 100 ? 'success' : 'active'"
/> />
<div class="progress-text">
{{ uploadProgressText }}
</div>
</div>
</div>
</a-form-item> </a-form-item>
<a-form-item label="自定义链接" name="pdfUrl"> <a-form-item label="PDF链接" name="pdfUrl" extra="用于PDF文件预览">
<a-input <a-input
allow-clear allow-clear
placeholder="https://" placeholder="https://oss.xxx.cn/xxx.pdf"
v-model:value="form.pdfUrl" v-model:value="form.pdfUrl"
/> />
</a-form-item> </a-form-item>
<a-form-item label="编号" name="code"> <a-form-item label="文章编号" name="code" extra="用于getByCode查询">
<a-input <a-input allow-clear placeholder="code" v-model:value="form.code" />
allow-clear
placeholder="code"
v-model:value="form.code"
/>
</a-form-item> </a-form-item>
<a-form-item label="文章来源" name="source"> <a-form-item label="文章来源" name="source">
<source-select <source-select
@@ -179,6 +217,14 @@
:placeholder="`文章来源`" :placeholder="`文章来源`"
/> />
</a-form-item> </a-form-item>
<a-form-item label="产品概述" name="overview">
<a-textarea
:rows="3"
show-count
placeholder="请输入描述"
v-model:value="form.overview"
/>
</a-form-item>
<a-form-item <a-form-item
label="虚拟阅读量" label="虚拟阅读量"
name="virtualViews" name="virtualViews"
@@ -255,13 +301,12 @@
class="file-selector-modal" class="file-selector-modal"
@done="onVideoSelected" @done="onVideoSelected"
/> />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, watch } from 'vue'; import { ref, reactive, watch } from 'vue';
import { Form, message, Modal } from 'ant-design-vue'; import { Form, message, Modal } from 'ant-design-vue';
import { UploadOutlined } from '@ant-design/icons-vue';
import { assignObject, htmlToText, uuid } from 'ele-admin-pro'; import { assignObject, htmlToText, uuid } from 'ele-admin-pro';
import { addCmsArticle, updateCmsArticle } from '@/api/cms/cmsArticle'; import { addCmsArticle, updateCmsArticle } from '@/api/cms/cmsArticle';
import { CmsArticle } from '@/api/cms/cmsArticle/model'; import { CmsArticle } from '@/api/cms/cmsArticle/model';
@@ -281,6 +326,8 @@ import {CmsNavigation} from '@/api/cms/cmsNavigation/model';
import SourceSelect from '@/views/cms/cmsArticle/dictionary/source-select.vue'; import SourceSelect from '@/views/cms/cmsArticle/dictionary/source-select.vue';
import { useWebsiteSettingStore } from '@/store/modules/setting'; import { useWebsiteSettingStore } from '@/store/modules/setting';
import SelectData from '@/components/SelectFile/components/select-data.vue'; import SelectData from '@/components/SelectFile/components/select-data.vue';
import request from '@/utils/request';
import { SERVER_API_URL } from '@/config/setting';
// 是否是修改 // 是否是修改
const isUpdate = ref(false); const isUpdate = ref(false);
@@ -328,6 +375,11 @@ const category = ref<string[]>([]);
const password = ref(); const password = ref();
const lang = localStorage.getItem('i18n-lang'); const lang = localStorage.getItem('i18n-lang');
// 视频上传相关状态
const videoUploading = ref(false);
const uploadProgress = ref(0);
const uploadProgressText = ref('');
// 用户信息 // 用户信息
const form = reactive<CmsArticle>({ const form = reactive<CmsArticle>({
articleId: undefined, articleId: undefined,
@@ -442,7 +494,11 @@ const onEditorTypeChange = (e: any) => {
} }
// 如果当前有内容,提示用户确认切换 // 如果当前有内容,提示用户确认切换
if (content.value && content.value.trim() !== '' && content.value !== '<p><br></p>') { if (
content.value &&
content.value.trim() !== '' &&
content.value !== '<p><br></p>'
) {
Modal.confirm({ Modal.confirm({
title: '🔄 切换编辑器类型', title: '🔄 切换编辑器类型',
content: '切换编辑器类型可能会影响内容格式,是否继续?', content: '切换编辑器类型可能会影响内容格式,是否继续?',
@@ -509,7 +565,10 @@ const convertContentFormat = (fromType: number, toType: number) => {
.replace(/<i[^>]*>(.*?)<\/i>/gi, '*$1*') .replace(/<i[^>]*>(.*?)<\/i>/gi, '*$1*')
.replace(/<code[^>]*>(.*?)<\/code>/gi, '`$1`') .replace(/<code[^>]*>(.*?)<\/code>/gi, '`$1`')
.replace(/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/gi, '[$2]($1)') .replace(/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/gi, '[$2]($1)')
.replace(/<img[^>]*src="([^"]*)"[^>]*alt="([^"]*)"[^>]*>/gi, '![$2]($1)') .replace(
/<img[^>]*src="([^"]*)"[^>]*alt="([^"]*)"[^>]*>/gi,
'![$2]($1)'
)
.replace(/<img[^>]*src="([^"]*)"[^>]*>/gi, '![图片]($1)') .replace(/<img[^>]*src="([^"]*)"[^>]*>/gi, '![图片]($1)')
.replace(/<p[^>]*>(.*?)<\/p>/gi, '$1\n\n') .replace(/<p[^>]*>(.*?)<\/p>/gi, '$1\n\n')
.replace(/<br[^>]*>/gi, '\n') .replace(/<br[^>]*>/gi, '\n')
@@ -532,7 +591,10 @@ const convertContentFormat = (fromType: number, toType: number) => {
.replace(/\*(.*?)\*/g, '<em>$1</em>') .replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/`(.*?)`/g, '<code>$1</code>') .replace(/`(.*?)`/g, '<code>$1</code>')
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>') .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" style="max-width: 100%;" />') .replace(
/!\[([^\]]*)\]\(([^)]+)\)/g,
'<img src="$2" alt="$1" style="max-width: 100%;" />'
)
.replace(/\n\n/g, '</p><p>') .replace(/\n\n/g, '</p><p>')
.replace(/\n/g, '<br>'); .replace(/\n/g, '<br>');
@@ -574,6 +636,104 @@ const onDeleteFile = (index: number) => {
files.value.splice(index, 1); files.value.splice(index, 1);
}; };
// 视频上传前的验证
const beforeVideoUpload = (file: File) => {
// 检查文件类型
const isVideo = file.type.startsWith('video/');
if (!isVideo) {
message.error('只能上传视频文件!');
return false;
}
// 检查文件大小200MB = 200 * 1024 * 1024 bytes
const maxSize = 200 * 1024 * 1024;
if (file.size > maxSize) {
message.error('视频文件大小不能超过 200MB');
return false;
}
return true;
};
// 处理视频上传
const handleVideoUpload = async (options: any) => {
const { file } = options;
videoUploading.value = true;
uploadProgress.value = 0;
uploadProgressText.value = '准备上传...';
try {
const formData = new FormData();
formData.append('file', file);
// 使用 axios 上传以支持进度跟踪
const res = await request.post<any>(
SERVER_API_URL + '/oss/upload',
formData,
{
onUploadProgress: (progressEvent: any) => {
if (progressEvent.total) {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
uploadProgress.value = percentCompleted;
// 计算已上传和总大小
const loadedMB = (progressEvent.loaded / (1024 * 1024)).toFixed(
2
);
const totalMB = (progressEvent.total / (1024 * 1024)).toFixed(2);
uploadProgressText.value = `正在上传: ${loadedMB}MB / ${totalMB}MB`;
}
}
}
);
if (res.data.code === 0 && res.data.data) {
// 获取上传后的URL视频文件不添加图片处理参数
let videoUrl =
res.data.data.url || res.data.data.path || res.data.data.downloadUrl;
// 如果URL中包含图片处理参数移除它因为这是视频文件
if (videoUrl && videoUrl.includes('?x-oss-process=')) {
videoUrl = videoUrl.split('?x-oss-process=')[0];
}
form.video = videoUrl;
message.success('视频上传成功!');
uploadProgressText.value = '上传完成';
} else {
throw new Error(res.data.message || '上传失败');
}
} catch (error: any) {
console.error('视频上传失败:', error);
message.error('视频上传失败:' + (error.message || '未知错误'));
uploadProgress.value = 0;
} finally {
// 延迟隐藏进度条,让用户看到完成状态
setTimeout(() => {
videoUploading.value = false;
uploadProgress.value = 0;
uploadProgressText.value = '';
}, 1500);
}
};
// 删除视频
const removeVideo = () => {
Modal.confirm({
title: '确认删除',
content: '确定要删除这个视频吗?',
okText: '确定',
cancelText: '取消',
onOk: () => {
form.video = undefined;
message.success('视频已删除');
}
});
};
const editorRef = ref<InstanceType<typeof TinymceEditor> | null>(null); const editorRef = ref<InstanceType<typeof TinymceEditor> | null>(null);
// 文件库选择弹窗状态 // 文件库选择弹窗状态
@@ -620,7 +780,10 @@ const markdownToolbars = [
] as any; ] as any;
// 📝 Markdown编辑器图片上传处理 // 📝 Markdown编辑器图片上传处理
const onMarkdownUploadImg = async (files: File[], callback: (urls: string[]) => void) => { const onMarkdownUploadImg = async (
files: File[],
callback: (urls: string[]) => void
) => {
try { try {
const uploadPromises = files.map(async (file) => { const uploadPromises = files.map(async (file) => {
// 检查文件大小限制为10MB // 检查文件大小限制为10MB
@@ -646,7 +809,7 @@ const onMarkdownUploadImg = async (files: File[], callback: (urls: string[]) =>
}); });
const results = await Promise.all(uploadPromises); const results = await Promise.all(uploadPromises);
const successUrls = results.filter(url => url !== null) as string[]; const successUrls = results.filter((url) => url !== null) as string[];
if (successUrls.length > 0) { if (successUrls.length > 0) {
callback(successUrls); callback(successUrls);
@@ -696,9 +859,9 @@ const config = ref({
paste_data_images: true, paste_data_images: true,
automatic_uploads: true, automatic_uploads: true,
// 自定义工具栏移除默认的image和media按钮添加自定义按钮 // 自定义工具栏移除默认的image和media按钮添加自定义按钮 (视频、上传视频按钮 custom_video_selector quick_video_upload首行缩进
toolbar: [ toolbar: [
'fullscreen preview code codesample emoticons custom_image_selector quick_upload custom_video_selector quick_video_upload auto_format toggle_indent', 'fullscreen preview code codesample emoticons auto_format custom_image_selector quick_upload',
'undo redo | forecolor backcolor', 'undo redo | forecolor backcolor',
'bold italic underline strikethrough', 'bold italic underline strikethrough',
'alignleft aligncenter alignright alignjustify', 'alignleft aligncenter alignright alignjustify',
@@ -740,7 +903,8 @@ const config = ref({
}, },
// 图片工具栏 // 图片工具栏
image_toolbar: 'alignleft aligncenter alignright | rotateleft rotateright | imageoptions', image_toolbar:
'alignleft aligncenter alignright | rotateleft rotateright | imageoptions',
// 图片标题 // 图片标题
image_title: true, image_title: true,
@@ -769,7 +933,9 @@ const config = ref({
onAction: () => { onAction: () => {
// 打开文件库选择弹窗 // 打开文件库选择弹窗
fileSelectCallback.value = (url: string) => { fileSelectCallback.value = (url: string) => {
editor.insertContent(`<img src="${url}" alt="图片" style="max-width: 100%;" />`); editor.insertContent(
`<img src="${url}" alt="图片" style="max-width: 100%;" />`
);
}; };
showFileSelector.value = true; showFileSelector.value = true;
} }
@@ -798,7 +964,9 @@ const config = ref({
.then((res) => { .then((res) => {
loadingMsg(); loadingMsg();
const imageUrl = res.url || res.path; const imageUrl = res.url || res.path;
editor.insertContent(`<img src="${imageUrl}" alt="${file.name}" style="max-width: 100%;" />`); editor.insertContent(
`<img src="${imageUrl}" alt="${file.name}" style="max-width: 100%;" />`
);
message.success('图片上传成功'); message.success('图片上传成功');
}) })
.catch((msg) => { .catch((msg) => {
@@ -874,7 +1042,7 @@ const config = ref({
// 添加一键排版按钮 // 添加一键排版按钮
editor.ui.registry.addButton('auto_format', { editor.ui.registry.addButton('auto_format', {
text: '一键排版', text: '排版',
icon: 'template', icon: 'template',
tooltip: '智能优化文章格式和排版', tooltip: '智能优化文章格式和排版',
onAction: () => { onAction: () => {
@@ -885,7 +1053,6 @@ const config = ref({
// 添加段落首行缩进切换按钮 // 添加段落首行缩进切换按钮
editor.ui.registry.addButton('toggle_indent', { editor.ui.registry.addButton('toggle_indent', {
text: '首行缩进',
icon: 'indent', icon: 'indent',
tooltip: '切换段落首行缩进(适合中文排版)', tooltip: '切换段落首行缩进(适合中文排版)',
onAction: () => { onAction: () => {
@@ -928,7 +1095,12 @@ const handleAutoFormat = (editor: any) => {
try { try {
// 1. 检查内容 // 1. 检查内容
const content = editor.getContent(); const content = editor.getContent();
if (!content || content.trim() === '' || content === '<p><br></p>' || content === '<p></p>') { if (
!content ||
content.trim() === '' ||
content === '<p><br></p>' ||
content === '<p></p>'
) {
message.warning({ message.warning({
content: '📝 请先输入一些内容,然后再使用一键排版功能', content: '📝 请先输入一些内容,然后再使用一键排版功能',
duration: 3 duration: 3
@@ -958,7 +1130,6 @@ const handleAutoFormat = (editor: any) => {
// 5. 可选:显示优化统计 // 5. 可选:显示优化统计
showOptimizationStats(content, optimizedContent); showOptimizationStats(content, optimizedContent);
} catch (error) { } catch (error) {
loadingMsg(); loadingMsg();
console.error('排版优化失败:', error); console.error('排版优化失败:', error);
@@ -968,7 +1139,6 @@ const handleAutoFormat = (editor: any) => {
}); });
} }
}, 800); // 给用户一个良好的反馈体验 }, 800); // 给用户一个良好的反馈体验
} catch (error) { } catch (error) {
console.error('一键排版功能错误:', error); console.error('一键排版功能错误:', error);
message.error({ message.error({
@@ -979,7 +1149,10 @@ const handleAutoFormat = (editor: any) => {
}; };
// 📊 显示优化统计信息 // 📊 显示优化统计信息
const showOptimizationStats = (originalContent: string, optimizedContent: string) => { const showOptimizationStats = (
originalContent: string,
optimizedContent: string
) => {
const stats = analyzeOptimization(originalContent, optimizedContent); const stats = analyzeOptimization(originalContent, optimizedContent);
if (stats.optimizations.length > 0) { if (stats.optimizations.length > 0) {
@@ -995,19 +1168,31 @@ const analyzeOptimization = (original: string, optimized: string) => {
const optimizations: string[] = []; const optimizations: string[] = [];
// 检查各种优化项目 // 检查各种优化项目
if ((optimized.match(/<h[1-6][^>]*style/g) || []).length > (original.match(/<h[1-6][^>]*style/g) || []).length) { if (
(optimized.match(/<h[1-6][^>]*style/g) || []).length >
(original.match(/<h[1-6][^>]*style/g) || []).length
) {
optimizations.push('标题样式'); optimizations.push('标题样式');
} }
if ((optimized.match(/<p[^>]*style/g) || []).length > (original.match(/<p[^>]*style/g) || []).length) { if (
(optimized.match(/<p[^>]*style/g) || []).length >
(original.match(/<p[^>]*style/g) || []).length
) {
optimizations.push('段落格式'); optimizations.push('段落格式');
} }
if ((optimized.match(/<img[^>]*style/g) || []).length > (original.match(/<img[^>]*style/g) || []).length) { if (
(optimized.match(/<img[^>]*style/g) || []).length >
(original.match(/<img[^>]*style/g) || []).length
) {
optimizations.push('图片布局'); optimizations.push('图片布局');
} }
if ((optimized.match(/<ul[^>]*style|<ol[^>]*style/g) || []).length > (original.match(/<ul[^>]*style|<ol[^>]*style/g) || []).length) { if (
(optimized.match(/<ul[^>]*style|<ol[^>]*style/g) || []).length >
(original.match(/<ul[^>]*style|<ol[^>]*style/g) || []).length
) {
optimizations.push('列表格式'); optimizations.push('列表格式');
} }
@@ -1019,12 +1204,30 @@ const smartFormatContent = (content: string): string => {
let optimized = content; let optimized = content;
// 1. 🏷️ 标题优化 - 让标题更有层次感 // 1. 🏷️ 标题优化 - 让标题更有层次感
optimized = optimized.replace(/<h1([^>]*)>/g, '<h1$1 style="font-size: 28px; font-weight: 700; margin: 24px 0 16px 0; line-height: 1.3; color: #1a1a1a; border-bottom: 2px solid #e8e8e8; padding-bottom: 10px;">'); optimized = optimized.replace(
optimized = optimized.replace(/<h2([^>]*)>/g, '<h2$1 style="font-size: 24px; font-weight: 600; margin: 20px 0 14px 0; line-height: 1.4; color: #2c2c2c;">'); /<h1([^>]*)>/g,
optimized = optimized.replace(/<h3([^>]*)>/g, '<h3$1 style="font-size: 20px; font-weight: 600; margin: 18px 0 12px 0; line-height: 1.4; color: #3c3c3c;">'); '<h1$1 style="font-size: 28px; font-weight: 700; margin: 24px 0 16px 0; line-height: 1.3; color: #1a1a1a; border-bottom: 2px solid #e8e8e8; padding-bottom: 10px;">'
optimized = optimized.replace(/<h4([^>]*)>/g, '<h4$1 style="font-size: 16px; font-weight: 600; margin: 14px 0 8px 0; line-height: 1.4; color: #4c4c4c;">'); );
optimized = optimized.replace(/<h5([^>]*)>/g, '<h5$1 style="font-size: 14px; font-weight: 600; margin: 12px 0 6px 0; line-height: 1.4; color: #5c5c5c;">'); optimized = optimized.replace(
optimized = optimized.replace(/<h6([^>]*)>/g, '<h6$1 style="font-size: 13px; font-weight: 600; margin: 10px 0 5px 0; line-height: 1.4; color: #6c6c6c;">'); /<h2([^>]*)>/g,
'<h2$1 style="font-size: 24px; font-weight: 600; margin: 20px 0 14px 0; line-height: 1.4; color: #2c2c2c;">'
);
optimized = optimized.replace(
/<h3([^>]*)>/g,
'<h3$1 style="font-size: 20px; font-weight: 600; margin: 18px 0 12px 0; line-height: 1.4; color: #3c3c3c;">'
);
optimized = optimized.replace(
/<h4([^>]*)>/g,
'<h4$1 style="font-size: 16px; font-weight: 600; margin: 14px 0 8px 0; line-height: 1.4; color: #4c4c4c;">'
);
optimized = optimized.replace(
/<h5([^>]*)>/g,
'<h5$1 style="font-size: 14px; font-weight: 600; margin: 12px 0 6px 0; line-height: 1.4; color: #5c5c5c;">'
);
optimized = optimized.replace(
/<h6([^>]*)>/g,
'<h6$1 style="font-size: 13px; font-weight: 600; margin: 10px 0 5px 0; line-height: 1.4; color: #6c6c6c;">'
);
// 2. 📝 段落优化 - 让阅读更舒适 // 2. 📝 段落优化 - 让阅读更舒适
optimized = optimized.replace(/<p([^>]*)>/g, (match, attrs) => { optimized = optimized.replace(/<p([^>]*)>/g, (match, attrs) => {
@@ -1038,33 +1241,68 @@ const smartFormatContent = (content: string): string => {
optimized = optimized.replace(/<img([^>]*?)>/g, (match, attrs) => { optimized = optimized.replace(/<img([^>]*?)>/g, (match, attrs) => {
if (!attrs.includes('style=')) { if (!attrs.includes('style=')) {
const hasAlt = attrs.includes('alt='); const hasAlt = attrs.includes('alt=');
return `<img${attrs} style="max-width: 100%; height: auto; margin: 20px auto; display: block; border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,0.1);"${!hasAlt ? ' alt="图片"' : ''}>`; return `<img${attrs} style="max-width: 100%; height: auto; margin: 20px auto; display: block; border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,0.1);"${
!hasAlt ? ' alt="图片"' : ''
}>`;
} }
return match; return match;
}); });
// 4. 📋 列表优化 - 让列表更清晰 // 4. 📋 列表优化 - 让列表更清晰
optimized = optimized.replace(/<ul([^>]*)>/g, '<ul$1 style="margin: 16px 0; padding-left: 24px; line-height: 1.6;">'); optimized = optimized.replace(
optimized = optimized.replace(/<ol([^>]*)>/g, '<ol$1 style="margin: 16px 0; padding-left: 24px; line-height: 1.6;">'); /<ul([^>]*)>/g,
optimized = optimized.replace(/<li([^>]*)>/g, '<li$1 style="margin: 8px 0; color: #333;">'); '<ul$1 style="margin: 16px 0; padding-left: 24px; line-height: 1.6;">'
);
optimized = optimized.replace(
/<ol([^>]*)>/g,
'<ol$1 style="margin: 16px 0; padding-left: 24px; line-height: 1.6;">'
);
optimized = optimized.replace(
/<li([^>]*)>/g,
'<li$1 style="margin: 8px 0; color: #333;">'
);
// 5. 💬 引用优化 - 让引用更突出 // 5. 💬 引用优化 - 让引用更突出
optimized = optimized.replace(/<blockquote([^>]*)>/g, '<blockquote$1 style="margin: 20px 0; padding: 16px 20px; border-left: 4px solid #1890ff; background: linear-gradient(90deg, #f6f8fa 0%, #ffffff 100%); font-style: italic; color: #555;">'); optimized = optimized.replace(
/<blockquote([^>]*)>/g,
'<blockquote$1 style="margin: 20px 0; padding: 16px 20px; border-left: 4px solid #1890ff; background: linear-gradient(90deg, #f6f8fa 0%, #ffffff 100%); font-style: italic; color: #555;">'
);
// 6. 💻 代码优化 - 让代码更专业 // 6. 💻 代码优化 - 让代码更专业
optimized = optimized.replace(/<code([^>]*)>/g, '<code$1 style="background-color: #f1f3f4; padding: 2px 6px; border-radius: 4px; font-family: \'Fira Code\', Consolas, Monaco, monospace; font-size: 0.9em; color: #d73a49;">'); optimized = optimized.replace(
optimized = optimized.replace(/<pre([^>]*)>/g, '<pre$1 style="margin: 20px 0; padding: 20px; background-color: #f8f9fa; border: 1px solid #e9ecef; border-radius: 8px; overflow-x: auto; font-family: \'Fira Code\', Consolas, Monaco, monospace; font-size: 14px; line-height: 1.5;">'); /<code([^>]*)>/g,
'<code$1 style="background-color: #f1f3f4; padding: 2px 6px; border-radius: 4px; font-family: \'Fira Code\', Consolas, Monaco, monospace; font-size: 0.9em; color: #d73a49;">'
);
optimized = optimized.replace(
/<pre([^>]*)>/g,
'<pre$1 style="margin: 20px 0; padding: 20px; background-color: #f8f9fa; border: 1px solid #e9ecef; border-radius: 8px; overflow-x: auto; font-family: \'Fira Code\', Consolas, Monaco, monospace; font-size: 14px; line-height: 1.5;">'
);
// 7. 📊 表格优化 - 让表格更美观 // 7. 📊 表格优化 - 让表格更美观
optimized = optimized.replace(/<table([^>]*)>/g, '<table$1 style="width: 100%; border-collapse: collapse; margin: 20px 0; box-shadow: 0 2px 8px rgba(0,0,0,0.1); border-radius: 8px; overflow: hidden;">'); optimized = optimized.replace(
optimized = optimized.replace(/<th([^>]*)>/g, '<th$1 style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 12px; text-align: left; font-weight: 600;">'); /<table([^>]*)>/g,
optimized = optimized.replace(/<td([^>]*)>/g, '<td$1 style="padding: 12px; border-bottom: 1px solid #eee; color: #333;">'); '<table$1 style="width: 100%; border-collapse: collapse; margin: 20px 0; box-shadow: 0 2px 8px rgba(0,0,0,0.1); border-radius: 8px; overflow: hidden;">'
);
optimized = optimized.replace(
/<th([^>]*)>/g,
'<th$1 style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 12px; text-align: left; font-weight: 600;">'
);
optimized = optimized.replace(
/<td([^>]*)>/g,
'<td$1 style="padding: 12px; border-bottom: 1px solid #eee; color: #333;">'
);
// 8. 🔗 链接优化 - 让链接更友好 // 8. 🔗 链接优化 - 让链接更友好
optimized = optimized.replace(/<a([^>]*)>/g, '<a$1 style="color: #1890ff; text-decoration: none; border-bottom: 1px solid transparent; transition: border-bottom 0.2s ease;" onmouseover="this.style.borderBottom=\'1px solid #1890ff\'" onmouseout="this.style.borderBottom=\'1px solid transparent\'">'); optimized = optimized.replace(
/<a([^>]*)>/g,
'<a$1 style="color: #1890ff; text-decoration: none; border-bottom: 1px solid transparent; transition: border-bottom 0.2s ease;" onmouseover="this.style.borderBottom=\'1px solid #1890ff\'" onmouseout="this.style.borderBottom=\'1px solid transparent\'">'
);
// 9. 分隔线优化 - 让分隔线更优雅 // 9. 分隔线优化 - 让分隔线更优雅
optimized = optimized.replace(/<hr([^>]*)>/g, '<hr$1 style="border: none; height: 2px; background: linear-gradient(90deg, transparent, #e8e8e8, transparent); margin: 30px 0;">'); optimized = optimized.replace(
/<hr([^>]*)>/g,
'<hr$1 style="border: none; height: 2px; background: linear-gradient(90deg, transparent, #e8e8e8, transparent); margin: 30px 0;">'
);
// 10. 🧹 清理多余空白 // 10. 🧹 清理多余空白
optimized = optimized.replace(/\s+/g, ' '); // 清理多余空格 optimized = optimized.replace(/\s+/g, ' '); // 清理多余空格
@@ -1079,7 +1317,12 @@ const toggleParagraphIndent = (editor: any) => {
try { try {
const content = editor.getContent(); const content = editor.getContent();
if (!content || content.trim() === '' || content === '<p><br></p>' || content === '<p></p>') { if (
!content ||
content.trim() === '' ||
content === '<p><br></p>' ||
content === '<p></p>'
) {
message.warning({ message.warning({
content: '📝 请先输入一些段落内容,然后再切换首行缩进', content: '📝 请先输入一些段落内容,然后再切换首行缩进',
duration: 3 duration: 3
@@ -1088,7 +1331,9 @@ const toggleParagraphIndent = (editor: any) => {
} }
// 检查当前是否有首行缩进 // 检查当前是否有首行缩进
const hasIndent = content.includes('text-indent: 2em') || content.includes('text-indent:2em'); const hasIndent =
content.includes('text-indent: 2em') ||
content.includes('text-indent:2em');
let newContent: string; let newContent: string;
let actionText: string; let actionText: string;
@@ -1109,7 +1354,6 @@ const toggleParagraphIndent = (editor: any) => {
content: `📐 ${actionText}`, content: `📐 ${actionText}`,
duration: 3 duration: 3
}); });
} catch (error) { } catch (error) {
console.error('首行缩进切换失败:', error); console.error('首行缩进切换失败:', error);
message.error({ message.error({
@@ -1130,7 +1374,10 @@ const addIndentToParagraphs = (content: string): string => {
return match.replace(/text-indent:\s*[^;]+;?/g, 'text-indent: 2em;'); return match.replace(/text-indent:\s*[^;]+;?/g, 'text-indent: 2em;');
} else { } else {
// 在现有 style 中添加 text-indent // 在现有 style 中添加 text-indent
return match.replace(/style="([^"]*)"/, 'style="$1 text-indent: 2em;"'); return match.replace(
/style="([^"]*)"/,
'style="$1 text-indent: 2em;"'
);
} }
} else { } else {
// 添加新的 style 属性 // 添加新的 style 属性
@@ -1224,8 +1471,7 @@ const save = () => {
loading.value = false; loading.value = false;
}); });
}) })
.catch(() => { .catch(() => {});
});
}; };
watch( watch(
@@ -1551,4 +1797,39 @@ watch(
} }
} }
} }
// 视频上传容器样式
.video-upload-container {
.video-preview {
display: flex;
flex-direction: column;
align-items: flex-start;
video {
border: 1px solid #e8e8e8;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
}
.upload-tip {
margin-top: 8px;
color: #999;
font-size: 12px;
}
.upload-progress {
margin-top: 16px;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e8e8e8;
.progress-text {
margin-top: 8px;
text-align: center;
color: #666;
font-size: 13px;
}
}
}
</style> </style>

View File

@@ -92,8 +92,6 @@
// 中文语言文件 // 中文语言文件
import zh_Hans from 'bytemd/locales/zh_Hans.json'; import zh_Hans from 'bytemd/locales/zh_Hans.json';
import 'bytemd/dist/index.min.css'; import 'bytemd/dist/index.min.css';
import highlight from '@bytemd/plugin-highlight-ssr';
import 'highlight.js/styles/default.css';
import { ShopMerchantAccount } from '@/api/shop/shopMerchantAccount/model'; import { ShopMerchantAccount } from '@/api/shop/shopMerchantAccount/model';
import { User } from '@/api/system/user/model'; import { User } from '@/api/system/user/model';
@@ -173,7 +171,7 @@
type: 'string', type: 'string',
message: '请填写消息内容', message: '请填写消息内容',
trigger: 'blur', trigger: 'blur',
validator: async (_rule: RuleObject, value: string) => { validator: async (_rule: RuleObject, _: string) => {
if (content.value == '') { if (content.value == '') {
return Promise.reject('请填写消息内容'); return Promise.reject('请填写消息内容');
} }
@@ -187,8 +185,7 @@
const plugins = ref([ const plugins = ref([
gfm({ gfm({
locale: zh_HansGfm locale: zh_HansGfm
}), })
highlight()
]); ]);
const onToUser = (item: User) => { const onToUser = (item: User) => {
@@ -199,7 +196,7 @@
// form.toUserName = item.nickname; // form.toUserName = item.nickname;
}; };
const chooseType = (item: any) => { const chooseType = (_: any) => {
form.type = 'text'; form.type = 'text';
}; };