增加个人中心
This commit is contained in:
parent
4e9720de5c
commit
06779d999f
8
frontend/components.d.ts
vendored
8
frontend/components.d.ts
vendored
@ -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']
|
||||
|
||||
@ -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>
|
||||
@ -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"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@ -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("跳转到账号管理");
|
||||
|
||||
569
frontend/src/views/user/profile/articlePublish.vue
Normal file
569
frontend/src/views/user/profile/articlePublish.vue
Normal 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>
|
||||
408
frontend/src/views/user/profile/basicInfo.vue
Normal file
408
frontend/src/views/user/profile/basicInfo.vue
Normal 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>支持 JPG、PNG、GIF 格式,大小不超过 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>
|
||||
405
frontend/src/views/user/profile/index.vue
Normal file
405
frontend/src/views/user/profile/index.vue
Normal 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>
|
||||
185
frontend/src/views/user/profile/messages.vue
Normal file
185
frontend/src/views/user/profile/messages.vue
Normal 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>
|
||||
257
frontend/src/views/user/profile/notifications.vue
Normal file
257
frontend/src/views/user/profile/notifications.vue
Normal 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>
|
||||
685
frontend/src/views/user/profile/resourcePublish.vue
Normal file
685
frontend/src/views/user/profile/resourcePublish.vue
Normal 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>支持 JPG、PNG、GIF 格式,单张图片不超过 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>
|
||||
150
frontend/src/views/user/profile/security.vue
Normal file
150
frontend/src/views/user/profile/security.vue
Normal 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>
|
||||
156
frontend/src/views/user/profile/wallet.vue
Normal file
156
frontend/src/views/user/profile/wallet.vue
Normal 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>
|
||||
Loading…
Reference in New Issue
Block a user