From 36704dbc78007ca051633aa015c1094de3a2630a Mon Sep 17 00:00:00 2001
From: he wei <858544502@qq.com>
Date: 星期五, 12 一月 2024 18:01:20 +0800
Subject: [PATCH] UA 技术规格书

---
 src/pages/resourceManage/specification/rowRes.vue      |   98 ++
 src/pages/resourceManage/product/list.vue              |    7 
 src/pages/resourceManage/specification/versionList.vue |  130 +++
 src/pages/resourceManage/specification/history.vue     |  424 ++++++++++++
 src/pages/resourceManage/specification/list.vue        |  996 +++++++++++++++++++++++++++++
 src/pages/resourceManage/specification/pop.vue         |  176 +++++
 src/pages/resourceManage/specification/index.js        |    2 
 src/router/config.js                                   |   16 
 src/pages/resourceManage/specification/apis.js         |   86 ++
 src/pages/resourceManage/specification/descRes.vue     |   94 ++
 10 files changed, 2,026 insertions(+), 3 deletions(-)

diff --git a/src/pages/resourceManage/product/list.vue b/src/pages/resourceManage/product/list.vue
index 29269f8..cc41d4c 100644
--- a/src/pages/resourceManage/product/list.vue
+++ b/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":
diff --git a/src/pages/resourceManage/specification/apis.js b/src/pages/resourceManage/specification/apis.js
new file mode 100644
index 0000000..f9068ad
--- /dev/null
+++ b/src/pages/resourceManage/specification/apis.js
@@ -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 },
+  });
+};
+
+/**
+ * 瑙f瀽瑙勬牸涔﹁鏄�
+ * multipartFile
+ * @returns
+ */
+export const excelParse = (data) => {
+  return axios({
+    method: "POST",
+    url: "technicalSpecification/excelParse",
+    headers: {
+      "Content-Type": "multipart/form-data",
+    },
+    data,
+  });
+};
+
+/**
+ * 瑙f瀽瑙勬牸涔﹁鏄�
+ * 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,
+  });
+};
\ No newline at end of file
diff --git a/src/pages/resourceManage/specification/descRes.vue b/src/pages/resourceManage/specification/descRes.vue
new file mode 100644
index 0000000..603b87f
--- /dev/null
+++ b/src/pages/resourceManage/specification/descRes.vue
@@ -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>
diff --git a/src/pages/resourceManage/specification/history.vue b/src/pages/resourceManage/specification/history.vue
new file mode 100644
index 0000000..5f0a2da
--- /dev/null
+++ b/src/pages/resourceManage/specification/history.vue
@@ -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()"
+                    >瑙i攣</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 ? "閿佸畾" : "瑙i攣" }}浜嗙増鏈�
+              <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>
diff --git a/src/pages/resourceManage/specification/index.js b/src/pages/resourceManage/specification/index.js
new file mode 100644
index 0000000..37bd0be
--- /dev/null
+++ b/src/pages/resourceManage/specification/index.js
@@ -0,0 +1,2 @@
+import list from './list';
+export default list;
\ No newline at end of file
diff --git a/src/pages/resourceManage/specification/list.vue b/src/pages/resourceManage/specification/list.vue
new file mode 100644
index 0000000..ec19f15
--- /dev/null
+++ b/src/pages/resourceManage/specification/list.vue
@@ -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)"
+                    >瑙i攣</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">璇存槑鏂囦欢瑙f瀽缁撴灉</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 ? "閿佸畾" : "瑙i攣" }}浜嗙増鏈�
+              <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("璇存槑鏂囦欢瑙f瀽寮傚父");
+        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("瑙勬牸涔︽枃浠剁被鍨嬩笉姝g‘");
+        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("璇存槑鏂囦欢瑙f瀽寮傚父");
+        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("瑙f瀽澶辫触");
+      //     }
+      //     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("瑙f瀽鎴愬姛");
+          } 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>
diff --git a/src/pages/resourceManage/specification/pop.vue b/src/pages/resourceManage/specification/pop.vue
new file mode 100644
index 0000000..9d06876
--- /dev/null
+++ b/src/pages/resourceManage/specification/pop.vue
@@ -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>
diff --git a/src/pages/resourceManage/specification/rowRes.vue b/src/pages/resourceManage/specification/rowRes.vue
new file mode 100644
index 0000000..437467a
--- /dev/null
+++ b/src/pages/resourceManage/specification/rowRes.vue
@@ -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>
\ No newline at end of file
diff --git a/src/pages/resourceManage/specification/versionList.vue b/src/pages/resourceManage/specification/versionList.vue
new file mode 100644
index 0000000..93c4d11
--- /dev/null
+++ b/src/pages/resourceManage/specification/versionList.vue
@@ -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>
diff --git a/src/router/config.js b/src/router/config.js
index 2da26dc..424bb41 100644
--- a/src/router/config.js
+++ b/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'),
+            },
           ]
         },
         {

--
Gitblit v1.9.1