Compare commits

..

5 Commits

15 changed files with 537 additions and 103 deletions

View File

@@ -15,11 +15,18 @@ export const uploadBlogImageApi = (md5: string, form: FormData): Promise<string>
data: form data: form
}) })
export const queryBlogApi = (currentPage: number, pageSize: number): Promise<IPageRecord<IBlog>> => export const queryBlogApi = (currentPage: number, pageSize: number,
category: string): Promise<IPageRecord<IBlog>> =>
cloudService({ cloudService({
url: '/blog-api/blog/all/page', url: '/blog-api/blog/condition/page',
method: 'get', method: 'get',
params: { currentPage, pageSize } params: { currentPage, pageSize, category }
})
export const queryUnapprovedBlogApi = (): Promise<IBlog[]> =>
cloudService({
url: '/blog-api/blog/unapproved',
method: 'get'
}) })
export const queryBlogByIdApi = (id: number): Promise<IBlog> => export const queryBlogByIdApi = (id: number): Promise<IBlog> =>
@@ -86,3 +93,11 @@ export const queryBlogVisitApi = (currentPage: number,
method: 'get', method: 'get',
params: { currentPage, pageSize } params: { currentPage, pageSize }
}) })
export const queryBlogAlterApi = (name: string): Promise<any> =>
cloudService({
url: '/alter-api/log-alter/name',
method: 'get',
params: { name }
})

54
src/apis/stats.ts Normal file
View File

@@ -0,0 +1,54 @@
import { cloudService } from './index'
import { IStatsChart } from 'vue3-common/types'
import { IBlogOverview } from '@/types/blog'
export const queryOverviewStatsApi = (): Promise<IBlogOverview> =>
cloudService({
url: '/blog-api/stats/overview',
method: 'get'
})
export const queryCategoryStatsApi = (): Promise<IStatsChart[]> =>
cloudService({
url: '/blog-api/stats/category',
method: 'get'
})
export const queryApprovedMonthlyStatsApi = (year: number): Promise<IStatsChart[]> =>
cloudService({
url: '/blog-api/stats/approved/monthly',
method: 'get',
params: { year }
})
export const queryVisitMonthlyStatsApi = (year: number): Promise<IStatsChart[]> =>
cloudService({
url: '/blog-api/stats/visit/monthly',
method: 'get',
params: { year }
})
export const queryVisitRankStatsApi = (): Promise<IStatsChart[]> =>
cloudService({
url: '/blog-api/stats/visit/rank',
method: 'get'
})
export const queryReadRankStatsApi = (): Promise<IStatsChart[]> =>
cloudService({
url: '/blog-api/stats/read/rank',
method: 'get'
})
export const queryCommentRankStatsApi = (): Promise<IStatsChart[]> =>
cloudService({
url: '/blog-api/stats/comment/rank',
method: 'get'
})

View File

@@ -1 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1728286912539" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9408" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M960 512c0-59.5-40.9-109.7-96.1-124v68.6c-9.4-5.5-20.4-8.6-32-8.6-35.3 0-64.1 28.7-64.1 64s28.7 64 64.1 64c11.7 0 22.6-3.1 32-8.6V636c-10.2 2.6-21 4.1-32 4.1-70.7 0-128.1-57.4-128.1-128s57.5-128 128.1-128c11.1 0 21.8 1.4 32 4V192c0-52.9-41.8-96-93.1-96H221.2c-51.3 0-93.1 43.1-93.1 96v159.5h128.1v64H128.1V352H64v64h64.1v191.5h128.1v64H127.1V608H64v64h64.1v160c0 52.9 41.8 96 93.1 96h549.6c51.3 0 93.1-43.1 93.1-96V635.9C919.1 621.7 960 571.5 960 512zM320.3 288h384.4v64H320.3v-64z m0 192h318.4v64H320.3v-64z m384.4 256H320.3v-64h384.4v64z m164.4-276z m4.9 3.9l-0.1-0.1s0.1 0 0.1 0.1zM869.1 564z m4.9-3.9l-0.1 0.1s0.1 0 0.1-0.1z m21.9-45.2c0-1 0.1-1.9 0.1-2.9s0-2-0.1-2.9c0 1 0.1 1.9 0.1 2.9s-0.1 2-0.1 2.9z m-17.7 41.2l0.3-0.3-0.3 0.3z m1.6-1.7l0.1-0.1-0.1 0.1z m2.3-2.8c0.2-0.2 0.3-0.4 0.5-0.6-0.2 0.2-0.3 0.4-0.5 0.6z m1.5-1.9c0.2-0.2 0.4-0.5 0.5-0.7-0.2 0.2-0.3 0.4-0.5 0.7z m2-2.9c0.2-0.3 0.4-0.6 0.6-1-0.2 0.4-0.4 0.7-0.6 1z m1.3-2.2c0.3-0.4 0.5-0.9 0.8-1.4-0.2 0.5-0.5 1-0.8 1.4z m1.7-2.9c0.2-0.5 0.5-0.9 0.7-1.4-0.2 0.5-0.5 0.9-0.7 1.4z m1.2-2.5c0.3-0.6 0.6-1.3 0.9-1.9-0.3 0.7-0.6 1.3-0.9 1.9z m1.3-2.9c0.2-0.6 0.5-1.2 0.7-1.8-0.2 0.6-0.4 1.2-0.7 1.8z m1.1-2.8c0.3-0.8 0.5-1.6 0.8-2.4-0.3 0.9-0.5 1.7-0.8 2.4z m1-2.9c0.2-0.7 0.4-1.5 0.6-2.2-0.2 0.8-0.4 1.5-0.6 2.2z m0.8-3c0.2-0.9 0.4-1.8 0.6-2.6-0.2 0.8-0.4 1.7-0.6 2.6z m0.7-2.9c0.2-0.8 0.3-1.7 0.5-2.5-0.2 0.8-0.4 1.6-0.5 2.5z m0.5-3.3c0.1-0.9 0.3-1.9 0.4-2.8-0.1 0.9-0.2 1.8-0.4 2.8z m0.4-3c0.1-0.9 0.2-1.9 0.2-2.8 0 1-0.1 1.9-0.2 2.8z m-17.4-50.5l0.3 0.3-0.3-0.3z m1.6 1.7l0.1 0.1-0.1-0.1z m2.3 2.8c0.2 0.2 0.3 0.4 0.5 0.6-0.2-0.2-0.3-0.4-0.5-0.6z m1.5 1.9c0.2 0.2 0.4 0.5 0.5 0.7-0.2-0.2-0.3-0.4-0.5-0.7z m2 2.9c0.2 0.3 0.4 0.7 0.6 1-0.2-0.4-0.4-0.7-0.6-1z m1.3 2.2c0.3 0.4 0.5 0.9 0.8 1.4-0.2-0.5-0.5-1-0.8-1.4z m1.7 2.9c0.2 0.5 0.5 0.9 0.7 1.4-0.2-0.5-0.5-0.9-0.7-1.4z m1.2 2.5c0.3 0.6 0.6 1.3 0.9 1.9-0.3-0.7-0.6-1.3-0.9-1.9z m1.3 2.9c0.2 0.6 0.5 1.2 0.7 1.8-0.2-0.6-0.4-1.2-0.7-1.8z m1.1 2.8c0.3 0.8 0.5 1.6 0.8 2.4-0.3-0.9-0.5-1.7-0.8-2.4z m1 2.9c0.2 0.7 0.4 1.5 0.6 2.2-0.2-0.8-0.4-1.5-0.6-2.2z m0.8 3c0.2 0.9 0.4 1.8 0.6 2.6-0.2-0.8-0.4-1.7-0.6-2.6z m0.7 2.9c0.2 0.8 0.3 1.7 0.5 2.5-0.2-0.8-0.4-1.6-0.5-2.5z m0.5 3.3c0.1 0.9 0.3 1.9 0.4 2.8-0.1-0.9-0.2-1.8-0.4-2.8z m0.4 3c0.1 0.9 0.2 1.9 0.2 2.8 0-1-0.1-1.9-0.2-2.8z" fill="#47444F" p-id="9409"></path></svg> <?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1728286912539" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9408" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M960 512c0-59.5-40.9-109.7-96.1-124v68.6c-9.4-5.5-20.4-8.6-32-8.6-35.3 0-64.1 28.7-64.1 64s28.7 64 64.1 64c11.7 0 22.6-3.1 32-8.6V636c-10.2 2.6-21 4.1-32 4.1-70.7 0-128.1-57.4-128.1-128s57.5-128 128.1-128c11.1 0 21.8 1.4 32 4V192c0-52.9-41.8-96-93.1-96H221.2c-51.3 0-93.1 43.1-93.1 96v159.5h128.1v64H128.1V352H64v64h64.1v191.5h128.1v64H127.1V608H64v64h64.1v160c0 52.9 41.8 96 93.1 96h549.6c51.3 0 93.1-43.1 93.1-96V635.9C919.1 621.7 960 571.5 960 512zM320.3 288h384.4v64H320.3v-64z m0 192h318.4v64H320.3v-64z m384.4 256H320.3v-64h384.4v64z m164.4-276z m4.9 3.9l-0.1-0.1s0.1 0 0.1 0.1zM869.1 564z m4.9-3.9l-0.1 0.1s0.1 0 0.1-0.1z m21.9-45.2c0-1 0.1-1.9 0.1-2.9s0-2-0.1-2.9c0 1 0.1 1.9 0.1 2.9s-0.1 2-0.1 2.9z m-17.7 41.2l0.3-0.3-0.3 0.3z m1.6-1.7l0.1-0.1-0.1 0.1z m2.3-2.8c0.2-0.2 0.3-0.4 0.5-0.6-0.2 0.2-0.3 0.4-0.5 0.6z m1.5-1.9c0.2-0.2 0.4-0.5 0.5-0.7-0.2 0.2-0.3 0.4-0.5 0.7z m2-2.9c0.2-0.3 0.4-0.6 0.6-1-0.2 0.4-0.4 0.7-0.6 1z m1.3-2.2c0.3-0.4 0.5-0.9 0.8-1.4-0.2 0.5-0.5 1-0.8 1.4z m1.7-2.9c0.2-0.5 0.5-0.9 0.7-1.4-0.2 0.5-0.5 0.9-0.7 1.4z m1.2-2.5c0.3-0.6 0.6-1.3 0.9-1.9-0.3 0.7-0.6 1.3-0.9 1.9z m1.3-2.9c0.2-0.6 0.5-1.2 0.7-1.8-0.2 0.6-0.4 1.2-0.7 1.8z m1.1-2.8c0.3-0.8 0.5-1.6 0.8-2.4-0.3 0.9-0.5 1.7-0.8 2.4z m1-2.9c0.2-0.7 0.4-1.5 0.6-2.2-0.2 0.8-0.4 1.5-0.6 2.2z m0.8-3c0.2-0.9 0.4-1.8 0.6-2.6-0.2 0.8-0.4 1.7-0.6 2.6z m0.7-2.9c0.2-0.8 0.3-1.7 0.5-2.5-0.2 0.8-0.4 1.6-0.5 2.5z m0.5-3.3c0.1-0.9 0.3-1.9 0.4-2.8-0.1 0.9-0.2 1.8-0.4 2.8z m0.4-3c0.1-0.9 0.2-1.9 0.2-2.8 0 1-0.1 1.9-0.2 2.8z m-17.4-50.5l0.3 0.3-0.3-0.3z m1.6 1.7l0.1 0.1-0.1-0.1z m2.3 2.8c0.2 0.2 0.3 0.4 0.5 0.6-0.2-0.2-0.3-0.4-0.5-0.6z m1.5 1.9c0.2 0.2 0.4 0.5 0.5 0.7-0.2-0.2-0.3-0.4-0.5-0.7z m2 2.9c0.2 0.3 0.4 0.7 0.6 1-0.2-0.4-0.4-0.7-0.6-1z m1.3 2.2c0.3 0.4 0.5 0.9 0.8 1.4-0.2-0.5-0.5-1-0.8-1.4z m1.7 2.9c0.2 0.5 0.5 0.9 0.7 1.4-0.2-0.5-0.5-0.9-0.7-1.4z m1.2 2.5c0.3 0.6 0.6 1.3 0.9 1.9-0.3-0.7-0.6-1.3-0.9-1.9z m1.3 2.9c0.2 0.6 0.5 1.2 0.7 1.8-0.2-0.6-0.4-1.2-0.7-1.8z m1.1 2.8c0.3 0.8 0.5 1.6 0.8 2.4-0.3-0.9-0.5-1.7-0.8-2.4z m1 2.9c0.2 0.7 0.4 1.5 0.6 2.2-0.2-0.8-0.4-1.5-0.6-2.2z m0.8 3c0.2 0.9 0.4 1.8 0.6 2.6-0.2-0.8-0.4-1.7-0.6-2.6z m0.7 2.9c0.2 0.8 0.3 1.7 0.5 2.5-0.2-0.8-0.4-1.6-0.5-2.5z m0.5 3.3c0.1 0.9 0.3 1.9 0.4 2.8-0.1-0.9-0.2-1.8-0.4-2.8z m0.4 3c0.1 0.9 0.2 1.9 0.2 2.8 0-1-0.1-1.9-0.2-2.8z" fill="" p-id="9409"></path></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761638922713" class="icon" viewBox="0 0 1080 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7439" xmlns:xlink="http://www.w3.org/1999/xlink" width="210.9375" height="200"><path d="M941.34455136 32H93.25188261A82.58521594 82.58521594 0 0 0 10.66666667 114.58521594v603.15623906a82.58521594 82.58521594 0 0 0 82.58521594 82.58521594H941.34455136a82.58521594 82.58521594 0 0 0 82.58521687-82.58521594V114.58521594A82.58521594 82.58521594 0 0 0 941.34455136 32zM533.05004417 684.347135c-139.89814781 0-253.31577562-111.66120937-253.31577562-249.47850469s113.41762687-249.47850469 253.31577562-249.47850562 253.31577562 111.69477094 253.31577562 249.47850562-113.40643969 249.47850469-253.31577562 249.47850469z" p-id="7440"></path><path d="M457.72543573 283.66004375a75.83922844 75.83922844 0 1 1-75.82804125 75.8392275 75.82804031 75.82804031 0 0 1 75.82804125-75.8392275zM157.24367886 913.68836187h712.63590937c18.88429219 0 34.19981156 13.72691156 34.19981063 30.65341219v17.00481282c0 16.92650156-15.31551937 30.65341312-34.19981063 30.65341312h-712.63590937c-18.88429219 0-34.19981156-13.72691156-34.19981156-30.65341313v-17.02718718c0-16.92650156 15.30433125-30.63103781 34.19981156-30.63103782z" p-id="7441"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761638996401" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9280" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M960 1024H69.487A69.487 69.487 0 0 1 0 954.513V577.5a5.487 5.487 0 0 1 0.738-2.75l126.525-219.175a5.487 5.487 0 0 0 0.737-2.75V69.488A69.487 69.487 0 0 1 197.5 0h504.225a5.487 5.487 0 0 1 3.888 1.613l188.75 188.75a5.487 5.487 0 0 1 1.612 3.887v188.088a5.487 5.487 0 0 0 0.925 3.05l126.175 189.225a5.487 5.487 0 0 1 0.925 3.05V960a64 64 0 0 1-64 64zM117.975 496.987l-48.112 70.425A5.487 5.487 0 0 0 74.4 576h48.1a5.487 5.487 0 0 0 5.487-5.488v-70.437a5.487 5.487 0 0 0-10.012-3.088zM704 122.5a69.5 69.5 0 0 0 69.5 69.5 5.487 5.487 0 0 0 3.888-9.375l-64.013-64A5.487 5.487 0 0 0 704 122.5zM826.5 256H709.487A69.487 69.487 0 0 1 640 186.512V69.487A5.487 5.487 0 0 0 634.513 64H261.487A69.487 69.487 0 0 0 192 133.487v437.025a5.487 5.487 0 0 0 5.5 5.488h117.1a5.475 5.475 0 0 1 5.475 5.325 192 192 0 0 0 383.85 0A5.475 5.475 0 0 1 709.4 576h117.1a5.487 5.487 0 0 0 5.488-5.487V261.487a5.487 5.487 0 0 0-5.475-5.487zM896 525.262v45.25a5.487 5.487 0 0 0 5.487 5.488h45.25a5.487 5.487 0 0 0 3.888-9.375l-45.25-45.25a5.487 5.487 0 0 0-9.375 3.888z m-570.513-141.25h373.025A5.487 5.487 0 0 1 704 389.5v53a5.487 5.487 0 0 1-5.488 5.487H325.488A5.487 5.487 0 0 1 320 442.5v-53.013a5.487 5.487 0 0 1 5.487-5.487z m0-128h245a5.487 5.487 0 0 1 5.488 5.488v53.013a5.487 5.487 0 0 1-5.488 5.487h-245a5.487 5.487 0 0 1-5.487-5.487v-53.025a5.487 5.487 0 0 1 5.487-5.488z m-5.487 0" p-id="9281"></path></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,12 +1,21 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { import {
IAlterLog,
IBlog, IBlogCategory, IBlogComment, IBlog, IBlogCategory, IBlogComment,
IBlogLatest, IBlogNestedComment, IBlogStats, INewBlog, IQueryBlog IBlogLatest, IBlogNestedComment, IBlogStats, INewBlog, IQueryBlog
} from '@/types/blog' } from '@/types/blog'
import { import {
addBlogApi, addBlogCommentApi, deleteBlogApi, addBlogApi,
queryBlogApi, queryBlogByIdApi, addBlogCommentApi,
queryBlogCategoryApi, queryBlogCommentApi, queryBlogStatsApi, queryLatestBlogApi, updateBlogApi deleteBlogApi, queryBlogAlterApi,
queryBlogApi,
queryBlogByIdApi,
queryBlogCategoryApi,
queryBlogCommentApi,
queryBlogStatsApi,
queryLatestBlogApi,
queryUnapprovedBlogApi,
updateBlogApi
} from '@/apis/blog.ts' } from '@/apis/blog.ts'
import { IHandleApi } from 'vue3-common/types' import { IHandleApi } from 'vue3-common/types'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
@@ -16,8 +25,10 @@ import { flattenToTwoLevel } from '@/utils/blog/blogUtil.ts'
export const useBlogStore = defineStore('blog', { export const useBlogStore = defineStore('blog', {
state: () => ({ state: () => ({
blogList: [] as IBlog[], blogList: [] as IBlog[],
unapprovedBlogList: [] as IBlog[],
blogCommentList: [] as IBlogComment[], blogCommentList: [] as IBlogComment[],
blogNestedCommentList: [] as IBlogNestedComment[], blogNestedCommentList: [] as IBlogNestedComment[],
alterLogList: [] as IAlterLog[],
currentBlogComment: { currentBlogComment: {
id: 0, id: 0,
blogId: 0, blogId: 0,
@@ -57,7 +68,9 @@ export const useBlogStore = defineStore('blog', {
blogCategoryList: [] as IBlogCategory[], blogCategoryList: [] as IBlogCategory[],
blogStats: {} as IBlogStats, blogStats: {} as IBlogStats,
latestBlogList: [] as IBlogLatest[], latestBlogList: [] as IBlogLatest[],
queryBlog: {} as IQueryBlog, queryBlog: {
category: ''
} as IQueryBlog,
blogImagePath: ImagePathEnum.BLOG, blogImagePath: ImagePathEnum.BLOG,
blogApiType: 'ADD' as IHandleApi, blogApiType: 'ADD' as IHandleApi,
pageInfo: { pageInfo: {
@@ -66,15 +79,22 @@ export const useBlogStore = defineStore('blog', {
totalSize: 1 totalSize: 1
}, },
isScrollEnd: false, isScrollEnd: false,
progressValue: '0%' progressValue: '0%',
isRefresh: false
}), }),
actions: { actions: {
async queryBlogList() { async queryBlogList() {
const { currentPage, pageSize } = this.pageInfo const { currentPage, pageSize } = this.pageInfo
const result = await queryBlogApi(currentPage, pageSize) const result = await queryBlogApi(currentPage, pageSize, this.queryBlog.category)
this.blogList = result.records this.blogList = result.records
this.pageInfo.totalSize = result.total this.pageInfo.totalSize = result.total
}, },
async queryUnapprovedBlogList() {
this.unapprovedBlogList = await queryUnapprovedBlogApi()
},
async queryBlogAlterLogList() {
this.alterLogList = await queryBlogAlterApi('puppy-service')
},
async queryBlogById(id: number) { async queryBlogById(id: number) {
this.currentBlog = await queryBlogByIdApi(id) this.currentBlog = await queryBlogByIdApi(id)
await this.queryBlogComment(this.currentBlogId) await this.queryBlogComment(this.currentBlogId)

View File

@@ -194,9 +194,9 @@ $transition-time: 0.28s;
.card-item { .card-item {
background-color: #FFFFFF; background-color: #FFFFFF;
border: 1px solid #FFFFFF; border: 1px solid #CFCFCF;
border-radius: 10px; border-radius: 10px;
box-shadow: 5px 5px 5px #CFCFCF; // box-shadow: 1px 1px 2px #CFCFCF;
} }
} }

View File

@@ -84,3 +84,20 @@ export interface IBlogNestedComment {
comment: IBlogComment; comment: IBlogComment;
replies: IBlogComment[]; replies: IBlogComment[];
} }
export interface IBlogOverview {
blogCount: number;
categoryCount: number;
wordCount: number;
greatCount: number;
visitCount: number;
}
export interface IAlterLog {
alterName: string;
alterTime: string;
logTime: string;
logTopic: string;
logMessage: string;
logStatus: string;
}

View File

@@ -4,6 +4,14 @@
<el-button type="primary" @click="addNewBlogClick">新增文章</el-button> <el-button type="primary" @click="addNewBlogClick">新增文章</el-button>
</div> </div>
<el-form :inline="true" :model="blogStore.queryBlog">
<el-form-item label="类别" style="margin-bottom: 0;">
<el-input v-model="blogStore.queryBlog.category"
placeholder="请输入博客类别" clearable
@input="changeCategoryClick"/>
</el-form-item>
</el-form>
<el-table <el-table
:data="blogStore.blogList" :data="blogStore.blogList"
border stripe border stripe
@@ -78,6 +86,10 @@ const changePageClick = async (page: number, pageSize: number) => {
await blogStore.queryBlogList() await blogStore.queryBlogList()
} }
const changeCategoryClick = async () => {
await blogStore.queryBlogList()
}
/** /**
* 点击新增博客按钮 * 点击新增博客按钮
*/ */

View File

@@ -1,82 +0,0 @@
<template>
<div class="admin-module">
<div class="button-container">
<el-button type="primary" @click="addNewCategoryClick">新增类别</el-button>
</div>
<el-table
:data="blogStore.blogCategoryList"
border stripe
:header-cell-style="tableHeaderStyle()"
:cell-style="tableCellStyle()"
:row-style="tableRowStyle()"
style="width: 50%"
class="card-item">
<el-table-column type="index" align="center" width="50" />
<el-table-column prop="name" label="名称" align="center" min-width="10%"/>
<el-table-column prop="count" label="数量" align="center" min-width="10%" />
<el-table-column fixed="right" label="操作" align="center" width="200">
<template #default="scope">
<el-button type="primary" @click="editCategoryClick(scope.row)">编辑</el-button>
<el-button type="danger" @click="deleteCategoryClick(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script lang="ts" setup>
import { onMounted } from 'vue'
import { useBlogStore } from '@/store/blog.ts'
import { IBlogCategory } from '@/types/blog.ts'
import { ElMessageBox } from 'element-plus'
import { tableCellStyle, tableHeaderStyle, tableRowStyle } from 'vue3-common/utils/elUtil'
const blogStore = useBlogStore()
onMounted(async () => {
await blogStore.queryBlogCategory()
})
/**
* 点击新增类别按钮
*/
const addNewCategoryClick = () => {
ElMessageBox.prompt('请输入类别名称', '新增类别', {
confirmButtonText: '确认',
cancelButtonText: '取消',
inputPlaceholder: '请输入类别名称',
inputPattern: /^.{0,10}$/,
inputErrorMessage: '请输入10字以内的类别名称'
})
.then(({ value }) => {
console.log(value)
})
}
/**
* 点击编辑类别按钮
* @param category 类别
*/
const editCategoryClick = async (category: IBlogCategory) => {
ElMessageBox.prompt('请输入类别名称', '编辑类别', {
confirmButtonText: '确认',
cancelButtonText: '取消',
inputValue: category.name,
inputPlaceholder: '请输入类别名称',
inputPattern: /^.{0,10}$/,
inputErrorMessage: '请输入10字以内的类别名称'
})
.then(({ value }) => {
console.log(value)
})
}
/**
* 点击删除类别按钮
* @param category 类别
*/
const deleteCategoryClick = (category: IBlogCategory) => {
}
</script>

View File

@@ -0,0 +1,96 @@
<template>
<div class="admin-module">
<div class="button-container">
<el-button type="primary" @click="addNewBlogClick">新增文章</el-button>
</div>
<el-table
:data="blogStore.unapprovedBlogList"
border stripe
:header-cell-style="tableHeaderStyle()"
:cell-style="tableCellStyle()"
:row-style="tableRowStyle()"
class="card-item">
<el-table-column type="index" align="center" width="50" />
<el-table-column prop="title" label="标题" align="center" min-width="15%"/>
<el-table-column prop="category" label="类别" align="center" min-width="10%" />
<el-table-column label="精品" align="center" min-width="5%">
<template #default="scope">
<el-tag type="success" v-if="scope.row.isGreat"></el-tag>
<el-tag type="danger" v-else></el-tag>
</template>
</el-table-column>
<el-table-column label="置顶" align="center" min-width="5%">
<template #default="scope">
<el-tag type="success" v-if="scope.row.topValue > 1"></el-tag>
<el-tag type="danger" v-else></el-tag>
</template>
</el-table-column>
<el-table-column prop="wordCount" label="字数" align="center" min-width="10%" />
<el-table-column prop="visitCount" label="阅读次数" align="center" min-width="10%" />
<el-table-column label="是否发布" align="center" min-width="10%">
<template #default="scope">
<el-tag type="success" v-if="scope.row.isApproved">已发布</el-tag>
<el-tag type="danger" v-else>草稿</el-tag>
</template>
</el-table-column>
<el-table-column fixed="right" label="操作" align="center" width="300">
<template #default="scope">
<el-button type="primary" @click="previewBlogClick(scope.row)">预览</el-button>
<el-button type="primary" @click="editBlogClick(scope.row)">编辑</el-button>
<el-button type="danger" @click="deleteBlogClick(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script lang="ts" setup>
import { onMounted } from 'vue'
import { useBlogStore } from '@/store/blog.ts'
import { IBlog } from '@/types/blog.ts'
import { commonElMessageBox, tableCellStyle, tableHeaderStyle, tableRowStyle } from 'vue3-common/utils/elUtil'
import { useRouter } from 'vue-router'
const router = useRouter()
const blogStore = useBlogStore()
onMounted(async () => {
await blogStore.queryUnapprovedBlogList()
})
/**
* 点击新增博客按钮
*/
const addNewBlogClick = () => {
router.push({ name: 'BlogDetailId', params: { id: 0 } })
}
/**
* 点击预览博客按钮
* @param blog 博客
*/
const previewBlogClick = async (blog: IBlog) => {
await router.push({ name: 'BlogPreviewId', params: { id: blog.id } })
}
/**
* 点击编辑博客按钮
* @param blog 博客
*/
const editBlogClick = async (blog: IBlog) => {
await router.push({ name: 'BlogDetailId', params: { id: blog.id } })
}
/**
* 点击删除博客按钮
* @param blog 博客
*/
const deleteBlogClick = (blog: IBlog) => {
blogStore.blogApiType = 'DELETE'
commonElMessageBox('是否确认删除该博客?').then(() => {
blogStore.handleBlogApi(blog.id)
})
}
</script>

View File

@@ -1,13 +1,203 @@
<template> <template>
<div class="admin-module"> <div class="admin-module blog-dashboard">
Dashboard <div class="blog-overview">
<el-statistic class="card-item" title="博客总数" suffix="篇"
:value="blogDashboard.overview.blogCount"
/>
<el-statistic class="card-item" title="类别总数" suffix="个"
:value="blogDashboard.overview.categoryCount"/>
<el-statistic class="card-item" title="字数总计" suffix="字"
:value="blogDashboard.overview.wordCount" />
<el-statistic class="card-item" title="访问总计" suffix="人"
:value="blogDashboard.overview.visitCount" />
</div>
<div class="blog-chart">
<div class="chart-container card-item">
<div class="content">
<span class="title">📊每月博客发布统计</span>
</div>
<div class="chart" ref="approvedMonthlyChartRef"/>
</div>
<div class="chart-container card-item">
<div class="content">
<span class="title">📊每月博客访问统计</span>
</div>
<div class="chart" ref="visitMonthlyChartRef"/>
</div>
<div class="chart-container card-item">
<div class="content">
<span class="title">📊博客访问数量排行</span>
</div>
<div class="chart" ref="visitRankChartRef"/>
</div>
<div class="chart-container card-item">
<div class="content">
<span class="title">📊博客类别统计</span>
</div>
<div class="chart" ref="categoryChartRef"/>
</div>
<div class="chart-container card-item">
<div class="content">
<span class="title">📊博客阅读时长排行</span>
</div>
<div class="chart" ref="readRankChartRef"/>
</div>
<div class="chart-container card-item">
<div class="content">
<span class="title">📊博客评论数量排行</span>
</div>
<div class="chart" ref="commentRankChartRef"/>
</div>
</div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import * as echarts from 'echarts'
import { barChartOptions, lineChartOptions, pieChartOptions } from 'vue3-common/utils/eChartsUtil'
import { useAppStore } from '@/store/app.ts'
import {
queryApprovedMonthlyStatsApi, queryCategoryStatsApi, queryCommentRankStatsApi, queryOverviewStatsApi,
queryReadRankStatsApi,
queryVisitMonthlyStatsApi,
queryVisitRankStatsApi
} from '@/apis/stats'
import { IBlogOverview } from '@/types/blog'
const appStore = useAppStore()
const approvedMonthlyChartRef = ref<HTMLElement>()
const visitMonthlyChartRef = ref<HTMLElement>()
const visitRankChartRef = ref<HTMLElement>()
const categoryChartRef = ref<HTMLElement>()
const readRankChartRef = ref<HTMLElement>()
const commentRankChartRef = ref<HTMLElement>()
let approvedMonthlyChart: echarts.ECharts
let visitMonthlyChart: echarts.ECharts
let visitRankChart: echarts.ECharts
let categoryChart: echarts.ECharts
let readRankChart: echarts.ECharts
let commentRankChart: echarts.ECharts
const blogDashboard = reactive({
overview: {
blogCount: 0,
categoryCount: 0,
wordCount: 0,
greatCount: 0,
visitCount: 0
} as IBlogOverview
})
onMounted(() => {
approvedMonthlyChart = echarts.init(approvedMonthlyChartRef.value as HTMLElement, appStore.isDark ? 'dark' : null)
visitMonthlyChart = echarts.init(visitMonthlyChartRef.value as HTMLElement, appStore.isDark ? 'dark' : null)
visitRankChart = echarts.init(visitRankChartRef.value as HTMLElement, appStore.isDark ? 'dark' : null)
categoryChart = echarts.init(categoryChartRef.value as HTMLElement, appStore.isDark ? 'dark' : null)
readRankChart = echarts.init(readRankChartRef.value as HTMLElement, appStore.isDark ? 'dark' : null)
commentRankChart = echarts.init(commentRankChartRef.value as HTMLElement, appStore.isDark ? 'dark' : null)
updateChart()
})
const updateChart = async () => {
blogDashboard.overview = await queryOverviewStatsApi()
const approvedMonthlyData = await queryApprovedMonthlyStatsApi(2025)
const approvedXAxis = approvedMonthlyData.map((item) => item.name)
const approvedYAxis = approvedMonthlyData.map((item) => item.value)
approvedMonthlyChart.setOption(lineChartOptions('日期', approvedXAxis, '数量(篇)', approvedYAxis))
const visitMonthlyData = await queryVisitMonthlyStatsApi(2025)
const visitMonthlyXAxis = visitMonthlyData.map((item) => item.name)
const visitMonthlyYAxis = visitMonthlyData.map((item) => item.value)
visitMonthlyChart.setOption(lineChartOptions('日期', visitMonthlyXAxis, '访问量(人)', visitMonthlyYAxis))
const visitRankData = await queryVisitRankStatsApi()
const visitRankXAxis = visitRankData.map((item) => item.name)
const visitRankYAxis = visitRankData.map((item) => item.value)
visitRankChart.setOption(barChartOptions('名称', visitRankXAxis, '访问量(人)', visitRankYAxis))
const categoryData = await queryCategoryStatsApi()
categoryChart.setOption(pieChartOptions('类别', '篇', categoryData))
const readRankData = await queryReadRankStatsApi()
const readRankXAxis = readRankData.map((item) => item.name)
const readRankYAxis = readRankData.map((item) => item.value)
readRankChart.setOption(barChartOptions('名称', readRankXAxis, '时长(分钟)', readRankYAxis))
const commentRankData = await queryCommentRankStatsApi()
const commentRankXAxis = commentRankData.map((item) => item.name)
const commentRankYAxis = commentRankData.map((item) => item.value)
commentRankChart.setOption(barChartOptions('名称', commentRankXAxis, '评论量(个)', commentRankYAxis))
}
</script> </script>
<style scoped lang="scss"> <style lang="scss">
.blog-dashboard {
overflow: auto;
.blog-overview {
width: 100%;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
.el-statistic {
padding: 10px;
.el-statistic__head {
font-size: larger;
}
.el-statistic__number {
font-size: xx-large;
}
}
}
.blog-chart {
flex-grow: 1;
width: 100%;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
.chart-container {
display: grid;
grid-template-rows: 40px 1fr;
.content {
padding: 0 5px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #009FA8;
.title {
font-size: 1em;
font-weight: bold;
}
}
.chart {
height: 300px;
width: 100%;
}
}
}
}
</style> </style>

View File

@@ -21,9 +21,9 @@ export const metaList: IRouteMetaConfig = {
title: '文章管理', title: '文章管理',
icon: 'admin-blogArticle' icon: 'admin-blogArticle'
}, },
'/blog/category': { '/blog/unapproved': {
title: '类别管理', title: '草稿箱',
icon: 'admin-blogCategory' icon: 'admin-blogUnapproved'
}, },
'/blog/detail/:id': { '/blog/detail/:id': {
title: '', title: '',
@@ -37,12 +37,16 @@ export const metaList: IRouteMetaConfig = {
}, },
'/monitor': { '/monitor': {
title: '监控管理', title: '监控管理',
icon: 'admin-blogVisit', icon: 'admin-blogMonitor',
order: 3, order: 3,
redirect: '/monitor/visit' redirect: '/monitor/visit'
}, },
'/monitor/visit': { '/monitor/visit': {
title: '监控管理', title: '访问记录',
icon: '' icon: 'admin-blogVisit'
},
'/monitor/alter': {
title: '告警记录',
icon: 'admin-blogLog'
} }
} }

101
src/views/monitor/alter.vue Normal file
View File

@@ -0,0 +1,101 @@
<template>
<div class="admin-module">
<el-table
:data="blogStore.alterLogList"
border stripe
:span-method="handleSpanMethod"
:header-cell-style="tableHeaderStyle()"
:cell-style="tableCellStyle()"
:row-style="tableRowStyle()"
style="width: 100%"
class="card-item">
<el-table-column type="index" align="center" width="50" />
<el-table-column prop="alterTime" label="告警时间" align="center" width="150px" />
<el-table-column prop="logTime" label="日志时间" align="center" width="150px" />
<el-table-column prop="logTopic" label="日志话题" align="center" width="150px" />
<el-table-column prop="logMessage" label="日志消息" align="center"/>
<el-table-column prop="logStatus" label="告警状态" align="center" width="100px">
<template #default="scope">
<el-tag v-if="scope.row.logStatus === 'pending'" type="danger">待处理</el-tag>
<el-tag v-if="scope.row.logStatus === 'complete'" type="success">已完成</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200px" align="center">
<template #default="scope">
<el-button type="primary">下载</el-button>
<el-button type="primary" :disabled="scope.row.logStatus === 'pending'">
已处理
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script lang="ts" setup>
import { onMounted } from 'vue'
import { tableCellStyle, tableHeaderStyle, tableRowStyle } from 'vue3-common/utils/elUtil'
import { useBlogStore } from '@/store/blog'
const blogStore = useBlogStore()
onMounted(async () => {
await blogStore.queryBlogAlterLogList()
})
const handleSpanMethod = ({ row, column, rowIndex, columnIndex }) => {
const data = blogStore.alterLogList
// 合并规则:相同 alterTime + logStatus 的行合并
if (column.property === 'alterTime' || column.property === 'logStatus') {
const currentAlterTime = row.alterTime
const currentLogStatus = row.logStatus
// 检查是否和上一行的告警时间和状态相同
if (rowIndex > 0) {
const prevRow = data[rowIndex - 1]
if (prevRow.alterTime === currentAlterTime && prevRow.logStatus === currentLogStatus) {
return { rowspan: 0, colspan: 0 } // 隐藏当前单元格
}
}
// 计算连续相同 alterTime + logStatus 的行数
let rowspan = 1
for (let i = rowIndex + 1; i < data.length; i++) {
const nextRow = data[i]
if (nextRow.alterTime === currentAlterTime && nextRow.logStatus === currentLogStatus) {
rowspan++
} else {
break
}
}
return { rowspan, colspan: 1 }
}
// 合并操作列(最后一列)
if (columnIndex === 6) { // 操作列是第7列索引从0开始
const currentAlterTime = row.alterTime
const currentLogStatus = row.logStatus
// 检查是否和上一行的告警时间和状态相同
if (rowIndex > 0) {
const prevRow = data[rowIndex - 1]
if (prevRow.alterTime === currentAlterTime && prevRow.logStatus === currentLogStatus) {
return { rowspan: 0, colspan: 0 }
}
}
// 计算连续相同 alterTime + logStatus 的行数
let rowspan = 1
for (let i = rowIndex + 1; i < data.length; i++) {
const nextRow = data[i]
if (nextRow.alterTime === currentAlterTime && nextRow.logStatus === currentLogStatus) {
rowspan++
} else {
break
}
}
return { rowspan, colspan: 1 }
}
}
</script>

View File

@@ -40,6 +40,11 @@ export default defineConfig({
target: `http://${API_HOST}:8082`, target: `http://${API_HOST}:8082`,
changeOrigin: true, changeOrigin: true,
rewrite: (item) => item.replace(/^\/blog-api/, '') rewrite: (item) => item.replace(/^\/blog-api/, '')
},
'/alter-api': {
target: `http://${API_HOST}:8000`,
changeOrigin: true,
rewrite: (item) => item.replace(/^\/alter-api/, '')
} }
} }
} }