feat:内容管理基本功能,富文本编辑器基本封装等

This commit is contained in:
LAPTOP-MI\Lau-mi 2024-05-28 00:12:17 +08:00
parent 1525f0abad
commit 5b893b29f4
15 changed files with 540 additions and 39 deletions

1
.gitignore vendored
View File

@ -8,6 +8,7 @@ pnpm-debug.log*
lerna-debug.log*
node_modules
package-lock.json
.DS_Store
dist
dist-ssr

58
package-lock.json generated
View File

@ -1,11 +1,11 @@
{
"name": "light-blog-frontend",
"name": "light-blog-admin",
"version": "0.1.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "light-blog-frontend",
"name": "light-blog-admin",
"version": "0.1.1",
"dependencies": {
"axios": "~1.7.2",
@ -15,7 +15,8 @@
"pinia": "~2.1.7",
"pinia-plugin-persistedstate": "~3.2.1",
"vue": "~3.4.21",
"vue-router": "~4.3.0"
"vue-router": "~4.3.0",
"wangeditor": "^4.7.15"
},
"devDependencies": {
"@rushstack/eslint-patch": "~1.8.0",
@ -44,6 +45,29 @@
"node": ">=6.0.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.24.6",
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.24.6.tgz",
"integrity": "sha512-Ja18XcETdEl5mzzACGd+DKgaGJzPTCow7EglgwTmHdwokzDFYh/MHua6lU6DV/hjF2IaOJ4oX2nqnjG7RElKOw==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/runtime-corejs3": {
"version": "7.24.6",
"resolved": "https://registry.npmmirror.com/@babel/runtime-corejs3/-/runtime-corejs3-7.24.6.tgz",
"integrity": "sha512-tbC3o8uHK9xMgMsvUm9qGqxVpbv6yborMBLbDteHIc7JDNHsTV0vDMQ5j1O1NXvO+BDELtL9KgoWYaUVIVGt8w==",
"dependencies": {
"core-js-pure": "^3.30.2",
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@ctrl/tinycolor": {
"version": "3.6.1",
"resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
@ -1306,6 +1330,16 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
},
"node_modules/core-js-pure": {
"version": "3.37.1",
"resolved": "https://registry.npmmirror.com/core-js-pure/-/core-js-pure-3.37.1.tgz",
"integrity": "sha512-J/r5JTHSmzTxbiYYrzXg9w1VpqrYt+gexenBE9pugeyhwPZTAEJddyiReJWsLO6uNQ8xJZFbod6XC7KKwatCiA==",
"hasInstallScript": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -2586,6 +2620,11 @@
"node": ">=8.10.0"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz",
@ -2867,8 +2906,7 @@
"node_modules/tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
"dev": true
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
},
"node_modules/type-check": {
"version": "0.4.0",
@ -3022,6 +3060,16 @@
"vue": "^3.2.0"
}
},
"node_modules/wangeditor": {
"version": "4.7.15",
"resolved": "https://registry.npmmirror.com/wangeditor/-/wangeditor-4.7.15.tgz",
"integrity": "sha512-aPTdREd8BxXVyJ5MI+LU83FQ7u1EPd341iXIorRNYSOvoimNoZ4nPg+yn3FGbB93/owEa6buLw8wdhYnMCJQLg==",
"dependencies": {
"@babel/runtime": "^7.11.2",
"@babel/runtime-corejs3": "^7.11.2",
"tslib": "^2.1.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",

View File

@ -18,7 +18,8 @@
"pinia": "~2.1.7",
"pinia-plugin-persistedstate": "~3.2.1",
"vue": "~3.4.21",
"vue-router": "~4.3.0"
"vue-router": "~4.3.0",
"wangeditor": "^4.7.15"
},
"devDependencies": {
"@rushstack/eslint-patch": "~1.8.0",

View File

@ -16,6 +16,17 @@ export const getContentList = (data = { page: 1, size: 10, sort: 'asc' }) => {
})
}
/**
* 获取分类列表
* @returns
*/
export const getCategoryList = () => {
return http({
url: '/admin/category',
method: 'get'
})
}
/**
* 删除此 id 的内容
* @param {String} id
@ -34,12 +45,35 @@ export const deleteOneById = (id = '') => {
/**
* 获取内容详情
*/
export const getContentDetailBy_uidOrTitle = (data = {}) => {
if (!data._uid && !data.title) return false
export const getContentDetailByIdOrTitle = (data = {}) => {
if (!data.id && !data.title) return false
return http({
url: '/admin/content',
method: 'get',
params: data
})
}
/**
* 更新内容
*/
export const updateContent = (data = {}) => {
if (!data.id) return false
return http({
url: '/admin/content',
method: 'put',
data: data
})
}
/**
* 更新内容
*/
export const createContent = (data = {}) => {
if (!data.title || !data.content || !data.summary || !data.category) return false
return http({
url: '/admin/content',
method: 'post',
data: data
})
}

View File

@ -1,3 +0,0 @@
import Editor from './src/Editor.vue'
export { Editor }

View File

@ -0,0 +1,3 @@
import myEditor from './src/my-editor.vue'
export { myEditor }

View File

@ -0,0 +1,173 @@
<script setup>
import { onBeforeUnmount, ref, onMounted, computed } from 'vue'
import E from 'wangeditor'
import { useLoginStore } from '@/stores/user/loginStore'
const { token } = useLoginStore()
const props = defineProps({
category: {
type: Array,
required: true,
default: () => {
return []
}
},
content: {
type: Object,
required: true,
default: () => {
return {
title: '',
_uid: '',
author: '',
category: '',
created_at: '',
summary: '',
content: '',
isTop: false,
isShow: false,
enableReward: false
}
}
},
loadingState: {
type: Boolean,
required: true,
default: false
}
})
const emit = defineEmits(['submit'])
const blogContent = computed(() => props.content)
let editor = ref(null)
//
const initEditor = () => {
editor.value = new E('#editor-w')
// 500px
editor.value.config.height = 540
editor.value.config.placeholder = '请输入内容...'
// focus
editor.value.config.focus = true
//
editor.value.config.uploadImgServer = '/api/admin/file'
// file
editor.value.config.uploadFileName = 'blog-files'
editor.value.config.uploadImgMaxSize = 4 * 1024 * 1024 // 4M
editor.value.config.uploadImgMaxLength = 9 // 9
editor.value.config.uploadImgAccept = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'ico']
// Authorization TOKEN
editor.value.config.uploadImgHeaders = {
Authorization: `Bearer ${token}`
}
//
editor.value.config.uploadVideoServer = '/api/admin/file'
editor.value.config.uploadVideoMaxSize = 1024 * 1024 * 200 // 200m
editor.value.config.uploadVideoFileName = 'blog-files'
editor.value.config.uploadVideoAccept = ['mp4', 'ogg', 'webm']
editor.value.config.uploadVideoHeaders = {
Authorization: `Bearer ${token}`
}
//
editor.value.create()
//
editor.value.txt.html(blogContent.value.content)
}
const confirmLeavePage = (event) => {
event.preventDefault() //
event.returnValue = '' //
}
onMounted(() => {
initEditor()
window.addEventListener('beforeunload', confirmLeavePage)
})
onBeforeUnmount(() => {
editor.value.destroy()
window.removeEventListener('beforeunload', confirmLeavePage)
})
const handleClear = () => {
editor.value.txt.clear()
}
const handleSubmit = () => {
// console.log(editor.value.txt.html())
blogContent.value.content = editor.value.txt.html()
emit('submit', blogContent.value)
}
</script>
<template>
<div class="content-others">
<el-form :data="blogContent" inline>
<el-form-item label="标题">
<el-input v-model="blogContent.title" placeholder="请输入标题" />
</el-form-item>
<el-form-item label="是否展示:">
<el-radio-group v-model="blogContent.isShow">
<el-radio :value="true">展示</el-radio>
<el-radio :value="false">不展示</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="所属分类:">
<el-select v-model="blogContent.category" style="width: 240px">
<el-option-group
v-for="group in props.category"
:key="group.name + group.id"
:label="group.name"
>
<el-option
v-for="item in group.children"
:key="item.name"
:label="item.name"
:value="item.name"
/>
</el-option-group>
</el-select>
</el-form-item>
<el-form-item label="是否置顶推荐:">
<el-radio-group v-model="blogContent.isTop">
<el-radio :value="true">置顶</el-radio>
<el-radio :value="false">不置顶</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="是否开启打赏:">
<el-radio-group v-model="blogContent.enableReward">
<el-radio :value="true">开启</el-radio>
<el-radio :value="false">关闭</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="摘要">
<el-input
v-model="blogContent.summary"
style="width: 800px"
:rows="3"
type="textarea"
placeholder="请输入摘要..."
/>
</el-form-item>
</el-form>
</div>
<div id="editor-w"></div>
<div class="handle-bar">
<el-button plain type="danger" @click="handleClear">清空</el-button>
<el-button type="warning" @click="handleSubmit" :loading="loadingState">提交</el-button>
</div>
</template>
<style lang="scss" scoped>
.content-others {
}
.handle-bar {
padding: 20px 40px;
display: flex;
justify-content: flex-end;
}
</style>

3
src/locale/index.js Normal file
View File

@ -0,0 +1,3 @@
import zhCn from 'element-plus/es/locale/lang/zh-cn'
export { zhCn }

View File

@ -8,6 +8,7 @@ import router from './router'
import { useStore } from './stores'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
const app = createApp(App)

View File

@ -5,6 +5,8 @@ const API_URL = import.meta.env.VITE_APP_API_BASE_URL || '/api'
import { useLoginStore } from '@/stores/user/loginStore'
import { useRouter } from 'vue-router'
const http = axios.create({
baseURL: API_URL,
timeout: 1000 * 15,
@ -57,6 +59,12 @@ http.interceptors.response.use(
return response.data
},
(error) => {
const { clearLoginInfo } = useLoginStore()
if (error.response.status === 401) {
const router = useRouter()
clearLoginInfo()
router.push({ name: 'login' })
}
ElNotification({
title: '请求错误',
message: `${error.response.status}-${!error.response.data.msg ? error.response.statusText : error.response.data.msg}`,

View File

@ -1,24 +1,73 @@
<script setup>
import { onMounted, ref } from 'vue'
import { getContentList, deleteOneById, getContentDetailBy_uidOrTitle } from '@/api/content/content'
import { ElMessage } from 'element-plus'
// wangEditor
import { myEditor } from '@c/my-editor'
import { useLoginStore } from '@/stores/user/loginStore'
import {
getContentList,
getCategoryList,
deleteOneById,
getContentDetailByIdOrTitle,
updateContent,
createContent
} from '@/api/content/content'
const { username } = useLoginStore()
const searchFromData = ref({
title: '',
timeRange: [],
page: 1,
size: 10,
sort: 'desc'
})
const handleSubmitSearch = () => {
getContentListData()
}
//
const blogList = ref([])
const getContentListData = () => {
getContentList({
page: 1,
size: 10,
sort: 'asc'
...searchFromData.value
}).then((res) => {
if (res.code === 20200) {
blogList.value = res.data.list
} else {
ElMessage({
message: '获取内容列表失败',
type: 'error',
duration: 2000
})
}
})
}
//
const categoryList = ref([])
const getCategoryListData = () => {
getCategoryList().then((res) => {
if (res.code === 20200) {
categoryList.value = res.data.list
} else {
ElMessage({
message: '获取分类失败',
type: 'error',
duration: 2000
})
}
})
}
//
onMounted(() => {
getContentListData()
getCategoryListData()
})
//
@ -41,18 +90,159 @@ const handleDelete = (id) => {
}
//
//
const editorData = ref({
title: '',
content: {
title: '',
_uid: '',
author: '',
category: '',
created_at: '',
summary: '',
content: '',
isTop: false,
isShow: false,
enableReward: false
},
dialogShow: false,
submitBtnLoading: false
})
const handleAdd = () => {
editorData.value.content = {
title: '',
_uid: '',
author: username,
category: '',
created_at: '',
summary: '',
content: '',
isTop: false,
isShow: false,
enableReward: false
}
editorData.value.dialogShow = true
}
const handleEdit = (id) => {
getContentDetailBy_uidOrTitle({ _uid: id }).then((res) => {
console.log(res)
getContentDetailByIdOrTitle({ id: id }).then((res) => {
if (res.code === 20200) {
editorData.value.content = res.data
editorData.value.dialogShow = true
}
})
}
const onEditorSubmit = (data) => {
// loding
editorData.value.submitBtnLoading = true
//
if (data._uid === '') {
console.log(data)
createContent({
title: data.title,
author: data.author,
category: data.category,
summary: data.summary,
content: data.content,
enable_reward: data.enableReward,
is_show: data.isShow,
is_top: data.isTop
})
.then(async (res) => {
if (res.code === 20200) {
await getContentListData()
//
editorData.value.dialogShow = false
ElMessage({
message: '新增成功',
type: 'success',
duration: 2000
})
} else {
ElMessage({
message: res.msg,
type: 'error',
duration: 2000
})
}
})
.finally(() => {
editorData.value.submitBtnLoading = false
})
}
//
else {
updateContent({
id: data._uid,
title: data.title,
category: data.category,
summary: data.summary,
content: data.content,
isShow: data.isShow,
isTop: data.isTop,
enableReward: data.enableReward
})
.then(async (res) => {
if (res.code === 20200) {
await getContentListData()
//
editorData.value.dialogShow = false
ElMessage({
message: '编辑成功',
type: 'success',
duration: 2000
})
} else {
ElMessage({
message: res.msg,
type: 'error',
duration: 2000
})
}
})
.finally(() => {
editorData.value.submitBtnLoading = false
})
}
}
</script>
<template>
<div class="blog-content">
<div class="search-box"></div>
<div class="search-box">
<el-form :data="searchFromData" inline>
<el-form-item>
<el-button type="success" @click="handleAdd">新增</el-button>
</el-form-item>
<el-form-item label="标题">
<el-input
v-model="searchFromData.title"
placeholder="请输入标题"
clearable
style="width: 220px"
></el-input>
</el-form-item>
<el-form-item label="时间">
<el-date-picker
v-model="searchFromData.timeRange"
placeholder="请选择时间段"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
clearable
></el-date-picker>
</el-form-item>
<el-form-item>
<el-button @click="handleSubmitSearch">搜索</el-button>
</el-form-item>
</el-form>
</div>
<div class="content-list">
<el-table :data="blogList" border>
<el-table :data="blogList" border height="500">
<el-table-column label="序号" type="index" align="center" width="90"></el-table-column>
<el-table-column label="标题" prop="title" align="center" width="140"></el-table-column>
<el-table-column label="作者" prop="author" align="center" width="100"></el-table-column>
@ -78,12 +268,42 @@ const handleEdit = (id) => {
</el-table-column>
<el-table-column label="操作" align="center" width="140">
<template #default="{ row }">
<el-button type="warning" size="small" @click="handleEdit(row._uid)">编辑</el-button>
<el-button type="danger" size="small" @click="handleDelete(row._uid)">删除</el-button>
<el-button plain type="warning" size="small" @click="handleEdit(row._uid)"
>编辑</el-button
>
<el-popconfirm
width="220"
confirm-button-text="确定"
confirm-button-type="danger"
cancel-button-text="取消"
icon-color="#ff4c6c"
title="确定删除此项吗?删除后不可恢复!"
@confirm="handleDelete(row._uid)"
>
<template #reference>
<el-button plain type="danger" size="small">删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
</div>
<!-- 编辑器dialog -->
<el-dialog
v-model="editorData.dialogShow"
:close-on-click-modal="false"
:close-on-press-escape="false"
destroy-on-close
fullscreen
>
<myEditor
:content="editorData.content"
:category="categoryList"
@submit="onEditorSubmit"
:loadingState="editorData.submitBtnLoading"
></myEditor>
</el-dialog>
</div>
</template>

View File

@ -23,5 +23,8 @@ const beian = '蜀ICP备2023007248号-2'
flex-direction: column;
justify-content: center;
align-items: center;
> div {
transform: scale(0.84);
}
}
</style>

View File

@ -1,11 +1,13 @@
<script setup>
import baseLayout from '@/layout/baseLayout/baseLayout.vue'
import { RouterView } from 'vue-router'
import { computed } from 'vue'
import { RouterView } from 'vue-router'
import { useLoginStore } from '@/stores/user/loginStore'
const loginStore = useLoginStore()
import { zhCn } from '@/locale/index'
let username = computed(() => loginStore.username)
import headerBar from './components/headerBar.vue'
@ -20,6 +22,7 @@ const handleLogout = () => {
</script>
<template>
<el-config-provider :locale="zhCn">
<div>
<baseLayout>
<template #header>
@ -36,6 +39,7 @@ const handleLogout = () => {
</template>
</baseLayout>
</div>
</el-config-provider>
</template>
<style lang="scss" scoped></style>

View File

@ -59,6 +59,11 @@ export default defineConfig(({ mode }) => {
target: 'http://localhost:8081/',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
},
'/static': {
target: 'http://localhost:8081/static',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/static/, '')
}
}
}