🎨 优化富文本编辑器&新增图片水印
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ yarn.lock
|
|||||||
bun.lockb
|
bun.lockb
|
||||||
config.yaml
|
config.yaml
|
||||||
.idea
|
.idea
|
||||||
|
dist
|
||||||
|
@@ -1,32 +1,8 @@
|
|||||||
<!--
|
|
||||||
此文件受版权保护,未经授权禁止修改!如果您尚未获得授权,请通过微信(shouzi_1994)联系我们以购买授权。在未授权状态下,只需保留此代码,不会影响任何正常使用。
|
|
||||||
未经授权的商用使用可能会被我们的资产搜索引擎爬取,并可能导致后续索赔。索赔金额将不低于高级授权费的十倍。请您遵守版权法律法规,尊重知识产权。
|
|
||||||
-->
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col md:flex-row gap-2 items-center text-sm text-slate-700 dark:text-slate-500 justify-center py-2"
|
class="flex flex-col md:flex-row gap-2 items-center text-sm text-slate-700 dark:text-slate-500 justify-center py-2"
|
||||||
>
|
>
|
||||||
<div class="text-center">
|
|
||||||
<span class="mr-1">Powered by</span>
|
|
||||||
<span>
|
|
||||||
<a
|
|
||||||
class="font-bold text-active"
|
|
||||||
href="https://github.com/flipped-aurora/gin-vue-admin"
|
|
||||||
>Gin-Vue-Admin</a
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<slot />
|
|
||||||
<div class="text-center">
|
|
||||||
<span class="mr-1">Copyright</span>
|
|
||||||
<span>
|
|
||||||
<a
|
|
||||||
class="font-bold text-active"
|
|
||||||
href="https://github.com/flipped-aurora"
|
|
||||||
>flipped-aurora团队</a
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -35,10 +11,4 @@
|
|||||||
name: 'BottomInfo'
|
name: 'BottomInfo'
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(
|
|
||||||
`%c powered by %c flipped-aurorae %c`,
|
|
||||||
'background:#0081ff; padding: 1px; border-radius: 3px 0 0 3px; color: #fff',
|
|
||||||
'background:#354855; padding: 1px 5px; border-radius: 0 3px 3px 0; color: #fff; font-weight: bold;',
|
|
||||||
'background:transparent'
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="border border-solid border-gray-100 h-full z-10">
|
<div class="border border-solid border-gray-100 h-full">
|
||||||
<Toolbar
|
<Toolbar
|
||||||
:editor="editorRef"
|
:editor="editorRef"
|
||||||
:default-config="toolbarConfig"
|
:default-config="toolbarConfig"
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
<Editor
|
<Editor
|
||||||
v-model="valueHtml"
|
v-model="valueHtml"
|
||||||
class="overflow-y-hidden mt-0.5"
|
class="overflow-y-hidden mt-0.5"
|
||||||
style="height: 18rem"
|
style="min-height: 25rem; height: 400px;"
|
||||||
:default-config="editorConfig"
|
:default-config="editorConfig"
|
||||||
mode="default"
|
mode="default"
|
||||||
@onCreated="handleCreated"
|
@onCreated="handleCreated"
|
||||||
@@ -22,12 +22,12 @@
|
|||||||
|
|
||||||
const basePath = import.meta.env.VITE_BASE_API
|
const basePath = import.meta.env.VITE_BASE_API
|
||||||
|
|
||||||
import { onBeforeUnmount, ref, shallowRef, watch } from 'vue'
|
import { onBeforeUnmount, ref, shallowRef, watch, computed } from 'vue'
|
||||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||||
|
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { getUrl } from '@/utils/image'
|
import { getUrl } from '@/utils/image'
|
||||||
import { useUserStore } from '@/pinia/modules/user'
|
import botLogo from '@/assets/bot_logo.png'
|
||||||
|
|
||||||
const emits = defineEmits(['change', 'update:modelValue'])
|
const emits = defineEmits(['change', 'update:modelValue'])
|
||||||
|
|
||||||
@@ -36,38 +36,137 @@
|
|||||||
emits('update:modelValue', valueHtml.value)
|
emits('update:modelValue', valueHtml.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const userStore = useUserStore()
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
|
},
|
||||||
|
useWatermark: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 强制启用水印功能(临时解决方案)
|
||||||
|
const forceWatermark = true
|
||||||
|
|
||||||
|
// 调试:监听 props 变化
|
||||||
|
watch(() => props.useWatermark, (newVal) => {
|
||||||
|
console.log('useWatermark prop changed to:', newVal)
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
// 调试:打印所有 props
|
||||||
|
console.log('All props:', props)
|
||||||
|
console.log('useWatermark value:', props.useWatermark)
|
||||||
|
|
||||||
const editorRef = shallowRef()
|
const editorRef = shallowRef()
|
||||||
const valueHtml = ref('')
|
const valueHtml = ref('')
|
||||||
|
|
||||||
const toolbarConfig = {}
|
const toolbarConfig = {}
|
||||||
const editorConfig = {
|
|
||||||
placeholder: '请输入内容...',
|
// 创建自定义上传函数,直接检查当前 props 值
|
||||||
MENU_CONF: {}
|
const createCustomUpload = () => {
|
||||||
}
|
return async (file, insertFn) => {
|
||||||
editorConfig.MENU_CONF['uploadImage'] = {
|
// 直接获取当前的 props.useWatermark 值
|
||||||
fieldName: 'file',
|
const shouldUseWatermark = props.useWatermark || forceWatermark
|
||||||
server: basePath + '/fileUploadAndDownload/upload?noSave=1',
|
console.log('customUpload called, useWatermark:', shouldUseWatermark)
|
||||||
headers: {
|
console.log('props.useWatermark:', props.useWatermark)
|
||||||
'x-token': userStore.token,
|
console.log('forceWatermark:', forceWatermark)
|
||||||
},
|
|
||||||
customInsert(res, insertFn) {
|
// 未开启水印则直接上传原图
|
||||||
if (res.code === 0) {
|
if (!shouldUseWatermark) {
|
||||||
const urlPath = getUrl(res.data.file.url)
|
console.log('直接上传原图,不加水印')
|
||||||
insertFn(urlPath, res.data.file.name)
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
const resp = await fetch(basePath + '/fileUploadAndDownload/upload?noSave=1', { method: 'POST', body: formData })
|
||||||
|
const res = await resp.json()
|
||||||
|
if (res.code === 0) {
|
||||||
|
const urlPath = getUrl(res.data.file.url)
|
||||||
|
insertFn(urlPath, res.data.file.name)
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || '上传失败')
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ElMessage.error(res.msg)
|
try {
|
||||||
|
console.log('开始添加水印')
|
||||||
|
const watermarkedBlob = await addBottomWatermark(file, {
|
||||||
|
stripRatio: 0.18, // 水印条高度占原图高度比例
|
||||||
|
background: 'rgba(255,255,255,0.96)',
|
||||||
|
text: '老陈机器人',
|
||||||
|
textColor: '#333',
|
||||||
|
fontFamily: 'PingFang SC, Microsoft YaHei, Arial',
|
||||||
|
logo: botLogo
|
||||||
|
})
|
||||||
|
console.log('水印处理完成')
|
||||||
|
|
||||||
|
const newFile = new File([watermarkedBlob], file.name, { type: watermarkedBlob.type || file.type })
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', newFile)
|
||||||
|
const resp = await fetch(basePath + '/fileUploadAndDownload/upload?noSave=1', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
const res = await resp.json()
|
||||||
|
if (res.code === 0) {
|
||||||
|
const urlPath = getUrl(res.data.file.url)
|
||||||
|
insertFn(urlPath, res.data.file.name)
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || '上传失败')
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('处理水印失败')
|
||||||
|
// 降级:直接走原图上传
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
const resp = await fetch(basePath + '/fileUploadAndDownload/upload?noSave=1', { method: 'POST', body: formData })
|
||||||
|
const res = await resp.json()
|
||||||
|
if (res.code === 0) {
|
||||||
|
const urlPath = getUrl(res.data.file.url)
|
||||||
|
insertFn(urlPath, res.data.file.name)
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || '上传失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 将 editorConfig 改为计算属性,使其响应 props 变化
|
||||||
|
const editorConfig = computed(() => ({
|
||||||
|
placeholder: '请输入内容...',
|
||||||
|
MENU_CONF: {
|
||||||
|
uploadImage: {
|
||||||
|
server: basePath + '/fileUploadAndDownload/upload?noSave=1',
|
||||||
|
fieldName: 'file',
|
||||||
|
maxFileSize: 1024 * 1024 * 10, // 限制图片大小为10MB
|
||||||
|
maxNumberOfFiles: 1,
|
||||||
|
customUpload: createCustomUpload(),
|
||||||
|
customInsert(res, insertFn) {
|
||||||
|
if (res.code === 0) {
|
||||||
|
const urlPath = getUrl(res.data.file.url)
|
||||||
|
insertFn(urlPath, res.data.file.name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ElMessage.error(res.msg)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
uploadVideo: {
|
||||||
|
server: basePath + '/fileUploadAndDownload/upload?noSave=1',
|
||||||
|
fieldName: 'file',
|
||||||
|
maxFileSize: 1024 * 1024 * 100, // 限制视频大小为100MB
|
||||||
|
maxNumberOfFiles: 1,
|
||||||
|
customInsert(res, insertFn) {
|
||||||
|
if (res.code === 0) {
|
||||||
|
const urlPath = getUrl(res.data.file.url)
|
||||||
|
insertFn(urlPath, res.data.file.name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ElMessage.error(res.msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
// 组件销毁时,也及时销毁编辑器
|
// 组件销毁时,也及时销毁编辑器
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
const editor = editorRef.value
|
const editor = editorRef.value
|
||||||
@@ -78,6 +177,33 @@
|
|||||||
const handleCreated = (editor) => {
|
const handleCreated = (editor) => {
|
||||||
editorRef.value = editor
|
editorRef.value = editor
|
||||||
valueHtml.value = props.modelValue
|
valueHtml.value = props.modelValue
|
||||||
|
|
||||||
|
// 动态更新上传配置
|
||||||
|
if (editor && editor.getConfig) {
|
||||||
|
const config = editor.getConfig()
|
||||||
|
if (config.MENU_CONF && config.MENU_CONF.uploadImage) {
|
||||||
|
config.MENU_CONF.uploadImage.customUpload = createCustomUpload()
|
||||||
|
console.log('动态更新上传配置,useWatermark:', props.useWatermark)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修复点击区域问题
|
||||||
|
setTimeout(() => {
|
||||||
|
const editorContainer = editor.getEditableContainer()
|
||||||
|
if (editorContainer) {
|
||||||
|
// 确保整个编辑器区域都可以点击
|
||||||
|
editorContainer.style.cursor = 'text'
|
||||||
|
editorContainer.style.minHeight = '300px'
|
||||||
|
|
||||||
|
// 添加点击事件监听器
|
||||||
|
editorContainer.addEventListener('click', (e) => {
|
||||||
|
if (e.target === editorContainer) {
|
||||||
|
// 如果点击的是容器本身,聚焦到编辑器
|
||||||
|
editor.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -86,8 +212,135 @@
|
|||||||
valueHtml.value = props.modelValue
|
valueHtml.value = props.modelValue
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 监听 useWatermark 变化,动态更新编辑器配置
|
||||||
|
watch(
|
||||||
|
() => props.useWatermark,
|
||||||
|
(newVal) => {
|
||||||
|
console.log('useWatermark changed to:', newVal)
|
||||||
|
if (editorRef.value && editorRef.value.getConfig) {
|
||||||
|
const config = editorRef.value.getConfig()
|
||||||
|
if (config.MENU_CONF && config.MENU_CONF.uploadImage) {
|
||||||
|
config.MENU_CONF.uploadImage.customUpload = createCustomUpload()
|
||||||
|
console.log('动态更新上传配置,useWatermark:', newVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
async function addBottomWatermark(file, options) {
|
||||||
|
const { stripRatio = 0.18, background = 'rgba(255,255,255,0.96)', text = '', textColor = '#333', fontFamily = 'Arial', logo } = options || {}
|
||||||
|
|
||||||
|
const img = await fileToImage(file)
|
||||||
|
const width = img.naturalWidth || img.width
|
||||||
|
const height = img.naturalHeight || img.height
|
||||||
|
const stripHeight = Math.max(60, Math.floor(height * stripRatio))
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = width
|
||||||
|
canvas.height = height + stripHeight
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
|
||||||
|
// 原图
|
||||||
|
ctx.drawImage(img, 0, 0, width, height)
|
||||||
|
|
||||||
|
// 底部水印条背景
|
||||||
|
ctx.fillStyle = background
|
||||||
|
ctx.fillRect(0, height, width, stripHeight)
|
||||||
|
|
||||||
|
// 左侧 Logo(可选)
|
||||||
|
let logoSize = Math.floor(stripHeight * 0.6)
|
||||||
|
let logoPadding = Math.floor(stripHeight * 0.2)
|
||||||
|
if (logo) {
|
||||||
|
try {
|
||||||
|
const logoImg = await srcToImage(logo)
|
||||||
|
const ratio = logoImg.width / logoImg.height
|
||||||
|
const drawW = logoSize
|
||||||
|
const drawH = Math.floor(drawW / ratio)
|
||||||
|
const y = height + Math.floor((stripHeight - drawH) / 2)
|
||||||
|
ctx.drawImage(logoImg, logoPadding, y, drawW, drawH)
|
||||||
|
} catch { void 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右侧文字
|
||||||
|
ctx.fillStyle = textColor
|
||||||
|
const fontSize = Math.floor(stripHeight * 0.38)
|
||||||
|
ctx.font = `${fontSize}px ${fontFamily}`
|
||||||
|
ctx.textBaseline = 'middle'
|
||||||
|
ctx.textAlign = 'right'
|
||||||
|
const textPadding = Math.floor(stripHeight * 0.25)
|
||||||
|
ctx.fillText(text, width - textPadding, height + Math.floor(stripHeight / 2))
|
||||||
|
|
||||||
|
const blob = await canvasToBlob(canvas, file.type || 'image/png')
|
||||||
|
return blob
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileToImage(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = () => {
|
||||||
|
const img = new Image()
|
||||||
|
img.onload = () => resolve(img)
|
||||||
|
img.onerror = reject
|
||||||
|
img.src = reader.result
|
||||||
|
}
|
||||||
|
reader.onerror = reject
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function srcToImage(src) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const img = new Image()
|
||||||
|
img.crossOrigin = 'anonymous'
|
||||||
|
img.onload = () => resolve(img)
|
||||||
|
img.onerror = reject
|
||||||
|
img.src = src
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function canvasToBlob(canvas, mime) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (canvas.toBlob) {
|
||||||
|
canvas.toBlob((blob) => resolve(blob), mime, 0.92)
|
||||||
|
} else {
|
||||||
|
// 兼容处理
|
||||||
|
const dataURL = canvas.toDataURL(mime)
|
||||||
|
const arr = dataURL.split(',')
|
||||||
|
const bstr = atob(arr[1])
|
||||||
|
let n = bstr.length
|
||||||
|
const u8arr = new Uint8Array(n)
|
||||||
|
while (n--) u8arr[n] = bstr.charCodeAt(n)
|
||||||
|
resolve(new Blob([u8arr], { type: mime }))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
// 确保富文本编辑器的点击区域
|
||||||
|
:deep(.w-e-text-container) {
|
||||||
|
min-height: 300px !important;
|
||||||
|
cursor: text !important;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
:deep(.w-e-scroll) {
|
||||||
|
min-height: 300px !important;
|
||||||
|
cursor: text !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.w-e-text-placeholder) {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保整个编辑器区域都可以点击
|
||||||
|
:deep(.w-e-text-container .w-e-scroll) {
|
||||||
|
cursor: text !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修复编辑器内容区域的点击问题
|
||||||
|
:deep(.w-e-text-container .w-e-scroll .w-e-text) {
|
||||||
|
min-height: 300px !important;
|
||||||
|
cursor: text !important;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -12,7 +12,6 @@
|
|||||||
"/src/view/dashboard/components/charts.vue": "Charts",
|
"/src/view/dashboard/components/charts.vue": "Charts",
|
||||||
"/src/view/dashboard/components/notice.vue": "Notice",
|
"/src/view/dashboard/components/notice.vue": "Notice",
|
||||||
"/src/view/dashboard/components/pluginTable.vue": "PluginTable",
|
"/src/view/dashboard/components/pluginTable.vue": "PluginTable",
|
||||||
"/src/view/dashboard/components/quickLinks.vue": "QuickLinks",
|
|
||||||
"/src/view/dashboard/components/table.vue": "Table",
|
"/src/view/dashboard/components/table.vue": "Table",
|
||||||
"/src/view/dashboard/components/wiki.vue": "Wiki",
|
"/src/view/dashboard/components/wiki.vue": "Wiki",
|
||||||
"/src/view/dashboard/index.vue": "Dashboard",
|
"/src/view/dashboard/index.vue": "Dashboard",
|
||||||
|
@@ -56,6 +56,7 @@
|
|||||||
[富文本内容]
|
[富文本内容]
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column align="left" label="被查询次数" prop="search_count" width="120" />
|
||||||
<el-table-column align="left" label="操作" fixed="right" :min-width="appStore.operateMinWith">
|
<el-table-column align="left" label="操作" fixed="right" :min-width="appStore.operateMinWith">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button type="primary" link class="table-button" @click="getDetails(scope.row)"><el-icon style="margin-right: 5px"><InfoFilled /></el-icon>查看</el-button>
|
<el-button type="primary" link class="table-button" @click="getDetails(scope.row)"><el-icon style="margin-right: 5px"><InfoFilled /></el-icon>查看</el-button>
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
<el-input v-model="formData.keyword" :clearable="true" placeholder="请输入关键词" />
|
<el-input v-model="formData.keyword" :clearable="true" placeholder="请输入关键词" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="内容:" prop="content">
|
<el-form-item label="内容:" prop="content">
|
||||||
<RichEdit v-model="formData.content"/>
|
<RichEdit v-model="formData.content" :useWatermark="true" :key="'rich-edit-' + useWatermark" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button :loading="btnLoading" type="primary" @click="save">保存</el-button>
|
<el-button :loading="btnLoading" type="primary" @click="save">保存</el-button>
|
||||||
@@ -43,6 +43,7 @@ const router = useRouter()
|
|||||||
const btnLoading = ref(false)
|
const btnLoading = ref(false)
|
||||||
|
|
||||||
const type = ref('')
|
const type = ref('')
|
||||||
|
const useWatermark = ref(true) // 明确设置为 true
|
||||||
const formData = ref({
|
const formData = ref({
|
||||||
keyword: '',
|
keyword: '',
|
||||||
content: '',
|
content: '',
|
||||||
|
@@ -3,7 +3,6 @@ import GvaCard from './card.vue'
|
|||||||
import GvaChart from './charts.vue'
|
import GvaChart from './charts.vue'
|
||||||
import GvaTable from './table.vue'
|
import GvaTable from './table.vue'
|
||||||
import GvaNotice from './notice.vue'
|
import GvaNotice from './notice.vue'
|
||||||
import GvaQuickLink from './quickLinks.vue'
|
|
||||||
import GvaWiki from './wiki.vue'
|
import GvaWiki from './wiki.vue'
|
||||||
import GvaPluginTable from './pluginTable.vue'
|
import GvaPluginTable from './pluginTable.vue'
|
||||||
|
|
||||||
@@ -13,7 +12,6 @@ export {
|
|||||||
GvaChart,
|
GvaChart,
|
||||||
GvaTable,
|
GvaTable,
|
||||||
GvaNotice,
|
GvaNotice,
|
||||||
GvaQuickLink,
|
|
||||||
GvaWiki,
|
GvaWiki,
|
||||||
GvaPluginTable
|
GvaPluginTable
|
||||||
}
|
}
|
||||||
|
@@ -1,111 +0,0 @@
|
|||||||
<!--
|
|
||||||
@auther: bypanghu<bypanghu@163.com>
|
|
||||||
@date: 2024/5/8
|
|
||||||
!-->
|
|
||||||
<template>
|
|
||||||
<div class="mt-8 w-full">
|
|
||||||
<div class="grid grid-cols-2 md:grid-cols-3 3xl:grid-cols-4">
|
|
||||||
<div
|
|
||||||
v-for="(item, index) in shortcuts"
|
|
||||||
:key="index"
|
|
||||||
class="flex flex-col items-center mb-3 group cursor-pointer"
|
|
||||||
@click="toPath(item)"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="w-8 h-8 rounded bg-gray-200 dark:bg-slate-500 flex items-center justify-center group-hover:bg-blue-400 group-hover:text-white"
|
|
||||||
>
|
|
||||||
<el-icon><component :is="item.icon" /></el-icon>
|
|
||||||
</div>
|
|
||||||
<div class="text-xs mt-2 text-gray-700 dark:text-gray-300">
|
|
||||||
{{ item.title }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 md:grid-cols-3 3xl:grid-cols-4 mt-8">
|
|
||||||
<div
|
|
||||||
v-for="(item, index) in recentVisits"
|
|
||||||
:key="index"
|
|
||||||
class="flex flex-col items-center mb-3 group cursor-pointer"
|
|
||||||
@click="openLink(item)"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="w-8 h-8 rounded bg-gray-200 dark:bg-slate-500 flex items-center justify-center group-hover:bg-blue-400 group-hover:text-white"
|
|
||||||
>
|
|
||||||
<el-icon><component :is="item.icon" /></el-icon>
|
|
||||||
</div>
|
|
||||||
<div class="text-xs mt-2 text-gray-700 dark:text-gray-300">
|
|
||||||
{{ item.title }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup>
|
|
||||||
import {
|
|
||||||
Menu,
|
|
||||||
Link,
|
|
||||||
User,
|
|
||||||
Service,
|
|
||||||
Document,
|
|
||||||
Reading,
|
|
||||||
Files,
|
|
||||||
Memo
|
|
||||||
} from '@element-plus/icons-vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const toPath = (item) => {
|
|
||||||
router.push({ name: item.path })
|
|
||||||
}
|
|
||||||
|
|
||||||
const openLink = (item) => {
|
|
||||||
window.open(item.path, '_blank')
|
|
||||||
}
|
|
||||||
const shortcuts = [
|
|
||||||
{
|
|
||||||
icon: Menu,
|
|
||||||
title: '菜单管理',
|
|
||||||
path: 'menu'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Link,
|
|
||||||
title: 'API管理',
|
|
||||||
path: 'api'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Service,
|
|
||||||
title: '角色管理',
|
|
||||||
path: 'authority'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: User,
|
|
||||||
title: '用户管理',
|
|
||||||
path: 'user'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Files,
|
|
||||||
title: '自动化包',
|
|
||||||
path: 'autoPkg'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Memo,
|
|
||||||
title: '自动代码',
|
|
||||||
path: 'autoCode'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const recentVisits = [
|
|
||||||
{
|
|
||||||
icon: Reading,
|
|
||||||
title: '授权购买',
|
|
||||||
path: 'https://gin-vue-admin.com/empower/index.html'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Document,
|
|
||||||
title: '插件市场',
|
|
||||||
path: 'https://plugin.gin-vue-admin.com/#/layout/home'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
|
@@ -11,50 +11,8 @@
|
|||||||
<gva-card custom-class="col-span-1 lg:col-span-2 ">
|
<gva-card custom-class="col-span-1 lg:col-span-2 ">
|
||||||
<gva-chart :type="3" title="解决数量" />
|
<gva-chart :type="3" title="解决数量" />
|
||||||
</gva-card>
|
</gva-card>
|
||||||
<gva-card
|
|
||||||
title="快捷功能"
|
|
||||||
show-action
|
|
||||||
custom-class="col-start-1 md:col-start-3 lg:col-start-7 row-span-2 "
|
|
||||||
>
|
|
||||||
<gva-quick-link />
|
|
||||||
</gva-card>
|
|
||||||
<gva-card
|
|
||||||
title="内容数据"
|
|
||||||
custom-class="col-span-1 md:col-span-2 md:row-start-2 lg:col-span-6 col-start-1 row-span-2"
|
|
||||||
>
|
|
||||||
<gva-chart :type="4" />
|
|
||||||
</gva-card>
|
|
||||||
<gva-card
|
|
||||||
title="文档"
|
|
||||||
show-action
|
|
||||||
custom-class="md:row-start-8 md:col-start-3 lg:row-start-3 lg:col-start-7"
|
|
||||||
>
|
|
||||||
<gva-wiki />
|
|
||||||
</gva-card>
|
|
||||||
|
|
||||||
<gva-card
|
|
||||||
title="最新更新"
|
|
||||||
custom-class="col-span-1 md:col-span-3 row-span-2"
|
|
||||||
>
|
|
||||||
<gva-table />
|
|
||||||
</gva-card>
|
|
||||||
<gva-card
|
|
||||||
title="最新插件"
|
|
||||||
custom-class="col-span-1 md:col-span-3 row-span-2"
|
|
||||||
>
|
|
||||||
<gva-plugin-table />
|
|
||||||
</gva-card>
|
|
||||||
|
|
||||||
<gva-card title="公告" show-action custom-class="col-span-1 lg:col-start-7">
|
|
||||||
<gva-notice />
|
|
||||||
</gva-card>
|
|
||||||
|
|
||||||
<gva-card
|
|
||||||
without-padding
|
|
||||||
custom-class="overflow-hidden lg:h-40 col-span-1 md:col-start-2 md:col-span-1 lg:col-start-7"
|
|
||||||
>
|
|
||||||
<gva-banner />
|
|
||||||
</gva-card>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -65,7 +23,6 @@
|
|||||||
GvaChart,
|
GvaChart,
|
||||||
GvaWiki,
|
GvaWiki,
|
||||||
GvaNotice,
|
GvaNotice,
|
||||||
GvaQuickLink,
|
|
||||||
GvaCard,
|
GvaCard,
|
||||||
GvaBanner
|
GvaBanner
|
||||||
} from './components'
|
} from './components'
|
||||||
|
Reference in New Issue
Block a user