feat(登录): 完善登录逻辑和用户信息处理
refactor(文章): 重构文章创建和分类选择功能 style(布局): 调整主布局样式和响应式设计 fix(状态管理): 修正全局状态存储和清除逻辑 feat(登出): 添加登出功能按钮和逻辑 docs(类型): 扩展文章类型定义字段
This commit is contained in:
@@ -52,19 +52,21 @@
|
|||||||
<h1 class="typewriter">{{ heroText }}</h1>
|
<h1 class="typewriter">{{ heroText }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 主内容区域 -->
|
<!-- 提示区域 -->
|
||||||
<div id="content-section" :class="{ 'visible': isconts }">
|
<div id="content-section" :class="{ 'visible': isconts }">
|
||||||
<div class="nonsensetitle" v-if="classnonsenset">
|
<div class="nonsensetitle" v-if="classnonsenset">
|
||||||
<div class="nonsensetitleconst">
|
<div class="nonsensetitleconst">
|
||||||
<h1>发癫中QAQ</h1>
|
<h1>{{Cardtitle}}</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 左侧模块 -->
|
<!-- 左侧模块 -->
|
||||||
<LeftModule class="leftmodluepage" :class="{ 'nonsensetmargintop': classnonsenset }" v-if="windowwidth" />
|
<div class="leftmodluecontainer" v-if="isleftmodluecontainer">
|
||||||
|
<LeftModule class="leftmodluepage" :class="{ 'nonsensetmargintop': classnonsenset }" v-if="windowwidth" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 内容模块 -->
|
<!-- 内容模块 -->
|
||||||
<RouterView class="RouterViewpage" :class="{ 'nonsensetmargintop': classnonsenset }" />
|
<RouterView class="RouterViewpage" :class="{'forbidwidth': !isleftmodluecontainer, 'nonsensetmargintop': classnonsenset }" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分页区域 -->
|
<!-- 分页区域 -->
|
||||||
@@ -84,10 +86,13 @@ const router = useRouter();
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
// 全局状态管理
|
// 全局状态管理
|
||||||
import { useGlobalStore } from '@/store/globalStore'
|
import { useGlobalStore } from '@/store/globalStore'
|
||||||
|
import { Card } from 'ant-design-vue';
|
||||||
const globalStore = useGlobalStore()
|
const globalStore = useGlobalStore()
|
||||||
const Login = computed(() => globalStore.Login)
|
const Login = computed(() => globalStore.Login)
|
||||||
|
|
||||||
// 响应式状态
|
// 响应式状态
|
||||||
|
const Cardtitle = ref('');
|
||||||
|
const isleftmodluecontainer = ref(true);
|
||||||
const classhero = ref(false);
|
const classhero = ref(false);
|
||||||
const isconts = ref(false);
|
const isconts = ref(false);
|
||||||
const isScrollingleftmodlue = ref(false);
|
const isScrollingleftmodlue = ref(false);
|
||||||
@@ -97,7 +102,7 @@ const windowwidth = ref(true);
|
|||||||
const activeIndex = ref('home');
|
const activeIndex = ref('home');
|
||||||
const localhome= 'home';
|
const localhome= 'home';
|
||||||
|
|
||||||
let rpsliturl = route.path.split('/')[1];
|
let rpsliturl = route.path.split('/');
|
||||||
|
|
||||||
// 搜索相关状态
|
// 搜索相关状态
|
||||||
const isSearchBoxOpen = ref(false);
|
const isSearchBoxOpen = ref(false);
|
||||||
@@ -105,7 +110,7 @@ const searchKeyword = ref('');
|
|||||||
let searchCloseTimer: number | undefined;
|
let searchCloseTimer: number | undefined;
|
||||||
|
|
||||||
// 打字机效果相关
|
// 打字机效果相关
|
||||||
let fullHeroText = '测试打字机效果';
|
let fullHeroText = '清疯不颠';
|
||||||
const heroText = ref('');
|
const heroText = ref('');
|
||||||
let heroIndex = 0;
|
let heroIndex = 0;
|
||||||
let heroTimer: number | undefined;
|
let heroTimer: number | undefined;
|
||||||
@@ -218,7 +223,7 @@ const handleResize = () => {
|
|||||||
windowwidth.value = window.innerWidth > 768;
|
windowwidth.value = window.innerWidth > 768;
|
||||||
|
|
||||||
// 根据屏幕大小调整内容区可见性
|
// 根据屏幕大小调整内容区可见性
|
||||||
if (rpsliturl === localhome) {
|
if (rpsliturl[1] === localhome) {
|
||||||
isconts.value = window.innerWidth <= 768 ? true : false;
|
isconts.value = window.innerWidth <= 768 ? true : false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -241,7 +246,7 @@ const handleScroll = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 首页内容区滚动动画
|
// 首页内容区滚动动画
|
||||||
if (rpsliturl === localhome) {
|
if (rpsliturl[1] === localhome) {
|
||||||
isconts.value = window.scrollY > 200;
|
isconts.value = window.scrollY > 200;
|
||||||
isScrollingleftmodlue.value = window.scrollY > 600;
|
isScrollingleftmodlue.value = window.scrollY > 600;
|
||||||
}
|
}
|
||||||
@@ -250,30 +255,34 @@ const handleScroll = () => {
|
|||||||
/**
|
/**
|
||||||
* 监听路由变化
|
* 监听路由变化
|
||||||
*/
|
*/
|
||||||
watch(() => route.path, (newPath) => {
|
watch(() => route.path, () => {
|
||||||
rpsliturl = route.path.split('/')[1];
|
rpsliturl = route.path.split('/');
|
||||||
updatePageState(rpsliturl);
|
updatePageState(rpsliturl[1]);
|
||||||
setActiveIndex(rpsliturl);
|
setActiveIndex(rpsliturl[1]);
|
||||||
|
|
||||||
const localname = route.path.split('/')[2];
|
console.log(rpsliturl[1])
|
||||||
let articledata;
|
let articledata;
|
||||||
// 优先使用attributeId参数(新接口)
|
// 优先使用attributeId参数(新接口)
|
||||||
if (localname==='aericletype') {
|
if (rpsliturl[2]==='aericletype') {
|
||||||
articledata = globalStore.getValue('attribute')
|
articledata = globalStore.getValue('attribute')
|
||||||
}
|
}
|
||||||
// 搜索标题
|
// 搜索标题
|
||||||
if (localname==='aericletitle') {
|
if (rpsliturl[2]==='aericletitle') {
|
||||||
articledata = globalStore.getValue('title')
|
articledata = globalStore.getValue('title')
|
||||||
}
|
}
|
||||||
|
if (rpsliturl[1]==='nonsense') {
|
||||||
|
articledata = "疯言疯语"
|
||||||
|
}
|
||||||
// hero 标题
|
// hero 标题
|
||||||
if (articledata) {
|
if (articledata) {
|
||||||
fullHeroText = articledata.name
|
Cardtitle.value = articledata.name
|
||||||
|
classhero.value = true;
|
||||||
}
|
}
|
||||||
// 跳转后回到顶部
|
// 跳转后回到顶部
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
// 首页内容区滚动动画仅大屏下生效
|
// 首页内容区滚动动画仅大屏下生效
|
||||||
if (newPath.split('/')[1] === localhome) {
|
if (rpsliturl[1] === localhome && rpsliturl[2] == '') {
|
||||||
isconts.value = window.innerWidth <= 768 ? true : false;
|
// isconts.value = window.innerWidth <= 768 ? true : false;
|
||||||
// 首页时启动打字机效果
|
// 首页时启动打字机效果
|
||||||
startTypewriter();
|
startTypewriter();
|
||||||
} else {
|
} else {
|
||||||
@@ -281,6 +290,12 @@ watch(() => route.path, (newPath) => {
|
|||||||
heroText.value =fullHeroText;
|
heroText.value =fullHeroText;
|
||||||
if (heroTimer) clearInterval(heroTimer);
|
if (heroTimer) clearInterval(heroTimer);
|
||||||
}
|
}
|
||||||
|
// 非首页时关闭左侧状态栏
|
||||||
|
if (rpsliturl[1] == "articlesave") {
|
||||||
|
isleftmodluecontainer.value = false;
|
||||||
|
} else {
|
||||||
|
isleftmodluecontainer.value = true;
|
||||||
|
}
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { articleService, messageService, categoryAttributeService } from '@/services'
|
import { articleService, messageService, categoryAttributeService ,loginService} from '@/services'
|
||||||
// 全局状态管理
|
// 全局状态管理
|
||||||
import { useGlobalStore } from '@/store/globalStore'
|
import { useGlobalStore } from '@/store/globalStore'
|
||||||
const globalStore = useGlobalStore()
|
const globalStore = useGlobalStore()
|
||||||
@@ -32,7 +32,8 @@ const buttons = [
|
|||||||
{ id: 1, label: '新建文章', icon: 'icon-add-article' },
|
{ id: 1, label: '新建文章', icon: 'icon-add-article' },
|
||||||
{ id: 2, label: '新建分类', icon: 'icon-create-category' },
|
{ id: 2, label: '新建分类', icon: 'icon-create-category' },
|
||||||
{ id: 3, label: '疯言疯语', icon: 'icon-upload-file' },
|
{ id: 3, label: '疯言疯语', icon: 'icon-upload-file' },
|
||||||
{ id: 4, label: '新建标签', icon: 'icon-new-tag' }
|
{ id: 4, label: '新建标签', icon: 'icon-new-tag' },
|
||||||
|
{ id: 5, label: '登出', icon: 'icon-logout' }
|
||||||
]
|
]
|
||||||
const buttonsave = [
|
const buttonsave = [
|
||||||
{ id: 1, label: '修改文章', icon: 'icon-add-article' },
|
{ id: 1, label: '修改文章', icon: 'icon-add-article' },
|
||||||
@@ -98,6 +99,24 @@ const handleButtonClick = (button) => {
|
|||||||
// 取消删除
|
// 取消删除
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (button.label == '登出') {
|
||||||
|
// 调用登出接口
|
||||||
|
loginService.logout().then(response => {
|
||||||
|
if (response.code == 200) {
|
||||||
|
ElMessage.success('登出成功')
|
||||||
|
// 清空全局状态
|
||||||
|
globalStore.clearAll()
|
||||||
|
// 清除localStorage中的token
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
// 跳转首页
|
||||||
|
router.push({ path: '/' })
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.message || '登出失败')
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
ElMessage.error(err.message || '登出失败')
|
||||||
|
})
|
||||||
|
}
|
||||||
isExpanded.value = false // 点击后收起
|
isExpanded.value = false // 点击后收起
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,6 +254,11 @@ onBeforeUnmount(() => {
|
|||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
.action-button.show:nth-child(5) {
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
margin-left: 0;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
/* 确保按钮按顺序关闭,形成卷帘效果 */
|
/* 确保按钮按顺序关闭,形成卷帘效果 */
|
||||||
.action-button:nth-child(1) {
|
.action-button:nth-child(1) {
|
||||||
@@ -261,7 +285,11 @@ onBeforeUnmount(() => {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-button:nth-child(5) {
|
||||||
|
transition: all 1.1s ease-in-out;
|
||||||
|
margin-left: 150px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
/* 响应式调整 */
|
/* 响应式调整 */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.establish-container {
|
.establish-container {
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ class ArticleService {
|
|||||||
* @param {import('../types').ArticleDto} articleData - 文章数据
|
* @param {import('../types').ArticleDto} articleData - 文章数据
|
||||||
* @returns {Promise<import('../types').ApiResponse<import('../types').Article>>}
|
* @returns {Promise<import('../types').ApiResponse<import('../types').Article>>}
|
||||||
*/
|
*/
|
||||||
createArticle(Article) {
|
createArticle(articleData) {
|
||||||
return api.post('/articles', Article)
|
return api.post('/articles', articleData)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ class LoginService {
|
|||||||
* 登出
|
* 登出
|
||||||
*/
|
*/
|
||||||
logout() {
|
logout() {
|
||||||
|
|
||||||
return api.post("/auth/logout");
|
return api.post("/auth/logout");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const useGlobalStore = defineStore('global', {
|
|||||||
// 全局数据对象,存储所有需要共享的数据
|
// 全局数据对象,存储所有需要共享的数据
|
||||||
globalData: initialGlobalData,
|
globalData: initialGlobalData,
|
||||||
// 特定状态属性,从localStorage读取初始值
|
// 特定状态属性,从localStorage读取初始值
|
||||||
user: initialSpecificData.user || null,
|
username: initialSpecificData.username || null,
|
||||||
Login: initialSpecificData.Login || false,
|
Login: initialSpecificData.Login || false,
|
||||||
notifications: initialSpecificData.notifications || []
|
notifications: initialSpecificData.notifications || []
|
||||||
}
|
}
|
||||||
@@ -80,7 +80,7 @@ export const useGlobalStore = defineStore('global', {
|
|||||||
localStorage.setItem('globalStoreData', JSON.stringify(this.globalData))
|
localStorage.setItem('globalStoreData', JSON.stringify(this.globalData))
|
||||||
// 持久化特定状态属性
|
// 持久化特定状态属性
|
||||||
localStorage.setItem('globalStoreSpecificData', JSON.stringify({
|
localStorage.setItem('globalStoreSpecificData', JSON.stringify({
|
||||||
user: this.user,
|
username: this.username,
|
||||||
Login: this.Login,
|
Login: this.Login,
|
||||||
notifications: this.notifications
|
notifications: this.notifications
|
||||||
}))
|
}))
|
||||||
@@ -118,13 +118,13 @@ export const useGlobalStore = defineStore('global', {
|
|||||||
*/
|
*/
|
||||||
clearAll() {
|
clearAll() {
|
||||||
this.globalData = {}
|
this.globalData = {}
|
||||||
this.user = null
|
this.username = null
|
||||||
this.Login = false
|
this.Login = false
|
||||||
this.notifications = []
|
this.notifications = []
|
||||||
// 清除localStorage中的数据
|
// 清除localStorage中的数据
|
||||||
try {
|
try {
|
||||||
localStorage.removeItem('globalStoreData')
|
localStorage.removeItem('globalStoreData')
|
||||||
// localStorage.removeItem('globalStoreSpecificData')
|
localStorage.removeItem('globalStoreSpecificData')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to clear data from localStorage:', error)
|
console.error('Failed to clear data from localStorage:', error)
|
||||||
}
|
}
|
||||||
@@ -134,8 +134,8 @@ export const useGlobalStore = defineStore('global', {
|
|||||||
* 设置用户信息
|
* 设置用户信息
|
||||||
* @param {Object} userInfo - 用户信息对象
|
* @param {Object} userInfo - 用户信息对象
|
||||||
*/
|
*/
|
||||||
setUser(userInfo) {
|
setUsername(username) {
|
||||||
this.user = userInfo
|
this.username = username
|
||||||
// 持久化到localStorage
|
// 持久化到localStorage
|
||||||
this._persistData()
|
this._persistData()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -240,6 +240,9 @@ p {
|
|||||||
margin: var(--content-margin);
|
margin: var(--content-margin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.RouterViewpage.forbidwidth {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
/* 分页区样式 */
|
/* 分页区样式 */
|
||||||
.Pagination {
|
.Pagination {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
@@ -247,7 +250,7 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 左侧状态栏样式 */
|
/* 左侧状态栏样式 */
|
||||||
.leftmodluepage {
|
.leftmodluecontainer {
|
||||||
width: var(--leftmodlue-width);
|
width: var(--leftmodlue-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ export interface ArticleDto {
|
|||||||
attributeid: number
|
attributeid: number
|
||||||
img?: string
|
img?: string
|
||||||
status?: number
|
status?: number
|
||||||
|
viewCount?: number
|
||||||
|
likes?: number
|
||||||
|
markdownscontent: string
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,35 +1,51 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="allstyle">
|
<div id="allstyle">
|
||||||
<div class="article-header-section">
|
<div class="article-header-section">
|
||||||
<h1 class="article-main-title">{{ Articleform.title }}11</h1>
|
<div style="text-align: center;">
|
||||||
<div class="article-meta-info">
|
<input type="text" v-model="Articleform.title" class="article-main-title" placeholder="请输入标题"
|
||||||
<span class="meta-item">
|
@focus="($event.target as HTMLInputElement).placeholder = ''"
|
||||||
<i class="el-icon-date"></i>
|
@blur="($event.target as HTMLInputElement).placeholder = '请输入标题'"
|
||||||
<!-- {{ formatDate(Articleform.createdAt) }} -->
|
style="border: none; outline: none; background: transparent; width: 100%; font-size: 2.5rem; font-weight: 700; line-height: 1.2; text-align: center;" />
|
||||||
22
|
|
||||||
</span>
|
|
||||||
<span class="meta-item">
|
|
||||||
<i class="el-icon-folder"></i>
|
|
||||||
{{ Articleform.categoryName || '未分类' }}33
|
|
||||||
</span>
|
|
||||||
<span class="meta-item">
|
|
||||||
<i class="el-icon-view"></i>
|
|
||||||
{{ Articleform.viewCount || 0 }} 阅读
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="article-meta-info" style="text-align: center;">
|
||||||
<MdEditor v-model="Articleform.markdownscontent" htmlPreview preview={false} class="markdown-editor" @on-save="handleSave" />
|
<span class="meta-item">
|
||||||
|
|
||||||
|
|
||||||
|
<i class="el-icon-document"></i>
|
||||||
|
<span> {{ new Date().toLocaleDateString() }} </span>
|
||||||
|
</span>
|
||||||
|
<span class="meta-item">
|
||||||
|
<i class="el-icon-document"></i>
|
||||||
|
<el-select v-model="Articleform.status" placeholder="请选择状态">
|
||||||
|
<el-option v-for="item in statusoptions" :key="item.value" :label="item.label" :value="item.value">
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</span>
|
||||||
|
<span class="meta-item">
|
||||||
|
<i class="el-icon-folder"></i>
|
||||||
|
<el-cascader :options="categorieoptions" v-model="selectedValues" @change="handleCascaderChange"
|
||||||
|
placeholder="选择分类和属性">
|
||||||
|
<template #default="{ node, data }">
|
||||||
|
<span>{{ data.label }}</span>
|
||||||
|
<span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
|
||||||
|
</template>
|
||||||
|
</el-cascader>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<MdEditor v-model="Articleform.markdownscontent" class="markdown-editor" @on-save="handleSave" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { MdEditor } from 'md-editor-v3';
|
import { MdEditor } from 'md-editor-v3';
|
||||||
import 'md-editor-v3/lib/style.css';
|
import 'md-editor-v3/lib/style.css';
|
||||||
import { articleService } from '@/services';
|
import { categoryService, categoryAttributeService, articleService } from '@/services';
|
||||||
import type { Article } from '@/types/index.ts';
|
import type { Article } from '@/types/index.ts';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
const Articleform = ref<Article>({
|
const Articleform = ref<Article>({
|
||||||
articleid: 0,
|
articleid: 0,
|
||||||
title: '',
|
title: '',
|
||||||
@@ -41,28 +57,373 @@ const Articleform = ref<Article>({
|
|||||||
markdownscontent: ''
|
markdownscontent: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 用于级联选择器的值绑定
|
||||||
|
const selectedValues = ref([]);
|
||||||
|
const categorieoptions = ref([]);
|
||||||
|
const statusoptions = ref([
|
||||||
|
{
|
||||||
|
label: '未发布',
|
||||||
|
value: '0'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '发布',
|
||||||
|
value: '1'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
const categories = ref([]);
|
||||||
|
|
||||||
|
// 初始化加载分类和属性,构建级联选择器的options
|
||||||
|
const loadCategories = async () => {
|
||||||
|
try {
|
||||||
|
const response = await categoryService.getAllCategories();
|
||||||
|
if (response.code === 200) {
|
||||||
|
categories.value = response.data;
|
||||||
|
|
||||||
|
// 为每个分类加载对应的属性,并构建options格式
|
||||||
|
const optionsData = await Promise.all(
|
||||||
|
categories.value.map(async (category) => {
|
||||||
|
try {
|
||||||
|
const attrResponse = await categoryAttributeService.getAttributesByCategory(category.typeid);
|
||||||
|
const children = attrResponse.code === 200 && attrResponse.data ?
|
||||||
|
attrResponse.data.map(attr => ({
|
||||||
|
label: attr.attributename,
|
||||||
|
value: attr.attributeid.toString()
|
||||||
|
})) : [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: category.typename,
|
||||||
|
value: category.typeid.toString(),
|
||||||
|
children
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`加载分类 ${category.typename} 的属性失败:`, error);
|
||||||
|
return {
|
||||||
|
label: category.typename,
|
||||||
|
value: category.typeid.toString(),
|
||||||
|
children: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
categorieoptions.value = optionsData;
|
||||||
|
console.log(optionsData);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载分类失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理级联选择变化
|
||||||
|
const handleCascaderChange = (values) => {
|
||||||
|
if (values && values.length > 0) {
|
||||||
|
// 最后一个值是属性ID
|
||||||
|
Articleform.value.attributeid = Number(values[values.length - 1]);
|
||||||
|
} else {
|
||||||
|
Articleform.value.attributeid = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件挂载时加载分类和属性
|
||||||
|
loadCategories();
|
||||||
|
|
||||||
const handleSave = (markdown) => {
|
const handleSave = (markdown) => {
|
||||||
console.log(Articleform.value);
|
|
||||||
Articleform.value.markdownscontent = markdown;
|
Articleform.value.markdownscontent = markdown;
|
||||||
// 这里可以添加保存逻辑,比如发送到服务器
|
|
||||||
articleService.createArticle({
|
// 验证必填字段
|
||||||
|
if (!Articleform.value.title || !Articleform.value.attributeid) {
|
||||||
|
ElMessage.warning('请填写必填字段:标题和分类属性');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建请求数据
|
||||||
|
const articleData = {
|
||||||
title: Articleform.value.title,
|
title: Articleform.value.title,
|
||||||
content: Articleform.value.content,
|
content: Articleform.value.content,
|
||||||
attributeid: Articleform.value.attributeid,
|
attributeid: Number(Articleform.value.attributeid),
|
||||||
categoryName: Articleform.value.categoryName,
|
status: Number(Articleform.value.status),
|
||||||
|
viewCount: 0,
|
||||||
|
likes: 0,
|
||||||
markdownscontent: Articleform.value.markdownscontent
|
markdownscontent: Articleform.value.markdownscontent
|
||||||
}).then(res => {
|
};
|
||||||
|
|
||||||
|
console.log('发送文章数据:', articleData);
|
||||||
|
console.log('当前认证token是否存在:', !!localStorage.getItem('token'));
|
||||||
|
|
||||||
|
// 保存文章
|
||||||
|
articleService.createArticle(articleData)
|
||||||
|
.then(res => {
|
||||||
|
console.log('API响应:', res);
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
ElMessage.success('文章保存成功')
|
ElMessage.success('文章保存成功')
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(res.msg || '文章保存失败')
|
ElMessage.error(res.message || '文章保存失败')
|
||||||
}
|
}
|
||||||
}).catch(err => {
|
})
|
||||||
ElMessage.error(err.message || '文章保存失败')
|
.catch(err => {
|
||||||
})
|
console.error('保存失败错误详情:', err);
|
||||||
|
// 更详细的错误信息
|
||||||
|
if (err.response) {
|
||||||
|
console.error('错误状态码:', err.response.status);
|
||||||
|
console.error('错误响应数据:', err.response.data);
|
||||||
|
|
||||||
|
if (err.response.status === 401) {
|
||||||
|
ElMessage.error('未授权访问,请先登录');
|
||||||
|
} else if (err.response.status === 403) {
|
||||||
|
ElMessage.error('没有权限创建文章,请检查账号权限');
|
||||||
|
} else if (err.response.status === 400) {
|
||||||
|
ElMessage.error('数据验证失败: ' + (err.response.data?.message || '请检查输入'));
|
||||||
|
} else {
|
||||||
|
ElMessage.error('请求被拒绝,错误代码: ' + err.response.status);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.error(err.message || '文章保存失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.error-state-container .el-button {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* 文章头部区域 */
|
||||||
|
.article-header-section {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
border-bottom: 2px solid #ecf0f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文章标题 */
|
||||||
|
.article-main-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #2c3e50;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文章元信息 */
|
||||||
|
.article-meta-info {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20px;
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 元信息项 */
|
||||||
|
.meta-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文章内容区域 */
|
||||||
|
.article-content-area {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #34495e;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文章内容中的段落 */
|
||||||
|
.article-content-area p {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文章内容中的二级标题 */
|
||||||
|
.article-content-area h2 {
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin: 32px 0 16px 0;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #ecf0f1;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文章内容中的三级标题 */
|
||||||
|
.article-content-area h3 {
|
||||||
|
color: #34495e;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
margin: 24px 0 12px 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文章内容中的图片 */
|
||||||
|
.article-content-area img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 20px auto;
|
||||||
|
display: block;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文章内容中的引用 */
|
||||||
|
.article-content-area blockquote {
|
||||||
|
border-left: 4px solid #3498db;
|
||||||
|
padding-left: 16px;
|
||||||
|
color: #7f8c8d;
|
||||||
|
margin: 16px 0;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文章底部区域 */
|
||||||
|
.article-footer-section {
|
||||||
|
padding-top: 24px;
|
||||||
|
border-top: 2px solid #ecf0f1;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标签列表 */
|
||||||
|
.article-tag-list {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文章操作按钮组 */
|
||||||
|
.article-actions-group {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 相关文章区域 */
|
||||||
|
.related-articles-section {
|
||||||
|
padding-top: 32px;
|
||||||
|
border-top: 2px solid #ecf0f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 相关文章标题 */
|
||||||
|
.related-articles-section h3 {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 相关文章列表容器 */
|
||||||
|
.related-articles-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 相关文章卡片 */
|
||||||
|
.related-article-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 相关文章卡片悬停效果 */
|
||||||
|
.related-article-card:hover {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
transform: translateX(5px);
|
||||||
|
border-color: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-article-card i {
|
||||||
|
color: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-article-card span {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #495057;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-article-card:hover span {
|
||||||
|
color: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 评论区样式 */
|
||||||
|
.comment-section {
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 - 平板和手机 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
#article-detail-page {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-detail-wrapper,
|
||||||
|
.loading-state-container,
|
||||||
|
.error-state-container,
|
||||||
|
.empty-state-container {
|
||||||
|
padding: 20px;
|
||||||
|
margin: 0 15px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-main-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-meta-info {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content-area {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content-area h2 {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
margin: 24px 0 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content-area h3 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin: 20px 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-articles-section h3 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-article-card {
|
||||||
|
padding: 10px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-article-card span {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 - 小屏幕手机 */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.article-detail-wrapper {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-main-title {
|
||||||
|
font-size: 1.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-meta-info {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -144,24 +144,28 @@ const handleLogin = async () => {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// 模拟登录请求
|
// 模拟登录请求
|
||||||
let user = await loginService.login(loginForm)
|
let user = await (await loginService.login(loginForm)).data
|
||||||
if (!user) {
|
if (!user) {
|
||||||
ElMessage.error('登录失败,请检查用户名和密码')
|
ElMessage.error('登录失败,请检查用户名和密码')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
console.log('登录成功', user)
|
||||||
// 这里应该是实际的登录API调用
|
// 这里应该是实际的登录API调用
|
||||||
// console.log('登录请求数据:', loginForm)
|
// console.log('登录请求数据:', loginForm)
|
||||||
|
|
||||||
// 模拟登录成功
|
// 模拟登录成功
|
||||||
ElMessage.success('登录成功')
|
ElMessage.success('登录成功')
|
||||||
// 登录成功后,设置全局状态为已登录
|
// 登录成功后,设置全局状态为已登录
|
||||||
globalStore.setLoginStatus(user.success)
|
globalStore.setLoginStatus(true)
|
||||||
globalStore.setLoading(user.success)
|
|
||||||
console.log('globalStore.Login', globalStore.Login)
|
console.log('globalStore.Login', globalStore.Login)
|
||||||
// 保存登录状态
|
// 保存登录状态token
|
||||||
// if (loginForm.rememberMe) {
|
if (user.token) {
|
||||||
// localStorage.setItem('username', loginForm.username)
|
localStorage.setItem('token', user.token)
|
||||||
// }
|
}
|
||||||
|
if (user.username) {
|
||||||
|
// 记住用户名
|
||||||
|
globalStore.setUsername(user.username)
|
||||||
|
}
|
||||||
// 跳转到首页
|
// 跳转到首页
|
||||||
router.push('/')
|
router.push('/')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user