| talent-card-admin/src/components/AdminRichTextEditor.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| talent-card-admin/src/views/TalentPolicyEditView.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| talent-card-admin/src/views/TalentPolicyPublishView.vue | ●●●●● 补丁 | 查看 | 原始文档 | 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>