概述:
本篇是接着上一篇,细分出说明书的编写部分,实现这个功能的需求,是内部很多同事反馈,需要有个地方存工具,并且可以写说明书,如果需要的人,那么可以在界面上直接下载工具和查看工具的说明,这样就不用每次都找人发文档,各种本地找,很浪费时间,故此需要实现这样的一个功能
新建说明书表
CREATE TABLE IF NOT EXISTS `manual` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tool_id` int(11) NOT NULL COMMENT '关联的工具ID',
`version` varchar(20) NOT NULL DEFAULT '1.0' COMMENT '版本号',
`title` varchar(255) NOT NULL COMMENT '说明书标题',
`content` text COMMENT '富文本内容',
`file_path` varchar(255) DEFAULT NULL COMMENT '附件存储路径',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_tool_version` (`tool_id`, `version`) COMMENT '工具ID和版本号的唯一索引',
KEY `idx_tool_id` (`tool_id`) COMMENT '工具ID索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工具说明书表';
验证数据表
-- 给 manual 表添加缺失的 version 和 file_path 字段
ALTER TABLE manual
ADD COLUMN version VARCHAR(20) NOT NULL DEFAULT '1.0' COMMENT '版本号',
ADD COLUMN file_path VARCHAR(255) NULL COMMENT '文件路径';
-- 验证字段是否添加成功
DESCRIBE manual; -- 应显示所有字段:id, tool_id, title, content, version, file_path, created_at
建立数据表模型
# app/models.py(Manual 模型定义)
from datetime import datetime
from extensions import db
class Manual(db.Model):
__tablename__ = 'manual' # 表名必须与数据库一致
id = db.Column(db.Integer, primary_key=True)
tool_id = db.Column(db.Integer, nullable=False, comment='工具ID')
title = db.Column(db.String(255), nullable=False, comment='标题') # 确保表中有 title 字段
content = db.Column(db.Text, comment='富文本内容')
version = db.Column(db.String(20), default='1.0', comment='版本号') # 新增字段
file_path = db.Column(db.String(255), nullable=True, comment='文件路径') # 新增字段
created_at = db.Column(db.DateTime, default=datetime.now, comment='创建时间')
# 确保没有其他多余字段(如 updated_at 若表中不存在需删除)
__table_args__ = (
db.Index('idx_tool_id', 'tool_id'), # 添加索引
)
新增保存说明书的接口和获取说明书的接口
保存说明书的接口开发
@tool_bp.post('/manual/save')
def save_manual():
try:
data = request.get_json()
if not data:
return jsonify({
"code": 40000,
"message": "请求数据不能为空",
"data": None,
"total": 0
})
# 强制校验字段
required = ['tool_id', 'title', 'content']
if not all(k in data for k in required):
return jsonify({
"code": 40000,
"message": f"缺少必填字段: {', '.join(required)}",
"data": None,
"total": 0
})
# 类型检查
try:
tool_id = int(data['tool_id'])
except ValueError:
return jsonify({
"code": 40000,
"message": "tool_id必须为整数",
"data": None,
"total": 0
})
# 数据库操作
manual = Manual.query.filter_by(tool_id=tool_id).first()
if manual:
manual.title = data['title']
manual.content = data['content']
else:
manual = Manual(
tool_id=tool_id,
title=data['title'],
content=data['content']
)
db.session.add(manual)
db.session.commit()
return jsonify({
"code": 20000,
"message": "保存成功",
"data": {"id": manual.id},
"total": 1
})
except Exception as e:
db.session.rollback()
current_app.logger.error(f"保存失败: {str(e)}") # 记录详细错误
return jsonify({
"code": 40000,
"message": f"保存失败: {str(e)}", # 返回具体错误信息
"data": None,
"total": 0
})
验证接口是不是可以保存数据成功,接口成功,至于前端界面的集成编辑器功能,我们在前面的文章中有提到,如何在vue2.x中集成编辑器,可以往上看上一篇文章;
另外这里的说明书我在此基础上增加了一个PDF导出的功能,说明书如果想发送给别人,那么这里可以直接导出,这样就可以在本地看到一个文件,也可以发送给其他人员
目前我们说明书部分可以保存了,接下来需要实现一个接口,从数据库中读取我们的数据展示,这样每次点击查看说明书时,默认展示存储的说明书数据
开发接口
@tool_bp.route('/info/<int:tool_id>', methods=['GET'])
def get_tool_info(tool_id):
"""获取工具基本信息(原接口,包含 toolId、toolName 等)"""
tool = Tool.query.get(tool_id)
return jsonify({
"code": 20000,
"data": {
"toolId": tool.id,
"toolName": tool.name,
"manuals": []
}
})
获取指定工具说明书,点击后自动获取工具关联的说明书数据
@tool_bp.route('/manual/<int:tool_id>', methods=['GET']) # 说明书回显接口(专属)
def get_manual(tool_id):
"""获取指定工具的说明书(仅返回 title 和 content)"""
manual = Manual.query.filter_by(tool_id=tool_id).first()
return jsonify({
"code": 20000,
"data": {
"title": manual.title if manual else "",
"content": manual.content if manual else ""
}
})
验证下效果,点击查看说明书后跳转如下


至此,说明书关联部分开发完成,完整前端代码如下
<template>
<div class="manual-edit-container">
<el-card>
<!-- 标题区域 -->
<div slot="header" class="card-header">
<el-breadcrumb separator="/">
<el-breadcrumb-item>工具管理</el-breadcrumb-item>
<el-breadcrumb-item>编辑说明书</el-breadcrumb-item>
</el-breadcrumb>
</div>
<!-- 表单内容 -->
<el-form ref="form" :model="form" label-width="120px">
<!-- 说明书标题 -->
<el-form-item label="说明书标题" required>
<el-input
v-model="form.title"
placeholder="请输入标题"
maxlength="200"
show-word-limit
style="width: 600px"
/>
</el-form-item>
<!-- 富文本编辑器 -->
<el-form-item label="说明书内容" required>
<TinymceEditor
v-model="form.content"
:height="500"
:disabled="loading"
/>
</el-form-item>
<!-- 操作按钮 -->
<el-form-item>
<el-button
type="primary"
@click="handleSave"
:loading="loading"
>
<i class="el-icon-check"></i> 保存
</el-button>
<el-button
type="success"
@click="handleExportPDF"
:disabled="!form.content.trim()"
>
<i class="el-icon-download"></i> 导出PDF
</el-button>
<el-button @click="handleCancel">取消</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script>
import TinymceEditor from '@/components/TinymceEditor.vue' // 富文本编辑器组件
import axios from 'axios' // HTTP请求库
import html2pdf from 'html2pdf.js' // PDF导出库
export default {
name: 'ManualEdit',
components: { TinymceEditor },
data() {
return {
form: {
tool_id: this.$route.params.id, // 从路由获取工具ID(例如31)
title: '', // 存储数据库中的标题
content: '' // 存储数据库中的富文本内容
},
loading: false // 保存按钮加载状态
}
},
created() {
// 页面加载时立即从数据库获取数据
this.loadManualFromDatabase()
},
methods: {
/**
* 从数据库加载说明书数据(核心方法)
*/
async loadManualFromDatabase() {
// 1. 显示加载提示
this.$message.info('正在加载说明书数据...')
try {
// 2. 调用后端回显接口(已验证返回正确数据)
const response = await axios.get(`http://172.16.60.60:5000/api/tool/manual/${this.form.tool_id}`)
// 3. 验证接口响应格式
if (response.data.code === 20000) {
const manualData = response.data.data || {}
// 4. 赋值到表单(覆盖默认空值)
this.form.title = manualData.title || '未命名说明书'
this.form.content = manualData.content || '<p>请输入说明书内容...</p>'
this.$message.success('加载成功!')
} else {
this.$message.warning('未找到说明书数据')
}
} catch (error) {
// 5. 捕获网络错误
this.$message.error(`加载失败: ${error.message || '网络异常'}`)
}
},
/**
* 保存数据到数据库
*/
async handleSave() {
// 1. 基础校验
if (!this.form.title.trim()) {
this.$message.warning('请输入说明书标题')
return
}
if (!this.form.content.trim()) {
this.$message.warning('请输入说明书内容')
return
}
this.loading = true
try {
// 2. 调用保存接口(确保后端保存接口路径正确)
const response = await axios.post('http://172.16.60.60:5000/api/tool/manual/save', this.form)
// 3. 处理响应
if (response.data.code === 20000) {
this.$message.success('保存成功!')
} else {
this.$message.error(`保存失败: ${response.data.message || '未知错误'}`)
}
} catch (error) {
this.$message.error(`请求失败: ${error.message}`)
} finally {
this.loading = false
}
},
/**
* 导出PDF(保留功能)
*/
handleExportPDF() {
const opt = {
margin: 15,
filename: `${this.form.title || '说明书'}.pdf`,
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, useCORS: true },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
}
// 导出当前编辑器内容
html2pdf().from(document.querySelector('.tox-edit-area__iframe').contentDocument.body).set(opt).save()
},
/**
* 取消编辑返回上一页
*/
handleCancel() {
this.$router.go(-1)
}
}
}
</script>
<style scoped>
.card-header {
background-color: #f5f7fa;
padding: 10px 20px;
}
.manual-edit-container {
padding: 20px;
}
</style>