zhengnaiwen_citu před 8 měsíci
rodič
revize
b2c2ab9dfd

+ 36 - 0
src/api/bonus.js

@@ -0,0 +1,36 @@
+import http from '@/utils/request'
+
+// 业绩分配 列表查询
+export function getAllocationPage (data) {
+  return http.post('/employee/performance/page', data)
+}
+
+// 业绩分配 绩效分配
+export function saveAllocationGrant (data) {
+  return http.post('/employee/performance/grant', data)
+}
+
+// 业绩分配 员工类型字典
+export function getAllocationEmployeeCategory (data) {
+  return http.post('/employee/performance/category/dict', data)
+}
+
+// 业绩分配 检查审批状态
+export function checkAllocationSubmitStatus (data) {
+  return http.post('/employee/performance/confirmation/check/submit', data)
+}
+
+// 业绩分配 绩效分配统计
+export function getAllocationStatistics (data) {
+  return http.post('/employee/performance/grant/statistics', data)
+}
+
+// 业绩分配 绩效分配统计
+export function saveAllocation (data) {
+  return http.post('/employee/performance/submit/grant', data)
+}
+
+// 奖金审核 列表
+export function getBonusApprovePage (data) {
+  return http.post('/employee/performance/confirmation/page', data)
+}

+ 0 - 30
src/api/salary.js

@@ -81,36 +81,6 @@ export function downloadPayroll (data) {
   return http.download('/employee/payroll/download/export', data)
 }
 
-// 业绩分配 列表查询
-export function getAllocationPage (data) {
-  return http.post('/employee/performance/page', data)
-}
-
-// 业绩分配 绩效分配
-export function saveAllocationGrant (data) {
-  return http.post('/employee/performance/grant', data)
-}
-
-// 业绩分配 员工类型字典
-export function getAllocationEmployeeCategory (data) {
-  return http.post('/employee/performance/category/dict', data)
-}
-
-// 业绩分配 检查审批状态
-export function checkAllocationSubmitStatus (data) {
-  return http.post('/employee/performance/confirmation/check/submit', data)
-}
-
-// 业绩分配 绩效分配统计
-export function getAllocationStatistics (data) {
-  return http.post('/employee/performance/grant/statistics', data)
-}
-
-// 业绩分配 绩效分配统计
-export function saveAllocation (data) {
-  return http.post('/employee/performance/submit/grant', data)
-}
-
 // 绩效方案 分页查询
 export function getSolutionPage (data) {
   return http.post('/performance/solution/page', data)

+ 10 - 3
src/components/AutoComponents/MDialog/index.vue

@@ -12,8 +12,8 @@
   >
     <slot></slot>
     <span slot="footer">
-      <m-button @click="show = false">取 消</m-button>
-      <m-button type="primary" @click="sure">确 定</m-button>
+      <m-button @click="show = false">{{ option?.textCancel ?? '取 消'}}</m-button>
+      <m-button type="primary" @click="sure">{{ option?.textSure ?? '确 定'}}</m-button>
     </span>
   </el-dialog>
 </template>
@@ -23,7 +23,14 @@ export default {
   name: 'm-dialog',
   props: {
     title: String,
-    width: String
+    width: String,
+    option: {
+      type: Object,
+      default: () => ({
+        textCancel: '取 消',
+        textSure: '确 定'
+      })
+    }
   },
   data () {
     return {

+ 9 - 0
src/components/AutoComponents/MSearch/index.vue

@@ -62,7 +62,16 @@
             v-on="item.handles"
           ></el-date-picker>
 
+          <!-- 级联 -->
+          <el-cascader
+            v-if="item.type === 'cascader'"
+            v-model="query[item.prop]"
+            v-bind="item.options"
+            v-on="item.handles"
+          ></el-cascader>
+
           <!-- 其他类型可以根据需要扩展 -->
+
         </el-form-item>
       </template>
         <el-form-item class="flex">

+ 13 - 2
src/utils/dict.js

@@ -11,15 +11,26 @@ export const MENU_TYPE = {
   COMPONENT: 3
 }
 
-const organizationTree = []
+let organizationTree = []
 export const getOrganizationTree = async (data) => {
   try {
     if (!organizationTree.length) {
       const { data } = await getOrganizationTreeApi()
-      return [data]
+      organizationTree = removeEmptyArray([data])
     }
     return organizationTree
   } catch (error) {
     Vue.$message.error(error.message)
   }
+
+  function removeEmptyArray (arr) {
+    return arr.map(e => {
+      if (e.child && e.child.length) {
+        e.child = removeEmptyArray(e.child)
+      } else {
+        e.child = null
+      }
+      return e
+    })
+  }
 }

+ 31 - 0
src/utils/panorama.js

@@ -25,3 +25,34 @@ export const PAYROLL_HEADER = [
   { label: '工资扣款', prop: 'payrollDeduction' },
   { label: '合计', prop: 'totalSalary', align: 'center' }
 ]
+
+/**
+ * 查询节点返回完整路径数组
+ * @param {Array} tree 树形结构数据
+ * @param {String} targetCode 目标节点
+ * @param {Array} path  路径存储
+ * @param {Object} obj  节点属性
+ * @returns
+ */
+export function findPath (tree, targetCode, path = [], obj = { children: 'child', value: 'organizationNo', label: 'organizationName' }) {
+  for (const node of tree) {
+    // 将当前节点加入路径
+    const currentPath = [...path, { value: node[obj.value], label: node[obj.label] }]
+
+    // 如果当前节点是目标节点,返回路径
+    if (node[obj.value] === targetCode) {
+      return currentPath
+    }
+
+    // 如果当前节点有子节点,递归查找
+    if (node[obj.children] && node[obj.children].length > 0) {
+      const result = findPath(node[obj.children], targetCode, currentPath, obj)
+      if (result) {
+        return result
+      }
+    }
+  }
+
+  // 如果没有找到目标节点,返回 null
+  return null
+}

+ 178 - 100
src/views/bonus/allocation/index.vue

@@ -1,64 +1,97 @@
 <template>
-  <div class="pa-3 white">
-    <m-search :items="searchItems" v-model="searchValues" class="mb-3" @search="onSearch">
-      <template #button>
-        <div class="ml-3">
-          <el-tag v-if="query.month">{{ query.month }}</el-tag>
-          <el-tag v-if="query.employeeCategory" class="ml-3">{{ query.employeeCategory }}</el-tag>
-          可发放绩效:<el-tag>{{ totalAllocationPerformanceSalary }}</el-tag>
-          总绩效: <el-tag>{{ totalGrantPerformanceSalary }}</el-tag>
+  <div class="white pa-3">
+    <m-search :items="searchItems" v-model="searchValues" class="mb-3" @search="onSearch"></m-search>
+    <BonusTable ref="bonusTableRefs">
+      <template #header="{ items }">
+        <div class="content">
+          <div>
+            可发放绩效:<el-tag>{{ totalAllocationPerformanceSalary }}</el-tag>
+            总绩效: <el-tag>{{ totalGrantPerformanceSalary }}</el-tag>
+            <template v-if="auditStatusList[auditStatus]">
+              审批状态:<el-tag :type="auditStatusList[auditStatus]?.type ?? 'info'">{{ auditStatusList[auditStatus]?.label ?? '未知状态' }}</el-tag>
+            </template>
+            <el-button
+              class="ml-3"
+              type="primary"
+              size="small"
+              :loading="auditStatusLoading"
+              :disabled="auditStatus === 0 || auditStatus === 1 || !items.length"
+              @click="onSave">
+              确认分配
+            </el-button>
+          </div>
         </div>
       </template>
-    </m-search>
-    <m-table
+      <template #actions="{ row }">
+        <el-input :disabled="auditStatusLoading" v-model="values[row.employeePerformanceId]" placeholder="请输入绩效" size="small"></el-input>
+      </template>
+    </BonusTable>
+    <!-- <m-table
       :items="items"
       :headers="headers"
-      :loading="loading"
+      v-loading="loading"
       :total="total"
       :page-size="pageInfo.size"
       :page-current="pageInfo.current"
       @page-change="onPageChange"
-      @expand-change="onExpandChange"
     >
+      <template #header>
+        <div class="content">
+          <div>
+            可发放绩效:<el-tag>{{ totalAllocationPerformanceSalary }}</el-tag>
+            总绩效: <el-tag>{{ totalGrantPerformanceSalary }}</el-tag>
+            <template v-if="auditStatusList[auditStatus]">
+              审批状态:<el-tag :type="auditStatusList[auditStatus]?.type ?? 'info'">{{ auditStatusList[auditStatus]?.label ?? '未知状态' }}</el-tag>
+            </template>
+            <el-button
+              class="ml-3"
+              type="primary"
+              size="small"
+              :loading="auditStatusLoading"
+              :disabled="auditStatus === 0 || auditStatus === 1 || !items.length"
+              @click="onSave">
+              确认分配
+            </el-button>
+          </div>
+        </div>
+      </template>
       <template #status="{ row }">
         <el-tag :type="statusList[row.status].color">{{ statusList[row.status].text }}</el-tag>
       </template>
       <template #dataType="{row}">
         {{ row.dataType === 1 ? '手工数据' : '系统数据' }}
       </template>
-      <template #actions-header>
-        <el-button
-          type="primary"
-          size="small"
-          :loading="auditStatusLoading"
-          :disabled="auditStatus === 0 || auditStatus === 1"
-          @click="onSave">
-          {{ auditStatusList[auditStatus] ?? '确认分配' }}
-        </el-button>
-      </template>
       <template #actions="{ row }">
         <el-input :disabled="auditStatusLoading" v-model="values[row.employeePerformanceId]" placeholder="请输入绩效" size="small"></el-input>
       </template>
-    </m-table>
+    </m-table> -->
   </div>
 </template>
 
 <script>
+import BonusTable from '../components/bonusTable.vue'
 import { dateFormat } from '@/utils/date'
 import Decimal from 'decimal.js'
 import {
-  getAllocationPage,
+  // getAllocationPage,
   saveAllocationGrant,
   getAllocationEmployeeCategory,
   checkAllocationSubmitStatus,
   getAllocationStatistics,
   saveAllocation
-} from '@/api/salary'
+} from '@/api/bonus'
+import {
+  getOrganizationTree
+} from '@/utils/dict'
+import { STATUS_LIST } from '../utils'
 export default {
-  name: 'salary-allocation',
+  name: 'bonusAllocation',
+  components: {
+    BonusTable
+  },
   data () {
     return {
-      auditStatusList: ['审核中', '审核已通过', '审核未通过'],
+      auditStatusList: STATUS_LIST,
       auditStatus: null,
       auditStatusLoading: true,
       values: {},
@@ -78,37 +111,38 @@ export default {
       },
       searchValues: {
         month: dateFormat('YYYY-mm', new Date()),
+        organizationNo: null,
         employeeCategory: null
       },
       query: {
         month: dateFormat('YYYY-mm', new Date())
       },
-      headers: [
-        { label: '月份', prop: 'month' },
-        { label: '机构名称', prop: 'organizationName', width: 150 },
-        { label: '员工姓名', prop: 'employeeName' },
-        { label: '分配状态', prop: 'status', align: 'center' },
-        { label: '员工类型', prop: 'employeeCategory', align: 'center' },
-        { label: '可分配绩效薪资', prop: 'assignablePerformanceSalary', align: 'center', width: 150 },
-        { label: '领导分配绩效薪资', prop: 'allocationPerformanceSalary', align: 'center', width: 150 },
-        { label: '基础绩效薪资', prop: 'basicPerformanceSalary', align: 'center', width: 120 },
-        { label: '总绩效薪资', prop: 'performanceSalary', align: 'center', width: 120 },
-        { label: '数据来源', prop: 'dataType', align: 'center' },
-        { label: '数据版本', prop: 'version', width: 160 },
-        { label: '创建时间', prop: 'createDate', width: 160 },
-        { label: '操作', prop: 'actions', width: 160, fixed: 'right' }
-      ],
-      items: [
-      ],
-      loading: false,
-      total: 0,
-      pageInfo: {
-        current: 1,
-        size: 50
-      },
-      orders: [],
+      // headers: [
+      //   { label: '月份', prop: 'month' },
+      //   { label: '机构名称', prop: 'organizationName', width: 150 },
+      //   { label: '员工姓名', prop: 'employeeName' },
+      //   { label: '分配状态', prop: 'status', align: 'center' },
+      //   { label: '员工类型', prop: 'employeeCategory', align: 'center' },
+      //   { label: '可分配绩效薪资', prop: 'assignablePerformanceSalary', align: 'center', width: 150 },
+      //   { label: '领导分配绩效薪资', prop: 'allocationPerformanceSalary', align: 'center', width: 150 },
+      //   { label: '基础绩效薪资', prop: 'basicPerformanceSalary', align: 'center', width: 120 },
+      //   { label: '总绩效薪资', prop: 'performanceSalary', align: 'center', width: 120 },
+      //   { label: '数据来源', prop: 'dataType', align: 'center' },
+      //   { label: '数据版本', prop: 'version', width: 160 },
+      //   { label: '创建时间', prop: 'createDate', width: 160 },
+      //   { label: '操作', prop: 'actions', width: 160, fixed: 'right' }
+      // ],
+      // items: [],
+      // loading: false,
+      // total: 0,
+      // pageInfo: {
+      //   current: 1,
+      //   size: 50
+      // },
+      // orders: [],
       employeeCategoryItems: [],
-      totalGrantPerformanceSalary: 0
+      totalGrantPerformanceSalary: 0,
+      deptItems: []
     }
   },
   computed: {
@@ -129,9 +163,23 @@ export default {
             type: 'month',
             valueFormat: 'yyyy-MM',
             format: 'yyyy 年 MM 月'
-          },
-          handles: {
-            change: this.getEmployeeCategoryItems
+          }
+        },
+        {
+          label: '机构',
+          prop: 'organizationNo',
+          type: 'cascader',
+          options: {
+            clearable: true,
+            placeholder: '请选择机构',
+            options: this.deptItems,
+            props: {
+              checkStrictly: true,
+              emitPath: false,
+              value: 'organizationNo',
+              label: 'organizationName',
+              children: 'child'
+            }
           }
         },
         {
@@ -147,42 +195,57 @@ export default {
       ]
     }
   },
+  watch: {
+    'searchValues.month': {
+      handler (v) {
+        this.getEmployeeCategoryItems()
+      }
+    }
+  },
   async created () {
     this.loading = true
+    this.onGetDept()
     await this.getEmployeeCategoryItems()
     this.onInit()
   },
   methods: {
-    async onInit () {
-      this.loading = true
-      try {
-        const { data } = await getAllocationPage({
-          page: {
-            ...this.pageInfo,
-            orders: this.orders
-          },
-          entity: {
-            ...this.searchValues
-          }
-        })
-        await this.onCheckStatus()
-        await this.onStatistics()
-        data.records.forEach(e => {
-          if (Object.prototype.hasOwnProperty.call(this.values, e.employeePerformanceId)) {
-            return
-          }
-          this.$set(this.values, e.employeePerformanceId, e.allocationPerformanceSalary || null)
-        })
-        this.items = data.records.map(e => {
-          e.createDate = dateFormat('YYYY-mm-dd HH:MM:SS', new Date(e.createDate))
-          return e
-        })
-        this.total = data.total
-      } catch (error) {
-        this.$message.error(error)
-      } finally {
-        this.loading = false
+    // async onInit () {
+    //   this.loading = true
+    //   try {
+    //     const { data } = await getAllocationPage({
+    //       page: {
+    //         ...this.pageInfo,
+    //         orders: this.orders
+    //       },
+    //       entity: {
+    //         ...this.searchValues
+    //       }
+    //     })
+    //     await this.onCheckStatus()
+    //     await this.onStatistics()
+    //     data.records.forEach(e => {
+    //       if (Object.prototype.hasOwnProperty.call(this.values, e.employeePerformanceId)) {
+    //         return
+    //       }
+    //       this.$set(this.values, e.employeePerformanceId, e.allocationPerformanceSalary || null)
+    //     })
+    //     this.items = data.records.map(e => {
+    //       e.createDate = dateFormat('YYYY-mm-dd HH:MM:SS', new Date(e.createDate))
+    //       return e
+    //     })
+    //     this.total = data.total
+    //   } catch (error) {
+    //     this.$message.error(error)
+    //   } finally {
+    //     this.loading = false
+    //   }
+    // },
+    async onGetDept () {
+      const data = await getOrganizationTree()
+      if (!data) {
+        return
       }
+      this.deptItems = data
     },
     async getEmployeeCategoryItems () {
       try {
@@ -212,9 +275,18 @@ export default {
     },
     // 领导分配绩效薪资
     onSave () {
+      const h = this.$createElement
+      const _el = [
+        h('p', undefined, `更新月份:${this.query.month}`),
+        h('p', undefined, `更新物业线:${this.query.category ?? null}`)
+      ]
       try {
         if (this.totalGrantPerformanceSalary !== this.totalAllocationPerformanceSalary) {
-          this.$confirm('当前非完全配额,是否强制提交', '提示', {
+          const el = h('div', [
+            h('p', undefined, '当前非完全配额,是否强制提交?'),
+            ..._el
+          ])
+          this.$confirm(el, '提示', {
             confirmButtonText: '确定',
             cancelButtonText: '取消',
             type: 'warning'
@@ -225,7 +297,11 @@ export default {
           })
           return
         }
-        this.$confirm('确定提交所有数据?', '提示', {
+        const el = h('div', [
+          h('p', undefined, '确定提交所有数据?'),
+          ..._el
+        ])
+        this.$confirm(el, '提示', {
           confirmButtonText: '确定',
           cancelButtonText: '取消',
           type: 'warning'
@@ -237,16 +313,21 @@ export default {
       }
     },
     async onSaveAll (force) {
+      const employeePerformanceGrantItems = Object.keys(this.values).reduce((res, key) => {
+        res.push({
+          employeePerformanceId: key,
+          allocationPerformanceSalary: this.values[key]
+        })
+        return res
+      }, [])
+      if (!employeePerformanceGrantItems.length) {
+        this.$message.error('请先分配金额')
+        return
+      }
       try {
         await saveAllocationGrant({
           ...this.query,
-          employeePerformanceGrantItems: Object.keys(this.values).reduce((res, key) => {
-            res.push({
-              employeePerformanceId: key,
-              allocationPerformanceSalary: this.values[key]
-            })
-            return res
-          }, [])
+          employeePerformanceGrantItems
         })
         await saveAllocation({
           ...this.query,
@@ -279,17 +360,14 @@ export default {
       } catch (error) {
         this.$message.error(error)
       }
-    },
-    onDetails () {},
-    onExpandChange () {},
-    onPageChange (index) {
-      this.pageInfo.current = index
-      this.onInit()
     }
   }
 }
 </script>
 
 <style lang="scss" scoped>
-
+.content {
+  display: flex;
+  justify-content: flex-end;
+}
 </style>

+ 29 - 0
src/views/bonus/approve/approveDetails.vue

@@ -0,0 +1,29 @@
+<template>
+  <m-dialog ref="dialog" :title="title" v-bind="$attrs" v-on="$listeners"></m-dialog>
+</template>
+
+<script>
+export default {
+  name: 'approveDetails',
+  components: {},
+  props: {},
+  data () {
+    return {
+      title: '审批详情'
+    }
+  },
+  computed: {},
+  watch: {},
+  created () {},
+  mounted () {},
+  methods: {
+    open () {
+      this.$refs.dialog.open()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 127 - 0
src/views/bonus/approve/index.vue

@@ -0,0 +1,127 @@
+<template>
+  <div class="white pa-3">
+    <m-search :items="searchItems" v-model="searchValues" class="mb-3" @search="onSearch"></m-search>
+    <m-table
+      :items="items"
+      :headers="headers"
+      v-loading="loading"
+      :total="total"
+      :page-size="pageInfo.size"
+      :page-current="pageInfo.current"
+      @page-change="onPageChange"
+    >
+      <template #status="{ row }">
+        <el-tag :type="statusList[row.status]?.type ?? 'info'">{{ statusList[row.status]?.label ?? '未知状态' }}</el-tag>
+      </template>
+      <template #actions="{ row }">
+        <el-button type="text" @click="onShowDetails(row)">查看详情</el-button>
+      </template>
+    </m-table>
+    <ApproveDetails ref="approveDetailsRefs"></ApproveDetails>
+  </div>
+</template>
+
+<script>
+import {
+  getBonusApprovePage
+} from '@/api/bonus'
+import ApproveDetails from './approveDetails.vue'
+import { STATUS_LIST } from '../utils'
+export default {
+  name: 'bonusAllocationApprove',
+  components: {
+    ApproveDetails
+  },
+  data () {
+    return {
+      statusList: STATUS_LIST,
+      loading: false,
+      items: [],
+      headers: [
+        { label: '月份', prop: 'month' },
+        { label: '分配机构', prop: 'opOrganizationName' },
+        // { label: '分配员工', prop: 'opNickname', align: 'center' },
+        { label: '员工类型', prop: 'employeeCategory', align: 'center' },
+        { label: '状态', prop: 'status', align: 'center' },
+        { label: '数据版本', prop: 'version', align: 'center' },
+        { label: '审核信息', prop: 'authMsg' },
+        { label: '操作', prop: 'actions' }
+      ],
+      total: 0,
+      pageInfo: {
+        current: 1,
+        size: 10
+      },
+      searchValues: {}
+    }
+  },
+  computed: {
+    searchItems () {
+      return [
+        {
+          label: '月份',
+          prop: 'month',
+          type: 'datePicker',
+          options: {
+            placeholder: '请选择月份',
+            clearable: false,
+            type: 'month',
+            valueFormat: 'yyyy-MM',
+            format: 'yyyy 年 MM 月'
+          }
+        },
+        {
+          label: '机构',
+          prop: 'organizationName',
+          type: 'input',
+          options: {
+            placeholder: '请输入机构'
+          }
+        },
+        {
+          label: '员工类型',
+          prop: 'employeeCategory',
+          type: 'input',
+          options: {
+            placeholder: '请输入员工类型'
+          }
+        }
+      ]
+    }
+  },
+  created () {
+    this.onInit()
+  },
+  methods: {
+    async onInit () {
+      try {
+        const { data } = await getBonusApprovePage({
+          page: {
+            ...this.pageInfo
+          },
+          entity: {}
+        })
+        this.items = data.records
+        this.total = data.total
+      } catch (error) {
+        this.$message.error(error)
+      }
+    },
+    onPageChange (page) {
+      this.pageInfo.current = page
+      this.onInit()
+    },
+    onSearch () {
+      this.pageInfo.current = 1
+      this.onInit()
+    },
+    onShowDetails () {
+      this.$refs.approveDetailsRefs.open()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 115 - 0
src/views/bonus/components/bonusTable.vue

@@ -0,0 +1,115 @@
+<template>
+  <m-table
+    :items="items"
+    :headers="headers"
+    v-loading="loading"
+    :total="total"
+    :page-size="pageInfo.size"
+    :page-current="pageInfo.current"
+    @page-change="onPageChange"
+  >
+    <template #header>
+      <slot name="header" :items="items"></slot>
+    </template>
+    <template #status="{ row }">
+      <el-tag :type="statusList[row.status].color">{{ statusList[row.status].text }}</el-tag>
+    </template>
+    <template #dataType="{row}">
+      {{ row.dataType === 1 ? '手工数据' : '系统数据' }}
+    </template>
+    <template #actions="{ row }">
+      <slot name="actions" :row="row"></slot>
+    </template>
+  </m-table>
+</template>
+
+<script>
+import {
+  getAllocationPage
+  // saveAllocationGrant,
+  // getAllocationEmployeeCategory,
+  // checkAllocationSubmitStatus,
+  // getAllocationStatistics,
+  // saveAllocation
+} from '@/api/bonus'
+import { dateFormat } from '@/utils/date'
+export default {
+  name: 'bonusTable',
+  props: {
+    showActions: {
+      type: Boolean,
+      default: true
+    }
+  },
+  data () {
+    return {
+      headers: [
+        { label: '月份', prop: 'month' },
+        { label: '机构名称', prop: 'organizationName', width: 150 },
+        { label: '员工姓名', prop: 'employeeName' },
+        { label: '分配状态', prop: 'status', align: 'center' },
+        { label: '员工类型', prop: 'employeeCategory', align: 'center' },
+        { label: '可分配绩效薪资', prop: 'assignablePerformanceSalary', align: 'center', width: 150 },
+        { label: '领导分配绩效薪资', prop: 'allocationPerformanceSalary', align: 'center', width: 150 },
+        { label: '基础绩效薪资', prop: 'basicPerformanceSalary', align: 'center', width: 120 },
+        { label: '总绩效薪资', prop: 'performanceSalary', align: 'center', width: 120 },
+        { label: '数据来源', prop: 'dataType', align: 'center' },
+        { label: '数据版本', prop: 'version', width: 160 },
+        { label: '创建时间', prop: 'createDate', width: 160 }
+      ],
+      items: [],
+      loading: false,
+      total: 0,
+      pageInfo: {
+        current: 1,
+        size: 50
+      },
+      orders: []
+    }
+  },
+  created () {
+    if (this.showActions) {
+      this.headers.push({ label: '操作', prop: 'actions', width: 160, fixed: 'right' })
+    }
+  },
+  methods: {
+    async onInit (searchValues) {
+      this.loading = true
+      try {
+        const { data } = await getAllocationPage({
+          page: {
+            ...this.pageInfo,
+            orders: this.orders
+          },
+          entity: {
+            ...searchValues
+          }
+        })
+        data.records.forEach(e => {
+          if (Object.prototype.hasOwnProperty.call(this.values, e.employeePerformanceId)) {
+            return
+          }
+          this.$set(this.values, e.employeePerformanceId, e.allocationPerformanceSalary || null)
+        })
+        this.items = data.records.map(e => {
+          e.createDate = dateFormat('YYYY-mm-dd HH:MM:SS', new Date(e.createDate))
+          return e
+        })
+        this.total = data.total
+      } catch (error) {
+        this.$message.error(error)
+      } finally {
+        this.loading = false
+      }
+    },
+    onPageChange (index) {
+      this.pageInfo.current = index
+      this.onInit()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 58 - 0
src/views/bonus/index.vue

@@ -0,0 +1,58 @@
+<template>
+  <div class="white pa-3">
+    <el-tabs v-model="activeName" @tab-click="handleClick">
+      <el-tab-pane
+        v-for="item in items"
+        :key="item.name"
+        :label="item.label"
+        :name="item.name"
+      >
+        <component :is="item.component" :ref="item.name" @hook:mounted="onComponentMounted"></component>
+      </el-tab-pane>
+    </el-tabs>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'salary-calculate',
+  data () {
+    return {
+      activeName: '',
+      items: [],
+      itemData: {}
+    }
+  },
+  created () {
+    this.items = this.$route.meta.roles.filter(e => e.hidden === 1).sort((a, b) => b - a).map(e => {
+      return {
+        name: e.name,
+        label: e.label,
+        component: () => import(`./${e.component}/index.vue`)
+      }
+    })
+    if (this.$route.query.name) {
+      this.activeName = this.$route.query.name
+    } else {
+      this.activeName = this.items[0].name
+    }
+  },
+  methods: {
+    handleClick () {
+      this.$router.push(`${this.$route.path}?name=${this.activeName}`)
+      this.$nextTick(() => {
+        this.$refs[this.activeName][0].onReady()
+      })
+    },
+    onComponentMounted () {
+      this.$refs[this.activeName] && this.$refs[this.activeName][0].onReady()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+::v-deep .el-tabs__content  {
+  overflow: visible !important;
+}
+</style>

+ 6 - 0
src/views/bonus/utils/index.js

@@ -0,0 +1,6 @@
+
+export const STATUS_LIST = {
+  0: { label: '未审核', type: 'warning' },
+  1: { label: '审核通过', type: 'success' },
+  2: { label: '审核拒绝', type: 'danger' }
+}

+ 0 - 4
src/views/humanResources/panorama/dynamic/salary/payroll.vue → src/views/humanResources/panorama/dynamic/humanResources/payroll.vue

@@ -85,10 +85,6 @@ export default {
           },
           ...this.query,
           ...this.searchValues
-          // organizationNo: '123',
-          // employeeName: '',
-          // month: ''
-
         })
         this.items = data.records
         this.total = data.total

+ 27 - 55
src/views/humanResources/panorama/panoramaDetails.vue

@@ -1,23 +1,24 @@
 <template>
   <div class="white pa-3 content">
     <el-menu :default-active="activeIndex" unique-opened class="el-menu-demo" mode="horizontal" @select="onSelect">
-      <PanoramaDetailsMenu v-for="_path in pathList" :key="_path.id" :item="_path"></PanoramaDetailsMenu>
+      <template  v-for="_menu in menuList">
+        <PanoramaDetailsMenu :key="_menu.id" :item="_menu"></PanoramaDetailsMenu>
+      </template>
     </el-menu>
     <div class="py-5">
       <el-breadcrumb>
         <el-breadcrumb-item
-          v-for="(_path, i) in paths"
+          v-for="(_path, i) in breadcrumbs"
           :key="_path.value"
-          :to="i < paths.length - 1 ? $route.path + '?organizationNo=' + _path.value : undefined"
+          :to="i < breadcrumbs.length - 1 ? $route.path + '?organizationNo=' + _path.value : undefined"
         >
           {{ _path.label }}
         </el-breadcrumb-item>
-        <el-breadcrumb-item>薪酬核算</el-breadcrumb-item>
-        <el-breadcrumb-item>工资单管理</el-breadcrumb-item>
+        <el-breadcrumb-item v-for="MP in menuPath" :key="MP.value">{{ MP.label }}</el-breadcrumb-item>
       </el-breadcrumb>
     </div>
     <div class="content-body">
-      <component :is="path" :key="$route.fullPath"></component>
+      <component :is="componentsPath" :key="$route.fullPath"></component>
     </div>
   </div>
 </template>
@@ -27,20 +28,30 @@ import { mapGetters } from 'vuex'
 import PanoramaDetailsMenu from './panoramaDetailsMenu.vue'
 
 import { getOrganizationTree } from '@/utils/dict'
+import {
+  findPath
+} from '@/utils/panorama'
 
 export default {
   name: 'panorama-details',
   components: { PanoramaDetailsMenu },
   data () {
     return {
-      activeIndex: 'salary/payroll',
-      paths: []
+      activeIndex: 'humanResources/payroll',
+      breadcrumbs: []
     }
   },
   computed: {
     ...mapGetters(['routes']),
-    pathList () {
+    menuList () {
       return this.filterRoute(JSON.parse(JSON.stringify(this.routes)))
+    },
+    menuPath () {
+      return findPath(this.menuList, this.activeIndex, [], {
+        children: 'children',
+        value: 'component',
+        label: 'label'
+      })
     }
   },
   watch: {
@@ -57,37 +68,13 @@ export default {
       if (!data) {
         return
       }
-      this.paths = findPath(data, this.$route.query.organizationNo)
+      this.breadcrumbs = findPath(data, this.$route.query.organizationNo)
       if (this.$route.query.employeeName && this.$route.query.employeeNo) {
-        this.paths.push({ value: this.$route.query.employeeNo, label: this.$route.query.employeeName })
-      }
-
-      // 递归查找机构编码,返回从根节点开始的路径数组{ value, label }
-      function findPath (tree, targetCode, path = []) {
-        for (const node of tree) {
-          // 将当前节点加入路径
-          const currentPath = [...path, { value: node.organizationNo, label: node.organizationName }]
-
-          // 如果当前节点是目标节点,返回路径
-          if (node.organizationNo === targetCode) {
-            return currentPath
-          }
-
-          // 如果当前节点有子节点,递归查找
-          if (node.child && node.child.length > 0) {
-            const result = findPath(node.child, targetCode, currentPath)
-            if (result) {
-              return result
-            }
-          }
-        }
-
-        // 如果没有找到目标节点,返回 null
-        return null
+        this.breadcrumbs.push({ value: this.$route.query.employeeNo, label: this.$route.query.employeeName })
       }
     },
-    path () {
-      return import('./dynamic/salary/payroll.vue')
+    componentsPath () {
+      return import(`./dynamic/${this.activeIndex}`)
     },
     filterRoute (items) {
       return items.filter(item => {
@@ -97,24 +84,9 @@ export default {
         return item.meta.panorama
       })
     },
-    // findFirst (items, path = []) {
-    //   const _first = items[0]
-    //   path.push(_first.label)
-    //   console.log(_first, MENU_TYPE.MENU, _first.children.length)
-    //   if (_first.type === 1) {
-    //     return path.join('/')
-    //   }
-    //   if (_first.children && _first.children.length) {
-    //     const _items = _first.children
-    //     const res = this.firstShow(_items, path)
-    //     if (res) {
-    //       return res
-    //     }
-    //   }
-    //   path.pop()
-    //   return null
-    // },
-    onSelect () {},
+    onSelect (v) {
+      console.log(v)
+    },
     onRun (path) {
       window.open(this.$route.path + '?organizationNo=' + path)
     }

+ 20 - 6
src/views/humanResources/panorama/panoramaDetailsMenu.vue

@@ -1,18 +1,32 @@
 <template>
-  <el-submenu v-if="item.children && item.children.length" :index="item.component">
-    <template slot="title">{{ item.label }}</template>
+  <el-submenu
+    v-if="item.children && item.children.length"
+    :index="item.id.toString()"
+    :popper-append-to-body="false"
+  >
+    <template #title>
+      <span>{{ item.label }}</span>
+    </template>
+    <!-- 递归渲染子菜单 -->
     <panorama-details-menu
-      v-for="(child, index) in item.children"
-      :key="index"
+      v-for="child in item.children"
+      :key="child.id"
       :item="child"
     />
   </el-submenu>
-  <el-menu-item v-else :index="item.component">{{ item.label }}</el-menu-item>
+
+  <!-- 无子菜单:渲染 el-menu-item -->
+  <el-menu-item
+    v-else
+    :index="item.component"
+  >
+    <span>{{ item.label }}</span>
+  </el-menu-item>
 </template>
 
 <script>
 export default {
-  name: 'panorama-details-menu',
+  name: 'panoramaDetailsMenu',
   props: {
     item: {
       type: Object,

+ 33 - 225
src/views/salary/calculate/index.vue

@@ -1,250 +1,58 @@
 <template>
   <div class="white pa-3">
-    <m-search :items="searchItems" v-model="searchValues" class="mb-3" @search="onSearch">
-      <template #button>
-        <m-button size="small" type="primary" plain icon="el-icon-finished" :loading="submitLoading" @click="onSave">提交当前临时文件</m-button>
-      </template>
-    </m-search>
-    <m-table
-      :headers="headers"
-      :items="items"
-      v-loading="loading"
-    >
-      <!-- <template #header>
-        <div class="header">
-          <m-button size="small" type="primary" icon="el-icon-finished" :loading="submitLoading" @click="onSave">提交当前临时文件</m-button>
-        </div>
-      </template> -->
-      <template #month>
-        {{ queryValues.month }}
-      </template>
-      <template #file="{ row }">
-        {{ filesValues[row.fileType]?.name }}
-      </template>
-      <template #actions="{ row }">
-        <div class="d-flex">
-          <el-upload
-            class="mr-3"
-            action="#"
-            :limit="1"
-            :show-file-list="false"
-            :on-exceed="(files, fileList) => onExceed(files, fileList, row.fileType)"
-            :http-request="e => onImport(e, row.fileType)"
-            :on-remove="() => onRemove(row.fileType)"
-          >
-            <m-button slot="trigger" text type="primary">导入文件</m-button>
-          </el-upload>
-          <m-button text type="danger" v-show="filesValues[row.fileType]" @click="onDelete(row)">移除临时文件</m-button>
-        </div>
-      </template>
-    </m-table>
+    <el-tabs v-model="activeName" @tab-click="handleClick">
+      <el-tab-pane
+        v-for="item in items"
+        :key="item.name"
+        :label="item.label"
+        :name="item.name"
+      >
+        <component :is="item.component" :ref="item.name" @hook:mounted="onComponentMounted"></component>
+      </el-tab-pane>
+    </el-tabs>
   </div>
 </template>
 
 <script>
-import {
-  getSalaryCalculateTemplate,
-  uploadSalaryCalculateFiles,
-  getSalaryCalculateFiles
-} from '@/api/salary'
-import { dateFormat } from '@/utils/date'
 export default {
   name: 'salary-calculate',
   data () {
     return {
-      searchValues: {
-        month: dateFormat('YYYY-mm', new Date()),
-        category: null
-      },
-      queryValues: {},
-      filesValues: {},
-      categoryItems: [],
-      historyItems: [],
-      formItems: [],
-      headers: [
-        { label: '月份', prop: 'month', width: 100 },
-        { label: '业务线', prop: 'category' },
-        { label: '文件名', prop: 'fileName' },
-        { label: '文件', prop: 'history' },
-        { label: '临时文件', prop: 'file' },
-        { label: '操作', prop: 'actions' }
-      ],
-      loading: false,
-      submitLoading: false
+      activeName: '',
+      items: [],
+      itemData: {}
     }
   },
-  computed: {
-    items () {
-      const items = this.categoryItems.find(e => e.category === this.queryValues.category)?.files
-      if (!items) {
-        return []
+  created () {
+    this.items = this.$route.meta.roles.filter(e => e.hidden === 1).sort((a, b) => b - a).map(e => {
+      return {
+        name: e.name,
+        label: e.label,
+        component: () => import(`./${e.component}/index.vue`)
       }
-      return items.map(e => {
-        return {
-          ...e,
-          history: this.historyItems.find(h => h.fileType === e.fileType)?.fileOriginalFilename
-        }
-      })
-    },
-    searchItems () {
-      return [
-        {
-          label: '月份',
-          prop: 'month',
-          type: 'datePicker',
-          options: {
-            clearable: false,
-            type: 'month',
-            format: 'yyyy-MM',
-            valueFormat: 'yyyy-MM',
-            placeholder: '选择更新月份'
-          }
-        },
-        {
-          label: '业务线',
-          prop: 'category',
-          type: 'select',
-          options: {
-            clearable: false,
-            placeholder: '选择业务线',
-            items: this.categoryItems.map(e => {
-              return {
-                label: e.category,
-                value: e.category
-              }
-            })
-          }
-        }
-      ]
+    })
+    if (this.$route.query.name) {
+      this.activeName = this.$route.query.name
+    } else {
+      this.activeName = this.items[0].name
     }
   },
-  created () {
-    this.onInit()
-  },
   methods: {
-    async onInit () {
-      try {
-        const { data } = await getSalaryCalculateTemplate()
-        this.categoryItems = data
-        if (!data.length) {
-          return
-        }
-        this.searchValues.category = data[0].category
-        this.queryValues = { ...this.searchValues }
-        this.onChange()
-        this.onGetHistory()
-      } catch (error) {
-        this.$message.error(error)
-      }
-    },
-    async onGetHistory () {
-      try {
-        this.loading = true
-        const { data } = await getSalaryCalculateFiles(this.queryValues)
-        this.historyItems = data
-      } catch (error) {
-        this.$message.error(error)
-      } finally {
-        this.loading = false
-      }
-    },
-    onSearch (query) {
-      if (!query.category) {
-        this.searchValues.category = this.categoryItems[0]?.category ?? null
-      }
-      this.queryValues = { ...this.searchValues }
-      this.onGetHistory()
-    },
-    onChange () {
-      const items = this.categoryItems.find(e => e.category === this.queryValues.category)?.files ?? []
-      if (!items.length) {
-        this.filesValues = {}
-        return
-      }
-      this.filesValues = {
-        ...items.reduce((res, v) => {
-          res[v.fileType] = null
-          return res
-        }, {})
-      }
-    },
-    onExceed (files, fileList, key) {
-      this.filesValues[key] = files[0]
-    },
-    onImport (e, key) {
-      this.filesValues[key] = e.file
-    },
-    onRemove (key) {
-      this.filesValues[key] = null
-    },
-    onDelete (row) {
-      this.filesValues[row.fileType] = null
+    handleClick () {
+      this.$router.push(`${this.$route.path}?name=${this.activeName}`)
+      this.$nextTick(() => {
+        this.$refs[this.activeName][0].onInit()
+      })
     },
-    async onSave () {
-      if (Object.values(this.filesValues).every(e => e === null)) {
-        this.$message.warning('请先上传文件')
-        return
-      }
-
-      const h = this.$createElement
-      this.$confirm(h('div', [
-        h('p', undefined, '确定要更新文件吗?'),
-        h('p', undefined, `更新月份:${this.queryValues.month}`),
-        h('p', undefined, `更新物业线:${this.queryValues.category}`),
-        h('p', { style: 'color: red' }, '上传文件后,将覆盖之前的文件,请谨慎操作!')
-      ]), '提示', {
-        confirmButtonText: '确定',
-        cancelButtonText: '取消',
-        type: 'warning'
-      }).then(async () => {
-        this.submitLoading = true
-        try {
-          const formData = new FormData()
-          Object.keys(this.filesValues).forEach(key => {
-            if (!this.filesValues[key]) {
-              return
-            }
-            formData.append('files', this.filesValues[key])
-            formData.append('fileTypes', key)
-          })
-          formData.append('month', this.queryValues.month)
-          formData.append('category', this.queryValues.category)
-          await uploadSalaryCalculateFiles(formData)
-          this.$message.success('保存成功')
-          this.onGetHistory()
-        } catch (error) {
-          this.$message.error(error)
-        } finally {
-          this.submitLoading = false
-        }
-      }).catch(_ => {})
+    onComponentMounted () {
+      this.$refs[this.activeName] && this.$refs[this.activeName][0].onInit()
     }
   }
 }
 </script>
 
 <style lang="scss" scoped>
-.d-flex {
-  display: flex;
-}
-.content {
-  width: 50%;
-  min-width: 500px;
-  margin: 0 auto;
-}
-.buttons {
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-::v-deep .el-upload-list__item .el-icon-close {
-    display: inline-block;
-    &::after {
-      content: '移除';
-    }
-}
-.header {
-  display: flex;
-  justify-content: flex-end;
+::v-deep .el-tabs__content  {
+  overflow: visible !important;
 }
 </style>

+ 15 - 0
src/views/salary/calculate/salaryCalculateHistory/index.vue

@@ -0,0 +1,15 @@
+<template>
+  <div>
+
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'salaryCalculateHistory'
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 253 - 0
src/views/salary/calculate/salaryCalculateUpload/index.vue

@@ -0,0 +1,253 @@
+<template>
+  <div class="white">
+    <m-search :items="searchItems" v-model="searchValues" class="mb-3" @search="onSearch">
+      <template #button>
+        <m-button size="small" type="primary" plain icon="el-icon-finished" :loading="submitLoading" @click="onSave">提交待上传文件</m-button>
+        <m-button size="small" type="warning" plain icon="el-icon-s-promotion" :loading="runLoading" @click="onRun">执行</m-button>
+      </template>
+    </m-search>
+    <m-table
+      :headers="headers"
+      :items="items"
+      v-loading="loading"
+    >
+      <!-- <template #header>
+        <div class="header">
+          <m-button size="small" type="primary" icon="el-icon-finished" :loading="submitLoading" @click="onSave">提交当前临时文件</m-button>
+        </div>
+      </template> -->
+      <template #month>
+        {{ queryValues.month }}
+      </template>
+      <template #file="{ row }">
+        {{ filesValues[row.fileType]?.name }}
+      </template>
+      <template #actions="{ row }">
+        <div class="d-flex">
+          <el-upload
+            class="mr-3"
+            action="#"
+            :limit="1"
+            :show-file-list="false"
+            :on-exceed="(files, fileList) => onExceed(files, fileList, row.fileType)"
+            :http-request="e => onImport(e, row.fileType)"
+            :on-remove="() => onRemove(row.fileType)"
+          >
+            <m-button slot="trigger" text type="primary">导入文件</m-button>
+          </el-upload>
+          <m-button text type="danger" v-show="filesValues[row.fileType]" @click="onDelete(row)">移除临时文件</m-button>
+        </div>
+      </template>
+    </m-table>
+  </div>
+</template>
+
+<script>
+import {
+  getSalaryCalculateTemplate,
+  uploadSalaryCalculateFiles,
+  getSalaryCalculateFiles
+} from '@/api/salary'
+import { dateFormat } from '@/utils/date'
+export default {
+  name: 'salaryCalculateUpload',
+  data () {
+    return {
+      searchValues: {
+        month: dateFormat('YYYY-mm', new Date()),
+        category: null
+      },
+      queryValues: {},
+      filesValues: {},
+      categoryItems: [],
+      historyItems: [],
+      formItems: [],
+      headers: [
+        { label: '月份', prop: 'month', width: 100 },
+        { label: '业务线', prop: 'category' },
+        { label: '文件名', prop: 'fileName' },
+        { label: '文件', prop: 'history' },
+        { label: '待上传文件', prop: 'file' },
+        { label: '操作', prop: 'actions' }
+      ],
+      loading: false,
+      submitLoading: false,
+      runLoading: false
+    }
+  },
+  computed: {
+    items () {
+      const items = this.categoryItems.find(e => e.category === this.queryValues.category)?.files
+      if (!items) {
+        return []
+      }
+      return items.map(e => {
+        return {
+          ...e,
+          history: this.historyItems.find(h => h.fileType === e.fileType)?.fileOriginalFilename
+        }
+      })
+    },
+    searchItems () {
+      return [
+        {
+          label: '月份',
+          prop: 'month',
+          type: 'datePicker',
+          options: {
+            clearable: false,
+            type: 'month',
+            format: 'yyyy-MM',
+            valueFormat: 'yyyy-MM',
+            placeholder: '选择更新月份'
+          }
+        },
+        {
+          label: '业务线',
+          prop: 'category',
+          type: 'select',
+          options: {
+            clearable: false,
+            placeholder: '选择业务线',
+            items: this.categoryItems.map(e => {
+              return {
+                label: e.category,
+                value: e.category
+              }
+            })
+          }
+        }
+      ]
+    }
+  },
+  // created () {
+  //   this.onInit()
+  // },
+  methods: {
+    async onInit () {
+      try {
+        const { data } = await getSalaryCalculateTemplate()
+        this.categoryItems = data
+        if (!data.length) {
+          return
+        }
+        this.searchValues.category = data[0].category
+        this.queryValues = { ...this.searchValues }
+        this.onChange()
+        this.onGetHistory()
+      } catch (error) {
+        this.$message.error(error)
+      }
+    },
+    async onGetHistory () {
+      try {
+        this.loading = true
+        const { data } = await getSalaryCalculateFiles(this.queryValues)
+        this.historyItems = data
+      } catch (error) {
+        this.$message.error(error)
+      } finally {
+        this.loading = false
+      }
+    },
+    onSearch (query) {
+      if (!query.category) {
+        this.searchValues.category = this.categoryItems[0]?.category ?? null
+      }
+      this.queryValues = { ...this.searchValues }
+      this.onGetHistory()
+    },
+    onChange () {
+      const items = this.categoryItems.find(e => e.category === this.queryValues.category)?.files ?? []
+      if (!items.length) {
+        this.filesValues = {}
+        return
+      }
+      this.filesValues = {
+        ...items.reduce((res, v) => {
+          res[v.fileType] = null
+          return res
+        }, {})
+      }
+    },
+    onExceed (files, fileList, key) {
+      this.filesValues[key] = files[0]
+    },
+    onImport (e, key) {
+      this.filesValues[key] = e.file
+    },
+    onRemove (key) {
+      this.filesValues[key] = null
+    },
+    onDelete (row) {
+      this.filesValues[row.fileType] = null
+    },
+    async onSave () {
+      if (Object.values(this.filesValues).every(e => e === null)) {
+        this.$message.warning('请先上传文件')
+        return
+      }
+
+      const h = this.$createElement
+      this.$confirm(h('div', [
+        h('p', undefined, '确定要更新文件吗?'),
+        h('p', undefined, `更新月份:${this.queryValues.month}`),
+        h('p', undefined, `更新物业线:${this.queryValues.category}`),
+        h('p', { style: 'color: red' }, '上传文件后,将覆盖之前的文件,请谨慎操作!')
+      ]), '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        this.submitLoading = true
+        try {
+          const formData = new FormData()
+          Object.keys(this.filesValues).forEach(key => {
+            if (!this.filesValues[key]) {
+              return
+            }
+            formData.append('files', this.filesValues[key])
+            formData.append('fileTypes', key)
+          })
+          formData.append('month', this.queryValues.month)
+          formData.append('category', this.queryValues.category)
+          await uploadSalaryCalculateFiles(formData)
+          this.$message.success('保存成功')
+          this.onGetHistory()
+        } catch (error) {
+          this.$message.error(error)
+        } finally {
+          this.submitLoading = false
+        }
+      }).catch(_ => {})
+    },
+    onRun () {}
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.d-flex {
+  display: flex;
+}
+.content {
+  width: 50%;
+  min-width: 500px;
+  margin: 0 auto;
+}
+.buttons {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+::v-deep .el-upload-list__item .el-icon-close {
+    display: inline-block;
+    &::after {
+      content: '移除';
+    }
+}
+.header {
+  display: flex;
+  justify-content: flex-end;
+}
+</style>