web-权限、文章

This commit is contained in:
2025-10-18 18:19:19 +08:00
parent b3424e554f
commit ccc1d6338b
35 changed files with 3314 additions and 463 deletions

View 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
**解决**:使用服务器存储或定期导出备份

View 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>