增加个人中心

This commit is contained in:
扫地僧 2025-12-26 22:45:34 +08:00
parent 4e9720de5c
commit 06779d999f
12 changed files with 2829 additions and 43 deletions

View File

@ -16,6 +16,9 @@ declare module 'vue' {
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
ElButton: typeof import('element-plus/es')['ElButton']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
ElCol: typeof import('element-plus/es')['ElCol']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
@ -27,8 +30,13 @@ declare module 'vue' {
ElInput: typeof import('element-plus/es')['ElInput']
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElRow: typeof import('element-plus/es')['ElRow']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
ElTag: typeof import('element-plus/es')['ElTag']
ElUpload: typeof import('element-plus/es')['ElUpload']
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']

View File

@ -1,41 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
defineProps<{ msg: string }>()
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Learn more about IDE Support for Vue in the
<a
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
target="_blank"
>Vue Docs Scaling up Guide</a
>.
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

View File

@ -57,6 +57,11 @@ const router = createRouter({
name: "login",
component: () => import("@/views/login/index.vue"),
},
{
path: "/user/profile",
name: "userProfile",
component: () => import("@/views/user/profile/index.vue"),
},
],
});

View File

@ -95,8 +95,7 @@ const handleCommand = (command: string) => {
switch (command) {
case "profile":
console.log("跳转到个人中心");
// TODO:
ElMessage.info("跳转到个人中心");
router.push('/user/profile');
break;
case "account":
console.log("跳转到账号管理");

View File

@ -0,0 +1,569 @@
<template>
<div class="content-section">
<div class="content-header">
<h2 class="content-title">文章管理</h2>
<p class="content-desc">管理您的技术文章</p>
</div>
<div class="content-body">
<!-- 操作栏 -->
<div class="action-bar">
<el-button type="primary" @click="showPublishDialog = true">
<i class="fa-solid fa-plus"></i>
发布文章
</el-button>
<div class="search-box">
<el-input
v-model="searchText"
placeholder="搜索文章..."
clearable
@input="handleSearch"
>
<template #prefix>
<i class="fa-solid fa-search"></i>
</template>
</el-input>
</div>
</div>
<!-- 文章列表 -->
<div class="article-list">
<div v-if="filteredArticles.length === 0" class="empty-state">
<i class="fa-solid fa-file-alt"></i>
<p>{{ searchText ? '未找到相关文章' : '暂无文章' }}</p>
</div>
<div v-else class="article-grid">
<div
v-for="article in paginatedArticles"
:key="article.id"
class="article-card"
>
<div class="article-content-area">
<div class="article-header">
<el-tag :type="getStatusType(article.status)" size="small">
{{ getStatusText(article.status) }}
</el-tag>
<h3 class="article-title">{{ article.title }}</h3>
</div>
<div class="article-meta">
<span class="article-category">{{ getCategoryText(article.category) }}</span>
<span class="article-date">{{ article.createTime }}</span>
<span class="article-views">{{ article.views }} 阅读</span>
</div>
<div class="article-content">
{{ article.content.substring(0, 100) }}...
</div>
</div>
<div class="article-actions">
<el-button size="small" @click="editArticle(article)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteArticle(article)">删除</el-button>
</div>
</div>
</div>
<!-- 分页组件 -->
<div v-if="filteredArticles.length > 0" class="pagination-wrapper">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[5, 10, 20, 50]"
:total="totalArticles"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</div>
</div>
</div>
<!-- 发布/编辑文章弹窗 -->
<el-dialog
v-model="showPublishDialog"
:title="isEditMode ? '编辑文章' : '发布文章'"
width="800px"
:close-on-click-modal="false"
>
<el-form :model="articleForm" label-width="80px" :rules="formRules" ref="formRef">
<el-form-item label="标题" prop="title" required>
<el-input
v-model="articleForm.title"
placeholder="请输入文章标题"
maxlength="100"
show-word-limit
/>
</el-form-item>
<el-form-item label="分类" prop="category" required>
<el-select v-model="articleForm.category" placeholder="请选择分类">
<el-option
v-for="category in categories"
:key="category.value"
:label="category.label"
:value="category.value"
/>
</el-select>
</el-form-item>
<el-form-item label="标签">
<el-input
v-model="articleForm.tags"
placeholder="请输入标签,用逗号分隔"
/>
</el-form-item>
<el-form-item label="内容" prop="content" required>
<el-input
v-model="articleForm.content"
type="textarea"
:rows="12"
placeholder="请输入文章内容..."
maxlength="10000"
show-word-limit
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showPublishDialog = false">取消</el-button>
<el-button type="primary" @click="submitArticle" :loading="loading">
{{ isEditMode ? '保存修改' : '发布文章' }}
</el-button>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
interface ArticleForm {
title: string;
category: string;
tags: string;
content: string;
}
interface Article {
id: string;
title: string;
content: string;
category: string;
tags: string;
status: 'draft' | 'published' | 'hidden';
createTime: string;
updateTime: string;
views: number;
}
interface Category {
value: string;
label: string;
}
//
const showPublishDialog = ref(false);
const isEditMode = ref(false);
const searchText = ref('');
const loading = ref(false);
const formRef = ref();
//
const currentPage = ref(1);
const pageSize = ref(10);
const totalArticles = computed(() => filteredArticles.value.length);
//
const articleForm = ref<ArticleForm>({
title: '',
category: '',
tags: '',
content: ''
});
//
const formRules = {
title: [
{ required: true, message: '请输入文章标题', trigger: 'blur' },
{ min: 1, max: 100, message: '标题长度在 1 到 100 个字符', trigger: 'blur' }
],
category: [
{ required: true, message: '请选择文章分类', trigger: 'change' }
],
content: [
{ required: true, message: '请输入文章内容', trigger: 'blur' },
{ min: 10, message: '内容不能少于10个字符', trigger: 'blur' }
]
};
//
const articles = ref<Article[]>([
{
id: '1',
title: 'Vue 3 响应式原理详解',
content: 'Vue 3 的响应式系统基于 Proxy 实现,与 Vue 2 的 Object.defineProperty 相比具有更好的性能和更完整的响应式覆盖...',
category: 'frontend',
tags: 'Vue,响应式,Proxy',
status: 'published',
createTime: '2024-01-15',
updateTime: '2024-01-15',
views: 1250
},
{
id: '2',
title: 'Go 语言高并发编程实践',
content: '在 Go 语言中,高并发编程是其核心特性之一。通过 goroutine 和 channel我们可以轻松实现并发编程...',
category: 'backend',
tags: 'Go,并发,goroutine',
status: 'published',
createTime: '2024-01-10',
updateTime: '2024-01-12',
views: 890
}
]);
//
const categories = ref<Category[]>([
{ value: 'frontend', label: '前端开发' },
{ value: 'backend', label: '后端开发' },
{ value: 'mobile', label: '移动开发' },
{ value: 'ai', label: '人工智能' },
{ value: 'devops', label: 'DevOps' },
{ value: 'other', label: '其他' }
]);
//
const filteredArticles = computed(() => {
let filtered = articles.value;
//
if (searchText.value) {
filtered = filtered.filter(article =>
article.title.toLowerCase().includes(searchText.value.toLowerCase()) ||
article.content.toLowerCase().includes(searchText.value.toLowerCase()) ||
article.tags.toLowerCase().includes(searchText.value.toLowerCase())
);
}
return filtered;
});
//
const paginatedArticles = computed(() => {
const start = (currentPage.value - 1) * pageSize.value;
const end = start + pageSize.value;
return filteredArticles.value.slice(start, end);
});
//
const handlePageChange = (page: number) => {
currentPage.value = page;
};
const handleSizeChange = (size: number) => {
pageSize.value = size;
currentPage.value = 1; //
};
//
const getCategoryText = (category: string) => {
const cat = categories.value.find(c => c.value === category);
return cat ? cat.label : category;
};
//
const getStatusText = (status: string) => {
switch (status) {
case 'draft': return '草稿';
case 'published': return '已发布';
case 'hidden': return '隐藏';
default: return status;
}
};
//
const getStatusType = (status: string) => {
switch (status) {
case 'draft': return '';
case 'published': return 'success';
case 'hidden': return 'warning';
default: return '';
}
};
//
const handleSearch = () => {
//
};
//
const editArticle = (article: Article) => {
isEditMode.value = true;
articleForm.value = {
title: article.title,
category: article.category,
tags: article.tags,
content: article.content
};
showPublishDialog.value = true;
};
//
const viewArticle = () => {
ElMessage.info('查看文章功能开发中...');
};
//
const deleteArticle = async (article: Article) => {
try {
await ElMessageBox.confirm(
`确定要删除文章"${article.title}"吗?此操作不可恢复。`,
'确认删除',
{
confirmButtonText: '确定删除',
cancelButtonText: '取消',
type: 'warning',
}
);
// API
const index = articles.value.findIndex(a => a.id === article.id);
if (index > -1) {
articles.value.splice(index, 1);
//
const totalPages = Math.ceil(articles.value.length / pageSize.value);
if (currentPage.value > totalPages && totalPages > 0) {
currentPage.value = totalPages;
}
ElMessage.success('文章删除成功');
}
} catch (error) {
//
}
};
//
const submitArticle = async () => {
if (!formRef.value) return;
await formRef.value.validate(async (valid: boolean) => {
if (!valid) return;
loading.value = true;
try {
if (isEditMode.value) {
//
ElMessage.success('文章更新成功');
} else {
//
const newArticle: Article = {
id: Date.now().toString(),
title: articleForm.value.title,
content: articleForm.value.content,
category: articleForm.value.category,
tags: articleForm.value.tags,
status: 'published',
createTime: new Date().toLocaleDateString(),
updateTime: new Date().toLocaleDateString(),
views: 0
};
articles.value.unshift(newArticle);
currentPage.value = 1; //
ElMessage.success('文章发布成功');
}
showPublishDialog.value = false;
resetForm();
} catch (error) {
ElMessage.error('操作失败,请重试');
} finally {
loading.value = false;
}
});
};
//
const resetForm = () => {
articleForm.value = {
title: '',
category: '',
tags: '',
content: ''
};
if (formRef.value) {
formRef.value.clearValidate();
}
};
//
const initArticles = () => {
// API
};
onMounted(() => {
initArticles();
});
</script>
<style lang="less" scoped>
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
gap: 16px;
.search-box {
width: 300px;
}
}
.article-list {
.article-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: 20px;
}
.article-card {
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 20px;
transition: all 0.3s ease;
display: flex;
align-items: flex-start;
gap: 16px;
&:hover {
border-color: #1677ff;
box-shadow: 0 4px 12px rgba(22, 119, 255, 0.15);
}
.article-content-area {
flex: 1;
min-width: 0; //
.article-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
.article-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin: 0;
flex: 1;
margin-left: 12px;
}
}
.article-meta {
display: flex;
gap: 16px;
margin-bottom: 12px;
font-size: 12px;
color: #909399;
.article-category {
color: #1677ff;
font-weight: 500;
}
}
.article-content {
color: #606266;
line-height: 1.6;
margin-bottom: 0;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
line-clamp: 3;
}
}
.article-actions {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8px;
flex-shrink: 0;
.el-button {
width: 80px;
justify-content: center;
}
}
}
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
i {
font-size: 48px;
margin-bottom: 16px;
display: block;
}
p {
font-size: 14px;
margin: 0;
}
}
.pagination-wrapper {
display: flex;
justify-content: center;
margin-top: 32px;
padding: 20px 0;
.el-pagination {
::v-deep(.el-pager li) {
min-width: 36px;
height: 36px;
line-height: 36px;
border-radius: 6px;
margin: 0 2px;
}
::v-deep(.el-pager li.is-active) {
background-color: #1677ff;
color: white;
}
::v-deep(.btn-prev, .btn-next) {
min-width: 36px;
height: 36px;
border-radius: 6px;
}
}
}
/* 弹窗表单样式 */
.el-dialog {
::v-deep(.el-dialog__body) {
padding: 20px 24px;
}
.el-form {
.el-form-item {
margin-bottom: 20px;
}
.el-textarea {
::v-deep(.el-textarea__inner) {
resize: vertical;
min-height: 200px;
}
}
}
}
</style>

View File

@ -0,0 +1,408 @@
<template>
<div class="content-section">
<div class="content-header">
<h2 class="content-title">基本资料</h2>
<p class="content-desc">管理您的个人信息</p>
</div>
<div class="content-body">
<el-tabs v-model="activeTab" class="profile-tabs">
<!-- 头像修改 Tab -->
<el-tab-pane label="头像修改" name="avatar">
<template #label>
<i class="fa-solid fa-camera"></i>
头像修改
</template>
<div class="avatar-section">
<div class="avatar-upload">
<div class="avatar-preview">
<img
v-if="avatarPreview || cachedAvatar || userInfo.avatar"
:src="avatarPreview || cachedAvatar || userInfo.avatar"
alt="头像预览"
class="avatar-image"
/>
<div v-else class="avatar-placeholder">
<i class="fa-solid fa-user"></i>
<span>未设置头像</span>
</div>
</div>
<div class="avatar-actions">
<el-upload
ref="uploadRef"
action=""
:auto-upload="false"
:show-file-list="false"
:on-change="handleAvatarChange"
accept="image/*"
class="avatar-uploader"
>
<el-button size="small" type="primary">
<i class="fa-solid fa-upload"></i>
选择头像
</el-button>
</el-upload>
<el-button
v-if="avatarPreview || cachedAvatar || userInfo.avatar"
size="small"
@click="removeAvatar"
type="danger"
plain
>
<i class="fa-solid fa-trash"></i>
移除头像
</el-button>
</div>
</div>
<div class="avatar-tips">
<p>支持 JPGPNGGIF 格式大小不超过 2MB</p>
</div>
<div class="avatar-save">
<el-button type="primary" @click="saveAvatar" :loading="avatarSaving">
保存头像
</el-button>
</div>
</div>
</el-tab-pane>
<!-- 资料修改 Tab -->
<el-tab-pane label="资料修改" name="info">
<template #label>
<i class="fa-solid fa-user-edit"></i>
资料修改
</template>
<el-form :model="basicForm" label-width="120px" class="profile-form">
<el-row :gutter="24">
<el-col :span="24">
<el-form-item label="用户名" required>
<el-input v-model="basicForm.user_name" placeholder="请输入用户名" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="邮箱" required>
<el-input v-model="basicForm.email" placeholder="请输入邮箱" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="24">
<el-form-item label="手机号">
<el-input v-model="basicForm.phone" placeholder="请输入手机号" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="注册时间">
<el-input v-model="userInfo.register_time" readonly />
</el-form-item>
</el-col>
</el-row>
<el-form-item>
<el-button type="primary" @click="saveBasicInfo" :loading="infoSaving">
保存修改
</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, withDefaults } from "vue";
import { ElMessage } from "element-plus";
// Props
interface UserInfo {
user_name: string;
user_account: string;
email: string;
phone: string;
avatar: string;
register_time: string;
last_login: string;
}
interface BasicForm {
user_name: string;
email: string;
phone: string;
}
interface Props {
userInfo?: UserInfo;
basicForm?: BasicForm;
}
const props = withDefaults(defineProps<Props>(), {
userInfo: () => ({
user_name: '',
user_account: '',
email: '',
phone: '',
avatar: '',
register_time: '',
last_login: ''
}),
basicForm: () => ({
user_name: '',
email: '',
phone: ''
})
});
// Emits
const emit = defineEmits<{
updateBasicInfo: [data: { basicForm: BasicForm; avatarPreview?: string }];
removeAvatar: [];
}>();
// Tab
const activeTab = ref('avatar');
//
const avatarFile = ref<File | null>(null);
const avatarPreview = ref<string>('');
const cachedAvatar = ref<string>('');
//
const avatarSaving = ref(false);
const infoSaving = ref(false);
//
const initCachedAvatar = (avatar: string) => {
cachedAvatar.value = avatar || '';
};
//
const handleAvatarChange = (file: any) => {
avatarFile.value = file.raw;
// URL
const reader = new FileReader();
reader.onload = (e) => {
if (e.target && typeof e.target.result === 'string') {
avatarPreview.value = e.target.result;
}
};
reader.readAsDataURL(file.raw);
};
//
const removeAvatar = () => {
avatarFile.value = null;
avatarPreview.value = '';
cachedAvatar.value = '';
emit('removeAvatar');
};
//
const saveAvatar = async () => {
try {
avatarSaving.value = true;
//
if (!avatarFile.value && !avatarPreview.value) {
ElMessage.warning('请先选择头像');
return;
}
//
if (avatarFile.value && avatarFile.value.size) {
const maxSize = 2 * 1024 * 1024; // 2MB
if (avatarFile.value.size > maxSize) {
ElMessage.error('头像文件大小不能超过 2MB');
return;
}
}
// API
// const formData = new FormData();
// if (avatarFile.value) {
// formData.append('avatar', avatarFile.value);
// }
ElMessage.success('头像保存成功');
//
emit('updateBasicInfo', {
basicForm: props.basicForm,
avatarPreview: avatarPreview.value
});
//
avatarPreview.value = '';
avatarFile.value = null;
} catch (error) {
ElMessage.error('保存失败,请重试');
} finally {
avatarSaving.value = false;
}
};
//
const saveBasicInfo = async () => {
try {
infoSaving.value = true;
//
if (!props.basicForm.user_name.trim()) {
ElMessage.error('请输入用户名');
return;
}
if (!props.basicForm.email.trim()) {
ElMessage.error('请输入邮箱');
return;
}
// API
// const formData = new FormData();
// formData.append('user_name', props.basicForm.user_name);
// formData.append('email', props.basicForm.email);
// formData.append('phone', props.basicForm.phone);
ElMessage.success('基本资料保存成功');
//
emit('updateBasicInfo', {
basicForm: props.basicForm
});
} catch (error) {
ElMessage.error('保存失败,请重试');
} finally {
infoSaving.value = false;
}
};
//
defineExpose({
initCachedAvatar
});
</script>
<style lang="less" scoped>
.profile-tabs {
::v-deep(.el-tabs__header) {
margin: 0 0 24px 0;
.el-tabs__nav-wrap::after {
display: none;
}
.el-tabs__item {
font-size: 16px;
font-weight: 500;
color: #666;
&.is-active {
color: #1677ff;
}
i {
margin-right: 8px;
}
}
}
::v-deep(.el-tabs__content) {
padding: 0;
}
}
.avatar-section {
text-align: center;
max-width: 400px;
margin: 0 auto;
.avatar-upload {
display: inline-block;
margin-bottom: 16px;
.avatar-preview {
width: 140px;
height: 140px;
border-radius: 50%;
border: 3px solid #e4e7ed;
margin: 0 auto 20px;
overflow: hidden;
background: #f5f7fa;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
.avatar-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-placeholder {
text-align: center;
color: #c0c4cc;
i {
font-size: 42px;
margin-bottom: 8px;
display: block;
}
span {
font-size: 14px;
}
}
}
.avatar-actions {
display: flex;
gap: 12px;
justify-content: center;
align-items: center;
margin-bottom: 16px;
.avatar-uploader {
.el-button {
height: 36px;
padding: 0 20px;
font-size: 14px;
}
}
}
}
.avatar-tips {
color: #909399;
font-size: 12px;
margin-bottom: 24px;
p {
margin: 0;
}
}
.avatar-save {
.el-button {
height: 40px;
padding: 0 32px;
font-size: 16px;
}
}
}
.profile-form {
max-width: 600px;
margin: 0 auto;
.el-form-item {
margin-bottom: 24px;
}
.el-button {
height: 40px;
padding: 0 32px;
font-size: 16px;
}
}
</style>

View File

@ -0,0 +1,405 @@
<template>
<Header />
<div class="profile-container">
<div class="profile-sidebar">
<div class="menu">
<!-- 动态渲染菜单分组 -->
<div
v-for="group in menuGroups"
:key="group.id"
class="menu-group"
:data-group="group.id"
>
<div class="menu-group-title" @click="toggleGroup(group.id)">
<i :class="group.icon"></i>
<span>{{ group.title }}</span>
<i class="layui-icon collapse-icon" :class="isGroupCollapsed(group.id) ? 'layui-icon-right' : 'layui-icon-down'"></i>
</div>
<div class="menu-group-content" :class="{ 'expanded': !isGroupCollapsed(group.id) }">
<div
v-for="item in group.items"
:key="item.id"
class="menu-item"
:class="{ active: activeMenu === item.id }"
@click="handleMenuClick(item.id)"
>
<div class="menu-icon">
<i :class="item.icon"></i>
</div>
<div class="menu-content">
<span class="menu-title">{{ item.title }}</span>
<span class="menu-desc">{{ item.desc }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="profile-main">
<div class="content-area">
<!-- 基本资料 -->
<BasicInfo v-if="activeMenu === 'profile-basic'" />
<!-- 我的钱包 -->
<Wallet v-if="activeMenu === 'profile-wallet'" />
<!-- 我的消息 -->
<Messages v-if="activeMenu === 'profile-messages'" />
<!-- 安全设置 -->
<Security v-if="activeMenu === 'profile-security'" />
<!-- 发布文章 -->
<ArticlePublish v-if="activeMenu === 'article-publish'" />
<!-- 发布资源 -->
<ResourcePublish v-if="activeMenu === 'apps-publish'" />
<!-- 系统通知 -->
<Notifications v-if="activeMenu === 'profile-notifications'" />
</div>
</div>
</div>
<Footer />
</template>
<script lang="ts" setup>
import Header from "@/views/components/header.vue";
import Footer from "@/views/components/footer.vue";
import BasicInfo from "@/views/user/profile/basicInfo.vue";
import Wallet from "@/views/user/profile/wallet.vue";
import Messages from "@/views/user/profile/messages.vue";
import Security from "@/views/user/profile/security.vue";
import ArticlePublish from "@/views/user/profile/articlePublish.vue";
import ResourcePublish from "@/views/user/profile/resourcePublish.vue";
import Notifications from "@/views/user/profile/notifications.vue";
import { ref } from "vue";
//
const activeMenu = ref('profile-basic'); //
const collapsedGroups = ref(['apps', 'system']); //
//
const menuGroups = ref([
{
id: 'personal',
title: '个人中心',
icon: 'fa-solid fa-user',
items: [
{
id: 'profile-basic',
title: '基本资料',
desc: '个人信息管理',
icon: 'fa-regular fa-file-lines'
},
{
id: 'profile-wallet',
title: '我的钱包',
desc: '余额与交易记录',
icon: 'fa-solid fa-wallet'
},
{
id: 'profile-messages',
title: '我的消息',
desc: '系统消息提醒',
icon: 'fa-solid fa-message'
},
{
id: 'profile-security',
title: '安全设置',
desc: '账户安全配置',
icon: 'fa-solid fa-shield'
}
]
},
{
id: 'apps',
title: '网站功能',
icon: 'fa-solid fa-grip',
items: [
{
id: 'article-publish',
title: '文章管理',
desc: '管理站内文章',
icon: 'fa-solid fa-file-alt'
},
{
id: 'apps-publish',
title: '资源管理',
desc: '管理应用资源',
icon: 'fa-solid fa-cubes'
}
]
},
{
id: 'system',
title: '系统设置',
icon: 'fa-solid fa-gear',
items: [
{
id: 'profile-notifications',
title: '系统通知',
desc: '通知偏好设置',
icon: 'fa-solid fa-bell'
}
]
}
]);
//
const handleMenuClick = (target: string) => {
activeMenu.value = target;
};
// /
const toggleGroup = (groupName: string) => {
const isCurrentlyCollapsed = collapsedGroups.value.includes(groupName);
if (isCurrentlyCollapsed) {
//
collapsedGroups.value = menuGroups.value
.map(group => group.id)
.filter(id => id !== groupName);
} else {
//
collapsedGroups.value.push(groupName);
}
};
//
const isGroupCollapsed = (groupName: string) => {
return collapsedGroups.value.includes(groupName);
};
</script>
<style lang="less">
.profile-container {
display: flex;
max-width: 1200px;
margin: 120px auto 40px;
gap: 24px;
padding: 0 20px;
.profile-sidebar {
width: 260px;
flex-shrink: 0;
.menu {
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
padding: 16px 0;
.menu-group {
&:last-child {
.menu-group-title {
border-bottom: none !important;
}
}
.menu-group-title {
padding: 16px 24px 12px;
font-size: 12px;
color: #1677ff;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
display: flex;
align-items: center;
gap: 8px;
border-bottom: 1px solid #f5f5f5;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.3s;
position: relative;
&:hover {
color: #1677ff;
background: rgba(22, 119, 255, 0.05);
transform: translateX(2px);
}
.layui-icon {
font-size: 14px;
color: #1677ff;
transition: transform 0.3s;
}
.collapse-icon {
margin-left: auto;
font-size: 12px;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
}
.menu-group-content {
max-height: 0;
overflow: hidden;
opacity: 0;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&.expanded {
max-height: 500px; //
opacity: 1;
}
.menu-item {
padding: 12px 24px;
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
transition: all 0.3s;
border-left: 3px solid transparent;
&:hover {
background: #f5f5f5;
border-left-color: #1677ff;
transform: translateX(4px);
}
&.active {
background: #f0f8ff;
border-left-color: #1677ff;
transform: translateX(4px);
box-shadow: 0 2px 8px rgba(22, 119, 255, 0.15);
.menu-title {
color: #1677ff;
}
}
.menu-icon {
width: 20px;
text-align: center;
i {
font-size: 16px;
color: #1677ff;
}
}
.menu-content {
flex: 1;
.menu-title {
display: block;
font-size: 14px;
color: #1677ff;
font-weight: 500;
margin-bottom: 2px;
}
.menu-desc {
display: block;
font-size: 12px;
color: #999;
}
}
}
}
}
}
}
.profile-main {
flex: 1;
.content-area {
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
min-height: 600px;
.content-section {
.content-header {
padding: 24px 32px 16px;
border-bottom: 1px solid #f0f0f0;
.content-title {
font-size: 20px;
color: #333;
margin: 0 0 8px 0;
font-weight: 600;
}
.content-desc {
color: #666;
margin: 0;
font-size: 14px;
}
}
.content-body {
padding: 24px 32px;
.profile-form, .security-form {
max-width: 600px;
.el-form-item {
margin-bottom: 20px;
}
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
i {
font-size: 48px;
margin-bottom: 16px;
display: block;
}
p {
font-size: 14px;
margin: 0;
}
}
}
}
}
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.profile-container {
flex-direction: column;
margin: 100px auto 20px;
padding: 0 15px;
.profile-sidebar {
width: 100%;
margin-bottom: 20px;
.menu {
.menu-group {
.menu-group-title {
padding: 12px 20px 8px;
}
.menu-group-content .menu-item {
padding: 10px 20px;
}
}
}
}
.profile-main .content-area .content-section {
.content-header {
padding: 20px 24px 12px;
}
.content-body {
padding: 20px 24px;
}
}
}
}
</style>

View File

@ -0,0 +1,185 @@
<template>
<div class="content-section">
<div class="content-header">
<h2 class="content-title">我的消息</h2>
<p class="content-desc">查看系统消息和通知</p>
</div>
<div class="content-body">
<div v-if="messages.length === 0" class="empty-state">
<i class="fa-solid fa-message"></i>
<p>暂无消息</p>
</div>
<div v-else class="message-list">
<div
v-for="message in messages"
:key="message.id"
class="message-item"
:class="{ 'unread': !message.read }"
@click="markAsRead(message.id)"
>
<div class="message-icon">
<i :class="getMessageIcon(message.type)"></i>
</div>
<div class="message-content">
<div class="message-header">
<h4 class="message-title">{{ message.title }}</h4>
<span class="message-time">{{ message.time }}</span>
</div>
<p class="message-text">{{ message.content }}</p>
</div>
<div v-if="!message.read" class="unread-dot"></div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from "vue";
interface Message {
id: string;
title: string;
content: string;
time: string;
type: 'system' | 'notification' | 'warning';
read: boolean;
}
//
const messages = ref<Message[]>([]);
//
const getMessageIcon = (type: string) => {
switch (type) {
case 'system':
return 'fa-solid fa-cog';
case 'notification':
return 'fa-solid fa-bell';
case 'warning':
return 'fa-solid fa-exclamation-triangle';
default:
return 'fa-solid fa-info';
}
};
//
const markAsRead = (messageId: string) => {
const message = messages.value.find(m => m.id === messageId);
if (message) {
message.read = true;
}
};
//
const initMessages = () => {
// API
messages.value = [];
};
onMounted(() => {
initMessages();
});
</script>
<style lang="less" scoped>
.message-list {
.message-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 16px;
border: 1px solid #f0f0f0;
border-radius: 8px;
margin-bottom: 12px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
&:hover {
border-color: #1677ff;
background: #f8f9ff;
}
&.unread {
border-left: 4px solid #1677ff;
background: #f0f8ff;
}
.message-icon {
width: 40px;
height: 40px;
border-radius: 50%;
background: #1677ff;
color: white;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
i {
font-size: 16px;
}
}
.message-content {
flex: 1;
.message-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px;
.message-title {
font-size: 16px;
color: #333;
margin: 0;
font-weight: 500;
}
.message-time {
font-size: 12px;
color: #999;
white-space: nowrap;
margin-left: 12px;
}
}
.message-text {
font-size: 14px;
color: #666;
margin: 0;
line-height: 1.5;
}
}
.unread-dot {
width: 8px;
height: 8px;
background: #1677ff;
border-radius: 50%;
position: absolute;
top: 16px;
right: 16px;
}
}
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
i {
font-size: 48px;
margin-bottom: 16px;
display: block;
}
p {
font-size: 14px;
margin: 0;
}
}
</style>

View File

@ -0,0 +1,257 @@
<template>
<div class="content-section">
<div class="content-header">
<h2 class="content-title">系统通知</h2>
<p class="content-desc">设置通知偏好</p>
</div>
<div class="content-body">
<div class="notification-settings">
<div class="setting-group">
<h3 class="group-title">邮件通知</h3>
<div class="setting-items">
<div class="setting-item">
<div class="setting-info">
<div class="setting-name">系统消息</div>
<div class="setting-desc">接收系统更新和维护通知</div>
</div>
<el-switch v-model="settings.email.system" />
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-name">账户安全</div>
<div class="setting-desc">接收登录异常和安全提醒</div>
</div>
<el-switch v-model="settings.email.security" />
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-name">内容更新</div>
<div class="setting-desc">接收文章和资源更新通知</div>
</div>
<el-switch v-model="settings.email.content" />
</div>
</div>
</div>
<div class="setting-group">
<h3 class="group-title">站内消息</h3>
<div class="setting-items">
<div class="setting-item">
<div class="setting-info">
<div class="setting-name">评论回复</div>
<div class="setting-desc">有人回复您的评论时通知</div>
</div>
<el-switch v-model="settings.inSite.comments" />
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-name">点赞通知</div>
<div class="setting-desc">收到点赞时通知</div>
</div>
<el-switch v-model="settings.inSite.likes" />
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-name">关注通知</div>
<div class="setting-desc">有人关注您时通知</div>
</div>
<el-switch v-model="settings.inSite.follows" />
</div>
</div>
</div>
<div class="setting-group">
<h3 class="group-title">推送通知</h3>
<div class="setting-items">
<div class="setting-item">
<div class="setting-info">
<div class="setting-name">浏览器推送</div>
<div class="setting-desc">在浏览器中接收推送通知</div>
</div>
<el-switch v-model="settings.push.browser" />
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-name">移动推送</div>
<div class="setting-desc">在移动设备上接收推送通知</div>
</div>
<el-switch v-model="settings.push.mobile" />
</div>
</div>
</div>
<div class="setting-actions">
<el-button type="primary" @click="saveSettings" :loading="loading">
保存设置
</el-button>
<el-button @click="resetSettings" style="margin-left: 12px;">
重置为默认
</el-button>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted } from "vue";
import { ElMessage } from "element-plus";
interface NotificationSettings {
email: {
system: boolean;
security: boolean;
content: boolean;
};
inSite: {
comments: boolean;
likes: boolean;
follows: boolean;
};
push: {
browser: boolean;
mobile: boolean;
};
}
//
const settings = reactive<NotificationSettings>({
email: {
system: true,
security: true,
content: false
},
inSite: {
comments: true,
likes: true,
follows: true
},
push: {
browser: false,
mobile: false
}
});
const loading = ref(false);
//
const saveSettings = async () => {
loading.value = true;
try {
// API
await new Promise(resolve => setTimeout(resolve, 1000)); // API
//
localStorage.setItem('notification_settings', JSON.stringify(settings));
ElMessage.success('设置保存成功');
} catch (error) {
ElMessage.error('保存失败,请重试');
} finally {
loading.value = false;
}
};
//
const resetSettings = () => {
Object.assign(settings, {
email: {
system: true,
security: true,
content: false
},
inSite: {
comments: true,
likes: true,
follows: true
},
push: {
browser: false,
mobile: false
}
});
};
//
const initSettings = () => {
//
const savedSettings = localStorage.getItem('notification_settings');
if (savedSettings) {
try {
const parsed = JSON.parse(savedSettings);
Object.assign(settings, parsed);
} catch (error) {
console.error('加载通知设置失败:', error);
}
}
};
onMounted(() => {
initSettings();
});
</script>
<style lang="less" scoped>
.notification-settings {
max-width: 800px;
.setting-group {
margin-bottom: 32px;
&:last-child {
margin-bottom: 24px;
}
.group-title {
font-size: 18px;
color: #333;
margin: 0 0 16px 0;
padding-bottom: 8px;
border-bottom: 2px solid #1677ff;
}
.setting-items {
.setting-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.setting-info {
flex: 1;
.setting-name {
font-size: 16px;
color: #333;
margin-bottom: 4px;
font-weight: 500;
}
.setting-desc {
font-size: 14px;
color: #666;
line-height: 1.4;
}
}
}
}
}
.setting-actions {
text-align: center;
padding-top: 24px;
border-top: 1px solid #f0f0f0;
}
}
</style>

View File

@ -0,0 +1,685 @@
<template>
<div class="content-section">
<div class="content-header">
<h2 class="content-title">资源管理</h2>
<p class="content-desc">管理您的应用资源</p>
</div>
<div class="content-body">
<!-- 操作栏 -->
<div class="action-bar">
<el-button type="primary" @click="showPublishDialog = true">
<i class="fa-solid fa-plus"></i>
发布资源
</el-button>
<div class="filters">
<el-input
v-model="searchText"
placeholder="搜索资源..."
clearable
@input="handleSearch"
style="width: 300px; margin-left: 12px;"
>
<template #prefix>
<i class="fa-solid fa-search"></i>
</template>
</el-input>
</div>
</div>
<!-- 资源列表 -->
<div class="resource-list">
<div v-if="filteredResources.length === 0" class="empty-state">
<i class="fa-solid fa-cubes"></i>
<p>{{ searchText || filterType ? '未找到相关资源' : '暂无资源' }}</p>
</div>
<div v-else class="resource-grid">
<div
v-for="resource in paginatedResources"
:key="resource.id"
class="resource-card"
>
<div class="resource-content-area">
<div class="resource-header">
<el-tag :type="getTypeColor(resource.type)" size="small">
{{ getTypeText(resource.type) }}
</el-tag>
<h3 class="resource-title">
{{ resource.name }}
</h3>
</div>
<div class="resource-meta">
<span class="resource-category">{{ resource.category.join(', ') }}</span>
<span class="resource-downloads">{{ resource.downloads }} 下载</span>
<span class="resource-date">{{ resource.createTime }}</span>
</div>
<div class="resource-description">
{{ resource.description }}
</div>
</div>
<div class="resource-actions">
<el-button size="small" @click="editResource(resource)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteResource(resource)">删除</el-button>
</div>
</div>
</div>
<!-- 分页组件 -->
<div v-if="filteredResources.length > 0" class="pagination-wrapper">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[5, 10, 20, 50]"
:total="totalResources"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</div>
</div>
</div>
<!-- 发布/编辑资源弹窗 -->
<el-dialog
v-model="showPublishDialog"
:title="isEditMode ? '编辑资源' : '发布资源'"
width="800px"
:close-on-click-modal="false"
>
<el-form :model="resourceForm" label-width="80px" :rules="formRules" ref="formRef">
<el-form-item label="资源名称" prop="name" required>
<el-input
v-model="resourceForm.name"
placeholder="请输入资源名称"
maxlength="50"
show-word-limit
/>
</el-form-item>
<el-form-item label="资源类型" prop="type" required>
<el-select v-model="resourceForm.type" placeholder="请选择资源类型">
<el-option
v-for="type in resourceTypes"
:key="type.value"
:label="type.label"
:value="type.value"
/>
</el-select>
</el-form-item>
<el-form-item label="分类" prop="category" required>
<el-checkbox-group v-model="resourceForm.category">
<el-checkbox
v-for="platform in platforms"
:key="platform.value"
:label="platform.value"
>
{{ platform.label }}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="版本号">
<el-input
v-model="resourceForm.version"
placeholder="例如1.0.0"
/>
</el-form-item>
<el-form-item label="下载链接" prop="downloadUrl" required>
<el-input
v-model="resourceForm.downloadUrl"
placeholder="请输入下载链接"
/>
</el-form-item>
<el-form-item label="资源描述" prop="description" required>
<el-input
v-model="resourceForm.description"
type="textarea"
:rows="4"
placeholder="请输入资源描述..."
maxlength="500"
show-word-limit
/>
</el-form-item>
<el-form-item label="截图上传">
<el-upload
ref="uploadRef"
:file-list="fileList"
:on-change="handleFileChange"
:on-remove="handleFileRemove"
action=""
:auto-upload="false"
multiple
accept="image/*"
list-type="picture-card"
>
<el-icon><Plus /></el-icon>
<div class="upload-text">上传截图</div>
</el-upload>
<div class="upload-tips">
<p>支持 JPGPNGGIF 格式单张图片不超过 2MB最多上传 5 </p>
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showPublishDialog = false">取消</el-button>
<el-button type="primary" @click="submitResource" :loading="loading">
{{ isEditMode ? '保存修改' : '发布资源' }}
</el-button>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Plus } from "@element-plus/icons-vue";
interface ResourceForm {
name: string;
type: string;
category: string[];
version: string;
downloadUrl: string;
description: string;
}
interface Resource {
id: string;
name: string;
type: string;
category: string[];
version: string;
downloadUrl: string;
description: string;
createTime: string;
downloads: number;
}
interface ResourceType {
value: string;
label: string;
}
interface Platform {
value: string;
label: string;
}
//
const showPublishDialog = ref(false);
const isEditMode = ref(false);
const searchText = ref('');
const filterType = ref('');
const loading = ref(false);
const formRef = ref();
const fileList = ref([]);
//
const currentPage = ref(1);
const pageSize = ref(10);
const totalResources = computed(() => filteredResources.value.length);
//
const resourceForm = ref<ResourceForm>({
name: '',
type: '',
category: [],
version: '',
downloadUrl: '',
description: ''
});
//
const formRules = {
name: [
{ required: true, message: '请输入资源名称', trigger: 'blur' },
{ min: 1, max: 50, message: '名称长度在 1 到 50 个字符', trigger: 'blur' }
],
type: [
{ required: true, message: '请选择资源类型', trigger: 'change' }
],
category: [
{ type: 'array', required: true, message: '请至少选择一个分类', trigger: 'change' }
],
downloadUrl: [
{ required: true, message: '请输入下载链接', trigger: 'blur' },
{ type: 'url', message: '请输入正确的URL格式', trigger: 'blur' }
],
description: [
{ required: true, message: '请输入资源描述', trigger: 'blur' },
{ min: 10, message: '描述不能少于10个字符', trigger: 'blur' }
]
};
//
const resources = ref<Resource[]>([
{
id: '1',
name: 'Vue DevTools',
type: 'plugin',
category: ['游戏下载'],
version: '6.5.0',
downloadUrl: 'https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd',
description: 'Vue.js 官方开发者工具,用于调试 Vue 应用。',
createTime: '2024-01-15',
downloads: 1250
},
{
id: '2',
name: 'GoLand IDE',
type: 'software',
category: ['办公下载'],
version: '2023.3',
downloadUrl: 'https://www.jetbrains.com/go/download/',
description: 'JetBrains 出品的 Go 语言集成开发环境。',
createTime: '2024-01-10',
downloads: 890
}
]);
//
const resourceTypes = ref<ResourceType[]>([
{ value: 'software', label: '软件工具' },
{ value: 'plugin', label: '插件扩展' },
{ value: 'library', label: '代码库' },
{ value: 'template', label: '模板素材' },
{ value: 'other', label: '其他' }
]);
//
const platforms = ref<Platform[]>([
{ value: 'windows', label: 'Windows' },
{ value: 'macos', label: 'macOS' },
{ value: 'linux', label: 'Linux' },
{ value: 'web', label: 'Web' },
{ value: 'mobile', label: '移动端' }
]);
//
const filteredResources = computed(() => {
let filtered = resources.value;
//
if (filterType.value) {
filtered = filtered.filter(resource => resource.type === filterType.value);
}
//
if (searchText.value) {
filtered = filtered.filter(resource =>
resource.name.toLowerCase().includes(searchText.value.toLowerCase()) ||
resource.description.toLowerCase().includes(searchText.value.toLowerCase())
);
}
return filtered;
});
//
const paginatedResources = computed(() => {
const start = (currentPage.value - 1) * pageSize.value;
const end = start + pageSize.value;
return filteredResources.value.slice(start, end);
});
//
const handlePageChange = (page: number) => {
currentPage.value = page;
};
const handleSizeChange = (size: number) => {
pageSize.value = size;
currentPage.value = 1; //
};
//
const getTypeText = (type: string) => {
const typeObj = resourceTypes.value.find(t => t.value === type);
return typeObj ? typeObj.label : type;
};
//
const getTypeColor = (type: string) => {
switch (type) {
case 'software': return 'success';
case 'plugin': return 'primary';
case 'library': return 'warning';
case 'template': return 'info';
default: return '';
}
};
//
const getPlatformText = (platform: string) => {
const platformObj = platforms.value.find(p => p.value === platform);
return platformObj ? platformObj.label : platform;
};
//
const handleSearch = () => {
//
};
//
const handleFilter = () => {
//
};
//
const handleFileChange = (file: any, fileList: any[]) => {
//
if (file.size > 2 * 1024 * 1024) {
ElMessage.error('图片大小不能超过 2MB');
return false;
}
//
if (fileList.length > 5) {
ElMessage.error('最多只能上传 5 张图片');
return false;
}
};
//
const handleFileRemove = (file: any, fileList: any[]) => {
//
};
//
const editResource = (resource: Resource) => {
isEditMode.value = true;
resourceForm.value = {
name: resource.name,
type: resource.type,
category: [...resource.category],
version: resource.version,
downloadUrl: resource.downloadUrl,
description: resource.description
};
showPublishDialog.value = true;
};
//
const downloadResource = (resource: Resource) => {
window.open(resource.downloadUrl, '_blank');
};
//
const deleteResource = async (resource: Resource) => {
try {
await ElMessageBox.confirm(
`确定要删除资源"${resource.name}"吗?此操作不可恢复。`,
'确认删除',
{
confirmButtonText: '确定删除',
cancelButtonText: '取消',
type: 'warning',
}
);
// API
const index = resources.value.findIndex(r => r.id === resource.id);
if (index > -1) {
resources.value.splice(index, 1);
//
const totalPages = Math.ceil(resources.value.length / pageSize.value);
if (currentPage.value > totalPages && totalPages > 0) {
currentPage.value = totalPages;
}
ElMessage.success('资源删除成功');
}
} catch (error) {
//
}
};
//
const submitResource = async () => {
if (!formRef.value) return;
await formRef.value.validate(async (valid: boolean) => {
if (!valid) return;
loading.value = true;
try {
if (isEditMode.value) {
//
ElMessage.success('资源更新成功');
} else {
//
const newResource: Resource = {
id: Date.now().toString(),
name: resourceForm.value.name,
type: resourceForm.value.type,
category: [...resourceForm.value.category],
version: resourceForm.value.version || '1.0.0',
downloadUrl: resourceForm.value.downloadUrl,
description: resourceForm.value.description,
createTime: new Date().toLocaleDateString(),
downloads: 0
};
resources.value.unshift(newResource);
currentPage.value = 1; //
ElMessage.success('资源发布成功');
}
showPublishDialog.value = false;
resetForm();
} catch (error) {
ElMessage.error('操作失败,请重试');
} finally {
loading.value = false;
}
});
};
//
const resetForm = () => {
resourceForm.value = {
name: '',
type: '',
category: [],
version: '',
downloadUrl: '',
description: ''
};
fileList.value = [];
if (formRef.value) {
formRef.value.clearValidate();
}
};
//
const initResources = () => {
// API
};
onMounted(() => {
initResources();
});
</script>
<style lang="less" scoped>
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
gap: 16px;
.filters {
display: flex;
gap: 12px;
align-items: center;
}
}
.resource-list {
.resource-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: 20px;
}
.resource-card {
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 20px;
transition: all 0.3s ease;
display: flex;
align-items: flex-start;
gap: 16px;
&:hover {
border-color: #1677ff;
box-shadow: 0 4px 12px rgba(22, 119, 255, 0.15);
}
.resource-content-area {
flex: 1;
min-width: 0; //
.resource-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
.resource-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin: 0;
flex: 1;
margin-left: 12px;
}
}
.resource-meta {
display: flex;
gap: 16px;
margin-bottom: 12px;
font-size: 12px;
color: #909399;
.resource-version {
color: #1677ff;
font-weight: 500;
}
}
.resource-platforms {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 12px;
}
.resource-description {
color: #606266;
line-height: 1.6;
margin-bottom: 0;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
line-clamp: 2;
}
}
.resource-actions {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8px;
flex-shrink: 0;
.el-button {
width: 80px;
justify-content: center;
}
}
}
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
i {
font-size: 48px;
margin-bottom: 16px;
display: block;
}
p {
font-size: 14px;
margin: 0;
}
}
.pagination-wrapper {
display: flex;
justify-content: center;
margin-top: 32px;
padding: 20px 0;
.el-pagination {
::v-deep(.el-pager li) {
min-width: 36px;
height: 36px;
line-height: 36px;
border-radius: 6px;
margin: 0 2px;
}
::v-deep(.el-pager li.is-active) {
background-color: #1677ff;
color: white;
}
::v-deep(.btn-prev, .btn-next) {
min-width: 36px;
height: 36px;
border-radius: 6px;
}
}
}
/* 弹窗表单样式 */
.el-dialog {
::v-deep(.el-dialog__body) {
padding: 20px 24px;
}
.el-form {
.el-form-item {
margin-bottom: 20px;
}
.upload-tips {
margin-top: 8px;
p {
margin: 0;
font-size: 12px;
color: #909399;
}
}
.upload-text {
font-size: 12px;
color: #909399;
margin-top: 8px;
}
}
}
</style>

View File

@ -0,0 +1,150 @@
<template>
<div class="content-section">
<div class="content-header">
<h2 class="content-title">安全设置</h2>
<p class="content-desc">保护您的账户安全</p>
</div>
<div class="content-body">
<el-form :model="passwordForm" label-width="120px" class="security-form">
<el-form-item label="当前密码" required>
<el-input
v-model="passwordForm.old_password"
type="password"
placeholder="请输入当前密码"
show-password
/>
</el-form-item>
<el-form-item label="新密码" required>
<el-input
v-model="passwordForm.new_password"
type="password"
placeholder="请输入新密码"
show-password
/>
</el-form-item>
<el-form-item label="确认密码" required>
<el-input
v-model="passwordForm.confirm_password"
type="password"
placeholder="请再次输入新密码"
show-password
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="changePassword" :loading="loading">
修改密码
</el-button>
</el-form-item>
</el-form>
<div class="security-tips">
<h4>安全提示</h4>
<ul>
<li>密码长度至少8位包含字母和数字</li>
<li>定期更换密码以确保账户安全</li>
<li>不要在多个网站使用相同的密码</li>
<li>启用双因子认证可以进一步保护账户</li>
</ul>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { ElMessage } from "element-plus";
interface PasswordForm {
old_password: string;
new_password: string;
confirm_password: string;
}
//
const passwordForm = ref<PasswordForm>({
old_password: '',
new_password: '',
confirm_password: ''
});
const loading = ref(false);
//
const changePassword = async () => {
if (!passwordForm.value.old_password || !passwordForm.value.new_password || !passwordForm.value.confirm_password) {
ElMessage.error('请填写完整信息');
return;
}
if (passwordForm.value.new_password !== passwordForm.value.confirm_password) {
ElMessage.error('两次输入的密码不一致');
return;
}
if (passwordForm.value.new_password.length < 8) {
ElMessage.error('密码长度至少8位');
return;
}
loading.value = true;
try {
// API
await new Promise(resolve => setTimeout(resolve, 1000)); // API
ElMessage.success('密码修改成功');
//
passwordForm.value = {
old_password: '',
new_password: '',
confirm_password: ''
};
} catch (error) {
ElMessage.error('密码修改失败,请重试');
} finally {
loading.value = false;
}
};
</script>
<style lang="less" scoped>
.security-form {
max-width: 600px;
margin-bottom: 32px;
.el-form-item {
margin-bottom: 20px;
}
}
.security-tips {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 20px;
h4 {
color: #333;
margin: 0 0 12px 0;
font-size: 16px;
font-weight: 500;
}
ul {
margin: 0;
padding-left: 20px;
li {
color: #666;
margin-bottom: 8px;
font-size: 14px;
line-height: 1.5;
&:last-child {
margin-bottom: 0;
}
}
}
}
</style>

View File

@ -0,0 +1,156 @@
<template>
<div class="content-section">
<div class="content-header">
<h2 class="content-title">我的钱包</h2>
<p class="content-desc">查看余额和交易记录</p>
</div>
<div class="content-body">
<div class="wallet-info">
<div class="balance-card">
<div class="balance-title">账户余额</div>
<div class="balance-amount">¥ {{ balance }}</div>
</div>
</div>
<div class="transaction-history">
<h3>交易记录</h3>
<el-divider></el-divider>
<div v-if="transactions.length === 0" class="empty-state">
<i class="fa-solid fa-wallet"></i>
<p>暂无交易记录</p>
</div>
<div v-else class="transaction-list">
<div
v-for="transaction in transactions"
:key="transaction.id"
class="transaction-item"
>
<div class="transaction-info">
<div class="transaction-desc">{{ transaction.description }}</div>
<div class="transaction-time">{{ transaction.time }}</div>
</div>
<div class="transaction-amount" :class="{ 'income': transaction.type === 'income', 'expense': transaction.type === 'expense' }">
{{ transaction.type === 'income' ? '+' : '-' }}¥{{ transaction.amount }}
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from "vue";
interface Transaction {
id: string;
description: string;
amount: string;
time: string;
type: 'income' | 'expense';
}
//
const balance = ref('0.00');
const transactions = ref<Transaction[]>([]);
//
const initWalletData = () => {
// API
balance.value = '0.00';
transactions.value = [];
};
onMounted(() => {
initWalletData();
});
</script>
<style lang="less" scoped>
.wallet-info {
margin-bottom: 32px;
.balance-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 24px;
border-radius: 12px;
text-align: center;
.balance-title {
font-size: 14px;
margin-bottom: 8px;
opacity: 0.9;
}
.balance-amount {
font-size: 32px;
font-weight: 600;
}
}
}
.transaction-history {
h3 {
font-size: 18px;
color: #333;
margin-bottom: 16px;
}
}
.transaction-list {
.transaction-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.transaction-info {
.transaction-desc {
font-size: 14px;
color: #333;
margin-bottom: 4px;
}
.transaction-time {
font-size: 12px;
color: #999;
}
}
.transaction-amount {
font-size: 16px;
font-weight: 500;
&.income {
color: #67c23a;
}
&.expense {
color: #f56c6c;
}
}
}
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
i {
font-size: 48px;
margin-bottom: 16px;
display: block;
}
p {
font-size: 14px;
margin: 0;
}
}
</style>