历史说明
本文主要记录 2025 年阶段的用户手册设计方案与接口草案。
后续在 2026 年迭代中,手册系统主线实现调整为“文件型内容管理”(docs/manual),并采用后台管理接口 /api/admin/manual*。
因此,本文中的部分数据模型与接口示例应理解为阶段性方案,用于保留技术演进背景。
用户手册系统概述
用户手册系统为平台用户提供了完整的使用指南和帮助文档。系统支持在线编辑、章节管理、导航等功能。
系统目标
- 提供完整的用户指南
- 支持在线编辑功能
- 实现灵活的章节管理
- 提供良好的导航体验
手册内容管理
数据模型
// Manual 用户手册模型
type Manual struct {
ID uint `gorm:"primaryKey"`
Title string `gorm:"index"`
Description string `gorm:"type:text"`
Content string `gorm:"type:longtext"`
Version int // 版本号
IsPublished bool // 是否已发布
PublishedAt *time.Time
CreatedBy uint // 创建者
UpdatedBy uint // 更新者
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
// ManualChapter 手册章节模型
type ManualChapter struct {
ID uint `gorm:"primaryKey"`
ManualID uint `gorm:"index"`
Title string
Content string `gorm:"type:longtext"`
Order int // 章节顺序
Level int // 层级(1-3)
ParentID *uint // 父章节ID
IsVisible bool // 是否可见
CreatedAt time.Time
UpdatedAt time.Time
// 关联
Manual *Manual
Children []ManualChapter `gorm:"foreignKey:ParentID"`
}
// ManualVersion 手册版本历史
type ManualVersion struct {
ID uint
ManualID uint
VersionNumber int
Content string `gorm:"type:longtext"`
ChangeSummary string
CreatedBy uint
CreatedAt time.Time
}
章节导航设计
章节树结构
// GetManualChapters 获取手册章节树
func (s *ManualService) GetManualChapters(ctx context.Context, manualID uint) ([]ManualChapter, error) {
var chapters []ManualChapter
// 获取所有章节
if err := s.db.Where("manual_id = ? AND is_visible = ?", manualID, true).
Order("order ASC").
Find(&chapters).Error; err != nil {
return nil, err
}
// 构建树结构
return s.buildChapterTree(chapters), nil
}
// buildChapterTree 构建章节树
func (s *ManualService) buildChapterTree(chapters []ManualChapter) []ManualChapter {
// 创建 map 用于快速查找
chapterMap := make(map[uint]*ManualChapter)
for i := range chapters {
chapterMap[chapters[i].ID] = &chapters[i]
}
// 构建树结构
var roots []ManualChapter
for _, chapter := range chapters {
if chapter.ParentID == nil {
roots = append(roots, chapter)
} else if parent, ok := chapterMap[*chapter.ParentID]; ok {
parent.Children = append(parent.Children, chapter)
}
}
return roots
}
// GetChapterByID 获取章节详情
func (s *ManualService) GetChapterByID(ctx context.Context, chapterID uint) (*ManualChapter, error) {
chapter := &ManualChapter{}
if err := s.db.Preload("Children").
First(chapter, chapterID).Error; err != nil {
return nil, err
}
return chapter, nil
}
导航组件
<template>
<div class="manual-navigation">
<div class="nav-tree">
<div v-for="chapter in chapters" :key="chapter.id" class="nav-item">
<div class="nav-title" @click="selectChapter(chapter)">
<i v-if="chapter.children.length > 0"
:class="{ 'expanded': expandedIds.includes(chapter.id) }"
class="icon-arrow"></i>
</div>
<div v-if="expandedIds.includes(chapter.id)" class="nav-children">
<div v-for="child in chapter.children" :key="child.id" class="nav-item">
<div class="nav-title" @click="selectChapter(child)">
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
chapters: Array
},
data() {
return {
expandedIds: []
}
},
methods: {
selectChapter(chapter) {
this.$emit('chapter-select', chapter)
},
toggleExpand(chapterId) {
const index = this.expandedIds.indexOf(chapterId)
if (index > -1) {
this.expandedIds.splice(index, 1)
} else {
this.expandedIds.push(chapterId)
}
}
}
}
</script>
<style scoped>
.manual-navigation {
width: 250px;
border-right: 1px solid #eee;
padding: 20px;
}
.nav-item {
margin-bottom: 10px;
}
.nav-title {
cursor: pointer;
padding: 8px;
border-radius: 4px;
transition: background-color 0.3s;
}
.nav-title:hover {
background-color: #f5f5f5;
}
.nav-children {
margin-left: 20px;
margin-top: 5px;
}
.icon-arrow {
display: inline-block;
transition: transform 0.3s;
}
.icon-arrow.expanded {
transform: rotate(90deg);
}
</style>
在线编辑功能
编辑器实现
<template>
<div class="manual-editor">
<div class="editor-toolbar">
<el-button @click="saveChapter">保存</el-button>
<el-button @click="previewChapter">预览</el-button>
<el-button @click="addSubChapter">添加子章节</el-button>
<el-button type="danger" @click="deleteChapter">删除</el-button>
</div>
<div class="editor-content">
<el-input v-model="chapter.title" placeholder="章节标题"></el-input>
<el-input
v-model="chapter.content"
type="textarea"
:rows="20"
placeholder="章节内容"
></el-input>
</div>
</div>
</template>
<script>
export default {
props: {
chapter: Object
},
methods: {
async saveChapter() {
try {
await this.$api.updateManualChapter(this.chapter.id, {
title: this.chapter.title,
content: this.chapter.content
})
this.$message.success('保存成功')
this.$emit('chapter-updated', this.chapter)
} catch (error) {
this.$message.error('保存失败')
}
},
previewChapter() {
this.$emit('preview', this.chapter)
},
async addSubChapter() {
const title = await this.$prompt('请输入子章节标题')
try {
const newChapter = await this.$api.createManualChapter({
manual_id: this.chapter.manual_id,
parent_id: this.chapter.id,
title: title
})
this.$message.success('子章节已创建')
this.$emit('chapter-created', newChapter)
} catch (error) {
this.$message.error('创建失败')
}
},
async deleteChapter() {
const confirm = await this.$confirm('确定删除此章节吗?')
if (confirm) {
try {
await this.$api.deleteManualChapter(this.chapter.id)
this.$message.success('删除成功')
this.$emit('chapter-deleted', this.chapter.id)
} catch (error) {
this.$message.error('删除失败')
}
}
}
}
}
</script>
<style scoped>
.manual-editor {
flex: 1;
padding: 20px;
}
.editor-toolbar {
margin-bottom: 20px;
}
.editor-content {
display: flex;
flex-direction: column;
gap: 10px;
}
</style>
内容组织结构
手册发布流程
// PublishManual 发布手册
func (s *ManualService) PublishManual(ctx context.Context, manualID uint) error {
// 1. 验证手册内容完整性
manual := &Manual{}
if err := s.db.First(manual, manualID).Error; err != nil {
return err
}
if manual.Title == "" || manual.Content == "" {
return errors.New("手册内容不完整")
}
// 2. 验证所有章节
var chapters []ManualChapter
if err := s.db.Where("manual_id = ?", manualID).Find(&chapters).Error; err != nil {
return err
}
for _, chapter := range chapters {
if chapter.Title == "" || chapter.Content == "" {
return fmt.Errorf("章节 %s 内容不完整", chapter.Title)
}
}
// 3. 创建版本记录
version := &ManualVersion{
ManualID: manualID,
VersionNumber: manual.Version + 1,
Content: manual.Content,
ChangeSummary: "发布版本",
CreatedBy: getUserID(ctx),
}
if err := s.db.Create(version).Error; err != nil {
return err
}
// 4. 更新手册状态
now := time.Now()
return s.db.Model(manual).Updates(map[string]interface{}{
"is_published": true,
"published_at": now,
"version": manual.Version + 1,
}).Error
}
// GetManualVersions 获取手册版本历史
func (s *ManualService) GetManualVersions(ctx context.Context, manualID uint) ([]ManualVersion, error) {
var versions []ManualVersion
if err := s.db.Where("manual_id = ?", manualID).
Order("version_number DESC").
Find(&versions).Error; err != nil {
return nil, err
}
return versions, nil
}
// RestoreManualVersion 恢复手册版本
func (s *ManualService) RestoreManualVersion(ctx context.Context, versionID uint) error {
version := &ManualVersion{}
if err := s.db.First(version, versionID).Error; err != nil {
return err
}
manual := &Manual{}
if err := s.db.First(manual, version.ManualID).Error; err != nil {
return err
}
// 更新手册内容
return s.db.Model(manual).Updates(map[string]interface{}{
"content": version.Content,
"version": version.VersionNumber,
}).Error
}
前后端协作
API 接口
说明:以下接口为当期设计稿接口。当前主线实现请参考后续文档《用户手册系统完善(二十一)》及代码中的
/api/admin/manual系列接口。
GET /api/manuals - 获取手册列表
GET /api/manuals/{id} - 获取手册详情
GET /api/manuals/{id}/chapters - 获取手册章节
POST /api/manuals - 创建手册
PUT /api/manuals/{id} - 更新手册
DELETE /api/manuals/{id} - 删除手册
POST /api/manuals/{id}/chapters - 创建章节
PUT /api/manuals/chapters/{id} - 更新章节
DELETE /api/manuals/chapters/{id} - 删除章节
POST /api/manuals/{id}/publish - 发布手册
GET /api/manuals/{id}/versions - 获取版本历史
POST /api/manuals/versions/{id}/restore - 恢复版本
总结
用户手册系统为平台提供了完整的帮助文档功能,支持在线编辑和版本管理,提升了用户体验。
关键特性
- 灵活的章节管理
- 在线编辑功能
- 版本控制
- 树形导航
- 发布流程
这个系统为用户提供了便捷的学习和参考资源。