This commit is contained in:
oneao committed 2025-12-25 17:31:09 +08:00
1 parent 5221d1cc91
commit a8a2657753
30 files changed
+552 -255

No files matched your search

@@ -1,8 +1,10 @@
<script lang="ts" setup>
import type { PostTag } from './types/base'
import { ElMessage } from 'element-plus'
import { onMounted } from 'vue'
import { initBaseApi } from '@/request/api/base'
import { useCommonStore } from '@/store/common'
import { getPostTagApi } from './request/api/post'
const commonOptions = useCommonStore()
@@ -25,6 +27,31 @@ function handleInitData() {
ElMessage.error('初始化失败,请刷新页面后重试')
})
getPostTagApi()
.then((res) => {
console.log(res)
if (res.isSuccess) {
const list: PostTag[] = res.data || []
commonOptions.postTagOptions.splice(
0,
commonOptions.postTagOptions.length,
{
id: 0,
name: '全部',
icon: 'fluent-color:apps-28',
},
...list,
)
}
else {
ElMessage.error(res.message || '初始化失败,请刷新页面后重试')
}
})
.catch((err) => {
console.error(err)
ElMessage.error('初始化失败,请刷新页面后重试')
})
}
</script>
@@ -132,42 +132,25 @@ function isActive(path: string) {
display: flex;
align-items: center;
cursor: pointer;
color: #000;
color: #9aa0a6;
outline: none;
margin-left: 36px;
transition: color 0.3s ease;
position: relative;
&:hover {
color: #0f6fec !important;
color: #2f2f2f !important;
}
&:first-child {
margin-left: 0px;
}
&.active {
color: #0f6fec !important;
}
color: #2f2f2f !important;
&::after {
content: '';
position: absolute;
bottom: 0px;
bottom: -8px;
left: 50%;
width: 0;
height: 2px;
background-color: #0f6fec;
transition: all 0.3s ease;
transform: translateX(-50%);
}
&:hover::after {
width: 100%;
}
&.active::after {
width: 100%;
.menu-item-name {
font-weight: 500;
}
}
.menu-item-name {
@@ -8,9 +8,9 @@ import Header from './components/header.vue'
<div class="content">
<RouterView />
</div>
<footer class="footer">
<!-- <footer class="footer">
TODO
</footer>
</footer> -->
</div>
</template>
@@ -0,0 +1,40 @@
import type { DashboardData, PostTag } from '@/types/post'
import type {
CreatePostParams,
DeletePostParams,
Post,
PostEditorForm,
QueryPostParams,
UpdatePostParams,
UpdatePostStatusParams,
} from '@/types/post/editor'
import { httpGet, httpPostJson } from '@/request/utils'
// ===== 编辑端 API =====
export function createPostApi(params: CreatePostParams) {
return httpPostJson<PostEditorForm>('/editor/post/create', params)
}
export function updatePostApi(params: UpdatePostParams) {
return httpPostJson<PostEditorForm>('/editor/post/update', params)
}
export function updatePostStatusApi(params: UpdatePostStatusParams) {
return httpPostJson<PostEditorForm>('/editor/post/update/status', params)
}
export function queryEditorPostsApi(params: QueryPostParams) {
return httpPostJson<BasePageResult<Post>>('/editor/post/query', params)
}
export function getEditorPostApi(id: number) {
return httpGet<PostEditorForm>(`/editor/post/${id}`)
}
export function deletePostApi(params: DeletePostParams) {
return httpPostJson<void>('/editor/post/delete', params)
}
export function getEditorDashboardApi() {
return httpGet<DashboardData>(`/editor/post/dashboard`)
}
@@ -1,38 +1,7 @@
import type { DashboardData, PostCategory, PostTag } from '@/types/post'
import type { AddPostParams, DeletePostParams, Post, PostEditorForm, QueryPostParams, UpdatePostParams, UpdatePostStatusParams } from '@/types/post/editor'
import type { PostTag } from '@/types/post'
import { httpGet, httpPostJson } from '@/request/utils'
export function addPostApi(params: AddPostParams) {
return httpPostJson<PostEditorForm>('/post/add', params)
}
export function updatePostApi(params: UpdatePostParams) {
return httpPostJson<PostEditorForm>('/post/update', params)
}
export function updatePostStatusApi(params: UpdatePostStatusParams) {
return httpPostJson<PostEditorForm>('/post/update/status', params)
}
export function queryPostApi(params: QueryPostParams) {
return httpPostJson<BasePageResult<Post>>('/post/query', params)
}
export function getPostApi(id: number) {
return httpGet<PostEditorForm>(`/post/${id}`)
}
export function deletePostApi(params: DeletePostParams) {
return httpPostJson<void>('/post/delete', params)
}
export function getDashboardDataApi() {
return httpGet<DashboardData>(`/post/dashboard`)
}
// ===== 公共接口 =====
export function getPostTagApi() {
return httpGet<PostTag[]>('/post/tag')
}
export function getPostCategoryApi() {
return httpGet<PostCategory[]>('/post/category')
}
@@ -1,4 +1,4 @@
import type { Area, CompanyQuery, Industry } from '@/types/base'
import type { Area, CompanyQuery, Industry, PostTag } from '@/types/base'
import { defineStore } from 'pinia'
import { reactive, ref } from 'vue'
@@ -8,6 +8,8 @@ export const useCommonStore = defineStore(
() => {
const areaOptions = reactive<Area[]>([])
const industryOptions = reactive<Industry[]>([])
const postTagOptions = reactive<PostTag[]>([])
const companyQuery = ref<CompanyQuery>({
industries: [],
areas: [],
@@ -18,6 +20,7 @@ export const useCommonStore = defineStore(
areaOptions,
industryOptions,
companyQuery,
postTagOptions,
}
},
)
@@ -12,6 +12,12 @@ export interface Industry {
name: string
}
export interface PostTag {
id: number
name: string
icon: string
}
export interface InitBaseDataResult {
industries: Industry[]
areas: Area[]
@@ -13,7 +13,7 @@ export interface PostEditorForm {
updateTime: string
}
export interface AddPostParams {
export interface CreatePostParams {
title: string
content: string
status: number
@@ -16,10 +16,13 @@ export interface DashboardData {
viewChart: DashboradChart[]
typeChart: DashboradChart[]
}
export interface PostTag {
id: number
name: string
icon: string
}
export interface PostCategory {
id: number
name: string
@@ -1,30 +1,96 @@
<script lang="ts" setup>
import type { CollapseModelValue } from 'element-plus'
import type { PostTag } from '@/types/base'
import { Search } from '@element-plus/icons-vue'
import { ref } from 'vue'
import CustomEmpty from '@/components/CustomEmpty.vue'
import SvgIcon from '@/components/SvgIcon.vue'
import { useCommonStore } from '@/store/common'
const { postTagOptions } = useCommonStore()
const activeNames = ref(['1'])
const selectedTag = ref<PostTag>(postTagOptions[0])
function handleChange(val: CollapseModelValue) {
console.log(val)
function handleChangeSelectedTag(tag: PostTag) {
selectedTag.value = tag
}
</script>
<template>
<div class="post-wrapper">
<div class="sidebar">
<div class="post-sidebar">
<el-collapse v-model="activeNames" expand-icon-position="left">
<el-collapse-item title="标签" name="1">
<ul class="sidebar-tags">
<li class="sidebar-tag">
<SvgIcon icon="streamline-color:earth-airplane" :size="18" />
<span class="sidebar-tag-text">空运</span>
<ul class="tag-list">
<li
v-for="item in postTagOptions"
:key="item.id"
class="tag-item"
:class="{ active: selectedTag.id === item.id }"
@click="handleChangeSelectedTag(item)"
>
<SvgIcon v-if="item.icon" :icon="item.icon" />
<span class="tag-item-text">{{ item.name }}</span>
</li>
</ul>
</el-collapse-item>
</el-collapse>
</div>
<div class="post-main">
<div class="post-main-header">
<el-input
:prefix-icon="Search"
placeholder="搜索" size="large"
/>
<div class="post-pagination">
<el-pagination layout="prev, pager, next,jumper" background :total="1000" />
</div>
</div>
<div class="post-content">
<!-- <CustomEmpty :in-container="true" /> -->
<div class="post-list">
<div v-for="i in 20" :key="i" class="post-item">
<div class="post-title-row">
<div class="post-title">
新项目为什么推荐WebFlux,而非SpringMVC?
</div>
</div>
<div class="post-meta">
<div class="post-meta-actions">
<div class="post-meta-item">
<div>刘大华</div>
</div>
<div class="post-meta-divider" />
<div class="post-meta-item">
<SvgIcon icon="mdi:eye-outline" />
<span>1.7k</span>
</div>
<div class="post-meta-item">
<SvgIcon icon="iconamoon:comment" />
<span>1.7k</span>
</div>
<div class="post-meta-divider" />
<div class="post-meta-item">
<div>20分钟前</div>
</div>
</div>
<div class="post-meta-tags">
<div class="post-tag">
后端
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
@@ -33,7 +99,8 @@ function handleChange(val: CollapseModelValue) {
display: flex;
padding: 0px 240px;
background-color: #fff;
.sidebar {
.post-sidebar {
background-color: #fff;
height: calc(100vh - var(--header-height));
font-size: 16px;
@@ -45,12 +112,12 @@ function handleChange(val: CollapseModelValue) {
width: 240px;
padding: 0px 16px;
.sidebar-tags {
.tag-list {
display: flex;
flex-direction: column;
gap: 4px;
.sidebar-tag {
.tag-item {
height: 32px;
display: inline-flex;
width: 100%;
@@ -59,24 +126,130 @@ function handleChange(val: CollapseModelValue) {
cursor: pointer;
padding: 0px 16px;
font-size: 16px;
transition:
background-color 0.2s ease,
color 0.2s ease;
&:hover {
background-color: #f2f2f2;
}
.sidebar-tag-text {
.tag-item-text {
margin-left: 12px;
color: #646464;
transition:
color 0.2s ease,
transform 0.2s ease;
}
&.active {
background-color: #f2f2f2;
.tag-item-text {
font-weight: bold;
}
}
}
}
}
.post-main {
flex: 1;
height: 100%;
.post-main-header {
position: sticky;
top: calc(var(--header-height));
background-color: #fff;
padding: 8px 16px 0px 16px;
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.06);
.post-pagination {
padding: 12px 0px;
}
}
.post-content {
padding-bottom: 36px;
}
.post-list {
display: flex;
flex-direction: column;
.post-item {
padding: 12px 16px;
border-bottom: 1px solid #ddd;
.post-title-row {
display: flex;
margin-bottom: 8px;
.post-title {
font-weight: 400;
font-size: 16px;
line-height: 24px;
color: #252933;
width: 100%;
cursor: pointer;
}
}
.post-meta {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
flex-wrap: wrap;
.post-meta-actions {
display: flex;
align-items: center;
.post-meta-item {
font-size: 14px;
line-height: 20px;
color: #8a919f;
flex-shrink: 0;
display: flex;
align-items: center;
& + .post-meta-item {
margin-left: 12px;
}
span {
margin-left: 4px;
}
}
.post-meta-divider {
width: 1px;
height: 12px;
background-color: #e4e6eb;
margin: 0 12px;
flex-shrink: 0;
}
}
.post-meta-tags {
display: flex;
flex-direction: row;
align-items: center;
.post-tag {
background-color: #f2f3f5;
padding: 0 6px;
border-radius: 2px;
max-width: 65px;
box-sizing: border-box;
margin-left: 6px;
color: #8a919f;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
min-height: 18px;
line-height: 18px;
font-size: 12px;
}
}
}
}
}
}
}
.sidebar {
width: 250px;
margin-left: 20px;
}
.post-list {
flex: 1;
}
</style>
@@ -8,10 +8,12 @@ import { computed, nextTick, onMounted, reactive, ref } from 'vue'
import { useRoute } from 'vue-router'
import CustomLoading from '@/components/CustomLoading.vue'
import WangEditor from '@/components/wang-editor/index.vue'
import { addPostApi, getPostApi, getPostCategoryApi, getPostTagApi, updatePostApi } from '@/request/api/post'
import { createPostApi, getEditorPostApi, updatePostApi } from '@/request/api/post/editor'
import { useCommonStore } from '@/store/common'
import { PostStatusEnum } from '@/types/post/editor'
const route = useRoute()
const { postTagOptions } = useCommonStore()
const titleInputRef = ref<HTMLInputElement | null>(null)
const wangEditorRef = ref<any>(null)
@@ -25,9 +27,6 @@ const loading = reactive({
const showPushDrawer = ref<boolean>(false)
const imageUpload = ref<UploadImageResponse | null>(null)
const tagOptions = ref<PostTag[]>([])
const categoryOptions = ref<PostCategory[]>([])
const statusOptions = [
{
label: '草稿',
@@ -78,7 +77,7 @@ onMounted(() => {
if (id) {
loading.init = true
getPostApi(id).then((res) => {
getEditorPostApi(id).then((res) => {
if (res.isSuccess) {
form.value = res.data
oldForm.value = JSON.parse(JSON.stringify(res.data))
@@ -102,19 +101,6 @@ onMounted(() => {
async function handleShowPushDrawer() {
loading.init = true
showPushDrawer.value = true
await getPostTagApi().then((res) => {
if (res.isSuccess) {
tagOptions.value = res.data
}
else {
ElMessage.error(res.message || '获取帖子标签失败,请稍后再试')
}
}).catch((err) => {
console.log(err)
ElMessage.error('获取帖子标签失败,请稍后再试')
})
loading.init = false
}
@@ -146,7 +132,7 @@ async function handleSave(formEl?: FormInstance) {
const res = isUpdate.value
? await updatePostApi({ id: form.value.id, ...payload })
: await addPostApi(payload)
: await createPostApi(payload)
if (!res.isSuccess) {
ElMessage.error(
@@ -215,7 +201,7 @@ async function handleSave(formEl?: FormInstance) {
<el-form-item label="标签" prop="tags">
<el-select v-model="form.tags" multiple placeholder="请选择标签">
<el-option
v-for="item in tagOptions"
v-for="item in postTagOptions"
:key="item.id"
:label="item.name"
:value="item.name"
@@ -6,7 +6,7 @@ import { onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import CustomLoading from '@/components/CustomLoading.vue'
import SvgIcon from '@/components/SvgIcon.vue'
import { deletePostApi, queryPostApi, updatePostStatusApi } from '@/request/api/post'
import { deletePostApi, queryEditorPostsApi, updatePostStatusApi } from '@/request/api/post/editor'
import { PostStatusEnum } from '@/types/post/editor'
const router = useRouter()
@@ -89,7 +89,7 @@ onMounted(() => {
function queryPost() {
loading.value = true
queryPostApi({
queryEditorPostsApi({
...queryForm,
currentPage: page.current,
pageSize: page.pageSize,
@@ -295,11 +295,9 @@ const activeCollapseNames = ref(['1', '2', '3'])
background-color: #fff;
:deep(.el-button) {
width: 100%;
height: 100%;
border: none;
width: 360px;
height: 40px;
border-radius: 0;
margin-left: 0;
}
}
}