kboss/f/web-kboss/src/views/homePage/news/ArticleEditorDialog.vue
2026-07-31 16:09:44 +08:00

1294 lines
35 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<el-dialog
:visible.sync="dialogVisible"
:fullscreen="true"
append-to-body
custom-class="article-editor-dialog"
:close-on-click-modal="false"
:before-close="handleBeforeClose"
@open="handleOpen"
@close="handleDialogClose"
>
<div slot="title" class="article-dialog-title">
<button type="button" class="dialog-back-btn" @click="requestClose">
<i class="el-icon-arrow-left"></i>
返回
</button>
<span>{{ dialogTitle }}</span>
</div>
<div class="article-editor" :class="{ 'is-editor-fullscreen': editorFullscreen }">
<div class="article-editor__left" :class="{ 'has-language-form': showEnglishForm }">
<div class="article-editor__form">
<el-form :model="form" label-width="76px" size="small">
<el-form-item>
<template slot="label"><span class="required-star">*</span>文章标题</template>
<el-input v-model.trim="form.title" placeholder="请输入文章标题" />
</el-form-item>
<el-form-item>
<template slot="label"><span class="required-star">*</span>文章摘要</template>
<el-input
v-model.trim="form.summary"
type="textarea"
:rows="2"
maxlength="120"
show-word-limit
placeholder="请输入文章摘要"
/>
</el-form-item>
<div class="article-editor__inline">
<el-form-item>
<template slot="label"><span class="required-star">*</span>文章类型</template>
<el-select v-model="form.type" placeholder="请选择文章类型">
<el-option label="企业动态" value="企业动态" />
<el-option label="产品动态" value="产品动态" />
<el-option label="行业洞察" value="行业洞察" />
<el-option label="活动资讯" value="活动资讯" />
</el-select>
</el-form-item>
<el-form-item>
<template slot="label"><span class="required-star">*</span>发布时间</template>
<el-date-picker
v-model="form.publishTime"
type="date"
value-format="yyyy-MM-dd"
placeholder="选择发布时间"
/>
</el-form-item>
</div>
<el-form-item>
<template slot="label"><span class="required-star">*</span>封面配图</template>
<div class="cover-row">
<el-button type="primary" size="small" @click="$refs.coverInput.click()">上传图片</el-button>
<el-input
v-model.trim="form.coverUrl"
class="cover-url-input"
placeholder="可填写图片地址,或点击左侧上传本地图片"
@input="handleCoverUrlInput"
/>
<span v-if="form.coverName" class="cover-name">{{ form.coverName }}</span>
<input ref="coverInput" type="file" accept="image/*" class="hidden-input" @change="handleCoverUpload">
</div>
</el-form-item>
</el-form>
<div class="language-trigger">
<div class="language-trigger__title">
<span class="language-trigger__badge">EN</span>
<div>
<strong>多语言输入</strong>
<small>添加英文版文章内容</small>
</div>
</div>
<button
type="button"
class="language-trigger__button"
:aria-expanded="showEnglishForm.toString()"
@click="showEnglishForm = !showEnglishForm"
>
{{ showEnglishForm ? '收起英文版' : '添加英文版' }}
<i :class="showEnglishForm ? 'el-icon-arrow-up' : 'el-icon-arrow-down'" />
</button>
</div>
</div>
<div v-if="dialogVisible" class="wang-editor-wrap">
<Toolbar
class="wang-toolbar"
:editor="editor"
:default-config="toolbarConfig"
:mode="editorMode"
/>
<Editor
v-model="form.content"
class="wang-editor"
:default-config="editorConfig"
:mode="editorMode"
@onCreated="handleEditorCreated"
/>
</div>
<section v-if="showEnglishForm" class="article-editor__language-section">
<div class="language-section-head">
<div>
<span class="language-section-head__eyebrow">ENGLISH VERSION</span>
<h3>英文版文章</h3>
<p>英文内容会与中文内容一并保存或发布</p>
</div>
<span class="language-section-head__tag">English</span>
</div>
<div class="language-form-grid">
<div class="language-field language-field--wide">
<label>Article Title</label>
<el-input v-model.trim="english.title" placeholder="Enter article title" />
</div>
<div class="language-field language-field--wide">
<label>Intro</label>
<el-input
v-model.trim="english.summary"
type="textarea"
:rows="2"
maxlength="120"
show-word-limit
placeholder="Enter article summary"
/>
</div>
<div class="language-field">
<label>Category</label>
<el-select v-model="english.category" placeholder="Select category">
<el-option label="Corporate News" value="Corporate News" />
<el-option label="Product News" value="Product News" />
<el-option label="Industry Insights" value="Industry Insights" />
<el-option label="Event News" value="Event News" />
</el-select>
</div>
<div class="language-field">
<label>Date</label>
<el-date-picker
v-model="english.publishTime"
type="date"
value-format="yyyy-MM-dd"
placeholder="Select date"
/>
</div>
<div class="language-field language-field--wide">
<label>Cover</label>
<div class="language-cover">
<button type="button" class="language-cover__upload" @click="$refs.englishCoverInput.click()">
<i class="el-icon-upload2" />
Upload cover
</button>
<el-input
v-model.trim="english.coverUrl"
class="language-cover__input"
placeholder="Upload an image or paste its URL"
/>
<span v-if="english.coverName" class="language-cover__name">{{ english.coverName }}</span>
<input ref="englishCoverInput" type="file" accept="image/*" class="hidden-input" @change="handleEnglishCoverUpload">
</div>
</div>
</div>
<div class="language-richtext">
<div class="language-richtext__head">
<span>English Content</span>
</div>
<div v-if="dialogVisible" class="wang-editor-wrap wang-editor-wrap--english">
<Toolbar
class="wang-toolbar"
:editor="englishEditor"
:default-config="toolbarConfig"
:mode="editorMode"
/>
<Editor
v-model="english.content"
class="wang-editor"
:default-config="englishEditorConfig"
:mode="editorMode"
@onCreated="handleEnglishEditorCreated"
@onDestroyed="handleEnglishEditorDestroyed"
/>
</div>
</div>
</section>
</div>
<div
class="resize-handle"
title="拖动调整预览宽度"
@mousedown="startResize"
></div>
<div class="article-editor__preview" :style="{ width: `${previewWidth}px` }">
<div class="preview-head">
<span>实时预览</span>
<em>自动同步</em>
</div>
<div class="preview-card">
<div v-if="coverImage" class="preview-cover">
<img :src="coverImage" alt="cover">
</div>
<span class="preview-type">{{ form.type || '企业动态' }}</span>
<h1>{{ form.title || '文章标题' }}</h1>
<p class="preview-summary">{{ form.summary || '文章摘要预览...' }}</p>
<p class="preview-date">{{ form.publishTime || today }}</p>
<div class="preview-content" v-html="form.content || '文章内容预览...'"></div>
</div>
</div>
</div>
<div slot="footer" class="article-editor-footer">
<span class="editor-status">{{ dirty ? '未保存' : '已同步' }}</span>
<div class="editor-footer-buttons">
<el-button size="small" @click="requestClose">取消</el-button>
<el-button size="small" @click="handleSave('0')">保存草稿</el-button>
<el-button type="primary" size="small" @click="handlePublishConfirm">保存并发布</el-button>
</div>
</div>
</el-dialog>
</template>
<script>
import '@wangeditor/editor/dist/css/style.css'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { reqUploadNewsImage } from '@/api/newsapi/newsapi'
const normalizeImageUrl = (url) => {
if (!url) return ''
if (/^(https?:|blob:|data:|\/\/)/.test(url)) return url
return `${window.location.origin}/idfile?path=${url}`
}
export default {
name: 'ArticleEditorDialog',
components: { Editor, Toolbar },
props: {
visible: {
type: Boolean,
default: false
},
article: {
type: Object,
default: null
}
},
data() {
return {
// 标记当前表单是否有未保存改动。
dirty: false,
// 保存 wangEditor 实例,组件销毁时需要手动释放。
editor: null,
englishEditor: null,
// wangEditor 模式配置。
editorMode: 'default',
// wangEditor 工具栏配置。
toolbarConfig: {},
// wangEditor 编辑器配置。
editorConfig: {
placeholder: '请输入文章内容...',
MENU_CONF: {
uploadImage: {
customUpload: async(file, insertFn) => {
try {
// 正文图片上传到文件接口,正文中保存可回显的图片地址。
const url = await this.uploadNewsImage(file)
insertFn(normalizeImageUrl(url), file.name, normalizeImageUrl(url))
} catch (error) {
this.$message.error('正文图片上传失败')
}
}
}
}
},
englishEditorConfig: {
placeholder: 'Please enter article content…',
MENU_CONF: {
uploadImage: {
customUpload: async(file, insertFn) => {
try {
const url = await this.uploadNewsImage(file)
insertFn(normalizeImageUrl(url), file.name, normalizeImageUrl(url))
} catch (error) {
this.$message.error('英文正文图片上传失败')
}
}
}
}
},
// 上传封面的本地预览地址,只用于页面显示,不提交给接口。
coverPreview: '',
imageUploading: false,
englishImageUploading: false,
editorFullscreen: false,
editorFullscreenObserver: null,
// 右侧预览区域宽度。
previewWidth: 400,
// 控制英文版区域展开状态。
showEnglishForm: false,
// 英文版内容,与中文字段一起提交到文章保存接口。
english: this.createEmptyEnglishForm(),
// 标记预览区域是否正在拖拽调整宽度。
resizing: false,
// 记录拖拽开始时的鼠标位置。
resizeStartX: 0,
// 记录拖拽开始时的预览宽度。
resizeStartWidth: 400,
// 文章编辑表单数据。
form: this.createEmptyForm()
}
},
computed: {
dialogVisible: {
get() {
// 使用父组件传入的 visible 控制弹窗显示。
return this.visible
},
set(value) {
// 通过 sync 事件通知父组件更新 visible。
this.$emit('update:visible', value)
}
},
dialogTitle() {
// 有文章数据时是编辑,否则是新建。
return this.article ? '编辑文章' : '新建文章'
},
today() {
// 生成默认发布时间,格式与接口要求一致。
const date = new Date()
const month = `${date.getMonth() + 1}`.padStart(2, '0')
const day = `${date.getDate()}`.padStart(2, '0')
return `${date.getFullYear()}-${month}-${day}`
},
coverImage() {
// 优先使用本地预览,没有预览时使用接口/手动输入的图片路径。
if (this.coverPreview) return this.coverPreview
return normalizeImageUrl(this.form.coverUrl)
}
},
watch: {
form: {
deep: true,
handler() {
// 任意表单字段变化后标记为未保存。
this.dirty = true
}
},
english: {
deep: true,
handler() {
this.dirty = true
}
}
},
beforeDestroy() {
// 组件销毁前释放 wangEditor 实例,避免内存泄漏。
if (this.editor) {
this.editor.destroy()
this.editor = null
}
if (this.englishEditor) {
this.englishEditor.destroy()
this.englishEditor = null
}
// 清理拖拽监听事件。
this.removeResizeListeners()
this.stopObserveEditorFullscreen()
this.editorFullscreen = false
},
methods: {
createEmptyForm() {
// 创建新增文章时使用的默认表单结构。
return {
id: '',
title: '',
summary: '',
type: '企业动态',
status: '0',
publishTime: '',
views: 0,
content: '',
coverUrl: '',
coverName: ''
}
},
createEmptyEnglishForm() {
return {
title: '',
summary: '',
category: '',
publishTime: '',
coverUrl: '',
coverName: '',
content: ''
}
},
handleOpen() {
// 弹窗打开时合并默认值和待编辑文章数据。
const article = this.article || {}
const english = article.english || {}
this.form = {
...this.createEmptyForm(),
...article,
title: article.title || article.title_zh || '',
summary: article.summary || article.summary_zh || '',
type: article.type || article.article_type_zh || '企业动态',
publishTime: article.publishTime || article.publish_time_zh || article.publish_time || '',
content: article.content || article.content_zh || '',
coverUrl: article.coverUrl || article.cover_img_zh || article.cover_img || '',
coverName: article.coverName || article.cover_name_zh || article.cover_name || ''
}
// 没有发布时间时默认使用当天日期。
if (!this.form.publishTime) this.form.publishTime = this.today
// 初始化封面预览。
this.coverPreview = this.form.coverUrl || ''
this.editorFullscreen = false
this.imageUploading = false
this.englishImageUploading = false
this.english = {
...this.createEmptyEnglishForm(),
...english,
title: english.title || article.title_en || '',
summary: english.summary || article.summary_en || '',
category: english.category || article.article_type_en || '',
publishTime: english.publishTime || article.publish_time_en || '',
content: english.content || article.content_en || '',
coverUrl: english.coverUrl || article.cover_img_en || '',
coverName: english.coverName || article.cover_name_en || ''
}
this.showEnglishForm = Boolean(
this.english.title ||
this.english.summary ||
this.english.category ||
this.english.publishTime ||
this.english.coverUrl ||
this.english.content
)
this.$nextTick(() => {
// 表单初始化完成后重置未保存状态。
this.dirty = false
this.startObserveEditorFullscreen()
})
},
handleEditorCreated(editor) {
// 保存编辑器实例,后续销毁时使用。
this.editor = Object.seal(editor)
},
handleEnglishEditorCreated(editor) {
this.englishEditor = Object.seal(editor)
},
handleEnglishEditorDestroyed() {
this.englishEditor = null
},
handleDialogClose() {
this.stopObserveEditorFullscreen()
this.editorFullscreen = false
},
requestClose() {
this.handleBeforeClose(() => {
this.dialogVisible = false
})
},
handleBeforeClose(done) {
if (!this.dirty) {
done()
return
}
this.$confirm('当前编辑内容尚未保存,是否保存为草稿?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
customClass: 'news-confirm-box news-confirm-box--warning',
confirmButtonClass: 'news-confirm-btn news-confirm-btn--confirm',
cancelButtonClass: 'news-confirm-btn news-confirm-btn--cancel'
}).then(() => {
this.handleSave('0')
}).catch(() => {
done()
})
},
handlePublishConfirm() {
this.$confirm('请确认该文章不包含敏感词汇。', '确认发布企业动态文章?', {
confirmButtonText: '确定发布',
cancelButtonText: '取消',
type: 'success',
customClass: 'news-confirm-box news-confirm-box--success',
confirmButtonClass: 'news-confirm-btn news-confirm-btn--confirm is-success',
cancelButtonClass: 'news-confirm-btn news-confirm-btn--cancel'
}).then(() => {
this.handleSave('1')
}).catch(() => {})
},
isEditorFullscreenActive() {
const fullscreenElement = document.fullscreenElement || document.webkitFullscreenElement
if (fullscreenElement) return true
return !!document.querySelector('.w-e-full-screen-container, .w-e-full-screen, .w-e-text-container-full-screen')
},
updateEditorFullscreenState() {
this.editorFullscreen = this.isEditorFullscreenActive()
},
startObserveEditorFullscreen() {
this.stopObserveEditorFullscreen()
this.updateEditorFullscreenState()
this.editorFullscreenObserver = new MutationObserver(() => {
this.updateEditorFullscreenState()
})
this.editorFullscreenObserver.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class', 'style']
})
},
stopObserveEditorFullscreen() {
if (this.editorFullscreenObserver) {
this.editorFullscreenObserver.disconnect()
this.editorFullscreenObserver = null
}
},
getUploadUrl(res) {
const pickUrl = (data) => {
if (!data) return ''
if (typeof data === 'string') return data
if (typeof data === 'object') {
return data.file_path ||
data.file_url ||
data.save_url ||
data.url ||
data.path ||
data.filePath ||
data.fileUrl ||
data.saveUrl ||
pickUrl(data.data)
}
return ''
}
return pickUrl(res && (res.data || res))
},
async uploadNewsImage(file) {
const formData = new FormData()
formData.append('file_data', file)
formData.append('domain_name', window.location.host)
const res = await reqUploadNewsImage(formData)
if (res && (res.status === true || res.status === 'true')) {
const url = this.getUploadUrl(res)
if (url) return url
}
throw new Error((res && (res.msg || res.message)) || '图片上传失败')
},
async handleCoverUpload(event) {
// 读取用户选择的封面文件。
const file = event.target.files && event.target.files[0]
if (!file) return
// 保存文件名用于页面提示。
this.form.coverName = file.name
this.imageUploading = true
try {
// 封面图片上传后保存接口返回路径,提交文章时作为 cover_img。
const url = await this.uploadNewsImage(file)
this.form.coverUrl = url
this.coverPreview = normalizeImageUrl(url)
this.$message.success('封面图片上传成功')
} catch (error) {
this.form.coverName = ''
this.$message.error('封面图片上传失败')
} finally {
this.imageUploading = false
// 清空 input 值,保证重复选择同一文件也能触发 change。
event.target.value = ''
}
},
async handleEnglishCoverUpload(event) {
const file = event.target.files && event.target.files[0]
if (!file) return
this.english.coverName = file.name
this.englishImageUploading = true
try {
this.english.coverUrl = await this.uploadNewsImage(file)
this.$message.success('英文封面图片上传成功')
} catch (error) {
this.english.coverName = ''
this.$message.error('英文封面图片上传失败')
} finally {
this.englishImageUploading = false
event.target.value = ''
}
},
handleCoverUrlInput() {
// 用户手动输入图片路径时,清空本地上传预览状态。
this.coverPreview = ''
this.form.coverName = ''
},
startResize(event) {
// 开始拖拽调整右侧预览区域宽度。
this.resizing = true
// 记录拖拽起点。
this.resizeStartX = event.clientX
// 记录拖拽开始时的宽度。
this.resizeStartWidth = this.previewWidth
// 修改全局鼠标样式,让拖拽反馈更明显。
document.body.style.cursor = 'col-resize'
// 禁止选中文本,避免拖拽时误选页面文字。
document.body.style.userSelect = 'none'
// 监听鼠标移动和松开事件。
window.addEventListener('mousemove', this.handleResize)
window.addEventListener('mouseup', this.stopResize)
},
handleResize(event) {
// 非拖拽状态下不处理移动事件。
if (!this.resizing) return
// 根据鼠标偏移计算新的预览宽度。
const delta = this.resizeStartX - event.clientX
const nextWidth = this.resizeStartWidth + delta
// 限制预览宽度范围,避免过窄或过宽。
this.previewWidth = Math.min(Math.max(nextWidth, 320), 720)
},
stopResize() {
// 结束拖拽状态。
this.resizing = false
// 恢复全局鼠标样式。
document.body.style.cursor = ''
// 恢复页面文本选择。
document.body.style.userSelect = ''
// 移除拖拽事件监听。
this.removeResizeListeners()
},
removeResizeListeners() {
// 移除鼠标移动监听。
window.removeEventListener('mousemove', this.handleResize)
// 移除鼠标松开监听。
window.removeEventListener('mouseup', this.stopResize)
},
handleSave(status) {
// 第一步:校验文章标题。
if (!this.form.title) {
this.$message.warning('请输入文章标题')
return
}
// 第二步:校验文章摘要。
if (!this.form.summary) {
this.$message.warning('请输入文章摘要')
return
}
// 第三步:校验发布时间,确保提交接口时有 publish_time。
if (!this.form.publishTime) {
this.$message.warning('请选择发布时间')
return
}
// 第四步:校验封面图片,确保提交接口时有 cover_img。
if (!this.form.coverUrl) {
this.$message.warning('请输入封面图片')
return
}
// 第五步:校验文章正文。
if (!this.form.content) {
this.$message.warning('请输入文章正文')
return
}
if (this.imageUploading || this.englishImageUploading) {
this.$message.warning('图片上传中,请稍后保存')
return
}
// 第六步组装保存参数status 由按钮决定是草稿还是发布。
const payload = {
...this.form,
english: {
...this.english
},
publish_time: this.form.publishTime,
status
}
// 第七步:把保存参数交给父组件,由父组件调用新增或编辑接口。
this.$emit('save', payload)
// 第八步:保存触发后重置未保存状态。
this.dirty = false
// 第九步:关闭弹窗。
this.dialogVisible = false
}
}
}
</script>
<style scoped>
::v-deep .el-form-item__content{
/* width: 100% !important; */
margin-left: 88px !important;
}
::v-deep .el-form-item__label{
width: auto !important;
}
.required-star{
color: #ff0000;
font-size: 16px;
/* font-weight: 700; */
margin-right: 4px;
}
::v-deep .article-editor-dialog {
--article-editor-radius: 8px;
margin: 0 !important;
height: 100vh;
display: flex;
flex-direction: column;
flex-shrink: 0;
padding:0 24px!important;
padding-top: 16px;
border-bottom: 1px solid #edf0f5;
}
::v-deep .el-dialog__header {
flex-shrink: 0;
padding: 16px 24px;
border-bottom: 1px solid #edf0f5;
}
.article-dialog-title {
display: flex;
align-items: center;
gap: 14px;
color: #1f2937;
font-size: 16px;
font-weight: 700;
}
.dialog-back-btn {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 5px 10px;
color: #64748b;
font-size: 13px;
background: #fff;
border: 1px solid #d8e0ec;
border-radius: var(--article-editor-radius);
cursor: pointer;
transition: all 0.2s ease;
}
.dialog-back-btn:hover {
color: #2563eb;
border-color: #bfdbfe;
background: #eff6ff;
}
::v-deep .article-editor-dialog .el-dialog__body {
flex: 1;
min-height: 0;
padding: 0 24px;
overflow: hidden;
}
::v-deep .article-editor-dialog .el-dialog__footer {
width: 100%;
height: 60px;
min-height: 60px;
flex-shrink: 0;
padding: 0 24px !important;
display: flex;
align-items: center;
box-sizing: border-box;
border-top: 1px solid #edf0f5;
}
::v-deep .article-editor-dialog .el-button {
border-radius: var(--article-editor-radius);
}
.article-editor {
height: 100%;
display: flex;
overflow: hidden;
}
.article-editor__left {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.article-editor__left.has-language-form {
overflow-y: auto;
scrollbar-color: #cbd5e1 transparent;
scrollbar-width: thin;
}
.article-editor__left.has-language-form .article-editor__form {
flex-shrink: 0;
}
.article-editor__left.has-language-form .wang-editor-wrap {
flex: 0 0 420px;
min-height: 420px;
}
.resize-handle {
position: relative;
flex: 0 0 10px;
width: 10px;
cursor: col-resize;
background: #f8fafc;
border-left: 1px solid #edf0f5;
border-right: 1px solid #edf0f5;
transition: background 0.2s ease;
}
.resize-handle::before {
position: absolute;
top: 50%;
left: 50%;
width: 3px;
height: 42px;
content: '';
border-radius: 999px;
background: #cbd5e1;
transform: translate(-50%, -50%);
transition: background 0.2s ease, height 0.2s ease;
}
.resize-handle:hover {
background: #eff6ff;
}
.resize-handle:hover::before {
height: 58px;
background: #3b82f6;
}
.article-editor.is-editor-fullscreen .resize-handle {
display: none;
}
.article-editor__form {
padding: 16px 18px 4px 0;
border-bottom: 1px solid #edf0f5;
}
.language-trigger {
min-height: 48px;
margin: 2px 0 12px 88px;
padding: 9px 12px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
background: linear-gradient(90deg, #f8fbff 0%, #f7f5ff 100%);
border: 1px dashed #bfdbfe;
border-radius: 8px;
}
.language-trigger__title {
display: flex;
align-items: center;
gap: 9px;
}
.language-trigger__badge {
width: 26px;
height: 26px;
display: inline-flex;
align-items: center;
justify-content: center;
color: #4f46e5;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
background: #ede9fe;
border-radius: 7px;
}
.language-trigger__title strong,
.language-trigger__title small {
display: block;
}
.language-trigger__title strong {
color: #334155;
font-size: 13px;
}
.language-trigger__title small {
margin-top: 2px;
color: #94a3b8;
font-size: 11px;
}
.language-trigger__button {
padding: 5px 8px;
color: #4f46e5;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
background: transparent;
border: 0;
cursor: pointer;
}
.language-trigger__button i {
margin-left: 4px;
}
.article-editor__inline {
display: flex;
gap: 16px;
}
.article-editor__inline .el-form-item {
flex: 1;
}
.cover-row {
display: flex;
align-items: center;
gap: 12px;
}
.cover-url-input {
flex: 1;
}
.cover-name {
color: #16a34a;
font-size: 13px;
}
.hidden-input {
display: none;
}
.article-editor__toolbar-tip {
padding: 10px 18px 10px 0;
color: #8a94a6;
font-size: 12px;
}
.wang-editor-wrap {
flex: 1;
min-height: 0;
padding-right: 18px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.wang-toolbar {
border: 1px solid #edf0f5;
border-bottom: 0;
border-radius: 10px 10px 0 0;
}
.wang-editor {
flex: 1;
min-height: 0;
border: 1px solid #edf0f5;
border-radius: 0 0 10px 10px;
overflow-y: auto;
}
.article-editor__language-section {
flex-shrink: 0;
margin-top: 22px;
padding: 24px 18px 40px 0;
border-top: 1px solid #dbeafe;
}
.language-section-head {
margin-bottom: 18px;
padding: 16px 18px;
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
background: linear-gradient(135deg, #f5f9ff 0%, #f7f5ff 100%);
border: 1px solid #dbeafe;
border-radius: 12px;
}
.language-section-head__eyebrow {
display: block;
margin-bottom: 5px;
color: #6366f1;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.12em;
}
.language-section-head h3 {
margin: 0;
color: #1e293b;
font-size: 16px;
line-height: 1.4;
}
.language-section-head p {
margin: 5px 0 0;
color: #94a3b8;
font-size: 12px;
}
.language-section-head__tag {
padding: 4px 9px;
color: #4f46e5;
font-size: 12px;
font-weight: 600;
background: #ede9fe;
border-radius: 999px;
}
.language-form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.language-field--wide {
grid-column: 1 / -1;
}
.language-field label {
display: block;
margin-bottom: 7px;
color: #475569;
font-size: 12px;
font-weight: 600;
}
.language-field .el-select,
.language-field .el-date-editor {
width: 100%;
}
.language-cover {
min-height: 32px;
padding: 5px 10px;
display: flex;
align-items: center;
gap: 10px;
color: #94a3b8;
font-size: 12px;
background: #fff;
/* border: 1px solid #dcdfe6; */
border-radius: 4px;
}
.language-cover__upload {
padding: 5px 9px;
color: #fff;
font-size: 12px;
background: #409eff;
border: 0;
border-radius: 3px;
cursor: pointer;
}
.language-cover__upload i {
margin-right: 3px;
}
.language-cover__input {
flex: 1;
min-width: 0;
}
.language-cover__name {
max-width: 120px;
overflow: hidden;
color: #16a34a;
font-size: 11px;
text-overflow: ellipsis;
white-space: nowrap;
}
.language-richtext {
margin-top: 20px;
}
.language-richtext .wang-editor-wrap {
height: 420px;
min-height: 420px;
padding-right: 0;
}
.language-richtext .wang-toolbar {
border-radius: 0;
}
.language-richtext__head {
padding: 12px 14px 8px;
display: flex;
align-items: center;
justify-content: space-between;
background: #fff;
border: 1px solid #edf0f5;
border-bottom: 0;
border-radius: 10px 10px 0 0;
}
.language-richtext__head span {
color: #334155;
font-size: 13px;
font-weight: 600;
}
.language-richtext__head small {
color: #94a3b8;
font-size: 11px;
}
.language-richtext__toolbar {
min-height: 38px;
padding: 0 10px;
display: flex;
align-items: center;
gap: 5px;
color: #64748b;
background: #f8fafc;
border-top: 1px solid #edf2f7;
border-bottom: 1px solid #edf2f7;
}
.language-richtext__toolbar > span {
min-width: 24px;
height: 24px;
padding: 0 5px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 12px;
border-radius: 4px;
}
.language-richtext__toolbar > span:not(.language-richtext__divider):hover {
color: #2563eb;
background: #eff6ff;
}
.language-richtext__strong {
font-weight: 700;
}
.language-richtext__italic {
font-family: Georgia, serif;
font-style: italic;
font-weight: 700;
}
.language-richtext__toolbar .language-richtext__divider {
min-width: 1px;
width: 1px;
height: 18px;
padding: 0;
margin: 0 4px;
background: #dbe2ea;
}
.language-richtext__canvas {
min-height: 230px;
padding: 16px;
color: #c0c7d1;
font-size: 13px;
line-height: 1.8;
cursor: text;
outline: none;
}
.language-richtext__canvas:empty::before {
content: attr(data-placeholder);
color: #c0c7d1;
pointer-events: none;
}
.article-editor__preview {
flex: 0 0 auto;
display: flex;
flex-direction: column;
background: #f8fafc;
}
.preview-head {
height: 48px;
padding: 0 18px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #edf0f5;
}
.preview-head span {
color: #344054;
font-size: 14px;
font-weight: 600;
}
.preview-head em {
color: #9ca3af;
font-size: 12px;
font-style: normal;
}
.preview-card {
flex: 1;
min-height: 0;
margin: 18px;
padding: 24px;
overflow-y: auto;
background: #fff;
border-radius: 12px;
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.06);
}
.preview-cover {
margin-bottom: 16px;
}
.preview-cover img {
width: 100%;
border-radius: 10px;
}
.preview-type {
display: inline-flex;
padding: 2px 8px;
margin-bottom: 10px;
color: #2563eb;
font-size: 12px;
background: #eff6ff;
border-radius: 999px;
}
.preview-card h1 {
margin: 0 0 8px;
color: #1f2937;
font-size: 30px;
font-weight: 700;
line-height: 1.4;
}
.preview-date {
margin: 0 0 18px;
color: #9ca3af;
font-size: 12px;
}
.preview-summary {
margin: 0 0 10px;
color: #667085;
font-size: 14px;
line-height: 1.7;
}
.preview-content {
color: #4b5563;
font-size: 14px;
line-height: 1.8;
}
.article-editor-footer {
width: 100%;
height: 100%;
min-height: 0;
display: flex;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
}
.editor-status {
color: #9ca3af;
font-size: 12px;
}
.editor-footer-buttons {
display: flex;
align-items: center;
padding: 0;
gap: 10px;
.el-button {
min-height: 32px;
margin-left: 0 !important;
border-radius: var(--article-editor-radius);
}
}
</style>