概述:
本篇是接着上一篇,细分出说明书的编写部分,实现这个功能的需求,是内部很多同事反馈,需要有个地方存工具,并且可以写说明书,如果需要的人,那么可以在界面上直接下载工具和查看工具的说明,这样就不用每次都找人发文档,各种本地找,很浪费时间,故此需要实现这样的一个功能
新建说明书表
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 dbclass 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-inputv-model="form.title"placeholder="请输入标题"maxlength="200"show-word-limitstyle="width: 600px"/></el-form-item><!-- 富文本编辑器 --><el-form-item label="说明书内容" required><TinymceEditorv-model="form.content":height="500":disabled="loading"/></el-form-item><!-- 操作按钮 --><el-form-item><el-buttontype="primary"@click="handleSave":loading="loading"><i class="el-icon-check"></i> 保存</el-button><el-buttontype="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 = truetry {// 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>