This commit is contained in:
oneao committed 2025-12-24 17:31:19 +08:00
1 parent e9ad523fcf
commit f310336188
39 files changed
+411 -303

No files matched your search

@@ -51,7 +51,7 @@ function onSearch() {
border-radius: 4px;
border: 0;
color: #000;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
// box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
background-color: #f4f4f4;
.query-item {
flex: 1;
@@ -99,11 +99,9 @@ function isActive(path: string) {
<style lang="scss" scoped>
.header {
position: fixed;
position: sticky;
top: 0;
right: 0;
left: 0;
z-index: 99;
z-index: 1000;
background-color: #fff;
.container {
@@ -112,16 +110,24 @@ function isActive(path: string) {
display: flex;
justify-content: space-between;
align-items: center;
height: 60px;
box-shadow: 0 5px 16px rgba(0, 0, 0, 0.1);
height: var(--header-height);
border-bottom: 1px solid rgb(232.9, 232.9, 232.9);
flex-shrink: 0;
min-width: 0;
.logo {
width: 160px;
height: 50px;
flex-shrink: 0;
min-width: 0;
}
.menu {
display: flex;
align-items: center;
flex-shrink: 0;
min-width: 0;
.menu-item {
display: flex;
align-items: center;
@@ -146,7 +152,8 @@ function isActive(path: string) {
&::after {
content: '';
position: absolute;
bottom: -4px;
bottom: 0px;
bottom: -8px;
left: 50%;
width: 0;
height: 2px;
@@ -173,6 +180,9 @@ function isActive(path: string) {
.action {
display: flex;
align-items: center;
flex-shrink: 0;
min-width: 0;
.action-item {
font-size: 20px;
margin-right: 24px;
@@ -3,7 +3,7 @@ import Header from './components/header.vue'
</script>
<template>
<div class="app">
<div class="layout">
<Header />
<div class="content">
<RouterView />
@@ -15,18 +15,16 @@ import Header from './components/header.vue'
</template>
<style lang="scss" scoped>
.app {
.layout {
position: relative;
}
.content {
padding-top: 60px;
}
.footer {
margin-top: 32px;
height: 100px;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
@@ -1,32 +1,14 @@
:root {
--header-height: 60px;
}
html,
body {
background-color: #f7f7f7;
}
body::-webkit-scrollbar {
display: none;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 10px;
border: 2px solid #f1f1f1;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
@@ -7,8 +7,6 @@ export interface PostEditorForm {
id: number
title: string
content: string
summary: string
cover: string
tags: string[]
status: number
publishTime: string
@@ -18,8 +16,6 @@ export interface PostEditorForm {
export interface AddPostParams {
title: string
content: string
summary: string
cover: string
status: number
tags: string[]
}
@@ -28,8 +24,6 @@ export interface UpdatePostParams {
id: number
title: string
content: string
summary: string
cover: string
status: number
tags: string[]
}
@@ -57,7 +51,6 @@ export interface UpdatePostStatusParams {
export interface Post {
id: number
title: string
cover: string
tags: string[]
status: number
@@ -44,8 +44,6 @@ const oldForm = ref<PostEditorForm>({
id: 0,
title: '',
content: '',
summary: '',
cover: '',
tags: [],
publishTime: '',
updateTime: '',
@@ -56,8 +54,6 @@ const form = ref<PostEditorForm>({
id: 0,
title: '',
content: '',
summary: '',
cover: '',
tags: [],
publishTime: '',
updateTime: '',
@@ -108,10 +104,6 @@ async function handleShowPushDrawer() {
loading.init = true
showPushDrawer.value = true
if (!isUpdate.value) {
form.value.summary = (wangEditorRef.value.getText() || '').slice(0, 100)
}
await getPostTagApi().then((res) => {
if (res.isSuccess) {
tagOptions.value = res.data
@@ -127,18 +119,6 @@ async function handleShowPushDrawer() {
loading.init = false
}
async function handleCoverUploadIfNeeded() {
if (form.value.cover === oldForm.value.cover)
return
if (!form.value.cover || !imageUpload.value)
return
const { blob, name, size } = imageUpload.value
const res = await uploadToAliyun(blob, name, size, false)
form.value.cover = res.url
}
async function handleSave(formEl?: FormInstance) {
if (!formEl)
return
@@ -158,14 +138,10 @@ async function handleSave(formEl?: FormInstance) {
}
try {
await handleCoverUploadIfNeeded()
const payload = {
title: form.value.title,
content: form.value.content,
summary: form.value.summary,
status: form.value.status,
cover: form.value.cover,
tags: form.value.tags,
}
@@ -244,7 +220,7 @@ async function handleSave(formEl?: FormInstance) {
</el-select>
</el-form-item>
<el-form-item label="文章封面 " prop="cover">
<!-- <el-form-item label="文章封面 " prop="cover">
<ImageUploadCard
v-model="form.cover"
@change="(data) => {
@@ -262,7 +238,7 @@ async function handleSave(formEl?: FormInstance) {
maxlength="100"
type="textarea"
/>
</el-form-item>
</el-form-item> -->
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" placeholder="请选择状态">
@@ -291,7 +267,6 @@ async function handleSave(formEl?: FormInstance) {
<style lang="scss" scoped>
.editor-container {
padding: 16px 32px;
.toolbar {
display: flex;
justify-content: space-between;
@@ -1,7 +1,15 @@
<script setup lang="ts">
import { onMounted } from 'vue'
import type { PropType } from 'vue'
import type { DashboradChart } from '@/types/post'
import { watch } from 'vue'
import { useEcharts } from '@/hooks/echarts'
const props = defineProps({
data: {
type: Array as PropType<DashboradChart[]>,
},
})
const { domRef, updateOptions } = useEcharts(() => ({
tooltip: {
trigger: 'item', // 鼠标悬停时触发提示框,显示单个数据点
@@ -25,36 +33,30 @@ const { domRef, updateOptions } = useEcharts(() => ({
},
],
}))
onMounted(() => {
getData()
})
function getData() {
getForumPostChartNumberApi().then((res) => {
const labels: string[] = res.data.map(item => item.label)
const values: number[] = res.data.map(item => item.value)
updateOptions((opts) => {
opts.xAxis.data = labels
opts.series = [
{
data: values,
type: 'bar',
barWidth: '40%', // 调整柱子的宽度
},
]
return opts
})
watch(() => props.data, (newVal) => {
if (!newVal) {
return
}
const labels: string[] = newVal.map(item => item.label)
const values: number[] = newVal.map(item => item.value)
updateOptions((opts) => {
opts.xAxis.data = labels
opts.series = [
{
data: values,
type: 'bar',
barWidth: '40%', // 调整柱子的宽度
},
]
return opts
})
}
})
</script>
<template>
<el-card class="card-demo" title="文章发布数量" hoverable>
<template #extra>
<a-link>刷新</a-link>
</template>
<div ref="domRef" class="h-360px" />
</el-card>
<div ref="domRef" style="height: 360px;" />
</template>
<style lang="scss" scoped></style>
@@ -1,63 +1,66 @@
<template>
<a-card class="card-demo" title="文章分类" hoverable>
<template #extra>
<a-link @click="getData">刷新</a-link>
</template>
<div ref="domRef" class="h-360px overflow-hidden"></div>
</a-card>
</template>
<script setup lang="ts">
import { useEcharts } from "@/hooks/echarts";
import { getForumPostChartTypeApi } from "@/service/forum/post";
import { onMounted } from "vue";
import type { PropType } from 'vue'
import type { DashboradChart } from '@/types/post'
import { watch } from 'vue'
import { useEcharts } from '@/hooks/echarts'
const props = defineProps({
data: {
type: Array as PropType<DashboradChart[]>,
default: () => [],
},
})
const { domRef, updateOptions } = useEcharts(() => ({
title: {
text: "文章分类",
left: "center"
text: '文章分类',
left: 'center',
},
tooltip: {
trigger: "item"
trigger: 'item',
},
legend: {
orient: "vertical",
left: "left"
orient: 'vertical',
left: 'left',
},
series: [
{
name: "文章分类",
type: "pie",
radius: "50%",
name: '文章分类',
type: 'pie',
radius: '50%',
data: [] as any[], // Initial empty data
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: "rgba(0, 0, 0, 0.5)"
}
}
}
]
}));
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
},
},
],
}))
// 数据请求并更新图表
function getData() {
getForumPostChartTypeApi().then((res) => {
// 更新饼图的数据
updateOptions((opts) => {
opts.series[0].data = res.data.map((item) => ({
value: item.value,
name: item.label
}));
return opts;
});
});
}
watch(() => props.data, (newVal) => {
if (!newVal) {
return
}
onMounted(() => {
getData(); // 在组件加载时获取数据
});
console.log('===type===')
console.log(newVal)
// 更新饼图的数据
updateOptions((opts) => {
opts.series[0].data = newVal.map(item => ({
value: item.value,
name: item.label,
}))
return opts
})
}, { deep: true })
</script>
<template>
<div ref="domRef" style="height: 360px;" />
</template>
<style lang="scss" scoped></style>
@@ -1,58 +1,59 @@
<template>
<a-card class="card-demo" title="文章浏览量" hoverable>
<template #extra>
<a-link @click="getData">刷新</a-link>
</template>
<div ref="domRef" class="h-360px overflow-hidden"></div>
</a-card>
</template>
<script setup lang="ts">
import { useEcharts } from "@/hooks/echarts";
import { getForumPostChartViewApi } from "@/service/forum/post";
import { onMounted } from "vue";
import type { PropType } from 'vue'
import type { DashboradChart } from '@/types/post'
import { onMounted, watch } from 'vue'
import { useEcharts } from '@/hooks/echarts'
const props = defineProps({
data: {
type: Array as PropType<DashboradChart[]>,
},
})
const { domRef, updateOptions } = useEcharts(() => ({
tooltip: {
trigger: "item", // 鼠标悬停时触发提示框,显示单个数据点
formatter: "{b}:{c}" // 自定义提示框显示格式:{b} 是 xAxis 的标签,{c} 是 series 的值
trigger: 'item', // 鼠标悬停时触发提示框,显示单个数据点
formatter: '{b}:{c}', // 自定义提示框显示格式:{b} 是 xAxis 的标签,{c} 是 series 的值
},
xAxis: {
type: "category",
data: [] as string[]
type: 'category',
data: [] as string[],
},
yAxis: {
type: "value"
type: 'value',
},
series: [
{
data: [] as number[],
type: "line"
}
]
}));
type: 'line',
},
],
}))
onMounted(() => {
getData();
});
watch(() => props.data, (newVal) => {
if (!newVal) {
return
}
const labels: string[] = newVal.map(item => item.label)
const values: number[] = newVal.map(item => item.value)
function getData() {
getForumPostChartViewApi().then((res) => {
const labels: string[] = res.data.map((item) => item.label);
const values: number[] = res.data.map((item) => item.value);
updateOptions((opts) => {
opts.xAxis.data = labels;
opts.series = [
{
data: values,
type: "line"
}
];
return opts;
});
});
}
updateOptions((opts) => {
opts.xAxis.data = labels
opts.series = [
{
data: values,
type: 'line',
},
]
return opts
})
})
</script>
<template>
<div ref="domRef" style="height: 360px;" />
</template>
<style lang="scss" scoped></style>
@@ -1,13 +1,44 @@
<script lang="ts" setup>
import type { DashboardData } from '@/types/post'
import { ElMessage } from 'element-plus'
import { onMounted, ref } from 'vue'
import CustomLoading from '@/components/CustomLoading.vue'
import { getDashboardDataApi } from '@/request/api/post'
import PostNumberChart from './components/post-number-chart.vue'
import PostTypeChart from './components/post-type-chart.vue'
import PostViewChart from './components/post-view-chart.vue'
const loading = ref<boolean>(false)
const dashboradData = ref<DashboardData>({
allCount: {
postCount: 0,
viewCount: 0,
likeCount: 0,
favoriteCount: 0,
},
numberChart: [],
viewChart: [],
typeChart: [],
})
onMounted(() => {
loading.value = true
getDashboardDataApi().then((res) => {
console.log(res)
if (res.isSuccess) {
// dashboradData.value = res.data
dashboradData.value.typeChart = [...res.data.typeChart]
console.log(dashboradData.value)
}
else {
ElMessage.error(res.message || '加载失败,请稍后再试')
}
}).catch((err) => {
console.log(err)
ElMessage.error('加载失败,请稍后再试')
}).finally(() => {
loading.value = false
})
})
</script>
@@ -30,7 +61,7 @@ onMounted(() => {
文章数
</div>
<div class="stat-value">
<el-statistic :value="98500" />
<el-statistic :value="dashboradData.allCount.postCount" />
</div>
</div>
</el-col>
@@ -40,7 +71,7 @@ onMounted(() => {
浏览量
</div>
<div class="stat-value">
<el-statistic :value="98500" />
<el-statistic :value="dashboradData.allCount.viewCount" />
</div>
</div>
</el-col>
@@ -50,7 +81,7 @@ onMounted(() => {
点赞数
</div>
<div class="stat-value">
<el-statistic :value="98500" />
<el-statistic :value="dashboradData.allCount.likeCount" />
</div>
</div>
</el-col>
@@ -60,7 +91,7 @@ onMounted(() => {
收藏数量
</div>
<div class="stat-value">
<el-statistic :value="98500" />
<el-statistic :value="dashboradData.allCount.favoriteCount" />
</div>
</div>
</el-col>
@@ -73,7 +104,11 @@ onMounted(() => {
数据报表
</div>
</div>
<div class="card-content" />
<div class="card-content">
<PostViewChart :data="dashboradData.viewChart" />
<PostNumberChart :data="dashboradData.numberChart" />
<PostTypeChart :data="dashboradData.typeChart" />
</div>
</div>
</div>
</template>
@@ -280,9 +280,6 @@ function handleToEdit(row?: Post) {
class="post-item"
@click="handleToEdit(item)"
>
<div v-if="item.cover" class="post-cover">
<img :src="item.cover">
</div>
<div class="post-content">
<div class="post-main">
<div class="post-title-area">
@@ -112,13 +112,15 @@ watch(searchKey, async (val) => {
<template>
<div class="settings-wrapper">
<div class="left-menu">
<el-input
v-model="searchKey"
placeholder="搜索菜单"
size="large"
:prefix-icon="Search"
clearable
/>
<div class="menu-search">
<el-input
v-model="searchKey"
placeholder="搜索菜单"
size="large"
:prefix-icon="Search"
clearable
/>
</div>
<el-menu
ref="menuRef"
@@ -128,7 +130,6 @@ watch(searchKey, async (val) => {
<NavMenu :menu="filteredMenu" />
</el-menu>
</div>
<div class="right-view">
<router-view />
</div>
@@ -138,33 +139,51 @@ watch(searchKey, async (val) => {
<style lang="scss" scoped>
.settings-wrapper {
display: flex;
gap: 40px;
margin-top: 30px;
background-color: #fff;
padding: 0px 240px;
height: 800px;
.left-menu {
height: calc(100vh - var(--header-height));
font-size: 16px;
overflow-y: auto;
width: 240px;
height: 100%;
background-color: #fff;
flex-shrink: 0;
:deep(.el-input__wrapper) {
margin: 12px 12px 4px 12px;
border-right: 1px solid rgb(232.9, 232.9, 232.9);
position: sticky;
top: var(--header-height);
align-self: flex-start;
padding: 12px 16px;
.menu-search {
margin-bottom: 8px;
}
:deep(.el-menu) {
border-right: none;
}
:deep(.el-menu-item) {
border-radius: 8px;
margin: 4px 0px;
}
:deep(.el-menu-item.is-active) {
background-color: #d1f0ff;
color: #303133;
font-weight: 600;
}
:deep(.el-sub-menu) {
margin: 4px 0px;
}
:deep(.el-sub-menu__title) {
border-radius: 8px;
}
}
.right-view {
width: 1200px;
flex: 1;
flex-shrink: 0;
overflow-y: auto;
background-color: #fff;
border-radius: 0px;
width: 100%;
}
}
</style>
@@ -239,8 +239,7 @@ const activeCollapseNames = ref(['1', '2', '3'])
<style lang="scss" scoped>
.company-wrapper {
padding: 24px 32px;
padding-bottom: 16px;
:deep(.el-collapse) {
border: none;
}
@@ -251,8 +250,12 @@ const activeCollapseNames = ref(['1', '2', '3'])
:deep(.el-collapse-item__header) {
font-size: 18px;
font-weight: 600;
padding: 16px;
border-bottom: 1px solid #ddd;
line-height: 0;
}
:deep(.el-collapse-item__content) {
padding: 20px 20px 0px 20px;
padding-bottom: 0px;
}
@@ -299,7 +299,6 @@ function handleDelteRows(val?: CompanyUser) {
:total="page.total"
layout="prev, pager, next"
background
size="large"
@change="getCompanyUser()"
/>
</div>
@@ -320,7 +319,6 @@ function handleDelteRows(val?: CompanyUser) {
display: flex;
flex-direction: column;
height: 100%;
padding: 8px 24px 0px 24px;
:deep(.el-collapse) {
border: none;
@@ -331,10 +329,14 @@ function handleDelteRows(val?: CompanyUser) {
:deep(.el-collapse-item__header) {
font-size: 18px;
font-weight: 500;
font-weight: 600;
padding: 16px;
border-bottom: 1px solid #ddd;
line-height: 0;
}
:deep(.el-collapse-item__content) {
padding-bottom: 0px;
padding: 20px 20px 0px 20px;
}
.table-container {
@@ -342,6 +344,7 @@ function handleDelteRows(val?: CompanyUser) {
display: flex;
flex-direction: column;
min-height: 0;
padding: 0px 20px;
.table-body-wrapper {
flex: 1;
@@ -357,7 +360,7 @@ function handleDelteRows(val?: CompanyUser) {
display: flex;
align-items: center;
justify-content: space-between;
height: 56px;
margin: 20px 0px;
}
:deep(.el-table__header) {
.el-table__cell {