web-权限、文章
This commit is contained in:
@@ -46,7 +46,9 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ElButton, ElInput, ElTable, ElTableColumn, ElTag, ElPagination, ElMessage } from 'element-plus';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
const searchKeyword = ref('');
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(10);
|
||||
@@ -61,8 +63,16 @@ function loadArticles() {
|
||||
// TODO: 加载文章数据
|
||||
}
|
||||
|
||||
function showCreateDialog() {
|
||||
// TODO: 显示创建文章对话框
|
||||
function showCreateDialog() {
|
||||
// 尝试跳转
|
||||
router.push('/article/add')
|
||||
.then(() => {
|
||||
console.log('路由跳转成功!');
|
||||
console.log('跳转后路由:', router.currentRoute.value.fullPath);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('路由跳转失败:', err);
|
||||
});
|
||||
}
|
||||
|
||||
function handleDataCollection() {
|
||||
|
||||
422
schoolNewsWeb/src/views/article/ArticleAddView.vue
Normal file
422
schoolNewsWeb/src/views/article/ArticleAddView.vue
Normal file
@@ -0,0 +1,422 @@
|
||||
<template>
|
||||
<div class="article-add-view">
|
||||
<div class="page-header">
|
||||
<el-button @click="handleBack" :icon="ArrowLeft">返回</el-button>
|
||||
<h1 class="page-title">{{ isEdit ? '编辑文章' : '创建文章' }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="article-form">
|
||||
<el-form ref="formRef" :model="articleForm" :rules="rules" label-width="100px" label-position="top">
|
||||
<!-- 标题 -->
|
||||
<el-form-item label="文章标题" prop="title">
|
||||
<el-input v-model="articleForm.title" placeholder="请输入文章标题" maxlength="100" show-word-limit />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 分类和标签 -->
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="文章分类" prop="category">
|
||||
<el-select v-model="articleForm.category" placeholder="请选择分类" style="width: 100%">
|
||||
<el-option label="新闻资讯" value="news" />
|
||||
<el-option label="技术文章" value="tech" />
|
||||
<el-option label="学习资料" value="study" />
|
||||
<el-option label="通知公告" value="notice" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="标签" prop="tags">
|
||||
<el-select v-model="articleForm.tags" multiple placeholder="请选择标签" style="width: 100%">
|
||||
<el-option label="重要" value="important" />
|
||||
<el-option label="推荐" value="recommend" />
|
||||
<el-option label="热门" value="hot" />
|
||||
<el-option label="原创" value="original" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 摘要 -->
|
||||
<el-form-item label="文章摘要" prop="summary">
|
||||
<el-input v-model="articleForm.summary" type="textarea" :rows="3" placeholder="请输入文章摘要(选填)"
|
||||
maxlength="200" show-word-limit />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 封面图 -->
|
||||
<el-form-item label="封面图片">
|
||||
<el-upload class="cover-uploader" :show-file-list="false" :on-success="handleCoverSuccess"
|
||||
:before-upload="beforeCoverUpload" action="#">
|
||||
<img v-if="articleForm.cover" :src="articleForm.cover" class="cover" />
|
||||
<el-icon v-else class="cover-uploader-icon">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
</el-upload>
|
||||
<div class="upload-tip">建议尺寸:800x450px,支持jpg、png格式</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 文章内容 -->
|
||||
<el-form-item label="文章内容" prop="content">
|
||||
<RichTextComponent ref="editorRef" v-model="articleForm.content" height="500px"
|
||||
placeholder="请输入文章内容..." />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 发布设置 -->
|
||||
<el-form-item label="发布设置">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-checkbox v-model="articleForm.allowComment">允许评论</el-checkbox>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-checkbox v-model="articleForm.isTop">置顶文章</el-checkbox>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-checkbox v-model="articleForm.isRecommend">推荐文章</el-checkbox>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handlePublish" :loading="publishing">
|
||||
{{ isEdit ? '保存修改' : '立即发布' }}
|
||||
</el-button>
|
||||
<el-button @click="handleSaveDraft" :loading="savingDraft">
|
||||
保存草稿
|
||||
</el-button>
|
||||
<el-button @click="handlePreview">
|
||||
预览
|
||||
</el-button>
|
||||
<el-button @click="handleBack">
|
||||
取消
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 预览对话框 -->
|
||||
<el-dialog v-model="previewVisible" title="文章预览" width="900px" :close-on-click-modal="false">
|
||||
<div class="article-preview">
|
||||
<h1 class="preview-title">{{ articleForm.title }}</h1>
|
||||
<div class="preview-meta">
|
||||
<span>分类:{{ getCategoryLabel(articleForm.category) }}</span>
|
||||
<span v-if="articleForm.tags.length">
|
||||
标签:{{ articleForm.tags.join(', ') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="preview-summary" v-if="articleForm.summary">
|
||||
{{ articleForm.summary }}
|
||||
</div>
|
||||
<img v-if="articleForm.cover" :src="articleForm.cover" class="preview-cover" />
|
||||
<div class="preview-content ql-editor" v-html="articleForm.content"></div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElSelect,
|
||||
ElOption,
|
||||
ElButton,
|
||||
ElRow,
|
||||
ElCol,
|
||||
ElCheckbox,
|
||||
ElUpload,
|
||||
ElIcon,
|
||||
ElMessage,
|
||||
ElDialog
|
||||
} from 'element-plus';
|
||||
import { ArrowLeft, Plus } from '@element-plus/icons-vue';
|
||||
import { RichTextComponent } from '@/components/text';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const formRef = ref();
|
||||
const editorRef = ref();
|
||||
const publishing = ref(false);
|
||||
const savingDraft = ref(false);
|
||||
const previewVisible = ref(false);
|
||||
|
||||
// 是否编辑模式
|
||||
const isEdit = ref(false);
|
||||
|
||||
// 表单数据
|
||||
const articleForm = reactive({
|
||||
title: '',
|
||||
category: '',
|
||||
tags: [] as string[],
|
||||
summary: '',
|
||||
cover: '',
|
||||
content: '',
|
||||
allowComment: true,
|
||||
isTop: false,
|
||||
isRecommend: false
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
title: [
|
||||
{ required: true, message: '请输入文章标题', trigger: 'blur' },
|
||||
{ min: 5, max: 100, message: '标题长度在 5 到 100 个字符', trigger: 'blur' }
|
||||
],
|
||||
category: [
|
||||
{ required: true, message: '请选择文章分类', trigger: 'change' }
|
||||
],
|
||||
content: [
|
||||
{ required: true, message: '请输入文章内容', trigger: 'blur' }
|
||||
]
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 检查是否是编辑模式
|
||||
const id = route.query.id;
|
||||
if (id) {
|
||||
isEdit.value = true;
|
||||
loadArticle(id as string);
|
||||
}
|
||||
});
|
||||
|
||||
// 加载文章数据(编辑模式)
|
||||
function loadArticle(id: string) {
|
||||
// TODO: 调用API加载文章数据
|
||||
console.log('加载文章:', id);
|
||||
}
|
||||
|
||||
// 返回
|
||||
function handleBack() {
|
||||
router.back();
|
||||
}
|
||||
|
||||
// 发布文章
|
||||
async function handlePublish() {
|
||||
try {
|
||||
await formRef.value?.validate();
|
||||
|
||||
publishing.value = true;
|
||||
|
||||
// TODO: 调用API发布文章
|
||||
console.log('发布文章:', articleForm);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
ElMessage.success(isEdit.value ? '修改成功' : '发布成功');
|
||||
router.push('/admin/manage/resource/articles');
|
||||
} catch (error) {
|
||||
console.error('发布失败:', error);
|
||||
} finally {
|
||||
publishing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存草稿
|
||||
async function handleSaveDraft() {
|
||||
savingDraft.value = true;
|
||||
|
||||
try {
|
||||
// TODO: 调用API保存草稿
|
||||
console.log('保存草稿:', articleForm);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
ElMessage.success('草稿已保存');
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error);
|
||||
ElMessage.error('保存失败');
|
||||
} finally {
|
||||
savingDraft.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 预览
|
||||
function handlePreview() {
|
||||
console.log(articleForm.content);
|
||||
if (!articleForm.title) {
|
||||
ElMessage.warning('请先输入文章标题');
|
||||
return;
|
||||
}
|
||||
previewVisible.value = true;
|
||||
}
|
||||
|
||||
// 封面上传成功
|
||||
function handleCoverSuccess(response: any) {
|
||||
// TODO: 处理上传成功的响应
|
||||
articleForm.cover = response.url;
|
||||
}
|
||||
|
||||
// 上传前验证
|
||||
function beforeCoverUpload(file: File) {
|
||||
const isImage = file.type.startsWith('image/');
|
||||
const isLt2M = file.size / 1024 / 1024 < 2;
|
||||
|
||||
if (!isImage) {
|
||||
ElMessage.error('只能上传图片文件!');
|
||||
}
|
||||
if (!isLt2M) {
|
||||
ElMessage.error('图片大小不能超过 2MB!');
|
||||
}
|
||||
return isImage && isLt2M;
|
||||
}
|
||||
|
||||
// 获取分类标签
|
||||
function getCategoryLabel(value: string): string {
|
||||
const map: Record<string, string> = {
|
||||
news: '新闻资讯',
|
||||
tech: '技术文章',
|
||||
study: '学习资料',
|
||||
notice: '通知公告'
|
||||
};
|
||||
return map[value] || value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.article-add-view {
|
||||
min-height: 100vh;
|
||||
background: #f5f7fa;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.article-form {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 32px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.cover-uploader {
|
||||
:deep(.el-upload) {
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cover-uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
width: 178px;
|
||||
height: 178px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cover {
|
||||
width: 178px;
|
||||
height: 178px;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.upload-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.article-preview {
|
||||
padding: 20px;
|
||||
|
||||
:deep(.ql-code-block-container) {
|
||||
margin: 12px 0; // 上下间距
|
||||
}
|
||||
|
||||
:deep(.ql-code-block) {
|
||||
background: #282c34; // 代码块背景色(类似深色主题)
|
||||
color: #abb2bf; // 代码文字颜色
|
||||
padding: 12px; // 内边距
|
||||
border-radius: 4px; // 圆角
|
||||
overflow-x: auto; // 横向滚动
|
||||
font-family: 'Courier New', monospace; // 等宽字体
|
||||
white-space: pre; // 保留空格和换行
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.preview-meta {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.preview-summary {
|
||||
background: #f5f7fa;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.preview-cover {
|
||||
width: 100%;
|
||||
max-height: 400px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
// ql-editor 类会自动应用 Quill 的默认样式
|
||||
// 这里只添加必要的自定义样式覆盖
|
||||
|
||||
// 图片和视频样式(保留用户设置的尺寸)
|
||||
:deep(img[width]),
|
||||
:deep(video[width]),
|
||||
:deep(img[style*="width"]),
|
||||
:deep(video[style*="width"]) {
|
||||
// 如果有 width 属性或 style 中包含 width,使用用户设置的尺寸
|
||||
max-width: 100%;
|
||||
// 不强制设置 height: auto,保留用户设置的固定尺寸
|
||||
display: block;
|
||||
margin: 12px auto;
|
||||
}
|
||||
|
||||
// 没有 width 属性的图片和视频使用默认样式
|
||||
:deep(img:not([width]):not([style*="width"])),
|
||||
:deep(video:not([width]):not([style*="width"])),
|
||||
:deep(iframe) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 12px auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
189
schoolNewsWeb/src/views/editor/README.md
Normal file
189
schoolNewsWeb/src/views/editor/README.md
Normal file
@@ -0,0 +1,189 @@
|
||||
# 富文本编辑器页面
|
||||
|
||||
完整的富文本编辑器功能页面,包含编辑、预览、导出等功能。
|
||||
|
||||
## 路由配置
|
||||
|
||||
在路由文件中添加以下配置:
|
||||
|
||||
```typescript
|
||||
{
|
||||
path: '/editor',
|
||||
name: 'RichTextEditor',
|
||||
component: () => import('@/views/editor/RichTextEditorView.vue'),
|
||||
meta: {
|
||||
title: '富文本编辑器',
|
||||
requiresAuth: true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 📝 编辑功能
|
||||
- ✅ 完整的富文本编辑器
|
||||
- ✅ 实时字数统计
|
||||
- ✅ 字数限制设置
|
||||
- ✅ 自动保存(每30秒)
|
||||
- ✅ 手动保存到本地存储
|
||||
|
||||
### 👁️ 预览功能
|
||||
- ✅ 实时预览编辑内容
|
||||
- ✅ 复制HTML代码
|
||||
- ✅ 弹窗预览模式
|
||||
|
||||
### 💾 导出功能
|
||||
- ✅ 导出为HTML
|
||||
- ✅ 导出为纯文本
|
||||
- ✅ 导出为Markdown
|
||||
- ✅ 自定义文件名
|
||||
|
||||
### 🎨 界面特性
|
||||
- ✅ 响应式设计
|
||||
- ✅ 美观的卡片布局
|
||||
- ✅ 功能介绍卡片
|
||||
- ✅ 移动端适配
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 1. 基础编辑
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<RichTextEditorView />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import RichTextEditorView from '@/views/editor/RichTextEditorView.vue';
|
||||
</script>
|
||||
```
|
||||
|
||||
### 2. 集成到系统菜单
|
||||
|
||||
在菜单配置中添加:
|
||||
|
||||
```json
|
||||
{
|
||||
"menuID": "editor",
|
||||
"name": "富文本编辑器",
|
||||
"url": "/editor",
|
||||
"icon": "Edit",
|
||||
"type": 1,
|
||||
"orderNum": 10
|
||||
}
|
||||
```
|
||||
|
||||
## 快捷键
|
||||
|
||||
| 快捷键 | 功能 |
|
||||
|--------|------|
|
||||
| Ctrl/Cmd + S | 保存 |
|
||||
| Ctrl/Cmd + B | 加粗 |
|
||||
| Ctrl/Cmd + I | 斜体 |
|
||||
| Ctrl/Cmd + U | 下划线 |
|
||||
| Ctrl/Cmd + Z | 撤销 |
|
||||
| Ctrl/Cmd + Y | 重做 |
|
||||
|
||||
## 本地存储
|
||||
|
||||
页面使用localStorage保存内容:
|
||||
|
||||
- `rich_text_content`: 手动保存的内容
|
||||
- `rich_text_content_auto`: 自动保存的内容
|
||||
- `rich_text_saved_at`: 保存时间戳
|
||||
|
||||
## 自定义配置
|
||||
|
||||
### 修改编辑器高度
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const editorHeight = ref('600px'); // 默认500px
|
||||
</script>
|
||||
```
|
||||
|
||||
### 修改自动保存间隔
|
||||
|
||||
```typescript
|
||||
// 默认30秒
|
||||
autoSaveTimer = window.setInterval(() => {
|
||||
// ...
|
||||
}, 60000); // 改为60秒
|
||||
```
|
||||
|
||||
### 修改字数限制
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const maxLength = ref(10000); // 默认5000
|
||||
</script>
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **依赖安装**:确保已安装 `quill` 依赖
|
||||
2. **图片上传**:默认使用base64,大图片可能影响性能
|
||||
3. **浏览器兼容**:现代浏览器支持,IE需要polyfill
|
||||
4. **数据持久化**:localStorage有存储限制(通常5-10MB)
|
||||
|
||||
## 扩展功能建议
|
||||
|
||||
### 1. 图片上传到服务器
|
||||
|
||||
```typescript
|
||||
// 配置Quill图片上传处理器
|
||||
const quill = new Quill(editor, {
|
||||
modules: {
|
||||
toolbar: {
|
||||
handlers: {
|
||||
image: imageHandler
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function imageHandler() {
|
||||
const input = document.createElement('input');
|
||||
input.setAttribute('type', 'file');
|
||||
input.setAttribute('accept', 'image/*');
|
||||
input.click();
|
||||
|
||||
input.onchange = async () => {
|
||||
const file = input.files[0];
|
||||
// 上传到服务器
|
||||
const url = await uploadImage(file);
|
||||
const range = quill.getSelection();
|
||||
quill.insertEmbed(range.index, 'image', url);
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 协同编辑
|
||||
|
||||
可以集成WebSocket实现多人协同编辑功能。
|
||||
|
||||
### 3. 版本历史
|
||||
|
||||
保存多个版本的内容,支持版本回退。
|
||||
|
||||
### 4. 模板功能
|
||||
|
||||
预设多种文档模板,快速开始编辑。
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 问题1:编辑器无法显示
|
||||
|
||||
**原因**:Quill依赖未安装
|
||||
**解决**:运行 `npm install quill`
|
||||
|
||||
### 问题2:样式异常
|
||||
|
||||
**原因**:Quill CSS未加载
|
||||
**解决**:确保组件中导入了 `quill/dist/quill.snow.css`
|
||||
|
||||
### 问题3:内容丢失
|
||||
|
||||
**原因**:浏览器清除了localStorage
|
||||
**解决**:使用服务器存储或定期导出备份
|
||||
|
||||
511
schoolNewsWeb/src/views/editor/RichTextEditorView.vue
Normal file
511
schoolNewsWeb/src/views/editor/RichTextEditorView.vue
Normal file
@@ -0,0 +1,511 @@
|
||||
<template>
|
||||
<div class="rich-text-editor-view">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">富文本编辑器</h1>
|
||||
<p class="page-description">强大的在线富文本编辑工具,支持多种格式和样式</p>
|
||||
</div>
|
||||
|
||||
<div class="editor-container">
|
||||
<!-- 编辑器工具栏 -->
|
||||
<div class="editor-actions">
|
||||
<div class="action-group">
|
||||
<el-button type="primary" @click="handleSave">
|
||||
<el-icon><Document /></el-icon>
|
||||
保存
|
||||
</el-button>
|
||||
<el-button @click="handlePreview">
|
||||
<el-icon><View /></el-icon>
|
||||
预览
|
||||
</el-button>
|
||||
<el-button @click="handleExport">
|
||||
<el-icon><Download /></el-icon>
|
||||
导出
|
||||
</el-button>
|
||||
<el-button @click="handleClear">
|
||||
<el-icon><Delete /></el-icon>
|
||||
清空
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="action-group">
|
||||
<el-switch
|
||||
v-model="showWordCount"
|
||||
active-text="显示字数"
|
||||
style="margin-right: 16px;"
|
||||
/>
|
||||
<el-input-number
|
||||
v-model="maxLength"
|
||||
:min="0"
|
||||
:max="10000"
|
||||
:step="100"
|
||||
placeholder="字数限制"
|
||||
style="width: 150px;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 富文本编辑器 -->
|
||||
<div class="editor-wrapper">
|
||||
<RichTextComponent
|
||||
ref="editorRef"
|
||||
v-model="content"
|
||||
:height="editorHeight"
|
||||
:max-length="maxLength > 0 ? maxLength : 0"
|
||||
:show-word-count="showWordCount"
|
||||
placeholder="在这里开始编写内容..."
|
||||
@change="handleContentChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 编辑器信息 -->
|
||||
<div class="editor-info">
|
||||
<div class="info-item">
|
||||
<span class="info-label">字符数:</span>
|
||||
<span class="info-value">{{ textLength }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">最后修改:</span>
|
||||
<span class="info-value">{{ lastModified }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 预览对话框 -->
|
||||
<el-dialog
|
||||
v-model="previewVisible"
|
||||
title="内容预览"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="preview-content" v-html="content"></div>
|
||||
<template #footer>
|
||||
<el-button @click="previewVisible = false">关闭</el-button>
|
||||
<el-button type="primary" @click="handleCopyHtml">复制HTML</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 导出对话框 -->
|
||||
<el-dialog
|
||||
v-model="exportVisible"
|
||||
title="导出内容"
|
||||
width="600px"
|
||||
>
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="导出格式">
|
||||
<el-radio-group v-model="exportFormat">
|
||||
<el-radio label="html">HTML</el-radio>
|
||||
<el-radio label="text">纯文本</el-radio>
|
||||
<el-radio label="markdown">Markdown</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="文件名">
|
||||
<el-input v-model="exportFilename" placeholder="请输入文件名">
|
||||
<template #append>.{{ exportFormat }}</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="exportVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmExport">确认导出</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 快捷功能卡片 -->
|
||||
<div class="feature-cards">
|
||||
<div class="feature-card">
|
||||
<div class="card-icon">📝</div>
|
||||
<h3>丰富格式</h3>
|
||||
<p>支持标题、列表、引用、代码块等多种格式</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="card-icon">🎨</div>
|
||||
<h3>样式定制</h3>
|
||||
<p>自定义文字颜色、背景色、对齐方式等样式</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="card-icon">📊</div>
|
||||
<h3>插入媒体</h3>
|
||||
<p>支持插入图片、视频、链接等多媒体内容</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="card-icon">💾</div>
|
||||
<h3>实时保存</h3>
|
||||
<p>自动保存编辑内容,防止意外丢失</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElRadioGroup,
|
||||
ElRadio,
|
||||
ElSwitch,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElIcon
|
||||
} from 'element-plus';
|
||||
import { Document, View, Download, Delete } from '@element-plus/icons-vue';
|
||||
import { RichTextComponent } from '@/components/text';
|
||||
|
||||
// 编辑器引用
|
||||
const editorRef = ref();
|
||||
|
||||
// 编辑器内容
|
||||
const content = ref(`
|
||||
<h2>欢迎使用富文本编辑器</h2>
|
||||
<p>这是一个功能强大的在线富文本编辑器,支持以下功能:</p>
|
||||
<ul>
|
||||
<li><strong>多种文本格式</strong>:支持标题、段落、列表等</li>
|
||||
<li><strong>样式定制</strong>:文字颜色、背景色、对齐方式</li>
|
||||
<li><strong>插入媒体</strong>:图片、视频、链接</li>
|
||||
<li><strong>代码支持</strong>:代码块和行内代码</li>
|
||||
</ul>
|
||||
<p>开始编辑您的内容吧!</p>
|
||||
`);
|
||||
|
||||
// 编辑器设置
|
||||
const editorHeight = ref('500px');
|
||||
const showWordCount = ref(true);
|
||||
const maxLength = ref(5000);
|
||||
|
||||
// 对话框状态
|
||||
const previewVisible = ref(false);
|
||||
const exportVisible = ref(false);
|
||||
|
||||
// 导出设置
|
||||
const exportFormat = ref('html');
|
||||
const exportFilename = ref('document');
|
||||
|
||||
// 自动保存定时器
|
||||
let autoSaveTimer: number | null = null;
|
||||
|
||||
// 计算属性
|
||||
const textLength = computed(() => {
|
||||
return editorRef.value?.getText()?.trim().length || 0;
|
||||
});
|
||||
|
||||
const lastModified = computed(() => {
|
||||
return new Date().toLocaleString('zh-CN');
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 启动自动保存
|
||||
startAutoSave();
|
||||
|
||||
// 尝试恢复上次的内容
|
||||
loadSavedContent();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// 清除自动保存定时器
|
||||
if (autoSaveTimer) {
|
||||
clearInterval(autoSaveTimer);
|
||||
}
|
||||
});
|
||||
|
||||
// 内容变化处理
|
||||
function handleContentChange(value: string) {
|
||||
console.log('内容已更新');
|
||||
}
|
||||
|
||||
// 保存
|
||||
function handleSave() {
|
||||
try {
|
||||
localStorage.setItem('rich_text_content', content.value);
|
||||
localStorage.setItem('rich_text_saved_at', new Date().toISOString());
|
||||
ElMessage.success('保存成功');
|
||||
} catch (error) {
|
||||
ElMessage.error('保存失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 自动保存
|
||||
function startAutoSave() {
|
||||
autoSaveTimer = window.setInterval(() => {
|
||||
if (content.value) {
|
||||
localStorage.setItem('rich_text_content_auto', content.value);
|
||||
}
|
||||
}, 30000); // 每30秒自动保存
|
||||
}
|
||||
|
||||
// 加载保存的内容
|
||||
function loadSavedContent() {
|
||||
const saved = localStorage.getItem('rich_text_content');
|
||||
if (saved) {
|
||||
// 可以选择是否自动加载
|
||||
// content.value = saved;
|
||||
}
|
||||
}
|
||||
|
||||
// 预览
|
||||
function handlePreview() {
|
||||
previewVisible.value = true;
|
||||
}
|
||||
|
||||
// 复制HTML
|
||||
function handleCopyHtml() {
|
||||
navigator.clipboard.writeText(content.value).then(() => {
|
||||
ElMessage.success('HTML已复制到剪贴板');
|
||||
}).catch(() => {
|
||||
ElMessage.error('复制失败');
|
||||
});
|
||||
}
|
||||
|
||||
// 导出
|
||||
function handleExport() {
|
||||
exportVisible.value = true;
|
||||
}
|
||||
|
||||
// 确认导出
|
||||
function confirmExport() {
|
||||
let exportContent = '';
|
||||
let mimeType = 'text/html';
|
||||
|
||||
switch (exportFormat.value) {
|
||||
case 'html':
|
||||
exportContent = content.value;
|
||||
mimeType = 'text/html';
|
||||
break;
|
||||
case 'text':
|
||||
exportContent = editorRef.value?.getText() || '';
|
||||
mimeType = 'text/plain';
|
||||
break;
|
||||
case 'markdown':
|
||||
// 简单的HTML转Markdown(可以使用第三方库如turndown来实现)
|
||||
exportContent = htmlToMarkdown(content.value);
|
||||
mimeType = 'text/markdown';
|
||||
break;
|
||||
}
|
||||
|
||||
// 创建下载链接
|
||||
const blob = new Blob([exportContent], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `${exportFilename.value || 'document'}.${exportFormat.value}`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
ElMessage.success('导出成功');
|
||||
exportVisible.value = false;
|
||||
}
|
||||
|
||||
// 简单的HTML转Markdown(基础版本)
|
||||
function htmlToMarkdown(html: string): string {
|
||||
let markdown = html;
|
||||
|
||||
// 标题
|
||||
markdown = markdown.replace(/<h1>(.*?)<\/h1>/g, '# $1\n\n');
|
||||
markdown = markdown.replace(/<h2>(.*?)<\/h2>/g, '## $1\n\n');
|
||||
markdown = markdown.replace(/<h3>(.*?)<\/h3>/g, '### $1\n\n');
|
||||
|
||||
// 加粗、斜体
|
||||
markdown = markdown.replace(/<strong>(.*?)<\/strong>/g, '**$1**');
|
||||
markdown = markdown.replace(/<em>(.*?)<\/em>/g, '*$1*');
|
||||
|
||||
// 链接
|
||||
markdown = markdown.replace(/<a href="(.*?)">(.*?)<\/a>/g, '[$2]($1)');
|
||||
|
||||
// 列表
|
||||
markdown = markdown.replace(/<li>(.*?)<\/li>/g, '- $1\n');
|
||||
markdown = markdown.replace(/<ul>(.*?)<\/ul>/gs, '$1\n');
|
||||
markdown = markdown.replace(/<ol>(.*?)<\/ol>/gs, '$1\n');
|
||||
|
||||
// 段落
|
||||
markdown = markdown.replace(/<p>(.*?)<\/p>/g, '$1\n\n');
|
||||
|
||||
// 移除其他HTML标签
|
||||
markdown = markdown.replace(/<[^>]+>/g, '');
|
||||
|
||||
return markdown.trim();
|
||||
}
|
||||
|
||||
// 清空
|
||||
function handleClear() {
|
||||
ElMessageBox.confirm(
|
||||
'确定要清空所有内容吗?此操作不可恢复。',
|
||||
'确认清空',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
).then(() => {
|
||||
editorRef.value?.clear();
|
||||
ElMessage.success('已清空');
|
||||
}).catch(() => {
|
||||
// 取消操作
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.rich-text-editor-view {
|
||||
min-height: 100vh;
|
||||
background: #f5f7fa;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.page-description {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.editor-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
|
||||
.action-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.editor-wrapper {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.editor-info {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
|
||||
.info-label {
|
||||
color: #909399;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
padding: 20px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
min-height: 200px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
|
||||
:deep(h1), :deep(h2), :deep(h3) {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
:deep(p) {
|
||||
margin: 8px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
:deep(ul), :deep(ol) {
|
||||
margin: 12px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
:deep(img) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.feature-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 36px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.rich-text-editor-view {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.editor-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 12px;
|
||||
|
||||
.action-group {
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.feature-cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<el-form :model="userForm" label-width="120px" class="info-form">
|
||||
<el-form-item label="头像">
|
||||
<div class="avatar-upload">
|
||||
<img :src="userForm.avatar" alt="头像" class="avatar-preview" />
|
||||
<img :src="userForm.avatar || defaultAvatar" alt="头像" class="avatar-preview" />
|
||||
<el-button size="small" @click="handleAvatarUpload">更换头像</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
@@ -56,6 +56,9 @@
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ElForm, ElFormItem, ElInput, ElButton, ElRadio, ElRadioGroup, ElMessage } from 'element-plus';
|
||||
|
||||
// 默认头像
|
||||
const defaultAvatar = new URL('@/assets/imgs/default-avatar.png', import.meta.url).href;
|
||||
|
||||
const userForm = ref({
|
||||
avatar: '',
|
||||
username: '',
|
||||
@@ -69,8 +72,23 @@ const userForm = ref({
|
||||
|
||||
onMounted(() => {
|
||||
// TODO: 加载用户信息
|
||||
loadUserInfo();
|
||||
});
|
||||
|
||||
function loadUserInfo() {
|
||||
// 模拟数据
|
||||
userForm.value = {
|
||||
avatar: '',
|
||||
username: '平台用户bc7a1b',
|
||||
realName: '张三',
|
||||
gender: 1,
|
||||
phone: '15268425987',
|
||||
email: 'zhangsan@example.com',
|
||||
deptName: '机械学院',
|
||||
bio: ''
|
||||
};
|
||||
}
|
||||
|
||||
function handleAvatarUpload() {
|
||||
// TODO: 上传头像
|
||||
ElMessage.info('上传头像功能开发中');
|
||||
@@ -82,7 +100,8 @@ function handleSave() {
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
// TODO: 重置表单
|
||||
// 重置表单
|
||||
loadUserInfo();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -109,4 +128,3 @@ function handleCancel() {
|
||||
border: 2px solid #e0e0e0;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,55 +1,87 @@
|
||||
<template>
|
||||
<div class="profile-page">
|
||||
<div class="profile-container">
|
||||
<h1 class="page-title">我导中心</h1>
|
||||
|
||||
<el-tabs v-model="activeTab" class="profile-tabs">
|
||||
<el-tab-pane label="个人信息" name="info">
|
||||
<PersonalInfo />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="账号设置" name="settings">
|
||||
<AccountSettings />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<div class="user-center-page">
|
||||
<div class="user-card-wrapper">
|
||||
<UserCard/>
|
||||
</div>
|
||||
<div class="content-wrapper">
|
||||
<div class="sidebar-wrapper">
|
||||
<FloatingSidebar :menus="menus" />
|
||||
</div>
|
||||
<div class="main-content">
|
||||
<router-view/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { ElTabs, ElTabPane } from 'element-plus';
|
||||
import PersonalInfo from './PersonalInfoView.vue';
|
||||
import AccountSettings from './AccountSettingsView.vue';
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { FloatingSidebar } from '@/components/base';
|
||||
import { UserCard } from '@/views/user-center/components';
|
||||
import { getParentChildrenRoutes } from '@/utils/routeUtils';
|
||||
import type { SysMenu } from '@/types/menu';
|
||||
|
||||
const activeTab = ref('info');
|
||||
const route = useRoute();
|
||||
|
||||
// 从当前路由配置中获取子路由,转换为菜单格式
|
||||
const menus = computed(() => {
|
||||
// 使用工具函数获取父路由的子路由
|
||||
const childRoutes = getParentChildrenRoutes(route);
|
||||
|
||||
if (childRoutes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 获取父路由路径(用于拼接相对路径)
|
||||
const parentRoute = route.matched[route.matched.length - 2];
|
||||
|
||||
// 将子路由转换为菜单格式
|
||||
return childRoutes
|
||||
.map((child: any) => ({
|
||||
menuID: child.name as string || child.path,
|
||||
name: child.meta?.title as string,
|
||||
url: child.path.startsWith('/') ? child.path : `${parentRoute.path}/${child.path}`,
|
||||
icon: child.meta?.icon as string,
|
||||
orderNum: child.meta?.orderNum as number || 0,
|
||||
} as SysMenu))
|
||||
.sort((a: any, b: any) => (a.orderNum || 0) - (b.orderNum || 0));
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.profile-page {
|
||||
.user-center-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.profile-container {
|
||||
.user-card-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.sidebar-wrapper {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
background: white;
|
||||
padding: 40px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: #141F38;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.profile-tabs {
|
||||
:deep(.el-tabs__nav-wrap) {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -17,62 +17,35 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
import { FloatingSidebar } from '@/components/base';
|
||||
import UserCard from './components/UserCard.vue';
|
||||
import { MenuType } from '@/types/enums';
|
||||
import { UserCard } from '@/views/user-center/components';
|
||||
import { getParentChildrenRoutes } from '@/utils/routeUtils';
|
||||
import type { SysMenu } from '@/types/menu';
|
||||
|
||||
const route = useRoute();
|
||||
const store = useStore();
|
||||
|
||||
// 从 store 中获取当前用户的菜单列表
|
||||
const allMenus = computed(() => store.state.auth.menus as SysMenu[]);
|
||||
|
||||
// 递归筛选出类型为 SIDEBAR 的菜单项(包含子菜单)
|
||||
const filterSidebarMenus = (menuList: SysMenu[]): SysMenu[] => {
|
||||
if (!menuList || menuList.length === 0) return [];
|
||||
|
||||
return menuList
|
||||
.filter(menu => menu.type === MenuType.SIDEBAR)
|
||||
.map(menu => {
|
||||
// 如果有子菜单,递归处理
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
return {
|
||||
...menu,
|
||||
children: filterSidebarMenus(menu.children)
|
||||
};
|
||||
}
|
||||
return menu;
|
||||
});
|
||||
};
|
||||
|
||||
// 查找当前路由对应的菜单项
|
||||
const findCurrentMenu = (menus: SysMenu[], path: string): SysMenu | null => {
|
||||
for (const menu of menus) {
|
||||
if (menu.url === path) {
|
||||
return menu;
|
||||
}
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
const found = findCurrentMenu(menu.children, path);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 获取当前路由对应的菜单及其子菜单(只显示 SIDEBAR 类型)
|
||||
// 从当前路由配置中获取子路由,转换为菜单格式
|
||||
const menus = computed(() => {
|
||||
const currentPath = route.path;
|
||||
const currentMenu = findCurrentMenu(allMenus.value, currentPath);
|
||||
// 使用工具函数获取父路由的子路由
|
||||
const childRoutes = getParentChildrenRoutes(route);
|
||||
|
||||
if (currentMenu && currentMenu.children) {
|
||||
// 递归筛选出 type === MenuType.SIDEBAR 的子菜单
|
||||
return filterSidebarMenus(currentMenu.children);
|
||||
if (childRoutes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 如果没有找到当前菜单,返回所有 SIDEBAR 类型的菜单
|
||||
return filterSidebarMenus(allMenus.value);
|
||||
// 获取父路由路径(用于拼接相对路径)
|
||||
const parentRoute = route.matched[route.matched.length - 2];
|
||||
|
||||
// 将子路由转换为菜单格式
|
||||
return childRoutes
|
||||
.map((child: any) => ({
|
||||
menuID: child.name as string || child.path,
|
||||
name: child.meta?.title as string,
|
||||
url: child.path.startsWith('/') ? child.path : `${parentRoute.path}/${child.path}`,
|
||||
icon: child.meta?.icon as string,
|
||||
orderNum: child.meta?.orderNum as number || 0,
|
||||
} as SysMenu))
|
||||
.sort((a: any, b: any) => (a.orderNum || 0) - (b.orderNum || 0));
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
<!-- 头像 -->
|
||||
<div class="avatar-wrapper">
|
||||
<img
|
||||
:src="userInfo?.avatar || defaultAvatar"
|
||||
:alt="userInfo?.nickname || userInfo?.username"
|
||||
:src="userInfo?.avatar && userInfo.avatar!='default' ? userInfo.avatar : defaultAvatar"
|
||||
:alt="userInfo?.username"
|
||||
class="avatar"
|
||||
/>
|
||||
</div>
|
||||
@@ -23,21 +23,21 @@
|
||||
<div class="user-details">
|
||||
<!-- 用户名和性别 -->
|
||||
<div class="user-name-row">
|
||||
<span class="username">{{ userInfo?.nickname || userInfo?.username || '未设置昵称' }}</span>
|
||||
<div class="gender-tag" v-if="userInfo?.gender && genderIcon">
|
||||
<img :src="genderIcon" :alt="genderText" class="gender-icon" />
|
||||
<span class="gender-text">{{ genderText }}</span>
|
||||
<span class="username">{{ userInfo?.username || '未设置昵称' }}</span>
|
||||
<div class="gender-tag" v-if="userInfo?.gender">
|
||||
<img :src="userInfo?.gender === 1 ? maleIcon : femaleIcon" :alt="userInfo?.gender === 1 ? '男' : '女'" class="gender-icon" />
|
||||
<span class="gender-text">{{ userInfo?.gender === 1 ? '男' : '女' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 详细信息 -->
|
||||
<div class="info-row">
|
||||
<span class="info-item">所属部门:{{ departmentName || '未分配部门' }}</span>
|
||||
<span class="info-item">联系方式:{{ userInfo?.phone || '未设置' }}</span>
|
||||
<span class="info-item">所属部门:{{ userInfo?.deptName || '未分配部门' }}</span>
|
||||
<span class="info-item" v-if="userInfo?.phone">手机号:{{ userInfo?.phone || '未设置' }}</span>
|
||||
<span class="info-item" v-if="userInfo?.email">邮箱:{{ userInfo?.email || '未设置' }}</span>
|
||||
<div class="level-item">
|
||||
<span class="info-label">学习等级:</span>
|
||||
<img :src="arrowDownIcon" alt="等级" class="level-icon" />
|
||||
<span class="level-text">等级</span>
|
||||
<img :src="levelIcon" alt="等级" class="level-icon" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -49,58 +49,45 @@
|
||||
|
||||
<script setup lang="ts" name="UserCard">
|
||||
import { computed, ref, onMounted } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { UserVO } from '@/types';
|
||||
import {userApi} from '@/apis/system/user'
|
||||
import {userProfileApi} from '@/apis/usercenter/profile'
|
||||
import defaultAvatarImg from '@/assets/imgs/default-avatar.png';
|
||||
import maleIcon from '@/assets/imgs/male.svg';
|
||||
import femaleIcon from '@/assets/imgs/female.svg';
|
||||
import arrowDownIcon from '@/assets/imgs/arrow-down.svg';
|
||||
import V1Icon from '@/assets/imgs/v1.svg';
|
||||
import V2Icon from '@/assets/imgs/v2.svg';
|
||||
import V3Icon from '@/assets/imgs/v3.svg';
|
||||
import V4Icon from '@/assets/imgs/v4.svg';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const store = useStore();
|
||||
const router = useRouter();
|
||||
const userInfo = ref<UserVO>();
|
||||
|
||||
// 默认头像
|
||||
const defaultAvatar = defaultAvatarImg;
|
||||
|
||||
// 从 store 获取用户信息
|
||||
const loginDomain = computed(() => store.state.auth.loginDomain);
|
||||
const userInfo = ref<UserVO>();
|
||||
|
||||
// 获取部门名称
|
||||
const departmentName = computed(() => {
|
||||
const roles = loginDomain.value?.roles || [];
|
||||
if (roles.length > 0 && roles[0].dept) {
|
||||
return roles[0].dept.name;
|
||||
const levelIcon = computed(() => {
|
||||
switch(userInfo.value?.level){
|
||||
case 1: return V1Icon;
|
||||
case 2: return V2Icon;
|
||||
case 3: return V3Icon;
|
||||
case 4: return V4Icon;
|
||||
}
|
||||
return '';
|
||||
return V1Icon;
|
||||
});
|
||||
|
||||
// 性别文本
|
||||
const genderText = computed(() => {
|
||||
const gender = userInfo.value?.gender;
|
||||
if (gender === 1) return '男';
|
||||
if (gender === 2) return '女';
|
||||
return '';
|
||||
});
|
||||
|
||||
// 性别图标
|
||||
const genderIcon = computed(() => {
|
||||
const gender = userInfo.value?.gender;
|
||||
if (gender === 1) return maleIcon;
|
||||
if (gender === 2) return femaleIcon;
|
||||
return '';
|
||||
});
|
||||
|
||||
// 编辑资料
|
||||
const handleEdit = () => {
|
||||
router.push('/profile/personal-info');
|
||||
};
|
||||
function handleEdit() {
|
||||
router.push('/profile');
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const res = await userApi.getCurrentUser();
|
||||
userInfo.value = res.data;
|
||||
const res = await userProfileApi.getUserProfile();
|
||||
if(res.success){
|
||||
userInfo.value = res.data;
|
||||
}else{
|
||||
ElMessage.error(res.message || '获取用户信息失败');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
1
schoolNewsWeb/src/views/user-center/components/index.ts
Normal file
1
schoolNewsWeb/src/views/user-center/components/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as UserCard } from './UserCard.vue';
|
||||
Reference in New Issue
Block a user