shinnosuke
2026-08-06 5a27d6cd362da71af1beed9208082a1950b410ef
fix(admin): 启用政策内容富文本编辑 (bug #24049)
1个文件已添加
2个文件已修改
81 ■■■■■ 已修改文件
talent-card-admin/src/components/AdminRichTextEditor.vue 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/TalentPolicyEditView.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/TalentPolicyPublishView.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/components/AdminRichTextEditor.vue
New file
@@ -0,0 +1,75 @@
<script setup lang="ts">
import { nextTick, ref, watch } from 'vue'
import { ArrowLeftBold, ArrowRightBold, Link, Picture } from '@element-plus/icons-vue'
import { uploadAdminImage, uploadAdminVideo } from '../api/admin'
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
const editorRef = ref<HTMLElement>()
const imageInputRef = ref<HTMLInputElement>()
const videoInputRef = ref<HTMLInputElement>()
const toolbarItems = ['H', 'B', 'I', 'U', 'S', 'color', 'highlight', 'list', 'center', 'quote', 'table', 'link', 'image', 'video']
function syncEditor() { emit('update:modelValue', editorRef.value?.innerHTML || '') }
function updateContent() { syncEditor() }
function runCommand(item: string) {
  const commands: Record<string, [string, string?]> = {
    H: ['formatBlock', 'h2'], B: ['bold'], I: ['italic'], U: ['underline'], S: ['strikeThrough'],
    color: ['foreColor', '#1f2937'], highlight: ['hiliteColor', '#fff3cd'], list: ['insertUnorderedList'],
    center: ['justifyCenter'], quote: ['formatBlock', 'blockquote'], table: ['insertHTML', '<table><tbody><tr><td>内容</td><td>内容</td></tr></tbody></table>'],
  }
  if (item === 'link') {
    const url = window.prompt('请输入链接地址')
    if (url) document.execCommand('createLink', false, url)
  } else if (item === 'image') imageInputRef.value?.click()
  else if (item === 'video') videoInputRef.value?.click()
  else {
    const command = commands[item]
    if (command) document.execCommand(command[0], false, command[1])
  }
  syncEditor()
}
function undo() { document.execCommand('undo'); syncEditor() }
function redo() { document.execCommand('redo'); syncEditor() }
async function insertImage(event: Event) {
  const input = event.target as HTMLInputElement
  const file = input.files?.[0]
  if (!file) return
  const result = await uploadAdminImage(file)
  editorRef.value?.focus()
  document.execCommand('insertImage', false, result.url)
  input.value = ''
  syncEditor()
}
async function insertVideo(event: Event) {
  const input = event.target as HTMLInputElement
  const file = input.files?.[0]
  if (!file) return
  const result = await uploadAdminVideo(file)
  editorRef.value?.focus()
  document.execCommand('insertHTML', false, `<video controls src="${result.url}"></video>`)
  input.value = ''
  syncEditor()
}
watch(() => props.modelValue, async value => {
  await nextTick()
  if (editorRef.value && editorRef.value.innerHTML !== value) editorRef.value.innerHTML = value || ''
}, { immediate: true })
</script>
<template>
  <div class="news-editor-shell">
    <div class="news-editor-toolbar">
      <button v-for="item in toolbarItems" :key="item" class="news-editor-tool" type="button" :title="item" @mousedown.prevent @click="runCommand(item)">
        <el-icon v-if="item === 'link'"><Link /></el-icon>
        <el-icon v-else-if="item === 'image'"><Picture /></el-icon>
        <span v-else>{{ item }}</span>
      </button>
      <button class="news-editor-tool" type="button" title="撤销" @mousedown.prevent @click="undo"><el-icon><ArrowLeftBold /></el-icon></button>
      <button class="news-editor-tool" type="button" title="重做" @mousedown.prevent @click="redo"><el-icon><ArrowRightBold /></el-icon></button>
    </div>
    <div ref="editorRef" class="news-editor-body" contenteditable="true" @input="updateContent" />
    <input ref="imageInputRef" class="news-editor-file-input" type="file" accept="image/*" @change="insertImage" />
    <input ref="videoInputRef" class="news-editor-file-input" type="file" accept="video/mp4" @change="insertVideo" />
  </div>
</template>
talent-card-admin/src/views/TalentPolicyEditView.vue
@@ -4,6 +4,7 @@
import { ElMessage } from 'element-plus'
import type { UploadFile } from 'element-plus'
import { getAdminPolicyDetail, updateAdminPolicy, uploadAdminImage } from '../api/admin'
import AdminRichTextEditor from '../components/AdminRichTextEditor.vue'
const route = useRoute(); const router = useRouter(); const loading = ref(false)
const form = reactive({ title: '', subtitle: '', author: '', source: '', content: '', coverUrl: '', isTop: false, sortNo: 0, status: 1 })
@@ -16,6 +17,6 @@
</script>
<template>
  <div class="policy-edit-page" v-loading="loading"><section class="news-meta-panel"><h3>发布信息</h3><div class="news-meta-grid"><div><span>发布时间</span><strong>{{ createdAt }}</strong></div><div><span>修改时间</span><strong>{{ updatedAt }}</strong></div></div></section><section class="policy-publish-panel"><h3>政策内容</h3><el-form class="policy-publish-form" :model="form" label-width="88px">
    <el-form-item label="政策标题"><el-input v-model="form.title" /></el-form-item><el-form-item label="政策标签"><el-input v-model="form.subtitle" placeholder="请输入政策标签" /></el-form-item><el-form-item label="政策详情"><el-input v-model="form.content" type="textarea" :rows="10" /></el-form-item><el-form-item label="封面图"><el-upload accept="image/*" :auto-upload="false" :show-file-list="false" :on-change="uploadCover"><el-button>上传图片</el-button></el-upload><el-image v-if="form.coverUrl" class="policy-cover-preview" :src="form.coverUrl" fit="cover" /></el-form-item><el-form-item label="排序"><el-input v-model.number="form.sortNo" type="number" min="0" max="9999" /></el-form-item><el-form-item><el-button type="primary" @click="save">保存</el-button></el-form-item>
    <el-form-item label="政策标题"><el-input v-model="form.title" /></el-form-item><el-form-item label="政策标签"><el-input v-model="form.subtitle" placeholder="请输入政策标签" /></el-form-item><el-form-item class="news-editor-form-item" label="政策内容"><AdminRichTextEditor v-model="form.content" /></el-form-item><el-form-item label="封面图"><el-upload accept="image/*" :auto-upload="false" :show-file-list="false" :on-change="uploadCover"><el-button>上传图片</el-button></el-upload><el-image v-if="form.coverUrl" class="policy-cover-preview" :src="form.coverUrl" fit="cover" /></el-form-item><el-form-item label="排序"><el-input v-model.number="form.sortNo" type="number" min="0" max="9999" /></el-form-item><el-form-item><el-button type="primary" @click="save">保存</el-button></el-form-item>
  </el-form></section></div>
</template>
talent-card-admin/src/views/TalentPolicyPublishView.vue
@@ -4,6 +4,7 @@
import { ElMessage } from 'element-plus'
import type { UploadFile } from 'element-plus'
import { createAdminPolicy, uploadAdminImage } from '../api/admin'
import AdminRichTextEditor from '../components/AdminRichTextEditor.vue'
const router = useRouter()
const form = reactive({ title: '', subtitle: '', author: '', source: '', content: '', coverUrl: '', isTop: false, sortNo: 0, status: 1 })
@@ -28,7 +29,7 @@
  <div class="policy-publish-page"><section class="policy-publish-panel"><h3>政策内容</h3><el-form class="policy-publish-form" :model="form" label-width="88px">
    <el-form-item label="政策标题"><el-input v-model="form.title" placeholder="请输入政策标题" /></el-form-item>
    <el-form-item label="政策标签"><el-input v-model="form.subtitle" placeholder="请输入政策标签" /></el-form-item>
    <el-form-item label="政策详情"><el-input v-model="form.content" type="textarea" :rows="10" /></el-form-item>
    <el-form-item class="news-editor-form-item" label="政策内容"><AdminRichTextEditor v-model="form.content" /></el-form-item>
    <el-form-item label="封面图"><el-upload accept="image/*" :auto-upload="false" :show-file-list="false" :on-change="uploadCover"><el-button>上传图片</el-button></el-upload><el-image v-if="form.coverUrl" class="policy-cover-preview" :src="form.coverUrl" fit="cover" /></el-form-item>
    <el-form-item label="排序"><el-input v-model.number="form.sortNo" type="number" min="0" max="9999" /></el-form-item>
    <el-form-item><el-button type="primary" @click="save">保存</el-button></el-form-item>