增加个人中心
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']
|
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
|
||||||
ElButton: typeof import('element-plus/es')['ElButton']
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
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']
|
ElDivider: typeof import('element-plus/es')['ElDivider']
|
||||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||||
@ -27,8 +30,13 @@ declare module 'vue' {
|
|||||||
ElInput: typeof import('element-plus/es')['ElInput']
|
ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
ElOption: typeof import('element-plus/es')['ElOption']
|
ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||||
|
ElRow: typeof import('element-plus/es')['ElRow']
|
||||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
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']
|
ElTag: typeof import('element-plus/es')['ElTag']
|
||||||
|
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||||
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
|
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
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",
|
name: "login",
|
||||||
component: () => import("@/views/login/index.vue"),
|
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) {
|
switch (command) {
|
||||||
case "profile":
|
case "profile":
|
||||||
console.log("跳转到个人中心");
|
console.log("跳转到个人中心");
|
||||||
// TODO: 实现跳转逻辑
|
router.push('/user/profile');
|
||||||
ElMessage.info("跳转到个人中心");
|
|
||||||
break;
|
break;
|
||||||
case "account":
|
case "account":
|
||||||
console.log("跳转到账号管理");
|
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