研发图纸文件管理系统-前端项目
he wei
2024-01-12 36704dbc78007ca051633aa015c1094de3a2630a
UA 技术规格书
2个文件已修改
8个文件已添加
2029 ■■■■■ 已修改文件
src/pages/resourceManage/product/list.vue 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/resourceManage/specification/apis.js 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/resourceManage/specification/descRes.vue 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/resourceManage/specification/history.vue 424 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/resourceManage/specification/index.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/resourceManage/specification/list.vue 996 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/resourceManage/specification/pop.vue 176 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/resourceManage/specification/rowRes.vue 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/resourceManage/specification/versionList.vue 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/config.js 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/resourceManage/product/list.vue
@@ -1208,16 +1208,19 @@
      }
      const { pageCurr, pageSize, conditions, columns } = this;
      let params = {};
      let col, index;
      Object.keys(conditions).forEach((v) => {
        switch (v) {
          case "isNormal":
            if (conditions[v]) {
              params["customCode"] = "";
              columns.forEach((val) => {
              columns.forEach((val, idx) => {
                if (val.dataIndex == "customCode") {
                  val.search.value = "";
                  col = { ...val, search: { value: "", backup: "" } };
                  index = idx;
                }
              });
              this.$set(this.columns, index, col);
            }
            break;
          case "customCode":
src/pages/resourceManage/specification/apis.js
New file
@@ -0,0 +1,86 @@
import axios from "@/assets/axios";
/**
 * 列表
 * @returns
 */
export const getList = (pageNum, pageSize, data) => {
  return axios({
    method: "POST",
    url: "technicalSpecification/getInfo",
    params: { pageNum, pageSize, ...data },
  });
};
/**
 * 解析规格书说明
 * multipartFile
 * @returns
 */
export const excelParse = (data) => {
  return axios({
    method: "POST",
    url: "technicalSpecification/excelParse",
    headers: {
      "Content-Type": "multipart/form-data",
    },
    data,
  });
};
/**
 * 解析规格书说明
 * file1  file2
 * technicalSpecificationStr
 * @returns
 */
export const upload = (data) => {
  return axios({
    method: "POST",
    url: "technicalSpecification/upload",
    headers: {
      "Content-Type": "multipart/form-data",
    },
    data,
  });
};
/**
 * 更新锁定状态
 * {id lockFlag reason}
 * @returns
 */
export const updateLock = (params) => {
  return axios({
    method: "POST",
    url: "technicalSpecification/updateLock",
    params,
  });
};
/**
 * 查询指定id的说明书的锁定日志
 * {id}
 * @returns
 */
export const getLogs = (id) => {
  return axios({
    method: "GET",
    url: "technicalSpecificationLog/getLockLogInfoById",
    params: { id },
  });
};
/**
 * 查询指定技术规格书的所有版本
 * {applyCustomCode, applyMaterialCode, applyModel}
 * @returns
 */
export const getVersions = (params) => {
  return axios({
    method: "POST",
    url: "technicalSpecification/getVersionByInfo",
    params,
  });
};
src/pages/resourceManage/specification/descRes.vue
New file
@@ -0,0 +1,94 @@
<template>
  <div class="">
    <table class="table">
      <tbody>
        <tr>
          <th class="title" colspan="6">文件基本信息</th>
        </tr>
        <tr>
          <th class="col-1">文件名称</th>
          <td colspan="5">{{ info.fileName }}</td>
        </tr>
        <tr>
          <th class="col-1">软件类型</th>
          <td colspan="5">{{ info.type }}</td>
        </tr>
        <tr>
          <th class="col-1">文件版本</th>
          <td colspan="1">{{ info.version }}</td>
          <th class="col-1">基于版本</th>
          <td colspan="3">{{ info.basedVersion }}</td>
        </tr>
        <tr>
          <th class="col-1">负责人</th>
          <td colspan="1">{{ info.owner }}</td>
          <th class="col-1">归档日期</th>
          <td colspan="3">{{ info.filingDate }}</td>
        </tr>
        <tr>
          <th class="title" colspan="6">规格书适用机型</th>
        </tr>
        <tr>
          <th class="col-1">物料编码</th>
          <td colspan="1">{{ info.applyMaterialCode }}</td>
          <th class="col-1">规格型号</th>
          <td colspan="1">{{ info.applyModel }}</td>
          <th class="col-1">定制单号</th>
          <td colspan="1">{{ info.applyCustomCode }}</td>
        </tr>
        <tr>
          <th class="col-1">发布说明</th>
          <td colspan="5">{{ info.releaseNotes }}</td>
        </tr>
      </tbody>
    </table>
  </div>
</template>
<script>
export default {
  name: "",
  props: {
    info: {
      type: Object,
      default() {
        return {};
      },
    },
  },
  data() {
    return {};
  },
  computed: {},
  watch: {},
  methods: {},
  mounted() {},
};
</script>
<style lang="less" scoped>
.table {
  width: 100%;
  // table-layout: fixed;
  border-collapse: collapse;
  th,
  td {
    border: 1px #333 solid;
    padding: 4px;
  }
  td {
    // color: #13c2c2;
    color: #333;
  }
  .title {
    font-weight: 900;
    padding-left: 2em;
    font-style: italic;
  }
}
.col-1 {
  // word-break:break-all;
  width: 6.4em;
}
</style>
src/pages/resourceManage/specification/history.vue
New file
@@ -0,0 +1,424 @@
<template>
  <div class="main">
    <a-layout class="main">
      <a-layout-sider width="260">
        <list class="list" :list="versionList" @select="selectChanged"></list>
      </a-layout-sider>
      <a-layout>
        <a-layout-header>
          <a-card>
            <a-descriptions title="详情" bordered>
              <a-descriptions-item label="文件名称">{{
                currentVersion.fileName
              }}</a-descriptions-item>
              <a-descriptions-item label="版本号">{{
                currentVersion.version
              }}</a-descriptions-item>
              <a-descriptions-item label="上传时间">{{
                currentVersion.createTime
              }}</a-descriptions-item>
              <a-descriptions-item label="负责人">{{
                currentVersion.owner
              }}</a-descriptions-item>
              <a-descriptions-item label="文件类型">{{
                currentVersion.type
              }}</a-descriptions-item>
              <a-descriptions-item label="归档日期">{{
                currentVersion.filingDate
              }}</a-descriptions-item>
              <a-descriptions-item label="发布说明">{{
                currentVersion.releaseNotes
              }}</a-descriptions-item>
            </a-descriptions>
          </a-card>
        </a-layout-header>
        <a-layout-content>
          <div class="wraper" ref="wraper">
            <div class="inner">
              <div class="title">适用产品</div>
              <a-table
                v-if="currentVersion.fileName"
                ref="aTable"
                size="small"
                bordered
                :columns="columns"
                :data-source="dataSource"
                :pagination="false"
                rowKey="id"
              >
              </a-table>
            </div>
          </div>
        </a-layout-content>
        <a-layout-footer>
          <a-card>
            <template>
              <a-button
                v-if="currentVersion.lockFlag == 0"
                type="primary"
                @click="view(currentVersion)"
                >预览</a-button
              >
              <a-button
                    v-if="canLock()"
                    type="primary"
                    @click="lock"
                    >锁定</a-button
                  >
                  <a-button
                    v-if="canUnLock()"
                    type="primary"
                    @click="lock()"
                    >解锁</a-button
                  >
              <a-button type="primary" @click="viewLog">锁定日志</a-button>
            </template>
          </a-card>
        </a-layout-footer>
      </a-layout>
    </a-layout>
    <!-- 操作原因 -->
    <a-modal
      :visible="reasonVisible"
      :width="460"
      title="操作原因"
      :destroyOnClose="true"
      :maskClosable="false"
      @cancel="reasonCancel"
      @ok="reasonOk"
    >
      <a-form-model-item ref="name" label="操作原因">
        <a-input
          type="textarea"
          v-model="reason"
          placeHolder="请输入操作原因"
        />
      </a-form-model-item>
    </a-modal>
    <!-- 日志 -->
    <a-modal
      :visible="logVisible"
      :footer="null"
      :width="800"
      title="操作日志"
      :destroyOnClose="true"
      @cancel="logCancel"
    >
      <div class="log-content">
        <a-timeline v-if="logList.length">
          <a-timeline-item
            v-for="(item, idx) in logList"
            :key="'log_' + idx"
            :color="item.status == 0 ? 'red' : 'green'"
          >
            <div>
              <span class="user">{{ item.userName }}</span> 在
              <span class="time">{{ item.createTime }}</span>
              {{ item.status == 0 ? "锁定" : "解锁" }}了版本
              <span class="version">{{ item.fileVersion }}</span>
            </div>
            <div>操作原因: {{ item.reason ? item.reason : "无" }}</div>
          </a-timeline-item>
        </a-timeline>
        <a-empty v-else />
      </div>
    </a-modal>
  </div>
</template>
<script>
import List from "./versionList";
import getWebUrl from "@/assets/js/tools/getWebUrl";
import { updateLock, getVersions, getLogs } from "./apis";
import checkPermit from "@/assets/js/tools/checkPermit";
import PERMITS from "@/assets/js/const/const_permits";
import { mapGetters } from "vuex";
export default {
  name: "",
  data() {
    let { applyMaterialCode, applyModel, applyCustomCode } = this.$route.query;
    return {
      logList: [],
      logVisible: false,
      reasonVisible: false,
      reason: "",
      applyMaterialCode,
      applyModel,
      applyCustomCode,
      versionList: [],
      webUrl: getWebUrl(),
      currentVersion: {},
      columns: [
        {
          title: "物料编码",
          dataIndex: "applyMaterialCode",
          align: "center",
        },
        {
          title: "物料型号",
          dataIndex: "applyModel",
          align: "center",
        },
        {
          title: "定制单号",
          dataIndex: "applyCustomCode",
          align: "center",
        },
      ],
      dataSource: [],
    };
  },
  components: {
    List,
  },
  computed: {
    ...mapGetters("account", ["permits", 'user']),
    canUpload() {
      return checkPermit(PERMITS.uploadSoftware, this.permits);
    },
    canDownload() {
      return checkPermit(PERMITS.downloadSoftware, this.permits);
    },
  },
  watch: {},
  methods: {
    canLock() {
      let row = this.currentVersion;
      let uname = this.user.name;
      return row.owner == uname && row.lockFlag == 0;
    },
    canUnLock() {
      let row = this.currentVersion;
      let uname = this.user.name;
      // flag 为1 是可操作的
      return row.owner == uname && row.lockFlag == 1 && row.flag == 1;
    },
    view(obj) {
      let { fileUrl } = obj;
      window.open(this.webUrl + fileUrl);
    },
    viewLog() {
      // console.log(obj);
      const { id } = this.currentVersion;
      getLogs(id).then((res) => {
        const { code, data, data2 } = res.data;
        if (code) {
          this.logList = data2;
          this.logVisible = true;
        } else {
          this.$message.error("日志查询失败");
        }
      });
    },
    logCancel() {
      this.logVisible = false;
    },
    downloadFile() {
      // console.log(record);
      let record = this.currentVersion;
      let loading = this.$layer.loading();
      let link = document.createElement("a");
      link.style.display = "none";
      let url = this.webUrl + record.fileUrl;
      let fileName = record.fileUrl.split("/").pop();
      link.href = url;
      link.download = fileName;
      document.body.appendChild(link);
      link.click();
      this.$layer.close(loading);
      document.body.removeChild(link);
    },
    getVersions() {
      let { applyMaterialCode, applyModel, applyCustomCode } = this;
      getVersions({ applyMaterialCode, applyModel, applyCustomCode }).then(
        (res) => {
          const { code, data, data2 } = res.data;
          let list = [];
          if (code && data) {
            list = data2;
          }
          this.versionList = list;
        }
      );
    },
    selectChanged(obj) {
      // console.log(obj, "--==");
      this.currentVersion = obj;
      this.dataSource = [obj];
    },
    reasonCancel() {
      this.reasonVisible = false;
    },
    reasonOk() {
      let { id, lockFlag } = this.currentVersion;
      let reason = this.reason;
      lockFlag = lockFlag == 0 ? 1 : 0;
      updateLock({id, lockFlag, reason}).then((res) => {
        const { code } = res.data;
        if (code) {
          this.$message.success("操作成功");
          this.reasonVisible = false;
          this.currentVersion.lockFlag = lockFlag;
        } else {
          this.$message.error("操作失败");
        }
      });
    },
    lock() {
      this.reason = "";
      this.reasonVisible = true;
    },
  },
  mounted() {
    this.getVersions();
  },
  beforeDestroy() {},
};
</script>
<style scoped lang="less">
.main {
  height: 100%;
  .ant-layout-header,
  .ant-layout-sider {
    background: transparent;
  }
  .ant-layout-header {
    height: auto;
  }
  .list {
    height: 100%;
  }
  .wraper {
    height: 100%;
    position: relative;
    .inner {
      position: absolute;
      left: 0;
      top: 0;
      right: 0;
      bottom: 0;
    }
  }
  /deep/.ant-layout {
    margin-left: 10px;
  }
  .ant-layout-header {
    padding: 0;
    line-height: inherit;
    margin-bottom: 10px;
  }
  .ant-btn + .ant-btn {
    margin-left: 1em;
  }
  .ant-layout-content {
    -webkit-box-sizing: border-box;
    box-sizing: border-box;
    margin: 0;
    padding: 0;
    color: rgba(0, 0, 0, 0.65);
    font-size: 14px;
    font-variant: tabular-nums;
    line-height: 1.5;
    list-style: none;
    -webkit-font-feature-settings: "tnum";
    font-feature-settings: "tnum";
    position: relative;
    background: #fff;
    border-radius: 2px;
    -webkit-transition: all 0.3s;
    transition: all 0.3s;
    padding: 24px;
    zoom: 1;
    /deep/.ant-descriptions-item-label {
      width: 12rem;
      text-align: right;
    }
  }
  .ant-layout-footer {
    padding: 0;
  }
  .img-wraper {
    width: 80px;
    height: 50px;
    display: inline-block;
    .image-view {
      width: 100%;
      height: 100%;
      /deep/img {
        width: 100%;
        height: 100%;
        object-fit: contain;
      }
    }
  }
  /deep/.is-replace > td {
    background: #00eaff;
  }
  /deep/.is-replace.is-replace.ant-table-row-hover > td,
  /deep/.is-replace.is-replace:hover > td {
    background: #affaff;
  }
  /deep/.ant-table-row-level-1 > td {
    background: #ff8ea2;
  }
  /deep/.ant-table-row-level-1.ant-table-row-level-1.ant-table-row-hover > td,
  /deep/.ant-table-row-level-1.ant-table-row-level-1:hover > td {
    background: #ffbcc9;
  }
  .title {
    margin-bottom: 20px;
    color: rgba(0, 0, 0, 0.85);
    font-weight: bold;
    font-size: 16px;
    line-height: 1.5;
  }
}
.diff-content {
  display: flex;
  flex-direction: column;
  height: 400px;
  .footer {
    text-align: right;
    .btn {
      padding: 6px;
      display: inline-block;
      background: #00eaff;
      border-radius: 4px;
      color: #fff;
    }
  }
  .img-wrap {
    flex: 1;
  }
  &.full {
    position: fixed;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
    height: auto;
  }
}
.img-wrap {
  width: 100%;
  position: relative;
  .img-wrap-inner {
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
    img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
  }
}
.all {
  padding: 10px 20px;
  font-size: 22px;
}
</style>
src/pages/resourceManage/specification/index.js
New file
@@ -0,0 +1,2 @@
import list from './list';
export default list;
src/pages/resourceManage/specification/list.vue
New file
@@ -0,0 +1,996 @@
<template>
  <div class="main">
    <div class="inner" ref="wraper">
      <a-spin class="" :spinning="spinning" tip="拼命加载中...">
        <a-card>
          <advance-table
            ref="table"
            class="doc-center-table"
            :data-source="dataSource"
            :columns="columns"
            title=""
            :row-key="(record, index) => index"
            @search="onSearch"
            @refresh="onRefresh"
            @reset="onReset"
            :format-conditions="true"
            :scroll="{ y }"
            :pagination="{
              current: pageCurr,
              pageSize: pageSize,
              total: total,
              showSizeChanger: true,
              showLessItems: true,
              showQuickJumper: true,
              pageSizeOptions: ['10', '20', '50', '100'],
              showTotal: (total, range) =>
                `第 ${range[0]}-${range[1]} 条,总计 ${total} 条`,
              onChange: onPageChange,
              onShowSizeChange: onSizeChange,
            }"
          >
            <template slot="title">
              <a-space class="operator">
                <span class="title">技术规格书</span>
                <a-button v-if="canUploadBom" type="primary" @click="showUpload"
                  >新增</a-button
                >
              </a-space>
            </template>
            <template slot="status" slot-scope="{ record }">
              <a-tag color="green" v-if="!record.lockFlag">可用</a-tag>
              <a-tag color="red" v-else>不可用</a-tag>
            </template>
            <template slot="action" slot-scope="{ record }">
              <a-button type="primary" @click="viewLog(record)"
                >锁定日志</a-button
              >
              <a-divider type="vertical"></a-divider>
              <a-popover title="" trigger="hover">
                <a-space class="btn-grp" direction="vertical" slot="content">
                  <!-- <a-button
                    v-if="canUpload"
                    type="primary"
                    @click="updateDesc(record)"
                    >更新说明</a-button
                  > -->
                  <a-button
                    v-if="record.lockFlag == 0"
                    type="primary"
                    @click="view(record)"
                    >预览</a-button
                  >
                  <a-button
                    v-if="canLock(record)"
                    type="primary"
                    @click="lock(record)"
                    >锁定</a-button
                  >
                  <a-button
                    v-if="canUnLock(record)"
                    type="primary"
                    @click="lock(record)"
                    >解锁</a-button
                  >
                  <!-- <a-button
                    v-if="canUpload"
                    type="primary"
                    @click="handleEmailShow(record)"
                    >邮件通知</a-button
                  > -->
                  <a-button type="primary" @click="goHistory(record)"
                    >历史版本</a-button
                  >
                </a-space>
                <a>更多</a>
              </a-popover>
            </template>
          </advance-table>
        </a-card>
      </a-spin>
    </div>
    <pop
      :visible.sync="popVisible"
      :x="popPosition.x"
      :y="popPosition.y"
      :position="popPosition.dir"
      :info="popInfo"
    ></pop>
    <a-modal
      :visible="uploadShow"
      :footer="null"
      :width="800"
      title="上传SOP"
      :destroyOnClose="true"
      :maskClosable="false"
      @cancel="uploadCancel"
    >
      <div class="">
        <template v-if="!onlyXls">
          <a-row type="flex" class="row">
            <a-col flex="6em" class="label">规格书文件</a-col>
            <a-col :flex="1">
              <a-upload
                class="upload"
                :before-upload="beforeUpload"
                @change="uploadChange"
                accept=".zip,.rar,.pdf"
              >
                <a-button type="primary">选文件</a-button>
              </a-upload>
            </a-col>
          </a-row>
        </template>
        <a-row type="flex" class="row">
          <a-col flex="6em" class="label">规格书说明</a-col>
          <a-col :flex="1">
            <a-upload
              class="upload"
              :before-upload="beforeUpload"
              @change="uploadChange1"
              accept=".xls,.xlsx"
            >
              <a-button type="primary">说明文件</a-button>
            </a-upload>
          </a-col>
        </a-row>
        <div class="sub-title">说明文件解析结果</div>
        <div class="res-content">
          <desc-res :info="resObj"></desc-res>
        </div>
        <div class="modal-footer">
          <a-button type="danger" @click="uploadCancel"> 取消 </a-button>
          <a-button v-if="!onlyXls" type="primary" @click="uploadSop">
            提交
          </a-button>
          <a-button v-else type="primary" @click="applyModel"> 提交 </a-button>
        </div>
      </div>
    </a-modal>
    <a-modal
      :visible="emailShow"
      :footer="null"
      :width="760"
      title="邮件发送"
      :destroyOnClose="true"
      :maskClosable="false"
      @cancel="emailCancel"
    >
      <email-card
        :visible.sync="emailShow"
        :users="userList"
        :title="emailInfo.title"
        :content="emailInfo.content"
        :type="2"
        v-if="emailShow"
      ></email-card>
    </a-modal>
    <!-- 操作原因 -->
    <a-modal
      :visible="reasonVisible"
      :width="460"
      title="操作原因"
      :destroyOnClose="true"
      :maskClosable="false"
      @cancel="reasonCancel"
      @ok="reasonOk"
    >
      <a-form-model-item ref="name" label="操作原因">
        <a-input
          type="textarea"
          v-model="reason"
          placeHolder="请输入操作原因"
        />
      </a-form-model-item>
    </a-modal>
    <!-- 日志 -->
    <a-modal
      :visible="logVisible"
      :footer="null"
      :width="800"
      title="操作日志"
      :destroyOnClose="true"
      @cancel="logCancel"
    >
      <div class="log-content">
        <a-timeline v-if="logList.length">
          <a-timeline-item
            v-for="(item, idx) in logList"
            :key="'log_' + idx"
            :color="item.status == 0 ? 'red' : 'green'"
          >
            <div>
              <span class="user">{{ item.userName }}</span> 在
              <span class="time">{{ item.createTime }}</span>
              {{ item.status == 0 ? "锁定" : "解锁" }}了版本
              <span class="version">{{ item.fileVersion }}</span>
            </div>
            <div>操作原因: {{ item.reason ? item.reason : "无" }}</div>
          </a-timeline-item>
        </a-timeline>
        <a-empty v-else />
      </div>
    </a-modal>
    <!-- 日志 -->
    <a-modal
      :visible="pdfInfo.visible"
      :footer="null"
      :width="960"
      title="文件预览"
      :destroyOnClose="true"
      @cancel="pdfCancel"
    >
      <div style="height:600px; overflow-y: auto">
        <iframe :src="pdfInfo.src"></iframe>
      </div>
    </a-modal>
  </div>
</template>
<script>
import AdvanceTable from "@/components/table/advance/AdvanceTable";
import getWebUrl from "@/assets/js/tools/getWebUrl";
import checkPermit from "@/assets/js/tools/checkPermit";
import PERMITS from "@/assets/js/const/const_permits";
import { mapGetters } from "vuex";
import EmailCard from "../../components/emailCard";
import { getUserList } from "../../permission/apis";
import {
  excelParse,
  upload,
  getList,
  updateLock,
  getLogs,
  // updateSop,
} from "./apis";
import { sendMail } from "../../components/emailCard/apis";
import offset from "@/assets/js/tools/offset";
import Pop from "./pop";
import DescRes from "./descRes";
import getFileTypeAndName from "@/assets/js/tools/getFileTypeAndName";
import VuePdf from "vue-pdf";
export default {
  components: {
    AdvanceTable,
    Pop,
    DescRes,
    EmailCard,
    VuePdf,
  },
  name: "list",
  data() {
    return {
      pdfInfo: {
        visible: false,
        src: "",
        loadedRatio: 0,
        page: 1,
        numPages: 0,
        rotate: 0,
      },
      rowFileName: "",
      logList: [],
      logVisible: false,
      currentObj: null,
      reasonVisible: false,
      reason: "",
      emailShow: false,
      emailInfo: {
        title: "",
        content: "",
      },
      userList: [],
      onlyXls: false,
      uploadShow: false,
      webUrl: getWebUrl(),
      prodsColumns: [
        {
          title: "物料编码",
          dataIndex: "code",
          align: "center",
        },
        {
          title: "型/板号",
          dataIndex: "model",
          align: "center",
          width: 180,
        },
        // {
        //   title: "操作",
        //   dataIndex: "operation",
        //   align: "center",
        //   width: 180,
        //   scopedSlots: { customRender: "action" },
        // },
      ],
      file: null,
      file1: null,
      resObj: {},
      popInfo: {},
      popVisible: false,
      popPosition: {
        x: 500,
        y: 100,
        dir: "bottom",
      },
      spinning: false,
      loading: false,
      pageCurr: 1,
      pageSize: 20,
      total: 0,
      y: 400,
      update: -1,
      conditions: {},
      columns: [
        {
          title: "文件名称",
          dataIndex: "fileName",
          align: "center",
          width: 140,
          // searchAble: true,
          customCell: this.customCell,
        },
        {
          title: "文件版本",
          dataIndex: "version",
          align: "center",
          width: 80,
          searchAble: false,
          customCell: this.customCell,
        },
        {
          title: "基于版本",
          dataIndex: "basedVersion",
          align: "center",
          width: 80,
          searchAble: false,
          customCell: this.customCell,
        },
        {
          title: "发布时间",
          dataIndex: "createTime",
          align: "center",
          width: 160,
          customCell: this.customCell,
        },
        {
          title: "负责人",
          dataIndex: "owner",
          align: "center",
          searchAble: true,
          width: 90,
          customCell: this.customCell,
        },
        {
          title: "适用产品料号",
          dataIndex: "applyMaterialCode",
          searchAble: true,
          align: "center",
          width: 120,
          customCell: this.customCell,
        },
        {
          title: "适用产品型号",
          dataIndex: "applyModel",
          searchAble: true,
          align: "center",
          width: 120,
          customCell: this.customCell,
        },
        {
          title: "适用产品定制单号",
          dataIndex: "applyCustomCode",
          searchAble: true,
          align: "center",
          width: 140,
          customCell: this.customCell,
        },
        {
          title: "标准机型",
          dataIndex: "isNormal",
          dataType: "boolean",
          align: "center",
          width: 40,
          searchAble: true,
          noSearch: true,
          visible: false,
        },
        {
          title: "发布说明",
          dataIndex: "releaseNotes",
          align: "center",
          width: 260,
          customCell: this.customCell,
        },
        {
          title: "是否可用",
          dataIndex: "lockFlag",
          dataType: "boolean",
          align: "center",
          searchAble: true,
          width: 100,
          // search: {
          //   default: true,
          // },
          scopedSlots: { customRender: "status" },
        },
        {
          title: "操作",
          dataIndex: "operation",
          align: "center",
          width: 200,
          fixed: "right",
          scopedSlots: { customRender: "action" },
          noSearch: true,
        },
      ],
      dataSource: [],
    };
  },
  computed: {
    ...mapGetters("account", ["permits", "user"]),
    ...mapGetters("setting", ["affixed"]),
    canUploadBom() {
      return checkPermit(PERMITS.uploadBom, this.permits);
    },
    canUpload() {
      return checkPermit(PERMITS.uploadSoftware, this.permits);
    },
    canDownload() {
      return checkPermit(PERMITS.downloadSop, this.permits);
    },
  },
  watch: {
    update(n) {
      if (-1 != n && !this._inactive) {
        this.$nextTick(() => {
          const table = this.$refs.table;
          const header = document.querySelectorAll(
            ".doc-center-table .ant-table-header"
          )[0].clientHeight;
          const bar = document.querySelectorAll(".header-bar")[0].clientHeight;
          if (table.fullScreen) {
            this.y = table.$el.clientHeight - bar - header - 64;
          } else {
            const wraper = this.$refs.wraper.clientHeight;
            const card = document.querySelectorAll(".ant-card-body")[0];
            const { paddingBottom, paddingTop } = getComputedStyle(card, null);
            const h =
              wraper -
              header -
              64 -
              bar -
              parseInt(paddingBottom) -
              parseInt(paddingTop);
            this.y = h;
          }
        });
      }
    },
    affixed() {
      setTimeout(() => {
        this.update = Math.random();
      }, 200);
    },
  },
  methods: {
    view(obj) {
      let { fileUrl } = obj;
      window.open(this.webUrl + fileUrl);
    },
    canLock(row) {
      let uname = this.user.name;
      return row.owner == uname && row.lockFlag == 0;
    },
    canUnLock(row) {
      let uname = this.user.name;
      // flag 为1 是可操作的
      return row.owner == uname && row.lockFlag == 1 && row.flag == 1;
    },
    searchAllUserList() {
      getUserList()
        .then((res) => {
          let rs = res.data;
          if (rs.code && rs.data) {
            this.userList = rs.data2;
          }
        })
        .catch((error) => {
          console.log(error);
        });
    },
    showUpload() {
      this.file = null;
      this.file1 = null;
      this.resObj = {};
      this.onlyXls = false;
      this.uploadShow = true;
    },
    updateDesc(row) {
      this.file = null;
      this.file1 = null;
      this.resObj = { rowId: row.id };
      this.rowFileName = row.fileName;
      this.onlyXls = true;
      this.uploadShow = true;
    },
    uploadCancel() {
      this.uploadShow = false;
    },
    sendEmail() {
      let { title, content } = this.handleEmailShow(this.curObj, true);
      let params = { mailList: this.mailList, title, content };
      sendMail(params);
    },
    handleEmailShow(record, get) {
      let title =
        "[技术规格书发布记录]" + record.fileName + " 版本:" + record.version;
      let content = [];
      content.push("负责人: " + record.owner);
      content.push("归档日期: " + record.filingDate);
      content.push("物料编码: " + record.applyMaterialCode);
      content.push("物料名称: " + record.applyModel);
      content.push("规格型号: " + record.applyModel);
      content.push("标准机型: " + (!record.applyCustomCode ? "是" : "否"));
      content.push("定制单号: " + (record.applyCustomCode || "无"));
      content.push("版本: " + record.version);
      content.push("发布说明: " + record.releaseNotes);
      if (get) {
        return {
          title,
          content: content.join("\n"),
        };
      }
    },
    emailCancel() {
      this.emailShow = false;
    },
    uploadSop() {
      if (!this.file) {
        this.$message.error("请选择要上传的文件");
        return false;
      }
      if (!this.file1) {
        this.$message.error("请选择说明文件");
        return false;
      }
      if (!this.resObj.fileName) {
        this.$message.error("说明文件解析异常");
        return false;
      }
      // 如果文件名称 != 说明里的文件名称 + 文件版本  则报错拒绝
      let reg = /(.*)\..{1,5}$/;
      let name1 = this.file.name.match(reg)[1];
      let { fileName, version, type } = this.resObj;
      let name2 = fileName + version;
      if (type.toLowerCase() != "pdf") {
        this.$message.error("规格书文件类型不正确");
        return false;
      }
      if (name1.toLowerCase() != name2.toLowerCase()) {
        console.log(name1.toLowerCase() + "&&&&&&" + name2.toLowerCase());
        this.$message.error("规格书与说明文件可能不匹配");
        return false;
      }
      let loading = this.$layer.loading();
      const formData = new FormData();
      formData.append("file1", this.file);
      formData.append("file2", this.file1);
      formData.append("technicalSpecificationStr", JSON.stringify(this.resObj));
      upload(formData)
        .then((res) => {
          let { code, data, msg } = res.data;
          if (code && data) {
            this.uploadShow = false;
            this.$message.success("上传成功");
            this.searchData();
          } else {
            this.$message.error(msg);
          }
          this.$layer.close(loading);
        })
        .catch((error) => {
          this.$layer.close(loading);
          console.log(error);
        });
    },
    applyModel() {
      if (!this.file1) {
        this.$message.error("请选择说明文件");
        return false;
      }
      if (!this.resObj.fileName) {
        this.$message.error("说明文件解析异常");
        return false;
      }
      if (
        this.rowFileName.toLowerCase() != this.resObj.fileName.toLowerCase()
      ) {
        this.$message.error("说明文件与该条记录可能不匹配");
        return false;
      }
      let loading = this.$layer.loading();
      // updateSop(this.resObj)
      //   .then((res) => {
      //     let { code, data, msg } = res.data;
      //     if (code) {
      //       this.uploadShow = false;
      //       this.$message.success("上传成功");
      //       this.searchData();
      //     } else {
      //       this.$message.error("解析失败");
      //     }
      //     this.$layer.close(loading);
      //   })
      //   .catch((error) => {
      //     this.$layer.close(loading);
      //     console.log(error);
      //   });
    },
    cellMouseenter(e, obj) {
      // console.log("enter", e, obj);
      const wraper = this.$refs.wraper;
      const { clientHeight, clientWidth } = wraper;
      const { target, clientX, clientY } = e;
      let { left: x, top: y } = offset(wraper);
      x = clientX - x;
      y = clientY - y;
      // 如果clientHeight 小于380 * 2 则左右布局
      let dir = "bottom";
      if (clientHeight < 380 * 2) {
        if (x + 420 + 18 > clientWidth) {
          dir = "left";
        } else {
          dir = "right";
        }
        if (y < 180) {
          y = 180;
        } else if (y > clientHeight - 378) {
          y = clientHeight / 2;
        }
      } else {
        if (y + 18 + 360 > clientHeight) {
          // y = clientHeight - 378;
          dir = "top";
        } else {
          dir = "bottom";
        }
        if (x < 400) {
          x = 400;
        }
        if (x + 400 > clientWidth) {
          x = clientWidth - 400;
        }
      }
      this.popPosition.x = x;
      this.popPosition.y = y;
      this.popPosition.dir = dir;
      this.popInfo = obj;
      this.popVisible = true;
    },
    cellMouseleave(e, obj) {
      // console.log("leave", obj);
      this.popVisible = false;
    },
    customCell(record) {
      return {
        on: {
          mouseenter: (e) => this.cellMouseenter(e, record),
          mouseleave: (e) => this.cellMouseleave(e, record),
        },
      };
    },
    beforeUpload() {
      return false;
    },
    uploadChange(data) {
      const { file, fileList } = data;
      if (fileList.length > 1) {
        fileList.shift();
      }
      if (fileList.length) {
        this.file = fileList[0].originFileObj;
      } else {
        this.file = null;
      }
    },
    uploadChange1(data) {
      const { file, fileList } = data;
      if (fileList.length > 1) {
        fileList.shift();
      }
      if (fileList.length) {
        this.file1 = fileList[0].originFileObj;
      } else {
        this.file1 = null;
        this.resObj = [];
        return false;
      }
      let loading = this.$layer.loading();
      const formData = new FormData();
      formData.append("multipartFile", this.file1);
      excelParse(formData)
        .then((res) => {
          this.$layer.close(loading);
          let { code, data, data2, msg } = res.data;
          if (code && data) {
            let rowId = this.resObj.rowId;
            this.resObj = data2;
            this.resObj.id = rowId;
            this.$message.success("解析成功");
          } else {
            this.$message.error(msg);
          }
        })
        .catch((error) => {
          this.$layer.close(loading);
          console.log(error);
        });
    },
    resize() {
      setTimeout(() => {
        this.update = Math.random();
      }, 200);
    },
    activeFN() {
      this.resize();
    },
    onSearch(conditions, searchOptions, col) {
      // console.log(conditions);
      // console.log(searchOptions, "00", col);
      this.pageCurr = 1;
      this.conditions = conditions;
      this.searchData();
    },
    onPageChange(page, pageSize) {
      this.pageCurr = page;
      this.pageSize = pageSize;
      this.searchData();
    },
    onSizeChange(current, size) {
      this.pageCurr = 1;
      this.pageSize = size;
      this.searchData();
    },
    onRefresh(conditions) {
      this.conditions = conditions;
      this.searchData();
    },
    onReset(conditions) {
      this.conditions = conditions;
      this.searchData();
    },
    searchData() {
      const { pageCurr, pageSize, conditions, columns } = this;
      let params2 = {};
      // console.log(this.conditions["status"]);
      let col, index;
      Object.keys(conditions).forEach((v) => {
        switch (v) {
          case "isNormal":
            if (conditions[v]) {
              params2["applyCustomCode"] = "";
              columns.forEach((val, idx) => {
                if (val.dataIndex == "applyCustomCode") {
                  col = { ...val, search: { value: "", backup: "" } };
                  index = idx;
                }
              });
              this.$set(this.columns, index, col);
            }
            break;
          default:
            params2[v] = conditions[v];
            break;
        }
      });
      let list = [];
      getList(pageCurr, pageSize, params2)
        .then((res) => {
          let { code, data, data2 } = res.data;
          let total = 0;
          if (code && data) {
            // console.log(data2);
            list = data2.list;
            total = data2.total;
          }
          this.dataSource = list.map((item) => {
            const fileInfo = getFileTypeAndName(item.fileUrl);
            item.isCanPreview = fileInfo.type === "pdf";
            return item;
          });
          this.total = total;
          if (-1 == this.update) {
            this.update = Math.random();
          }
        })
        .catch((err) => {
          console.log(err);
        });
    },
    downloadFile(record) {
      const fileInfo = getFileTypeAndName(record.fileUrl);
      let url = this.webUrl + record.fileUrl;
      if (fileInfo.type === "pdf") {
        // 预览
        window.open(url);
      } else {
        // 下载
        let loading = this.$layer.loading();
        let link = document.createElement("a");
        link.style.display = "none";
        let fileName = record.fileUrl.split("/").pop();
        link.href = url;
        link.download = fileName;
        document.body.appendChild(link);
        link.click();
        this.$layer.close(loading);
        document.body.removeChild(link);
      }
    },
    reasonCancel() {
      this.reasonVisible = false;
    },
    reasonOk() {
      let { id, lockFlag } = this.currentObj;
      let reason = this.reason;
      lockFlag = lockFlag == 0 ? 1 : 0;
      updateLock({ id, lockFlag, reason }).then((res) => {
        const { code } = res.data;
        if (code) {
          this.$message.success("操作成功");
          this.reasonVisible = false;
          this.searchData();
        } else {
          this.$message.error("操作失败");
        }
      });
    },
    lock(record) {
      this.reason = "";
      this.currentObj = record;
      this.reasonVisible = true;
    },
    viewLog(obj) {
      // console.log(obj);
      const { id } = obj;
      getLogs(id).then((res) => {
        const { code, data, data2 } = res.data;
        if (code) {
          this.logList = data2;
          this.logVisible = true;
        } else {
          this.$message.error("日志查询失败");
        }
      });
    },
    logCancel() {
      this.logVisible = false;
    },
    pdfCancel() {
      this.pdfInfo.visible = false;
    },
    goHistory(record) {
      let { applyMaterialCode, applyModel, applyCustomCode } = record;
      this.$router.push({
        path: "/resource/specification-history",
        query: { applyMaterialCode, applyModel, applyCustomCode },
      });
    },
  },
  mounted() {
    this.searchData();
    this.searchAllUserList();
    window.addEventListener("resize", this.resize);
  },
  destroyed() {
    window.removeEventListener("resize", this.resize);
  },
};
</script>
<style scoped lang="less">
.main {
  height: 100%;
  position: relative;
  .inner {
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
  }
}
.img-wraper {
  width: 80px;
  height: 50px;
  display: inline-block;
  .image-view {
    width: 100%;
    height: 100%;
    /deep/img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
  }
}
/deep/table {
  table-layout: fixed;
}
.modal-footer {
  text-align: right;
  button + button {
    margin-left: 8px;
  }
}
.label {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  padding-right: 0.4em;
  height: 32px;
  &::after {
    content: ":";
  }
}
.row ~ .row {
  margin-top: 10px;
}
.sub-title {
  font-size: 14px;
  font-weight: 700;
  margin-top: 10px;
}
.res-content {
  max-height: 260px;
  overflow-y: auto;
  margin-bottom: 10px;
}
.btn-grp button {
  width: 6.4em;
}
.tag-all {
  margin: 0;
}
/deep/ .ant-table-fixed-right {
  .ant-table-body-outer {
    margin-bottom: 0 !important;
  }
  .ant-table-body-inner {
    overflow-x: hidden;
  }
}
.log-content {
  max-height: 400px;
  overflow-y: auto;
  .user {
    color: #23aaf2;
    font-weight: 700;
  }
  .time {
    color: #f9be13;
    font-weight: 700;
  }
  .version {
    color: #0aedb2;
    font-weight: 700;
  }
  .ant-timeline-item:first-of-type {
    padding-top: 6px;
  }
}
</style>
src/pages/resourceManage/specification/pop.vue
New file
@@ -0,0 +1,176 @@
<template>
  <div
    ref="pop"
    :class="['pop', position]"
    :style="{
      left: x + 'px',
      top: y + 'px',
      display: visible ? 'block' : 'none',
    }"
  >
    <div class="inner">
      <desc-res :info="info"></desc-res>
    </div>
  </div>
</template>
<script>
import DescRes from "./rowRes";
export default {
  name: "",
  components: {
    DescRes,
  },
  props: {
    info: {
      type: Object,
      default() {
        return {};
      },
    },
    x: {
      type: Number,
      default: 0,
    },
    y: {
      type: Number,
      default: 0,
    },
    visible: {
      type: Boolean,
      default: false,
    },
    position: {
      type: String,
      default: "bottom",
    },
  },
  data() {
    return {};
  },
  methods: {
    onMouseenter() {
      this.$emit('update:visible', true);
    },
    onMouseleave() {
      this.$emit('update:visible', false);
    }
  },
  mounted() {
    this.$refs.pop.addEventListener('mouseenter', this.onMouseenter);
    this.$refs.pop.addEventListener('mouseleave', this.onMouseleave);
  },
  beforeDestroy() {
    this.$refs.pop.removeEventListener('mouseenter', this.onMouseenter);
    this.$refs.pop.removeEventListener('mouseleave', this.onMouseleave);
  }
};
</script>
<style lang="less" scoped>
.pop {
  position: absolute;
  z-index: 1;
  background: rgba(0, 0, 0, 0.6);
  width: 800px;
  padding: 10px;
  border-radius: 6px;
  /deep/ th,
  /deep/ td {
    border: 1px #fff solid;
    color: #fff;
  }
  &.top {
    transform: translate(-50%, -100%);
    margin-top: -10px;
    &::after {
      content: "";
      position: absolute;
      top: 100%;
      left: 50%;
      transform: translateX(-5px);
      display: inline-block;
      width: 0;
      height: 0;
      border-top: 10px solid rgba(0, 0, 0, 0.6);
      border-left: 5px solid transparent;
      border-right: 5px solid transparent;
    }
    &::before {
      content: "";
      position: absolute;
      top: 100%;
      left: 0;
      right: 0;
      display: inline-block;
      height: 10px;
      background: transparent;
    }
  }
  &.bottom {
    margin-top: 10px;
    transform: translate(-50%, 0%);
    &::after {
      content: "";
      position: absolute;
      bottom: 100%;
      left: 50%;
      transform: translateX(-5px);
      display: inline-block;
      width: 0;
      height: 0;
      border-bottom: 10px solid rgba(0, 0, 0, 0.6);
      border-left: 5px solid transparent;
      border-right: 5px solid transparent;
    }
    &::before {
      content: "";
      position: absolute;
      bottom: 100%;
      left: 0;
      right: 0;
      height: 10px;
      background: transparent;
    }
  }
  &.left {
    margin-left: -10px;
    transform: translate(-100%, -50%);
    &::after {
      content: "";
      position: absolute;
      left: 100%;
      top: 50%;
      transform: translateY(-5px);
      display: inline-block;
      width: 0;
      height: 0;
      border-left: 10px solid rgba(0, 0, 0, 0.6);
      border-top: 5px solid transparent;
      border-bottom: 5px solid transparent;
    }
  }
  &.right {
    margin-left: 10px;
    transform: translate(0%, -50%);
    &::after {
      content: "";
      position: absolute;
      right: 100%;
      top: 50%;
      transform: translateY(-5px);
      display: inline-block;
      width: 0;
      height: 0;
      border-right: 10px solid rgba(0, 0, 0, 0.6);
      border-top: 5px solid transparent;
      border-bottom: 5px solid transparent;
    }
  }
  .inner {
    max-height: 360px;
    overflow-y: auto;
  }
}
</style>
src/pages/resourceManage/specification/rowRes.vue
New file
@@ -0,0 +1,98 @@
<template>
  <div class="">
    <table class="table">
      <tbody>
        <tr>
          <th class="title" colspan="6">文件基本信息</th>
        </tr>
        <tr>
          <th class="col-1">文件名称</th>
          <td colspan="5">{{ info.fileName }}</td>
        </tr>
        <tr>
          <th class="col-1">软件类型</th>
          <td colspan="5">{{ info.type }}</td>
        </tr>
        <tr>
          <th class="col-1">文件版本</th>
          <td colspan="1">{{ info.version }}</td>
          <th class="col-1">基于版本</th>
          <td colspan="3">{{ info.basedVersion }}</td>
        </tr>
        <tr>
          <th class="col-1">负责人</th>
          <td colspan="1">{{ info.owner }}</td>
          <th class="col-1">归档日期</th>
          <td colspan="3">{{ info.filingDate }}</td>
        </tr>
        <tr>
          <th class="title" colspan="6">规格书适用机型</th>
        </tr>
        <tr >
          <th class="col-1">物料编码</th>
          <td colspan="1">{{ info.applyMaterialCode }}</td>
          <th class="col-1">规格型号</th>
          <td colspan="1">{{ info.applyModel }}</td>
          <th class="col-1">定制单号</th>
          <td colspan="1">{{ info.applyCustomCode }}</td>
        </tr>
        <tr>
          <th class="col-1">发布说明</th>
          <td colspan="5">{{ info.releaseNotes }}</td>
        </tr>
      </tbody>
    </table>
  </div>
</template>
<script>
export default {
  name: "",
  props: {
    info: {
      type: Object,
      default() {
        return {};
      },
    },
  },
  data() {
    return {
    };
  },
  computed: {
  },
  watch: {
  },
  methods: {
  },
  mounted() {
  },
};
</script>
<style lang="less" scoped>
.table {
  width: 100%;
  // table-layout: fixed;
  border-collapse: collapse;
  th,
  td {
    border: 1px #fff solid;
    padding: 4px;
  }
  td {
    color: #fff;
  }
  .title {
    font-weight: 900;
    padding-left: 2em;
    font-style: italic;
  }
}
.col-1 {
  // word-break:break-all;
  width: 6.4em;
}
</style>
src/pages/resourceManage/specification/versionList.vue
New file
@@ -0,0 +1,130 @@
<template>
  <div class="posR">
    <div class="inner">
      <a-card class="main">
        <!-- 列表 -->
        <div class="contain">
          <div
            :class="['item', { selected: currentV == item.id }]"
            v-for="(item, idx) in list"
            :key="'item_' + idx"
            @click="selectHandle(item)"
          >
            <span :class="['status', { actived: item.lockFlag == 0 }]"></span>
            <div class="version">{{ item.version }}</div>
          </div>
        </div>
      </a-card>
    </div>
  </div>
</template>
<script>
export default {
  name: "",
  props: {
    list: {
      type: Array,
      default() {
        return [];
      },
    },
  },
  computed: {
    // data() {
    //   return this.list.filter((v) => {
    //     const reg = new RegExp(this.keyword, "i");
    //     return reg.test(v.subModel);
    //   });
    // },
  },
  watch: {
    list(n) {
      if (n.length) {
        this.selectHandle(this.list[0]);
      }
    },
  },
  data() {
    return {
      // keyword: '',
      currentV: "-1",
    };
  },
  components: {},
  methods: {
    selectHandle(item) {
      this.currentV = item.id;
      this.$emit("select", item);
    },
  },
  mounted() {},
};
</script>
<style scoped lang="less">
.posR {
  position: relative;
}
.inner {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  bottom: 0;
}
.main {
  height: 100%;
  /deep/.ant-card-body {
    height: 100%;
    display: flex;
    flex-direction: column;
  }
}
.contain {
  margin-top: 8px;
  border: 1px solid #e8e8e8;
  flex: 1;
  overflow: auto;
  padding: 0 4px;
}
.item {
  cursor: pointer;
  box-shadow: 0px 4px 5px -2px #000;
  padding: 6px 0;
  border-radius: 4px;
  display: flex;
  flex-direction: row;
  align-items: center;
  & + .item {
    margin-top: 4px;
  }
  &:hover {
    transform: scale(0.98, 0.9);
    box-shadow: 0px 2px 5px -2px #000;
    background: #f0f0f0;
  }
  &.selected {
    transform: scale(0.98, 0.9);
    box-shadow: 0px 2px 5px -2px #000;
    color: #13c2c2;
    font-weight: bold;
  }
  .version {
    flex: 1;
  }
  .status {
    display: inline-block;
    width: 10px;
    height: 10px;
    background: #aaa;
    border-radius: 50%;
    margin-left: 10px;
    margin-right: 20px;
    &.actived {
      background: #00ff79;
    }
  }
}
</style>
src/router/config.js
@@ -151,10 +151,24 @@
              name: 'sop历史版本',
              meta: {
                invisible: true,
                highlight: '/resource/sop-history'
                highlight: '/resource/sop-file'
              },
              component: () => import('@/pages/resourceManage/sopFile/sop-history'),
            },
            {
              path: 'specification',
              name: '技术规格书',
              component: () => import('@/pages/resourceManage/specification'),
            },
            {
              path: 'specification-history',
              name: '技术规格书历史版本',
              meta: {
                invisible: true,
                highlight: '/resource/specification'
              },
              component: () => import('@/pages/resourceManage/specification/history'),
            },
          ]
        },
        {