优化backend报销管理模块
This commit is contained in:
Generated
+466
@@ -16,9 +16,12 @@
|
|||||||
"docx-preview": "^0.3.7",
|
"docx-preview": "^0.3.7",
|
||||||
"echarts": "^6.0.0",
|
"echarts": "^6.0.0",
|
||||||
"element-plus": "^2.11.7",
|
"element-plus": "^2.11.7",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
|
"jspdf": "^4.2.1",
|
||||||
"less": "^4.4.2",
|
"less": "^4.4.2",
|
||||||
"marked": "^16.4.1",
|
"marked": "^16.4.1",
|
||||||
"os": "^0.1.2",
|
"os": "^0.1.2",
|
||||||
|
"pdfjs-dist": "^6.2.108",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"v-viewer": "^3.0.11",
|
"v-viewer": "^3.0.11",
|
||||||
"vue": "^3.5.22",
|
"vue": "^3.5.22",
|
||||||
@@ -82,6 +85,15 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/types": {
|
"node_modules/@babel/types": {
|
||||||
"version": "7.29.0",
|
"version": "7.29.0",
|
||||||
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz",
|
||||||
@@ -726,6 +738,256 @@
|
|||||||
"@chevrotain/types": "~11.1.2"
|
"@chevrotain/types": "~11.1.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@napi-rs/canvas": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas/-/canvas-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-GaPlicMtnvgPr5SowFRprkEJicDSrV3qCq17U4jiF5u0kNORZo3IbdN2Bk4SfcZJAMYFHbMVJ81O3w21CYxazg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"workspaces": [
|
||||||
|
"e2e/*"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@napi-rs/canvas-android-arm64": "1.0.5",
|
||||||
|
"@napi-rs/canvas-darwin-arm64": "1.0.5",
|
||||||
|
"@napi-rs/canvas-darwin-x64": "1.0.5",
|
||||||
|
"@napi-rs/canvas-linux-arm-gnueabihf": "1.0.5",
|
||||||
|
"@napi-rs/canvas-linux-arm64-gnu": "1.0.5",
|
||||||
|
"@napi-rs/canvas-linux-arm64-musl": "1.0.5",
|
||||||
|
"@napi-rs/canvas-linux-riscv64-gnu": "1.0.5",
|
||||||
|
"@napi-rs/canvas-linux-x64-gnu": "1.0.5",
|
||||||
|
"@napi-rs/canvas-linux-x64-musl": "1.0.5",
|
||||||
|
"@napi-rs/canvas-win32-arm64-msvc": "1.0.5",
|
||||||
|
"@napi-rs/canvas-win32-x64-msvc": "1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-android-arm64": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-ZzDlpKQocwFfCwhMh17UWre6Qt5yZN3kNIJoUpGfRZqwDDZ164IKOsPOHsRd3d8Tuj5KM6bDjGuPmZxuPuG3NQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-darwin-arm64": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-Hr8v6CA/TBe+OJOePdV3sXWxzQHQKfQsTKPbc8wG7iPqVeAx6MMdzKGXlYID6SVvpfwV/zqkvGcdImYWSlhrZg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-darwin-x64": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-9BXlLHBXpYnK4jSae1MdFdyPq09Xi1I3PeCNpvRzqgmUUBhgJS7aC1z7SZEP8JUXDHtbfIrolCS3sFuT9IGP2A==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-zEW4fgvtYsOJ/N56Us4TQPfaFrUf0shGr9CgGSj3GACc+NHfUM3ci0YF9xFTjwJWGDmaEhYPL6KqCfFCxwm/qg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-HFprwLspelJxCEtZvdMcz95Mwvfs63GzFVedLFmC/wslHnaOXhjXxgYxPCM/VdM4Jhx3CV4Lk0vVmh6hJv2etQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-FNMGFAx8DvtDwlLfWyBJ+oQjgPXoIAqCnNTJYqtJCFRwwzK3AyAe5B1Ll3NZ6hcOzqw7HylZsq4nQxVyPCQX1Q==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-2vd5v8Lui+37Hh/spITKIvTT384ip4dnUc5XBn0E+sMNS4b7au7IswyT5YDG2udPTjSh/eLUs3sGuB5YHooFOA==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-iQIPy+Uey0expZTOszLri5n8rY7x4WUpMaY82mcXNIgbil30iHvc01OsiizhZC1KQHTK90RFMFWSpwFU+ON3aA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-x64-musl": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-Npthji25t7FUqIAKsoEkFS0qY5CYVkHjvI3tuZjDTG92wX8g4dst+Lfb4hhubdqPazlDcIwalPzInsFCtf3FFg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-win32-arm64-msvc": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-bi+JsdCdbfVJDoAQybTYmkLKwh1xpYpptg5j/BNr2BB56u4/R26jrVvtjqff+CxYMXV6Kz1/jeDVhZCuj6bDng==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-KQQwG9/sBmcGxqaLFIQf+k2OefREGoyEaIBmRuTM8bUuFKOEE9Xk5pel90hE6pmBwk/vo4RB0OdX+FxobJGMFw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@@ -2068,6 +2330,19 @@
|
|||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/pako": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/pako/-/pako-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/raf": {
|
||||||
|
"version": "3.4.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/raf/-/raf-3.4.3.tgz",
|
||||||
|
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@types/trusted-types": {
|
"node_modules/@types/trusted-types": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
@@ -2992,6 +3267,15 @@
|
|||||||
"proxy-from-env": "^1.1.0"
|
"proxy-from-env": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-arraybuffer": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/birpc": {
|
"node_modules/birpc": {
|
||||||
"version": "2.9.0",
|
"version": "2.9.0",
|
||||||
"resolved": "https://registry.npmmirror.com/birpc/-/birpc-2.9.0.tgz",
|
"resolved": "https://registry.npmmirror.com/birpc/-/birpc-2.9.0.tgz",
|
||||||
@@ -3060,6 +3344,26 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/canvg": {
|
||||||
|
"version": "3.0.11",
|
||||||
|
"resolved": "https://registry.npmmirror.com/canvg/-/canvg-3.0.11.tgz",
|
||||||
|
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@types/raf": "^3.4.0",
|
||||||
|
"core-js": "^3.8.3",
|
||||||
|
"raf": "^3.4.1",
|
||||||
|
"regenerator-runtime": "^0.13.7",
|
||||||
|
"rgbcolor": "^1.0.1",
|
||||||
|
"stackblur-canvas": "^2.0.0",
|
||||||
|
"svg-pathdata": "^6.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cfb": {
|
"node_modules/cfb": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmmirror.com/cfb/-/cfb-1.2.2.tgz",
|
"resolved": "https://registry.npmmirror.com/cfb/-/cfb-1.2.2.tgz",
|
||||||
@@ -3212,6 +3516,15 @@
|
|||||||
"integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==",
|
"integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/css-line-break": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"utrie": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
|
||||||
@@ -4222,6 +4535,33 @@
|
|||||||
"node": ">=8.6.0"
|
"node": ">=8.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-png": {
|
||||||
|
"version": "6.4.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/fast-png/-/fast-png-6.4.0.tgz",
|
||||||
|
"integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/pako": "^2.0.3",
|
||||||
|
"iobuffer": "^5.3.2",
|
||||||
|
"pako": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fast-png/node_modules/pako": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/pako/-/pako-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/puzrin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/nodeca"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "(MIT AND Zlib)"
|
||||||
|
},
|
||||||
"node_modules/fastq": {
|
"node_modules/fastq": {
|
||||||
"version": "1.20.1",
|
"version": "1.20.1",
|
||||||
"resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz",
|
"resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz",
|
||||||
@@ -4249,6 +4589,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fflate": {
|
||||||
|
"version": "0.8.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/fflate/-/fflate-0.8.3.tgz",
|
||||||
|
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/file-saver": {
|
"node_modules/file-saver": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmmirror.com/file-saver/-/file-saver-2.0.5.tgz",
|
"resolved": "https://registry.npmmirror.com/file-saver/-/file-saver-2.0.5.tgz",
|
||||||
@@ -4619,6 +4965,19 @@
|
|||||||
"url": "https://jaywcjlove.github.io/#/sponsor"
|
"url": "https://jaywcjlove.github.io/#/sponsor"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/html2canvas": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"css-line-break": "^2.1.0",
|
||||||
|
"text-segmentation": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/iconv-lite": {
|
"node_modules/iconv-lite": {
|
||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
@@ -4689,6 +5048,12 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/iobuffer": {
|
||||||
|
"version": "5.4.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/iobuffer/-/iobuffer-5.4.0.tgz",
|
||||||
|
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/is-array-buffer": {
|
"node_modules/is-array-buffer": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmmirror.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
"resolved": "https://registry.npmmirror.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||||
@@ -5076,6 +5441,23 @@
|
|||||||
"integrity": "sha512-CuHU9hC6dPsHF5oVFMo8NW76uQVjH4L22CsP4hW+dNnGywJHC/B0ThA1CTDVLnxKLrrpYdicBLnd2xsgTfRnvg==",
|
"integrity": "sha512-CuHU9hC6dPsHF5oVFMo8NW76uQVjH4L22CsP4hW+dNnGywJHC/B0ThA1CTDVLnxKLrrpYdicBLnd2xsgTfRnvg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/jspdf": {
|
||||||
|
"version": "4.2.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/jspdf/-/jspdf-4.2.1.tgz",
|
||||||
|
"integrity": "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.28.6",
|
||||||
|
"fast-png": "^6.2.0",
|
||||||
|
"fflate": "^0.8.1"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"canvg": "^3.0.11",
|
||||||
|
"core-js": "^3.6.0",
|
||||||
|
"dompurify": "^3.3.1",
|
||||||
|
"html2canvas": "^1.0.0-rc.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jszip": {
|
"node_modules/jszip": {
|
||||||
"version": "3.10.1",
|
"version": "3.10.1",
|
||||||
"resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz",
|
"resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz",
|
||||||
@@ -5685,12 +6067,31 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/pdfjs-dist": {
|
||||||
|
"version": "6.2.108",
|
||||||
|
"resolved": "https://registry.npmmirror.com/pdfjs-dist/-/pdfjs-dist-6.2.108.tgz",
|
||||||
|
"integrity": "sha512-YxFb+SQcodN2rnX9Tn3dHYlqfb7NjlzzfONPpJd+AKoKtUjEdevTfbC07d5TcczzOK6261auRkP/M8OBHs9vFQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.13.0 || >=24"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@napi-rs/canvas": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/perfect-debounce": {
|
"node_modules/perfect-debounce": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
||||||
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
|
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/performance-now": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/performance-now/-/performance-now-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -6109,6 +6510,16 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/raf": {
|
||||||
|
"version": "3.4.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/raf/-/raf-3.4.1.tgz",
|
||||||
|
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"performance-now": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/rangetouch": {
|
"node_modules/rangetouch": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/rangetouch/-/rangetouch-2.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/rangetouch/-/rangetouch-2.0.1.tgz",
|
||||||
@@ -6166,6 +6577,13 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/regenerator-runtime": {
|
||||||
|
"version": "0.13.11",
|
||||||
|
"resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||||
|
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/regexp.prototype.flags": {
|
"node_modules/regexp.prototype.flags": {
|
||||||
"version": "1.5.4",
|
"version": "1.5.4",
|
||||||
"resolved": "https://registry.npmmirror.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
|
"resolved": "https://registry.npmmirror.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
|
||||||
@@ -6202,6 +6620,16 @@
|
|||||||
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/rgbcolor": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/rgbcolor/-/rgbcolor-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
|
||||||
|
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.15"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/robust-predicates": {
|
"node_modules/robust-predicates": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmmirror.com/robust-predicates/-/robust-predicates-3.0.3.tgz",
|
"resolved": "https://registry.npmmirror.com/robust-predicates/-/robust-predicates-3.0.3.tgz",
|
||||||
@@ -6573,6 +7001,16 @@
|
|||||||
"node": ">=0.8"
|
"node": ">=0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/stackblur-canvas": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.1.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/stop-iteration-iterator": {
|
"node_modules/stop-iteration-iterator": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
||||||
@@ -6709,6 +7147,25 @@
|
|||||||
"url": "https://github.com/sponsors/mesqueeb"
|
"url": "https://github.com/sponsors/mesqueeb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/svg-pathdata": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/text-segmentation": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"utrie": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tinyexec": {
|
"node_modules/tinyexec": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.2.4.tgz",
|
"resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.2.4.tgz",
|
||||||
@@ -7067,6 +7524,15 @@
|
|||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/utrie": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/utrie/-/utrie-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-arraybuffer": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/uuid": {
|
"node_modules/uuid": {
|
||||||
"version": "10.0.0",
|
"version": "10.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-10.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-10.0.0.tgz",
|
||||||
|
|||||||
@@ -17,9 +17,12 @@
|
|||||||
"docx-preview": "^0.3.7",
|
"docx-preview": "^0.3.7",
|
||||||
"echarts": "^6.0.0",
|
"echarts": "^6.0.0",
|
||||||
"element-plus": "^2.11.7",
|
"element-plus": "^2.11.7",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
|
"jspdf": "^4.2.1",
|
||||||
"less": "^4.4.2",
|
"less": "^4.4.2",
|
||||||
"marked": "^16.4.1",
|
"marked": "^16.4.1",
|
||||||
"os": "^0.1.2",
|
"os": "^0.1.2",
|
||||||
|
"pdfjs-dist": "^6.2.108",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"v-viewer": "^3.0.11",
|
"v-viewer": "^3.0.11",
|
||||||
"vue": "^3.5.22",
|
"vue": "^3.5.22",
|
||||||
|
|||||||
@@ -45,6 +45,14 @@ export function submitReimbursement(id) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateReimbursementStatus(id, data) {
|
||||||
|
return request({
|
||||||
|
url: `/backend/reimbursements/${id}/status`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function getReimbursementRecords(id) {
|
export function getReimbursementRecords(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/backend/reimbursements/${id}/records`,
|
url: `/backend/reimbursements/${id}/records`,
|
||||||
@@ -88,3 +96,33 @@ export function getReimbursementExpenseTypes() {
|
|||||||
method: 'get'
|
method: 'get'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getReimbursementExpenseTypesManage() {
|
||||||
|
return request({
|
||||||
|
url: '/backend/reimbursements/types/manage',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createReimbursementExpenseType(data) {
|
||||||
|
return request({
|
||||||
|
url: '/backend/reimbursements/types',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateReimbursementExpenseType(id, data) {
|
||||||
|
return request({
|
||||||
|
url: `/backend/reimbursements/types/${id}`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteReimbursementExpenseType(id) {
|
||||||
|
return request({
|
||||||
|
url: `/backend/reimbursements/types/${id}`,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ onUnmounted(() => {
|
|||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.erp-dashboard {
|
.erp-dashboard {
|
||||||
background-color: #f5f7fa;
|
background-color: #f5f7fa;
|
||||||
min-height: calc(100vh - 84px);
|
// min-height: calc(100vh - 84px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-row {
|
.stat-row {
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ onUnmounted(() => {
|
|||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.erp-dashboard {
|
.erp-dashboard {
|
||||||
background-color: #f5f7fa;
|
background-color: #f5f7fa;
|
||||||
min-height: calc(100vh - 84px);
|
// min-height: calc(100vh - 84px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-row {
|
.stat-row {
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog v-model="visible" title="报销详情" width="980px" destroy-on-close>
|
<el-dialog v-model="visible" title="报销详情" width="980px" destroy-on-close>
|
||||||
<el-descriptions v-if="detail" :column="3" border>
|
<el-descriptions v-if="detail" :column="3" border>
|
||||||
<el-descriptions-item label="报销事由" :span="2">{{ detail.title }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="状态">
|
<el-descriptions-item label="状态">
|
||||||
<el-tag :type="statusType(detail.status)">{{ statusLabel(detail.status) }}</el-tag>
|
<el-tag :type="statusType(detail.status)">{{ statusLabel(detail.status) }}</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="费用类型">{{ detail.expense_type || '-' }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="申请日期">{{ detail.apply_date || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="申请日期">{{ detail.apply_date || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="合计金额">¥ {{ money(detail.total_amount) }}</el-descriptions-item>
|
<el-descriptions-item label="合计金额">¥ {{ money(detail.total_amount) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="备注说明" :span="3">{{ detail.description || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="备注说明" :span="3">{{ detail.description || '-' }}</el-descriptions-item>
|
||||||
@@ -13,8 +11,10 @@
|
|||||||
|
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h4 class="detail-title">费用明细</h4>
|
<h4 class="detail-title">费用明细</h4>
|
||||||
|
<div class="section-actions" style="margin-bottom: 12px;">
|
||||||
<el-button v-if="isDraft" type="primary" size="small" @click="openAddItem">添加费用</el-button>
|
<el-button v-if="isDraft" type="primary" size="small" @click="openAddItem">添加费用</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-table :data="detail?.items || []" border size="small">
|
<el-table :data="detail?.items || []" border size="small">
|
||||||
<el-table-column prop="expense_type" label="费用类型" min-width="120" />
|
<el-table-column prop="expense_type" label="费用类型" min-width="120" />
|
||||||
@@ -22,15 +22,17 @@
|
|||||||
<el-table-column prop="amount" label="金额" width="110">
|
<el-table-column prop="amount" label="金额" width="110">
|
||||||
<template #default="{ row }">¥ {{ money(row.amount) }}</template>
|
<template #default="{ row }">¥ {{ money(row.amount) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="receipt_flag" label="发票" width="70">
|
<el-table-column prop="receipt_flag" label="发票" width="90">
|
||||||
<template #default="{ row }">{{ row.receipt_flag ? '有' : '无' }}</template>
|
<template #default="{ row }">
|
||||||
|
{{ itemHasReceipt(row) ? '有' : '无' }}({{ getItemReceiptFiles(row).length }})
|
||||||
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="description" label="描述" min-width="160" show-overflow-tooltip />
|
<el-table-column prop="description" label="描述" min-width="160" show-overflow-tooltip />
|
||||||
<el-table-column label="操作" :width="isDraft ? 176 : 70" fixed="right">
|
<el-table-column label="操作" :width="isDraft ? 176 : 70" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="item-actions">
|
<div class="item-actions">
|
||||||
<el-button
|
<el-button
|
||||||
v-if="row.receipt_url"
|
v-if="itemHasReceipt(row)"
|
||||||
link
|
link
|
||||||
type="primary"
|
type="primary"
|
||||||
title="查看票据"
|
title="查看票据"
|
||||||
@@ -40,24 +42,18 @@
|
|||||||
<i class="fa-solid fa-eye"></i>
|
<i class="fa-solid fa-eye"></i>
|
||||||
</el-button>
|
</el-button>
|
||||||
<template v-if="isDraft">
|
<template v-if="isDraft">
|
||||||
<el-upload
|
|
||||||
:show-file-list="false"
|
|
||||||
:auto-upload="false"
|
|
||||||
accept="image/*,.pdf"
|
|
||||||
:on-change="file => uploadInvoice(row, file)"
|
|
||||||
:disabled="uploadingItemId === row.id"
|
|
||||||
>
|
|
||||||
<el-button
|
<el-button
|
||||||
link
|
link
|
||||||
type="success"
|
type="success"
|
||||||
title="上传发票"
|
title="上传发票(可多选,追加到当前费用项)"
|
||||||
aria-label="上传发票"
|
aria-label="上传发票"
|
||||||
|
:disabled="batchUploading"
|
||||||
|
@click="openInvoiceFilePicker(row)"
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
:class="uploadingItemId === row.id ? 'fa-solid fa-spinner fa-spin' : 'fa-solid fa-file-arrow-up'"
|
:class="batchUploading ? 'fa-solid fa-spinner fa-spin' : 'fa-solid fa-file-arrow-up'"
|
||||||
></i>
|
></i>
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-upload>
|
|
||||||
<el-button link type="primary" title="编辑" aria-label="编辑" @click="openEditItem(row)">
|
<el-button link type="primary" title="编辑" aria-label="编辑" @click="openEditItem(row)">
|
||||||
<i class="fa-solid fa-pen-to-square"></i>
|
<i class="fa-solid fa-pen-to-square"></i>
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -71,11 +67,36 @@
|
|||||||
<template #empty><el-empty description="暂无费用明细" :image-size="60" /></template>
|
<template #empty><el-empty description="暂无费用明细" :image-size="60" /></template>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
|
<input
|
||||||
|
ref="invoiceFileInputRef"
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
accept="image/*,.pdf,application/pdf"
|
||||||
|
class="invoice-file-input"
|
||||||
|
@change="handleInvoiceFileInputChange"
|
||||||
|
/>
|
||||||
|
|
||||||
<el-dialog v-model="receiptPreviewVisible" title="查看票据" width="980px" append-to-body destroy-on-close>
|
<el-dialog v-model="receiptPreviewVisible" title="查看票据" width="980px" append-to-body destroy-on-close>
|
||||||
<div v-if="receiptList.length" class="receipt-preview">
|
<div v-if="receiptList.length" class="receipt-preview">
|
||||||
<aside class="receipt-list">
|
<aside class="receipt-list">
|
||||||
|
<el-select
|
||||||
|
v-model="receiptTypeFilter"
|
||||||
|
clearable
|
||||||
|
placeholder="筛选费用类型"
|
||||||
|
size="small"
|
||||||
|
class="receipt-filter"
|
||||||
|
@change="handleReceiptFilterChange"
|
||||||
|
>
|
||||||
|
<el-option label="全部" value="" />
|
||||||
|
<el-option
|
||||||
|
v-for="type in receiptTypeOptions"
|
||||||
|
:key="type"
|
||||||
|
:label="type"
|
||||||
|
:value="type"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
<button
|
<button
|
||||||
v-for="receipt in receiptList"
|
v-for="receipt in filteredReceiptList"
|
||||||
:key="receipt.id"
|
:key="receipt.id"
|
||||||
type="button"
|
type="button"
|
||||||
class="receipt-thumb"
|
class="receipt-thumb"
|
||||||
@@ -90,7 +111,7 @@
|
|||||||
lazy
|
lazy
|
||||||
/>
|
/>
|
||||||
<i v-else class="fa-regular fa-file-pdf receipt-thumb-pdf"></i>
|
<i v-else class="fa-regular fa-file-pdf receipt-thumb-pdf"></i>
|
||||||
<span>{{ receipt.name }}</span>
|
<span class="receipt-thumb-name" :title="receipt.name">{{ receipt.name }}</span>
|
||||||
<el-button
|
<el-button
|
||||||
v-if="isDraft"
|
v-if="isDraft"
|
||||||
link
|
link
|
||||||
@@ -101,9 +122,10 @@
|
|||||||
<i class="fa-solid fa-trash"></i>
|
<i class="fa-solid fa-trash"></i>
|
||||||
</el-button>
|
</el-button>
|
||||||
</button>
|
</button>
|
||||||
|
<el-empty v-if="!filteredReceiptList.length" description="暂无匹配的票据" :image-size="48" />
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main v-if="receiptPreviewUrl" class="receipt-viewer">
|
<main v-if="receiptPreviewUrl && filteredReceiptList.some(item => item.id === receiptPreviewItemId)" class="receipt-viewer">
|
||||||
<el-image
|
<el-image
|
||||||
v-if="isReceiptImage"
|
v-if="isReceiptImage"
|
||||||
:src="receiptPreviewUrl"
|
:src="receiptPreviewUrl"
|
||||||
@@ -112,7 +134,11 @@
|
|||||||
/>
|
/>
|
||||||
<iframe v-else :src="receiptPreviewUrl" title="票据 PDF 预览" class="receipt-pdf"></iframe>
|
<iframe v-else :src="receiptPreviewUrl" title="票据 PDF 预览" class="receipt-pdf"></iframe>
|
||||||
</main>
|
</main>
|
||||||
|
<main v-else class="receipt-viewer receipt-viewer-empty">
|
||||||
|
<el-empty description="请选择左侧票据查看" :image-size="80" />
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
<el-empty v-else description="暂无票据" :image-size="80" />
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<h4 class="detail-title" style="margin-top: 20px;">审批记录</h4>
|
<h4 class="detail-title" style="margin-top: 20px;">审批记录</h4>
|
||||||
@@ -137,6 +163,45 @@
|
|||||||
:expense-type-options="expenseTypeOptions"
|
:expense-type-options="expenseTypeOptions"
|
||||||
@saved="handleItemsSaved"
|
@saved="handleItemsSaved"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="manualConfirmVisible"
|
||||||
|
title="手动确认"
|
||||||
|
width="420px"
|
||||||
|
append-to-body
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<p class="manual-tip">选择要变更的单据状态,保存后立即生效。</p>
|
||||||
|
<el-radio-group v-model="manualStatus" class="manual-status-group">
|
||||||
|
<el-radio
|
||||||
|
v-for="item in statusOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:value="item.value"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="manualConfirmVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="manualSaving" @click="saveManualStatus">确认</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="detail-toolbar">
|
||||||
|
<el-button @click="openManualConfirm">手动确认</el-button>
|
||||||
|
<div class="toolbar-right">
|
||||||
|
<el-button
|
||||||
|
v-if="isDraft"
|
||||||
|
type="primary"
|
||||||
|
:loading="submitting"
|
||||||
|
@click="handleSubmit"
|
||||||
|
>
|
||||||
|
提交审核
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -144,7 +209,7 @@
|
|||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { uploadFile } from '@/api/file'
|
import { uploadFile } from '@/api/file'
|
||||||
import { deleteReimbursementItem, updateReimbursementItem } from '@/api/reimburse'
|
import { deleteReimbursementItem, submitReimbursement, updateReimbursementItem, updateReimbursementStatus } from '@/api/reimburse'
|
||||||
import ItemForm from './itemForm.vue'
|
import ItemForm from './itemForm.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -155,39 +220,101 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue', 'changed'])
|
const emit = defineEmits(['update:modelValue', 'changed'])
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{ value: 0, label: '草稿' },
|
||||||
|
{ value: 1, label: '审批中' },
|
||||||
|
{ value: 2, label: '已通过' },
|
||||||
|
{ value: 3, label: '已驳回' },
|
||||||
|
{ value: 4, label: '已撤回' },
|
||||||
|
{ value: 5, label: '已打款' }
|
||||||
|
]
|
||||||
|
const statusLabel = value => statusOptions.find(item => item.value === value)?.label || '未知'
|
||||||
|
const statusType = value => ({ 0: 'info', 1: 'warning', 2: 'success', 3: 'danger', 4: 'info', 5: 'success' })[value] || 'info'
|
||||||
|
|
||||||
const visible = computed({
|
const visible = computed({
|
||||||
get: () => props.modelValue,
|
get: () => props.modelValue,
|
||||||
set: value => emit('update:modelValue', value)
|
set: value => emit('update:modelValue', value)
|
||||||
})
|
})
|
||||||
const isDraft = computed(() => props.detail?.status === 0)
|
const isDraft = computed(() => props.detail?.status === 0)
|
||||||
|
const submitting = ref(false)
|
||||||
|
const manualConfirmVisible = ref(false)
|
||||||
|
const manualSaving = ref(false)
|
||||||
|
const manualStatus = ref(0)
|
||||||
const itemVisible = ref(false)
|
const itemVisible = ref(false)
|
||||||
const uploadingItemId = ref(0)
|
const batchUploading = ref(false)
|
||||||
|
const invoiceFileInputRef = ref(null)
|
||||||
|
const pendingUploadRow = ref(null)
|
||||||
const editingItemId = ref(0)
|
const editingItemId = ref(0)
|
||||||
const editingItem = ref(null)
|
const editingItem = ref(null)
|
||||||
const receiptPreviewVisible = ref(false)
|
const receiptPreviewVisible = ref(false)
|
||||||
const receiptPreviewUrl = ref('')
|
const receiptPreviewUrl = ref('')
|
||||||
const receiptPreviewType = ref('')
|
const receiptPreviewType = ref('')
|
||||||
const receiptPreviewItemId = ref(0)
|
const receiptPreviewItemId = ref('')
|
||||||
|
const receiptTypeFilter = ref('')
|
||||||
|
|
||||||
|
const getReceiptUrl = value => (value ? String(value).split(',').filter(Boolean)[0] || '' : '')
|
||||||
|
|
||||||
|
const getItemReceiptFiles = item => {
|
||||||
|
if (Array.isArray(item?.receipt_files) && item.receipt_files.length) {
|
||||||
|
return item.receipt_files
|
||||||
|
}
|
||||||
|
const ids = String(item?.receipt_file_ids || '')
|
||||||
|
.split(',')
|
||||||
|
.map(v => v.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
const urls = String(item?.receipt_urls || '')
|
||||||
|
.split(',')
|
||||||
|
.map(v => v.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
if (ids.length) {
|
||||||
|
return ids.map((id, index) => ({
|
||||||
|
file_id: Number(id),
|
||||||
|
url: urls[index] || urls[0] || '',
|
||||||
|
name: ''
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
if (getReceiptUrl(item?.receipt_urls)) {
|
||||||
|
return [{ file_id: 0, url: item.receipt_urls, name: '' }]
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemHasReceipt = item => getItemReceiptFiles(item).length > 0
|
||||||
|
|
||||||
const receiptList = computed(() => {
|
const receiptList = computed(() => {
|
||||||
const items = props.detail?.items || []
|
const items = props.detail?.items || []
|
||||||
const list = []
|
const list = []
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
if (!item.receipt_url) return
|
const files = getItemReceiptFiles(item)
|
||||||
const urls = item.receipt_url.split(',').filter(Boolean)
|
files.forEach((file, index) => {
|
||||||
urls.forEach((url, index) => {
|
const rawUrl = file.url || ''
|
||||||
|
if (!rawUrl) return
|
||||||
list.push({
|
list.push({
|
||||||
id: `${item.id}-${index}`,
|
id: `${item.id}-${file.file_id || index}`,
|
||||||
originalId: item.id, // 记录原始明细ID,用于删除时定位
|
originalId: item.id,
|
||||||
url: resolveReceiptUrl(url),
|
fileId: file.file_id || 0,
|
||||||
isImage: /\.(jpe?g|png|gif|bmp|webp)(\?|#|$)/i.test(url || ''),
|
expense_type: item.expense_type || '未分类',
|
||||||
name: `${item.expense_type || '票据'} ${index + 1}`
|
url: resolveReceiptUrl(rawUrl),
|
||||||
|
rawUrl,
|
||||||
|
isImage: /\.(jpe?g|png|gif|bmp|webp)(\?|#|$)/i.test(rawUrl),
|
||||||
|
name: file.name || `${item.expense_type || '票据'} ${index + 1}`
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
return list
|
return list
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const receiptTypeOptions = computed(() => {
|
||||||
|
const types = new Set(receiptList.value.map(item => item.expense_type))
|
||||||
|
return Array.from(types).sort((a, b) => a.localeCompare(b, 'zh-CN'))
|
||||||
|
})
|
||||||
|
|
||||||
|
const filteredReceiptList = computed(() => {
|
||||||
|
if (!receiptTypeFilter.value) return receiptList.value
|
||||||
|
return receiptList.value.filter(item => item.expense_type === receiptTypeFilter.value)
|
||||||
|
})
|
||||||
|
|
||||||
const isReceiptImage = computed(() => receiptPreviewType.value.startsWith('image/'))
|
const isReceiptImage = computed(() => receiptPreviewType.value.startsWith('image/'))
|
||||||
|
|
||||||
const resolveReceiptUrl = url => {
|
const resolveReceiptUrl = url => {
|
||||||
@@ -197,24 +324,32 @@ const resolveReceiptUrl = url => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const selectReceipt = receipt => {
|
const selectReceipt = receipt => {
|
||||||
|
if (!receipt) return
|
||||||
receiptPreviewItemId.value = receipt.id
|
receiptPreviewItemId.value = receipt.id
|
||||||
receiptPreviewUrl.value = receipt.url
|
receiptPreviewUrl.value = receipt.url
|
||||||
receiptPreviewType.value = receipt.isImage ? 'image/*' : 'application/pdf'
|
receiptPreviewType.value = receipt.isImage ? 'image/*' : 'application/pdf'
|
||||||
}
|
}
|
||||||
|
|
||||||
const openReceiptPreview = row => {
|
const handleReceiptFilterChange = () => {
|
||||||
// 查找列表项,注意这里的 row.id 可能是原始记录ID,或者是多票据生成出的唯一ID
|
const current = filteredReceiptList.value.find(item => item.id === receiptPreviewItemId.value)
|
||||||
// 如果是从表格点击查看,row 对应的是明细项,其本身可能包含多个票据
|
if (current) return
|
||||||
// 我们直接找到该明细对应的第一张票据并打开预览窗口
|
selectReceipt(filteredReceiptList.value[0] || null)
|
||||||
const firstReceipt = receiptList.value.find(item => item.originalId === row.id)
|
if (!filteredReceiptList.value.length) {
|
||||||
|
receiptPreviewUrl.value = ''
|
||||||
|
receiptPreviewType.value = ''
|
||||||
|
receiptPreviewItemId.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!firstReceipt) {
|
const openReceiptPreview = row => {
|
||||||
|
const receipt = receiptList.value.find(item => item.originalId === row.id)
|
||||||
|
if (!receipt) {
|
||||||
ElMessage.warning('暂无票据可查看')
|
ElMessage.warning('暂无票据可查看')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
receiptTypeFilter.value = row.expense_type || ''
|
||||||
receiptPreviewVisible.value = true
|
receiptPreviewVisible.value = true
|
||||||
selectReceipt(firstReceipt)
|
selectReceipt(receipt)
|
||||||
}
|
}
|
||||||
|
|
||||||
const openAddItem = () => {
|
const openAddItem = () => {
|
||||||
@@ -233,31 +368,171 @@ const handleItemsSaved = reimbursementId => {
|
|||||||
editingItem.value = null
|
editingItem.value = null
|
||||||
emit('changed', reimbursementId)
|
emit('changed', reimbursementId)
|
||||||
}
|
}
|
||||||
const uploadInvoice = async (row, file) => {
|
|
||||||
if (!file?.raw) return
|
|
||||||
|
|
||||||
const rawFile = file.raw
|
const handleSubmit = async () => {
|
||||||
const isAllowed = rawFile.type.startsWith('image/') || rawFile.type === 'application/pdf'
|
if (!props.detail?.id) return
|
||||||
if (!isAllowed) {
|
|
||||||
ElMessage.error('发票仅支持图片或 PDF 文件')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadingItemId.value = row.id
|
|
||||||
try {
|
try {
|
||||||
|
await ElMessageBox.confirm('提交后将进入审批流程,确认提交吗?', '提示', { type: 'warning' })
|
||||||
|
submitting.value = true
|
||||||
|
await submitReimbursement(props.detail.id)
|
||||||
|
ElMessage.success('提交成功')
|
||||||
|
emit('changed', props.detail.id)
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel' && error !== 'close') {
|
||||||
|
ElMessage.error(error?.message || '提交失败')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveManualStatus = status => (status >= 0 && status <= 5 ? status : 0)
|
||||||
|
|
||||||
|
const openManualConfirm = () => {
|
||||||
|
manualStatus.value = resolveManualStatus(props.detail?.status ?? 0)
|
||||||
|
manualConfirmVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveManualStatus = async () => {
|
||||||
|
if (!props.detail?.id) return
|
||||||
|
manualSaving.value = true
|
||||||
|
try {
|
||||||
|
await updateReimbursementStatus(props.detail.id, { status: manualStatus.value })
|
||||||
|
ElMessage.success('状态已更新')
|
||||||
|
manualConfirmVisible.value = false
|
||||||
|
emit('changed', props.detail.id)
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error?.message || '更新状态失败')
|
||||||
|
} finally {
|
||||||
|
manualSaving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateInvoiceFile = rawFile => {
|
||||||
|
if (!rawFile) return false
|
||||||
|
const name = rawFile.name.toLowerCase()
|
||||||
|
const byExt = /\.(jpe?g|png|gif|bmp|webp|pdf)$/i.test(name)
|
||||||
|
const byType = !rawFile.type || rawFile.type.startsWith('image/') || rawFile.type === 'application/pdf'
|
||||||
|
if (!byExt && !byType) {
|
||||||
|
ElMessage.error(`「${rawFile.name}」格式不支持,仅支持图片或 PDF`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadFileToSystem = async rawFile => {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', rawFile)
|
formData.append('file', rawFile)
|
||||||
formData.append('cate', 'reimbursement-invoice')
|
formData.append('cate', 'reimbursement-invoice')
|
||||||
const uploadResponse = await uploadFile(formData)
|
const uploadResponse = await uploadFile(formData)
|
||||||
if (uploadResponse?.code !== 200 && uploadResponse?.code !== 201) {
|
if (uploadResponse?.code !== 200 && uploadResponse?.code !== 201) {
|
||||||
throw new Error(uploadResponse?.msg || '发票上传失败')
|
throw new Error(uploadResponse?.msg || `「${rawFile.name}」上传失败`)
|
||||||
}
|
}
|
||||||
|
const url = uploadResponse?.data?.url || uploadResponse?.url || ''
|
||||||
|
const fileId = uploadResponse?.data?.id || uploadResponse?.id
|
||||||
|
if (!fileId) {
|
||||||
|
throw new Error(`「${rawFile.name}」上传成功但未返回文件ID`)
|
||||||
|
}
|
||||||
|
return { id: fileId, url, name: rawFile.name }
|
||||||
|
}
|
||||||
|
|
||||||
const newUrl = uploadResponse?.data?.url || uploadResponse?.url || ''
|
const saveItemReceiptFiles = async (item, files, { append = true } = {}) => {
|
||||||
// 获取当前已有的 URL 列表
|
const existing = append ? getItemReceiptFiles(item) : []
|
||||||
const existingUrls = row.receipt_url ? row.receipt_url.split(',').filter(Boolean) : []
|
const merged = [...existing]
|
||||||
existingUrls.push(newUrl)
|
const seen = new Set(merged.map(file => Number(file.file_id)).filter(Boolean))
|
||||||
|
files.forEach(file => {
|
||||||
|
const id = Number(file.id)
|
||||||
|
if (!id || seen.has(id)) return
|
||||||
|
seen.add(id)
|
||||||
|
merged.push({ file_id: id, url: file.url, name: file.name })
|
||||||
|
})
|
||||||
|
if (!merged.length) return
|
||||||
|
|
||||||
|
const ids = merged.map(file => file.file_id).filter(Boolean)
|
||||||
|
const urls = merged.map(file => file.url).filter(Boolean)
|
||||||
|
await updateReimbursementItem(props.detail.id, item.id, {
|
||||||
|
expense_type: item.expense_type,
|
||||||
|
expense_date: item.expense_date,
|
||||||
|
amount: item.amount,
|
||||||
|
quantity: item.quantity || 1,
|
||||||
|
unit_price: item.unit_price || 0,
|
||||||
|
description: item.description || '',
|
||||||
|
receipt_flag: 1,
|
||||||
|
receipt_file_ids: ids.join(','),
|
||||||
|
receipt_urls: urls.join(',')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadFilesToItem = async (item, rawFiles) => {
|
||||||
|
const uploaded = []
|
||||||
|
for (const file of rawFiles) {
|
||||||
|
uploaded.push(await uploadFileToSystem(file))
|
||||||
|
}
|
||||||
|
await saveItemReceiptFiles(item, uploaded, { append: true })
|
||||||
|
return uploaded.length
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadInvoicesFromRow = async (startRow, rawFiles) => {
|
||||||
|
const files = rawFiles.filter(Boolean)
|
||||||
|
if (!files.length) return
|
||||||
|
|
||||||
|
const validFiles = files.filter(file => validateInvoiceFile(file))
|
||||||
|
if (!validFiles.length) return
|
||||||
|
|
||||||
|
batchUploading.value = true
|
||||||
|
try {
|
||||||
|
const count = await uploadFilesToItem(startRow, validFiles)
|
||||||
|
ElMessage.success(`已向「${startRow.expense_type || '当前费用项'}」追加 ${count} 张发票`)
|
||||||
|
emit('changed', props.detail.id)
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error?.message || '发票上传失败')
|
||||||
|
} finally {
|
||||||
|
batchUploading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openInvoiceFilePicker = row => {
|
||||||
|
pendingUploadRow.value = row
|
||||||
|
const input = invoiceFileInputRef.value
|
||||||
|
if (!input) return
|
||||||
|
input.value = ''
|
||||||
|
input.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleInvoiceFileInputChange = event => {
|
||||||
|
const row = pendingUploadRow.value
|
||||||
|
const files = Array.from(event.target.files || [])
|
||||||
|
pendingUploadRow.value = null
|
||||||
|
event.target.value = ''
|
||||||
|
if (!files.length || !row) return
|
||||||
|
uploadInvoicesFromRow(row, files)
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeReceipt = async receipt => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确认删除该票据吗?', '删除确认', { type: 'warning' })
|
||||||
|
const row = props.detail.items.find(item => item.id === receipt.originalId)
|
||||||
|
if (!row) return
|
||||||
|
|
||||||
|
const remaining = getItemReceiptFiles(row).filter(file => {
|
||||||
|
if (receipt.fileId) return Number(file.file_id) !== Number(receipt.fileId)
|
||||||
|
return resolveReceiptUrl(file.url) !== receipt.url
|
||||||
|
})
|
||||||
|
if (!remaining.length) {
|
||||||
|
await updateReimbursementItem(props.detail.id, row.id, {
|
||||||
|
expense_type: row.expense_type,
|
||||||
|
expense_date: row.expense_date,
|
||||||
|
amount: row.amount,
|
||||||
|
quantity: row.quantity || 1,
|
||||||
|
unit_price: row.unit_price || 0,
|
||||||
|
description: row.description || '',
|
||||||
|
receipt_flag: 0,
|
||||||
|
receipt_file_ids: '',
|
||||||
|
receipt_urls: ''
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const ids = remaining.map(file => file.file_id).filter(Boolean)
|
||||||
|
const urls = remaining.map(file => file.url).filter(Boolean)
|
||||||
await updateReimbursementItem(props.detail.id, row.id, {
|
await updateReimbursementItem(props.detail.id, row.id, {
|
||||||
expense_type: row.expense_type,
|
expense_type: row.expense_type,
|
||||||
expense_date: row.expense_date,
|
expense_date: row.expense_date,
|
||||||
@@ -266,50 +541,18 @@ const uploadInvoice = async (row, file) => {
|
|||||||
unit_price: row.unit_price || 0,
|
unit_price: row.unit_price || 0,
|
||||||
description: row.description || '',
|
description: row.description || '',
|
||||||
receipt_flag: 1,
|
receipt_flag: 1,
|
||||||
receipt_url: existingUrls.join(',')
|
receipt_file_ids: ids.join(','),
|
||||||
|
receipt_urls: urls.join(',')
|
||||||
})
|
})
|
||||||
ElMessage.success('发票上传成功')
|
|
||||||
emit('changed', props.detail.id)
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error(error?.message || '发票上传失败')
|
|
||||||
} finally {
|
|
||||||
uploadingItemId.value = 0
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const removeReceipt = async (receipt) => {
|
|
||||||
try {
|
|
||||||
await ElMessageBox.confirm('确认删除该票据吗?', '删除确认', { type: 'warning' })
|
|
||||||
// 通过原始 ID 查找对应的明细行
|
|
||||||
const row = props.detail.items.find(item => item.id === receipt.originalId)
|
|
||||||
if (!row) return
|
|
||||||
|
|
||||||
// 获取并过滤掉要删除的 URL
|
|
||||||
const existingUrls = row.receipt_url ? row.receipt_url.split(',').filter(Boolean) : []
|
|
||||||
const updatedUrls = existingUrls.filter(url => resolveReceiptUrl(url) !== receipt.url)
|
|
||||||
|
|
||||||
await updateReimbursementItem(props.detail.id, row.id, {
|
|
||||||
...row,
|
|
||||||
receipt_flag: updatedUrls.length > 0 ? 1 : 0,
|
|
||||||
receipt_url: updatedUrls.join(',')
|
|
||||||
})
|
|
||||||
ElMessage.success('票据已移除')
|
ElMessage.success('票据已移除')
|
||||||
emit('changed', props.detail.id)
|
emit('changed', props.detail.id)
|
||||||
|
handleReceiptFilterChange()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error !== 'cancel' && error !== 'close') ElMessage.error('移除票据失败')
|
if (error !== 'cancel' && error !== 'close') ElMessage.error('移除票据失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusOptions = [
|
|
||||||
{ value: 0, label: '草稿' },
|
|
||||||
{ value: 1, label: '审批中' },
|
|
||||||
{ value: 2, label: '已通过' },
|
|
||||||
{ value: 3, label: '已驳回' },
|
|
||||||
{ value: 4, label: '已撤回' },
|
|
||||||
{ value: 5, label: '已打款' }
|
|
||||||
]
|
|
||||||
const statusLabel = value => statusOptions.find(item => item.value === value)?.label || '未知'
|
|
||||||
const statusType = value => ({ 0: 'info', 1: 'warning', 2: 'success', 3: 'danger', 4: 'info', 5: 'success' })[value] || 'info'
|
|
||||||
const actionLabel = value => ({ 1: '提交申请', 2: '审批通过', 3: '审批驳回', 4: '转审', 5: '撤回' })[value] || '审批操作'
|
const actionLabel = value => ({ 1: '提交申请', 2: '审批通过', 3: '审批驳回', 4: '转审', 5: '撤回' })[value] || '审批操作'
|
||||||
const money = value => Number(value || 0).toFixed(2)
|
const money = value => Number(value || 0).toFixed(2)
|
||||||
const formatDateTime = value => value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '-'
|
const formatDateTime = value => value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '-'
|
||||||
@@ -323,6 +566,12 @@ const formatDateTime = value => value ? new Date(value).toLocaleString('zh-CN',
|
|||||||
margin-top: 22px;
|
margin-top: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.detail-title {
|
.detail-title {
|
||||||
margin: 0 0 12px;
|
margin: 0 0 12px;
|
||||||
color: #303133;
|
color: #303133;
|
||||||
@@ -340,7 +589,7 @@ const formatDateTime = value => value ? new Date(value).toLocaleString('zh-CN',
|
|||||||
}
|
}
|
||||||
|
|
||||||
.receipt-list {
|
.receipt-list {
|
||||||
flex: 0 0 200px;
|
flex: 0 0 220px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -350,6 +599,10 @@ const formatDateTime = value => value ? new Date(value).toLocaleString('zh-CN',
|
|||||||
max-height: 620px;
|
max-height: 620px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.receipt-filter {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.receipt-thumb {
|
.receipt-thumb {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -371,10 +624,13 @@ const formatDateTime = value => value ? new Date(value).toLocaleString('zh-CN',
|
|||||||
color: var(--el-color-primary);
|
color: var(--el-color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
span,
|
||||||
|
.receipt-thumb-name {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: visible;
|
min-width: 0;
|
||||||
word-break: break-all;
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,6 +657,11 @@ const formatDateTime = value => value ? new Date(value).toLocaleString('zh-CN',
|
|||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.receipt-viewer-empty {
|
||||||
|
background: var(--el-fill-color-lighter);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.receipt-image {
|
.receipt-image {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -423,14 +684,39 @@ const formatDateTime = value => value ? new Date(value).toLocaleString('zh-CN',
|
|||||||
gap: 4px;
|
gap: 4px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
:deep(.el-upload) {
|
|
||||||
display: inline-flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button {
|
.el-button {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 4px 6px;
|
padding: 4px 6px;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.invoice-file-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-right {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manual-tip {
|
||||||
|
margin: 0 0 16px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manual-status-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="visible = false">取消</el-button>
|
<el-button @click="visible = false">取消</el-button>
|
||||||
<el-button type="primary" :loading="saving" @click="save">创建</el-button>
|
<el-button type="primary" :loading="saving" @click="save">{{ submitLabel }}</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -36,6 +36,7 @@ const props = defineProps({
|
|||||||
modelValue: { type: Boolean, default: false },
|
modelValue: { type: Boolean, default: false },
|
||||||
title: { type: String, default: '新建报销单' },
|
title: { type: String, default: '新建报销单' },
|
||||||
formData: { type: Object, required: true },
|
formData: { type: Object, required: true },
|
||||||
|
isEdit: { type: Boolean, default: false },
|
||||||
expenseTypeOptions: { type: Array, default: () => [] },
|
expenseTypeOptions: { type: Array, default: () => [] },
|
||||||
saving: { type: Boolean, default: false }
|
saving: { type: Boolean, default: false }
|
||||||
})
|
})
|
||||||
@@ -55,6 +56,7 @@ const cloneForm = data => ({
|
|||||||
|
|
||||||
const form = reactive(cloneForm(props.formData))
|
const form = reactive(cloneForm(props.formData))
|
||||||
const formRef = ref()
|
const formRef = ref()
|
||||||
|
const submitLabel = computed(() => (props.isEdit ? '保存' : '创建'))
|
||||||
const formRules = {
|
const formRules = {
|
||||||
apply_date: [{ required: true, message: '请选择申请日期', trigger: 'change' }]
|
apply_date: [{ required: true, message: '请选择申请日期', trigger: 'change' }]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="visible"
|
||||||
|
title="报销类别管理"
|
||||||
|
width="720px"
|
||||||
|
append-to-body
|
||||||
|
destroy-on-close
|
||||||
|
@open="loadList"
|
||||||
|
>
|
||||||
|
<div class="toolbar">
|
||||||
|
<p class="tip">通用费项由平台统一维护,租户仅可管理自行添加的专有费项。</p>
|
||||||
|
<el-button type="primary" :icon="Plus" @click="openForm()">新增专有费项</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="list" stripe max-height="420">
|
||||||
|
<el-table-column prop="name" label="费项名称" min-width="140" />
|
||||||
|
<el-table-column prop="sort_order" label="排序" width="80" align="center" />
|
||||||
|
<el-table-column label="状态" width="90" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.is_active ? 'success' : 'info'" size="small">
|
||||||
|
{{ row.is_active ? '启用' : '禁用' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="类型" width="110" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.is_global ? 'warning' : ''" size="small">
|
||||||
|
{{ row.is_global ? '通用' : '租户专有' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="140" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<template v-if="row.editable">
|
||||||
|
<el-button link type="primary" @click="openForm(row)">编辑</el-button>
|
||||||
|
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
<span v-else class="readonly-tip">平台维护</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<template #empty><el-empty description="暂无费项" /></template>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="formVisible"
|
||||||
|
:title="editingId ? '编辑专有费项' : '新增专有费项'"
|
||||||
|
width="420px"
|
||||||
|
append-to-body
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
|
||||||
|
<el-form-item label="名称" prop="name">
|
||||||
|
<el-input v-model="form.name" maxlength="50" show-word-limit placeholder="请输入费项名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="排序" prop="sort_order">
|
||||||
|
<el-input-number v-model="form.sort_order" :min="0" :max="9999" controls-position="right" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态" prop="is_active">
|
||||||
|
<el-switch v-model="form.is_active" :active-value="1" :inactive-value="0" active-text="启用" inactive-text="禁用" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="formVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="saving" @click="saveForm">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { reactive, ref, watch } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { Plus } from '@element-plus/icons-vue'
|
||||||
|
import {
|
||||||
|
createReimbursementExpenseType,
|
||||||
|
deleteReimbursementExpenseType,
|
||||||
|
getReimbursementExpenseTypesManage,
|
||||||
|
updateReimbursementExpenseType
|
||||||
|
} from '@/api/reimburse'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: Boolean, default: false }
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['update:modelValue', 'changed'])
|
||||||
|
|
||||||
|
const visible = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
const list = ref([])
|
||||||
|
const formVisible = ref(false)
|
||||||
|
const editingId = ref(0)
|
||||||
|
const formRef = ref(null)
|
||||||
|
|
||||||
|
const emptyForm = () => ({ name: '', sort_order: 0, is_active: 1 })
|
||||||
|
const form = reactive(emptyForm())
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
name: [
|
||||||
|
{ required: true, message: '请输入费项名称', trigger: 'blur' },
|
||||||
|
{ min: 1, max: 50, message: '名称长度 1-50 个字符', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseData = res => res?.data?.data ?? res?.data ?? res ?? []
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
val => { visible.value = val }
|
||||||
|
)
|
||||||
|
watch(visible, val => {
|
||||||
|
if (!val) emit('update:modelValue', false)
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const data = responseData(await getReimbursementExpenseTypesManage())
|
||||||
|
list.value = Array.isArray(data) ? data : data.list || []
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error?.message || '加载报销类别失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openForm = row => {
|
||||||
|
editingId.value = row?.id || 0
|
||||||
|
Object.assign(form, row
|
||||||
|
? { name: row.name, sort_order: row.sort_order ?? 0, is_active: row.is_active ?? 1 }
|
||||||
|
: emptyForm())
|
||||||
|
formVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveForm = async () => {
|
||||||
|
if (!formRef.value) return
|
||||||
|
try {
|
||||||
|
await formRef.value.validate()
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const payload = { name: form.name.trim(), sort_order: form.sort_order, is_active: form.is_active }
|
||||||
|
if (editingId.value) {
|
||||||
|
await updateReimbursementExpenseType(editingId.value, payload)
|
||||||
|
ElMessage.success('更新成功')
|
||||||
|
} else {
|
||||||
|
await createReimbursementExpenseType(payload)
|
||||||
|
ElMessage.success('新增成功')
|
||||||
|
}
|
||||||
|
formVisible.value = false
|
||||||
|
await loadList()
|
||||||
|
emit('changed')
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error?.message || '保存失败')
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async row => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确认删除费项「${row.name}」吗?`, '删除确认', { type: 'warning' })
|
||||||
|
await deleteReimbursementExpenseType(row.id)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
await loadList()
|
||||||
|
emit('changed')
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel' && error !== 'close') {
|
||||||
|
ElMessage.error(error?.message || '删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
margin: 0;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 13px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.readonly-tip {
|
||||||
|
color: #c0c4cc;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="visible"
|
v-model="visible"
|
||||||
:title="editingItemId ? '编辑费用明细' : '批量添加费用明细'"
|
:title="editingItemId ? '编辑费用明细' : '添加费用明细'"
|
||||||
width="860px"
|
width="860px"
|
||||||
append-to-body
|
append-to-body
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
<el-form-item label="金额" prop="amount">
|
<el-form-item label="金额" prop="amount">
|
||||||
<el-input-number
|
<el-input-number
|
||||||
v-model="item.amount"
|
v-model="item.amount"
|
||||||
:min="0.01"
|
:min="0.00"
|
||||||
:precision="2"
|
:precision="2"
|
||||||
:step="10"
|
:step="10"
|
||||||
controls-position="right"
|
controls-position="right"
|
||||||
@@ -123,7 +123,7 @@ const createRow = (source = {}) => ({
|
|||||||
amount: Number(source.amount || 0),
|
amount: Number(source.amount || 0),
|
||||||
description: source.description || '',
|
description: source.description || '',
|
||||||
unit_price: Number(source.unit_price || 0),
|
unit_price: Number(source.unit_price || 0),
|
||||||
receipt_url: source.receipt_url || ''
|
receipt_urls: source.receipt_urls || ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
@@ -169,7 +169,8 @@ const submit = async () => {
|
|||||||
unit_price: item.unit_price || 0,
|
unit_price: item.unit_price || 0,
|
||||||
description: item.description || '',
|
description: item.description || '',
|
||||||
receipt_flag: props.editingItem?.receipt_flag || 0,
|
receipt_flag: props.editingItem?.receipt_flag || 0,
|
||||||
receipt_url: item.receipt_url || ''
|
receipt_file_ids: props.editingItem?.receipt_file_ids || '',
|
||||||
|
receipt_urls: props.editingItem?.receipt_urls || ''
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
for (const item of items.value) {
|
for (const item of items.value) {
|
||||||
@@ -181,7 +182,7 @@ const submit = async () => {
|
|||||||
unit_price: item.unit_price || 0,
|
unit_price: item.unit_price || 0,
|
||||||
description: item.description || '',
|
description: item.description || '',
|
||||||
receipt_flag: 0,
|
receipt_flag: 0,
|
||||||
receipt_url: ''
|
receipt_urls: ''
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,21 +5,45 @@
|
|||||||
<h2>报销管理</h2>
|
<h2>报销管理</h2>
|
||||||
<p>管理个人报销申请及审批进度</p>
|
<p>管理个人报销申请及审批进度</p>
|
||||||
</div>
|
</div>
|
||||||
<el-button type="primary" :icon="Plus" @click="openCreate">新建报销</el-button>
|
<div class="header-actions">
|
||||||
|
<el-button :icon="Setting" @click="expenseTypeVisible = true"
|
||||||
|
>报销类别</el-button
|
||||||
|
>
|
||||||
|
<el-button type="primary" :icon="Plus" @click="openCreate"
|
||||||
|
>新建报销</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="filter-card">
|
<div class="filter-card">
|
||||||
<el-form :inline="true" :model="filters" @submit.prevent>
|
<el-form :inline="true" :model="filters" @submit.prevent>
|
||||||
<el-form-item label="关键词">
|
<el-form-item label="关键词">
|
||||||
<el-input v-model="filters.keyword" clearable placeholder="请输入备注说明" @keyup.enter="loadList" />
|
<el-input
|
||||||
|
v-model="filters.keyword"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入备注说明"
|
||||||
|
@keyup.enter="loadList"
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="状态">
|
<el-form-item label="状态">
|
||||||
<el-select v-model="filters.status" clearable placeholder="全部状态" style="width: 150px">
|
<el-select
|
||||||
<el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
|
v-model="filters.status"
|
||||||
|
clearable
|
||||||
|
placeholder="全部状态"
|
||||||
|
style="width: 150px"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in statusOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" :icon="Search" @click="loadList">查询</el-button>
|
<el-button type="primary" :icon="Search" @click="loadList"
|
||||||
|
>查询</el-button
|
||||||
|
>
|
||||||
<el-button :icon="Refresh" @click="resetFilters">重置</el-button>
|
<el-button :icon="Refresh" @click="resetFilters">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
@@ -27,29 +51,69 @@
|
|||||||
|
|
||||||
<div class="table-card">
|
<div class="table-card">
|
||||||
<el-table v-loading="loading" :data="list" stripe>
|
<el-table v-loading="loading" :data="list" stripe>
|
||||||
<el-table-column prop="apply_date" label="申请日期" width="120">
|
<el-table-column prop="status" label="状态" width="80">
|
||||||
<template #default="{ row }">{{ formatDate(row.apply_date) }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="description" label="备注说明" min-width="220" show-overflow-tooltip>
|
|
||||||
<template #default="{ row }">{{ row.description || '-' }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="total_amount" label="报销金额" width="130">
|
|
||||||
<template #default="{ row }">¥ {{ money(row.total_amount) }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="status" label="状态" width="110">
|
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="statusType(row.status)">{{ statusLabel(row.status) }}</el-tag>
|
<el-tag :type="statusType(row.status)">{{
|
||||||
|
statusLabel(row.status)
|
||||||
|
}}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="create_time" label="创建时间" width="175">
|
<el-table-column prop="apply_date" label="申请日期" width="120">
|
||||||
<template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
|
<template #default="{ row }">{{
|
||||||
|
formatDate(row.apply_date)
|
||||||
|
}}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" fixed="right" width="245">
|
<el-table-column prop="total_amount" label="报销金额" width="150">
|
||||||
|
<template #default="{ row }"
|
||||||
|
>¥ {{ money(row.total_amount) }}</template
|
||||||
|
>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
prop="description"
|
||||||
|
label="备注说明"
|
||||||
|
min-width="220"
|
||||||
|
show-overflow-tooltip
|
||||||
|
>
|
||||||
|
<template #default="{ row }">{{ row.description || "-" }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="create_time" label="创建时间" width="175">
|
||||||
|
<template #default="{ row }">{{
|
||||||
|
formatDateTime(row.create_time)
|
||||||
|
}}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" fixed="right" width="300">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button link type="primary" @click="openDetail(row.id)">详情</el-button>
|
<el-button link type="primary" @click="openDetail(row.id)"
|
||||||
<el-button v-if="row.status === 0" link type="primary" @click="openEdit(row.id)">编辑</el-button>
|
>详情</el-button
|
||||||
<el-button v-if="row.status === 0" link type="success" @click="handleSubmit(row)">提交</el-button>
|
>
|
||||||
<el-button v-if="row.status === 0" link type="danger" @click="handleDelete(row)">删除</el-button>
|
<el-button
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:loading="exportingId === row.id"
|
||||||
|
@click="handleExport(row)"
|
||||||
|
>导出</el-button
|
||||||
|
>
|
||||||
|
<el-button
|
||||||
|
v-if="row.status === 0"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
@click="openEdit(row.id)"
|
||||||
|
>编辑</el-button
|
||||||
|
>
|
||||||
|
<el-button
|
||||||
|
v-if="row.status === 0"
|
||||||
|
link
|
||||||
|
type="success"
|
||||||
|
@click="handleSubmit(row)"
|
||||||
|
>提交</el-button
|
||||||
|
>
|
||||||
|
<el-button
|
||||||
|
v-if="row.status === 0"
|
||||||
|
link
|
||||||
|
type="danger"
|
||||||
|
@click="handleDelete(row)"
|
||||||
|
>删除</el-button
|
||||||
|
>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<template #empty><el-empty description="暂无报销记录" /></template>
|
<template #empty><el-empty description="暂无报销记录" /></template>
|
||||||
@@ -83,15 +147,22 @@
|
|||||||
:expense-type-options="expenseTypeOptions"
|
:expense-type-options="expenseTypeOptions"
|
||||||
@changed="reloadDetail"
|
@changed="reloadDetail"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ExpenseTypeDialog
|
||||||
|
v-model="expenseTypeVisible"
|
||||||
|
@changed="loadExpenseTypes"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from "vue";
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import { Plus, Refresh, Search } from '@element-plus/icons-vue'
|
import { Plus, Refresh, Search, Setting } from "@element-plus/icons-vue";
|
||||||
import ReimburseEdit from './components/edit.vue'
|
import ReimburseEdit from "./components/edit.vue";
|
||||||
import ReimburseDetail from './components/detail.vue'
|
import ReimburseDetail from "./components/detail.vue";
|
||||||
|
import ExpenseTypeDialog from "./components/expenseTypeDialog.vue";
|
||||||
|
import { exportReimbursementPdf } from "./utils/exportReimbursementPdf";
|
||||||
import {
|
import {
|
||||||
createReimbursement,
|
createReimbursement,
|
||||||
deleteReimbursement,
|
deleteReimbursement,
|
||||||
@@ -100,174 +171,203 @@ import {
|
|||||||
getReimbursementList,
|
getReimbursementList,
|
||||||
getReimbursementRecords,
|
getReimbursementRecords,
|
||||||
submitReimbursement,
|
submitReimbursement,
|
||||||
updateReimbursement
|
updateReimbursement,
|
||||||
} from '@/api/reimburse'
|
} from "@/api/reimburse";
|
||||||
|
|
||||||
const statusOptions = [
|
const statusOptions = [
|
||||||
{ value: 0, label: '草稿' },
|
{ value: 0, label: "草稿" },
|
||||||
{ value: 1, label: '审批中' },
|
{ value: 1, label: "审批中" },
|
||||||
{ value: 2, label: '已通过' },
|
{ value: 2, label: "已通过" },
|
||||||
{ value: 3, label: '已驳回' },
|
{ value: 3, label: "已驳回" },
|
||||||
{ value: 4, label: '已撤回' },
|
{ value: 4, label: "已撤回" },
|
||||||
{ value: 5, label: '已打款' }
|
{ value: 5, label: "已打款" },
|
||||||
]
|
];
|
||||||
const filters = reactive({ keyword: '', status: undefined })
|
const filters = reactive({ keyword: "", status: undefined });
|
||||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
const pagination = reactive({ page: 1, pageSize: 10, total: 0 });
|
||||||
const list = ref([])
|
const list = ref([]);
|
||||||
const loading = ref(false)
|
const loading = ref(false);
|
||||||
const saving = ref(false)
|
const saving = ref(false);
|
||||||
const formVisible = ref(false)
|
const formVisible = ref(false);
|
||||||
const detailVisible = ref(false)
|
const detailVisible = ref(false);
|
||||||
const editingId = ref(0)
|
const expenseTypeVisible = ref(false);
|
||||||
const detail = ref(null)
|
const editingId = ref(0);
|
||||||
const records = ref([])
|
const detail = ref(null);
|
||||||
const expenseTypeOptions = ref([])
|
const records = ref([]);
|
||||||
|
const expenseTypeOptions = ref([]);
|
||||||
|
const exportingId = ref(0);
|
||||||
|
|
||||||
const emptyForm = () => ({
|
const emptyForm = () => ({
|
||||||
apply_date: new Date().toISOString().slice(0, 10),
|
apply_date: new Date().toISOString().slice(0, 10),
|
||||||
description: ''
|
description: "",
|
||||||
})
|
});
|
||||||
const form = reactive(emptyForm())
|
const form = reactive(emptyForm());
|
||||||
const formTitle = computed(() => editingId.value ? '编辑报销单' : '新建报销单')
|
const formTitle = computed(() =>
|
||||||
|
editingId.value ? "编辑报销单" : "新建报销单",
|
||||||
|
);
|
||||||
|
|
||||||
const responseData = res => res?.data?.data ?? res?.data ?? res ?? {}
|
const responseData = (res) => res?.data?.data ?? res?.data ?? res ?? {};
|
||||||
|
|
||||||
const loadExpenseTypes = async () => {
|
const loadExpenseTypes = async () => {
|
||||||
try {
|
try {
|
||||||
const data = responseData(await getReimbursementExpenseTypes())
|
const data = responseData(await getReimbursementExpenseTypes());
|
||||||
expenseTypeOptions.value = Array.isArray(data) ? data : data.list || []
|
const rows = Array.isArray(data) ? data : data.list || [];
|
||||||
|
expenseTypeOptions.value = rows.filter((item) => item.is_active !== 0);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(error?.message || '加载费用类型失败')
|
ElMessage.error(error?.message || "加载费用类型失败");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const loadList = async () => {
|
const loadList = async () => {
|
||||||
loading.value = true
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const data = responseData(await getReimbursementList({ ...filters, ...pagination }))
|
const data = responseData(
|
||||||
list.value = data.list || []
|
await getReimbursementList({ ...filters, ...pagination }),
|
||||||
pagination.total = Number(data.total || 0)
|
);
|
||||||
|
list.value = data.list || [];
|
||||||
|
pagination.total = Number(data.total || 0);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(error?.message || '加载报销列表失败')
|
ElMessage.error(error?.message || "加载报销列表失败");
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const resetFilters = () => {
|
const resetFilters = () => {
|
||||||
filters.keyword = ''
|
filters.keyword = "";
|
||||||
filters.status = undefined
|
filters.status = undefined;
|
||||||
pagination.page = 1
|
pagination.page = 1;
|
||||||
loadList()
|
loadList();
|
||||||
}
|
};
|
||||||
|
|
||||||
const resetForm = () => Object.assign(form, emptyForm())
|
const resetForm = () => Object.assign(form, emptyForm());
|
||||||
|
|
||||||
const openCreate = () => {
|
const openCreate = () => {
|
||||||
editingId.value = 0
|
editingId.value = 0;
|
||||||
resetForm()
|
resetForm();
|
||||||
formVisible.value = true
|
formVisible.value = true;
|
||||||
}
|
};
|
||||||
|
|
||||||
const openEdit = async id => {
|
const openEdit = async (id) => {
|
||||||
try {
|
try {
|
||||||
const data = responseData(await getReimbursementDetail(id))
|
const data = responseData(await getReimbursementDetail(id));
|
||||||
editingId.value = id
|
editingId.value = id;
|
||||||
Object.assign(form, {
|
Object.assign(form, {
|
||||||
apply_date: data.apply_date || new Date().toISOString().slice(0, 10),
|
apply_date: data.apply_date || new Date().toISOString().slice(0, 10),
|
||||||
description: data.description || ''
|
description: data.description || "",
|
||||||
})
|
});
|
||||||
formVisible.value = true
|
formVisible.value = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(error?.message || '加载报销详情失败')
|
ElMessage.error(error?.message || "加载报销详情失败");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const saveForm = async payload => {
|
const saveForm = async (payload) => {
|
||||||
saving.value = true
|
saving.value = true;
|
||||||
try {
|
try {
|
||||||
if (editingId.value) await updateReimbursement(editingId.value, payload)
|
if (editingId.value) await updateReimbursement(editingId.value, payload);
|
||||||
else await createReimbursement(payload)
|
else await createReimbursement(payload);
|
||||||
ElMessage.success('保存成功')
|
ElMessage.success("保存成功");
|
||||||
formVisible.value = false
|
formVisible.value = false;
|
||||||
loadList()
|
loadList();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(error?.message || '保存失败')
|
ElMessage.error(error?.message || "保存失败");
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleSubmit = async row => {
|
const handleSubmit = async (row) => {
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm('提交后将进入审批流程,确认提交吗?', '提示', { type: 'warning' })
|
await ElMessageBox.confirm("提交后将进入审批流程,确认提交吗?", "提示", {
|
||||||
await submitReimbursement(row.id)
|
type: "warning",
|
||||||
ElMessage.success('提交成功')
|
});
|
||||||
loadList()
|
await submitReimbursement(row.id);
|
||||||
|
ElMessage.success("提交成功");
|
||||||
|
loadList();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error !== 'cancel' && error !== 'close') ElMessage.error(error?.message || '提交失败')
|
if (error !== "cancel" && error !== "close")
|
||||||
|
ElMessage.error(error?.message || "提交失败");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleDelete = async row => {
|
const handleDelete = async (row) => {
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm('确认删除这条报销记录吗?', '删除确认', { type: 'warning' })
|
await ElMessageBox.confirm("确认删除这条报销记录吗?", "删除确认", {
|
||||||
await deleteReimbursement(row.id)
|
type: "warning",
|
||||||
ElMessage.success('删除成功')
|
});
|
||||||
loadList()
|
await deleteReimbursement(row.id);
|
||||||
|
ElMessage.success("删除成功");
|
||||||
|
loadList();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error !== 'cancel' && error !== 'close') ElMessage.error(error?.message || '删除失败')
|
if (error !== "cancel" && error !== "close")
|
||||||
|
ElMessage.error(error?.message || "删除失败");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const reloadDetail = async id => {
|
const reloadDetail = async (id) => {
|
||||||
try {
|
try {
|
||||||
const [detailRes, recordsRes] = await Promise.all([
|
const [detailRes, recordsRes] = await Promise.all([
|
||||||
getReimbursementDetail(id),
|
getReimbursementDetail(id),
|
||||||
getReimbursementRecords(id)
|
getReimbursementRecords(id),
|
||||||
])
|
]);
|
||||||
detail.value = responseData(detailRes)
|
detail.value = responseData(detailRes);
|
||||||
records.value = responseData(recordsRes) || []
|
records.value = responseData(recordsRes) || [];
|
||||||
await loadList()
|
await loadList();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(error?.message || '刷新详情失败')
|
ElMessage.error(error?.message || "刷新详情失败");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const openDetail = async id => {
|
const openDetail = async (id) => {
|
||||||
await reloadDetail(id)
|
await reloadDetail(id);
|
||||||
detailVisible.value = true
|
detailVisible.value = true;
|
||||||
}
|
};
|
||||||
|
|
||||||
const money = value => Number(value || 0).toFixed(2)
|
const handleExport = async (row) => {
|
||||||
const formatDate = value => {
|
exportingId.value = row.id;
|
||||||
if (!value) return '-'
|
try {
|
||||||
const text = String(value)
|
const data = responseData(await getReimbursementDetail(row.id));
|
||||||
const match = text.match(/^(\d{4}-\d{2}-\d{2})/)
|
await exportReimbursementPdf(data, {
|
||||||
if (match) return match[1]
|
statusLabel: statusLabel(data.status),
|
||||||
return text
|
});
|
||||||
}
|
ElMessage.success("导出成功");
|
||||||
const formatDateTime = value => value
|
} catch (error) {
|
||||||
? new Date(value).toLocaleString('zh-CN', { hour12: false })
|
ElMessage.error(error?.message || "导出失败");
|
||||||
: '-'
|
} finally {
|
||||||
const statusLabel = value => statusOptions.find(item => item.value === value)?.label || '未知'
|
exportingId.value = 0;
|
||||||
const statusType = value => ({
|
}
|
||||||
0: 'info',
|
};
|
||||||
1: 'warning',
|
|
||||||
2: 'success',
|
const money = (value) => Number(value || 0).toFixed(2);
|
||||||
3: 'danger',
|
const formatDate = (value) => {
|
||||||
4: 'info',
|
if (!value) return "-";
|
||||||
5: 'success'
|
const text = String(value);
|
||||||
})[value] || 'info'
|
const match = text.match(/^(\d{4}-\d{2}-\d{2})/);
|
||||||
|
if (match) return match[1];
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
const formatDateTime = (value) =>
|
||||||
|
value ? new Date(value).toLocaleString("zh-CN", { hour12: false }) : "-";
|
||||||
|
const statusLabel = (value) =>
|
||||||
|
statusOptions.find((item) => item.value === value)?.label || "未知";
|
||||||
|
const statusType = (value) =>
|
||||||
|
({
|
||||||
|
0: "info",
|
||||||
|
1: "warning",
|
||||||
|
2: "success",
|
||||||
|
3: "danger",
|
||||||
|
4: "info",
|
||||||
|
5: "success",
|
||||||
|
})[value] || "info";
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadExpenseTypes()
|
loadExpenseTypes();
|
||||||
loadList()
|
loadList();
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.reimburse-page {
|
.reimburse-page {
|
||||||
min-height: calc(100vh - 84px);
|
// min-height: calc(100vh - 84px);
|
||||||
background: #f5f7fa;
|
background: #f5f7fa;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
@@ -279,6 +379,12 @@ onMounted(() => {
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.page-header h2 {
|
.page-header h2 {
|
||||||
margin: 0 0 8px;
|
margin: 0 0 8px;
|
||||||
color: #303133;
|
color: #303133;
|
||||||
@@ -296,7 +402,7 @@ onMounted(() => {
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 18px;
|
padding: 18px;
|
||||||
box-shadow: 0 1px 4px rgba(0, 0, 0, .05);
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-card {
|
.filter-card {
|
||||||
|
|||||||
@@ -0,0 +1,461 @@
|
|||||||
|
import { jsPDF } from 'jspdf'
|
||||||
|
import html2canvas from 'html2canvas'
|
||||||
|
import * as pdfjsLib from 'pdfjs-dist'
|
||||||
|
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?url'
|
||||||
|
|
||||||
|
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker
|
||||||
|
|
||||||
|
const PAGE_WIDTH = 794
|
||||||
|
const PAGE_HEIGHT = 1123
|
||||||
|
const RECEIPTS_PER_PAGE = 2
|
||||||
|
const money = value => Number(value || 0).toFixed(2)
|
||||||
|
|
||||||
|
const resolveUrl = url => {
|
||||||
|
if (!url) return ''
|
||||||
|
if (/^https?:\/\//i.test(url) || url.startsWith('blob:') || url.startsWith('data:')) return url
|
||||||
|
return `${window.location.origin}/${String(url).replace(/^\/+/, '')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const isImageUrl = url => /\.(jpe?g|png|gif|bmp|webp)(\?|#|$)/i.test(url || '')
|
||||||
|
const isPdfUrl = url => /\.pdf(\?|#|$)/i.test(String(url || '').toLowerCase())
|
||||||
|
|
||||||
|
const getReceiptFiles = item => {
|
||||||
|
if (Array.isArray(item?.receipt_files) && item.receipt_files.length) {
|
||||||
|
return item.receipt_files
|
||||||
|
}
|
||||||
|
const ids = String(item?.receipt_file_ids || '')
|
||||||
|
.split(',')
|
||||||
|
.map(v => v.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
const urls = String(item?.receipt_urls || '')
|
||||||
|
.split(',')
|
||||||
|
.map(v => v.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
if (ids.length) {
|
||||||
|
return ids.map((id, index) => ({
|
||||||
|
file_id: Number(id),
|
||||||
|
url: urls[index] || urls[0] || '',
|
||||||
|
name: ''
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
const firstUrl = urls[0] || ''
|
||||||
|
if (firstUrl) {
|
||||||
|
return [{ file_id: 0, url: firstUrl, name: '' }]
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchAuthorizedBlob = async url => {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
const response = await fetch(resolveUrl(url), {
|
||||||
|
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`文件加载失败 (${response.status})`)
|
||||||
|
}
|
||||||
|
return response.blob()
|
||||||
|
}
|
||||||
|
|
||||||
|
const blobToDataUrl = blob =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = () => resolve(reader.result)
|
||||||
|
reader.onerror = reject
|
||||||
|
reader.readAsDataURL(blob)
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadImageDataUrl = async url => {
|
||||||
|
const blob = await fetchAuthorizedBlob(url)
|
||||||
|
return blobToDataUrl(blob)
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadPdfPreviewDataUrl = async url => {
|
||||||
|
const blob = await fetchAuthorizedBlob(url)
|
||||||
|
const buffer = await blob.arrayBuffer()
|
||||||
|
const pdf = await pdfjsLib.getDocument({ data: buffer }).promise
|
||||||
|
const page = await pdf.getPage(1)
|
||||||
|
const viewport = page.getViewport({ scale: 1.4 })
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const context = canvas.getContext('2d')
|
||||||
|
canvas.width = viewport.width
|
||||||
|
canvas.height = viewport.height
|
||||||
|
await page.render({ canvasContext: context, viewport }).promise
|
||||||
|
return canvas.toDataURL('image/jpeg', 0.92)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveReceiptPreview = async file => {
|
||||||
|
const rawUrl = file.url || ''
|
||||||
|
const name = file.name || rawUrl.split('/').pop()?.split('?')[0] || '发票'
|
||||||
|
if (!rawUrl) {
|
||||||
|
return { name, dataUrl: '', note: '无可用文件地址' }
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (isImageUrl(rawUrl)) {
|
||||||
|
return { name, dataUrl: await loadImageDataUrl(rawUrl), note: '' }
|
||||||
|
}
|
||||||
|
if (isPdfUrl(rawUrl)) {
|
||||||
|
return { name, dataUrl: await loadPdfPreviewDataUrl(rawUrl), note: '' }
|
||||||
|
}
|
||||||
|
return { name, dataUrl: '', note: '暂不支持预览该格式,请单独查看原文件' }
|
||||||
|
} catch (error) {
|
||||||
|
return { name, dataUrl: '', note: error?.message || '发票加载失败' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const escapeHtml = value =>
|
||||||
|
String(value ?? '')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
|
||||||
|
const EXPORT_STYLES = `
|
||||||
|
.reimburse-export-page {
|
||||||
|
width: ${PAGE_WIDTH}px;
|
||||||
|
height: ${PAGE_HEIGHT}px;
|
||||||
|
padding: 28px 32px 32px;
|
||||||
|
background: #fff;
|
||||||
|
color: #303133;
|
||||||
|
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.reimburse-export-page h1 {
|
||||||
|
margin: 0 0 20px;
|
||||||
|
font-size: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.reimburse-export-page h2 {
|
||||||
|
margin: 0 0 14px;
|
||||||
|
font-size: 16px;
|
||||||
|
border-left: 4px solid #409eff;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
.summary-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 10px 24px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.summary-item span,
|
||||||
|
.summary-desc span,
|
||||||
|
.item-meta span {
|
||||||
|
color: #909399;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.summary-desc {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.items-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.items-table th,
|
||||||
|
.items-table td {
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
padding: 8px 10px;
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.items-table th {
|
||||||
|
background: #f5f7fa;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
.item-meta {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 8px 20px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fafafa;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.item-meta-desc {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
line-height: 1.6;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.reimburse-export-page.is-continuation {
|
||||||
|
height: auto;
|
||||||
|
min-height: 0;
|
||||||
|
padding-top: 28px;
|
||||||
|
}
|
||||||
|
.receipt-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.receipt-card {
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.receipt-title {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
.receipt-image {
|
||||||
|
display: block;
|
||||||
|
width: auto;
|
||||||
|
max-width: min(100%, 680px);
|
||||||
|
height: auto;
|
||||||
|
max-height: var(--receipt-max-height, 480px);
|
||||||
|
object-fit: contain;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.receipt-note,
|
||||||
|
.empty-tip {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const createPageElement = html => {
|
||||||
|
const page = document.createElement('div')
|
||||||
|
page.className = 'reimburse-export-page'
|
||||||
|
page.innerHTML = html
|
||||||
|
return page
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildItemsTableHtml = items => {
|
||||||
|
if (!items.length) {
|
||||||
|
return '<p class="empty-tip">暂无费用明细</p>'
|
||||||
|
}
|
||||||
|
const rows = items
|
||||||
|
.map(item => {
|
||||||
|
const count = getReceiptFiles(item).length
|
||||||
|
return `<tr>
|
||||||
|
<td>${escapeHtml(item.expense_type || '-')}</td>
|
||||||
|
<td>${escapeHtml(item.expense_date || '-')}</td>
|
||||||
|
<td>¥ ${money(item.amount)}</td>
|
||||||
|
<td>${count > 0 ? `有(${count})` : '无(0)'}</td>
|
||||||
|
<td>${escapeHtml(item.description || '-')}</td>
|
||||||
|
</tr>`
|
||||||
|
})
|
||||||
|
.join('')
|
||||||
|
return `<table class="items-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>费用类型</th>
|
||||||
|
<th>发生日期</th>
|
||||||
|
<th>金额</th>
|
||||||
|
<th>发票</th>
|
||||||
|
<th>描述</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>${rows}</tbody>
|
||||||
|
</table>`
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildSummaryPageElement = (detail, items, statusLabel) =>
|
||||||
|
createPageElement(`
|
||||||
|
<h1>报销单</h1>
|
||||||
|
<div class="summary-grid">
|
||||||
|
<div class="summary-item"><span>报销单号</span>${escapeHtml(detail.id)}</div>
|
||||||
|
<div class="summary-item"><span>状态</span>${escapeHtml(statusLabel)}</div>
|
||||||
|
<div class="summary-item"><span>申请日期</span>${escapeHtml(detail.apply_date || '-')}</div>
|
||||||
|
<div class="summary-item"><span>合计金额</span>¥ ${money(detail.total_amount)}</div>
|
||||||
|
<div class="summary-desc"><span>备注说明</span>${escapeHtml(detail.description || '-')}</div>
|
||||||
|
</div>
|
||||||
|
<h2>费用明细</h2>
|
||||||
|
${buildItemsTableHtml(items)}
|
||||||
|
`)
|
||||||
|
|
||||||
|
const chunkList = (list, size) => {
|
||||||
|
const chunks = []
|
||||||
|
for (let i = 0; i < list.length; i += size) {
|
||||||
|
chunks.push(list.slice(i, i + size))
|
||||||
|
}
|
||||||
|
return chunks
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildItemMetaHtml = (item, index, total) => `
|
||||||
|
<h2>费用项 ${index + 1} / ${total}</h2>
|
||||||
|
<div class="item-meta">
|
||||||
|
<div><span>费用类型</span>${escapeHtml(item.expense_type || '-')}</div>
|
||||||
|
<div><span>发生日期</span>${escapeHtml(item.expense_date || '-')}</div>
|
||||||
|
<div><span>金额</span>¥ ${money(item.amount)}</div>
|
||||||
|
<div><span>发票</span>${getReceiptFiles(item).length ? `有(${getReceiptFiles(item).length})` : '无(0)'}</div>
|
||||||
|
<div class="item-meta-desc"><span>描述</span>${escapeHtml(item.description || '-')}</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
const buildReceiptCardsHtml = previews =>
|
||||||
|
previews
|
||||||
|
.map((preview, index) => `
|
||||||
|
<div class="receipt-card">
|
||||||
|
<div class="receipt-title">${escapeHtml(preview.name || `发票 ${index + 1}`)}</div>
|
||||||
|
${
|
||||||
|
preview.dataUrl
|
||||||
|
? `<img src="${preview.dataUrl}" alt="${escapeHtml(preview.name)}" class="receipt-image" />`
|
||||||
|
: `<div class="receipt-note">${escapeHtml(preview.note || '无法嵌入该发票')}</div>`
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
`)
|
||||||
|
.join('')
|
||||||
|
|
||||||
|
const getReceiptImageHeight = (countOnPage, hasFullMeta) => {
|
||||||
|
if (countOnPage <= 1) {
|
||||||
|
return hasFullMeta ? 760 : 860
|
||||||
|
}
|
||||||
|
return hasFullMeta ? 385 : 440
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildItemReceiptPageElement = (
|
||||||
|
item,
|
||||||
|
index,
|
||||||
|
total,
|
||||||
|
previews,
|
||||||
|
{ showFullMeta }
|
||||||
|
) => {
|
||||||
|
const countOnPage = previews.length
|
||||||
|
const imageHeight = getReceiptImageHeight(countOnPage, showFullMeta)
|
||||||
|
const html = showFullMeta
|
||||||
|
? `
|
||||||
|
${buildItemMetaHtml(item, index, total)}
|
||||||
|
<h2>发票附件</h2>
|
||||||
|
<div class="receipt-list">${buildReceiptCardsHtml(previews)}</div>
|
||||||
|
`
|
||||||
|
: `<div class="receipt-list">${buildReceiptCardsHtml(previews)}</div>`
|
||||||
|
|
||||||
|
const page = createPageElement(html)
|
||||||
|
if (!showFullMeta) {
|
||||||
|
page.classList.add('is-continuation')
|
||||||
|
}
|
||||||
|
page.style.setProperty('--receipt-max-height', `${imageHeight}px`)
|
||||||
|
return page
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildItemEmptyPageElement = (item, index, total) =>
|
||||||
|
createPageElement(`
|
||||||
|
${buildItemMetaHtml(item, index, total)}
|
||||||
|
<h2>发票附件</h2>
|
||||||
|
<p class="empty-tip">该费用项暂无发票</p>
|
||||||
|
`)
|
||||||
|
|
||||||
|
const buildItemPages = async (item, index, total) => {
|
||||||
|
const files = getReceiptFiles(item)
|
||||||
|
if (!files.length) {
|
||||||
|
return [buildItemEmptyPageElement(item, index, total)]
|
||||||
|
}
|
||||||
|
|
||||||
|
const previews = []
|
||||||
|
for (const file of files) {
|
||||||
|
previews.push(await resolveReceiptPreview(file))
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunks = chunkList(previews, RECEIPTS_PER_PAGE)
|
||||||
|
return chunks.map((chunk, chunkIndex) =>
|
||||||
|
buildItemReceiptPageElement(item, index, total, chunk, {
|
||||||
|
showFullMeta: chunkIndex === 0
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const waitForImages = async root => {
|
||||||
|
const images = Array.from(root.querySelectorAll('img'))
|
||||||
|
await Promise.all(
|
||||||
|
images.map(
|
||||||
|
img =>
|
||||||
|
new Promise(resolve => {
|
||||||
|
if (img.complete) {
|
||||||
|
resolve()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
img.onload = () => resolve()
|
||||||
|
img.onerror = () => resolve()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderElementToCanvas = async (element, isContinuation = false) => {
|
||||||
|
await waitForImages(element)
|
||||||
|
const options = {
|
||||||
|
scale: 2,
|
||||||
|
useCORS: true,
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
logging: false,
|
||||||
|
windowWidth: PAGE_WIDTH
|
||||||
|
}
|
||||||
|
if (!isContinuation) {
|
||||||
|
options.width = PAGE_WIDTH
|
||||||
|
}
|
||||||
|
return html2canvas(element, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addCanvasToPdf = (pdf, canvas, isFirstPage) => {
|
||||||
|
if (!isFirstPage) {
|
||||||
|
pdf.addPage()
|
||||||
|
}
|
||||||
|
const pageWidth = pdf.internal.pageSize.getWidth()
|
||||||
|
const pageHeight = pdf.internal.pageSize.getHeight()
|
||||||
|
const margin = 8
|
||||||
|
const maxWidth = pageWidth - margin * 2
|
||||||
|
const maxHeight = pageHeight - margin * 2
|
||||||
|
|
||||||
|
let drawWidth = maxWidth
|
||||||
|
let drawHeight = (canvas.height * drawWidth) / canvas.width
|
||||||
|
|
||||||
|
if (drawHeight > maxHeight) {
|
||||||
|
drawHeight = maxHeight
|
||||||
|
drawWidth = (canvas.width * drawHeight) / canvas.height
|
||||||
|
}
|
||||||
|
|
||||||
|
const x = (pageWidth - drawWidth) / 2
|
||||||
|
const y = margin
|
||||||
|
pdf.addImage(canvas.toDataURL('image/jpeg', 0.95), 'JPEG', x, y, drawWidth, drawHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function exportReimbursementPdf(detail, { statusLabel }) {
|
||||||
|
if (!detail?.id) {
|
||||||
|
throw new Error('报销单数据无效')
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = detail?.items || []
|
||||||
|
const host = document.createElement('div')
|
||||||
|
host.style.cssText = 'position:fixed;left:-10000px;top:0;z-index:-1;'
|
||||||
|
const style = document.createElement('style')
|
||||||
|
style.textContent = EXPORT_STYLES
|
||||||
|
host.appendChild(style)
|
||||||
|
document.body.appendChild(host)
|
||||||
|
|
||||||
|
const pdf = new jsPDF({ orientation: 'p', unit: 'mm', format: 'a4' })
|
||||||
|
let pageIndex = 0
|
||||||
|
|
||||||
|
try {
|
||||||
|
const summaryPage = buildSummaryPageElement(detail, items, statusLabel)
|
||||||
|
host.appendChild(summaryPage)
|
||||||
|
const summaryCanvas = await renderElementToCanvas(summaryPage)
|
||||||
|
addCanvasToPdf(pdf, summaryCanvas, pageIndex === 0)
|
||||||
|
pageIndex += 1
|
||||||
|
host.removeChild(summaryPage)
|
||||||
|
|
||||||
|
for (let index = 0; index < items.length; index += 1) {
|
||||||
|
const itemPages = await buildItemPages(items[index], index, items.length)
|
||||||
|
for (const itemPage of itemPages) {
|
||||||
|
const isContinuation = itemPage.classList.contains('is-continuation')
|
||||||
|
host.appendChild(itemPage)
|
||||||
|
const itemCanvas = await renderElementToCanvas(itemPage, isContinuation)
|
||||||
|
addCanvasToPdf(pdf, itemCanvas, pageIndex === 0)
|
||||||
|
pageIndex += 1
|
||||||
|
host.removeChild(itemPage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pdf.save(`报销单-${detail.id}-${detail.apply_date || 'export'}.pdf`)
|
||||||
|
} finally {
|
||||||
|
document.body.removeChild(host)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,12 +4,14 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"server/models"
|
"server/models"
|
||||||
"server/pkg/jwtutil"
|
"server/pkg/jwtutil"
|
||||||
|
"server/services"
|
||||||
|
|
||||||
"github.com/beego/beego/v2/client/orm"
|
"github.com/beego/beego/v2/client/orm"
|
||||||
beego "github.com/beego/beego/v2/server/web"
|
beego "github.com/beego/beego/v2/server/web"
|
||||||
@@ -29,13 +31,12 @@ type reimbursementItemPayload struct {
|
|||||||
UnitPrice float64 `json:"unit_price"`
|
UnitPrice float64 `json:"unit_price"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
ReceiptFlag int8 `json:"receipt_flag"`
|
ReceiptFlag int8 `json:"receipt_flag"`
|
||||||
ReceiptURL string `json:"receipt_url"`
|
ReceiptFileIDs string `json:"receipt_file_ids"`
|
||||||
|
ReceiptURLs string `json:"receipt_urls"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type reimbursementPayload struct {
|
type reimbursementPayload struct {
|
||||||
Title string `json:"title"`
|
|
||||||
DepartmentID *uint64 `json:"department_id"`
|
DepartmentID *uint64 `json:"department_id"`
|
||||||
ExpenseType string `json:"expense_type"`
|
|
||||||
ApplyDate string `json:"apply_date"`
|
ApplyDate string `json:"apply_date"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Items []reimbursementItemPayload `json:"items"`
|
Items []reimbursementItemPayload `json:"items"`
|
||||||
@@ -96,26 +97,140 @@ func (c *BackendReimburseController) findOwned(id uint64, claims *jwtutil.Claims
|
|||||||
return row, err
|
return row, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func reimbursementItemDTO(item models.BackendReimbursementItem) map[string]interface{} {
|
func parseReceiptFileIDs(raw string) []uint64 {
|
||||||
|
ids := make([]uint64, 0)
|
||||||
|
seen := map[uint64]bool{}
|
||||||
|
for _, part := range strings.Split(raw, ",") {
|
||||||
|
id, err := strconv.ParseUint(strings.TrimSpace(part), 10, 64)
|
||||||
|
if err != nil || id == 0 || seen[id] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[id] = true
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
|
func joinReceiptFileIDs(ids []uint64) string {
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
parts := make([]string, 0, len(ids))
|
||||||
|
for _, id := range ids {
|
||||||
|
parts = append(parts, strconv.FormatUint(id, 10))
|
||||||
|
}
|
||||||
|
return strings.Join(parts, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveReceiptFiles(scheme, host string, item models.BackendReimbursementItem) []map[string]interface{} {
|
||||||
|
ids := parseReceiptFileIDs(item.ReceiptFileIDs)
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return []map[string]interface{}{}
|
||||||
|
}
|
||||||
|
urlParts := strings.Split(item.ReceiptURLs, ",")
|
||||||
|
files := make([]map[string]interface{}, 0, len(ids))
|
||||||
|
for i, id := range ids {
|
||||||
|
url := ""
|
||||||
|
if i < len(urlParts) {
|
||||||
|
url = services.ResolvePublicURL(scheme, host, strings.TrimSpace(urlParts[i]))
|
||||||
|
}
|
||||||
|
if url == "" {
|
||||||
|
idCopy := id
|
||||||
|
url = services.ResolveSoftwareDownloadURL(scheme, host, nil, &idCopy)
|
||||||
|
}
|
||||||
|
name := ""
|
||||||
|
var f models.SystemFile
|
||||||
|
if err := models.Orm.QueryTable(new(models.SystemFile)).
|
||||||
|
Filter("id", id).Filter("delete_time__isnull", true).One(&f); err == nil {
|
||||||
|
name = f.Name
|
||||||
|
}
|
||||||
|
if name == "" && url != "" {
|
||||||
|
name = path.Base(strings.Split(url, "?")[0])
|
||||||
|
}
|
||||||
|
files = append(files, map[string]interface{}{
|
||||||
|
"file_id": id, "url": url, "name": name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncReceiptFields(item *models.BackendReimbursementItem, scheme, host string) {
|
||||||
|
files := resolveReceiptFiles(scheme, host, *item)
|
||||||
|
if len(files) == 0 {
|
||||||
|
item.ReceiptFileIDs = ""
|
||||||
|
item.ReceiptURLs = ""
|
||||||
|
item.ReceiptFlag = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ids := make([]uint64, 0, len(files))
|
||||||
|
urls := make([]string, 0, len(files))
|
||||||
|
for _, file := range files {
|
||||||
|
if id, ok := file["file_id"].(uint64); ok {
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
if url, ok := file["url"].(string); ok && url != "" {
|
||||||
|
urls = append(urls, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item.ReceiptFileIDs = joinReceiptFileIDs(ids)
|
||||||
|
item.ReceiptURLs = strings.Join(urls, ",")
|
||||||
|
item.ReceiptFlag = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyReceiptToItem(item *models.BackendReimbursementItem, payload reimbursementItemPayload, scheme, host string) {
|
||||||
|
if payload.ReceiptFlag == 0 && strings.TrimSpace(payload.ReceiptFileIDs) == "" {
|
||||||
|
item.ReceiptFileIDs = ""
|
||||||
|
item.ReceiptURLs = ""
|
||||||
|
item.ReceiptFlag = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(payload.ReceiptFileIDs) != "" {
|
||||||
|
item.ReceiptFileIDs = strings.TrimSpace(payload.ReceiptFileIDs)
|
||||||
|
syncReceiptFields(item, scheme, host)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item.ReceiptFlag = payload.ReceiptFlag
|
||||||
|
item.ReceiptURLs = strings.TrimSpace(payload.ReceiptURLs)
|
||||||
|
if item.ReceiptURLs == "" && item.ReceiptFlag == 0 {
|
||||||
|
item.ReceiptFileIDs = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func reimbursementItemDTO(item models.BackendReimbursementItem, scheme, host string) map[string]interface{} {
|
||||||
|
receiptFiles := resolveReceiptFiles(scheme, host, item)
|
||||||
|
receiptURLs := strings.TrimSpace(item.ReceiptURLs)
|
||||||
|
receiptFlag := item.ReceiptFlag
|
||||||
|
if len(receiptFiles) > 0 {
|
||||||
|
receiptFlag = 1
|
||||||
|
if receiptURLs == "" {
|
||||||
|
if url, ok := receiptFiles[0]["url"].(string); ok {
|
||||||
|
receiptURLs = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if receiptURLs != "" {
|
||||||
|
receiptFlag = 1
|
||||||
|
}
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"id": item.ID, "reimbursement_id": item.ReimbursementID,
|
"id": item.ID, "reimbursement_id": item.ReimbursementID,
|
||||||
"expense_type": item.ExpenseType, "expense_date": item.ExpenseDate.Format("2006-01-02"),
|
"expense_type": item.ExpenseType, "expense_date": item.ExpenseDate.Format("2006-01-02"),
|
||||||
"amount": item.Amount, "quantity": item.Quantity, "unit_price": item.UnitPrice,
|
"amount": item.Amount, "quantity": item.Quantity, "unit_price": item.UnitPrice,
|
||||||
"description": item.Description, "receipt_flag": item.ReceiptFlag, "receipt_url": item.ReceiptURL,
|
"description": item.Description, "receipt_flag": receiptFlag,
|
||||||
|
"receipt_file_ids": item.ReceiptFileIDs,
|
||||||
|
"receipt_urls": receiptURLs, "receipt_files": receiptFiles,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func reimbursementDTO(row models.BackendReimbursement, items []models.BackendReimbursementItem) map[string]interface{} {
|
func reimbursementDTO(row models.BackendReimbursement, items []models.BackendReimbursementItem, scheme, host string) map[string]interface{} {
|
||||||
result := map[string]interface{}{
|
result := map[string]interface{}{
|
||||||
"id": row.ID, "tid": row.Tid, "uid": row.UID, "user_id": row.UserID,
|
"id": row.ID, "tid": row.Tid, "uid": row.UID, "user_id": row.UserID,
|
||||||
"department_id": row.DepartmentID, "title": row.Title, "total_amount": row.TotalAmount,
|
"department_id": row.DepartmentID, "total_amount": row.TotalAmount,
|
||||||
"status": row.Status, "expense_type": row.ExpenseType, "apply_date": row.ApplyDate.Format("2006-01-02"),
|
"status": row.Status, "apply_date": row.ApplyDate.Format("2006-01-02"),
|
||||||
"description": row.Description, "current_approver_id": row.CurrentApproverID,
|
"description": row.Description, "current_approver_id": row.CurrentApproverID,
|
||||||
"current_step": row.CurrentStep, "create_time": row.CreateTime, "update_time": row.UpdateTime,
|
"current_step": row.CurrentStep, "create_time": row.CreateTime, "update_time": row.UpdateTime,
|
||||||
}
|
}
|
||||||
list := make([]map[string]interface{}, 0, len(items))
|
list := make([]map[string]interface{}, 0, len(items))
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
list = append(list, reimbursementItemDTO(item))
|
list = append(list, reimbursementItemDTO(item, scheme, host))
|
||||||
}
|
}
|
||||||
result["items"] = list
|
result["items"] = list
|
||||||
return result
|
return result
|
||||||
@@ -170,10 +285,8 @@ func (c *BackendReimburseController) List() {
|
|||||||
"uid": row.UID,
|
"uid": row.UID,
|
||||||
"user_id": row.UserID,
|
"user_id": row.UserID,
|
||||||
"department_id": row.DepartmentID,
|
"department_id": row.DepartmentID,
|
||||||
"title": row.Title,
|
|
||||||
"total_amount": row.TotalAmount,
|
"total_amount": row.TotalAmount,
|
||||||
"status": row.Status,
|
"status": row.Status,
|
||||||
"expense_type": row.ExpenseType,
|
|
||||||
"apply_date": row.ApplyDate.Format("2006-01-02"),
|
"apply_date": row.ApplyDate.Format("2006-01-02"),
|
||||||
"description": row.Description,
|
"description": row.Description,
|
||||||
"current_approver_id": row.CurrentApproverID,
|
"current_approver_id": row.CurrentApproverID,
|
||||||
@@ -207,10 +320,11 @@ func (c *BackendReimburseController) Detail() {
|
|||||||
Filter("reimbursement_id", id).Filter("tid", claims.TenantId).Filter("is_deleted", 0).
|
Filter("reimbursement_id", id).Filter("tid", claims.TenantId).Filter("is_deleted", 0).
|
||||||
OrderBy("id").All(&items)
|
OrderBy("id").All(&items)
|
||||||
if err != nil && err != orm.ErrNoRows {
|
if err != nil && err != orm.ErrNoRows {
|
||||||
c.reply(500, "查询明细失败", nil)
|
c.reply(500, "查询明细失败: "+err.Error(), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.reply(200, "success", reimbursementDTO(*row, items))
|
scheme, host := services.PublicRequestBaseURL(&c.Controller)
|
||||||
|
c.reply(200, "success", reimbursementDTO(*row, items, scheme, host))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create POST /backend/reimbursements
|
// Create POST /backend/reimbursements
|
||||||
@@ -233,8 +347,7 @@ func (c *BackendReimburseController) Create() {
|
|||||||
uid := uint64(claims.UserID)
|
uid := uint64(claims.UserID)
|
||||||
row := &models.BackendReimbursement{
|
row := &models.BackendReimbursement{
|
||||||
Tid: uint64(claims.TenantId), UID: &uid, UserID: uid, DepartmentID: payload.DepartmentID,
|
Tid: uint64(claims.TenantId), UID: &uid, UserID: uid, DepartmentID: payload.DepartmentID,
|
||||||
Title: "", TotalAmount: 0, Status: 0,
|
TotalAmount: 0, Status: 0, ApplyDate: applyDate,
|
||||||
ExpenseType: strings.TrimSpace(payload.ExpenseType), ApplyDate: applyDate,
|
|
||||||
Description: nullableReimburseString(payload.Description),
|
Description: nullableReimburseString(payload.Description),
|
||||||
}
|
}
|
||||||
if err := saveReimbursement(row); err != nil {
|
if err := saveReimbursement(row); err != nil {
|
||||||
@@ -419,13 +532,14 @@ func (c *BackendReimburseController) AddItem() {
|
|||||||
if quantity <= 0 {
|
if quantity <= 0 {
|
||||||
quantity = 1
|
quantity = 1
|
||||||
}
|
}
|
||||||
|
scheme, host := services.PublicRequestBaseURL(&c.Controller)
|
||||||
item := &models.BackendReimbursementItem{
|
item := &models.BackendReimbursementItem{
|
||||||
Tid: uint64(claims.TenantId), UID: &uid, ReimbursementID: reimbursementID,
|
Tid: uint64(claims.TenantId), UID: &uid, ReimbursementID: reimbursementID,
|
||||||
ExpenseType: strings.TrimSpace(payload.ExpenseType), ExpenseDate: date,
|
ExpenseType: strings.TrimSpace(payload.ExpenseType), ExpenseDate: date,
|
||||||
Amount: payload.Amount, Quantity: quantity, UnitPrice: payload.UnitPrice,
|
Amount: payload.Amount, Quantity: quantity, UnitPrice: payload.UnitPrice,
|
||||||
Description: strings.TrimSpace(payload.Description), ReceiptFlag: payload.ReceiptFlag,
|
Description: strings.TrimSpace(payload.Description),
|
||||||
ReceiptURL: strings.TrimSpace(payload.ReceiptURL),
|
|
||||||
}
|
}
|
||||||
|
applyReceiptToItem(item, payload, scheme, host)
|
||||||
if _, err := models.Orm.Insert(item); err != nil {
|
if _, err := models.Orm.Insert(item); err != nil {
|
||||||
c.reply(500, "新增费用明细失败", nil)
|
c.reply(500, "新增费用明细失败", nil)
|
||||||
return
|
return
|
||||||
@@ -434,7 +548,7 @@ func (c *BackendReimburseController) AddItem() {
|
|||||||
c.reply(500, "更新报销总额失败", nil)
|
c.reply(500, "更新报销总额失败", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.reply(200, "success", reimbursementItemDTO(*item))
|
c.reply(200, "success", reimbursementItemDTO(*item, scheme, host))
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateItem POST /backend/reimbursements/:id/items/:item_id
|
// UpdateItem POST /backend/reimbursements/:id/items/:item_id
|
||||||
@@ -476,14 +590,14 @@ func (c *BackendReimburseController) UpdateItem() {
|
|||||||
if quantity <= 0 {
|
if quantity <= 0 {
|
||||||
quantity = 1
|
quantity = 1
|
||||||
}
|
}
|
||||||
|
scheme, host := services.PublicRequestBaseURL(&c.Controller)
|
||||||
item.ExpenseType = strings.TrimSpace(payload.ExpenseType)
|
item.ExpenseType = strings.TrimSpace(payload.ExpenseType)
|
||||||
item.ExpenseDate = date
|
item.ExpenseDate = date
|
||||||
item.Amount = payload.Amount
|
item.Amount = payload.Amount
|
||||||
item.Quantity = quantity
|
item.Quantity = quantity
|
||||||
item.UnitPrice = payload.UnitPrice
|
item.UnitPrice = payload.UnitPrice
|
||||||
item.Description = strings.TrimSpace(payload.Description)
|
item.Description = strings.TrimSpace(payload.Description)
|
||||||
item.ReceiptFlag = payload.ReceiptFlag
|
applyReceiptToItem(item, payload, scheme, host)
|
||||||
item.ReceiptURL = strings.TrimSpace(payload.ReceiptURL)
|
|
||||||
if _, err := models.Orm.Update(item); err != nil {
|
if _, err := models.Orm.Update(item); err != nil {
|
||||||
c.reply(500, "更新费用明细失败", nil)
|
c.reply(500, "更新费用明细失败", nil)
|
||||||
return
|
return
|
||||||
@@ -492,7 +606,7 @@ func (c *BackendReimburseController) UpdateItem() {
|
|||||||
c.reply(500, "更新报销总额失败", nil)
|
c.reply(500, "更新报销总额失败", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.reply(200, "success", reimbursementItemDTO(*item))
|
c.reply(200, "success", reimbursementItemDTO(*item, scheme, host))
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteItem DELETE /backend/reimbursements/:id/items/:item_id
|
// DeleteItem DELETE /backend/reimbursements/:id/items/:item_id
|
||||||
@@ -583,6 +697,65 @@ func (c *BackendReimburseController) Submit() {
|
|||||||
c.reply(200, "success", nil)
|
c.reply(200, "success", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type reimbursementStatusPayload struct {
|
||||||
|
Status int8 `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var manualConfirmStatuses = map[int8]bool{
|
||||||
|
0: true, // 草稿
|
||||||
|
1: true, // 审批中
|
||||||
|
2: true, // 已通过
|
||||||
|
3: true, // 已驳回
|
||||||
|
4: true, // 已撤回
|
||||||
|
5: true, // 已打款
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateStatus POST /backend/reimbursements/:id/status — 手动确认更改单据状态
|
||||||
|
func (c *BackendReimburseController) UpdateStatus() {
|
||||||
|
claims, ok := c.claims()
|
||||||
|
if !ok {
|
||||||
|
c.reply(401, "未登录或无权限", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, ok := c.getID()
|
||||||
|
if !ok {
|
||||||
|
c.reply(400, "无效ID", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||||
|
if err != nil {
|
||||||
|
c.reply(400, "参数错误", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var payload reimbursementStatusPayload
|
||||||
|
if err := json.Unmarshal(raw, &payload); err != nil {
|
||||||
|
c.reply(400, "参数错误", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !manualConfirmStatuses[payload.Status] {
|
||||||
|
c.reply(400, "不支持的状态值", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
row, err := c.findOwned(id, claims)
|
||||||
|
if err != nil {
|
||||||
|
c.reply(404, "报销单不存在", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if row.Status == payload.Status {
|
||||||
|
c.reply(200, "success", map[string]interface{}{"status": row.Status})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
_, err = models.Orm.QueryTable(new(models.BackendReimbursement)).Filter("id", id).Update(orm.Params{
|
||||||
|
"status": payload.Status, "update_time": now,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
c.reply(500, "更新状态失败", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.reply(200, "success", map[string]interface{}{"status": payload.Status})
|
||||||
|
}
|
||||||
|
|
||||||
// Records GET /backend/reimbursements/:id/records
|
// Records GET /backend/reimbursements/:id/records
|
||||||
func (c *BackendReimburseController) Records() {
|
func (c *BackendReimburseController) Records() {
|
||||||
claims, ok := c.claims()
|
claims, ok := c.claims()
|
||||||
@@ -613,25 +786,215 @@ func (c *BackendReimburseController) Records() {
|
|||||||
c.reply(200, "success", rows)
|
c.reply(200, "success", rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Types GET /backend/reimbursements/types
|
type expenseTypePayload struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
SortOrder int `json:"sort_order"`
|
||||||
|
IsActive *int8 `json:"is_active"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func expenseTypeScopeCond(tenantID int) *orm.Condition {
|
||||||
|
cond := orm.NewCondition()
|
||||||
|
return cond.Or("tid", 0).Or("tid", tenantID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func expenseTypeDTO(row models.BackendReimbursementExpenseType, tenantID int) map[string]interface{} {
|
||||||
|
isGlobal := row.Tid == 0
|
||||||
|
return map[string]interface{}{
|
||||||
|
"id": row.ID, "tid": row.Tid, "name": row.Name,
|
||||||
|
"sort_order": row.SortOrder, "is_active": row.IsActive,
|
||||||
|
"is_global": isGlobal, "editable": !isGlobal && row.Tid == uint64(tenantID),
|
||||||
|
"create_time": row.CreateTime, "update_time": row.UpdateTime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BackendReimburseController) queryExpenseTypes(tenantID int, activeOnly bool) ([]models.BackendReimbursementExpenseType, error) {
|
||||||
|
qs := models.Orm.QueryTable(new(models.BackendReimbursementExpenseType)).
|
||||||
|
SetCond(expenseTypeScopeCond(tenantID)).
|
||||||
|
Filter("is_deleted", 0)
|
||||||
|
if activeOnly {
|
||||||
|
qs = qs.Filter("is_active", 1)
|
||||||
|
}
|
||||||
|
var rows []models.BackendReimbursementExpenseType
|
||||||
|
_, err := qs.OrderBy("sort_order", "id").All(&rows)
|
||||||
|
if err != nil && err != orm.ErrNoRows {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if rows == nil {
|
||||||
|
rows = []models.BackendReimbursementExpenseType{}
|
||||||
|
}
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BackendReimburseController) getTypeID() (uint64, bool) {
|
||||||
|
id, err := strconv.ParseUint(c.Ctx.Input.Param(":type_id"), 10, 64)
|
||||||
|
return id, err == nil && id > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BackendReimburseController) findTenantExpenseType(id uint64, tenantID int) (*models.BackendReimbursementExpenseType, error) {
|
||||||
|
row := &models.BackendReimbursementExpenseType{}
|
||||||
|
err := models.Orm.QueryTable(new(models.BackendReimbursementExpenseType)).
|
||||||
|
Filter("id", id).
|
||||||
|
Filter("tid", tenantID).
|
||||||
|
Filter("is_deleted", 0).
|
||||||
|
One(row)
|
||||||
|
return row, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BackendReimburseController) parseExpenseTypePayload() (expenseTypePayload, error) {
|
||||||
|
var payload expenseTypePayload
|
||||||
|
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||||
|
if err != nil {
|
||||||
|
return payload, err
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(raw, &payload)
|
||||||
|
return payload, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Types GET /backend/reimbursements/types — 下拉选项:全局通用 + 当前租户专有(仅启用)
|
||||||
func (c *BackendReimburseController) Types() {
|
func (c *BackendReimburseController) Types() {
|
||||||
claims, ok := c.claims()
|
claims, ok := c.claims()
|
||||||
if !ok {
|
if !ok {
|
||||||
c.reply(401, "未登录或无权限", nil)
|
c.reply(401, "未登录或无权限", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var rows []models.BackendReimbursementExpenseType
|
rows, err := c.queryExpenseTypes(claims.TenantId, true)
|
||||||
_, err := models.Orm.QueryTable(new(models.BackendReimbursementExpenseType)).
|
if err != nil {
|
||||||
Filter("tid", claims.TenantId).Filter("is_active", 1).Filter("is_deleted", 0).
|
|
||||||
OrderBy("sort_order", "id").All(&rows)
|
|
||||||
if err != nil && err != orm.ErrNoRows {
|
|
||||||
c.reply(500, "查询费用类型失败", nil)
|
c.reply(500, "查询费用类型失败", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if rows == nil {
|
list := make([]map[string]interface{}, 0, len(rows))
|
||||||
rows = []models.BackendReimbursementExpenseType{}
|
for _, row := range rows {
|
||||||
|
list = append(list, expenseTypeDTO(row, claims.TenantId))
|
||||||
}
|
}
|
||||||
c.reply(200, "success", rows)
|
c.reply(200, "success", list)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TypesManage GET /backend/reimbursements/types/manage — 管理列表:含禁用项,标注是否可编辑
|
||||||
|
func (c *BackendReimburseController) TypesManage() {
|
||||||
|
claims, ok := c.claims()
|
||||||
|
if !ok {
|
||||||
|
c.reply(401, "未登录或无权限", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rows, err := c.queryExpenseTypes(claims.TenantId, false)
|
||||||
|
if err != nil {
|
||||||
|
c.reply(500, "查询费用类型失败", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
list := make([]map[string]interface{}, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
list = append(list, expenseTypeDTO(row, claims.TenantId))
|
||||||
|
}
|
||||||
|
c.reply(200, "success", list)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateType POST /backend/reimbursements/types — 租户新增专有费项
|
||||||
|
func (c *BackendReimburseController) CreateType() {
|
||||||
|
claims, ok := c.claims()
|
||||||
|
if !ok {
|
||||||
|
c.reply(401, "未登录或无权限", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
payload, err := c.parseExpenseTypePayload()
|
||||||
|
if err != nil {
|
||||||
|
c.reply(400, "参数错误", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
name := strings.TrimSpace(payload.Name)
|
||||||
|
if name == "" {
|
||||||
|
c.reply(400, "费项名称不能为空", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isActive := int8(1)
|
||||||
|
if payload.IsActive != nil {
|
||||||
|
isActive = *payload.IsActive
|
||||||
|
}
|
||||||
|
uid := uint64(claims.UserID)
|
||||||
|
row := &models.BackendReimbursementExpenseType{
|
||||||
|
Tid: uint64(claims.TenantId), UID: &uid,
|
||||||
|
Name: name, SortOrder: payload.SortOrder, IsActive: isActive,
|
||||||
|
}
|
||||||
|
if _, err := models.Orm.Insert(row); err != nil {
|
||||||
|
if strings.Contains(err.Error(), "Duplicate") || strings.Contains(err.Error(), "1062") {
|
||||||
|
c.reply(400, "该费项名称已存在", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.reply(500, "新增费项失败", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.reply(200, "success", expenseTypeDTO(*row, claims.TenantId))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateType POST /backend/reimbursements/types/:type_id — 仅可修改本租户专有费项
|
||||||
|
func (c *BackendReimburseController) UpdateType() {
|
||||||
|
claims, ok := c.claims()
|
||||||
|
if !ok {
|
||||||
|
c.reply(401, "未登录或无权限", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, ok := c.getTypeID()
|
||||||
|
if !ok {
|
||||||
|
c.reply(400, "无效ID", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
row, err := c.findTenantExpenseType(id, claims.TenantId)
|
||||||
|
if err != nil {
|
||||||
|
c.reply(404, "费项不存在或无权修改", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
payload, err := c.parseExpenseTypePayload()
|
||||||
|
if err != nil {
|
||||||
|
c.reply(400, "参数错误", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if name := strings.TrimSpace(payload.Name); name != "" {
|
||||||
|
row.Name = name
|
||||||
|
}
|
||||||
|
if payload.SortOrder >= 0 {
|
||||||
|
row.SortOrder = payload.SortOrder
|
||||||
|
}
|
||||||
|
if payload.IsActive != nil {
|
||||||
|
row.IsActive = *payload.IsActive
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
row.UpdateTime = &now
|
||||||
|
if _, err := models.Orm.Update(row); err != nil {
|
||||||
|
if strings.Contains(err.Error(), "Duplicate") || strings.Contains(err.Error(), "1062") {
|
||||||
|
c.reply(400, "该费项名称已存在", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.reply(500, "更新费项失败", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.reply(200, "success", expenseTypeDTO(*row, claims.TenantId))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteType DELETE /backend/reimbursements/types/:type_id — 仅可删除本租户专有费项
|
||||||
|
func (c *BackendReimburseController) DeleteType() {
|
||||||
|
claims, ok := c.claims()
|
||||||
|
if !ok {
|
||||||
|
c.reply(401, "未登录或无权限", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, ok := c.getTypeID()
|
||||||
|
if !ok {
|
||||||
|
c.reply(400, "无效ID", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
row, err := c.findTenantExpenseType(id, claims.TenantId)
|
||||||
|
if err != nil {
|
||||||
|
c.reply(404, "费项不存在或无权删除", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
_, err = models.Orm.QueryTable(new(models.BackendReimbursementExpenseType)).
|
||||||
|
Filter("id", row.ID).
|
||||||
|
Update(orm.Params{"is_deleted": 1, "delete_time": now, "update_time": now})
|
||||||
|
if err != nil {
|
||||||
|
c.reply(500, "删除费项失败", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.reply(200, "success", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flows GET /backend/reimbursements/flows
|
// Flows GET /backend/reimbursements/flows
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"server/models"
|
||||||
|
"server/pkg/jwtutil"
|
||||||
|
|
||||||
|
beego "github.com/beego/beego/v2/server/web"
|
||||||
|
"github.com/beego/beego/v2/client/orm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PlatformReimburseExpenseTypeController 平台维护全局通用报销费项(tid=0)。
|
||||||
|
type PlatformReimburseExpenseTypeController struct {
|
||||||
|
beego.Controller
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PlatformReimburseExpenseTypeController) platformClaims() (*jwtutil.Claims, error) {
|
||||||
|
auth := c.Ctx.Request.Header.Get("Authorization")
|
||||||
|
if auth == "" {
|
||||||
|
return nil, fmt.Errorf("未登录")
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(auth, " ", 2)
|
||||||
|
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||||
|
return nil, fmt.Errorf("认证信息格式错误")
|
||||||
|
}
|
||||||
|
claims, err := jwtutil.ParseToken(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("无效的token")
|
||||||
|
}
|
||||||
|
if claims.UserType != "platform" {
|
||||||
|
return nil, fmt.Errorf("无权访问")
|
||||||
|
}
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PlatformReimburseExpenseTypeController) jsonErr(httpStatus, bizCode int, msg string) {
|
||||||
|
c.Ctx.Output.SetStatus(httpStatus)
|
||||||
|
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
||||||
|
_ = c.ServeJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PlatformReimburseExpenseTypeController) ok(data interface{}) {
|
||||||
|
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
|
||||||
|
_ = c.ServeJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
func globalExpenseTypeDTO(row models.BackendReimbursementExpenseType) map[string]interface{} {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"id": row.ID, "tid": row.Tid, "name": row.Name,
|
||||||
|
"sort_order": row.SortOrder, "is_active": row.IsActive,
|
||||||
|
"is_global": true, "editable": true,
|
||||||
|
"create_time": row.CreateTime, "update_time": row.UpdateTime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PlatformReimburseExpenseTypeController) findGlobalType(id uint64) (*models.BackendReimbursementExpenseType, error) {
|
||||||
|
row := &models.BackendReimbursementExpenseType{}
|
||||||
|
err := models.Orm.QueryTable(new(models.BackendReimbursementExpenseType)).
|
||||||
|
Filter("id", id).Filter("tid", 0).Filter("is_deleted", 0).One(row)
|
||||||
|
return row, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// List GET /platform/reimburseExpenseTypes
|
||||||
|
func (c *PlatformReimburseExpenseTypeController) List() {
|
||||||
|
if _, err := c.platformClaims(); err != nil {
|
||||||
|
c.jsonErr(401, 401, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var rows []models.BackendReimbursementExpenseType
|
||||||
|
_, err := models.Orm.QueryTable(new(models.BackendReimbursementExpenseType)).
|
||||||
|
Filter("tid", 0).Filter("is_deleted", 0).
|
||||||
|
OrderBy("sort_order", "id").All(&rows)
|
||||||
|
if err != nil && err != orm.ErrNoRows {
|
||||||
|
c.jsonErr(500, 500, "查询失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if rows == nil {
|
||||||
|
rows = []models.BackendReimbursementExpenseType{}
|
||||||
|
}
|
||||||
|
list := make([]map[string]interface{}, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
list = append(list, globalExpenseTypeDTO(row))
|
||||||
|
}
|
||||||
|
c.ok(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
type platformExpenseTypePayload struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
SortOrder int `json:"sort_order"`
|
||||||
|
IsActive *int8 `json:"is_active"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create POST /platform/reimburseExpenseTypes
|
||||||
|
func (c *PlatformReimburseExpenseTypeController) Create() {
|
||||||
|
claims, err := c.platformClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.jsonErr(401, 401, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
body, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||||
|
var p platformExpenseTypePayload
|
||||||
|
if err := json.Unmarshal(body, &p); err != nil {
|
||||||
|
c.jsonErr(400, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
name := strings.TrimSpace(p.Name)
|
||||||
|
if name == "" {
|
||||||
|
c.jsonErr(400, 400, "费项名称不能为空")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isActive := int8(1)
|
||||||
|
if p.IsActive != nil {
|
||||||
|
isActive = *p.IsActive
|
||||||
|
}
|
||||||
|
uid := uint64(claims.UserID)
|
||||||
|
row := models.BackendReimbursementExpenseType{
|
||||||
|
Tid: 0, UID: &uid, Name: name, SortOrder: p.SortOrder, IsActive: isActive,
|
||||||
|
}
|
||||||
|
id, err := models.Orm.Insert(&row)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "Duplicate") || strings.Contains(err.Error(), "1062") {
|
||||||
|
c.jsonErr(400, 400, "该费项名称已存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.jsonErr(500, 500, "创建失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
row.ID = uint64(id)
|
||||||
|
c.ok(globalExpenseTypeDTO(row))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update POST /platform/reimburseExpenseTypes/:id
|
||||||
|
func (c *PlatformReimburseExpenseTypeController) Update() {
|
||||||
|
if _, err := c.platformClaims(); err != nil {
|
||||||
|
c.jsonErr(401, 401, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||||
|
if err != nil || id == 0 {
|
||||||
|
c.jsonErr(400, 400, "无效ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
row, err := c.findGlobalType(id)
|
||||||
|
if err != nil {
|
||||||
|
c.jsonErr(404, 404, "费项不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
body, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||||
|
var p platformExpenseTypePayload
|
||||||
|
if err := json.Unmarshal(body, &p); err != nil {
|
||||||
|
c.jsonErr(400, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if name := strings.TrimSpace(p.Name); name != "" {
|
||||||
|
row.Name = name
|
||||||
|
}
|
||||||
|
if p.SortOrder >= 0 {
|
||||||
|
row.SortOrder = p.SortOrder
|
||||||
|
}
|
||||||
|
if p.IsActive != nil {
|
||||||
|
row.IsActive = *p.IsActive
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
row.UpdateTime = &now
|
||||||
|
if _, err := models.Orm.Update(row); err != nil {
|
||||||
|
if strings.Contains(err.Error(), "Duplicate") || strings.Contains(err.Error(), "1062") {
|
||||||
|
c.jsonErr(400, 400, "该费项名称已存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.jsonErr(500, 500, "更新失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.ok(globalExpenseTypeDTO(*row))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete DELETE /platform/reimburseExpenseTypes/:id
|
||||||
|
func (c *PlatformReimburseExpenseTypeController) Delete() {
|
||||||
|
if _, err := c.platformClaims(); err != nil {
|
||||||
|
c.jsonErr(401, 401, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||||
|
if err != nil || id == 0 {
|
||||||
|
c.jsonErr(400, 400, "无效ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
row, err := c.findGlobalType(id)
|
||||||
|
if err != nil {
|
||||||
|
c.jsonErr(404, 404, "费项不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
_, err = models.Orm.QueryTable(new(models.BackendReimbursementExpenseType)).
|
||||||
|
Filter("id", row.ID).
|
||||||
|
Update(orm.Params{"is_deleted": 1, "delete_time": now, "update_time": now})
|
||||||
|
if err != nil {
|
||||||
|
c.jsonErr(500, 500, "删除失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.ok(nil)
|
||||||
|
}
|
||||||
@@ -52,10 +52,8 @@ type BackendReimbursement struct {
|
|||||||
UID *uint64 `orm:"column(uid);null" json:"uid"`
|
UID *uint64 `orm:"column(uid);null" json:"uid"`
|
||||||
UserID uint64 `orm:"column(user_id)" json:"user_id"`
|
UserID uint64 `orm:"column(user_id)" json:"user_id"`
|
||||||
DepartmentID *uint64 `orm:"column(department_id);null" json:"department_id"`
|
DepartmentID *uint64 `orm:"column(department_id);null" json:"department_id"`
|
||||||
Title string `orm:"column(title);size(200)" json:"title"`
|
|
||||||
TotalAmount float64 `orm:"column(total_amount);type(decimal);digits(10);decimals(2)" json:"total_amount"`
|
TotalAmount float64 `orm:"column(total_amount);type(decimal);digits(10);decimals(2)" json:"total_amount"`
|
||||||
Status int8 `orm:"column(status);default(0)" json:"status"`
|
Status int8 `orm:"column(status);default(0)" json:"status"`
|
||||||
ExpenseType string `orm:"column(expense_type);size(50)" json:"expense_type"`
|
|
||||||
ApplyDate time.Time `orm:"column(apply_date);type(date)" json:"apply_date"`
|
ApplyDate time.Time `orm:"column(apply_date);type(date)" json:"apply_date"`
|
||||||
Description *string `orm:"column(description);null;type(text)" json:"description"`
|
Description *string `orm:"column(description);null;type(text)" json:"description"`
|
||||||
CurrentApproverID uint64 `orm:"column(current_approver_id);default(0)" json:"current_approver_id"`
|
CurrentApproverID uint64 `orm:"column(current_approver_id);default(0)" json:"current_approver_id"`
|
||||||
@@ -83,7 +81,8 @@ type BackendReimbursementItem struct {
|
|||||||
UnitPrice float64 `orm:"column(unit_price);type(decimal);digits(10);decimals(2)" json:"unit_price"`
|
UnitPrice float64 `orm:"column(unit_price);type(decimal);digits(10);decimals(2)" json:"unit_price"`
|
||||||
Description string `orm:"column(description);size(200)" json:"description"`
|
Description string `orm:"column(description);size(200)" json:"description"`
|
||||||
ReceiptFlag int8 `orm:"column(receipt_flag);default(0)" json:"receipt_flag"`
|
ReceiptFlag int8 `orm:"column(receipt_flag);default(0)" json:"receipt_flag"`
|
||||||
ReceiptURL string `orm:"column(receipt_url);size(500)" json:"receipt_url"`
|
ReceiptFileIDs string `orm:"column(receipt_file_ids);size(500)" json:"receipt_file_ids"`
|
||||||
|
ReceiptURLs string `orm:"column(receipt_urls);size(500)" json:"receipt_urls"`
|
||||||
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
|
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
|
||||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||||
UpdateTime *time.Time `orm:"column(update_time);null;type(datetime)" json:"update_time"`
|
UpdateTime *time.Time `orm:"column(update_time);null;type(datetime)" json:"update_time"`
|
||||||
|
|||||||
@@ -156,12 +156,15 @@ func RegisterAuthRoutes() {
|
|||||||
|
|
||||||
// 报销与审批管理
|
// 报销与审批管理
|
||||||
beego.Router("/backend/reimbursements", &controllers.BackendReimburseController{}, "get:List;post:Create")
|
beego.Router("/backend/reimbursements", &controllers.BackendReimburseController{}, "get:List;post:Create")
|
||||||
beego.Router("/backend/reimbursements/types", &controllers.BackendReimburseController{}, "get:Types")
|
beego.Router("/backend/reimbursements/types/manage", &controllers.BackendReimburseController{}, "get:TypesManage")
|
||||||
|
beego.Router("/backend/reimbursements/types/:type_id", &controllers.BackendReimburseController{}, "post:UpdateType;delete:DeleteType")
|
||||||
|
beego.Router("/backend/reimbursements/types", &controllers.BackendReimburseController{}, "get:Types;post:CreateType")
|
||||||
beego.Router("/backend/reimbursements/flows", &controllers.BackendReimburseController{}, "get:Flows")
|
beego.Router("/backend/reimbursements/flows", &controllers.BackendReimburseController{}, "get:Flows")
|
||||||
beego.Router("/backend/reimbursements/:id/items", &controllers.BackendReimburseController{}, "post:AddItem")
|
beego.Router("/backend/reimbursements/:id/items", &controllers.BackendReimburseController{}, "post:AddItem")
|
||||||
beego.Router("/backend/reimbursements/:id/items/:item_id", &controllers.BackendReimburseController{}, "post:UpdateItem;delete:DeleteItem")
|
beego.Router("/backend/reimbursements/:id/items/:item_id", &controllers.BackendReimburseController{}, "post:UpdateItem;delete:DeleteItem")
|
||||||
beego.Router("/backend/reimbursements/:id", &controllers.BackendReimburseController{}, "get:Detail;post:Update;delete:Delete")
|
beego.Router("/backend/reimbursements/:id", &controllers.BackendReimburseController{}, "get:Detail;post:Update;delete:Delete")
|
||||||
beego.Router("/backend/reimbursements/:id/submit", &controllers.BackendReimburseController{}, "post:Submit")
|
beego.Router("/backend/reimbursements/:id/submit", &controllers.BackendReimburseController{}, "post:Submit")
|
||||||
|
beego.Router("/backend/reimbursements/:id/status", &controllers.BackendReimburseController{}, "post:UpdateStatus")
|
||||||
beego.Router("/backend/reimbursements/:id/records", &controllers.BackendReimburseController{}, "get:Records")
|
beego.Router("/backend/reimbursements/:id/records", &controllers.BackendReimburseController{}, "get:Records")
|
||||||
|
|
||||||
// 日程提醒管理
|
// 日程提醒管理
|
||||||
|
|||||||
@@ -108,6 +108,11 @@ func Register() {
|
|||||||
beego.Router("/platform/complaintCategory/select", &controllers.PlatformComplaintCategoryController{}, "get:SelectList")
|
beego.Router("/platform/complaintCategory/select", &controllers.PlatformComplaintCategoryController{}, "get:SelectList")
|
||||||
beego.Router("/platform/complaintCategory", &controllers.PlatformComplaintCategoryController{}, "post:Create")
|
beego.Router("/platform/complaintCategory", &controllers.PlatformComplaintCategoryController{}, "post:Create")
|
||||||
beego.Router("/platform/complaintCategory/:id", &controllers.PlatformComplaintCategoryController{}, "post:Update;delete:Delete")
|
beego.Router("/platform/complaintCategory/:id", &controllers.PlatformComplaintCategoryController{}, "post:Update;delete:Delete")
|
||||||
|
|
||||||
|
// 报销全局通用费项(tid=0,仅平台可维护)
|
||||||
|
beego.Router("/platform/reimburseExpenseTypes", &controllers.PlatformReimburseExpenseTypeController{}, "get:List;post:Create")
|
||||||
|
beego.Router("/platform/reimburseExpenseTypes/:id", &controllers.PlatformReimburseExpenseTypeController{}, "post:Update;delete:Delete")
|
||||||
|
|
||||||
beego.Router("/platform/complaint/list", &controllers.PlatformComplaintController{}, "get:List")
|
beego.Router("/platform/complaint/list", &controllers.PlatformComplaintController{}, "get:List")
|
||||||
beego.Router("/platform/complaint", &controllers.PlatformComplaintController{}, "post:Create")
|
beego.Router("/platform/complaint", &controllers.PlatformComplaintController{}, "post:Create")
|
||||||
beego.Router("/platform/complaint/:id", &controllers.PlatformComplaintController{}, "get:Detail;post:Update;delete:Delete")
|
beego.Router("/platform/complaint/:id", &controllers.PlatformComplaintController{}, "get:Detail;post:Update;delete:Delete")
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ onUnmounted(() => {
|
|||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.erp-dashboard {
|
.erp-dashboard {
|
||||||
background-color: #f5f7fa;
|
background-color: #f5f7fa;
|
||||||
min-height: calc(100vh - 84px);
|
// min-height: calc(100vh - 84px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-row {
|
.stat-row {
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- 报销明细发票关联统一文件表 yz_system_files(逗号分隔多个文件 ID)
|
||||||
|
ALTER TABLE `yz_backend_reimbursement_items`
|
||||||
|
ADD COLUMN `receipt_file_ids` varchar(500) NOT NULL DEFAULT '' COMMENT '关联 yz_system_files.id,逗号分隔' AFTER `receipt_flag`;
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
ALTER TABLE `yz_backend_reimbursement_items`
|
|
||||||
ADD COLUMN `receipt_url` varchar(500) NOT NULL DEFAULT '' COMMENT '票据文件地址' AFTER `receipt_flag`,
|
|
||||||
ADD KEY `idx_receipt_url` (`receipt_url`);
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- 票据地址(多个 URL 逗号分隔,与 receipt_file_ids 对应)
|
||||||
|
ALTER TABLE `yz_backend_reimbursement_items`
|
||||||
|
ADD COLUMN `receipt_urls` varchar(500) NOT NULL DEFAULT '' COMMENT '票据文件地址,逗号分隔' AFTER `receipt_file_ids`;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-- 报销明细发票:统一使用 receipt_file_ids,移除 receipt_file_id
|
||||||
|
-- 若已存在 receipt_file_id 列,先合并数据再删除
|
||||||
|
|
||||||
|
UPDATE `yz_backend_reimbursement_items`
|
||||||
|
SET `receipt_file_ids` = CAST(`receipt_file_id` AS CHAR)
|
||||||
|
WHERE (`receipt_file_ids` = '' OR `receipt_file_ids` IS NULL)
|
||||||
|
AND `receipt_file_id` IS NOT NULL
|
||||||
|
AND `receipt_file_id` > 0;
|
||||||
|
|
||||||
|
ALTER TABLE `yz_backend_reimbursement_items`
|
||||||
|
DROP INDEX `idx_receipt_file_id`,
|
||||||
|
DROP COLUMN `receipt_file_id`;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- receipt_url 重命名为 receipt_urls(与代码字段一致)
|
||||||
|
ALTER TABLE `yz_backend_reimbursement_items`
|
||||||
|
CHANGE COLUMN `receipt_url` `receipt_urls` varchar(500) NOT NULL DEFAULT '' COMMENT '票据文件地址,逗号分隔';
|
||||||
|
|
||||||
|
ALTER TABLE `yz_backend_reimbursement_items`
|
||||||
|
DROP INDEX `idx_receipt_url`;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- 移除报销主表不再使用的字段:报销事由(title)、主表费用类型(expense_type)
|
||||||
|
-- 费用明细表 yz_backend_reimbursement_items 的 expense_type 保留不变
|
||||||
|
|
||||||
|
ALTER TABLE `yz_backend_reimbursements`
|
||||||
|
DROP INDEX `idx_expense_type`,
|
||||||
|
DROP COLUMN `title`,
|
||||||
|
DROP COLUMN `expense_type`;
|
||||||
@@ -31,10 +31,8 @@ CREATE TABLE IF NOT EXISTS `yz_backend_reimbursements` (
|
|||||||
`uid` int(11) DEFAULT NULL COMMENT '用户ID(数据隔离)',
|
`uid` int(11) DEFAULT NULL COMMENT '用户ID(数据隔离)',
|
||||||
`user_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '申请人ID',
|
`user_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '申请人ID',
|
||||||
`department_id` int(11) unsigned DEFAULT '0' COMMENT '部门ID',
|
`department_id` int(11) unsigned DEFAULT '0' COMMENT '部门ID',
|
||||||
`title` varchar(200) NOT NULL DEFAULT '' COMMENT '报销事由',
|
|
||||||
`total_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '总金额',
|
`total_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '总金额',
|
||||||
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态:0-草稿 1-审批中 2-已通过 3-已驳回 4-已撤回 5-已打款',
|
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态:0-草稿 1-审批中 2-已通过 3-已驳回 4-已撤回 5-已打款',
|
||||||
`expense_type` varchar(50) NOT NULL DEFAULT '' COMMENT '费用类型主分类',
|
|
||||||
`apply_date` date NOT NULL COMMENT '申请日期',
|
`apply_date` date NOT NULL COMMENT '申请日期',
|
||||||
`description` text COMMENT '备注说明',
|
`description` text COMMENT '备注说明',
|
||||||
`current_approver_id` int(11) unsigned DEFAULT '0' COMMENT '当前审批人ID',
|
`current_approver_id` int(11) unsigned DEFAULT '0' COMMENT '当前审批人ID',
|
||||||
@@ -47,7 +45,6 @@ CREATE TABLE IF NOT EXISTS `yz_backend_reimbursements` (
|
|||||||
KEY `idx_user_id` (`user_id`) USING BTREE,
|
KEY `idx_user_id` (`user_id`) USING BTREE,
|
||||||
KEY `idx_department_id` (`department_id`) USING BTREE,
|
KEY `idx_department_id` (`department_id`) USING BTREE,
|
||||||
KEY `idx_status` (`status`) USING BTREE,
|
KEY `idx_status` (`status`) USING BTREE,
|
||||||
KEY `idx_expense_type` (`expense_type`) USING BTREE,
|
|
||||||
KEY `idx_apply_date` (`apply_date`) USING BTREE,
|
KEY `idx_apply_date` (`apply_date`) USING BTREE,
|
||||||
KEY `idx_current_approver_id` (`current_approver_id`) USING BTREE,
|
KEY `idx_current_approver_id` (`current_approver_id`) USING BTREE,
|
||||||
KEY `idx_create_time` (`create_time`) USING BTREE,
|
KEY `idx_create_time` (`create_time`) USING BTREE,
|
||||||
|
|||||||
Reference in New Issue
Block a user