feat(登录): 完善登录逻辑和用户信息处理

refactor(文章): 重构文章创建和分类选择功能

style(布局): 调整主布局样式和响应式设计

fix(状态管理): 修正全局状态存储和清除逻辑

feat(登出): 添加登出功能按钮和逻辑

docs(类型): 扩展文章类型定义字段
This commit is contained in:
qingfeng1121
2025-11-03 16:14:55 +08:00
parent 6d90b5842f
commit a927ad5a4d
9 changed files with 487 additions and 70 deletions

View File

@@ -52,19 +52,21 @@
<h1 class="typewriter">{{ heroText }}</h1>
</div>
<!-- 主内容区域 -->
<!-- 提示区域 -->
<div id="content-section" :class="{ 'visible': isconts }">
<div class="nonsensetitle" v-if="classnonsenset">
<div class="nonsensetitleconst">
<h1>发癫中QAQ</h1>
<h1>{{Cardtitle}}</h1>
</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>
<!-- 分页区域 -->
@@ -84,10 +86,13 @@ const router = useRouter();
const route = useRoute();
// 全局状态管理
import { useGlobalStore } from '@/store/globalStore'
import { Card } from 'ant-design-vue';
const globalStore = useGlobalStore()
const Login = computed(() => globalStore.Login)
// 响应式状态
const Cardtitle = ref('');
const isleftmodluecontainer = ref(true);
const classhero = ref(false);
const isconts = ref(false);
const isScrollingleftmodlue = ref(false);
@@ -97,7 +102,7 @@ const windowwidth = ref(true);
const activeIndex = ref('home');
const localhome= 'home';
let rpsliturl = route.path.split('/')[1];
let rpsliturl = route.path.split('/');
// 搜索相关状态
const isSearchBoxOpen = ref(false);
@@ -105,7 +110,7 @@ const searchKeyword = ref('');
let searchCloseTimer: number | undefined;
// 打字机效果相关
let fullHeroText = '测试打字机效果';
let fullHeroText = '清疯不颠';
const heroText = ref('');
let heroIndex = 0;
let heroTimer: number | undefined;
@@ -218,7 +223,7 @@ const handleResize = () => {
windowwidth.value = window.innerWidth > 768;
// 根据屏幕大小调整内容区可见性
if (rpsliturl === localhome) {
if (rpsliturl[1] === localhome) {
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;
isScrollingleftmodlue.value = window.scrollY > 600;
}
@@ -250,30 +255,34 @@ const handleScroll = () => {
/**
* 监听路由变化
*/
watch(() => route.path, (newPath) => {
rpsliturl = route.path.split('/')[1];
updatePageState(rpsliturl);
setActiveIndex(rpsliturl);
watch(() => route.path, () => {
rpsliturl = route.path.split('/');
updatePageState(rpsliturl[1]);
setActiveIndex(rpsliturl[1]);
const localname = route.path.split('/')[2];
console.log(rpsliturl[1])
let articledata;
// 优先使用attributeId参数新接口
if (localname==='aericletype') {
if (rpsliturl[2]==='aericletype') {
articledata = globalStore.getValue('attribute')
}
// 搜索标题
if (localname==='aericletitle') {
if (rpsliturl[2]==='aericletitle') {
articledata = globalStore.getValue('title')
}
if (rpsliturl[1]==='nonsense') {
articledata = "疯言疯语"
}
// hero 标题
if (articledata) {
fullHeroText = articledata.name
Cardtitle.value = articledata.name
classhero.value = true;
}
// 跳转后回到顶部
window.scrollTo({ top: 0, behavior: 'smooth' });
// 首页内容区滚动动画仅大屏下生效
if (newPath.split('/')[1] === localhome) {
isconts.value = window.innerWidth <= 768 ? true : false;
if (rpsliturl[1] === localhome && rpsliturl[2] == '') {
// isconts.value = window.innerWidth <= 768 ? true : false;
// 首页时启动打字机效果
startTypewriter();
} else {
@@ -281,6 +290,12 @@ watch(() => route.path, (newPath) => {
heroText.value =fullHeroText;
if (heroTimer) clearInterval(heroTimer);
}
// 非首页时关闭左侧状态栏
if (rpsliturl[1] == "articlesave") {
isleftmodluecontainer.value = false;
} else {
isleftmodluecontainer.value = true;
}
}, { immediate: true });
/**

View File

@@ -19,7 +19,7 @@
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { useRouter } from 'vue-router';
import { articleService, messageService, categoryAttributeService } from '@/services'
import { articleService, messageService, categoryAttributeService ,loginService} from '@/services'
// 全局状态管理
import { useGlobalStore } from '@/store/globalStore'
const globalStore = useGlobalStore()
@@ -32,7 +32,8 @@ const buttons = [
{ id: 1, label: '新建文章', icon: 'icon-add-article' },
{ id: 2, label: '新建分类', icon: 'icon-create-category' },
{ 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 = [
{ 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 // 点击后收起
}
@@ -235,6 +254,11 @@ onBeforeUnmount(() => {
margin-left: 0;
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) {
@@ -261,7 +285,11 @@ onBeforeUnmount(() => {
opacity: 1;
}
.action-button:nth-child(5) {
transition: all 1.1s ease-in-out;
margin-left: 150px;
opacity: 1;
}
/* 响应式调整 */
@media (max-width: 768px) {
.establish-container {

View File

@@ -54,8 +54,8 @@ class ArticleService {
* @param {import('../types').ArticleDto} articleData - 文章数据
* @returns {Promise<import('../types').ApiResponse<import('../types').Article>>}
*/
createArticle(Article) {
return api.post('/articles', Article)
createArticle(articleData) {
return api.post('/articles', articleData)
}
/**

View File

@@ -27,7 +27,9 @@ class LoginService {
* 登出
*/
logout() {
return api.post("/auth/logout");
}
/**

View File

@@ -22,7 +22,7 @@ export const useGlobalStore = defineStore('global', {
// 全局数据对象,存储所有需要共享的数据
globalData: initialGlobalData,
// 特定状态属性从localStorage读取初始值
user: initialSpecificData.user || null,
username: initialSpecificData.username || null,
Login: initialSpecificData.Login || false,
notifications: initialSpecificData.notifications || []
}
@@ -80,7 +80,7 @@ export const useGlobalStore = defineStore('global', {
localStorage.setItem('globalStoreData', JSON.stringify(this.globalData))
// 持久化特定状态属性
localStorage.setItem('globalStoreSpecificData', JSON.stringify({
user: this.user,
username: this.username,
Login: this.Login,
notifications: this.notifications
}))
@@ -118,13 +118,13 @@ export const useGlobalStore = defineStore('global', {
*/
clearAll() {
this.globalData = {}
this.user = null
this.username = null
this.Login = false
this.notifications = []
// 清除localStorage中的数据
try {
localStorage.removeItem('globalStoreData')
// localStorage.removeItem('globalStoreSpecificData')
localStorage.removeItem('globalStoreSpecificData')
} catch (error) {
console.error('Failed to clear data from localStorage:', error)
}
@@ -134,8 +134,8 @@ export const useGlobalStore = defineStore('global', {
* 设置用户信息
* @param {Object} userInfo - 用户信息对象
*/
setUser(userInfo) {
this.user = userInfo
setUsername(username) {
this.username = username
// 持久化到localStorage
this._persistData()
},

View File

@@ -240,6 +240,9 @@ p {
margin: var(--content-margin);
}
.RouterViewpage.forbidwidth {
width: 100%;
}
/* 分页区样式 */
.Pagination {
align-self: center;
@@ -247,7 +250,7 @@ p {
}
/* 左侧状态栏样式 */
.leftmodluepage {
.leftmodluecontainer {
width: var(--leftmodlue-width);
}

View File

@@ -28,6 +28,10 @@ export interface ArticleDto {
attributeid: number
img?: string
status?: number
viewCount?: number
likes?: number
markdownscontent: string
}
/**

View File

@@ -1,35 +1,51 @@
<template>
<div id="allstyle">
<div class="article-header-section">
<h1 class="article-main-title">{{ Articleform.title }}11</h1>
<div class="article-meta-info">
<span class="meta-item">
<i class="el-icon-date"></i>
<!-- {{ formatDate(Articleform.createdAt) }} -->
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 id="allstyle">
<div class="article-header-section">
<div style="text-align: center;">
<input type="text" v-model="Articleform.title" class="article-main-title" placeholder="请输入标题"
@focus="($event.target as HTMLInputElement).placeholder = ''"
@blur="($event.target as HTMLInputElement).placeholder = '请输入标题'"
style="border: none; outline: none; background: transparent; width: 100%; font-size: 2.5rem; font-weight: 700; line-height: 1.2; text-align: center;" />
</div>
<div>
<MdEditor v-model="Articleform.markdownscontent" htmlPreview preview={false} class="markdown-editor" @on-save="handleSave" />
<div class="article-meta-info" style="text-align: center;">
<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>
<MdEditor v-model="Articleform.markdownscontent" class="markdown-editor" @on-save="handleSave" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { MdEditor } from 'md-editor-v3';
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 { ElMessage } from 'element-plus';
const Articleform = ref<Article>({
articleid: 0,
title: '',
@@ -41,28 +57,373 @@ const Articleform = ref<Article>({
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) => {
console.log(Articleform.value);
Articleform.value.markdownscontent = markdown;
// 这里可以添加保存逻辑,比如发送到服务器
articleService.createArticle({
// 验证必填字段
if (!Articleform.value.title || !Articleform.value.attributeid) {
ElMessage.warning('请填写必填字段:标题和分类属性');
return;
}
// 构建请求数据
const articleData = {
title: Articleform.value.title,
content: Articleform.value.content,
attributeid: Articleform.value.attributeid,
categoryName: Articleform.value.categoryName,
attributeid: Number(Articleform.value.attributeid),
status: Number(Articleform.value.status),
viewCount: 0,
likes: 0,
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) {
ElMessage.success('文章保存成功')
ElMessage.success('文章保存成功')
} 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>
<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>

View File

@@ -144,24 +144,28 @@ const handleLogin = async () => {
}
try {
// 模拟登录请求
let user = await loginService.login(loginForm)
let user = await (await loginService.login(loginForm)).data
if (!user) {
ElMessage.error('登录失败,请检查用户名和密码')
return
}
console.log('登录成功', user)
// 这里应该是实际的登录API调用
// console.log('登录请求数据:', loginForm)
// 模拟登录成功
ElMessage.success('登录成功')
// 登录成功后,设置全局状态为已登录
globalStore.setLoginStatus(user.success)
globalStore.setLoading(user.success)
globalStore.setLoginStatus(true)
console.log('globalStore.Login', globalStore.Login)
// 保存登录状态
// if (loginForm.rememberMe) {
// localStorage.setItem('username', loginForm.username)
// }
// 保存登录状态token
if (user.token) {
localStorage.setItem('token', user.token)
}
if (user.username) {
// 记住用户名
globalStore.setUsername(user.username)
}
// 跳转到首页
router.push('/')
} catch (error) {