feat(F006): 资讯列表封面上传 2M→5M + 内容改富文本编辑器(RichEditor, 支持图文粘贴)
This commit is contained in:
parent
d60df0dc06
commit
22d3ecd71e
74
package-lock.json
generated
74
package-lock.json
generated
@ -9,6 +9,7 @@
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@vueup/vue-quill": "^1.5.5",
|
||||
"axios": "^1.6.7",
|
||||
"dayjs": "^1.11.10",
|
||||
"echarts": "^5.5.0",
|
||||
@ -1489,6 +1490,19 @@
|
||||
"integrity": "sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vueup/vue-quill": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@vueup/vue-quill/-/vue-quill-1.5.5.tgz",
|
||||
"integrity": "sha512-ygA61ZZPaRzTHXi8H1w5td1XCtjwRsO6oBuJHdCbfotGQ7hBQukVp5dsYeVXWKuSiF8hGDKwIlROyNNrMalQCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"quill": "2.0.2 || >=2.0.4 <3",
|
||||
"quill-delta": "^5.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.2.41"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/core": {
|
||||
"version": "14.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-14.3.0.tgz",
|
||||
@ -1938,6 +1952,12 @@
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
|
||||
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/exsolve": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.8.tgz",
|
||||
@ -1945,6 +1965,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-diff": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
|
||||
"integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||
@ -2285,6 +2311,19 @@
|
||||
"lodash-es": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash.clonedeep": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isequal": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
|
||||
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
|
||||
@ -2463,6 +2502,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/parchment": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parchment/-/parchment-3.0.0.tgz",
|
||||
"integrity": "sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/path-browserify": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz",
|
||||
@ -2627,6 +2672,35 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/quill": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/quill/-/quill-2.0.2.tgz",
|
||||
"integrity": "sha512-QfazNrhMakEdRG57IoYFwffUIr04LWJxbS/ZkidRFXYCQt63c1gK6Z7IHUXMx/Vh25WgPBU42oBaNzQ0K1R/xw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"eventemitter3": "^5.0.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"parchment": "^3.0.0",
|
||||
"quill-delta": "^5.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">=8.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/quill-delta": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz",
|
||||
"integrity": "sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-diff": "^1.3.0",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.isequal": "^4.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-5.0.0.tgz",
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@vueup/vue-quill": "^1.5.5",
|
||||
"axios": "^1.6.7",
|
||||
"dayjs": "^1.11.10",
|
||||
"echarts": "^5.5.0",
|
||||
|
||||
146
src/components/RichEditor/RichEditor.vue
Normal file
146
src/components/RichEditor/RichEditor.vue
Normal file
@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<div class="rich-editor">
|
||||
<quill-editor
|
||||
ref="editorRef"
|
||||
v-model:content="model"
|
||||
:options="options"
|
||||
content-type="html"
|
||||
class="quill-editor-area"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, nextTick } from "vue"
|
||||
import { Quill, QuillEditor } from "@vueup/vue-quill"
|
||||
import "@vueup/vue-quill/dist/vue-quill.snow.css"
|
||||
|
||||
const model = defineModel<string>({ default: "" })
|
||||
|
||||
const editorRef = ref<any>(null)
|
||||
let quill: any = null
|
||||
|
||||
// === 图片上传 handler (支持多选, 5MB) ===
|
||||
const imageHandler = () => {
|
||||
const input = document.createElement("input")
|
||||
input.type = "file"
|
||||
input.accept = "image/*"
|
||||
input.multiple = true
|
||||
input.click()
|
||||
input.onchange = async () => {
|
||||
const files = input.files ? Array.from(input.files) : []
|
||||
if (!files.length) return
|
||||
try {
|
||||
const urls = await Promise.all(files.map(uploadFile))
|
||||
const quillNow = quill || editorRef.value?.getQuill()
|
||||
if (!quillNow) return
|
||||
const sel = quillNow.getSelection(true)
|
||||
let idx = sel ? sel.index : quillNow.getLength()
|
||||
urls.forEach((url) => {
|
||||
quillNow.insertEmbed(idx, "image", url)
|
||||
quillNow.insertText(idx + 1, "\n", "api")
|
||||
idx += 2
|
||||
})
|
||||
quillNow.setSelection(idx)
|
||||
} catch (e: any) {
|
||||
console.error("图片上传失败:", e)
|
||||
alert(e?.message || "图片上传失败,请重试")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === 单文件上传 (admin_new: /api 前缀 + access_token) ===
|
||||
async function uploadFile(file: File): Promise<string> {
|
||||
if (file.size > 5 * 1024 * 1024) throw new Error("图片大小不能超过5MB,请压缩后重试")
|
||||
const fd = new FormData()
|
||||
fd.append("file", file)
|
||||
const token = localStorage.getItem("access_token") || ""
|
||||
let resp: Response
|
||||
try {
|
||||
resp = await fetch("/api/sys/file/upload", { method: "POST", headers: { Authorization: `Bearer ${token}` }, body: fd })
|
||||
} catch {
|
||||
throw new Error("图片上传失败,请检查网络后重试")
|
||||
}
|
||||
if (resp.status === 413) {
|
||||
throw new Error("图片超过大小限制(5MB),请压缩后重试")
|
||||
}
|
||||
const data = await resp.json()
|
||||
if (data.code === 0) return data?.data?.url || data?.url || ""
|
||||
throw new Error(data.msg || "图片上传失败")
|
||||
}
|
||||
|
||||
// === Quill 配置 ===
|
||||
const options = ref({
|
||||
theme: "snow",
|
||||
modules: {
|
||||
toolbar: {
|
||||
container: [
|
||||
["bold", "italic", "underline", "strike"],
|
||||
[{ color: [] }, { background: [] }],
|
||||
[{ align: [] }],
|
||||
[{ size: ["small", false, "large", "huge"] }],
|
||||
[{ font: [] }],
|
||||
[{ header: [1, 2, 3, 4, 5, 6, false] }],
|
||||
[{ indent: "-1" }, { indent: "+1" }],
|
||||
[{ list: "ordered" }, { list: "bullet" }],
|
||||
[{ script: "sub" }, { script: "super" }],
|
||||
["blockquote", "code-block"],
|
||||
["clean"],
|
||||
["link", "image", "video"],
|
||||
],
|
||||
handlers: {
|
||||
image: imageHandler,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// === 工具栏中文提示 ===
|
||||
const titleConfig: { Choice: string; title: string }[] = [
|
||||
{ Choice: ".ql-bold", title: "加粗" },
|
||||
{ Choice: ".ql-italic", title: "斜体" },
|
||||
{ Choice: ".ql-underline", title: "下划线" },
|
||||
{ Choice: ".ql-strike", title: "删除线" },
|
||||
{ Choice: ".ql-blockquote", title: "块引用" },
|
||||
{ Choice: ".ql-code-block", title: "插入代码段" },
|
||||
{ Choice: ".ql-font", title: "字体" },
|
||||
{ Choice: ".ql-size", title: "字体大小" },
|
||||
{ Choice: ".ql-list[value=\"ordered\"]", title: "编号列表" },
|
||||
{ Choice: ".ql-list[value=\"bullet\"]", title: "项目列表" },
|
||||
{ Choice: ".ql-header", title: "标题" },
|
||||
{ Choice: ".ql-align", title: "对齐方式" },
|
||||
{ Choice: ".ql-color", title: "字体颜色" },
|
||||
{ Choice: ".ql-background", title: "背景颜色" },
|
||||
{ Choice: ".ql-image", title: "图片(支持多选)" },
|
||||
{ Choice: ".ql-video", title: "视频" },
|
||||
{ Choice: ".ql-link", title: "添加链接" },
|
||||
{ Choice: ".ql-clean", title: "清除格式" },
|
||||
{ Choice: ".ql-script[value=\"sub\"]", title: "下标" },
|
||||
{ Choice: ".ql-script[value=\"super\"]", title: "上标" },
|
||||
{ Choice: ".ql-indent[value=\"-1\"]", title: "向左缩进" },
|
||||
{ Choice: ".ql-indent[value=\"+1\"]", title: "向右缩进" },
|
||||
]
|
||||
|
||||
onMounted(() => nextTick(initTitle))
|
||||
|
||||
function initTitle() {
|
||||
for (const item of titleConfig) {
|
||||
const el = document.querySelector(`.rich-editor ${item.Choice}`) as HTMLElement | null
|
||||
if (el) el.setAttribute("title", item.title)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.rich-editor :deep(.ql-toolbar) {
|
||||
border-radius: 6px 6px 0 0;
|
||||
}
|
||||
.rich-editor :deep(.ql-container) {
|
||||
min-height: 280px;
|
||||
font-size: 14px;
|
||||
border-radius: 0 0 6px 6px;
|
||||
}
|
||||
.rich-editor :deep(.ql-editor) {
|
||||
min-height: 280px;
|
||||
}
|
||||
</style>
|
||||
@ -14,6 +14,7 @@ import { httpGet, httpPost, httpDelete } from '@/utils/request'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||
import RichEditor from '@/components/RichEditor/RichEditor.vue'
|
||||
|
||||
interface Info {
|
||||
id: string
|
||||
@ -152,7 +153,7 @@ async function onDelete(row: Info) {
|
||||
// === 图片上传 (FormData + Bearer 头由拦截器自动带) ===
|
||||
async function customUpload(opt: any) {
|
||||
const file = opt.file as File
|
||||
if (file.size > 2 * 1024 * 1024) { ElMessage.error('图片大小不能超过 2MB!'); return }
|
||||
if (file.size > 5 * 1024 * 1024) { ElMessage.error('图片大小不能超过 5MB,请压缩后重试'); return }
|
||||
uploadLoading.value = true
|
||||
try {
|
||||
const fd = new FormData()
|
||||
@ -235,11 +236,11 @@ async function customUpload(opt: any) {
|
||||
<img v-if="editForm.images" :src="editForm.images" alt="封面" class="upload-preview" referrerpolicy="no-referrer" />
|
||||
<el-button v-else :loading="uploadLoading">上传封面</el-button>
|
||||
</el-upload>
|
||||
<span class="rr-muted">支持 JPG/PNG, 不超过 2MB</span>
|
||||
<span class="rr-muted">支持 JPG/PNG, 不超过 5MB</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="资讯内容">
|
||||
<el-input v-model="editForm.content" type="textarea" :rows="8" placeholder="资讯内容(支持 HTML)" maxlength="20000" />
|
||||
<RichEditor v-model="editForm.content" style="margin-bottom: 8px" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
|
||||
@ -34,6 +34,7 @@ const enterpriseId = computed(() => String(route.params.id || ''))
|
||||
const loading = ref(false)
|
||||
const activeTab = ref('basic')
|
||||
const detail = ref<any>({})
|
||||
function isPdfUrl(u: string) { return (u || '').toLowerCase().endsWith('.pdf') }
|
||||
const otherList = ref<any[]>([])
|
||||
const performanceList = ref<any[]>([])
|
||||
|
||||
@ -486,7 +487,14 @@ onMounted(load)
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="cert.imageList?.length" class="cert-imgs">
|
||||
<el-image v-for="(img, j) in cert.imageList" :key="j" :src="img" fit="cover" class="cert-thumb" :preview-src-list="cert.imageList" preview-teleported />
|
||||
<template v-for="(img, j) in cert.imageList" :key="j">
|
||||
<a v-if="isPdfUrl(img)" :href="img" target="_blank" rel="noopener"
|
||||
class="pdf-card" :title="'打开 PDF 文件'">
|
||||
<el-icon style="font-size:22px;color:#e34d4d"><Document /></el-icon>
|
||||
<span style="font-size:11px;font-weight:bold;color:#e34d4d">PDF</span>
|
||||
</a>
|
||||
<el-image v-else :src="img" fit="cover" class="cert-thumb" :preview-src-list="[img]" preview-teleported />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -744,6 +752,8 @@ onMounted(load)
|
||||
.cert-row .label { min-width: 76px; }
|
||||
.cert-imgs { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 12px; }
|
||||
.cert-thumb { width: 96px; height: 120px; border-radius: 6px; border: 1px solid var(--border); cursor: zoom-in; }
|
||||
.pdf-card { width: 96px; height: 120px; border-radius: 6px; background: #fef2f2; border: 1px solid #fecaca; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; cursor: pointer; text-decoration: none; }
|
||||
.pdf-card:hover { border-color: #fca5a5; background: #fee2e2; }
|
||||
|
||||
/* ===== 履约 ===== */
|
||||
.kpi-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user