已完成,学生端考勤记录现在按课程展示。
每门课程会独立显示: 课程名称、代码、教学班和任课教师。 课程总体出勤率。 出勤、迟到、缺勤、请假、免修次数。 按日期排列的历次点名记录。 缺勤或迟到记录仍可单独申诉。
This commit is contained in:
1 parent
72297aa23f
commit
3b73292d93
12 files changed
+2172
-54
No files matched your search
Generated
+317
@@ -15,11 +15,13 @@
|
||||
"html2canvas": "^1.4.1",
|
||||
"jspdf": "^4.2.1",
|
||||
"pinia": "^4.0.2",
|
||||
"qrcode": "1.5.4",
|
||||
"vue": "^3.5.39",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.13.2",
|
||||
"@types/qrcode": "1.5.6",
|
||||
"@vitejs/plugin-vue": "^6.0.7",
|
||||
"@vue/tsconfig": "^0.9.1",
|
||||
"typescript": "~6.0.2",
|
||||
@@ -581,6 +583,16 @@
|
||||
"integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/qrcode": {
|
||||
"version": "1.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz",
|
||||
"integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/raf": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
|
||||
@@ -880,6 +892,30 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/async-validator": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
|
||||
@@ -936,6 +972,15 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/camelcase": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/canvg": {
|
||||
"version": "3.0.11",
|
||||
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
|
||||
@@ -972,6 +1017,35 @@
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^6.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
@@ -1041,6 +1115,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
@@ -1060,6 +1143,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dijkstrajs": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.4.12",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz",
|
||||
@@ -1126,6 +1215,12 @@
|
||||
"vue": "^3.3.7"
|
||||
}
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
|
||||
@@ -1244,6 +1339,19 @@
|
||||
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"locate-path": "^5.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||
@@ -1304,6 +1412,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
@@ -1431,6 +1548,15 @@
|
||||
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
|
||||
@@ -1746,6 +1872,18 @@
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-locate": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||
@@ -1903,6 +2041,42 @@
|
||||
"node": ">=12.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-try": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-locate": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-limit": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-2.2.0.tgz",
|
||||
@@ -1926,6 +2100,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pathe": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
@@ -2003,6 +2186,15 @@
|
||||
"pathe": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/pngjs": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.22",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz",
|
||||
@@ -2040,6 +2232,23 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
|
||||
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dijkstrajs": "^1.0.1",
|
||||
"pngjs": "^5.0.0",
|
||||
"yargs": "^15.3.1"
|
||||
},
|
||||
"bin": {
|
||||
"qrcode": "bin/qrcode"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/quansync": {
|
||||
"version": "0.2.11",
|
||||
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
|
||||
@@ -2088,6 +2297,21 @@
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-main-filename": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/rgbcolor": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
|
||||
@@ -2139,6 +2363,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -2158,6 +2388,32 @@
|
||||
"node": ">=0.1.14"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-literal": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
|
||||
@@ -2599,6 +2855,67 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/which-module": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "15.4.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^6.0.0",
|
||||
"decamelize": "^1.2.0",
|
||||
"find-up": "^4.1.0",
|
||||
"get-caller-file": "^2.0.1",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^2.0.0",
|
||||
"set-blocking": "^2.0.0",
|
||||
"string-width": "^4.2.0",
|
||||
"which-module": "^2.0.0",
|
||||
"y18n": "^4.0.0",
|
||||
"yargs-parser": "^18.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "18.1.3",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"camelcase": "^5.0.0",
|
||||
"decamelize": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/zrender": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/zrender/-/zrender-6.1.0.tgz",
|
||||
|
||||
@@ -16,11 +16,13 @@
|
||||
"html2canvas": "^1.4.1",
|
||||
"jspdf": "^4.2.1",
|
||||
"pinia": "^4.0.2",
|
||||
"qrcode": "1.5.4",
|
||||
"vue": "^3.5.39",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.13.2",
|
||||
"@types/qrcode": "1.5.6",
|
||||
"@vitejs/plugin-vue": "^6.0.7",
|
||||
"@vue/tsconfig": "^0.9.1",
|
||||
"typescript": "~6.0.2",
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { Refresh, Warning } from '@element-plus/icons-vue'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Check, Clock, Location, Refresh, Warning } from '@element-plus/icons-vue'
|
||||
import http, { apiErrorMessage } from '../api/http'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const records = ref<any[]>([])
|
||||
const openActivities = ref<any[]>([])
|
||||
const scannedActivity = ref<any>(null)
|
||||
const loading = ref(false)
|
||||
const scanLoading = ref(false)
|
||||
const checkInTargetId = ref('')
|
||||
const now = ref(Date.now())
|
||||
const appealDialog = ref(false)
|
||||
const appealTarget = ref<any>(null)
|
||||
const appealReason = ref('')
|
||||
@@ -18,14 +26,165 @@ const statusColors: Record<string, 'success' | 'danger' | 'warning' | 'info' | u
|
||||
const appealStatusLabels: Record<string, string> = {
|
||||
None: '', Pending: '申诉中', Approved: '已通过', Rejected: '已驳回',
|
||||
}
|
||||
const courseGroups = computed(() => {
|
||||
const groups = new Map<string, any>()
|
||||
records.value.forEach((record: any) => {
|
||||
const key = record.teachingTaskId ?? `${record.courseCode}-${record.taskNumber}`
|
||||
let group = groups.get(key)
|
||||
if (!group) {
|
||||
group = {
|
||||
key,
|
||||
courseCode: record.courseCode,
|
||||
courseName: record.courseName,
|
||||
taskNumber: record.taskNumber,
|
||||
teacherNames: [...record.teacherNames],
|
||||
records: [],
|
||||
presentCount: 0,
|
||||
absentCount: 0,
|
||||
lateCount: 0,
|
||||
leaveCount: 0,
|
||||
excusedCount: 0,
|
||||
requiredCount: 0,
|
||||
attendedCount: 0,
|
||||
attendanceRate: null,
|
||||
latestAt: 0,
|
||||
}
|
||||
groups.set(key, group)
|
||||
}
|
||||
group.records.push(record)
|
||||
group.latestAt = Math.max(group.latestAt, new Date(record.attendanceDate).getTime())
|
||||
const countKey = `${record.status.charAt(0).toLowerCase()}${record.status.slice(1)}Count`
|
||||
if (countKey in group) group[countKey] += 1
|
||||
if (record.status !== 'Excused') group.requiredCount += 1
|
||||
if (record.status === 'Present' || record.status === 'Late') group.attendedCount += 1
|
||||
})
|
||||
return [...groups.values()]
|
||||
.map(group => ({
|
||||
...group,
|
||||
attendanceRate: group.requiredCount > 0
|
||||
? Math.round(group.attendedCount * 1000 / group.requiredCount) / 10
|
||||
: null,
|
||||
}))
|
||||
.sort((a, b) => b.latestAt - a.latestAt)
|
||||
})
|
||||
let clockTimer: number | undefined
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try { records.value = (await http.get('/attendance/my-records')).data }
|
||||
try {
|
||||
const [recordResponse, activityResponse] = await Promise.all([
|
||||
http.get('/attendance/my-records'),
|
||||
http.get('/attendance/open-check-ins'),
|
||||
])
|
||||
records.value = recordResponse.data
|
||||
openActivities.value = activityResponse.data
|
||||
const token = typeof route.query.token === 'string' ? route.query.token : ''
|
||||
if (token) await loadScannedActivity(token)
|
||||
else scannedActivity.value = null
|
||||
}
|
||||
catch (e) { ElMessage.error(apiErrorMessage(e)) }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function loadScannedActivity(token: string) {
|
||||
scanLoading.value = true
|
||||
try {
|
||||
scannedActivity.value = (
|
||||
await http.get('/attendance/check-in-info', { params: { token } })
|
||||
).data
|
||||
} catch (error: any) {
|
||||
scannedActivity.value = null
|
||||
if (error?.response?.status === 404) {
|
||||
ElMessage.error('签到码无效,或你不在本次课程名单中。')
|
||||
} else {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
}
|
||||
} finally {
|
||||
scanLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function serverUtcTime(value: string | null | undefined) {
|
||||
if (!value) return Number.NaN
|
||||
const normalized = /(?:Z|[+-]\d{2}:\d{2})$/i.test(value) ? value : `${value}Z`
|
||||
return new Date(normalized).getTime()
|
||||
}
|
||||
|
||||
function formatServerTime(value: string) {
|
||||
return new Date(serverUtcTime(value)).toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
function remainingLabel(activity: any) {
|
||||
if (!activity?.checkInEndsAt) return ''
|
||||
const remaining = serverUtcTime(activity.checkInEndsAt) - now.value
|
||||
if (remaining <= 0) return '签到已结束'
|
||||
const minutes = Math.floor(remaining / 60000)
|
||||
const seconds = Math.floor((remaining % 60000) / 1000)
|
||||
return `${minutes}分${String(seconds).padStart(2, '0')}秒后结束`
|
||||
}
|
||||
|
||||
async function confirmQrCheckIn() {
|
||||
const token = typeof route.query.token === 'string' ? route.query.token : ''
|
||||
if (!token || !scannedActivity.value) return
|
||||
checkInTargetId.value = scannedActivity.value.sheetId
|
||||
try {
|
||||
const { data } = await http.post('/attendance/check-in', { token })
|
||||
ElMessage.success(data.alreadyCheckedIn ? '你已完成本次签到' : '签到成功')
|
||||
await router.replace({ path: route.path, query: {} })
|
||||
await load()
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
} finally {
|
||||
checkInTargetId.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function captureLocation() {
|
||||
if (!navigator.geolocation) throw new Error('unsupported')
|
||||
return await new Promise<GeolocationPosition>((resolve, reject) => {
|
||||
navigator.geolocation.getCurrentPosition(resolve, reject, {
|
||||
enableHighAccuracy: true,
|
||||
timeout: 12000,
|
||||
maximumAge: 0,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function locationCheckIn(activity: any) {
|
||||
checkInTargetId.value = activity.sheetId
|
||||
try {
|
||||
const position = await captureLocation()
|
||||
const { data } = await http.post('/attendance/check-in', {
|
||||
attendanceSheetId: activity.sheetId,
|
||||
latitude: position.coords.latitude,
|
||||
longitude: position.coords.longitude,
|
||||
accuracyMeters: position.coords.accuracy,
|
||||
})
|
||||
ElMessage.success(data.alreadyCheckedIn
|
||||
? '你已完成本次签到'
|
||||
: `签到成功${data.checkInDistanceMeters === null
|
||||
? ''
|
||||
: `,距签到点约 ${Math.round(data.checkInDistanceMeters)} 米`}`)
|
||||
await load()
|
||||
} catch (error: any) {
|
||||
if (!error?.response) {
|
||||
const message = error?.code === 1
|
||||
? '定位权限被拒绝,请在浏览器中允许本网站获取位置。'
|
||||
: error?.code === 3
|
||||
? '获取位置超时,请移到信号较好的位置后重试。'
|
||||
: '无法获取当前位置,请检查手机定位服务后重试。'
|
||||
ElMessage.error(message)
|
||||
} else {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
}
|
||||
} finally {
|
||||
checkInTargetId.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function openAppeal(record: any) {
|
||||
appealTarget.value = record
|
||||
appealReason.value = ''
|
||||
@@ -49,7 +208,32 @@ function canAppeal(record: any) {
|
||||
return record.appealStatus === 'None' || record.appealStatus === 'Rejected'
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
function formatRate(value: number | null) {
|
||||
return value === null ? '暂无' : `${value.toFixed(1)}%`
|
||||
}
|
||||
|
||||
function rateTone(value: number | null) {
|
||||
if (value === null) return 'neutral'
|
||||
if (value < 80) return 'danger'
|
||||
if (value < 90) return 'warning'
|
||||
return 'good'
|
||||
}
|
||||
|
||||
function recordDate(value: string) {
|
||||
const date = new Date(value)
|
||||
return {
|
||||
month: `${date.getMonth() + 1}月`,
|
||||
day: String(date.getDate()).padStart(2, '0'),
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
clockTimer = window.setInterval(() => { now.value = Date.now() }, 1000)
|
||||
load()
|
||||
})
|
||||
onUnmounted(() => {
|
||||
if (clockTimer) window.clearInterval(clockTimer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -58,39 +242,149 @@ onMounted(load)
|
||||
<div>
|
||||
<span class="section-kicker">ATTENDANCE RECORD</span>
|
||||
<h2>我的考勤</h2>
|
||||
<p>查看所有已提交的考勤记录。对记录有异议可以提交申诉,辅导员将进行审核。</p>
|
||||
<p>完成课堂扫码或定位签到,并查看已提交的考勤记录。</p>
|
||||
</div>
|
||||
<el-button :icon="Refresh" @click="load">刷新</el-button>
|
||||
</section>
|
||||
|
||||
<section v-loading="loading" class="att-list">
|
||||
<article v-for="r in records" :key="`${r.attendanceSheetId}`" class="att-card">
|
||||
<div class="att-info">
|
||||
<span>{{ r.courseCode }} · {{ r.taskNumber }}</span>
|
||||
<h3>{{ r.courseName }}</h3>
|
||||
<p>{{ r.sheetName }} · {{ new Date(r.attendanceDate).toLocaleDateString('zh-CN') }} · {{ r.teacherNames.join('、') }}</p>
|
||||
<section
|
||||
v-if="route.query.token || openActivities.length"
|
||||
v-loading="loading || scanLoading"
|
||||
class="check-in-board"
|
||||
>
|
||||
<header>
|
||||
<div>
|
||||
<span>LIVE CHECK-IN</span>
|
||||
<strong>待签到</strong>
|
||||
</div>
|
||||
<div class="att-status">
|
||||
<el-tag :type="statusColors[r.status]" size="small">{{ statusLabels[r.status] }}</el-tag>
|
||||
<span v-if="r.notes" class="att-note">{{ r.notes }}</span>
|
||||
<small>签到由服务器校验课程名单、有效时间和位置范围</small>
|
||||
</header>
|
||||
|
||||
<article v-if="scannedActivity" class="scan-ticket">
|
||||
<div class="ticket-mark"><el-icon><Check /></el-icon></div>
|
||||
<div class="ticket-course">
|
||||
<span>{{ scannedActivity.courseCode }} · {{ scannedActivity.taskNumber }}</span>
|
||||
<strong>{{ scannedActivity.courseName }}</strong>
|
||||
<p>{{ scannedActivity.sheetName }} · 扫码签到</p>
|
||||
</div>
|
||||
<div class="att-appeal">
|
||||
<template v-if="r.appealStatus !== 'None'">
|
||||
<el-tag size="small" :type="r.appealStatus === 'Pending' ? 'warning' : r.appealStatus === 'Approved' ? 'success' : 'danger'">
|
||||
{{ appealStatusLabels[r.appealStatus] }}
|
||||
</el-tag>
|
||||
<span v-if="r.appealReviewComment" class="att-note">{{ r.appealReviewComment }}</span>
|
||||
</template>
|
||||
<div class="ticket-time">
|
||||
<Clock />
|
||||
<b>{{ remainingLabel(scannedActivity) }}</b>
|
||||
<small v-if="scannedActivity.checkInAt">
|
||||
已于 {{ formatServerTime(scannedActivity.checkInAt) }} 签到
|
||||
</small>
|
||||
<small v-else>请确认课程信息后完成签到</small>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
:disabled="!scannedActivity.isOpen || Boolean(scannedActivity.checkInAt)"
|
||||
:loading="checkInTargetId === scannedActivity.sheetId"
|
||||
@click="confirmQrCheckIn"
|
||||
>{{ scannedActivity.checkInAt ? '已签到' : scannedActivity.isOpen ? '确认签到' : '签到已结束' }}</el-button>
|
||||
</article>
|
||||
|
||||
<div v-if="openActivities.length" class="location-list">
|
||||
<article v-for="activity in openActivities" :key="activity.sheetId">
|
||||
<div class="location-pin"><Location /></div>
|
||||
<div>
|
||||
<span>{{ activity.courseCode }} · {{ activity.taskNumber }}</span>
|
||||
<strong>{{ activity.courseName }}</strong>
|
||||
<p>{{ activity.sheetName }} · {{ activity.locationRadiusMeters }} 米范围内</p>
|
||||
</div>
|
||||
<div class="location-clock">
|
||||
<b>{{ remainingLabel(activity) }}</b>
|
||||
<small v-if="activity.checkInAt">
|
||||
已于 {{ formatServerTime(activity.checkInAt) }} 签到
|
||||
</small>
|
||||
<small v-else>将获取一次当前位置用于本次签到</small>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="canAppeal(r) && (r.status === 'Absent' || r.status === 'Late')"
|
||||
size="small"
|
||||
type="warning"
|
||||
:icon="Warning"
|
||||
@click="openAppeal(r)"
|
||||
>申诉</el-button>
|
||||
type="primary"
|
||||
plain
|
||||
:disabled="Boolean(activity.checkInAt)"
|
||||
:loading="checkInTargetId === activity.sheetId"
|
||||
@click="locationCheckIn(activity)"
|
||||
>{{ activity.checkInAt ? '已签到' : '定位并签到' }}</el-button>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="record-head">
|
||||
<div>
|
||||
<strong>课程考勤档案</strong>
|
||||
<span>按课程汇总出勤率,仅统计教师已经提交的考勤结果</span>
|
||||
</div>
|
||||
<span>{{ courseGroups.length }} 门课程 · {{ records.length }} 次点名</span>
|
||||
</section>
|
||||
|
||||
<section v-loading="loading" class="course-archive">
|
||||
<article v-for="course in courseGroups" :key="course.key" class="course-record">
|
||||
<header>
|
||||
<div class="course-identity">
|
||||
<span>{{ course.courseCode }} · {{ course.taskNumber }}</span>
|
||||
<h3>{{ course.courseName }}</h3>
|
||||
<small>{{ course.teacherNames.join('、') || '任课教师未登记' }}</small>
|
||||
</div>
|
||||
<div class="course-rate" :class="rateTone(course.attendanceRate)">
|
||||
<span>课程出勤率</span>
|
||||
<strong>{{ formatRate(course.attendanceRate) }}</strong>
|
||||
<small>{{ course.attendedCount }} / {{ course.requiredCount }} 次到课</small>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="course-summary">
|
||||
<div><b>{{ course.records.length }}</b><span>点名次数</span></div>
|
||||
<div class="present"><b>{{ course.presentCount }}</b><span>出勤</span></div>
|
||||
<div class="late"><b>{{ course.lateCount }}</b><span>迟到</span></div>
|
||||
<div class="absent"><b>{{ course.absentCount }}</b><span>缺勤</span></div>
|
||||
<div><b>{{ course.leaveCount }}</b><span>请假</span></div>
|
||||
<div><b>{{ course.excusedCount }}</b><span>免修</span></div>
|
||||
</div>
|
||||
|
||||
<div class="session-list">
|
||||
<div
|
||||
v-for="r in course.records"
|
||||
:key="r.attendanceSheetId"
|
||||
class="session-row"
|
||||
>
|
||||
<div class="session-date" aria-hidden="true">
|
||||
<span>{{ recordDate(r.attendanceDate).month }}</span>
|
||||
<strong>{{ recordDate(r.attendanceDate).day }}</strong>
|
||||
</div>
|
||||
<div class="session-info">
|
||||
<strong>{{ r.sheetName }}</strong>
|
||||
<span v-if="r.notes">{{ r.notes }}</span>
|
||||
<span v-else>本次点名没有备注</span>
|
||||
</div>
|
||||
<div class="session-result">
|
||||
<el-tag :type="statusColors[r.status]" size="small">
|
||||
{{ statusLabels[r.status] }}
|
||||
</el-tag>
|
||||
<template v-if="r.appealStatus !== 'None'">
|
||||
<el-tag
|
||||
size="small"
|
||||
:type="r.appealStatus === 'Pending' ? 'warning' : r.appealStatus === 'Approved' ? 'success' : 'danger'"
|
||||
>
|
||||
{{ appealStatusLabels[r.appealStatus] }}
|
||||
</el-tag>
|
||||
<span v-if="r.appealReviewComment" class="review-comment">
|
||||
{{ r.appealReviewComment }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="canAppeal(r) && (r.status === 'Absent' || r.status === 'Late')"
|
||||
size="small"
|
||||
type="warning"
|
||||
plain
|
||||
:icon="Warning"
|
||||
@click="openAppeal(r)"
|
||||
>申诉</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<el-empty v-if="!records.length" description="暂无考勤记录" />
|
||||
<el-empty v-if="!courseGroups.length" description="还没有已提交的课程考勤记录" />
|
||||
</section>
|
||||
|
||||
<el-dialog v-model="appealDialog" title="考勤申诉" width="500px">
|
||||
@@ -108,12 +402,261 @@ onMounted(load)
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.att-list { display: grid; gap: 10px; }
|
||||
.att-card { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 14px 18px; background: #fff; border: 1px solid #e4e7ed; border-radius: 8px; flex-wrap: wrap; }
|
||||
.att-info span { font-size: 11px; color: var(--muted); }
|
||||
.att-info h3 { font-size: 14px; margin: 2px 0; }
|
||||
.att-info p { font-size: 12px; color: var(--muted); margin: 0; }
|
||||
.att-status { display: flex; align-items: center; gap: 8px; }
|
||||
.att-appeal { display: flex; align-items: center; gap: 8px; }
|
||||
.att-note { font-size: 11px; color: var(--muted); max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.check-in-board {
|
||||
border: 1px solid #cdd9e5;
|
||||
background: #f6f9fc;
|
||||
}
|
||||
.check-in-board > header {
|
||||
min-height: 58px;
|
||||
padding: 10px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
color: white;
|
||||
background: #17395e;
|
||||
}
|
||||
.check-in-board > header > div { display: grid; }
|
||||
.check-in-board > header span {
|
||||
color: #6de0c0;
|
||||
font-size: 9px;
|
||||
font-weight: 800;
|
||||
letter-spacing: .14em;
|
||||
}
|
||||
.check-in-board > header strong { font-size: 17px; }
|
||||
.check-in-board > header small { font-size: 10px; opacity: .72; }
|
||||
.scan-ticket {
|
||||
margin: 14px;
|
||||
padding: 16px;
|
||||
display: grid;
|
||||
grid-template-columns: 44px minmax(180px, 1fr) minmax(150px, .7fr) auto;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
border-left: 4px solid #2d8975;
|
||||
background: white;
|
||||
box-shadow: 0 4px 14px rgba(23, 43, 77, .07);
|
||||
}
|
||||
.ticket-mark {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #247261;
|
||||
font-size: 23px;
|
||||
background: #e8f5ef;
|
||||
}
|
||||
.ticket-course,
|
||||
.ticket-time { display: grid; gap: 2px; }
|
||||
.ticket-course span,
|
||||
.location-list article > div:nth-child(2) > span {
|
||||
color: #176b87;
|
||||
font: 700 10px/1.2 Consolas, monospace;
|
||||
}
|
||||
.ticket-course strong,
|
||||
.location-list article strong { color: #172b4d; font-size: 15px; }
|
||||
.ticket-course p,
|
||||
.location-list article p { margin: 0; color: var(--muted); font-size: 10px; }
|
||||
.ticket-time { grid-template-columns: 16px minmax(0, 1fr); }
|
||||
.ticket-time svg { width: 14px; color: #176b87; }
|
||||
.ticket-time b { color: #344054; font-size: 12px; }
|
||||
.ticket-time small { grid-column: 2; color: var(--muted); font-size: 9px; }
|
||||
.location-list { border-top: 1px solid #dce4ed; }
|
||||
.location-list article {
|
||||
padding: 13px 16px;
|
||||
display: grid;
|
||||
grid-template-columns: 38px minmax(180px, 1fr) minmax(160px, .7fr) auto;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border-bottom: 1px solid #e3e9ef;
|
||||
background: white;
|
||||
}
|
||||
.location-list article:last-child { border-bottom: none; }
|
||||
.location-pin {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #176b87;
|
||||
background: #e9f3f5;
|
||||
}
|
||||
.location-pin svg { width: 19px; }
|
||||
.location-list article > div:nth-child(2) { display: grid; gap: 2px; }
|
||||
.location-clock { display: grid; gap: 2px; }
|
||||
.location-clock b { color: #247261; font-size: 11px; }
|
||||
.location-clock small { color: var(--muted); font-size: 9px; }
|
||||
.record-head {
|
||||
padding: 11px 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border: 1px solid var(--line);
|
||||
background: #fbfcfd;
|
||||
}
|
||||
.record-head > div { display: grid; gap: 1px; }
|
||||
.record-head strong { color: #172b4d; font-size: 14px; }
|
||||
.record-head span { color: var(--muted); font-size: 10px; }
|
||||
.course-archive { display: grid; gap: 14px; }
|
||||
.course-record {
|
||||
overflow: hidden;
|
||||
border: 1px solid #d8e0e9;
|
||||
background: white;
|
||||
}
|
||||
.course-record > header {
|
||||
min-height: 104px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 176px;
|
||||
border-bottom: 1px solid #dce4ed;
|
||||
}
|
||||
.course-identity {
|
||||
padding: 18px 20px;
|
||||
display: grid;
|
||||
align-content: center;
|
||||
gap: 2px;
|
||||
}
|
||||
.course-identity > span {
|
||||
color: #176b87;
|
||||
font: 700 10px/1.2 Consolas, monospace;
|
||||
letter-spacing: .03em;
|
||||
}
|
||||
.course-identity h3 {
|
||||
margin: 2px 0;
|
||||
color: #172b4d;
|
||||
font-size: 20px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
.course-identity small { color: var(--muted); font-size: 10px; }
|
||||
.course-rate {
|
||||
padding: 14px 18px;
|
||||
display: grid;
|
||||
align-content: center;
|
||||
color: white;
|
||||
background: #17395e;
|
||||
}
|
||||
.course-rate > span {
|
||||
font-size: 9px;
|
||||
font-weight: 800;
|
||||
letter-spacing: .08em;
|
||||
opacity: .75;
|
||||
}
|
||||
.course-rate strong {
|
||||
margin: 3px 0;
|
||||
font: 700 30px/1 "Arial Narrow", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
.course-rate small { font-size: 9px; opacity: .72; }
|
||||
.course-rate.good { background: #245f57; }
|
||||
.course-rate.warning { background: #8b5a18; }
|
||||
.course-rate.danger { background: #8d403d; }
|
||||
.course-rate.neutral { background: #526276; }
|
||||
.course-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
border-bottom: 1px solid #e3e9ef;
|
||||
background: #f8fafc;
|
||||
}
|
||||
.course-summary > div {
|
||||
min-width: 0;
|
||||
padding: 10px 12px;
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
border-right: 1px solid #e3e9ef;
|
||||
}
|
||||
.course-summary > div:last-child { border-right: none; }
|
||||
.course-summary b {
|
||||
color: #344054;
|
||||
font: 700 18px/1.1 "Arial Narrow", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
.course-summary span { color: var(--muted); font-size: 9px; }
|
||||
.course-summary .present b { color: #247261; }
|
||||
.course-summary .late b { color: #a66716; }
|
||||
.course-summary .absent b { color: #a9433e; }
|
||||
.session-list { display: grid; }
|
||||
.session-row {
|
||||
min-height: 68px;
|
||||
padding: 9px 14px;
|
||||
display: grid;
|
||||
grid-template-columns: 46px minmax(160px, 1fr) minmax(120px, auto) auto;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border-bottom: 1px solid #edf0f4;
|
||||
}
|
||||
.session-row:last-child { border-bottom: none; }
|
||||
.session-date {
|
||||
width: 42px;
|
||||
height: 46px;
|
||||
display: grid;
|
||||
align-content: center;
|
||||
justify-items: center;
|
||||
color: #17395e;
|
||||
border: 1px solid #cbd7e3;
|
||||
background: #f5f8fb;
|
||||
}
|
||||
.session-date span { font-size: 8px; font-weight: 700; }
|
||||
.session-date strong {
|
||||
font: 700 19px/1 "Arial Narrow", sans-serif;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.session-info { min-width: 0; display: grid; gap: 3px; }
|
||||
.session-info strong {
|
||||
overflow: hidden;
|
||||
color: #344054;
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.session-info span,
|
||||
.review-comment {
|
||||
overflow: hidden;
|
||||
color: var(--muted);
|
||||
font-size: 9px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.session-result {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.review-comment { max-width: 150px; }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.check-in-board > header { align-items: flex-start; flex-direction: column; }
|
||||
.scan-ticket,
|
||||
.location-list article {
|
||||
grid-template-columns: 38px minmax(0, 1fr);
|
||||
}
|
||||
.ticket-time,
|
||||
.location-clock,
|
||||
.scan-ticket > .el-button,
|
||||
.location-list article > .el-button {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.scan-ticket > .el-button,
|
||||
.location-list article > .el-button { width: 100%; }
|
||||
.ticket-time { grid-template-columns: 16px minmax(0, 1fr); }
|
||||
.record-head { align-items: flex-start; flex-direction: column; gap: 5px; }
|
||||
.record-head > span { text-align: left; }
|
||||
.course-record > header { grid-template-columns: minmax(0, 1fr) 118px; }
|
||||
.course-identity { padding: 15px 13px; }
|
||||
.course-identity h3 { font-size: 17px; }
|
||||
.course-rate { padding: 12px; }
|
||||
.course-rate strong { font-size: 22px; }
|
||||
.course-summary { grid-template-columns: repeat(3, 1fr); }
|
||||
.course-summary > div:nth-child(3) { border-right: none; }
|
||||
.course-summary > div:nth-child(-n + 3) { border-bottom: 1px solid #e3e9ef; }
|
||||
.session-row {
|
||||
grid-template-columns: 42px minmax(0, 1fr) auto;
|
||||
gap: 9px;
|
||||
}
|
||||
.session-result {
|
||||
grid-column: 2 / -1;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.session-row > .el-button {
|
||||
grid-column: 2 / -1;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loaded 3 of 12 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user