init: 初始化 web_hgcg_admin 后台管理项目

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 17:12:21 +08:00
co-authored by Claude Opus 4.7
commit 523e066ca5
379 changed files with 37320 additions and 0 deletions
+264
View File
@@ -0,0 +1,264 @@
<!--
* @Author:
* @Mail:
* @Date: 2021-11-13 14:46:20
* @LastEditTime: 2023-01-02 19:27:36
* @LastEditors: Please set LastEditors
* @FilePath: /web_admin/src/components/QuillEditor/index.vue
-->
<template>
<div>
<quill-editor
v-model="content"
ref="myQuillEditor"
:options="editorOption"
@ready="onEditorReady($event)"></quill-editor>
<!-- el-uploader不好手动触发上传功能 -->
<uploader
:style="{ display: 'none' }"
:options="{ ...options, target: '/api/web/file/uploadImg' }"
class="uploader-example"
@file-success="imageSuccess">
<uploader-drop>
<uploader-btn ref="editorUploadImg" :attrs="attrsImg"></uploader-btn>
</uploader-drop>
</uploader>
</div>
</template>
<script>
import { handleVerAutoImg } from '@/utils/index.js'
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'
import Quill from 'quill'
const Link = Quill.import('formats/link')
Link.PROTOCOL_WHITELIST = ['http', 'https', 'mailto', 'tel', 'yinseinner', 'blob']
import { quillEditor } from 'vue-quill-editor'
import { ImageDrop } from 'quill-image-drop-module'
import ImageResize from 'quill-image-resize-module'
Quill.register('modules/imageDrop', ImageDrop)
Quill.register('modules/imageResize', ImageResize)
// Quill 默认 Image.sanitize 只允许 http/https/data,解密后的 blob: 会被清成 //:0
const ImageFormat = Quill.import('formats/image')
const originalImageSanitize = ImageFormat.sanitize
ImageFormat.sanitize = function(url) {
if (url && typeof url === 'string' && url.indexOf('blob:') === 0) {
return url
}
return originalImageSanitize.call(this, url)
}
Quill.register(ImageFormat, true)
// 自定义字体类型
var fonts = ['SimSun', 'SimHei', 'Microsoft-YaHei', 'KaiTi', 'FangSong', 'Arial', 'Times-New-Roman', 'sans-serif']
var Font = Quill.import('formats/font')
Font.whitelist = fonts // 将字体加入到白名单
Quill.register(Font, true)
export default {
name: 'Editor',
components: {
quillEditor
},
props: {
textNode: {
default: ''
}
},
computed: {
currentDomain() {
return this.domain || this.$store.getters.getSystemConfig.imgWebCdn || ''
},
options() {
return {
chunkSize: 1024 * 1024 * 30, // 切片大小
method: 'application/json',
testChunks: false,
allowDuplicateUploads: false, // 是否可以重复选择
query: {},
singleFile: false, // 是否只能选择单项
headers: {
Authorization: this.$store.getters.token
}
}
}
},
data() {
const _that = this
return {
content: '',
imgPath: [],
domain: this.$store.getters.getSystemConfig.imgWebCdn, // 不能暴露源站地址
attrsImg: {
accept: ['image/*']
},
trackId: undefined,
editorOption: {
placeholder: '请输入帖子内容',
modules: {
toolbar: {
container: [
['bold', 'italic', 'underline', 'strike'], // toggled buttons
['blockquote', 'code-block'],
[{ header: 1 }, { header: 2 }], // custom button values
[{ list: 'ordered' }, { list: 'bullet' }],
[{ script: 'sub' }, { script: 'super' }], // superscript/subscript
[{ indent: '-1' }, { indent: '+1' }], // outdent/indent
[{ direction: 'rtl' }], // text direction
[{ size: ['small', false, 'large', 'huge'] }], // custom dropdown
[{ header: [1, 2, 3, 4, 5, 6, false] }],
[{ color: [] }, { background: [] }], // dropdown with defaults from theme
[{ font: [] }],
[{ align: [] }],
['link', 'image'],
['clean']
],
handlers: {
// 自定义上传图片
image: value => {
_that.$refs.editorUploadImg.$el.childNodes[0].click()
}
}
},
// //配置富文本框图片大小调整功能
imageDrop: true,
imageResize: {
displayStyles: {
backgroundColor: 'black',
border: 'none',
color: 'white'
},
modules: ['Resize', 'DisplaySize', 'Toolbar']
}
}
}
// 暂存图片base64
}
},
methods: {
// 与 ShowImg.buildImageUrl 保持一致
buildImageUrl(path) {
if (!path) return ''
if (/^(blob:|https?:\/\/|data:)/.test(path)) return path
const base = this.currentDomain || ''
if (!base) return path
const normalizedBase = base.endsWith('/') ? base.slice(0, -1) : base
const normalizedPath = path.startsWith('/') ? path : `/${path}`
return `${normalizedBase}${normalizedPath}`
},
/**
* 回显:先粘贴原始 HTML,再按 ShowImg 方式逐张解密并写回 DOM
*/
async pasteAndDecryptImages(html) {
const sourceHtml = html || ''
if (!this.$refs.myQuillEditor || !this.$refs.myQuillEditor.quill) return
const myeditor = this.$refs.myQuillEditor.quill
myeditor.setContents([])
if (sourceHtml) {
myeditor.clipboard.dangerouslyPasteHTML(0, sourceHtml)
}
const imgNodes = Array.from(myeditor.container.querySelectorAll('img'))
for (const node of imgNodes) {
// 已有 data-custom 说明业务侧已解密过,src 已是 blob
const existingCustom = node.getAttribute('data-custom')
if (existingCustom) continue
const originalSrc = node.getAttribute('src')
if (!originalSrc || /^(blob:|data:)/.test(originalSrc)) continue
try {
const blobUrl = await handleVerAutoImg(this.buildImageUrl(originalSrc))
if (blobUrl) {
node.setAttribute('data-custom', originalSrc)
node.setAttribute('src', blobUrl)
}
} catch (e) {
console.error('富文本图片解密失败:', originalSrc, e)
}
}
this.content = myeditor.root.innerHTML
},
getdata() {
const myeditor = this.$refs.myQuillEditor.quill
// 找到所有包含 data-custom 属性的 img 标签
const imgNodes = myeditor.container.querySelectorAll('img[data-custom]')
imgNodes.forEach((node) => {
const dataCustomValue = node.getAttribute('data-custom') // 获取 data-custom 的值
// 交换 src 和 data-custom 的值
node.setAttribute('src', dataCustomValue)
node.removeAttribute('data-custom') // 删除 data-custom 属性
})
// 检查内容是否为空
if (this.content === '<p><br></p>' || this.content.trim() === '') {
this.content = '' // 将内容设置为空字符串
} else {
// 直接更新 this.content 中的内容
this.content = myeditor.root.innerHTML // 获取编辑器内容
}
return this.content
},
onEditorReady() {
if (!this.textNode) return
this.pasteAndDecryptImages(this.textNode)
},
extractImageSources(htmlString) {
const imgSrcRegex = /<img[^>]+src="([^">]+)"/g
const srcArray = []
let match
while ((match = imgSrcRegex.exec(htmlString)) !== null) {
srcArray.push(match[1])
}
return srcArray
},
imageSuccess(rootFile, file, message) {
const { code, data } = JSON.parse(message)
if (code === 200) {
const reader = new FileReader()
reader.onload = (e) => {
const base64Data = e.target.result // 获取转换后的 Data URL
const myeditor = this.$refs.myQuillEditor.quill
const index = myeditor.getSelection().index
// 插入图片
myeditor.insertEmbed(index, 'image', base64Data)
// 使用 setTimeout 确保 DOM 更新
setTimeout(() => {
// 查找插入的 img 元素
const imgNode = myeditor.container.querySelector(`img[src="${base64Data}"]`)
if (imgNode) {
imgNode.setAttribute('data-custom', data.path) // 设置自定义属性
} else {
console.log('Image not found in the DOM') // 调试信息
}
}, 0)
// 设置光标位置
myeditor.setSelection(index + 1)
}
// 读取 Blob 数据
reader.readAsDataURL(file.file)
}
},
clearInput() {
this.value = ''
}
}
}
</script>