【Flask】测试平台开发,新增说明书编写和展示功能 第二十三篇

概述:

本篇是接着上一篇,细分出说明书的编写部分,实现这个功能的需求,是内部很多同事反馈,需要有个地方存工具,并且可以写说明书,如果需要的人,那么可以在界面上直接下载工具和查看工具的说明,这样就不用每次都找人发文档,各种本地找,很浪费时间,故此需要实现这样的一个功能

新建说明书表

复制代码
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>
相关推荐
星空寻流年2 小时前
设计模式第一章(建造者模式)
java·设计模式·建造者模式
索迪迈科技3 小时前
网络请求库——Axios库深度解析
前端·网络·vue.js·北京百思可瑞教育·百思可瑞教育
自学也学好编程3 小时前
【数据库】Redis详解:内存数据库与缓存之王
数据库·redis
gnip3 小时前
JavaScript二叉树相关概念
前端
gb42152873 小时前
java中将租户ID包装为JSQLParser的StringValue表达式对象,JSQLParser指的是?
java·开发语言·python
JAVA不会写3 小时前
在Mybatis plus中如何使用自定义Sql
数据库·sql
IT 小阿姨(数据库)3 小时前
PgSQL监控死元组和自动清理状态的SQL语句执行报错ERROR: division by zero原因分析和解决方法
linux·运维·数据库·sql·postgresql·centos
曾经的三心草4 小时前
Python2-工具安装使用-anaconda-jupyter-PyCharm-Matplotlib
android·java·服务器
Metaphor6924 小时前
Java 高效处理 Word 文档:查找并替换文本的全面指南
java·经验分享·word