添加后台启动脚本和修改域名

This commit is contained in:
xuqiuyun
2025-11-17 09:18:31 +08:00
parent eca2040e5b
commit 8615549a6f
45 changed files with 15564 additions and 384 deletions

View 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 -->

View 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 -->

View 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
View 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 -->

View File

@@ -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",

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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);

View File

@@ -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);
};
/**

View File

@@ -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
// 使用原始IDstrainId如果没有则使用strain兼容旧数据
formData.strain = record.strainId !== undefined ? record.strainId : (record.strain || '') // iot_cattle.strain
// 使用原始IDvarietiesId如果没有则使用varieties兼容旧数据
formData.varieties = record.varietiesId !== undefined ? record.varietiesId : (record.varieties || '') // iot_cattle.varieties
// 使用原始IDcateId如果没有则使用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'
}
})
// 调用导入APIFormData会自动处理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
}

View File

@@ -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) => {

View File

@@ -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) => {

View File

@@ -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) => {

View File

@@ -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) => {

View File

@@ -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) => {

View File

@@ -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>

View File

@@ -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;

View File

@@ -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>

View File

@@ -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')
)
}
}

View File

@@ -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: '获取离栏记录详情失败',

View File

@@ -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: {

View File

@@ -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) {

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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

File diff suppressed because it is too large Load Diff

117
backend/swagger-alerts.js Normal file
View 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
};

View 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
View 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
View 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
View 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
View 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
};

View 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
View 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
View 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
View 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
};

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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;

View 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

View File

@@ -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
View 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 12 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 arent 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
View 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]