Commit 5d0b8441 by lijiabin

【需求 17679】 wip: 继续完善页面,新增后台管理页面

parent 4a950630
......@@ -121,6 +121,8 @@ export interface ArticleItemDto {
imgUrl: string
createUserAvatar: string
createUserName: string
showAvatar: string
showName: string
}
/**
......@@ -205,6 +207,7 @@ export interface InterviewItemDto {
*/
export interface CommentSearchParams extends PageSearchParams {
articleId: number | string
sortType: number
}
/**
......
import service from '@/utils/request/index'
import type { AddOrUpdateTagDto, BackendTagListItemDto, BackendTagSearchParams } from './types'
import type { BackendServicePageResult } from '@/utils/request/types'
// 后台管理标签相关接口
/**
* 获取轮播图列表 不分页 数量不多
*/
export const getCarouselList = () => {
return service.request<BackendServicePageResult<BackendTagListItemDto>>({
url: '/api/cultureCarousel/listNoPage',
method: 'POST',
data: {},
})
}
/**
* 添加轮播图
*/
export const addCarousel = (data: AddOrUpdateCarouselDto) => {
return service.request({
url: '/api/cultureCarousel/addCarousel',
method: 'POST',
data,
})
}
/**
* 更新轮播图
*/
export const editCarousel = (data: AddOrUpdateCarouselDto) => {
return service.request({
url: '/api/cultureCarousel/editCarousel',
method: 'POST',
data,
})
}
/**
* 更改发布状态
*/
export const deleteCarousel = (id: number) => {
return service.request<boolean>({
url: `/api/cultureCarousel/updateRelease?id=${id}`,
method: 'POST',
})
}
import service from '@/utils/request/index'
import type {
AddOrUpdateColumnDto,
BackendColumnSearchParams,
BackendColumnListItemDto,
} from './types'
import type { BackendServicePageResult } from '@/utils/request/types'
// 后台管理栏目相关内容
/**
* 新增或者修改栏目
*/
export const addOrUpdateColumn = (data: AddOrUpdateColumnDto) => {
return service.request({
url: '/api/cultureColumn/addOrUpdate',
method: 'POST',
data,
})
}
/**
* 栏目列表
*/
export const listOfCultureColumn = (data: BackendColumnSearchParams) => {
return service.request<BackendServicePageResult<BackendColumnListItemDto>>({
url: '/api/cultureColumn/listByPage',
method: 'POST',
data,
})
}
/**
* 删除栏目设置
*/
export const deleteColumn = (ids: number[]) => {
return service.request<boolean>({
url: `/api/cultureColumn/delete`,
method: 'POST',
data: ids,
})
}
/**
* 隐藏栏目设置
*/
export const hideColumn = (ids: number[]) => {
return service.request<boolean>({
url: `/api/cultureColumn/hidden`,
method: 'POST',
data: ids,
})
}
import type { PageSearchParams } from '@/utils/request/types'
export interface BackendColumnSearchParams extends PageSearchParams {
type: string
status: number
title: string
}
export interface AddOrUpdateColumnDto {
id?: number
color: string
title: string
type: string
sort: number
}
export interface BackendColumnListItemDto {
color: string
createTime: number
createUserId: number
id: number
isDelete: number
postCount: number
sort: number
status: number
title: string
type: string
}
// 方法
export * from './tags'
export * from './carousel'
export * from './columnSettings'
// 类型
export * from './tags/types'
export * from './carousel/types'
export * from './columnSettings/types'
import service from '@/utils/request/index'
import type { AddOrUpdateTagDto, BackendTagListItemDto, BackendTagSearchParams } from './types'
import type { BackendServicePageResult } from '@/utils/request/types'
// 后台管理标签相关接口
/**
* 获取标签列表 分页
*/
export const getTagList = (params: BackendTagSearchParams) => {
return service.request<BackendServicePageResult<BackendTagListItemDto>>({
url: '/api/cultureTag/listByPage',
method: 'POST',
data: params,
})
}
/**
* 新增或者更新标签
*/
export const addOrUpdateTag = (data: AddOrUpdateTagDto) => {
return service.request({
url: '/api/cultureTag/addOrUpdate',
method: 'POST',
data,
})
}
/**
* 删除标签
*/
export const deleteTag = (id: number) => {
return service.request<boolean>({
url: `/api/cultureTag/delete?id=${id}`,
method: 'POST',
})
}
import type { PageSearchParams } from '@/utils/request/types'
export interface BackendTagSearchParams extends PageSearchParams {
title: string
type: string
}
export interface AddOrUpdateTagDto {
id?: number
color: string
description: string
title: string
type: string
}
export interface BackendTagListItemDto {
color: string
createId: number
createTime: number
description: string
id: number
imgUrl: string
style: number
title: string
type: string
}
......@@ -22,6 +22,7 @@ export interface AddOrUpdatePracticeDto {
*/
export interface PracticeSearchParams extends PageSearchParams {
sortLogic?: number
tagIdList?: number[]
}
/**
......
......@@ -5,6 +5,11 @@ import type {
AuditListSearchParams,
AuditListItemDto,
AuditArticleDto,
SelfPublishSearchParams,
SelfCollectSearchParams,
SelfCollectDetailDto,
SelfPraiseSearchParams,
SelfPraiseDetailDto,
} from './types'
import type { BackendServicePageResult, PageSearchParams } from '@/utils/request/types'
......@@ -33,7 +38,7 @@ export const hasOfficialAccount = () => {
/**
* 获取我的发布列表
*/
export const getSelfPublishList = (data: PageSearchParams) => {
export const getSelfPublishList = (data: SelfPublishSearchParams) => {
return service.request<BackendServicePageResult<SelfPublishDetailDto>>({
url: '/api/personalCenter/selfPublish',
method: 'POST',
......@@ -55,16 +60,8 @@ export const getSelfDraftList = (data: PageSearchParams) => {
/**
* 获取我的收藏列表
*/
export const getSelfCollectList = (data: PageSearchParams) => {
return service.request<
BackendServicePageResult<{
id: number
title: string
content: string
createTime: string
updateTime: string
}>
>({
export const getSelfCollectList = (data: SelfCollectSearchParams) => {
return service.request<BackendServicePageResult<SelfCollectDetailDto>>({
url: '/api/personalCenter/selfCollect',
method: 'POST',
data,
......@@ -74,16 +71,8 @@ export const getSelfCollectList = (data: PageSearchParams) => {
/**
* 获取我的点赞列表
*/
export const getSelfPraiseList = (data: PageSearchParams) => {
return service.request<
BackendServicePageResult<{
id: number
title: string
content: string
createTime: string
updateTime: string
}>
>({
export const getSelfPraiseList = (data: SelfPraiseSearchParams) => {
return service.request<BackendServicePageResult<SelfPraiseDetailDto>>({
url: '/api/personalCenter/selfPraise',
method: 'POST',
data,
......
import { AuditStatusEnum } from '@/constants'
import { ArticleTypeEnum, AuditStatusEnum } from '@/constants'
import type { PageSearchParams } from '@/utils/request/types'
// 我的发布 详情
export interface SelfPublishSearchParams extends PageSearchParams {
type: ArticleTypeEnum
}
// 我的发布
export interface SelfPublishDetailDto {
collectionCount: number
content: string
......@@ -23,6 +26,55 @@ export interface SelfPublishDetailDto {
viewCount: number
}
// 我的收藏
export interface SelfCollectSearchParams extends PageSearchParams {
type: ArticleTypeEnum
}
export interface SelfCollectDetailDto {
collectionCount: number
content: string
createTime: number
createUserId: number
description: string
faceUrl: string
hasPraised: boolean
id: number
isRecommend: boolean
praiseCount: number
replyCount: number
tagNameList: string[]
title: string
type: string
viewCount: number
}
// 我的点赞
export interface SelfPraiseSearchParams extends PageSearchParams {
type: ArticleTypeEnum
}
export interface SelfPraiseDetailDto {
collectionCount: number
content: string
createTime: number
createUserId: number
description: string
faceUrl: string
hasPraised: boolean
id: number
isRecommend: boolean
isRelateColleague: boolean
praiseCount: number
releaseStatus: number
replyCount: number
tagNameList: string[]
title: string
type: string
videoUrl: string
viewCount: number
}
// 我的草稿 详情
export interface SelfDraftDetailDto {
id: number
......
......@@ -12,6 +12,7 @@
:multiple="multiple"
:limit="limit"
:disabled="hasReachedLimit && !multiple"
class="custom-upload"
>
<el-icon><Plus /></el-icon>
</el-upload>
......@@ -97,11 +98,7 @@ watch(
{ deep: true },
)
/**
* 处理文件变化(上传)- 修复版本
*/
const handleChange: UploadProps['onChange'] = async (uploadFile, uploadFiles) => {
// 检查是否超出限制
if (uploadFiles.length > props.limit) {
ElMessage.error(`最多上传 ${props.limit} 个文件`)
const index = fileList.value.findIndex((file) => file.uid === uploadFile.uid)
......@@ -111,28 +108,23 @@ const handleChange: UploadProps['onChange'] = async (uploadFile, uploadFiles) =>
return
}
// 如果是新上传的文件
if (uploadFile.raw && uploadFile.status === 'ready') {
// 保存 uid 用于后续查找
const uid = uploadFile.uid
try {
// 更新状态为上传中(第一次查找)
let fileIndex = fileList.value.findIndex((file) => file.uid === uid)
if (fileIndex !== -1) {
fileList.value[fileIndex].status = 'uploading'
}
// 上传文件
const { data } = await uploadFileApi(uploadFile.raw)
console.log('data', data)
const url = data.fileUrl || data.data[0].filePath
const name = data.fileName || data.data[0].finalName
// ✅ 上传完成后重新查找索引(第二次查找)
fileIndex = fileList.value.findIndex((file) => file.uid === uid)
if (fileIndex !== -1) {
// 更新文件信息
fileList.value[fileIndex] = {
...fileList.value[fileIndex],
url,
......@@ -147,7 +139,6 @@ const handleChange: UploadProps['onChange'] = async (uploadFile, uploadFiles) =>
console.error('上传失败:', error)
ElMessage.error('上传失败,请重试')
// 移除上传失败的文件
const fileIndex = fileList.value.findIndex((file) => file.uid === uid)
if (fileIndex !== -1) {
fileList.value.splice(fileIndex, 1)
......@@ -189,4 +180,48 @@ defineExpose({
})
</script>
<style scoped></style>
<style scoped>
/* 方案1: 适中尺寸(推荐) */
.custom-upload :deep(.el-upload--picture-card) {
width: 100px;
height: 100px;
line-height: 100px;
}
.custom-upload :deep(.el-upload-list--picture-card .el-upload-list__item) {
width: 100px;
height: 100px;
}
/* 方案2: 更小尺寸 */
/* .custom-upload :deep(.el-upload--picture-card) {
width: 80px;
height: 80px;
line-height: 80px;
}
.custom-upload :deep(.el-upload-list--picture-card .el-upload-list__item) {
width: 80px;
height: 80px;
}
.custom-upload :deep(.el-icon) {
font-size: 20px;
} */
/* 方案3: 迷你尺寸 */
/* .custom-upload :deep(.el-upload--picture-card) {
width: 60px;
height: 60px;
line-height: 60px;
}
.custom-upload :deep(.el-upload-list--picture-card .el-upload-list__item) {
width: 60px;
height: 60px;
}
.custom-upload :deep(.el-icon) {
font-size: 16px;
} */
</style>
......@@ -77,7 +77,7 @@ export function usePageSearch<
list.value = data.list || []
total.value = data.total || 0
} catch (error) {
console.error('分页搜索失败:', error)
console.log('分页搜索失败:', error)
list.value = []
total.value = 0
} finally {
......
......@@ -17,7 +17,7 @@
<!-- 时长显示卡片 -->
<div
class="w-24 md:w-28 lg:w-32 h-16 md:h-18 lg:h-20 rounded-2xl bg-gradient-to-br from-white via-blue-50 to-purple-50 shadow-lg hover:shadow-xl border border-white/50 flex flex-col justify-center items-center transition-all duration-300 ease-out hover:scale-105 hover:-translate-y-1 backdrop-blur-sm relative overflow-hidden group transform-gpu backface-hidden will-change-transform"
class="w-24 md:w-28 lg:w-32 h-16 md:h-18 lg:h-20 rounded-lg bg-gradient-to-br from-white via-blue-50 to-purple-50 shadow-lg hover:shadow-xl border border-white/50 flex flex-col justify-center items-center transition-all duration-300 ease-out hover:scale-105 hover:-translate-y-1 backdrop-blur-sm relative overflow-hidden group transform-gpu backface-hidden will-change-transform"
>
<!-- 其他内容保持不变 -->
<div
......
......@@ -96,6 +96,56 @@ const routes = [
name: 'Test',
component: () => import('@/test.vue'),
},
{
path: '/backend',
name: 'Backend',
component: () => import('@/views/backend/index.vue'),
redirect: '/backend/manager',
children: [
{
path: 'manager',
name: 'ManagerManagement',
component: () => import('@/views/backend/manager/index.vue'),
meta: { title: '企业文化管理员' },
},
{
path: 'tags',
name: 'OfficialManagement',
component: () => import('@/views/backend/tags/index.vue'),
meta: { title: '官方标签' },
},
{
path: 'carousel',
name: 'CarouselManagement',
component: () => import('@/views/backend/carousel/index.vue'),
meta: { title: '轮播图设置' },
},
// {
// path: 'topic-admin',
// name: 'TopicAdminManagement',
// component: () => import('@/views/backend/topic-admin/index.vue'),
// meta: { title: '专栏——管理员' },
// },
{
path: 'columnSettings',
name: 'ColumnSettingsManagement',
component: () => import('@/views/backend/columnSettings/index.vue'),
meta: { title: '专栏——栏目管理' },
},
// {
// path: 'interview-admin',
// name: 'InterviewAdminManagement',
// component: () => import('@/views/backend/interview-admin/index.vue'),
// meta: { title: '专访——管理员' },
// },
{
path: 'interviewSettings',
name: 'InterviewSettingsManagement',
component: () => import('@/views/backend/interviewSettings/index.vue'),
meta: { title: '专访——栏目管理' },
},
],
},
]
const scrollPositionMap = new Map<string, number>()
......
<!-- views/backend/carousel/index.vue -->
<template>
<div class="carousel-page">
<!-- 顶部操作栏 -->
<div class="action-section">
<el-button type="primary" :icon="Search" @click="getList">搜索</el-button>
<el-button type="primary" class="add-btn" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增
</el-button>
</div>
<!-- 表格区域 -->
<div class="table-section">
<el-table v-loading="loading" :data="list">
<el-table-column type="selection" width="55" />
<el-table-column prop="image" label="跳转路径" min-width="300">
<template #default="{ row }">
<div class="image-cell">
<el-image
:src="row.assetUrl"
fit="cover"
class="carousel-image"
:preview-src-list="[row.assetUrl]"
/>
</div>
</template>
</el-table-column>
<el-table-column prop="sort" label="排序" width="100" align="center" />
<el-table-column prop="type" label="类型" width="120" align="center">
<template #default="{ row }">
<el-tag :type="row.type === 0 ? 'success' : 'primary'">
{{ row.type === 0 ? '图片' : '视频' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="creator" label="操作人/时间" width="200">
<template #default="{ row }">
<div class="creator-info">
<div>{{ row.creator }}</div>
<div class="create-time">{{ row.createTime }}</div>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 新增/编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="600px"
:close-on-click-modal="false"
>
<el-alert
title="以下网址请仔细核对是否正确,若输入错误,会导致跳转错误页面,或者资源加载不出来"
type="info"
:closable="false"
style="margin-bottom: 20px"
/>
<el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
<el-form-item label="轮播类型" prop="type">
<el-radio-group v-model="form.type">
<el-radio :value="0">图片</el-radio>
<el-radio value="1">视频</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="显示资源" prop="image">
<div class="upload-section">
<UploadFile v-model="form.assetUrl" />
<div class="upload-hint">上传图片,推荐比例为 W/H: 1280 / 480</div>
</div>
</el-form-item>
<el-form-item label="跳转链接" prop="link">
<el-input
v-model="form.link"
placeholder="不填表示点击不跳转"
maxlength="100"
show-word-limit
/>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number
v-model="form.sort"
:min="0"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">
<el-icon class="btn-icon"><Upload /></el-icon>
保存
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { Search, Plus, Upload } from '@element-plus/icons-vue'
import { useResetData } from '@/hooks'
import { getCarouselList, addCarousel, editCarousel } from '@/api/backend'
import type { FormInstance, FormRules } from 'element-plus'
import UploadFile from '@/components/common/UploadFile/index.vue'
import type { CarouselListItemDto, AddOrUpdateCarouselDto } from '@/api/backend'
// 列表数据
const loading = ref(false)
const list = ref<CarouselListItemDto[]>([])
// 获取列表
const getList = async () => {
loading.value = true
try {
const res = await getCarouselList()
list.value = res.data
} finally {
loading.value = false
}
}
// 对话框
const dialogVisible = ref(false)
const dialogTitle = computed(() => (form.value.id ? '编辑轮播' : '新增轮播'))
const formRef = ref<FormInstance>()
// 表单数据
const [form, resetForm] = useResetData<AddOrUpdateCarouselDto>({
type: 0,
assetUrl: '',
link: '',
sort: 0,
id: undefined,
isRelease: 1,
})
// 表单验证规则
const formRules: FormRules = {
type: [{ required: true, message: '请选择轮播类型', trigger: 'change' }],
assetUrl: [{ required: true, message: '请上传资源', trigger: 'change' }],
sort: [{ required: true, message: '请输入排序', trigger: 'blur' }],
}
// 新增
const handleAdd = () => {
resetForm()
dialogVisible.value = true
}
// 编辑
const handleEdit = (row: CarouselListItemDto) => {
resetForm()
form.value = {
type: row.type,
assetUrl: row.assetUrl,
link: row.link,
sort: row.sort,
id: row.id,
isRelease: row.isRelease,
}
dialogVisible.value = true
}
// 删除
const handleDelete = async (row: CarouselListItemDto) => {
try {
await ElMessageBox.confirm('确定要删除该轮播吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
await editCarousel({
id: row.id,
isRelease: 0,
})
ElMessage.success('删除成功')
getList()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败')
}
}
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
if (form.value.id) {
await editCarousel(form.value)
} else {
await addCarousel(form.value)
}
ElMessage.success(form.value.id ? '编辑成功' : '新增成功')
dialogVisible.value = false
getList()
} catch (error) {
console.error('表单验证失败:', error)
}
}
// 初始化
onMounted(() => {
getList()
})
</script>
<style scoped lang="scss">
.carousel-page {
height: 100%;
display: flex;
flex-direction: column;
gap: 16px;
}
// 操作区域
.action-section {
background: #fff;
border-radius: 8px;
padding: 20px;
display: flex;
gap: 12px;
flex-shrink: 0;
.add-btn {
margin-left: auto;
}
}
// 表格区域
.table-section {
flex: 1;
background: #fff;
border-radius: 8px;
padding: 20px;
min-height: 0;
overflow: auto;
}
// 图片单元格
.image-cell {
padding: 8px 0;
.carousel-image {
width: 240px;
height: 90px;
border-radius: 4px;
cursor: pointer;
}
}
// 操作人信息
.creator-info {
.create-time {
color: #909399;
font-size: 12px;
margin-top: 4px;
}
}
// 上传区域
.upload-section {
width: 100%;
.upload-hint {
color: #409eff;
font-size: 12px;
margin-top: 8px;
}
}
.btn-icon {
margin-right: 4px;
}
</style>
<!-- views/backend/official/index.vue -->
<template>
<div class="official-tag-page">
<!-- 搜索栏 -->
<div class="search-section">
<div class="flex-1 flex gap-2">
<el-input
v-model="searchParams.title"
placeholder="请输入栏目标题"
class="w-200px"
></el-input>
<el-select
v-model="searchParams.status"
placeholder="请选择发布状态"
class="search-select"
clearable
>
<el-option label="发布" :value="1" />
<el-option label="隐藏" :value="0" />
</el-select>
<el-button type="primary" :icon="Search" @click="refresh">搜索</el-button>
<el-button @click="reset">重置</el-button>
</div>
<div class="flex justify-end">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增
</el-button>
<el-button type="primary" @click="handleBatchPublish"> 批量发布/隐藏 </el-button>
<el-button type="danger" @click="handleBatchDelete"> 批量删除 </el-button>
</div>
</div>
<!-- 表格区域 -->
<div class="table-section">
<!-- 表格 -->
<div class="table-wrapper">
<el-table
v-loading="loading"
:data="list"
height="100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55"> </el-table-column>
<el-table-column prop="sort" label="栏目顺序" width="180"> </el-table-column>
<el-table-column prop="title" label="栏目名称" min-width="200" />
<el-table-column prop="postCount" label="栏目帖子数量" min-width="200" />
<el-table-column prop="color" label="颜色" width="300">
<template #default="{ row }">
<div class="color-cell">
<div class="color-block" :style="{ backgroundColor: row.color }" />
<span class="color-text">{{ row.color }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="createUserId" label="创建人" min-width="200" />
<el-table-column prop="createTime" label="创建时间" min-width="200">
<template #default="{ row }">
{{ dayjs(row.createTime * 1000).format('YYYY-MM-DD HH:mm:ss') }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" min-width="200">
<template #default="{ row }">
<el-switch
:model-value="row.status"
:active-value="1"
:inactive-value="0"
@change="handleStatusChange(row)"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size"
:total="total"
:page-sizes="[10, 20, 30]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="changePageSize"
@current-change="goToPage"
/>
</div>
</div>
<!-- 新增/编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="500px"
:close-on-click-modal="false"
>
<el-form ref="formRef" :model="form" :rules="formRules" label-width="80px">
<el-form-item label="标题" prop="title">
<el-input v-model="form.title" placeholder="请输入标题" />
</el-form-item>
<el-form-item label="颜色" prop="color">
<el-color-picker v-model="form.color" />
<span class="color-value">{{ form.color }}</span>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="form.sort" :min="0" :max="100" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">
<el-icon class="btn-icon"><Upload /></el-icon>
保存
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { Search, Plus, Upload } from '@element-plus/icons-vue'
import { usePageSearch, useResetData } from '@/hooks'
import { listOfCultureColumn, addOrUpdateColumn, deleteColumn, hideColumn } from '@/api/backend'
import type { FormInstance, FormRules } from 'element-plus'
import type { BackendColumnListItemDto, AddOrUpdateColumnDto } from '@/api/backend'
import dayjs from 'dayjs'
const { loading, list, total, reset, goToPage, changePageSize, refresh, searchParams, search } =
usePageSearch(listOfCultureColumn, {
defaultParams: {
type: 'column',
},
})
// 对话框
const dialogVisible = ref(false)
const dialogTitle = computed(() => (form.value.id ? '编辑标签' : '新增标签'))
const formRef = ref<FormInstance>()
// 表单数据
const [form, resetForm] = useResetData<AddOrUpdateColumnDto>({
title: '',
color: '#000000',
id: undefined,
sort: 0,
type: 'column',
})
// 表单验证规则
const formRules: FormRules = {
title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
color: [{ required: true, message: '请选择颜色', trigger: 'change' }],
sort: [{ required: true, message: '请输入排序', trigger: 'blur' }],
}
// 新增
const handleAdd = () => {
resetForm()
dialogVisible.value = true
}
// 编辑
const handleEdit = (row: BackendColumnListItemDto) => {
resetForm()
form.value = {
title: row.title,
color: row.color,
id: row.id,
sort: row.sort,
type: 'column',
}
dialogVisible.value = true
}
// 状态改变
const handleStatusChange = async (row: BackendColumnListItemDto) => {
await hideColumn([row.id])
refresh()
}
// 删除
const handleDelete = async (row: BackendColumnListItemDto) => {
try {
await ElMessageBox.confirm(`确定要删除标签"${row.title}"吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
await deleteColumn([row.id])
ElMessage.success('删除成功')
refresh()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败')
}
}
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
if (form.value.id) {
await addOrUpdateColumn(form.value)
} else {
await addOrUpdateColumn(form.value)
}
ElMessage.success(form.value.id ? '编辑成功' : '新增成功')
dialogVisible.value = false
if (form.value.id) {
search()
} else {
refresh()
}
} catch (error) {
console.error('表单验证失败:', error)
}
}
const selectedRows = ref<BackendColumnListItemDto[]>([])
// 选择
const handleSelectionChange = (selection: BackendColumnListItemDto[]) => {
selectedRows.value = selection
}
// 批量发布/隐藏
const handleBatchPublish = async () => {
await hideColumn(selectedRows.value.map((item) => item.id))
refresh()
selectedRows.value = []
ElMessage.success('发布/隐藏成功')
}
// 批量删除
const handleBatchDelete = async () => {
await deleteColumn(selectedRows.value.map((item) => item.id))
refresh()
selectedRows.value = []
ElMessage.success('删除成功')
}
</script>
<style scoped lang="scss">
.official-tag-page {
height: 100%;
display: flex;
flex-direction: column;
gap: 16px;
}
// 搜索区域
.search-section {
background: #fff;
border-radius: 8px;
padding: 20px;
display: flex;
gap: 12px;
flex-shrink: 0;
.search-select {
width: 200px;
}
}
// 表格区域
.table-section {
flex: 1;
background: #fff;
border-radius: 8px;
padding: 20px;
display: flex;
flex-direction: column;
min-height: 0;
}
.table-wrapper {
flex: 1;
min-height: 0;
.color-cell {
display: flex;
align-items: center;
gap: 12px;
.color-block {
width: 100%;
height: 36px;
border-radius: 4px;
border: 1px solid #e5e7eb;
flex: 1;
}
.color-text {
color: #fff;
font-size: 14px;
font-weight: 500;
position: absolute;
left: 50%;
transform: translateX(-50%);
text-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
}
}
}
.pagination-wrapper {
display: flex;
justify-content: flex-end;
padding-top: 16px;
flex-shrink: 0;
}
// 对话框内的颜色显示
.color-value {
margin-left: 12px;
color: #606266;
font-family: monospace;
}
.btn-icon {
margin-right: 4px;
}
</style>
<!-- views/backend/index.vue -->
<template>
<div class="backend-layout">
<!-- 侧边栏 -->
<aside class="backend-sidebar">
<div class="sidebar-header">
<img src="/webicon.png" alt="SOUNDASIA" class="logo" />
<span class="header-title">企业文化后台管理</span>
</div>
<el-menu :default-active="activeMenu" class="sidebar-menu" @select="handleMenuSelect">
<el-menu-item v-for="item in menuList" :key="item.path" :index="item.path">
<span>{{ item.title }}</span>
</el-menu-item>
</el-menu>
</aside>
<!-- 右侧主内容区 -->
<div class="backend-content">
<!-- 顶部标题栏 -->
<header class="content-header">
<div class="header-left">
<img src="/webicon.png" alt="" class="header-icon" />
<h1 class="header-title">{{ currentTitle }}</h1>
</div>
<div class="header-right">
<!-- 可以放置用户信息、退出登录等 -->
</div>
</header>
<!-- 内容区 -->
<main class="content-main">
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</main>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
interface MenuItem {
path: string
title: string
}
const menuList: MenuItem[] = [
{ path: '/backend/manager', title: '企业文化管理员' },
{ path: '/backend/tags', title: '官方标签' },
{ path: '/backend/carousel', title: '轮播图设置' },
{ path: '/backend/topic-admin', title: '专栏——管理员' },
{ path: '/backend/columnSettings', title: '专栏——栏目管理' },
{ path: '/backend/interview-admin', title: '专访——管理员' },
{ path: '/backend/interviewSettings', title: '专访——栏目管理' },
]
const activeMenu = computed(() => route.path)
const currentTitle = computed(() => {
const current = menuList.find((item) => item.path === route.path)
return current?.title || ''
})
const handleMenuSelect = (path: string) => {
router.push(path)
}
</script>
<style scoped lang="scss">
.backend-layout {
display: flex;
height: 100vh;
overflow: hidden;
}
.backend-sidebar {
width: 200px;
background: #fff;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
.sidebar-header {
height: 70px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 16px;
border-bottom: 1px solid #f0f0f0;
.logo {
height: 32px;
width: 32px;
}
.header-title {
font-size: 14px;
color: #333;
font-weight: 500;
}
}
.sidebar-menu {
flex: 1;
border-right: none;
:deep(.el-menu-item) {
height: 48px;
line-height: 48px;
font-size: 14px;
&:hover {
background: #f5f7fa;
}
&.is-active {
background: #e6f4ff;
color: #409eff;
border-right: 3px solid #409eff;
}
}
}
}
.backend-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
background: #f5f7fa;
}
.content-header {
height: 60px;
background: #fff;
padding: 0 24px;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
z-index: 10;
.header-left {
display: flex;
align-items: center;
gap: 12px;
.header-icon {
width: 32px;
height: 32px;
}
.header-title {
font-size: 18px;
font-weight: 500;
color: #333;
margin: 0;
}
}
}
.content-main {
flex: 1;
overflow: auto;
padding: 20px;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
<!-- views/backend/official/index.vue -->
<template>
<div class="official-tag-page">
<!-- 搜索栏 -->
<div class="search-section">
<div class="flex-1 flex gap-2">
<el-input
v-model="searchParams.title"
placeholder="请输入栏目标题"
class="w-200px"
></el-input>
<el-select
v-model="searchParams.status"
placeholder="请选择发布状态"
class="search-select"
clearable
>
<el-option label="发布" :value="1" />
<el-option label="隐藏" :value="0" />
</el-select>
<el-button type="primary" :icon="Search" @click="refresh">搜索</el-button>
<el-button @click="reset">重置</el-button>
</div>
<div class="flex justify-end">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增
</el-button>
<el-button type="primary" @click="handleBatchPublish"> 批量发布/隐藏 </el-button>
<el-button type="danger" @click="handleBatchDelete"> 批量删除 </el-button>
</div>
</div>
<!-- 表格区域 -->
<div class="table-section">
<!-- 表格 -->
<div class="table-wrapper">
<el-table
v-loading="loading"
:data="list"
height="100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55"> </el-table-column>
<el-table-column prop="sort" label="栏目顺序" width="180"> </el-table-column>
<el-table-column prop="title" label="栏目名称" min-width="200" />
<el-table-column prop="postCount" label="栏目帖子数量" min-width="200" />
<el-table-column prop="color" label="颜色" width="300">
<template #default="{ row }">
<div class="color-cell">
<div class="color-block" :style="{ backgroundColor: row.color }" />
<span class="color-text">{{ row.color }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="createUserId" label="创建人" min-width="200" />
<el-table-column prop="createTime" label="创建时间" min-width="200">
<template #default="{ row }">
{{ dayjs(row.createTime * 1000).format('YYYY-MM-DD HH:mm:ss') }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" min-width="200">
<template #default="{ row }">
<el-switch
:model-value="row.status"
:active-value="1"
:inactive-value="0"
@change="handleStatusChange(row)"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size"
:total="total"
:page-sizes="[10, 20, 30]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="changePageSize"
@current-change="goToPage"
/>
</div>
</div>
<!-- 新增/编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="500px"
:close-on-click-modal="false"
>
<el-form ref="formRef" :model="form" :rules="formRules" label-width="80px">
<el-form-item label="标题" prop="title">
<el-input v-model="form.title" placeholder="请输入标题" />
</el-form-item>
<el-form-item label="颜色" prop="color">
<el-color-picker v-model="form.color" />
<span class="color-value">{{ form.color }}</span>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="form.sort" :min="0" :max="100" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">
<el-icon class="btn-icon"><Upload /></el-icon>
保存
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { Search, Plus, Upload } from '@element-plus/icons-vue'
import { usePageSearch, useResetData } from '@/hooks'
import { listOfCultureColumn, addOrUpdateColumn, deleteColumn, hideColumn } from '@/api/backend'
import type { FormInstance, FormRules } from 'element-plus'
import type { BackendColumnListItemDto, AddOrUpdateColumnDto } from '@/api/backend'
import dayjs from 'dayjs'
const { loading, list, total, reset, goToPage, changePageSize, refresh, searchParams, search } =
usePageSearch(listOfCultureColumn, {
defaultParams: {
type: 'interview',
},
})
// 对话框
const dialogVisible = ref(false)
const dialogTitle = computed(() => (form.value.id ? '编辑标签' : '新增标签'))
const formRef = ref<FormInstance>()
// 表单数据
const [form, resetForm] = useResetData<AddOrUpdateColumnDto>({
title: '',
color: '#000000',
id: undefined,
sort: 0,
type: 'interview',
})
// 表单验证规则
const formRules: FormRules = {
title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
color: [{ required: true, message: '请选择颜色', trigger: 'change' }],
sort: [{ required: true, message: '请输入排序', trigger: 'blur' }],
}
// 新增
const handleAdd = () => {
resetForm()
dialogVisible.value = true
}
// 编辑
const handleEdit = (row: BackendColumnListItemDto) => {
resetForm()
form.value = {
title: row.title,
color: row.color,
id: row.id,
sort: row.sort,
type: 'interview',
}
dialogVisible.value = true
}
// 状态改变
const handleStatusChange = async (row: BackendColumnListItemDto) => {
await hideColumn([row.id])
refresh()
}
// 删除
const handleDelete = async (row: BackendColumnListItemDto) => {
try {
await ElMessageBox.confirm(`确定要删除标签"${row.title}"吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
await deleteColumn([row.id])
ElMessage.success('删除成功')
refresh()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败')
}
}
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
if (form.value.id) {
await addOrUpdateColumn(form.value)
} else {
await addOrUpdateColumn(form.value)
}
ElMessage.success(form.value.id ? '编辑成功' : '新增成功')
dialogVisible.value = false
if (form.value.id) {
search()
} else {
refresh()
}
} catch (error) {
console.error('表单验证失败:', error)
}
}
const selectedRows = ref<BackendColumnListItemDto[]>([])
// 选择
const handleSelectionChange = (selection: BackendColumnListItemDto[]) => {
selectedRows.value = selection
}
// 批量发布/隐藏
const handleBatchPublish = async () => {
await hideColumn(selectedRows.value.map((item) => item.id))
refresh()
selectedRows.value = []
ElMessage.success('发布/隐藏成功')
}
// 批量删除
const handleBatchDelete = async () => {
await deleteColumn(selectedRows.value.map((item) => item.id))
refresh()
selectedRows.value = []
ElMessage.success('删除成功')
}
</script>
<style scoped lang="scss">
.official-tag-page {
height: 100%;
display: flex;
flex-direction: column;
gap: 16px;
}
// 搜索区域
.search-section {
background: #fff;
border-radius: 8px;
padding: 20px;
display: flex;
gap: 12px;
flex-shrink: 0;
.search-select {
width: 200px;
}
}
// 表格区域
.table-section {
flex: 1;
background: #fff;
border-radius: 8px;
padding: 20px;
display: flex;
flex-direction: column;
min-height: 0;
}
.table-wrapper {
flex: 1;
min-height: 0;
.color-cell {
display: flex;
align-items: center;
gap: 12px;
.color-block {
width: 100%;
height: 36px;
border-radius: 4px;
border: 1px solid #e5e7eb;
flex: 1;
}
.color-text {
color: #fff;
font-size: 14px;
font-weight: 500;
position: absolute;
left: 50%;
transform: translateX(-50%);
text-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
}
}
}
.pagination-wrapper {
display: flex;
justify-content: flex-end;
padding-top: 16px;
flex-shrink: 0;
}
// 对话框内的颜色显示
.color-value {
margin-left: 12px;
color: #606266;
font-family: monospace;
}
.btn-icon {
margin-right: 4px;
}
</style>
<!-- views/backend/official/index.vue -->
<template>
<div class="official-tag-page">
<!-- 搜索栏 -->
<div class="search-section">
<el-select
v-model="searchParams.type"
placeholder="请选择类型"
class="search-select"
clearable
>
<el-option label="文化关键词" value="culture" />
<el-option label="年度主推关键词" value="year_recommend" />
<el-option label="关联场景" value="related_scenarios" />
</el-select>
<el-button type="primary" :icon="Search" @click="refresh">搜索</el-button>
<el-button @click="reset">重置</el-button>
<el-button type="primary" class="add-btn" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增
</el-button>
</div>
<!-- 表格区域 -->
<div class="table-section">
<!-- 表格 -->
<div class="table-wrapper">
<el-table v-loading="loading" :data="list" height="100%">
<el-table-column type="selection" width="55"> </el-table-column>
<el-table-column prop="type" label="类型" width="180">
<template #default="{ row }">
{{
row.type === 'culture'
? '文化关键词'
: row.type === 'year_recommend'
? '年度主推关键词'
: '关联场景'
}}
</template>
</el-table-column>
<el-table-column prop="title" label="标题" min-width="200" />
<el-table-column prop="description" label="描述" min-width="200" />
<el-table-column prop="color" label="颜色" width="300">
<template #default="{ row }">
<div class="color-cell">
<div class="color-block" :style="{ backgroundColor: row.color }" />
<span class="color-text">{{ row.color }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size"
:total="total"
:page-sizes="[10, 20, 30]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="changePageSize"
@current-change="goToPage"
/>
</div>
</div>
<!-- 新增/编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="500px"
:close-on-click-modal="false"
>
<el-form ref="formRef" :model="form" :rules="formRules" label-width="80px">
<el-form-item label="类型" prop="type">
<el-select v-model="form.type" placeholder="请选择类型" style="width: 100%">
<el-option label="文化关键词" value="culture" />
<el-option label="年度主推关键词" value="year_recommend" />
<el-option label="关联场景" value="related_scenarios" />
</el-select>
</el-form-item>
<el-form-item label="标题" prop="title">
<el-input v-model="form.title" placeholder="请输入标题" />
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input v-model="form.description" placeholder="请输入描述" />
</el-form-item>
<el-form-item label="颜色" prop="color">
<el-color-picker v-model="form.color" />
<span class="color-value">{{ form.color }}</span>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">
<el-icon class="btn-icon"><Upload /></el-icon>
保存
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { Search, Plus, Upload } from '@element-plus/icons-vue'
import { usePageSearch, useResetData } from '@/hooks'
import { getTagList, addOrUpdateTag, deleteTag } from '@/api/backend'
import type { FormInstance, FormRules } from 'element-plus'
import type { BackendTagListItemDto, AddOrUpdateTagDto } from '@/api/backend'
const { loading, list, total, reset, goToPage, changePageSize, refresh, searchParams, search } =
usePageSearch(getTagList)
// 对话框
const dialogVisible = ref(false)
const dialogTitle = computed(() => (form.value.id ? '编辑标签' : '新增标签'))
const formRef = ref<FormInstance>()
// 表单数据
const [form, resetForm] = useResetData<AddOrUpdateTagDto>({
type: '',
title: '',
description: '',
color: '#000000',
id: undefined,
})
// 表单验证规则
const formRules: FormRules = {
type: [{ required: true, message: '请选择类型', trigger: 'change' }],
title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
description: [{ required: true, message: '请输入描述', trigger: 'blur' }],
color: [{ required: true, message: '请选择颜色', trigger: 'change' }],
}
// 新增
const handleAdd = () => {
resetForm()
dialogVisible.value = true
}
// 编辑
const handleEdit = (row: BackendTagListItemDto) => {
resetForm()
form.value = {
type: row.type,
title: row.title,
description: row.description,
color: row.color,
id: row.id,
}
dialogVisible.value = true
}
// 删除
const handleDelete = async (row: BackendTagListItemDto) => {
try {
await ElMessageBox.confirm(`确定要删除标签"${row.title}"吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
await deleteTag(row.id)
ElMessage.success('删除成功')
refresh()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败')
}
}
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
if (form.value.id) {
await addOrUpdateTag(form.value)
} else {
await addOrUpdateTag(form.value)
}
ElMessage.success(form.value.id ? '编辑成功' : '新增成功')
dialogVisible.value = false
if (form.value.id) {
search()
} else {
refresh()
}
} catch (error) {
console.error('表单验证失败:', error)
}
}
</script>
<style scoped lang="scss">
.official-tag-page {
height: 100%;
display: flex;
flex-direction: column;
gap: 16px;
}
// 搜索区域
.search-section {
background: #fff;
border-radius: 8px;
padding: 20px;
display: flex;
gap: 12px;
flex-shrink: 0;
.search-select {
width: 200px;
}
.add-btn {
margin-left: auto;
}
}
// 表格区域
.table-section {
flex: 1;
background: #fff;
border-radius: 8px;
padding: 20px;
display: flex;
flex-direction: column;
min-height: 0;
}
.table-wrapper {
flex: 1;
min-height: 0;
.color-cell {
display: flex;
align-items: center;
gap: 12px;
.color-block {
width: 100%;
height: 36px;
border-radius: 4px;
border: 1px solid #e5e7eb;
flex: 1;
}
.color-text {
color: #fff;
font-size: 14px;
font-weight: 500;
position: absolute;
left: 50%;
transform: translateX(-50%);
text-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
}
}
}
.pagination-wrapper {
display: flex;
justify-content: flex-end;
padding-top: 16px;
flex-shrink: 0;
}
// 对话框内的颜色显示
.color-value {
margin-left: 12px;
color: #606266;
font-family: monospace;
}
.btn-icon {
margin-right: 4px;
}
</style>
......@@ -8,7 +8,7 @@
<!-- 主要内容区域 -->
<div class="mx-auto py-6">
<!-- 发布问题卡片 -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 mb-6 overflow-hidden">
<div class="bg-white rounded-lg shadow-sm border border-gray-100 mb-6 overflow-hidden">
<div class="p-6">
<!-- 用户头像和输入区域 -->
<div class="flex items-start">
......@@ -118,7 +118,7 @@
<el-card
v-for="(question, index) in questions"
:key="index"
class="question-card !rounded-2xl mb-4"
class="question-card !rounded-lg mb-4"
shadow="hover"
>
<!-- 问题标题 -->
......@@ -239,7 +239,7 @@
<!-- 右侧:分页器 -->
<div class="right">
<div
class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3"
class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-3"
>
<el-pagination
v-model:current-page="currentPage"
......
......@@ -6,7 +6,7 @@
v-for="item in list"
:key="item.id"
class="group bg-white rounded-lg p-4 sm:p-6 cursor-pointer transition-all duration-300 hover:shadow-lg hover:shadow-gray-100 hover:-translate-y-1 border border-gray-100 hover:border-gray-200 mb-3 sm:mb-4"
@click="router.push(`/postDetail/${item.id}`)"
@click="handleClickItem(item)"
>
<div class="flex gap-3 justify-between">
<!-- 内容区域 -->
......@@ -59,12 +59,12 @@
<img
:src="item.faceUrl"
alt="文章配图"
class="w-full h-full object-cover rounded-lg sm:rounded-xl group-hover:scale-105 transition-transform duration-300"
class="w-full h-full object-cover rounded-lg sm:rounded-lg group-hover:scale-105 transition-transform duration-300"
loading="lazy"
/>
<!-- 图片遮罩效果 -->
<div
class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-5 rounded-lg sm:rounded-xl transition-all duration-300"
class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-5 rounded-lg sm:rounded-lg transition-all duration-300"
></div>
</div>
</div>
......@@ -83,7 +83,7 @@
<!-- 右侧:分页器 -->
<div class="right">
<div
class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3"
class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-3"
>
<el-pagination
v-model:current-page="searchParams.current"
......@@ -115,8 +115,8 @@
<script setup lang="ts" name="RecommendList">
import { usePageSearch } from '@/hooks'
import { getArticleList } from '@/api'
import { TABS_REF_KEY } from '@/constants'
import { getArticleList, type ArticleItemDto } from '@/api'
import { TABS_REF_KEY, ArticleTypeEnum } from '@/constants'
import { useScrollTop } from '@/hooks'
import dayjs from 'dayjs'
......@@ -135,6 +135,14 @@ const tabsRef = inject(TABS_REF_KEY)
const { ScrollTopComp, handleBackTop } = useScrollTop(tabsRef!)
const handleClickItem = (item: ArticleItemDto) => {
if (item.type === ArticleTypeEnum.VIDEO) {
router.push(`/videoDetail/${item.id}`)
} else {
router.push(`/postDetail/${item.id}`)
}
}
defineExpose({
refresh: (sortLogic?: number) => {
console.log('sortLogic', sortLogic)
......
<template>
<div class="bg-gray-50/30">
<div>
<!-- tabs -->
<div class="shadow-sm">
<div class="max-w-7xl mx-auto px-4">
......@@ -8,14 +8,14 @@
<div class="flex items-center space-x-1">
<div
v-for="tab in tabs"
:key="tab.key"
:key="tab.sortLogic"
:class="[
'px-6 py-2 rounded-full text-sm font-medium cursor-pointer transition-all duration-200',
activeTab === tab.key
searchParams.sortLogic === tab.sortLogic
? 'bg-orange-400 text-white shadow-md'
: 'text-gray-600 hover:text-orange-500 hover:bg-orange-50',
]"
@click="activeTab = tab.key"
@click="toggleTab(tab.sortLogic)"
>
{{ tab.label }}
</div>
......@@ -23,13 +23,14 @@
</div>
</div>
</div>
<div v-loading="loading">
<!-- 第一页的特殊布局 -->
<template v-if="searchParams.current === 0">
<!-- 前三个特殊布局 -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
<!-- 第一个视频 - 占据两列 -->
<div
class="lg:col-span-2 group relative rounded-2xl overflow-hidden bg-white shadow-sm hover:shadow-xl transition-all duration-500 cursor-pointer"
class="lg:col-span-2 group relative rounded-lg overflow-hidden bg-white shadow-sm hover:shadow-xl transition-all duration-500 cursor-pointer"
>
<div class="relative overflow-hidden">
<img
......@@ -141,7 +142,7 @@
<div class="p-4">
<h3
class="font-semibold text-base mb-2 group-hover:text-blue-600 transition-colors line-clamp-2"
class="font-semibold text-base mb-2 group-hover:text-blue-600 transition-colors line-clamp-1"
>
{{ n === 1 ? '复日奶茶' : 'PS最新版零基础全套' }}
</h3>
......@@ -221,7 +222,7 @@
<div class="p-4">
<h3
class="font-semibold text-base mb-2 group-hover:text-blue-600 transition-colors line-clamp-2"
class="font-semibold text-base mb-2 group-hover:text-blue-600 transition-colors line-clamp-1"
>
{{ item.title }}
</h3>
......@@ -235,7 +236,7 @@
>
{{ item.createUserName }}
</div>
<span class="font-medium">UP主{{ item.createUserName }}</span>
<span class="font-medium">{{ item.createUserName }}</span>
</div>
<span class="bg-gray-100 px-2 py-1 rounded-full">{{
dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm')
......@@ -298,7 +299,7 @@
</div>
</div>
<!-- 播放按钮 -->
<div
<!-- <div
class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300"
>
<div
......@@ -306,26 +307,19 @@
>
<el-icon size="50" color="#333"><VideoPlay /></el-icon>
</div>
</div>
</div> -->
</div>
<div class="p-4">
<h3
class="font-semibold text-base mb-2 group-hover:text-blue-600 transition-colors line-clamp-2"
class="font-semibold text-base mb-2 group-hover:text-blue-600 transition-colors line-clamp-1"
>
{{ item.title }}
</h3>
<p class="text-gray-600 text-sm mb-3 line-clamp-2 leading-relaxed">
{{ item.content }}
</p>
<div class="flex items-center justify-between text-gray-500 text-xs">
<div class="flex items-center gap-2">
<div
class="w-6 h-6 bg-gradient-to-r from-green-400 to-blue-500 rounded-full flex items-center justify-center text-white text-xs font-bold"
>
{{ item.createUserName }}
</div>
<span class="font-medium">UP主{{ item.createUserName }}</span>
<img :src="item.showAvatar" alt="" class="w-6 h-6 rounded-full object-cover" />
<span class="font-medium">{{ item.showName }}</span>
</div>
<span>{{ dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm') }}</span>
</div>
......@@ -336,7 +330,7 @@
<!-- 底部分页 -->
<div class="bottom-pagination backdrop-blur-8 border-t border-gray-200">
<div class="max-w-7xl mx-auto px-8 py-6">
<div class="max-w-7xl mx-auto py-6">
<div class="flex items-center justify-between">
<!-- 左侧:回到顶部按钮 -->
<div class="left">
......@@ -346,7 +340,7 @@
<!-- 右侧:分页器 -->
<div class="right">
<div
class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3"
class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-3"
>
<el-pagination
v-model:current-page="searchParams.current"
......@@ -355,6 +349,12 @@
layout="prev, pager, next, jumper, total"
:total="total"
class="custom-pagination"
@current-change="
(e) => {
;(handleBackTop(), goToPage(e))
}
"
@size-change="changePageSize"
/>
</div>
</div>
......@@ -362,6 +362,7 @@
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts" name="RecommendList">
......@@ -374,25 +375,27 @@ import dayjs from 'dayjs'
const tabsRef = inject(TABS_REF_KEY)
const router = useRouter()
const { ScrollTopComp, handleBackTop } = useScrollTop(tabsRef!)
const { list, total, searchParams, loading, goToPage, changePageSize, refresh } = usePageSearch(
getArticleList,
{
defaultParams: { type: ArticleTypeEnum.VIDEO },
defaultParams: { type: ArticleTypeEnum.VIDEO, sortLogic: 0 },
defaultCurrent: 1,
defaultSize: 12,
immediate: false,
},
)
// Tabs 配置
const router = useRouter()
const tabs = [
{ key: 'latest', label: '最新发布' },
{ key: 'popular', label: '最多模仿' },
{ key: 'favorites', label: '最多收藏' },
{ label: '最多播放', sortLogic: 0 },
{ label: '最新发布', sortLogic: 1 },
]
const activeTab = ref('latest')
const toggleTab = (sortLogic: number) => {
searchParams.value.sortLogic = sortLogic
refresh()
}
const goVideoDetail = (n: number) => {
router.push(`/videoDetail?id=${n}`)
......
......@@ -18,7 +18,7 @@
@click="toggleTab(tab)"
>
<div
class="flex items-center gap-2 px-12 py-2.5 rounded-xl transition-all duration-300"
class="flex items-center gap-2 px-12 py-2.5 rounded-lg transition-all duration-300"
:class="{
'bg-#fffdfd shadow-[inset_0_2px_4px_0_rgb(0,0,0,0.1)]': activeTab === tab.name,
'hover:bg-white/60': activeTab !== tab.name,
......@@ -204,19 +204,20 @@
<div
class="flex flex-col items-start justify-center ml-2 sm:ml-3 min-w-0 flex-1"
>
<div class="text-14px truncate w-full font-medium mb-1">
<el-tooltip :content="item.description" placement="top">
<div class="text-14px truncate w-full font-medium mb-1 cursor-pointer">
{{ item.title }}({{ item.currentCount }}/{{ item.limitCount }})
</div>
</el-tooltip>
<div class="color-#333 text-xs w-full flex items-center flex-nowrap">
<svg-icon name="small_coin" size="16" class="mr-1" />
<el-tooltip :content="item.description" placement="top">
<div class="truncate w-130px cursor-pointer">
<div class="truncate w-130px">
+{{ item.rewardValue }}亚币{{
TaskDateLimitTypeText[item.limitType] &&
`(${TaskDateLimitTypeText[item.limitType] + '刷新'})`
}}
</div>
</el-tooltip>
</div>
</div>
</div>
......@@ -292,6 +293,13 @@ const tabs = [
const activeTab = ref(
tabs.find((item) => item.path === route.path.split('/').at(-1))?.name || '首页',
)
watch(
() => route.path,
(newPath) => {
activeTab.value = tabs.find((item) => item.path === newPath.split('/').at(-1))?.name || '首页'
},
)
const toggleTab = (tab: { name: string; path: string }) => {
activeTab.value = tab.name
router.push(`/homePage/${tab.path}`)
......@@ -349,46 +357,52 @@ const userRecordData = ref({} as UserRecordDataDto)
const onDailySign = async () => {
await dailySign()
await refreshTaskData(true)
}
const handleTask = (item: TaskItemDto) => {
if (item.currentCount === item.limitCount) return
if ((item.taskKey = 'VALID_COMMENT')) {
router.push(`/homePage/homeTab`)
} else {
console.log(item)
// if (item.svgName === 'svgName') {
handleBackTop()
triggerAnimation()
}
// }
}
const initPage = () => {
Promise.allSettled([
getCarouselList(),
getTaskList(),
getUserAccountData(),
getRecordData(),
]).then(([r1, r2, r3, r4]) => {
Promise.allSettled([getCarouselList(), getUserAccountData(), getRecordData()]).then(
([r1, r2, r3]) => {
if (r1.status === 'fulfilled') {
carouselList.value = r1.value.data
}
if (r2.status === 'fulfilled') {
regularTaskList.value = r2.value.data.filter(
(item) => item.taskType === TaskTypeEnum.REGULAR_TASK,
)
specialTaskList.value = r2.value.data.filter(
(item) => item.taskType === TaskTypeEnum.SPECIAL_TASK,
)
userAccountData.value = r2.value.data
}
if (r3.status === 'fulfilled') {
console.log(r3)
userAccountData.value = r3.value.data
userRecordData.value = r3.value.data
}
if (r4.status === 'fulfilled') {
console.log(r4)
userRecordData.value = r4.value.data
},
)
}
const refreshTaskData = async (refreshRecordData = false) => {
if (refreshRecordData) {
const { data } = await getRecordData()
userRecordData.value = data
}
})
const { data } = await getTaskList()
regularTaskList.value = data.filter((item) => item.taskType === TaskTypeEnum.REGULAR_TASK)
specialTaskList.value = data.filter((item) => item.taskType === TaskTypeEnum.SPECIAL_TASK)
}
onActivated(() => {
refreshTaskData(false)
})
onMounted(() => {
initPage()
})
......
......@@ -83,7 +83,7 @@
<!-- 右侧:分页器 -->
<div class="right">
<div
class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3"
class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-3"
>
<el-pagination
v-model:current-page="searchParams.current"
......@@ -114,7 +114,7 @@
</template>
<script setup lang="ts">
import { ArrowRight, View, ChatDotRound, Star } from '@element-plus/icons-vue'
import { View, ChatDotRound, Star } from '@element-plus/icons-vue'
import { getColumnList } from '@/api'
import { usePageSearch, useScrollTop } from '@/hooks'
import { TABS_REF_KEY } from '@/constants'
......@@ -124,12 +124,19 @@ const router = useRouter()
const tabsRef = inject(TABS_REF_KEY)
const { handleBackTop, ScrollTopComp } = useScrollTop(tabsRef!)
const { list, total, searchParams, goToPage, changePageSize, loading } = usePageSearch(
const { list, total, searchParams, goToPage, changePageSize, loading, refresh } = usePageSearch(
getColumnList,
{
defaultSize: 3,
immediate: false,
},
)
defineExpose({
refresh: () => {
searchParams.value.current = 0
refresh()
},
})
</script>
<style scoped></style>
......@@ -85,7 +85,7 @@
<!-- 右侧:分页器 -->
<div class="right">
<div
class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3"
class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-3"
>
<el-pagination
v-model:current-page="searchParams.current"
......@@ -126,12 +126,18 @@ const router = useRouter()
const tabsRef = inject(TABS_REF_KEY)
const { handleBackTop, ScrollTopComp } = useScrollTop(tabsRef!)
const { list, total, searchParams, goToPage, changePageSize, loading } = usePageSearch(
const { list, total, searchParams, goToPage, changePageSize, loading, refresh } = usePageSearch(
getInterviewList,
{
defaultSize: 3,
immediate: false,
},
)
defineExpose({
refresh: () => {
refresh()
},
})
</script>
<style scoped></style>
<template>
<div class="min-h-screen">
<div>
<!-- 发布区域 -->
<div class="bg-white p-6 mb-6 rounded-lg shadow-sm">
<div class="flex-1 bg-white rounded-lg border border-gray-200">
......@@ -116,10 +116,10 @@
<el-tag
v-for="tag in tagList"
:key="tag.id"
:type="activeTag === tag.id ? 'primary' : 'info'"
:effect="activeTag === tag.id ? 'dark' : 'plain'"
:type="searchParams.tagIdList?.includes(tag.id) ? 'primary' : 'info'"
:effect="searchParams.tagIdList?.includes(tag.id) ? 'dark' : 'plain'"
class="cursor-pointer"
@click="activeTag = tag.id"
@click="toggleTag(tag.id)"
>
{{ tag.title }}
</el-tag>
......@@ -133,7 +133,7 @@
<div class="flex items-center gap-2">
<div class="w-1 h-6 bg-red-500 rounded"></div>
<h2 class="text-lg font-medium">
{{ tagList.find((tag) => tag.id === activeTag)?.title ?? '最新' }}
{{ tagList.find((tag) => searchParams.tagIdList?.includes(tag.id))?.title ?? '最新' }}
</h2>
</div>
<div
......@@ -143,6 +143,7 @@
查看更多 >>
</div>
</div>
<el-divider />
<!-- 动态列表 -->
<div class="divide-y divide-gray-100">
......@@ -174,11 +175,6 @@
</div>
</div>
<!-- 无图片的内容 -->
<!-- <div v-else class="text-gray-600 text-sm leading-relaxed mb-3">
{{ item.content }}
</div> -->
<!-- 互动数据 -->
<div class="flex items-center gap-4 text-gray-400 text-sm">
<div class="flex items-center gap-1">
......@@ -218,7 +214,7 @@
<!-- 右侧:分页器 -->
<div class="right">
<div
class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3"
class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-3"
>
<el-pagination
v-model:current-page="searchParams.current"
......@@ -253,18 +249,16 @@ import { useTagsStore } from '@/stores/tags'
const tagsStore = useTagsStore()
const { tagList } = storeToRefs(tagsStore)
const activeTag = ref(tagList.value[0]?.id ?? 0)
const router = useRouter()
const tabsRef = inject(TABS_REF_KEY)
const { userInfo } = storeToRefs(useUserStore())
// 标签数据
const filterOptions = ref([
{ title: '最', id: 0 },
{ title: '最', id: 1 },
{ title: '最观看', id: 2 },
{ title: '最', id: 0 },
{ title: '最', id: 1 },
{ title: '最观看', id: 2 },
])
const tagInput = ref('')
......@@ -273,9 +267,10 @@ const { handleBackTop, ScrollTopComp } = useScrollTop(tabsRef!)
const { list, total, searchParams, goToPage, changePageSize, refresh } = usePageSearch(
getPracticeList,
{
defaultParams: { sortLogic: filterOptions.value[0]?.id },
defaultParams: { sortLogic: filterOptions.value[0]?.id, tagIdList: [] },
defaultCurrent: 1,
defaultSize: 5,
immediate: false,
},
)
......@@ -284,6 +279,17 @@ const toggleFilter = (id: number) => {
refresh()
handleBackTop()
}
const toggleTag = (id: number) => {
searchParams.value.tagIdList = [id]
refresh()
handleBackTop()
}
defineExpose({
refresh: () => {
refresh()
},
})
</script>
<style scoped></style>
......@@ -5,40 +5,64 @@
<div class="left flex gap-3 flex items-center">
<Tabs v-model="activeTab" :tabs="tabs" />
<!-- 刷新图标 -->
<el-icon size="15" class="cursor-pointer hover:rotate-180 transition-all duration-300"
<el-icon
size="15"
class="cursor-pointer hover:rotate-180 transition-all duration-300"
@click="handleRefresh"
><Refresh
/></el-icon>
</div>
</div>
<el-divider style="margin: 10px 0 20px 0" />
<transition name="fade" mode="out-in" @enter="handleEnter">
<keep-alive>
<component :is="curretComponent" />
<component ref="activeTabComponentRef" :is="activeTabComponent" />
</keep-alive>
</transition>
</div>
</template>
<script setup lang="ts" name="CultureAsk">
import { ref } from 'vue'
import Tabs from '@/components/common/Tabs'
import { Refresh } from '@element-plus/icons-vue'
import ColumnList from './components/columnList.vue'
import InterviewList from './components/interviewList.vue'
import PracticeList from './components/practiceList.vue'
const curretComponent = computed(() => {
return {
专栏: ColumnList,
专访: InterviewList,
实践: PracticeList,
}[activeTab.value]
})
const activeTab = ref('专栏')
const tabs = [
{ label: '专栏', value: '专栏' },
{ label: '实践', value: '实践' },
{ label: '专访', value: '专访' },
{ label: '视频', value: '视频' },
{ label: '专栏', value: '专栏', component: ColumnList },
{ label: '实践', value: '实践', component: PracticeList },
{ label: '专访', value: '专访', component: InterviewList },
{ label: '视频', value: '视频', component: () => h('h1', '11') },
]
const activeTab = ref('专栏')
const activeTabComponent = computed(() => {
return tabs.find((tab) => tab.value === activeTab.value)?.component
})
const activeTabComponentRef =
useTemplateRef<InstanceType<typeof ColumnList>>('activeTabComponentRef')
const handleEnter = () => {
handleRefresh()
}
const handleRefresh = () => {
activeTabComponentRef.value?.refresh?.()
}
onMounted(() => {
handleRefresh()
})
</script>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateX(30px);
filter: blur(4px);
}
.fade-enter-active,
.fade-leave-active {
transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
</style>
......@@ -24,7 +24,7 @@ export default function ExchangeContent(
<div class="flex justify-center mb-8">
<div class="relative">
<div class="w-32 h-32 bg-gradient-to-br from-orange-100 to-pink-100 rounded-3xl flex items-center justify-center shadow-lg">
<div class="w-20 h-20 bg-white rounded-2xl flex items-center justify-center shadow-sm">
<div class="w-20 h-20 bg-white rounded-lg flex items-center justify-center shadow-sm">
<img src={ask} alt={item.name} class="w-16 h-16 object-contain" />
</div>
</div>
......@@ -51,7 +51,7 @@ export default function ExchangeContent(
{/* 办公点选择和数量 */}
{item.itemType === ShopGoodsTypeEnum.REAL_GOODS && (
<div class=" rounded-2xl px-5 mx-2 space-y-4">
<div class=" rounded-lg px-5 mx-2 space-y-4">
{/* 办公点选择 */}
<div>
<label class="text-gray-700 text-sm font-medium mb-2 block">办公点</label>
......
......@@ -3,7 +3,7 @@
<div class="max-w-[1440px] mx-auto">
<!-- 顶部积分卡片 -->
<div
class="bg-gradient-to-r from-purple-50 to-blue-50 rounded-2xl p-5 shadow-sm mb-8 border border-purple-100"
class="bg-gradient-to-r from-purple-50 to-blue-50 rounded-lg p-5 shadow-sm mb-8 border border-purple-100"
>
<div class="flex justify-between items-center flex-wrap gap-4">
<div class="flex items-baseline gap-3">
......@@ -40,7 +40,7 @@
<div
v-for="item in virtualGoodsList"
:key="item.id"
class="group bg-gradient-to-br from-purple-50 to-pink-50 rounded-2xl p-5 flex flex-col items-center hover:shadow-xl transition-all duration-300 cursor-pointer border border-transparent hover:border-purple-200 hover:-translate-y-1"
class="group bg-gradient-to-br from-purple-50 to-pink-50 rounded-lg p-5 flex flex-col items-center hover:shadow-xl transition-all duration-300 cursor-pointer border border-transparent hover:border-purple-200 hover:-translate-y-1"
@click="onExchangeGoods(item)"
>
<div class="w-24 h-24 mb-3 flex items-center justify-center">
......@@ -65,7 +65,7 @@
<!-- 分页 -->
<div class="flex justify-end mt-6">
<div class="bg-gray-50 rounded-xl shadow-sm border border-gray-200 p-3">
<div class="bg-gray-50 rounded-lg shadow-sm border border-gray-200 p-3">
<el-pagination
size="small"
v-model:current-page="virtualGoodsSearchParams.current"
......@@ -111,7 +111,7 @@
<div
v-for="item in realGoodsList"
:key="item.id"
class="group bg-gradient-to-br from-blue-50 to-indigo-50 rounded-2xl p-5 flex flex-col items-center hover:shadow-xl transition-all duration-300 cursor-pointer border border-transparent hover:border-blue-200 hover:-translate-y-1"
class="group bg-gradient-to-br from-blue-50 to-indigo-50 rounded-lg p-5 flex flex-col items-center hover:shadow-xl transition-all duration-300 cursor-pointer border border-transparent hover:border-blue-200 hover:-translate-y-1"
@click="onExchangeGoods(item)"
>
<div class="w-24 h-24 mb-3 flex items-center justify-center">
......@@ -136,7 +136,7 @@
<!-- 分页 -->
<div class="flex justify-end mt-6">
<div class="bg-gray-50 rounded-xl shadow-sm border border-gray-200 p-3">
<div class="bg-gray-50 rounded-lg shadow-sm border border-gray-200 p-3">
<el-pagination
v-model:current-page="realGoodsSearchParams.current"
v-model:page-size="realGoodsSearchParams.size"
......
......@@ -40,14 +40,14 @@ import type { ArticleItemDto } from '@/api'
import type { Component } from 'vue'
import { useScrollTop } from '@/hooks'
import { addOrCanceArticlelCollect, addOrCanceArticlelLike } from '@/api'
import { COMMENT_REF_KEY } from '@/constants'
const modelValue = defineModel<ArticleItemDto>('modelValue', { required: true })
const commentRef = inject(COMMENT_REF_KEY)
const emit = defineEmits<{
(e: 'scrollToCommentBox'): void
}>()
const { ScrollTopComp } = useScrollTop(window)
const { handleBackTop } = useScrollTop(commentRef!)
interface StatItem {
icon: Component
......@@ -100,7 +100,9 @@ const stats = computed(() => {
count: modelValue.value?.replyCount ?? 0,
label: '评论',
// active: modelValue.value?.replyCount > 0,
actionFn: handleBackTop,
actionFn: () => {
emit('scrollToCommentBox')
},
},
]
})
......
......@@ -2,7 +2,7 @@
<div class="min-h-screen bg-gradient-to-br">
<div class="max-w-4xl mx-auto">
<!-- 主表单卡片 -->
<div class="bg-white rounded-2xl shadow-lg p-8">
<div class="bg-white rounded-lg shadow-lg p-8">
<el-form ref="formRef" :model="form" label-position="top" size="default">
<!-- 案例编号 -->
<div class="mb-6 flex items-center gap-2">
......@@ -22,7 +22,6 @@
placeholder="请输入【案例】标题"
:maxlength="100"
show-word-limit
size="large"
class="text-lg"
/>
</el-form-item>
......@@ -57,12 +56,12 @@
</el-form-item>
<!-- 是否同步发布 -->
<!-- <el-form-item label="*是否同步发布到实践" class="mb-6">
<el-form-item label="*是否同步发布到实践" class="mb-6">
<el-radio-group v-model="form.publishToPractice">
<el-radio :label="true"></el-radio>
<el-radio :label="false"></el-radio>
</el-radio-group>
</el-form-item> -->
</el-form-item>
<!-- 发布时间 -->
<!-- <el-form-item label="*发布时间" class="mb-8">
......@@ -82,11 +81,9 @@
<!-- 底部按钮组 -->
<div class="flex items-center justify-between gap-4 pt-4">
<div class="flex gap-4">
<el-button size="large" @click="handleCancel"> 取消 </el-button>
<el-button size="large" @click="handlePreview"> 预览 </el-button>
<el-button size="large" type="info" plain @click="handleSaveDraft">
存草稿
</el-button>
<el-button @click="handleCancel"> 取消 </el-button>
<el-button @click="handlePreview"> 预览 </el-button>
<el-button type="info" plain @click="handleSaveDraft"> 存草稿 </el-button>
</div>
<el-button type="primary" @click="handleSubmit"> 提交 </el-button>
</div>
......
......@@ -6,7 +6,7 @@
<div class="col-span-8 space-y-6">
<!-- 视频上传区域 -->
<div
class="bg-white backdrop-blur-sm rounded-2xl shadow-lg border border-white/20 p-8 hover:shadow-xl transition-all duration-300"
class="bg-white backdrop-blur-sm rounded-lg shadow-lg border border-white/20 p-8 hover:shadow-xl transition-all duration-300"
>
<div class="flex items-center justify-between mb-6">
<h3 class="text-xl font-bold text-gray-800">上传视频</h3>
......@@ -17,7 +17,7 @@
</div>
<!-- 基本设置 -->
<div
class="bg-white backdrop-blur-sm rounded-2xl shadow-lg border border-white/20 p-8 hover:shadow-xl transition-all duration-300"
class="bg-white backdrop-blur-sm rounded-lg shadow-lg border border-white/20 p-8 hover:shadow-xl transition-all duration-300"
>
<div class="flex items-center justify-between mb-6">
<h3 class="text-xl font-bold text-gray-800">基本设置</h3>
......@@ -30,7 +30,7 @@
<!-- 主封面 -->
<div class="relative group">
<div
class="w-48 h-28 bg-gradient-to-br from-gray-100 to-gray-200 rounded-xl overflow-hidden shadow-md hover:shadow-lg transition-all duration-300"
class="w-48 h-28 bg-gradient-to-br from-gray-100 to-gray-200 rounded-lg overflow-hidden shadow-md hover:shadow-lg transition-all duration-300"
>
<img
src="@/assets/img/culture/ask.png"
......@@ -126,12 +126,12 @@
</div>
</div>
<div
class="bg-white backdrop-blur-sm rounded-2xl shadow-lg border border-white/20 p-6 hover:shadow-xl transition-all duration-300"
class="bg-white backdrop-blur-sm rounded-lg shadow-lg border border-white/20 p-6 hover:shadow-xl transition-all duration-300"
>
<h4 class="text-lg font-bold text-gray-800 mb-4">发布设置</h4>
<div class="space-y-4">
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-xl">
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div>
<div class="text-sm font-medium text-gray-800">定时发布</div>
<div class="text-xs text-gray-500">设置发布时间</div>
......@@ -160,9 +160,7 @@
</div>
</div>
</div>
<div
class="bg-white/70 backdrop-blur-sm rounded-2xl shadow-lg border border-white/20 p-6"
>
<div class="bg-white/70 backdrop-blur-sm rounded-lg shadow-lg border border-white/20 p-6">
<div class="space-y-3 flex justify-center items-center gap-4">
<el-button
size="large"
......@@ -188,7 +186,7 @@
</el-form>
<!-- 右上角tips -->
<!-- <div
class="w-200px fixed bottom-0 left-0 bg-gradient-to-br from-amber-50 to-orange-50 rounded-2xl p-6 border border-amber-200"
class="w-200px fixed bottom-0 left-0 bg-gradient-to-br from-amber-50 to-orange-50 rounded-lg p-6 border border-amber-200"
>
<div class="flex items-start gap-3">
<el-icon class="text-amber-500 mt-1"><Warning /></el-icon>
......@@ -246,6 +244,8 @@ const tansformData = () => {
sort: index,
})),
mainTagId: Number(form.value.mainTagId),
faceUrl:
'https://soundasia.oss-cn-shenzhen.aliyuncs.com/OA/readName/png/2025/11/21/Common/1763710823097.png',
}
}
const handleSubmit = async () => {
......
......@@ -88,7 +88,7 @@
v-if="list.length"
class="flex items-center justify-end px-6 py-4 border-t border-gray-200"
>
<div class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3">
<div class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-3">
<el-pagination
v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size"
......
......@@ -43,7 +43,7 @@
v-if="list.length"
class="flex items-center justify-end px-6 py-4 border-t border-gray-200"
>
<div class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3">
<div class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-3">
<el-pagination
v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size"
......
<template>
<div class="flex-1 flex flex-col" v-loading="loading">
<!-- List Container -->
<div class="flex-1 p-4 pt-1">
<el-tabs v-model="searchParams.type" @tab-change="toggleTab">
<el-tab-pane
......@@ -16,7 +15,6 @@
<el-icon class="text-2xl text-gray-300"><Document /></el-icon>
</div>
<div class="text-gray-500 text-lg mb-2">暂无内容</div>
<div class="text-gray-400 text-sm">{{ getEmptyText() }}</div>
</div>
<div v-else class="space-y-4">
......@@ -25,7 +23,6 @@
:key="item.id"
class="flex items-center p-2 rounded-lg hover:bg-gray-100 transition-colors cursor-pointer"
>
<!-- Content -->
<div class="flex-1 min-w-0">
<div class="text-gray-900 font-medium truncate">{{ item.title }}</div>
<div class="text-gray-500 text-sm mt-1 truncate">
......@@ -39,10 +36,8 @@
</div>
</div>
<!-- Meta Info -->
<div class="flex items-center text-gray-400 text-sm ml-4">
<el-button type="primary" link>编辑</el-button>
<el-button type="danger" link>删除</el-button>
<el-button type="primary" link @click="handleView(item)">查看</el-button>
</div>
</div>
</div>
......@@ -52,7 +47,7 @@
v-if="list.length"
class="flex items-center justify-end px-6 py-4 border-t border-gray-200"
>
<div class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3">
<div class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-3">
<el-pagination
v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size"
......@@ -76,13 +71,16 @@ import { usePageSearch } from '@/hooks'
import { articleTypeListOptions } from '@/constants/options'
import dayjs from 'dayjs'
import { ArticleTypeEnum } from '@/constants/enums'
import type { TabPaneName } from 'element-plus'
import type { SelfCollectDetailDto } from '@/api'
const toggleTab = (key: string) => {
searchParams.value.type = key
const router = useRouter()
const toggleTab = (key: TabPaneName) => {
searchParams.value.type = key as ArticleTypeEnum
refresh()
}
// State
const { list, loading, searchParams, total, refresh, goToPage, changePageSize } = usePageSearch(
getSelfCollectList,
{
......@@ -92,22 +90,12 @@ const { list, loading, searchParams, total, refresh, goToPage, changePageSize }
},
)
// Computed
const paginatedList = computed(() => {
const start = (searchParams.value.current - 1) * searchParams.value.size
const end = start + searchParams.value.size
return list.value.slice(start, end)
})
const getEmptyText = () => {
const emptyTexts: Record<string, string> = {
posts: '还没有发布任何帖子',
videos: '还没有上传任何视频',
questions: '还没有提出任何问题',
articles: '还没有发表任何专栏文章',
practice: '还没有分享任何实践经验',
interviews: '还没有参与任何专访',
const handleView = (item: SelfCollectDetailDto) => {
console.log(item)
if (item.type === ArticleTypeEnum.VIDEO) {
router.push(`/videoDetail/${item.id}`)
} else {
router.push(`/postDetail/${item.id}`)
}
return emptyTexts[searchParams.type] || '暂无数据'
}
</script>
......@@ -52,7 +52,7 @@
v-if="list.length"
class="flex items-center justify-end px-6 py-4 border-t border-gray-200"
>
<div class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3">
<div class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-3">
<el-pagination
v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size"
......
......@@ -52,7 +52,7 @@
v-if="list.length"
class="flex items-center justify-end px-6 py-4 border-t border-gray-200"
>
<div class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3">
<div class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-3">
<el-pagination
v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size"
......
......@@ -52,7 +52,7 @@
v-if="list.length"
class="flex items-center justify-end px-6 py-4 border-t border-gray-200"
>
<div class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3">
<div class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-3">
<el-pagination
v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size"
......
<template>
<div class="flex-1 flex flex-col" v-loading="loading">
<!-- List Container -->
<div class="flex-1 p-4 pt-1">
<el-tabs v-model="searchParams.type" @tab-change="toggleTab">
<el-tab-pane
......@@ -16,7 +15,6 @@
<el-icon class="text-2xl text-gray-300"><Document /></el-icon>
</div>
<div class="text-gray-500 text-lg mb-2">暂无内容</div>
<div class="text-gray-400 text-sm">{{ getEmptyText() }}</div>
</div>
<div v-else class="space-y-4">
......@@ -25,7 +23,6 @@
:key="item.id"
class="flex items-center p-2 rounded-lg hover:bg-gray-100 transition-colors cursor-pointer"
>
<!-- Content -->
<div class="flex-1 min-w-0">
<div class="text-gray-900 font-medium truncate">{{ item.title }}</div>
<div class="text-gray-500 text-sm mt-1 truncate">
......@@ -39,10 +36,8 @@
</div>
</div>
<!-- Meta Info -->
<div class="flex items-center text-gray-400 text-sm ml-4">
<el-button type="primary" link>编辑</el-button>
<el-button type="danger" link>删除</el-button>
<el-button type="primary" link @click="handleView(item)">查看</el-button>
</div>
</div>
</div>
......@@ -52,7 +47,7 @@
v-if="list.length"
class="flex items-center justify-end px-6 py-4 border-t border-gray-200"
>
<div class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3">
<div class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-3">
<el-pagination
v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size"
......@@ -70,19 +65,21 @@
<script lang="ts" setup>
import { Document } from '@element-plus/icons-vue'
import type { TabPaneName } from 'element-plus'
import { getSelfPraiseList } from '@/api'
import { usePageSearch } from '@/hooks'
import { articleTypeListOptions } from '@/constants/options'
import dayjs from 'dayjs'
import { ArticleTypeEnum } from '@/constants/enums'
import type { SelfPraiseDetailDto } from '@/api/user/types'
const router = useRouter()
const toggleTab = (key: string) => {
searchParams.value.type = key
const toggleTab = (key: TabPaneName) => {
searchParams.value.type = key as ArticleTypeEnum
refresh()
}
// State
const { list, loading, searchParams, total, refresh, goToPage, changePageSize } = usePageSearch(
getSelfPraiseList,
{
......@@ -92,22 +89,11 @@ const { list, loading, searchParams, total, refresh, goToPage, changePageSize }
},
)
// Computed
const paginatedList = computed(() => {
const start = (searchParams.value.current - 1) * searchParams.value.size
const end = start + searchParams.value.size
return list.value.slice(start, end)
})
const getEmptyText = () => {
const emptyTexts: Record<string, string> = {
posts: '还没有发布任何帖子',
videos: '还没有上传任何视频',
questions: '还没有提出任何问题',
articles: '还没有发表任何专栏文章',
practice: '还没有分享任何实践经验',
interviews: '还没有参与任何专访',
const handleView = (item: SelfPraiseDetailDto) => {
if (item.type === ArticleTypeEnum.VIDEO) {
router.push(`/videoDetail/${item.id}`)
} else {
router.push(`/postDetail/${item.id}`)
}
return emptyTexts[searchParams.type] || '暂无数据'
}
</script>
<template>
<div class="flex-1 flex flex-col" v-loading="loading">
<!-- List Container -->
<div class="flex-1 p-4 pt-1">
<el-tabs v-model="searchParams.type" @tab-change="toggleTab">
<el-tab-pane
......@@ -16,7 +15,6 @@
<el-icon class="text-2xl text-gray-300"><Document /></el-icon>
</div>
<div class="text-gray-500 text-lg mb-2">暂无内容</div>
<div class="text-gray-400 text-sm">{{ getEmptyText() }}</div>
</div>
<div v-else class="space-y-4">
......@@ -25,7 +23,6 @@
:key="item.id"
class="flex items-center p-2 rounded-lg hover:bg-gray-100 transition-colors cursor-pointer"
>
<!-- Content -->
<div class="flex-1 min-w-0">
<div class="text-gray-900 font-medium truncate">{{ item.title }}</div>
<div class="text-gray-500 text-sm mt-1 truncate">
......@@ -39,9 +36,8 @@
</div>
</div>
<!-- Meta Info -->
<div class="flex items-center text-gray-400 text-sm ml-4">
<el-button type="primary" link>编辑</el-button>
<el-button type="primary" link @click="handleView(item)">查看</el-button>
<el-button type="danger" link>删除</el-button>
</div>
</div>
......@@ -52,7 +48,7 @@
v-if="list.length"
class="flex items-center justify-end px-6 py-4 border-t border-gray-200"
>
<div class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3">
<div class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-3">
<el-pagination
v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size"
......@@ -70,19 +66,20 @@
<script lang="ts" setup>
import { Document } from '@element-plus/icons-vue'
import { getSelfPublishList } from '@/api'
import { usePageSearch } from '@/hooks'
import { articleTypeListOptions } from '@/constants/options'
import dayjs from 'dayjs'
import { ArticleTypeEnum } from '@/constants/enums'
import type { SelfPublishDetailDto } from '@/api'
import type { TabPaneName } from 'element-plus'
const router = useRouter()
const toggleTab = (key: string) => {
searchParams.value.type = key
const toggleTab = (key: TabPaneName) => {
searchParams.value.type = key as ArticleTypeEnum
refresh()
}
// State
const { list, loading, searchParams, total, refresh, goToPage, changePageSize } = usePageSearch(
getSelfPublishList,
{
......@@ -92,22 +89,11 @@ const { list, loading, searchParams, total, refresh, goToPage, changePageSize }
},
)
// Computed
const paginatedList = computed(() => {
const start = (searchParams.value.current - 1) * searchParams.value.size
const end = start + searchParams.value.size
return list.value.slice(start, end)
})
const getEmptyText = () => {
const emptyTexts: Record<string, string> = {
posts: '还没有发布任何帖子',
videos: '还没有上传任何视频',
questions: '还没有提出任何问题',
articles: '还没有发表任何专栏文章',
practice: '还没有分享任何实践经验',
interviews: '还没有参与任何专访',
const handleView = (item: SelfPublishDetailDto) => {
if (item.type === ArticleTypeEnum.VIDEO) {
router.push(`/videoDetail/${item.id}`)
} else {
router.push(`/postDetail/${item.id}`)
}
return emptyTexts[searchParams.type] || '暂无数据'
}
</script>
......@@ -16,7 +16,6 @@
<el-icon class="text-2xl text-gray-300"><Document /></el-icon>
</div>
<div class="text-gray-500 text-lg mb-2">暂无内容</div>
<div class="text-gray-400 text-sm">{{ getEmptyText() }}</div>
</div>
<div v-else class="space-y-4">
......@@ -25,24 +24,27 @@
:key="item.id"
class="flex items-center p-2 rounded-lg hover:bg-gray-100 transition-colors cursor-pointer"
>
<!-- Content -->
<div class="flex-1 min-w-0">
<div class="text-gray-900 font-medium truncate">{{ item.title }}</div>
<div class="text-gray-900 font-medium truncate">
{{ item.title }}({{ item.currentCount }}/{{ item.limitCount }})
</div>
<div class="text-gray-500 text-sm mt-1 truncate">
<span class="mr-2">{{ item.description }}</span>
<span class="mr-2"> +{{ item.rewardValue }}亚币</span>
<span class="mr-2">
{{ dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm:ss') }}
{{
TaskDateLimitTypeText[item.limitType] &&
`${TaskDateLimitTypeText[item.limitType] + '刷新'}`
}}
</span>
<span class="mr-2">浏览 {{ item.viewCount }}</span>
<span class="mr-2">点赞 {{ item.replyCount }}</span>
<span class="mr-2">评论 {{ item.collectionCount }}</span>
<span class="mr-2">收藏 {{ item.praiseCount }}</span>
</div>
</div>
<!-- Meta Info -->
<div class="flex items-center text-gray-400 text-sm ml-4">
<el-button type="primary" link>编辑</el-button>
<el-button type="danger" link>删除</el-button>
<el-button v-if="item.currentCount === item.limitCount" type="info" link disabled
>已完成</el-button
>
<el-button v-else type="primary" link>去完成</el-button>
</div>
</div>
</div>
......@@ -52,7 +54,7 @@
v-if="list.length"
class="flex items-center justify-end px-6 py-4 border-t border-gray-200"
>
<div class="pagination-wrapper bg-white rounded-xl shadow-sm border border-gray-100 p-3">
<div class="pagination-wrapper bg-white rounded-lg shadow-sm border border-gray-100 p-3">
<el-pagination
v-model:current-page="searchParams.current"
v-model:page-size="searchParams.size"
......@@ -76,13 +78,14 @@ import { usePageSearch } from '@/hooks'
import { taskTypeListOptions } from '@/constants/options'
import dayjs from 'dayjs'
import { TaskTypeEnum } from '@/constants/enums'
import type { TabPaneName } from 'element-plus'
import { TaskDateLimitTypeText } from '@/constants'
const toggleTab = (key: string) => {
searchParams.value.taskType = key
const toggleTab = (key: TabPaneName) => {
searchParams.value.taskType = key as TaskTypeEnum
refresh()
}
// State
const { list, loading, searchParams, total, refresh, goToPage, changePageSize } = usePageSearch(
getSelfTaskList,
{
......@@ -91,23 +94,4 @@ const { list, loading, searchParams, total, refresh, goToPage, changePageSize }
},
},
)
// Computed
const paginatedList = computed(() => {
const start = (searchParams.value.current - 1) * searchParams.value.size
const end = start + searchParams.value.size
return list.value.slice(start, end)
})
const getEmptyText = () => {
const emptyTexts: Record<string, string> = {
posts: '还没有发布任何帖子',
videos: '还没有上传任何视频',
questions: '还没有提出任何问题',
articles: '还没有发表任何专栏文章',
practice: '还没有分享任何实践经验',
interviews: '还没有参与任何专访',
}
return emptyTexts[searchParams.value.taskType] || '暂无数据'
}
</script>
......@@ -6,6 +6,7 @@
<div class="absolute top-4 right-4 flex gap-2">
<el-button type="info" plain size="small">清除缓存</el-button>
<el-button type="info" plain size="small">切换账号</el-button>
<el-button type="info" plain size="small" @click="handleAdmin">后台管理</el-button>
</div>
</div>
......@@ -81,53 +82,6 @@
<component :is="currentComponent" />
</keep-alive>
</transition>
<!-- <div class="border-b border-gray-200">
<div class="flex">
<div
v-for="tab in tabs"
:key="tab.key"
@click="activeTab = tab.key"
:class="[
'px-6 py-4 cursor-pointer text-sm font-medium transition-colors relative',
activeTab === tab.key ? 'text-blue-600' : 'text-gray-500 hover:text-gray-700',
]"
>
{{ tab.label }}
<div
v-if="activeTab === tab.key"
class="absolute bottom-0 left-0 right-0 h-2px bg-blue-600"
></div>
</div>
</div>
</div>
<div class="p-8">
<div class="flex flex-col items-center justify-center py-20">
<div class="w-120px h-120px mb-6 opacity-30">
<svg viewBox="0 0 1024 1024" class="w-full h-full fill-gray-400">
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"
/>
<path
d="M623.6 316.7C593.6 290.4 554 276 512 276s-81.6 14.4-111.6 40.7C369.2 344 352 380.7 352 420v7.6c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V420c0-44.1 43.1-80 96-80s96 35.9 96 80c0 31.1-22 59.6-56.1 72.7-21.2 8.1-39.2 22.3-52.1 40.9-13.1 19-19.9 41.8-19.9 65.1V620c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8v-21.3c0-33.6 32.2-62.1 72.9-81.68C625.9 495.1 672 442.2 672 420c0-39.3-17.2-76-48.4-103.3z"
/>
<circle cx="512" cy="732" r="40" />
</svg>
</div>
<p class="text-gray-400 text-sm">暂无帖子</p>
</div>
<div class="flex justify-center mt-8">
<el-pagination
v-model:current-page="currentPage"
:page-size="10"
:total="0"
layout="prev, pager, next"
:hide-on-single-page="true"
/>
</div>
</div> -->
</div>
</div>
</div>
......@@ -138,15 +92,10 @@
<script lang="ts" setup>
import {
Edit,
User,
Trophy,
Document,
Star,
ChatDotRound,
Link,
View,
Setting,
Pointer,
Collection,
Finished,
......@@ -170,16 +119,10 @@ const { userInfo } = storeToRefs(userStore)
const route = useRoute()
const key = route.query.key as string
console.log(key)
// 当前激活的菜单
const activeMenu = ref(key || 'posts')
// 当前激活的标签页
const activeTab = ref('published')
// 当前页码
const currentPage = ref(1)
// 左侧普通用户菜单
const menuUserItems = [
{ key: 'posts', label: '我的帖子', icon: User, component: SelfPublish, tab: '发布' },
......@@ -224,6 +167,10 @@ const getIsOfficial = () => {
}, 1000)
}
const handleAdmin = () => {
window.open('/backend')
}
onMounted(() => {
getIsOfficial()
})
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment