修改bug

dev-tmp1
huwei 2026-08-14 18:14:12 +08:00
parent 1b69c4938a
commit 8e4d8b2d76
3 changed files with 112 additions and 16 deletions

View File

@ -28,18 +28,19 @@
"@eigenpal/docx-editor-react": "^1.9.0", "@eigenpal/docx-editor-react": "^1.9.0",
"@rc-component/motion": "^1.0.0", "@rc-component/motion": "^1.0.0",
"@rc-component/util": "^1.11.1", "@rc-component/util": "^1.11.1",
"pizzip": "latest",
"docxtemplater": "latest",
"antd": "^5.29.2", "antd": "^5.29.2",
"china-division": "^2.7.0", "china-division": "^2.7.0",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"docxtemplater": "latest",
"echarts": "^6.1.0", "echarts": "^6.1.0",
"history": "^4.10.1", "history": "^4.10.1",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"jspdf": "^2.5.2", "jspdf": "^2.5.2",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"pizzip": "latest",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"xlsx": "^0.18.5",
"zy-react-library": "^1.2.3" "zy-react-library": "^1.2.3"
}, },
"devDependencies": { "devDependencies": {

View File

@ -68,6 +68,9 @@ importers:
react-dom: react-dom:
specifier: ^18.2.0 specifier: ^18.2.0
version: 18.2.0(react@18.2.0) version: 18.2.0(react@18.2.0)
xlsx:
specifier: ^0.18.5
version: 0.18.5
zy-react-library: zy-react-library:
specifier: ^1.2.3 specifier: ^1.2.3
version: 1.3.26(@types/react@19.2.17)(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.0.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.16)(slate@0.72.0)(snabbdom@3.6.4))(debug@4.4.3(supports-color@8.0.0))(prop-types@15.8.1)(rc-field-form@2.7.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react-router@5.1.2(react@18.2.0))(supports-color@8.0.0) version: 1.3.26(@types/react@19.2.17)(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.0.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.16)(slate@0.72.0)(snabbdom@3.6.4))(debug@4.4.3(supports-color@8.0.0))(prop-types@15.8.1)(rc-field-form@2.7.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react-router@5.1.2(react@18.2.0))(supports-color@8.0.0)
@ -2570,6 +2573,10 @@ packages:
add-dom-event-listener@1.1.0: add-dom-event-listener@1.1.0:
resolution: {integrity: sha512-WCxx1ixHT0GQU9hb0KI/mhgRQhnU+U3GvwY6ZvVjYq8rsihIGoaIOUbY0yMPBxLH5MDtr0kz3fisWGNcbWW7Jw==} resolution: {integrity: sha512-WCxx1ixHT0GQU9hb0KI/mhgRQhnU+U3GvwY6ZvVjYq8rsihIGoaIOUbY0yMPBxLH5MDtr0kz3fisWGNcbWW7Jw==}
adler-32@1.3.1:
resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==}
engines: {node: '>=0.8'}
agent-base@6.0.0: agent-base@6.0.0:
resolution: {integrity: sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw==} resolution: {integrity: sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw==}
engines: {node: '>= 6.0.0'} engines: {node: '>= 6.0.0'}
@ -2797,6 +2804,10 @@ packages:
ccount@2.0.1: ccount@2.0.1:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
cfb@1.2.2:
resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==}
engines: {node: '>=0.8'}
chalk@2.4.2: chalk@2.4.2:
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
engines: {node: '>=4'} engines: {node: '>=4'}
@ -2857,6 +2868,10 @@ packages:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'} engines: {node: '>=6'}
codepage@1.15.0:
resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==}
engines: {node: '>=0.8'}
color-convert@1.9.3: color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
@ -2980,6 +2995,11 @@ packages:
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
engines: {node: '>= 0.10'} engines: {node: '>= 0.10'}
crc-32@1.2.2:
resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==}
engines: {node: '>=0.8'}
hasBin: true
cropperjs@1.6.2: cropperjs@1.6.2:
resolution: {integrity: sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==} resolution: {integrity: sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==}
@ -3721,6 +3741,10 @@ packages:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
frac@1.1.2:
resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==}
engines: {node: '>=0.8'}
fresh@0.5.2: fresh@0.5.2:
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@ -5689,6 +5713,10 @@ packages:
resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==}
engines: {node: '>=6'} engines: {node: '>=6'}
ssf@0.11.2:
resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==}
engines: {node: '>=0.8'}
ssr-window@3.0.0: ssr-window@3.0.0:
resolution: {integrity: sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA==} resolution: {integrity: sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA==}
@ -6140,10 +6168,18 @@ packages:
wildcard@1.1.2: wildcard@1.1.2:
resolution: {integrity: sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==} resolution: {integrity: sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==}
wmf@1.0.2:
resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==}
engines: {node: '>=0.8'}
word-wrap@1.2.5: word-wrap@1.2.5:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
word@0.3.0:
resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==}
engines: {node: '>=0.8'}
wrap-ansi@7.0.0: wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -6180,6 +6216,11 @@ packages:
resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==}
engines: {node: '>=18'} engines: {node: '>=18'}
xlsx@0.18.5:
resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==}
engines: {node: '>=0.8'}
hasBin: true
xml-js@1.6.11: xml-js@1.6.11:
resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==}
hasBin: true hasBin: true
@ -9643,6 +9684,8 @@ snapshots:
dependencies: dependencies:
object-assign: 4.1.1 object-assign: 4.1.1
adler-32@1.3.1: {}
agent-base@6.0.0(supports-color@8.0.0): agent-base@6.0.0(supports-color@8.0.0):
dependencies: dependencies:
debug: 4.4.3(supports-color@8.0.0) debug: 4.4.3(supports-color@8.0.0)
@ -9997,6 +10040,11 @@ snapshots:
ccount@2.0.1: {} ccount@2.0.1: {}
cfb@1.2.2:
dependencies:
adler-32: 1.3.1
crc-32: 1.2.2
chalk@2.4.2: chalk@2.4.2:
dependencies: dependencies:
ansi-styles: 3.2.1 ansi-styles: 3.2.1
@ -10055,6 +10103,8 @@ snapshots:
clsx@2.1.1: {} clsx@2.1.1: {}
codepage@1.15.0: {}
color-convert@1.9.3: color-convert@1.9.3:
dependencies: dependencies:
color-name: 1.1.3 color-name: 1.1.3
@ -10162,6 +10212,8 @@ snapshots:
object-assign: 4.1.1 object-assign: 4.1.1
vary: 1.1.2 vary: 1.1.2
crc-32@1.2.2: {}
cropperjs@1.6.2: {} cropperjs@1.6.2: {}
cross-env@7.0.3: cross-env@7.0.3:
@ -11079,6 +11131,8 @@ snapshots:
forwarded@0.2.0: {} forwarded@0.2.0: {}
frac@1.1.2: {}
fresh@0.5.2: {} fresh@0.5.2: {}
fsevents@2.3.3: fsevents@2.3.3:
@ -13662,6 +13716,10 @@ snapshots:
split-on-first@1.1.0: {} split-on-first@1.1.0: {}
ssf@0.11.2:
dependencies:
frac: 1.1.2
ssr-window@3.0.0: {} ssr-window@3.0.0: {}
ssr-window@4.0.2: {} ssr-window@4.0.2: {}
@ -14133,8 +14191,12 @@ snapshots:
wildcard@1.1.2: {} wildcard@1.1.2: {}
wmf@1.0.2: {}
word-wrap@1.2.5: {} word-wrap@1.2.5: {}
word@0.3.0: {}
wrap-ansi@7.0.0: wrap-ansi@7.0.0:
dependencies: dependencies:
ansi-styles: 4.1.0 ansi-styles: 4.1.0
@ -14155,6 +14217,16 @@ snapshots:
dependencies: dependencies:
is-wsl: 3.1.1 is-wsl: 3.1.1
xlsx@0.18.5:
dependencies:
adler-32: 1.3.1
cfb: 1.2.2
codepage: 1.15.0
crc-32: 1.2.2
ssf: 0.11.2
wmf: 1.0.2
word: 0.3.0
xml-js@1.6.11: xml-js@1.6.11:
dependencies: dependencies:
sax: 1.6.1 sax: 1.6.1

View File

@ -17,6 +17,8 @@ import {
} from "antd"; } from "antd";
import { AntdTableFuncControl } from "@cqsjjb/jjb-common-decorator/antd"; import { AntdTableFuncControl } from "@cqsjjb/jjb-common-decorator/antd";
import { DownloadOutlined } from "@ant-design/icons"; import { DownloadOutlined } from "@ant-design/icons";
// 2026-08-14 导出打卡记录使用 xlsx 库生成标准 Excel 文件
import * as XLSX from "xlsx";
import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout"; import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout";
import SearchForm from "~/components/SearchForm"; import SearchForm from "~/components/SearchForm";
import ControlWrapper from "@cqsjjb/jjb-react-admin-component/ControlWrapper"; import ControlWrapper from "@cqsjjb/jjb-react-admin-component/ControlWrapper";
@ -126,7 +128,7 @@ const parseRoleList = (v) => {
const parsed = JSON.parse(trimmed); const parsed = JSON.parse(trimmed);
return Array.isArray(parsed) ? parsed : [parsed]; return Array.isArray(parsed) ? parsed : [parsed];
} catch (e) { } catch (e) {
// 解析失败按逗号拆分兜底 // 解析失败按逗号拆分兜底(过滤空白元素)
return trimmed.slice(1, -1).split(",").map((s) => s.trim()).filter(Boolean); return trimmed.slice(1, -1).split(",").map((s) => s.trim()).filter(Boolean);
} }
} }
@ -135,10 +137,16 @@ const parseRoleList = (v) => {
return []; return [];
}; };
/** 项目角色编码 → 中文展示(多角色用"、"连接),未匹配到则原样展示 */ /** ""
* 2026-08-14 修复过滤空白元素并清理 join 后开头/结尾多余的分隔符避免"角色1、角色2、" */
const roleText = (v) => { const roleText = (v) => {
const list = parseRoleList(v); const list = parseRoleList(v)
return list.map((r) => ROLE_DEFINITIONS_MAP[r] || r).join("、") || "-"; .map((r) => (r || "").toString().trim())
.filter(Boolean)
.map((r) => ROLE_DEFINITIONS_MAP[r] || r);
if (!list.length) return "-";
// join 后去掉开头/结尾可能残留的顿号(兼容解析异常场景)
return list.join("、").replace(/^、+|、+$/g, "");
}; };
const STAT_ITEMS = [ const STAT_ITEMS = [
@ -273,7 +281,10 @@ const SurveyMonitor = (props) => {
} }
}; };
// 2026-08-14 新增:导出打卡记录(前端 CSV 导出,无需后端接口) // 2026-08-14 新增:导出打卡记录(前端生成真正的 xlsx 文件,列严格对齐)
// 原因CSV/HTML 表格在 WPS/Excel 中可能列错位或格子被隐藏。
// 采用 xlsx 库生成标准 Excel 文件cell 内容由 sheet 严格按行列放置,
// 并显式设置列宽,保证所有格子内容完整显示、不被隐藏。
// 字段对应原型打卡弹窗表格:人员/项目角色/人脸识别/ GPS 定位/签到时间/签退时间/现场照片/本人签字 // 字段对应原型打卡弹窗表格:人员/项目角色/人脸识别/ GPS 定位/签到时间/签退时间/现场照片/本人签字
const handleExportClockIn = () => { const handleExportClockIn = () => {
if (!clockInData.length) { if (!clockInData.length) {
@ -285,7 +296,7 @@ const SurveyMonitor = (props) => {
]; ];
const rows = clockInData.map((row) => [ const rows = clockInData.map((row) => [
row.userName || "-", row.userName || "-",
// 2026-08-14 修复:复用全局 roleText兼容后端 JSON 数组字符串 // 2026-08-14 修复:复用全局 roleText兼容后端 JSON 数组字符串,并清理末尾分隔符
roleText(row.role), roleText(row.role),
row.faceState === 1 ? "通过" : "未通过", row.faceState === 1 ? "通过" : "未通过",
row.gpsState === 1 ? "正常" : "异常", row.gpsState === 1 ? "正常" : "异常",
@ -294,14 +305,26 @@ const SurveyMonitor = (props) => {
row.sceneUrl ? row.sceneUrl.split(",").filter(Boolean).length + "张" : "-", row.sceneUrl ? row.sceneUrl.split(",").filter(Boolean).length + "张" : "-",
row.signatureUrl ? "已签字" : "未签字", row.signatureUrl ? "已签字" : "未签字",
]); ]);
// 构造 CSV含 BOM 头,保证 Excel 正确识别中文)
const csv = "" + [header, ...rows].map((r) => r.join(",")).join("\r\n"); // 2026-08-14 使用 xlsx 库生成标准 .xlsx 文件
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); // 引入方式:为避免打包 tree-shaking 问题,用 `import * as XLSX from "xlsx"`(文件顶部已导入)
const link = document.createElement("a"); const aoa = [header, ...rows];
link.href = URL.createObjectURL(blob); const ws = XLSX.utils.aoa_to_sheet(aoa);
link.download = `${clockInProjectName || "项目"}_打卡记录.csv`; // 显式设置每列宽度,避免内容被隐藏/截断(宽度按列索引,单位约为字符数)
link.click(); ws["!cols"] = [
URL.revokeObjectURL(link.href); { wch: 18 }, // 人员
{ wch: 32 }, // 项目角色(多角色较长)
{ wch: 12 }, // 人脸识别
{ wch: 12 }, // GPS定位
{ wch: 22 }, // 签到时间
{ wch: 22 }, // 签退时间
{ wch: 12 }, // 现场照片
{ wch: 12 }, // 本人签字
];
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "打卡记录");
// writeFile 直接触发浏览器下载,生成真正的 .xlsx
XLSX.writeFile(wb, `${clockInProjectName || "项目"}_打卡记录.xlsx`);
}; };
const clockInColumns = [ const clockInColumns = [