zhangjie před 1 rokem
rodič
revize
f800a03d78

+ 2 - 0
components.d.ts

@@ -42,6 +42,7 @@ declare module 'vue' {
     TRadioGroup: typeof import('tdesign-vue-next')['RadioGroup']
     TRadioGroup: typeof import('tdesign-vue-next')['RadioGroup']
     TRow: typeof import('tdesign-vue-next')['Row']
     TRow: typeof import('tdesign-vue-next')['Row']
     TSelect: typeof import('tdesign-vue-next')['Select']
     TSelect: typeof import('tdesign-vue-next')['Select']
+    TSpace: typeof import('tdesign-vue-next')['Space']
     TSubmenu: typeof import('tdesign-vue-next')['Submenu']
     TSubmenu: typeof import('tdesign-vue-next')['Submenu']
     TSwitch: typeof import('tdesign-vue-next')['Switch']
     TSwitch: typeof import('tdesign-vue-next')['Switch']
     TTable: typeof import('tdesign-vue-next')['Table']
     TTable: typeof import('tdesign-vue-next')['Table']
@@ -54,5 +55,6 @@ declare module 'vue' {
     TTree: typeof import('tdesign-vue-next')['Tree']
     TTree: typeof import('tdesign-vue-next')['Tree']
     TTreeSelect: typeof import('tdesign-vue-next')['TreeSelect']
     TTreeSelect: typeof import('tdesign-vue-next')['TreeSelect']
     TUpload: typeof import('tdesign-vue-next')['Upload']
     TUpload: typeof import('tdesign-vue-next')['Upload']
+    UploadButton: typeof import('./src/components/common/upload-button/index.vue')['default']
   }
   }
 }
 }

+ 1 - 0
package.json

@@ -22,6 +22,7 @@
     "@vueuse/core": "^9.13.0",
     "@vueuse/core": "^9.13.0",
     "autoprefixer": "^10.4.14",
     "autoprefixer": "^10.4.14",
     "axios": "^1.2.1",
     "axios": "^1.2.1",
+    "crypto-js": "^4.1.1",
     "dayjs": "^1.11.7",
     "dayjs": "^1.11.7",
     "echarts": "^5.4.2",
     "echarts": "^5.4.2",
     "element-resize-detector": "^1.2.4",
     "element-resize-detector": "^1.2.4",

+ 11 - 0
src/api/work-hours.js

@@ -31,11 +31,22 @@ export const workAttendanceListApi = (data) =>
     url: '/api/system/work-attendance/list',
     url: '/api/system/work-attendance/list',
     data,
     data,
   });
   });
+export const workAttendanceInfoApi = (data) =>
+  request({
+    url: '/api/system/work-attendance/info',
+    data,
+  });
 export const workAttendanceSubmitApi = (ids) =>
 export const workAttendanceSubmitApi = (ids) =>
   request({
   request({
     url: '/api/system/work-attendance/submit',
     url: '/api/system/work-attendance/submit',
     data: { ids },
     data: { ids },
   });
   });
+export const workAttendanceExportApi = (ids) =>
+  request({
+    url: '/api/system/work-attendance/export',
+    data: { ids },
+    download: true,
+  });
 export const workAttendanceWithdrawApi = (id) =>
 export const workAttendanceWithdrawApi = (id) =>
   request({
   request({
     url: '/api/system/work-attendance/withdraw',
     url: '/api/system/work-attendance/withdraw',

+ 129 - 0
src/components/common/upload-button/index.vue

@@ -0,0 +1,129 @@
+<template>
+  <t-upload
+    class="upload-button"
+    theme="custom"
+    :size-limit="maxSize"
+    :accept="accept"
+    :disabled="disabled"
+    :before-upload="handleBeforeUpload"
+    :request-method="upload"
+    @fail="handleError"
+    @success="handleSuccess"
+    @validate="handleValidate"
+  >
+    <t-button theme="primary" v-bind="buttonProps"></t-button>
+  </t-upload>
+</template>
+
+<script setup name="UploadButton">
+import { MessagePlugin } from 'tdesign-vue-next';
+import { request } from '@/utils/request.js';
+import { getFileMD5 } from '@/utils/crypto.js';
+
+const props = defineProps({
+  accept: {
+    type: String,
+  },
+  format: {
+    type: Array,
+    default() {
+      return ['jpg', 'jpeg', 'png'];
+    },
+  },
+  uploadUrl: {
+    type: String,
+    required: true,
+  },
+  uploadData: {
+    type: Object,
+    default() {
+      return {};
+    },
+  },
+  maxSize: {
+    // 单位kb
+    type: Number,
+    default: 20 * 1024,
+  },
+  headers: {
+    type: Object,
+    default() {
+      return {};
+    },
+  },
+  buttonProps: {
+    type: Object,
+    default() {
+      return {};
+    },
+  },
+  disabled: {
+    type: Boolean,
+    default: false,
+  },
+});
+
+const emit = defineEmits(['on-fail', 'on-success', 'on-validate-error']);
+
+const checkFileFormat = (fileType) => {
+  const _file_format = fileType.split('.').pop().toLocaleLowerCase();
+  return props.format.length
+    ? props.format.some((item) => item.toLocaleLowerCase() === _file_format)
+    : true;
+};
+const handleBeforeUpload = (file) => {
+  if (file.size > props.maxSize * 1024) {
+    const size =
+      props.maxSize < 1024
+        ? `${props.maxSize}kb`
+        : `${Math.floor(props.maxSize / 1024)}M`;
+    const content = '文件大小不能超过' + size;
+    MessagePlugin.error(content);
+    return false;
+  }
+
+  if (!checkFileFormat(file.name)) {
+    const content = '只支持文件格式为' + props.format.join('/');
+    MessagePlugin.error(content);
+    return false;
+  }
+
+  return true;
+};
+
+const upload = async (file) => {
+  // console.log(file);
+  let formData = new FormData();
+  Object.entries(props.uploadData).forEach(([k, v]) => {
+    formData.append(k, v);
+  });
+  formData.append('file', file.raw);
+
+  const md5 = await getFileMD5(file.raw);
+  // console.log(md5);
+  const res = await request({
+    url: props.uploadUrl,
+    data: formData,
+    headers: { ...props.headers, md5 },
+  }).catch(() => {});
+
+  if (res) {
+    return { status: 'success', response: res };
+  } else {
+    return { status: 'fail', error: '上传失败' };
+  }
+};
+
+const handleValidate = (data) => {
+  console.log(data);
+  emit('on-validate-error', data);
+};
+const handleError = (error) => {
+  console.log(error);
+  emit('on-fail', error);
+};
+const handleSuccess = (data) => {
+  console.log(data);
+  emit('on-success', data);
+};
+</script>

+ 69 - 0
src/utils/crypto.js

@@ -0,0 +1,69 @@
+// const CryptoJS = require('crypto-js');
+import Base64 from 'crypto-js/enc-base64';
+import Utf8 from 'crypto-js/enc-utf8';
+import AES from 'crypto-js/aes';
+import SHA1 from 'crypto-js/sha1';
+import MD5 from 'crypto-js/md5';
+
+export const getBase64 = (content) => {
+  const words = Utf8.parse(content);
+  const base64Str = Base64.stringify(words);
+
+  return base64Str;
+};
+
+export const getAES = (content) => {
+  const KEY = '1234567890123456';
+  const IV = '1234567890123456';
+
+  var key = Utf8.parse(KEY);
+  var iv = Utf8.parse(IV);
+  var encrypted = AES.encrypt(content, key, { iv: iv });
+  return encrypted.toString();
+};
+
+/**
+ * 获取authorisation
+ * @param {Object} infos 相关信息
+ * @param {String} type 类别:secret、token两种
+ */
+export const getAuthorization = (infos, type) => {
+  // {type} {invoker}:base64(sha1(method&uri&timestamp&{secret}))
+  if (type === 'secret') {
+    // accessKey | method&uri&timestamp&accessSecret
+    const str = `${infos.method.toLowerCase()}&${infos.uri}&${
+      infos.timestamp
+    }&${infos.accessSecret}`;
+    const sign = Base64.stringify(SHA1(str));
+    return `Secret ${infos.accessKey}:${sign}`;
+  } else if (type === 'token') {
+    // userId | method&uri&timestamp&token
+    const str = `${infos.method.toLowerCase()}&${infos.uri}&${
+      infos.timestamp
+    }&${infos.token}`;
+    const sign = Base64.stringify(SHA1(str));
+    return `Token ${infos.account}:${sign}`;
+  }
+};
+
+/**
+ *
+ * @param {any} str 字符串
+ */
+export const getMD5 = (content) => {
+  return MD5(content);
+};
+
+export const getFileMD5 = (file) => {
+  return new Promise((resolve, reject) => {
+    const reader = new FileReader();
+    reader.onloadend = function () {
+      const arrayBuffer = reader.result;
+      resolve(MD5(arrayBuffer).toString());
+    };
+    reader.onerror = function (err) {
+      reject(err);
+    };
+    reader.readAsArrayBuffer(file);
+  });
+};

+ 1 - 1
src/views/system/config-manage/checkin-manage/index.vue

@@ -129,7 +129,7 @@ const handleEdit = (row) => {
 };
 };
 const handleDelete = (row) => {
 const handleDelete = (row) => {
   const confirmDia = DialogPlugin({
   const confirmDia = DialogPlugin({
-    header: '操作警告',
+    header: '操作提示',
     body: `确定要删除当前记录吗`,
     body: `确定要删除当前记录吗`,
     confirmBtn: '确定',
     confirmBtn: '确定',
     cancelBtn: '取消',
     cancelBtn: '取消',

+ 11 - 4
src/views/system/config-manage/customer-manage/index.vue

@@ -2,10 +2,16 @@
   <div class="registration-query flex flex-col h-full">
   <div class="registration-query flex flex-col h-full">
     <SearchForm :fields="fields" :params="params"></SearchForm>
     <SearchForm :fields="fields" :params="params"></SearchForm>
     <div class="flex-1 page-wrap">
     <div class="flex-1 page-wrap">
-      <div class="btn-group">
+      <t-space size="small">
         <t-button theme="success" @click="handleAdd">新增</t-button>
         <t-button theme="success" @click="handleAdd">新增</t-button>
-        <t-button theme="success" @click="handleImport">批量导入</t-button>
-      </div>
+        <upload-button
+          upload-url="/api/upload"
+          :button-props="{
+            content: '批量导入',
+          }"
+          :max-size="512"
+        ></upload-button>
+      </t-space>
       <t-table
       <t-table
         size="small"
         size="small"
         row-key="id"
         row-key="id"
@@ -36,6 +42,7 @@ import { ref, reactive } from 'vue';
 import { DialogPlugin, MessagePlugin } from 'tdesign-vue-next';
 import { DialogPlugin, MessagePlugin } from 'tdesign-vue-next';
 import useFetchTable from '@/hooks/useFetchTable';
 import useFetchTable from '@/hooks/useFetchTable';
 import EditCustomerDialog from './edit-customer-dialog.vue';
 import EditCustomerDialog from './edit-customer-dialog.vue';
+import UploadButton from '@/components/common/upload-button/index.vue';
 import { customerListApi, customerDeleteApi } from '@/api/system';
 import { customerListApi, customerDeleteApi } from '@/api/system';
 const showEditCustomerDialog = ref(false);
 const showEditCustomerDialog = ref(false);
 const curRow = ref(null);
 const curRow = ref(null);
@@ -159,7 +166,7 @@ const handleImport = () => {
 };
 };
 const handleDelete = (row) => {
 const handleDelete = (row) => {
   const confirmDia = DialogPlugin({
   const confirmDia = DialogPlugin({
-    header: '操作警告',
+    header: '操作提示',
     body: `确定要删除当前记录吗`,
     body: `确定要删除当前记录吗`,
     confirmBtn: '确定',
     confirmBtn: '确定',
     cancelBtn: '取消',
     cancelBtn: '取消',

+ 1 - 1
src/views/system/config-manage/device-manage/index.vue

@@ -89,7 +89,7 @@ const handleAdd = () => {
 };
 };
 const handleDistroy = (row) => {
 const handleDistroy = (row) => {
   const confirmDia = DialogPlugin({
   const confirmDia = DialogPlugin({
-    header: '操作警告',
+    header: '操作提示',
     body: `确定要作废选中的记录吗`,
     body: `确定要作废选中的记录吗`,
     confirmBtn: '确定',
     confirmBtn: '确定',
     cancelBtn: '取消',
     cancelBtn: '取消',

+ 1 - 1
src/views/system/config-manage/service-level-manage/index.vue

@@ -90,7 +90,7 @@ const handleEdit = (row) => {
 };
 };
 const handleDelete = (row) => {
 const handleDelete = (row) => {
   const confirmDia = DialogPlugin({
   const confirmDia = DialogPlugin({
-    header: '操作警告',
+    header: '操作提示',
     body: `确定要删除当前记录吗`,
     body: `确定要删除当前记录吗`,
     confirmBtn: '确定',
     confirmBtn: '确定',
     cancelBtn: '取消',
     cancelBtn: '取消',

+ 3 - 3
src/views/system/notice-log/notice-manage/index.vue

@@ -183,7 +183,7 @@ const handleDestroy = () => {
   }
   }
 
 
   const confirmDia = DialogPlugin({
   const confirmDia = DialogPlugin({
-    header: '操作警告',
+    header: '操作提示',
     body: `确定要作废当前选择的所有记录吗`,
     body: `确定要作废当前选择的所有记录吗`,
     confirmBtn: '确定',
     confirmBtn: '确定',
     cancelBtn: '取消',
     cancelBtn: '取消',
@@ -198,7 +198,7 @@ const handleDestroy = () => {
 };
 };
 const handleCancelPublish = (row) => {
 const handleCancelPublish = (row) => {
   const confirmDia = DialogPlugin({
   const confirmDia = DialogPlugin({
-    header: '操作警告',
+    header: '操作提示',
     body: `确定要撤销发布当前信息吗`,
     body: `确定要撤销发布当前信息吗`,
     confirmBtn: '确定',
     confirmBtn: '确定',
     cancelBtn: '取消',
     cancelBtn: '取消',
@@ -220,7 +220,7 @@ const handleEdit = (row) => {
 };
 };
 const handlePublish = (row) => {
 const handlePublish = (row) => {
   const confirmDia = DialogPlugin({
   const confirmDia = DialogPlugin({
-    header: '操作警告',
+    header: '操作提示',
     body: `确定要发布当前信息吗`,
     body: `确定要发布当前信息吗`,
     confirmBtn: '确定',
     confirmBtn: '确定',
     cancelBtn: '取消',
     cancelBtn: '取消',

+ 1 - 1
src/views/work-hours/work-hours-manage/abnormal-check/wait-check.vue

@@ -184,7 +184,7 @@ const handleAudit = async (selectedIds, pass) => {
       : `确定要${actionName}当前记录吗`;
       : `确定要${actionName}当前记录吗`;
 
 
   const confirmDia = DialogPlugin({
   const confirmDia = DialogPlugin({
-    header: '操作警告',
+    header: '操作提示',
     body: warningBody,
     body: warningBody,
     confirmBtn: '确定',
     confirmBtn: '确定',
     cancelBtn: '取消',
     cancelBtn: '取消',

+ 57 - 13
src/views/work-hours/work-hours-manage/work-attendance/index.vue

@@ -3,14 +3,30 @@
     <SearchForm :fields="fields" :params="params"></SearchForm>
     <SearchForm :fields="fields" :params="params"></SearchForm>
 
 
     <div class="flex-1 page-wrap">
     <div class="flex-1 page-wrap">
-      <div class="btn-group">
-        <t-button
-          theme="success"
-          :disabled="!selectedRowKeys.length"
-          @click="multSubmit"
-          >批量提交</t-button
-        >
+      <div class="flex justify-between items-center">
+        <t-space>
+          <span>考勤总计:{{ statisticsInfo.a }}</span>
+          <span>已提交:{{ statisticsInfo.b }}</span>
+          <span>待提交:{{ statisticsInfo.c }}</span>
+          <span>累计人天:{{ statisticsInfo.d }}天</span>
+          <span>累计工时:{{ statisticsInfo.e }}小时</span>
+        </t-space>
+        <div class="btn-group">
+          <t-button
+            theme="success"
+            :disabled="!selectedRowKeys.length"
+            @click="multSubmit"
+            >批量提交</t-button
+          >
+          <t-button
+            theme="success"
+            :disabled="!selectedRowKeys.length"
+            @click="multExport"
+            >批量导出</t-button
+          >
+        </div>
       </div>
       </div>
+
       <t-table
       <t-table
         size="small"
         size="small"
         row-key="id"
         row-key="id"
@@ -39,7 +55,9 @@ import { DialogPlugin, MessagePlugin } from 'tdesign-vue-next';
 import useFetchTable from '@/hooks/useFetchTable';
 import useFetchTable from '@/hooks/useFetchTable';
 import {
 import {
   workAttendanceListApi,
   workAttendanceListApi,
+  workAttendanceInfoApi,
   workAttendanceSubmitApi,
   workAttendanceSubmitApi,
+  workAttendanceExportApi,
   workAttendanceWithdrawApi,
   workAttendanceWithdrawApi,
   workAttendanceCancelWithdrawApi,
   workAttendanceCancelWithdrawApi,
 } from '@/api/work-hours';
 } from '@/api/work-hours';
@@ -123,6 +141,11 @@ const columns = [
 const { pagination, tableData, fetchData, search, onChange } = useFetchTable(
 const { pagination, tableData, fetchData, search, onChange } = useFetchTable(
   workAttendanceListApi
   workAttendanceListApi
 );
 );
+let statisticsInfo = reactive({ a: 1, b: 2, c: 3, d: 4, e: 5 });
+const getStatisticsInfo = async () => {
+  const res = await workAttendanceInfoApi(params);
+  statisticsInfo = res.data || {};
+};
 
 
 const fields = ref([
 const fields = ref([
   {
   {
@@ -161,6 +184,7 @@ const fields = ref([
         text: '查询',
         text: '查询',
         onClick: () => {
         onClick: () => {
           search();
           search();
+          getStatisticsInfo();
         },
         },
       },
       },
     ],
     ],
@@ -229,7 +253,7 @@ const multSubmit = () => {
     return;
     return;
   }
   }
   const confirmDia = DialogPlugin({
   const confirmDia = DialogPlugin({
-    header: '操作警告',
+    header: '操作提示',
     body: `确定要提交选择的所有记录吗?`,
     body: `确定要提交选择的所有记录吗?`,
     confirmBtn: '确定',
     confirmBtn: '确定',
     cancelBtn: '取消',
     cancelBtn: '取消',
@@ -244,11 +268,31 @@ const multSubmit = () => {
     },
     },
   });
   });
 };
 };
+const multExport = () => {
+  if (!selectedRowKeys.value.length) {
+    MessagePlugin.error('请选择要导出的记录');
+    return;
+  }
+  const confirmDia = DialogPlugin({
+    header: '操作提示',
+    body: `确定要导出选择的所有记录吗?`,
+    confirmBtn: '确定',
+    cancelBtn: '取消',
+    onConfirm: async () => {
+      confirmDia.hide();
+      const res = await workAttendanceExportApi(selectedRowKeys.value).catch(
+        () => {}
+      );
+      if (!res) return;
+      MessagePlugin.success('开始下载');
+    },
+  });
+};
 
 
 const handleSubmit = (row) => {
 const handleSubmit = (row) => {
   const confirmDia = DialogPlugin({
   const confirmDia = DialogPlugin({
-    header: '操作警告',
-    body: `确定要提交当前记录吗?`,
+    header: '提交提示',
+    body: `该工时数据提交后就可以进行工时统计结算,且该工时数据无法进行任何修改操作,您要继续提交吗?`,
     confirmBtn: '确定',
     confirmBtn: '确定',
     cancelBtn: '取消',
     cancelBtn: '取消',
     onConfirm: async () => {
     onConfirm: async () => {
@@ -262,8 +306,8 @@ const handleSubmit = (row) => {
 };
 };
 const handleWithdraw = (row) => {
 const handleWithdraw = (row) => {
   const confirmDia = DialogPlugin({
   const confirmDia = DialogPlugin({
-    header: '操作警告',
-    body: `确定要撤回当前记录吗?`,
+    header: '撤回提示',
+    body: `您确定要撤回这条已提交的工时数据吗?您操作撤回后,还需要工时管理人员同意撤回,这条工时数据才能被真正撤回。`,
     confirmBtn: '确定',
     confirmBtn: '确定',
     cancelBtn: '取消',
     cancelBtn: '取消',
     onConfirm: async () => {
     onConfirm: async () => {
@@ -277,7 +321,7 @@ const handleWithdraw = (row) => {
 };
 };
 const handleCancelWithdraw = (row) => {
 const handleCancelWithdraw = (row) => {
   const confirmDia = DialogPlugin({
   const confirmDia = DialogPlugin({
-    header: '操作警告',
+    header: '操作提示',
     body: `确定要取消撤回当前记录吗?`,
     body: `确定要取消撤回当前记录吗?`,
     confirmBtn: '确定',
     confirmBtn: '确定',
     cancelBtn: '取消',
     cancelBtn: '取消',

+ 7 - 7
src/views/work-hours/work-hours-manage/work-statistics/index.vue

@@ -2,13 +2,13 @@
   <div class="work-statistics flex flex-col h-full">
   <div class="work-statistics flex flex-col h-full">
     <SearchForm :fields="fields" :params="params"></SearchForm>
     <SearchForm :fields="fields" :params="params"></SearchForm>
     <div class="flex-1 page-wrap">
     <div class="flex-1 page-wrap">
-      <p>
+      <t-space>
         <span>考勤总计:{{ statisticsInfo.a }}</span>
         <span>考勤总计:{{ statisticsInfo.a }}</span>
-        <span class="m-l-20px">已提交:{{ statisticsInfo.b }}</span>
-        <span class="m-l-20px">待提交:{{ statisticsInfo.c }}</span>
-        <span class="m-l-20px">已提交累计人天:{{ statisticsInfo.d }}天</span>
-        <span class="m-l-20px">已提交累计工时:{{ statisticsInfo.e }}小时</span>
-      </p>
+        <span>已提交:{{ statisticsInfo.b }}</span>
+        <span>待提交:{{ statisticsInfo.c }}</span>
+        <span>已提交累计人天:{{ statisticsInfo.d }}天</span>
+        <span>已提交累计工时:{{ statisticsInfo.e }}小时</span>
+      </t-space>
       <t-table
       <t-table
         size="small"
         size="small"
         row-key="id"
         row-key="id"
@@ -85,7 +85,7 @@ const { pagination, tableData, fetchData, search, onChange } = useFetchTable(
   workStatisticsListApi
   workStatisticsListApi
 );
 );
 
 
-const statisticsInfo = reactive({ a: 1, b: 2, c: 3, d: 4, e: 5 });
+let statisticsInfo = reactive({ a: 1, b: 2, c: 3, d: 4, e: 5 });
 const getStatisticsInfo = async () => {
 const getStatisticsInfo = async () => {
   const res = await workStatisticsInfoApi(params);
   const res = await workStatisticsInfoApi(params);
   statisticsInfo = res.data || {};
   statisticsInfo = res.data || {};