index.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. <template>
  2. <el-card>
  3. <template #header>
  4. <div class="card-header">
  5. <PlusForm
  6. :key="queryKey"
  7. v-model="queryState"
  8. :group="queryGroup"
  9. label-position="right"
  10. >
  11. <template #footer="{}">
  12. <div></div>
  13. </template>
  14. <template #plus-group-one="scoped">
  15. <div>
  16. <el-date-picker
  17. v-model="queryState.queryTime"
  18. type="datetimerange"
  19. :shortcuts="queryTimeShortCuts"
  20. range-separator="至"
  21. start-placeholder="开始日期时间"
  22. end-placeholder="结束日期时间"
  23. :popper-options="{
  24. placement: 'bottom-start'
  25. }"
  26. :size="dynamicSize"
  27. :disabled="size === 'disabled'"
  28. />
  29. <el-button
  30. style="margin-left: 10px"
  31. type="primary"
  32. :icon="useRenderIcon(Search)"
  33. @click="handleQuery"
  34. >查询</el-button
  35. >
  36. <el-button
  37. :icon="useRenderIcon(IconDownLoad)"
  38. @click="handleExport"
  39. >导出到Excel</el-button
  40. >
  41. <el-button type="primary" @click="isHideInGroup = !isHideInGroup">
  42. {{ isHideInGroup ? "显示筛选器" : "隐藏筛选器" }}
  43. </el-button>
  44. <el-button
  45. :icon="useRenderIcon(IconUpload)"
  46. @click="updateAttends"
  47. >更新到服务器</el-button
  48. >
  49. </div>
  50. </template>
  51. </PlusForm>
  52. </div>
  53. </template>
  54. <div>
  55. <pure-table
  56. align-whole="center"
  57. :header-cell-style="{
  58. background: 'var(--el-fill-color-light)',
  59. color: 'var(--el-text-color-primary)'
  60. }"
  61. :columns="columns"
  62. :table-key="tableKey"
  63. border
  64. adaptive
  65. :adaptiveConfig="adaptiveConfig"
  66. showOverflowTooltip
  67. :loading="false"
  68. :loading-config="loadingConfig"
  69. :data="
  70. dataFilter.slice(
  71. (pagination.currentPage - 1) * pagination.pageSize,
  72. pagination.currentPage * pagination.pageSize
  73. )
  74. "
  75. :pagination="pagination"
  76. @page-size-change="onSizeChange"
  77. @page-current-change="onCurrentChange"
  78. :has-toolbar="true"
  79. >
  80. <template #toolbar>
  81. <el-button plain size="small">查看日志</el-button>
  82. <el-button plain size="small">导出数据</el-button>
  83. <el-button type="primary" size="small">创建应用</el-button>
  84. </template>
  85. <template #operation="{ row, index }">
  86. <div v-if="!editMap[index]?.editable">
  87. <el-button
  88. class="reset-margin"
  89. link
  90. type="primary"
  91. :icon="useRenderIcon(Delete)"
  92. @click="onDel(row)"
  93. >
  94. 删除
  95. </el-button>
  96. <el-button
  97. class="reset-margin"
  98. link
  99. type="primary"
  100. @click="onEdit(row, index)"
  101. :icon="useRenderIcon(EditFill)"
  102. >
  103. 修改
  104. </el-button>
  105. </div>
  106. <div v-if="editMap[index]?.editable">
  107. <el-button
  108. class="reset-margin"
  109. link
  110. type="primary"
  111. @click="onSave(index)"
  112. :icon="useRenderIcon(SaveFill)"
  113. >
  114. 保存
  115. </el-button>
  116. <el-button
  117. class="reset-margin"
  118. link
  119. @click="onCancel(index)"
  120. :icon="useRenderIcon(Close)"
  121. >
  122. 取消
  123. </el-button>
  124. </div>
  125. </template>
  126. </pure-table>
  127. </div>
  128. </el-card>
  129. </template>
  130. <script setup lang="ts">
  131. import { ref, onMounted, computed, watch } from "vue";
  132. import { useColumns } from "./columns";
  133. import {
  134. type PlusColumn,
  135. type FieldValues,
  136. PlusForm,
  137. PlusFormGroupRow
  138. } from "plus-pro-components";
  139. import { useRenderIcon } from "@/components/ReIcon/src/hooks";
  140. import AddFill from "~icons/ep/plus";
  141. import Delete from "~icons/ep/delete";
  142. import EditFill from "~icons/ep/edit";
  143. import SaveFill from "~icons/bi/save";
  144. import Close from "~icons/ep/close";
  145. import Search from "~icons/ep/search";
  146. import IconDownLoad from "~icons/ep/download";
  147. import IconUpload from "~icons/ep/upload";
  148. import { ElNotification, ElMessageBox } from "element-plus";
  149. import * as XLSX from "xlsx";
  150. import Attend from "@/model/attend";
  151. import {
  152. fetchHisAttend,
  153. deleteHisAttend,
  154. modifyHisAttends
  155. } from "@/api/attend";
  156. import { debounce } from "lodash";
  157. const queryTimeShortCuts = [
  158. {
  159. text: "今天",
  160. value: () => {
  161. const now = new Date();
  162. const start = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  163. const end = new Date(
  164. now.getFullYear(),
  165. now.getMonth(),
  166. now.getDate() + 1
  167. );
  168. return [start, end];
  169. }
  170. },
  171. {
  172. text: "昨天",
  173. value: () => {
  174. const now = new Date();
  175. const start = new Date(
  176. now.getFullYear(),
  177. now.getMonth(),
  178. now.getDate() - 1
  179. );
  180. const end = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  181. return [start, end];
  182. }
  183. },
  184. {
  185. text: "本周",
  186. value: () => {
  187. const now = new Date();
  188. const start = new Date(
  189. now.getFullYear(),
  190. now.getMonth(),
  191. now.getDate() - now.getDay()
  192. );
  193. const end = new Date(
  194. now.getFullYear(),
  195. now.getMonth(),
  196. now.getDate() - now.getDay() + 6
  197. );
  198. return [start, end];
  199. }
  200. },
  201. {
  202. text: "本月",
  203. value: () => {
  204. const now = new Date();
  205. const start = new Date(now.getFullYear(), now.getMonth(), 1);
  206. const end = new Date(now.getFullYear(), now.getMonth() + 1, 0);
  207. return [start, end];
  208. }
  209. }
  210. ];
  211. const isHideInGroup = ref(true);
  212. const QC_IDX_PROJ = 0;
  213. const queryGroup: PlusFormGroupRow[] = [
  214. {
  215. title: "记录搜索",
  216. icon: Search,
  217. // 自动生成 plus-group-one 插槽,v0.1.25 新增
  218. name: "one",
  219. // 当plus-group-one 插槽存在时,这里的配置将不会生效
  220. columns: [
  221. {
  222. label: "名称",
  223. prop: "name"
  224. }
  225. ]
  226. },
  227. {
  228. title: "记录筛选",
  229. // icon: Calendar,
  230. name: "two",
  231. hideInGroup: computed(() => {
  232. return isHideInGroup.value;
  233. }),
  234. columns: [
  235. {
  236. label: "项目名称",
  237. width: 120,
  238. prop: "queryProjectName",
  239. valueType: "input",
  240. colProps: {
  241. span: 6
  242. }
  243. },
  244. {
  245. label: "区域名称",
  246. width: 120,
  247. prop: "queryLocationName",
  248. valueType: "input",
  249. colProps: {
  250. span: 6
  251. }
  252. },
  253. {
  254. label: "人员名称",
  255. width: 120,
  256. prop: "queryPersonName",
  257. valueType: "input",
  258. colProps: {
  259. span: 6
  260. }
  261. },
  262. {
  263. label: "电话",
  264. width: 120,
  265. prop: "queryPhone",
  266. valueType: "input",
  267. colProps: {
  268. span: 6
  269. }
  270. }
  271. ]
  272. }
  273. ];
  274. const dynamicSize = ref();
  275. const queryState = ref({
  276. queryTime: [
  277. new Date(
  278. new Date().getFullYear(),
  279. new Date().getMonth(),
  280. new Date().getDate()
  281. ),
  282. new Date(
  283. new Date().getFullYear(),
  284. new Date().getMonth(),
  285. new Date().getDate() + 1
  286. )
  287. ],
  288. queryProjectName: "",
  289. queryLocationName: "",
  290. queryPersonName: "",
  291. queryPhone: ""
  292. });
  293. const queryKey = ref(0);
  294. // 防抖处理,500ms内连续输入只执行一次
  295. const debouncedDoFilter = debounce(() => {
  296. doFilter();
  297. }, 200);
  298. // 监听变化并使用防抖处理
  299. watch(
  300. [
  301. () => queryState.value.queryPersonName,
  302. () => queryState.value.queryProjectName,
  303. () => queryState.value.queryLocationName,
  304. () => queryState.value.queryPhone
  305. ],
  306. debouncedDoFilter
  307. );
  308. const projects = ref([]);
  309. const {
  310. editMap,
  311. columns,
  312. dataList,
  313. dataFilter,
  314. pagination,
  315. loadingConfig,
  316. adaptiveConfig,
  317. getProjectName,
  318. getLocName,
  319. onEdit,
  320. onSave,
  321. onCancel,
  322. onDel,
  323. onSizeChange,
  324. onCurrentChange
  325. } = useColumns(projects);
  326. const swEditable = ref(true);
  327. const tableKey = ref(0);
  328. const vGlobal = window.vueGlobal;
  329. const dlgImportExcelVisible = ref(false);
  330. const excelData = ref([]);
  331. const attends = ref([]);
  332. const reloadProjects = () => {
  333. vGlobal.refreshProjects(() => {
  334. projects.value = [];
  335. projects.value = vGlobal.vecProject.slice(0);
  336. console.log("reloadProjects 1", projects.value);
  337. queryKey.value += 1;
  338. });
  339. };
  340. const getAttendRecord = () => {
  341. const queryParams = {
  342. beginTime: queryState.value.queryTime[0].getTime(),
  343. endTime: queryState.value.queryTime[1].getTime()
  344. };
  345. fetchHisAttend(queryParams).then(res => {
  346. if (res.success) {
  347. attends.value = [];
  348. dataList.value = [];
  349. console.log("res ", res.items);
  350. for (const item of res.items) {
  351. const obj = JSON.parse(item.jstr);
  352. const attend = new Attend(obj);
  353. attends.value.push(attend);
  354. attend.ancAddr = Number(item["ancAddr"]) || 0;
  355. for (const attendItem of attend.items) {
  356. dataList.value.push(attendItem);
  357. }
  358. }
  359. doFilter();
  360. ElNotification.success({
  361. message: "查询成功"
  362. });
  363. }
  364. });
  365. };
  366. const doFilter = () => {
  367. dataFilter.value = dataList.value.filter(item => {
  368. if (
  369. queryState.value.queryPersonName.length > 0 &&
  370. item.name.indexOf(queryState.value.queryPersonName) < 0
  371. ) {
  372. return false;
  373. }
  374. if (
  375. queryState.value.queryProjectName.length > 0 &&
  376. getProjectName(item.projId).indexOf(queryState.value.queryProjectName) < 0
  377. ) {
  378. return false;
  379. }
  380. if (
  381. queryState.value.queryLocationName.length > 0 &&
  382. getLocName(item.projId, item.locId).indexOf(
  383. queryState.value.queryLocationName
  384. ) < 0
  385. ) {
  386. return false;
  387. }
  388. if (
  389. queryState.value.queryPhone.length > 0 &&
  390. item.phone.indexOf(queryState.value.queryPhone) < 0
  391. ) {
  392. return false;
  393. }
  394. return true;
  395. });
  396. pagination.total = dataFilter.value.length;
  397. // tableKey.value++;
  398. };
  399. const updateAttends = () => {
  400. ElMessageBox.confirm("确定更新考勤数据?更新后将无法撤回", "Warning", {
  401. confirmButtonText: "确认",
  402. cancelButtonText: "取消",
  403. type: "warning"
  404. })
  405. .then(() => {
  406. ElNotification.info({
  407. message: "开始更新"
  408. });
  409. const vecAttend = [];
  410. for (const v of attends.value) {
  411. vecAttend.push(v.toJson());
  412. }
  413. modifyHisAttends({ attends: vecAttend }).then(res => {
  414. if (res.success) {
  415. ElNotification.success({
  416. message: "更新成功"
  417. });
  418. } else {
  419. ElNotification.error({
  420. message: "更新失败"
  421. });
  422. }
  423. });
  424. })
  425. .catch(() => {
  426. // ElMessage({
  427. // type: 'info',
  428. // message: 'Delete canceled',
  429. // })
  430. });
  431. };
  432. // 导出逻辑
  433. const handleExport = () => {
  434. try {
  435. // 1. 处理导出数据
  436. const exportData = dataFilter.value.map(item => ({
  437. 时间: new Date(item.tm).toLocaleString(),
  438. 项目名称: getProjectName(item.projId),
  439. 区域名称: getLocName(item.projId, item.locId),
  440. 人员ID: item.id,
  441. 人员名称: item.name,
  442. 职位: item.job,
  443. 电话: item.phone,
  444. 标签ID: item.tagId,
  445. 在线状态: item.rssi != 0 ? "在线" : "离线",
  446. 到岗状态: item.confirm ? "到岗" : "缺岗"
  447. }));
  448. if (exportData.length === 0) {
  449. ElNotification.warning({ message: "没有可导出的数据" });
  450. return;
  451. }
  452. // 2. 创建工作表
  453. const worksheet = XLSX.utils.json_to_sheet(exportData);
  454. // 3. 计算列宽(关键步骤)
  455. const header = Object.keys(exportData[0]);
  456. const cols = header.map(key => {
  457. // 计算表头文字长度
  458. let maxLength = key.length * 2;
  459. // 计算数据列最大文字长度
  460. exportData.forEach(row => {
  461. const value = String(row[key] || "");
  462. // 中文字符宽度约为英文字符的2倍,这里做特殊处理
  463. const length = value.replace(/[^\x00-\xff]/g, "aa").length;
  464. if (length > maxLength) {
  465. maxLength = length;
  466. }
  467. });
  468. // 增加缓冲宽度(避免内容太满)
  469. return { width: maxLength + 2 };
  470. });
  471. // 设置列宽配置
  472. worksheet["!cols"] = cols;
  473. // 4. 创建工作簿并添加工作表
  474. const workbook = XLSX.utils.book_new();
  475. XLSX.utils.book_append_sheet(workbook, worksheet, "考勤记录");
  476. // 5. 生成Excel文件并下载
  477. const fileName = `考勤记录_${new Date().getTime()}.xlsx`;
  478. XLSX.writeFile(workbook, fileName);
  479. } catch (error) {
  480. console.error("导出失败:", error);
  481. ElNotification.error({
  482. message: "导出失败,请重试"
  483. });
  484. }
  485. };
  486. const handleQuery = () => {
  487. getAttendRecord();
  488. };
  489. onMounted(() => {
  490. reloadProjects();
  491. });
  492. </script>