添加后台启动脚本和修改域名
This commit is contained in:
23
.cursor/commands/openspec-apply.md
Normal file
23
.cursor/commands/openspec-apply.md
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: /openspec-apply
|
||||
id: openspec-apply
|
||||
category: OpenSpec
|
||||
description: Implement an approved OpenSpec change and keep tasks in sync.
|
||||
---
|
||||
<!-- OPENSPEC:START -->
|
||||
**Guardrails**
|
||||
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
|
||||
- Keep changes tightly scoped to the requested outcome.
|
||||
- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
|
||||
|
||||
**Steps**
|
||||
Track these steps as TODOs and complete them one by one.
|
||||
1. Read `changes/<id>/proposal.md`, `design.md` (if present), and `tasks.md` to confirm scope and acceptance criteria.
|
||||
2. Work through tasks sequentially, keeping edits minimal and focused on the requested change.
|
||||
3. Confirm completion before updating statuses—make sure every item in `tasks.md` is finished.
|
||||
4. Update the checklist after all work is done so each task is marked `- [x]` and reflects reality.
|
||||
5. Reference `openspec list` or `openspec show <item>` when additional context is required.
|
||||
|
||||
**Reference**
|
||||
- Use `openspec show <id> --json --deltas-only` if you need additional context from the proposal while implementing.
|
||||
<!-- OPENSPEC:END -->
|
||||
27
.cursor/commands/openspec-archive.md
Normal file
27
.cursor/commands/openspec-archive.md
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: /openspec-archive
|
||||
id: openspec-archive
|
||||
category: OpenSpec
|
||||
description: Archive a deployed OpenSpec change and update specs.
|
||||
---
|
||||
<!-- OPENSPEC:START -->
|
||||
**Guardrails**
|
||||
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
|
||||
- Keep changes tightly scoped to the requested outcome.
|
||||
- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
|
||||
|
||||
**Steps**
|
||||
1. Determine the change ID to archive:
|
||||
- If this prompt already includes a specific change ID (for example inside a `<ChangeId>` block populated by slash-command arguments), use that value after trimming whitespace.
|
||||
- If the conversation references a change loosely (for example by title or summary), run `openspec list` to surface likely IDs, share the relevant candidates, and confirm which one the user intends.
|
||||
- Otherwise, review the conversation, run `openspec list`, and ask the user which change to archive; wait for a confirmed change ID before proceeding.
|
||||
- If you still cannot identify a single change ID, stop and tell the user you cannot archive anything yet.
|
||||
2. Validate the change ID by running `openspec list` (or `openspec show <id>`) and stop if the change is missing, already archived, or otherwise not ready to archive.
|
||||
3. Run `openspec archive <id> --yes` so the CLI moves the change and applies spec updates without prompts (use `--skip-specs` only for tooling-only work).
|
||||
4. Review the command output to confirm the target specs were updated and the change landed in `changes/archive/`.
|
||||
5. Validate with `openspec validate --strict` and inspect with `openspec show <id>` if anything looks off.
|
||||
|
||||
**Reference**
|
||||
- Use `openspec list` to confirm change IDs before archiving.
|
||||
- Inspect refreshed specs with `openspec list --specs` and address any validation issues before handing off.
|
||||
<!-- OPENSPEC:END -->
|
||||
27
.cursor/commands/openspec-proposal.md
Normal file
27
.cursor/commands/openspec-proposal.md
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: /openspec-proposal
|
||||
id: openspec-proposal
|
||||
category: OpenSpec
|
||||
description: Scaffold a new OpenSpec change and validate strictly.
|
||||
---
|
||||
<!-- OPENSPEC:START -->
|
||||
**Guardrails**
|
||||
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
|
||||
- Keep changes tightly scoped to the requested outcome.
|
||||
- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
|
||||
- Identify any vague or ambiguous details and ask the necessary follow-up questions before editing files.
|
||||
|
||||
**Steps**
|
||||
1. Review `openspec/project.md`, run `openspec list` and `openspec list --specs`, and inspect related code or docs (e.g., via `rg`/`ls`) to ground the proposal in current behaviour; note any gaps that require clarification.
|
||||
2. Choose a unique verb-led `change-id` and scaffold `proposal.md`, `tasks.md`, and `design.md` (when needed) under `openspec/changes/<id>/`.
|
||||
3. Map the change into concrete capabilities or requirements, breaking multi-scope efforts into distinct spec deltas with clear relationships and sequencing.
|
||||
4. Capture architectural reasoning in `design.md` when the solution spans multiple systems, introduces new patterns, or demands trade-off discussion before committing to specs.
|
||||
5. Draft spec deltas in `changes/<id>/specs/<capability>/spec.md` (one folder per capability) using `## ADDED|MODIFIED|REMOVED Requirements` with at least one `#### Scenario:` per requirement and cross-reference related capabilities when relevant.
|
||||
6. Draft `tasks.md` as an ordered list of small, verifiable work items that deliver user-visible progress, include validation (tests, tooling), and highlight dependencies or parallelizable work.
|
||||
7. Validate with `openspec validate <id> --strict` and resolve every issue before sharing the proposal.
|
||||
|
||||
**Reference**
|
||||
- Use `openspec show <id> --json --deltas-only` or `openspec show <spec> --type spec` to inspect details when validation fails.
|
||||
- Search existing requirements with `rg -n "Requirement:|Scenario:" openspec/specs` before writing new ones.
|
||||
- Explore the codebase with `rg <keyword>`, `ls`, or direct file reads so proposals align with current implementation realities.
|
||||
<!-- OPENSPEC:END -->
|
||||
18
AGENTS.md
Normal file
18
AGENTS.md
Normal file
@@ -0,0 +1,18 @@
|
||||
<!-- OPENSPEC:START -->
|
||||
# OpenSpec Instructions
|
||||
|
||||
These instructions are for AI assistants working in this project.
|
||||
|
||||
Always open `@/openspec/AGENTS.md` when the request:
|
||||
- Mentions planning or proposals (words like proposal, spec, change, plan)
|
||||
- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work
|
||||
- Sounds ambiguous and you need the authoritative spec before coding
|
||||
|
||||
Use `@/openspec/AGENTS.md` to learn:
|
||||
- How to create and apply change proposals
|
||||
- Spec format and conventions
|
||||
- Project structure and guidelines
|
||||
|
||||
Keep this managed block so 'openspec update' can refresh the instructions.
|
||||
|
||||
<!-- OPENSPEC:END -->
|
||||
457
admin-system/package-lock.json
generated
457
admin-system/package-lock.json
generated
@@ -19,6 +19,7 @@
|
||||
"moment": "^2.29.4",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.1.7",
|
||||
"qrcode": "^1.5.4",
|
||||
"vue": "^3.4.15",
|
||||
"vue-router": "^4.2.5",
|
||||
"xlsx": "^0.18.5"
|
||||
@@ -1464,7 +1465,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -1473,7 +1473,6 @@
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
@@ -1672,6 +1671,15 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/camelcase": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz",
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/cfb": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/cfb/-/cfb-1.2.2.tgz",
|
||||
@@ -1730,6 +1738,51 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/cliui/-/cliui-6.0.0.tgz",
|
||||
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^6.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cliui/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/codepage": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmmirror.com/codepage/-/codepage-1.15.0.tgz",
|
||||
@@ -1742,7 +1795,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
@@ -1753,8 +1805,7 @@
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
@@ -1876,6 +1927,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-eql": {
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmmirror.com/deep-eql/-/deep-eql-4.1.4.tgz",
|
||||
@@ -1957,6 +2017,12 @@
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dijkstrajs": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dir-glob": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||
@@ -2614,6 +2680,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-func-name": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/get-func-name/-/get-func-name-2.0.2.tgz",
|
||||
@@ -2926,7 +3001,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -3548,6 +3622,15 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
@@ -3576,7 +3659,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -3694,6 +3776,15 @@
|
||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/pngjs": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/pngjs/-/pngjs-5.0.0.tgz",
|
||||
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz",
|
||||
@@ -3783,6 +3874,23 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmmirror.com/qrcode/-/qrcode-1.5.4.tgz",
|
||||
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dijkstrajs": "^1.0.1",
|
||||
"pngjs": "^5.0.0",
|
||||
"yargs": "^15.3.1"
|
||||
},
|
||||
"bin": {
|
||||
"qrcode": "bin/qrcode"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
@@ -3809,6 +3917,21 @@
|
||||
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-main-filename": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/resize-observer-polyfill": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
||||
@@ -4017,6 +4140,12 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/shallow-equal": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/shallow-equal/-/shallow-equal-1.2.1.tgz",
|
||||
@@ -4193,7 +4322,6 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
@@ -4855,6 +4983,12 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/which-module": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/which-module/-/which-module-2.0.1.tgz",
|
||||
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/why-is-node-running": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||
@@ -5025,6 +5159,119 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/y18n/-/y18n-4.0.3.tgz",
|
||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "15.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/yargs/-/yargs-15.4.1.tgz",
|
||||
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^6.0.0",
|
||||
"decamelize": "^1.2.0",
|
||||
"find-up": "^4.1.0",
|
||||
"get-caller-file": "^2.0.1",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^2.0.0",
|
||||
"set-blocking": "^2.0.0",
|
||||
"string-width": "^4.2.0",
|
||||
"which-module": "^2.0.0",
|
||||
"y18n": "^4.0.0",
|
||||
"yargs-parser": "^18.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "18.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"camelcase": "^5.0.0",
|
||||
"decamelize": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/yargs/node_modules/find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"locate-path": "^5.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-locate": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-try": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/p-locate": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-limit": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
@@ -5987,14 +6234,12 @@
|
||||
"ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
@@ -6147,6 +6392,11 @@
|
||||
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
|
||||
"dev": true
|
||||
},
|
||||
"camelcase": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz",
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
|
||||
},
|
||||
"cfb": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/cfb/-/cfb-1.2.2.tgz",
|
||||
@@ -6190,6 +6440,43 @@
|
||||
"get-func-name": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"cliui": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/cliui/-/cliui-6.0.0.tgz",
|
||||
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||
"requires": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^6.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||
},
|
||||
"string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"requires": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
}
|
||||
},
|
||||
"wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"requires": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"codepage": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmmirror.com/codepage/-/codepage-1.15.0.tgz",
|
||||
@@ -6199,7 +6486,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
@@ -6207,8 +6493,7 @@
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.8",
|
||||
@@ -6299,6 +6584,11 @@
|
||||
"ms": "^2.1.3"
|
||||
}
|
||||
},
|
||||
"decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="
|
||||
},
|
||||
"deep-eql": {
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmmirror.com/deep-eql/-/deep-eql-4.1.4.tgz",
|
||||
@@ -6353,6 +6643,11 @@
|
||||
"integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==",
|
||||
"dev": true
|
||||
},
|
||||
"dijkstrajs": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="
|
||||
},
|
||||
"dir-glob": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||
@@ -6847,6 +7142,11 @@
|
||||
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
|
||||
},
|
||||
"get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
|
||||
},
|
||||
"get-func-name": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/get-func-name/-/get-func-name-2.0.2.tgz",
|
||||
@@ -7060,8 +7360,7 @@
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
|
||||
},
|
||||
"is-glob": {
|
||||
"version": "4.0.3",
|
||||
@@ -7516,6 +7815,11 @@
|
||||
"p-limit": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
|
||||
},
|
||||
"package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
@@ -7540,8 +7844,7 @@
|
||||
"path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"dev": true
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="
|
||||
},
|
||||
"path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
@@ -7622,6 +7925,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"pngjs": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/pngjs/-/pngjs-5.0.0.tgz",
|
||||
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="
|
||||
},
|
||||
"postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz",
|
||||
@@ -7678,6 +7986,16 @@
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"dev": true
|
||||
},
|
||||
"qrcode": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmmirror.com/qrcode/-/qrcode-1.5.4.tgz",
|
||||
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
||||
"requires": {
|
||||
"dijkstrajs": "^1.0.1",
|
||||
"pngjs": "^5.0.0",
|
||||
"yargs": "^15.3.1"
|
||||
}
|
||||
},
|
||||
"queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
@@ -7690,6 +8008,16 @@
|
||||
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||
"dev": true
|
||||
},
|
||||
"require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="
|
||||
},
|
||||
"require-main-filename": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
|
||||
},
|
||||
"resize-observer-polyfill": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
||||
@@ -7824,6 +8152,11 @@
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"dev": true
|
||||
},
|
||||
"set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
|
||||
},
|
||||
"shallow-equal": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/shallow-equal/-/shallow-equal-1.2.1.tgz",
|
||||
@@ -7955,7 +8288,6 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
}
|
||||
@@ -8365,6 +8697,11 @@
|
||||
"isexe": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"which-module": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/which-module/-/which-module-2.0.1.tgz",
|
||||
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="
|
||||
},
|
||||
"why-is-node-running": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||
@@ -8481,6 +8818,88 @@
|
||||
"integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
|
||||
"dev": true
|
||||
},
|
||||
"y18n": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/y18n/-/y18n-4.0.3.tgz",
|
||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="
|
||||
},
|
||||
"yargs": {
|
||||
"version": "15.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/yargs/-/yargs-15.4.1.tgz",
|
||||
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||
"requires": {
|
||||
"cliui": "^6.0.0",
|
||||
"decamelize": "^1.2.0",
|
||||
"find-up": "^4.1.0",
|
||||
"get-caller-file": "^2.0.1",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^2.0.0",
|
||||
"set-blocking": "^2.0.0",
|
||||
"string-width": "^4.2.0",
|
||||
"which-module": "^2.0.0",
|
||||
"y18n": "^4.0.0",
|
||||
"yargs-parser": "^18.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||
},
|
||||
"find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"requires": {
|
||||
"locate-path": "^5.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"requires": {
|
||||
"p-locate": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"requires": {
|
||||
"p-try": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"p-locate": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"requires": {
|
||||
"p-limit": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"requires": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"yargs-parser": {
|
||||
"version": "18.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||
"requires": {
|
||||
"camelcase": "^5.0.0",
|
||||
"decamelize": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
|
||||
@@ -33,37 +33,38 @@
|
||||
"deploy": "npm run build && npm run preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons-vue": "^7.0.1",
|
||||
"ant-design-vue": "^4.0.6",
|
||||
"axios": "^1.6.2",
|
||||
"dayjs": "^1.11.10",
|
||||
"echarts": "^5.4.3",
|
||||
"file-saver": "^2.0.5",
|
||||
"lodash-es": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.1.7",
|
||||
"qrcode": "^1.5.4",
|
||||
"vue": "^3.4.15",
|
||||
"vue-router": "^4.2.5",
|
||||
"xlsx": "^0.18.5",
|
||||
"@ant-design/icons-vue": "^7.0.1",
|
||||
"dayjs": "^1.11.10",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nprogress": "^0.2.0"
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.6.2",
|
||||
"vite": "^4.5.3",
|
||||
"@types/node": "^16.18.68",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^16.18.68",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"@vitejs/plugin-vue": "^4.6.2",
|
||||
"@vitest/coverage-v8": "^0.34.6",
|
||||
"@vitest/ui": "^0.34.6",
|
||||
"@vue/eslint-config-typescript": "^11.0.3",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-plugin-vue": "^9.19.2",
|
||||
"rimraf": "^5.0.5",
|
||||
"typescript": "^4.9.5",
|
||||
"vite": "^4.5.3",
|
||||
"vite-bundle-analyzer": "^0.7.0",
|
||||
"vitest": "^0.34.6",
|
||||
"@vitest/ui": "^0.34.6",
|
||||
"@vitest/coverage-v8": "^0.34.6",
|
||||
"vue-tsc": "^1.8.25"
|
||||
}
|
||||
}
|
||||
|
||||
3426
admin-system/pnpm-lock.yaml
generated
Normal file
3426
admin-system/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -31,7 +31,13 @@ const createHeaders = (headers = {}) => {
|
||||
throw new Error('未认证,请先登录');
|
||||
}
|
||||
|
||||
return { ...defaultHeaders, ...headers };
|
||||
// 如果headers中明确设置了Content-Type为undefined,则移除它
|
||||
const mergedHeaders = { ...defaultHeaders, ...headers };
|
||||
if (mergedHeaders['Content-Type'] === undefined) {
|
||||
delete mergedHeaders['Content-Type'];
|
||||
}
|
||||
|
||||
return mergedHeaders;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -342,16 +348,26 @@ export const api = {
|
||||
/**
|
||||
* POST请求
|
||||
* @param {string} endpoint - API端点
|
||||
* @param {Object} data - 请求数据
|
||||
* @param {Object|FormData} data - 请求数据
|
||||
* @param {Object} options - 请求选项
|
||||
* @returns {Promise} 响应数据
|
||||
*/
|
||||
async post(endpoint, data, options = {}) {
|
||||
const url = `${API_BASE_URL}${endpoint}`;
|
||||
|
||||
// 如果是FormData,不设置Content-Type,让浏览器自动处理
|
||||
const isFormData = data instanceof FormData;
|
||||
const headers = isFormData
|
||||
? createHeaders({ ...options.headers, 'Content-Type': undefined })
|
||||
: createHeaders(options.headers);
|
||||
|
||||
// 如果是FormData,直接使用data;否则使用JSON.stringify
|
||||
const body = isFormData ? data : JSON.stringify(data);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: createHeaders(options.headers),
|
||||
body: JSON.stringify(data),
|
||||
headers: headers,
|
||||
body: body,
|
||||
...options,
|
||||
});
|
||||
return handleResponse(response);
|
||||
|
||||
@@ -104,11 +104,12 @@ const generateRequestId = () => {
|
||||
* @returns {boolean} 是否为标准格式
|
||||
*/
|
||||
export const isValidApiResponse = (response) => {
|
||||
// 基本格式检查:必须有 success 和 message 字段
|
||||
// timestamp 为可选字段(兼容旧接口)
|
||||
return response &&
|
||||
typeof response === 'object' &&
|
||||
typeof response.success === 'boolean' &&
|
||||
typeof response.message === 'string' &&
|
||||
typeof response.timestamp === 'string';
|
||||
(typeof response.message === 'string' || response.message === undefined);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -128,10 +128,10 @@
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="品系" name="strain">
|
||||
<a-form-item label="品类" name="strain">
|
||||
<a-select
|
||||
v-model:value="formData.strain"
|
||||
placeholder="请选择品系"
|
||||
placeholder="请选择品类"
|
||||
:loading="cattleUsersLoading"
|
||||
@change="handleFieldChange('strain', $event)"
|
||||
show-search
|
||||
@@ -157,6 +157,7 @@
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
>
|
||||
<!-- 动态选项 -->
|
||||
<a-select-option
|
||||
v-for="type in cattleTypes"
|
||||
:key="type.id"
|
||||
@@ -168,10 +169,10 @@
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="类别" name="cate">
|
||||
<a-form-item label="生理阶段" name="cate">
|
||||
<a-select
|
||||
v-model:value="formData.cate"
|
||||
placeholder="请选择类别"
|
||||
placeholder="请选择生理阶段"
|
||||
@change="handleFieldChange('cate', $event)"
|
||||
show-search
|
||||
:filter-option="filterCateOption"
|
||||
@@ -240,16 +241,7 @@
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="生理阶段" name="physiologicalStage">
|
||||
<a-input
|
||||
v-model:value="formData.physiologicalStage"
|
||||
placeholder="请输入生理阶段"
|
||||
@input="handleFieldChange('physiologicalStage', $event.target.value)"
|
||||
@change="handleFieldChange('physiologicalStage', $event.target.value)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="12">
|
||||
<a-form-item label="胎次" name="parity">
|
||||
<a-input-number
|
||||
@@ -323,13 +315,19 @@
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="来源" name="source">
|
||||
<a-input-number
|
||||
<a-select
|
||||
v-model:value="formData.source"
|
||||
placeholder="请输入来源"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
placeholder="请选择来源"
|
||||
@change="handleFieldChange('source', $event)"
|
||||
/>
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
>
|
||||
<a-select-option :value="1">购买</a-select-option>
|
||||
<a-select-option :value="2">自繁</a-select-option>
|
||||
<a-select-option :value="3">放生</a-select-option>
|
||||
<a-select-option :value="4">合作社</a-select-option>
|
||||
<a-select-option :value="5">入股</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
@@ -459,7 +457,7 @@ const formData = reactive({
|
||||
penId: null, // 映射iot_cattle.pen_id
|
||||
intoTime: null,
|
||||
parity: 0,
|
||||
source: 0,
|
||||
source: '', // 映射iot_cattle.source,改为空字符串以便验证
|
||||
sourceDay: 0,
|
||||
sourceWeight: 0,
|
||||
ageInMonths: 0, // 从iot_cattle.birthday计算得出
|
||||
@@ -484,7 +482,8 @@ const rules = {
|
||||
cate: [{ required: true, message: '请输入类别', trigger: 'blur' }], // iot_cattle.cate
|
||||
birthWeight: [{ required: true, message: '请输入出生体重', trigger: 'blur' }], // iot_cattle.birth_weight
|
||||
birthday: [{ required: true, message: '请选择出生日期', trigger: 'change' }], // iot_cattle.birthday
|
||||
orgId: [{ required: true, message: '请选择所属农场', trigger: 'change' }] // iot_cattle.org_id
|
||||
orgId: [{ required: true, message: '请选择所属农场', trigger: 'change' }], // iot_cattle.org_id
|
||||
source: [{ required: true, message: '请选择来源', trigger: 'change' }] // iot_cattle.source
|
||||
}
|
||||
|
||||
// 表格列配置(基于iot_cattle表字段映射)
|
||||
@@ -637,16 +636,34 @@ const fetchFarms = async () => {
|
||||
const fetchPens = async (farmId = null) => {
|
||||
try {
|
||||
pensLoading.value = true
|
||||
const token = localStorage.getItem('token')
|
||||
const params = farmId ? { farmId } : {}
|
||||
const response = await api.get('/iot-cattle/pens/list', {
|
||||
params
|
||||
})
|
||||
if (response.success) {
|
||||
pens.value = response.data
|
||||
console.log('🔍 [牛只档案] 开始获取栏舍列表')
|
||||
|
||||
// 调用 /cattle-pens 接口
|
||||
const params = {
|
||||
page: 1,
|
||||
pageSize: 10
|
||||
}
|
||||
|
||||
// 如果有 farmId,可以添加到参数中(如果后端支持)
|
||||
if (farmId) {
|
||||
params.farmId = farmId
|
||||
}
|
||||
|
||||
console.log('📤 [牛只档案] 栏舍列表请求参数:', params)
|
||||
const response = await api.cattlePens.getList(params)
|
||||
console.log('📥 [牛只档案] 栏舍列表响应:', response)
|
||||
|
||||
if (response.success && response.data) {
|
||||
// 从 response.data.list 中提取栏舍列表
|
||||
const pensList = response.data.list || []
|
||||
pens.value = pensList
|
||||
console.log('✅ [牛只档案] 获取栏舍列表成功,共', pensList.length, '条')
|
||||
return pensList
|
||||
}
|
||||
return []
|
||||
} catch (error) {
|
||||
console.error('获取栏舍列表失败:', error)
|
||||
console.error('❌ [牛只档案] 获取栏舍列表失败:', error)
|
||||
return []
|
||||
} finally {
|
||||
pensLoading.value = false
|
||||
}
|
||||
@@ -656,16 +673,37 @@ const fetchPens = async (farmId = null) => {
|
||||
const fetchBatches = async (farmId = null) => {
|
||||
try {
|
||||
batchesLoading.value = true
|
||||
const token = localStorage.getItem('token')
|
||||
const params = farmId ? { farmId } : {}
|
||||
const response = await api.get('/iot-cattle/batches/list', {
|
||||
params
|
||||
})
|
||||
if (response.success) {
|
||||
batches.value = response.data
|
||||
console.log('🔍 [牛只档案] 开始获取批次列表')
|
||||
|
||||
// 调用 /cattle-batches 接口
|
||||
const params = {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
search: '',
|
||||
exactMatch: true,
|
||||
strictMatch: true
|
||||
}
|
||||
|
||||
// 如果有 farmId,可以添加到参数中(如果后端支持)
|
||||
if (farmId) {
|
||||
params.farmId = farmId
|
||||
}
|
||||
|
||||
console.log('📤 [牛只档案] 批次列表请求参数:', params)
|
||||
const response = await api.cattleBatches.getList(params)
|
||||
console.log('📥 [牛只档案] 批次列表响应:', response)
|
||||
|
||||
if (response.success && response.data) {
|
||||
// 从 response.data.list 中提取批次列表
|
||||
const batchesList = response.data.list || []
|
||||
batches.value = batchesList
|
||||
console.log('✅ [牛只档案] 获取批次列表成功,共', batchesList.length, '条')
|
||||
return batchesList
|
||||
}
|
||||
return []
|
||||
} catch (error) {
|
||||
console.error('获取批次列表失败:', error)
|
||||
console.error('❌ [牛只档案] 获取批次列表失败:', error)
|
||||
return []
|
||||
} finally {
|
||||
batchesLoading.value = false
|
||||
}
|
||||
@@ -801,12 +839,27 @@ const initializeAddMode = () => {
|
||||
|
||||
// ==================== 编辑牛只档案功能 ====================
|
||||
// 编辑牛只档案
|
||||
const editAnimal = (record) => {
|
||||
console.log('=== 点击编辑按钮 ===')
|
||||
console.log('编辑记录:', record)
|
||||
initializeEditMode(record)
|
||||
openModal()
|
||||
loadRequiredData()
|
||||
// 编辑牛只档案
|
||||
const editAnimal = async (record) => {
|
||||
try {
|
||||
console.log('=== 点击编辑按钮 ===')
|
||||
console.log('编辑记录ID:', record.id)
|
||||
|
||||
// 调用后端接口获取详情
|
||||
const response = await api.get(`/iot-cattle/${record.id}`)
|
||||
|
||||
if (response.success && response.data) {
|
||||
console.log('获取到的详情数据:', response.data)
|
||||
initializeEditMode(response.data)
|
||||
openModal()
|
||||
loadRequiredData()
|
||||
} else {
|
||||
message.error('获取牛只档案详情失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取牛只档案详情失败:', error)
|
||||
message.error(error.message || '获取牛只档案详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化编辑模式
|
||||
@@ -842,27 +895,99 @@ const populateFormWithRecord = (record) => {
|
||||
formData.id = record.id
|
||||
formData.earNumber = record.earNumber || '' // iot_cattle.ear_number
|
||||
formData.sex = record.sex || 1 // iot_cattle.sex
|
||||
formData.strain = record.strain || '' // iot_cattle.strain
|
||||
formData.varieties = record.varieties || '' // iot_cattle.varieties
|
||||
formData.cate = record.cate || '' // iot_cattle.cate
|
||||
// 使用原始ID(strainId),如果没有则使用strain(兼容旧数据)
|
||||
formData.strain = record.strainId !== undefined ? record.strainId : (record.strain || '') // iot_cattle.strain
|
||||
// 使用原始ID(varietiesId),如果没有则使用varieties(兼容旧数据)
|
||||
formData.varieties = record.varietiesId !== undefined ? record.varietiesId : (record.varieties || '') // iot_cattle.varieties
|
||||
// 使用原始ID(cateId),如果没有则使用cate(兼容旧数据)
|
||||
formData.cate = record.cateId !== undefined ? record.cateId : (record.cate || '') // iot_cattle.cate
|
||||
formData.birthWeight = record.birthWeight || 0 // iot_cattle.birth_weight
|
||||
formData.birthday = record.birthday ? dayjs(record.birthday) : null // iot_cattle.birthday
|
||||
formData.penId = record.penId || null // iot_cattle.pen_id
|
||||
formData.intoTime = record.intoTime || null
|
||||
// 处理出生日期:如果是时间戳(秒),使用dayjs.unix;如果是日期字符串,使用dayjs
|
||||
if (record.birthday) {
|
||||
if (typeof record.birthday === 'number') {
|
||||
// 时间戳(秒)转换为dayjs对象
|
||||
const birthdayDate = dayjs.unix(record.birthday)
|
||||
formData.birthday = birthdayDate.isValid() ? birthdayDate : null
|
||||
console.log('转换出生日期:', record.birthday, '->', formData.birthday?.format('YYYY-MM-DD'))
|
||||
} else {
|
||||
formData.birthday = dayjs(record.birthday) // 日期字符串转换为dayjs对象
|
||||
}
|
||||
} else {
|
||||
formData.birthday = null
|
||||
}
|
||||
// 处理所属栏舍:如果为0或null,则设为null
|
||||
formData.penId = (record.penId && record.penId !== 0) ? record.penId : null // iot_cattle.pen_id
|
||||
// 处理入场日期:如果是时间戳(秒),使用dayjs.unix;如果是日期字符串,使用dayjs
|
||||
if (record.intoTime) {
|
||||
if (typeof record.intoTime === 'number') {
|
||||
formData.intoTime = dayjs.unix(record.intoTime) // 时间戳(秒)转换为dayjs对象
|
||||
} else {
|
||||
formData.intoTime = dayjs(record.intoTime) // 日期字符串转换为dayjs对象
|
||||
}
|
||||
} else {
|
||||
formData.intoTime = null
|
||||
}
|
||||
formData.parity = record.parity || 0
|
||||
formData.source = record.source || 0
|
||||
// 处理来源:如果是数字(包括0),直接使用;否则设为空字符串
|
||||
if (typeof record.source === 'number') {
|
||||
formData.source = record.source
|
||||
} else if (record.source !== undefined && record.source !== null && record.source !== '') {
|
||||
formData.source = record.source
|
||||
} else {
|
||||
formData.source = ''
|
||||
}
|
||||
formData.sourceDay = record.sourceDay || 0
|
||||
formData.sourceWeight = record.sourceWeight || 0
|
||||
formData.ageInMonths = calculateAgeInMonths(record.birthday) // 从iot_cattle.birthday计算月龄
|
||||
// 优先使用JSON中的ageInMonths,如果没有则计算
|
||||
formData.ageInMonths = record.ageInMonths !== undefined ? record.ageInMonths : calculateAgeInMonths(record.birthday)
|
||||
formData.physiologicalStage = record.physiologicalStage || ''
|
||||
formData.currentWeight = record.currentWeight || 0
|
||||
formData.weightCalculateTime = record.weightCalculateTime ? dayjs(record.weightCalculateTime) : null
|
||||
// 处理体重计算时间:如果是时间戳(秒),使用dayjs.unix;如果是日期字符串,使用dayjs
|
||||
if (record.weightCalculateTime) {
|
||||
if (typeof record.weightCalculateTime === 'number') {
|
||||
formData.weightCalculateTime = dayjs.unix(record.weightCalculateTime) // 时间戳(秒)转换为dayjs对象
|
||||
} else {
|
||||
formData.weightCalculateTime = dayjs(record.weightCalculateTime) // 日期字符串转换为dayjs对象
|
||||
}
|
||||
} else {
|
||||
formData.weightCalculateTime = null
|
||||
}
|
||||
formData.dayOfBirthday = record.dayOfBirthday || 0
|
||||
formData.orgId = record.farmId || record.orgId || null // iot_cattle.org_id
|
||||
formData.penId = record.penId || null // iot_cattle.pen_id
|
||||
formData.batchId = record.batchId || null // iot_cattle.batch_id
|
||||
|
||||
// 保存 penId 和 batchId 的值,等待选项加载完成后再设置
|
||||
const savedPenId = (record.penId && record.penId !== 0) ? record.penId : null
|
||||
const savedBatchId = (record.batchId && record.batchId !== 0) ? record.batchId : null
|
||||
|
||||
console.log('填充后的 formData:', JSON.parse(JSON.stringify(formData)));
|
||||
console.log('保存的 penId:', savedPenId, 'batchId:', savedBatchId);
|
||||
console.log('出生日期转换结果:', formData.birthday);
|
||||
|
||||
// 如果所属农场有值,先加载对应的栏舍和批次选项,然后再设置值
|
||||
if (formData.orgId) {
|
||||
Promise.all([
|
||||
fetchPens(formData.orgId),
|
||||
fetchBatches(formData.orgId)
|
||||
]).then(() => {
|
||||
// 选项加载完成后再设置值,使用 nextTick 确保 DOM 更新
|
||||
setTimeout(() => {
|
||||
if (savedPenId !== null) {
|
||||
formData.penId = savedPenId
|
||||
console.log('设置 penId:', savedPenId, '当前栏舍选项:', pens.value)
|
||||
}
|
||||
if (savedBatchId !== null) {
|
||||
formData.batchId = savedBatchId
|
||||
console.log('设置 batchId:', savedBatchId, '当前批次选项:', batches.value)
|
||||
}
|
||||
}, 100) // 延迟100ms确保选项已加载到DOM
|
||||
}).catch(error => {
|
||||
console.error('加载栏舍或批次选项失败:', error)
|
||||
})
|
||||
} else {
|
||||
// 如果没有农场ID,直接设置值(虽然可能没有对应的选项)
|
||||
formData.penId = savedPenId
|
||||
formData.batchId = savedBatchId
|
||||
}
|
||||
}
|
||||
|
||||
// 配置编辑模式设置
|
||||
@@ -932,7 +1057,7 @@ const handleSubmit = async () => {
|
||||
console.log('原始表单数据:', JSON.parse(JSON.stringify(formData)));
|
||||
|
||||
// 检查必填字段
|
||||
const requiredFields = ['earNumber', 'sex', 'strain', 'varieties', 'cate', 'birthWeight', 'birthday', 'orgId'];
|
||||
const requiredFields = ['earNumber', 'sex', 'strain', 'varieties', 'cate', 'birthWeight', 'birthday', 'orgId', 'source'];
|
||||
const missingFields = requiredFields.filter(field => !formData[field] && formData[field] !== 0);
|
||||
if (missingFields.length > 0) {
|
||||
console.error('缺少必填字段:', missingFields);
|
||||
@@ -1006,7 +1131,7 @@ const resetForm = () => {
|
||||
penId: null,
|
||||
intoTime: null,
|
||||
parity: 0,
|
||||
source: 0,
|
||||
source: '',
|
||||
sourceDay: 0,
|
||||
sourceWeight: 0,
|
||||
ageInMonths: 0,
|
||||
@@ -1268,12 +1393,8 @@ const confirmImport = async () => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', importFile.value)
|
||||
|
||||
// 调用导入API
|
||||
const response = await api.post('/iot-cattle/import', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
// 调用导入API(FormData会自动处理Content-Type,不需要手动设置)
|
||||
const response = await api.post('/iot-cattle/import', formData)
|
||||
|
||||
if (response.data.success) {
|
||||
message.destroy()
|
||||
@@ -1290,7 +1411,16 @@ const confirmImport = async () => {
|
||||
} catch (error) {
|
||||
message.destroy()
|
||||
console.error('导入失败:', error)
|
||||
message.error('导入失败,请检查文件格式是否正确')
|
||||
// 显示具体的错误信息
|
||||
const errorMessage = error.message || error.response?.data?.message || '导入失败,请检查文件格式是否正确'
|
||||
message.error(errorMessage)
|
||||
|
||||
// 如果是认证错误,提示用户重新登录
|
||||
if (errorMessage.includes('认证') || errorMessage.includes('未授权') || errorMessage.includes('登录')) {
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login'
|
||||
}, 2000)
|
||||
}
|
||||
} finally {
|
||||
importLoading.value = false
|
||||
}
|
||||
|
||||
@@ -110,27 +110,22 @@
|
||||
>
|
||||
<a-form-item label="批次名称" name="name">
|
||||
<a-input
|
||||
v-model="formData.name"
|
||||
v-model:value="formData.name"
|
||||
placeholder="请输入批次名称"
|
||||
@input="handleFieldChange('name', $event.target.value)"
|
||||
@change="handleFieldChange('name', $event.target.value)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="批次编号" name="code">
|
||||
<a-input
|
||||
v-model="formData.code"
|
||||
v-model:value="formData.code"
|
||||
placeholder="请输入批次编号"
|
||||
@input="handleFieldChange('code', $event.target.value)"
|
||||
@change="handleFieldChange('code', $event.target.value)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="批次类型" name="type">
|
||||
<a-select
|
||||
v-model="formData.type"
|
||||
v-model:value="formData.type"
|
||||
placeholder="请选择批次类型"
|
||||
@change="handleFieldChange('type', $event)"
|
||||
>
|
||||
<a-select-option value="育成批次">育成批次</a-select-option>
|
||||
<a-select-option value="繁殖批次">繁殖批次</a-select-option>
|
||||
@@ -142,66 +137,58 @@
|
||||
|
||||
<a-form-item label="开始日期" name="startDate">
|
||||
<a-date-picker
|
||||
v-model="formData.startDate"
|
||||
v-model:value="formData.startDate"
|
||||
style="width: 100%"
|
||||
placeholder="请选择开始日期"
|
||||
@change="handleFieldChange('startDate', $event)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="预计结束日期" name="expectedEndDate">
|
||||
<a-date-picker
|
||||
v-model="formData.expectedEndDate"
|
||||
v-model:value="formData.expectedEndDate"
|
||||
style="width: 100%"
|
||||
placeholder="请选择预计结束日期"
|
||||
@change="handleFieldChange('expectedEndDate', $event)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="实际结束日期" name="actualEndDate">
|
||||
<a-date-picker
|
||||
v-model="formData.actualEndDate"
|
||||
v-model:value="formData.actualEndDate"
|
||||
style="width: 100%"
|
||||
placeholder="请选择实际结束日期"
|
||||
@change="handleFieldChange('actualEndDate', $event)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="目标牛只数量" name="targetCount">
|
||||
<a-input-number
|
||||
v-model="formData.targetCount"
|
||||
v-model:value="formData.targetCount"
|
||||
:min="1"
|
||||
:max="1000"
|
||||
style="width: 100%"
|
||||
placeholder="请输入目标牛只数量"
|
||||
@change="handleFieldChange('targetCount', $event)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="当前牛只数量" name="currentCount">
|
||||
<a-input-number
|
||||
v-model="formData.currentCount"
|
||||
v-model:value="formData.currentCount"
|
||||
:min="0"
|
||||
:max="formData.targetCount || 1000"
|
||||
style="width: 100%"
|
||||
placeholder="当前牛只数量"
|
||||
@change="handleFieldChange('currentCount', $event)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="负责人" name="manager">
|
||||
<a-input
|
||||
v-model="formData.manager"
|
||||
v-model:value="formData.manager"
|
||||
placeholder="请输入负责人姓名"
|
||||
@input="handleFieldChange('manager', $event.target.value)"
|
||||
@change="handleFieldChange('manager', $event.target.value)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-radio-group
|
||||
v-model="formData.status"
|
||||
@change="handleFieldChange('status', $event.target.value)"
|
||||
v-model:value="formData.status"
|
||||
>
|
||||
<a-radio value="进行中">进行中</a-radio>
|
||||
<a-radio value="已完成">已完成</a-radio>
|
||||
@@ -211,11 +198,9 @@
|
||||
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea
|
||||
v-model="formData.remark"
|
||||
v-model:value="formData.remark"
|
||||
:rows="3"
|
||||
placeholder="请输入备注信息"
|
||||
@input="handleFieldChange('remark', $event.target.value)"
|
||||
@change="handleFieldChange('remark', $event.target.value)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
@@ -646,7 +631,7 @@ const handleAdd = () => {
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (record) => {
|
||||
const handleEdit = async (record) => {
|
||||
console.log('🔄 [批次设置] 开始编辑操作')
|
||||
console.log('📋 [批次设置] 原始记录数据:', {
|
||||
id: record.id,
|
||||
@@ -663,15 +648,76 @@ const handleEdit = (record) => {
|
||||
})
|
||||
|
||||
modalTitle.value = '编辑批次'
|
||||
Object.assign(formData, {
|
||||
...record,
|
||||
startDate: record.startDate ? dayjs(record.startDate) : null,
|
||||
expectedEndDate: record.expectedEndDate ? dayjs(record.expectedEndDate) : null,
|
||||
actualEndDate: record.actualEndDate ? dayjs(record.actualEndDate) : null
|
||||
})
|
||||
|
||||
// 处理日期转换
|
||||
let startDateValue = null
|
||||
let expectedEndDateValue = null
|
||||
let actualEndDateValue = null
|
||||
|
||||
if (record.startDate) {
|
||||
if (typeof record.startDate === 'string') {
|
||||
startDateValue = dayjs(record.startDate)
|
||||
} else if (record.startDate instanceof Date) {
|
||||
startDateValue = dayjs(record.startDate)
|
||||
} else if (record.startDate && typeof record.startDate === 'object' && record.startDate.format) {
|
||||
startDateValue = record.startDate
|
||||
} else {
|
||||
startDateValue = dayjs(record.startDate)
|
||||
}
|
||||
}
|
||||
|
||||
if (record.expectedEndDate) {
|
||||
if (typeof record.expectedEndDate === 'string') {
|
||||
expectedEndDateValue = dayjs(record.expectedEndDate)
|
||||
} else if (record.expectedEndDate instanceof Date) {
|
||||
expectedEndDateValue = dayjs(record.expectedEndDate)
|
||||
} else if (record.expectedEndDate && typeof record.expectedEndDate === 'object' && record.expectedEndDate.format) {
|
||||
expectedEndDateValue = record.expectedEndDate
|
||||
} else {
|
||||
expectedEndDateValue = dayjs(record.expectedEndDate)
|
||||
}
|
||||
}
|
||||
|
||||
if (record.actualEndDate) {
|
||||
if (typeof record.actualEndDate === 'string') {
|
||||
actualEndDateValue = dayjs(record.actualEndDate)
|
||||
} else if (record.actualEndDate instanceof Date) {
|
||||
actualEndDateValue = dayjs(record.actualEndDate)
|
||||
} else if (record.actualEndDate && typeof record.actualEndDate === 'object' && record.actualEndDate.format) {
|
||||
actualEndDateValue = record.actualEndDate
|
||||
} else {
|
||||
actualEndDateValue = dayjs(record.actualEndDate)
|
||||
}
|
||||
}
|
||||
|
||||
// 填充表单数据 - 使用直接赋值确保响应式更新
|
||||
formData.id = record.id
|
||||
formData.name = record.name || ''
|
||||
formData.code = record.code || ''
|
||||
formData.type = record.type || ''
|
||||
formData.startDate = startDateValue
|
||||
formData.expectedEndDate = expectedEndDateValue
|
||||
formData.actualEndDate = actualEndDateValue
|
||||
formData.targetCount = record.targetCount || 0
|
||||
formData.currentCount = record.currentCount || 0
|
||||
formData.manager = record.manager || ''
|
||||
formData.status = record.status || '进行中'
|
||||
formData.remark = record.remark || ''
|
||||
formData.farmId = record.farmId || record.farm_id || null
|
||||
|
||||
console.log('📝 [批次设置] 表单数据已填充:', formData)
|
||||
console.log('📝 [批次设置] startDate 是否为 dayjs:', formData.startDate && typeof formData.startDate.format === 'function')
|
||||
console.log('📝 [批次设置] expectedEndDate 是否为 dayjs:', formData.expectedEndDate && typeof formData.expectedEndDate.format === 'function')
|
||||
console.log('📝 [批次设置] actualEndDate 是否为 dayjs:', formData.actualEndDate && typeof formData.actualEndDate.format === 'function')
|
||||
|
||||
// 打开模态框
|
||||
modalVisible.value = true
|
||||
|
||||
// 等待模态框和表单渲染完成
|
||||
await nextTick()
|
||||
await new Promise(resolve => setTimeout(resolve, 200))
|
||||
|
||||
console.log('✅ [批次设置] 模态框已打开,表单应该已填充')
|
||||
}
|
||||
|
||||
const handleDelete = (record) => {
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
<a-button type="link" size="small" @click="() => handleEdit(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="handleViewDetails(record)">
|
||||
@@ -167,27 +167,23 @@
|
||||
>
|
||||
<a-form-item label="牛只耳号" name="earNumber">
|
||||
<a-input
|
||||
v-model="formData.earNumber"
|
||||
v-model:value="formData.earNumber"
|
||||
placeholder="请输入牛只耳号"
|
||||
@input="handleFieldChange('earNumber', $event.target.value)"
|
||||
@change="handleFieldChange('earNumber', $event.target.value)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="离栏日期" name="exitDate">
|
||||
<a-date-picker
|
||||
v-model="formData.exitDate"
|
||||
v-model:value="formData.exitDate"
|
||||
style="width: 100%"
|
||||
placeholder="请选择离栏日期"
|
||||
@change="handleFieldChange('exitDate', $event)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="离栏原因" name="exitReason">
|
||||
<a-select
|
||||
v-model="formData.exitReason"
|
||||
v-model:value="formData.exitReason"
|
||||
placeholder="请选择离栏原因"
|
||||
@change="handleFieldChange('exitReason', $event)"
|
||||
>
|
||||
<a-select-option value="出售">出售</a-select-option>
|
||||
<a-select-option value="死亡">死亡</a-select-option>
|
||||
@@ -199,9 +195,8 @@
|
||||
|
||||
<a-form-item label="原栏舍" name="originalPenId">
|
||||
<a-select
|
||||
v-model="formData.originalPenId"
|
||||
v-model:value="formData.originalPenId"
|
||||
placeholder="请选择原栏舍"
|
||||
@change="handleFieldChange('originalPenId', $event)"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="pen in penList"
|
||||
@@ -215,18 +210,15 @@
|
||||
|
||||
<a-form-item label="去向" name="destination">
|
||||
<a-input
|
||||
v-model="formData.destination"
|
||||
v-model:value="formData.destination"
|
||||
placeholder="请输入牛只去向"
|
||||
@input="handleFieldChange('destination', $event.target.value)"
|
||||
@change="handleFieldChange('destination', $event.target.value)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="处理方式" name="disposalMethod">
|
||||
<a-select
|
||||
v-model="formData.disposalMethod"
|
||||
v-model:value="formData.disposalMethod"
|
||||
placeholder="请选择处理方式"
|
||||
@change="handleFieldChange('disposalMethod', $event)"
|
||||
>
|
||||
<a-select-option value="屠宰">屠宰</a-select-option>
|
||||
<a-select-option value="转售">转售</a-select-option>
|
||||
@@ -238,17 +230,14 @@
|
||||
|
||||
<a-form-item label="处理人员" name="handler">
|
||||
<a-input
|
||||
v-model="formData.handler"
|
||||
v-model:value="formData.handler"
|
||||
placeholder="请输入处理人员姓名"
|
||||
@input="handleFieldChange('handler', $event.target.value)"
|
||||
@change="handleFieldChange('handler', $event.target.value)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-radio-group
|
||||
v-model="formData.status"
|
||||
@change="handleFieldChange('status', $event.target.value)"
|
||||
v-model:value="formData.status"
|
||||
>
|
||||
<a-radio value="已确认">已确认</a-radio>
|
||||
<a-radio value="待确认">待确认</a-radio>
|
||||
@@ -258,11 +247,9 @@
|
||||
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea
|
||||
v-model="formData.remark"
|
||||
v-model:value="formData.remark"
|
||||
:rows="3"
|
||||
placeholder="请输入备注信息"
|
||||
@input="handleFieldChange('remark', $event.target.value)"
|
||||
@change="handleFieldChange('remark', $event.target.value)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
@@ -579,38 +566,207 @@ const handleAdd = () => {
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (record) => {
|
||||
console.log('🔄 [离栏记录] 开始编辑操作')
|
||||
console.log('📋 [离栏记录] 原始记录数据:', {
|
||||
id: record.id,
|
||||
earNumber: record.earNumber,
|
||||
exitDate: record.exitDate,
|
||||
exitReason: record.exitReason,
|
||||
originalPenId: record.originalPenId,
|
||||
originalPenName: record.originalPenName,
|
||||
destination: record.destination,
|
||||
disposalMethod: record.disposalMethod,
|
||||
handler: record.handler,
|
||||
status: record.status,
|
||||
remark: record.remark
|
||||
})
|
||||
// 获取离栏记录详情(用于编辑)
|
||||
const getExitRecordDetail = async (id) => {
|
||||
console.log('🔵 [离栏记录-编辑] ========== 开始获取详情 ==========')
|
||||
console.log('🔵 [离栏记录-编辑] 函数被调用,记录ID:', id)
|
||||
console.log('🔵 [离栏记录-编辑] 调用时间:', new Date().toISOString())
|
||||
|
||||
modalTitle.value = '编辑离栏记录'
|
||||
Object.assign(formData, {
|
||||
id: record.id,
|
||||
earNumber: record.earNumber, // 映射耳号字段
|
||||
exitDate: record.exitDate ? dayjs(record.exitDate) : null,
|
||||
exitReason: record.exitReason,
|
||||
originalPenId: record.originalPenId, // 映射原栏舍ID
|
||||
destination: record.destination,
|
||||
disposalMethod: record.disposalMethod,
|
||||
handler: record.handler,
|
||||
status: record.status,
|
||||
remark: record.remark || ''
|
||||
})
|
||||
try {
|
||||
console.log('🔄 [离栏记录-编辑] 开始获取详情')
|
||||
console.log('📋 [离栏记录-编辑] 记录ID:', id)
|
||||
|
||||
// 调用后端接口获取详情
|
||||
console.log('📡 [离栏记录-编辑] 准备调用 API: /cattle-exit-records/' + id)
|
||||
const response = await api.cattleExitRecords.getById(id)
|
||||
console.log('📡 [离栏记录-编辑] API 调用完成,响应状态:', response.success)
|
||||
|
||||
if (response.success && response.data) {
|
||||
console.log('✅ [离栏记录-编辑] 获取详情成功:', response.data)
|
||||
console.log('🔵 [离栏记录-编辑] ========== 获取详情成功 ==========')
|
||||
return response.data
|
||||
} else {
|
||||
console.error('❌ [离栏记录-编辑] 获取详情失败:', response.message)
|
||||
throw new Error(response.message || '获取离栏记录详情失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ [离栏记录-编辑] 获取详情异常:', error)
|
||||
console.error('🔵 [离栏记录-编辑] ========== 获取详情失败 ==========')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑离栏记录
|
||||
const handleEdit = async (record) => {
|
||||
// 立即输出日志,确认函数被调用
|
||||
console.log('🟢🟢🟢 [离栏记录-编辑] ========== handleEdit 函数被调用 ==========')
|
||||
console.log('🟢 [离栏记录-编辑] 调用时间:', new Date().toISOString())
|
||||
console.log('🟢 [离栏记录-编辑] 记录数据:', record)
|
||||
console.log('🟢 [离栏记录-编辑] 记录ID:', record?.id)
|
||||
|
||||
console.log('📝 [离栏记录] 表单数据已填充:', formData)
|
||||
modalVisible.value = true
|
||||
// 验证 record 对象
|
||||
if (!record) {
|
||||
console.error('❌ [离栏记录-编辑] record 为空')
|
||||
message.error('记录数据为空')
|
||||
return
|
||||
}
|
||||
|
||||
if (!record.id) {
|
||||
console.error('❌ [离栏记录-编辑] record.id 为空')
|
||||
message.error('记录ID为空')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('🔄 [离栏记录-编辑] 开始编辑操作')
|
||||
console.log('📋 [离栏记录-编辑] 编辑记录ID:', record.id)
|
||||
|
||||
// 调用封装的获取详情接口
|
||||
console.log('📞 [离栏记录-编辑] 准备调用 getExitRecordDetail 函数,ID:', record.id)
|
||||
console.log('📞 [离栏记录-编辑] getExitRecordDetail 函数是否存在:', typeof getExitRecordDetail)
|
||||
|
||||
const detailData = await getExitRecordDetail(record.id)
|
||||
console.log('📞 [离栏记录-编辑] getExitRecordDetail 函数调用完成,返回数据:', detailData)
|
||||
|
||||
if (detailData) {
|
||||
console.log('📋 [离栏记录] 获取到的详情数据:', detailData)
|
||||
|
||||
modalTitle.value = '编辑离栏记录'
|
||||
|
||||
// 确保栏舍列表已加载
|
||||
if (penList.value.length === 0) {
|
||||
await loadPenList()
|
||||
}
|
||||
|
||||
// 填充表单数据 - 直接赋值给 reactive 对象的属性
|
||||
|
||||
// 处理日期转换
|
||||
let exitDateValue = null
|
||||
if (detailData.exitDate) {
|
||||
// 如果是字符串,转换为 dayjs 对象
|
||||
if (typeof detailData.exitDate === 'string') {
|
||||
exitDateValue = dayjs(detailData.exitDate)
|
||||
} else if (detailData.exitDate instanceof Date) {
|
||||
exitDateValue = dayjs(detailData.exitDate)
|
||||
} else if (detailData.exitDate && typeof detailData.exitDate === 'object' && detailData.exitDate.format) {
|
||||
// 如果已经是 dayjs 对象,直接使用
|
||||
exitDateValue = detailData.exitDate
|
||||
} else {
|
||||
exitDateValue = dayjs(detailData.exitDate)
|
||||
}
|
||||
console.log('📅 [离栏记录] 日期转换:', detailData.exitDate, '->', exitDateValue?.format('YYYY-MM-DD'))
|
||||
console.log('📅 [离栏记录] exitDateValue 类型:', typeof exitDateValue, '是否为 dayjs:', exitDateValue && typeof exitDateValue.format === 'function')
|
||||
}
|
||||
|
||||
// 先填充表单数据(在打开模态框之前)
|
||||
console.log('📝 [离栏记录] 开始填充表单数据')
|
||||
|
||||
// 直接赋值给 reactive 对象
|
||||
formData.id = detailData.id || null
|
||||
formData.earNumber = detailData.earNumber || ''
|
||||
formData.exitDate = exitDateValue // 确保是 dayjs 对象
|
||||
formData.exitReason = detailData.exitReason || ''
|
||||
formData.originalPenId = detailData.originalPenId || null
|
||||
formData.destination = detailData.destination || ''
|
||||
formData.disposalMethod = detailData.disposalMethod || ''
|
||||
formData.handler = detailData.handler || ''
|
||||
formData.status = detailData.status || '待确认'
|
||||
formData.remark = detailData.remark || ''
|
||||
|
||||
console.log('📝 [离栏记录] 表单数据已填充到 formData')
|
||||
console.log('📝 [离栏记录] formData.exitDate:', formData.exitDate)
|
||||
console.log('📝 [离栏记录] formData.exitDate 类型:', typeof formData.exitDate)
|
||||
console.log('📝 [离栏记录] formData.exitDate 是否为 dayjs:', formData.exitDate && typeof formData.exitDate.format === 'function')
|
||||
console.log('📝 [离栏记录] formData 完整数据:', {
|
||||
id: formData.id,
|
||||
earNumber: formData.earNumber,
|
||||
exitDate: formData.exitDate ? (formData.exitDate.format ? formData.exitDate.format('YYYY-MM-DD') : formData.exitDate) : null,
|
||||
exitReason: formData.exitReason,
|
||||
originalPenId: formData.originalPenId,
|
||||
destination: formData.destination,
|
||||
disposalMethod: formData.disposalMethod,
|
||||
handler: formData.handler,
|
||||
status: formData.status,
|
||||
remark: formData.remark
|
||||
})
|
||||
|
||||
// 先打开模态框
|
||||
modalVisible.value = true
|
||||
console.log('📝 [离栏记录] 模态框已打开')
|
||||
|
||||
// 等待模态框完全打开和表单渲染
|
||||
await nextTick()
|
||||
await new Promise(resolve => setTimeout(resolve, 300))
|
||||
|
||||
// 确保日期字段是 dayjs 对象(防止被序列化)
|
||||
if (detailData.exitDate && (!formData.exitDate || typeof formData.exitDate.format !== 'function')) {
|
||||
formData.exitDate = exitDateValue
|
||||
console.log('🔄 [离栏记录] 重新设置 exitDate 为 dayjs 对象:', formData.exitDate)
|
||||
}
|
||||
|
||||
console.log('🔍 [离栏记录] 检查 formData.exitDate 是否为 dayjs:',
|
||||
formData.exitDate && typeof formData.exitDate.format === 'function' ? '是' : '否',
|
||||
formData.exitDate ? (formData.exitDate.format ? formData.exitDate.format('YYYY-MM-DD') : formData.exitDate) : 'null'
|
||||
)
|
||||
|
||||
// 由于表单使用 v-model 绑定到 formData,直接修改 formData 即可更新表单
|
||||
// 但为了确保表单组件正确响应,我们尝试使用 setFieldsValue(如果可用)
|
||||
if (formRef.value) {
|
||||
// 检查表单实例是否有 setFieldsValue 方法
|
||||
if (typeof formRef.value.setFieldsValue === 'function') {
|
||||
try {
|
||||
const fieldsValue = {
|
||||
id: formData.id,
|
||||
earNumber: formData.earNumber,
|
||||
exitDate: formData.exitDate, // 确保是 dayjs 对象
|
||||
exitReason: formData.exitReason,
|
||||
originalPenId: formData.originalPenId,
|
||||
destination: formData.destination,
|
||||
disposalMethod: formData.disposalMethod,
|
||||
handler: formData.handler,
|
||||
status: formData.status,
|
||||
remark: formData.remark
|
||||
}
|
||||
console.log('🔧 [离栏记录] 使用 setFieldsValue 设置表单值')
|
||||
console.log('🔧 [离栏记录] exitDate 值:', fieldsValue.exitDate, '类型:', typeof fieldsValue.exitDate)
|
||||
formRef.value.setFieldsValue(fieldsValue)
|
||||
console.log('✅ [离栏记录] setFieldsValue 设置成功')
|
||||
} catch (error) {
|
||||
console.warn('⚠️ [离栏记录] setFieldsValue 失败,但 formData 已更新,表单应该会自动响应:', error)
|
||||
}
|
||||
} else {
|
||||
console.log('ℹ️ [离栏记录] 表单实例没有 setFieldsValue 方法,使用 v-model 绑定(formData 已更新)')
|
||||
}
|
||||
} else {
|
||||
console.warn('⚠️ [离栏记录] formRef.value 为空,但 formData 已更新,表单应该会自动响应')
|
||||
}
|
||||
|
||||
// 再次使用 nextTick 确保值已更新
|
||||
await nextTick()
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
|
||||
// 验证表单值
|
||||
console.log('✅ [离栏记录] 模态框已打开')
|
||||
console.log('✅ [离栏记录] formData.exitDate 最终值:', formData.exitDate)
|
||||
console.log('✅ [离栏记录] formData.exitDate 是否为 dayjs:', formData.exitDate && typeof formData.exitDate.format === 'function')
|
||||
|
||||
// 如果表单实例有 getFieldsValue 方法,验证表单值
|
||||
if (formRef.value && typeof formRef.value.getFieldsValue === 'function') {
|
||||
try {
|
||||
const currentFormData = formRef.value.getFieldsValue()
|
||||
console.log('✅ [离栏记录] 表单当前值(通过 getFieldsValue):', currentFormData)
|
||||
console.log('✅ [离栏记录] 表单 exitDate 值:', currentFormData.exitDate)
|
||||
} catch (error) {
|
||||
console.warn('⚠️ [离栏记录] 无法获取表单值:', error)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
message.error('获取离栏记录详情失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ [离栏记录] 编辑操作失败:', error)
|
||||
message.error(error.message || '获取离栏记录详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = (record) => {
|
||||
|
||||
@@ -97,27 +97,22 @@
|
||||
>
|
||||
<a-form-item label="栏舍名称" name="name">
|
||||
<a-input
|
||||
v-model="formData.name"
|
||||
v-model:value="formData.name"
|
||||
placeholder="请输入栏舍名称"
|
||||
@input="handleFieldChange('name', $event.target.value)"
|
||||
@change="handleFieldChange('name', $event.target.value)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="栏舍编号" name="code">
|
||||
<a-input
|
||||
v-model="formData.code"
|
||||
v-model:value="formData.code"
|
||||
placeholder="请输入栏舍编号"
|
||||
@input="handleFieldChange('code', $event.target.value)"
|
||||
@change="handleFieldChange('code', $event.target.value)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="栏舍类型" name="type">
|
||||
<a-select
|
||||
v-model="formData.type"
|
||||
v-model:value="formData.type"
|
||||
placeholder="请选择栏舍类型"
|
||||
@change="handleFieldChange('type', $event)"
|
||||
>
|
||||
<a-select-option value="育成栏">育成栏</a-select-option>
|
||||
<a-select-option value="产房">产房</a-select-option>
|
||||
@@ -129,51 +124,45 @@
|
||||
|
||||
<a-form-item label="容量" name="capacity">
|
||||
<a-input-number
|
||||
v-model="formData.capacity"
|
||||
v-model:value="formData.capacity"
|
||||
:min="1"
|
||||
:max="1000"
|
||||
style="width: 100%"
|
||||
placeholder="请输入栏舍容量"
|
||||
@change="handleFieldChange('capacity', $event)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="当前牛只数量" name="currentCount">
|
||||
<a-input-number
|
||||
v-model="formData.currentCount"
|
||||
v-model:value="formData.currentCount"
|
||||
:min="0"
|
||||
:max="formData.capacity || 1000"
|
||||
style="width: 100%"
|
||||
placeholder="当前牛只数量"
|
||||
@change="handleFieldChange('currentCount', $event)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="面积(平方米)" name="area">
|
||||
<a-input-number
|
||||
v-model="formData.area"
|
||||
v-model:value="formData.area"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
placeholder="请输入栏舍面积"
|
||||
@change="handleFieldChange('area', $event)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="位置描述" name="location">
|
||||
<a-textarea
|
||||
v-model="formData.location"
|
||||
v-model:value="formData.location"
|
||||
:rows="3"
|
||||
placeholder="请输入栏舍位置描述"
|
||||
@input="handleFieldChange('location', $event.target.value)"
|
||||
@change="handleFieldChange('location', $event.target.value)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-radio-group
|
||||
v-model="formData.status"
|
||||
@change="handleFieldChange('status', $event.target.value)"
|
||||
v-model:value="formData.status"
|
||||
>
|
||||
<a-radio value="启用">启用</a-radio>
|
||||
<a-radio value="停用">停用</a-radio>
|
||||
@@ -182,11 +171,9 @@
|
||||
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea
|
||||
v-model="formData.remark"
|
||||
v-model:value="formData.remark"
|
||||
:rows="3"
|
||||
placeholder="请输入备注信息"
|
||||
@input="handleFieldChange('remark', $event.target.value)"
|
||||
@change="handleFieldChange('remark', $event.target.value)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
@@ -219,7 +206,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { ref, reactive, onMounted, computed, nextTick } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import {
|
||||
PlusOutlined,
|
||||
@@ -415,9 +402,9 @@ const loadData = async () => {
|
||||
pageSize: pagination.pageSize
|
||||
}
|
||||
|
||||
// 精确匹配栏舍名称
|
||||
if (searchValue.value) {
|
||||
params.name = searchValue.value
|
||||
// 搜索关键词(后端使用 search 参数)
|
||||
if (searchValue.value && searchValue.value.trim()) {
|
||||
params.search = searchValue.value.trim()
|
||||
}
|
||||
|
||||
console.log('📤 [栏舍设置] 发送请求参数:', params)
|
||||
@@ -454,7 +441,7 @@ const handleAdd = () => {
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (record) => {
|
||||
const handleEdit = async (record) => {
|
||||
console.log('🔄 [栏舍设置] 开始编辑操作')
|
||||
console.log('📋 [栏舍设置] 原始记录数据:', {
|
||||
id: record.id,
|
||||
@@ -471,10 +458,30 @@ const handleEdit = (record) => {
|
||||
})
|
||||
|
||||
modalTitle.value = '编辑栏舍'
|
||||
Object.assign(formData, record)
|
||||
|
||||
// 填充表单数据 - 使用直接赋值确保响应式更新
|
||||
formData.id = record.id || null
|
||||
formData.name = record.name || ''
|
||||
formData.code = record.code || ''
|
||||
formData.type = record.type || ''
|
||||
formData.capacity = record.capacity || 0
|
||||
formData.currentCount = record.currentCount || 0
|
||||
formData.area = record.area || null
|
||||
formData.location = record.location || ''
|
||||
formData.status = record.status || '启用'
|
||||
formData.remark = record.remark || ''
|
||||
formData.farmId = record.farmId || record.farm_id || null
|
||||
|
||||
console.log('📝 [栏舍设置] 表单数据已填充:', formData)
|
||||
|
||||
// 打开模态框
|
||||
modalVisible.value = true
|
||||
|
||||
// 等待模态框和表单渲染完成
|
||||
await nextTick()
|
||||
await new Promise(resolve => setTimeout(resolve, 200))
|
||||
|
||||
console.log('✅ [栏舍设置] 模态框已打开,表单应该已填充')
|
||||
}
|
||||
|
||||
const handleDelete = (record) => {
|
||||
|
||||
@@ -168,18 +168,15 @@
|
||||
>
|
||||
<a-form-item label="牛只耳号" name="earNumber">
|
||||
<a-input
|
||||
v-model="formData.earNumber"
|
||||
v-model:value="formData.earNumber"
|
||||
placeholder="请输入牛只耳号"
|
||||
@input="handleFieldChange('earNumber', $event.target.value)"
|
||||
@change="handleFieldChange('earNumber', $event.target.value)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="转出栏舍" name="fromPenId">
|
||||
<a-select
|
||||
v-model="formData.fromPenId"
|
||||
v-model:value="formData.fromPenId"
|
||||
placeholder="请选择转出栏舍"
|
||||
@change="handleFieldChange('fromPenId', $event)"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="pen in penList"
|
||||
@@ -193,9 +190,8 @@
|
||||
|
||||
<a-form-item label="转入栏舍" name="toPenId">
|
||||
<a-select
|
||||
v-model="formData.toPenId"
|
||||
v-model:value="formData.toPenId"
|
||||
placeholder="请选择转入栏舍"
|
||||
@change="handleFieldChange('toPenId', $event)"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="pen in penList"
|
||||
@@ -209,18 +205,16 @@
|
||||
|
||||
<a-form-item label="转栏日期" name="transferDate">
|
||||
<a-date-picker
|
||||
v-model="formData.transferDate"
|
||||
v-model:value="formData.transferDate"
|
||||
style="width: 100%"
|
||||
placeholder="请选择转栏日期"
|
||||
@change="handleFieldChange('transferDate', $event)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="转栏原因" name="reason">
|
||||
<a-select
|
||||
v-model="formData.reason"
|
||||
v-model:value="formData.reason"
|
||||
placeholder="请选择转栏原因"
|
||||
@change="handleFieldChange('reason', $event)"
|
||||
>
|
||||
<a-select-option value="正常调栏">正常调栏</a-select-option>
|
||||
<a-select-option value="疾病治疗">疾病治疗</a-select-option>
|
||||
@@ -233,17 +227,14 @@
|
||||
|
||||
<a-form-item label="操作人员" name="operator">
|
||||
<a-input
|
||||
v-model="formData.operator"
|
||||
v-model:value="formData.operator"
|
||||
placeholder="请输入操作人员姓名"
|
||||
@input="handleFieldChange('operator', $event.target.value)"
|
||||
@change="handleFieldChange('operator', $event.target.value)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-radio-group
|
||||
v-model="formData.status"
|
||||
@change="handleFieldChange('status', $event.target.value)"
|
||||
v-model:value="formData.status"
|
||||
>
|
||||
<a-radio value="已完成">已完成</a-radio>
|
||||
<a-radio value="进行中">进行中</a-radio>
|
||||
@@ -252,11 +243,9 @@
|
||||
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea
|
||||
v-model="formData.remark"
|
||||
v-model:value="formData.remark"
|
||||
:rows="3"
|
||||
placeholder="请输入备注信息"
|
||||
@input="handleFieldChange('remark', $event.target.value)"
|
||||
@change="handleFieldChange('remark', $event.target.value)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
@@ -567,7 +556,7 @@ const handleAdd = () => {
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (record) => {
|
||||
const handleEdit = async (record) => {
|
||||
console.log('🔄 [转栏记录] 开始编辑操作')
|
||||
console.log('📋 [转栏记录] 原始记录数据:', {
|
||||
id: record.id,
|
||||
@@ -584,20 +573,50 @@ const handleEdit = (record) => {
|
||||
})
|
||||
|
||||
modalTitle.value = '编辑转栏记录'
|
||||
Object.assign(formData, {
|
||||
id: record.id,
|
||||
earNumber: record.earNumber,
|
||||
fromPenId: record.fromPenId,
|
||||
toPenId: record.toPenId,
|
||||
transferDate: record.transferDate ? dayjs(record.transferDate) : null,
|
||||
reason: record.reason,
|
||||
operator: record.operator,
|
||||
status: record.status,
|
||||
remark: record.remark || ''
|
||||
})
|
||||
|
||||
// 确保栏舍列表已加载
|
||||
if (penList.value.length === 0) {
|
||||
await loadPenList()
|
||||
}
|
||||
|
||||
// 处理日期转换
|
||||
let transferDateValue = null
|
||||
if (record.transferDate) {
|
||||
if (typeof record.transferDate === 'string') {
|
||||
transferDateValue = dayjs(record.transferDate)
|
||||
} else if (record.transferDate instanceof Date) {
|
||||
transferDateValue = dayjs(record.transferDate)
|
||||
} else if (record.transferDate && typeof record.transferDate === 'object' && record.transferDate.format) {
|
||||
// 如果已经是 dayjs 对象,直接使用
|
||||
transferDateValue = record.transferDate
|
||||
} else {
|
||||
transferDateValue = dayjs(record.transferDate)
|
||||
}
|
||||
console.log('📅 [转栏记录] 日期转换:', record.transferDate, '->', transferDateValue?.format('YYYY-MM-DD'))
|
||||
}
|
||||
|
||||
// 填充表单数据
|
||||
formData.id = record.id
|
||||
formData.earNumber = record.earNumber || ''
|
||||
formData.fromPenId = record.fromPenId || null
|
||||
formData.toPenId = record.toPenId || null
|
||||
formData.transferDate = transferDateValue
|
||||
formData.reason = record.reason || ''
|
||||
formData.operator = record.operator || ''
|
||||
formData.status = record.status || '进行中'
|
||||
formData.remark = record.remark || ''
|
||||
|
||||
console.log('📝 [转栏记录] 表单数据已填充:', formData)
|
||||
console.log('📝 [转栏记录] transferDate 是否为 dayjs:', formData.transferDate && typeof formData.transferDate.format === 'function')
|
||||
|
||||
// 打开模态框
|
||||
modalVisible.value = true
|
||||
|
||||
// 等待模态框和表单渲染完成
|
||||
await nextTick()
|
||||
await new Promise(resolve => setTimeout(resolve, 200))
|
||||
|
||||
console.log('✅ [转栏记录] 模态框已打开,表单应该已填充')
|
||||
}
|
||||
|
||||
const handleDelete = (record) => {
|
||||
|
||||
@@ -95,19 +95,16 @@
|
||||
<a-col :span="12">
|
||||
<a-form-item label="栏舍名" name="name" required>
|
||||
<a-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入栏舍名"
|
||||
@input="(e) => { console.log('栏舍名输入:', e.target.value); formData.name = e.target.value; }"
|
||||
@change="(e) => { console.log('栏舍名变化:', e.target.value); }"
|
||||
v-model:value="formData.name"
|
||||
placeholder="请输入栏舍名"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="动物类型" name="animal_type" required>
|
||||
<a-select
|
||||
v-model="formData.animal_type"
|
||||
v-model:value="formData.animal_type"
|
||||
placeholder="请选择动物类型"
|
||||
@change="(value) => { console.log('动物类型变化:', value); }"
|
||||
>
|
||||
<a-select-option value="马">马</a-select-option>
|
||||
<a-select-option value="牛">牛</a-select-option>
|
||||
@@ -123,20 +120,16 @@
|
||||
<a-col :span="12">
|
||||
<a-form-item label="栏舍类型" name="pen_type">
|
||||
<a-input
|
||||
v-model="formData.pen_type"
|
||||
v-model:value="formData.pen_type"
|
||||
placeholder="请输入栏舍类型"
|
||||
@input="(e) => { console.log('栏舍类型输入:', e.target.value); formData.pen_type = e.target.value; }"
|
||||
@change="(e) => { console.log('栏舍类型变化:', e.target.value); }"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="负责人" name="responsible" required>
|
||||
<a-input
|
||||
v-model="formData.responsible"
|
||||
v-model:value="formData.responsible"
|
||||
placeholder="请输入负责人"
|
||||
@input="(e) => { console.log('负责人输入:', e.target.value); formData.responsible = e.target.value; }"
|
||||
@change="(e) => { console.log('负责人变化:', e.target.value); }"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
@@ -146,20 +139,18 @@
|
||||
<a-col :span="12">
|
||||
<a-form-item label="容量" name="capacity" required>
|
||||
<a-input-number
|
||||
v-model="formData.capacity"
|
||||
v-model:value="formData.capacity"
|
||||
:min="1"
|
||||
:max="10000"
|
||||
placeholder="请输入容量"
|
||||
style="width: 100%"
|
||||
@change="(value) => { console.log('容量变化:', value); formData.capacity = value; }"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-switch
|
||||
:checked="formData.status"
|
||||
@change="(checked) => { console.log('状态变化:', checked); formData.status = checked; }"
|
||||
v-model:checked="formData.status"
|
||||
:checked-children="'开启'"
|
||||
:un-checked-children="'关闭'"
|
||||
/>
|
||||
@@ -169,11 +160,9 @@
|
||||
|
||||
<a-form-item label="备注" name="description">
|
||||
<a-textarea
|
||||
v-model="formData.description"
|
||||
v-model:value="formData.description"
|
||||
placeholder="请输入备注信息"
|
||||
:rows="3"
|
||||
@input="(e) => { console.log('备注输入:', e.target.value); formData.description = e.target.value; }"
|
||||
@change="(e) => { console.log('备注变化:', e.target.value); }"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
@@ -182,7 +171,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import { PlusOutlined, SearchOutlined, ExportOutlined } from '@ant-design/icons-vue'
|
||||
import { api } from '../utils/api'
|
||||
@@ -364,21 +353,21 @@ const showAddModal = () => {
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (record) => {
|
||||
const handleEdit = async (record) => {
|
||||
console.log('=== 开始编辑栏舍 ===')
|
||||
console.log('点击编辑按钮,原始记录数据:', record)
|
||||
|
||||
isEdit.value = true
|
||||
Object.assign(formData, {
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
animal_type: record.animal_type,
|
||||
pen_type: record.pen_type,
|
||||
responsible: record.responsible,
|
||||
capacity: record.capacity,
|
||||
status: record.status,
|
||||
description: record.description
|
||||
})
|
||||
|
||||
// 填充表单数据 - 使用直接赋值确保响应式更新
|
||||
formData.id = record.id || null
|
||||
formData.name = record.name || ''
|
||||
formData.animal_type = record.animal_type || ''
|
||||
formData.pen_type = record.pen_type || ''
|
||||
formData.responsible = record.responsible || ''
|
||||
formData.capacity = record.capacity || 0
|
||||
formData.status = record.status || false
|
||||
formData.description = record.description || ''
|
||||
|
||||
console.log('编辑模式:表单数据已填充')
|
||||
console.log('formData对象:', formData)
|
||||
@@ -390,8 +379,14 @@ const handleEdit = (record) => {
|
||||
console.log('formData.status:', formData.status)
|
||||
console.log('formData.description:', formData.description)
|
||||
|
||||
// 打开模态框
|
||||
modalVisible.value = true
|
||||
console.log('编辑模态框已打开')
|
||||
|
||||
// 等待模态框和表单渲染完成
|
||||
await nextTick()
|
||||
await new Promise(resolve => setTimeout(resolve, 200))
|
||||
|
||||
console.log('编辑模态框已打开,表单应该已填充')
|
||||
}
|
||||
|
||||
const handleDelete = (record) => {
|
||||
|
||||
@@ -166,24 +166,24 @@
|
||||
<!-- 操作 -->
|
||||
<template v-else-if="column.dataIndex === 'action'">
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 2px;">
|
||||
<a-button type="link" size="small" style="color: #1890ff; padding: 0 4px;" @click="editDevice(record)">
|
||||
<!-- <a-button type="link" size="small" style="color: #1890ff; padding: 0 4px;" @click="editDevice(record)">
|
||||
修改绑定
|
||||
</a-button>
|
||||
</a-button> -->
|
||||
<a-button type="link" size="small" style="color: #1890ff; padding: 0 4px;" @click="clearData(record)">
|
||||
溯源二维码
|
||||
</a-button>
|
||||
<a-button type="link" size="small" style="color: #1890ff; padding: 0 4px;" @click="bindDevice(record)">
|
||||
绑定信息
|
||||
</a-button>
|
||||
<a-button type="link" size="small" style="color: #1890ff; padding: 0 4px;" @click="setDefault(record)">
|
||||
<!-- <a-button type="link" size="small" style="color: #1890ff; padding: 0 4px;" @click="setDefault(record)">
|
||||
设置频次
|
||||
</a-button>
|
||||
<a-button type="link" size="small" style="color: #1890ff; padding: 0 4px;" @click="viewTrack(record)">
|
||||
</a-button> -->
|
||||
<!-- <a-button type="link" size="small" style="color: #1890ff; padding: 0 4px;" @click="viewTrack(record)">
|
||||
日志
|
||||
</a-button>
|
||||
<a-button type="link" size="small" style="color: #1890ff; padding: 0 4px;" @click="viewTrack(record)">
|
||||
查看轨迹
|
||||
</a-button>
|
||||
</a-button> -->
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
@@ -553,6 +553,24 @@
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<!-- 溯源二维码模态框 -->
|
||||
<a-modal
|
||||
:open="qrcodeVisible"
|
||||
title="溯源二维码"
|
||||
:footer="null"
|
||||
width="450px"
|
||||
@cancel="handleQRCodeCancel"
|
||||
>
|
||||
<div class="qrcode-modal-content">
|
||||
<div class="qrcode-container">
|
||||
<img v-if="qrcodeUrl" :src="qrcodeUrl" alt="溯源二维码" class="qrcode-image" />
|
||||
</div>
|
||||
<div class="device-sn-display">
|
||||
{{ currentDeviceSn }}
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<!-- 添加/编辑模态框 -->
|
||||
<a-modal
|
||||
:open="modalVisible"
|
||||
@@ -613,6 +631,7 @@ import { PlusOutlined, ReloadOutlined, ExportOutlined, EnvironmentOutlined, Crow
|
||||
import { api, directApi } from '../utils/api'
|
||||
import { ExportUtils } from '../utils/exportUtils'
|
||||
import { loadBMapScript, createMap } from '@/utils/mapService'
|
||||
import QRCode from 'qrcode'
|
||||
|
||||
// 响应式数据
|
||||
const collars = ref([])
|
||||
@@ -643,6 +662,11 @@ const currentLocation = ref(null)
|
||||
const baiduMap = ref(null)
|
||||
const locationMarker = ref(null)
|
||||
|
||||
// 二维码相关数据
|
||||
const qrcodeVisible = ref(false)
|
||||
const qrcodeUrl = ref('')
|
||||
const currentDeviceSn = ref('')
|
||||
|
||||
// 标签页配置
|
||||
const tabs = [
|
||||
{ key: 'identity', label: '身份信息' },
|
||||
@@ -1013,8 +1037,30 @@ const editDevice = (record) => {
|
||||
message.info(`修改设备 ${record.deviceId}`)
|
||||
}
|
||||
|
||||
const clearData = (record) => {
|
||||
message.info(`清除设备 ${record.deviceId} 的数据`)
|
||||
// 显示溯源二维码
|
||||
const clearData = async (record) => {
|
||||
try {
|
||||
const deviceSn = record.sn
|
||||
if (!deviceSn) {
|
||||
message.warning('设备编号不存在')
|
||||
return
|
||||
}
|
||||
|
||||
currentDeviceSn.value = deviceSn
|
||||
const traceUrl = `https://farm.aiotagro.com/source/source-index.html?device_sn=${deviceSn}`
|
||||
|
||||
// 生成二维码
|
||||
const qrCodeDataUrl = await QRCode.toDataURL(traceUrl, {
|
||||
width: 250,
|
||||
margin: 2
|
||||
})
|
||||
|
||||
qrcodeUrl.value = qrCodeDataUrl
|
||||
qrcodeVisible.value = true
|
||||
} catch (error) {
|
||||
console.error('生成二维码失败:', error)
|
||||
message.error('生成二维码失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索项圈
|
||||
@@ -1334,6 +1380,13 @@ const handleLocationCancel = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭二维码模态框
|
||||
const handleQRCodeCancel = () => {
|
||||
qrcodeVisible.value = false
|
||||
qrcodeUrl.value = ''
|
||||
currentDeviceSn.value = ''
|
||||
}
|
||||
|
||||
// 初始化百度地图
|
||||
const initBaiduMap = async () => {
|
||||
if (!currentLocation.value) return
|
||||
@@ -1773,4 +1826,37 @@ onUnmounted(() => {
|
||||
border-top: 1px solid #f0f0f0;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* 二维码模态框样式 */
|
||||
.qrcode-modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.qrcode-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.qrcode-image {
|
||||
width: 250px;
|
||||
height: 250px;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.device-sn-display {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -154,18 +154,18 @@
|
||||
<!-- 操作列 -->
|
||||
<template #action="{ record }">
|
||||
<div class="action-buttons">
|
||||
<a-button type="link" size="small" @click="bindLivestock(record)">
|
||||
<!-- <a-button type="link" size="small" @click="bindLivestock(record)">
|
||||
绑定牲畜
|
||||
</a-button>
|
||||
</a-button> -->
|
||||
<a-button type="link" size="small" @click="showQRCode(record)">
|
||||
溯源二维码
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="showBindingInfo(record)">
|
||||
绑定信息
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="showLog(record)">
|
||||
<!-- <a-button type="link" size="small" @click="showLog(record)">
|
||||
日志
|
||||
</a-button>
|
||||
</a-button> -->
|
||||
</div>
|
||||
</template>
|
||||
</a-table>
|
||||
@@ -219,6 +219,24 @@
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<!-- 溯源二维码模态框 -->
|
||||
<a-modal
|
||||
:open="qrcodeVisible"
|
||||
title="溯源二维码"
|
||||
:footer="null"
|
||||
width="450px"
|
||||
@cancel="handleQRCodeCancel"
|
||||
>
|
||||
<div class="qrcode-modal-content">
|
||||
<div class="qrcode-container">
|
||||
<img v-if="qrcodeUrl" :src="qrcodeUrl" alt="溯源二维码" class="qrcode-image" />
|
||||
</div>
|
||||
<div class="device-sn-display">
|
||||
{{ currentDeviceSn }}
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<!-- 绑定信息模态框 -->
|
||||
<a-modal
|
||||
:open="bindInfoVisible"
|
||||
@@ -337,6 +355,7 @@ import { EnvironmentOutlined, SearchOutlined, ExportOutlined } from '@ant-design
|
||||
import { api, directApi } from '../utils/api'
|
||||
import { ExportUtils } from '../utils/exportUtils'
|
||||
import { formatBindingInfo } from '../utils/fieldMappings'
|
||||
import QRCode from 'qrcode'
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
@@ -362,6 +381,11 @@ const bindInfoData = ref({
|
||||
const activeTab = ref('basic')
|
||||
const currentEartagNumber = ref('')
|
||||
|
||||
// 二维码相关数据
|
||||
const qrcodeVisible = ref(false)
|
||||
const qrcodeUrl = ref('')
|
||||
const currentDeviceSn = ref('')
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
@@ -752,9 +776,30 @@ const bindLivestock = (record) => {
|
||||
message.info('绑定牲畜功能开发中...')
|
||||
}
|
||||
|
||||
// 显示二维码
|
||||
const showQRCode = (record) => {
|
||||
message.info('溯源二维码功能开发中...')
|
||||
// 显示溯源二维码
|
||||
const showQRCode = async (record) => {
|
||||
try {
|
||||
const deviceSn = record.eartagNumber
|
||||
if (!deviceSn) {
|
||||
message.warning('设备编号不存在')
|
||||
return
|
||||
}
|
||||
|
||||
currentDeviceSn.value = deviceSn
|
||||
const traceUrl = `https://farm.aiotagro.com/source/source-index.html?device_sn=${deviceSn}`
|
||||
|
||||
// 生成二维码
|
||||
const qrCodeDataUrl = await QRCode.toDataURL(traceUrl, {
|
||||
width: 250,
|
||||
margin: 2
|
||||
})
|
||||
|
||||
qrcodeUrl.value = qrCodeDataUrl
|
||||
qrcodeVisible.value = true
|
||||
} catch (error) {
|
||||
console.error('生成二维码失败:', error)
|
||||
message.error('生成二维码失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭绑定信息模态框
|
||||
@@ -773,6 +818,13 @@ const handleBindingInfoCancel = () => {
|
||||
activeTab.value = 'basic'
|
||||
}
|
||||
|
||||
// 关闭二维码模态框
|
||||
const handleQRCodeCancel = () => {
|
||||
qrcodeVisible.value = false
|
||||
qrcodeUrl.value = ''
|
||||
currentDeviceSn.value = ''
|
||||
}
|
||||
|
||||
// 显示绑定信息
|
||||
const showBindingInfo = async (record) => {
|
||||
try {
|
||||
@@ -1238,6 +1290,39 @@ onUnmounted(() => {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 二维码模态框样式 */
|
||||
.qrcode-modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.qrcode-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.qrcode-image {
|
||||
width: 250px;
|
||||
height: 250px;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.device-sn-display {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.search-bar .ant-col {
|
||||
margin-bottom: 12px;
|
||||
|
||||
@@ -96,9 +96,9 @@
|
||||
<a-button type="link" class="action-link" @click="viewLocation(record)">
|
||||
查看定位
|
||||
</a-button>
|
||||
<a-button type="link" class="action-link" @click="viewCollectionInfo(record)">
|
||||
<!-- <a-button type="link" class="action-link" @click="viewCollectionInfo(record)">
|
||||
查看采集信息
|
||||
</a-button>
|
||||
</a-button> -->
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -60,7 +60,7 @@ export default defineConfig(({ mode }) => {
|
||||
__APP_ENV__: JSON.stringify(env),
|
||||
// 在生产环境中强制使用正确的API URL
|
||||
'import.meta.env.VITE_API_BASE_URL': JSON.stringify(
|
||||
mode === 'production' ? 'https://ad.ningmuyun.com/farm/api' : (env.VITE_API_BASE_URL || '/api')
|
||||
mode === 'production' ? 'https://ad.liaoniuyun.com/farm/api' : (env.VITE_API_BASE_URL || '/api')
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +113,17 @@ class CattleExitRecordController {
|
||||
async getExitRecordById(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
console.log('=== 获取离栏记录详情 ===');
|
||||
console.log('请求时间:', new Date().toISOString());
|
||||
console.log('记录ID:', id);
|
||||
console.log('请求来源:', req.ip);
|
||||
console.log('用户信息:', req.user ? { id: req.user.id, username: req.user.username } : '未登录');
|
||||
console.log('User-Agent:', req.get('User-Agent'));
|
||||
console.log('请求URL:', req.originalUrl);
|
||||
console.log('请求方法:', req.method);
|
||||
console.log('Referer:', req.get('Referer'));
|
||||
console.log('操作类型: 获取详情(可能用于编辑)');
|
||||
|
||||
const record = await CattleExitRecord.findByPk(id, {
|
||||
attributes: ['id', 'recordId', 'animalId', 'earNumber', 'exitDate', 'exitReason', 'originalPenId', 'destination', 'disposalMethod', 'handler', 'status', 'remark', 'farmId', 'created_at', 'updated_at'],
|
||||
@@ -136,19 +147,88 @@ class CattleExitRecordController {
|
||||
});
|
||||
|
||||
if (!record) {
|
||||
console.log('离栏记录不存在,ID:', id);
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '离栏记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
console.log('找到离栏记录:', {
|
||||
id: record.id,
|
||||
recordId: record.recordId,
|
||||
earNumber: record.earNumber,
|
||||
exitDate: record.exitDate,
|
||||
exitReason: record.exitReason,
|
||||
originalPenId: record.originalPenId,
|
||||
farmId: record.farmId,
|
||||
status: record.status
|
||||
});
|
||||
|
||||
// 格式化返回数据
|
||||
const formattedData = {
|
||||
id: record.id,
|
||||
recordId: record.recordId,
|
||||
animalId: record.animalId,
|
||||
earNumber: record.earNumber,
|
||||
exitDate: record.exitDate,
|
||||
exitReason: record.exitReason,
|
||||
originalPenId: record.originalPenId,
|
||||
originalPen: record.originalPen ? {
|
||||
id: record.originalPen.id,
|
||||
name: record.originalPen.name,
|
||||
code: record.originalPen.code
|
||||
} : null,
|
||||
destination: record.destination,
|
||||
disposalMethod: record.disposalMethod,
|
||||
handler: record.handler,
|
||||
status: record.status,
|
||||
remark: record.remark,
|
||||
farmId: record.farmId,
|
||||
farm: record.farm ? {
|
||||
id: record.farm.id,
|
||||
name: record.farm.name
|
||||
} : null,
|
||||
cattle: record.cattle ? {
|
||||
id: record.cattle.id,
|
||||
earNumber: record.cattle.earNumber,
|
||||
strain: record.cattle.strain,
|
||||
sex: record.cattle.sex
|
||||
} : null,
|
||||
created_at: record.created_at,
|
||||
updated_at: record.updated_at
|
||||
};
|
||||
|
||||
console.log('=== 返回格式化后的数据 ===');
|
||||
console.log('格式化数据示例:', {
|
||||
id: formattedData.id,
|
||||
recordId: formattedData.recordId,
|
||||
earNumber: formattedData.earNumber,
|
||||
exitDate: formattedData.exitDate,
|
||||
exitReason: formattedData.exitReason,
|
||||
originalPenId: formattedData.originalPenId,
|
||||
originalPenName: formattedData.originalPen?.name,
|
||||
destination: formattedData.destination,
|
||||
disposalMethod: formattedData.disposalMethod,
|
||||
handler: formattedData.handler,
|
||||
status: formattedData.status,
|
||||
farmId: formattedData.farmId,
|
||||
farmName: formattedData.farm?.name
|
||||
});
|
||||
console.log('完整返回数据字段:', Object.keys(formattedData));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: record,
|
||||
data: formattedData,
|
||||
message: '获取离栏记录详情成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取离栏记录详情失败:', error);
|
||||
console.error('=== 获取离栏记录详情失败 ===');
|
||||
console.error('错误时间:', new Date().toISOString());
|
||||
console.error('记录ID:', req.params.id);
|
||||
console.error('错误信息:', error.message);
|
||||
console.error('错误堆栈:', error.stack);
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取离栏记录详情失败',
|
||||
|
||||
@@ -10,24 +10,38 @@ class CattlePenController {
|
||||
*/
|
||||
async getPens(req, res) {
|
||||
try {
|
||||
const { page = 1, pageSize = 10, search, status, type } = req.query;
|
||||
const { page = 1, pageSize = 10, search, name, status, type } = req.query;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
console.log('=== 获取栏舍列表 ===');
|
||||
console.log('请求时间:', new Date().toISOString());
|
||||
console.log('请求参数:', { page, pageSize, search, name, status, type });
|
||||
console.log('请求来源:', req.ip);
|
||||
|
||||
// 构建查询条件
|
||||
const where = {};
|
||||
if (search) {
|
||||
|
||||
// 支持 search 和 name 参数(兼容性处理)
|
||||
const searchKeyword = search || name;
|
||||
if (searchKeyword) {
|
||||
console.log('🔍 [后端-栏舍设置] 搜索关键词:', searchKeyword);
|
||||
where[Op.or] = [
|
||||
{ name: { [Op.like]: `%${search}%` } },
|
||||
{ code: { [Op.like]: `%${search}%` } }
|
||||
{ name: { [Op.like]: `%${searchKeyword}%` } },
|
||||
{ code: { [Op.like]: `%${searchKeyword}%` } }
|
||||
];
|
||||
console.log('🔍 [后端-栏舍设置] 搜索条件构建完成');
|
||||
}
|
||||
|
||||
if (status) {
|
||||
where.status = status;
|
||||
}
|
||||
if (type) {
|
||||
where.type = type;
|
||||
}
|
||||
|
||||
console.log('🔍 [后端-栏舍设置] 构建的查询条件:', JSON.stringify(where, null, 2));
|
||||
|
||||
console.log('🔍 [后端-栏舍设置] 开始执行查询...');
|
||||
const { count, rows } = await CattlePen.findAndCountAll({
|
||||
where,
|
||||
include: [
|
||||
@@ -42,6 +56,18 @@ class CattlePenController {
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
console.log('📊 [后端-栏舍设置] 查询结果:', {
|
||||
总数: count,
|
||||
当前页记录数: rows.length,
|
||||
记录列表: rows.map(item => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
code: item.code,
|
||||
type: item.type,
|
||||
status: item.status
|
||||
}))
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
const { Op } = require('sequelize');
|
||||
const XLSX = require('xlsx');
|
||||
const path = require('path');
|
||||
const IotCattle = require('../models/IotCattle');
|
||||
const Farm = require('../models/Farm');
|
||||
const CattlePen = require('../models/CattlePen');
|
||||
@@ -38,6 +40,77 @@ const getCategoryName = (cate) => {
|
||||
return categoryMap[cate] || '未知';
|
||||
};
|
||||
|
||||
/**
|
||||
* 类别名称到ID的映射(用于导入)
|
||||
*/
|
||||
const getCategoryId = (name) => {
|
||||
const categoryMap = {
|
||||
'犊牛': 1,
|
||||
'育成母牛': 2,
|
||||
'架子牛': 3,
|
||||
'青年牛': 4,
|
||||
'基础母牛': 5,
|
||||
'育肥牛': 6
|
||||
};
|
||||
return categoryMap[name] || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 性别名称到ID的映射(用于导入)
|
||||
*/
|
||||
const getSexId = (name) => {
|
||||
const sexMap = {
|
||||
'公': 1,
|
||||
'公牛': 1,
|
||||
'母': 2,
|
||||
'母牛': 2
|
||||
};
|
||||
return sexMap[name] || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 来源名称到ID的映射(用于导入)
|
||||
*/
|
||||
const getSourceId = (name) => {
|
||||
const sourceMap = {
|
||||
'购买': 1,
|
||||
'自繁': 2,
|
||||
'放生': 3,
|
||||
'合作社': 4,
|
||||
'入股': 5
|
||||
};
|
||||
return sourceMap[name] || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 血统纯度名称到ID的映射(用于导入)
|
||||
*/
|
||||
const getDescentId = (name) => {
|
||||
const descentMap = {
|
||||
'纯血': 1,
|
||||
'纯种': 1,
|
||||
'杂交': 2,
|
||||
'杂交一代': 2,
|
||||
'杂交二代': 3,
|
||||
'杂交三代': 4
|
||||
};
|
||||
return descentMap[name] || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 日期字符串转换为时间戳(秒)
|
||||
*/
|
||||
const dateToTimestamp = (dateStr) => {
|
||||
if (!dateStr) return null;
|
||||
|
||||
// 支持多种日期格式:2023-01-01, 2023/01/01, 2023-1-1
|
||||
const date = new Date(dateStr);
|
||||
if (isNaN(date.getTime())) {
|
||||
return null;
|
||||
}
|
||||
return Math.floor(date.getTime() / 1000);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取栏舍、批次、品种和用途名称
|
||||
*/
|
||||
@@ -224,9 +297,12 @@ class IotCattleController {
|
||||
id: cattle.id,
|
||||
earNumber: cattle.earNumber, // 映射iot_cattle.ear_number
|
||||
sex: cattle.sex, // 映射iot_cattle.sex
|
||||
strain: userNames[cattle.strain] || `品系ID:${cattle.strain}`, // 映射iot_cattle.strain为用途名称
|
||||
varieties: typeNames[cattle.varieties] || `品种ID:${cattle.varieties}`, // 映射iot_cattle.varieties为品种名称
|
||||
cate: getCategoryName(cattle.cate), // 映射iot_cattle.cate为中文
|
||||
strain: userNames[cattle.strain] || `品系ID:${cattle.strain}`, // 映射iot_cattle.strain为用途名称(用于显示)
|
||||
strainId: cattle.strain, // 原始ID(用于编辑和提交)
|
||||
varieties: typeNames[cattle.varieties] || `品种ID:${cattle.varieties}`, // 映射iot_cattle.varieties为品种名称(用于显示)
|
||||
varietiesId: cattle.varieties, // 原始ID(用于编辑和提交)
|
||||
cate: getCategoryName(cattle.cate), // 映射iot_cattle.cate为中文(用于显示)
|
||||
cateId: cattle.cate, // 原始ID(用于编辑和提交)
|
||||
birthWeight: cattle.birthWeight, // 映射iot_cattle.birth_weight
|
||||
birthday: cattle.birthday, // 映射iot_cattle.birthday
|
||||
intoTime: cattle.intoTime,
|
||||
@@ -287,6 +363,13 @@ class IotCattleController {
|
||||
async getCattleArchiveById(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
console.log('=== 获取牛只档案详情 ===');
|
||||
console.log('请求时间:', new Date().toISOString());
|
||||
console.log('档案ID:', id);
|
||||
console.log('请求来源:', req.ip);
|
||||
console.log('用户信息:', req.user ? { id: req.user.id, username: req.user.username } : '未登录');
|
||||
console.log('User-Agent:', req.get('User-Agent'));
|
||||
|
||||
const cattle = await IotCattle.findByPk(id, {
|
||||
include: [
|
||||
@@ -309,20 +392,36 @@ class IotCattleController {
|
||||
});
|
||||
|
||||
if (!cattle) {
|
||||
console.log('牛只档案不存在,ID:', id);
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '牛只档案不存在'
|
||||
});
|
||||
}
|
||||
|
||||
console.log('找到牛只档案:', {
|
||||
id: cattle.id,
|
||||
earNumber: cattle.earNumber,
|
||||
orgId: cattle.orgId,
|
||||
penId: cattle.penId,
|
||||
batchId: cattle.batchId
|
||||
});
|
||||
|
||||
// 获取栏舍、批次、品种和用途名称
|
||||
const cattleList = [cattle];
|
||||
const { penNames, batchNames, typeNames, userNames } = await getPenBatchTypeAndUserNames(cattleList);
|
||||
|
||||
// 格式化数据(基于iot_cattle表字段映射)
|
||||
const formattedData = {
|
||||
id: cattle.id,
|
||||
earNumber: cattle.earNumber, // 映射iot_cattle.ear_number
|
||||
sex: cattle.sex, // 映射iot_cattle.sex
|
||||
strain: cattle.strain, // 映射iot_cattle.strain
|
||||
varieties: cattle.varieties, // 映射iot_cattle.varieties(单个记录不需要名称映射)
|
||||
cate: cattle.cate, // 映射iot_cattle.cate
|
||||
strain: userNames[cattle.strain] || `品系ID:${cattle.strain}`, // 映射iot_cattle.strain为用途名称(用于显示)
|
||||
strainId: cattle.strain, // 原始ID(用于编辑和提交)
|
||||
varieties: typeNames[cattle.varieties] || `品种ID:${cattle.varieties}`, // 映射iot_cattle.varieties为品种名称(用于显示)
|
||||
varietiesId: cattle.varieties, // 原始ID(用于编辑和提交)
|
||||
cate: getCategoryName(cattle.cate), // 映射iot_cattle.cate为中文(用于显示)
|
||||
cateId: cattle.cate, // 原始ID(用于编辑和提交)
|
||||
birthWeight: cattle.birthWeight, // 映射iot_cattle.birth_weight
|
||||
birthday: cattle.birthday, // 映射iot_cattle.birthday
|
||||
intoTime: cattle.intoTime,
|
||||
@@ -336,20 +435,34 @@ class IotCattleController {
|
||||
weightCalculateTime: cattle.weightCalculateTime,
|
||||
dayOfBirthday: cattle.dayOfBirthday,
|
||||
farmName: `农场ID:${cattle.orgId}`, // 暂时显示ID,后续可优化
|
||||
penName: cattle.penId ? `栏舍ID:${cattle.penId}` : '未分配栏舍', // 暂时显示ID,后续可优化
|
||||
batchName: cattle.batchId === 0 ? '未分配批次' : `批次ID:${cattle.batchId}`, // 暂时显示ID,后续可优化
|
||||
penName: cattle.penId ? (penNames[cattle.penId] || `栏舍ID:${cattle.penId}`) : '未分配栏舍', // 映射栏舍名称
|
||||
batchName: cattle.batchId === 0 ? '未分配批次' : (batchNames[cattle.batchId] || `批次ID:${cattle.batchId}`), // 映射批次名称
|
||||
farmId: cattle.orgId, // 映射iot_cattle.org_id
|
||||
penId: cattle.penId, // 映射iot_cattle.pen_id
|
||||
batchId: cattle.batchId // 映射iot_cattle.batch_id
|
||||
};
|
||||
|
||||
console.log('=== 返回格式化后的数据 ===');
|
||||
console.log('格式化数据示例:', {
|
||||
id: formattedData.id,
|
||||
earNumber: formattedData.earNumber,
|
||||
farmId: formattedData.farmId,
|
||||
penId: formattedData.penId,
|
||||
batchId: formattedData.batchId
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: formattedData,
|
||||
message: '获取牛只档案详情成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取牛只档案详情失败:', error);
|
||||
console.error('=== 获取牛只档案详情失败 ===');
|
||||
console.error('错误时间:', new Date().toISOString());
|
||||
console.error('档案ID:', req.params.id);
|
||||
console.error('错误信息:', error.message);
|
||||
console.error('错误堆栈:', error.stack);
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取牛只档案详情失败',
|
||||
@@ -500,23 +613,84 @@ class IotCattleController {
|
||||
}
|
||||
}
|
||||
|
||||
// 转换数据类型
|
||||
// 转换数据类型,只更新实际提交的字段
|
||||
const processedData = {};
|
||||
if (updateData.earNumber) processedData.earNumber = parseInt(updateData.earNumber);
|
||||
if (updateData.sex) processedData.sex = parseInt(updateData.sex);
|
||||
if (updateData.strain) processedData.strain = parseInt(updateData.strain);
|
||||
if (updateData.varieties) processedData.varieties = parseInt(updateData.varieties);
|
||||
if (updateData.cate) processedData.cate = parseInt(updateData.cate);
|
||||
if (updateData.birthWeight) processedData.birthWeight = parseFloat(updateData.birthWeight);
|
||||
if (updateData.birthday) processedData.birthday = parseInt(updateData.birthday);
|
||||
if (updateData.penId) processedData.penId = parseInt(updateData.penId);
|
||||
if (updateData.intoTime) processedData.intoTime = parseInt(updateData.intoTime);
|
||||
if (updateData.parity) processedData.parity = parseInt(updateData.parity);
|
||||
if (updateData.source) processedData.source = parseInt(updateData.source);
|
||||
if (updateData.sourceDay) processedData.sourceDay = parseInt(updateData.sourceDay);
|
||||
if (updateData.sourceWeight) processedData.sourceWeight = parseFloat(updateData.sourceWeight);
|
||||
if (updateData.orgId) processedData.orgId = parseInt(updateData.orgId);
|
||||
if (updateData.batchId) processedData.batchId = parseInt(updateData.batchId);
|
||||
|
||||
// 辅助函数:安全转换为整数,如果转换失败则返回原值(不更新该字段)
|
||||
const safeParseInt = (value) => {
|
||||
if (value === null || value === undefined || value === '') return undefined;
|
||||
const parsed = parseInt(value);
|
||||
return isNaN(parsed) ? undefined : parsed;
|
||||
};
|
||||
|
||||
// 辅助函数:安全转换为浮点数,如果转换失败则返回原值(不更新该字段)
|
||||
const safeParseFloat = (value) => {
|
||||
if (value === null || value === undefined || value === '') return undefined;
|
||||
const parsed = parseFloat(value);
|
||||
return isNaN(parsed) ? undefined : parsed;
|
||||
};
|
||||
|
||||
// 只更新实际提交的字段,如果字段值无效则跳过(保持原有值)
|
||||
if (updateData.hasOwnProperty('earNumber')) {
|
||||
const parsed = safeParseInt(updateData.earNumber);
|
||||
if (parsed !== undefined) processedData.earNumber = parsed;
|
||||
}
|
||||
if (updateData.hasOwnProperty('sex')) {
|
||||
const parsed = safeParseInt(updateData.sex);
|
||||
if (parsed !== undefined) processedData.sex = parsed;
|
||||
}
|
||||
if (updateData.hasOwnProperty('strain')) {
|
||||
const parsed = safeParseInt(updateData.strain);
|
||||
if (parsed !== undefined) processedData.strain = parsed;
|
||||
}
|
||||
if (updateData.hasOwnProperty('varieties')) {
|
||||
const parsed = safeParseInt(updateData.varieties);
|
||||
if (parsed !== undefined) processedData.varieties = parsed;
|
||||
}
|
||||
if (updateData.hasOwnProperty('cate')) {
|
||||
const parsed = safeParseInt(updateData.cate);
|
||||
if (parsed !== undefined) processedData.cate = parsed;
|
||||
}
|
||||
if (updateData.hasOwnProperty('birthWeight')) {
|
||||
const parsed = safeParseFloat(updateData.birthWeight);
|
||||
if (parsed !== undefined) processedData.birthWeight = parsed;
|
||||
}
|
||||
if (updateData.hasOwnProperty('birthday')) {
|
||||
const parsed = safeParseInt(updateData.birthday);
|
||||
if (parsed !== undefined) processedData.birthday = parsed;
|
||||
}
|
||||
if (updateData.hasOwnProperty('penId')) {
|
||||
const parsed = safeParseInt(updateData.penId);
|
||||
if (parsed !== undefined) processedData.penId = parsed;
|
||||
}
|
||||
if (updateData.hasOwnProperty('intoTime')) {
|
||||
const parsed = safeParseInt(updateData.intoTime);
|
||||
if (parsed !== undefined) processedData.intoTime = parsed;
|
||||
}
|
||||
if (updateData.hasOwnProperty('parity')) {
|
||||
const parsed = safeParseInt(updateData.parity);
|
||||
if (parsed !== undefined) processedData.parity = parsed;
|
||||
}
|
||||
if (updateData.hasOwnProperty('source')) {
|
||||
const parsed = safeParseInt(updateData.source);
|
||||
if (parsed !== undefined) processedData.source = parsed;
|
||||
}
|
||||
if (updateData.hasOwnProperty('sourceDay')) {
|
||||
const parsed = safeParseInt(updateData.sourceDay);
|
||||
if (parsed !== undefined) processedData.sourceDay = parsed;
|
||||
}
|
||||
if (updateData.hasOwnProperty('sourceWeight')) {
|
||||
const parsed = safeParseFloat(updateData.sourceWeight);
|
||||
if (parsed !== undefined) processedData.sourceWeight = parsed;
|
||||
}
|
||||
if (updateData.hasOwnProperty('orgId')) {
|
||||
const parsed = safeParseInt(updateData.orgId);
|
||||
if (parsed !== undefined) processedData.orgId = parsed;
|
||||
}
|
||||
if (updateData.hasOwnProperty('batchId')) {
|
||||
const parsed = safeParseInt(updateData.batchId);
|
||||
if (parsed !== undefined) processedData.batchId = parsed;
|
||||
}
|
||||
|
||||
await cattle.update(processedData);
|
||||
|
||||
@@ -728,22 +902,280 @@ class IotCattleController {
|
||||
});
|
||||
}
|
||||
|
||||
// 这里需要添加Excel解析逻辑
|
||||
// 由于没有安装xlsx库,先返回模拟数据
|
||||
const importedCount = 0;
|
||||
const errors = [];
|
||||
// 解析Excel文件
|
||||
const workbook = XLSX.readFile(file.path);
|
||||
const sheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[sheetName];
|
||||
const data = XLSX.utils.sheet_to_json(worksheet);
|
||||
|
||||
// TODO: 实现Excel文件解析和数据库插入逻辑
|
||||
// 1. 使用xlsx库解析Excel文件
|
||||
// 2. 验证数据格式
|
||||
// 3. 批量插入到数据库
|
||||
// 4. 返回导入结果
|
||||
console.log(`解析到 ${data.length} 行数据`);
|
||||
|
||||
if (data.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Excel文件中没有数据'
|
||||
});
|
||||
}
|
||||
|
||||
// 获取所有品种和品系(品类)的映射关系
|
||||
const cattleTypes = await CattleType.findAll({ attributes: ['id', 'name'] });
|
||||
const typeNameToId = {};
|
||||
cattleTypes.forEach(type => {
|
||||
typeNameToId[type.name] = type.id;
|
||||
});
|
||||
|
||||
const cattleUsers = await CattleUser.findAll({ attributes: ['id', 'name'] });
|
||||
const userNameToId = {};
|
||||
cattleUsers.forEach(user => {
|
||||
userNameToId[user.name] = user.id;
|
||||
});
|
||||
|
||||
// 获取所有栏舍和批次的映射关系
|
||||
const pens = await CattlePen.findAll({ attributes: ['id', 'name'] });
|
||||
const penNameToId = {};
|
||||
pens.forEach(pen => {
|
||||
penNameToId[pen.name] = pen.id;
|
||||
});
|
||||
|
||||
const batches = await CattleBatch.findAll({ attributes: ['id', 'name'] });
|
||||
const batchNameToId = {};
|
||||
batches.forEach(batch => {
|
||||
batchNameToId[batch.name] = batch.id;
|
||||
});
|
||||
|
||||
// 获取默认农场ID(从请求中获取或使用第一个农场)
|
||||
let defaultOrgId = req.body.orgId || req.query.orgId;
|
||||
if (!defaultOrgId) {
|
||||
const firstFarm = await Farm.findOne({ order: [['id', 'ASC']] });
|
||||
defaultOrgId = firstFarm ? firstFarm.id : null;
|
||||
}
|
||||
|
||||
if (!defaultOrgId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请指定所属农场'
|
||||
});
|
||||
}
|
||||
|
||||
const errors = [];
|
||||
const successData = [];
|
||||
|
||||
// 处理每一行数据
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const row = data[i];
|
||||
const rowNum = i + 2; // Excel行号(从2开始,第1行是表头)
|
||||
|
||||
try {
|
||||
// 验证必填字段
|
||||
if (!row['耳号']) {
|
||||
errors.push({ row: rowNum, field: '耳号', message: '耳号不能为空' });
|
||||
continue;
|
||||
}
|
||||
|
||||
// 映射字段
|
||||
const earNumber = String(row['耳号']).trim();
|
||||
|
||||
// 检查耳号是否已存在
|
||||
const existingCattle = await IotCattle.findOne({
|
||||
where: { earNumber: parseInt(earNumber) }
|
||||
});
|
||||
|
||||
if (existingCattle) {
|
||||
errors.push({ row: rowNum, field: '耳号', message: `耳号 ${earNumber} 已存在` });
|
||||
continue;
|
||||
}
|
||||
|
||||
// 品类(strain)- 从名称查找ID(必填)
|
||||
const strainName = row['品类'] ? String(row['品类']).trim() : '';
|
||||
if (!strainName) {
|
||||
errors.push({ row: rowNum, field: '品类', message: '品类不能为空' });
|
||||
continue;
|
||||
}
|
||||
const strainId = userNameToId[strainName];
|
||||
if (!strainId) {
|
||||
errors.push({ row: rowNum, field: '品类', message: `品类 "${strainName}" 不存在` });
|
||||
continue;
|
||||
}
|
||||
|
||||
// 品种(varieties)- 从名称查找ID(必填)
|
||||
const varietiesName = row['品种'] ? String(row['品种']).trim() : '';
|
||||
if (!varietiesName) {
|
||||
errors.push({ row: rowNum, field: '品种', message: '品种不能为空' });
|
||||
continue;
|
||||
}
|
||||
const varietiesId = typeNameToId[varietiesName];
|
||||
if (!varietiesId) {
|
||||
errors.push({ row: rowNum, field: '品种', message: `品种 "${varietiesName}" 不存在` });
|
||||
continue;
|
||||
}
|
||||
|
||||
// 生理阶段(cate)(必填)
|
||||
const cateName = row['生理阶段'] ? String(row['生理阶段']).trim() : '';
|
||||
if (!cateName) {
|
||||
errors.push({ row: rowNum, field: '生理阶段', message: '生理阶段不能为空' });
|
||||
continue;
|
||||
}
|
||||
const cateId = getCategoryId(cateName);
|
||||
if (!cateId) {
|
||||
errors.push({ row: rowNum, field: '生理阶段', message: `生理阶段 "${cateName}" 无效` });
|
||||
continue;
|
||||
}
|
||||
|
||||
// 性别(sex)(必填)
|
||||
const sexName = row['性别'] ? String(row['性别']).trim() : '';
|
||||
if (!sexName) {
|
||||
errors.push({ row: rowNum, field: '性别', message: '性别不能为空' });
|
||||
continue;
|
||||
}
|
||||
const sexId = getSexId(sexName);
|
||||
if (!sexId) {
|
||||
errors.push({ row: rowNum, field: '性别', message: `性别 "${sexName}" 无效,应为"公"或"母"` });
|
||||
continue;
|
||||
}
|
||||
|
||||
// 来源(source)(必填)
|
||||
const sourceName = row['来源'] ? String(row['来源']).trim() : '';
|
||||
if (!sourceName) {
|
||||
errors.push({ row: rowNum, field: '来源', message: '来源不能为空' });
|
||||
continue;
|
||||
}
|
||||
const sourceId = getSourceId(sourceName);
|
||||
if (!sourceId) {
|
||||
errors.push({ row: rowNum, field: '来源', message: `来源 "${sourceName}" 无效` });
|
||||
continue;
|
||||
}
|
||||
|
||||
// 血统纯度(descent)
|
||||
const descentName = row['血统纯度'] ? String(row['血统纯度']).trim() : '';
|
||||
const descentId = descentName ? getDescentId(descentName) : 0;
|
||||
|
||||
// 栏舍(penId)- 从名称查找ID
|
||||
const penName = row['栏舍'] ? String(row['栏舍']).trim() : '';
|
||||
const penId = penName ? (penNameToId[penName] || null) : null;
|
||||
|
||||
// 所属批次(batchId)- 从名称查找ID
|
||||
const batchName = row['所属批次'] ? String(row['所属批次']).trim() : '';
|
||||
const batchId = batchName ? (batchNameToId[batchName] || null) : null;
|
||||
|
||||
// 已产胎次(parity)
|
||||
const parity = row['已产胎次'] ? parseInt(row['已产胎次']) || 0 : 0;
|
||||
|
||||
// 出生日期(birthday)(必填)
|
||||
const birthdayStr = row['出生日期(格式必须为2023-01-01)'] || row['出生日期'] || '';
|
||||
if (!birthdayStr) {
|
||||
errors.push({ row: rowNum, field: '出生日期', message: '出生日期不能为空' });
|
||||
continue;
|
||||
}
|
||||
const birthday = dateToTimestamp(birthdayStr);
|
||||
if (!birthday) {
|
||||
errors.push({ row: rowNum, field: '出生日期', message: `出生日期格式错误: "${birthdayStr}",格式应为:2023-01-01` });
|
||||
continue;
|
||||
}
|
||||
|
||||
// 现估重(weight)(必填)
|
||||
const currentWeightStr = row['现估重(公斤)'] || row['现估重'] || '';
|
||||
if (!currentWeightStr) {
|
||||
errors.push({ row: rowNum, field: '现估重(公斤)', message: '现估重(公斤)不能为空' });
|
||||
continue;
|
||||
}
|
||||
const currentWeight = parseFloat(currentWeightStr);
|
||||
if (isNaN(currentWeight) || currentWeight < 0) {
|
||||
errors.push({ row: rowNum, field: '现估重(公斤)', message: `现估重(公斤)格式错误: "${currentWeightStr}"` });
|
||||
continue;
|
||||
}
|
||||
|
||||
// 代数(algebra)
|
||||
const algebra = row['代数'] ? parseInt(row['代数']) || 0 : 0;
|
||||
|
||||
// 入场日期(intoTime)
|
||||
const intoTimeStr = row['入场日期(格式必须为2023-01-01)'] || row['入场日期'] || '';
|
||||
const intoTime = intoTimeStr ? dateToTimestamp(intoTimeStr) : null;
|
||||
if (intoTimeStr && !intoTime) {
|
||||
errors.push({ row: rowNum, field: '入场日期', message: `入场日期格式错误: "${intoTimeStr}"` });
|
||||
continue;
|
||||
}
|
||||
|
||||
// 出生体重(birthWeight)
|
||||
const birthWeight = row['出生体重'] ? parseFloat(row['出生体重']) || 0 : 0;
|
||||
|
||||
// 冻精编号(semenNum)
|
||||
const semenNum = row['冻精编号'] ? String(row['冻精编号']).trim() : '';
|
||||
|
||||
// 构建插入数据
|
||||
const cattleData = {
|
||||
orgId: parseInt(defaultOrgId),
|
||||
earNumber: parseInt(earNumber),
|
||||
sex: sexId,
|
||||
strain: strainId || 0,
|
||||
varieties: varietiesId || 0,
|
||||
cate: cateId || 0,
|
||||
birthWeight: birthWeight,
|
||||
birthday: birthday || 0,
|
||||
penId: penId || 0,
|
||||
intoTime: intoTime || 0,
|
||||
parity: parity,
|
||||
source: sourceId || 0,
|
||||
sourceDay: 0,
|
||||
sourceWeight: 0,
|
||||
weight: currentWeight,
|
||||
event: 1,
|
||||
eventTime: Math.floor(Date.now() / 1000),
|
||||
lactationDay: 0,
|
||||
semenNum: semenNum,
|
||||
isWear: 0,
|
||||
imgs: '',
|
||||
isEleAuth: 0,
|
||||
isQuaAuth: 0,
|
||||
isDelete: 0,
|
||||
isOut: 0,
|
||||
createUid: req.user ? req.user.id : 1,
|
||||
createTime: Math.floor(Date.now() / 1000),
|
||||
algebra: algebra,
|
||||
colour: '',
|
||||
infoWeight: 0,
|
||||
descent: descentId || 0,
|
||||
isVaccin: 0,
|
||||
isInsemination: 0,
|
||||
isInsure: 0,
|
||||
isMortgage: 0,
|
||||
updateTime: Math.floor(Date.now() / 1000),
|
||||
breedBullTime: 0,
|
||||
level: 0,
|
||||
sixWeight: 0,
|
||||
eighteenWeight: 0,
|
||||
twelveDayWeight: 0,
|
||||
eighteenDayWeight: 0,
|
||||
xxivDayWeight: 0,
|
||||
semenBreedImgs: '',
|
||||
sellStatus: 100,
|
||||
batchId: batchId || 0
|
||||
};
|
||||
|
||||
// 插入数据库
|
||||
await IotCattle.create(cattleData);
|
||||
successData.push({ row: rowNum, earNumber: earNumber });
|
||||
|
||||
} catch (error) {
|
||||
console.error(`处理第 ${rowNum} 行数据失败:`, error);
|
||||
errors.push({
|
||||
row: rowNum,
|
||||
field: '数据',
|
||||
message: `处理失败: ${error.message}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const importedCount = successData.length;
|
||||
|
||||
console.log(`导入完成: 成功 ${importedCount} 条,失败 ${errors.length} 条`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '导入功能开发中',
|
||||
importedCount,
|
||||
errors
|
||||
message: `导入完成: 成功 ${importedCount} 条,失败 ${errors.length} 条`,
|
||||
importedCount: importedCount,
|
||||
errorCount: errors.length,
|
||||
errors: errors,
|
||||
successData: successData
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
@@ -763,55 +1195,65 @@ class IotCattleController {
|
||||
try {
|
||||
console.log('=== 下载牛只档案导入模板 ===');
|
||||
|
||||
// 创建模板数据 - 按照截图格式
|
||||
// 创建模板数据 - 按照图片格式(16列)
|
||||
const templateData = [
|
||||
{
|
||||
'耳标编号': '2105523006',
|
||||
'性别': '1为公牛2为母牛',
|
||||
'品系': '1:乳肉兼用',
|
||||
'品种': '1:西藏高山牦牛2:宁夏牛',
|
||||
'类别': '1:犊牛,2:育成母牛,3:架子牛,4:青年牛,5:基础母牛,6:育肥牛',
|
||||
'出生体重(kg)': '30',
|
||||
'出生日期': '格式必须为(2023-1-15)',
|
||||
'栏舍ID': '1',
|
||||
'入栏时间': '2023-01-20',
|
||||
'胎次': '0',
|
||||
'来源': '1',
|
||||
'来源天数': '5',
|
||||
'来源体重': '35.5',
|
||||
'当前体重': '450.0',
|
||||
'事件': '正常',
|
||||
'事件时间': '2023-01-20',
|
||||
'泌乳天数': '0',
|
||||
'精液编号': '',
|
||||
'是否佩戴': '1',
|
||||
'批次ID': '1'
|
||||
'耳号': '202308301035',
|
||||
'品类': '肉用型牛',
|
||||
'品种': '蒙古牛',
|
||||
'生理阶段': '犊牛',
|
||||
'性别': '公',
|
||||
'血统纯度': '纯血',
|
||||
'栏舍': '牛舍-20230819',
|
||||
'所属批次': '230508357',
|
||||
'已产胎次': '0',
|
||||
'来源': '购买',
|
||||
'现估重(公斤)': '50',
|
||||
'代数': '0',
|
||||
'出生日期(格式必须为2023-01-01)': '2023-08-30',
|
||||
'入场日期(格式必须为2023-01-01)': '2023-08-30',
|
||||
'出生体重': '50.00',
|
||||
'冻精编号': '51568'
|
||||
},
|
||||
{
|
||||
'耳号': '202308301036',
|
||||
'品类': '肉用型牛',
|
||||
'品种': '蒙古牛',
|
||||
'生理阶段': '犊牛',
|
||||
'性别': '母',
|
||||
'血统纯度': '杂交',
|
||||
'栏舍': '牛舍-20230819',
|
||||
'所属批次': '230508357',
|
||||
'已产胎次': '1',
|
||||
'来源': '购买',
|
||||
'现估重(公斤)': '50',
|
||||
'代数': '1',
|
||||
'出生日期(格式必须为2023-01-01)': '2023-08-30',
|
||||
'入场日期(格式必须为2023-01-01)': '2023-08-30',
|
||||
'出生体重': '45.00',
|
||||
'冻精编号': '51568'
|
||||
}
|
||||
];
|
||||
|
||||
// 使用ExportUtils生成Excel文件
|
||||
// 使用ExportUtils生成Excel文件,按照图片中的列顺序
|
||||
const ExportUtils = require('../utils/exportUtils');
|
||||
const result = ExportUtils.exportToExcel(templateData, [
|
||||
{ title: '耳标编号', dataIndex: '耳标编号', key: 'earNumber' },
|
||||
{ title: '性别', dataIndex: '性别', key: 'sex' },
|
||||
{ title: '品系', dataIndex: '品系', key: 'strain' },
|
||||
{ title: '品种', dataIndex: '品种', key: 'varieties' },
|
||||
{ title: '类别', dataIndex: '类别', key: 'cate' },
|
||||
{ title: '出生体重(kg)', dataIndex: '出生体重(kg)', key: 'birthWeight' },
|
||||
{ title: '出生日期', dataIndex: '出生日期', key: 'birthday' },
|
||||
{ title: '栏舍ID', dataIndex: '栏舍ID', key: 'penId' },
|
||||
{ title: '入栏时间', dataIndex: '入栏时间', key: 'intoTime' },
|
||||
{ title: '胎次', dataIndex: '胎次', key: 'parity' },
|
||||
{ title: '来源', dataIndex: '来源', key: 'source' },
|
||||
{ title: '来源天数', dataIndex: '来源天数', key: 'sourceDay' },
|
||||
{ title: '来源体重', dataIndex: '来源体重', key: 'sourceWeight' },
|
||||
{ title: '当前体重', dataIndex: '当前体重', key: 'weight' },
|
||||
{ title: '事件', dataIndex: '事件', key: 'event' },
|
||||
{ title: '事件时间', dataIndex: '事件时间', key: 'eventTime' },
|
||||
{ title: '泌乳天数', dataIndex: '泌乳天数', key: 'lactationDay' },
|
||||
{ title: '精液编号', dataIndex: '精液编号', key: 'semenNum' },
|
||||
{ title: '是否佩戴', dataIndex: '是否佩戴', key: 'isWear' },
|
||||
{ title: '批次ID', dataIndex: '批次ID', key: 'batchId' }
|
||||
const result = await ExportUtils.exportToExcelWithStyle(templateData, [
|
||||
{ title: '耳号', dataIndex: '耳号', key: 'earNumber', width: 15, required: true },
|
||||
{ title: '品类', dataIndex: '品类', key: 'strain', width: 12, required: true },
|
||||
{ title: '品种', dataIndex: '品种', key: 'varieties', width: 12, required: true },
|
||||
{ title: '生理阶段', dataIndex: '生理阶段', key: 'cate', width: 12, required: true },
|
||||
{ title: '性别', dataIndex: '性别', key: 'sex', width: 8, required: true },
|
||||
{ title: '血统纯度', dataIndex: '血统纯度', key: 'descent', width: 12, required: false },
|
||||
{ title: '栏舍', dataIndex: '栏舍', key: 'penName', width: 15, required: false },
|
||||
{ title: '所属批次', dataIndex: '所属批次', key: 'batchName', width: 15, required: false },
|
||||
{ title: '已产胎次', dataIndex: '已产胎次', key: 'parity', width: 10, required: false },
|
||||
{ title: '来源', dataIndex: '来源', key: 'source', width: 10, required: true },
|
||||
{ title: '现估重(公斤)', dataIndex: '现估重(公斤)', key: 'currentWeight', width: 12, required: true },
|
||||
{ title: '代数', dataIndex: '代数', key: 'algebra', width: 8, required: false },
|
||||
{ title: '出生日期(格式必须为2023-01-01)', dataIndex: '出生日期(格式必须为2023-01-01)', key: 'birthday', width: 25, required: true },
|
||||
{ title: '入场日期(格式必须为2023-01-01)', dataIndex: '入场日期(格式必须为2023-01-01)', key: 'intoTime', width: 25, required: false },
|
||||
{ title: '出生体重', dataIndex: '出生体重', key: 'birthWeight', width: 12, required: false },
|
||||
{ title: '冻精编号', dataIndex: '冻精编号', key: 'semenNum', width: 12, required: false }
|
||||
], '牛只档案导入模板');
|
||||
|
||||
if (result.success) {
|
||||
|
||||
@@ -263,6 +263,10 @@ const generateOperationDesc = (req, responseBody) => {
|
||||
if (url.includes('/reports')) {
|
||||
return `查看${moduleName}报表`;
|
||||
}
|
||||
// 如果是获取单个记录详情,可能是用于编辑
|
||||
if (url.match(/\/\d+$/)) {
|
||||
return `获取${moduleName}详情`;
|
||||
}
|
||||
return `查看${moduleName}数据`;
|
||||
|
||||
default:
|
||||
|
||||
1293
backend/package-lock.json
generated
1293
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -46,6 +46,7 @@
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"ejs": "^3.1.9",
|
||||
"exceljs": "^4.4.0",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"express-validator": "^7.0.1",
|
||||
@@ -65,16 +66,16 @@
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.2",
|
||||
"@types/jest": "^29.5.8",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-plugin-import": "^2.29.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"jest": "^29.7.0",
|
||||
"supertest": "^6.3.3",
|
||||
"nodemon": "^3.0.2",
|
||||
"rimraf": "^5.0.5",
|
||||
"@types/jest": "^29.5.8"
|
||||
"supertest": "^6.3.3"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "node",
|
||||
@@ -88,10 +89,16 @@
|
||||
"!**/seeds/**"
|
||||
],
|
||||
"coverageDirectory": "coverage",
|
||||
"coverageReporters": ["text", "lcov", "html"]
|
||||
"coverageReporters": [
|
||||
"text",
|
||||
"lcov",
|
||||
"html"
|
||||
]
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": ["standard"],
|
||||
"extends": [
|
||||
"standard"
|
||||
],
|
||||
"env": {
|
||||
"node": true,
|
||||
"es2021": true,
|
||||
|
||||
6631
backend/pnpm-lock.yaml
generated
Normal file
6631
backend/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
117
backend/swagger-alerts.js
Normal file
117
backend/swagger-alerts.js
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* 预警管理模块 Swagger 文档
|
||||
* @file swagger-alerts.js
|
||||
* @description 预警管理相关的 Swagger API 文档定义
|
||||
*/
|
||||
|
||||
// 预警管理相关的 API 路径定义
|
||||
const alertsPaths = {
|
||||
'/api/alerts': {
|
||||
get: {
|
||||
summary: '获取所有预警',
|
||||
tags: ['预警管理'],
|
||||
parameters: [
|
||||
{ $ref: '#/components/parameters/PageParam' },
|
||||
{ $ref: '#/components/parameters/LimitParam' },
|
||||
{ $ref: '#/components/parameters/SearchParam' }
|
||||
],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' }
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/alerts/{id}': {
|
||||
get: {
|
||||
summary: '根据ID获取预警',
|
||||
tags: ['预警管理'],
|
||||
parameters: [{ $ref: '#/components/parameters/IdParam' }],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' },
|
||||
404: { $ref: '#/components/responses/NotFound' }
|
||||
}
|
||||
},
|
||||
put: {
|
||||
summary: '更新预警状态',
|
||||
tags: ['预警管理'],
|
||||
parameters: [{ $ref: '#/components/parameters/IdParam' }],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['pending', 'processing', 'resolved', 'ignored'],
|
||||
description: '预警状态'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' },
|
||||
404: { $ref: '#/components/responses/NotFound' }
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/alerts/stats/type': {
|
||||
get: {
|
||||
summary: '获取预警类型统计',
|
||||
tags: ['预警管理'],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' }
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/alerts/stats/level': {
|
||||
get: {
|
||||
summary: '获取预警级别统计',
|
||||
tags: ['预警管理'],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' }
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/alerts/stats/status': {
|
||||
get: {
|
||||
summary: '获取预警状态统计',
|
||||
tags: ['预警管理'],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 预警管理相关的数据模型定义
|
||||
const alertSchemas = {
|
||||
Alert: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'integer', description: '预警ID' },
|
||||
type: { type: 'string', description: '预警类型' },
|
||||
level: { type: 'string', enum: ['high', 'medium', 'low'], description: '预警级别' },
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['pending', 'processing', 'resolved', 'ignored'],
|
||||
description: '预警状态'
|
||||
},
|
||||
title: { type: 'string', description: '预警标题' },
|
||||
message: { type: 'string', description: '预警消息' },
|
||||
deviceId: { type: 'integer', description: '设备ID' },
|
||||
animalId: { type: 'integer', description: '动物ID' },
|
||||
farmId: { type: 'integer', description: '养殖场ID' },
|
||||
createdAt: { type: 'string', format: 'date-time', description: '创建时间' },
|
||||
updatedAt: { type: 'string', format: 'date-time', description: '更新时间' }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
alertsPaths,
|
||||
alertSchemas
|
||||
};
|
||||
|
||||
90
backend/swagger-animals.js
Normal file
90
backend/swagger-animals.js
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 动物管理模块 Swagger 文档
|
||||
* @file swagger-animals.js
|
||||
* @description 动物管理相关的 Swagger API 文档定义
|
||||
*/
|
||||
|
||||
// 动物管理相关的 API 路径定义
|
||||
const animalsPaths = {
|
||||
'/api/animals': {
|
||||
get: {
|
||||
summary: '获取所有动物',
|
||||
tags: ['动物管理'],
|
||||
parameters: [
|
||||
{ $ref: '#/components/parameters/PageParam' },
|
||||
{ $ref: '#/components/parameters/LimitParam' },
|
||||
{ $ref: '#/components/parameters/SearchParam' }
|
||||
],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' }
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/animals/public': {
|
||||
get: {
|
||||
summary: '获取所有动物(公开接口)',
|
||||
tags: ['动物管理'],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' }
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/animals/binding-info/{collarNumber}': {
|
||||
get: {
|
||||
summary: '获取动物绑定信息',
|
||||
tags: ['动物管理'],
|
||||
parameters: [
|
||||
{
|
||||
name: 'collarNumber',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
description: '项圈编号'
|
||||
}
|
||||
],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' },
|
||||
404: { $ref: '#/components/responses/NotFound' }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 动物管理相关的数据模型定义
|
||||
const animalsSchemas = {
|
||||
Animal: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'integer', description: '动物ID' },
|
||||
earNumber: { type: 'string', description: '耳标号' },
|
||||
collarNumber: { type: 'string', description: '项圈编号' },
|
||||
name: { type: 'string', description: '动物名称' },
|
||||
breed: { type: 'string', description: '品种' },
|
||||
sex: { type: 'string', enum: ['公', '母'], description: '性别' },
|
||||
birthDate: { type: 'string', format: 'date', description: '出生日期' },
|
||||
farmId: { type: 'integer', description: '养殖场ID' },
|
||||
penId: { type: 'integer', description: '圈舍ID' },
|
||||
status: { type: 'string', description: '状态' },
|
||||
createdAt: { type: 'string', format: 'date-time', description: '创建时间' },
|
||||
updatedAt: { type: 'string', format: 'date-time', description: '更新时间' }
|
||||
}
|
||||
},
|
||||
AnimalBindingInfo: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
basicInfo: { type: 'object', description: '基础信息' },
|
||||
birthInfo: { type: 'object', description: '出生信息' },
|
||||
pedigreeInfo: { type: 'object', description: '族谱信息' },
|
||||
insuranceInfo: { type: 'object', description: '保险信息' },
|
||||
loanInfo: { type: 'object', description: '贷款信息' },
|
||||
deviceInfo: { type: 'object', description: '设备信息' },
|
||||
farmInfo: { type: 'object', description: '农场信息' }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
animalsPaths,
|
||||
animalsSchemas
|
||||
};
|
||||
|
||||
201
backend/swagger-auth.js
Normal file
201
backend/swagger-auth.js
Normal file
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* 认证模块 Swagger 文档
|
||||
* @file swagger-auth.js
|
||||
* @description 用户认证相关的 Swagger API 文档定义
|
||||
*/
|
||||
|
||||
// 认证相关的 API 路径定义
|
||||
const authPaths = {
|
||||
'/api/auth/login': {
|
||||
post: {
|
||||
summary: '用户登录',
|
||||
tags: ['用户认证'],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { $ref: '#/components/schemas/LoginRequest' }
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' },
|
||||
401: { $ref: '#/components/responses/Unauthorized' }
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/auth/register': {
|
||||
post: {
|
||||
summary: '用户注册',
|
||||
tags: ['用户认证'],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { $ref: '#/components/schemas/RegisterRequest' }
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
201: { $ref: '#/components/responses/Created' },
|
||||
400: { $ref: '#/components/responses/BadRequest' }
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/auth/me': {
|
||||
get: {
|
||||
summary: '获取当前用户信息',
|
||||
tags: ['用户认证'],
|
||||
security: [{ bearerAuth: [] }],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' },
|
||||
401: { $ref: '#/components/responses/Unauthorized' }
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/auth/validate': {
|
||||
get: {
|
||||
summary: '验证Token有效性',
|
||||
tags: ['用户认证'],
|
||||
security: [{ bearerAuth: [] }],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' },
|
||||
401: { $ref: '#/components/responses/Unauthorized' }
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/auth/roles': {
|
||||
get: {
|
||||
summary: '获取所有角色',
|
||||
tags: ['用户认证'],
|
||||
security: [{ bearerAuth: [] }],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' }
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/auth/users/{userId}/roles': {
|
||||
post: {
|
||||
summary: '为用户分配角色',
|
||||
tags: ['用户认证'],
|
||||
security: [{ bearerAuth: [] }],
|
||||
parameters: [
|
||||
{
|
||||
name: 'userId',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: { type: 'integer' },
|
||||
description: '用户ID'
|
||||
}
|
||||
],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
required: ['roleId'],
|
||||
properties: {
|
||||
roleId: { type: 'integer', description: '角色ID' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' },
|
||||
400: { $ref: '#/components/responses/BadRequest' },
|
||||
403: { $ref: '#/components/responses/Forbidden' }
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/auth/users/{userId}/roles/{roleId}': {
|
||||
delete: {
|
||||
summary: '移除用户的角色',
|
||||
tags: ['用户认证'],
|
||||
security: [{ bearerAuth: [] }],
|
||||
parameters: [
|
||||
{
|
||||
name: 'userId',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: { type: 'integer' },
|
||||
description: '用户ID'
|
||||
},
|
||||
{
|
||||
name: 'roleId',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: { type: 'integer' },
|
||||
description: '角色ID'
|
||||
}
|
||||
],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' },
|
||||
404: { $ref: '#/components/responses/NotFound' }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 认证相关的数据模型定义
|
||||
const authSchemas = {
|
||||
LoginRequest: {
|
||||
type: 'object',
|
||||
required: ['username', 'password'],
|
||||
properties: {
|
||||
username: { type: 'string', description: '用户名或邮箱' },
|
||||
password: { type: 'string', format: 'password', description: '密码' }
|
||||
}
|
||||
},
|
||||
LoginResponse: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean' },
|
||||
message: { type: 'string' },
|
||||
token: { type: 'string', description: 'JWT令牌' },
|
||||
user: { $ref: '#/components/schemas/User' },
|
||||
permissions: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: '用户权限列表'
|
||||
},
|
||||
accessibleMenus: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: '可访问的菜单列表'
|
||||
}
|
||||
}
|
||||
},
|
||||
RegisterRequest: {
|
||||
type: 'object',
|
||||
required: ['username', 'email', 'password'],
|
||||
properties: {
|
||||
username: { type: 'string', description: '用户名' },
|
||||
email: { type: 'string', format: 'email', description: '邮箱地址' },
|
||||
password: { type: 'string', format: 'password', description: '密码' }
|
||||
}
|
||||
},
|
||||
RegisterResponse: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean' },
|
||||
message: { type: 'string' },
|
||||
user: { $ref: '#/components/schemas/User' }
|
||||
}
|
||||
},
|
||||
Role: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'integer', description: '角色ID' },
|
||||
name: { type: 'string', description: '角色名称' },
|
||||
description: { type: 'string', description: '角色描述' }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
authPaths,
|
||||
authSchemas
|
||||
};
|
||||
|
||||
131
backend/swagger-devices.js
Normal file
131
backend/swagger-devices.js
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* 设备管理模块 Swagger 文档
|
||||
* @file swagger-devices.js
|
||||
* @description 设备管理相关的 Swagger API 文档定义
|
||||
*/
|
||||
|
||||
// 设备管理相关的 API 路径定义
|
||||
const devicesPaths = {
|
||||
'/api/devices': {
|
||||
get: {
|
||||
summary: '获取所有设备',
|
||||
tags: ['设备管理'],
|
||||
parameters: [
|
||||
{ $ref: '#/components/parameters/PageParam' },
|
||||
{ $ref: '#/components/parameters/LimitParam' },
|
||||
{ $ref: '#/components/parameters/SearchParam' }
|
||||
],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' }
|
||||
}
|
||||
},
|
||||
post: {
|
||||
summary: '创建设备',
|
||||
tags: ['设备管理'],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { $ref: '#/components/schemas/DeviceInput' }
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
201: { $ref: '#/components/responses/Created' },
|
||||
400: { $ref: '#/components/responses/BadRequest' }
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/devices/{id}': {
|
||||
get: {
|
||||
summary: '根据ID获取设备',
|
||||
tags: ['设备管理'],
|
||||
parameters: [{ $ref: '#/components/parameters/IdParam' }],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' },
|
||||
404: { $ref: '#/components/responses/NotFound' }
|
||||
}
|
||||
},
|
||||
put: {
|
||||
summary: '更新设备信息',
|
||||
tags: ['设备管理'],
|
||||
parameters: [{ $ref: '#/components/parameters/IdParam' }],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { $ref: '#/components/schemas/DeviceInput' }
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' },
|
||||
404: { $ref: '#/components/responses/NotFound' }
|
||||
}
|
||||
},
|
||||
delete: {
|
||||
summary: '删除设备',
|
||||
tags: ['设备管理'],
|
||||
parameters: [{ $ref: '#/components/parameters/IdParam' }],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' },
|
||||
404: { $ref: '#/components/responses/NotFound' }
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/devices/stats/status': {
|
||||
get: {
|
||||
summary: '获取设备状态统计',
|
||||
tags: ['设备管理'],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' }
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/devices/stats/type': {
|
||||
get: {
|
||||
summary: '获取设备类型统计',
|
||||
tags: ['设备管理'],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 设备管理相关的数据模型定义
|
||||
const devicesSchemas = {
|
||||
Device: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'integer', description: '设备ID' },
|
||||
deviceId: { type: 'string', description: '设备编号' },
|
||||
name: { type: 'string', description: '设备名称' },
|
||||
type: { type: 'string', description: '设备类型' },
|
||||
status: { type: 'string', enum: ['online', 'offline', 'fault'], description: '设备状态' },
|
||||
batteryLevel: { type: 'integer', description: '电池电量' },
|
||||
location: { type: 'string', description: '位置信息' },
|
||||
farmId: { type: 'integer', description: '养殖场ID' },
|
||||
animalId: { type: 'integer', description: '绑定的动物ID' },
|
||||
createdAt: { type: 'string', format: 'date-time', description: '创建时间' },
|
||||
updatedAt: { type: 'string', format: 'date-time', description: '更新时间' }
|
||||
}
|
||||
},
|
||||
DeviceInput: {
|
||||
type: 'object',
|
||||
required: ['deviceId', 'name', 'type'],
|
||||
properties: {
|
||||
deviceId: { type: 'string', description: '设备编号' },
|
||||
name: { type: 'string', description: '设备名称' },
|
||||
type: { type: 'string', description: '设备类型' },
|
||||
farmId: { type: 'integer', description: '养殖场ID' },
|
||||
animalId: { type: 'integer', description: '绑定的动物ID' }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
devicesPaths,
|
||||
devicesSchemas
|
||||
};
|
||||
|
||||
124
backend/swagger-farms.js
Normal file
124
backend/swagger-farms.js
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* 养殖场管理模块 Swagger 文档
|
||||
* @file swagger-farms.js
|
||||
* @description 养殖场管理相关的 Swagger API 文档定义
|
||||
*/
|
||||
|
||||
// 养殖场管理相关的 API 路径定义
|
||||
const farmsPaths = {
|
||||
'/api/farms': {
|
||||
get: {
|
||||
summary: '获取所有养殖场',
|
||||
tags: ['养殖场管理'],
|
||||
parameters: [
|
||||
{ $ref: '#/components/parameters/PageParam' },
|
||||
{ $ref: '#/components/parameters/LimitParam' },
|
||||
{ $ref: '#/components/parameters/SearchParam' }
|
||||
],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' }
|
||||
}
|
||||
},
|
||||
post: {
|
||||
summary: '创建新养殖场',
|
||||
tags: ['养殖场管理'],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { $ref: '#/components/schemas/FarmInput' }
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
201: { $ref: '#/components/responses/Created' },
|
||||
400: { $ref: '#/components/responses/BadRequest' }
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/farms/{id}': {
|
||||
get: {
|
||||
summary: '根据ID获取养殖场',
|
||||
tags: ['养殖场管理'],
|
||||
parameters: [{ $ref: '#/components/parameters/IdParam' }],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' },
|
||||
404: { $ref: '#/components/responses/NotFound' }
|
||||
}
|
||||
},
|
||||
put: {
|
||||
summary: '更新养殖场信息',
|
||||
tags: ['养殖场管理'],
|
||||
parameters: [{ $ref: '#/components/parameters/IdParam' }],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { $ref: '#/components/schemas/FarmInput' }
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' },
|
||||
404: { $ref: '#/components/responses/NotFound' }
|
||||
}
|
||||
},
|
||||
delete: {
|
||||
summary: '删除养殖场',
|
||||
tags: ['养殖场管理'],
|
||||
parameters: [{ $ref: '#/components/parameters/IdParam' }],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' },
|
||||
404: { $ref: '#/components/responses/NotFound' }
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/farms/search': {
|
||||
get: {
|
||||
summary: '搜索养殖场',
|
||||
tags: ['养殖场管理'],
|
||||
parameters: [
|
||||
{ $ref: '#/components/parameters/SearchParam' }
|
||||
],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 养殖场管理相关的数据模型定义
|
||||
const farmsSchemas = {
|
||||
Farm: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'integer', description: '养殖场ID' },
|
||||
name: { type: 'string', description: '养殖场名称' },
|
||||
address: { type: 'string', description: '地址' },
|
||||
contact: { type: 'string', description: '联系人' },
|
||||
phone: { type: 'string', description: '联系电话' },
|
||||
area: { type: 'number', description: '面积(平方米)' },
|
||||
capacity: { type: 'integer', description: '容量(头)' },
|
||||
createdAt: { type: 'string', format: 'date-time', description: '创建时间' },
|
||||
updatedAt: { type: 'string', format: 'date-time', description: '更新时间' }
|
||||
}
|
||||
},
|
||||
FarmInput: {
|
||||
type: 'object',
|
||||
required: ['name'],
|
||||
properties: {
|
||||
name: { type: 'string', description: '养殖场名称' },
|
||||
address: { type: 'string', description: '地址' },
|
||||
contact: { type: 'string', description: '联系人' },
|
||||
phone: { type: 'string', description: '联系电话' },
|
||||
area: { type: 'number', description: '面积(平方米)' },
|
||||
capacity: { type: 'integer', description: '容量(头)' }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
farmsPaths,
|
||||
farmsSchemas
|
||||
};
|
||||
|
||||
147
backend/swagger-reports.js
Normal file
147
backend/swagger-reports.js
Normal file
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* 报表管理模块 Swagger 文档
|
||||
* @file swagger-reports.js
|
||||
* @description 报表管理相关的 Swagger API 文档定义
|
||||
*/
|
||||
|
||||
// 报表管理相关的 API 路径定义
|
||||
const reportsPaths = {
|
||||
'/api/reports/farm': {
|
||||
post: {
|
||||
summary: '生成养殖统计报表',
|
||||
tags: ['报表管理'],
|
||||
security: [{ bearerAuth: [] }],
|
||||
requestBody: {
|
||||
required: false,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
startDate: { type: 'string', format: 'date', description: '开始日期' },
|
||||
endDate: { type: 'string', format: 'date', description: '结束日期' },
|
||||
farmIds: {
|
||||
type: 'array',
|
||||
items: { type: 'integer' },
|
||||
description: '农场ID列表'
|
||||
},
|
||||
format: {
|
||||
type: 'string',
|
||||
enum: ['excel', 'pdf'],
|
||||
description: '报表格式'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' },
|
||||
400: { $ref: '#/components/responses/BadRequest' }
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/reports/animal': {
|
||||
post: {
|
||||
summary: '生成动物统计报表',
|
||||
tags: ['报表管理'],
|
||||
security: [{ bearerAuth: [] }],
|
||||
requestBody: {
|
||||
required: false,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
startDate: { type: 'string', format: 'date', description: '开始日期' },
|
||||
endDate: { type: 'string', format: 'date', description: '结束日期' },
|
||||
farmId: { type: 'integer', description: '农场ID' },
|
||||
format: {
|
||||
type: 'string',
|
||||
enum: ['excel', 'pdf'],
|
||||
description: '报表格式'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' },
|
||||
400: { $ref: '#/components/responses/BadRequest' }
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/reports/device': {
|
||||
post: {
|
||||
summary: '生成设备统计报表',
|
||||
tags: ['报表管理'],
|
||||
security: [{ bearerAuth: [] }],
|
||||
requestBody: {
|
||||
required: false,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
startDate: { type: 'string', format: 'date', description: '开始日期' },
|
||||
endDate: { type: 'string', format: 'date', description: '结束日期' },
|
||||
format: {
|
||||
type: 'string',
|
||||
enum: ['excel', 'pdf'],
|
||||
description: '报表格式'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' },
|
||||
400: { $ref: '#/components/responses/BadRequest' }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 报表管理相关的数据模型定义
|
||||
const reportsSchemas = {
|
||||
ReportRequest: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
startDate: { type: 'string', format: 'date', description: '开始日期' },
|
||||
endDate: { type: 'string', format: 'date', description: '结束日期' },
|
||||
farmIds: {
|
||||
type: 'array',
|
||||
items: { type: 'integer' },
|
||||
description: '农场ID列表'
|
||||
},
|
||||
format: {
|
||||
type: 'string',
|
||||
enum: ['excel', 'pdf'],
|
||||
description: '报表格式'
|
||||
}
|
||||
}
|
||||
},
|
||||
ReportResponse: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean' },
|
||||
message: { type: 'string' },
|
||||
data: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
reportId: { type: 'string', description: '报表ID' },
|
||||
downloadUrl: { type: 'string', description: '下载链接' },
|
||||
fileName: { type: 'string', description: '文件名' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
reportsPaths,
|
||||
reportsSchemas
|
||||
};
|
||||
|
||||
96
backend/swagger-smart-alerts.js
Normal file
96
backend/swagger-smart-alerts.js
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* 智能预警模块 Swagger 文档
|
||||
* @file swagger-smart-alerts.js
|
||||
* @description 智能预警相关的 Swagger API 文档定义
|
||||
*/
|
||||
|
||||
// 智能预警相关的 API 路径定义
|
||||
const smartAlertsPaths = {
|
||||
'/api/smart-alerts': {
|
||||
get: {
|
||||
summary: '获取所有智能预警',
|
||||
tags: ['智能预警'],
|
||||
parameters: [
|
||||
{ $ref: '#/components/parameters/PageParam' },
|
||||
{ $ref: '#/components/parameters/LimitParam' },
|
||||
{ $ref: '#/components/parameters/SearchParam' }
|
||||
],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' }
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/smart-alerts/{id}': {
|
||||
get: {
|
||||
summary: '根据ID获取智能预警',
|
||||
tags: ['智能预警'],
|
||||
parameters: [{ $ref: '#/components/parameters/IdParam' }],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' },
|
||||
404: { $ref: '#/components/responses/NotFound' }
|
||||
}
|
||||
},
|
||||
put: {
|
||||
summary: '更新智能预警状态',
|
||||
tags: ['智能预警'],
|
||||
parameters: [{ $ref: '#/components/parameters/IdParam' }],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['pending', 'processing', 'resolved', 'ignored'],
|
||||
description: '预警状态'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' },
|
||||
404: { $ref: '#/components/responses/NotFound' }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 智能预警相关的数据模型定义
|
||||
const smartAlertsSchemas = {
|
||||
SmartAlert: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'integer', description: '智能预警ID' },
|
||||
alertType: {
|
||||
type: 'string',
|
||||
enum: ['battery', 'offline', 'temperature', 'movement', 'wear'],
|
||||
description: '预警类型'
|
||||
},
|
||||
alertLevel: {
|
||||
type: 'string',
|
||||
enum: ['high', 'medium', 'low'],
|
||||
description: '预警级别'
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['pending', 'processing', 'resolved', 'ignored'],
|
||||
description: '预警状态'
|
||||
},
|
||||
deviceId: { type: 'string', description: '设备编号' },
|
||||
animalId: { type: 'integer', description: '动物ID' },
|
||||
message: { type: 'string', description: '预警消息' },
|
||||
createdAt: { type: 'string', format: 'date-time', description: '创建时间' },
|
||||
updatedAt: { type: 'string', format: 'date-time', description: '更新时间' }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
smartAlertsPaths,
|
||||
smartAlertsSchemas
|
||||
};
|
||||
|
||||
98
backend/swagger-stats.js
Normal file
98
backend/swagger-stats.js
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* 数据统计模块 Swagger 文档
|
||||
* @file swagger-stats.js
|
||||
* @description 数据统计相关的 Swagger API 文档定义
|
||||
*/
|
||||
|
||||
// 数据统计相关的 API 路径定义
|
||||
const statsPaths = {
|
||||
'/api/stats/dashboard': {
|
||||
get: {
|
||||
summary: '获取仪表盘统计数据',
|
||||
tags: ['数据统计'],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' }
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/stats/monitoring': {
|
||||
get: {
|
||||
summary: '获取监控数据',
|
||||
tags: ['数据统计'],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' }
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/stats/monthly-trends': {
|
||||
get: {
|
||||
summary: '获取月度数据趋势',
|
||||
tags: ['数据统计'],
|
||||
parameters: [
|
||||
{
|
||||
name: 'startDate',
|
||||
in: 'query',
|
||||
schema: { type: 'string', format: 'date' },
|
||||
description: '开始日期'
|
||||
},
|
||||
{
|
||||
name: 'endDate',
|
||||
in: 'query',
|
||||
schema: { type: 'string', format: 'date' },
|
||||
description: '结束日期'
|
||||
}
|
||||
],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' }
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/stats/farm-count': {
|
||||
get: {
|
||||
summary: '获取养殖场总数统计',
|
||||
tags: ['数据统计'],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' }
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/stats/animal-count': {
|
||||
get: {
|
||||
summary: '获取动物总数统计',
|
||||
tags: ['数据统计'],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 数据统计相关的数据模型定义
|
||||
const statsSchemas = {
|
||||
DashboardStats: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
totalFarms: { type: 'integer', description: '养殖场总数' },
|
||||
totalAnimals: { type: 'integer', description: '动物总数' },
|
||||
totalDevices: { type: 'integer', description: '设备总数' },
|
||||
activeAlerts: { type: 'integer', description: '活跃预警数' },
|
||||
onlineDevices: { type: 'integer', description: '在线设备数' },
|
||||
offlineDevices: { type: 'integer', description: '离线设备数' }
|
||||
}
|
||||
},
|
||||
MonthlyTrend: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
month: { type: 'string', description: '月份' },
|
||||
farms: { type: 'integer', description: '养殖场数量' },
|
||||
animals: { type: 'integer', description: '动物数量' },
|
||||
devices: { type: 'integer', description: '设备数量' }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
statsPaths,
|
||||
statsSchemas
|
||||
};
|
||||
|
||||
153
backend/swagger-system.js
Normal file
153
backend/swagger-system.js
Normal file
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* 系统管理模块 Swagger 文档
|
||||
* @file swagger-system.js
|
||||
* @description 系统管理相关的 Swagger API 文档定义
|
||||
*/
|
||||
|
||||
// 系统管理相关的 API 路径定义
|
||||
const systemPaths = {
|
||||
'/api/system/configs': {
|
||||
get: {
|
||||
summary: '获取系统配置列表',
|
||||
tags: ['系统管理'],
|
||||
security: [{ bearerAuth: [] }],
|
||||
parameters: [
|
||||
{
|
||||
name: 'category',
|
||||
in: 'query',
|
||||
schema: { type: 'string' },
|
||||
description: '配置分类'
|
||||
},
|
||||
{
|
||||
name: 'is_public',
|
||||
in: 'query',
|
||||
schema: { type: 'boolean' },
|
||||
description: '是否公开配置'
|
||||
}
|
||||
],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' },
|
||||
401: { $ref: '#/components/responses/Unauthorized' },
|
||||
403: { $ref: '#/components/responses/Forbidden' }
|
||||
}
|
||||
},
|
||||
post: {
|
||||
summary: '创建系统配置',
|
||||
tags: ['系统管理'],
|
||||
security: [{ bearerAuth: [] }],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { $ref: '#/components/schemas/SystemConfigInput' }
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
201: { $ref: '#/components/responses/Created' },
|
||||
400: { $ref: '#/components/responses/BadRequest' },
|
||||
403: { $ref: '#/components/responses/Forbidden' }
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/system/configs/{id}': {
|
||||
get: {
|
||||
summary: '根据ID获取系统配置',
|
||||
tags: ['系统管理'],
|
||||
security: [{ bearerAuth: [] }],
|
||||
parameters: [{ $ref: '#/components/parameters/IdParam' }],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' },
|
||||
404: { $ref: '#/components/responses/NotFound' }
|
||||
}
|
||||
},
|
||||
put: {
|
||||
summary: '更新系统配置',
|
||||
tags: ['系统管理'],
|
||||
security: [{ bearerAuth: [] }],
|
||||
parameters: [{ $ref: '#/components/parameters/IdParam' }],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { $ref: '#/components/schemas/SystemConfigInput' }
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' },
|
||||
404: { $ref: '#/components/responses/NotFound' }
|
||||
}
|
||||
},
|
||||
delete: {
|
||||
summary: '删除系统配置',
|
||||
tags: ['系统管理'],
|
||||
security: [{ bearerAuth: [] }],
|
||||
parameters: [{ $ref: '#/components/parameters/IdParam' }],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' },
|
||||
404: { $ref: '#/components/responses/NotFound' }
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/system/menus': {
|
||||
get: {
|
||||
summary: '获取菜单列表',
|
||||
tags: ['系统管理'],
|
||||
security: [{ bearerAuth: [] }],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 系统管理相关的数据模型定义
|
||||
const systemSchemas = {
|
||||
SystemConfig: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'integer', description: '配置ID' },
|
||||
key: { type: 'string', description: '配置键' },
|
||||
value: { type: 'string', description: '配置值' },
|
||||
category: { type: 'string', description: '配置分类' },
|
||||
description: { type: 'string', description: '配置描述' },
|
||||
isPublic: { type: 'boolean', description: '是否公开' },
|
||||
createdAt: { type: 'string', format: 'date-time', description: '创建时间' },
|
||||
updatedAt: { type: 'string', format: 'date-time', description: '更新时间' }
|
||||
}
|
||||
},
|
||||
SystemConfigInput: {
|
||||
type: 'object',
|
||||
required: ['key', 'value'],
|
||||
properties: {
|
||||
key: { type: 'string', description: '配置键' },
|
||||
value: { type: 'string', description: '配置值' },
|
||||
category: { type: 'string', description: '配置分类' },
|
||||
description: { type: 'string', description: '配置描述' },
|
||||
isPublic: { type: 'boolean', description: '是否公开' }
|
||||
}
|
||||
},
|
||||
Menu: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'integer', description: '菜单ID' },
|
||||
name: { type: 'string', description: '菜单名称' },
|
||||
path: { type: 'string', description: '菜单路径' },
|
||||
icon: { type: 'string', description: '菜单图标' },
|
||||
parentId: { type: 'integer', description: '父菜单ID' },
|
||||
order: { type: 'integer', description: '排序' },
|
||||
permissions: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: '权限列表'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
systemPaths,
|
||||
systemSchemas
|
||||
};
|
||||
|
||||
138
backend/swagger-users.js
Normal file
138
backend/swagger-users.js
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* 用户管理模块 Swagger 文档
|
||||
* @file swagger-users.js
|
||||
* @description 用户管理相关的 Swagger API 文档定义
|
||||
*/
|
||||
|
||||
// 用户管理相关的 API 路径定义
|
||||
const usersPaths = {
|
||||
'/api/users': {
|
||||
get: {
|
||||
summary: '获取所有用户',
|
||||
tags: ['用户管理'],
|
||||
security: [{ bearerAuth: [] }],
|
||||
parameters: [
|
||||
{ $ref: '#/components/parameters/PageParam' },
|
||||
{ $ref: '#/components/parameters/LimitParam' },
|
||||
{ $ref: '#/components/parameters/SearchParam' }
|
||||
],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' },
|
||||
401: { $ref: '#/components/responses/Unauthorized' }
|
||||
}
|
||||
},
|
||||
post: {
|
||||
summary: '创建新用户',
|
||||
tags: ['用户管理'],
|
||||
security: [{ bearerAuth: [] }],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { $ref: '#/components/schemas/UserInput' }
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
201: { $ref: '#/components/responses/Created' },
|
||||
400: { $ref: '#/components/responses/BadRequest' }
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/users/{id}': {
|
||||
get: {
|
||||
summary: '根据ID获取用户',
|
||||
tags: ['用户管理'],
|
||||
security: [{ bearerAuth: [] }],
|
||||
parameters: [{ $ref: '#/components/parameters/IdParam' }],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' },
|
||||
404: { $ref: '#/components/responses/NotFound' }
|
||||
}
|
||||
},
|
||||
put: {
|
||||
summary: '更新用户信息',
|
||||
tags: ['用户管理'],
|
||||
security: [{ bearerAuth: [] }],
|
||||
parameters: [{ $ref: '#/components/parameters/IdParam' }],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { $ref: '#/components/schemas/UserInput' }
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' },
|
||||
404: { $ref: '#/components/responses/NotFound' }
|
||||
}
|
||||
},
|
||||
delete: {
|
||||
summary: '删除用户',
|
||||
tags: ['用户管理'],
|
||||
security: [{ bearerAuth: [] }],
|
||||
parameters: [{ $ref: '#/components/parameters/IdParam' }],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' },
|
||||
404: { $ref: '#/components/responses/NotFound' }
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/users/search': {
|
||||
get: {
|
||||
summary: '搜索用户',
|
||||
tags: ['用户管理'],
|
||||
security: [{ bearerAuth: [] }],
|
||||
parameters: [
|
||||
{ $ref: '#/components/parameters/SearchParam' }
|
||||
],
|
||||
responses: {
|
||||
200: { $ref: '#/components/responses/Success' }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 用户管理相关的数据模型定义
|
||||
const usersSchemas = {
|
||||
User: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'integer', description: '用户ID' },
|
||||
username: { type: 'string', description: '用户名' },
|
||||
email: { type: 'string', format: 'email', description: '邮箱地址' },
|
||||
phone: { type: 'string', description: '手机号码' },
|
||||
avatar: { type: 'string', description: '头像URL' },
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['active', 'inactive', 'suspended'],
|
||||
description: '用户状态'
|
||||
},
|
||||
createdAt: { type: 'string', format: 'date-time', description: '创建时间' },
|
||||
updatedAt: { type: 'string', format: 'date-time', description: '更新时间' }
|
||||
}
|
||||
},
|
||||
UserInput: {
|
||||
type: 'object',
|
||||
required: ['username', 'email', 'password'],
|
||||
properties: {
|
||||
username: { type: 'string', description: '用户名' },
|
||||
email: { type: 'string', format: 'email', description: '邮箱地址' },
|
||||
password: { type: 'string', format: 'password', description: '密码' },
|
||||
phone: { type: 'string', description: '手机号码' },
|
||||
avatar: { type: 'string', description: '头像URL' },
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['active', 'inactive', 'suspended'],
|
||||
description: '用户状态'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
usersPaths,
|
||||
usersSchemas
|
||||
};
|
||||
|
||||
BIN
backend/uploads/file-1762764442942-940302041.xlsx
Normal file
BIN
backend/uploads/file-1762764442942-940302041.xlsx
Normal file
Binary file not shown.
BIN
backend/uploads/file-1762764451876-839276653.xlsx
Normal file
BIN
backend/uploads/file-1762764451876-839276653.xlsx
Normal file
Binary file not shown.
BIN
backend/uploads/file-1762764643092-57114032.xlsx
Normal file
BIN
backend/uploads/file-1762764643092-57114032.xlsx
Normal file
Binary file not shown.
BIN
backend/uploads/file-1762765019902-267449730.xlsx
Normal file
BIN
backend/uploads/file-1762765019902-267449730.xlsx
Normal file
Binary file not shown.
@@ -1,4 +1,5 @@
|
||||
const XLSX = require('xlsx');
|
||||
const ExcelJS = require('exceljs');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
@@ -73,6 +74,108 @@ class ExportUtils {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出数据到Excel文件(带样式)
|
||||
* @param {Array} data - 要导出的数据数组
|
||||
* @param {Array} columns - 列定义数组,包含 required 属性用于标识必填字段
|
||||
* @param {String} filename - 文件名(不含扩展名)
|
||||
* @returns {Object} 导出结果
|
||||
*/
|
||||
static async exportToExcelWithStyle(data, columns, filename = 'export') {
|
||||
try {
|
||||
// 使用ExcelJS创建工作簿
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const worksheet = workbook.addWorksheet('Sheet1');
|
||||
|
||||
// 设置表头
|
||||
const headerRow = worksheet.addRow(columns.map(col => col.title));
|
||||
|
||||
// 设置表头样式(必填字段为红色,可选字段为黑色)
|
||||
headerRow.eachCell((cell, colNumber) => {
|
||||
const colIndex = colNumber - 1;
|
||||
const col = columns[colIndex];
|
||||
|
||||
// 设置表头样式
|
||||
cell.font = { bold: true, size: 11 };
|
||||
cell.alignment = { vertical: 'middle', horizontal: 'center' };
|
||||
cell.fill = {
|
||||
type: 'pattern',
|
||||
pattern: 'solid',
|
||||
fgColor: { argb: 'FFFFFFFF' } // 白色背景
|
||||
};
|
||||
|
||||
// 必填字段设置为红色字体
|
||||
if (col.required) {
|
||||
cell.font = { ...cell.font, color: { argb: 'FFFF0000' } }; // 红色
|
||||
} else {
|
||||
cell.font = { ...cell.font, color: { argb: 'FF000000' } }; // 黑色
|
||||
}
|
||||
|
||||
// 设置边框
|
||||
cell.border = {
|
||||
top: { style: 'thin' },
|
||||
left: { style: 'thin' },
|
||||
bottom: { style: 'thin' },
|
||||
right: { style: 'thin' }
|
||||
};
|
||||
});
|
||||
|
||||
// 添加数据行
|
||||
data.forEach(row => {
|
||||
const rowData = columns.map(col => {
|
||||
const value = row[col.dataIndex] || row[col.key] || '';
|
||||
return value;
|
||||
});
|
||||
const dataRow = worksheet.addRow(rowData);
|
||||
|
||||
// 设置数据行样式
|
||||
dataRow.eachCell((cell) => {
|
||||
cell.alignment = { vertical: 'middle', horizontal: 'left' };
|
||||
cell.border = {
|
||||
top: { style: 'thin' },
|
||||
left: { style: 'thin' },
|
||||
bottom: { style: 'thin' },
|
||||
right: { style: 'thin' }
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// 设置列宽
|
||||
columns.forEach((col, index) => {
|
||||
worksheet.getColumn(index + 1).width = col.width || 15;
|
||||
});
|
||||
|
||||
// 生成文件路径
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const fileName = `${filename}_${timestamp}.xlsx`;
|
||||
const filePath = path.join(__dirname, '..', 'uploads', fileName);
|
||||
|
||||
// 确保uploads目录存在
|
||||
const uploadsDir = path.dirname(filePath);
|
||||
if (!fs.existsSync(uploadsDir)) {
|
||||
fs.mkdirSync(uploadsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 写入文件
|
||||
await workbook.xlsx.writeFile(filePath);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
filePath: filePath,
|
||||
fileName: fileName,
|
||||
message: '导出成功'
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('导出Excel失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: error.message,
|
||||
error: error
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ExportUtils;
|
||||
|
||||
329
insurance_backend/node_manager.sh
Normal file
329
insurance_backend/node_manager.sh
Normal file
@@ -0,0 +1,329 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Node.js 服务管理脚本 - 保险端口后端服务
|
||||
|
||||
# 使用方法: ./node_manager.sh [start|stop|restart|status|logs]
|
||||
|
||||
# 配置区域
|
||||
APP_NAME="insurance-backend"
|
||||
ENTRY_FILE="src/app.js"
|
||||
APP_PORT="3000"
|
||||
LOG_DIR="logs"
|
||||
LOG_FILE="${LOG_DIR}/${APP_NAME}.log"
|
||||
PID_FILE="pid.${APP_NAME}"
|
||||
NODE_ENV="production"
|
||||
|
||||
# 颜色输出
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 创建日志目录
|
||||
if [ ! -d "$LOG_DIR" ]; then
|
||||
mkdir -p "$LOG_DIR"
|
||||
echo "已创建日志目录: $LOG_DIR"
|
||||
fi
|
||||
|
||||
# 检查 Node.js 是否安装
|
||||
if ! command -v node &> /dev/null; then
|
||||
echo -e "${RED}错误: Node.js 未安装或不在 PATH 中${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查入口文件是否存在
|
||||
if [ ! -f "$ENTRY_FILE" ]; then
|
||||
echo -e "${RED}错误: 入口文件 $ENTRY_FILE 不存在${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 显示 Node.js 版本信息
|
||||
NODE_VERSION=$(node --version)
|
||||
echo -e "${GREEN}Node.js 版本: $NODE_VERSION${NC}"
|
||||
|
||||
# 停止服务函数
|
||||
function stopApp() {
|
||||
echo -e "${YELLOW}正在停止服务: $APP_NAME${NC}"
|
||||
|
||||
# 从 PID 文件读取进程 ID
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
PID=$(cat "$PID_FILE")
|
||||
if ps -p $PID > /dev/null 2>&1; then
|
||||
echo "找到进程 PID: $PID,正在停止..."
|
||||
kill -TERM $PID
|
||||
|
||||
# 等待进程优雅退出
|
||||
for i in {1..10}; do
|
||||
if ! ps -p $PID > /dev/null 2>&1; then
|
||||
echo -e "${GREEN}服务已优雅停止${NC}"
|
||||
rm -f "$PID_FILE"
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# 如果优雅停止失败,强制杀死
|
||||
echo -e "${YELLOW}优雅停止失败,强制终止进程${NC}"
|
||||
kill -9 $PID
|
||||
rm -f "$PID_FILE"
|
||||
else
|
||||
echo "PID 文件存在但进程不存在,清理 PID 文件"
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
else
|
||||
# 如果没有 PID 文件,尝试通过端口查找
|
||||
PID=$(lsof -ti:$APP_PORT 2>/dev/null)
|
||||
if [ -n "$PID" ]; then
|
||||
echo "通过端口 $APP_PORT 找到 PID: $PID,正在停止..."
|
||||
kill -TERM $PID
|
||||
sleep 2
|
||||
if ps -p $PID > /dev/null 2>&1; then
|
||||
kill -9 $PID
|
||||
fi
|
||||
echo -e "${GREEN}服务已停止${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}未找到运行中的服务: $APP_NAME${NC}"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# 启动服务函数
|
||||
function startApp() {
|
||||
echo -e "${YELLOW}正在启动服务: $APP_NAME${NC}"
|
||||
|
||||
# 检查服务是否已经运行
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
PID=$(cat "$PID_FILE")
|
||||
if ps -p $PID > /dev/null 2>&1; then
|
||||
echo -e "${YELLOW}服务已在运行中 (PID: $PID)${NC}"
|
||||
return 0
|
||||
else
|
||||
echo "PID 文件存在但进程不存在,清理后重新启动"
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 检查端口是否被占用
|
||||
if command -v lsof &> /dev/null; then
|
||||
PORT_CHECK=$(lsof -ti:$APP_PORT 2>/dev/null)
|
||||
if [ -n "$PORT_CHECK" ]; then
|
||||
echo -e "${RED}错误: 端口 $APP_PORT 已被占用 (PID: $PORT_CHECK)${NC}"
|
||||
echo "请先停止占用端口的进程或使用 restart 命令"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 检查 .env 文件是否存在
|
||||
if [ ! -f ".env" ]; then
|
||||
echo -e "${YELLOW}警告: .env 文件不存在,将使用默认配置${NC}"
|
||||
echo "建议从 env.example 复制并配置 .env 文件"
|
||||
fi
|
||||
|
||||
# 检查 node_modules 是否存在
|
||||
if [ ! -d "node_modules" ]; then
|
||||
echo -e "${RED}错误: node_modules 目录不存在${NC}"
|
||||
echo "请先运行: npm install"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 启动 Node.js 应用
|
||||
echo "启动命令: NODE_ENV=$NODE_ENV nohup node $ENTRY_FILE > $LOG_FILE 2>&1 &"
|
||||
NODE_ENV=$NODE_ENV nohup node $ENTRY_FILE > $LOG_FILE 2>&1 &
|
||||
|
||||
# 获取新进程的 PID
|
||||
PID=$!
|
||||
echo $PID > "$PID_FILE"
|
||||
|
||||
# 等待应用启动
|
||||
echo "等待服务启动..."
|
||||
sleep 3
|
||||
|
||||
# 验证启动是否成功
|
||||
if ps -p $PID > /dev/null 2>&1; then
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
echo -e "${GREEN}服务启动成功!${NC}"
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
echo "应用名称: $APP_NAME"
|
||||
echo "进程 ID: $PID"
|
||||
echo "监听端口: $APP_PORT"
|
||||
echo "日志文件: $LOG_FILE"
|
||||
echo "PID 文件: $PID_FILE"
|
||||
echo "环境变量: NODE_ENV=$NODE_ENV"
|
||||
echo -e "${GREEN}API 文档: http://localhost:$APP_PORT/api-docs${NC}"
|
||||
echo ""
|
||||
|
||||
# 等待端口监听
|
||||
echo "检查端口监听状态..."
|
||||
sleep 2
|
||||
if command -v lsof &> /dev/null; then
|
||||
if lsof -ti:$APP_PORT > /dev/null 2>&1; then
|
||||
echo -e "${GREEN}✓ 端口 $APP_PORT 正在监听${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠ 端口 $APP_PORT 尚未监听,请检查日志${NC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 显示最近的日志
|
||||
echo ""
|
||||
echo "最近的启动日志:"
|
||||
echo "------------------------"
|
||||
tail -20 "$LOG_FILE"
|
||||
else
|
||||
echo -e "${RED}========================================${NC}"
|
||||
echo -e "${RED}服务启动失败!${NC}"
|
||||
echo -e "${RED}========================================${NC}"
|
||||
echo "请检查日志文件: $LOG_FILE"
|
||||
echo ""
|
||||
echo "最近的错误日志:"
|
||||
echo "------------------------"
|
||||
tail -30 "$LOG_FILE"
|
||||
rm -f "$PID_FILE"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 重启服务函数
|
||||
function restartApp() {
|
||||
echo -e "${YELLOW}正在重启服务: $APP_NAME${NC}"
|
||||
stopApp
|
||||
sleep 3
|
||||
startApp
|
||||
}
|
||||
|
||||
# 状态检查函数
|
||||
function statusApp() {
|
||||
echo -e "${YELLOW}检查服务状态: $APP_NAME${NC}"
|
||||
echo "========================================"
|
||||
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
PID=$(cat "$PID_FILE")
|
||||
if ps -p $PID > /dev/null 2>&1; then
|
||||
echo -e "${GREEN}✓ 服务正在运行${NC}"
|
||||
echo "----------------------------------------"
|
||||
echo "进程 ID: $PID"
|
||||
echo "应用名称: $APP_NAME"
|
||||
echo "监听端口: $APP_PORT"
|
||||
|
||||
# 启动时间
|
||||
if command -v ps &> /dev/null; then
|
||||
START_TIME=$(ps -o lstart= -p $PID 2>/dev/null)
|
||||
if [ -n "$START_TIME" ]; then
|
||||
echo "启动时间: $START_TIME"
|
||||
fi
|
||||
|
||||
# 内存使用
|
||||
MEM_USAGE=$(ps -o rss= -p $PID 2>/dev/null | awk '{printf "%.2f MB", $1/1024}')
|
||||
if [ -n "$MEM_USAGE" ]; then
|
||||
echo "内存使用: $MEM_USAGE"
|
||||
fi
|
||||
|
||||
# CPU使用率
|
||||
CPU_USAGE=$(ps -o %cpu= -p $PID 2>/dev/null)
|
||||
if [ -n "$CPU_USAGE" ]; then
|
||||
echo "CPU 使用: ${CPU_USAGE}%"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 检查端口监听状态
|
||||
if command -v lsof &> /dev/null; then
|
||||
PORT_INFO=$(lsof -ti:$APP_PORT 2>/dev/null)
|
||||
if [ -n "$PORT_INFO" ]; then
|
||||
echo -e "端口状态: ${GREEN}监听中 ($APP_PORT)${NC}"
|
||||
else
|
||||
echo -e "端口状态: ${RED}未监听${NC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 日志文件信息
|
||||
if [ -f "$LOG_FILE" ]; then
|
||||
LOG_SIZE=$(du -h "$LOG_FILE" | cut -f1)
|
||||
echo "日志大小: $LOG_SIZE"
|
||||
echo "日志文件: $LOG_FILE"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "最近的日志 (最后10行):"
|
||||
echo "----------------------------------------"
|
||||
tail -10 "$LOG_FILE" 2>/dev/null || echo "无法读取日志文件"
|
||||
|
||||
else
|
||||
echo -e "${RED}✗ 服务未运行 (PID 文件存在但进程不存在)${NC}"
|
||||
echo "建议清理 PID 文件: rm -f $PID_FILE"
|
||||
fi
|
||||
else
|
||||
# 通过端口检查
|
||||
if command -v lsof &> /dev/null; then
|
||||
PID=$(lsof -ti:$APP_PORT 2>/dev/null)
|
||||
if [ -n "$PID" ]; then
|
||||
echo -e "${YELLOW}⚠ 服务正在运行但未通过脚本管理${NC}"
|
||||
echo "进程 ID: $PID"
|
||||
echo "监听端口: $APP_PORT"
|
||||
echo "建议使用: ./node_manager.sh stop 停止服务后重新启动"
|
||||
else
|
||||
echo -e "${RED}✗ 服务未运行${NC}"
|
||||
echo "使用以下命令启动: ./node_manager.sh start"
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}✗ 服务未运行 (PID 文件不存在)${NC}"
|
||||
echo "使用以下命令启动: ./node_manager.sh start"
|
||||
fi
|
||||
fi
|
||||
echo "========================================"
|
||||
}
|
||||
|
||||
# 查看日志函数
|
||||
function viewLogs() {
|
||||
if [ -f "$LOG_FILE" ]; then
|
||||
echo -e "${YELLOW}实时查看日志 (Ctrl+C 退出):${NC}"
|
||||
tail -f "$LOG_FILE"
|
||||
else
|
||||
echo -e "${RED}日志文件不存在: $LOG_FILE${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
# 主逻辑
|
||||
case "$1" in
|
||||
start)
|
||||
startApp
|
||||
;;
|
||||
stop)
|
||||
stopApp
|
||||
;;
|
||||
restart)
|
||||
restartApp
|
||||
;;
|
||||
status)
|
||||
statusApp
|
||||
;;
|
||||
logs)
|
||||
viewLogs
|
||||
;;
|
||||
*)
|
||||
echo "========================================"
|
||||
echo "保险端口后端服务 - 服务管理脚本"
|
||||
echo "========================================"
|
||||
echo "使用方法: $0 {start|stop|restart|status|logs}"
|
||||
echo ""
|
||||
echo "命令说明:"
|
||||
echo " start - 启动服务"
|
||||
echo " stop - 停止服务"
|
||||
echo " restart - 重启服务"
|
||||
echo " status - 查看服务状态"
|
||||
echo " logs - 实时查看日志"
|
||||
echo ""
|
||||
echo "配置信息:"
|
||||
echo " 应用名称: $APP_NAME"
|
||||
echo " 入口文件: $ENTRY_FILE"
|
||||
echo " 监听端口: $APP_PORT"
|
||||
echo " 日志文件: $LOG_FILE"
|
||||
echo ""
|
||||
echo "示例:"
|
||||
echo " $0 start # 启动服务"
|
||||
echo " $0 status # 查看状态"
|
||||
echo " $0 logs # 查看日志"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -27,6 +27,7 @@ app.use(cors({
|
||||
'http://localhost:3002',
|
||||
'http://127.0.0.1:3002',
|
||||
'https://ad.ningmuyun.com',
|
||||
'https://ad.liaoniuyun.com',
|
||||
'https://www.ningmuyun.com',
|
||||
'https://ningmuyun.com'
|
||||
];
|
||||
|
||||
456
openspec/AGENTS.md
Normal file
456
openspec/AGENTS.md
Normal file
@@ -0,0 +1,456 @@
|
||||
# OpenSpec Instructions
|
||||
|
||||
Instructions for AI coding assistants using OpenSpec for spec-driven development.
|
||||
|
||||
## TL;DR Quick Checklist
|
||||
|
||||
- Search existing work: `openspec spec list --long`, `openspec list` (use `rg` only for full-text search)
|
||||
- Decide scope: new capability vs modify existing capability
|
||||
- Pick a unique `change-id`: kebab-case, verb-led (`add-`, `update-`, `remove-`, `refactor-`)
|
||||
- Scaffold: `proposal.md`, `tasks.md`, `design.md` (only if needed), and delta specs per affected capability
|
||||
- Write deltas: use `## ADDED|MODIFIED|REMOVED|RENAMED Requirements`; include at least one `#### Scenario:` per requirement
|
||||
- Validate: `openspec validate [change-id] --strict` and fix issues
|
||||
- Request approval: Do not start implementation until proposal is approved
|
||||
|
||||
## Three-Stage Workflow
|
||||
|
||||
### Stage 1: Creating Changes
|
||||
Create proposal when you need to:
|
||||
- Add features or functionality
|
||||
- Make breaking changes (API, schema)
|
||||
- Change architecture or patterns
|
||||
- Optimize performance (changes behavior)
|
||||
- Update security patterns
|
||||
|
||||
Triggers (examples):
|
||||
- "Help me create a change proposal"
|
||||
- "Help me plan a change"
|
||||
- "Help me create a proposal"
|
||||
- "I want to create a spec proposal"
|
||||
- "I want to create a spec"
|
||||
|
||||
Loose matching guidance:
|
||||
- Contains one of: `proposal`, `change`, `spec`
|
||||
- With one of: `create`, `plan`, `make`, `start`, `help`
|
||||
|
||||
Skip proposal for:
|
||||
- Bug fixes (restore intended behavior)
|
||||
- Typos, formatting, comments
|
||||
- Dependency updates (non-breaking)
|
||||
- Configuration changes
|
||||
- Tests for existing behavior
|
||||
|
||||
**Workflow**
|
||||
1. Review `openspec/project.md`, `openspec list`, and `openspec list --specs` to understand current context.
|
||||
2. Choose a unique verb-led `change-id` and scaffold `proposal.md`, `tasks.md`, optional `design.md`, and spec deltas under `openspec/changes/<id>/`.
|
||||
3. Draft spec deltas using `## ADDED|MODIFIED|REMOVED Requirements` with at least one `#### Scenario:` per requirement.
|
||||
4. Run `openspec validate <id> --strict` and resolve any issues before sharing the proposal.
|
||||
|
||||
### Stage 2: Implementing Changes
|
||||
Track these steps as TODOs and complete them one by one.
|
||||
1. **Read proposal.md** - Understand what's being built
|
||||
2. **Read design.md** (if exists) - Review technical decisions
|
||||
3. **Read tasks.md** - Get implementation checklist
|
||||
4. **Implement tasks sequentially** - Complete in order
|
||||
5. **Confirm completion** - Ensure every item in `tasks.md` is finished before updating statuses
|
||||
6. **Update checklist** - After all work is done, set every task to `- [x]` so the list reflects reality
|
||||
7. **Approval gate** - Do not start implementation until the proposal is reviewed and approved
|
||||
|
||||
### Stage 3: Archiving Changes
|
||||
After deployment, create separate PR to:
|
||||
- Move `changes/[name]/` → `changes/archive/YYYY-MM-DD-[name]/`
|
||||
- Update `specs/` if capabilities changed
|
||||
- Use `openspec archive <change-id> --skip-specs --yes` for tooling-only changes (always pass the change ID explicitly)
|
||||
- Run `openspec validate --strict` to confirm the archived change passes checks
|
||||
|
||||
## Before Any Task
|
||||
|
||||
**Context Checklist:**
|
||||
- [ ] Read relevant specs in `specs/[capability]/spec.md`
|
||||
- [ ] Check pending changes in `changes/` for conflicts
|
||||
- [ ] Read `openspec/project.md` for conventions
|
||||
- [ ] Run `openspec list` to see active changes
|
||||
- [ ] Run `openspec list --specs` to see existing capabilities
|
||||
|
||||
**Before Creating Specs:**
|
||||
- Always check if capability already exists
|
||||
- Prefer modifying existing specs over creating duplicates
|
||||
- Use `openspec show [spec]` to review current state
|
||||
- If request is ambiguous, ask 1–2 clarifying questions before scaffolding
|
||||
|
||||
### Search Guidance
|
||||
- Enumerate specs: `openspec spec list --long` (or `--json` for scripts)
|
||||
- Enumerate changes: `openspec list` (or `openspec change list --json` - deprecated but available)
|
||||
- Show details:
|
||||
- Spec: `openspec show <spec-id> --type spec` (use `--json` for filters)
|
||||
- Change: `openspec show <change-id> --json --deltas-only`
|
||||
- Full-text search (use ripgrep): `rg -n "Requirement:|Scenario:" openspec/specs`
|
||||
|
||||
## Quick Start
|
||||
|
||||
### CLI Commands
|
||||
|
||||
```bash
|
||||
# Essential commands
|
||||
openspec list # List active changes
|
||||
openspec list --specs # List specifications
|
||||
openspec show [item] # Display change or spec
|
||||
openspec validate [item] # Validate changes or specs
|
||||
openspec archive <change-id> [--yes|-y] # Archive after deployment (add --yes for non-interactive runs)
|
||||
|
||||
# Project management
|
||||
openspec init [path] # Initialize OpenSpec
|
||||
openspec update [path] # Update instruction files
|
||||
|
||||
# Interactive mode
|
||||
openspec show # Prompts for selection
|
||||
openspec validate # Bulk validation mode
|
||||
|
||||
# Debugging
|
||||
openspec show [change] --json --deltas-only
|
||||
openspec validate [change] --strict
|
||||
```
|
||||
|
||||
### Command Flags
|
||||
|
||||
- `--json` - Machine-readable output
|
||||
- `--type change|spec` - Disambiguate items
|
||||
- `--strict` - Comprehensive validation
|
||||
- `--no-interactive` - Disable prompts
|
||||
- `--skip-specs` - Archive without spec updates
|
||||
- `--yes`/`-y` - Skip confirmation prompts (non-interactive archive)
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
openspec/
|
||||
├── project.md # Project conventions
|
||||
├── specs/ # Current truth - what IS built
|
||||
│ └── [capability]/ # Single focused capability
|
||||
│ ├── spec.md # Requirements and scenarios
|
||||
│ └── design.md # Technical patterns
|
||||
├── changes/ # Proposals - what SHOULD change
|
||||
│ ├── [change-name]/
|
||||
│ │ ├── proposal.md # Why, what, impact
|
||||
│ │ ├── tasks.md # Implementation checklist
|
||||
│ │ ├── design.md # Technical decisions (optional; see criteria)
|
||||
│ │ └── specs/ # Delta changes
|
||||
│ │ └── [capability]/
|
||||
│ │ └── spec.md # ADDED/MODIFIED/REMOVED
|
||||
│ └── archive/ # Completed changes
|
||||
```
|
||||
|
||||
## Creating Change Proposals
|
||||
|
||||
### Decision Tree
|
||||
|
||||
```
|
||||
New request?
|
||||
├─ Bug fix restoring spec behavior? → Fix directly
|
||||
├─ Typo/format/comment? → Fix directly
|
||||
├─ New feature/capability? → Create proposal
|
||||
├─ Breaking change? → Create proposal
|
||||
├─ Architecture change? → Create proposal
|
||||
└─ Unclear? → Create proposal (safer)
|
||||
```
|
||||
|
||||
### Proposal Structure
|
||||
|
||||
1. **Create directory:** `changes/[change-id]/` (kebab-case, verb-led, unique)
|
||||
|
||||
2. **Write proposal.md:**
|
||||
```markdown
|
||||
# Change: [Brief description of change]
|
||||
|
||||
## Why
|
||||
[1-2 sentences on problem/opportunity]
|
||||
|
||||
## What Changes
|
||||
- [Bullet list of changes]
|
||||
- [Mark breaking changes with **BREAKING**]
|
||||
|
||||
## Impact
|
||||
- Affected specs: [list capabilities]
|
||||
- Affected code: [key files/systems]
|
||||
```
|
||||
|
||||
3. **Create spec deltas:** `specs/[capability]/spec.md`
|
||||
```markdown
|
||||
## ADDED Requirements
|
||||
### Requirement: New Feature
|
||||
The system SHALL provide...
|
||||
|
||||
#### Scenario: Success case
|
||||
- **WHEN** user performs action
|
||||
- **THEN** expected result
|
||||
|
||||
## MODIFIED Requirements
|
||||
### Requirement: Existing Feature
|
||||
[Complete modified requirement]
|
||||
|
||||
## REMOVED Requirements
|
||||
### Requirement: Old Feature
|
||||
**Reason**: [Why removing]
|
||||
**Migration**: [How to handle]
|
||||
```
|
||||
If multiple capabilities are affected, create multiple delta files under `changes/[change-id]/specs/<capability>/spec.md`—one per capability.
|
||||
|
||||
4. **Create tasks.md:**
|
||||
```markdown
|
||||
## 1. Implementation
|
||||
- [ ] 1.1 Create database schema
|
||||
- [ ] 1.2 Implement API endpoint
|
||||
- [ ] 1.3 Add frontend component
|
||||
- [ ] 1.4 Write tests
|
||||
```
|
||||
|
||||
5. **Create design.md when needed:**
|
||||
Create `design.md` if any of the following apply; otherwise omit it:
|
||||
- Cross-cutting change (multiple services/modules) or a new architectural pattern
|
||||
- New external dependency or significant data model changes
|
||||
- Security, performance, or migration complexity
|
||||
- Ambiguity that benefits from technical decisions before coding
|
||||
|
||||
Minimal `design.md` skeleton:
|
||||
```markdown
|
||||
## Context
|
||||
[Background, constraints, stakeholders]
|
||||
|
||||
## Goals / Non-Goals
|
||||
- Goals: [...]
|
||||
- Non-Goals: [...]
|
||||
|
||||
## Decisions
|
||||
- Decision: [What and why]
|
||||
- Alternatives considered: [Options + rationale]
|
||||
|
||||
## Risks / Trade-offs
|
||||
- [Risk] → Mitigation
|
||||
|
||||
## Migration Plan
|
||||
[Steps, rollback]
|
||||
|
||||
## Open Questions
|
||||
- [...]
|
||||
```
|
||||
|
||||
## Spec File Format
|
||||
|
||||
### Critical: Scenario Formatting
|
||||
|
||||
**CORRECT** (use #### headers):
|
||||
```markdown
|
||||
#### Scenario: User login success
|
||||
- **WHEN** valid credentials provided
|
||||
- **THEN** return JWT token
|
||||
```
|
||||
|
||||
**WRONG** (don't use bullets or bold):
|
||||
```markdown
|
||||
- **Scenario: User login** ❌
|
||||
**Scenario**: User login ❌
|
||||
### Scenario: User login ❌
|
||||
```
|
||||
|
||||
Every requirement MUST have at least one scenario.
|
||||
|
||||
### Requirement Wording
|
||||
- Use SHALL/MUST for normative requirements (avoid should/may unless intentionally non-normative)
|
||||
|
||||
### Delta Operations
|
||||
|
||||
- `## ADDED Requirements` - New capabilities
|
||||
- `## MODIFIED Requirements` - Changed behavior
|
||||
- `## REMOVED Requirements` - Deprecated features
|
||||
- `## RENAMED Requirements` - Name changes
|
||||
|
||||
Headers matched with `trim(header)` - whitespace ignored.
|
||||
|
||||
#### When to use ADDED vs MODIFIED
|
||||
- ADDED: Introduces a new capability or sub-capability that can stand alone as a requirement. Prefer ADDED when the change is orthogonal (e.g., adding "Slash Command Configuration") rather than altering the semantics of an existing requirement.
|
||||
- MODIFIED: Changes the behavior, scope, or acceptance criteria of an existing requirement. Always paste the full, updated requirement content (header + all scenarios). The archiver will replace the entire requirement with what you provide here; partial deltas will drop previous details.
|
||||
- RENAMED: Use when only the name changes. If you also change behavior, use RENAMED (name) plus MODIFIED (content) referencing the new name.
|
||||
|
||||
Common pitfall: Using MODIFIED to add a new concern without including the previous text. This causes loss of detail at archive time. If you aren’t explicitly changing the existing requirement, add a new requirement under ADDED instead.
|
||||
|
||||
Authoring a MODIFIED requirement correctly:
|
||||
1) Locate the existing requirement in `openspec/specs/<capability>/spec.md`.
|
||||
2) Copy the entire requirement block (from `### Requirement: ...` through its scenarios).
|
||||
3) Paste it under `## MODIFIED Requirements` and edit to reflect the new behavior.
|
||||
4) Ensure the header text matches exactly (whitespace-insensitive) and keep at least one `#### Scenario:`.
|
||||
|
||||
Example for RENAMED:
|
||||
```markdown
|
||||
## RENAMED Requirements
|
||||
- FROM: `### Requirement: Login`
|
||||
- TO: `### Requirement: User Authentication`
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Errors
|
||||
|
||||
**"Change must have at least one delta"**
|
||||
- Check `changes/[name]/specs/` exists with .md files
|
||||
- Verify files have operation prefixes (## ADDED Requirements)
|
||||
|
||||
**"Requirement must have at least one scenario"**
|
||||
- Check scenarios use `#### Scenario:` format (4 hashtags)
|
||||
- Don't use bullet points or bold for scenario headers
|
||||
|
||||
**Silent scenario parsing failures**
|
||||
- Exact format required: `#### Scenario: Name`
|
||||
- Debug with: `openspec show [change] --json --deltas-only`
|
||||
|
||||
### Validation Tips
|
||||
|
||||
```bash
|
||||
# Always use strict mode for comprehensive checks
|
||||
openspec validate [change] --strict
|
||||
|
||||
# Debug delta parsing
|
||||
openspec show [change] --json | jq '.deltas'
|
||||
|
||||
# Check specific requirement
|
||||
openspec show [spec] --json -r 1
|
||||
```
|
||||
|
||||
## Happy Path Script
|
||||
|
||||
```bash
|
||||
# 1) Explore current state
|
||||
openspec spec list --long
|
||||
openspec list
|
||||
# Optional full-text search:
|
||||
# rg -n "Requirement:|Scenario:" openspec/specs
|
||||
# rg -n "^#|Requirement:" openspec/changes
|
||||
|
||||
# 2) Choose change id and scaffold
|
||||
CHANGE=add-two-factor-auth
|
||||
mkdir -p openspec/changes/$CHANGE/{specs/auth}
|
||||
printf "## Why\n...\n\n## What Changes\n- ...\n\n## Impact\n- ...\n" > openspec/changes/$CHANGE/proposal.md
|
||||
printf "## 1. Implementation\n- [ ] 1.1 ...\n" > openspec/changes/$CHANGE/tasks.md
|
||||
|
||||
# 3) Add deltas (example)
|
||||
cat > openspec/changes/$CHANGE/specs/auth/spec.md << 'EOF'
|
||||
## ADDED Requirements
|
||||
### Requirement: Two-Factor Authentication
|
||||
Users MUST provide a second factor during login.
|
||||
|
||||
#### Scenario: OTP required
|
||||
- **WHEN** valid credentials are provided
|
||||
- **THEN** an OTP challenge is required
|
||||
EOF
|
||||
|
||||
# 4) Validate
|
||||
openspec validate $CHANGE --strict
|
||||
```
|
||||
|
||||
## Multi-Capability Example
|
||||
|
||||
```
|
||||
openspec/changes/add-2fa-notify/
|
||||
├── proposal.md
|
||||
├── tasks.md
|
||||
└── specs/
|
||||
├── auth/
|
||||
│ └── spec.md # ADDED: Two-Factor Authentication
|
||||
└── notifications/
|
||||
└── spec.md # ADDED: OTP email notification
|
||||
```
|
||||
|
||||
auth/spec.md
|
||||
```markdown
|
||||
## ADDED Requirements
|
||||
### Requirement: Two-Factor Authentication
|
||||
...
|
||||
```
|
||||
|
||||
notifications/spec.md
|
||||
```markdown
|
||||
## ADDED Requirements
|
||||
### Requirement: OTP Email Notification
|
||||
...
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Simplicity First
|
||||
- Default to <100 lines of new code
|
||||
- Single-file implementations until proven insufficient
|
||||
- Avoid frameworks without clear justification
|
||||
- Choose boring, proven patterns
|
||||
|
||||
### Complexity Triggers
|
||||
Only add complexity with:
|
||||
- Performance data showing current solution too slow
|
||||
- Concrete scale requirements (>1000 users, >100MB data)
|
||||
- Multiple proven use cases requiring abstraction
|
||||
|
||||
### Clear References
|
||||
- Use `file.ts:42` format for code locations
|
||||
- Reference specs as `specs/auth/spec.md`
|
||||
- Link related changes and PRs
|
||||
|
||||
### Capability Naming
|
||||
- Use verb-noun: `user-auth`, `payment-capture`
|
||||
- Single purpose per capability
|
||||
- 10-minute understandability rule
|
||||
- Split if description needs "AND"
|
||||
|
||||
### Change ID Naming
|
||||
- Use kebab-case, short and descriptive: `add-two-factor-auth`
|
||||
- Prefer verb-led prefixes: `add-`, `update-`, `remove-`, `refactor-`
|
||||
- Ensure uniqueness; if taken, append `-2`, `-3`, etc.
|
||||
|
||||
## Tool Selection Guide
|
||||
|
||||
| Task | Tool | Why |
|
||||
|------|------|-----|
|
||||
| Find files by pattern | Glob | Fast pattern matching |
|
||||
| Search code content | Grep | Optimized regex search |
|
||||
| Read specific files | Read | Direct file access |
|
||||
| Explore unknown scope | Task | Multi-step investigation |
|
||||
|
||||
## Error Recovery
|
||||
|
||||
### Change Conflicts
|
||||
1. Run `openspec list` to see active changes
|
||||
2. Check for overlapping specs
|
||||
3. Coordinate with change owners
|
||||
4. Consider combining proposals
|
||||
|
||||
### Validation Failures
|
||||
1. Run with `--strict` flag
|
||||
2. Check JSON output for details
|
||||
3. Verify spec file format
|
||||
4. Ensure scenarios properly formatted
|
||||
|
||||
### Missing Context
|
||||
1. Read project.md first
|
||||
2. Check related specs
|
||||
3. Review recent archives
|
||||
4. Ask for clarification
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Stage Indicators
|
||||
- `changes/` - Proposed, not yet built
|
||||
- `specs/` - Built and deployed
|
||||
- `archive/` - Completed changes
|
||||
|
||||
### File Purposes
|
||||
- `proposal.md` - Why and what
|
||||
- `tasks.md` - Implementation steps
|
||||
- `design.md` - Technical decisions
|
||||
- `spec.md` - Requirements and behavior
|
||||
|
||||
### CLI Essentials
|
||||
```bash
|
||||
openspec list # What's in progress?
|
||||
openspec show [item] # View details
|
||||
openspec validate --strict # Is it correct?
|
||||
openspec archive <change-id> [--yes|-y] # Mark complete (add --yes for automation)
|
||||
```
|
||||
|
||||
Remember: Specs are truth. Changes are proposals. Keep them in sync.
|
||||
31
openspec/project.md
Normal file
31
openspec/project.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Project Context
|
||||
|
||||
## Purpose
|
||||
[Describe your project's purpose and goals]
|
||||
|
||||
## Tech Stack
|
||||
- [List your primary technologies]
|
||||
- [e.g., TypeScript, React, Node.js]
|
||||
|
||||
## Project Conventions
|
||||
|
||||
### Code Style
|
||||
[Describe your code style preferences, formatting rules, and naming conventions]
|
||||
|
||||
### Architecture Patterns
|
||||
[Document your architectural decisions and patterns]
|
||||
|
||||
### Testing Strategy
|
||||
[Explain your testing approach and requirements]
|
||||
|
||||
### Git Workflow
|
||||
[Describe your branching strategy and commit conventions]
|
||||
|
||||
## Domain Context
|
||||
[Add domain-specific knowledge that AI assistants need to understand]
|
||||
|
||||
## Important Constraints
|
||||
[List any technical, business, or regulatory constraints]
|
||||
|
||||
## External Dependencies
|
||||
[Document key external services, APIs, or systems]
|
||||
Reference in New Issue
Block a user