研发图纸文件管理系统-前端项目
he wei
2022-09-17 4fb882e6dff547547ca49233b0f7d67d2b253ab6
UA 按上次会议补充需求做相应修改
17个文件已修改
2个文件已添加
1197 ■■■■ 已修改文件
src/assets/js/const/const_permits.js 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/js/tools/offset.js 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/datetimeRange/datetimeRange.vue 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/table/advance/SearchArea.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/transition/PageToggleTransition.vue 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/config/default/setting.config.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/components/filesTable.vue 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/resourceManage/materialsCenter/apis.js 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/resourceManage/materialsCenter/editLink.vue 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/resourceManage/materialsCenter/history/history.vue 311 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/resourceManage/materialsCenter/history/list.vue 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/resourceManage/product/apis.js 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/resourceManage/product/details/details.vue 124 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/resourceManage/product/list.vue 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/resourceManage/product/prodUpload.vue 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/resourceManage/software/apis.js 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/resourceManage/software/descRes.vue 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/resourceManage/software/list.vue 241 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/resourceManage/software/pop.vue 142 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/js/const/const_permits.js
@@ -3,9 +3,14 @@
  downloadDoc: 10002,
  downloadBom: 10003,
  downloadOther: 10004,
  viewDoc: 10006,
  viewOther: 10007,
  
  uploadBom: 10009,
  uploadSoftware: 10008,
  uploadBom: 10009,
  lockBom: 10010,
  lockOther: 10011
  lockOther: 10011,
  lockSoftware: 10012
}
src/assets/js/tools/offset.js
New file
@@ -0,0 +1,12 @@
export default function offset(el) {
  let left = 0, top = 0;
  let elPar = el.offsetParent;
  left += el.offsetLeft;
  top += el.offsetTop;
  while (elPar) {
    left += elPar.offsetLeft;
    top += elPar.offsetTop;
    elPar = elPar.offsetParent;
  }
  return { left, top };
}
src/components/datetimeRange/datetimeRange.vue
@@ -1,6 +1,7 @@
<template>
  <div>
    <a-date-picker
      :getCalendarContainer="getCalendarContainer"
      v-model="startValue"
      :disabled-date="disabledStartDate"
      show-time
@@ -13,6 +14,7 @@
    />
    -
    <a-date-picker
      :getCalendarContainer="getCalendarContainer"
      v-model="endValue"
      :disabled-date="disabledEndDate"
      show-time
@@ -35,26 +37,26 @@
      endOpen: false,
    };
  },
  props: ['value'],
  props: ["value", "getCalendarContainer"],
  model: {
    prop: 'value',
    event: 'change'
    prop: "value",
    event: "change",
  },
  watch: {
    value(newV) {
      this.resultData = newV;
    }
    },
  },
  computed: {
    resultData: {
      get () {
      get() {
        return [this.startValue, this.endValue];
      },
      set (value) {
      set(value) {
        this.startValue = value[0];
        this.endValue = value[1];
      }
    }
      },
    },
  },
  methods: {
    disabledStartDate(startValue) {
@@ -84,8 +86,8 @@
      // }
    },
    change() {
      this.$emit('change', this.resultData);
    }
      this.$emit("change", this.resultData);
    },
  },
};
</script>
src/components/table/advance/SearchArea.vue
@@ -39,7 +39,7 @@
          {{col.title}}:
        </template>
        <slot v-else-if="col.slots && col.slots.title" :name="col.slots.title"></slot>
        <datetime-range v-model="col.search.value" @change="onDateRangeChange(col)"></datetime-range>
        <datetime-range v-model="col.search.value" :getCalendarContainer="() => $refs.root" @change="onDateRangeChange(col)"></datetime-range>
      </div>
      <div v-else-if="col.dataType === 'select'" :class="['title', {active: col.search.value !== undefined}]">
        <template v-if="col.title">
src/components/transition/PageToggleTransition.vue
@@ -6,7 +6,7 @@
  >
    <slot></slot>
  </transition>
  <div v-else><slot></slot></div>
  <div class="page-content" v-else><slot></slot></div>
</template>
<script>
@@ -89,6 +89,9 @@
    animation-duration: 0.8s !important;
    width: calc(100%) !important;
  }
  .page-content {
    height: 100%;
  }
  .page-toggle-enter{
  }
  .page-toggle-leave-to{
src/config/default/setting.config.js
@@ -23,7 +23,7 @@
  showPageTitle: true,                  //是否显示页面标题(PageLayout 布局中的页面标题),true:显示,false:不显示
  filterMenu: true,                    //根据权限过滤菜单,true:过滤,false:不过滤
  animate: {                            //动画设置
    disabled: false,                    //禁用动画,true:禁用,false:启用
    disabled: true,                    //禁用动画,true:禁用,false:启用
    name: 'bounce',                     //动画效果,支持的动画效果可参考 ./animate.config.js
    direction: 'left'                   //动画方向,切换页面时动画的方向,参考 ./animate.config.js
  },
src/pages/components/filesTable.vue
@@ -13,8 +13,8 @@
    >
      <template slot="action" slot-scope="text, record">
        <div v-if="record.url">
          <a v-if="!record.lockFlag" @click="view(record)">预览</a>
          <template v-if="canDownloadOther && !record.lockFlag">
          <a v-if="!record.lockFlag && ((record.fileType == 'dwg' && canViewDoc) || (record.fileType != 'dwg' && canViewOther))" @click="view(record)">预览</a>
          <template v-if="((record.fileType == 'dwg' && canDownloadDoc) || (record.fileType != 'dwg' && canDownloadOther)) && !record.lockFlag">
            <a-divider type="vertical"></a-divider>
            <a @click="downloadLog(record)">下载</a>
          </template>
@@ -251,9 +251,18 @@
    canDownloadOther() {
      return checkPermit(PERMITS.downloadOther, this.permits);
    },
    canDownloadDoc() {
      return checkPermit(PERMITS.downloadDoc, this.permits);
    },
    canLockOther() {
      return checkPermit(PERMITS.lockOther, this.permits);
    },
    canViewOther() {
      return checkPermit(PERMITS.viewOther, this.permits);
    },
    canViewDoc() {
      return checkPermit(PERMITS.viewDoc, this.permits);
    },
  },
  mounted() {},
};
src/pages/resourceManage/materialsCenter/apis.js
@@ -134,4 +134,15 @@
    url: "attachLock/updateAttachLock",
    data
  })
}
/**
 * 物料图纸对比
 * @returns
 */
export const dwgCompare = (materialId, materialId2) => {
  return axios({
    method: "GET",
    url: "material/dwgCompare",
    params: { materialId, materialId2 }
  })
}
src/pages/resourceManage/materialsCenter/editLink.vue
@@ -159,7 +159,7 @@
      selectedRows: [],
      columns: [
        {
          title: "母料编号",
          title: "产品编号",
          dataIndex: "parentCode",
          key: "parentCode",
          align: "center",
@@ -173,12 +173,18 @@
          width: 80,
        },
        {
          title: "母料名称",
          title: "产品名称",
          dataIndex: "parentName",
          key: "parentName",
          align: "center",
          width: 80,
        },
        {
          title: "产品型号",
          dataIndex: "parentModel",
          align: "center",
          width: 80,
        }
      ],
      userList: [],
      info: {
src/pages/resourceManage/materialsCenter/history/history.vue
@@ -1,88 +1,95 @@
<template>
  <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>
          <div class="title">物料所属产品</div>
          <a-table
            ref="aTable"
            size="small"
            :scroll="{ y: 300 }"
            bordered
            :columns="columns"
            :data-source="dataSource"
            :pagination="false"
            rowKey="id"
          >
          </a-table>
        </a-card>
      </a-layout-header>
      <a-layout-content>
        <div class="wraper" ref="wraper">
          <div class="inner">
            <a-spin :spinning="spinning" tip="拼命加载中...">
              <a-descriptions title="详情" bordered>
                <a-descriptions-item label="料号">{{
                  record.subCode
                }}</a-descriptions-item>
                <a-descriptions-item label="型号">{{
                  record.subModel
                }}</a-descriptions-item>
                <a-descriptions-item label="名称">{{
                  record.subName
                }}</a-descriptions-item>
                <a-descriptions-item label="单位">{{
                  record.unit
                }}</a-descriptions-item>
                <a-descriptions-item label="生产商">{{
                  record.producer
                }}</a-descriptions-item>
                <a-descriptions-item label="封装类型/材质">{{
                  record.material
                }}</a-descriptions-item>
                <a-descriptions-item label="元件编号/料厚">{{
                  record.thickness
                }}</a-descriptions-item>
                <a-descriptions-item label="表面处理/物料详情">{{
                  record.surfaceDetail
                }}</a-descriptions-item>
                <a-descriptions-item label="创建日期">{{
                  record.createDate
                }}</a-descriptions-item>
                <a-descriptions-item label="更新日期">{{
                  record.updateDate
                }}</a-descriptions-item>
                <a-descriptions-item label="备注">{{
                  record.notes
                }}</a-descriptions-item>
                <a-descriptions-item label="图片">
                  <div class="img-wraper">
                    <image-view
                      v-if="record && record.pictureUrl"
                      :url="webUrl + record.pictureUrl"
                    ></image-view>
                  </div>
                </a-descriptions-item>
              </a-descriptions>
            </a-spin>
  <div class="main">
    <a-layout class="main">
      <a-layout-sider width="260">
        <list
          class="list"
          :list="versionList"
          @select="selectChanged"
          @diff="diff"
        ></list>
      </a-layout-sider>
      <a-layout>
        <a-layout-header>
          <a-card>
            <div class="title">物料所属产品</div>
            <a-table
              ref="aTable"
              size="small"
              :scroll="{ y: 300 }"
              bordered
              :columns="columns"
              :data-source="dataSource"
              :pagination="false"
              rowKey="id"
            >
            </a-table>
          </a-card>
        </a-layout-header>
        <a-layout-content>
          <div class="wraper" ref="wraper">
            <div class="inner">
              <a-spin :spinning="spinning" tip="拼命加载中...">
                <a-descriptions title="详情" bordered>
                  <a-descriptions-item label="料号">{{
                    record.subCode
                  }}</a-descriptions-item>
                  <a-descriptions-item label="型号">{{
                    record.subModel
                  }}</a-descriptions-item>
                  <a-descriptions-item label="名称">{{
                    record.subName
                  }}</a-descriptions-item>
                  <a-descriptions-item label="单位">{{
                    record.unit
                  }}</a-descriptions-item>
                  <a-descriptions-item label="生产商">{{
                    record.producer
                  }}</a-descriptions-item>
                  <a-descriptions-item label="封装类型/材质">{{
                    record.material
                  }}</a-descriptions-item>
                  <a-descriptions-item label="元件编号/料厚">{{
                    record.thickness
                  }}</a-descriptions-item>
                  <a-descriptions-item label="表面处理/物料详情">{{
                    record.surfaceDetail
                  }}</a-descriptions-item>
                  <a-descriptions-item label="创建日期">{{
                    record.createDate
                  }}</a-descriptions-item>
                  <a-descriptions-item label="更新日期">{{
                    record.updateDate
                  }}</a-descriptions-item>
                  <a-descriptions-item label="备注">{{
                    record.notes
                  }}</a-descriptions-item>
                  <a-descriptions-item label="图片">
                    <div class="img-wraper">
                      <image-view
                        v-if="record && record.pictureUrl"
                        :url="webUrl + record.pictureUrl"
                      ></image-view>
                    </div>
                  </a-descriptions-item>
                </a-descriptions>
              </a-spin>
            </div>
          </div>
        </div>
      </a-layout-content>
      <a-layout-footer>
        <a-card>
          <template v-if="dataSource.length">
            <!-- <a-popover title="" v-if="otherDoc.length" trigger="click">
              <div class="file-list" slot="content">
                <files-table :list="otherDoc" :info="info"></files-table>
              </div>
            </a-popover> -->
            <a-button v-if="otherDoc.length" type="primary" @click="showOtherDoc">其他附件</a-button>
          </template>
        </a-card>
      </a-layout-footer>
        </a-layout-content>
        <a-layout-footer>
          <a-card>
            <template v-if="dataSource.length">
              <a-button
                v-if="otherDoc.length"
                type="primary"
                @click="showOtherDoc"
                >其他附件</a-button
              >
            </template>
          </a-card>
        </a-layout-footer>
      </a-layout>
    </a-layout>
    <!-- 其他附件列表弹窗 -->
    <a-modal
@@ -95,7 +102,29 @@
    >
      <files-table :list="otherDoc" :info="info"></files-table>
    </a-modal>
  </a-layout>
    <!-- 差异 -->
    <a-modal
      :visible="diffVisible"
      title="差异"
      :destroyOnClose="true"
      :width="900"
      :footer="null"
      @cancel="diffCancel"
    >
      <div class="diff-content" ref="imgWrap">
        <div class="img-wrap">
          <div class="img-wrap-inner">
            <img v-if="diffImg" :src="diffImg" />
          </div>
        </div>
        <div class="footer">
          <div class="btn" @click="toggleScreen">
            {{ fullScreen ? "还原" : "全屏" }}
          </div>
        </div>
      </div>
    </a-modal>
  </div>
</template>
<script>
@@ -105,13 +134,16 @@
import List from "./list";
import getWebUrl from "@/assets/js/tools/getWebUrl";
import { getVersions, getMaterialById } from "../apis";
import { getVersions, getMaterialById, dwgCompare } from "../apis";
import { dwgReview } from "@/pages/workplace/apis";
export default {
  name: "",
  data() {
    return {
      fullScreen: false,
      diffVisible: false,
      diffImg: "",
      otherDocVisible: false,
      diffShow: false,
      diffData: [],
@@ -188,7 +220,11 @@
    DiffList,
  },
  computed: {},
  watch: {},
  watch: {
    // fullScreen() {
    //   this.toggleScreen();
    // },
  },
  methods: {
    getVersions() {
      getVersions(this.rootModel).then((res) => {
@@ -256,10 +292,70 @@
    otherDocCancel() {
      this.otherDocVisible = false;
    },
    diff(data) {
      // console.log("比较两个版本", data);
      let params = data.map((v) => v * 1).sort();
      dwgCompare(...params).then((res) => {
        const { code, data, data2, msg } = res.data;
        if (code && data) {
          this.diffImg = "data:image/png;base64," + data2;
          this.diffVisible = true;
        } else {
          this.$message.error(msg);
        }
      });
    },
    diffCancel() {
      this.diffVisible = false;
    },
    fullScreenListener(e) {
      const el = this.$refs.imgWrap;
      if (e.target === el) {
        this.fullScreen = !this.fullScreen;
        if (this.fullScreen) {
          el.classList.add("full");
        } else {
          el.classList.remove("full");
        }
      }
    },
    toggleScreen() {
      let el = this.$refs.imgWrap;
      if (this.fullScreen) {
        if (document.exitFullscreen) {
          document.exitFullscreen();
        } else if (document.webkitCancelFullScreen) {
          document.webkitCancelFullScreen();
        }
        el.classList.remove("full");
      } else {
        el.classList.add("full");
        if (el.requestFullscreen) {
          el.requestFullscreen();
          return true;
        } else if (el.webkitRequestFullScreen) {
          el.webkitRequestFullScreen();
          return true;
        }
      }
    },
  },
  mounted() {
    this.getVersions();
    document.addEventListener("fullscreenchange", this.fullScreenListener);
    document.addEventListener(
      "webkitfullscreenchange",
      this.fullScreenListener
    );
  },
  beforeDestroy() {
    document.removeEventListener("fullscreenchange", this.fullScreenListener);
    document.removeEventListener(
      "webkitfullscreenchange",
      this.fullScreenListener
    );
  },
};
</script>
@@ -362,7 +458,46 @@
    line-height: 1.5;
  }
}
.file-list {
  width: 600px;
.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;
    }
  }
}
</style>
src/pages/resourceManage/materialsCenter/history/list.vue
@@ -4,6 +4,12 @@
      <a-card class="main">
        <!-- <a-input v-model="keyword" placeholder="输入关键字过滤" /> -->
        <!-- 列表 -->
        <a-button
          type="primary"
          :disabled="selectedKeys.length != 2"
          @click="diff"
          >比较差异</a-button
        >
        <div class="contain">
          <div
            :class="['item', { selected: currentV == item.id }]"
@@ -12,6 +18,11 @@
            @click="selectHandle(item)"
          >
            <div class="version">{{ item.subModel }}</div>
            <a-checkbox
              @click.stop
              :checked="item.selected"
              @change="(value) => checkChange(value, item)"
            ></a-checkbox>
          </div>
        </div>
      </a-card>
@@ -44,6 +55,10 @@
  },
  watch: {
    list(n) {
      this.selectedKeys = this.selectedKeys.filter((v) => n.some((val) => val.id == v));
      this.list.forEach((v) => {
        v.selected = this.selectedKeys.some((val) => val == v.id);
      });
      if (n.length) {
        this.selectHandle(this.list[0]);
      }
@@ -53,6 +68,7 @@
    return {
      // keyword: '',
      currentV: "-1",
      selectedKeys: [],
    };
  },
  components: {},
@@ -61,6 +77,27 @@
      this.currentV = item.id;
      this.$emit("select", item);
    },
    checkChange(value, item) {
      const {
        target: { checked },
      } = value;
      // console.log(value, checked, item);
      if (checked) {
        this.selectedKeys.push(item.id);
        if (this.selectedKeys.length > 2) {
          this.selectedKeys.shift();
        }
      } else {
        let idx = this.selectedKeys.indexOf(item.id);
        this.selectedKeys.splice(idx, 1);
      }
      this.list.forEach((v) => {
        v.selected = this.selectedKeys.some((val) => val == v.id);
      });
    },
    diff() {
      this.$emit("diff", this.selectedKeys);
    },
  },
  mounted() {},
src/pages/resourceManage/product/apis.js
@@ -91,4 +91,15 @@
    url: "productLockLog/listByParentCodeAndCustomCode",
    params
  })
}
/**
 * 根据产品id查询被锁定的物料dwg和产品丝印
 * @returns
 */
export const getLockedList = (productId) => {
  return axios({
    method: "GET",
    url: "product/getLockedByProductId",
    params: { productId }
  })
}
src/pages/resourceManage/product/details/details.vue
@@ -56,12 +56,16 @@
                  </template>
                  <template slot="action" slot-scope="text, record">
                    <div v-if="record.dwgUrl">
                      <a @click="dwgReview(record.dwgUrl)">预览</a>
                      <a-divider type="vertical"></a-divider>
                      <a @click="downloadLog(record)">下载</a>
                      <a v-if="canViewDoc" @click="dwgReview(record.dwgUrl)">预览</a>
                      <a-divider v-if="canViewDoc && canDownloadDoc" type="vertical"></a-divider>
                      <a v-if="canDownloadDoc" @click="downloadLog(record)">下载</a>
                    </div>
                    <template
                      v-if="record.softwares.length && canDownloadSoftware"
                      v-if="
                        record.softwares &&
                        record.softwares.length &&
                        canDownloadSoftware
                      "
                    >
                      <a-divider
                        v-if="record.dwgUrl"
@@ -80,7 +84,7 @@
                            :row-key="(record1, index) => index"
                          >
                            <template slot="action" slot-scope="text, record1">
                              <a @click="downloadLog(record1)">下载</a>
                              <a v-if="canDownloadSoftware" @click="downloadLog(record1)">下载</a>
                            </template>
                          </a-table>
                        </div>
@@ -118,7 +122,7 @@
            <a-button
              v-if="canDownloadBom && currentVersion.enabled"
              type="primary"
              @click="zipDownload"
              @click="checkLock"
              >bom下载</a-button
            >
            <a-button
@@ -213,6 +217,44 @@
    >
      <files-table :list="otherDoc" :info="info"></files-table>
    </a-modal>
    <!-- 锁定清单 -->
    <a-modal
      :visible="lockListVisible"
      :width="800"
      title="下载提示 (有锁定文件)"
      :destroyOnClose="true"
      @cancel="lockListCancel"
      @ok="lockListOk"
    >
      <!-- bom清单中存在锁定图纸的物料 -->
      <template v-if="bomLockList.length">
        <div class="table-title">bom清单中存在锁定图纸的物料</div>
        <a-table
          size="small"
          :scroll="{ y: 150 }"
          bordered
          :columns="bomLockColumns"
          :data-source="bomLockList"
          :pagination="false"
          :expandRowByClick="true"
          :row-key="(record, index) => index"
        ></a-table>
      </template>
      <!-- 其他附件中存在锁定文件 -->
      <template v-if="otherLockList.length">
        <div class="table-title">其他附件中存在锁定文件</div>
        <a-table
          size="small"
          :scroll="{ y: 150 }"
          bordered
          :columns="otherLockColumns"
          :data-source="otherLockList"
          :pagination="false"
          :expandRowByClick="true"
          :row-key="(record, index) => index"
        ></a-table>
      </template>
    </a-modal>
  </a-layout>
</template>
@@ -231,7 +273,7 @@
  getBomHistoryAndMaterial,
  compare,
} from "./apis";
import { setpHistoryEnable } from "../apis";
import { setpHistoryEnable, getLockedList } from "../apis";
import { dwgReview } from "@/pages/workplace/apis";
import { downloadLog } from "@/pages/system/logs/apis";
import { mapGetters } from "vuex";
@@ -242,6 +284,9 @@
  mixins: [WSMixin],
  data() {
    return {
      lockListVisible: false,
      bomLockList: [],
      otherLockList: [],
      otherDocVisible: false,
      reasonVisible: false,
      reason: "",
@@ -403,6 +448,35 @@
          scopedSlots: { customRender: "action" },
        },
      ],
      bomLockColumns: [
        {
          title: "子件名称",
          dataIndex: "subName",
          align: "center",
        },
        {
          title: "文件名称",
          dataIndex: "attachName",
          align: "center",
        },
        {
          title: "锁定说明",
          dataIndex: "localReason",
          align: "center",
        }
      ],
      otherLockColumns: [
        {
          title: "文件名称",
          dataIndex: "attachName",
          align: "center",
        },
        {
          title: "锁定说明",
          dataIndex: "localReason",
          align: "center",
        }
      ]
    };
  },
  components: {
@@ -419,6 +493,12 @@
    },
    canDownloadBom() {
      return checkPermit(PERMITS.downloadBom, this.permits);
    },
    canDownloadDoc() {
      return checkPermit(PERMITS.downloadDoc, this.permits);
    },
    canViewDoc() {
      return checkPermit(PERMITS.viewDoc, this.permits);
    },
    canLockBom() {
      return checkPermit(PERMITS.lockBom, this.permits);
@@ -558,6 +638,31 @@
      link.click();
      document.body.removeChild(link);
      downloadLog(parentCode, subModel);
    },
    checkLock() {
      getLockedList(this.currentVersion.id).then((res) => {
        const { code, data, data2, data3 } = res.data;
        if (code) {
          if (data) {
            // 有锁定
            this.bomLockList = data2;
            this.otherLockList = data3;
            this.lockListVisible = true;
          } else {
            // 没有锁定
            this.zipDownload();
          }
        } else {
          this.$message.error("查询锁定清单出错");
        }
      });
    },
    lockListCancel() {
      this.lockListVisible = false;
    },
    lockListOk() {
      this.lockListVisible = false;
      this.zipDownload();
    },
    zipDownload() {
      // const { parentCode, currentVersion } = this;
@@ -760,7 +865,8 @@
    background: #ffbcc9;
  }
}
.file-list {
  width: 600px;
.table-title {
  font-weight: 700;
  color: #13c2c2;
}
</style>
src/pages/resourceManage/product/list.vue
@@ -62,7 +62,7 @@
                    v-if="canDownloadBom"
                    :disabled="record.version == -1"
                    type="primary"
                    @click="download(record)"
                    @click="checkLock(record)"
                    >下载</a-button
                  >
                  <a-button
@@ -286,15 +286,54 @@
            :color="item.lockFlag == 1 ? 'red' : 'green'"
          >
            <div>
              <span class="user">{{ item.owner }}</span
              > 在 <span class="time">{{ item.createTime }}</span
              > {{ item.lockFlag ? "锁定" : "激活" }}了版本 <span class="version">{{ item.versionTime }}</span>
              <span class="user">{{ item.owner }}</span> 在
              <span class="time">{{ item.createTime }}</span>
              {{ item.lockFlag ? "锁定" : "激活" }}了版本
              <span class="version">{{ item.versionTime }}</span>
            </div>
            <div>操作原因: {{ item.reason ? item.reason : "无" }}</div>
          </a-timeline-item>
        </a-timeline>
        <a-empty v-else />
      </div>
    </a-modal>
    <!-- 锁定清单 -->
    <a-modal
      :visible="lockListVisible"
      :width="800"
      title="下载提示 (有锁定文件)"
      :destroyOnClose="true"
      @cancel="lockListCancel"
      @ok="lockListOk"
    >
      <!-- bom清单中存在锁定图纸的物料 -->
      <template v-if="bomLockList.length">
        <div class="table-title">bom清单中存在锁定图纸的物料</div>
        <a-table
          size="small"
          :scroll="{ y: 150 }"
          bordered
          :columns="bomLockColumns"
          :data-source="bomLockList"
          :pagination="false"
          :expandRowByClick="true"
          :row-key="(record, index) => index"
        ></a-table>
      </template>
      <!-- 其他附件中存在锁定文件 -->
      <template v-if="otherLockList.length">
        <div class="table-title"> 其他附件中存在锁定文件</div>
        <a-table
          size="small"
          :scroll="{ y: 150 }"
          bordered
          :columns="otherLockColumns"
          :data-source="otherLockList"
          :pagination="false"
          :expandRowByClick="true"
          :row-key="(record, index) => index"
        ></a-table>
      </template>
    </a-modal>
  </div>
</template>
@@ -306,7 +345,7 @@
import DrawUpload from "@/pages/components/drawUpload";
import getWebUrl from "@/assets/js/tools/getWebUrl";
import { addProduct, downloadBom, getAllProducts, getLogList } from "./apis";
import { addProduct, downloadBom, getAllProducts, getLogList, getLockedList } from "./apis";
import { productSoftwareSubmit } from "../software/apis";
import { zipParse } from "@/pages/workplace/myDraw/apis";
import { mapGetters } from "vuex";
@@ -315,6 +354,7 @@
import createWs from "@/assets/js/websocket";
import DiffList from "@/pages/components/diffList";
import ApiTable from '../../../components/table/api/ApiTable.vue';
const WSMixin = createWs("product");
export default {
@@ -322,6 +362,10 @@
  mixins: [WSMixin],
  data() {
    return {
      lockListVisible: false,
      currentObj: null,
      bomLockList: [],
      otherLockList: [],
      logVisible: false,
      logList: [],
      fromProd: undefined,
@@ -415,6 +459,13 @@
        //   width: 160,
        // },
        {
          title: "子件编码",
          dataIndex: "subCode",
          align: "center",
          searchAble: true,
          visible: false,
        },
        {
          title: "版本时间",
          dataIndex: "versionTime",
          key: "versionTime",
@@ -434,6 +485,35 @@
        },
      ],
      dataSource: [],
      bomLockColumns: [
        {
          title: "子件名称",
          dataIndex: "subName",
          align: "center",
        },
        {
          title: "文件名称",
          dataIndex: "attachName",
          align: "center",
        },
        {
          title: "锁定说明",
          dataIndex: "localReason",
          align: "center",
        }
      ],
      otherLockColumns: [
        {
          title: "文件名称",
          dataIndex: "attachName",
          align: "center",
        },
        {
          title: "锁定说明",
          dataIndex: "localReason",
          align: "center",
        }
      ]
    };
  },
  components: {
@@ -441,7 +521,7 @@
    ChangeParts,
    ProdUpload,
    DrawUpload,
    DiffList,
    DiffList
  },
  methods: {
    onSearch(conditions, searchOptions) {
@@ -498,6 +578,32 @@
      console.log(obj, 99);
      this.editObj = obj;
      this.editShow = true;
    },
    checkLock(obj) {
      getLockedList(obj.id).then((res) => {
        const {code, data, data2, data3} = res.data;
        if (code) {
          if (data) {
            // 有锁定
            this.currentObj = obj;
            this.bomLockList = data2;
            this.otherLockList = data3;
            this.lockListVisible = true;
          } else {
            // 没有锁定
            this.download(obj);
          }
        } else {
          this.$message.error('查询锁定清单出错');
        }
      });
    },
    lockListCancel() {
      this.lockListVisible = false;
    },
    lockListOk() {
      this.lockListVisible = false;
      this.download(this.currentObj);
    },
    download(obj) {
      const { id, version } = obj;
@@ -834,9 +940,9 @@
    canUploadBom() {
      return checkPermit(PERMITS.uploadBom, this.permits);
    },
    canUploadSoftware() {
      return checkPermit(PERMITS.uploadSoftware, this.permits);
    },
    // canUploadSoftware() {
    //   return checkPermit(PERMITS.uploadSoftware, this.permits);
    // },
    canDownloadBom() {
      return checkPermit(PERMITS.downloadBom, this.permits);
    },
@@ -934,4 +1040,8 @@
    padding-top: 6px;
  }
}
.table-title {
  font-weight: 700;
  color: #13c2c2;
}
</style>
src/pages/resourceManage/product/prodUpload.vue
@@ -34,21 +34,6 @@
        </a-col>
        <a-col :span="14">
          <a-form-model-item
            v-if="prodInfo"
            class="ant-row-flex"
            label="定制单号"
            :labelCol="{ flex: '8em' }"
            :wrapperCol="{ flex: 1 }"
            prop="customCode1"
          >
            <a-textarea
              placeholder="请输入定制单号"
              v-model.trim="info.customCode1"
              :rows="2"
            />
          </a-form-model-item>
          <a-form-model-item
            v-else
            class="ant-row-flex"
            label="定制单号"
            :labelCol="{ flex: '8em' }"
@@ -62,6 +47,21 @@
            />
          </a-form-model-item>
        </a-col>
        <a-col :span="10" v-if="prodInfo">
          <a-form-model-item
            class="ant-row-flex"
            label="定制单操作"
            :labelCol="{ flex: '8em' }"
            :wrapperCol="{ flex: 1 }"
            prop="type"
          >
            <a-select v-model="info.type" @change="typeChange">
              <a-select-option :value="0">修改定制单号</a-select-option>
              <a-select-option :value="1">清空定制单号</a-select-option>
              <a-select-option :value="2">追加定制单号</a-select-option>
            </a-select>
          </a-form-model-item>
        </a-col>
      </a-row>
    </a-form-model>
    <div class="modal-footer">
@@ -73,7 +73,7 @@
<script>
import { mapGetters } from "vuex";
import moment from 'moment';
import moment from "moment";
export default {
  name: "",
  props: {
@@ -85,11 +85,9 @@
    return {
      userList: [],
      info: {
        nextUser: "",
        description: "",
        customCode: "",
        customCode1: "",
        versionTime: moment().format('YYYY-MM-DD HH:mm:ss'),
        type: 0,
        versionTime: moment().format("YYYY-MM-DD HH:mm:ss"),
      },
      rules: {
        nextUser: [
@@ -100,13 +98,6 @@
          },
        ],
        customCode: [],
        customCode1: [
          {
            required: true,
            message: "请输入定制单号",
            trigger: "blur",
          },
        ],
      },
    };
  },
@@ -129,29 +120,28 @@
    cancel() {
      this.$emit("cancel");
    },
    typeChange(v) {
      console.log(v);
      if (v == 1) {
        this.info.customCode = '';
      }
    },
    ok() {
      this.$refs.formRef.validate((valid) => {
        if (!valid) {
          this.$message.error("存在未通过检验的表单项");
          return false;
        } else {
          let {
            info: { nextUser, description, customCode, customCode1, versionTime },
            prodInfo,
          } = this;
          let obj = prodInfo
            ? { nextUser, description, customCode: customCode1, versionTime }
            : { nextUser, description, customCode, versionTime };
          this.$emit("ok", obj);
        }
      });
      let {
        info: { customCode, versionTime, type },
        prodInfo
      } = this;
      if (type == 2) {
        customCode = prodInfo.customCode ? prodInfo.customCode + ',' + customCode : customCode;
      }
      let obj = { customCode, versionTime };
      this.$emit("ok", obj);
    },
    disabledDate(current) {
      // Can not select days before today and today
      return current > moment().endOf("day");
    },
  },
  mounted() {
    this.getUserByRoleId();
  },
src/pages/resourceManage/software/apis.js
@@ -79,3 +79,14 @@
    data
  })
}
/**
 * 软件锁定
 * @returns
 */
export const updateSoftwareLock = (params) => {
  return axios({
    method: "GET",
    url: "software/updateSoftwareLock",
    params
  })
}
src/pages/resourceManage/software/descRes.vue
@@ -6,40 +6,40 @@
          <th colspan="4">软件基本信息</th>
        </tr>
        <tr>
          <th>文件名称</th>
          <th class="col-1">文件名称</th>
          <td colspan="3">{{ summarize.fileName }}</td>
        </tr>
        <tr>
          <th>板号</th>
          <th class="col-1">板号</th>
          <td colspan="3">{{ summarize.boardNumber }}</td>
        </tr>
        <tr>
          <th>软件类型</th>
          <th class="col-1">软件类型</th>
          <td colspan="3">{{ summarize.type }}</td>
        </tr>
        <tr>
          <th>软件版本</th>
          <th class="col-1">软件版本</th>
          <td>{{ summarize.version }}</td>
          <th>软件基于版本</th>
          <th class="col-3">软件基于版本</th>
          <td>{{ summarize.basedVersion }}</td>
        </tr>
        <tr>
          <th>软件负责人</th>
          <th class="col-1">软件负责人</th>
          <td>{{ summarize.owner }}</td>
          <th>归档日期</th>
          <th class="col-3">归档日期</th>
          <td>{{ summarize.filingDate }}</td>
        </tr>
        <tr>
          <th colspan="4">软件适用机型</th>
        </tr>
        <tr v-for="(item, idx) in list" :key="idx">
          <th>物料编码</th>
          <th class="col-1">物料编码</th>
          <td>{{ item.parentCode }}</td>
          <th>规格型号</th>
          <th class="col-3">规格型号</th>
          <td>{{ item.parentModel }}</td>
        </tr>
        <tr>
          <th>发布说明</th>
          <th class="col-1">发布说明</th>
          <td colspan="3">{{ summarize.releaseNotes }}</td>
        </tr>
      </tbody>
@@ -69,14 +69,14 @@
      }));
    },
    summarize() {
      console.log(this.info[0])
      // console.log(this.info[0]);
      return this.info[0] || {};
    },
  },
  methods: {},
  mounted() {
    console.log(this.summarize, 90909)
    // console.log(this.summarize, 90909);
  },
};
</script>
@@ -84,6 +84,7 @@
<style lang="less" scoped>
.table {
  width: 100%;
  // table-layout: fixed;
  border-collapse: collapse;
  th,
  td {
@@ -94,4 +95,9 @@
    color: #13c2c2;
  }
}
// .col-1,
// .col-3 {
//   // word-break:break-all;
//   width: 5.4em;
// }
</style>
src/pages/resourceManage/software/list.vue
@@ -14,7 +14,7 @@
            @refresh="onRefresh"
            @reset="onReset"
            :format-conditions="true"
            :scroll="{ x: 1920, y }"
            :scroll="{ x: 2020, y }"
            :pagination="{
              current: pageCurr,
              pageSize: pageSize,
@@ -59,12 +59,29 @@
              </a-popover>
              <template v-if="canDownloadSoftware">
                <a-divider type="vertical"></a-divider>
                <a @click="download(record)">下载</a>
              </template>
              <template v-if="canUploadSoftware">
                <a :disabled="!!record.soft.lockFlag" @click="download(record)"
                  >下载</a
                >
                <a-divider type="vertical"></a-divider>
                <a @click="updateDesc(record)">更新说明</a>
              </template>
              <a-popover title="" trigger="hover">
                <a-space class="btn-grp" direction="vertical" slot="content">
                  <a-button
                    v-if="canUploadSoftware"
                    :disabled="!!record.soft.lockFlag"
                    type="primary"
                    @click="updateDesc(record)"
                    >更新说明</a-button
                  >
                  <a-button
                    v-if="canLock"
                    type="primary"
                    @click="lock(record)"
                    >{{ record.soft.lockFlag ? "解锁" : "锁定" }}</a-button
                  >
                </a-space>
                <a>更多</a>
              </a-popover>
            </template>
          </advance-table>
        </a-card>
@@ -81,19 +98,34 @@
      @cancel="uploadCancel"
    >
      <div class="">
        <a-row v-if="!onlyXls" 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"
            >
              <a-button type="primary">选择软件</a-button>
            </a-upload>
          </a-col>
        </a-row>
        <template v-if="!onlyXls">
          <a-row type="flex" class="row">
            <a-col flex="6em" class="label">更新时间</a-col>
            <a-col :flex="1">
              <a-date-picker
                format="YYYY-MM-DD HH:mm:ss"
                valueFormat="YYYY-MM-DD HH:mm:ss"
                :allowClear="false"
                :disabled-date="disabledDate"
                :show-time="{ defaultValue: moment('00:00:00', 'HH:mm:ss') }"
                v-model="versionTime"
              />
            </a-col>
          </a-row>
          <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"
              >
                <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">
@@ -113,10 +145,37 @@
        </div>
        <div class="modal-footer">
          <a-button type="danger" @click="uploadCancel"> 取消 </a-button>
          <a-button v-if="!onlyXls" type="primary" @click="uploadSoftware"> 提交 </a-button>
          <a-button v-if="!onlyXls" type="primary" @click="uploadSoftware">
            提交
          </a-button>
          <a-button v-else type="primary" @click="applyModel"> 提交 </a-button>
        </div>
      </div>
    </a-modal>
    <pop
      :visible.sync="popVisible"
      :x="popPosition.x"
      :y="popPosition.y"
      :info="popInfo"
      :position="popPosition.dir"
    ></pop>
    <!-- 操作原因 -->
    <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>
  </div>
</template>
@@ -126,13 +185,17 @@
import checkPermit from "@/assets/js/tools/checkPermit";
import PERMITS from "@/assets/js/const/const_permits";
import DescRes from "./descRes";
import Pop from "./pop";
import offset from "@/assets/js/tools/offset";
import moment from 'moment';
import {
  getList,
  downLoadSoftware,
  excelParse,
  productSoftwareSubmit,
  applyModel
  applyModel,
  updateSoftwareLock,
} from "./apis";
import { mapGetters } from "vuex";
@@ -140,6 +203,17 @@
  name: "",
  data() {
    return {
      versionTime: moment().format('YYYY-MM-DD HH:mm:ss'),
      reasonVisible: false,
      reason: "",
      currentObj: null,
      popVisible: false,
      popPosition: {
        x: 0,
        y: 0,
        dir: "bottom",
      },
      popInfo: [],
      onlyXls: false,
      resData: [],
      file: null,
@@ -159,6 +233,7 @@
          dataIndex: "soft.fileName",
          align: "center",
          searchAble: true,
          customCell: this.customCell,
        },
        {
          title: "类型",
@@ -166,12 +241,14 @@
          align: "center",
          width: 180,
          noSearch: true,
          customCell: this.customCell,
        },
        {
          title: "版本",
          dataIndex: "soft.version",
          align: "center",
          noSearch: true,
          customCell: this.customCell,
        },
        {
          title: "负责人",
@@ -179,6 +256,7 @@
          align: "center",
          searchAble: true,
          width: 160,
          customCell: this.customCell,
        },
        {
          title: "适用机型号",
@@ -195,10 +273,25 @@
          visible: false,
        },
        {
          title: "板号",
          dataIndex: "soft.boardNumber",
          align: "center",
          searchAble: true,
          customCell: this.customCell,
        },
        {
          title: "升级说明",
          dataIndex: "soft.releaseNotes",
          align: "center",
          noSearch: true,
          customCell: this.customCell,
        },
        {
          title: "最后一次操作原因",
          dataIndex: "soft.localReason",
          align: "center",
          noSearch: true,
          customCell: this.customCell,
        },
        {
          title: "操作",
@@ -231,8 +324,14 @@
  components: {
    AdvanceTable,
    DescRes,
    Pop,
  },
  methods: {
    moment,
    disabledDate(current) {
      // Can not select days before today and today
      return current > moment().endOf("day");
    },
    onSearch(conditions, searchOptions) {
      // console.log(conditions);
      // console.log(searchOptions);
@@ -264,10 +363,13 @@
      Object.keys(conditions).forEach((v) => {
        switch (v) {
          case "soft.fileName":
            params[v] = conditions["fileName"];
            params["fileName"] = conditions[v];
            break;
          case "soft.owner":
            params[v] = conditions["owner"];
            params["owner"] = conditions[v];
            break;
          case "soft.boardNumber":
            params["boardNumber"] = conditions[v];
            break;
          default:
            params[v] = conditions[v];
@@ -334,6 +436,7 @@
      this.file1 = null;
      this.resData = [];
      this.onlyXls = false;
      this.versionTime = moment().format('YYYY-MM-DD HH:mm:ss');
      this.uploadShow = true;
    },
    updateDesc() {
@@ -378,7 +481,7 @@
          list = data2;
          this.$message.success(msg);
        } else {
          this.$message.error('解析失败');
          this.$message.error("解析失败");
        }
        this.resData = list;
      });
@@ -404,6 +507,7 @@
      const formData = new FormData();
      formData.append("file1", this.file);
      formData.append("file2", this.file1);
      formData.append("fontUpdateTime", this.versionTime);
      formData.append("softwareStr", JSON.stringify(this.resData));
      productSoftwareSubmit(formData)
        .then((res) => {
@@ -444,7 +548,7 @@
            this.$message.success(msg);
            this.searchData();
          } else {
            this.$message.error('解析失败');
            this.$message.error("解析失败");
          }
          this.$layer.close(loading);
        })
@@ -452,6 +556,89 @@
          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;
        }
        if (y > clientHeight - 378) {
          y = clientHeight - 378;
        }
      } else {
        if (y + 18 + 360 > clientHeight) {
          // y = clientHeight - 378;
          dir = "top";
        } else {
          dir = "bottom";
        }
        if (x < 300) {
          x = 300;
        }
        if (x + 300 > clientWidth) {
          x = clientWidth - 300;
        }
      }
      this.popPosition.x = x;
      this.popPosition.y = y;
      this.popPosition.dir = dir;
      this.popInfo = obj.links;
      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),
        },
      };
    },
    reasonCancel() {
      this.reasonVisible = false;
    },
    reasonOk() {
      let {
        soft: { fileUrl, lockFlag },
      } = this.currentObj;
      lockFlag = !lockFlag * 1;
      let params = {
        fileUrl,
        localReason: this.reason,
        lockFlag,
      };
      updateSoftwareLock(params).then((res) => {
        const { code, data } = res.data;
        if (code && data) {
          this.$message.success("操作成功");
          this.reasonVisible = false;
          this.searchData();
        } else {
          this.$message.error("操作失败");
        }
      });
    },
    lock(record) {
      this.reason = "";
      this.currentObj = record;
      this.reasonVisible = true;
    },
  },
  watch: {
@@ -496,6 +683,9 @@
    },
    canDownloadSoftware() {
      return checkPermit(PERMITS.downloadSoftware, this.permits);
    },
    canLock() {
      return checkPermit(PERMITS.lockOther, this.permits);
    },
  },
  mounted() {
@@ -564,4 +754,7 @@
  overflow-y: auto;
  margin-bottom: 10px;
}
.btn-grp button {
  width: 6.4em;
}
</style>
src/pages/resourceManage/software/pop.vue
New file
@@ -0,0 +1,142 @@
<template>
  <div
    :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 "./descRes";
export default {
  name: "",
  components: {
    DescRes,
  },
  props: {
    info: {
      type: Array,
      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: {},
  mounted() {},
};
</script>
<style lang="less" scoped>
.pop {
  position: absolute;
  z-index: 1;
  background: rgba(0, 0, 0, 0.6);
  width: 420px;
  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;
    }
  }
  &.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;
    }
  }
  &.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>