Browse Source

优化:编辑器功能

dev
科技小王子 3 months ago
parent
commit
742b6e466c
  1. 1
      .gitignore
  2. 531
      src/views/cms/cmsArticle/components/articleEdit.vue

1
.gitignore

@ -26,3 +26,4 @@ pnpm-debug.log*
*.sln
*.sw?
node_modules
node_modules

531
src/views/cms/cmsArticle/components/articleEdit.vue

@ -1,7 +1,7 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
width="80%"
width="75%"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
@ -29,12 +29,24 @@
<!-- </a-radio-group>-->
<!-- </template>-->
<a-tab-pane tab="基本信息" key="base">
<a-form-item label="文章标题" name="title">
<div class="title-input-container">
<a-input
allow-clear
style="width: 600px"
placeholder="文章标题"
v-model:value="form.title"
@pressEnter="save"
:maxlength="100"
/>
</div>
</a-form-item>
<a-form-item label="所属栏目" name="categoryId">
<a-tree-select
allow-clear
:tree-data="navigationList"
tree-default-expand-all
style="width: 558px"
style="width: 320px"
placeholder="请选择栏目"
:value="form.categoryId || undefined"
:listHeight="700"
@ -43,24 +55,15 @@
@change="onCategoryId"
/>
</a-form-item>
<a-form-item label="文章标题" name="title">
<a-input
allow-clear
style="width: 558px"
placeholder="文章标题"
v-model:value="form.title"
@pressEnter="save"
/>
</a-form-item>
<a-form-item label="封面图" name="image">
<SelectFile
:placeholder="`请选择图片`"
:limit="1"
:data="images"
@done="chooseImage"
@del="onDeleteItem"
/>
</a-form-item>
<!-- <a-form-item label="封面图" name="image">-->
<!-- <SelectFile-->
<!-- :placeholder="`请选择图片`"-->
<!-- :limit="1"-->
<!-- :data="images"-->
<!-- @done="chooseImage"-->
<!-- @del="onDeleteItem"-->
<!-- />-->
<!-- </a-form-item>-->
<a-form-item label="预览图" name="files">
<SelectFile
:placeholder="`请选择图片`"
@ -70,6 +73,36 @@
@del="onDeleteFile"
/>
</a-form-item>
<a-form-item label="内容详情">
<!-- 编辑器 -->
<tinymce-editor
ref="editorRef"
v-if="editor == 1"
class="editor-content"
v-model:value="content"
:disabled="disabled"
:init="config"
placeholder="支持直接粘贴或拖拽图片,也可点击工具栏图片按钮从文件库选择"
/>
<div class="file-selector-tip" v-if="editor == 1">
💡 提示工具栏"图片"按钮从图片库选择"上传"按钮快速上传图片"视频"按钮从视频库选择"上传视频"按钮快速上传视频"一键排版"按钮自动优化文章格式"首行缩进"按钮切换段落缩进
</div>
<MdEditor
v-if="editor == 2"
v-model="content"
:disabled="disabled"
/>
<a-space
class="py-2 flex items-center text-gray-400"
v-if="lang == 'zh_CN'"
>
<a-switch
checked-children="AI翻译"
v-model:checked="form.translation"
/>
<div v-if="form.translation">启用后将自动翻译其他语言版本</div>
</a-space>
</a-form-item>
<a-form-item label="摘要">
<a-textarea
:rows="3"
@ -100,36 +133,6 @@
</a-radio-group>
</a-form-item>
</a-tab-pane>
<a-tab-pane tab="内容详情" key="content">
<!-- 编辑器 -->
<tinymce-editor
ref="editorRef"
v-if="editor == 1"
class="editor-content"
v-model:value="content"
:disabled="disabled"
:init="config"
placeholder="支持直接粘贴或拖拽图片,也可点击工具栏图片按钮从文件库选择"
/>
<div class="file-selector-tip" v-if="editor == 1">
💡 提示工具栏"图片"按钮从图片库选择"上传"按钮快速上传图片"视频"按钮从视频库选择"上传视频"按钮快速上传视频
</div>
<MdEditor
v-if="editor == 2"
v-model="content"
:disabled="disabled"
/>
<a-space
class="py-2 flex items-center text-gray-400"
v-if="lang == 'zh_CN'"
>
<a-switch
checked-children="AI翻译"
v-model:checked="form.translation"
/>
<div v-if="form.translation">启用后将自动翻译其他语言版本</div>
</a-space>
</a-tab-pane>
<a-tab-pane tab="其他选项" key="other">
<a-form-item label="关键词" name="tags">
<a-select
@ -229,6 +232,8 @@
class="file-selector-modal"
@done="onVideoSelected"
/>
</template>
<script lang="ts" setup>
@ -339,7 +344,7 @@
files: '',
lang: locale.value || undefined,
//
sortNumber: undefined,
sortNumber: 100,
//
comments: undefined,
//
@ -401,6 +406,10 @@
//
const onCategoryId = (id: number) => {
form.categoryId = id;
// 💾
if (!isUpdate.value && id) {
saveLastCategory(id);
}
};
const onChange = () => {
@ -453,7 +462,7 @@
// imagemedia
toolbar: [
'fullscreen preview code codesample emoticons custom_image_selector quick_upload custom_video_selector quick_video_upload',
'fullscreen preview code codesample emoticons custom_image_selector quick_upload custom_video_selector quick_video_upload auto_format toggle_indent',
'undo redo | forecolor backcolor',
'bold italic underline strikethrough',
'alignleft aligncenter alignright alignjustify',
@ -626,6 +635,27 @@
input.click();
}
});
//
editor.ui.registry.addButton('auto_format', {
text: '一键排版',
icon: 'template',
tooltip: '智能优化文章格式和排版',
onAction: () => {
//
handleAutoFormat(editor);
}
});
//
editor.ui.registry.addButton('toggle_indent', {
text: '首行缩进',
icon: 'indent',
tooltip: '切换段落首行缩进(适合中文排版)',
onAction: () => {
toggleParagraphIndent(editor);
}
});
}
});
@ -657,8 +687,257 @@
showVideoSelector.value = false;
};
// 🎨 -
const handleAutoFormat = (editor: any) => {
try {
// 1.
const content = editor.getContent();
if (!content || content.trim() === '' || content === '<p><br></p>' || content === '<p></p>') {
message.warning({
content: '📝 请先输入一些内容,然后再使用一键排版功能',
duration: 3
});
return;
}
// 2.
const loadingMsg = message.loading({
content: '✨ 正在为您的文章进行智能排版优化...',
duration: 0
});
// 3.
setTimeout(() => {
try {
const optimizedContent = smartFormatContent(content);
editor.setContent(optimizedContent);
loadingMsg();
// 4.
message.success({
content: '🎉 排版优化完成!您的文章现在看起来更专业了',
duration: 4
});
// 5.
showOptimizationStats(content, optimizedContent);
} catch (error) {
loadingMsg();
console.error('排版优化失败:', error);
message.error({
content: '😅 排版优化遇到了问题,请检查文章内容后重试',
duration: 4
});
}
}, 800); //
} catch (error) {
console.error('一键排版功能错误:', error);
message.error({
content: '🔧 功能暂时不可用,请刷新页面后重试',
duration: 4
});
}
};
// 📊
const showOptimizationStats = (originalContent: string, optimizedContent: string) => {
const stats = analyzeOptimization(originalContent, optimizedContent);
if (stats.optimizations.length > 0) {
message.info({
content: `📈 本次优化: ${stats.optimizations.join('、')}`,
duration: 6
});
}
};
// 🔍
const analyzeOptimization = (original: string, optimized: string) => {
const optimizations: string[] = [];
//
if ((optimized.match(/<h[1-6][^>]*style/g) || []).length > (original.match(/<h[1-6][^>]*style/g) || []).length) {
optimizations.push('标题样式');
}
if ((optimized.match(/<p[^>]*style/g) || []).length > (original.match(/<p[^>]*style/g) || []).length) {
optimizations.push('段落格式');
}
if ((optimized.match(/<img[^>]*style/g) || []).length > (original.match(/<img[^>]*style/g) || []).length) {
optimizations.push('图片布局');
}
if ((optimized.match(/<ul[^>]*style|<ol[^>]*style/g) || []).length > (original.match(/<ul[^>]*style|<ol[^>]*style/g) || []).length) {
optimizations.push('列表格式');
}
return { optimizations };
};
// 🎨 -
const smartFormatContent = (content: string): string => {
let optimized = content;
// 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(/<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. 📝 -
optimized = optimized.replace(/<p([^>]*)>/g, (match, attrs) => {
if (!attrs.includes('style=')) {
return `<p${attrs} style="line-height: 1.8; margin: 16px 0; text-indent: 2em; color: #333;">`;
}
return match;
});
// 3. 🖼 -
optimized = optimized.replace(/<img([^>]*?)>/g, (match, attrs) => {
if (!attrs.includes('style=')) {
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 match;
});
// 4. 📋 -
optimized = optimized.replace(/<ul([^>]*)>/g, '<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. 💬 -
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. 💻 -
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(/<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. 📊 -
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(/<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. 🔗 -
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. 线 - 线
optimized = optimized.replace(/<hr([^>]*)>/g, '<hr$1 style="border: none; height: 2px; background: linear-gradient(90deg, transparent, #e8e8e8, transparent); margin: 30px 0;">');
// 10. 🧹
optimized = optimized.replace(/\s+/g, ' '); //
optimized = optimized.replace(/<p[^>]*>\s*<\/p>/g, ''); //
optimized = optimized.replace(/(<\/[^>]+>)\s+(<[^>]+>)/g, '$1$2'); //
return optimized;
};
// 🔄
const toggleParagraphIndent = (editor: any) => {
try {
const content = editor.getContent();
if (!content || content.trim() === '' || content === '<p><br></p>' || content === '<p></p>') {
message.warning({
content: '📝 请先输入一些段落内容,然后再切换首行缩进',
duration: 3
});
return;
}
//
const hasIndent = content.includes('text-indent: 2em') || content.includes('text-indent:2em');
let newContent: string;
let actionText: string;
if (hasIndent) {
//
newContent = removeIndentFromParagraphs(content);
actionText = '已移除段落首行缩进';
} else {
//
newContent = addIndentToParagraphs(content);
actionText = '已添加段落首行缩进';
}
editor.setContent(newContent);
message.success({
content: `📐 ${actionText}`,
duration: 3
});
} catch (error) {
console.error('首行缩进切换失败:', error);
message.error({
content: '🔧 首行缩进切换失败,请重试',
duration: 3
});
}
};
//
const addIndentToParagraphs = (content: string): string => {
return content.replace(/<p([^>]*)>/g, (match, attrs) => {
// style
if (attrs.includes('style=')) {
// text-indent
if (attrs.includes('text-indent')) {
// text-indent
return match.replace(/text-indent:\s*[^;]+;?/g, 'text-indent: 2em;');
} else {
// style text-indent
return match.replace(/style="([^"]*)"/, 'style="$1 text-indent: 2em;"');
}
} else {
// style
return `<p${attrs} style="text-indent: 2em;">`;
}
});
};
//
const removeIndentFromParagraphs = (content: string): string => {
return content.replace(/<p([^>]*)>/g, (match, attrs) => {
if (attrs.includes('text-indent')) {
// text-indent
let newAttrs = attrs.replace(/text-indent:\s*[^;]+;?\s*/g, '');
// style style
newAttrs = newAttrs.replace(/style="\s*"/g, '');
newAttrs = newAttrs.replace(/style=''\s*/g, '');
return `<p${newAttrs}>`;
}
return match;
});
};
const { resetFields } = useForm(form, rules);
// 💾
const LAST_CATEGORY_KEY = 'cms_article_last_category';
//
const saveLastCategory = (categoryId: number | undefined) => {
if (categoryId) {
localStorage.setItem(LAST_CATEGORY_KEY, categoryId.toString());
}
};
//
const getLastCategory = (): number | undefined => {
const saved = localStorage.getItem(LAST_CATEGORY_KEY);
return saved ? parseInt(saved) : undefined;
};
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
@ -689,6 +968,12 @@
.then((msg) => {
loading.value = false;
message.success(msg);
// 💾
if (!isUpdate.value && form.categoryId) {
saveLastCategory(form.categoryId);
}
updateVisible(false);
emit('done');
})
@ -705,9 +990,6 @@
watch(
() => props.visible,
(visible) => {
if (props.categoryId) {
form.categoryId = props.categoryId;
}
if (localStorage.getItem('Editor')) {
editor.value = Number(localStorage.getItem('Editor'));
}
@ -717,7 +999,9 @@
category.value = [];
files.value = [];
content.value = '';
if (props.data) {
//
loading.value = true;
const data = props.data;
//
@ -754,19 +1038,22 @@
});
}
loading.value = false;
// assignObject(form, props.data);
//
// if(props.data.categoryParent){
// category.value.push(props.data.categoryParent);
// }
// if(props.data.categoryChildren){
// category.value.push(props.data.categoryChildren);
// }
isUpdate.value = true;
} else {
//
isUpdate.value = false;
// 🎯
// 1. categoryId使
// 2. 使
if (props.categoryId) {
form.categoryId = props.categoryId;
} else {
const lastCategory = getLastCategory();
if (lastCategory) {
form.categoryId = lastCategory;
}
}
}
} else {
resetFields();
@ -821,4 +1108,118 @@
z-index: 9999 !important;
}
}
//
:deep(.format-options-modal) {
.ant-modal {
z-index: 10000 !important;
}
.ant-modal-mask {
z-index: 9999 !important;
}
}
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
.format-presets {
.format-preset-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.format-preset-card {
border: 2px solid #e8e8e8;
border-radius: 8px;
padding: 16px;
cursor: pointer;
transition: all 0.3s ease;
background: #ffffff;
&:hover {
border-color: #1890ff;
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.15);
transform: translateY(-2px);
}
.preset-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #1a1a1a;
}
.preset-icon {
font-size: 24px;
}
}
.preset-description {
color: #666;
font-size: 14px;
margin-bottom: 12px;
line-height: 1.5;
}
.preset-preview {
background: #f8f9fa;
border-radius: 4px;
padding: 12px;
.preview-title {
font-weight: 600;
font-size: 14px;
color: #1a1a1a;
margin-bottom: 6px;
}
.preview-text {
font-size: 12px;
color: #666;
line-height: 1.4;
}
}
}
.format-tips {
background: #f6f8fa;
border-radius: 8px;
padding: 16px;
border-left: 4px solid #1890ff;
h4 {
margin: 0 0 12px 0;
font-size: 14px;
font-weight: 600;
color: #1a1a1a;
}
ul {
margin: 0;
padding-left: 20px;
li {
color: #666;
font-size: 13px;
line-height: 1.6;
margin-bottom: 4px;
}
}
}
}
</style>

Loading…
Cancel
Save