薪资管理和考勤分析

This commit is contained in:
砂糖
2025-08-09 14:39:14 +08:00
parent 996e8d3258
commit 7e160a1623
22 changed files with 1871 additions and 222 deletions

View File

@@ -17,6 +17,7 @@
},
"dependencies": {
"@element-plus/icons-vue": "2.3.1",
"@tailwindcss/vite": "^4.1.11",
"@vueup/vue-quill": "1.2.0",
"@vueuse/core": "13.3.0",
"axios": "1.9.0",
@@ -32,6 +33,7 @@
"pinia": "3.0.2",
"sortablejs": "^1.15.6",
"splitpanes": "4.0.4",
"tailwindcss": "^4.1.11",
"vue": "3.5.16",
"vue-cropper": "1.1.1",
"vue-router": "4.5.1",

419
gear-ui3/pnpm-lock.yaml generated
View File

@@ -11,6 +11,9 @@ importers:
'@element-plus/icons-vue':
specifier: 2.3.1
version: 2.3.1(vue@3.5.16)
'@tailwindcss/vite':
specifier: ^4.1.11
version: 4.1.11(vite@6.3.5(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass-embedded@1.89.1))
'@vueup/vue-quill':
specifier: 1.2.0
version: 1.2.0(vue@3.5.16)
@@ -56,6 +59,9 @@ importers:
splitpanes:
specifier: 4.0.4
version: 4.0.4(vue@3.5.16)
tailwindcss:
specifier: ^4.1.11
version: 4.1.11
vue:
specifier: 3.5.16
version: 3.5.16
@@ -71,7 +77,7 @@ importers:
devDependencies:
'@vitejs/plugin-vue':
specifier: 5.2.4
version: 5.2.4(vite@6.3.5(@types/node@24.2.0)(sass-embedded@1.89.1))(vue@3.5.16)
version: 5.2.4(vite@6.3.5(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass-embedded@1.89.1))(vue@3.5.16)
sass-embedded:
specifier: 1.89.1
version: 1.89.1
@@ -83,16 +89,20 @@ importers:
version: 1.0.1
vite:
specifier: 6.3.5
version: 6.3.5(@types/node@24.2.0)(sass-embedded@1.89.1)
version: 6.3.5(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass-embedded@1.89.1)
vite-plugin-compression:
specifier: 0.5.1
version: 0.5.1(vite@6.3.5(@types/node@24.2.0)(sass-embedded@1.89.1))
version: 0.5.1(vite@6.3.5(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass-embedded@1.89.1))
vite-plugin-svg-icons:
specifier: 2.0.1
version: 2.0.1(vite@6.3.5(@types/node@24.2.0)(sass-embedded@1.89.1))
version: 2.0.1(vite@6.3.5(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass-embedded@1.89.1))
packages:
'@ampproject/remapping@2.3.0':
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
engines: {node: '>=6.0.0'}
'@antfu/utils@0.7.10':
resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==}
@@ -294,9 +304,23 @@ packages:
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
'@isaacs/fs-minipass@4.0.1':
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
engines: {node: '>=18.0.0'}
'@jridgewell/gen-mapping@0.3.12':
resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==}
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
'@jridgewell/sourcemap-codec@1.5.4':
resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==}
'@jridgewell/trace-mapping@0.3.29':
resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==}
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@@ -439,6 +463,100 @@ packages:
'@sxzz/popperjs-es@2.11.7':
resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==}
'@tailwindcss/node@4.1.11':
resolution: {integrity: sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==}
'@tailwindcss/oxide-android-arm64@4.1.11':
resolution: {integrity: sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
'@tailwindcss/oxide-darwin-arm64@4.1.11':
resolution: {integrity: sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@tailwindcss/oxide-darwin-x64@4.1.11':
resolution: {integrity: sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@tailwindcss/oxide-freebsd-x64@4.1.11':
resolution: {integrity: sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [freebsd]
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11':
resolution: {integrity: sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@tailwindcss/oxide-linux-arm64-gnu@4.1.11':
resolution: {integrity: sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.1.11':
resolution: {integrity: sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.1.11':
resolution: {integrity: sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.1.11':
resolution: {integrity: sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.1.11':
resolution: {integrity: sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==}
engines: {node: '>=14.0.0'}
cpu: [wasm32]
bundledDependencies:
- '@napi-rs/wasm-runtime'
- '@emnapi/core'
- '@emnapi/runtime'
- '@tybys/wasm-util'
- '@emnapi/wasi-threads'
- tslib
'@tailwindcss/oxide-win32-arm64-msvc@4.1.11':
resolution: {integrity: sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@tailwindcss/oxide-win32-x64-msvc@4.1.11':
resolution: {integrity: sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@tailwindcss/oxide@4.1.11':
resolution: {integrity: sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==}
engines: {node: '>= 10'}
'@tailwindcss/vite@4.1.11':
resolution: {integrity: sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==}
peerDependencies:
vite: ^5.2.0 || ^6 || ^7
'@trysound/sax@0.2.0':
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
engines: {node: '>=10.13.0'}
@@ -694,6 +812,10 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
chownr@3.0.0:
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
engines: {node: '>=18'}
class-utils@0.3.6:
resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==}
engines: {node: '>=0.10.0'}
@@ -844,6 +966,10 @@ packages:
delegate@3.2.0:
resolution: {integrity: sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==}
detect-libc@2.0.4:
resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
engines: {node: '>=8'}
dom-serializer@0.2.2:
resolution: {integrity: sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==}
@@ -899,6 +1025,10 @@ packages:
resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==}
engines: {node: '>= 4'}
enhanced-resolve@5.18.3:
resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
engines: {node: '>=10.13.0'}
entities@1.1.2:
resolution: {integrity: sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==}
@@ -1356,6 +1486,10 @@ packages:
jackspeak@3.4.3:
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
jiti@2.5.1:
resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==}
hasBin: true
js-base64@2.6.4:
resolution: {integrity: sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==}
@@ -1397,6 +1531,74 @@ packages:
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
engines: {node: '>=0.10.0'}
lightningcss-darwin-arm64@1.30.1:
resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [darwin]
lightningcss-darwin-x64@1.30.1:
resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [darwin]
lightningcss-freebsd-x64@1.30.1:
resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [freebsd]
lightningcss-linux-arm-gnueabihf@1.30.1:
resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==}
engines: {node: '>= 12.0.0'}
cpu: [arm]
os: [linux]
lightningcss-linux-arm64-gnu@1.30.1:
resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.30.1:
resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.30.1:
resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.30.1:
resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.30.1:
resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [win32]
lightningcss-win32-x64-msvc@1.30.1:
resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [win32]
lightningcss@1.30.1:
resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==}
engines: {node: '>= 12.0.0'}
loader-utils@1.4.2:
resolution: {integrity: sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==}
engines: {node: '>=4.0.0'}
@@ -1496,6 +1698,10 @@ packages:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
minizlib@3.0.2:
resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==}
engines: {node: '>= 18'}
mitt@3.0.1:
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
@@ -1503,6 +1709,11 @@ packages:
resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==}
engines: {node: '>=0.10.0'}
mkdirp@3.0.1:
resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==}
engines: {node: '>=10'}
hasBin: true
mlly@1.7.4:
resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==}
@@ -2057,6 +2268,17 @@ packages:
resolution: {integrity: sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==}
engines: {node: '>=16.0.0'}
tailwindcss@4.1.11:
resolution: {integrity: sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==}
tapable@2.2.2:
resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==}
engines: {node: '>=6'}
tar@7.4.3:
resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==}
engines: {node: '>=18'}
tiny-emitter@2.1.0:
resolution: {integrity: sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==}
@@ -2286,11 +2508,20 @@ packages:
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
engines: {node: '>=12'}
yallist@5.0.0:
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
engines: {node: '>=18'}
zrender@5.6.1:
resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==}
snapshots:
'@ampproject/remapping@2.3.0':
dependencies:
'@jridgewell/gen-mapping': 0.3.12
'@jridgewell/trace-mapping': 0.3.29
'@antfu/utils@0.7.10': {}
'@babel/helper-string-parser@7.27.1': {}
@@ -2412,8 +2643,24 @@ snapshots:
wrap-ansi: 8.1.0
wrap-ansi-cjs: wrap-ansi@7.0.0
'@isaacs/fs-minipass@4.0.1':
dependencies:
minipass: 7.1.2
'@jridgewell/gen-mapping@0.3.12':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.4
'@jridgewell/trace-mapping': 0.3.29
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/sourcemap-codec@1.5.4': {}
'@jridgewell/trace-mapping@0.3.29':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.4
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -2501,6 +2748,77 @@ snapshots:
'@sxzz/popperjs-es@2.11.7': {}
'@tailwindcss/node@4.1.11':
dependencies:
'@ampproject/remapping': 2.3.0
enhanced-resolve: 5.18.3
jiti: 2.5.1
lightningcss: 1.30.1
magic-string: 0.30.17
source-map-js: 1.2.1
tailwindcss: 4.1.11
'@tailwindcss/oxide-android-arm64@4.1.11':
optional: true
'@tailwindcss/oxide-darwin-arm64@4.1.11':
optional: true
'@tailwindcss/oxide-darwin-x64@4.1.11':
optional: true
'@tailwindcss/oxide-freebsd-x64@4.1.11':
optional: true
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11':
optional: true
'@tailwindcss/oxide-linux-arm64-gnu@4.1.11':
optional: true
'@tailwindcss/oxide-linux-arm64-musl@4.1.11':
optional: true
'@tailwindcss/oxide-linux-x64-gnu@4.1.11':
optional: true
'@tailwindcss/oxide-linux-x64-musl@4.1.11':
optional: true
'@tailwindcss/oxide-wasm32-wasi@4.1.11':
optional: true
'@tailwindcss/oxide-win32-arm64-msvc@4.1.11':
optional: true
'@tailwindcss/oxide-win32-x64-msvc@4.1.11':
optional: true
'@tailwindcss/oxide@4.1.11':
dependencies:
detect-libc: 2.0.4
tar: 7.4.3
optionalDependencies:
'@tailwindcss/oxide-android-arm64': 4.1.11
'@tailwindcss/oxide-darwin-arm64': 4.1.11
'@tailwindcss/oxide-darwin-x64': 4.1.11
'@tailwindcss/oxide-freebsd-x64': 4.1.11
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.11
'@tailwindcss/oxide-linux-arm64-gnu': 4.1.11
'@tailwindcss/oxide-linux-arm64-musl': 4.1.11
'@tailwindcss/oxide-linux-x64-gnu': 4.1.11
'@tailwindcss/oxide-linux-x64-musl': 4.1.11
'@tailwindcss/oxide-wasm32-wasi': 4.1.11
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.11
'@tailwindcss/oxide-win32-x64-msvc': 4.1.11
'@tailwindcss/vite@4.1.11(vite@6.3.5(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass-embedded@1.89.1))':
dependencies:
'@tailwindcss/node': 4.1.11
'@tailwindcss/oxide': 4.1.11
tailwindcss: 4.1.11
vite: 6.3.5(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass-embedded@1.89.1)
'@trysound/sax@0.2.0': {}
'@types/estree@1.0.8': {}
@@ -2523,9 +2841,9 @@ snapshots:
'@types/web-bluetooth@0.0.21': {}
'@vitejs/plugin-vue@5.2.4(vite@6.3.5(@types/node@24.2.0)(sass-embedded@1.89.1))(vue@3.5.16)':
'@vitejs/plugin-vue@5.2.4(vite@6.3.5(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass-embedded@1.89.1))(vue@3.5.16)':
dependencies:
vite: 6.3.5(@types/node@24.2.0)(sass-embedded@1.89.1)
vite: 6.3.5(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass-embedded@1.89.1)
vue: 3.5.16
'@vue/compiler-core@3.5.16':
@@ -2822,6 +3140,8 @@ snapshots:
ansi-styles: 4.3.0
supports-color: 7.2.0
chownr@3.0.0: {}
class-utils@0.3.6:
dependencies:
arr-union: 3.1.0
@@ -2975,6 +3295,8 @@ snapshots:
delegate@3.2.0: {}
detect-libc@2.0.4: {}
dom-serializer@0.2.2:
dependencies:
domelementtype: 2.3.0
@@ -3056,6 +3378,11 @@ snapshots:
emojis-list@3.0.0: {}
enhanced-resolve@5.18.3:
dependencies:
graceful-fs: 4.2.11
tapable: 2.2.2
entities@1.1.2: {}
entities@2.2.0: {}
@@ -3599,6 +3926,8 @@ snapshots:
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
jiti@2.5.1: {}
js-base64@2.6.4: {}
js-beautify@1.14.11:
@@ -3636,6 +3965,51 @@ snapshots:
kind-of@6.0.3: {}
lightningcss-darwin-arm64@1.30.1:
optional: true
lightningcss-darwin-x64@1.30.1:
optional: true
lightningcss-freebsd-x64@1.30.1:
optional: true
lightningcss-linux-arm-gnueabihf@1.30.1:
optional: true
lightningcss-linux-arm64-gnu@1.30.1:
optional: true
lightningcss-linux-arm64-musl@1.30.1:
optional: true
lightningcss-linux-x64-gnu@1.30.1:
optional: true
lightningcss-linux-x64-musl@1.30.1:
optional: true
lightningcss-win32-arm64-msvc@1.30.1:
optional: true
lightningcss-win32-x64-msvc@1.30.1:
optional: true
lightningcss@1.30.1:
dependencies:
detect-libc: 2.0.4
optionalDependencies:
lightningcss-darwin-arm64: 1.30.1
lightningcss-darwin-x64: 1.30.1
lightningcss-freebsd-x64: 1.30.1
lightningcss-linux-arm-gnueabihf: 1.30.1
lightningcss-linux-arm64-gnu: 1.30.1
lightningcss-linux-arm64-musl: 1.30.1
lightningcss-linux-x64-gnu: 1.30.1
lightningcss-linux-x64-musl: 1.30.1
lightningcss-win32-arm64-msvc: 1.30.1
lightningcss-win32-x64-msvc: 1.30.1
loader-utils@1.4.2:
dependencies:
big.js: 5.2.2
@@ -3736,6 +4110,10 @@ snapshots:
minipass@7.1.2: {}
minizlib@3.0.2:
dependencies:
minipass: 7.1.2
mitt@3.0.1: {}
mixin-deep@1.3.2:
@@ -3743,6 +4121,8 @@ snapshots:
for-in: 1.0.2
is-extendable: 1.0.1
mkdirp@3.0.1: {}
mlly@1.7.4:
dependencies:
acorn: 8.15.0
@@ -4369,6 +4749,19 @@ snapshots:
sync-message-port@1.1.3: {}
tailwindcss@4.1.11: {}
tapable@2.2.2: {}
tar@7.4.3:
dependencies:
'@isaacs/fs-minipass': 4.0.1
chownr: 3.0.0
minipass: 7.1.2
minizlib: 3.0.2
mkdirp: 3.0.1
yallist: 5.0.0
tiny-emitter@2.1.0: {}
tinyglobby@0.2.14:
@@ -4530,16 +4923,16 @@ snapshots:
vary@1.1.2: {}
vite-plugin-compression@0.5.1(vite@6.3.5(@types/node@24.2.0)(sass-embedded@1.89.1)):
vite-plugin-compression@0.5.1(vite@6.3.5(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass-embedded@1.89.1)):
dependencies:
chalk: 4.1.2
debug: 4.4.1
fs-extra: 10.1.0
vite: 6.3.5(@types/node@24.2.0)(sass-embedded@1.89.1)
vite: 6.3.5(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass-embedded@1.89.1)
transitivePeerDependencies:
- supports-color
vite-plugin-svg-icons@2.0.1(vite@6.3.5(@types/node@24.2.0)(sass-embedded@1.89.1)):
vite-plugin-svg-icons@2.0.1(vite@6.3.5(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass-embedded@1.89.1)):
dependencies:
'@types/svgo': 2.6.4
cors: 2.8.5
@@ -4549,11 +4942,11 @@ snapshots:
pathe: 0.2.0
svg-baker: 1.7.0
svgo: 2.8.0
vite: 6.3.5(@types/node@24.2.0)(sass-embedded@1.89.1)
vite: 6.3.5(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass-embedded@1.89.1)
transitivePeerDependencies:
- supports-color
vite@6.3.5(@types/node@24.2.0)(sass-embedded@1.89.1):
vite@6.3.5(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass-embedded@1.89.1):
dependencies:
esbuild: 0.25.8
fdir: 6.4.6(picomatch@4.0.3)
@@ -4564,6 +4957,8 @@ snapshots:
optionalDependencies:
'@types/node': 24.2.0
fsevents: 2.3.3
jiti: 2.5.1
lightningcss: 1.30.1
sass-embedded: 1.89.1
vue-cropper@1.1.1: {}
@@ -4649,6 +5044,8 @@ snapshots:
string-width: 5.1.2
strip-ansi: 7.1.0
yallist@5.0.0: {}
zrender@5.6.1:
dependencies:
tslib: 2.3.0

View File

@@ -13,3 +13,7 @@ onMounted(() => {
})
})
</script>
<style>
/* @import "tailwindcss"; */
</style>

View File

@@ -0,0 +1,53 @@
import request from '@/utils/request'
// 查询工资发放记录列表
export function listSalaryRecords(query) {
return request({
url: '/oa/salaryRecords/list',
method: 'get',
params: query
})
}
// 查询工资发放记录详细
export function getSalaryRecords(salaryId) {
return request({
url: '/oa/salaryRecords/' + salaryId,
method: 'get'
})
}
// 新增工资发放记录
export function addSalaryRecords(data) {
return request({
url: '/oa/salaryRecords',
method: 'post',
data: data
})
}
// 修改工资发放记录
export function updateSalaryRecords(data) {
return request({
url: '/oa/salaryRecords',
method: 'put',
data: data
})
}
// 删除工资发放记录
export function delSalaryRecords(salaryId) {
return request({
url: '/oa/salaryRecords/' + salaryId,
method: 'delete'
})
}
// 批量发放薪资
export function batchPaySalary(data) {
return request({
url: '/oa/salaryRecords/batchSendSalary',
method: 'post',
data: data
})
}

View File

@@ -1,6 +1,6 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="120px">
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="120px">
<!-- <el-form-item label="关联汇报概述ID" prop="summaryId">
<el-input
v-model="queryParams.summaryId"
@@ -93,26 +93,25 @@
<el-table v-loading="loading" :data="reportDetailList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" align="center" type="index" v-if="true"/>
<!-- <el-table-column label="关联汇报概述ID" align="center" prop="summaryId" /> -->
<el-table-column label="设备编号" align="center" prop="deviceCode" />
<el-table-column label="设备类别" align="center" prop="category" />
<!-- <el-table-column label="设备生产说明" align="center" prop="deviceDescription" /> -->
<el-table-column label="汇报详情内容" align="center" prop="reportDetail" />
<el-table-column label="图片概况" align="center" prop="ossIds" width="100">
<template slot-scope="scope">
<!-- <el-table-column label="图片概况" align="center" prop="ossIds" width="100">
<template #default="scope">
<image-preview :src="scope.row.ossIds" :width="50" :height="50"/>
</template>
</el-table-column>
</el-table-column> -->
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
<template #default="scope">
<!-- <el-button
size="mini"
type="text"
icon="el-icon-view"
@click="handleImageDetail(scope.row)"
>图片详情</el-button>
>图片详情</el-button> -->
<el-button
@@ -142,7 +141,7 @@
/>
<!-- 添加或修改设计项目汇报详情对话框 -->
<el-dialog :title="title" :visible.sync="open" width="50%" append-to-body>
<el-dialog :title="title" v-model="open" width="50%" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="120px">
<el-form-item label="设备编号" prop="deviceCode">
<el-input v-model="form.deviceCode" placeholder="请输入设备唯一编号" />
@@ -162,14 +161,14 @@
<el-form-item label="详情内容" prop="reportDetail">
<el-input v-model="form.reportDetail" type="textarea" placeholder="请输入内容" />
</el-form-item>
<el-form-item label="图像" prop="ossIds">
<!-- <el-form-item label="图像" prop="ossIds">
<image-upload v-model="form.ossIds"/>
</el-form-item>
</el-form-item> -->
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>

View File

@@ -1,6 +1,6 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="100px">
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
<el-form-item label="汇报标题" prop="reportTitle">
<el-input
v-model="queryParams.reportTitle"
@@ -75,26 +75,25 @@
<el-table v-loading="loading" :data="reportSummaryList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" align="center" type="index" v-if="true"/>
<el-table-column label="汇报标题" align="center" prop="reportTitle">
<template slot-scope="scope">
<router-link class="link-type" :to="'/produce/construction/detail/' + scope.row.summaryId">{{ scope.row.reportTitle }}</router-link>
<template #default="scope">
<router-link class="link-type" :to="'/info/construction/detail/' + scope.row.summaryId">{{ scope.row.reportTitle }}</router-link>
</template>
</el-table-column>
<el-table-column label="最近汇报时间" align="center" prop="lastUpdateTime" width="180">
<template slot-scope="scope">
<template #default="scope">
<span>{{ parseTime(scope.row.lastUpdateTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="汇报日期" align="center" prop="reportDate" width="180">
<template slot-scope="scope">
<template #default="scope">
<span>{{ parseTime(scope.row.reportDate, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="汇报人" align="center" prop="reporter" />
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<template #default="scope">
<el-button
size="mini"
type="text"
@@ -129,7 +128,7 @@
<el-date-picker clearable
v-model="form.reportDate"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择汇报日期"
style="width: 100%;">
</el-date-picker>
@@ -141,7 +140,7 @@
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>

View File

@@ -1,6 +1,6 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="96px">
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="96px">
<el-form-item label="物流编号" prop="expressCode">
<el-input
v-model="queryParams.expressCode"
@@ -42,7 +42,7 @@
</el-date-picker>
</el-form-item>
<el-form-item label="物流公司" prop="expressType">
<el-select v-model="queryParams.expressType" placeholder="请选择物流公司标识" clearable>
<el-select style="width: 200px;" v-model="queryParams.expressType" placeholder="请选择物流公司标识" clearable>
<el-option
v-for="dict in oa_express_type"
:key="dict.value"
@@ -117,7 +117,7 @@
<el-table-column type="selection" width="55" align="center"/>
<el-table-column label="物流编号" align="center" prop="expressCode"/>
<el-table-column label="数据状态" align="center" prop="status">
<template slot-scope="scope">
<template #default="scope">
<el-tag v-if="scope.row.status===0" type="warning">未确认</el-tag>
<el-tag v-if="scope.row.status===1" type="primary">进行中</el-tag>
<el-tag v-if="scope.row.status===2" type="success">已完成</el-tag>
@@ -125,7 +125,7 @@
</template>
</el-table-column>
<el-table-column label="物流状态" align="center" prop="status" width="200">
<template slot-scope="scope">
<template #default="scope">
<ExpressStatusEditor
:lastStatus.sync="scope.row.lastStatus"
:lastUpdateTime.sync="scope.row.lastUpdateTime"
@@ -135,7 +135,7 @@
</template>
</el-table-column>
<el-table-column label="剩余时间" align="center" width="120">
<template slot-scope="scope">
<template #default="scope">
<ExpressRemainTime :planDate="scope.row.planDate" :status="scope.row.status" />
</template>
</el-table-column>
@@ -143,23 +143,23 @@
<el-table-column label="负责人" align="center" prop="ownerName"/>
<el-table-column label="负责人手机" align="center" prop="ownerPhone"/>
<el-table-column label="计划到货时间" align="center" prop="planDate" width="180">
<template slot-scope="scope">
<template #default="scope">
<span>{{parseTime(scope.row.planDate,'{y}-{m}-{d}')}}</span>
</template>
</el-table-column>
<el-table-column label="更新时间" align="center" prop="planDate" width="180">
<template slot-scope="scope">
<template #default="scope">
<span>{{scope.row.updateTime}}</span>
</template>
</el-table-column>
<el-table-column label="物流公司标识" align="center" prop="expressType">
<template slot-scope="scope">
<template #default="scope">
<dict-tag :options="oa_express_type" :value="scope.row.expressType"/>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark"/>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<template #default="scope">
<el-button
size="mini"
type="text"
@@ -228,7 +228,7 @@
/>
<!-- 添加或修改物流预览对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="120px">
<el-form-item label="物流编号" prop="expressCode">
<el-input v-model="form.expressCode" placeholder="请输入物流编号"/>
@@ -249,8 +249,8 @@
<el-form-item label="计划到货时间" prop="planDate">
<el-date-picker clearable
v-model="form.planDate"
value-format="YYYY-MM-DD HH:mm:ss"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择计划到货时间">
</el-date-picker>
</el-form-item>
@@ -265,17 +265,6 @@
</el-select>
</el-form-item>
<el-form-item label="发货记录" prop="detailId">
<el-select v-model="form.detailId" placeholder="请选择发货记录">
<el-option
v-for="item in reportDetailList"
:key="item.detailId"
:label="item.reportDetail"
:value="item.detailId"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容"/>
</el-form-item>
@@ -289,7 +278,7 @@
<!-- 物流详情弹窗 -->
<el-dialog
title="物流详情"
:visible.sync="detailOpen"
v-model="detailOpen"
width="600px"
append-to-body
>
@@ -324,7 +313,7 @@
<!-- 异常登记弹窗表单 -->
<el-dialog
title="异常问题登记"
:visible.sync="questionDialogVisible"
v-model="questionDialogVisible"
width="500px"
append-to-body
>
@@ -375,7 +364,13 @@ import ExpressStatusEditor from './components/ExpressStatusEditor.vue';
export default {
name: "Express",
components: {UserSelect, ExpressStatusEditor, ExpressRemainTime},
dicts: ['oa_express_type'],
setup() {
const { proxy } = getCurrentInstance()
const { oa_express_type } = proxy.useDict("oa_express_type")
return {
oa_express_type
}
},
data() {
return {
// 按钮loading
@@ -444,9 +439,6 @@ export default {
expressType: [
{required: true, message: "物流公司标识不能为空", trigger: "change"}
],
remark: [
{required: true, message: "备注不能为空", trigger: "blur"}
]
},
allProject:[],
reportDetailList:[],

View File

@@ -1,6 +1,6 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="关联快递" prop="expressId">
<el-input
v-model="queryParams.expressId"
@@ -40,15 +40,15 @@
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<!-- <el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-check"
icon="Check"
size="mini"
@click="handleUpdateStatus"
>修复</el-button>
</el-col>
</el-col> -->
<el-col :span="1.5">
<el-button
type="success"
@@ -87,20 +87,20 @@
<el-table-column label="快递单号" align="center" prop="expressCode" />
<el-table-column label="问题描述" align="center" prop="description" />
<el-table-column label="汇报时间" align="center" prop="reportTime" width="180">
<template slot-scope="scope">
<template #default="scope">
<span>{{ parseTime(scope.row.reportTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="汇报人" align="center" prop="reportBy" />
<el-table-column label="是否解决" align="center" prop="status">
<template slot-scope="scope">
<template #default="scope">
<el-tag v-if="scope.row.status===0" type="danger">未解决</el-tag>
<el-tag v-if="scope.row.status===1" type="success">完成</el-tag>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<template #default="scope">
<el-button
size="mini"
type="text"
@@ -126,7 +126,7 @@
/>
<!-- 添加或修改快递问题对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="问题描述" prop="description">
<el-input v-model="form.description" type="textarea" placeholder="请输入内容" />
@@ -146,7 +146,7 @@
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>

View File

@@ -63,7 +63,7 @@
<!-- 对话框新增 Feedback -->
<el-dialog
title="新增反馈"
:visible.sync="dialogVisible"
v-model="dialogVisible"
width="800px"
@close="resetForm"
>
@@ -82,7 +82,7 @@
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleAddFeedback">提交</el-button>
</span>

View File

@@ -1,12 +1,12 @@
<template>
<el-dialog
:visible.sync="visible"
v-model="visible"
width="860px"
custom-class="project-report-detail"
:before-close="handleClose"
append-to-body
>
<template #title>
<template #header>
<div class="dialog-title flex items-center gap-2">
<i class="el-icon-document"></i>
<span>项目报工详情</span>

View File

@@ -3,6 +3,7 @@
</template>
<script>
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
import * as echarts from 'echarts';
export default {
@@ -13,30 +14,20 @@ export default {
required: true
}
},
watch: {
data: {
handler() {
this.renderChart();
},
deep: true,
immediate: true
setup(props) {
const chartRef = ref(null);
let chart = null;
const renderChart = () => {
// 确保 chart 已初始化
if (!chart && chartRef.value) {
chart = echarts.init(chartRef.value);
}
},
mounted() {
this.chart = echarts.init(this.$refs.chartRef);
this.renderChart();
window.addEventListener('resize', this.resizeChart);
},
beforeDestroy() {
window.removeEventListener('resize', this.resizeChart);
this.chart && this.chart.dispose();
},
methods: {
renderChart() {
// 如果数据为空,清空图表并显示暂无数据 loading
if (!this.data || this.data.length === 0) {
this.chart.clear();
this.chart.showLoading({
// 如果没有数据
if (!props.data || props.data.length === 0) {
chart?.clear();
chart?.showLoading({
text: '暂无数据',
color: '#999',
textColor: '#999',
@@ -45,20 +36,18 @@ export default {
return;
}
// 有数据时,隐藏 loading 并渲染图表
this.chart.hideLoading();
// 有数据时渲染图表
const option = {
xAxis: {
type: 'category',
data: this.data.map(i => i.date)
data: props.data.map(i => i.date)
},
yAxis: {
type: 'value'
},
series: [
{
data: this.data.map(i => i.count),
data: props.data.map(i => i.count),
type: 'line',
areaStyle: {}
}
@@ -67,12 +56,36 @@ export default {
trigger: 'axis'
}
};
// 第二个参数 true 避免与之前配置合并
this.chart.setOption(option, true);
chart?.setOption(option, true);
};
const resizeChart = () => {
chart?.resize();
};
// 生命周期钩子
onMounted(() => {
renderChart();
window.addEventListener('resize', resizeChart);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', resizeChart);
chart?.dispose();
});
// 监听 props.data 变化
watch(
() => props.data,
() => {
renderChart();
},
resizeChart() {
this.chart && this.chart.resize();
}
{ deep: true, immediate: true }
);
return {
chartRef
};
}
};
</script>

View File

@@ -18,7 +18,7 @@
</el-row>
<!-- Summary Cards with Skeleton -->
<el-skeleton :loading="loading" animated>
<!-- <el-skeleton :loading="loading" animated>
<template #template>
<el-row :gutter="20" class="summary-cards">
<el-col :span="6" v-for="n in 4" :key="n">
@@ -48,20 +48,20 @@
</el-col>
</el-row>
</template>
</el-skeleton>
</el-skeleton> -->
<!-- Charts & Ranking with Skeleton -->
<!-- 图表部分 -->
<el-skeleton :loading="loading" animated>
<template #template>
<el-row :gutter="20" class="charts-ranking">
<el-col :span="8" v-for="n in 3" :key="n">
<el-col :span="12" v-for="n in 2" :key="n">
<el-skeleton-item variant="rect" style="width: 100%; height: 300px" />
</el-col>
</el-row>
</template>
<template #default>
<el-row type="flex" :gutter="20" class="charts-ranking">
<el-col :span="8">
<el-col :span="12">
<el-card
class="charts-card"
:body-style="{ height: '300px', padding: '16px' }"
@@ -70,16 +70,7 @@
<TrendChart :data="trendData" style="height: 100%;" />
</el-card>
</el-col>
<el-col :span="8">
<el-card
class="charts-card"
:body-style="{ height: '300px', padding: '16px' }"
>
<template #header>项目分布</template>
<PieChart :data="pieData" style="height: 100%;" />
</el-card>
</el-col>
<el-col :span="8">
<el-col :span="12">
<el-card
class="charts-card"
:body-style="{ height: '300px', padding: '16px' }"
@@ -96,7 +87,6 @@
<el-card class="table-card">
<el-tabs v-model="activeTab">
<el-tab-pane label="数据总结" name="summary" />
<el-tab-pane label="项目进度" name="progress" />
<el-tab-pane label="历史记录" name="history" />
</el-tabs>
@@ -135,55 +125,11 @@
</el-table>
</div>
<div v-show="activeTab==='progress'">
<el-table :data="projectList" stripe>
<el-table-column label="项目代号">
<template #default="{row}">
<el-tag v-if="row.projectCode==null" type="danger"></el-tag>
<el-tag v-else>{{ row.projectCode }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="projectNum" label="项目编号" />
<el-table-column prop="projectName" label="项目名称" />
<el-table-column prop="reportCount" label="参与人天" />
<el-table-column label="剩余时间" align="center" prop="remainTime">
<template #default="scope">
<div v-if="scope.row.remainTime>=0">
<div v-if="scope.row.projectStatus===0">
<span v-if="scope.row.remainTime>5">剩余{{ scope.row.remainTime }}</span>
<el-tag v-else-if="scope.row.remainTime<=5 &&scope.row.remainTime>3" type="warning">
剩余{{ scope.row.remainTime }}
</el-tag>
<el-tag v-else type="danger">剩余{{ scope.row.remainTime }}</el-tag>
</div>
<div v-else>-</div>
</div>
<div v-else>
<el-tag type="danger" v-if="scope.row.projectStatus===0">过期{{
Math.abs(scope.row.remainTime)
}}
</el-tag>
<div v-else>-</div>
</div>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="projectStatus">
<template #default="scope">
<dict-tag :options="sys_project_status" :value="scope.row.projectStatus"/>
</template>
</el-table-column>
</el-table>
</div>
<div v-show="activeTab==='history'">
<el-row>
<el-col :span="12">
<export-dialog />
</el-col>
<el-col>
<el-button icon="Download" type="text" size="small" @click="loadMore">更多</el-button>
</el-col>
</el-row>
<el-table :data="filteredHistory" stripe style="margin-top:16px">
@@ -227,28 +173,22 @@
<script>
import {
getCardData,
getPieData,
getProjectData,
getProjectReport,
getRankData, getSummaryList,
getTrendData,
listClearProjectReport
} from '@/api/oa/projectReport';
import ExportDialog from './components/ExportDialog.vue';
import PieChart from './components/PieChart.vue';
import ProjectReportDetail from "./components/ProjectReportDetail.vue";
import RankList from "./components/RankList.vue";
import TrendChart from './components/TrendChart.vue';
export default {
name: 'ReportDashboard',
dicts:['sys_project_status'],
components: {
ProjectReportDetail,
RankList,
TrendChart,
PieChart,
ExportDialog
},
data() {
@@ -307,10 +247,6 @@ export default {
this.summaryList = res.data
})
},
loadMore(){
this.$router.push('/hint/projectReport')
},
/** 查询详情 */
openDetail(row) {
getProjectReport(row.reportId).then(response => {
@@ -324,33 +260,27 @@ export default {
},
async getBaseData() {
this.fetchSummary()
const [ cardRes, trendRes, pieRes,rankRes,ProjRes,reportRes ] = await Promise.all([
getCardData(),
const [ trendRes,rankRes,reportRes ] = await Promise.all([
getTrendData(this.dateRange[0], this.dateRange[1]),
getPieData(this.dateRange[0], this.dateRange[1]),
getRankData(this.dateRange[0], this.dateRange[1]),
getProjectData(this.dateRange[0], this.dateRange[1]),
listClearProjectReport(this.dateRange[0], this.dateRange[1])
]);
// 处理卡片
const { todayCount, todayCountChange: yc,
inProgressProjects, projectChange: yp,
completionRate, completionChange: ycr,
exceptions
} = cardRes.data;
const pct = (t,y) => y===0 ? '—' : (((t-y)/y)*100).toFixed(1) + '%';
this.summaryCards = [
{ title:'今日报工人数', value:todayCount, displayChange:`${pct(todayCount,yc)} 较昨日`, changeClass: todayCount-yc>=0?'up':'down', icon:'el-icon-user', iconColor:'#67C23A' },
{ title:'进行中项目', value:inProgressProjects, displayChange:`${pct(inProgressProjects,yp)} 较昨日`, changeClass: inProgressProjects-yp>=0?'up':'down', icon:'el-icon-document', iconColor:'#409EFF' },
{ title:'本月报工', value:completionRate, displayChange:`${pct(completionRate,ycr)} 较上月`, changeClass: completionRate-ycr>=0?'up':'down', icon:'el-icon-data-analysis', iconColor:'#E6A23C' },
{ title:'异常预警', value:exceptions, exception:true, icon:'el-icon-warning', iconColor:'#F56C6C' }
];
// const { todayCount, todayCountChange: yc,
// inProgressProjects, projectChange: yp,
// completionRate, completionChange: ycr,
// exceptions
// } = cardRes.data;
// const pct = (t,y) => y===0 ? '—' : (((t-y)/y)*100).toFixed(1) + '%';
// this.summaryCards = [
// { title:'今日报工人数', value:todayCount, displayChange:`${pct(todayCount,yc)} 较昨日`, changeClass: todayCount-yc>=0?'up':'down', icon:'el-icon-user', iconColor:'#67C23A' },
// { title:'进行中项目', value:inProgressProjects, displayChange:`${pct(inProgressProjects,yp)} 较昨日`, changeClass: inProgressProjects-yp>=0?'up':'down', icon:'el-icon-document', iconColor:'#409EFF' },
// { title:'本月报工', value:completionRate, displayChange:`${pct(completionRate,ycr)} 较上月`, changeClass: completionRate-ycr>=0?'up':'down', icon:'el-icon-data-analysis', iconColor:'#E6A23C' },
// { title:'异常预警', value:exceptions, exception:true, icon:'el-icon-warning', iconColor:'#F56C6C' }
// ];
// 赋值图表数据
this.trendData = trendRes.data;
this.pieData = pieRes.data;
this.ranking = rankRes.data;
this.projectList = ProjRes.data;
console.log(this.projectList)
this.historyList = reportRes.data;
this.loading = false;
},

View File

@@ -1,6 +1,6 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="96px">
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="96px">
<el-form-item label="报工人" prop="nickName">
<el-input
v-model="queryParams.nickName"
@@ -87,21 +87,8 @@
<el-table v-loading="loading" :data="projectReportList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center"/>
<el-table-column label="项目代号" prop="projectCode" width="100" align="center">
<template slot-scope="scope">
<el-tag v-if="scope.row.projectCode==null" type="danger"></el-tag>
<el-tag v-else>{{ scope.row.projectCode }}</el-tag>
</template>
</el-table-column>
<el-table-column label="项目名称" align="center" width="150" prop="projectName">
<template slot-scope="scope">
<span v-if="scope.row.prePay>0"></span>
<span>{{ scope.row.projectName }}</span>
</template>
</el-table-column>
<el-table-column label="项目编号" align="left" prop="projectNum"/>
<el-table-column label="经办人" align="center" prop="nickName">
<template slot-scope="scope">
<template #default="scope">
<span>{{ scope.row.nickName }}
<template v-if="scope.row.deptName!=null">
({{ scope.row.deptName }})
@@ -111,19 +98,19 @@
</el-table-column>
<el-table-column label="工作地点" align="center" prop="workPlace"/>
<el-table-column label="国内/国外" align="center" prop="workType">
<template slot-scope="scope">
<template #default="scope">
<el-tag :type="scope.row.workType===0?'':'warning'">{{ scope.row.workType===0?'国内':'国外' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="报工时间" align="center" prop="createTime">
<template slot-scope="scope">
<template #default="scope">
<span>{{parseTime(scope.row.createTime,'{y}-{m}-{d}')}}</span>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark"/>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<template #default="scope">
<el-button
size="mini"
type="text"
@@ -338,7 +325,6 @@ export default {
this.projectReportList = response.rows;
this.total = response.total;
this.loading = false;
this.getProjectList()
this.getDeptList();
});

View File

@@ -1,7 +1,948 @@
<template>
考勤分析
<div class="page-container">
<!-- 顶部筛选区 -->
<div class="filter-bar">
<div class="container flex items-center justify-between gap-4">
<!-- 日期选择 -->
<div class="date-filter flex items-center gap-2">
<el-radio-group v-model="selectedDateRange">
<el-radio-button v-for="btn in dateButtons" :key="btn.value" :label="btn.value">
{{ btn.label }}
</el-radio-button>
</el-radio-group>
<el-date-picker :disabled="selectedDateRange !== 'custom'" v-model="selectedDate" type="datetimerange"
format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss" placeholder="选择日期" />
</div>
<!-- 搜索框 -->
<!-- <div class="search-container relative flex-1 max-w-xs">
<el-input v-model="searchQuery" placeholder="搜索用户 ID" class="search-input">
<template #prefix>
<el-icon>
<Search />
</el-icon>
</template>
</el-input>
</div> -->
<!-- 筛选器 -->
<div class="filters flex items-center gap-4">
<el-dropdown trigger="hover">
<el-button class="filter-btn whitespace-nowrap">
记录类型<el-icon class="el-icon--right">
<ArrowDown />
</el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="type in recordTypes" :key="type.value">
<el-checkbox v-model="selectedTypes" :label="type.value">
{{ type.label }}
</el-checkbox>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- <el-dropdown trigger="click">
<el-button class="filter-btn whitespace-nowrap">
状态筛选<el-icon class="el-icon--right">
<ArrowDown />
</el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="status in statusList" :key="status.value">
<el-checkbox v-model="selectedStatus" :label="status.value">
{{ status.label }}
</el-checkbox>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown> -->
<el-button type="primary" icon="Search" @click="listData">
查询
</el-button>
</div>
</div>
</div>
<!-- 核心指标看板 -->
<div class="container mb-6" v-loading="loading">
<el-row :gutter="24">
<el-col :span="6" v-for="(stat, index) in statistics" :key="index">
<el-card shadow="hover" class="stat-card h-full">
<div class="stat-header flex items-center justify-between">
<h3 class="stat-label text-gray-500 text-sm">{{ stat.label }}</h3>
<el-icon :class="stat.iconColor" :size="20">
<component :is="stat.icon" />
</el-icon>
</div>
<div class="stat-value-container mt-2">
<span class="stat-value text-2xl font-semibold">{{ stat.value }}</span>
<!-- <span class="stat-trend text-sm ml-2" :class="stat.trend >= 0 ? 'text-green-500' : 'text-red-500'">
<el-icon>
<component :is="stat.trend >= 0 ? 'ArrowUp' : 'ArrowDown'" />
</el-icon>
{{ Math.abs(stat.trend) }}%
</span> -->
</div>
</el-card>
</el-col>
</el-row>
</div>
<!-- 图表区域 -->
<div class="container mb-6" v-loading="loading">
<el-row :gutter="24">
<el-col :span="12">
<el-card shadow="hover" class="chart-card">
<template #header>
<h3 class="chart-title text-lg font-medium">记录趋势</h3>
</template>
<div id="trendChart" class="chart-container"></div>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="hover" class="chart-card">
<template #header>
<h3 class="chart-title text-lg font-medium">出勤排行</h3>
</template>
<!-- 使用条形图 -->
<div class="chart-container" id="attendanceRankChart" />
</el-card>
</el-col>
<el-col :span="12" class="mt-6">
<el-card shadow="hover" class="chart-card">
<template #header>
<h3 class="chart-title text-lg font-medium">出勤时长分布</h3>
</template>
<div id="durationChart" class="chart-container"></div>
</el-card>
</el-col>
<el-col :span="12" class="mt-6">
<el-card shadow="hover" class="chart-card">
<template #header>
<h3 class="chart-title text-lg font-medium">出勤记录分布</h3>
</template>
<div id="heatmapChart" class="chart-container"></div>
</el-card>
</el-col>
</el-row>
</div>
<!-- 数据表格 -->
<div class="container" v-loading="loading">
<el-card shadow="hover" class="table-card">
<el-table :data="tableList" class="full-width-table">
<el-table-column v-for="col in tableColumns" :key="col.key" :prop="col.key" :label="col.label">
<template #default="{ row }" v-if="col.key === 'recordType'">
<div class="flex items-center">
<el-icon :class="getTypeIconColor(row.recordType)">
<component :is="getTypeIcon(row.recordType)" />
</el-icon>
<span class="ml-2">{{ getTypeLabel(row.recordType) }}</span>
</div>
</template>
<template #default="{ row }" v-else-if="col.key === 'timeRange'">
{{ row.startTime }} - {{ row.endTime }}
</template>
</el-table-column>
</el-table>
<div class="pagination-container flex justify-between items-center mt-4">
<el-pagination v-model:current-page="pager.pageNum" :page-size="pager.pageSize" :total="pager.total"
layout="prev, pager, next" />
<span class="total-records text-sm text-gray-500"> {{ pager.total }} 条记录</span>
</div>
</el-card>
</div>
</div>
</template>
<script setup>
import { listAttendanceRecord } from '@/api/oa/attendanceRecord';
import { ref, onMounted, watch } from 'vue';
import * as echarts from 'echarts';
import {
ArrowDown,
Document,
Check,
Clock,
Minus,
} from '@element-plus/icons-vue';
import { reactive } from 'vue';
const { proxy } = getCurrentInstance();
const selectedDateRange = ref('today');
const selectedDate = ref([
proxy.parseTime(new Date(new Date().setHours(0, 0, 0, 0)), '{y}-{m}-{d} 00:00:00'),
proxy.parseTime(new Date(new Date().setHours(23, 59, 59, 999)), '{y}-{m}-{d} 23:59:59')
]);
const selectedTypes = ref([]);
watch(selectedDateRange, (newVal) => {
switch (newVal) {
case 'today':
// 将日期设置为今天整天的范围00:00:00 - 23:59:59
// 设置YYYY-MM-DD HH:MM:SS
selectedDate.value = [
proxy.parseTime(new Date(new Date().setHours(0, 0, 0, 0)), '{y}-{m}-{d} 00:00:00'),
proxy.parseTime(new Date(new Date().setHours(23, 59, 59, 999)), '{y}-{m}-{d} 23:59:59')
];
break;
case 'week':
// 将日期设置为本周的开始和结束日期
const today = new Date();
const startOfWeek = new Date(today);
startOfWeek.setDate(today.getDate() - today.getDay());
startOfWeek.setHours(0, 0, 0, 0);
const endOfWeek = new Date(today);
endOfWeek.setDate(today.getDate() + (6 - today.getDay()));
selectedDate.value = [
proxy.parseTime(startOfWeek, '{y}-{m}-{d} 00:00:00'),
proxy.parseTime(endOfWeek, '{y}-{m}-{d} 23:59:59')
];
break;
case 'month':
// 将日期设置为本月开始和结束日期
const startOfMonth = new Date(new Date().getFullYear(), new Date().getMonth(), 1);
const endOfMonth = new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0);
selectedDate.value = [
proxy.parseTime(startOfMonth, '{y}-{m}-{d} 00:00:00'),
proxy.parseTime(endOfMonth, '{y}-{m}-{d} 23:59:59')
];
break;
case 'custom':
selectedDate.value = [];
break;
}
});
const dateButtons = [
{ label: '今日', value: 'today' },
{ label: '本周', value: 'week' },
{ label: '本月', value: 'month' },
{ label: '自定义', value: 'custom' }
];
const recordTypes = [
{ label: '考勤', value: 'attendance' },
{ label: '加班', value: 'overtime' },
{ label: '出差', value: 'travel' }
];
const statistics = ref([
{
label: '总记录数',
value: '1,286',
trend: 12.5,
icon: 'Document',
iconColor: 'text-blue-500'
},
{
label: '平均考勤时长',
value: '2.5h',
trend: -5.2,
icon: 'Clock',
iconColor: 'text-yellow-500'
},
{
label: '考勤总时长',
value: '526h',
trend: 8.3,
icon: 'Document',
iconColor: 'text-green-500'
}
]);
const setStatistics = ({ totalDuration, totalRecords, averageDuration }) => {
statistics.value[0].value = totalRecords
statistics.value[1].value = averageDuration
statistics.value[2].value = totalDuration
}
const setTrend = ({ attendanceData, overtimeData, travelData, xAxis }) => {
trendChart.value.setOption({
animation: false,
tooltip: {
trigger: 'axis'
},
legend: {
data: ['考勤', '加班', '请假', '出差']
},
xAxis: {
type: 'category',
data: xAxis
},
yAxis: {
type: 'value'
},
series: [
{
name: '考勤',
type: 'line',
data: attendanceData.map(item => item.duration)
},
{
name: '加班',
type: 'line',
data: overtimeData.map(item => item.duration)
},
{
name: '出差',
type: 'line',
data: travelData.map(item => item.duration)
}
]
})
}
const setHeatmap = ({ heatmapData, hours, days }) => {
console.log(heatmapData, hours, days, heatmapChart.value)
// 计算数据中的最大值和最小值用于visualMap映射
const values = heatmapData.map(item => item[2]);
const minValue = Math.min(...values);
const maxValue = Math.max(...values);
heatmapChart.value.setOption({
animation: false,
tooltip: {
position: 'top',
// 格式化提示信息,显示更友好的内容
formatter: (params) => {
const hour = hours[params.data[0]];
const day = days[params.data[1]];
const value = params.data[2].toFixed(1);
return `${day} ${hour}<br/>时长: ${value}小时`;
}
},
grid: {
height: '50%',
top: '10%',
left: '5%',
right: '5%',
bottom: '20%'
},
xAxis: {
type: 'category',
data: hours,
splitArea: {
show: true
}
},
yAxis: {
type: 'category',
data: days,
splitArea: {
show: true
}
},
// 添加visualMap配置解决错误
visualMap: {
min: minValue,
max: maxValue,
calculable: true,
orient: 'horizontal',
left: 'center',
bottom: '5%',
// 设置颜色渐变范围
inRange: {
color: ['#f0f9e8', '#bae4bc', '#7bccc4', '#43a2ca', '#0868ac']
},
// 显示数值标签
label: {
show: true,
formatter: (value) => `${value}h`
}
},
series: [{
name: '出勤分布',
type: 'heatmap',
data: heatmapData,
label: {
show: true,
// 只显示有值的单元格
formatter: (params) => params.data[2] > 0 ? params.data[2].toFixed(1) : ''
},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowColor: 'rgba(0, 0, 0, 0.3)'
}
}
}]
})
}
const setAttendanceRank = ({ users, durations }) => {
attendanceRankChart.value.setOption({
animation: false,
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'value',
axisLabel: {
rotate: 45
}
},
yAxis: {
type: 'category',
data: users,
axisLabel: {
rotate: 45
}
},
series: [
{
type: 'bar',
data: durations
}
]
});
}
const setDuration = ({ durationData, xAxis }) => {
durationChart.value.setOption({
animation: false,
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow' // 阴影指示器,提升交互体验
},
// 简化提示框样式
backgroundColor: 'rgba(255, 255, 255, 0.9)',
borderColor: '#eee',
borderWidth: 1,
padding: 8,
formatter: (params) => {
// 只显示基础的人数信息
return `人数:${params[0].value}`;
}
},
xAxis: {
type: 'category',
data: xAxis
},
yAxis: {
type: 'value',
name: '人数' // 明确y轴含义
},
// 将具体数字展示在柱子上
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow' // 阴影指示器,提升交互体验
},
},
series: [
{
type: 'bar',
data: durationData,
itemStyle: {
color: '#4361ee' // 统一柱状图颜色
}
}
]
});
}
const getTypeIcon = (type) => {
const icons = {
attendance: Check,
overtime: Clock,
leave: Minus,
travel: Minus
};
return icons[type] || Document;
};
const loading = ref(false)
// 创建多个ref记录init的chart
const trendChart = ref(null);
const durationChart = ref(null);
const heatmapChart = ref(null);
const attendanceRankChart = ref(null);
const initCharts = () => {
trendChart.value = echarts.init(document.querySelector('#trendChart'));
// 横向的条形图,显示用户名和出勤时长
attendanceRankChart.value = echarts.init(document.querySelector('#attendanceRankChart'));
durationChart.value = echarts.init(document.querySelector('#durationChart'));
heatmapChart.value = echarts.init(document.querySelector('#heatmapChart'));
};
const updateCharts = () => {
setStatistics(formatters.status(list.value))
const trendData = formatters.trend(list.value)
setTrend(trendData)
const heatmapData = formatters.heatmap(list.value)
setHeatmap(heatmapData)
const attendanceRankData = formatters.rank(list.value)
setAttendanceRank(attendanceRankData)
const durationData = formatters.duration(list.value)
setDuration(durationData)
}
const formatters = {
status: (list) => {
// 统计出勤时长,计算总记录数,平均出勤时长和总时长
const totalDuration = list.reduce((acc, item) => acc + item.durationHour, 0)
const totalRecords = list.length
// 可能出现NAN所以需要处理
const averageDuration = totalRecords > 0 ? (totalDuration / totalRecords).toFixed(2) : 0
console.log(totalDuration, totalRecords, averageDuration)
return {
totalDuration,
totalRecords,
averageDuration
}
},
trend: (list) => {
// 用折线图按照时间汇总, 根据recordType分组给考勤 加班和出差给出echarts所需的数据格式
const attendanceData = []
const overtimeData = []
const travelData = []
const xAxis = []
for (const item of list) {
// 如果是相同的recordData则将durationHour相加, xAxis单独去重
const date = item.recordDate
const duration = item.durationHour
if (item.recordType === 'attendance') {
const index = attendanceData.findIndex(item => item.date === date)
if (index !== -1) {
attendanceData[index].duration += duration
} else {
attendanceData.push({ date, duration })
xAxis.push(date)
}
} else if (item.recordType === 'overtime') {
const index = overtimeData.findIndex(item => item.date === date)
if (index !== -1) {
overtimeData[index].duration += duration
} else {
overtimeData.push({ date, duration })
xAxis.push(date)
}
} else if (item.recordType === 'travel') {
const index = travelData.findIndex(item => item.date === date)
if (index !== -1) {
travelData[index].duration += duration
} else {
travelData.push({ date, duration })
xAxis.push(date)
}
}
}
// 对xAxis去重
const xAxisSet = [...new Set(xAxis)]
console.log(attendanceData, overtimeData, travelData)
return {
attendanceData,
overtimeData,
travelData,
xAxis: xAxisSet
}
},
// 出勤排行整理, 只保留排名前10的用户
rank: (list) => {
const map = {};
for (const item of list) {
const user = item.nickName
const duration = item.durationHour
if (map[user]) {
map[user] += duration
} else {
map[user] = duration
}
}
const users = Object.keys(map).sort((a, b) => map[a] - map[b]).slice(0, 10)
const durations = users.map(user => map[user])
return { users, durations }
},
// 出勤时长分布热力图
heatmap: (list) => {
// 定义小时和星期的映射关系
const hours = ['12a', '1a', '2a', '3a', '4a', '5a', '6a',
'7a', '8a', '9a', '10a', '11a',
'12p', '1p', '2p', '3p', '4p', '5p',
'6p', '7p', '8p', '9p', '10p', '11p'];
const days = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
// 初始化7天×24小时的时长矩阵初始值为0
const durationMatrix = Array.from({ length: 7 }, () => Array(24).fill(0));
// 辅助函数将日期字符串转换为星期几索引0=周一6=周日)
const getDayIndex = (dateStr) => {
const date = new Date(dateStr);
let day = date.getDay(); // getDay()返回0=周日1=周一,...6=周六
return day === 0 ? 6 : day - 1; // 转换为0=周一6=周日
};
// 辅助函数将时间字符串转换为小时索引0=12a23=11p
const getHourIndex = (timeStr) => {
const hour = parseInt(timeStr.split(' ')[1].split(':')[0], 10);
return hour; // 0-23直接对应小时索引
};
// 辅助函数:计算两个时间之间每个小时的时长(单位:小时)
const calculateHourlyDurations = (startTime, endTime) => {
const start = new Date(startTime);
const end = new Date(endTime);
// 如果开始时间晚于结束时间,视为当天内的记录(忽略跨天情况)
if (start > end) return {};
const startHour = start.getHours();
const endHour = end.getHours();
const hourly = {};
// 同一小时内
if (startHour === endHour) {
const durationHours = (end - start) / (1000 * 60 * 60);
hourly[startHour] = (hourly[startHour] || 0) + durationHours;
return hourly;
}
// 跨多个小时
// 开始小时的时长
const startEndOfHour = new Date(start);
startEndOfHour.setHours(startHour + 1, 0, 0, 0);
hourly[startHour] = (startEndOfHour - start) / (1000 * 60 * 60);
// 中间完整小时的时长
for (let h = startHour + 1; h < endHour; h++) {
hourly[h] = 1; // 完整小时为1小时
}
// 结束小时的时长
const endStartOfHour = new Date(end);
endStartOfHour.setHours(endHour, 0, 0, 0);
hourly[endHour] = (end - endStartOfHour) / (1000 * 60 * 60);
return hourly;
};
// 处理每条记录
for (const item of list) {
try {
// 获取星期索引y轴
const dayIndex = getDayIndex(item.recordDate);
// 计算每个小时的时长分布
const hourlyDurations = calculateHourlyDurations(item.startTime, item.endTime);
// 累加时长到矩阵中
Object.entries(hourlyDurations).forEach(([hourStr, durationHours]) => {
const hourIndex = parseInt(hourStr, 10); // x轴索引
if (hourIndex >= 0 && hourIndex < 24 && dayIndex >= 0 && dayIndex < 7) {
durationMatrix[dayIndex][hourIndex] += parseFloat(durationHours.toFixed(2));
}
});
} catch (error) {
console.error('处理记录出错:', item, '错误:', error);
}
}
// 转换为热力图所需格式 [[x, y, value], ...]
const heatmapData = [];
for (let y = 0; y < 7; y++) {
for (let x = 0; x < 24; x++) {
heatmapData.push([x, y, durationMatrix[y][x]]);
}
}
// 计算最大值和最小值用于热力图颜色映射
const allValues = heatmapData.map(item => item[2]);
const minValue = Math.min(...allValues);
const maxValue = Math.max(...allValues);
return {
heatmapData,
minValue,
maxValue,
hours,
days
};
},
// 出勤时长分布条形图, 汇总每个人的出勤时长按照时长分成5组统计每一组的人数
duration: (list) => {
const map = {};
for (const item of list) {
const duration = item.durationHour
if (map[duration]) {
map[duration]++
} else {
map[duration] = 1
}
}
const durationData = Object.keys(map)
// 按照durationData的值统计最大和最小的数字均分成5组
const min = Math.min(...durationData)
const max = Math.max(...durationData)
const step = (max - min) / 5
const groups = []
for (let i = 0; i < 5; i++) {
groups.push(min + i * step)
}
// 记录每个分组所在的数量
Object.keys(map).forEach(item => {
// 判断item数据哪个分组
const index = groups.findIndex(group => item <= group)
groups[index] += map[item]
})
return {
xAxis: Object.keys(map),
durationData: Object.values(map)
}
}
}
const params = reactive({
pageNum: 1,
pageSize: 9999,
startTime: '',
endTime: '',
recordType: ''
})
// 请求到的原始数据
const list = ref([]);
// 查询数据
const listData = async () => {
loading.value = true
const { rows, total } = await listAttendanceRecord({
...params,
recordType: selectedTypes.value.join(','),
// YYYY-MM-DD HH:MM:SS
startTime: selectedDate.value[0],
endTime: selectedDate.value[1]
})
list.value = rows
pager.total = total;
updateCharts();
loading.value = false
}
// 表格的分页和表格的展示数据
const pager = reactive({
pageNum: 1,
pageSize: 10,
total: 0
})
const tableList = computed(() => {
return list.value.slice((pager.pageNum - 1) * pager.pageSize, pager.pageNum * pager.pageSize)
})
// 表格相关数据
const tableColumns = [
{ key: 'nickName', label: '用户' },
{ key: 'recordDate', label: '记录日期' },
{ key: 'recordType', label: '类型' },
{ key: 'timeRange', label: '时间范围' },
{ key: 'durationHour', label: '时长(小时)' },
];
const getTypeIconColor = (type) => {
const colors = {
attendance: 'text-blue-500',
overtime: 'text-yellow-500',
leave: 'text-red-500',
travel: 'text-green-500'
};
return colors[type] || '';
};
const getTypeLabel = (type) => {
const labels = {
attendance: '考勤',
overtime: '加班',
leave: '请假',
travel: '出差'
};
return labels[type] || type;
};
onMounted(async () => {
initCharts();
await listData()
updateCharts();
});
</script>
<style scoped>
/* 基础样式 */
.page-container {
min-height: 100vh;
background-color: #f9fafb;
}
.container {
margin-left: 30px;
margin-right: 30px;
}
.flex {
display: flex;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.gap-2 {
gap: 0.5rem;
}
.gap-4 {
gap: 1rem;
}
.mb-6 {
margin-bottom: 1.5rem;
}
.mt-2 {
margin-top: 0.5rem;
}
.mt-4 {
margin-top: 1rem;
}
.mt-6 {
margin-top: 1.5rem;
}
.ml-2 {
margin-left: 0.5rem;
}
.text-sm {
font-size: 0.875rem;
}
.text-lg {
font-size: 1.125rem;
}
.text-2xl {
font-size: 1.5rem;
}
.font-medium {
font-weight: 500;
}
.font-semibold {
font-weight: 600;
}
.text-gray-500 {
color: #6b7280;
}
.text-green-500 {
color: #10b981;
}
.text-red-500 {
color: #ef4444;
}
.text-blue-500 {
color: #3b82f6;
}
.text-yellow-500 {
color: #f59e0b;
}
.whitespace-nowrap {
white-space: nowrap;
}
.full-width-table {
width: 100%;
}
/* 筛选区样式 */
.filter-bar {
width: 100%;
background-color: white;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
padding: 1rem;
margin-bottom: 1.5rem;
}
.search-input {
border-radius: 0.375rem;
}
/* 统计卡片样式 */
.stat-card {
height: 100%;
}
.stat-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.stat-value-container {
margin-top: 0.5rem;
}
/* 图表样式 */
.chart-card {
height: 100%;
}
.chart-title {
font-size: 1.125rem;
font-weight: 500;
}
.chart-container {
height: 300px;
}
/* 表格样式 */
.table-card {
overflow: hidden;
}
.pagination-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 1rem;
}
/* 滚动条样式 */
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #888;
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* 数字输入框样式 */
:deep(input[type="number"]::-webkit-inner-spin-button),
:deep(input[type="number"]::-webkit-outer-spin-button) {
-webkit-appearance: none;
margin: 0;
}
</style>

View File

@@ -0,0 +1,326 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="80px">
<el-form-item label="员工" prop="employeeName">
<user-select v-model="queryParams.employeeId" placeholder="请选择员工" />
</el-form-item>
<el-form-item label="发薪月份" prop="payPeriod">
<!-- 只选择年和月拼接 -01 -->
<el-date-picker clearable
v-model="queryParams.payPeriod"
type="month"
value-format="YYYY-MM-01 00:00:00"
placeholder="请选择发薪月份">
</el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Plus"
@click="handleAdd"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="Edit"
:disabled="single"
@click="handleUpdate"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="Delete"
:disabled="multiple"
@click="handleDelete"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="Download"
@click="handleExport"
>导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="salaryRecordsList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="唯一记录ID" align="center" prop="salaryId" v-if="false"/>
<el-table-column label="员工" align="center" prop="employeeName" />
<el-table-column label="发薪月份" align="center" prop="payPeriod" width="180">
<template #default="scope">
<span>{{ formatterTime(scope.row.payPeriod) }}</span>
</template>
</el-table-column>
<el-table-column label="基本工资" align="center" prop="baseSalary" />
<el-table-column label="绩效奖金" align="center" prop="performanceBonus" />
<el-table-column label="加班工资" align="center" prop="overtimePay" />
<el-table-column label="各类补贴" align="center" prop="allowance" />
<el-table-column label="社保个人部分" align="center" prop="socialSecurity" />
<el-table-column label="公积金个人部分" align="center" prop="housingFund" />
<el-table-column label="个人所得税" align="center" prop="incomeTax" />
<!-- <el-table-column label="应发工资" align="center" prop="grossSalary" />
<el-table-column label="实发工资" align="center" prop="netSalary" /> -->
<el-table-column label="发放状态" align="center" prop="payStatus">
<template #default="scope">
<dict-tag :options="oa_salary_status" :value="scope.row.payStatus"/>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)">修改</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加或修改工资发放记录对话框 -->
<el-dialog :title="title" v-model="open" width="600px" append-to-body>
<el-form ref="salaryRecordsRef" :model="form" :rules="rules" label-width="120px">
<el-form-item label="员工" prop="employeeId">
<user-select v-model="form.employeeId" placeholder="请选择员工" />
</el-form-item>
<el-form-item label="发薪月份" prop="payPeriod">
<el-date-picker clearable
v-model="form.payPeriod"
type="month"
value-format="YYYY-MM-01 00:00:00"
placeholder="请选择发薪月份">
</el-date-picker>
</el-form-item>
<el-form-item label="基本工资" prop="baseSalary">
<el-input v-model="form.baseSalary" placeholder="请输入基本工资" />
</el-form-item>
<el-form-item label="绩效奖金" prop="performanceBonus">
<el-input v-model="form.performanceBonus" placeholder="请输入绩效奖金" />
</el-form-item>
<el-form-item label="加班工资" prop="overtimePay">
<el-input v-model="form.overtimePay" placeholder="请输入加班工资" />
</el-form-item>
<el-form-item label="各类补贴" prop="allowance">
<el-input v-model="form.allowance" placeholder="请输入各类补贴" />
</el-form-item>
<el-form-item label="社保个人部分" prop="socialSecurity">
<el-input v-model="form.socialSecurity" placeholder="请输入社保个人部分" />
</el-form-item>
<el-form-item label="公积金个人部分" prop="housingFund">
<el-input v-model="form.housingFund" placeholder="请输入公积金个人部分" />
</el-form-item>
<el-form-item label="个人所得税" prop="incomeTax">
<el-input v-model="form.incomeTax" placeholder="请输入个人所得税" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
<el-form-item label="发放状态" prop="payStatus">
<el-select v-model="form.payStatus" placeholder="请选择发放状态">
<el-option
v-for="dict in oa_salary_status"
:key="dict.value"
:label="dict.label"
:value="dict.value"
></el-option>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="SalaryRecords">
import { listSalaryRecords, getSalaryRecords, delSalaryRecords, addSalaryRecords, updateSalaryRecords, batchPaySalary } from "@/api/oa/salaryRecords";
import UserSelect from "@/components/UserSelect/index.vue";
import { ElMessageBox } from 'element-plus'
const { proxy } = getCurrentInstance();
const { oa_salary_status } = proxy.useDict('oa_salary_status');
const salaryRecordsList = ref([]);
const open = ref(false);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const title = ref("");
const data = reactive({
form: {},
queryParams: {
pageNum: 1,
pageSize: 10,
employeeId: undefined,
payPeriod: undefined,
},
rules: {
}
});
const { queryParams, form, rules } = toRefs(data);
const formatterTime = (time) => {
return time ? proxy.parseTime(time, '{y}-{m}') : null;
}
/** 查询工资发放记录列表 */
function getList() {
loading.value = true;
listSalaryRecords(queryParams.value).then(response => {
salaryRecordsList.value = response.rows;
total.value = response.total;
loading.value = false;
});
}
// 取消按钮
function cancel() {
open.value = false;
reset();
}
// 表单重置
function reset() {
form.value = {
salaryId: null,
employeeId: null,
payPeriod: null,
baseSalary: null,
performanceBonus: null,
overtimePay: null,
allowance: null,
socialSecurity: null,
housingFund: null,
incomeTax: null,
grossSalary: null,
netSalary: null,
createBy: null,
createTime: null,
updateBy: null,
updateTime: null,
delFlag: null,
remark: null,
payStatus: null
};
proxy.resetForm("salaryRecordsRef");
}
/** 搜索按钮操作 */
function handleQuery() {
queryParams.value.pageNum = 1;
getList();
}
/** 重置按钮操作 */
function resetQuery() {
proxy.resetForm("queryRef");
handleQuery();
}
// 多选框选中数据
function handleSelectionChange(selection) {
ids.value = selection.map(item => item.salaryId);
single.value = selection.length != 1;
multiple.value = !selection.length;
}
/** 新增按钮操作 */
function handleAdd() {
reset();
open.value = true;
title.value = "添加工资发放记录";
}
/** 修改按钮操作 */
function handleUpdate(row) {
loading.value = true
reset();
const _salaryId = row.salaryId || ids.value
getSalaryRecords(_salaryId).then(response => {
loading.value = false;
form.value = response.data;
open.value = true;
title.value = "修改工资发放记录";
});
}
/** 提交按钮 */
function submitForm() {
proxy.$refs["salaryRecordsRef"].validate(valid => {
if (valid) {
buttonLoading.value = true;
if (form.value.salaryId != null) {
updateSalaryRecords(form.value).then(response => {
proxy.$modal.msgSuccess("修改成功");
open.value = false;
getList();
}).finally(() => {
buttonLoading.value = false;
});
} else {
addSalaryRecords(form.value).then(response => {
proxy.$modal.msgSuccess("新增成功");
open.value = false;
getList();
}).finally(() => {
buttonLoading.value = false;
});
}
}
});
}
/** 删除按钮操作 */
function handleDelete(row) {
const _salaryIds = row.salaryId || ids.value;
proxy.$modal.confirm('是否确认删除工资发放记录编号为"' + _salaryIds + '"的数据项?').then(function() {
loading.value = true;
return delSalaryRecords(_salaryIds);
}).then(() => {
loading.value = true;
getList();
proxy.$modal.msgSuccess("删除成功");
}).catch(() => {
}).finally(() => {
loading.value = false;
});
}
/** 导出按钮操作 */
function handleExport() {
proxy.download('oa/salaryRecords/export', {
...queryParams.value
}, `salaryRecords_${new Date().getTime()}.xlsx`)
}
getList();
</script>

View File

@@ -4,12 +4,14 @@ import createAutoImport from './auto-import'
import createSvgIcon from './svg-icon'
import createCompression from './compression'
import createSetupExtend from './setup-extend'
import createTailwindcss from './tailwind'
export default function createVitePlugins(viteEnv, isBuild = false) {
const vitePlugins = [vue()]
vitePlugins.push(createAutoImport())
vitePlugins.push(createSetupExtend())
vitePlugins.push(createSvgIcon(isBuild))
// vitePlugins.push(createTailwindcss())
isBuild && vitePlugins.push(...createCompression(viteEnv))
return vitePlugins
}

View File

@@ -0,0 +1,5 @@
import tailwindcss from '@tailwindcss/vite'
export default function createTailwindcss() {
return tailwindcss()
}