commit dee0ca70f6c98af9c3f5d6168962ec0bcdf76cb3 Author: WuYongJie Date: Tue Mar 10 16:19:47 2026 +0800 first commit diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..64d4738 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +FROM node:20-alpine as builder + +WORKDIR /app + +# 构建参数 +ARG VITE_API_BASE_URL=http://localhost:3000/api +ARG VITE_AUTH_MODE=local + +# 设置环境变量 +ENV VITE_API_BASE_URL=$VITE_API_BASE_URL +ENV VITE_AUTH_MODE=$VITE_AUTH_MODE + +# 安装依赖 +COPY package*.json ./ +RUN npm install --registry=https://registry.npmmirror.com + +# 复制源代码 +COPY . . + +# 构建前端 +RUN npm run build + +# 生产环境使用nginx +FROM nginx:alpine + +# 复制构建产物 +COPY --from=builder /app/dist /usr/share/nginx/html + +# 复制nginx配置 +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..12a9c1b --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,27 @@ +FROM node:18-alpine + +# 安装OpenSSL +RUN apk add --no-cache openssl openssl-dev + +WORKDIR /app + +# 安装依赖 +COPY package*.json ./ +RUN npm install --registry=https://registry.npmmirror.com + +# 复制源代码 +COPY . . + +# 生成Prisma客户端 +RUN npx prisma generate + +# 构建TypeScript +RUN npm run build + +# 创建uploads目录 +RUN mkdir -p uploads + +EXPOSE 3000 + +# 启动脚本 +CMD ["sh", "-c", "npx prisma migrate deploy && node dist/index.js"] diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..e69cdad --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,2868 @@ +{ + "name": "backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "backend", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@prisma/client": "^5.0.0", + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "dotenv": "^16.0.0", + "express": "^4.18.0", + "express-session": "^1.17.3", + "keycloak-connect": "^23.0.7", + "multer": "^1.4.5-lts.1", + "mysql2": "^3.0.0" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.2", + "@types/cors": "^2.8.0", + "@types/express": "^4.17.0", + "@types/express-session": "^1.17.7", + "@types/multer": "^1.4.7", + "@types/node": "^20.0.0", + "nodemon": "^3.0.0", + "prisma": "^5.0.0", + "ts-node": "^10.9.0", + "typescript": "^5.0.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@prisma/client": { + "version": "5.22.0", + "resolved": "https://registry.npmmirror.com/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.22.0", + "resolved": "https://registry.npmmirror.com/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "5.22.0", + "resolved": "https://registry.npmmirror.com/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmmirror.com/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.22.0", + "resolved": "https://registry.npmmirror.com/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.22.0", + "resolved": "https://registry.npmmirror.com/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0" + } + }, + "node_modules/@testim/chrome-version": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/@testim/chrome-version/-/chrome-version-1.1.4.tgz", + "integrity": "sha512-kIhULpw9TrGYnHp/8VfdcneIcxKnLixmADtukQRtJUmsVlMg0niMkwV0xZmi8hqa57xqilIHjWFA0GKvEjVU5g==", + "license": "MIT", + "optional": true + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmmirror.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT", + "optional": true + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmmirror.com/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.7", + "resolved": "https://registry.npmmirror.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", + "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express-session": { + "version": "1.18.2", + "resolved": "https://registry.npmmirror.com/@types/express-session/-/express-session-1.18.2.tgz", + "integrity": "sha512-k+I0BxwVXsnEU2hV77cCobC08kIsn4y44C3gC0b46uxZVMaXA04lSPgRLR/bSL2w0t0ShJiG8o4jPzRG/nscFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/multer": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.13.tgz", + "integrity": "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/node": { + "version": "20.19.25", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.25.tgz", + "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmmirror.com/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmmirror.com/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmmirror.com/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmmirror.com/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT", + "optional": true + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "optional": true, + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmmirror.com/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmmirror.com/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "license": "MIT" + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmmirror.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chromedriver": { + "version": "142.0.1", + "resolved": "https://registry.npmmirror.com/chromedriver/-/chromedriver-142.0.1.tgz", + "integrity": "sha512-Cgm5rPQ1aNdkoA4wiwJiKweQErbv+8w5xnN5SA5EIAxN0bcZzWrbcFtwlaKG3DUcJJYQKKArLe4vg+s85wmhfQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@testim/chrome-version": "^1.1.4", + "axios": "^1.12.0", + "compare-versions": "^6.1.0", + "extract-zip": "^2.0.1", + "proxy-agent": "^6.4.0", + "proxy-from-env": "^1.1.0", + "tcp-port-used": "^1.0.2" + }, + "bin": { + "chromedriver": "bin/chromedriver" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "optional": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compare-versions": { + "version": "6.1.1", + "resolved": "https://registry.npmmirror.com/compare-versions/-/compare-versions-6.1.1.tgz", + "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==", + "license": "MIT", + "optional": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "license": "MIT", + "optional": true + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmmirror.com/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "optional": true, + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "optional": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "optional": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmmirror.com/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-session": { + "version": "1.18.2", + "resolved": "https://registry.npmmirror.com/express-session/-/express-session-1.18.2.tgz", + "integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.1.0", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/express-session/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express-session/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "optional": true, + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "optional": true, + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "optional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "optional": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmmirror.com/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "optional": true, + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "optional": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmmirror.com/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "license": "MIT", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-regex": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ip-regex/-/ip-regex-4.3.0.tgz", + "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "license": "MIT", + "optional": true + }, + "node_modules/is2": { + "version": "2.0.9", + "resolved": "https://registry.npmmirror.com/is2/-/is2-2.0.9.tgz", + "integrity": "sha512-rZkHeBn9Zzq52sd9IUIV3a5mfwBY+o2HePMh0wkGBM4z4qjvy2GwVxQ6nNXSfw6MmVP6gf1QIlWjiOavhM3x5g==", + "license": "MIT", + "optional": true, + "dependencies": { + "deep-is": "^0.1.3", + "ip-regex": "^4.1.0", + "is-url": "^1.2.4" + }, + "engines": { + "node": ">=v0.10.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jwk-to-pem": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/jwk-to-pem/-/jwk-to-pem-2.0.7.tgz", + "integrity": "sha512-cSVphrmWr6reVchuKQZdfSs4U9c5Y4hwZggPoz6cbVnTpAVgGRpEuQng86IyqLeGZlhTh+c4MAreB6KbdQDKHQ==", + "license": "Apache-2.0", + "dependencies": { + "asn1.js": "^5.3.0", + "elliptic": "^6.6.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keycloak-connect": { + "version": "23.0.7", + "resolved": "https://registry.npmmirror.com/keycloak-connect/-/keycloak-connect-23.0.7.tgz", + "integrity": "sha512-7Coz2m/ar3QPYIPRhTr+sAuJB0BQn4r0vw+vsStRj0LpMVn2Uokgb2ufb9+OnTMIR3kjFLjQEJO3rtxBQJWSPA==", + "license": "Apache-2.0", + "dependencies": { + "jwk-to-pem": "^2.0.0" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "chromedriver": "latest" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/lru.min": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/lru.min/-/lru.min-1.1.2.tgz", + "integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/mysql2": { + "version": "3.15.3", + "resolved": "https://registry.npmmirror.com/mysql2/-/mysql2-3.15.3.tgz", + "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "license": "MIT", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/nodemon": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz", + "integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "optional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "optional": true, + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT", + "optional": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmmirror.com/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT", + "optional": true + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmmirror.com/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmmirror.com/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmmirror.com/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmmirror.com/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tcp-port-used": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/tcp-port-used/-/tcp-port-used-1.0.2.tgz", + "integrity": "sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4.3.1", + "is2": "^2.0.6" + } + }, + "node_modules/tcp-port-used/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/tcp-port-used/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "license": "MIT", + "optional": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC", + "optional": true + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmmirror.com/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "optional": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..1d5e1e0 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,41 @@ +{ + "name": "backend", + "version": "1.0.0", + "description": "摄影设备借还系统后端", + "main": "dist/index.js", + "scripts": { + "dev": "nodemon --exec ts-node src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "prisma:generate": "prisma generate", + "prisma:migrate": "prisma migrate dev", + "prisma:studio": "prisma studio" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@prisma/client": "^5.0.0", + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "dotenv": "^16.0.0", + "exceljs": "^4.4.0", + "express": "^4.18.0", + "express-session": "^1.17.3", + "keycloak-connect": "^23.0.7", + "multer": "^1.4.5-lts.1", + "mysql2": "^3.0.0" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.2", + "@types/cors": "^2.8.0", + "@types/express": "^4.17.0", + "@types/express-session": "^1.17.7", + "@types/multer": "^1.4.7", + "@types/node": "^20.0.0", + "nodemon": "^3.0.0", + "prisma": "^5.0.0", + "ts-node": "^10.9.0", + "typescript": "^5.0.0" + } +} diff --git a/backend/prisma.config.ts b/backend/prisma.config.ts new file mode 100644 index 0000000..6b6d3b6 --- /dev/null +++ b/backend/prisma.config.ts @@ -0,0 +1,12 @@ +import { defineConfig, env } from "prisma/config"; + +export default defineConfig({ + schema: "prisma/schema.prisma", + migrations: { + path: "prisma/migrations", + }, + engine: "classic", + datasource: { + url: env("DATABASE_URL"), + }, +}); diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma new file mode 100644 index 0000000..9d1b17b --- /dev/null +++ b/backend/prisma/schema.prisma @@ -0,0 +1,302 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "mysql" + url = env("DATABASE_URL") +} + +model AssetType { + id Int @id @default(autoincrement()) + name String @unique @db.VarChar(50) + status String @default("active") @db.VarChar(20) + can_be_parent Boolean @default(false) // 可作为父资产 + can_be_child Boolean @default(false) // 可作为子资产 + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + + equipments Equipment[] + + @@index([status]) + @@map("asset_type") +} + +model Department { + id Int @id @default(autoincrement()) + name String @unique @db.VarChar(100) + status String @default("active") @db.VarChar(20) + parent_id Int? // 父部门ID,null表示根部门 + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + + // 自关联 - 部门层级 + parent Department? @relation("DepartmentHierarchy", fields: [parent_id], references: [id]) + children Department[] @relation("DepartmentHierarchy") + + equipments Equipment[] + + @@index([status]) + @@index([parent_id]) + @@map("department") +} + +model Equipment { + id Int @id @default(autoincrement()) + equipment_code String @unique @db.VarChar(50) + equipment_name String @db.VarChar(100) + equipment_model String? @db.VarChar(100) + equipment_type String @db.VarChar(50) + code_generation_mode String @default("auto") @db.VarChar(20) + status String @default("可用") @db.VarChar(20) + total_quantity Int @default(0) + remaining_quantity Int @default(0) + unit_price Decimal @default(0.00) @db.Decimal(10, 2) + total_value Decimal @default(0.00) @db.Decimal(12, 2) + borrowed_value Decimal @default(0.00) @db.Decimal(12, 2) + available_value Decimal @default(0.00) @db.Decimal(12, 2) + description String? @db.Text + purchase_date DateTime? @db.Date + supplier String? @db.VarChar(100) + storage_location_id Int? + asset_type_id Int? + department_id Int? + // 父子资产关联字段 + parent_id Int? // 父资产ID(如果已挂载) + mount_status String @default("none") @db.VarChar(20) // none/mounted/unmounted + total_value_with_children Decimal @default(0.00) @db.Decimal(12, 2) // 含配件总价值 + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + + borrow_records BorrowRecord[] + storageLocation StorageLocation? @relation(fields: [storage_location_id], references: [id]) + assetType AssetType? @relation(fields: [asset_type_id], references: [id]) + department Department? @relation(fields: [department_id], references: [id]) + // 父子资产自关联 + parent Equipment? @relation("ParentChild", fields: [parent_id], references: [id]) + children Equipment[] @relation("ParentChild") + // 挂载历史关联 + mountHistories MountHistory[] + // 报废记录关联 + disposalRecords DisposalRecord[] + // 维修记录关联 + maintenanceRecords MaintenanceRecord[] + + @@index([equipment_code]) + @@index([equipment_type]) + @@index([status]) + @@index([code_generation_mode]) + @@index([unit_price]) + @@index([storage_location_id]) + @@index([asset_type_id]) + @@index([department_id]) + @@index([parent_id]) + @@index([mount_status]) + @@map("equipment") +} + +model BorrowRecord { + id Int @id @default(autoincrement()) + equipment_id Int + equipment_code String @db.VarChar(50) + person_id Int + person_name String @db.VarChar(50) + person_nickname String @db.VarChar(50) + person_identifier String? @db.VarChar(50) + borrow_time DateTime @default(now()) + expected_return_time DateTime? + is_long_term Boolean @default(false) + return_time DateTime? + status String @default("已借出") @db.VarChar(20) + return_condition String @default("正常") @db.VarChar(20) + notes String? @db.Text + children_snapshot String? @db.Text // JSON格式的配件快照 + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + + equipment Equipment @relation(fields: [equipment_id], references: [id]) + person Person @relation(fields: [person_id], references: [id]) + + @@index([equipment_id]) + @@index([person_id]) + @@index([status]) + @@index([borrow_time]) + @@map("borrow_record") +} + +model User { + id Int @id @default(autoincrement()) + username String @unique @db.VarChar(50) + password String @db.VarChar(255) + name String @db.VarChar(50) + email String? @db.VarChar(100) + phone String? @db.VarChar(20) + role String @default("user") @db.VarChar(20) + status String @default("active") @db.VarChar(20) + last_login_at DateTime? @db.DateTime + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + + operation_logs OperationLog[] + site_configs SiteConfig[] @relation("SiteConfigUpdatedBy") + mount_histories MountHistory[] + // 报废记录关联 + disposal_applied DisposalRecord[] @relation("DisposalApplier") + disposal_approved DisposalRecord[] @relation("DisposalApprover") + // 维修记录关联 + maintenance_records MaintenanceRecord[] + + @@index([role]) + @@index([status]) + @@map("user") +} + +model SiteConfig { + id Int @id @default(autoincrement()) + site_name String @db.VarChar(100) + logo_url String? @db.VarChar(255) + favicon_url String? @db.VarChar(255) + theme_color String? @db.VarChar(20) + updated_by Int? + created_at DateTime @default(now()) + updated_at DateTime @default(now()) + + updatedBy User? @relation("SiteConfigUpdatedBy", fields: [updated_by], references: [id]) + + @@map("site_config") +} + +model Person { + id Int @id @default(autoincrement()) + nickname String @db.VarChar(50) + real_name String @db.VarChar(50) + id_number String? @unique @db.VarChar(50) + department String? @db.VarChar(100) + position String? @db.VarChar(100) + phone String? @db.VarChar(20) + email String? @db.VarChar(100) + status String @default("active") @db.VarChar(20) + remarks String? @db.Text + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + + borrow_records BorrowRecord[] + + @@index([nickname]) + @@index([real_name]) + @@index([status]) + @@map("person") +} + +model StorageLocation { + id Int @id @default(autoincrement()) + name String @db.VarChar(100) + code String? @db.VarChar(50) + status String @default("active") @db.VarChar(20) + description String? @db.Text + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + + equipments Equipment[] + + @@index([status]) + @@map("storage_location") +} + +model OperationLog { + id Int @id @default(autoincrement()) + user_id Int? + username String? @db.VarChar(50) + operation_type String @db.VarChar(50) + equipment_id Int? + equipment_code String? @db.VarChar(50) + operation_details String? @db.Text + ip_address String? @db.VarChar(50) + created_at DateTime @default(now()) + + user User? @relation(fields: [user_id], references: [id]) + + @@index([user_id]) + @@index([operation_type]) + @@index([created_at]) + @@map("operation_log") +} + + +model MountHistory { + id Int @id @default(autoincrement()) + child_id Int // 子资产ID + parent_id Int? // 父资产ID + operation_type String @db.VarChar(20) // mount/unmount/replace + operated_at DateTime @default(now()) + operated_by Int // 操作人ID + notes String? @db.Text + old_parent_id Int? // 更换时的旧父资产ID + new_parent_id Int? // 更换时的新父资产ID + + child Equipment @relation(fields: [child_id], references: [id]) + operator User @relation(fields: [operated_by], references: [id]) + + @@index([child_id]) + @@index([parent_id]) + @@index([operation_type]) + @@index([operated_at]) + @@map("mount_history") +} + +// 报废记录 +model DisposalRecord { + id Int @id @default(autoincrement()) + equipment_id Int + equipment_code String @db.VarChar(50) + disposal_reason String @db.Text // 报废原因 + disposal_method String @db.VarChar(50) // 处置方式:报废/转让/捐赠 + estimated_value Decimal @db.Decimal(12,2) // 残值估价 + actual_value Decimal? @db.Decimal(12,2) // 实际处置价值 + status String @default("pending") @db.VarChar(20) // pending/approved/rejected/completed + applied_by Int // 申请人 + applied_at DateTime @default(now()) + approved_by Int? // 审批人 + approved_at DateTime? + completed_at DateTime? // 完成时间 + notes String? @db.Text + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + + equipment Equipment @relation(fields: [equipment_id], references: [id]) + applier User @relation("DisposalApplier", fields: [applied_by], references: [id]) + approver User? @relation("DisposalApprover", fields: [approved_by], references: [id]) + + @@index([equipment_id]) + @@index([status]) + @@map("disposal_record") +} + +// 维修记录 +model MaintenanceRecord { + id Int @id @default(autoincrement()) + equipment_id Int + equipment_code String @db.VarChar(50) + issue_description String @db.Text // 故障描述 + repair_vendor String? @db.VarChar(100) // 维修商 + estimated_cost Decimal @db.Decimal(10,2) // 预估费用 + actual_cost Decimal? @db.Decimal(10,2) // 实际费用 + start_date DateTime @default(now()) // 送修日期 + expected_end_date DateTime? // 预计完成日期 + actual_end_date DateTime? // 实际完成日期 + status String @default("in_progress") @db.VarChar(20) // in_progress/completed/cancelled + repair_result String? @db.Text // 维修结果 + created_by Int + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + + equipment Equipment @relation(fields: [equipment_id], references: [id]) + creator User @relation(fields: [created_by], references: [id]) + + @@index([equipment_id]) + @@index([status]) + @@map("maintenance_record") +} diff --git a/backend/src/controllers/assetTypeController.ts b/backend/src/controllers/assetTypeController.ts new file mode 100644 index 0000000..c1a79ba --- /dev/null +++ b/backend/src/controllers/assetTypeController.ts @@ -0,0 +1,180 @@ +import { Request, Response } from 'express' +import prisma from '../utils/prisma' +import { successResponse, errorResponse } from '../utils/response' + +const baseSelect = { + id: true, + name: true, + status: true, + can_be_parent: true, + can_be_child: true, + created_at: true, + updated_at: true +} + +// 获取启用的资产类型列表(下拉选项) +export const getAssetTypeOptions = async (_req: Request, res: Response) => { + try { + const types = await prisma.assetType.findMany({ + where: { status: 'active' }, + orderBy: { name: 'asc' }, + select: baseSelect + }) + + successResponse(res, types, '获取成功') + } catch (error: any) { + errorResponse(res, error.message || '获取资产类型列表失败') + } +} + +// 获取资产类型列表(分页) +export const getAssetTypeList = async (req: Request, res: Response) => { + try { + const { page = 1, pageSize = 10, keyword, status } = req.query + + const where: any = {} + if (status) { + where.status = status + } + if (keyword) { + where.name = { contains: keyword as string } + } + + const skip = (Number(page) - 1) * Number(pageSize) + + const [list, total] = await Promise.all([ + prisma.assetType.findMany({ + where, + skip, + take: Number(pageSize), + orderBy: { updated_at: 'desc' }, + select: { + ...baseSelect, + _count: { + select: { equipments: true } + } + } + }), + prisma.assetType.count({ where }) + ]) + + successResponse( + res, + { + list, + total, + page: Number(page), + pageSize: Number(pageSize) + }, + '获取成功' + ) + } catch (error: any) { + errorResponse(res, error.message || '获取资产类型列表失败') + } +} + +// 创建资产类型 +export const createAssetType = async (req: Request, res: Response) => { + try { + const { name, can_be_parent, can_be_child } = req.body as { + name?: string + can_be_parent?: boolean + can_be_child?: boolean + } + + if (!name || !name.trim()) { + return errorResponse(res, '类型名称不能为空', 422) + } + + const assetType = await prisma.assetType.create({ + data: { + name: name.trim(), + can_be_parent: can_be_parent ?? false, + can_be_child: can_be_child ?? false + }, + select: baseSelect + }) + + successResponse(res, assetType, '新增资产类型成功') + } catch (error: any) { + if (error.code === 'P2002') { + return errorResponse(res, '类型名称已存在') + } + errorResponse(res, error.message || '新增资产类型失败') + } +} + +// 更新资产类型 +export const updateAssetType = async (req: Request, res: Response) => { + try { + const { id } = req.params + const { name, status, can_be_parent, can_be_child } = req.body as { + name?: string + status?: string + can_be_parent?: boolean + can_be_child?: boolean + } + + const existing = await prisma.assetType.findUnique({ + where: { id: Number(id) } + }) + + if (!existing) { + return errorResponse(res, '资产类型不存在', 404) + } + + if (name !== undefined && !name.trim()) { + return errorResponse(res, '类型名称不能为空', 422) + } + + const updated = await prisma.assetType.update({ + where: { id: Number(id) }, + data: { + name: name?.trim() ?? existing.name, + status: status ?? existing.status, + can_be_parent: can_be_parent ?? existing.can_be_parent, + can_be_child: can_be_child ?? existing.can_be_child + }, + select: baseSelect + }) + + successResponse(res, updated, '更新资产类型成功') + } catch (error: any) { + if (error.code === 'P2002') { + return errorResponse(res, '类型名称已存在') + } + errorResponse(res, error.message || '更新资产类型失败') + } +} + +// 删除资产类型 +export const deleteAssetType = async (req: Request, res: Response) => { + try { + const { id } = req.params + + const existing = await prisma.assetType.findUnique({ + where: { id: Number(id) }, + include: { + _count: { + select: { equipments: true } + } + } + }) + + if (!existing) { + return errorResponse(res, '资产类型不存在', 404) + } + + if (existing._count.equipments > 0) { + return errorResponse(res, '该类型已被资产使用,无法删除') + } + + await prisma.assetType.delete({ + where: { id: Number(id) } + }) + + successResponse(res, null, '删除资产类型成功') + } catch (error: any) { + errorResponse(res, error.message || '删除资产类型失败') + } +} diff --git a/backend/src/controllers/barcodeController.ts b/backend/src/controllers/barcodeController.ts new file mode 100644 index 0000000..f5127b4 --- /dev/null +++ b/backend/src/controllers/barcodeController.ts @@ -0,0 +1,103 @@ +import { Request, Response } from 'express' +import prisma from '../utils/prisma' +import { successResponse, errorResponse } from '../utils/response' + +// 获取资产信息用于生成条码(条码在前端生成) +export const getEquipmentForBarcode = async (req: Request, res: Response) => { + try { + const { equipment_code } = req.params + + const equipment = await prisma.equipment.findUnique({ + where: { equipment_code }, + select: { + id: true, + equipment_code: true, + equipment_name: true, + equipment_model: true, + equipment_type: true + } + }) + + if (!equipment) { + return errorResponse(res, '资产不存在', 404) + } + + successResponse(res, { + equipment_code: equipment.equipment_code, + equipment_name: equipment.equipment_name, + equipment_model: equipment.equipment_model, + equipment_type: equipment.equipment_type + }, '获取成功') + } catch (error: any) { + errorResponse(res, error.message || '获取资产信息失败') + } +} + +// 批量获取资产信息用于生成条码 +export const getEquipmentBatchForBarcode = async (req: Request, res: Response) => { + try { + const { equipment_ids } = req.body + + // 使用 select 明确指定所有需要的字段,包括关联数据 + const equipmentList = await prisma.equipment.findMany({ + where: { + id: { + in: equipment_ids.map((id: number) => Number(id)) + } + }, + select: { + id: true, + equipment_code: true, + equipment_name: true, + equipment_model: true, + equipment_type: true, + storage_location_id: true, + storageLocation: { + select: { + id: true, + name: true, + code: true + } + } + } + }) + + // 调试:打印原始查询结果 + console.log('条码打印 - 原始查询结果:', JSON.stringify(equipmentList, null, 2)) + equipmentList.forEach((eq, index) => { + console.log(`原始资产 ${index + 1} (ID: ${eq.id}):`, { + name: eq.equipment_name, + storage_location_id: eq.storage_location_id, + storageLocation: eq.storageLocation, + allKeys: Object.keys(eq) + }) + }) + + // 直接返回查询结果,不需要再次映射 + const result = equipmentList.map(eq => ({ + id: eq.id, + equipment_code: eq.equipment_code, + equipment_name: eq.equipment_name, + equipment_model: eq.equipment_model, + equipment_type: eq.equipment_type, + storage_location_id: eq.storage_location_id, + storageLocation: eq.storageLocation + })) + + // 调试:打印返回的数据 + console.log('条码打印 - 最终返回结果:', JSON.stringify(result, null, 2)) + result.forEach((eq, index) => { + console.log(`资产 ${index + 1} (ID: ${eq.id}):`, { + name: eq.equipment_name, + storage_location_id: eq.storage_location_id, + storageLocation: eq.storageLocation + }) + }) + + successResponse(res, result, '获取成功') + } catch (error: any) { + console.error('批量获取资产信息失败:', error) + errorResponse(res, error.message || '批量获取资产信息失败') + } +} + diff --git a/backend/src/controllers/batchController.ts b/backend/src/controllers/batchController.ts new file mode 100644 index 0000000..f4bf16d --- /dev/null +++ b/backend/src/controllers/batchController.ts @@ -0,0 +1,238 @@ +import { Request, Response } from 'express' +import prisma from '../utils/prisma' +import { successResponse, errorResponse } from '../utils/response' +import { logOperation } from './operationLogController' + +// 批量借出 +export const batchBorrow = async (req: Request, res: Response) => { + try { + const { equipment_ids, person_id, expected_return_time, is_long_term, notes } = req.body + + if (!Array.isArray(equipment_ids) || equipment_ids.length === 0) { + return errorResponse(res, '请选择要借出的资产') + } + + const person = await prisma.person.findUnique({ where: { id: Number(person_id) } }) + if (!person || person.status !== 'active') { + return errorResponse(res, '使用者不存在或已禁用') + } + + const results: any[] = [] + let successCount = 0 + let failedCount = 0 + + for (const equipmentId of equipment_ids) { + try { + const equipment = await prisma.equipment.findUnique({ where: { id: Number(equipmentId) } }) + + if (!equipment) { + results.push({ equipment_id: equipmentId, status: 'failed', message: '资产不存在' }) + failedCount++ + continue + } + + if (equipment.status !== '可用' || equipment.remaining_quantity <= 0) { + results.push({ equipment_id: equipmentId, equipment_code: equipment.equipment_code, status: 'failed', message: '资产不可借用' }) + failedCount++ + continue + } + + const longTerm = Boolean(is_long_term) + await prisma.borrowRecord.create({ + data: { + equipment_id: equipment.id, + equipment_code: equipment.equipment_code, + person_id: person.id, + person_name: person.real_name, + person_nickname: person.nickname, + person_identifier: person.id_number, + expected_return_time: longTerm ? null : (expected_return_time ? new Date(expected_return_time) : null), + is_long_term: longTerm, + notes: longTerm ? `${notes || ''};在职长期` : notes, + status: '已借出' + } + }) + + const newRemaining = equipment.remaining_quantity - 1 + await prisma.equipment.update({ + where: { id: equipment.id }, + data: { + remaining_quantity: newRemaining, + status: newRemaining === 0 ? '已借出' : '可用', + borrowed_value: Number(equipment.unit_price) * (equipment.total_quantity - newRemaining), + available_value: Number(equipment.unit_price) * newRemaining + } + }) + + results.push({ equipment_id: equipmentId, equipment_code: equipment.equipment_code, status: 'success', message: '借出成功' }) + successCount++ + } catch (error: any) { + results.push({ equipment_id: equipmentId, status: 'failed', message: error.message }) + failedCount++ + } + } + + // 记录操作日志 + const userId = res.locals.user?.id + const username = res.locals.user?.username || res.locals.user?.name || 'unknown' + const successCodes = results.filter(r => r.status === 'success').map(r => r.equipment_code).join(', ') + if (successCount > 0) { + await logOperation( + userId, + username, + '批量借出', + `批量借出 ${successCount} 件资产给 ${person.real_name}(${person.nickname}),资产编码:${successCodes}`, + undefined, + undefined, + req.ip + ) + } + + successResponse(res, { success_count: successCount, failed_count: failedCount, results }, '批量借出完成') + } catch (error: any) { + errorResponse(res, error.message || '批量借出失败') + } +} + +// 批量归还 +export const batchReturn = async (req: Request, res: Response) => { + try { + const { equipment_ids, return_condition = '正常', notes } = req.body + + if (!Array.isArray(equipment_ids) || equipment_ids.length === 0) { + return errorResponse(res, '请选择要归还的资产') + } + + const results: any[] = [] + let successCount = 0 + let failedCount = 0 + + for (const equipmentId of equipment_ids) { + try { + const equipment = await prisma.equipment.findUnique({ where: { id: Number(equipmentId) } }) + + if (!equipment) { + results.push({ equipment_id: equipmentId, status: 'failed', message: '资产不存在' }) + failedCount++ + continue + } + + const borrowRecord = await prisma.borrowRecord.findFirst({ + where: { equipment_id: equipment.id, status: '已借出' }, + orderBy: { borrow_time: 'desc' } + }) + + if (!borrowRecord) { + results.push({ equipment_id: equipmentId, equipment_code: equipment.equipment_code, status: 'failed', message: '未找到借用记录' }) + failedCount++ + continue + } + + await prisma.borrowRecord.update({ + where: { id: borrowRecord.id }, + data: { return_time: new Date(), status: '已归还', return_condition, notes } + }) + + const newRemaining = equipment.remaining_quantity + 1 + await prisma.equipment.update({ + where: { id: equipment.id }, + data: { + remaining_quantity: newRemaining, + status: '可用', + borrowed_value: Number(equipment.unit_price) * (equipment.total_quantity - newRemaining), + available_value: Number(equipment.unit_price) * newRemaining + } + }) + + results.push({ equipment_id: equipmentId, equipment_code: equipment.equipment_code, status: 'success', message: '归还成功' }) + successCount++ + } catch (error: any) { + results.push({ equipment_id: equipmentId, status: 'failed', message: error.message }) + failedCount++ + } + } + + // 记录操作日志 + const userId = res.locals.user?.id + const username = res.locals.user?.username || res.locals.user?.name || 'unknown' + const successCodes = results.filter(r => r.status === 'success').map(r => r.equipment_code).join(', ') + if (successCount > 0) { + await logOperation( + userId, + username, + '批量归还', + `批量归还 ${successCount} 件资产,归还状态:${return_condition},资产编码:${successCodes}`, + undefined, + undefined, + req.ip + ) + } + + successResponse(res, { success_count: successCount, failed_count: failedCount, results }, '批量归还完成') + } catch (error: any) { + errorResponse(res, error.message || '批量归还失败') + } +} + +// 批量转移部门 +export const batchTransfer = async (req: Request, res: Response) => { + try { + const { equipment_ids, department_id } = req.body + + if (!Array.isArray(equipment_ids) || equipment_ids.length === 0) { + return errorResponse(res, '请选择要转移的资产') + } + + const department = await prisma.department.findUnique({ where: { id: Number(department_id) } }) + if (!department) { + return errorResponse(res, '目标部门不存在') + } + + const results: any[] = [] + let successCount = 0 + let failedCount = 0 + + for (const equipmentId of equipment_ids) { + try { + const equipment = await prisma.equipment.findUnique({ where: { id: Number(equipmentId) } }) + + if (!equipment) { + results.push({ equipment_id: equipmentId, status: 'failed', message: '资产不存在' }) + failedCount++ + continue + } + + await prisma.equipment.update({ + where: { id: equipment.id }, + data: { department_id: department.id } + }) + + results.push({ equipment_id: equipmentId, equipment_code: equipment.equipment_code, status: 'success', message: '转移成功' }) + successCount++ + } catch (error: any) { + results.push({ equipment_id: equipmentId, status: 'failed', message: error.message }) + failedCount++ + } + } + + // 记录操作日志 + const userId = res.locals.user?.id + const username = res.locals.user?.username || res.locals.user?.name || 'unknown' + const successCodes = results.filter(r => r.status === 'success').map(r => r.equipment_code).join(', ') + if (successCount > 0) { + await logOperation( + userId, + username, + '批量转移', + `批量转移 ${successCount} 件资产到部门 ${department.name},资产编码:${successCodes}`, + undefined, + undefined, + req.ip + ) + } + + successResponse(res, { success_count: successCount, failed_count: failedCount, results }, '批量转移完成') + } catch (error: any) { + errorResponse(res, error.message || '批量转移失败') + } +} diff --git a/backend/src/controllers/batchImportController.ts b/backend/src/controllers/batchImportController.ts new file mode 100644 index 0000000..26d7cea --- /dev/null +++ b/backend/src/controllers/batchImportController.ts @@ -0,0 +1,219 @@ +import { Request, Response } from 'express' +import prisma from '../utils/prisma' +import { successResponse, errorResponse } from '../utils/response' +import { generateEquipmentCode } from '../utils/equipmentCode' + +interface PersonImportData { + nickname: string + real_name: string + id_number?: string + department?: string + position?: string + phone?: string + email?: string +} + +interface EquipmentImportData { + equipment_name: string + equipment_type: string + equipment_model?: string + department?: string + storage_location?: string + unit_price?: number + total_quantity: number + supplier?: string + purchase_date?: string + description?: string +} + +// 批量导入使用者 +export const batchImportPersons = async (req: Request, res: Response) => { + try { + const { persons } = req.body as { persons: PersonImportData[] } + + if (!persons || !Array.isArray(persons) || persons.length === 0) { + return errorResponse(res, '导入数据不能为空') + } + + const results = { success: 0, failed: 0, errors: [] as { row: number; message: string }[] } + + for (let i = 0; i < persons.length; i++) { + const person = persons[i] + const rowNum = i + 2 // Excel行号从2开始(第1行是表头) + + try { + // 验证必填字段 + if (!person.nickname || !person.real_name) { + results.errors.push({ row: rowNum, message: '昵称和真实姓名为必填项' }) + results.failed++ + continue + } + + // 检查工号是否重复 + if (person.id_number) { + const existing = await prisma.person.findUnique({ + where: { id_number: person.id_number } + }) + if (existing) { + results.errors.push({ row: rowNum, message: `工号 ${person.id_number} 已存在` }) + results.failed++ + continue + } + } + + await prisma.person.create({ + data: { + nickname: person.nickname, + real_name: person.real_name, + id_number: person.id_number || null, + department: person.department || null, + position: person.position || null, + phone: person.phone || null, + email: person.email || null, + status: 'active' + } + }) + results.success++ + } catch (error: any) { + results.errors.push({ row: rowNum, message: error.message || '创建失败' }) + results.failed++ + } + } + + // 记录操作日志 + const user = (req as any).user + await prisma.operationLog.create({ + data: { + user_id: user?.id || null, + username: user?.username || 'system', + operation_type: '批量导入使用者', + operation_details: `成功: ${results.success}, 失败: ${results.failed}`, + ip_address: req.ip || req.socket.remoteAddress + } + }) + + successResponse(res, results, '导入完成') + } catch (error: any) { + errorResponse(res, error.message || '批量导入失败') + } +} + + +// 批量导入资产 +export const batchImportEquipments = async (req: Request, res: Response) => { + try { + const { equipments } = req.body as { equipments: EquipmentImportData[] } + + if (!equipments || !Array.isArray(equipments) || equipments.length === 0) { + return errorResponse(res, '导入数据不能为空') + } + + // 预加载资产类型、部门、地址映射 + const [assetTypes, departments, locations] = await Promise.all([ + prisma.assetType.findMany({ where: { status: 'active' } }), + prisma.department.findMany({ where: { status: 'active' } }), + prisma.storageLocation.findMany({ where: { status: 'active' } }) + ]) + + const assetTypeMap = new Map(assetTypes.map(t => [t.name, t.id])) + const departmentMap = new Map(departments.map(d => [d.name, d.id])) + const locationMap = new Map(locations.map(l => [l.name, l.id])) + + const results = { success: 0, failed: 0, errors: [] as { row: number; message: string }[] } + + for (let i = 0; i < equipments.length; i++) { + const eq = equipments[i] + const rowNum = i + 2 + + try { + // 验证必填字段 + if (!eq.equipment_name || !eq.equipment_type || !eq.total_quantity) { + results.errors.push({ row: rowNum, message: '资产名称、资产类型、数量为必填项' }) + results.failed++ + continue + } + + // 查找资产类型 + const assetTypeId = assetTypeMap.get(eq.equipment_type) + if (!assetTypeId) { + results.errors.push({ row: rowNum, message: `资产类型 "${eq.equipment_type}" 不存在` }) + results.failed++ + continue + } + + // 查找部门(可选) + let departmentId: number | null = null + if (eq.department) { + departmentId = departmentMap.get(eq.department) || null + if (!departmentId) { + results.errors.push({ row: rowNum, message: `部门 "${eq.department}" 不存在` }) + results.failed++ + continue + } + } + + // 查找地址(可选) + let locationId: number | null = null + if (eq.storage_location) { + locationId = locationMap.get(eq.storage_location) || null + if (!locationId) { + results.errors.push({ row: rowNum, message: `地址 "${eq.storage_location}" 不存在` }) + results.failed++ + continue + } + } + + // 生成资产编码 + const equipmentCode = await generateEquipmentCode(eq.equipment_type, prisma, locationId) + + // 计算价值 + const unitPrice = Number(eq.unit_price) || 0 + const totalQuantity = Number(eq.total_quantity) || 1 + const totalValue = unitPrice * totalQuantity + + await prisma.equipment.create({ + data: { + equipment_code: equipmentCode, + equipment_name: eq.equipment_name, + equipment_model: eq.equipment_model || null, + equipment_type: eq.equipment_type, + code_generation_mode: 'auto', + unit_price: unitPrice, + total_quantity: totalQuantity, + remaining_quantity: totalQuantity, + total_value: totalValue, + borrowed_value: 0, + available_value: totalValue, + supplier: eq.supplier || null, + purchase_date: eq.purchase_date ? new Date(eq.purchase_date) : null, + description: eq.description || null, + status: '可用', + storage_location_id: locationId, + department_id: departmentId, + asset_type_id: assetTypeId + } + }) + results.success++ + } catch (error: any) { + results.errors.push({ row: rowNum, message: error.message || '创建失败' }) + results.failed++ + } + } + + // 记录操作日志 + const user = (req as any).user + await prisma.operationLog.create({ + data: { + user_id: user?.id || null, + username: user?.username || 'system', + operation_type: '批量导入资产', + operation_details: `成功: ${results.success}, 失败: ${results.failed}`, + ip_address: req.ip || req.socket.remoteAddress + } + }) + + successResponse(res, results, '导入完成') + } catch (error: any) { + errorResponse(res, error.message || '批量导入失败') + } +} diff --git a/backend/src/controllers/borrowController.ts b/backend/src/controllers/borrowController.ts new file mode 100644 index 0000000..61699a4 --- /dev/null +++ b/backend/src/controllers/borrowController.ts @@ -0,0 +1,830 @@ +import { Request, Response } from 'express' +import prisma from '../utils/prisma' +import { successResponse, errorResponse } from '../utils/response' + +const parseNumber = (value: any): number | undefined => { + const num = Number(value) + return Number.isNaN(num) ? undefined : num +} + +const parseDate = (value?: any): Date | null => { + if (!value) return null + const date = new Date(value) + return Number.isNaN(date.getTime()) ? null : date +} + +const equipmentInclude = { + storageLocation: { + select: { + id: true, + name: true, + code: true, + status: true + } + } +} + +const getEquipmentByIdentifier = async (equipmentId?: number, equipmentCode?: string) => { + if (equipmentId) { + return prisma.equipment.findUnique({ where: { id: equipmentId }, include: equipmentInclude }) + } + if (equipmentCode) { + return prisma.equipment.findUnique({ where: { equipment_code: equipmentCode }, include: equipmentInclude }) + } + return null +} + +const ensurePersonAvailable = async (personId?: number) => { + if (!personId) { + throw new Error('使用者ID不能为空') + } + const person = await prisma.person.findUnique({ where: { id: personId } }) + if (!person || person.status !== 'active') { + throw new Error('使用者不存在或已禁用') + } + return person +} + +const ensureEquipmentBorrowable = (equipment: any) => { + // 检查报废状态 + if (equipment.status === '待报废' || equipment.status === '已报废') { + throw new Error('资产已报废或待报废,不可借用') + } + // 检查维护状态 + if (equipment.status === '维护中') { + throw new Error('资产正在维护中,不可借用') + } + if (equipment.status !== '可用') { + throw new Error('资产状态不可借用') + } + if (equipment.remaining_quantity <= 0) { + throw new Error('资产库存不足') + } +} + +const updateEquipmentAfterBorrow = async (equipment: any) => { + const newRemainingQuantity = equipment.remaining_quantity - 1 + if (newRemainingQuantity < 0) { + throw new Error('资产库存不足') + } + const borrowedQuantity = equipment.total_quantity - newRemainingQuantity + await prisma.equipment.update({ + where: { id: equipment.id }, + data: { + remaining_quantity: newRemainingQuantity, + status: newRemainingQuantity === 0 ? '已借出' : '可用', + borrowed_value: Number(equipment.unit_price) * borrowedQuantity, + available_value: Number(equipment.unit_price) * newRemainingQuantity + } + }) +} + +const updateEquipmentAfterReturn = async (equipment: any) => { + const newRemainingQuantity = equipment.remaining_quantity + 1 + const borrowedQuantity = equipment.total_quantity - newRemainingQuantity + await prisma.equipment.update({ + where: { id: equipment.id }, + data: { + remaining_quantity: newRemainingQuantity, + status: '可用', + borrowed_value: Number(equipment.unit_price) * borrowedQuantity, + available_value: Number(equipment.unit_price) * newRemainingQuantity + } + }) +} + +const mapBorrowRecord = (record: any) => ({ + id: record.id, + equipment_id: record.equipment_id, + equipment_code: record.equipment_code, + equipment_name: record.equipment?.equipment_name, + storage_location_id: record.equipment?.storage_location_id ?? null, + storageLocation: record.equipment?.storageLocation ?? null, + person: record.person + ? { + id: record.person.id, + nickname: record.person.nickname, + real_name: record.person.real_name, + department: record.person.department, + phone: record.person.phone + } + : { + id: record.person_id, + nickname: record.person_nickname, + real_name: record.person_name + }, + person_id: record.person_id, + person_name: record.person_name, + person_nickname: record.person_nickname, + person_identifier: record.person_identifier, + borrow_time: record.borrow_time, + expected_return_time: record.expected_return_time, + is_long_term: record.is_long_term, + return_time: record.return_time, + status: record.status, + return_condition: record.return_condition, + notes: record.notes +}) + +// 借用资产 +export const borrowEquipment = async (req: Request, res: Response) => { + try { + const { equipment_id, equipment_code, person_id, expected_return_time, notes, is_long_term } = + req.body + + const equipmentIdNum = parseNumber(equipment_id) + const equipment = await getEquipmentByIdentifier(equipmentIdNum, equipment_code) + + if (!equipment) { + return errorResponse(res, '资产不存在', 404) + } + + if (equipmentIdNum && equipment_code && equipment.equipment_code !== equipment_code) { + return errorResponse(res, '资产编号与ID不匹配', 422) + } + + let person + try { + person = await ensurePersonAvailable(parseNumber(person_id)) + } catch (error: any) { + return errorResponse(res, error.message || '使用者验证失败', 422) + } + + try { + ensureEquipmentBorrowable(equipment) + } catch (error: any) { + return errorResponse(res, error.message) + } + + const longTerm = Boolean(is_long_term) + const expectedReturn = longTerm ? null : parseDate(expected_return_time) + + const combinedNotes = + longTerm && notes + ? `${notes};在职长期` + : longTerm + ? '在职长期' + : notes || undefined + + // 获取子资产快照(如果是父资产) + let childrenSnapshot: string | undefined + const children = await prisma.equipment.findMany({ + where: { parent_id: equipment.id }, + select: { + id: true, + equipment_code: true, + equipment_name: true, + equipment_model: true, + equipment_type: true, + unit_price: true + } + }) + if (children.length > 0) { + childrenSnapshot = JSON.stringify(children) + } + + const borrowRecord = await prisma.borrowRecord.create({ + data: { + equipment_id: equipment.id, + equipment_code: equipment.equipment_code, + person_id: person.id, + person_name: person.real_name, + person_nickname: person.nickname, + person_identifier: person.id_number, + expected_return_time: expectedReturn, + is_long_term: longTerm, + notes: combinedNotes, + status: '已借出', + children_snapshot: childrenSnapshot + } + }) + + await updateEquipmentAfterBorrow(equipment) + + successResponse( + res, + { + id: borrowRecord.id, + borrow_time: borrowRecord.borrow_time, + children_count: children.length + }, + '借用成功' + ) + } catch (error: any) { + console.error('[借用资产失败]', { + error: error?.message || error, + stack: error?.stack, + body: req.body, + user: res.locals.user + }) + errorResponse(res, error.message || '借用资产失败') + } +} + +// 扫码出库 +export const scanBorrow = async (req: Request, res: Response) => { + try { + const { equipment_code, person_id, expected_return_time, notes, is_long_term } = req.body + + const equipment = await getEquipmentByIdentifier(undefined, equipment_code) + if (!equipment) { + return errorResponse(res, '资产不存在', 404) + } + + let person + try { + person = await ensurePersonAvailable(parseNumber(person_id)) + } catch (error: any) { + return errorResponse(res, error.message || '使用者验证失败', 422) + } + + try { + ensureEquipmentBorrowable(equipment) + } catch (error: any) { + return errorResponse(res, error.message) + } + + const longTerm = Boolean(is_long_term) + const expectedReturn = longTerm ? null : parseDate(expected_return_time) + const combinedNotes = + longTerm && notes + ? `${notes};在职长期` + : longTerm + ? '在职长期' + : notes || undefined + + // 获取子资产快照(如果是父资产) + let childrenSnapshot: string | undefined + const children = await prisma.equipment.findMany({ + where: { parent_id: equipment.id }, + select: { + id: true, + equipment_code: true, + equipment_name: true, + equipment_model: true, + equipment_type: true, + unit_price: true + } + }) + if (children.length > 0) { + childrenSnapshot = JSON.stringify(children) + } + + const borrowRecord = await prisma.borrowRecord.create({ + data: { + equipment_id: equipment.id, + equipment_code: equipment.equipment_code, + person_id: person.id, + person_name: person.real_name, + person_nickname: person.nickname, + person_identifier: person.id_number, + expected_return_time: expectedReturn, + is_long_term: longTerm, + notes: combinedNotes, + status: '已借出', + children_snapshot: childrenSnapshot + } + }) + + await updateEquipmentAfterBorrow(equipment) + const updatedEquipment = await prisma.equipment.findUnique({ + where: { id: equipment.id }, + include: equipmentInclude + }) + + successResponse( + res, + { + id: borrowRecord.id, + equipment_code: equipment.equipment_code, + equipment_name: equipment.equipment_name, + borrow_time: borrowRecord.borrow_time, + status: '已借出', + remaining_quantity: updatedEquipment?.remaining_quantity ?? 0, + equipment_status: updatedEquipment?.status ?? '已借出', + storage_location_id: equipment.storage_location_id, + storageLocation: equipment.storageLocation, + children_count: children.length + }, + '出库成功' + ) + } catch (error: any) { + console.error('[扫码出库失败]', { + error: error?.message || error, + stack: error?.stack, + body: req.body, + user: res.locals.user + }) + errorResponse(res, error.message || '出库失败') + } +} + +// 批量扫码出库 +export const scanBorrowBatch = async (req: Request, res: Response) => { + try { + const { equipment_codes, person_id, expected_return_time, notes, is_long_term } = req.body as { + equipment_codes: string[] + person_id: number + expected_return_time?: string + notes?: string + is_long_term?: boolean + } + + if (!Array.isArray(equipment_codes) || equipment_codes.length === 0) { + return errorResponse(res, '资产编码列表不能为空', 422) + } + + let person + try { + person = await ensurePersonAvailable(parseNumber(person_id)) + } catch (error: any) { + return errorResponse(res, error.message || '使用者验证失败', 422) + } + + const longTerm = Boolean(is_long_term) + const expectedReturn = longTerm ? null : parseDate(expected_return_time) + const combinedNotes = + longTerm && notes + ? `${notes};在职长期` + : longTerm + ? '在职长期' + : notes || undefined + const results: any[] = [] + let successCount = 0 + let failedCount = 0 + + for (const code of equipment_codes) { + try { + const equipment = await getEquipmentByIdentifier(undefined, code) + if (!equipment) { + results.push({ equipment_code: code, status: 'failed', message: '资产不存在' }) + failedCount++ + continue + } + + try { + ensureEquipmentBorrowable(equipment) + } catch (err: any) { + results.push({ equipment_code: code, status: 'failed', message: err.message }) + failedCount++ + continue + } + + await prisma.borrowRecord.create({ + data: { + equipment_id: equipment.id, + equipment_code: equipment.equipment_code, + person_id: person.id, + person_name: person.real_name, + person_nickname: person.nickname, + person_identifier: person.id_number, + expected_return_time: expectedReturn, + is_long_term: longTerm, + notes: combinedNotes, + status: '已借出' + } + }) + + await updateEquipmentAfterBorrow(equipment) + + results.push({ equipment_code: code, status: 'success', message: '出库成功' }) + successCount++ + } catch (error: any) { + results.push({ + equipment_code: code, + status: 'failed', + message: error.message || '出库失败' + }) + failedCount++ + } + } + + successResponse( + res, + { + success_count: successCount, + failed_count: failedCount, + results + }, + '批量出库完成' + ) + } catch (error: any) { + console.error('[批量出库失败]', { + error: error?.message || error, + stack: error?.stack, + body: req.body, + user: res.locals.user + }) + errorResponse(res, error.message || '批量出库失败') + } +} + +// 归还资产 +export const returnEquipment = async (req: Request, res: Response) => { + try { + const { + equipment_id, + equipment_code, + return_condition = '正常', + notes, + confirm_children_diff + } = req.body as Record + + const equipmentIdNum = parseNumber(equipment_id) + const equipment = await getEquipmentByIdentifier(equipmentIdNum, equipment_code) + + if (!equipment) { + return errorResponse(res, '资产不存在', 404) + } + + const borrowRecord = await prisma.borrowRecord.findFirst({ + where: { + equipment_id: equipment.id, + status: '已借出' + }, + orderBy: { borrow_time: 'desc' } + }) + + if (!borrowRecord) { + return errorResponse(res, '未找到对应的借用记录') + } + + // 检查配件差异 + let childrenDiff: any = null + if (borrowRecord.children_snapshot) { + const borrowedChildren = JSON.parse(borrowRecord.children_snapshot) as any[] + const currentChildren = await prisma.equipment.findMany({ + where: { parent_id: equipment.id }, + select: { + id: true, + equipment_code: true, + equipment_name: true, + equipment_model: true, + equipment_type: true, + unit_price: true + } + }) + + const borrowedIds = new Set(borrowedChildren.map(c => c.id)) + const currentIds = new Set(currentChildren.map(c => c.id)) + + const removed = borrowedChildren.filter(c => !currentIds.has(c.id)) + const added = currentChildren.filter(c => !borrowedIds.has(c.id)) + + if (removed.length > 0 || added.length > 0) { + childrenDiff = { removed, added } + + // 如果有差异且未确认,返回差异信息要求确认 + if (!confirm_children_diff) { + return successResponse(res, { + need_confirm: true, + children_diff: childrenDiff, + message: '配件与借出时不一致,请确认后继续' + }, '配件差异提醒') + } + } + } + + await prisma.borrowRecord.update({ + where: { id: borrowRecord.id }, + data: { + return_time: new Date(), + status: '已归还', + return_condition, + notes: childrenDiff + ? `${notes || ''};配件变更:移除${childrenDiff.removed.length}个,新增${childrenDiff.added.length}个` + : notes + } + }) + + await updateEquipmentAfterReturn(equipment) + + successResponse( + res, + { + id: borrowRecord.id, + return_time: new Date(), + children_diff: childrenDiff + }, + '归还成功' + ) + } catch (error: any) { + console.error('[归还资产失败]', { + error: error?.message || error, + stack: error?.stack, + body: req.body, + user: res.locals.user + }) + errorResponse(res, error.message || '归还资产失败') + } +} + +// 扫码入库 +export const scanReturn = async (req: Request, res: Response) => { + try { + const { + equipment_code, + return_condition = '正常', + notes, + person_id, + borrow_record_id + } = req.body as Record + + const equipment = await getEquipmentByIdentifier(undefined, equipment_code) + + if (!equipment) { + return errorResponse(res, '资产不存在', 404) + } + + const personIdNum = parseNumber(person_id) + const borrowRecordIdNum = parseNumber(borrow_record_id) + + let borrowRecord = null + + if (borrowRecordIdNum) { + borrowRecord = await prisma.borrowRecord.findFirst({ + where: { + id: borrowRecordIdNum, + equipment_id: equipment.id, + status: '已借出' + } + }) + } + + if (!borrowRecord && personIdNum) { + borrowRecord = await prisma.borrowRecord.findFirst({ + where: { + equipment_id: equipment.id, + person_id: personIdNum, + status: '已借出' + }, + orderBy: { borrow_time: 'desc' } + }) + } + + if (!borrowRecord) { + borrowRecord = await prisma.borrowRecord.findFirst({ + where: { + equipment_id: equipment.id, + status: '已借出' + }, + orderBy: { borrow_time: 'desc' } + }) + } + + if (!borrowRecord) { + return errorResponse(res, '未找到对应的借用记录,请选择正确的使用者', 404) + } + + const updatedRecord = await prisma.borrowRecord.update({ + where: { id: borrowRecord.id }, + data: { + return_time: new Date(), + status: '已归还', + return_condition, + notes + } + }) + + await updateEquipmentAfterReturn(equipment) + + successResponse( + res, + { + id: updatedRecord.id, + equipment_code: equipment.equipment_code, + equipment_name: equipment.equipment_name, + return_time: updatedRecord.return_time, + status: '已归还', + person_id: updatedRecord.person_id + }, + '入库成功' + ) + } catch (error: any) { + console.error('[扫码入库失败]', { + error: error?.message || error, + stack: error?.stack, + body: req.body, + user: res.locals.user + }) + errorResponse(res, error.message || '入库失败') + } +} + +// 批量扫码入库 +export const scanReturnBatch = async (req: Request, res: Response) => { + try { + const { + equipment_codes, + return_condition = '正常', + notes + } = req.body as { equipment_codes: string[]; return_condition?: string; notes?: string } + + if (!Array.isArray(equipment_codes) || equipment_codes.length === 0) { + return errorResponse(res, '资产编码列表不能为空', 422) + } + + const results: any[] = [] + let successCount = 0 + let failedCount = 0 + + for (const code of equipment_codes) { + try { + const equipment = await getEquipmentByIdentifier(undefined, code) + + if (!equipment) { + results.push({ equipment_code: code, status: 'failed', message: '资产不存在' }) + failedCount++ + continue + } + + const borrowRecord = await prisma.borrowRecord.findFirst({ + where: { + equipment_id: equipment.id, + status: '已借出' + }, + orderBy: { borrow_time: 'desc' } + }) + + if (!borrowRecord) { + results.push({ equipment_code: code, status: 'failed', message: '未找到对应的借用记录' }) + failedCount++ + continue + } + + await prisma.borrowRecord.update({ + where: { id: borrowRecord.id }, + data: { + return_time: new Date(), + status: '已归还', + return_condition, + notes + } + }) + + await updateEquipmentAfterReturn(equipment) + + results.push({ equipment_code: code, status: 'success', message: '入库成功' }) + successCount++ + } catch (error: any) { + results.push({ + equipment_code: code, + status: 'failed', + message: error.message || '入库失败' + }) + failedCount++ + } + } + + successResponse( + res, + { + success_count: successCount, + failed_count: failedCount, + results + }, + '批量入库完成' + ) + } catch (error: any) { + console.error('[批量入库失败]', { + error: error?.message || error, + stack: error?.stack, + body: req.body, + user: res.locals.user + }) + errorResponse(res, error.message || '批量入库失败') + } +} + +// 根据资产编码查询借用记录 +export const getBorrowRecordByCode = async (req: Request, res: Response) => { + try { + const { equipment_code } = req.params + + const equipment = await getEquipmentByIdentifier(undefined, equipment_code) + + if (!equipment) { + return errorResponse(res, '资产不存在', 404) + } + + const borrowRecords = await prisma.borrowRecord.findMany({ + where: { + equipment_id: equipment.id, + status: '已借出' + }, + orderBy: { borrow_time: 'desc' }, + include: { + person: true, + equipment: { + select: { + equipment_name: true, + storage_location_id: true, + storageLocation: { + select: { + id: true, + name: true, + code: true, + status: true + } + } + } + } + } + }) + + if (borrowRecords.length === 0) { + return errorResponse(res, '未找到对应的借用记录', 404) + } + + successResponse( + res, + { + equipment_code: equipment.equipment_code, + equipment_name: equipment.equipment_name, + unit_price: Number(equipment.unit_price), + equipment_value: Number(equipment.unit_price), + storage_location_id: equipment.storage_location_id, + storageLocation: equipment.storageLocation, + borrow_records: borrowRecords.map(mapBorrowRecord), + can_return: true, + message: '资产已借出,可以选择对应使用者归还' + }, + '查询成功' + ) + } catch (error: any) { + errorResponse(res, error.message || '查询借用记录失败') + } +} + +// 获取借用记录列表 +export const getBorrowRecordList = async (req: Request, res: Response) => { + try { + const { page = 1, pageSize = 10, status, person_name, person_id } = req.query + + const filters: any[] = [] + + if (status) { + filters.push({ status }) + } + + const personIdNum = parseNumber(person_id) + if (personIdNum) { + filters.push({ person_id: personIdNum }) + } + + if (person_name) { + const kw = person_name as string + filters.push({ + OR: [ + { person_name: { contains: kw } }, + { person_nickname: { contains: kw } }, + { person_identifier: { contains: kw } } + ] + }) + } + + const where = filters.length ? { AND: filters } : undefined + const skip = (Number(page) - 1) * Number(pageSize) + + const [list, total] = await Promise.all([ + prisma.borrowRecord.findMany({ + where, + skip, + take: Number(pageSize), + orderBy: { borrow_time: 'desc' }, + include: { + person: true, + equipment: { + select: { + equipment_name: true, + storage_location_id: true, + storageLocation: { + select: { + id: true, + name: true, + code: true, + status: true + } + } + } + } + } + }), + prisma.borrowRecord.count({ where }) + ]) + + successResponse( + res, + { + list: list.map(mapBorrowRecord), + total, + page: Number(page), + pageSize: Number(pageSize) + }, + '获取成功' + ) + } catch (error: any) { + errorResponse(res, error.message || '获取借用记录列表失败') + } +} + diff --git a/backend/src/controllers/departmentController.ts b/backend/src/controllers/departmentController.ts new file mode 100644 index 0000000..4227b63 --- /dev/null +++ b/backend/src/controllers/departmentController.ts @@ -0,0 +1,303 @@ +import { Request, Response } from 'express' +import prisma from '../utils/prisma' +import { successResponse, errorResponse } from '../utils/response' + +const baseSelect = { + id: true, + name: true, + status: true, + parent_id: true, + created_at: true, + updated_at: true +} + +// 获取启用的部门列表(下拉选项,含层级信息) +export const getDepartmentOptions = async (_req: Request, res: Response) => { + try { + const departments = await prisma.department.findMany({ + where: { status: 'active' }, + orderBy: [{ parent_id: 'asc' }, { name: 'asc' }], + select: { + ...baseSelect, + parent: { + select: { id: true, name: true } + } + } + }) + + // 添加层级信息 + const options = departments.map(dept => ({ + ...dept, + parent_name: dept.parent?.name || null, + level: dept.parent_id ? 1 : 0 + })) + + successResponse(res, options, '获取成功') + } catch (error: any) { + errorResponse(res, error.message || '获取部门列表失败') + } +} + +// 获取部门树形结构 +export const getDepartmentTree = async (_req: Request, res: Response) => { + try { + const departments = await prisma.department.findMany({ + orderBy: { name: 'asc' }, + select: { + ...baseSelect, + _count: { + select: { equipments: true, children: true } + } + } + }) + + // 构建树形结构 + const rootDepts = departments.filter(d => !d.parent_id) + const childDepts = departments.filter(d => d.parent_id) + + const tree = rootDepts.map(root => ({ + ...root, + equipment_count: root._count.equipments, + children_count: root._count.children, + children: childDepts + .filter(child => child.parent_id === root.id) + .map(child => ({ + ...child, + equipment_count: child._count.equipments, + children_count: child._count.children, + children: [] + })) + })) + + successResponse(res, tree, '获取成功') + } catch (error: any) { + errorResponse(res, error.message || '获取部门树失败') + } +} + +// 获取部门列表(分页) +export const getDepartmentList = async (req: Request, res: Response) => { + try { + const { page = 1, pageSize = 10, keyword, status } = req.query + + const where: any = {} + if (status) { + where.status = status + } + if (keyword) { + where.name = { contains: keyword as string } + } + + const skip = (Number(page) - 1) * Number(pageSize) + + const [list, total] = await Promise.all([ + prisma.department.findMany({ + where, + skip, + take: Number(pageSize), + orderBy: [{ parent_id: 'asc' }, { updated_at: 'desc' }], + select: { + ...baseSelect, + parent: { + select: { id: true, name: true } + }, + _count: { + select: { equipments: true, children: true } + } + } + }), + prisma.department.count({ where }) + ]) + + const formattedList = list.map(dept => ({ + ...dept, + parent_name: dept.parent?.name || null, + level: dept.parent_id ? 1 : 0, + equipment_count: dept._count.equipments, + children_count: dept._count.children + })) + + successResponse( + res, + { + list: formattedList, + total, + page: Number(page), + pageSize: Number(pageSize) + }, + '获取成功' + ) + } catch (error: any) { + errorResponse(res, error.message || '获取部门列表失败') + } +} + +// 验证父部门有效性 +const validateParentDepartment = async (parentId: number | null | undefined, currentId?: number): Promise => { + if (!parentId) return null + + const parent = await prisma.department.findUnique({ + where: { id: parentId }, + select: { id: true, parent_id: true, status: true } + }) + + if (!parent) { + return '指定的父部门不存在' + } + + if (parent.status !== 'active') { + return '父部门已禁用,无法设置' + } + + // 检查父部门是否已经是子部门(限制两级) + if (parent.parent_id) { + return '不能将子部门设为父部门(最多支持两级层级)' + } + + // 检查是否设置自己为父部门 + if (currentId && parentId === currentId) { + return '不能将部门设为自己的子部门' + } + + return null +} + +// 创建部门 +export const createDepartment = async (req: Request, res: Response) => { + try { + const { name, parent_id } = req.body as { name?: string; parent_id?: number | null } + + if (!name || !name.trim()) { + return errorResponse(res, '部门名称不能为空', 422) + } + + // 验证父部门 + const parentError = await validateParentDepartment(parent_id) + if (parentError) { + return errorResponse(res, parentError, 400) + } + + const department = await prisma.department.create({ + data: { + name: name.trim(), + parent_id: parent_id || null + }, + select: { + ...baseSelect, + parent: { + select: { id: true, name: true } + } + } + }) + + successResponse(res, { + ...department, + parent_name: department.parent?.name || null, + level: department.parent_id ? 1 : 0 + }, '新增部门成功') + } catch (error: any) { + if (error.code === 'P2002') { + return errorResponse(res, '部门名称已存在') + } + errorResponse(res, error.message || '新增部门失败') + } +} + +// 更新部门 +export const updateDepartment = async (req: Request, res: Response) => { + try { + const { id } = req.params + const { name, status, parent_id } = req.body as { name?: string; status?: string; parent_id?: number | null } + + const existing = await prisma.department.findUnique({ + where: { id: Number(id) }, + include: { + _count: { select: { children: true } } + } + }) + + if (!existing) { + return errorResponse(res, '部门不存在', 404) + } + + if (name !== undefined && !name.trim()) { + return errorResponse(res, '部门名称不能为空', 422) + } + + // 如果要设置父部门,验证有效性 + if (parent_id !== undefined) { + // 如果该部门有子部门,不能将其变为子部门 + if (parent_id && existing._count.children > 0) { + return errorResponse(res, '该部门下存在子部门,不能将其设为子部门', 400) + } + + const parentError = await validateParentDepartment(parent_id, Number(id)) + if (parentError) { + return errorResponse(res, parentError, 400) + } + } + + const updated = await prisma.department.update({ + where: { id: Number(id) }, + data: { + name: name?.trim() || existing.name, + status: status || existing.status, + parent_id: parent_id !== undefined ? (parent_id || null) : existing.parent_id + }, + select: { + ...baseSelect, + parent: { + select: { id: true, name: true } + } + } + }) + + successResponse(res, { + ...updated, + parent_name: updated.parent?.name || null, + level: updated.parent_id ? 1 : 0 + }, '更新部门成功') + } catch (error: any) { + if (error.code === 'P2002') { + return errorResponse(res, '部门名称已存在') + } + errorResponse(res, error.message || '更新部门失败') + } +} + +// 删除部门 +export const deleteDepartment = async (req: Request, res: Response) => { + try { + const { id } = req.params + + const existing = await prisma.department.findUnique({ + where: { id: Number(id) }, + include: { + _count: { + select: { equipments: true, children: true } + } + } + }) + + if (!existing) { + return errorResponse(res, '部门不存在', 404) + } + + // 检查是否有子部门 + if (existing._count.children > 0) { + return errorResponse(res, '该部门下存在子部门,请先删除或转移子部门', 400) + } + + if (existing._count.equipments > 0) { + return errorResponse(res, '该部门已被资产使用,无法删除') + } + + await prisma.department.delete({ + where: { id: Number(id) } + }) + + successResponse(res, null, '删除部门成功') + } catch (error: any) { + errorResponse(res, error.message || '删除部门失败') + } +} diff --git a/backend/src/controllers/disposalController.ts b/backend/src/controllers/disposalController.ts new file mode 100644 index 0000000..190832f --- /dev/null +++ b/backend/src/controllers/disposalController.ts @@ -0,0 +1,254 @@ +import { Request, Response } from 'express' +import prisma from '../utils/prisma' +import { successResponse, errorResponse } from '../utils/response' +import { logOperation } from './operationLogController' + +// 创建报废申请 +export const createDisposal = async (req: Request, res: Response) => { + try { + const { equipment_id, disposal_reason, disposal_method, estimated_value, notes } = req.body + const userId = res.locals.user?.id + + if (!userId) { + return errorResponse(res, '用户未登录', 401) + } + + const equipment = await prisma.equipment.findUnique({ + where: { id: Number(equipment_id) } + }) + + if (!equipment) { + return errorResponse(res, '资产不存在', 404) + } + + if (equipment.status === '已借出') { + return errorResponse(res, '资产已借出,无法申请报废') + } + if (equipment.status === '待报废' || equipment.status === '已报废') { + return errorResponse(res, '资产已在报废流程中') + } + + const disposal = await prisma.disposalRecord.create({ + data: { + equipment_id: equipment.id, + equipment_code: equipment.equipment_code, + disposal_reason, + disposal_method, + estimated_value: Number(estimated_value) || 0, + applied_by: userId, + notes + } + }) + + await prisma.equipment.update({ + where: { id: equipment.id }, + data: { status: '待报废' } + }) + + // 记录操作日志 + const username = res.locals.user?.username || res.locals.user?.name || 'unknown' + await logOperation( + userId, + username, + '报废申请', + `申请报废资产 ${equipment.equipment_name}(${equipment.equipment_code}),原因:${disposal_reason},处置方式:${disposal_method}`, + equipment.id, + equipment.equipment_code, + req.ip + ) + + successResponse(res, disposal, '报废申请创建成功') + } catch (error: any) { + console.error('[创建报废申请失败]', error) + errorResponse(res, error.message || '创建报废申请失败') + } +} + +// 获取报废记录列表 +export const getDisposalList = async (req: Request, res: Response) => { + try { + const { page = 1, pageSize = 10, status, keyword, equipment_id } = req.query + const skip = (Number(page) - 1) * Number(pageSize) + + const where: any = {} + if (status) where.status = status + if (equipment_id) where.equipment_id = Number(equipment_id) + if (keyword) { + where.OR = [ + { equipment_code: { contains: String(keyword) } }, + { disposal_reason: { contains: String(keyword) } } + ] + } + + const [list, total] = await Promise.all([ + prisma.disposalRecord.findMany({ + where, + skip, + take: Number(pageSize), + orderBy: { created_at: 'desc' }, + include: { + equipment: { select: { equipment_name: true, equipment_type: true, unit_price: true } }, + applier: { select: { id: true, name: true, username: true } }, + approver: { select: { id: true, name: true, username: true } } + } + }), + prisma.disposalRecord.count({ where }) + ]) + + successResponse(res, { list, total, page: Number(page), pageSize: Number(pageSize) }, '获取成功') + } catch (error: any) { + errorResponse(res, error.message || '获取报废记录列表失败') + } +} + +// 获取报废记录详情 +export const getDisposalDetail = async (req: Request, res: Response) => { + try { + const { id } = req.params + const disposal = await prisma.disposalRecord.findUnique({ + where: { id: Number(id) }, + include: { + equipment: true, + applier: { select: { id: true, name: true, username: true } }, + approver: { select: { id: true, name: true, username: true } } + } + }) + + if (!disposal) { + return errorResponse(res, '报废记录不存在', 404) + } + + successResponse(res, disposal, '获取成功') + } catch (error: any) { + errorResponse(res, error.message || '获取报废记录详情失败') + } +} + +// 审批报废申请 +export const approveDisposal = async (req: Request, res: Response) => { + try { + const { id } = req.params + const { approved, reject_reason } = req.body + const userId = res.locals.user?.id + + if (!userId) { + return errorResponse(res, '用户未登录', 401) + } + + const disposal = await prisma.disposalRecord.findUnique({ + where: { id: Number(id) }, + include: { equipment: true } + }) + + if (!disposal) { + return errorResponse(res, '报废记录不存在', 404) + } + + if (disposal.status !== 'pending') { + return errorResponse(res, '该申请已处理') + } + + if (approved) { + await prisma.disposalRecord.update({ + where: { id: Number(id) }, + data: { status: 'approved', approved_by: userId, approved_at: new Date() } + }) + } else { + await prisma.disposalRecord.update({ + where: { id: Number(id) }, + data: { status: 'rejected', approved_by: userId, approved_at: new Date(), notes: reject_reason } + }) + await prisma.equipment.update({ + where: { id: disposal.equipment_id }, + data: { status: '可用' } + }) + } + + // 记录操作日志 + const username = res.locals.user?.username || res.locals.user?.name || 'unknown' + await logOperation( + userId, + username, + approved ? '报废审批通过' : '报废审批驳回', + `${approved ? '批准' : '驳回'}资产 ${disposal.equipment?.equipment_name || ''}(${disposal.equipment_code}) 的报废申请${!approved && reject_reason ? `,原因:${reject_reason}` : ''}`, + disposal.equipment_id, + disposal.equipment_code, + req.ip + ) + + successResponse(res, null, approved ? '审批通过' : '已驳回') + } catch (error: any) { + errorResponse(res, error.message || '审批失败') + } +} + +// 完成报废处置 +export const completeDisposal = async (req: Request, res: Response) => { + try { + const { id } = req.params + const { actual_value, notes } = req.body + + const disposal = await prisma.disposalRecord.findUnique({ + where: { id: Number(id) } + }) + + if (!disposal) { + return errorResponse(res, '报废记录不存在', 404) + } + + if (disposal.status !== 'approved') { + return errorResponse(res, '只有已审批的申请才能完成处置') + } + + await prisma.disposalRecord.update({ + where: { id: Number(id) }, + data: { + status: 'completed', + actual_value: actual_value !== undefined ? Number(actual_value) : disposal.estimated_value, + completed_at: new Date(), + notes: notes || disposal.notes + } + }) + + await prisma.equipment.update({ + where: { id: disposal.equipment_id }, + data: { status: '已报废' } + }) + + // 记录操作日志 + const userId = res.locals.user?.id + const username = res.locals.user?.username || res.locals.user?.name || 'unknown' + await logOperation( + userId, + username, + '报废完成', + `完成资产 (${disposal.equipment_code}) 的报废处置,实际残值:¥${actual_value !== undefined ? actual_value : disposal.estimated_value}`, + disposal.equipment_id, + disposal.equipment_code, + req.ip + ) + + successResponse(res, null, '报废处置完成') + } catch (error: any) { + errorResponse(res, error.message || '完成报废处置失败') + } +} + +// 获取资产的报废历史 +export const getEquipmentDisposalHistory = async (req: Request, res: Response) => { + try { + const { equipment_id } = req.params + const history = await prisma.disposalRecord.findMany({ + where: { equipment_id: Number(equipment_id) }, + orderBy: { created_at: 'desc' }, + include: { + applier: { select: { id: true, name: true } }, + approver: { select: { id: true, name: true } } + } + }) + + successResponse(res, history, '获取成功') + } catch (error: any) { + errorResponse(res, error.message || '获取报废历史失败') + } +} diff --git a/backend/src/controllers/equipmentController.ts b/backend/src/controllers/equipmentController.ts new file mode 100644 index 0000000..2fda518 --- /dev/null +++ b/backend/src/controllers/equipmentController.ts @@ -0,0 +1,568 @@ +import { Request, Response } from 'express' +import prisma from '../utils/prisma' +import { successResponse, errorResponse } from '../utils/response' +import { generateEquipmentCode, validateEquipmentCode } from '../utils/equipmentCode' + +// 计算资产价值 +const calculateEquipmentValues = (unitPrice: number, totalQuantity: number, remainingQuantity: number) => { + const totalValue = Number(unitPrice) * totalQuantity + const borrowedQuantity = totalQuantity - remainingQuantity + const borrowedValue = Number(unitPrice) * borrowedQuantity + const availableValue = Number(unitPrice) * remainingQuantity + + return { + total_value: totalValue, + borrowed_value: borrowedValue, + available_value: availableValue + } +} + +const includeStorageLocation = { + storageLocation: { + select: { + id: true, + name: true, + code: true, + status: true + } + }, + department: { + select: { + id: true, + name: true, + status: true + } + }, + assetType: { + select: { + id: true, + name: true, + status: true + } + } +} + +const ensureLocationExists = async (locationId?: number | null) => { + if (!locationId) { + throw new Error('地址信息不能为空') + } + + const location = await prisma.storageLocation.findUnique({ + where: { id: locationId } + }) + + if (!location || location.status !== 'active') { + throw new Error('所选地址不存在或已停用') + } + + return location +} + +// 获取设备列表 +export const getEquipmentList = async (req: Request, res: Response) => { + try { + const { page = 1, pageSize = 10, equipmentType, status, keyword, storage_location_id, department_id, asset_type_id, mount_status, parent_id } = req.query + + const where: any = {} + + if (equipmentType) { + where.equipment_type = equipmentType + } + + if (status) { + where.status = status + } + + if (keyword) { + where.OR = [ + { equipment_name: { contains: keyword as string } }, + { equipment_code: { contains: keyword as string } }, + { equipment_model: { contains: keyword as string } } + ] + } + + const locationId = Number(storage_location_id) + if (!Number.isNaN(locationId) && storage_location_id) { + where.storage_location_id = locationId + } + + const deptId = Number(department_id) + if (!Number.isNaN(deptId) && department_id) { + where.department_id = deptId + } + + const typeId = Number(asset_type_id) + if (!Number.isNaN(typeId) && asset_type_id) { + where.asset_type_id = typeId + } + + // 挂载状态筛选 + if (mount_status && mount_status !== 'all') { + if (mount_status === 'mounted') { + where.mount_status = 'mounted' + } else if (mount_status === 'unmounted') { + where.mount_status = { in: ['none', 'unmounted'] } + where.parent_id = null + } + } + + // 父资产筛选(显示某主机的所有配件) + const parentIdNum = Number(parent_id) + if (!Number.isNaN(parentIdNum) && parent_id) { + where.parent_id = parentIdNum + } + + const skip = (Number(page) - 1) * Number(pageSize) + + const [list, total] = await Promise.all([ + prisma.equipment.findMany({ + where, + skip, + take: Number(pageSize), + orderBy: { created_at: 'desc' }, + include: { + ...includeStorageLocation, + parent: { + select: { + id: true, + equipment_code: true, + equipment_name: true + } + }, + children: { + select: { + id: true, + equipment_code: true, + equipment_name: true, + equipment_model: true, + unit_price: true + } + } + } + }), + prisma.equipment.count({ where }) + ]) + + successResponse( + res, + { + list, + total, + page: Number(page), + pageSize: Number(pageSize) + }, + '获取成功' + ) + } catch (error: any) { + errorResponse(res, error.message || '获取资产列表失败') + } +} + +// 获取资产详情 +export const getEquipmentDetail = async (req: Request, res: Response) => { + try { + const { id } = req.params + + const equipment = await prisma.equipment.findUnique({ + where: { id: Number(id) }, + include: { + ...includeStorageLocation, + parent: { + select: { + id: true, + equipment_code: true, + equipment_name: true, + equipment_model: true, + unit_price: true + } + }, + children: { + select: { + id: true, + equipment_code: true, + equipment_name: true, + equipment_model: true, + equipment_type: true, + unit_price: true, + status: true, + mount_status: true + } + } + } + }) + + if (!equipment) { + return errorResponse(res, '资产不存在', 404) + } + + successResponse(res, equipment, '获取成功') + } catch (error: any) { + errorResponse(res, error.message || '获取资产详情失败') + } +} + +// 新增资产 +export const createEquipment = async (req: Request, res: Response) => { + try { + const { + equipment_code, + equipment_name, + equipment_model, + equipment_type, + code_generation_mode = 'auto', + unit_price, + total_quantity, + purchase_date, + supplier, + description, + storage_location_id, + department_id, + asset_type_id + } = req.body + + // 生成或验证设备编码 + let finalCode = equipment_code + + const locationId = storage_location_id ? Number(storage_location_id) : null + const deptId = department_id ? Number(department_id) : null + const typeId = asset_type_id ? Number(asset_type_id) : null + + if (code_generation_mode === 'auto') { + // 自动生成编码,确保唯一性(generateEquipmentCode内部已处理唯一性检查) + finalCode = await generateEquipmentCode(equipment_type, prisma, locationId) + + // 再次检查编码唯一性(双重保险) + const existingCheck = await prisma.equipment.findUnique({ + where: { equipment_code: finalCode } + }) + + if (existingCheck) { + // 如果编码已存在,重新生成(这种情况理论上不应该发生,但作为安全措施) + finalCode = await generateEquipmentCode(equipment_type, prisma, locationId) + } + } else { + if (!equipment_code) { + return errorResponse(res, '自定义录入模式下资产编码不能为空') + } + + if (!validateEquipmentCode(equipment_code)) { + return errorResponse(res, '资产编码格式不正确') + } + + // 检查编码是否已存在 + const existing = await prisma.equipment.findUnique({ + where: { equipment_code } + }) + + if (existing) { + return errorResponse(res, '资产编码已存在') + } + } + + try { + await ensureLocationExists(locationId) + } catch (error: any) { + return errorResponse(res, error.message || '地址无效', 422) + } + + // 计算价值 + const remaining_quantity = total_quantity || 1 + const values = calculateEquipmentValues( + Number(unit_price) || 0, + Number(total_quantity) || 1, + remaining_quantity + ) + + const created = await prisma.equipment.create({ + data: { + equipment_code: finalCode, + equipment_name, + equipment_model, + equipment_type, + code_generation_mode, + unit_price: Number(unit_price) || 0, + total_quantity: Number(total_quantity) || 1, + remaining_quantity, + total_value: values.total_value, + borrowed_value: values.borrowed_value, + available_value: values.available_value, + purchase_date: purchase_date ? new Date(purchase_date) : null, + supplier, + description, + status: '可用', + storage_location_id: locationId || undefined, + department_id: deptId || undefined, + asset_type_id: typeId || undefined + } + }) + + const equipment = await prisma.equipment.findUnique({ + where: { id: created.id }, + include: includeStorageLocation + }) + + successResponse(res, equipment, '新增成功') + } catch (error: any) { + if (error.code === 'P2002') { + return errorResponse(res, '资产编码已存在') + } + errorResponse(res, error.message || '新增资产失败') + } +} + +// 更新资产 +export const updateEquipment = async (req: Request, res: Response) => { + try { + const { id } = req.params + const { + equipment_name, + equipment_model, + equipment_type, + unit_price, + total_quantity, + status, + purchase_date, + supplier, + description, + storage_location_id, + department_id, + asset_type_id + } = req.body + + // 获取当前设备信息 + const currentEquipment = await prisma.equipment.findUnique({ + where: { id: Number(id) } + }) + + if (!currentEquipment) { + return errorResponse(res, '资产不存在', 404) + } + + let targetLocationId = currentEquipment.storage_location_id + if (storage_location_id !== undefined) { + const normalizedLocationId = Number(storage_location_id) + try { + await ensureLocationExists(normalizedLocationId) + } catch (error: any) { + return errorResponse(res, error.message || '地址无效', 422) + } + targetLocationId = normalizedLocationId + } + + // 处理部门ID + let targetDeptId = currentEquipment.department_id + if (department_id !== undefined) { + targetDeptId = department_id ? Number(department_id) : null + } + + // 处理资产类型ID + let targetTypeId = currentEquipment.asset_type_id + if (asset_type_id !== undefined) { + targetTypeId = asset_type_id ? Number(asset_type_id) : null + } + + // 计算价值与剩余库存同步逻辑 + const finalTotalQuantity = total_quantity !== undefined ? Number(total_quantity) : currentEquipment.total_quantity + const finalUnitPrice = unit_price !== undefined ? Number(unit_price) : Number(currentEquipment.unit_price) + // 已借数量 = 原总数 - 原剩余数(保证不为负) + const borrowedQuantity = Math.max(currentEquipment.total_quantity - currentEquipment.remaining_quantity, 0) + // 若修改了总库存,则根据已借数量同步计算新的剩余库存;否则保持原剩余库存 + const newRemainingQuantity = + total_quantity !== undefined + ? Math.max(finalTotalQuantity - borrowedQuantity, 0) + : currentEquipment.remaining_quantity + const values = calculateEquipmentValues(finalUnitPrice, finalTotalQuantity, newRemainingQuantity) + + // 调试日志:观察请求与计算过程 + console.log('[更新资产] 输入参数:', { + id, + body: { + equipment_name, + equipment_model, + equipment_type, + unit_price, + total_quantity, + status, + purchase_date, + supplier, + description, + storage_location_id, + department_id, + asset_type_id + } + }) + console.log('[更新资产] 当前资产:', { + current_total: currentEquipment.total_quantity, + current_remaining: currentEquipment.remaining_quantity, + current_status: currentEquipment.status + }) + console.log('[更新资产] 计算结果:', { + borrowedQuantity, + finalTotalQuantity, + newRemainingQuantity, + finalUnitPrice, + total_value: values.total_value, + borrowed_value: values.borrowed_value, + available_value: values.available_value + }) + + await prisma.equipment.update({ + where: { id: Number(id) }, + data: { + equipment_name, + equipment_model, + equipment_type, + unit_price: finalUnitPrice, + total_quantity: finalTotalQuantity, + total_value: values.total_value, + borrowed_value: values.borrowed_value, + available_value: values.available_value, + // 若未显式传入状态,则根据剩余库存自动设定 + status: status ?? (newRemainingQuantity > 0 ? '可用' : '已借出'), + purchase_date: purchase_date ? new Date(purchase_date) : currentEquipment.purchase_date, + supplier, + description, + storage_location_id: targetLocationId, + department_id: targetDeptId, + asset_type_id: targetTypeId, + remaining_quantity: newRemainingQuantity + } + }) + + console.log('[更新资产] 已写入:', { + id, + total_quantity: finalTotalQuantity, + remaining_quantity: newRemainingQuantity + }) + + const equipment = await prisma.equipment.findUnique({ + where: { id: Number(id) }, + include: includeStorageLocation + }) + + successResponse(res, equipment, '更新成功') + } catch (error: any) { + errorResponse(res, error.message || '更新资产失败') + } +} + +// 删除资产 +export const deleteEquipment = async (req: Request, res: Response) => { + try { + const { id } = req.params + + const equipmentId = Number(id) + + // 检查是否有未归还的借用记录 + const activeBorrowCount = await prisma.borrowRecord.count({ + where: { + equipment_id: equipmentId, + status: '已借出' + } + }) + + if (activeBorrowCount > 0) { + return errorResponse(res, '该资产存在未归还的借用记录,无法删除') + } + + // 清理历史借用记录,避免外键约束阻止删除 + await prisma.borrowRecord.deleteMany({ + where: { + equipment_id: equipmentId + } + }) + + await prisma.equipment.delete({ + where: { id: equipmentId } + }) + + successResponse(res, null, '删除成功') + } catch (error: any) { + if (error.code === 'P2025') { + return errorResponse(res, '资产不存在', 404) + } + errorResponse(res, error.message || '删除资产失败') + } +} + +// 生成资产编码 +export const generateCode = async (req: Request, res: Response) => { + try { + const { equipment_type, code_generation_mode = 'auto', storage_location_id } = req.body + + if (code_generation_mode === 'auto') { + const locationId = storage_location_id ? Number(storage_location_id) : null + const code = await generateEquipmentCode(equipment_type, prisma, locationId) + successResponse(res, { + equipment_code: code, + code_format: '{类型数字}{地址ID(4位)}{序号(5位)}' + }, '生成成功') + } else { + errorResponse(res, '自定义录入模式不需要生成编码') + } + } catch (error: any) { + errorResponse(res, error.message || '生成编码失败') + } +} + +// 验证资产编码 +export const verifyCode = async (req: Request, res: Response) => { + try { + const { equipment_code } = req.body + + if (!validateEquipmentCode(equipment_code)) { + return successResponse(res, { + is_valid: false, + is_unique: false, + message: '编码格式不正确' + }, '验证成功') + } + + const existing = await prisma.equipment.findUnique({ + where: { equipment_code } + }) + + if (existing) { + return successResponse(res, { + is_valid: true, + is_unique: false, + message: '编码已存在' + }, '验证成功') + } + + successResponse(res, { + is_valid: true, + is_unique: true, + message: '编码可用' + }, '验证成功') + } catch (error: any) { + errorResponse(res, error.message || '验证编码失败') + } +} + +// 根据资产编码查询资产信息(扫码) +export const scanEquipment = async (req: Request, res: Response) => { + try { + const { equipment_code } = req.params + + const equipment = await prisma.equipment.findUnique({ + where: { equipment_code }, + include: includeStorageLocation + }) + + if (!equipment) { + return errorResponse(res, '资产不存在', 404) + } + + const is_available = equipment.status === '可用' && equipment.remaining_quantity > 0 + + successResponse(res, { + ...equipment, + is_available, + message: is_available ? '资产可用' : '资产不可用' + }, '查询成功') + } catch (error: any) { + errorResponse(res, error.message || '查询资产失败') + } +} + diff --git a/backend/src/controllers/exportController.ts b/backend/src/controllers/exportController.ts new file mode 100644 index 0000000..c938c8b --- /dev/null +++ b/backend/src/controllers/exportController.ts @@ -0,0 +1,457 @@ +import { Request, Response } from 'express' +import prisma from '../utils/prisma' +// eslint-disable-next-line @typescript-eslint/no-var-requires +const ExcelJS = require('exceljs') + +// 生成带下拉选项的导入模板 +export const generateImportTemplate = async (req: Request, res: Response) => { + try { + // 从数据库获取所有启用的选项数据 + const [departments, assetTypes, storageLocations] = await Promise.all([ + prisma.department.findMany({ + where: { status: 'active' }, + select: { name: true }, + orderBy: { name: 'asc' } + }), + prisma.assetType.findMany({ + where: { status: 'active' }, + select: { name: true }, + orderBy: { name: 'asc' } + }), + prisma.storageLocation.findMany({ + where: { status: 'active' }, + select: { name: true }, + orderBy: { name: 'asc' } + }) + ]) + + const departmentNames = departments.map(d => d.name) + const assetTypeNames = assetTypes.map(t => t.name) + const locationNames = storageLocations.map(l => l.name) + + const workbook = new ExcelJS.Workbook() + + // 创建主数据表 + const mainSheet = workbook.addWorksheet('资产导入') + + // 设置列 + mainSheet.columns = [ + { header: '资产名称 *', key: 'equipment_name', width: 25 }, + { header: '资产类型 *', key: 'equipment_type', width: 18 }, + { header: '型号', key: 'equipment_model', width: 20 }, + { header: '使用部门', key: 'department', width: 18 }, + { header: '地址', key: 'storage_location', width: 20 }, + { header: '单价', key: 'unit_price', width: 12 }, + { header: '数量 *', key: 'total_quantity', width: 10 }, + { header: '供应商', key: 'supplier', width: 20 }, + { header: '采购日期', key: 'purchase_date', width: 15 }, + { header: '描述', key: 'description', width: 30 } + ] + + // 设置表头样式 + const headerRow = mainSheet.getRow(1) + headerRow.font = { bold: true, color: { argb: 'FFFFFFFF' } } + headerRow.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF4A90D9' } } + headerRow.alignment = { horizontal: 'center', vertical: 'middle' } + headerRow.height = 25 + + // 添加示例数据行 + mainSheet.addRow({ + equipment_name: '示例:笔记本电脑', + equipment_type: assetTypeNames[0] || '电子设备', + equipment_model: 'ThinkPad X1 Carbon', + department: departmentNames[0] || '技术部', + storage_location: locationNames[0] || '总部仓库', + unit_price: 8000, + total_quantity: 1, + supplier: 'Lenovo', + purchase_date: '2024-01-01', + description: '办公用笔记本电脑' + }) + + // 设置示例行样式(灰色斜体,提示用户删除) + const exampleRow = mainSheet.getRow(2) + exampleRow.font = { italic: true, color: { argb: 'FF888888' } } + + // 创建隐藏的数据源表 + const dataSheet = workbook.addWorksheet('数据源') + dataSheet.state = 'hidden' // 隐藏此表 + + // 写入资产类型列表 + dataSheet.getCell('A1').value = '资产类型' + assetTypeNames.forEach((name, index) => { + dataSheet.getCell(`A${index + 2}`).value = name + }) + + // 写入部门列表 + dataSheet.getCell('B1').value = '使用部门' + departmentNames.forEach((name, index) => { + dataSheet.getCell(`B${index + 2}`).value = name + }) + + // 写入地址列表 + dataSheet.getCell('C1').value = '地址' + locationNames.forEach((name, index) => { + dataSheet.getCell(`C${index + 2}`).value = name + }) + + // 为主表设置数据验证(下拉列表) + // 预设100行的下拉选项 + const maxRows = 100 + + // 资产类型下拉(B列) + if (assetTypeNames.length > 0) { + for (let row = 2; row <= maxRows; row++) { + mainSheet.getCell(`B${row}`).dataValidation = { + type: 'list', + allowBlank: true, + formulae: [`数据源!$A$2:$A$${assetTypeNames.length + 1}`], + showErrorMessage: true, + errorTitle: '无效输入', + error: '请从下拉列表中选择资产类型' + } + } + } + + // 使用部门下拉(D列) + if (departmentNames.length > 0) { + for (let row = 2; row <= maxRows; row++) { + mainSheet.getCell(`D${row}`).dataValidation = { + type: 'list', + allowBlank: true, + formulae: [`数据源!$B$2:$B$${departmentNames.length + 1}`], + showErrorMessage: true, + errorTitle: '无效输入', + error: '请从下拉列表中选择使用部门' + } + } + } + + // 地址下拉(E列) + if (locationNames.length > 0) { + for (let row = 2; row <= maxRows; row++) { + mainSheet.getCell(`E${row}`).dataValidation = { + type: 'list', + allowBlank: true, + formulae: [`数据源!$C$2:$C$${locationNames.length + 1}`], + showErrorMessage: true, + errorTitle: '无效输入', + error: '请从下拉列表中选择地址' + } + } + } + + // 添加说明表 + const helpSheet = workbook.addWorksheet('填写说明') + helpSheet.columns = [ + { header: '字段', key: 'field', width: 15 }, + { header: '是否必填', key: 'required', width: 12 }, + { header: '说明', key: 'description', width: 50 } + ] + + helpSheet.getRow(1).font = { bold: true } + helpSheet.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE0E0E0' } } + + const helpData = [ + { field: '资产名称', required: '是', description: '资产的名称,如"笔记本电脑"、"投影仪"等' }, + { field: '资产类型', required: '是', description: '从下拉列表选择,当前可选:' + (assetTypeNames.join('、') || '暂无') }, + { field: '型号', required: '否', description: '资产的具体型号,如"ThinkPad X1 Carbon"' }, + { field: '使用部门', required: '否', description: '从下拉列表选择,当前可选:' + (departmentNames.join('、') || '暂无') }, + { field: '地址', required: '否', description: '从下拉列表选择,当前可选:' + (locationNames.join('、') || '暂无') }, + { field: '单价', required: '否', description: '资产单价(元),只填数字' }, + { field: '数量', required: '是', description: '资产数量,默认为1' }, + { field: '供应商', required: '否', description: '供应商名称' }, + { field: '采购日期', required: '否', description: '格式:YYYY-MM-DD,如2024-01-01' }, + { field: '描述', required: '否', description: '资产的详细描述信息' } + ] + + helpData.forEach(item => helpSheet.addRow(item)) + + // 设置响应头 + const timestamp = new Date().toISOString().slice(0, 10).replace(/-/g, '') + const filename = `资产导入模板_${timestamp}.xlsx` + + res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') + res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`) + + await workbook.xlsx.write(res) + res.end() + } catch (error: any) { + console.error('[生成导入模板失败]', error) + res.status(500).json({ code: 500, message: error.message || '生成模板失败', data: null }) + } +} + +// 导出资产列表 +export const exportEquipment = async (req: Request, res: Response) => { + try { + const { status, equipment_type, department_id, keyword } = req.query + + const where: any = {} + if (status) where.status = status + if (equipment_type) where.equipment_type = equipment_type + if (department_id) where.department_id = Number(department_id) + if (keyword) { + where.OR = [ + { equipment_code: { contains: String(keyword) } }, + { equipment_name: { contains: String(keyword) } } + ] + } + + const equipments = await prisma.equipment.findMany({ + where, + include: { + department: { select: { name: true } }, + storageLocation: { select: { name: true } }, + assetType: { select: { name: true } } + }, + orderBy: { created_at: 'desc' } + }) + + const workbook = new ExcelJS.Workbook() + const worksheet = workbook.addWorksheet('资产列表') + + worksheet.columns = [ + { header: '资产编码', key: 'equipment_code', width: 20 }, + { header: '资产名称', key: 'equipment_name', width: 25 }, + { header: '资产类型', key: 'equipment_type', width: 15 }, + { header: '型号', key: 'equipment_model', width: 20 }, + { header: '使用部门', key: 'department', width: 15 }, + { header: '存放地址', key: 'storage_location', width: 20 }, + { header: '单价', key: 'unit_price', width: 12 }, + { header: '总数量', key: 'total_quantity', width: 10 }, + { header: '剩余数量', key: 'remaining_quantity', width: 10 }, + { header: '状态', key: 'status', width: 10 }, + { header: '采购日期', key: 'purchase_date', width: 15 }, + { header: '供应商', key: 'supplier', width: 20 } + ] + + // 设置表头样式 + worksheet.getRow(1).font = { bold: true } + worksheet.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE0E0E0' } } + + equipments.forEach(eq => { + worksheet.addRow({ + equipment_code: eq.equipment_code, + equipment_name: eq.equipment_name, + equipment_type: eq.assetType?.name || eq.equipment_type, + equipment_model: eq.equipment_model, + department: eq.department?.name || '', + storage_location: eq.storageLocation?.name || '', + unit_price: Number(eq.unit_price), + total_quantity: eq.total_quantity, + remaining_quantity: eq.remaining_quantity, + status: eq.status, + purchase_date: eq.purchase_date ? new Date(eq.purchase_date).toLocaleDateString('zh-CN') : '', + supplier: eq.supplier || '' + }) + }) + + const timestamp = new Date().toISOString().slice(0, 10).replace(/-/g, '') + const filename = `资产列表_${timestamp}.xlsx` + + res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') + res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`) + + await workbook.xlsx.write(res) + res.end() + } catch (error: any) { + console.error('[导出资产列表失败]', error) + res.status(500).json({ code: 500, message: error.message || '导出失败', data: null }) + } +} + +// 导出借用记录 +export const exportBorrowRecords = async (req: Request, res: Response) => { + try { + const { status, person_name, start_date, end_date } = req.query + + const where: any = {} + if (status) where.status = status + if (person_name) { + where.OR = [ + { person_name: { contains: String(person_name) } }, + { person_nickname: { contains: String(person_name) } } + ] + } + if (start_date || end_date) { + where.borrow_time = {} + if (start_date) where.borrow_time.gte = new Date(String(start_date)) + if (end_date) where.borrow_time.lte = new Date(String(end_date)) + } + + const records = await prisma.borrowRecord.findMany({ + where, + include: { + equipment: { select: { equipment_name: true, unit_price: true } }, + person: { select: { nickname: true, real_name: true, department: true } } + }, + orderBy: { borrow_time: 'desc' } + }) + + const workbook = new ExcelJS.Workbook() + const worksheet = workbook.addWorksheet('借用记录') + + worksheet.columns = [ + { header: '资产编码', key: 'equipment_code', width: 20 }, + { header: '资产名称', key: 'equipment_name', width: 25 }, + { header: '使用者', key: 'person_name', width: 15 }, + { header: '使用者昵称', key: 'person_nickname', width: 15 }, + { header: '部门', key: 'department', width: 15 }, + { header: '借用时间', key: 'borrow_time', width: 20 }, + { header: '预计归还', key: 'expected_return_time', width: 20 }, + { header: '实际归还', key: 'return_time', width: 20 }, + { header: '状态', key: 'status', width: 10 }, + { header: '归还状况', key: 'return_condition', width: 10 }, + { header: '备注', key: 'notes', width: 30 } + ] + + worksheet.getRow(1).font = { bold: true } + worksheet.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE0E0E0' } } + + records.forEach(record => { + worksheet.addRow({ + equipment_code: record.equipment_code, + equipment_name: record.equipment?.equipment_name || '', + person_name: record.person_name, + person_nickname: record.person_nickname, + department: record.person?.department || '', + borrow_time: record.borrow_time ? new Date(record.borrow_time).toLocaleString('zh-CN') : '', + expected_return_time: record.is_long_term ? '长期' : (record.expected_return_time ? new Date(record.expected_return_time).toLocaleDateString('zh-CN') : ''), + return_time: record.return_time ? new Date(record.return_time).toLocaleString('zh-CN') : '', + status: record.status, + return_condition: record.return_condition, + notes: record.notes || '' + }) + }) + + const timestamp = new Date().toISOString().slice(0, 10).replace(/-/g, '') + const filename = `借用记录_${timestamp}.xlsx` + + res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') + res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`) + + await workbook.xlsx.write(res) + res.end() + } catch (error: any) { + console.error('[导出借用记录失败]', error) + res.status(500).json({ code: 500, message: error.message || '导出失败', data: null }) + } +} + + +// 生成带下拉选项的使用者导入模板 +export const generatePersonTemplate = async (req: Request, res: Response) => { + try { + // 从数据库获取所有启用的部门 + const departments = await prisma.department.findMany({ + where: { status: 'active' }, + select: { name: true }, + orderBy: { name: 'asc' } + }) + + const departmentNames = departments.map(d => d.name) + + const workbook = new ExcelJS.Workbook() + + // 创建主数据表 + const mainSheet = workbook.addWorksheet('使用者导入') + + // 设置列 + mainSheet.columns = [ + { header: '昵称 *', key: 'nickname', width: 15 }, + { header: '真实姓名 *', key: 'real_name', width: 15 }, + { header: '工号', key: 'employee_id', width: 15 }, + { header: '部门', key: 'department', width: 18 }, + { header: '职位', key: 'position', width: 15 }, + { header: '电话', key: 'phone', width: 15 }, + { header: '邮箱', key: 'email', width: 25 } + ] + + // 设置表头样式 + const headerRow = mainSheet.getRow(1) + headerRow.font = { bold: true, color: { argb: 'FFFFFFFF' } } + headerRow.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF4A90D9' } } + headerRow.alignment = { horizontal: 'center', vertical: 'middle' } + headerRow.height = 25 + + // 添加示例数据行 + mainSheet.addRow({ + nickname: '示例:张三', + real_name: '张三', + employee_id: 'EMP001', + department: departmentNames[0] || '技术部', + position: '工程师', + phone: '13800138000', + email: 'zhangsan@example.com' + }) + + // 设置示例行样式(灰色斜体,提示用户删除) + const exampleRow = mainSheet.getRow(2) + exampleRow.font = { italic: true, color: { argb: 'FF888888' } } + + // 创建隐藏的数据源表 + const dataSheet = workbook.addWorksheet('数据源') + dataSheet.state = 'hidden' // 隐藏此表 + + // 写入部门列表 + dataSheet.getCell('A1').value = '部门' + departmentNames.forEach((name, index) => { + dataSheet.getCell(`A${index + 2}`).value = name + }) + + // 为主表设置数据验证(下拉列表) + // 预设100行的下拉选项 + const maxRows = 100 + + // 部门下拉(D列) + if (departmentNames.length > 0) { + for (let row = 2; row <= maxRows; row++) { + mainSheet.getCell(`D${row}`).dataValidation = { + type: 'list', + allowBlank: true, + formulae: [`数据源!$A$2:$A$${departmentNames.length + 1}`], + showErrorMessage: true, + errorTitle: '无效输入', + error: '请从下拉列表中选择部门' + } + } + } + + // 添加说明表 + const helpSheet = workbook.addWorksheet('填写说明') + helpSheet.columns = [ + { header: '字段', key: 'field', width: 15 }, + { header: '是否必填', key: 'required', width: 12 }, + { header: '说明', key: 'description', width: 50 } + ] + + helpSheet.getRow(1).font = { bold: true } + helpSheet.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE0E0E0' } } + + const helpData = [ + { field: '昵称', required: '是', description: '使用者的昵称或常用名' }, + { field: '真实姓名', required: '是', description: '使用者的真实姓名' }, + { field: '工号', required: '否', description: '员工工号' }, + { field: '部门', required: '否', description: '从下拉列表选择,当前可选:' + (departmentNames.join('、') || '暂无') }, + { field: '职位', required: '否', description: '职位名称,如"工程师"、"经理"等' }, + { field: '电话', required: '否', description: '联系电话' }, + { field: '邮箱', required: '否', description: '电子邮箱地址' } + ] + + helpData.forEach(item => helpSheet.addRow(item)) + + // 设置响应头 + const timestamp = new Date().toISOString().slice(0, 10).replace(/-/g, '') + const filename = `使用者导入模板_${timestamp}.xlsx` + + res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') + res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`) + + await workbook.xlsx.write(res) + res.end() + } catch (error: any) { + console.error('[生成使用者导入模板失败]', error) + res.status(500).json({ code: 500, message: error.message || '生成模板失败', data: null }) + } +} diff --git a/backend/src/controllers/maintenanceController.ts b/backend/src/controllers/maintenanceController.ts new file mode 100644 index 0000000..bd3c074 --- /dev/null +++ b/backend/src/controllers/maintenanceController.ts @@ -0,0 +1,282 @@ +import { Request, Response } from 'express' +import prisma from '../utils/prisma' +import { successResponse, errorResponse } from '../utils/response' +import { logOperation } from './operationLogController' + +// 创建维修记录 +export const createMaintenance = async (req: Request, res: Response) => { + try { + const { equipment_id, issue_description, repair_vendor, estimated_cost, expected_end_date, notes } = req.body + const userId = res.locals.user?.id + + if (!userId) { + return errorResponse(res, '用户未登录', 401) + } + + const equipment = await prisma.equipment.findUnique({ + where: { id: Number(equipment_id) } + }) + + if (!equipment) { + return errorResponse(res, '资产不存在', 404) + } + + if (equipment.status === '已借出') { + return errorResponse(res, '资产已借出,无法送修') + } + if (equipment.status === '维护中') { + return errorResponse(res, '资产已在维修中') + } + if (equipment.status === '待报废' || equipment.status === '已报废') { + return errorResponse(res, '资产已报废,无法送修') + } + + const maintenance = await prisma.maintenanceRecord.create({ + data: { + equipment_id: equipment.id, + equipment_code: equipment.equipment_code, + issue_description, + repair_vendor, + estimated_cost: Number(estimated_cost) || 0, + expected_end_date: expected_end_date ? new Date(expected_end_date) : null, + created_by: userId + } + }) + + await prisma.equipment.update({ + where: { id: equipment.id }, + data: { status: '维护中' } + }) + + // 记录操作日志 + const username = res.locals.user?.username || res.locals.user?.name || 'unknown' + await logOperation( + userId, + username, + '送修', + `资产 ${equipment.equipment_name}(${equipment.equipment_code}) 送修,故障描述:${issue_description}${repair_vendor ? `,维修商:${repair_vendor}` : ''}`, + equipment.id, + equipment.equipment_code, + req.ip + ) + + successResponse(res, maintenance, '维修记录创建成功') + } catch (error: any) { + console.error('[创建维修记录失败]', error) + errorResponse(res, error.message || '创建维修记录失败') + } +} + +// 获取维修记录列表 +export const getMaintenanceList = async (req: Request, res: Response) => { + try { + const { page = 1, pageSize = 10, status, keyword, equipment_id } = req.query + const skip = (Number(page) - 1) * Number(pageSize) + + const where: any = {} + if (status) where.status = status + if (equipment_id) where.equipment_id = Number(equipment_id) + if (keyword) { + where.OR = [ + { equipment_code: { contains: String(keyword) } }, + { issue_description: { contains: String(keyword) } }, + { repair_vendor: { contains: String(keyword) } } + ] + } + + const [list, total] = await Promise.all([ + prisma.maintenanceRecord.findMany({ + where, + skip, + take: Number(pageSize), + orderBy: { created_at: 'desc' }, + include: { + equipment: { select: { equipment_name: true, equipment_type: true } }, + creator: { select: { id: true, name: true, username: true } } + } + }), + prisma.maintenanceRecord.count({ where }) + ]) + + successResponse(res, { list, total, page: Number(page), pageSize: Number(pageSize) }, '获取成功') + } catch (error: any) { + errorResponse(res, error.message || '获取维修记录列表失败') + } +} + +// 获取维修记录详情 +export const getMaintenanceDetail = async (req: Request, res: Response) => { + try { + const { id } = req.params + const maintenance = await prisma.maintenanceRecord.findUnique({ + where: { id: Number(id) }, + include: { + equipment: true, + creator: { select: { id: true, name: true, username: true } } + } + }) + + if (!maintenance) { + return errorResponse(res, '维修记录不存在', 404) + } + + successResponse(res, maintenance, '获取成功') + } catch (error: any) { + errorResponse(res, error.message || '获取维修记录详情失败') + } +} + +// 更新维修记录 +export const updateMaintenance = async (req: Request, res: Response) => { + try { + const { id } = req.params + const { repair_vendor, estimated_cost, expected_end_date, notes } = req.body + + const maintenance = await prisma.maintenanceRecord.findUnique({ + where: { id: Number(id) } + }) + + if (!maintenance) { + return errorResponse(res, '维修记录不存在', 404) + } + + if (maintenance.status !== 'in_progress') { + return errorResponse(res, '只能更新进行中的维修记录') + } + + const updated = await prisma.maintenanceRecord.update({ + where: { id: Number(id) }, + data: { + repair_vendor, + estimated_cost: estimated_cost !== undefined ? Number(estimated_cost) : undefined, + expected_end_date: expected_end_date ? new Date(expected_end_date) : undefined + } + }) + + successResponse(res, updated, '更新成功') + } catch (error: any) { + errorResponse(res, error.message || '更新维修记录失败') + } +} + +// 完成维修 +export const completeMaintenance = async (req: Request, res: Response) => { + try { + const { id } = req.params + const { actual_cost, repair_result } = req.body + + const maintenance = await prisma.maintenanceRecord.findUnique({ + where: { id: Number(id) } + }) + + if (!maintenance) { + return errorResponse(res, '维修记录不存在', 404) + } + + if (maintenance.status !== 'in_progress') { + return errorResponse(res, '只能完成进行中的维修记录') + } + + await prisma.maintenanceRecord.update({ + where: { id: Number(id) }, + data: { + status: 'completed', + actual_cost: actual_cost !== undefined ? Number(actual_cost) : maintenance.estimated_cost, + actual_end_date: new Date(), + repair_result + } + }) + + await prisma.equipment.update({ + where: { id: maintenance.equipment_id }, + data: { status: '可用' } + }) + + // 记录操作日志 + const userId = res.locals.user?.id + const username = res.locals.user?.username || res.locals.user?.name || 'unknown' + await logOperation( + userId, + username, + '维修完成', + `资产 (${maintenance.equipment_code}) 维修完成,实际费用:¥${actual_cost !== undefined ? actual_cost : maintenance.estimated_cost}${repair_result ? `,维修结果:${repair_result}` : ''}`, + maintenance.equipment_id, + maintenance.equipment_code, + req.ip + ) + + successResponse(res, null, '维修完成') + } catch (error: any) { + errorResponse(res, error.message || '完成维修失败') + } +} + +// 取消维修 +export const cancelMaintenance = async (req: Request, res: Response) => { + try { + const { id } = req.params + const { reason } = req.body + + const maintenance = await prisma.maintenanceRecord.findUnique({ + where: { id: Number(id) } + }) + + if (!maintenance) { + return errorResponse(res, '维修记录不存在', 404) + } + + if (maintenance.status !== 'in_progress') { + return errorResponse(res, '只能取消进行中的维修记录') + } + + await prisma.maintenanceRecord.update({ + where: { id: Number(id) }, + data: { status: 'cancelled', repair_result: reason || '已取消' } + }) + + await prisma.equipment.update({ + where: { id: maintenance.equipment_id }, + data: { status: '可用' } + }) + + // 记录操作日志 + const userId = res.locals.user?.id + const username = res.locals.user?.username || res.locals.user?.name || 'unknown' + await logOperation( + userId, + username, + '维修取消', + `取消资产 (${maintenance.equipment_code}) 的维修${reason ? `,原因:${reason}` : ''}`, + maintenance.equipment_id, + maintenance.equipment_code, + req.ip + ) + + successResponse(res, null, '维修已取消') + } catch (error: any) { + errorResponse(res, error.message || '取消维修失败') + } +} + +// 获取资产的维修历史 +export const getEquipmentMaintenanceHistory = async (req: Request, res: Response) => { + try { + const { equipment_id } = req.params + const history = await prisma.maintenanceRecord.findMany({ + where: { equipment_id: Number(equipment_id) }, + orderBy: { created_at: 'desc' }, + include: { + creator: { select: { id: true, name: true } } + } + }) + + // 计算总维修费用 + const totalCost = history.reduce((sum, record) => { + return sum + Number(record.actual_cost || record.estimated_cost || 0) + }, 0) + + successResponse(res, { history, totalCost }, '获取成功') + } catch (error: any) { + errorResponse(res, error.message || '获取维修历史失败') + } +} diff --git a/backend/src/controllers/mountController.ts b/backend/src/controllers/mountController.ts new file mode 100644 index 0000000..2c51740 --- /dev/null +++ b/backend/src/controllers/mountController.ts @@ -0,0 +1,413 @@ +import { Request, Response } from 'express' +import prisma from '../utils/prisma' +import { successResponse, errorResponse } from '../utils/response' +import { Decimal } from '@prisma/client/runtime/library' + +// 计算父资产含配件总价值 +async function calculateTotalValueWithChildren(parentId: number): Promise { + const parent = await prisma.equipment.findUnique({ + where: { id: parentId }, + include: { + children: { + select: { unit_price: true } + } + } + }) + + if (!parent) return new Decimal(0) + + const childrenValue = parent.children.reduce( + (sum, child) => sum.add(child.unit_price), + new Decimal(0) + ) + + return parent.unit_price.add(childrenValue) +} + +// 更新父资产的含配件总价值 +async function updateParentTotalValue(parentId: number): Promise { + const totalValue = await calculateTotalValueWithChildren(parentId) + await prisma.equipment.update({ + where: { id: parentId }, + data: { total_value_with_children: totalValue } + }) +} + +// 挂载子资产到父资产 +export const mountChild = async (req: Request, res: Response) => { + try { + const { parentId } = req.params + const { child_id, notes } = req.body as { child_id: number; notes?: string } + const userId = (req as any).user?.id || 1 + + // 验证父资产存在 + const parent = await prisma.equipment.findUnique({ + where: { id: Number(parentId) }, + include: { assetType: true } + }) + if (!parent) { + return errorResponse(res, '父资产不存在', 404) + } + + // 验证父资产类型支持挂载 + if (!parent.assetType?.can_be_parent) { + return errorResponse(res, '该资产类型不支持挂载配件', 400) + } + + // 验证子资产存在 + const child = await prisma.equipment.findUnique({ + where: { id: child_id }, + include: { assetType: true } + }) + if (!child) { + return errorResponse(res, '子资产不存在', 404) + } + + // 验证子资产类型支持被挂载 + if (!child.assetType?.can_be_child) { + return errorResponse(res, '该资产类型不能作为配件挂载', 400) + } + + // 验证子资产未被挂载 + if (child.parent_id) { + const currentParent = await prisma.equipment.findUnique({ + where: { id: child.parent_id } + }) + return errorResponse(res, `该配件已挂载到 ${currentParent?.equipment_name || '其他资产'}`, 400) + } + + // 执行挂载操作 + await prisma.$transaction(async (tx) => { + // 更新子资产 + await tx.equipment.update({ + where: { id: child_id }, + data: { + parent_id: Number(parentId), + mount_status: 'mounted' + } + }) + + // 记录挂载历史 + await tx.mountHistory.create({ + data: { + child_id, + parent_id: Number(parentId), + operation_type: 'mount', + operated_by: userId, + notes + } + }) + }) + + // 更新父资产含配件总价值 + await updateParentTotalValue(Number(parentId)) + + // 获取更新后的数据 + const updatedParent = await prisma.equipment.findUnique({ + where: { id: Number(parentId) }, + include: { + children: { + select: { + id: true, + equipment_code: true, + equipment_name: true, + equipment_model: true, + equipment_type: true, + unit_price: true, + status: true + } + } + } + }) + + successResponse(res, updatedParent, '挂载成功') + } catch (error: any) { + errorResponse(res, error.message || '挂载失败') + } +} + +// 从父资产卸载子资产 +export const unmountChild = async (req: Request, res: Response) => { + try { + const { parentId } = req.params + const { child_id, notes } = req.body as { child_id: number; notes?: string } + const userId = (req as any).user?.id || 1 + + // 验证子资产存在且挂载在该父资产上 + const child = await prisma.equipment.findUnique({ + where: { id: child_id } + }) + if (!child) { + return errorResponse(res, '子资产不存在', 404) + } + if (child.parent_id !== Number(parentId)) { + return errorResponse(res, '该配件未挂载到此资产', 400) + } + + // 执行卸载操作 + await prisma.$transaction(async (tx) => { + // 更新子资产 + await tx.equipment.update({ + where: { id: child_id }, + data: { + parent_id: null, + mount_status: 'unmounted' + } + }) + + // 记录卸载历史 + await tx.mountHistory.create({ + data: { + child_id, + parent_id: Number(parentId), + operation_type: 'unmount', + operated_by: userId, + notes + } + }) + }) + + // 更新父资产含配件总价值 + await updateParentTotalValue(Number(parentId)) + + successResponse(res, null, '卸载成功') + } catch (error: any) { + errorResponse(res, error.message || '卸载失败') + } +} + +// 更换配件(卸载旧配件 + 挂载新配件) +export const replaceChild = async (req: Request, res: Response) => { + try { + const { parentId } = req.params + const { old_child_id, new_child_id, notes } = req.body as { + old_child_id: number + new_child_id: number + notes?: string + } + const userId = (req as any).user?.id || 1 + + // 验证旧配件挂载在该父资产上 + const oldChild = await prisma.equipment.findUnique({ + where: { id: old_child_id } + }) + if (!oldChild || oldChild.parent_id !== Number(parentId)) { + return errorResponse(res, '旧配件未挂载到此资产', 400) + } + + // 验证新配件存在且未被挂载 + const newChild = await prisma.equipment.findUnique({ + where: { id: new_child_id }, + include: { assetType: true } + }) + if (!newChild) { + return errorResponse(res, '新配件不存在', 404) + } + if (!newChild.assetType?.can_be_child) { + return errorResponse(res, '新配件类型不能作为配件挂载', 400) + } + if (newChild.parent_id) { + return errorResponse(res, '新配件已挂载到其他资产', 400) + } + + // 在事务中执行更换操作 + await prisma.$transaction(async (tx) => { + // 卸载旧配件 + await tx.equipment.update({ + where: { id: old_child_id }, + data: { + parent_id: null, + mount_status: 'unmounted' + } + }) + + // 挂载新配件 + await tx.equipment.update({ + where: { id: new_child_id }, + data: { + parent_id: Number(parentId), + mount_status: 'mounted' + } + }) + + // 记录更换历史 + await tx.mountHistory.create({ + data: { + child_id: old_child_id, + parent_id: Number(parentId), + operation_type: 'replace', + operated_by: userId, + notes, + old_parent_id: Number(parentId), + new_parent_id: null + } + }) + + await tx.mountHistory.create({ + data: { + child_id: new_child_id, + parent_id: Number(parentId), + operation_type: 'replace', + operated_by: userId, + notes, + old_parent_id: null, + new_parent_id: Number(parentId) + } + }) + }) + + // 更新父资产含配件总价值 + await updateParentTotalValue(Number(parentId)) + + successResponse(res, null, '更换成功') + } catch (error: any) { + errorResponse(res, error.message || '更换失败') + } +} + +// 获取父资产的子资产列表 +export const getChildren = async (req: Request, res: Response) => { + try { + const { parentId } = req.params + + const parent = await prisma.equipment.findUnique({ + where: { id: Number(parentId) }, + include: { + children: { + select: { + id: true, + equipment_code: true, + equipment_name: true, + equipment_model: true, + equipment_type: true, + unit_price: true, + status: true, + mount_status: true, + created_at: true + } + } + } + }) + + if (!parent) { + return errorResponse(res, '资产不存在', 404) + } + + const totalChildrenValue = parent.children.reduce( + (sum, child) => sum.add(child.unit_price), + new Decimal(0) + ) + + successResponse(res, { + children: parent.children, + total_children_value: Number(totalChildrenValue), + total_value_with_children: Number(parent.total_value_with_children) + }, '获取成功') + } catch (error: any) { + errorResponse(res, error.message || '获取子资产列表失败') + } +} + +// 获取资产的挂载历史 +export const getMountHistory = async (req: Request, res: Response) => { + try { + const { id } = req.params + + const history = await prisma.mountHistory.findMany({ + where: { + OR: [ + { child_id: Number(id) }, + { parent_id: Number(id) } + ] + }, + include: { + child: { + select: { + id: true, + equipment_code: true, + equipment_name: true + } + }, + operator: { + select: { + id: true, + name: true, + username: true + } + } + }, + orderBy: { operated_at: 'desc' } + }) + + // 补充父资产信息 + const historyWithParent = await Promise.all( + history.map(async (h) => { + let parentInfo = null + if (h.parent_id) { + const parent = await prisma.equipment.findUnique({ + where: { id: h.parent_id }, + select: { id: true, equipment_code: true, equipment_name: true } + }) + parentInfo = parent + } + return { ...h, parent: parentInfo } + }) + ) + + successResponse(res, historyWithParent, '获取成功') + } catch (error: any) { + errorResponse(res, error.message || '获取挂载历史失败') + } +} + +// 获取可挂载的子资产列表(未挂载的配件类型资产) +export const getAvailableChildren = async (req: Request, res: Response) => { + try { + const { keyword, equipment_type } = req.query + + // 获取可作为子资产的类型 + const childTypes = await prisma.assetType.findMany({ + where: { can_be_child: true, status: 'active' }, + select: { name: true } + }) + const childTypeNames = childTypes.map(t => t.name) + + const where: any = { + parent_id: null, + mount_status: { in: ['none', 'unmounted'] }, + equipment_type: { in: childTypeNames }, + status: '可用' + } + + if (keyword) { + where.OR = [ + { equipment_name: { contains: keyword as string } }, + { equipment_code: { contains: keyword as string } }, + { equipment_model: { contains: keyword as string } } + ] + } + + if (equipment_type) { + where.equipment_type = equipment_type + } + + const children = await prisma.equipment.findMany({ + where, + select: { + id: true, + equipment_code: true, + equipment_name: true, + equipment_model: true, + equipment_type: true, + unit_price: true, + status: true + }, + orderBy: { equipment_name: 'asc' }, + take: 50 + }) + + successResponse(res, children, '获取成功') + } catch (error: any) { + errorResponse(res, error.message || '获取可挂载配件列表失败') + } +} diff --git a/backend/src/controllers/operationLogController.ts b/backend/src/controllers/operationLogController.ts new file mode 100644 index 0000000..99f9b22 --- /dev/null +++ b/backend/src/controllers/operationLogController.ts @@ -0,0 +1,111 @@ +import { Request, Response } from 'express' +import prisma from '../utils/prisma' +import { successResponse, errorResponse } from '../utils/response' + +// 获取操作日志列表 +export const getOperationLogList = async (req: Request, res: Response) => { + try { + const { + page = 1, + pageSize = 20, + operationType, + username, + startDate, + endDate + } = req.query + + const where: any = {} + + if (operationType) { + where.operation_type = operationType + } + + if (username) { + where.username = { contains: username as string } + } + + if (startDate || endDate) { + where.created_at = {} + if (startDate) { + where.created_at.gte = new Date(startDate as string) + } + if (endDate) { + const end = new Date(endDate as string) + end.setHours(23, 59, 59, 999) + where.created_at.lte = end + } + } + + const skip = (Number(page) - 1) * Number(pageSize) + + const [list, total] = await Promise.all([ + prisma.operationLog.findMany({ + where, + skip, + take: Number(pageSize), + orderBy: { created_at: 'desc' }, + include: { + user: { + select: { + id: true, + username: true, + name: true + } + } + } + }), + prisma.operationLog.count({ where }) + ]) + + successResponse(res, { + list, + total, + page: Number(page), + pageSize: Number(pageSize) + }, '获取成功') + } catch (error: any) { + errorResponse(res, error.message || '获取操作日志失败') + } +} + +// 获取操作类型列表 +export const getOperationTypes = async (req: Request, res: Response) => { + try { + const types = await prisma.operationLog.findMany({ + select: { operation_type: true }, + distinct: ['operation_type'] + }) + + const typeList = types.map(t => t.operation_type) + successResponse(res, typeList, '获取成功') + } catch (error: any) { + errorResponse(res, error.message || '获取操作类型失败') + } +} + +// 记录操作日志(工具函数) +export const logOperation = async ( + userId: number | null, + username: string, + operationType: string, + details: string, + equipmentId?: number, + equipmentCode?: string, + ipAddress?: string +) => { + try { + await prisma.operationLog.create({ + data: { + user_id: userId, + username, + operation_type: operationType, + equipment_id: equipmentId || null, + equipment_code: equipmentCode || null, + operation_details: details, + ip_address: ipAddress || null + } + }) + } catch (error) { + console.error('记录操作日志失败:', error) + } +} diff --git a/backend/src/controllers/personController.ts b/backend/src/controllers/personController.ts new file mode 100644 index 0000000..1b53ca7 --- /dev/null +++ b/backend/src/controllers/personController.ts @@ -0,0 +1,255 @@ +import { Request, Response } from 'express' +import prisma from '../utils/prisma' +import { successResponse, errorResponse } from '../utils/response' + +const sanitizePerson = (person: any) => ({ + id: person.id, + nickname: person.nickname, + real_name: person.real_name, + id_number: person.id_number, + department: person.department, + position: person.position, + phone: person.phone, + email: person.email, + status: person.status, + remarks: person.remarks, + created_at: person.created_at, + updated_at: person.updated_at +}) + +export const getPersonList = async (req: Request, res: Response) => { + try { + const { page = 1, pageSize = 10, keyword, status } = req.query + + const where: any = {} + + if (keyword) { + const kw = keyword as string + where.OR = [ + { nickname: { contains: kw } }, + { real_name: { contains: kw } }, + { id_number: { contains: kw } } + ] + } + + if (status) { + where.status = status + } + + const skip = (Number(page) - 1) * Number(pageSize) + + const [list, total] = await Promise.all([ + prisma.person.findMany({ + where, + skip, + take: Number(pageSize), + orderBy: { created_at: 'desc' } + }), + prisma.person.count({ where }) + ]) + + successResponse( + res, + { + list: list.map(sanitizePerson), + total, + page: Number(page), + pageSize: Number(pageSize) + }, + '获取成功' + ) + } catch (error: any) { + errorResponse(res, error.message || '获取使用者列表失败') + } +} + +export const getPersonDetail = async (req: Request, res: Response) => { + try { + const { id } = req.params + const person = await prisma.person.findUnique({ where: { id: Number(id) } }) + if (!person) { + return errorResponse(res, '使用者不存在', 404) + } + successResponse(res, sanitizePerson(person), '获取成功') + } catch (error: any) { + errorResponse(res, error.message || '获取使用者详情失败') + } +} + +export const createPerson = async (req: Request, res: Response) => { + try { + const { + nickname, + real_name, + id_number, + department, + position, + phone, + email, + status = 'active', + remarks + } = req.body as Record + + if (!nickname || !real_name) { + return errorResponse(res, '花名与真实姓名为必填项', 422) + } + + if (id_number) { + const exists = await prisma.person.findUnique({ where: { id_number } }) + if (exists) { + return errorResponse(res, '证件编号已存在', 409) + } + } + + const created = await prisma.person.create({ + data: { + nickname, + real_name, + id_number, + department, + position, + phone, + email, + status, + remarks + } + }) + + successResponse(res, sanitizePerson(created), '创建成功') + } catch (error: any) { + errorResponse(res, error.message || '创建使用者失败') + } +} + +export const updatePerson = async (req: Request, res: Response) => { + try { + const { id } = req.params + const { + nickname, + real_name, + id_number, + department, + position, + phone, + email, + status, + remarks + } = req.body as Record + + const person = await prisma.person.findUnique({ where: { id: Number(id) } }) + if (!person) { + return errorResponse(res, '使用者不存在', 404) + } + + if (id_number && id_number !== person.id_number) { + const exists = await prisma.person.findUnique({ where: { id_number } }) + if (exists) { + return errorResponse(res, '证件编号已存在', 409) + } + } + + const updated = await prisma.person.update({ + where: { id: Number(id) }, + data: { + nickname: nickname ?? person.nickname, + real_name: real_name ?? person.real_name, + id_number: id_number ?? person.id_number, + department, + position, + phone, + email, + status: status ?? person.status, + remarks + } + }) + + successResponse(res, sanitizePerson(updated), '更新成功') + } catch (error: any) { + errorResponse(res, error.message || '更新使用者失败') + } +} + +export const updatePersonStatus = async (req: Request, res: Response) => { + try { + const { id } = req.params + const { status } = req.body as { status?: string } + + if (!status) { + return errorResponse(res, '状态不能为空', 422) + } + + const allowed = ['active', 'inactive'] + if (!allowed.includes(status)) { + return errorResponse(res, '状态值无效', 422) + } + + const person = await prisma.person.findUnique({ where: { id: Number(id) } }) + if (!person) { + return errorResponse(res, '使用者不存在', 404) + } + + const updated = await prisma.person.update({ + where: { id: Number(id) }, + data: { status } + }) + + successResponse(res, sanitizePerson(updated), '状态更新成功') + } catch (error: any) { + errorResponse(res, error.message || '更新使用者状态失败') + } +} + + +export const deletePerson = async (req: Request, res: Response) => { + try { + const { id } = req.params + const personId = Number(id) + + const person = await prisma.person.findUnique({ where: { id: personId } }) + if (!person) { + return errorResponse(res, '使用者不存在', 404) + } + + // 检查是否有未归还的借用记录 + const activeBorrowCount = await prisma.borrowRecord.count({ + where: { + person_id: personId, + status: '已借出' + } + }) + + if (activeBorrowCount > 0) { + return errorResponse(res, '该使用者存在未归还的借用记录,无法删除') + } + + // 删除历史借用记录 + await prisma.borrowRecord.deleteMany({ + where: { person_id: personId } + }) + + await prisma.person.delete({ where: { id: personId } }) + + successResponse(res, null, '删除成功') + } catch (error: any) { + errorResponse(res, error.message || '删除使用者失败') + } +} + +// 获取使用者选项列表(用于下拉选择) +export const getPersonOptions = async (_req: Request, res: Response) => { + try { + const persons = await prisma.person.findMany({ + where: { status: 'active' }, + orderBy: { nickname: 'asc' }, + select: { + id: true, + nickname: true, + real_name: true, + department: true + } + }) + successResponse(res, persons, '获取成功') + } catch (error: any) { + errorResponse(res, error.message || '获取使用者选项失败') + } +} diff --git a/backend/src/controllers/searchController.ts b/backend/src/controllers/searchController.ts new file mode 100644 index 0000000..f9cf7ce --- /dev/null +++ b/backend/src/controllers/searchController.ts @@ -0,0 +1,49 @@ +import { Request, Response } from 'express' +import prisma from '../utils/prisma' +import { successResponse, errorResponse } from '../utils/response' + +// 全局搜索 +export const globalSearch = async (req: Request, res: Response) => { + try { + const { keyword } = req.query + + if (!keyword || String(keyword).trim().length === 0) { + return successResponse(res, [], '请输入搜索关键词') + } + + const kw = String(keyword).trim() + + const equipments = await prisma.equipment.findMany({ + where: { + OR: [ + { equipment_code: { contains: kw } }, + { equipment_name: { contains: kw } }, + { equipment_model: { contains: kw } } + ] + }, + select: { + id: true, + equipment_code: true, + equipment_name: true, + equipment_type: true, + status: true, + assetType: { select: { name: true } } + }, + take: 10, + orderBy: { updated_at: 'desc' } + }) + + const results = equipments.map(eq => ({ + id: eq.id, + code: eq.equipment_code, + name: eq.equipment_name, + type: eq.assetType?.name || eq.equipment_type, + status: eq.status, + category: 'equipment' + })) + + successResponse(res, results, '搜索成功') + } catch (error: any) { + errorResponse(res, error.message || '搜索失败') + } +} diff --git a/backend/src/controllers/settingsController.ts b/backend/src/controllers/settingsController.ts new file mode 100644 index 0000000..337b9b7 --- /dev/null +++ b/backend/src/controllers/settingsController.ts @@ -0,0 +1,108 @@ +import type { Request, Response } from 'express' +import prisma from '../utils/prisma' +import { successResponse, errorResponse } from '../utils/response' + +const DEFAULT_SITE_NAME = '摄影资产借还系统' +const DEFAULT_THEME_COLOR = '#409EFF' +const DEFAULT_LOGO_URL = 'https://s3.1080p.hk/shuzhou/2025/05/18/8ebadf0a7cc34eaea00e8619b8efb827.png' + +const mapConfigResponse = (config: any) => { + if (!config) { + return { + site_name: DEFAULT_SITE_NAME, + logo_url: DEFAULT_LOGO_URL, + favicon_url: '', + theme_color: DEFAULT_THEME_COLOR, + updated_at: null, + updated_by: null + } + } + + return { + site_name: config.site_name || DEFAULT_SITE_NAME, + logo_url: DEFAULT_LOGO_URL, + favicon_url: '', + theme_color: config.theme_color || DEFAULT_THEME_COLOR, + updated_at: config.updated_at, + updated_by: config.updatedBy + ? { + id: config.updatedBy.id, + username: config.updatedBy.username, + name: config.updatedBy.name + } + : null + } +} + +export const getSiteConfig = async (req: Request, res: Response) => { + try { + const config = await prisma.siteConfig.findFirst({ + orderBy: { updated_at: 'desc' }, + include: { + updatedBy: { + select: { + id: true, + username: true, + name: true + } + } + } + }) + + successResponse(res, mapConfigResponse(config), '获取成功') + } catch (error: any) { + errorResponse(res, error.message || '获取站点配置失败') + } +} + +export const updateSiteConfig = async (req: Request, res: Response) => { + try { + const { site_name, theme_color } = req.body as { site_name?: string; theme_color?: string } + + if (!site_name) { + return errorResponse(res, '网站名称不能为空', 422) + } + + const existing = await prisma.siteConfig.findFirst({ + orderBy: { updated_at: 'desc' } + }) + + const data: any = { + site_name: site_name.trim(), + theme_color: theme_color || DEFAULT_THEME_COLOR, + updated_at: new Date(), + updated_by: res.locals.user?.id ?? null, + logo_url: DEFAULT_LOGO_URL, + favicon_url: null + } + + const result = existing + ? await prisma.siteConfig.update({ + where: { id: existing.id }, + data + }) + : await prisma.siteConfig.create({ + data: { + site_name: data.site_name, + theme_color: data.theme_color, + logo_url: data.logo_url, + favicon_url: data.favicon_url, + updated_by: data.updated_by + } + }) + + const response = await prisma.siteConfig.findUnique({ + where: { id: result.id }, + include: { + updatedBy: { + select: { id: true, username: true, name: true } + } + } + }) + + successResponse(res, mapConfigResponse(response), '保存成功') + } catch (error: any) { + errorResponse(res, error.message || '更新站点配置失败') + } +} + diff --git a/backend/src/controllers/statisticsController.ts b/backend/src/controllers/statisticsController.ts new file mode 100644 index 0000000..1f4c7bb --- /dev/null +++ b/backend/src/controllers/statisticsController.ts @@ -0,0 +1,202 @@ +import { Request, Response } from 'express' +import prisma from '../utils/prisma' +import { successResponse, errorResponse } from '../utils/response' + +// 获取首页统计数据 +export const getDashboardStats = async (req: Request, res: Response) => { + try { + const startOfToday = new Date(new Date().setHours(0, 0, 0, 0)) + + const [equipmentList, todayBorrowCount, todayReturnCount] = await Promise.all([ + prisma.equipment.findMany(), + prisma.borrowRecord.count({ + where: { + borrow_time: { + gte: startOfToday + } + } + }), + prisma.borrowRecord.count({ + where: { + return_time: { + gte: startOfToday + } + } + }) + ]) + + const totalEquipmentQuantity = equipmentList.reduce((sum, eq) => sum + (eq.total_quantity || 0), 0) + const availableEquipmentQuantity = equipmentList.reduce( + (sum, eq) => sum + (eq.remaining_quantity || 0), + 0 + ) + const borrowedEquipmentQuantity = equipmentList.reduce( + (sum, eq) => sum + Math.max((eq.total_quantity || 0) - (eq.remaining_quantity || 0), 0), + 0 + ) + const maintenanceEquipmentQuantity = equipmentList + .filter((eq) => eq.status === '维护中') + .reduce((sum, eq) => sum + (eq.total_quantity || 0), 0) + + const inventoryByTypeMap = equipmentList.reduce((acc, eq) => { + const type = eq.equipment_type || '其他' + if (!acc[type]) { + acc[type] = { + equipment_type: type, + total_quantity: 0, + available_quantity: 0, + borrowed_quantity: 0 + } + } + const availableQuantity = eq.remaining_quantity || 0 + const totalQuantity = eq.total_quantity || 0 + acc[type].total_quantity += totalQuantity + acc[type].available_quantity += availableQuantity + acc[type].borrowed_quantity += Math.max(totalQuantity - availableQuantity, 0) + return acc + }, {} as Record< + string, + { + equipment_type: string + total_quantity: number + available_quantity: number + borrowed_quantity: number + } + >) + + const inventoryByType = Object.values(inventoryByTypeMap).filter( + (item) => item.total_quantity > 0 + ) + + const totalValue = equipmentList.reduce((sum, eq) => sum + Number(eq.total_value), 0) + const borrowedValue = equipmentList.reduce((sum, eq) => sum + Number(eq.borrowed_value), 0) + const availableValue = equipmentList.reduce((sum, eq) => sum + Number(eq.available_value), 0) + const maintenanceValue = equipmentList + .filter(eq => eq.status === '维护中') + .reduce((sum, eq) => sum + Number(eq.available_value), 0) + + // 获取今日借用/归还价值 + const todayBorrowRecords = await prisma.borrowRecord.findMany({ + where: { + borrow_time: { + gte: startOfToday + } + }, + include: { + equipment: true + } + }) + + const todayBorrowValue = todayBorrowRecords.reduce( + (sum, record) => sum + Number(record.equipment.unit_price), + 0 + ) + + const todayReturnRecords = await prisma.borrowRecord.findMany({ + where: { + return_time: { + gte: startOfToday + } + }, + include: { + equipment: true + } + }) + + const todayReturnValue = todayReturnRecords.reduce( + (sum, record) => sum + Number(record.equipment.unit_price), + 0 + ) + + const usageRate = + totalEquipmentQuantity > 0 ? (borrowedEquipmentQuantity / totalEquipmentQuantity) * 100 : 0 + const valueUsageRate = totalValue > 0 ? (borrowedValue / totalValue) * 100 : 0 + + successResponse(res, { + total_equipment: totalEquipmentQuantity, + available_equipment: availableEquipmentQuantity, + borrowed_equipment: borrowedEquipmentQuantity, + maintenance_equipment: maintenanceEquipmentQuantity, + usage_rate: usageRate, + today_borrow: todayBorrowCount, + today_return: todayReturnCount, + total_value: totalValue, + borrowed_value: borrowedValue, + available_value: availableValue, + maintenance_value: maintenanceValue, + value_usage_rate: valueUsageRate, + today_borrow_value: todayBorrowValue, + today_return_value: todayReturnValue, + inventory_by_type: inventoryByType + }, '获取成功') + } catch (error: any) { + errorResponse(res, error.message || '获取统计数据失败') + } +} + +// 获取资产价值统计 +export const getValueStatistics = async (req: Request, res: Response) => { + try { + const { equipment_type, start_date, end_date } = req.query + + const where: any = {} + + if (equipment_type) { + where.equipment_type = equipment_type + } + + if (start_date || end_date) { + where.created_at = {} + if (start_date) { + where.created_at.gte = new Date(start_date as string) + } + if (end_date) { + where.created_at.lte = new Date(end_date as string) + } + } + + const equipmentList = await prisma.equipment.findMany({ where }) + + const totalValue = equipmentList.reduce((sum, eq) => sum + Number(eq.total_value), 0) + const borrowedValue = equipmentList.reduce((sum, eq) => sum + Number(eq.borrowed_value), 0) + const availableValue = equipmentList.reduce((sum, eq) => sum + Number(eq.available_value), 0) + const maintenanceValue = equipmentList + .filter(eq => eq.status === '维护中') + .reduce((sum, eq) => sum + Number(eq.available_value), 0) + + // 按资产类型统计 + const valueByType = ['相机', '镜头', '三脚架', '闪光灯', '摄影配饰品'].map(type => { + const typeEquipment = equipmentList.filter(eq => eq.equipment_type === type) + return { + equipment_type: type, + total_value: typeEquipment.reduce((sum, eq) => sum + Number(eq.total_value), 0), + borrowed_value: typeEquipment.reduce((sum, eq) => sum + Number(eq.borrowed_value), 0), + available_value: typeEquipment.reduce((sum, eq) => sum + Number(eq.available_value), 0) + } + }).filter(item => item.total_value > 0) + + // 价值最高的资产(Top 10) + const topEquipment = equipmentList + .map(eq => ({ + equipment_name: eq.equipment_name, + unit_price: Number(eq.unit_price), + total_value: Number(eq.total_value), + borrowed_value: Number(eq.borrowed_value), + available_value: Number(eq.available_value) + })) + .sort((a, b) => b.total_value - a.total_value) + .slice(0, 10) + + successResponse(res, { + total_value: totalValue, + borrowed_value: borrowedValue, + available_value: availableValue, + maintenance_value: maintenanceValue, + value_by_type: valueByType, + top_equipment: topEquipment + }, '获取成功') + } catch (error: any) { + errorResponse(res, error.message || '获取价值统计失败') + } +} + diff --git a/backend/src/controllers/storageLocationController.ts b/backend/src/controllers/storageLocationController.ts new file mode 100644 index 0000000..cc2f4fb --- /dev/null +++ b/backend/src/controllers/storageLocationController.ts @@ -0,0 +1,166 @@ +import { Request, Response } from 'express' +import prisma from '../utils/prisma' +import { successResponse, errorResponse } from '../utils/response' + +const baseSelect = { + id: true, + name: true, + code: true, + status: true, + description: true, + created_at: true, + updated_at: true +} + +export const getStorageLocationOptions = async (_req: Request, res: Response) => { + try { + const locations = await prisma.storageLocation.findMany({ + where: { status: 'active' }, + orderBy: { name: 'asc' }, + select: baseSelect + }) + + successResponse(res, locations, '获取成功') + } catch (error: any) { + errorResponse(res, error.message || '获取地址列表失败') + } +} + +export const getStorageLocationList = async (req: Request, res: Response) => { + try { + const { page = 1, pageSize = 10, keyword, status } = req.query + + const where: any = {} + if (status) { + where.status = status + } + if (keyword) { + where.OR = [ + { name: { contains: keyword as string } }, + { code: { contains: keyword as string } }, + { description: { contains: keyword as string } } + ] + } + + const skip = (Number(page) - 1) * Number(pageSize) + + const [list, total] = await Promise.all([ + prisma.storageLocation.findMany({ + where, + skip, + take: Number(pageSize), + orderBy: { updated_at: 'desc' }, + select: { + ...baseSelect, + _count: { + select: { equipments: true } + } + } + }), + prisma.storageLocation.count({ where }) + ]) + + successResponse( + res, + { + list, + total, + page: Number(page), + pageSize: Number(pageSize) + }, + '获取成功' + ) + } catch (error: any) { + errorResponse(res, error.message || '获取地址列表失败') + } +} + +export const createStorageLocation = async (req: Request, res: Response) => { + try { + const { name, code, description } = req.body as { + name?: string + code?: string + description?: string + } + + if (!name || !name.trim()) { + return errorResponse(res, '地址名称不能为空', 422) + } + + const location = await prisma.storageLocation.create({ + data: { + name: name.trim(), + code: code?.trim() || null, + description: description?.trim() || null + }, + select: baseSelect + }) + + successResponse(res, location, '新增地址成功') + } catch (error: any) { + if (error.code === 'P2002') { + return errorResponse(res, '地址编码已存在') + } + errorResponse(res, error.message || '新增地址失败') + } +} + +export const updateStorageLocation = async (req: Request, res: Response) => { + try { + const { id } = req.params + const { name, code, description, status } = req.body as { + name?: string + code?: string + description?: string + status?: string + } + + const existing = await prisma.storageLocation.findUnique({ + where: { id: Number(id) } + }) + + if (!existing) { + return errorResponse(res, '地址不存在', 404) + } + + const updated = await prisma.storageLocation.update({ + where: { id: Number(id) }, + data: { + name: name?.trim() || existing.name, + code: code?.trim() || null, + description: description?.trim() || null, + status: status || existing.status + }, + select: baseSelect + }) + + successResponse(res, updated, '更新地址成功') + } catch (error: any) { + if (error.code === 'P2002') { + return errorResponse(res, '地址编码已存在') + } + errorResponse(res, error.message || '更新地址失败') + } +} + +export const updateStorageLocationStatus = async (req: Request, res: Response) => { + try { + const { id } = req.params + const { status } = req.body as { status?: string } + + if (!status) { + return errorResponse(res, '状态不能为空', 422) + } + + const location = await prisma.storageLocation.update({ + where: { id: Number(id) }, + data: { status }, + select: baseSelect + }) + + successResponse(res, location, '更新状态成功') + } catch (error: any) { + errorResponse(res, error.message || '更新状态失败') + } +} + diff --git a/backend/src/controllers/userController.ts b/backend/src/controllers/userController.ts new file mode 100644 index 0000000..f3aec3f --- /dev/null +++ b/backend/src/controllers/userController.ts @@ -0,0 +1,225 @@ +import { Request, Response } from 'express' +import bcrypt from 'bcryptjs' +import prisma from '../utils/prisma' +import { successResponse, errorResponse } from '../utils/response' + +const SALT_ROUNDS = 10 + +const sanitizeUser = (user: any) => ({ + id: user.id, + username: user.username, + name: user.name, + email: user.email, + phone: user.phone, + role: user.role, + roles: [user.role], + status: user.status, + last_login_at: user.last_login_at, + created_at: user.created_at, + updated_at: user.updated_at +}) + +export const getUserList = async (req: Request, res: Response) => { + try { + const { page = 1, pageSize = 10, keyword, role, status } = req.query + + const where: any = {} + + if (keyword) { + where.OR = [ + { username: { contains: keyword as string } }, + { name: { contains: keyword as string } }, + { email: { contains: keyword as string } } + ] + } + + if (role) { + where.role = role + } + + if (status) { + where.status = status + } + + const skip = (Number(page) - 1) * Number(pageSize) + + const [list, total] = await Promise.all([ + prisma.user.findMany({ + where, + skip, + take: Number(pageSize), + orderBy: { created_at: 'desc' } + }), + prisma.user.count({ where }) + ]) + + successResponse( + res, + { + list: list.map(sanitizeUser), + total, + page: Number(page), + pageSize: Number(pageSize) + }, + '获取成功' + ) + } catch (error: any) { + errorResponse(res, error.message || '获取用户列表失败') + } +} + +export const createUser = async (req: Request, res: Response) => { + try { + const { username, password, name, email, phone, roles, role, status } = req.body as Record< + string, + any + > + + if (!username || !password || !name) { + return errorResponse(res, '用户名、姓名和密码为必填项', 422) + } + + const existing = await prisma.user.findUnique({ where: { username } }) + if (existing) { + return errorResponse(res, '用户名已存在', 409) + } + + const primaryRole = + (Array.isArray(roles) ? roles[0] : roles) || role || (username === 'admin' ? 'admin' : 'user') + + const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS) + + const created = await prisma.user.create({ + data: { + username, + password: hashedPassword, + name, + email, + phone, + role: primaryRole, + status: status || 'active' + } + }) + + successResponse(res, sanitizeUser(created), '创建成功') + } catch (error: any) { + errorResponse(res, error.message || '创建用户失败') + } +} + +export const updateUser = async (req: Request, res: Response) => { + try { + const { id } = req.params + const { name, email, phone, roles, role, status } = req.body as Record + + const user = await prisma.user.findUnique({ where: { id: Number(id) } }) + if (!user) { + return errorResponse(res, '用户不存在', 404) + } + + const updateData: any = { + name: name ?? user.name, + email: email ?? user.email, + phone: phone ?? user.phone + } + + const newRole = (Array.isArray(roles) ? roles[0] : roles) || role + if (newRole) { + updateData.role = newRole + } + + if (status) { + updateData.status = status + } + + const updated = await prisma.user.update({ + where: { id: Number(id) }, + data: updateData + }) + + successResponse(res, sanitizeUser(updated), '更新成功') + } catch (error: any) { + errorResponse(res, error.message || '更新用户失败') + } +} + +export const resetPassword = async (req: Request, res: Response) => { + try { + const { id } = req.params + const { new_password } = req.body as { new_password?: string } + + if (!new_password) { + return errorResponse(res, '新密码不能为空', 422) + } + + const user = await prisma.user.findUnique({ where: { id: Number(id) } }) + if (!user) { + return errorResponse(res, '用户不存在', 404) + } + + const hashedPassword = await bcrypt.hash(new_password, SALT_ROUNDS) + + await prisma.user.update({ + where: { id: Number(id) }, + data: { password: hashedPassword } + }) + + successResponse(res, null, '密码重置成功') + } catch (error: any) { + errorResponse(res, error.message || '重置密码失败') + } +} + +export const updateUserStatus = async (req: Request, res: Response) => { + try { + const { id } = req.params + const { status } = req.body as { status?: string } + + if (!status) { + return errorResponse(res, '状态不能为空', 422) + } + + const user = await prisma.user.findUnique({ where: { id: Number(id) } }) + if (!user) { + return errorResponse(res, '用户不存在', 404) + } + + const updated = await prisma.user.update({ + where: { id: Number(id) }, + data: { status } + }) + + successResponse(res, sanitizeUser(updated), '状态更新成功') + } catch (error: any) { + errorResponse(res, error.message || '更新用户状态失败') + } +} + + +export const deleteUser = async (req: Request, res: Response) => { + try { + const { id } = req.params + const userId = Number(id) + + const user = await prisma.user.findUnique({ where: { id: userId } }) + if (!user) { + return errorResponse(res, '用户不存在', 404) + } + + // 不允许删除 admin 用户 + if (user.username === 'admin') { + return errorResponse(res, '不能删除管理员账户') + } + + // 删除关联的操作日志 + await prisma.operationLog.deleteMany({ + where: { user_id: userId } + }) + + await prisma.user.delete({ where: { id: userId } }) + + successResponse(res, null, '删除成功') + } catch (error: any) { + errorResponse(res, error.message || '删除用户失败') + } +} diff --git a/backend/src/index.ts b/backend/src/index.ts new file mode 100644 index 0000000..79aba25 --- /dev/null +++ b/backend/src/index.ts @@ -0,0 +1,98 @@ +import path from 'path' +import express from 'express' +import cors from 'cors' +import dotenv from 'dotenv' +import session from 'express-session' +import equipmentRoutes from './routes/equipment' +import { scanEquipment } from './controllers/equipmentController' +import borrowRoutes, { returnRouter } from './routes/borrow' +import statisticsRoutes from './routes/statistics' +import authRoutes from './routes/auth' +import barcodeRoutes from './routes/barcode' +import settingsRoutes from './routes/settings' +import usersRoutes from './routes/users' +import personRoutes from './routes/persons' +import storageLocationRoutes from './routes/storageLocations' +import assetTypeRoutes from './routes/assetTypes' +import departmentRoutes from './routes/departments' +import operationLogRoutes from './routes/operationLog' +import batchImportRoutes from './routes/batchImport' +import mountRoutes from './routes/mount' +import disposalRoutes from './routes/disposal' +import maintenanceRoutes from './routes/maintenance' +import batchRoutes from './routes/batch' +import exportRoutes from './routes/export' +import searchRoutes from './routes/search' +import { keycloak, memoryStore } from './utils/keycloak' +import { protect, requireAdmin } from './middleware/auth' + +dotenv.config() + +const app = express() +const PORT = process.env.PORT || 3000 +const SESSION_SECRET = process.env.SESSION_SECRET || 'equipment-management-secret' + +// Middleware +app.use(cors({ origin: true, credentials: true })) +app.use(express.json()) +app.use(express.urlencoded({ extended: true })) +app.use( + session({ + secret: SESSION_SECRET, + resave: false, + saveUninitialized: false, + store: memoryStore, + cookie: { + secure: false, + maxAge: 1000 * 60 * 60 // 1 hour + } + }) +) +if (process.env.AUTH_MODE !== 'local') { +app.use(keycloak.middleware({ logout: '/api/auth/logout', admin: '/keycloak' })) +} + +app.use('/uploads', express.static(path.resolve(__dirname, '..', 'uploads'))) + +// Routes +app.get('/api/equipment/scan/:equipment_code', protect(), scanEquipment) +app.use('/api/equipment', protect(), requireAdmin(), equipmentRoutes) +app.use('/api/borrow', protect(), borrowRoutes) +app.use('/api/return', protect(), returnRouter) +app.use('/api/statistics', protect(), statisticsRoutes) +app.use('/api/barcode', protect(), barcodeRoutes) +app.use('/api/auth', authRoutes) +app.use('/api/settings', protect(), requireAdmin(), settingsRoutes) +app.use('/api/users', protect(), requireAdmin(), usersRoutes) +app.use('/api/persons', personRoutes) +app.use('/api/storage-locations', storageLocationRoutes) +app.use('/api/asset-types', assetTypeRoutes) +app.use('/api/departments', departmentRoutes) +app.use('/api/operation-logs', operationLogRoutes) +app.use('/api/batch-import', batchImportRoutes) +app.use('/api/mount', protect(), mountRoutes) +app.use('/api/disposal', protect(), disposalRoutes) +app.use('/api/maintenance', protect(), maintenanceRoutes) +app.use('/api/batch', protect(), batchRoutes) +app.use('/api/export', protect(), exportRoutes) +app.use('/api/search', protect(), searchRoutes) + +// Health check +app.get('/api/health', (req, res) => { + res.json({ status: 'ok', message: 'Server is running' }) +}) + +// Error handling middleware +app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => { + console.error('Error:', err) + res.status(err.status || 500).json({ + code: err.status || 500, + message: err.message || 'Internal Server Error', + data: null + }) +}) + +app.listen(PORT, () => { + console.log(`Server is running on http://localhost:${PORT}`) +}) + diff --git a/backend/src/middleware/auth.ts b/backend/src/middleware/auth.ts new file mode 100644 index 0000000..4a9de4d --- /dev/null +++ b/backend/src/middleware/auth.ts @@ -0,0 +1,58 @@ +import type { Request, Response, NextFunction, RequestHandler } from 'express' +import { keycloak } from '../utils/keycloak' + +const isLocalAuth = () => process.env.AUTH_MODE === 'local' + +// 选择保护策略:local 使用会话校验;默认使用 Keycloak 保护 +export function protect(): RequestHandler { + if (isLocalAuth()) { + return function localSessionProtect(req: Request, res: Response, next: NextFunction) { + const sessionUser = (req as any).session?.user + if (sessionUser) { + res.locals.user = sessionUser + return next() + } + return res.status(401).json({ code: 401, message: '未登录', data: null }) + } + } + + const keycloakMiddleware = keycloak.protect() + + return function keycloakWrapper(req: Request, res: Response, next: NextFunction) { + keycloakMiddleware(req, res, (err?: any) => { + if (err) { + return next(err) + } + const tokenContent = (req as any).kauth?.grant?.access_token?.content as Record | undefined + if (tokenContent) { + res.locals.user = { + id: tokenContent.sub, + username: tokenContent.preferred_username, + name: tokenContent.name || tokenContent.preferred_username, + roles: tokenContent.realm_access?.roles || [] + } + } + next() + }) + } +} + +export function requireAdmin(): RequestHandler { + return function adminGuard(req: Request, res: Response, next: NextFunction) { + if (isLocalAuth()) { + const sessionUser = (req as any).session?.user + if (sessionUser && (sessionUser.role === 'admin' || sessionUser.roles?.includes('admin'))) { + res.locals.user = sessionUser + return next() + } + return res.status(403).json({ code: 403, message: '权限不足', data: null }) + } + + const roles: string[] = + ((req as any).kauth?.grant?.access_token?.content?.realm_access?.roles as string[]) || [] + if (roles.includes('admin')) { + return next() + } + return res.status(403).json({ code: 403, message: '权限不足', data: null }) + } +} diff --git a/backend/src/routes/assetTypes.ts b/backend/src/routes/assetTypes.ts new file mode 100644 index 0000000..3144e6b --- /dev/null +++ b/backend/src/routes/assetTypes.ts @@ -0,0 +1,19 @@ +import { Router } from 'express' +import { + getAssetTypeOptions, + getAssetTypeList, + createAssetType, + updateAssetType, + deleteAssetType +} from '../controllers/assetTypeController' +import { protect, requireAdmin } from '../middleware/auth' + +const router = Router() + +router.get('/options', protect(), getAssetTypeOptions) +router.get('/list', protect(), requireAdmin(), getAssetTypeList) +router.post('/', protect(), requireAdmin(), createAssetType) +router.put('/:id', protect(), requireAdmin(), updateAssetType) +router.delete('/:id', protect(), requireAdmin(), deleteAssetType) + +export default router diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts new file mode 100644 index 0000000..85e0709 --- /dev/null +++ b/backend/src/routes/auth.ts @@ -0,0 +1,117 @@ +import { Router } from 'express' +import { successResponse, errorResponse } from '../utils/response' +import { keycloak } from '../utils/keycloak' +import type { Request, Response } from 'express' +import bcrypt from 'bcryptjs' +import prisma from '../utils/prisma' + +const router = Router() + +// 获取当前用户信息 +router.get('/user', (req, res) => { + if (process.env.AUTH_MODE === 'local') { + const user = (req as any).session?.user + if (!user) { + return errorResponse(res, '未登录', 401) + } + return successResponse(res, user, '获取成功') + } + const grant = (req as any).kauth?.grant + const token = grant?.access_token + + if (!token) { + return errorResponse(res, '无法获取用户信息', 401) + } + + const tokenContent = token.content as Record + + successResponse(res, { + id: tokenContent.sub, + username: tokenContent.preferred_username, + name: tokenContent.name || tokenContent.preferred_username, + email: tokenContent.email, + roles: tokenContent.realm_access?.roles || [] + }, '获取成功') +}) + +// Keycloak 配置信息(前端使用) +router.get('/config', (req, res) => { + successResponse(res, { + url: process.env.KEYCLOAK_URL || 'http://localhost:8080', + realm: process.env.KEYCLOAK_REALM || 'equipment-management', + clientId: process.env.KEYCLOAK_CLIENT_ID || 'equipment-client' + }, '获取成功') +}) + +// 退出登录 +router.post('/logout', async (req, res) => { + try { + if (process.env.AUTH_MODE === 'local') { + await new Promise((resolve, reject) => { + req.session?.destroy((err) => { + if (err) { + reject(err) + return + } + resolve() + }) + }) + return successResponse(res, null, '退出成功') + } + // Keycloak 模式下仅返回成功,由前端触发 keycloak.logout() + return successResponse(res, null, '退出成功') + } catch (error: any) { + errorResponse(res, error?.message || '退出失败') + } +}) + +// 本地登录模式(不跳转 Keycloak) +router.post('/local/login', async (req: Request, res: Response) => { + if (process.env.AUTH_MODE !== 'local') { + return errorResponse(res, '本地登录未启用', 400) + } + const { username, password } = req.body as { username?: string; password?: string } + if (!username || !password) { + return errorResponse(res, '用户名与密码必填', 422) + } + + const user = await prisma.user.findUnique({ where: { username } }) + if (!user || user.status !== 'active') { + return errorResponse(res, '用户名或密码错误', 401) + } + + const validPassword = await bcrypt.compare(password, user.password) + if (!validPassword) { + return errorResponse(res, '用户名或密码错误', 401) + } + + await prisma.user.update({ + where: { id: user.id }, + data: { last_login_at: new Date() } + }) + + const userPayload = { + id: user.id, + username: user.username, + name: user.name, + email: user.email, + phone: user.phone, + role: user.role, + roles: [user.role], + status: user.status + } + + ;(req as any).session.user = userPayload + + return successResponse( + res, + { + token: 'session', + user: userPayload + }, + '登录成功' + ) +}) + +export default router + diff --git a/backend/src/routes/barcode.ts b/backend/src/routes/barcode.ts new file mode 100644 index 0000000..fd3e060 --- /dev/null +++ b/backend/src/routes/barcode.ts @@ -0,0 +1,13 @@ +import { Router } from 'express' +import { + getEquipmentForBarcode, + getEquipmentBatchForBarcode +} from '../controllers/barcodeController' + +const router = Router() + +router.get('/:equipment_code', getEquipmentForBarcode) +router.post('/batch', getEquipmentBatchForBarcode) + +export default router + diff --git a/backend/src/routes/batch.ts b/backend/src/routes/batch.ts new file mode 100644 index 0000000..5c2e9cb --- /dev/null +++ b/backend/src/routes/batch.ts @@ -0,0 +1,10 @@ +import { Router } from 'express' +import { batchBorrow, batchReturn, batchTransfer } from '../controllers/batchController' + +const router = Router() + +router.post('/borrow', batchBorrow) +router.post('/return', batchReturn) +router.post('/transfer', batchTransfer) + +export default router diff --git a/backend/src/routes/batchImport.ts b/backend/src/routes/batchImport.ts new file mode 100644 index 0000000..d2ba70f --- /dev/null +++ b/backend/src/routes/batchImport.ts @@ -0,0 +1,13 @@ +import { Router } from 'express' +import { batchImportPersons, batchImportEquipments } from '../controllers/batchImportController' +import { protect, requireAdmin } from '../middleware/auth' + +const router = Router() + +router.use(protect()) +router.use(requireAdmin()) + +router.post('/persons', batchImportPersons) +router.post('/equipments', batchImportEquipments) + +export default router diff --git a/backend/src/routes/borrow.ts b/backend/src/routes/borrow.ts new file mode 100644 index 0000000..8309c7e --- /dev/null +++ b/backend/src/routes/borrow.ts @@ -0,0 +1,32 @@ +import { Router } from 'express' +import { + borrowEquipment, + scanBorrow, + scanBorrowBatch, + getBorrowRecordByCode, + getBorrowRecordList +} from '../controllers/borrowController' + +const router = Router() + +// 借用相关 +router.post('/', borrowEquipment) +router.post('/scan', scanBorrow) +router.post('/scan-batch', scanBorrowBatch) +router.get('/scan/:equipment_code', getBorrowRecordByCode) +router.get('/records', getBorrowRecordList) + +export default router + +// 归还路由单独导出 +export const returnRouter = Router() + +import { + returnEquipment, + scanReturn, + scanReturnBatch +} from '../controllers/borrowController' + +returnRouter.post('/', returnEquipment) +returnRouter.post('/scan', scanReturn) +returnRouter.post('/scan-batch', scanReturnBatch) diff --git a/backend/src/routes/departments.ts b/backend/src/routes/departments.ts new file mode 100644 index 0000000..484e54b --- /dev/null +++ b/backend/src/routes/departments.ts @@ -0,0 +1,21 @@ +import { Router } from 'express' +import { + getDepartmentOptions, + getDepartmentTree, + getDepartmentList, + createDepartment, + updateDepartment, + deleteDepartment +} from '../controllers/departmentController' +import { protect, requireAdmin } from '../middleware/auth' + +const router = Router() + +router.get('/options', protect(), getDepartmentOptions) +router.get('/tree', protect(), getDepartmentTree) +router.get('/list', protect(), requireAdmin(), getDepartmentList) +router.post('/', protect(), requireAdmin(), createDepartment) +router.put('/:id', protect(), requireAdmin(), updateDepartment) +router.delete('/:id', protect(), requireAdmin(), deleteDepartment) + +export default router diff --git a/backend/src/routes/disposal.ts b/backend/src/routes/disposal.ts new file mode 100644 index 0000000..4fc0ea1 --- /dev/null +++ b/backend/src/routes/disposal.ts @@ -0,0 +1,20 @@ +import { Router } from 'express' +import { + createDisposal, + getDisposalList, + getDisposalDetail, + approveDisposal, + completeDisposal, + getEquipmentDisposalHistory +} from '../controllers/disposalController' + +const router = Router() + +router.post('/', createDisposal) +router.get('/list', getDisposalList) +router.get('/:id', getDisposalDetail) +router.put('/:id/approve', approveDisposal) +router.put('/:id/complete', completeDisposal) +router.get('/equipment/:equipment_id/history', getEquipmentDisposalHistory) + +export default router diff --git a/backend/src/routes/equipment.ts b/backend/src/routes/equipment.ts new file mode 100644 index 0000000..0314a6d --- /dev/null +++ b/backend/src/routes/equipment.ts @@ -0,0 +1,29 @@ +import { Router } from 'express' +import { + getEquipmentList, + getEquipmentDetail, + createEquipment, + updateEquipment, + deleteEquipment, + generateCode, + verifyCode, + scanEquipment +} from '../controllers/equipmentController' +import { requireAdmin } from '../middleware/auth' + +const router = Router() + +router.get('/scan/:equipment_code', scanEquipment) + +router.use(requireAdmin()) + +router.get('/list', getEquipmentList) +router.get('/:id', getEquipmentDetail) +router.post('/', createEquipment) +router.put('/:id', updateEquipment) +router.delete('/:id', deleteEquipment) +router.post('/generate-code', generateCode) +router.post('/verify-code', verifyCode) + +export default router + diff --git a/backend/src/routes/export.ts b/backend/src/routes/export.ts new file mode 100644 index 0000000..a9dbc30 --- /dev/null +++ b/backend/src/routes/export.ts @@ -0,0 +1,11 @@ +import { Router } from 'express' +import { exportEquipment, exportBorrowRecords, generateImportTemplate, generatePersonTemplate } from '../controllers/exportController' + +const router = Router() + +router.get('/equipment', exportEquipment) +router.get('/borrow-records', exportBorrowRecords) +router.get('/template', generateImportTemplate) +router.get('/person-template', generatePersonTemplate) + +export default router diff --git a/backend/src/routes/maintenance.ts b/backend/src/routes/maintenance.ts new file mode 100644 index 0000000..db958f2 --- /dev/null +++ b/backend/src/routes/maintenance.ts @@ -0,0 +1,22 @@ +import { Router } from 'express' +import { + createMaintenance, + getMaintenanceList, + getMaintenanceDetail, + updateMaintenance, + completeMaintenance, + cancelMaintenance, + getEquipmentMaintenanceHistory +} from '../controllers/maintenanceController' + +const router = Router() + +router.post('/', createMaintenance) +router.get('/list', getMaintenanceList) +router.get('/:id', getMaintenanceDetail) +router.put('/:id', updateMaintenance) +router.put('/:id/complete', completeMaintenance) +router.put('/:id/cancel', cancelMaintenance) +router.get('/equipment/:equipment_id/history', getEquipmentMaintenanceHistory) + +export default router diff --git a/backend/src/routes/mount.ts b/backend/src/routes/mount.ts new file mode 100644 index 0000000..b6ad0a5 --- /dev/null +++ b/backend/src/routes/mount.ts @@ -0,0 +1,31 @@ +import { Router } from 'express' +import { + mountChild, + unmountChild, + replaceChild, + getChildren, + getMountHistory, + getAvailableChildren +} from '../controllers/mountController' + +const router = Router() + +// 获取可挂载的子资产列表 +router.get('/available-children', getAvailableChildren) + +// 获取资产的挂载历史 +router.get('/:id/mount-history', getMountHistory) + +// 获取父资产的子资产列表 +router.get('/:parentId/children', getChildren) + +// 挂载子资产到父资产 +router.post('/:parentId/mount', mountChild) + +// 从父资产卸载子资产 +router.post('/:parentId/unmount', unmountChild) + +// 更换配件 +router.post('/:parentId/replace', replaceChild) + +export default router diff --git a/backend/src/routes/operationLog.ts b/backend/src/routes/operationLog.ts new file mode 100644 index 0000000..0429401 --- /dev/null +++ b/backend/src/routes/operationLog.ts @@ -0,0 +1,13 @@ +import { Router } from 'express' +import { getOperationLogList, getOperationTypes } from '../controllers/operationLogController' +import { protect, requireAdmin } from '../middleware/auth' + +const router = Router() + +router.use(protect()) +router.use(requireAdmin()) + +router.get('/list', getOperationLogList) +router.get('/types', getOperationTypes) + +export default router diff --git a/backend/src/routes/persons.ts b/backend/src/routes/persons.ts new file mode 100644 index 0000000..b2c724e --- /dev/null +++ b/backend/src/routes/persons.ts @@ -0,0 +1,27 @@ +import { Router } from 'express' +import { + getPersonList, + getPersonOptions, + createPerson, + updatePerson, + updatePersonStatus, + getPersonDetail, + deletePerson +} from '../controllers/personController' +import { protect, requireAdmin } from '../middleware/auth' + +const router = Router() + +router.use(protect()) + +router.get('/', getPersonList) +router.get('/options', getPersonOptions) +router.get('/:id', getPersonDetail) + +router.post('/', requireAdmin(), createPerson) +router.put('/:id', requireAdmin(), updatePerson) +router.patch('/:id/status', requireAdmin(), updatePersonStatus) +router.delete('/:id', requireAdmin(), deletePerson) + +export default router + diff --git a/backend/src/routes/search.ts b/backend/src/routes/search.ts new file mode 100644 index 0000000..404bfd9 --- /dev/null +++ b/backend/src/routes/search.ts @@ -0,0 +1,8 @@ +import { Router } from 'express' +import { globalSearch } from '../controllers/searchController' + +const router = Router() + +router.get('/', globalSearch) + +export default router diff --git a/backend/src/routes/settings.ts b/backend/src/routes/settings.ts new file mode 100644 index 0000000..07e3e8d --- /dev/null +++ b/backend/src/routes/settings.ts @@ -0,0 +1,17 @@ +import { Router } from 'express' +import { getSiteConfig, updateSiteConfig } from '../controllers/settingsController' +import { protect, requireAdmin } from '../middleware/auth' + +const router = Router() + +router.get('/site', getSiteConfig) + +router.put( + '/site', + protect(), + requireAdmin(), + updateSiteConfig +) + +export default router + diff --git a/backend/src/routes/statistics.ts b/backend/src/routes/statistics.ts new file mode 100644 index 0000000..6c0d80e --- /dev/null +++ b/backend/src/routes/statistics.ts @@ -0,0 +1,13 @@ +import { Router } from 'express' +import { + getDashboardStats, + getValueStatistics +} from '../controllers/statisticsController' + +const router = Router() + +router.get('/dashboard', getDashboardStats) +router.get('/value', getValueStatistics) + +export default router + diff --git a/backend/src/routes/storageLocations.ts b/backend/src/routes/storageLocations.ts new file mode 100644 index 0000000..683b384 --- /dev/null +++ b/backend/src/routes/storageLocations.ts @@ -0,0 +1,20 @@ +import { Router } from 'express' +import { + getStorageLocationOptions, + getStorageLocationList, + createStorageLocation, + updateStorageLocation, + updateStorageLocationStatus +} from '../controllers/storageLocationController' +import { protect, requireAdmin } from '../middleware/auth' + +const router = Router() + +router.get('/options', protect(), getStorageLocationOptions) +router.get('/list', protect(), requireAdmin(), getStorageLocationList) +router.post('/', protect(), requireAdmin(), createStorageLocation) +router.put('/:id', protect(), requireAdmin(), updateStorageLocation) +router.patch('/:id/status', protect(), requireAdmin(), updateStorageLocationStatus) + +export default router + diff --git a/backend/src/routes/users.ts b/backend/src/routes/users.ts new file mode 100644 index 0000000..9c7403e --- /dev/null +++ b/backend/src/routes/users.ts @@ -0,0 +1,21 @@ +import { Router } from 'express' +import { + getUserList, + createUser, + updateUser, + resetPassword, + updateUserStatus, + deleteUser +} from '../controllers/userController' + +const router = Router() + +router.get('/', getUserList) +router.post('/', createUser) +router.put('/:id', updateUser) +router.post('/:id/reset-password', resetPassword) +router.patch('/:id/status', updateUserStatus) +router.delete('/:id', deleteUser) + +export default router + diff --git a/backend/src/types/express-session.d.ts b/backend/src/types/express-session.d.ts new file mode 100644 index 0000000..d0c9978 --- /dev/null +++ b/backend/src/types/express-session.d.ts @@ -0,0 +1,42 @@ +declare module 'express-session' { + import { RequestHandler } from 'express' + + interface SessionOptions { + secret: string + resave?: boolean + saveUninitialized?: boolean + store?: Store + cookie?: { + secure?: boolean + maxAge?: number + } + } + + interface Store { + get: (sid: string, callback: (err: any, sess: any | null) => void) => void + set: (sid: string, sess: any, callback?: (err?: any) => void) => void + destroy: (sid: string, callback?: (err?: any) => void) => void + } + + class MemoryStore implements Store { + get: Store['get'] + set: Store['set'] + destroy: Store['destroy'] + } + + interface Session { + [key: string]: any + } + + interface SessionRequest extends Express.Request { + session?: Session + } + + function session(options: SessionOptions): RequestHandler + + namespace session { + export { MemoryStore } + } + + export = session +} diff --git a/backend/src/utils/equipmentCode.ts b/backend/src/utils/equipmentCode.ts new file mode 100644 index 0000000..bfe0418 --- /dev/null +++ b/backend/src/utils/equipmentCode.ts @@ -0,0 +1,106 @@ +// 资产类型数字代码映射(纯数字编码) +const EQUIPMENT_TYPE_NUMBERS: Record = { + '相机': 1, + '镜头': 2, + '三脚架': 3, + '闪光灯': 4, + '摄影配饰品': 5 +} + +/** + * 生成资产编码(纯数字格式) + * 格式: {类型数字}{地址ID(4位)}{序号(5位)} + * 例如: 1000100001 (类型1,地址1,序号00001) + * 如果没有地址,地址ID为0000 + * + * 确保唯一性: + * 1. 查找该类型和地址的最大序号,避免并发问题 + * 2. 检查生成的编码是否已存在,如果存在则递增序号重试 + * 3. 最多重试10次,防止无限循环 + */ +export const generateEquipmentCode = async ( + equipmentType: string, + prisma: any, + storageLocationId?: number | null +): Promise => { + const typeNumber = EQUIPMENT_TYPE_NUMBERS[equipmentType] || 0 + + // 获取地址ID,如果没有地址则为0 + const locationId = storageLocationId || 0 + + // 格式前缀:{类型数字}{地址ID(4位)} + const typeCode = String(typeNumber) + const locationCode = String(locationId).padStart(4, '0') + const prefix = `${typeCode}${locationCode}` + + // 查找该类型和地址的所有资产,提取最大序号 + // 使用这种方式比count更安全,可以处理并发情况 + const existingEquipment = await prisma.equipment.findMany({ + where: { + equipment_type: equipmentType, + storage_location_id: storageLocationId || null, + equipment_code: { + startsWith: prefix + } + }, + select: { + equipment_code: true + }, + orderBy: { + equipment_code: 'desc' + }, + take: 1 // 只取最大的一条 + }) + + // 计算起始序号 + let startSequence = 1 + if (existingEquipment.length > 0) { + // 从最大编码中提取序号 + const maxCode = existingEquipment[0].equipment_code + if (maxCode.startsWith(prefix)) { + const sequencePart = maxCode.substring(prefix.length) + const maxSequence = parseInt(sequencePart, 10) + if (!isNaN(maxSequence)) { + startSequence = maxSequence + 1 + } + } + } + + // 检查序号是否溢出(5位数字最大99999) + if (startSequence > 99999) { + throw new Error(`资产编码序号已溢出,该类型和地址的资产数量已超过99999`) + } + + // 生成编码并检查唯一性,最多重试10次 + const maxRetries = 10 + for (let attempt = 0; attempt < maxRetries; attempt++) { + const sequence = String(startSequence + attempt).padStart(5, '0') + const code = `${prefix}${sequence}` + + // 检查编码是否已存在 + const existing = await prisma.equipment.findUnique({ + where: { equipment_code: code } + }) + + if (!existing) { + // 编码唯一,返回 + return code + } + + // 如果编码已存在,继续尝试下一个序号 + console.warn(`资产编码 ${code} 已存在,尝试下一个序号`) + } + + // 如果重试10次后仍然冲突,抛出错误 + throw new Error(`无法生成唯一的资产编码,已重试${maxRetries}次`) +} + +/** + * 验证资产编码格式(纯数字) + */ +export const validateEquipmentCode = (code: string): boolean => { + // 纯数字格式,长度10位:{类型1位}{地址4位}{序号5位} + const pattern = /^\d{10}$/ + return pattern.test(code) +} + diff --git a/backend/src/utils/keycloak.ts b/backend/src/utils/keycloak.ts new file mode 100644 index 0000000..9c77168 --- /dev/null +++ b/backend/src/utils/keycloak.ts @@ -0,0 +1,49 @@ +import session from 'express-session' +import Keycloak from 'keycloak-connect' + +type KeycloakEnvConfig = { + serverUrl: string + realm: string + clientId: string + clientSecret?: string +} + +const requiredEnv = ['KEYCLOAK_URL', 'KEYCLOAK_REALM', 'KEYCLOAK_CLIENT_ID'] as const + +requiredEnv.forEach((key) => { + if (!process.env[key]) { + console.warn(`[Keycloak] 环境变量 ${key} 未设置,Keycloak 认证可能无法正常工作。`) + } +}) + +const keycloakConfig: KeycloakEnvConfig = { + serverUrl: process.env.KEYCLOAK_URL || 'http://localhost:8080', + realm: process.env.KEYCLOAK_REALM || 'equipment-management', + clientId: process.env.KEYCLOAK_CLIENT_ID || 'equipment-client', + clientSecret: process.env.KEYCLOAK_CLIENT_SECRET +} + +const memoryStore = new session.MemoryStore() + +const keycloak = new Keycloak( + { + store: memoryStore, + scope: 'openid profile email' + }, + { + realm: keycloakConfig.realm, + 'auth-server-url': keycloakConfig.serverUrl, + 'ssl-required': 'external', + resource: keycloakConfig.clientId, + 'public-client': !keycloakConfig.clientSecret, + credentials: keycloakConfig.clientSecret + ? { secret: keycloakConfig.clientSecret } + : undefined, + 'confidential-port': 0, + 'bearer-only': false, + 'verify-token-audience': true, + 'use-resource-role-mappings': true + } as any +) + +export { keycloak, memoryStore } diff --git a/backend/src/utils/prisma.ts b/backend/src/utils/prisma.ts new file mode 100644 index 0000000..7a55085 --- /dev/null +++ b/backend/src/utils/prisma.ts @@ -0,0 +1,8 @@ +import { PrismaClient } from '@prisma/client' + +const prisma = new PrismaClient({ + log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'] +}) + +export default prisma + diff --git a/backend/src/utils/response.ts b/backend/src/utils/response.ts new file mode 100644 index 0000000..ab2c4e5 --- /dev/null +++ b/backend/src/utils/response.ts @@ -0,0 +1,24 @@ +import { Response } from 'express' + +export interface ApiResponse { + code: number + message: string + data: T +} + +export const successResponse = (res: Response, data: T, message: string = '操作成功'): void => { + res.json({ + code: 200, + message, + data + }) +} + +export const errorResponse = (res: Response, message: string = '操作失败', code: number = 400): void => { + res.status(code).json({ + code, + message, + data: null + }) +} + diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 0000000..0fcfcf7 --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "moduleResolution": "node", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "types": ["node"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} + diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..f54eb1e --- /dev/null +++ b/deploy.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# 摄影设备借还系统部署脚本 +set -e + +echo "==========================================" +echo " 摄影设备借还系统 - 部署脚本" +echo "==========================================" + +# 进入项目目录 +cd /opt/equipment-management + +# 停止旧容器 +echo ">>> 停止旧容器..." +sudo docker-compose down 2>/dev/null || true + +# 构建并启动容器 +echo ">>> 构建Docker镜像..." +sudo docker-compose build --no-cache + +echo ">>> 启动服务..." +sudo docker-compose up -d + +# 等待服务启动 +echo ">>> 等待服务启动..." +sleep 10 + +# 检查服务状态 +echo ">>> 检查服务状态..." +sudo docker-compose ps + +# 检查后端健康状态 +echo ">>> 检查后端API..." +curl -s http://localhost:3000/api/health || echo "后端服务启动中..." + +echo "" +echo "==========================================" +echo " 部署完成!" +echo " 前端访问: http://1.13.22.133" +echo " 后端API: http://1.13.22.133:3000/api" +echo "==========================================" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..37ca716 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,62 @@ +version: '3.8' + +services: + # MySQL数据库 + mysql: + image: mysql:8.0 + container_name: equipment-mysql + restart: always + environment: + MYSQL_ROOT_PASSWORD: jsjj8888 + MYSQL_DATABASE: equipment_management + MYSQL_CHARSET: utf8mb4 + MYSQL_COLLATION: utf8mb4_unicode_ci + ports: + - "3306:3306" + volumes: + - mysql_data:/var/lib/mysql + command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-pjsjj8888"] + interval: 10s + timeout: 5s + retries: 5 + + # 后端服务 + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: equipment-backend + restart: always + ports: + - "3000:3000" + environment: + DATABASE_URL: mysql://root:jsjj8888@mysql:3306/equipment_management?charset=utf8mb4 + SESSION_SECRET: equipment-management-secret-prod + AUTH_MODE: local + PORT: 3000 + depends_on: + mysql: + condition: service_healthy + volumes: + - backend_uploads:/app/uploads + + # 前端服务 + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + args: + VITE_API_BASE_URL: /api + VITE_AUTH_MODE: local + container_name: equipment-frontend + restart: always + ports: + - "80:80" + depends_on: + - backend + +volumes: + mysql_data: + backend_uploads: diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..64d4738 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,34 @@ +FROM node:20-alpine as builder + +WORKDIR /app + +# 构建参数 +ARG VITE_API_BASE_URL=http://localhost:3000/api +ARG VITE_AUTH_MODE=local + +# 设置环境变量 +ENV VITE_API_BASE_URL=$VITE_API_BASE_URL +ENV VITE_AUTH_MODE=$VITE_AUTH_MODE + +# 安装依赖 +COPY package*.json ./ +RUN npm install --registry=https://registry.npmmirror.com + +# 复制源代码 +COPY . . + +# 构建前端 +RUN npm run build + +# 生产环境使用nginx +FROM nginx:alpine + +# 复制构建产物 +COPY --from=builder /app/dist /usr/share/nginx/html + +# 复制nginx配置 +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..79e115c --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + 固定资产管理系统 + + +
+ + + diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..a52ca15 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,40 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Gzip压缩 + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + # 前端路由支持 + location / { + try_files $uri $uri/ /index.html; + } + + # API代理 + location /api { + proxy_pass http://backend:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_cache_bypass $http_upgrade; + } + + # 上传文件代理 + location /uploads { + proxy_pass http://backend:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + + # 静态资源缓存 + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..4d51317 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3909 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@heroicons/vue": "^2.2.0", + "axios": "^1.13.2", + "echarts": "^6.0.0", + "html2canvas": "^1.4.1", + "jsbarcode": "^3.12.1", + "keycloak-js": "^23.0.7", + "pinia": "^3.0.4", + "vue": "^3.5.24", + "vue-router": "^4.6.3" + }, + "devDependencies": { + "@tailwindcss/forms": "^0.5.10", + "@types/node": "^24.10.0", + "@vitejs/plugin-vue": "^6.0.1", + "@vue/tsconfig": "^0.8.1", + "autoprefixer": "^10.4.22", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.15", + "typescript": "~5.9.3", + "vite": "^7.2.2", + "vue-tsc": "^3.1.3" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@heroicons/vue": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@heroicons/vue/-/vue-2.2.0.tgz", + "integrity": "sha512-G3dbSxoeEKqbi/DFalhRxJU4mTXJn7GwZ7ae8NuEQzd1bqdd0jAbdaBZlHPcvPD2xI1iGzNVB4k20Un2AguYPw==", + "license": "MIT", + "peerDependencies": { + "vue": ">= 3" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.29", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.29.tgz", + "integrity": "sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", + "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz", + "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz", + "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz", + "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz", + "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz", + "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz", + "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz", + "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz", + "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz", + "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz", + "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz", + "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz", + "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz", + "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz", + "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz", + "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz", + "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz", + "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz", + "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz", + "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz", + "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", + "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/forms": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz", + "integrity": "sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mini-svg-data-uri": "^1.2.3" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz", + "integrity": "sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.29" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.23", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.23.tgz", + "integrity": "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.23" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.23", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.23.tgz", + "integrity": "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.23", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.23.tgz", + "integrity": "sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.23", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.24.tgz", + "integrity": "sha512-eDl5H57AOpNakGNAkFDH+y7kTqrQpJkZFXhWZQGyx/5Wh7B1uQYvcWkvZi11BDhscPgj8N7XV3oRwiPnx1Vrig==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.24", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.24.tgz", + "integrity": "sha512-1QHGAvs53gXkWdd3ZMGYuvQFXHW4ksKWPG8HP8/2BscrbZ0brw183q2oNWjMrSWImYLHxHrx1ItBQr50I/q2zw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.24", + "@vue/shared": "3.5.24" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.24.tgz", + "integrity": "sha512-8EG5YPRgmTB+YxYBM3VXy8zHD9SWHUJLIGPhDovo3Z8VOgvP+O7UP5vl0J4BBPWYD9vxtBabzW1EuEZ+Cqs14g==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.24", + "@vue/compiler-dom": "3.5.24", + "@vue/compiler-ssr": "3.5.24", + "@vue/shared": "3.5.24", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.24.tgz", + "integrity": "sha512-trOvMWNBMQ/odMRHW7Ae1CdfYx+7MuiQu62Jtu36gMLXcaoqKvAyh+P73sYG9ll+6jLB6QPovqoKGGZROzkFFg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.24", + "@vue/shared": "3.5.24" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.8", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.8.tgz", + "integrity": "sha512-BtFcAmDbtXGwurWUFf8ogIbgZyR+rcVES1TSNEI8Em80fD8Anu+qTRN1Fc3J6vdRHlVM3fzPV1qIo+B4AiqGzw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.8" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.8", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.8.tgz", + "integrity": "sha512-4Y8op+AoxOJhB9fpcEF6d5vcJXWKgHxC3B0ytUB8zz15KbP9g9WgVzral05xluxi2fOeAy6t140rdQ943GcLRQ==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.8", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.8", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.8.tgz", + "integrity": "sha512-XHpO3jC5nOgYr40M9p8Z4mmKfTvUxKyRcUnpBAYg11pE78eaRFBKb0kG5yKLroMuJeeNH9LWmKp2zMU5LUc7CA==", + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/language-core": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.1.3.tgz", + "integrity": "sha512-KpR1F/eGAG9D1RZ0/T6zWJs6dh/pRLfY5WupecyYKJ1fjVmDMgTPw9wXmKv2rBjo4zCJiOSiyB8BDP1OUwpMEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.23", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.0.0", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.2" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.24.tgz", + "integrity": "sha512-BM8kBhtlkkbnyl4q+HiF5R5BL0ycDPfihowulm02q3WYp2vxgPcJuZO866qa/0u3idbMntKEtVNuAUp5bw4teg==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.24" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.24.tgz", + "integrity": "sha512-RYP/byyKDgNIqfX/gNb2PB55dJmM97jc9wyF3jK7QUInYKypK2exmZMNwnjueWwGceEkP6NChd3D2ZVEp9undQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.24", + "@vue/shared": "3.5.24" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.24.tgz", + "integrity": "sha512-Z8ANhr/i0XIluonHVjbUkjvn+CyrxbXRIxR7wn7+X7xlcb7dJsfITZbkVOeJZdP8VZwfrWRsWdShH6pngMxRjw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.24", + "@vue/runtime-core": "3.5.24", + "@vue/shared": "3.5.24", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.24.tgz", + "integrity": "sha512-Yh2j2Y4G/0/4z/xJ1Bad4mxaAk++C2v4kaa8oSYTMJBJ00/ndPuxCnWeot0/7/qafQFLh5pr6xeV6SdMcE/G1w==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.24", + "@vue/shared": "3.5.24" + }, + "peerDependencies": { + "vue": "3.5.24" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.24.tgz", + "integrity": "sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A==", + "license": "MIT" + }, + "node_modules/@vue/tsconfig": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.8.1.tgz", + "integrity": "sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": "5.x", + "vue": "^3.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/alien-signals": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.0.tgz", + "integrity": "sha512-yufC6VpSy8tK3I0lO67pjumo5JvDQVQyr38+3OHqe6CHl1t2VZekKZ7EKKZSqk0cRmE7U7tfZbpXiKNzuc+ckg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", + "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.27.0", + "caniuse-lite": "^1.0.30001754", + "fraction.js": "^5.3.4", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.26", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.26.tgz", + "integrity": "sha512-73lC1ugzwoaWCLJ1LvOgrR5xsMLTqSKIEoMHVtL9E/HNk0PXtTM76ZIm84856/SF7Nv8mPZxKoBsgpm0tR1u1Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/birpc": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.8.0.tgz", + "integrity": "sha512-Bz2a4qD/5GRhiHSwj30c/8kC8QGj12nNDwz3D4ErQ4Xhy35dsSDvF+RA/tWpjyU0pdGtSDiEk6B5fBGE1qNVhw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", + "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.25", + "caniuse-lite": "^1.0.30001754", + "electron-to-chromium": "^1.5.249", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001754", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz", + "integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/echarts": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz", + "integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "6.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.250", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.250.tgz", + "integrity": "sha512-/5UMj9IiGDMOFBnN4i7/Ry5onJrAGSbOGo3s9FEKmwobGq6xw832ccET0CE3CkkMBZ8GJSlUIesZofpyurqDXw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/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/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmmirror.com/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-sha256": { + "version": "0.10.1", + "resolved": "https://registry.npmmirror.com/js-sha256/-/js-sha256-0.10.1.tgz", + "integrity": "sha512-5obBtsz9301ULlsgggLg542s/jqtddfOpV5KJc4hajc9JV8GeY2gZHSVpYBn4nWqAUTJ9v+xwtbJ1mIBgIH5Vw==", + "license": "MIT" + }, + "node_modules/jsbarcode": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/jsbarcode/-/jsbarcode-3.12.1.tgz", + "integrity": "sha512-QZQSqIknC2Rr/YOUyOkCBqsoiBAOTYK+7yNN3JsqfoUtJtkazxNw1dmPpxuv7VVvqW13kA3/mKiLq+s/e3o9hQ==", + "license": "MIT" + }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/keycloak-js": { + "version": "23.0.7", + "resolved": "https://registry.npmmirror.com/keycloak-js/-/keycloak-js-23.0.7.tgz", + "integrity": "sha512-OmszsKzBhhm5yP4W1q/tMd+nNnKpOAdeVYcoGhphlv8Fj1bNk4wRTYzp7pn5BkvueLz7fhvKHz7uOc33524YrA==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.5.1", + "js-sha256": "^0.10.1", + "jwt-decode": "^4.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "dev": true, + "license": "MPL-2.0", + "optional": true, + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "dev": true, + "license": "MIT", + "bin": { + "mini-svg-data-uri": "cli.js" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinia": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", + "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^7.7.7" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.5.0", + "vue": "^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", + "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.2", + "@rollup/rollup-android-arm64": "4.53.2", + "@rollup/rollup-darwin-arm64": "4.53.2", + "@rollup/rollup-darwin-x64": "4.53.2", + "@rollup/rollup-freebsd-arm64": "4.53.2", + "@rollup/rollup-freebsd-x64": "4.53.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", + "@rollup/rollup-linux-arm-musleabihf": "4.53.2", + "@rollup/rollup-linux-arm64-gnu": "4.53.2", + "@rollup/rollup-linux-arm64-musl": "4.53.2", + "@rollup/rollup-linux-loong64-gnu": "4.53.2", + "@rollup/rollup-linux-ppc64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-musl": "4.53.2", + "@rollup/rollup-linux-s390x-gnu": "4.53.2", + "@rollup/rollup-linux-x64-gnu": "4.53.2", + "@rollup/rollup-linux-x64-musl": "4.53.2", + "@rollup/rollup-openharmony-arm64": "4.53.2", + "@rollup/rollup-win32-arm64-msvc": "4.53.2", + "@rollup/rollup-win32-ia32-msvc": "4.53.2", + "@rollup/rollup-win32-x64-gnu": "4.53.2", + "@rollup/rollup-win32-x64-msvc": "4.53.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/superjson": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.5.tgz", + "integrity": "sha512-zWPTX96LVsA/eVYnqOM2+ofcdPqdS1dAF1LN4TS2/MWuUpfitd9ctTa87wt4xrYnZnkLtS69xpBdSxVBP5Rm6w==", + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz", + "integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/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/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, + "node_modules/vite": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", + "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.24.tgz", + "integrity": "sha512-uTHDOpVQTMjcGgrqFPSb8iO2m1DUvo+WbGqoXQz8Y1CeBYQ0FXf2z1gLRaBtHjlRz7zZUBHxjVB5VTLzYkvftg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.24", + "@vue/compiler-sfc": "3.5.24", + "@vue/runtime-dom": "3.5.24", + "@vue/server-renderer": "3.5.24", + "@vue/shared": "3.5.24" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.3.tgz", + "integrity": "sha512-ARBedLm9YlbvQomnmq91Os7ck6efydTSpRP3nuOKCvgJOHNrhRoJDSKtee8kcL1Vf7nz6U+PMBL+hTvR3bTVQg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue-router/node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/vue-tsc": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.1.3.tgz", + "integrity": "sha512-StMNfZHwPIXQgY3KxPKM0Jsoc8b46mDV3Fn2UlHCBIwRJApjqrSwqeMYgWf0zpN+g857y74pv7GWuBm+UqQe1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.23", + "@vue/language-core": "3.1.3" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/zrender": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz", + "integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..d9badb1 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,38 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@heroicons/vue": "^2.2.0", + "@types/qrcode": "^1.5.6", + "axios": "^1.13.2", + "echarts": "^6.0.0", + "html2canvas": "^1.4.1", + "jsbarcode": "^3.12.1", + "keycloak-js": "^23.0.7", + "pinia": "^3.0.4", + "qrcode": "^1.5.4", + "vue": "^3.5.24", + "vue-router": "^4.6.3", + "xlsx": "^0.18.5" + }, + "devDependencies": { + "@tailwindcss/forms": "^0.5.10", + "@types/node": "^24.10.0", + "@types/xlsx": "^0.0.35", + "@vitejs/plugin-vue": "^6.0.1", + "@vue/tsconfig": "^0.8.1", + "autoprefixer": "^10.4.22", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.15", + "typescript": "~5.9.3", + "vite": "^7.2.2", + "vue-tsc": "^3.1.3" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..78ecdc7 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,10 @@ +import tailwindcss from 'tailwindcss' +import autoprefixer from 'autoprefixer' + +export default { + plugins: [ + tailwindcss(), + autoprefixer(), + ], +} + diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..9a91297 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/frontend/src/api/assetType.ts b/frontend/src/api/assetType.ts new file mode 100644 index 0000000..8a0b018 --- /dev/null +++ b/frontend/src/api/assetType.ts @@ -0,0 +1,45 @@ +import api from './index' +import type { ApiResponse, PaginatedResponse, AssetType } from '@/types' + +export const getAssetTypeOptions = async () => { + const response = await api.get>('/asset-types/options') + return response.data +} + +export const getAssetTypeList = async (params: { + page?: number + pageSize?: number + keyword?: string + status?: string +}) => { + const response = await api.get>>('/asset-types/list', { + params + }) + return response.data +} + +export const createAssetType = async (data: { + name: string + short_name?: string + can_be_parent?: boolean + can_be_child?: boolean +}) => { + const response = await api.post>('/asset-types', data) + return response.data +} + +export const updateAssetType = async (id: number, data: { + name?: string + short_name?: string + status?: string + can_be_parent?: boolean + can_be_child?: boolean +}) => { + const response = await api.put>(`/asset-types/${id}`, data) + return response.data +} + +export const deleteAssetType = async (id: number) => { + const response = await api.delete>(`/asset-types/${id}`) + return response.data +} diff --git a/frontend/src/api/barcode.ts b/frontend/src/api/barcode.ts new file mode 100644 index 0000000..0d4c847 --- /dev/null +++ b/frontend/src/api/barcode.ts @@ -0,0 +1,41 @@ +import api from './index' +import type { ApiResponse } from '@/types' + +// 获取设备信息用于生成条码 +export const getEquipmentForBarcode = async (equipmentCode: string) => { + const response = await api.get< + ApiResponse<{ + equipment_code: string + equipment_name: string + equipment_model?: string + equipment_type: string + }> + >(`/barcode/${equipmentCode}`) + return response.data +} + +// 批量获取设备信息用于生成条码 +export const getEquipmentBatchForBarcode = async (equipmentIds: number[]) => { + const response = await api.post< + ApiResponse< + Array<{ + id: number + equipment_code: string + equipment_name: string + equipment_model?: string + equipment_type: string + storage_location_id?: number | null + storageLocation?: { + id: number + name: string + code?: string | null + } | null + }> + > + >(`/barcode/batch`, { + equipment_ids: equipmentIds + }) + return response.data +} + + diff --git a/frontend/src/api/batch.ts b/frontend/src/api/batch.ts new file mode 100644 index 0000000..e5d3c3d --- /dev/null +++ b/frontend/src/api/batch.ts @@ -0,0 +1,40 @@ +import request from './index' + +export interface BatchResult { + success_count: number + failed_count: number + results: Array<{ + equipment_id: number + equipment_code?: string + status: 'success' | 'failed' + message: string + }> +} + +// 批量借出 +export const batchBorrow = (data: { + equipment_ids: number[] + person_id: number + expected_return_time?: string + is_long_term?: boolean + notes?: string +}) => { + return request.post('/batch/borrow', data) +} + +// 批量归还 +export const batchReturn = (data: { + equipment_ids: number[] + return_condition?: string + notes?: string +}) => { + return request.post('/batch/return', data) +} + +// 批量转移部门 +export const batchTransfer = (data: { + equipment_ids: number[] + department_id: number +}) => { + return request.post('/batch/transfer', data) +} diff --git a/frontend/src/api/batchImport.ts b/frontend/src/api/batchImport.ts new file mode 100644 index 0000000..80a8d0f --- /dev/null +++ b/frontend/src/api/batchImport.ts @@ -0,0 +1,43 @@ +import api from '@/api' +import type { ApiResponse } from '@/types' + +interface PersonImportData { + nickname: string + real_name: string + id_number?: string + department?: string + position?: string + phone?: string + email?: string +} + +interface EquipmentImportData { + equipment_name: string + equipment_type: string + equipment_model?: string + department?: string + storage_location?: string + unit_price?: number + total_quantity: number + supplier?: string + purchase_date?: string + description?: string +} + +interface ImportResult { + success: number + failed: number + errors: { row: number; message: string }[] +} + +export const batchImportPersons = async (persons: PersonImportData[]) => { + const response = await api.post>('/batch-import/persons', { persons }) + return response.data +} + +export const batchImportEquipments = async (equipments: EquipmentImportData[]) => { + const response = await api.post>('/batch-import/equipments', { equipments }) + return response.data +} + +export type { ImportResult } diff --git a/frontend/src/api/borrow.ts b/frontend/src/api/borrow.ts new file mode 100644 index 0000000..502ff0c --- /dev/null +++ b/frontend/src/api/borrow.ts @@ -0,0 +1,146 @@ +import api from './index' +import type { + BorrowRecord, + PaginatedResponse, + ApiResponse, + EquipmentStatus, + StorageLocation +} from '@/types' + +export interface ScanBorrowResult { + id: number + equipment_code: string + equipment_name: string + borrow_time: string + status: string + remaining_quantity?: number + equipment_status?: EquipmentStatus + storage_location_id?: number | null + storageLocation?: StorageLocation | null +} + +// 借用设备 +export const borrowEquipment = async (data: { + equipment_id?: number + equipment_code?: string + person_id: number + expected_return_time?: string + notes?: string + is_long_term?: boolean +}) => { + const response = await api.post>('/borrow', data) + return response.data +} + +// 扫码出库 +export const scanBorrow = async (data: { + equipment_code: string + person_id: number + expected_return_time?: string + notes?: string + is_long_term?: boolean +}) => { + const response = await api.post>('/borrow/scan', data) + return response.data +} + +// 批量扫码出库 +export const scanBorrowBatch = async (data: { + equipment_codes: string[] + person_id: number + expected_return_time?: string + notes?: string + is_long_term?: boolean +}) => { + const response = await api.post + }>>('/borrow/scan-batch', data) + return response.data +} + +// 归还设备 +export const returnEquipment = async (data: { + equipment_id: number + equipment_code: string + return_condition?: string + notes?: string +}) => { + const response = await api.post>('/return', data) + return response.data +} + +// 扫码入库 +export const scanReturn = async (data: { + equipment_code: string + return_condition?: string + notes?: string + person_id?: number + borrow_record_id?: number +}) => { + const response = await api.post< + ApiResponse<{ + id: number + equipment_code: string + equipment_name: string + return_time: string + status: string + person_id?: number + }> + >('/return/scan', data) + return response.data +} + +// 批量扫码入库 +export const scanReturnBatch = async (data: { + equipment_codes: string[] + return_condition?: string + notes?: string +}) => { + const response = await api.post + }>>('/return/scan-batch', data) + return response.data +} + +// 根据设备编码查询借用记录 +export const getBorrowRecordByCode = async (equipmentCode: string) => { + const response = await api.get>(`/borrow/scan/${equipmentCode}`) + return response.data +} + +// 获取借用记录列表 +export const getBorrowRecordList = async (params: { + page?: number + pageSize?: number + status?: string + person_name?: string + person_id?: number +}) => { + const response = await api.get>>('/borrow/records', { + params + }) + return response.data +} + diff --git a/frontend/src/api/department.ts b/frontend/src/api/department.ts new file mode 100644 index 0000000..d2ef814 --- /dev/null +++ b/frontend/src/api/department.ts @@ -0,0 +1,39 @@ +import api from './index' +import type { ApiResponse, PaginatedResponse, Department, DepartmentTreeNode } from '@/types' + +export const getDepartmentOptions = async () => { + const response = await api.get>('/departments/options') + return response.data +} + +export const getDepartmentTree = async () => { + const response = await api.get>('/departments/tree') + return response.data +} + +export const getDepartmentList = async (params: { + page?: number + pageSize?: number + keyword?: string + status?: string +}) => { + const response = await api.get>>('/departments/list', { + params + }) + return response.data +} + +export const createDepartment = async (data: { name: string; parent_id?: number | null }) => { + const response = await api.post>('/departments', data) + return response.data +} + +export const updateDepartment = async (id: number, data: { name?: string; status?: string; parent_id?: number | null }) => { + const response = await api.put>(`/departments/${id}`, data) + return response.data +} + +export const deleteDepartment = async (id: number) => { + const response = await api.delete>(`/departments/${id}`) + return response.data +} diff --git a/frontend/src/api/disposal.ts b/frontend/src/api/disposal.ts new file mode 100644 index 0000000..cdaec27 --- /dev/null +++ b/frontend/src/api/disposal.ts @@ -0,0 +1,63 @@ +import request from './index' + +export interface DisposalRecord { + id: number + equipment_id: number + equipment_code: string + disposal_reason: string + disposal_method: string + estimated_value: number + actual_value?: number + status: 'pending' | 'approved' | 'rejected' | 'completed' + applied_by: number + applied_at: string + approved_by?: number + approved_at?: string + completed_at?: string + notes?: string + equipment?: { + equipment_name: string + equipment_type: string + unit_price: number + } + applier?: { id: number; name: string; username: string } + approver?: { id: number; name: string; username: string } +} + +export interface CreateDisposalParams { + equipment_id: number + disposal_reason: string + disposal_method: string + estimated_value: number + notes?: string +} + +// 创建报废申请 +export const createDisposal = (data: CreateDisposalParams) => { + return request.post('/disposal', data) +} + +// 获取报废记录列表 +export const getDisposalList = (params: { page?: number; pageSize?: number; status?: string; keyword?: string; equipment_id?: number }) => { + return request.get('/disposal/list', { params }) +} + +// 获取报废记录详情 +export const getDisposalDetail = (id: number) => { + return request.get(`/disposal/${id}`) +} + +// 审批报废申请 +export const approveDisposal = (id: number, data: { approved: boolean; reject_reason?: string }) => { + return request.put(`/disposal/${id}/approve`, data) +} + +// 完成报废处置 +export const completeDisposal = (id: number, data: { actual_value?: number; notes?: string }) => { + return request.put(`/disposal/${id}/complete`, data) +} + +// 获取资产报废历史 +export const getEquipmentDisposalHistory = (equipmentId: number) => { + return request.get(`/disposal/equipment/${equipmentId}/history`) +} diff --git a/frontend/src/api/equipment.ts b/frontend/src/api/equipment.ts new file mode 100644 index 0000000..35027af --- /dev/null +++ b/frontend/src/api/equipment.ts @@ -0,0 +1,97 @@ +import api from './index' +import type { Equipment, PaginatedResponse, ApiResponse } from '@/types' + +const transformEquipmentPayload = (data: Partial) => { + const payload: Record = { ...data } + if (payload.storageLocation) { + payload.storage_location_id = payload.storageLocation?.id + delete payload.storageLocation + } + if (payload.storage_location_id !== undefined && payload.storage_location_id !== null) { + payload.storage_location_id = Number(payload.storage_location_id) + } else { + delete payload.storage_location_id + } + return payload +} + +// 获取资产列表 +export const getEquipmentList = async (params: { + page?: number + pageSize?: number + equipmentType?: string + status?: string + keyword?: string + storageLocationId?: number + departmentId?: number + mount_status?: string +}) => { + const { storageLocationId, departmentId, ...rest } = params + const response = await api.get>>('/equipment/list', { + params: { + ...rest, + storage_location_id: storageLocationId, + department_id: departmentId + } + }) + return response.data +} + +// 获取资产详情 +export const getEquipmentDetail = async (id: number) => { + const response = await api.get>(`/equipment/${id}`) + return response.data +} + +// 新增资产 +export const createEquipment = async (data: Partial) => { + const response = await api.post>('/equipment', transformEquipmentPayload(data)) + return response.data +} + +// 更新资产 +export const updateEquipment = async (id: number, data: Partial) => { + const response = await api.put>(`/equipment/${id}`, transformEquipmentPayload(data)) + return response.data +} + +// 删除资产 +export const deleteEquipment = async (id: number) => { + const response = await api.delete>(`/equipment/${id}`) + return response.data +} + +// 生成资产编码 +export const generateEquipmentCode = async ( + equipmentType: string, + codeGenerationMode: string = 'auto', + storageLocationId?: number | null +) => { + const response = await api.post>( + '/equipment/generate-code', + { + equipment_type: equipmentType, + code_generation_mode: codeGenerationMode, + storage_location_id: storageLocationId + } + ) + return response.data +} + +// 验证资产编码 +export const verifyEquipmentCode = async (equipmentCode: string) => { + const response = await api.post>( + '/equipment/verify-code', + { + equipment_code: equipmentCode + } + ) + return response.data +} + +// 根据资产编码查询资产信息(扫码) +export const scanEquipment = async (equipmentCode: string) => { + const response = await api.get>(`/equipment/scan/${equipmentCode}`) + return response.data +} + diff --git a/frontend/src/api/export.ts b/frontend/src/api/export.ts new file mode 100644 index 0000000..bca7f8b --- /dev/null +++ b/frontend/src/api/export.ts @@ -0,0 +1,39 @@ +import request from './index' + +// 导出资产列表 +export const exportEquipment = (params?: { + status?: string + equipment_type?: string + department_id?: number + keyword?: string +}) => { + return request.get('/export/equipment', { + params, + responseType: 'blob' + }) +} + +// 导出借用记录 +export const exportBorrowRecords = (params?: { + status?: string + person_name?: string + start_date?: string + end_date?: string +}) => { + return request.get('/export/borrow-records', { + params, + responseType: 'blob' + }) +} + +// 下载文件辅助函数 +export const downloadFile = (blob: Blob, filename: string) => { + const url = window.URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = filename + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + window.URL.revokeObjectURL(url) +} diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts new file mode 100644 index 0000000..4217794 --- /dev/null +++ b/frontend/src/api/index.ts @@ -0,0 +1,81 @@ +import axios from 'axios' +import type { AxiosInstance, AxiosResponse } from 'axios' +import type { ApiResponse } from '@/types' + +// 创建axios实例 +const api: AxiosInstance = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api', + timeout: 10000, + headers: { + 'Content-Type': 'application/json' + } +}) + +api.defaults.withCredentials = true + +// 请求拦截器 +api.interceptors.request.use( + (config) => { + // 从localStorage获取token + const token = localStorage.getItem('token') + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + return config + }, + (error) => { + return Promise.reject(error) + } +) + +const authMode = import.meta.env.VITE_AUTH_MODE || 'keycloak' + +// 响应拦截器 +api.interceptors.response.use( + (response: AxiosResponse) => { + const data = response.data as ApiResponse + if (data?.code === 200) { + return response + } + return Promise.reject(new Error(data?.message || '请求失败')) + }, + (error) => { + // 处理HTTP错误 + if (error.response) { + const requestUrl: string = error.config?.url || '' + switch (error.response.status) { + case 401: + // 未授权,跳转到登录页 + localStorage.removeItem('token') + { + const isAuthStatusCheck = requestUrl.includes('/auth/user') + const isLoginAttempt = requestUrl.includes('/auth/local/login') + const shouldRedirect = + authMode !== 'local' || (!isAuthStatusCheck && !isLoginAttempt) + if (shouldRedirect && window.location.pathname !== '/login') { + window.location.href = '/login' + } + } + break + case 403: + // 禁止访问 + console.error('禁止访问') + break + case 404: + // 资源不存在 + console.error('资源不存在') + break + case 500: + // 服务器错误 + console.error('服务器错误') + break + default: + console.error('请求失败') + } + } + return Promise.reject(error) + } +) + +export default api + diff --git a/frontend/src/api/maintenance.ts b/frontend/src/api/maintenance.ts new file mode 100644 index 0000000..f428fb4 --- /dev/null +++ b/frontend/src/api/maintenance.ts @@ -0,0 +1,65 @@ +import request from './index' + +export interface MaintenanceRecord { + id: number + equipment_id: number + equipment_code: string + issue_description: string + repair_vendor?: string + estimated_cost: number + actual_cost?: number + start_date: string + expected_end_date?: string + actual_end_date?: string + status: 'in_progress' | 'completed' | 'cancelled' + repair_result?: string + created_by: number + equipment?: { + equipment_name: string + equipment_type: string + } + creator?: { id: number; name: string; username: string } +} + +export interface CreateMaintenanceParams { + equipment_id: number + issue_description: string + repair_vendor?: string + estimated_cost: number + expected_end_date?: string +} + +// 创建维修记录 +export const createMaintenance = (data: CreateMaintenanceParams) => { + return request.post('/maintenance', data) +} + +// 获取维修记录列表 +export const getMaintenanceList = (params: { page?: number; pageSize?: number; status?: string; keyword?: string; equipment_id?: number }) => { + return request.get('/maintenance/list', { params }) +} + +// 获取维修记录详情 +export const getMaintenanceDetail = (id: number) => { + return request.get(`/maintenance/${id}`) +} + +// 更新维修记录 +export const updateMaintenance = (id: number, data: Partial) => { + return request.put(`/maintenance/${id}`, data) +} + +// 完成维修 +export const completeMaintenance = (id: number, data: { actual_cost?: number; repair_result?: string }) => { + return request.put(`/maintenance/${id}/complete`, data) +} + +// 取消维修 +export const cancelMaintenance = (id: number, data: { reason?: string }) => { + return request.put(`/maintenance/${id}/cancel`, data) +} + +// 获取资产维修历史 +export const getEquipmentMaintenanceHistory = (equipmentId: number) => { + return request.get(`/maintenance/equipment/${equipmentId}/history`) +} diff --git a/frontend/src/api/mount.ts b/frontend/src/api/mount.ts new file mode 100644 index 0000000..53d0630 --- /dev/null +++ b/frontend/src/api/mount.ts @@ -0,0 +1,97 @@ +import api from './index' +import type { ApiResponse } from '@/types' + +export interface MountChildRequest { + child_id: number + notes?: string +} + +export interface UnmountChildRequest { + child_id: number + notes?: string +} + +export interface ReplaceChildRequest { + old_child_id: number + new_child_id: number + notes?: string +} + +export interface ChildEquipment { + id: number + equipment_code: string + equipment_name: string + equipment_model?: string + equipment_type: string + unit_price: number + status: string + mount_status?: string +} + +export interface MountHistory { + id: number + child_id: number + parent_id: number | null + operation_type: 'mount' | 'unmount' | 'replace' + operated_at: string + operated_by: number + notes?: string + child: { + id: number + equipment_code: string + equipment_name: string + } + parent?: { + id: number + equipment_code: string + equipment_name: string + } | null + operator: { + id: number + name: string + username: string + } +} + +// 挂载子资产到父资产 +export const mountChild = async (parentId: number, data: MountChildRequest): Promise> => { + const response = await api.post(`/mount/${parentId}/mount`, data) + return response.data +} + +// 从父资产卸载子资产 +export const unmountChild = async (parentId: number, data: UnmountChildRequest): Promise> => { + const response = await api.post(`/mount/${parentId}/unmount`, data) + return response.data +} + +// 更换配件 +export const replaceChild = async (parentId: number, data: ReplaceChildRequest): Promise> => { + const response = await api.post(`/mount/${parentId}/replace`, data) + return response.data +} + +// 获取父资产的子资产列表 +export const getChildren = async (parentId: number): Promise> => { + const response = await api.get(`/mount/${parentId}/children`) + return response.data +} + +// 获取资产的挂载历史 +export const getMountHistory = async (equipmentId: number): Promise> => { + const response = await api.get(`/mount/${equipmentId}/mount-history`) + return response.data +} + +// 获取可挂载的子资产列表 +export const getAvailableChildren = async (params?: { + keyword?: string + equipment_type?: string +}): Promise> => { + const response = await api.get('/mount/available-children', { params }) + return response.data +} diff --git a/frontend/src/api/operationLog.ts b/frontend/src/api/operationLog.ts new file mode 100644 index 0000000..c6ffdfa --- /dev/null +++ b/frontend/src/api/operationLog.ts @@ -0,0 +1,42 @@ +import api from '@/api' + +interface OperationLog { + id: number + user_id: number | null + username: string | null + operation_type: string + equipment_id: number | null + equipment_code: string | null + operation_details: string | null + ip_address: string | null + created_at: string + user?: { + id: number + username: string + name: string + } +} + +interface OperationLogListResponse { + list: OperationLog[] + total: number + page: number + pageSize: number +} + +interface OperationLogQuery { + page?: number + pageSize?: number + operationType?: string + username?: string + startDate?: string + endDate?: string +} + +export const getOperationLogList = (params: OperationLogQuery) => { + return api.get<{ data: OperationLogListResponse }>('/operation-logs/list', { params }) +} + +export const getOperationTypes = () => { + return api.get<{ data: string[] }>('/operation-logs/types') +} diff --git a/frontend/src/api/person.ts b/frontend/src/api/person.ts new file mode 100644 index 0000000..baad34f --- /dev/null +++ b/frontend/src/api/person.ts @@ -0,0 +1,66 @@ +import api from './index' +import type { ApiResponse, PaginatedResponse, Person } from '@/types' + +export const getPersonList = async (params: { + page?: number + pageSize?: number + keyword?: string + status?: string +}) => { + const response = await api.get>>('/persons', { params }) + return response.data +} + +export const getPersonDetail = async (id: number) => { + const response = await api.get>(`/persons/${id}`) + return response.data +} + +export const createPerson = async (data: { + nickname: string + real_name: string + id_number?: string + department?: string + position?: string + phone?: string + email?: string + status?: string + remarks?: string +}) => { + const response = await api.post>('/persons', data) + return response.data +} + +export const updatePerson = async ( + id: number, + data: { + nickname?: string + real_name?: string + id_number?: string + department?: string + position?: string + phone?: string + email?: string + status?: string + remarks?: string + } +) => { + const response = await api.put>(`/persons/${id}`, data) + return response.data +} + +export const updatePersonStatus = async (id: number, status: 'active' | 'inactive') => { + const response = await api.patch>(`/persons/${id}/status`, { status }) + return response.data +} + + +export const deletePerson = async (id: number) => { + const response = await api.delete>(`/persons/${id}`) + return response.data +} + +export const getPersonOptions = async () => { + const response = await api.get>('/persons/options') + return response.data +} diff --git a/frontend/src/api/search.ts b/frontend/src/api/search.ts new file mode 100644 index 0000000..32fa1de --- /dev/null +++ b/frontend/src/api/search.ts @@ -0,0 +1,15 @@ +import request from './index' + +export interface SearchResult { + id: number + code: string + name: string + type: string + status: string + category: 'equipment' +} + +// 全局搜索 +export const globalSearch = (keyword: string) => { + return request.get('/search', { params: { keyword } }) +} diff --git a/frontend/src/api/settings.ts b/frontend/src/api/settings.ts new file mode 100644 index 0000000..4c849df --- /dev/null +++ b/frontend/src/api/settings.ts @@ -0,0 +1,26 @@ +import api from './index' +import type { ApiResponse } from '@/types' + +export interface SiteConfigResponse { + site_name: string + logo_url: string + favicon_url: string + theme_color: string + updated_at: string | null + updated_by: { + id: number + username: string + name: string + } | null +} + +export const getSiteConfig = async () => { + const response = await api.get>('/settings/site') + return response.data +} + +export const updateSiteConfig = async (data: { site_name: string; theme_color?: string }) => { + const response = await api.put>('/settings/site', data) + return response.data +} + diff --git a/frontend/src/api/statistics.ts b/frontend/src/api/statistics.ts new file mode 100644 index 0000000..addbfbc --- /dev/null +++ b/frontend/src/api/statistics.ts @@ -0,0 +1,19 @@ +import api from './index' +import type { DashboardStats, ValueStatistics, ApiResponse } from '@/types' + +// 获取首页统计数据 +export const getDashboardStats = async () => { + const response = await api.get>('/statistics/dashboard') + return response.data +} + +// 获取设备价值统计 +export const getValueStatistics = async (params?: { + equipment_type?: string + start_date?: string + end_date?: string +}) => { + const response = await api.get>('/statistics/value', { params }) + return response.data +} + diff --git a/frontend/src/api/storageLocation.ts b/frontend/src/api/storageLocation.ts new file mode 100644 index 0000000..7ae81da --- /dev/null +++ b/frontend/src/api/storageLocation.ts @@ -0,0 +1,37 @@ +import api from './index' +import type { ApiResponse, PaginatedResponse, StorageLocation } from '@/types' + +export const getStorageLocationOptions = async () => { + const response = await api.get>('/storage-locations/options') + return response.data +} + +export const getStorageLocationList = async (params: { + page?: number + pageSize?: number + keyword?: string + status?: string +}) => { + const response = await api.get>>('/storage-locations/list', { + params + }) + return response.data +} + +export const createStorageLocation = async (data: Partial) => { + const response = await api.post>('/storage-locations', data) + return response.data +} + +export const updateStorageLocation = async (id: number, data: Partial) => { + const response = await api.put>(`/storage-locations/${id}`, data) + return response.data +} + +export const updateStorageLocationStatus = async (id: number, status: 'active' | 'inactive') => { + const response = await api.patch>(`/storage-locations/${id}/status`, { + status + }) + return response.data +} + diff --git a/frontend/src/api/users.ts b/frontend/src/api/users.ts new file mode 100644 index 0000000..6f5ed13 --- /dev/null +++ b/frontend/src/api/users.ts @@ -0,0 +1,62 @@ +import api from './index' +import type { ApiResponse, PaginatedResponse, User } from '@/types' + +export interface UserListParams { + page?: number + pageSize?: number + keyword?: string + role?: string + status?: string +} + +export const getUserList = async (params: UserListParams = {}) => { + const response = await api.get>>('/users', { params }) + return response.data +} + +export const createUser = async (data: { + username: string + password: string + name: string + email?: string + phone?: string + roles?: string[] + role?: string + status?: string +}) => { + const response = await api.post>('/users', data) + return response.data +} + +export const updateUser = async ( + id: number | string, + data: { + name?: string + email?: string + phone?: string + roles?: string[] + role?: string + status?: string + } +) => { + const response = await api.put>(`/users/${id}`, data) + return response.data +} + +export const updateUserStatus = async (id: number | string, status: 'active' | 'inactive') => { + const response = await api.patch>(`/users/${id}/status`, { status }) + return response.data +} + +export const resetUserPassword = async (id: number | string, newPassword: string) => { + const response = await api.post>(`/users/${id}/reset-password`, { + new_password: newPassword + }) + return response.data +} + + +export const deleteUser = async (id: number | string) => { + const response = await api.delete>(`/users/${id}`) + return response.data +} diff --git a/frontend/src/assets/vue.svg b/frontend/src/assets/vue.svg new file mode 100644 index 0000000..770e9d3 --- /dev/null +++ b/frontend/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/auth/keycloak.ts b/frontend/src/auth/keycloak.ts new file mode 100644 index 0000000..e284ee5 --- /dev/null +++ b/frontend/src/auth/keycloak.ts @@ -0,0 +1,9 @@ +import Keycloak from 'keycloak-js' + +const keycloak = new Keycloak({ + url: import.meta.env.VITE_KEYCLOAK_URL || 'http://localhost:8080', + realm: import.meta.env.VITE_KEYCLOAK_REALM || 'equipment-management', + clientId: import.meta.env.VITE_KEYCLOAK_CLIENT_ID || 'equipment-client' +}) + +export default keycloak diff --git a/frontend/src/components/BarcodePrint.vue b/frontend/src/components/BarcodePrint.vue new file mode 100644 index 0000000..a55e816 --- /dev/null +++ b/frontend/src/components/BarcodePrint.vue @@ -0,0 +1,340 @@ + + + + + diff --git a/frontend/src/components/GlobalSearch.vue b/frontend/src/components/GlobalSearch.vue new file mode 100644 index 0000000..e8bedf6 --- /dev/null +++ b/frontend/src/components/GlobalSearch.vue @@ -0,0 +1,106 @@ + + + diff --git a/frontend/src/components/HelloWorld.vue b/frontend/src/components/HelloWorld.vue new file mode 100644 index 0000000..b58e52b --- /dev/null +++ b/frontend/src/components/HelloWorld.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/frontend/src/components/MountHistory.vue b/frontend/src/components/MountHistory.vue new file mode 100644 index 0000000..4cbbdf9 --- /dev/null +++ b/frontend/src/components/MountHistory.vue @@ -0,0 +1,122 @@ + + + diff --git a/frontend/src/components/MountManager.vue b/frontend/src/components/MountManager.vue new file mode 100644 index 0000000..0a863f7 --- /dev/null +++ b/frontend/src/components/MountManager.vue @@ -0,0 +1,208 @@ + + + diff --git a/frontend/src/components/StatCard.vue b/frontend/src/components/StatCard.vue new file mode 100644 index 0000000..b1c372e --- /dev/null +++ b/frontend/src/components/StatCard.vue @@ -0,0 +1,68 @@ + + + + + + diff --git a/frontend/src/layouts/MainLayout.vue b/frontend/src/layouts/MainLayout.vue new file mode 100644 index 0000000..3f65722 --- /dev/null +++ b/frontend/src/layouts/MainLayout.vue @@ -0,0 +1,96 @@ + + + + + + diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..f458d53 --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,92 @@ +import { createApp } from 'vue' +import { createPinia, setActivePinia } from 'pinia' +import App from './App.vue' +import router from './router' +import './style.css' +import keycloak from './auth/keycloak' +import { useAuthStore } from './stores/auth' +import api from './api' +import type { User } from './types' + +const bootstrap = async () => { + const app = createApp(App) + const pinia = createPinia() + app.use(pinia) + setActivePinia(pinia) + + const authStore = useAuthStore() + + const authMode = import.meta.env.VITE_AUTH_MODE || 'keycloak' + + if (authMode === 'local') { + localStorage.removeItem('token') + authStore.$patch({ + token: null, + user: null, + isAuthenticated: false + }) + try { + const resp = await api.get('/auth/user') + if (resp?.data?.data) { + const user = resp.data.data + const normalizedUser: User = { + ...user, + id: String(user.id ?? user.username ?? ''), + username: user.username || '', + name: user.name || user.username || '', + role: user.role || (user.roles?.includes('admin') ? 'admin' : 'user'), + roles: user.roles || [] + } + authStore.setUser(normalizedUser) + authStore.setToken('session') + } + } catch { + // ignore, user not logged in + } + + app.use(router) + app.mount('#app') + return + } + + try { + const authenticated = await keycloak.init({ + onLoad: 'login-required', + checkLoginIframe: false, + pkceMethod: 'S256' + }) + + if (!authenticated) { + await keycloak.login() + return + } + + if (keycloak.token) { + authStore.setToken(keycloak.token) + } + + const roles = (keycloak.tokenParsed?.realm_access?.roles as string[]) || [] + authStore.setUser({ + id: (keycloak.tokenParsed?.sub as string) || '', + username: (keycloak.tokenParsed?.preferred_username as string) || '', + name: (keycloak.tokenParsed?.name as string) || (keycloak.tokenParsed?.preferred_username as string) || '', + role: roles.includes('admin') ? 'admin' : 'user', + email: (keycloak.tokenParsed?.email as string) || '' + }) + + // 定期刷新token + setInterval(() => { + keycloak.updateToken(60).catch(() => { + keycloak.login() + }) + }, 60000) + + app.use(router) + app.mount('#app') + } catch (error) { + console.error('Keycloak 初始化失败:', error) + await keycloak.login() + } +} + +bootstrap() diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts new file mode 100644 index 0000000..a785035 --- /dev/null +++ b/frontend/src/router/index.ts @@ -0,0 +1,109 @@ +import { createRouter, createWebHistory } from 'vue-router' +import type { RouteRecordRaw } from 'vue-router' +import { useAuthStore } from '@/stores/auth' +import keycloak from '@/auth/keycloak' + +const routes: RouteRecordRaw[] = [ + { + path: '/login', + name: 'Login', + component: () => import('../views/Login.vue'), + meta: { requiresAuth: false } + }, + { + path: '/', + component: () => import('../layouts/MainLayout.vue'), + redirect: '/dashboard', + meta: { requiresAuth: true }, + children: [ + { + path: 'dashboard', + name: 'Dashboard', + component: () => import('../views/dashboard/Dashboard.vue'), + meta: { title: '首页' } + }, + { + path: 'equipment', + name: 'Equipment', + component: () => import('../views/equipment/EquipmentList.vue'), + meta: { title: '设备管理', requiresAdmin: true } + }, + { + path: 'equipment/add', + name: 'EquipmentAdd', + component: () => import('../views/equipment/EquipmentForm.vue'), + meta: { title: '新增设备', requiresAdmin: true } + }, + { + path: 'equipment/edit/:id', + name: 'EquipmentEdit', + component: () => import('../views/equipment/EquipmentForm.vue'), + meta: { title: '编辑设备', requiresAdmin: true } + }, + { + path: 'borrow', + name: 'Borrow', + component: () => import('../views/borrow/BorrowList.vue'), + meta: { title: '借用管理' } + }, + { + path: 'borrow/scan', + name: 'BorrowScan', + component: () => import('../views/borrow/BorrowScan.vue'), + meta: { title: '扫码出库' } + }, + { + path: 'return/scan', + name: 'ReturnScan', + component: () => import('../views/borrow/ReturnScan.vue'), + meta: { title: '扫码入库' } + }, + { + path: 'statistics', + name: 'Statistics', + component: () => import('../views/statistics/Statistics.vue'), + meta: { title: '统计分析' } + }, + { + path: 'settings', + name: 'Settings', + component: () => import('../views/settings/Settings.vue'), + meta: { title: '系统设置', requiresAdmin: true } + } + ] + } +] + +const router = createRouter({ + history: createWebHistory(), + routes +}) + +router.beforeEach((to, _from, next) => { + const authStore = useAuthStore() + const authMode = import.meta.env.VITE_AUTH_MODE || 'keycloak' + + if (to.path === '/login' && authStore.isAuthenticated) { + next('/dashboard') + return + } + + if (to.meta.requiresAuth && !authStore.isAuthenticated) { + if (authMode === 'local') { + next('/login') + } else { + keycloak.login() + } + return + } + + if (to.meta.requiresAdmin && authStore.user?.role !== 'admin') { + next('/dashboard') + return + } + + next() +}) + +export default router + diff --git a/frontend/src/shims-vue.d.ts b/frontend/src/shims-vue.d.ts new file mode 100644 index 0000000..c2fce74 --- /dev/null +++ b/frontend/src/shims-vue.d.ts @@ -0,0 +1,5 @@ +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent, Record, any> + export default component +} diff --git a/frontend/src/stores/auth.ts b/frontend/src/stores/auth.ts new file mode 100644 index 0000000..b67be07 --- /dev/null +++ b/frontend/src/stores/auth.ts @@ -0,0 +1,49 @@ +import { defineStore } from 'pinia' +import type { User } from '@/types' +import keycloak from '@/auth/keycloak' +import api from '@/api' + +interface AuthState { + user: User | null + token: string | null + isAuthenticated: boolean +} + +export const useAuthStore = defineStore('auth', { + state: (): AuthState => ({ + user: null, + token: localStorage.getItem('token'), + isAuthenticated: !!localStorage.getItem('token') + }), + + actions: { + setUser(user: User) { + this.user = user + this.isAuthenticated = true + }, + + setToken(token: string) { + this.token = token + this.isAuthenticated = true + localStorage.setItem('token', token) + }, + + async logout() { + this.user = null + this.token = null + this.isAuthenticated = false + localStorage.removeItem('token') + try { + const authMode = import.meta.env.VITE_AUTH_MODE || 'keycloak' + if (authMode === 'local') { + await api.post('/auth/logout') + } else if (keycloak.authenticated) { + await keycloak.logout() + } + } catch (error) { + console.error('Keycloak 登出失败:', error) + } + } + } +}) + diff --git a/frontend/src/stores/equipment.ts b/frontend/src/stores/equipment.ts new file mode 100644 index 0000000..55bbaf5 --- /dev/null +++ b/frontend/src/stores/equipment.ts @@ -0,0 +1,42 @@ +import { defineStore } from 'pinia' +import type { Equipment } from '@/types' +import { getEquipmentList } from '@/api/equipment' + +interface EquipmentState { + equipmentList: Equipment[] + total: number + loading: boolean +} + +export const useEquipmentStore = defineStore('equipment', { + state: (): EquipmentState => ({ + equipmentList: [], + total: 0, + loading: false + }), + + actions: { + async fetchEquipmentList(params?: { + page?: number + pageSize?: number + equipmentType?: string + status?: string + keyword?: string + storageLocationId?: number + }) { + this.loading = true + try { + const response = await getEquipmentList(params || {}) + if (response.data) { + this.equipmentList = response.data.list + this.total = response.data.total + } + } catch (error) { + console.error('获取设备列表失败:', error) + } finally { + this.loading = false + } + } + } +}) + diff --git a/frontend/src/stores/site.ts b/frontend/src/stores/site.ts new file mode 100644 index 0000000..fe6ce64 --- /dev/null +++ b/frontend/src/stores/site.ts @@ -0,0 +1,142 @@ +import { defineStore } from 'pinia' +import { getSiteConfig } from '@/api/settings' +import type { SiteConfigResponse } from '@/api/settings' + +const DEFAULT_SITE_NAME = '固定资产管理系统' +const DEFAULT_THEME_COLOR = '#409EFF' +const DEFAULT_LOGO_URL = 'https://s3.1080p.hk/shuzhou/2025/05/18/8ebadf0a7cc34eaea00e8619b8efb827.png' + +const normalizeHex = (color: string) => { + if (!color) return null + const hex = color.trim() + const shortHexMatch = /^#?[0-9A-Fa-f]{3}$/ + const longHexMatch = /^#?[0-9A-Fa-f]{6}$/ + if (!shortHexMatch.test(hex) && !longHexMatch.test(hex)) { + return null + } + const value = hex.startsWith('#') ? hex.slice(1) : hex + const expanded = + value.length === 3 + ? value + .split('') + .map((char) => char + char) + .join('') + : value + return `#${expanded.toUpperCase()}` +} + +const hexChannel = (hex: string, index: number) => parseInt(hex.slice(index, index + 2), 16) + +const mixChannel = (channel: number, amount: number, target: number) => + Math.round(channel + (target - channel) * amount) + +const lightenHex = (hex: string, amount: number) => { + const normalized = normalizeHex(hex) + if (!normalized) return hex + const r = hexChannel(normalized, 1) + const g = hexChannel(normalized, 3) + const b = hexChannel(normalized, 5) + const nr = mixChannel(r, amount, 255) + const ng = mixChannel(g, amount, 255) + const nb = mixChannel(b, amount, 255) + return `#${nr.toString(16).padStart(2, '0')}${ng.toString(16).padStart(2, '0')}${nb + .toString(16) + .padStart(2, '0')}`.toUpperCase() +} + +const darkenHex = (hex: string, amount: number) => { + const normalized = normalizeHex(hex) + if (!normalized) return hex + const r = hexChannel(normalized, 1) + const g = hexChannel(normalized, 3) + const b = hexChannel(normalized, 5) + const nr = mixChannel(r, amount, 0) + const ng = mixChannel(g, amount, 0) + const nb = mixChannel(b, amount, 0) + return `#${nr.toString(16).padStart(2, '0')}${ng.toString(16).padStart(2, '0')}${nb + .toString(16) + .padStart(2, '0')}`.toUpperCase() +} + +const hexToRgba = (hex: string, alpha: number) => { + const normalized = normalizeHex(hex) + if (!normalized) return `rgba(0, 122, 255, ${alpha})` + const r = hexChannel(normalized, 1) + const g = hexChannel(normalized, 3) + const b = hexChannel(normalized, 5) + return `rgba(${r}, ${g}, ${b}, ${alpha})` +} + +const applyThemeColor = (color: string) => { + if (typeof document === 'undefined') return + const normalized = normalizeHex(color) || DEFAULT_THEME_COLOR + const root = document.documentElement + root.style.setProperty('--theme-primary', normalized) + root.style.setProperty('--theme-primary-hover', lightenHex(normalized, 0.1)) + root.style.setProperty('--theme-primary-active', darkenHex(normalized, 0.15)) + root.style.setProperty('--theme-primary-ring', hexToRgba(normalized, 0.35)) +} + +interface SiteState { + siteName: string + logoUrl: string + faviconUrl: string + themeColor: string + loaded: boolean + updatedAt: string | null + updatedBy: SiteConfigResponse['updated_by'] +} + +export const useSiteStore = defineStore('site', { + state: (): SiteState => ({ + siteName: DEFAULT_SITE_NAME, + logoUrl: DEFAULT_LOGO_URL, + faviconUrl: '', + themeColor: DEFAULT_THEME_COLOR, + loaded: false, + updatedAt: null, + updatedBy: null + }), + actions: { + setSiteConfig(config: SiteConfigResponse) { + this.siteName = config.site_name || DEFAULT_SITE_NAME + this.logoUrl = config.logo_url || DEFAULT_LOGO_URL + this.faviconUrl = config.favicon_url || '' + this.themeColor = config.theme_color || DEFAULT_THEME_COLOR + this.updatedAt = config.updated_at + this.updatedBy = config.updated_by + this.loaded = true + applyThemeColor(this.themeColor) + if (typeof document !== 'undefined') { + document.title = this.siteName + if (this.faviconUrl) { + this.updateFavicon(this.faviconUrl) + } + } + }, + updateFavicon(iconUrl: string) { + if (typeof document === 'undefined') return + const link = + (document.querySelector("link[rel*='icon']") as HTMLLinkElement | null) || + document.createElement('link') + link.type = 'image/x-icon' + link.rel = 'shortcut icon' + link.href = iconUrl + if (!link.parentNode) { + document.getElementsByTagName('head')[0].appendChild(link) + } + }, + async fetchSiteConfig(force = false) { + if (this.loaded && !force) return + try { + const response = await getSiteConfig() + if (response.data) { + this.setSiteConfig(response.data) + } + } catch (error) { + console.warn('[site] 获取站点配置失败', error) + } + } + } +}) + diff --git a/frontend/src/style.css b/frontend/src/style.css new file mode 100644 index 0000000..5e40689 --- /dev/null +++ b/frontend/src/style.css @@ -0,0 +1,190 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* Apple Design System Base Styles */ +@layer base { + :root { + --theme-primary: #007AFF; + --theme-primary-hover: #2F8CFF; + --theme-primary-active: #0063CC; + --theme-primary-ring: rgba(0, 122, 255, 0.35); + } + + * { + @apply antialiased; + } + + body { + @apply bg-system-background-secondary text-system-label-primary font-sans; + font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', Helvetica, Arial, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + + h1, h2, h3, h4, h5, h6 { + @apply font-semibold text-system-label-primary; + } + + h1 { + @apply text-3xl; + } + + h2 { + @apply text-2xl; + } + + h3 { + @apply text-xl; + } +} + +/* Apple Design System Components */ +@layer components { + /* Apple Button Styles */ + .btn-apple { + @apply px-4 py-2 rounded-apple font-medium text-sm transition-all duration-200; + @apply active:scale-95; +} + + .btn-apple-primary { + @apply btn-apple text-white; + background-color: var(--theme-primary); + } + .btn-apple-primary:hover { + background-color: var(--theme-primary-hover); + } + .btn-apple-primary:active { + background-color: var(--theme-primary-active); + } + .btn-apple-primary:disabled { + @apply opacity-60 cursor-not-allowed; + } + + .btn-apple-secondary { + @apply btn-apple bg-system-background-primary text-system-label-primary border border-system-gray-5; + @apply hover:bg-system-gray-6; + @apply active:bg-system-gray-5; + } + + .btn-apple-danger { + @apply btn-apple bg-apple-red text-white; + @apply hover:bg-opacity-90; + @apply active:bg-opacity-80; +} + + /* Apple Card Styles */ + .card-apple { + @apply bg-system-background-primary rounded-apple-lg shadow-apple p-6; + @apply border border-system-gray-5; + } + + .card-apple-elevated { + @apply card-apple shadow-apple-lg; +} + + /* Apple Input Styles */ + .input-apple { + @apply w-full px-4 py-3 rounded-apple border border-system-gray-5 bg-system-background-primary; + @apply text-system-label-primary placeholder-system-label-quaternary; + @apply focus:outline-none; + @apply transition-all duration-200; + box-shadow: 0 0 0 0 transparent; + } + + .input-apple:focus { + border-color: transparent; + box-shadow: 0 0 0 2px var(--theme-primary); + } + + /* Apple Glass Effect */ + .glass-apple { + @apply bg-white bg-opacity-80 backdrop-blur-apple; + @apply border border-white border-opacity-20; + } + + /* Apple Navigation Bar */ + .nav-apple { + @apply bg-system-background-primary border-b border-system-gray-5; + @apply backdrop-blur-apple bg-opacity-80; + } + + /* Apple Tab Bar */ + .tab-bar-apple { + @apply bg-system-background-primary border-t border-system-gray-5; + @apply backdrop-blur-apple bg-opacity-80; +} +} + +/* Apple Design System Utilities */ +@layer utilities { + .text-apple-large-title { + @apply text-4xl font-bold; +} + + .text-apple-title { + @apply text-2xl font-semibold; + } + + .text-apple-title-2 { + @apply text-xl font-semibold; + } + + .text-apple-title-3 { + @apply text-lg font-semibold; + } + + .text-apple-headline { + @apply text-base font-semibold; +} + + .text-apple-body { + @apply text-base font-normal; +} + + .text-apple-callout { + @apply text-base font-medium; + } + + .text-apple-subhead { + @apply text-sm font-normal; + } + + .text-apple-footnote { + @apply text-xs font-normal; + } + + .text-apple-caption { + @apply text-xs font-normal; +} +} + +/* Custom Scrollbar - Apple Style */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + @apply bg-system-background-secondary; +} + +::-webkit-scrollbar-thumb { + @apply bg-system-gray-4 rounded-full; + } + +::-webkit-scrollbar-thumb:hover { + @apply bg-system-gray-3; +} + +/* Focus Ring - Apple Style */ +*:focus-visible { + outline: none; + box-shadow: 0 0 0 2px var(--theme-primary); +} + +/* Selection - Apple Style */ +::selection { + background-color: var(--theme-primary); + color: #ffffff; +} diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts new file mode 100644 index 0000000..d57a962 --- /dev/null +++ b/frontend/src/types/index.ts @@ -0,0 +1,231 @@ +// 资产类型(动态加载,不再硬编码) +export type EquipmentType = string + +// 资产状态 +export type EquipmentStatus = '可用' | '已借出' | '维护中' | '已损坏' | '待报废' | '已报废' + +// 编码生成方式 +export type CodeGenerationMode = 'auto' | 'manual' + +export interface StorageLocation { + id: number + name: string + code?: string | null + status: 'active' | 'inactive' + description?: string | null + created_at: string + updated_at: string + _count?: { + equipments: number + } +} + +// 资产类型接口 +export interface AssetType { + id: number + name: string + short_name?: string | null // 简称,用于标签编码 + status: 'active' | 'inactive' + can_be_parent?: boolean + can_be_child?: boolean + created_at: string + updated_at: string + _count?: { + equipments: number + } +} + +// 使用部门接口 +export interface Department { + id: number + name: string + status: 'active' | 'inactive' + parent_id?: number | null + parent_name?: string | null + level?: number // 0=根部门, 1=子部门 + parent?: { + id: number + name: string + } | null + created_at: string + updated_at: string + _count?: { + equipments: number + children?: number + } + equipment_count?: number + children_count?: number +} + +// 部门树形结构节点 +export interface DepartmentTreeNode { + id: number + name: string + status: 'active' | 'inactive' + parent_id: number | null + equipment_count: number + children_count: number + children: DepartmentTreeNode[] + created_at: string + updated_at: string +} + +// 设备接口 +export interface Equipment { + id: number + equipment_code: string + equipment_name: string + equipment_model?: string + equipment_type: EquipmentType + code_generation_mode: CodeGenerationMode + status: EquipmentStatus + total_quantity: number + remaining_quantity: number + unit_price: number + total_value: number + borrowed_value: number + available_value: number + description?: string + purchase_date?: string + supplier?: string + storage_location_id?: number | null + storageLocation?: StorageLocation | null + department_id?: number | null + department?: Department | null + asset_type_id?: number | null + assetType?: AssetType | null + // 父子资产关联字段 + parent_id?: number | null + mount_status?: 'none' | 'mounted' | 'unmounted' + total_value_with_children?: number + parent?: { + id: number + equipment_code: string + equipment_name: string + equipment_model?: string + unit_price?: number + } | null + children?: Array<{ + id: number + equipment_code: string + equipment_name: string + equipment_model?: string + equipment_type?: string + unit_price?: number + status?: string + mount_status?: string + }> + created_at: string + updated_at: string +} + +// 借用记录接口 +export interface Person { + id: number + nickname: string + real_name: string + id_number?: string + department?: string + position?: string + phone?: string + email?: string + status: 'active' | 'inactive' + remarks?: string + created_at: string + updated_at: string +} + +export interface BorrowRecord { + id: number + equipment_id: number + equipment_code: string + equipment_name?: string + storage_location_id?: number | null + storageLocation?: StorageLocation | null + person_id: number + person_name: string + person_nickname: string + person_identifier?: string + person?: Person + borrow_time: string + expected_return_time?: string + is_long_term?: boolean + return_time?: string + status: '已借出' | '已归还' | '逾期' + return_condition?: '正常' | '损坏' | '丢失' + notes?: string + created_at: string + updated_at: string +} + +// 用户接口 +export interface User { + id: number | string + username: string + name: string + role: 'admin' | 'user' + email?: string + phone?: string + roles?: string[] +} + +// API响应接口 +export interface ApiResponse { + code: number + message: string + data: T +} + +// 分页响应接口 +export interface PaginatedResponse { + list: T[] + total: number + page: number + pageSize: number +} + +// 统计数据接口 +export interface DashboardStats { + total_equipment: number + available_equipment: number + borrowed_equipment: number + maintenance_equipment: number + usage_rate: number + today_borrow: number + today_return: number + total_value: number + borrowed_value: number + available_value: number + maintenance_value: number + value_usage_rate: number + today_borrow_value: number + today_return_value: number + inventory_by_type: Array<{ + equipment_type: string + total_quantity: number + available_quantity: number + borrowed_quantity: number + }> +} + +// 资产价值统计接口 +export interface ValueStatistics { + total_value: number + borrowed_value: number + available_value: number + maintenance_value: number + value_by_type: Array<{ + equipment_type: string + total_value: number + borrowed_value: number + available_value: number + }> + top_equipment: Array<{ + equipment_name: string + unit_price: number + total_value: number + borrowed_value: number + available_value: number + }> +} + diff --git a/frontend/src/utils/format.ts b/frontend/src/utils/format.ts new file mode 100644 index 0000000..0be1082 --- /dev/null +++ b/frontend/src/utils/format.ts @@ -0,0 +1,86 @@ +// 格式化金额 +export const formatCurrency = (amount: number, options?: { fractionDigits?: number }): string => { + const fractionDigits = options?.fractionDigits ?? 2 + return new Intl.NumberFormat('zh-CN', { + style: 'currency', + currency: 'CNY', + minimumFractionDigits: fractionDigits, + maximumFractionDigits: fractionDigits + }).format(amount) +} + +export const formatCurrencyNoFraction = (amount: number): string => { + return formatCurrency(amount, { fractionDigits: 0 }) +} + +// 格式化数字(千分位) +export const formatNumber = (num: number): string => { + return new Intl.NumberFormat('zh-CN').format(num) +} + +// 格式化日期 +export const formatDate = (date: string | Date): string => { + const d = typeof date === 'string' ? new Date(date) : date + return new Intl.DateTimeFormat('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit' + }).format(d) +} + +// 格式化日期时间 +export const formatDateTime = (date: string | Date): string => { + const d = typeof date === 'string' ? new Date(date) : date + return new Intl.DateTimeFormat('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }).format(d) +} + +// 格式化百分比 +export const formatPercent = (value: number): string => { + return `${value.toFixed(1)}%` +} + +// 播放提示音 +export const playBeepSound = () => { + // 使用Web Audio API生成提示音 + const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)() + const oscillator = audioContext.createOscillator() + const gainNode = audioContext.createGain() + + oscillator.connect(gainNode) + gainNode.connect(audioContext.destination) + + oscillator.frequency.value = 800 + oscillator.type = 'sine' + + gainNode.gain.setValueAtTime(0.3, audioContext.currentTime) + gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1) + + oscillator.start(audioContext.currentTime) + oscillator.stop(audioContext.currentTime + 0.1) +} + +// 播放错误提示音 +export const playErrorSound = () => { + const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)() + const oscillator = audioContext.createOscillator() + const gainNode = audioContext.createGain() + + oscillator.connect(gainNode) + gainNode.connect(audioContext.destination) + + oscillator.frequency.value = 400 + oscillator.type = 'sawtooth' + + gainNode.gain.setValueAtTime(0.3, audioContext.currentTime) + gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.2) + + oscillator.start(audioContext.currentTime) + oscillator.stop(audioContext.currentTime + 0.2) +} + diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue new file mode 100644 index 0000000..b1a3fa3 --- /dev/null +++ b/frontend/src/views/Login.vue @@ -0,0 +1,170 @@ + + + + diff --git a/frontend/src/views/borrow/BorrowList.vue b/frontend/src/views/borrow/BorrowList.vue new file mode 100644 index 0000000..77af803 --- /dev/null +++ b/frontend/src/views/borrow/BorrowList.vue @@ -0,0 +1,152 @@ + + + + diff --git a/frontend/src/views/borrow/BorrowScan.vue b/frontend/src/views/borrow/BorrowScan.vue new file mode 100644 index 0000000..438a82e --- /dev/null +++ b/frontend/src/views/borrow/BorrowScan.vue @@ -0,0 +1,415 @@ + + + + diff --git a/frontend/src/views/borrow/ReturnScan.vue b/frontend/src/views/borrow/ReturnScan.vue new file mode 100644 index 0000000..4f4745f --- /dev/null +++ b/frontend/src/views/borrow/ReturnScan.vue @@ -0,0 +1,374 @@ + + + + diff --git a/frontend/src/views/dashboard/Dashboard.vue b/frontend/src/views/dashboard/Dashboard.vue new file mode 100644 index 0000000..38b081f --- /dev/null +++ b/frontend/src/views/dashboard/Dashboard.vue @@ -0,0 +1,282 @@ + + + + + + diff --git a/frontend/src/views/equipment/EquipmentForm.vue b/frontend/src/views/equipment/EquipmentForm.vue new file mode 100644 index 0000000..d47b7fe --- /dev/null +++ b/frontend/src/views/equipment/EquipmentForm.vue @@ -0,0 +1,625 @@ + + + + diff --git a/frontend/src/views/equipment/EquipmentList.vue b/frontend/src/views/equipment/EquipmentList.vue new file mode 100644 index 0000000..865754e --- /dev/null +++ b/frontend/src/views/equipment/EquipmentList.vue @@ -0,0 +1,540 @@ + + + diff --git a/frontend/src/views/settings/Settings.vue b/frontend/src/views/settings/Settings.vue new file mode 100644 index 0000000..c41b96e --- /dev/null +++ b/frontend/src/views/settings/Settings.vue @@ -0,0 +1,89 @@ + + + + + + diff --git a/frontend/src/views/settings/components/AssetTypeManagement.vue b/frontend/src/views/settings/components/AssetTypeManagement.vue new file mode 100644 index 0000000..7a93458 --- /dev/null +++ b/frontend/src/views/settings/components/AssetTypeManagement.vue @@ -0,0 +1,339 @@ + + + diff --git a/frontend/src/views/settings/components/DepartmentManagement.vue b/frontend/src/views/settings/components/DepartmentManagement.vue new file mode 100644 index 0000000..6dfc4e2 --- /dev/null +++ b/frontend/src/views/settings/components/DepartmentManagement.vue @@ -0,0 +1,416 @@ + + + diff --git a/frontend/src/views/settings/components/DisposalManagement.vue b/frontend/src/views/settings/components/DisposalManagement.vue new file mode 100644 index 0000000..a210606 --- /dev/null +++ b/frontend/src/views/settings/components/DisposalManagement.vue @@ -0,0 +1,200 @@ + + + diff --git a/frontend/src/views/settings/components/MaintenanceManagement.vue b/frontend/src/views/settings/components/MaintenanceManagement.vue new file mode 100644 index 0000000..9cd345d --- /dev/null +++ b/frontend/src/views/settings/components/MaintenanceManagement.vue @@ -0,0 +1,200 @@ + + + diff --git a/frontend/src/views/settings/components/OperationLogList.vue b/frontend/src/views/settings/components/OperationLogList.vue new file mode 100644 index 0000000..c23b4af --- /dev/null +++ b/frontend/src/views/settings/components/OperationLogList.vue @@ -0,0 +1,184 @@ + + + diff --git a/frontend/src/views/settings/components/PersonManagement.vue b/frontend/src/views/settings/components/PersonManagement.vue new file mode 100644 index 0000000..304d0e8 --- /dev/null +++ b/frontend/src/views/settings/components/PersonManagement.vue @@ -0,0 +1,351 @@ + + + diff --git a/frontend/src/views/settings/components/SiteSettings.vue b/frontend/src/views/settings/components/SiteSettings.vue new file mode 100644 index 0000000..0c6a9a7 --- /dev/null +++ b/frontend/src/views/settings/components/SiteSettings.vue @@ -0,0 +1,135 @@ + + + + diff --git a/frontend/src/views/settings/components/StorageLocationManagement.vue b/frontend/src/views/settings/components/StorageLocationManagement.vue new file mode 100644 index 0000000..5a2ff18 --- /dev/null +++ b/frontend/src/views/settings/components/StorageLocationManagement.vue @@ -0,0 +1,307 @@ + + + + diff --git a/frontend/src/views/settings/components/UserManagement.vue b/frontend/src/views/settings/components/UserManagement.vue new file mode 100644 index 0000000..133c388 --- /dev/null +++ b/frontend/src/views/settings/components/UserManagement.vue @@ -0,0 +1,326 @@ + + + + diff --git a/frontend/src/views/statistics/Statistics.vue b/frontend/src/views/statistics/Statistics.vue new file mode 100644 index 0000000..e811932 --- /dev/null +++ b/frontend/src/views/statistics/Statistics.vue @@ -0,0 +1,126 @@ + + + + diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..6a7a02f --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1,11 @@ +/// + +interface ImportMetaEnv { + readonly VITE_API_BASE_URL: string + // 更多环境变量... +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} + diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..f8ed6c0 --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,76 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{vue,js,ts,jsx,tsx}", + ], + theme: { + extend: { + colors: { + // Apple Design System Colors + apple: { + blue: '#007AFF', + green: '#34C759', + indigo: '#5856D6', + orange: '#FF9500', + pink: '#FF2D55', + purple: '#AF52DE', + red: '#FF3B30', + teal: '#5AC8FA', + yellow: '#FFCC00', + }, + // System Colors + system: { + gray: { + 1: '#8E8E93', + 2: '#AEAEB2', + 3: '#C7C7CC', + 4: '#D1D1D6', + 5: '#E5E5EA', + 6: '#F2F2F7', + }, + background: { + primary: '#FFFFFF', + secondary: '#F2F2F7', + tertiary: '#FFFFFF', + }, + label: { + primary: '#000000', + secondary: '#3C3C43', + tertiary: '#3C3C4399', + quaternary: '#3C3C434D', + }, + }, + }, + fontFamily: { + sans: [ + '-apple-system', + 'BlinkMacSystemFont', + '"SF Pro Display"', + '"SF Pro Text"', + '"Helvetica Neue"', + 'Helvetica', + 'Arial', + 'sans-serif', + ], + }, + borderRadius: { + 'apple': '10px', + 'apple-lg': '14px', + 'apple-xl': '20px', + }, + boxShadow: { + 'apple': '0 2px 8px rgba(0, 0, 0, 0.1)', + 'apple-lg': '0 4px 16px rgba(0, 0, 0, 0.15)', + 'apple-xl': '0 8px 32px rgba(0, 0, 0, 0.2)', + }, + backdropBlur: { + 'apple': '20px', + }, + }, + }, + plugins: [ + require('@tailwindcss/forms'), + ], +} + diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..8d16e42 --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,16 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "types": ["vite/client"], + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..16a4c7c --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + /* Path Aliases */ + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..b3fe13b --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "composite": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..3bc56f3 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,30 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { resolve } from 'path' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': resolve(__dirname, 'src') + } + }, + server: { + host: '0.0.0.0', + port: 5173, + strictPort: true, + hmr: { + host: '192.168.150.22', + port: 5173, + clientPort: 5173, + protocol: 'ws' + }, + proxy: { + '/api': { + target: 'http://localhost:3000', + changeOrigin: true + } + } + } +})