项目创建
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<h1 class="title">留言墙</h1>
|
||||
<p class="desc">每一份留言都记录了您的想法,也为我们提供了珍贵的回忆</p>
|
||||
|
||||
<div class="list">
|
||||
<ul class="offset">
|
||||
<li
|
||||
class="comment-box"
|
||||
v-for="item in commentList"
|
||||
:key="item.id"
|
||||
:style="{ background: randomColor() }"
|
||||
@click="openDrawer(item)"
|
||||
>
|
||||
<p class="date">{{ item.date }}</p>
|
||||
<p class="content">{{ item.content }}</p>
|
||||
<div class="bottom">
|
||||
<div class="left">
|
||||
<span><i class="iconfont-sys"></i>{{ item.collection }}</span>
|
||||
<span><i class="iconfont-sys"></i>{{ item.comment }}</span>
|
||||
</div>
|
||||
<div class="right">
|
||||
<span>{{ item.userName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<ElDrawer
|
||||
lDrawer
|
||||
v-model="showDrawer"
|
||||
:lock-scroll="false"
|
||||
:size="360"
|
||||
modal-class="comment-modal"
|
||||
>
|
||||
<template #header>
|
||||
<h4>详情</h4>
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="drawer-default">
|
||||
<div class="comment-box" :style="{ background: randomColor() }">
|
||||
<p class="date">{{ clickItem.date }}</p>
|
||||
<p class="content">{{ clickItem.content }}</p>
|
||||
<div class="bottom">
|
||||
<div class="left">
|
||||
<span><i class="iconfont-sys"></i>{{ clickItem.collection }}</span>
|
||||
<span><i class="iconfont-sys"></i>{{ clickItem.comment }}</span>
|
||||
</div>
|
||||
<div class="right">
|
||||
<span>{{ clickItem.userName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 评论组件 -->
|
||||
<CommentWidget />
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div>
|
||||
<!-- <ElButton @click="cancelClick">cancel</ElButton> -->
|
||||
<!-- <ElButton type="primary" @click="confirmClick">confirm</ElButton> -->
|
||||
</div>
|
||||
</template>
|
||||
</ElDrawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { commentList } from '@/mock/temp/commentList'
|
||||
const showDrawer = ref(false)
|
||||
|
||||
defineOptions({ name: 'ArticleComment' })
|
||||
|
||||
// const colorList = reactive([
|
||||
// 'rgba(216, 248, 255, 0.8)',
|
||||
// 'rgba(253, 223, 217, 0.8)',
|
||||
// 'rgba(252, 230, 240, 0.8)',
|
||||
// 'rgba(211, 248, 240, 0.8)',
|
||||
// 'rgba(255, 234, 188, 0.8)',
|
||||
// 'rgba(245, 225, 255, 0.8)',
|
||||
// 'rgba(225, 230, 254, 0.8)'
|
||||
// ])
|
||||
|
||||
const colorList = reactive([
|
||||
'#D8F8FF',
|
||||
'#FDDFD9',
|
||||
'#FCE6F0',
|
||||
'#D3F8F0',
|
||||
'#FFEABC',
|
||||
'#F5E1FF',
|
||||
'#E1E6FE'
|
||||
])
|
||||
|
||||
let lastColor: string | null = null
|
||||
|
||||
const randomColor = () => {
|
||||
let newColor: string
|
||||
|
||||
do {
|
||||
const index = Math.floor(Math.random() * colorList.length)
|
||||
newColor = colorList[index]
|
||||
} while (newColor === lastColor)
|
||||
|
||||
lastColor = newColor
|
||||
return newColor
|
||||
}
|
||||
|
||||
const clickItem = ref({
|
||||
id: 1,
|
||||
date: '2024-9-3',
|
||||
content: '加油!学好Node 自己写个小Demo',
|
||||
collection: 5,
|
||||
comment: 8,
|
||||
userName: '匿名'
|
||||
})
|
||||
|
||||
const openDrawer = (item: any) => {
|
||||
showDrawer.value = true
|
||||
clickItem.value = item
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
background-color: transparent !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
:deep(.comment-modal) {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-top: 20px;
|
||||
font-size: 36px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin-top: 15px;
|
||||
font-size: 14px;
|
||||
color: var(--art-text-gray-600);
|
||||
}
|
||||
|
||||
.list {
|
||||
margin-top: 40px;
|
||||
|
||||
.offset {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: calc(100% + 16px);
|
||||
}
|
||||
}
|
||||
|
||||
.comment-box {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
width: calc(20% - 16px);
|
||||
aspect-ratio: 16 / 12;
|
||||
padding: 16px;
|
||||
margin: 0 16px 16px 0;
|
||||
cursor: pointer;
|
||||
background-color: #eae2cb;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 12px;
|
||||
color: #949494;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
left: 0;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 0 16px;
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 20px;
|
||||
font-size: 12px;
|
||||
color: #949494;
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
span {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-default {
|
||||
.comment-box {
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $device-notebook) {
|
||||
.page-content {
|
||||
.comment-box {
|
||||
width: calc(25% - 16px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $device-ipad-pro) {
|
||||
.page-content {
|
||||
.comment-box {
|
||||
width: calc(33.333% - 16px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $device-ipad) {
|
||||
.page-content {
|
||||
.comment-box {
|
||||
width: calc(50% - 16px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $device-phone) {
|
||||
.page-content {
|
||||
.comment-box {
|
||||
width: calc(100% - 16px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.page-content {
|
||||
.comment-box {
|
||||
color: #333 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div class="article-detail page-content">
|
||||
<div class="content">
|
||||
<h1>{{ articleTitle }}</h1>
|
||||
<div class="markdown-body" v-highlight v-html="articleHtml"></div>
|
||||
</div>
|
||||
<ArtBackToTop />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import '@/assets/styles/markdown.scss'
|
||||
import '@/assets/styles/one-dark-pro.scss'
|
||||
import { useCommon } from '@/composables/useCommon'
|
||||
import axios from 'axios'
|
||||
// import 'highlight.js/styles/atom-one-dark.css';
|
||||
// import 'highlight.js/styles/vs2015.css';
|
||||
|
||||
defineOptions({ name: 'ArticleDetail' })
|
||||
|
||||
const articleId = ref(0)
|
||||
const router = useRoute()
|
||||
const articleTitle = ref('')
|
||||
const articleHtml = ref('')
|
||||
|
||||
onMounted(() => {
|
||||
useCommon().scrollToTop()
|
||||
articleId.value = Number(router.query.id)
|
||||
getArticleDetail()
|
||||
})
|
||||
|
||||
const getArticleDetail = async () => {
|
||||
if (articleId.value) {
|
||||
const res = await axios.get('https://www.qiniu.lingchen.kim/blog_detail.json')
|
||||
if (res.data.code === 200) {
|
||||
articleTitle.value = res.data.data.title
|
||||
articleHtml.value = res.data.data.html_content
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.article-detail {
|
||||
.content {
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
margin-top: 60px;
|
||||
|
||||
.markdown-body {
|
||||
margin-top: 60px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
border: 1px solid var(--art-gray-200);
|
||||
}
|
||||
|
||||
pre {
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
.copy-button {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50px;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
content: '';
|
||||
background: #0a0a0e;
|
||||
}
|
||||
}
|
||||
|
||||
.code-wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.line-number {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
margin-right: 10px;
|
||||
font-size: 14px;
|
||||
color: #9e9e9e;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
z-index: 1;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 20px;
|
||||
line-height: 40px;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
background-color: #000;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
opacity: 0;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,380 @@
|
||||
<template>
|
||||
<div class="page-content article-list">
|
||||
<ElRow justify="space-between" :gutter="10">
|
||||
<ElCol :lg="6" :md="6" :sm="14" :xs="16">
|
||||
<ElInput
|
||||
v-model="searchVal"
|
||||
:prefix-icon="Search"
|
||||
clearable
|
||||
placeholder="输入文章标题查询"
|
||||
@keyup.enter="searchArticle"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :lg="12" :md="12" :sm="0" :xs="0">
|
||||
<div class="custom-segmented">
|
||||
<ElSegmented v-model="yearVal" :options="options" @change="searchArticleByYear" />
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :lg="6" :md="6" :sm="10" :xs="6" style="display: flex; justify-content: end">
|
||||
<ElButton @click="toAddArticle" v-auth="'add'">新增文章</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<div class="list">
|
||||
<div class="offset">
|
||||
<div class="item" v-for="item in articleList" :key="item.id" @click="toDetail(item)">
|
||||
<!-- 骨架屏 -->
|
||||
<ElSkeleton animated :loading="isLoading" style="width: 100%; height: 100%">
|
||||
<template #template>
|
||||
<div class="top">
|
||||
<ElSkeletonItem
|
||||
variant="image"
|
||||
style="width: 100%; height: 100%; border-radius: 10px"
|
||||
/>
|
||||
<div style="padding: 16px 0">
|
||||
<ElSkeletonItem variant="p" style="width: 80%" />
|
||||
<ElSkeletonItem variant="p" style="width: 40%; margin-top: 10px" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<div class="top">
|
||||
<ElImage class="cover" :src="item.home_img" lazy fit="cover">
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<ElIcon><icon-picture /></ElIcon>
|
||||
</div>
|
||||
</template>
|
||||
</ElImage>
|
||||
|
||||
<span class="type">{{ item.type_name }}</span>
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<h2>{{ item.title }}</h2>
|
||||
<div class="info">
|
||||
<div class="text">
|
||||
<i class="iconfont-sys"></i>
|
||||
<span>{{ useDateFormat(item.create_time, 'YYYY-MM-DD') }}</span>
|
||||
<div class="line"></div>
|
||||
<i class="iconfont-sys"></i>
|
||||
<span>{{ item.count }}</span>
|
||||
</div>
|
||||
<ElButton v-auth="'edit'" size="small" @click.stop="toEdit(item)">编辑</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ElSkeleton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 16vh" v-if="showEmpty">
|
||||
<ElEmpty :description="`未找到相关数据 ${EmojiText[0]}`" />
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: center; margin-top: 20px">
|
||||
<ElPagination
|
||||
size="default"
|
||||
background
|
||||
v-model:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:pager-count="9"
|
||||
layout="prev, pager, next, total,jumper"
|
||||
:total="total"
|
||||
:hide-on-single-page="true"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Picture as IconPicture } from '@element-plus/icons-vue'
|
||||
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { router } from '@/router'
|
||||
import { useDateFormat } from '@vueuse/core'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import EmojiText from '@/utils/ui/emojo'
|
||||
import { ArticleList } from '@/mock/temp/articleList'
|
||||
import { useCommon } from '@/composables/useCommon'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
|
||||
defineOptions({ name: 'ArticleList' })
|
||||
|
||||
const yearVal = ref('All')
|
||||
|
||||
const options = ['All', '2024', '2023', '2022', '2021', '2020', '2019']
|
||||
|
||||
const searchVal = ref('')
|
||||
const articleList = ref<any[]>([])
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(40)
|
||||
// const lastPage = ref(0)
|
||||
const total = ref(0)
|
||||
const isLoading = ref(true)
|
||||
|
||||
const showEmpty = computed(() => {
|
||||
return articleList.value.length === 0 && !isLoading.value
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getArticleList({ backTop: false })
|
||||
})
|
||||
|
||||
// 搜索文章
|
||||
const searchArticle = () => {
|
||||
getArticleList({ backTop: true })
|
||||
}
|
||||
|
||||
// 根据年份查询文章
|
||||
const searchArticleByYear = () => {
|
||||
getArticleList({ backTop: true })
|
||||
}
|
||||
|
||||
const getArticleList = async ({ backTop = false }) => {
|
||||
isLoading.value = true
|
||||
// let year = yearVal.value
|
||||
|
||||
if (searchVal.value) {
|
||||
yearVal.value = 'All'
|
||||
}
|
||||
|
||||
if (yearVal.value === 'All') {
|
||||
// year = ''
|
||||
}
|
||||
|
||||
// const params = {
|
||||
// page: currentPage.value,
|
||||
// size: pageSize.value,
|
||||
// searchVal: searchVal.value,
|
||||
// year
|
||||
// }
|
||||
|
||||
articleList.value = ArticleList
|
||||
isLoading.value = false
|
||||
|
||||
if (backTop) {
|
||||
useCommon().scrollToTop()
|
||||
}
|
||||
|
||||
// const res = await ArticleService.getArticleList(params)
|
||||
// if (res.code === ApiStatus.success) {
|
||||
// currentPage.value = res.currentPage
|
||||
// pageSize.value = res.pageSize
|
||||
// lastPage.value = res.lastPage
|
||||
// total.value = res.total
|
||||
// articleList.value = res.data
|
||||
|
||||
// // setTimeout(() => {
|
||||
// isLoading.value = false
|
||||
// // }, 3000)
|
||||
|
||||
// if (searchVal.value) {
|
||||
// searchVal.value = ''
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val: number) => {
|
||||
currentPage.value = val
|
||||
getArticleList({ backTop: true })
|
||||
}
|
||||
|
||||
const toDetail = (item: any) => {
|
||||
router.push({
|
||||
path: RoutesAlias.ArticleDetail,
|
||||
query: {
|
||||
id: item.id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const toEdit = (item: any) => {
|
||||
router.push({
|
||||
path: RoutesAlias.ArticlePublish,
|
||||
query: {
|
||||
id: item.id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const toAddArticle = () => {
|
||||
router.push({
|
||||
path: RoutesAlias.ArticlePublish
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.article-list {
|
||||
.custom-segmented .el-segmented {
|
||||
height: 40px;
|
||||
padding: 6px;
|
||||
|
||||
--el-border-radius-base: 8px;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin-top: 20px;
|
||||
|
||||
.offset {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: calc(100% + 20px);
|
||||
|
||||
.item {
|
||||
box-sizing: border-box;
|
||||
width: calc(20% - 20px);
|
||||
margin: 0 20px 20px 0;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--art-border-color);
|
||||
border-radius: calc(var(--custom-radius) / 2 + 2px) !important;
|
||||
|
||||
&:hover {
|
||||
.el-button {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.top {
|
||||
position: relative;
|
||||
aspect-ratio: 16/9.5;
|
||||
|
||||
.cover {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
background: var(--art-gray-200);
|
||||
border-radius: calc(var(--custom-radius) / 2 + 2px)
|
||||
calc(var(--custom-radius) / 2 + 2px) 0 0;
|
||||
|
||||
.image-slot {
|
||||
font-size: 26px;
|
||||
color: var(--art-gray-400);
|
||||
}
|
||||
}
|
||||
|
||||
.type {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
padding: 5px 4px;
|
||||
font-size: 12px;
|
||||
color: rgba(#fff, 0.8);
|
||||
background: rgba($color: #000, $alpha: 60%);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
padding: 5px 10px;
|
||||
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
|
||||
@include ellipsis();
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 25px;
|
||||
margin-top: 6px;
|
||||
line-height: 25px;
|
||||
|
||||
.text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--art-text-gray-600);
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 13px;
|
||||
color: var(--art-gray-600);
|
||||
}
|
||||
|
||||
.line {
|
||||
width: 1px;
|
||||
height: 12px;
|
||||
margin: 0 15px;
|
||||
background-color: var(--art-border-dashed-color);
|
||||
}
|
||||
}
|
||||
|
||||
.el-button {
|
||||
opacity: 0;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $device-notebook) {
|
||||
.article-list {
|
||||
.list {
|
||||
.offset {
|
||||
.item {
|
||||
width: calc(25% - 20px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $device-ipad-pro) {
|
||||
.article-list {
|
||||
.list {
|
||||
.offset {
|
||||
.item {
|
||||
width: calc(33.333% - 20px);
|
||||
|
||||
.bottom {
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $device-ipad) {
|
||||
.article-list {
|
||||
.list {
|
||||
.offset {
|
||||
.item {
|
||||
width: calc(50% - 20px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $device-phone) {
|
||||
.article-list {
|
||||
.list {
|
||||
.offset {
|
||||
.item {
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,359 @@
|
||||
<template>
|
||||
<div class="article-edit">
|
||||
<div>
|
||||
<div class="editor-wrap">
|
||||
<!-- 文章标题、类型 -->
|
||||
<ElRow :gutter="10">
|
||||
<ElCol :span="18">
|
||||
<ElInput
|
||||
v-model.trim="articleName"
|
||||
placeholder="请输入文章标题(最多100个字符)"
|
||||
maxlength="100"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :span="6">
|
||||
<ElSelect v-model="articleType" placeholder="请选择文章类型" filterable>
|
||||
<ElOption
|
||||
v-for="item in articleTypes"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<!-- 富文本编辑器 -->
|
||||
<ArtWangEditor class="el-top" v-model="editorHtml" />
|
||||
|
||||
<div class="form-wrap">
|
||||
<h2>发布设置</h2>
|
||||
<!-- 图片上传 -->
|
||||
<ElForm>
|
||||
<ElFormItem label="封面">
|
||||
<div class="el-top upload-container">
|
||||
<ElUpload
|
||||
class="cover-uploader"
|
||||
:action="uploadImageUrl"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:on-success="onSuccess"
|
||||
:on-error="onError"
|
||||
:before-upload="beforeUpload"
|
||||
>
|
||||
<div v-if="!cover" class="upload-placeholder">
|
||||
<ElIcon class="upload-icon"><Plus /></ElIcon>
|
||||
<div class="upload-text">点击上传封面</div>
|
||||
</div>
|
||||
<img v-else :src="cover" class="cover-image" />
|
||||
</ElUpload>
|
||||
<div class="el-upload__tip">建议尺寸 16:9,jpg/png 格式</div>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="可见">
|
||||
<ElSwitch v-model="visible" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<div style="display: flex; justify-content: flex-end">
|
||||
<ElButton type="primary" @click="submit" style="width: 100px">
|
||||
{{ pageMode === PageModeEnum.Edit ? '保存' : '发布' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="outline-wrap">
|
||||
<div class="item" v-for="(item, index) in outlineList" :key="index">
|
||||
<p :class="`level${item.level}`">{{ item.text }}</p>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { ApiStatus } from '@/utils/http/status'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import EmojiText from '@/utils/ui/emojo'
|
||||
import { PageModeEnum } from '@/enums/formEnum'
|
||||
import axios from 'axios'
|
||||
import { useCommon } from '@/composables/useCommon'
|
||||
|
||||
defineOptions({ name: 'ArticlePublish' })
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const userStore = useUserStore()
|
||||
let { accessToken } = userStore
|
||||
|
||||
// 上传路径
|
||||
const uploadImageUrl = `${import.meta.env.VITE_API_URL}/api/common/upload`
|
||||
// 传递 token
|
||||
const uploadHeaders = { Authorization: accessToken }
|
||||
|
||||
let pageMode: PageModeEnum = PageModeEnum.Add // 页面类型 新增 | 编辑
|
||||
const articleName = ref('') // 文章标题
|
||||
const articleType = ref() // 文章类型
|
||||
const articleTypes = ref() // 类型列表
|
||||
const editorHtml = ref('') // 编辑器内容
|
||||
const createDate = ref('') // 创建时间
|
||||
const cover = ref('') // 图片
|
||||
const visible = ref(true) // 可见
|
||||
// const outlineList = ref()
|
||||
|
||||
onMounted(() => {
|
||||
useCommon().scrollToTop()
|
||||
getArticleTypes()
|
||||
initPageMode()
|
||||
})
|
||||
|
||||
// 初始化页面类型 新增 | 编辑
|
||||
const initPageMode = () => {
|
||||
const { id } = route.query
|
||||
pageMode = id ? PageModeEnum.Edit : PageModeEnum.Add
|
||||
if (pageMode === PageModeEnum.Edit && id) {
|
||||
initEditArticle()
|
||||
} else {
|
||||
initAddArticle()
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化编辑文章的逻辑
|
||||
const initEditArticle = () => {
|
||||
getArticleDetail()
|
||||
}
|
||||
|
||||
// 初始化新增文章逻辑
|
||||
const initAddArticle = () => {
|
||||
createDate.value = formDate(useNow().value)
|
||||
}
|
||||
|
||||
// 获取文章类型
|
||||
const getArticleTypes = async () => {
|
||||
try {
|
||||
const response = await axios.get('https://www.qiniu.lingchen.kim/classify.json')
|
||||
if (response.data.code === 200) {
|
||||
articleTypes.value = response.data.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching JSON data:', error)
|
||||
}
|
||||
// try {
|
||||
// const res = await ArticleService.getArticleTypes({})
|
||||
// if (res.code === ApiStatus.success) {
|
||||
// articleTypes.value = res.data
|
||||
// }
|
||||
// } catch (err) { }
|
||||
}
|
||||
|
||||
const getArticleDetail = async () => {
|
||||
const res = await axios.get('https://www.qiniu.lingchen.kim/blog_list.json')
|
||||
|
||||
if (res.data.code === ApiStatus.success) {
|
||||
let { title, blog_class, html_content } = res.data.data
|
||||
articleName.value = title
|
||||
articleType.value = Number(blog_class)
|
||||
editorHtml.value = html_content
|
||||
}
|
||||
}
|
||||
|
||||
// const getOutline = (content: string) => {
|
||||
// const regex = /<h([1-3])>(.*?)<\/h\1>/g
|
||||
// const headings = []
|
||||
// let match
|
||||
|
||||
// while ((match = regex.exec(content)) !== null) {
|
||||
// headings.push({ level: match[1], text: match[2] })
|
||||
// }
|
||||
// outlineList.value = headings
|
||||
// }
|
||||
|
||||
// 提交
|
||||
const submit = () => {
|
||||
if (pageMode === PageModeEnum.Edit) {
|
||||
editArticle()
|
||||
} else {
|
||||
addArticle()
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formDate = (date: string | Date): string => {
|
||||
return useDateFormat(date, 'YYYY-MM-DD').value
|
||||
}
|
||||
|
||||
// 验证输入
|
||||
const validateArticle = () => {
|
||||
if (!articleName.value) {
|
||||
ElMessage.error(`请输入文章标题`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (!articleType.value) {
|
||||
ElMessage.error(`请选择文章类型`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (editorHtml.value === '<p><br></p>') {
|
||||
ElMessage.error(`请输入文章内容`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (!cover.value) {
|
||||
ElMessage.error(`请上传图片`)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 添加文章
|
||||
const addArticle = async () => {
|
||||
try {
|
||||
if (!validateArticle()) return
|
||||
editorHtml.value = delCodeTrim(editorHtml.value)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑文章
|
||||
const editArticle = async () => {
|
||||
try {
|
||||
if (!validateArticle()) return
|
||||
|
||||
editorHtml.value = delCodeTrim(editorHtml.value)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
const delCodeTrim = (content: string): string => {
|
||||
return content.replace(/(\s*)<\/code>/g, '</code>')
|
||||
}
|
||||
|
||||
const onSuccess = (response: any) => {
|
||||
cover.value = response.data.url
|
||||
ElMessage.success(`图片上传成功 ${EmojiText[200]}`)
|
||||
}
|
||||
|
||||
const onError = () => {
|
||||
ElMessage.error(`图片上传失败 ${EmojiText[500]}`)
|
||||
}
|
||||
|
||||
// 添加上传前的校验
|
||||
const beforeUpload = (file: File) => {
|
||||
const isImage = file.type.startsWith('image/')
|
||||
const isLt2M = file.size / 1024 / 1024 < 2
|
||||
|
||||
if (!isImage) {
|
||||
ElMessage.error('只能上传图片文件!')
|
||||
return false
|
||||
}
|
||||
if (!isLt2M) {
|
||||
ElMessage.error('图片大小不能超过 2MB!')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.article-edit {
|
||||
.editor-wrap {
|
||||
max-width: 1000px;
|
||||
margin: 20px auto;
|
||||
|
||||
.el-top {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.form-wrap {
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
background-color: var(--art-main-bg-color);
|
||||
border: 1px solid var(--art-border-color);
|
||||
border-radius: calc(var(--custom-radius) / 2 + 2px) !important;
|
||||
|
||||
h2 {
|
||||
margin-bottom: 20px;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.outline-wrap {
|
||||
box-sizing: border-box;
|
||||
width: 280px;
|
||||
padding: 20px;
|
||||
border: 1px solid #e3e3e3;
|
||||
border-radius: 8px;
|
||||
|
||||
.item {
|
||||
p {
|
||||
height: 30px;
|
||||
font-size: 13px;
|
||||
line-height: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.level3 {
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-container {
|
||||
.cover-uploader {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition: var(--el-transition-duration);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.upload-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 260px;
|
||||
height: 160px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
|
||||
.upload-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
color: #8c939d;
|
||||
}
|
||||
}
|
||||
|
||||
.cover-image {
|
||||
display: block;
|
||||
width: 260px;
|
||||
height: 160px;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.el-upload__tip {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user