Compare commits

..

5 Commits

Author SHA1 Message Date
Leo
83dedd9e1b 配置:更新项目依赖和图片上传API
- 更新package.json依赖配置
- 优化App.vue应用入口配置
- 完善图片上传API接口
2025-10-15 21:04:05 +08:00
Leo
77378e395b 功能:新增非遗管理模块相关页面和API
- 新增传承人管理、新闻资讯管理、活动管理、统计分析等页面
- 新增非遗相关API接口定义和类型
- 新增IconPreloader图标预加载组件
- 完善非遗管理业务功能模块
2025-10-15 21:02:38 +08:00
Leo
fe4c0df494 优化:为非遗项目管理页面添加空状态处理功能
- 添加智能空状态提示信息
- 新增getEmptyTitle、getEmptyDescription等辅助函数
- 升级CoiEmpty组件配置,支持重置搜索和新增操作
- 搜索无结果时显示重置搜索按钮
- 完全无数据时显示新增项目按钮
- 提供友好的操作引导和用户体验
2025-10-15 21:01:42 +08:00
Leo
b5f6c04506 优化:为评论管理页面添加空状态处理功能
- 添加智能空状态提示信息
- 新增getEmptyTitle、getEmptyDescription等辅助函数
- 升级CoiEmpty组件配置,支持重置搜索和刷新操作
- 搜索无结果时显示重置搜索按钮
- 完全无数据时显示刷新数据按钮
- 统一空状态交互体验
2025-10-15 21:00:56 +08:00
Leo
019234162f 优化:为前台用户管理页面添加空状态处理功能
- 添加智能空状态提示信息
- 新增getEmptyTitle、getEmptyDescription等辅助函数
- 升级CoiEmpty组件配置,支持重置搜索和新增操作
- 搜索无结果时显示重置搜索按钮
- 完全无数据时显示新增用户按钮
- 提升用户体验和操作引导
2025-10-15 21:00:11 +08:00
25 changed files with 9612 additions and 4 deletions

View File

@ -53,6 +53,7 @@
"@vueuse/core": "^13.3.0",
"alova": "^3.3.2",
"colord": "^2.9.3",
"echarts": "6.0.0",
"pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.3.0",
"radash": "^12.1.0",

View File

@ -4,6 +4,7 @@
:locale="naiveLocale.locale" :date-locale="naiveLocale.dateLocale" :theme-overrides="appStore.theme"
>
<naive-provider>
<IconPreloader />
<router-view />
</naive-provider>
</n-config-provider>

View File

@ -0,0 +1,24 @@
<template>
<!-- 图标预加载组件 - 确保所有图标都被注册 -->
<div style="display: none;">
<icon-park-outline:palace />
<icon-park-outline:peoples />
<icon-park-outline:user />
<icon-park-outline:calendar />
<icon-park-outline:file-text />
<icon-park-outline:comment />
<icon-park-outline:like />
<icon-park-outline:star />
<icon-park-outline:search />
<icon-park-outline:refresh />
<icon-park-outline:check />
<icon-park-outline:close />
<icon-park-outline:delete />
<icon-park-outline:preview-open />
</div>
</template>
<script setup lang="ts">
// unplugin-vue-components
// ,
</script>

View File

@ -0,0 +1,68 @@
import { request } from '../../../http'
import type {
CommentAdminBo,
CommentAdminDetailVo,
CommentAdminQueryBo,
PageCommentAdminVo,
} from './types'
// 重新导出类型供外部使用
export type {
CommentAdminBo,
CommentAdminDetailVo,
CommentAdminQueryBo,
CommentAdminSearchForm,
CommentAdminVo,
PageCommentAdminVo,
} from './types'
/**
*
*/
export function getCommentAdminList(params: CommentAdminQueryBo) {
return request.Get<Service.ResponseResult<PageCommentAdminVo>>('/coder/admin/comment/listPage', { params })
}
/**
*
*/
export function getCommentAdminDetail(id: string) {
return request.Get<Service.ResponseResult<CommentAdminDetailVo>>(`/coder/admin/comment/detail/${id}`)
}
/**
*
*/
export function auditComment(data: CommentAdminBo) {
return request.Put<Service.ResponseResult<boolean>>('/coder/admin/comment/audit', data)
}
/**
*
*/
export function batchAuditComments(ids: string[], status: number) {
return request.Put<Service.ResponseResult<boolean>>('/coder/admin/comment/batchAudit', ids, {
params: { status },
})
}
/**
*
*/
export function deleteComment(id: string) {
return request.Delete<Service.ResponseResult<boolean>>(`/coder/admin/comment/delete/${id}`)
}
/**
*
*/
export function batchDeleteComments(ids: string[]) {
return request.Delete<Service.ResponseResult<boolean>>('/coder/admin/comment/batchDelete', { data: ids })
}
/**
*
*/
export function getPendingCommentCount() {
return request.Get<Service.ResponseResult<number>>('/coder/admin/comment/pendingCount')
}

View File

@ -0,0 +1,89 @@
/**
*
*/
export interface CommentAdminQueryBo {
pageNum?: number
pageSize?: number
userId?: string
targetType?: string
targetId?: string
content?: string
status?: number
startTime?: string
endTime?: string
userKeyword?: string
}
/**
* VO
*/
export interface CommentAdminVo {
id: string
userId: string
username: string
nickname: string
avatar?: string
targetType: string
targetId: string
targetTitle: string
content: string
rating?: number
parentId: string
likeCount: number
status: number
createTime: string
updateTime: string
}
/**
* VO
*/
export interface CommentAdminDetailVo {
id: string
userId: string
username: string
nickname: string
avatar?: string
targetType: string
targetId: string
targetTitle: string
content: string
rating?: number
parentId: string
likeCount: number
status: number
createTime: string
updateTime: string
}
/**
* BO
*/
export interface CommentAdminBo {
id: string
status: number
remark?: string
}
/**
*
*/
export interface PageCommentAdminVo {
records: CommentAdminVo[]
total: number
size: number
current: number
pages: number
}
/**
*
*/
export interface CommentAdminSearchForm {
userId?: string
targetType?: string | ''
targetId?: string
content?: string | ''
status?: number | null
userKeyword?: string | ''
}

View File

@ -0,0 +1,77 @@
import { request } from '../../../http'
import type {
EventAdminBo,
EventAdminDetailVo,
EventAdminQueryBo,
PageEventAdminVo,
} from './types'
// 重新导出类型供外部使用
export type {
EventAdminBo,
EventAdminDetailVo,
EventAdminQueryBo,
EventAdminSearchForm,
EventAdminVo,
PageEventAdminVo,
} from './types'
/**
*
*/
export function getEventAdminList(params: EventAdminQueryBo) {
return request.Get<Service.ResponseResult<PageEventAdminVo>>('/coder/admin/event/listPage', { params })
}
/**
*
*/
export function getEventAdminDetail(id: string) {
return request.Get<Service.ResponseResult<EventAdminDetailVo>>(`/coder/admin/event/detail/${id}`)
}
/**
*
*/
export function addEventAdmin(data: EventAdminBo) {
return request.Post<Service.ResponseResult<string>>('/coder/admin/event/add', data)
}
/**
*
*/
export function updateEventAdmin(data: EventAdminBo) {
return request.Put<Service.ResponseResult<boolean>>('/coder/admin/event/edit', data)
}
/**
*
*/
export function deleteEventAdmin(id: string) {
return request.Delete<Service.ResponseResult<boolean>>(`/coder/admin/event/delete/${id}`)
}
/**
*
*/
export function batchDeleteEventAdmin(ids: string[]) {
return request.Delete<Service.ResponseResult<boolean>>('/coder/admin/event/batchDelete', { data: ids })
}
/**
*
*/
export function changeEventPublishStatus(id: string, publishStatus: number) {
return request.Put<Service.ResponseResult<boolean>>('/coder/admin/event/changePublishStatus', null, {
params: { id, publishStatus },
})
}
/**
*
*/
export function changeEventStatus(id: string, status: string) {
return request.Put<Service.ResponseResult<boolean>>('/coder/admin/event/changeEventStatus', null, {
params: { id, status },
})
}

View File

@ -0,0 +1,106 @@
/**
*
*/
export interface EventAdminQueryBo {
pageNum?: number
pageSize?: number
title?: string
location?: string
status?: string
keyword?: string
publishStatus?: number
startTimeBegin?: string
startTimeEnd?: string
sortField?: string
sortOrder?: string
}
/**
* VO
*/
export interface EventAdminVo {
id: string
title: string
summary?: string
coverImage?: string
location?: string
startTime?: string
endTime?: string
maxParticipants?: number
currentParticipants?: number
registrationStart?: string
registrationEnd?: string
status: string
viewCount?: number
publishStatus: number
createBy?: string
createTime?: string
updateBy?: string
updateTime?: string
}
/**
* VO
*/
export interface EventAdminDetailVo {
id: string
title: string
summary?: string
content?: string
coverImage?: string
location?: string
startTime?: string
endTime?: string
maxParticipants?: number
currentParticipants?: number
registrationStart?: string
registrationEnd?: string
status: string
viewCount?: number
publishStatus: number
createBy?: string
createTime?: string
updateBy?: string
updateTime?: string
}
/**
* BO/
*/
export interface EventAdminBo {
id?: string
title: string
summary?: string
content?: string
coverImage?: string
location?: string
startTime?: string
endTime?: string
maxParticipants?: number
registrationStart?: string
registrationEnd?: string
status?: string
publishStatus?: number
}
/**
*
*/
export interface PageEventAdminVo {
records: EventAdminVo[]
total: number
size: number
current: number
pages: number
}
/**
*
*/
export interface EventAdminSearchForm {
title?: string
location?: string
status?: string
keyword?: string
publishStatus?: number
}

View File

@ -0,0 +1,77 @@
import { request } from '../../../http'
import type {
InheritorBo,
InheritorDetailVo,
InheritorQueryBo,
PageInheritorVo,
} from './types'
// 重新导出类型供外部使用
export type {
InheritorBo,
InheritorDetailVo,
InheritorQueryBo,
InheritorSearchForm,
InheritorVo,
PageInheritorVo,
} from './types'
/**
*
*/
export function getInheritorList(params: InheritorQueryBo) {
return request.Get<Service.ResponseResult<PageInheritorVo>>('/coder/admin/inheritor/listPage', { params })
}
/**
*
*/
export function getInheritorDetail(id: string) {
return request.Get<Service.ResponseResult<InheritorDetailVo>>(`/coder/admin/inheritor/detail/${id}`)
}
/**
*
*/
export function addInheritor(data: InheritorBo) {
return request.Post<Service.ResponseResult<string>>('/coder/admin/inheritor/add', data)
}
/**
*
*/
export function updateInheritor(data: InheritorBo) {
return request.Put<Service.ResponseResult<boolean>>('/coder/admin/inheritor/edit', data)
}
/**
*
*/
export function deleteInheritor(id: string) {
return request.Delete<Service.ResponseResult<boolean>>(`/coder/admin/inheritor/delete/${id}`)
}
/**
*
*/
export function batchDeleteInheritors(ids: string[]) {
return request.Delete<Service.ResponseResult<boolean>>('/coder/admin/inheritor/batchDelete', { data: ids })
}
/**
*
*/
export function changePublishStatus(id: string, publishStatus: number) {
return request.Put<Service.ResponseResult<boolean>>('/coder/admin/inheritor/changePublishStatus', null, {
params: { id, publishStatus },
})
}
/**
*
*/
export function setFeatured(id: string, isFeatured: number) {
return request.Put<Service.ResponseResult<boolean>>('/coder/admin/inheritor/setFeatured', null, {
params: { id, isFeatured },
})
}

View File

@ -0,0 +1,133 @@
/**
*
*/
export interface InheritorQueryBo {
pageNum?: number
pageSize?: number
name?: string
nameEn?: string
gender?: number
heritageId?: string
heritageName?: string
level?: string
province?: string
city?: string
keyword?: string
publishStatus?: number
isFeatured?: number
sortField?: string
sortOrder?: string
}
/**
* VO
*/
export interface InheritorVo {
id: string
name: string
nameEn?: string
gender?: number
birthYear?: number
avatar?: string
heritageId?: string
heritageName?: string
level?: string
province?: string
city?: string
introduction?: string
viewCount?: number
likeCount?: number
isFeatured: number
sortOrder?: number
publishStatus: number
createBy?: string
createTime?: string
updateBy?: string
updateTime?: string
}
/**
* VO
*/
export interface InheritorDetailVo {
id: string
name: string
nameEn?: string
gender?: number
birthYear?: number
avatar?: string
heritageId?: string
heritageName?: string
level?: string
province?: string
city?: string
introduction?: string
story?: string
achievements?: string
works?: string
images?: string
videoUrl?: string
viewCount?: number
likeCount?: number
isFeatured: number
sortOrder?: number
publishStatus: number
createBy?: string
createTime?: string
updateBy?: string
updateTime?: string
}
/**
* BO/
*/
export interface InheritorBo {
id?: string
name: string
nameEn?: string
gender?: number
birthYear?: number
avatar?: string
heritageId?: string
heritageName?: string
level?: string
province?: string
city?: string
introduction?: string
story?: string
achievements?: string
works?: string
images?: string
videoUrl?: string
isFeatured?: number
sortOrder?: number
publishStatus?: number
}
/**
*
*/
export interface PageInheritorVo {
records: InheritorVo[]
total: number
size: number
current: number
pages: number
}
/**
*
*/
export interface InheritorSearchForm {
name?: string
nameEn?: string
gender?: number
heritageId?: string
heritageName?: string
level?: string
province?: string
city?: string
keyword?: string
publishStatus?: number
isFeatured?: number
}

View File

@ -0,0 +1,77 @@
import { request } from '../../../http'
import type {
HeritageItemBo,
HeritageItemDetailVo,
HeritageItemQueryBo,
PageHeritageItemVo,
} from './types'
// 重新导出类型供外部使用
export type {
HeritageItemBo,
HeritageItemDetailVo,
HeritageItemQueryBo,
HeritageItemSearchForm,
HeritageItemVo,
PageHeritageItemVo,
} from './types'
/**
*
*/
export function getHeritageItemList(params: HeritageItemQueryBo) {
return request.Get<Service.ResponseResult<PageHeritageItemVo>>('/coder/admin/heritage/listPage', { params })
}
/**
*
*/
export function getHeritageItemDetail(id: string) {
return request.Get<Service.ResponseResult<HeritageItemDetailVo>>(`/coder/admin/heritage/detail/${id}`)
}
/**
*
*/
export function addHeritageItem(data: HeritageItemBo) {
return request.Post<Service.ResponseResult<string>>('/coder/admin/heritage/add', data)
}
/**
*
*/
export function updateHeritageItem(data: HeritageItemBo) {
return request.Put<Service.ResponseResult<boolean>>('/coder/admin/heritage/edit', data)
}
/**
*
*/
export function deleteHeritageItem(id: string) {
return request.Delete<Service.ResponseResult<boolean>>(`/coder/admin/heritage/delete/${id}`)
}
/**
*
*/
export function batchDeleteHeritageItems(ids: string[]) {
return request.Delete<Service.ResponseResult<boolean>>('/coder/admin/heritage/batchDelete', { data: ids })
}
/**
*
*/
export function changePublishStatus(id: string, publishStatus: number) {
return request.Put<Service.ResponseResult<boolean>>('/coder/admin/heritage/changePublishStatus', null, {
params: { id, publishStatus },
})
}
/**
*
*/
export function setFeatured(id: string, isFeatured: number) {
return request.Put<Service.ResponseResult<boolean>>('/coder/admin/heritage/setFeatured', null, {
params: { id, isFeatured },
})
}

View File

@ -0,0 +1,134 @@
/**
*
*/
export interface HeritageItemQueryBo {
pageNum?: number
pageSize?: number
name?: string
nameEn?: string
category?: string
level?: string
province?: string
city?: string
status?: string
tag?: string
keyword?: string
publishStatus?: number
isFeatured?: number
sortField?: string
sortOrder?: string
}
/**
* VO
*/
export interface HeritageItemVo {
id: string
name: string
nameEn?: string
category: string
level: string
province?: string
city?: string
description?: string
coverImage?: string
tags?: string
status: string
viewCount?: number
likeCount?: number
favoriteCount?: number
commentCount?: number
isFeatured: number
sortOrder?: number
publishStatus: number
createBy?: string
createTime?: string
updateBy?: string
updateTime?: string
}
/**
* VO
*/
export interface HeritageItemDetailVo {
id: string
name: string
nameEn?: string
category: string
level: string
province?: string
city?: string
description?: string
history?: string
skills?: string
significance?: string
coverImage?: string
images?: string
videoUrl?: string
tags?: string
status: string
viewCount?: number
likeCount?: number
favoriteCount?: number
commentCount?: number
isFeatured: number
sortOrder?: number
publishStatus: number
createBy?: string
createTime?: string
updateBy?: string
updateTime?: string
}
/**
* BO/
*/
export interface HeritageItemBo {
id?: string
name: string
nameEn?: string
category: string
level: string
province?: string
city?: string
description?: string
history?: string
skills?: string
significance?: string
coverImage?: string
images?: string
videoUrl?: string
tags?: string
status?: string
isFeatured?: number
sortOrder?: number
publishStatus?: number
}
/**
*
*/
export interface PageHeritageItemVo {
records: HeritageItemVo[]
total: number
size: number
current: number
pages: number
}
/**
*
*/
export interface HeritageItemSearchForm {
name?: string
nameEn?: string
category?: string
level?: string
province?: string
city?: string
status?: string
tag?: string
keyword?: string
publishStatus?: number
isFeatured?: number
}

View File

@ -0,0 +1,77 @@
import { request } from '../../../http'
import type {
NewsBo,
NewsDetailVo,
NewsQueryBo,
PageNewsVo,
} from './types'
// 重新导出类型供外部使用
export type {
NewsBo,
NewsDetailVo,
NewsQueryBo,
NewsSearchForm,
NewsVo,
PageNewsVo,
} from './types'
/**
*
*/
export function getNewsList(params: NewsQueryBo) {
return request.Get<Service.ResponseResult<PageNewsVo>>('/coder/admin/news/listPage', { params })
}
/**
*
*/
export function getNewsDetail(id: string) {
return request.Get<Service.ResponseResult<NewsDetailVo>>(`/coder/admin/news/detail/${id}`)
}
/**
*
*/
export function addNews(data: NewsBo) {
return request.Post<Service.ResponseResult<string>>('/coder/admin/news/add', data)
}
/**
*
*/
export function updateNews(data: NewsBo) {
return request.Put<Service.ResponseResult<boolean>>('/coder/admin/news/edit', data)
}
/**
*
*/
export function deleteNews(id: string) {
return request.Delete<Service.ResponseResult<boolean>>(`/coder/admin/news/delete/${id}`)
}
/**
*
*/
export function batchDeleteNews(ids: string[]) {
return request.Delete<Service.ResponseResult<boolean>>('/coder/admin/news/batchDelete', { data: ids })
}
/**
*
*/
export function changePublishStatus(id: string, publishStatus: number) {
return request.Put<Service.ResponseResult<boolean>>('/coder/admin/news/changePublishStatus', null, {
params: { id, publishStatus },
})
}
/**
*
*/
export function setTop(id: string, isTop: number) {
return request.Put<Service.ResponseResult<boolean>>('/coder/admin/news/setTop', null, {
params: { id, isTop },
})
}

View File

@ -0,0 +1,111 @@
/**
*
*/
export interface NewsQueryBo {
pageNum?: number
pageSize?: number
title?: string
author?: string
source?: string
category?: string
tag?: string
keyword?: string
isTop?: number
publishStatus?: number
publishTimeStart?: string
publishTimeEnd?: string
sortField?: string
sortOrder?: string
}
/**
* VO
*/
export interface NewsVo {
id: string
title: string
summary?: string
coverImage?: string
author?: string
source?: string
category?: string
tags?: string
viewCount?: number
likeCount?: number
isTop: number
publishTime?: string
publishStatus: number
createBy?: string
createTime?: string
updateBy?: string
updateTime?: string
}
/**
* VO
*/
export interface NewsDetailVo {
id: string
title: string
summary?: string
content?: string
coverImage?: string
author?: string
source?: string
category?: string
tags?: string
viewCount?: number
likeCount?: number
isTop: number
publishTime?: string
publishStatus: number
createBy?: string
createTime?: string
updateBy?: string
updateTime?: string
}
/**
* BO/
*/
export interface NewsBo {
id?: string
title: string
summary?: string
content?: string
coverImage?: string
author?: string
source?: string
category?: string
tags?: string
isTop?: number
publishTime?: string
publishStatus?: number
}
/**
*
*/
export interface PageNewsVo {
records: NewsVo[]
total: number
size: number
current: number
pages: number
}
/**
*
*/
export interface NewsSearchForm {
title?: string
author?: string
source?: string
category?: string
tag?: string
keyword?: string
isTop?: number
publishStatus?: number
publishTimeStart?: string
publishTimeEnd?: string
}

View File

@ -0,0 +1,56 @@
import { request } from '../../../http'
import type {
RankingVo,
StatisticsVo,
TrendVo,
} from './types'
// 重新导出类型供外部使用
export type {
RankingVo,
StatisticsVo,
TrendVo,
} from './types'
/**
*
*/
export function getStatistics() {
return request.Get<Service.ResponseResult<StatisticsVo>>('/coder/admin/statistics/core')
}
/**
*
*/
export function getUserTrend(days: number = 7) {
return request.Get<Service.ResponseResult<TrendVo[]>>('/coder/admin/statistics/userTrend', {
params: { days },
})
}
/**
*
*/
export function getContentTrend(days: number = 7) {
return request.Get<Service.ResponseResult<TrendVo[]>>('/coder/admin/statistics/contentTrend', {
params: { days },
})
}
/**
*
*/
export function getHeritageRanking(type: string = 'view', limit: number = 10) {
return request.Get<Service.ResponseResult<RankingVo[]>>('/coder/admin/statistics/heritageRanking', {
params: { type, limit },
})
}
/**
*
*/
export function getActiveUserRanking(limit: number = 10) {
return request.Get<Service.ResponseResult<RankingVo[]>>('/coder/admin/statistics/activeUserRanking', {
params: { limit },
})
}

View File

@ -0,0 +1,66 @@
/**
* VO
*/
export interface StatisticsVo {
// 非遗项目统计
heritageTotal: number
heritageTodayCount: number
// 传承人统计
inheritorTotal: number
inheritorTodayCount: number
// 用户统计
userTotal: number
userTodayCount: number
// 活动统计
eventTotal: number
eventTodayCount: number
// 新闻统计
newsTotal: number
newsTodayCount: number
// 评论统计
commentTotal: number
commentTodayCount: number
commentPendingCount: number
// 点赞统计
likeTotal: number
likeTodayCount: number
// 收藏统计
favoriteTotal: number
favoriteTodayCount: number
// 浏览统计
viewTotal: number
viewTodayCount: number
// 活动报名统计
registrationTotal: number
registrationTodayCount: number
}
/**
* VO
*/
export interface TrendVo {
date: string
value: number
}
/**
* VO
*/
export interface RankingVo {
rank: number
id: string
title: string
type: string
coverImage?: string
value: number
valueType: string
}

View File

@ -0,0 +1,77 @@
import { request } from '../../../http'
import type {
PageUserAdminVo,
UserAdminBo,
UserAdminDetailVo,
UserAdminQueryBo,
} from './types'
// 重新导出类型供外部使用
export type {
PageUserAdminVo,
UserAdminBo,
UserAdminDetailVo,
UserAdminQueryBo,
UserAdminSearchForm,
UserAdminVo,
} from './types'
/**
*
*/
export function getUserAdminList(params: UserAdminQueryBo) {
return request.Get<Service.ResponseResult<PageUserAdminVo>>('/coder/admin/user/listPage', { params })
}
/**
*
*/
export function getUserAdminDetail(id: string) {
return request.Get<Service.ResponseResult<UserAdminDetailVo>>(`/coder/admin/user/detail/${id}`)
}
/**
*
*/
export function addUserAdmin(data: UserAdminBo) {
return request.Post<Service.ResponseResult<string>>('/coder/admin/user/add', data)
}
/**
*
*/
export function updateUserAdmin(data: UserAdminBo) {
return request.Put<Service.ResponseResult<boolean>>('/coder/admin/user/edit', data)
}
/**
*
*/
export function deleteUserAdmin(id: string) {
return request.Delete<Service.ResponseResult<boolean>>(`/coder/admin/user/delete/${id}`)
}
/**
*
*/
export function batchDeleteUserAdmin(ids: string[]) {
return request.Delete<Service.ResponseResult<boolean>>('/coder/admin/user/batchDelete', { data: ids })
}
/**
*
*/
export function changeUserStatus(id: string, status: number) {
return request.Put<Service.ResponseResult<boolean>>('/coder/admin/user/changeStatus', null, {
params: { id, status },
})
}
/**
*
*/
export function resetUserPassword(id: string, newPassword: string) {
return request.Put<Service.ResponseResult<boolean>>('/coder/admin/user/resetPassword', null, {
params: { id, newPassword },
})
}

View File

@ -0,0 +1,109 @@
/**
*
*/
export interface UserAdminQueryBo {
pageNum?: number
pageSize?: number
username?: string
nickname?: string
phone?: string
email?: string
keyword?: string
status?: number
gender?: number
province?: string
city?: string
createTimeBegin?: string
createTimeEnd?: string
sortField?: string
sortOrder?: string
}
/**
* VO
*/
export interface UserAdminVo {
id: string
username: string
nickname?: string
avatar?: string
email?: string
phone?: string
gender?: number
birthday?: string
province?: string
city?: string
status: number
loginIp?: string
loginTime?: string
createTime?: string
updateTime?: string
remark?: string
}
/**
* VO
*/
export interface UserAdminDetailVo {
id: string
username: string
nickname?: string
avatar?: string
email?: string
phone?: string
gender?: number
birthday?: string
province?: string
city?: string
status: number
loginIp?: string
loginTime?: string
createTime?: string
updateTime?: string
remark?: string
}
/**
* BO/
*/
export interface UserAdminBo {
id?: string
username: string
password?: string
nickname?: string
avatar?: string
email?: string
phone?: string
gender?: number
birthday?: string
province?: string
city?: string
status?: number
remark?: string
}
/**
*
*/
export interface PageUserAdminVo {
records: UserAdminVo[]
total: number
size: number
current: number
pages: number
}
/**
*
*/
export interface UserAdminSearchForm {
username?: string
nickname?: string
phone?: string
email?: string
keyword?: string
status?: number
gender?: number
province?: string
city?: string
}

View File

@ -62,17 +62,18 @@ export function batchDeleteSysPictures(ids: number[]) {
// 图片上传相关API
// 上传图片
export function uploadPicture(file: File, pictureType = '9', fileSize = 2, storageType?: string) {
// 上传图片到指定存储服务
export function uploadPicture(file: File, folderName = 'heritage', fileParam = '2', fileSize = 2, storageType = 'minio') {
const formData = new FormData()
formData.append('file', file)
// 如果指定了存储类型,添加到表单数据中
// 添加存储类型参数
if (storageType) {
formData.append('storageType', storageType)
}
return request.Post<PictureUploadResult>(`/coder/file/uploadFile/${fileSize}/pictures/${pictureType}`, formData)
// 使用通用文件上传接口:/coder/file/uploadFile/{fileSize}/{folderName}/{fileParam}
return request.Post<PictureUploadResult>(`/coder/file/uploadFile/${fileSize}/${folderName}/${fileParam}`, formData)
}
// 匿名上传图片(无需登录)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,715 @@
<template>
<div class="statistics-dashboard p-4 bg-gray-50 min-h-screen">
<!-- 页面标题 -->
<div class="mb-4">
<h2 class="text-2xl font-semibold text-gray-800">
统计分析
</h2>
<p class="text-sm text-gray-500 mt-1">
数据概览与趋势分析
</p>
</div>
<!-- 核心统计卡片 -->
<div class="statistics-cards mb-4">
<NGrid :cols="4" :x-gap="16" :y-gap="16">
<!-- 非遗项目统计 -->
<NGridItem>
<div class="stat-card bg-white rounded-lg shadow-sm border border-gray-100 p-4 hover:shadow-md transition-shadow">
<div class="flex items-center justify-between">
<div class="flex-1">
<p class="text-sm text-gray-500 mb-1">
非遗项目
</p>
<p class="text-3xl font-bold text-gray-800 mb-1">
{{ formatNumber(statistics.heritageTotal) }}
</p>
<p class="text-xs text-green-600">
今日新增 {{ statistics.heritageTodayCount }}
</p>
</div>
<div class="stat-icon bg-blue-50 rounded-full p-3">
<NIcon size="32" color="#3b82f6">
<icon-park-outline:palace />
</NIcon>
</div>
</div>
</div>
</NGridItem>
<!-- 传承人统计 -->
<NGridItem>
<div class="stat-card bg-white rounded-lg shadow-sm border border-gray-100 p-4 hover:shadow-md transition-shadow">
<div class="flex items-center justify-between">
<div class="flex-1">
<p class="text-sm text-gray-500 mb-1">
传承人
</p>
<p class="text-3xl font-bold text-gray-800 mb-1">
{{ formatNumber(statistics.inheritorTotal) }}
</p>
<p class="text-xs text-green-600">
今日新增 {{ statistics.inheritorTodayCount }}
</p>
</div>
<div class="stat-icon bg-purple-50 rounded-full p-3">
<NIcon size="32" color="#9333ea">
<icon-park-outline:peoples />
</NIcon>
</div>
</div>
</div>
</NGridItem>
<!-- 用户统计 -->
<NGridItem>
<div class="stat-card bg-white rounded-lg shadow-sm border border-gray-100 p-4 hover:shadow-md transition-shadow">
<div class="flex items-center justify-between">
<div class="flex-1">
<p class="text-sm text-gray-500 mb-1">
注册用户
</p>
<p class="text-3xl font-bold text-gray-800 mb-1">
{{ formatNumber(statistics.userTotal) }}
</p>
<p class="text-xs text-green-600">
今日新增 {{ statistics.userTodayCount }}
</p>
</div>
<div class="stat-icon bg-green-50 rounded-full p-3">
<NIcon size="32" color="#16a34a">
<icon-park-outline:user />
</NIcon>
</div>
</div>
</div>
</NGridItem>
<!-- 活动统计 -->
<NGridItem>
<div class="stat-card bg-white rounded-lg shadow-sm border border-gray-100 p-4 hover:shadow-md transition-shadow">
<div class="flex items-center justify-between">
<div class="flex-1">
<p class="text-sm text-gray-500 mb-1">
活动总数
</p>
<p class="text-3xl font-bold text-gray-800 mb-1">
{{ formatNumber(statistics.eventTotal) }}
</p>
<p class="text-xs text-green-600">
今日新增 {{ statistics.eventTodayCount }}
</p>
</div>
<div class="stat-icon bg-yellow-50 rounded-full p-3">
<NIcon size="32" color="#eab308">
<icon-park-outline:calendar />
</NIcon>
</div>
</div>
</div>
</NGridItem>
<!-- 新闻统计 -->
<NGridItem>
<div class="stat-card bg-white rounded-lg shadow-sm border border-gray-100 p-4 hover:shadow-md transition-shadow">
<div class="flex items-center justify-between">
<div class="flex-1">
<p class="text-sm text-gray-500 mb-1">
新闻资讯
</p>
<p class="text-3xl font-bold text-gray-800 mb-1">
{{ formatNumber(statistics.newsTotal) }}
</p>
<p class="text-xs text-green-600">
今日新增 {{ statistics.newsTodayCount }}
</p>
</div>
<div class="stat-icon bg-red-50 rounded-full p-3">
<NIcon size="32" color="#dc2626">
<icon-park-outline:file-text />
</NIcon>
</div>
</div>
</div>
</NGridItem>
<!-- 评论统计 -->
<NGridItem>
<div class="stat-card bg-white rounded-lg shadow-sm border border-gray-100 p-4 hover:shadow-md transition-shadow">
<div class="flex items-center justify-between">
<div class="flex-1">
<p class="text-sm text-gray-500 mb-1">
评论总数
</p>
<p class="text-3xl font-bold text-gray-800 mb-1">
{{ formatNumber(statistics.commentTotal) }}
</p>
<p class="text-xs text-orange-600">
待审核 {{ statistics.commentPendingCount }}
</p>
</div>
<div class="stat-icon bg-indigo-50 rounded-full p-3">
<NIcon size="32" color="#6366f1">
<icon-park-outline:comment />
</NIcon>
</div>
</div>
</div>
</NGridItem>
<!-- 点赞统计 -->
<NGridItem>
<div class="stat-card bg-white rounded-lg shadow-sm border border-gray-100 p-4 hover:shadow-md transition-shadow">
<div class="flex items-center justify-between">
<div class="flex-1">
<p class="text-sm text-gray-500 mb-1">
点赞总数
</p>
<p class="text-3xl font-bold text-gray-800 mb-1">
{{ formatNumber(statistics.likeTotal) }}
</p>
<p class="text-xs text-green-600">
今日新增 {{ statistics.likeTodayCount }}
</p>
</div>
<div class="stat-icon bg-pink-50 rounded-full p-3">
<NIcon size="32" color="#ec4899">
<icon-park-outline:like />
</NIcon>
</div>
</div>
</div>
</NGridItem>
<!-- 收藏统计 -->
<NGridItem>
<div class="stat-card bg-white rounded-lg shadow-sm border border-gray-100 p-4 hover:shadow-md transition-shadow">
<div class="flex items-center justify-between">
<div class="flex-1">
<p class="text-sm text-gray-500 mb-1">
收藏总数
</p>
<p class="text-3xl font-bold text-gray-800 mb-1">
{{ formatNumber(statistics.favoriteTotal) }}
</p>
<p class="text-xs text-green-600">
今日新增 {{ statistics.favoriteTodayCount }}
</p>
</div>
<div class="stat-icon bg-amber-50 rounded-full p-3">
<NIcon size="32" color="#f59e0b">
<icon-park-outline:star />
</NIcon>
</div>
</div>
</div>
</NGridItem>
</NGrid>
</div>
<!-- 趋势图表区域 -->
<div class="charts-section mb-4">
<NGrid :cols="2" :x-gap="16">
<!-- 用户增长趋势 -->
<NGridItem>
<div class="chart-card bg-white rounded-lg shadow-sm border border-gray-100 p-4">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-800">
用户增长趋势
</h3>
<NRadioGroup v-model:value="userTrendDays" size="small" @update:value="loadUserTrend">
<NRadioButton :value="7">
近7天
</NRadioButton>
<NRadioButton :value="30">
近30天
</NRadioButton>
</NRadioGroup>
</div>
<div ref="userTrendChartRef" class="chart-container" style="height: 300px;" />
</div>
</NGridItem>
<!-- 内容发布趋势 -->
<NGridItem>
<div class="chart-card bg-white rounded-lg shadow-sm border border-gray-100 p-4">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-800">
内容发布趋势
</h3>
<NRadioGroup v-model:value="contentTrendDays" size="small" @update:value="loadContentTrend">
<NRadioButton :value="7">
近7天
</NRadioButton>
<NRadioButton :value="30">
近30天
</NRadioButton>
</NRadioGroup>
</div>
<div ref="contentTrendChartRef" class="chart-container" style="height: 300px;" />
</div>
</NGridItem>
</NGrid>
</div>
<!-- 排行榜区域 -->
<div class="rankings-section">
<NGrid :cols="2" :x-gap="16">
<!-- 热门非遗项目排行榜 -->
<NGridItem>
<div class="ranking-card bg-white rounded-lg shadow-sm border border-gray-100 p-4">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-800">
热门非遗项目排行榜
</h3>
<NSelect
v-model:value="heritageRankingType"
size="small"
style="width: 120px"
:options="rankingTypeOptions"
@update:value="loadHeritageRanking"
/>
</div>
<div v-if="heritageRankingLoading" class="flex items-center justify-center py-12">
<NSpin size="medium" />
</div>
<div v-else-if="heritageRanking.length > 0" class="ranking-list">
<div
v-for="(item, index) in heritageRanking"
:key="item.id"
class="ranking-item flex items-center py-3 border-b border-gray-100 last:border-b-0 hover:bg-gray-50 transition-colors"
>
<div class="ranking-number flex-shrink-0 w-8 h-8 flex items-center justify-center mr-3">
<span
:class="{
'text-lg font-bold text-red-500': index === 0,
'text-lg font-bold text-orange-500': index === 1,
'text-lg font-bold text-yellow-500': index === 2,
'text-sm text-gray-500': index > 2,
}"
>
{{ item.rank }}
</span>
</div>
<div v-if="item.coverImage" class="ranking-cover flex-shrink-0 w-12 h-12 mr-3">
<img :src="item.coverImage" :alt="item.title" class="w-full h-full object-cover rounded">
</div>
<div class="ranking-info flex-1 min-w-0">
<p class="text-sm font-medium text-gray-800 truncate">
{{ item.title }}
</p>
<p class="text-xs text-gray-500 mt-1">
<NTag type="primary" size="small">
{{ item.type }}
</NTag>
</p>
</div>
<div class="ranking-value flex-shrink-0 ml-3 text-right">
<p class="text-lg font-semibold text-blue-600">
{{ formatNumber(item.value) }}
</p>
<p class="text-xs text-gray-500">
{{ getRankingValueLabel(item.valueType) }}
</p>
</div>
</div>
</div>
<div v-else class="flex items-center justify-center py-12 text-gray-400">
<span>暂无数据</span>
</div>
</div>
</NGridItem>
<!-- 活跃用户排行榜 -->
<NGridItem>
<div class="ranking-card bg-white rounded-lg shadow-sm border border-gray-100 p-4">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-800">
活跃用户排行榜
</h3>
</div>
<div v-if="activeUserRankingLoading" class="flex items-center justify-center py-12">
<NSpin size="medium" />
</div>
<div v-else-if="activeUserRanking.length > 0" class="ranking-list">
<div
v-for="(item, index) in activeUserRanking"
:key="item.id"
class="ranking-item flex items-center py-3 border-b border-gray-100 last:border-b-0 hover:bg-gray-50 transition-colors"
>
<div class="ranking-number flex-shrink-0 w-8 h-8 flex items-center justify-center mr-3">
<span
:class="{
'text-lg font-bold text-red-500': index === 0,
'text-lg font-bold text-orange-500': index === 1,
'text-lg font-bold text-yellow-500': index === 2,
'text-sm text-gray-500': index > 2,
}"
>
{{ item.rank }}
</span>
</div>
<div class="ranking-info flex-1 min-w-0">
<p class="text-sm font-medium text-gray-800 truncate">
{{ item.title }}
</p>
</div>
<div class="ranking-value flex-shrink-0 ml-3 text-right">
<p class="text-lg font-semibold text-green-600">
{{ formatNumber(item.value) }}
</p>
<p class="text-xs text-gray-500">
{{ getRankingValueLabel(item.valueType) }}
</p>
</div>
</div>
</div>
<div v-else class="flex items-center justify-center py-12 text-gray-400">
<span>暂无数据</span>
</div>
</div>
</NGridItem>
</NGrid>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { NGrid, NGridItem, NIcon, NRadioButton, NRadioGroup, NSelect, NSpin, NTag } from 'naive-ui'
import * as echarts from 'echarts'
import type { ECharts } from 'echarts'
import {
getActiveUserRanking,
getContentTrend,
getHeritageRanking,
getStatistics,
getUserTrend,
} from '@/service/api/heritage/statistics'
import type {
RankingVo,
StatisticsVo,
TrendVo,
} from '@/service/api/heritage/statistics'
import { coiMsgError } from '@/utils/coi'
//
const statistics = ref<StatisticsVo>({
heritageTotal: 0,
heritageTodayCount: 0,
inheritorTotal: 0,
inheritorTodayCount: 0,
userTotal: 0,
userTodayCount: 0,
eventTotal: 0,
eventTodayCount: 0,
newsTotal: 0,
newsTodayCount: 0,
commentTotal: 0,
commentTodayCount: 0,
commentPendingCount: 0,
likeTotal: 0,
likeTodayCount: 0,
favoriteTotal: 0,
favoriteTodayCount: 0,
viewTotal: 0,
viewTodayCount: 0,
registrationTotal: 0,
registrationTodayCount: 0,
})
//
const userTrendChartRef = ref<HTMLDivElement>()
const contentTrendChartRef = ref<HTMLDivElement>()
let userTrendChart: ECharts | null = null
let contentTrendChart: ECharts | null = null
const userTrendDays = ref(7)
const contentTrendDays = ref(7)
//
const heritageRankingType = ref('view')
const heritageRanking = ref<RankingVo[]>([])
const heritageRankingLoading = ref(false)
const activeUserRanking = ref<RankingVo[]>([])
const activeUserRankingLoading = ref(false)
const rankingTypeOptions = [
{ label: '浏览量', value: 'view' },
{ label: '收藏量', value: 'favorite' },
]
//
function formatNumber(num: number): string {
if (num >= 10000) {
return `${(num / 10000).toFixed(1)}`
}
return num.toString()
}
//
function getRankingValueLabel(valueType: string): string {
const labelMap: Record<string, string> = {
view: '浏览量',
favorite: '收藏量',
activity: '活跃度',
}
return labelMap[valueType] || valueType
}
//
async function loadStatistics() {
try {
const { data, isSuccess } = await getStatistics()
if (isSuccess && data) {
statistics.value = data
}
}
catch (error) {
coiMsgError('加载统计数据失败')
}
}
//
async function loadUserTrend() {
try {
const { data, isSuccess } = await getUserTrend(userTrendDays.value)
if (isSuccess && data) {
renderUserTrendChart(data)
}
}
catch (error) {
coiMsgError('加载用户增长趋势失败')
}
}
//
async function loadContentTrend() {
try {
const { data, isSuccess } = await getContentTrend(contentTrendDays.value)
if (isSuccess && data) {
renderContentTrendChart(data)
}
}
catch (error) {
coiMsgError('加载内容发布趋势失败')
}
}
//
async function loadHeritageRanking() {
heritageRankingLoading.value = true
try {
const { data, isSuccess } = await getHeritageRanking(heritageRankingType.value, 10)
if (isSuccess && data) {
heritageRanking.value = data
}
}
catch (error) {
coiMsgError('加载非遗项目排行榜失败')
}
finally {
heritageRankingLoading.value = false
}
}
//
async function loadActiveUserRanking() {
activeUserRankingLoading.value = true
try {
const { data, isSuccess } = await getActiveUserRanking(10)
if (isSuccess && data) {
activeUserRanking.value = data
}
}
catch (error) {
coiMsgError('加载活跃用户排行榜失败')
}
finally {
activeUserRankingLoading.value = false
}
}
//
function renderUserTrendChart(data: TrendVo[]) {
if (!userTrendChartRef.value)
return
if (!userTrendChart) {
userTrendChart = echarts.init(userTrendChartRef.value)
}
const dates = data.map(item => item.date)
const values = data.map(item => item.value)
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985',
},
},
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: {
type: 'category',
boundaryGap: false,
data: dates,
},
yAxis: {
type: 'value',
},
series: [
{
name: '新增用户',
type: 'line',
smooth: true,
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(34, 197, 94, 0.3)' },
{ offset: 1, color: 'rgba(34, 197, 94, 0.05)' },
],
},
},
lineStyle: {
color: '#22c55e',
width: 2,
},
itemStyle: {
color: '#22c55e',
},
data: values,
},
],
}
userTrendChart.setOption(option)
}
//
function renderContentTrendChart(data: TrendVo[]) {
if (!contentTrendChartRef.value)
return
if (!contentTrendChart) {
contentTrendChart = echarts.init(contentTrendChartRef.value)
}
const dates = data.map(item => item.date)
const values = data.map(item => item.value)
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985',
},
},
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: {
type: 'category',
boundaryGap: false,
data: dates,
},
yAxis: {
type: 'value',
},
series: [
{
name: '发布内容',
type: 'line',
smooth: true,
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(59, 130, 246, 0.3)' },
{ offset: 1, color: 'rgba(59, 130, 246, 0.05)' },
],
},
},
lineStyle: {
color: '#3b82f6',
width: 2,
},
itemStyle: {
color: '#3b82f6',
},
data: values,
},
],
}
contentTrendChart.setOption(option)
}
//
onMounted(() => {
loadStatistics()
loadUserTrend()
loadContentTrend()
loadHeritageRanking()
loadActiveUserRanking()
//
window.addEventListener('resize', () => {
userTrendChart?.resize()
contentTrendChart?.resize()
})
})
</script>
<style scoped>
.statistics-dashboard {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
.stat-card {
cursor: pointer;
}
.stat-card:hover .stat-icon {
transform: scale(1.1);
transition: transform 0.2s ease-in-out;
}
.chart-card,
.ranking-card {
min-height: 400px;
}
.ranking-item:hover {
cursor: pointer;
}
.ranking-cover img {
object-fit: cover;
border-radius: 4px;
}
</style>

File diff suppressed because it is too large Load Diff