From 609f7d06cf2d332d3bd5a61ec6677518d39c678e Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 3 Jul 2026 15:33:31 +0800 Subject: [PATCH] =?UTF-8?q?init:=20=E6=AF=9B=E5=AD=A9=E5=AD=90=E8=AE=A1?= =?UTF-8?q?=E5=88=92=20=E5=B0=8F=E7=A8=8B=E5=BA=8F=20+=20Go=20=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=20+=20=E5=86=85=E5=B5=8C=E5=90=8E=E5=8F=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pets-fe: 微信原生小程序(首页/计划/记录/报告/社区/引导), 服务端驱动、无假数据;弹层改用 scroll-view,打开时隐藏自定义 tabBar - pets-be: Gin + GORM(MySQL, sundynix_ 前缀) + MinIO,统一响应/分页, 微信 code2session 登录,provider-neutral AI(DeepSeek),go:embed React 后台 - 修复:分段选择类型不匹配(字符串 vs 数字)导致选不中 Co-Authored-By: Claude Opus 4.8 --- .gitignore | 21 + pets-be/.gitignore | 11 + pets-be/.idea/.gitignore | 10 + pets-be/.idea/go.imports.xml | 10 + pets-be/.idea/modules.xml | 8 + pets-be/.idea/pets-be.iml | 9 + pets-be/.idea/vcs.xml | 6 + pets-be/Makefile | 29 + pets-be/README.md | 49 + pets-be/cmd/server/main.go | 53 + pets-be/configs/config.example.yaml | 43 + pets-be/deployments/docker-compose.yml | 45 + pets-be/go.mod | 70 + pets-be/go.sum | 188 + .../admin/dist/assets/index-WALewLI5.js | 59 + .../admin/dist/assets/index-ZqmdrDyd.css | 1 + pets-be/internal/admin/dist/favicon.svg | 1 + pets-be/internal/admin/dist/icons.svg | 24 + pets-be/internal/admin/dist/index.html | 14 + pets-be/internal/admin/embed.go | 48 + pets-be/internal/ai/ai.go | 73 + pets-be/internal/ai/openai_compat.go | 122 + pets-be/internal/config/config.go | 107 + pets-be/internal/database/database.go | 48 + pets-be/internal/database/seed.go | 53 + pets-be/internal/handler/admin.go | 223 ++ pets-be/internal/handler/auth.go | 120 + pets-be/internal/handler/community.go | 130 + pets-be/internal/handler/handler.go | 18 + pets-be/internal/handler/helper.go | 33 + pets-be/internal/handler/misc.go | 117 + pets-be/internal/handler/pet.go | 143 + pets-be/internal/handler/plan.go | 111 + pets-be/internal/handler/record.go | 77 + pets-be/internal/handler/reminder.go | 85 + pets-be/internal/handler/report.go | 49 + pets-be/internal/handler/upload.go | 42 + pets-be/internal/middleware/auth.go | 77 + pets-be/internal/middleware/cors.go | 28 + pets-be/internal/model/admin.go | 10 + pets-be/internal/model/ai_message.go | 11 + pets-be/internal/model/article.go | 13 + pets-be/internal/model/base.go | 31 + pets-be/internal/model/comment.go | 11 + pets-be/internal/model/daily_advice.go | 9 + pets-be/internal/model/daily_task.go | 17 + pets-be/internal/model/health_record.go | 37 + pets-be/internal/model/pet.go | 25 + pets-be/internal/model/plan.go | 41 + pets-be/internal/model/post.go | 26 + pets-be/internal/model/post_like.go | 8 + pets-be/internal/model/pro_membership.go | 21 + pets-be/internal/model/reminder.go | 22 + pets-be/internal/model/user.go | 12 + pets-be/internal/router/router.go | 128 + pets-be/internal/service/admin.go | 161 + pets-be/internal/service/ai.go | 31 + pets-be/internal/service/ai_llm.go | 141 + pets-be/internal/service/article.go | 26 + pets-be/internal/service/auth.go | 122 + pets-be/internal/service/community.go | 177 + pets-be/internal/service/home.go | 229 ++ pets-be/internal/service/pet.go | 168 + pets-be/internal/service/plan.go | 185 + pets-be/internal/service/pro.go | 62 + pets-be/internal/service/record.go | 108 + pets-be/internal/service/reminder.go | 64 + pets-be/internal/service/report.go | 186 + pets-be/internal/service/service.go | 40 + pets-be/internal/service/task.go | 63 + pets-be/internal/service/util.go | 8 + pets-be/internal/storage/minio.go | 85 + pets-be/pkg/errcode/errcode.go | 34 + pets-be/pkg/jwt/jwt.go | 74 + pets-be/pkg/response/pagination.go | 70 + pets-be/pkg/response/response.go | 60 + pets-be/web/admin/.gitignore | 24 + pets-be/web/admin/.oxlintrc.json | 8 + pets-be/web/admin/README.md | 32 + pets-be/web/admin/index.html | 13 + pets-be/web/admin/package-lock.json | 3332 +++++++++++++++++ pets-be/web/admin/package.json | 37 + pets-be/web/admin/postcss.config.js | 6 + pets-be/web/admin/public/favicon.svg | 1 + pets-be/web/admin/public/icons.svg | 24 + pets-be/web/admin/src/App.css | 184 + pets-be/web/admin/src/App.tsx | 41 + pets-be/web/admin/src/assets/hero.png | Bin 0 -> 13057 bytes pets-be/web/admin/src/assets/react.svg | 1 + pets-be/web/admin/src/assets/vite.svg | 1 + pets-be/web/admin/src/components/Layout.tsx | 59 + pets-be/web/admin/src/components/Pager.tsx | 30 + pets-be/web/admin/src/components/ui/badge.tsx | 28 + .../web/admin/src/components/ui/button.tsx | 43 + pets-be/web/admin/src/components/ui/card.tsx | 30 + .../web/admin/src/components/ui/dialog.tsx | 41 + pets-be/web/admin/src/components/ui/input.tsx | 19 + pets-be/web/admin/src/components/ui/label.tsx | 13 + pets-be/web/admin/src/components/ui/table.tsx | 46 + pets-be/web/admin/src/index.css | 37 + pets-be/web/admin/src/lib/api.ts | 72 + pets-be/web/admin/src/lib/usePaged.ts | 33 + pets-be/web/admin/src/lib/utils.ts | 6 + pets-be/web/admin/src/main.tsx | 13 + pets-be/web/admin/src/pages/Articles.tsx | 147 + pets-be/web/admin/src/pages/Comments.tsx | 59 + pets-be/web/admin/src/pages/Dashboard.tsx | 39 + pets-be/web/admin/src/pages/Login.tsx | 57 + pets-be/web/admin/src/pages/Members.tsx | 46 + pets-be/web/admin/src/pages/Pets.tsx | 46 + pets-be/web/admin/src/pages/Posts.tsx | 78 + pets-be/web/admin/src/pages/Users.tsx | 71 + pets-be/web/admin/tailwind.config.js | 46 + pets-be/web/admin/tsconfig.app.json | 28 + pets-be/web/admin/tsconfig.json | 7 + pets-be/web/admin/tsconfig.node.json | 23 + pets-be/web/admin/vite.config.ts | 25 + pets-fe/app.js | 28 + pets-fe/app.json | 32 + pets-fe/app.wxss | 252 ++ .../components/bottom-sheet/bottom-sheet.js | 462 +++ .../components/bottom-sheet/bottom-sheet.json | 4 + .../components/bottom-sheet/bottom-sheet.wxml | 287 ++ .../components/bottom-sheet/bottom-sheet.wxss | 36 + pets-fe/components/fab/fab.js | 8 + pets-fe/components/fab/fab.json | 4 + pets-fe/components/fab/fab.wxml | 1 + pets-fe/components/fab/fab.wxss | 10 + pets-fe/components/nav-bar/nav-bar.js | 31 + pets-fe/components/nav-bar/nav-bar.json | 4 + pets-fe/components/nav-bar/nav-bar.wxml | 8 + pets-fe/components/nav-bar/nav-bar.wxss | 19 + pets-fe/components/pet-switch/pet-switch.js | 34 + pets-fe/components/pet-switch/pet-switch.json | 4 + pets-fe/components/pet-switch/pet-switch.wxml | 10 + pets-fe/components/pet-switch/pet-switch.wxss | 5 + pets-fe/custom-tab-bar/index.js | 20 + pets-fe/custom-tab-bar/index.json | 4 + pets-fe/custom-tab-bar/index.wxml | 7 + pets-fe/custom-tab-bar/index.wxss | 16 + pets-fe/pages/community/community.js | 94 + pets-fe/pages/community/community.json | 7 + pets-fe/pages/community/community.wxml | 44 + pets-fe/pages/community/community.wxss | 48 + pets-fe/pages/home/home.js | 130 + pets-fe/pages/home/home.json | 8 + pets-fe/pages/home/home.wxml | 93 + pets-fe/pages/home/home.wxss | 59 + pets-fe/pages/learn/learn.js | 35 + pets-fe/pages/learn/learn.json | 7 + pets-fe/pages/learn/learn.wxml | 18 + pets-fe/pages/learn/learn.wxss | 9 + pets-fe/pages/onboarding/onboarding.js | 101 + pets-fe/pages/onboarding/onboarding.json | 6 + pets-fe/pages/onboarding/onboarding.wxml | 101 + pets-fe/pages/onboarding/onboarding.wxss | 55 + pets-fe/pages/plan/plan.js | 133 + pets-fe/pages/plan/plan.json | 8 + pets-fe/pages/plan/plan.wxml | 85 + pets-fe/pages/plan/plan.wxss | 54 + pets-fe/pages/profile/profile.js | 55 + pets-fe/pages/profile/profile.json | 7 + pets-fe/pages/profile/profile.wxml | 33 + pets-fe/pages/profile/profile.wxss | 18 + pets-fe/pages/record/record.js | 79 + pets-fe/pages/record/record.json | 8 + pets-fe/pages/record/record.wxml | 44 + pets-fe/pages/record/record.wxss | 32 + pets-fe/pages/report/report.js | 62 + pets-fe/pages/report/report.json | 8 + pets-fe/pages/report/report.wxml | 59 + pets-fe/pages/report/report.wxss | 19 + pets-fe/preview.html | 2385 ++++++++++++ pets-fe/project.config.json | 53 + pets-fe/project.private.config.json | 22 + pets-fe/sitemap.json | 4 + pets-fe/utils/api.js | 99 + pets-fe/utils/config.js | 5 + pets-fe/utils/request.js | 69 + pets-fe/utils/store.js | 100 + 180 files changed, 15259 insertions(+) create mode 100644 .gitignore create mode 100644 pets-be/.gitignore create mode 100644 pets-be/.idea/.gitignore create mode 100644 pets-be/.idea/go.imports.xml create mode 100644 pets-be/.idea/modules.xml create mode 100644 pets-be/.idea/pets-be.iml create mode 100644 pets-be/.idea/vcs.xml create mode 100644 pets-be/Makefile create mode 100644 pets-be/README.md create mode 100644 pets-be/cmd/server/main.go create mode 100644 pets-be/configs/config.example.yaml create mode 100644 pets-be/deployments/docker-compose.yml create mode 100644 pets-be/go.mod create mode 100644 pets-be/go.sum create mode 100644 pets-be/internal/admin/dist/assets/index-WALewLI5.js create mode 100644 pets-be/internal/admin/dist/assets/index-ZqmdrDyd.css create mode 100644 pets-be/internal/admin/dist/favicon.svg create mode 100644 pets-be/internal/admin/dist/icons.svg create mode 100644 pets-be/internal/admin/dist/index.html create mode 100644 pets-be/internal/admin/embed.go create mode 100644 pets-be/internal/ai/ai.go create mode 100644 pets-be/internal/ai/openai_compat.go create mode 100644 pets-be/internal/config/config.go create mode 100644 pets-be/internal/database/database.go create mode 100644 pets-be/internal/database/seed.go create mode 100644 pets-be/internal/handler/admin.go create mode 100644 pets-be/internal/handler/auth.go create mode 100644 pets-be/internal/handler/community.go create mode 100644 pets-be/internal/handler/handler.go create mode 100644 pets-be/internal/handler/helper.go create mode 100644 pets-be/internal/handler/misc.go create mode 100644 pets-be/internal/handler/pet.go create mode 100644 pets-be/internal/handler/plan.go create mode 100644 pets-be/internal/handler/record.go create mode 100644 pets-be/internal/handler/reminder.go create mode 100644 pets-be/internal/handler/report.go create mode 100644 pets-be/internal/handler/upload.go create mode 100644 pets-be/internal/middleware/auth.go create mode 100644 pets-be/internal/middleware/cors.go create mode 100644 pets-be/internal/model/admin.go create mode 100644 pets-be/internal/model/ai_message.go create mode 100644 pets-be/internal/model/article.go create mode 100644 pets-be/internal/model/base.go create mode 100644 pets-be/internal/model/comment.go create mode 100644 pets-be/internal/model/daily_advice.go create mode 100644 pets-be/internal/model/daily_task.go create mode 100644 pets-be/internal/model/health_record.go create mode 100644 pets-be/internal/model/pet.go create mode 100644 pets-be/internal/model/plan.go create mode 100644 pets-be/internal/model/post.go create mode 100644 pets-be/internal/model/post_like.go create mode 100644 pets-be/internal/model/pro_membership.go create mode 100644 pets-be/internal/model/reminder.go create mode 100644 pets-be/internal/model/user.go create mode 100644 pets-be/internal/router/router.go create mode 100644 pets-be/internal/service/admin.go create mode 100644 pets-be/internal/service/ai.go create mode 100644 pets-be/internal/service/ai_llm.go create mode 100644 pets-be/internal/service/article.go create mode 100644 pets-be/internal/service/auth.go create mode 100644 pets-be/internal/service/community.go create mode 100644 pets-be/internal/service/home.go create mode 100644 pets-be/internal/service/pet.go create mode 100644 pets-be/internal/service/plan.go create mode 100644 pets-be/internal/service/pro.go create mode 100644 pets-be/internal/service/record.go create mode 100644 pets-be/internal/service/reminder.go create mode 100644 pets-be/internal/service/report.go create mode 100644 pets-be/internal/service/service.go create mode 100644 pets-be/internal/service/task.go create mode 100644 pets-be/internal/service/util.go create mode 100644 pets-be/internal/storage/minio.go create mode 100644 pets-be/pkg/errcode/errcode.go create mode 100644 pets-be/pkg/jwt/jwt.go create mode 100644 pets-be/pkg/response/pagination.go create mode 100644 pets-be/pkg/response/response.go create mode 100644 pets-be/web/admin/.gitignore create mode 100644 pets-be/web/admin/.oxlintrc.json create mode 100644 pets-be/web/admin/README.md create mode 100644 pets-be/web/admin/index.html create mode 100644 pets-be/web/admin/package-lock.json create mode 100644 pets-be/web/admin/package.json create mode 100644 pets-be/web/admin/postcss.config.js create mode 100644 pets-be/web/admin/public/favicon.svg create mode 100644 pets-be/web/admin/public/icons.svg create mode 100644 pets-be/web/admin/src/App.css create mode 100644 pets-be/web/admin/src/App.tsx create mode 100644 pets-be/web/admin/src/assets/hero.png create mode 100644 pets-be/web/admin/src/assets/react.svg create mode 100644 pets-be/web/admin/src/assets/vite.svg create mode 100644 pets-be/web/admin/src/components/Layout.tsx create mode 100644 pets-be/web/admin/src/components/Pager.tsx create mode 100644 pets-be/web/admin/src/components/ui/badge.tsx create mode 100644 pets-be/web/admin/src/components/ui/button.tsx create mode 100644 pets-be/web/admin/src/components/ui/card.tsx create mode 100644 pets-be/web/admin/src/components/ui/dialog.tsx create mode 100644 pets-be/web/admin/src/components/ui/input.tsx create mode 100644 pets-be/web/admin/src/components/ui/label.tsx create mode 100644 pets-be/web/admin/src/components/ui/table.tsx create mode 100644 pets-be/web/admin/src/index.css create mode 100644 pets-be/web/admin/src/lib/api.ts create mode 100644 pets-be/web/admin/src/lib/usePaged.ts create mode 100644 pets-be/web/admin/src/lib/utils.ts create mode 100644 pets-be/web/admin/src/main.tsx create mode 100644 pets-be/web/admin/src/pages/Articles.tsx create mode 100644 pets-be/web/admin/src/pages/Comments.tsx create mode 100644 pets-be/web/admin/src/pages/Dashboard.tsx create mode 100644 pets-be/web/admin/src/pages/Login.tsx create mode 100644 pets-be/web/admin/src/pages/Members.tsx create mode 100644 pets-be/web/admin/src/pages/Pets.tsx create mode 100644 pets-be/web/admin/src/pages/Posts.tsx create mode 100644 pets-be/web/admin/src/pages/Users.tsx create mode 100644 pets-be/web/admin/tailwind.config.js create mode 100644 pets-be/web/admin/tsconfig.app.json create mode 100644 pets-be/web/admin/tsconfig.json create mode 100644 pets-be/web/admin/tsconfig.node.json create mode 100644 pets-be/web/admin/vite.config.ts create mode 100644 pets-fe/app.js create mode 100644 pets-fe/app.json create mode 100644 pets-fe/app.wxss create mode 100644 pets-fe/components/bottom-sheet/bottom-sheet.js create mode 100644 pets-fe/components/bottom-sheet/bottom-sheet.json create mode 100644 pets-fe/components/bottom-sheet/bottom-sheet.wxml create mode 100644 pets-fe/components/bottom-sheet/bottom-sheet.wxss create mode 100644 pets-fe/components/fab/fab.js create mode 100644 pets-fe/components/fab/fab.json create mode 100644 pets-fe/components/fab/fab.wxml create mode 100644 pets-fe/components/fab/fab.wxss create mode 100644 pets-fe/components/nav-bar/nav-bar.js create mode 100644 pets-fe/components/nav-bar/nav-bar.json create mode 100644 pets-fe/components/nav-bar/nav-bar.wxml create mode 100644 pets-fe/components/nav-bar/nav-bar.wxss create mode 100644 pets-fe/components/pet-switch/pet-switch.js create mode 100644 pets-fe/components/pet-switch/pet-switch.json create mode 100644 pets-fe/components/pet-switch/pet-switch.wxml create mode 100644 pets-fe/components/pet-switch/pet-switch.wxss create mode 100644 pets-fe/custom-tab-bar/index.js create mode 100644 pets-fe/custom-tab-bar/index.json create mode 100644 pets-fe/custom-tab-bar/index.wxml create mode 100644 pets-fe/custom-tab-bar/index.wxss create mode 100644 pets-fe/pages/community/community.js create mode 100644 pets-fe/pages/community/community.json create mode 100644 pets-fe/pages/community/community.wxml create mode 100644 pets-fe/pages/community/community.wxss create mode 100644 pets-fe/pages/home/home.js create mode 100644 pets-fe/pages/home/home.json create mode 100644 pets-fe/pages/home/home.wxml create mode 100644 pets-fe/pages/home/home.wxss create mode 100644 pets-fe/pages/learn/learn.js create mode 100644 pets-fe/pages/learn/learn.json create mode 100644 pets-fe/pages/learn/learn.wxml create mode 100644 pets-fe/pages/learn/learn.wxss create mode 100644 pets-fe/pages/onboarding/onboarding.js create mode 100644 pets-fe/pages/onboarding/onboarding.json create mode 100644 pets-fe/pages/onboarding/onboarding.wxml create mode 100644 pets-fe/pages/onboarding/onboarding.wxss create mode 100644 pets-fe/pages/plan/plan.js create mode 100644 pets-fe/pages/plan/plan.json create mode 100644 pets-fe/pages/plan/plan.wxml create mode 100644 pets-fe/pages/plan/plan.wxss create mode 100644 pets-fe/pages/profile/profile.js create mode 100644 pets-fe/pages/profile/profile.json create mode 100644 pets-fe/pages/profile/profile.wxml create mode 100644 pets-fe/pages/profile/profile.wxss create mode 100644 pets-fe/pages/record/record.js create mode 100644 pets-fe/pages/record/record.json create mode 100644 pets-fe/pages/record/record.wxml create mode 100644 pets-fe/pages/record/record.wxss create mode 100644 pets-fe/pages/report/report.js create mode 100644 pets-fe/pages/report/report.json create mode 100644 pets-fe/pages/report/report.wxml create mode 100644 pets-fe/pages/report/report.wxss create mode 100644 pets-fe/preview.html create mode 100644 pets-fe/project.config.json create mode 100644 pets-fe/project.private.config.json create mode 100644 pets-fe/sitemap.json create mode 100644 pets-fe/utils/api.js create mode 100644 pets-fe/utils/config.js create mode 100644 pets-fe/utils/request.js create mode 100644 pets-fe/utils/store.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..583b32b --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# macOS +.DS_Store + +# Claude Code local config +.claude/ + +# Go build output +pets-be/bin/ +*.exe +*.out + +# Node +pets-be/web/admin/node_modules/ +npm-debug.log* +yarn-error.log* + +# Logs / runtime +*.log + +# Secrets — real config stays local; copy from config.example.yaml +pets-be/configs/config.yaml diff --git a/pets-be/.gitignore b/pets-be/.gitignore new file mode 100644 index 0000000..83fbbc3 --- /dev/null +++ b/pets-be/.gitignore @@ -0,0 +1,11 @@ +# Go +/bin/ +*.out + +# Node +web/admin/node_modules/ +web/admin/.vite/ + +# 本地环境/日志 +*.log +.env diff --git a/pets-be/.idea/.gitignore b/pets-be/.idea/.gitignore new file mode 100644 index 0000000..30cf57e --- /dev/null +++ b/pets-be/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/pets-be/.idea/go.imports.xml b/pets-be/.idea/go.imports.xml new file mode 100644 index 0000000..644cdf0 --- /dev/null +++ b/pets-be/.idea/go.imports.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/pets-be/.idea/modules.xml b/pets-be/.idea/modules.xml new file mode 100644 index 0000000..a84bf36 --- /dev/null +++ b/pets-be/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/pets-be/.idea/pets-be.iml b/pets-be/.idea/pets-be.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/pets-be/.idea/pets-be.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/pets-be/.idea/vcs.xml b/pets-be/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/pets-be/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/pets-be/Makefile b/pets-be/Makefile new file mode 100644 index 0000000..28678a0 --- /dev/null +++ b/pets-be/Makefile @@ -0,0 +1,29 @@ +.PHONY: env-up env-down admin-install admin-build admin-dev build run tidy + +# 起/停 MySQL + MinIO +env-up: + cd deployments && docker compose up -d + +env-down: + cd deployments && docker compose down + +# 后台前端 +admin-install: + cd web/admin && npm install + +admin-build: + cd web/admin && npm run build + +admin-dev: + cd web/admin && npm run dev + +# 构建单二进制(先出后台产物再 go build,embed 打包) +build: admin-build + go build -o bin/pets-be ./cmd/server + +# 直接运行后端(使用当前已存在的 internal/admin/dist) +run: + go run ./cmd/server + +tidy: + go mod tidy diff --git a/pets-be/README.md b/pets-be/README.md new file mode 100644 index 0000000..725fd80 --- /dev/null +++ b/pets-be/README.md @@ -0,0 +1,49 @@ +# 毛孩子计划 · 后端 (pets-be) + +Gin + GORM + MySQL + MinIO 的宠物小程序后端,内嵌 React(Vite + Tailwind + shadcn/ui) 后台管理。 + +## 技术栈与约定 +- 模块路径 `github.com/sundynix/pets-be`,标准 Go 布局(`cmd/ internal/ pkg/ configs/ deployments/ web/`) +- 所有数据库表带前缀 `sundynix_` +- 统一响应 `{code, message, data}`(`pkg/response`),列表接口统一分页参数 `page` / `page_size`(`response.PageQuery` + `PageResult`) +- MySQL `root/root` 库 `pets`;MinIO `sundynix/sundynix` 桶 `pets` +- 服务端口 `:8080`;后台在 `http://localhost:8080/admin`(seed 管理员 `sundynix/sundynix`) + +## 快速开始 +```bash +# 1. 起依赖(MySQL + MinIO) +make env-up # 或 cd deployments && docker compose up -d + +# 2. 构建后台前端 + 后端单二进制 +make build # 产物 bin/pets-be(已 embed 后台) +./bin/pets-be + +# 或开发期直接运行(使用现有 internal/admin/dist) +make run +``` + +### 后台前端开发(热更新) +```bash +make admin-dev # vite dev server :5173,已代理 /api → :8080 +# 改完执行 make admin-build 让产物落到 internal/admin/dist,再 go build 即 embed +``` + +## 目录 +- `cmd/server` 入口:载配置→连库→迁移→seed→建桶→路由→启动 +- `internal/config` 配置(viper,支持 `PETS_` 环境变量覆盖) +- `internal/model` GORM 模型(`sundynix_` 前缀) +- `internal/database` 连接/迁移/seed +- `internal/storage` MinIO 封装(自动建桶 + 上传) +- `internal/service` 业务逻辑;`internal/handler` HTTP 处理器;`internal/router` 路由 +- `internal/middleware` CORS / 用户鉴权 / 管理员鉴权 +- `internal/admin` `go:embed` 内嵌后台产物 + SPA fallback +- `pkg/response` 统一响应与分页;`pkg/jwt` 令牌;`pkg/errcode` 错误码 +- `web/admin` 后台前端源码 + +## 接口 +- 小程序 `/api/*`(用户 JWT):auth / user / pets / onboarding / records / tasks / plan / reminders / report / bill / community / articles / pro / ai / upload +- 后台 `/api/admin/*`(管理员 JWT):login / stats / users / pets / posts / comments / articles / memberships + +## 登录鉴权 +默认 `auth.dev_login: true`,`POST /api/auth/login {"nickname":"xxx"}` 即发用户 JWT。 +接真微信:在 `configs/config.yaml` 填 `wechat.app_secret` 并把 `dev_login` 置 false,实现 `/api/auth/wechat`(code2session)。 diff --git a/pets-be/cmd/server/main.go b/pets-be/cmd/server/main.go new file mode 100644 index 0000000..2075e47 --- /dev/null +++ b/pets-be/cmd/server/main.go @@ -0,0 +1,53 @@ +package main + +import ( + "fmt" + "log" + + "github.com/sundynix/pets-be/internal/ai" + "github.com/sundynix/pets-be/internal/config" + "github.com/sundynix/pets-be/internal/database" + "github.com/sundynix/pets-be/internal/handler" + "github.com/sundynix/pets-be/internal/router" + "github.com/sundynix/pets-be/internal/service" + "github.com/sundynix/pets-be/internal/storage" + appjwt "github.com/sundynix/pets-be/pkg/jwt" +) + +func main() { + cfg, err := config.Load() + if err != nil { + log.Fatalf("load config: %v", err) + } + + db, err := database.New(cfg) + if err != nil { + log.Fatalf("connect mysql: %v", err) + } + if err := database.Migrate(db); err != nil { + log.Fatalf("migrate: %v", err) + } + if err := database.Seed(db, cfg); err != nil { + log.Fatalf("seed: %v", err) + } + + st, err := storage.New(cfg.MinIO) + if err != nil { + log.Fatalf("connect minio: %v", err) + } + + engine := ai.New(cfg.AI) + log.Printf("AI provider: %s", engine.ProviderName()) + + svc := service.New(db, st, cfg, engine) + jm := appjwt.NewManager(cfg.JWT.Secret, cfg.JWT.ExpireHours) + h := handler.New(svc, jm, cfg) + + r := router.New(h, jm, cfg.Server.Mode) + + addr := fmt.Sprintf(":%d", cfg.Server.Port) + log.Printf("pets-be listening on %s (admin: http://localhost%s/admin)", addr, addr) + if err := r.Run(addr); err != nil { + log.Fatalf("server run: %v", err) + } +} diff --git a/pets-be/configs/config.example.yaml b/pets-be/configs/config.example.yaml new file mode 100644 index 0000000..64c877d --- /dev/null +++ b/pets-be/configs/config.example.yaml @@ -0,0 +1,43 @@ +server: + port: 8080 + mode: debug # debug | release + +mysql: + host: 127.0.0.1 + port: 3306 + user: root + password: root + database: pets + charset: utf8mb4 + +minio: + endpoint: 127.0.0.1:9000 + access_key: sundynix + secret_key: sundynix + bucket: pets + use_ssl: false + public_base_url: http://127.0.0.1:9000 # 拼接可访问的对象 URL + +jwt: + secret: change-me-to-a-long-random-string # 生产用 PETS_JWT_SECRET 覆盖 + +wechat: + app_id: your-wechat-app-id + app_secret: your-wechat-app-secret # 敏感凭证:生产用 PETS_WECHAT_APP_SECRET 环境变量覆盖 + +auth: + dev_login: true # 仍保留 Mock 登录便于本地 curl 调试;上线可置 false 只留微信登录 + +admin: + username: sundynix + password: change-me # 启动时若无同名管理员则 seed 此账号 + +ai: + enabled: false # false 时全部走规则化文案(不烧 token);接大模型改 true + provider: openai # openai 兼容协议,适配 DeepSeek/Qwen(兼容模式)/Gemini(兼容端点)等 + base_url: https://api.deepseek.com # 换供应商只改这里 + model + api_key: your-ai-api-key # 敏感:生产用 PETS_AI_API_KEY 环境变量覆盖 + model: deepseek-chat + temperature: 0.6 + max_tokens: 1024 + timeout_sec: 30 diff --git a/pets-be/deployments/docker-compose.yml b/pets-be/deployments/docker-compose.yml new file mode 100644 index 0000000..2719fa3 --- /dev/null +++ b/pets-be/deployments/docker-compose.yml @@ -0,0 +1,45 @@ +services: + mysql: + image: mysql:8.0 + container_name: pets-mysql + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: pets + TZ: Asia/Shanghai + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + - --default-authentication-plugin=mysql_native_password + ports: + - "3306:3306" + volumes: + - mysql_data:/var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-proot"] + interval: 5s + timeout: 5s + retries: 20 + + minio: + image: minio/minio:latest + container_name: pets-minio + restart: unless-stopped + environment: + MINIO_ROOT_USER: sundynix + MINIO_ROOT_PASSWORD: sundynix + command: server /data --console-address ":9001" + ports: + - "9000:9000" + - "9001:9001" + volumes: + - minio_data:/data + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 5s + timeout: 5s + retries: 20 + +volumes: + mysql_data: + minio_data: diff --git a/pets-be/go.mod b/pets-be/go.mod new file mode 100644 index 0000000..62975bc --- /dev/null +++ b/pets-be/go.mod @@ -0,0 +1,70 @@ +module github.com/sundynix/pets-be + +go 1.26.4 + +require ( + github.com/gin-gonic/gin v1.12.0 + github.com/golang-jwt/jwt/v5 v5.3.1 + github.com/minio/minio-go/v7 v7.2.1 + github.com/spf13/viper v1.21.0 + golang.org/x/crypto v0.53.0 + gorm.io/datatypes v1.2.7 + gorm.io/driver/mysql v1.6.0 + gorm.io/gorm v1.31.2 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.12 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.30.1 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.6 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/klauspost/crc32 v1.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/minio/crc64nvme v1.1.1 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.3.1 // indirect + github.com/philhofer/fwd v1.2.0 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/tinylib/msgp v1.6.1 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect + github.com/zeebo/xxh3 v1.1.0 // indirect + go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/arch v0.22.0 // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/text v0.38.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/ini.v1 v1.67.2 // indirect +) diff --git a/pets-be/go.sum b/pets-be/go.sum new file mode 100644 index 0000000..e3879c0 --- /dev/null +++ b/pets-be/go.sum @@ -0,0 +1,188 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= +github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= +github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM= +github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA= +github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= +github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI= +github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.2.1 h1:PfBfwvKB/MmqyN8Vb1G9voWisaM9OrLv+WwOvMwS9Dw= +github.com/minio/minio-go/v7 v7.2.1/go.mod h1:EU9hENAStx/xXduNdrGO5e4X5vk19NtgB+RIPjZO8o0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc= +github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= +github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY= +github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= +golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/ini.v1 v1.67.2 h1:JtOSMb9OuaCZKr7h5D/h6iii14sK0hLbplTc6frx4Ss= +gopkg.in/ini.v1 v1.67.2/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk= +gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY= +gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= +gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= +gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U= +gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc= +gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw= +gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo= +gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= diff --git a/pets-be/internal/admin/dist/assets/index-WALewLI5.js b/pets-be/internal/admin/dist/assets/index-WALewLI5.js new file mode 100644 index 0000000..2100163 --- /dev/null +++ b/pets-be/internal/admin/dist/assets/index-WALewLI5.js @@ -0,0 +1,59 @@ +var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),s=(e,n)=>{let r={};for(var i in e)t(r,i,{get:e[i],enumerable:!0});return n||t(r,Symbol.toStringTag,{value:`Module`}),r},c=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;li[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},l=(n,r,a)=>(a=n==null?{}:e(i(n)),c(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var u=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.portal`),r=Symbol.for(`react.fragment`),i=Symbol.for(`react.strict_mode`),a=Symbol.for(`react.profiler`),o=Symbol.for(`react.consumer`),s=Symbol.for(`react.context`),c=Symbol.for(`react.forward_ref`),l=Symbol.for(`react.suspense`),u=Symbol.for(`react.memo`),d=Symbol.for(`react.lazy`),f=Symbol.for(`react.activity`),p=Symbol.iterator;function m(e){return typeof e!=`object`||!e?null:(e=p&&e[p]||e[`@@iterator`],typeof e==`function`?e:null)}var h={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},g=Object.assign,_={};function v(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}v.prototype.isReactComponent={},v.prototype.setState=function(e,t){if(typeof e!=`object`&&typeof e!=`function`&&e!=null)throw Error(`takes an object of state variables to update or a function which returns an object of state variables.`);this.updater.enqueueSetState(this,e,t,`setState`)},v.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,`forceUpdate`)};function y(){}y.prototype=v.prototype;function b(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}var x=b.prototype=new y;x.constructor=b,g(x,v.prototype),x.isPureReactComponent=!0;var S=Array.isArray;function C(){}var w={H:null,A:null,T:null,S:null},T=Object.prototype.hasOwnProperty;function ee(e,n,r){var i=r.ref;return{$$typeof:t,type:e,key:n,ref:i===void 0?null:i,props:r}}function te(e,t){return ee(e.type,t,e.props)}function ne(e){return typeof e==`object`&&!!e&&e.$$typeof===t}function E(e){var t={"=":`=0`,":":`=2`};return`$`+e.replace(/[=:]/g,function(e){return t[e]})}var D=/\/+/g;function O(e,t){return typeof e==`object`&&e&&e.key!=null?E(``+e.key):t.toString(36)}function re(e){switch(e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason;default:switch(typeof e.status==`string`?e.then(C,C):(e.status=`pending`,e.then(function(t){e.status===`pending`&&(e.status=`fulfilled`,e.value=t)},function(t){e.status===`pending`&&(e.status=`rejected`,e.reason=t)})),e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason}}throw e}function ie(e,r,i,a,o){var s=typeof e;(s===`undefined`||s===`boolean`)&&(e=null);var c=!1;if(e===null)c=!0;else switch(s){case`bigint`:case`string`:case`number`:c=!0;break;case`object`:switch(e.$$typeof){case t:case n:c=!0;break;case d:return c=e._init,ie(c(e._payload),r,i,a,o)}}if(c)return o=o(e),c=a===``?`.`+O(e,0):a,S(o)?(i=``,c!=null&&(i=c.replace(D,`$&/`)+`/`),ie(o,r,i,``,function(e){return e})):o!=null&&(ne(o)&&(o=te(o,i+(o.key==null||e&&e.key===o.key?``:(``+o.key).replace(D,`$&/`)+`/`)+c)),r.push(o)),1;c=0;var l=a===``?`.`:a+`:`;if(S(e))for(var u=0;u{t.exports=u()})),f=o((e=>{function t(e,t){var n=e.length;e.push(t);a:for(;0>>1,a=e[r];if(0>>1;ri(c,n))li(u,c)?(e[r]=u,e[l]=n,r=l):(e[r]=c,e[s]=n,r=s);else if(li(u,n))e[r]=u,e[l]=n,r=l;else break a}}return t}function i(e,t){var n=e.sortIndex-t.sortIndex;return n===0?e.id-t.id:n}if(e.unstable_now=void 0,typeof performance==`object`&&typeof performance.now==`function`){var a=performance;e.unstable_now=function(){return a.now()}}else{var o=Date,s=o.now();e.unstable_now=function(){return o.now()-s}}var c=[],l=[],u=1,d=null,f=3,p=!1,m=!1,h=!1,g=!1,_=typeof setTimeout==`function`?setTimeout:null,v=typeof clearTimeout==`function`?clearTimeout:null,y=typeof setImmediate<`u`?setImmediate:null;function b(e){for(var i=n(l);i!==null;){if(i.callback===null)r(l);else if(i.startTime<=e)r(l),i.sortIndex=i.expirationTime,t(c,i);else break;i=n(l)}}function x(e){if(h=!1,b(e),!m)if(n(c)!==null)m=!0,S||(S=!0,ne());else{var t=n(l);t!==null&&O(x,t.startTime-e)}}var S=!1,C=-1,w=5,T=-1;function ee(){return g?!0:!(e.unstable_now()-Tt&&ee());){var o=d.callback;if(typeof o==`function`){d.callback=null,f=d.priorityLevel;var s=o(d.expirationTime<=t);if(t=e.unstable_now(),typeof s==`function`){d.callback=s,b(t),i=!0;break b}d===n(c)&&r(c),b(t)}else r(c);d=n(c)}if(d!==null)i=!0;else{var u=n(l);u!==null&&O(x,u.startTime-t),i=!1}}break a}finally{d=null,f=a,p=!1}i=void 0}}finally{i?ne():S=!1}}}var ne;if(typeof y==`function`)ne=function(){y(te)};else if(typeof MessageChannel<`u`){var E=new MessageChannel,D=E.port2;E.port1.onmessage=te,ne=function(){D.postMessage(null)}}else ne=function(){_(te,0)};function O(t,n){C=_(function(){t(e.unstable_now())},n)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(e){e.callback=null},e.unstable_forceFrameRate=function(e){0>e||125o?(r.sortIndex=a,t(l,r),n(c)===null&&r===n(l)&&(h?(v(C),C=-1):h=!0,O(x,a-o))):(r.sortIndex=s,t(c,r),m||p||(m=!0,S||(S=!0,ne()))),r},e.unstable_shouldYield=ee,e.unstable_wrapCallback=function(e){var t=f;return function(){var n=f;f=t;try{return e.apply(this,arguments)}finally{f=n}}}})),p=o(((e,t)=>{t.exports=f()})),m=o((e=>{var t=d();function n(e){var t=`https://react.dev/errors/`+e;if(1{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=m()})),g=o((e=>{var t=p(),n=d(),r=h();function i(e){var t=`https://react.dev/errors/`+e;if(1le||(e.current=ce[le],ce[le]=null,le--)}function N(e,t){le++,ce[le]=e.current,e.current=t}var ue=j(null),de=j(null),fe=j(null),pe=j(null);function me(e,t){switch(N(fe,t),N(de,e),N(ue,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?Vd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=Vd(t),e=Hd(t,e);else switch(e){case`svg`:e=1;break;case`math`:e=2;break;default:e=0}}M(ue),N(ue,e)}function he(){M(ue),M(de),M(fe)}function ge(e){e.memoizedState!==null&&N(pe,e);var t=ue.current,n=Hd(t,e.type);t!==n&&(N(de,e),N(ue,n))}function _e(e){de.current===e&&(M(ue),M(de)),pe.current===e&&(M(pe),Qf._currentValue=se)}var ve,ye;function be(e){if(ve===void 0)try{throw Error()}catch(e){var t=e.stack.trim().match(/\n( *(at )?)/);ve=t&&t[1]||``,ye=-1)`:-1i||c[r]!==l[i]){var u=` +`+c[r].replace(` at new `,` at `);return e.displayName&&u.includes(``)&&(u=u.replace(``,e.displayName)),u}while(1<=r&&0<=i);break}}}finally{xe=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:``)?be(n):``}function Ce(e,t){switch(e.tag){case 26:case 27:case 5:return be(e.type);case 16:return be(`Lazy`);case 13:return e.child!==t&&t!==null?be(`Suspense Fallback`):be(`Suspense`);case 19:return be(`SuspenseList`);case 0:case 15:return Se(e.type,!1);case 11:return Se(e.type.render,!1);case 1:return Se(e.type,!0);case 31:return be(`Activity`);default:return``}}function we(e){try{var t=``,n=null;do t+=Ce(e,n),n=e,e=e.return;while(e);return t}catch(e){return` +Error generating stack: `+e.message+` +`+e.stack}}var Te=Object.prototype.hasOwnProperty,Ee=t.unstable_scheduleCallback,De=t.unstable_cancelCallback,Oe=t.unstable_shouldYield,ke=t.unstable_requestPaint,Ae=t.unstable_now,je=t.unstable_getCurrentPriorityLevel,Me=t.unstable_ImmediatePriority,Ne=t.unstable_UserBlockingPriority,Pe=t.unstable_NormalPriority,Fe=t.unstable_LowPriority,Ie=t.unstable_IdlePriority,Le=t.log,Re=t.unstable_setDisableYieldValue,ze=null,Be=null;function Ve(e){if(typeof Le==`function`&&Re(e),Be&&typeof Be.setStrictMode==`function`)try{Be.setStrictMode(ze,e)}catch{}}var He=Math.clz32?Math.clz32:Ge,Ue=Math.log,We=Math.LN2;function Ge(e){return e>>>=0,e===0?32:31-(Ue(e)/We|0)|0}var Ke=256,qe=262144,Je=4194304;function Ye(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function Xe(e,t,n){var r=e.pendingLanes;if(r===0)return 0;var i=0,a=e.suspendedLanes,o=e.pingedLanes;e=e.warmLanes;var s=r&134217727;return s===0?(s=r&~a,s===0?o===0?n||(n=r&~e,n!==0&&(i=Ye(n))):i=Ye(o):i=Ye(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=Ye(n))):i=Ye(o)):i=Ye(r)),i===0?0:t!==0&&t!==i&&(t&a)===0&&(a=i&-i,n=t&-t,a>=n||a===32&&n&4194048)?t:i}function Ze(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function Qe(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function $e(){var e=Je;return Je<<=1,!(Je&62914560)&&(Je=4194304),e}function et(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function tt(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function nt(e,t,n,r,i,a){var o=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var s=e.entanglements,c=e.expirationTimes,l=e.hiddenUpdates;for(n=o&~n;0`u`||window.document===void 0||window.document.createElement===void 0),mn=!1;if(pn)try{var hn={};Object.defineProperty(hn,"passive",{get:function(){mn=!0}}),window.addEventListener(`test`,hn,hn),window.removeEventListener(`test`,hn,hn)}catch{mn=!1}var gn=null,_n=null,vn=null;function yn(){if(vn)return vn;var e,t=_n,n=t.length,r,i=`value`in gn?gn.value:gn.textContent,a=i.length;for(e=0;e=Zn),er=` `,tr=!1;function nr(e,t){switch(e){case`keyup`:return Yn.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function rr(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var ir=!1;function ar(e,t){switch(e){case`compositionend`:return rr(t);case`keypress`:return t.which===32?(tr=!0,er):null;case`textInput`:return e=t.data,e===er&&tr?null:e;default:return null}}function or(e,t){if(ir)return e===`compositionend`||!Xn&&nr(e,t)?(e=yn(),vn=_n=gn=null,ir=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=Or(n)}}function Ar(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Ar(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function jr(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=Bt(e.document);t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=Bt(e.document)}return t}function Mr(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t===`input`&&(e.type===`text`||e.type===`search`||e.type===`tel`||e.type===`url`||e.type===`password`)||t===`textarea`||e.contentEditable===`true`)}var Nr=pn&&`documentMode`in document&&11>=document.documentMode,Pr=null,Fr=null,Ir=null,Lr=!1;function Rr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Lr||Pr==null||Pr!==Bt(r)||(r=Pr,`selectionStart`in r&&Mr(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Ir&&Dr(Ir,r)||(Ir=r,r=Ed(Fr,`onSelect`),0>=o,i-=o,ki=1<<32-He(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(i,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(i,d),a=o(_,a,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(i,d),L&&ji(i,h),l;if(d===null){for(;hg?(_=h,h=null):_=h.sibling;var y=p(a,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(a,h),s=o(y,s,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(a,h),L&&ji(a,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(a,v.value,l),v!==null&&(s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return L&&ji(a,g),u}for(h=r(h);!v.done;g++,v=c.next())v=m(h,a,g,v.value,l),v!==null&&(e&&v.alternate!==null&&h.delete(v.key===null?g:v.key),s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(a,e)}),L&&ji(a,g),u}function b(e,r,o,c){if(typeof o==`object`&&o&&o.type===y&&o.key===null&&(o=o.props.children),typeof o==`object`&&o){switch(o.$$typeof){case _:a:{for(var l=o.key;r!==null;){if(r.key===l){if(l=o.type,l===y){if(r.tag===7){n(e,r.sibling),c=a(r,o.props.children),c.return=e,e=c;break a}}else if(r.elementType===l||typeof l==`object`&&l&&l.$$typeof===ne&&Aa(l)===r.type){n(e,r.sibling),c=a(r,o.props),La(c,o),c.return=e,e=c;break a}n(e,r);break}else t(e,r);r=r.sibling}o.type===y?(c=gi(o.props.children,e.mode,c,o.key),c.return=e,e=c):(c=hi(o.type,o.key,o.props,null,e.mode,c),La(c,o),c.return=e,e=c)}return s(e);case v:a:{for(l=o.key;r!==null;){if(r.key===l)if(r.tag===4&&r.stateNode.containerInfo===o.containerInfo&&r.stateNode.implementation===o.implementation){n(e,r.sibling),c=a(r,o.children||[]),c.return=e,e=c;break a}else{n(e,r);break}else t(e,r);r=r.sibling}c=yi(o,e.mode,c),c.return=e,e=c}return s(e);case ne:return o=Aa(o),b(e,r,o,c)}if(oe(o))return h(e,r,o,c);if(re(o)){if(l=re(o),typeof l!=`function`)throw Error(i(150));return o=l.call(o),g(e,r,o,c)}if(typeof o.then==`function`)return b(e,r,Ia(o),c);if(o.$$typeof===C)return b(e,r,ia(e,o),c);Ra(e,o)}return typeof o==`string`&&o!==``||typeof o==`number`||typeof o==`bigint`?(o=``+o,r!==null&&r.tag===6?(n(e,r.sibling),c=a(r,o),c.return=e,e=c):(n(e,r),c=_i(o,e.mode,c),c.return=e,e=c),s(e)):n(e,r)}return function(e,t,n,r){try{Fa=0;var i=b(e,t,n,r);return Pa=null,i}catch(t){if(t===wa||t===Ea)throw t;var a=fi(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var Ba=za(!0),Va=za(!1),Ha=!1;function Ua(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Wa(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function Ga(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Ka(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,q&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,t=li(e),ci(e,null,n),t}return ai(e,r,t,n),li(e)}function qa(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194048)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,it(e,n)}}function Ja(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,callbacks:r.callbacks},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}var Ya=!1;function Xa(){if(Ya){var e=ha;if(e!==null)throw e}}function Za(e,t,n,r){Ya=!1;var i=e.updateQueue;Ha=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var f=s.lane&-536870913,p=f!==s.lane;if(p?(Y&f)===f:(r&f)===f){f!==0&&f===ma&&(Ya=!0),u!==null&&(u=u.next={lane:0,tag:s.tag,payload:s.payload,callback:null,next:null});a:{var h=e,g=s;f=t;var _=n;switch(g.tag){case 1:if(h=g.payload,typeof h==`function`){d=h.call(_,d,f);break a}d=h;break a;case 3:h.flags=h.flags&-65537|128;case 0:if(h=g.payload,f=typeof h==`function`?h.call(_,d,f):h,f==null)break a;d=m({},d,f);break a;case 2:Ha=!0}}f=s.callback,f!==null&&(e.flags|=64,p&&(e.flags|=8192),p=i.callbacks,p===null?i.callbacks=[f]:p.push(f))}else p={lane:f,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=p,c=d):u=u.next=p,o|=f;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;p=s,s=p.next,p.next=null,i.lastBaseUpdate=p,i.shared.pending=null}}while(1);u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,a===null&&(i.shared.lanes=0),Gl|=o,e.lanes=o,e.memoizedState=d}}function Qa(e,t){if(typeof e!=`function`)throw Error(i(191,e));e.call(t)}function $a(e,t){var n=e.callbacks;if(n!==null)for(e.callbacks=null,e=0;ea?a:8;var o=k.T,s={};k.T=s,Is(e,!1,t,n);try{var c=i(),l=k.S;l!==null&&l(s,c),typeof c==`object`&&c&&typeof c.then==`function`?V(e,t,va(c,r),pu(e)):V(e,t,r,pu(e))}catch(n){V(e,t,{then:function(){},status:`rejected`,reason:n},pu())}finally{A.p=a,o!==null&&s.types!==null&&(o.types=s.types),k.T=o}}function Es(){}function Ds(e,t,n,r){if(e.tag!==5)throw Error(i(476));var a=Os(e).queue;Ts(e,a,t,se,n===null?Es:function(){return ks(e),n(r)})}function Os(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:se,baseState:se,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Ro,lastRenderedState:se},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Ro,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function ks(e){var t=Os(e);t.next===null&&(t=e.alternate.memoizedState),V(e,t.next.queue,{},pu())}function As(){return ra(Qf)}function js(){return No().memoizedState}function Ms(){return No().memoizedState}function Ns(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=pu();e=Ga(n);var r=Ka(t,e,n);r!==null&&(hu(r,t,n),qa(r,t,n)),t={cache:ua()},e.payload=t;return}t=t.return}}function Ps(e,t,n){var r=pu();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},Ls(e)?Rs(t,n):(n=oi(e,t,n,r),n!==null&&(hu(n,e,r),zs(n,t,r)))}function Fs(e,t,n){V(e,t,n,pu())}function V(e,t,n,r){var i={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(Ls(e))Rs(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,Er(s,o))return ai(e,t,i,0),Rl===null&&ii(),!1}catch{}if(n=oi(e,t,i,r),n!==null)return hu(n,e,r),zs(n,t,r),!0}return!1}function Is(e,t,n,r){if(r={lane:2,revertLane:dd(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},Ls(e)){if(t)throw Error(i(479))}else t=oi(e,n,r,2),t!==null&&hu(t,e,2)}function Ls(e){var t=e.alternate;return e===R||t!==null&&t===R}function Rs(e,t){vo=_o=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function zs(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,it(e,n)}}var Bs={readContext:ra,use:Io,useCallback:B,useContext:B,useEffect:B,useImperativeHandle:B,useLayoutEffect:B,useInsertionEffect:B,useMemo:B,useReducer:B,useRef:B,useState:B,useDebugValue:B,useDeferredValue:B,useTransition:B,useSyncExternalStore:B,useId:B,useHostTransitionStatus:B,useFormState:B,useActionState:B,useOptimistic:B,useMemoCache:B,useCacheRefresh:B};Bs.useEffectEvent=B;var Vs={readContext:ra,use:Io,useCallback:function(e,t){return Mo().memoizedState=[e,t===void 0?null:t],e},useContext:ra,useEffect:fs,useImperativeHandle:function(e,t,n){n=n==null?null:n.concat([e]),us(4194308,4,vs.bind(null,t,e),n)},useLayoutEffect:function(e,t){return us(4194308,4,e,t)},useInsertionEffect:function(e,t){us(4,2,e,t)},useMemo:function(e,t){var n=Mo();t=t===void 0?null:t;var r=e();if(yo){Ve(!0);try{e()}finally{Ve(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=Mo();if(n!==void 0){var i=n(t);if(yo){Ve(!0);try{n(t)}finally{Ve(!1)}}}else i=t;return r.memoizedState=r.baseState=i,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:i},r.queue=e,e=e.dispatch=Ps.bind(null,R,e),[r.memoizedState,e]},useRef:function(e){var t=Mo();return e={current:e},t.memoizedState=e},useState:function(e){e=Jo(e);var t=e.queue,n=Fs.bind(null,R,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:bs,useDeferredValue:function(e,t){return Cs(Mo(),e,t)},useTransition:function(){var e=Jo(!1);return e=Ts.bind(null,R,e.queue,!0,!1),Mo().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=R,a=Mo();if(L){if(n===void 0)throw Error(i(407));n=n()}else{if(n=t(),Rl===null)throw Error(i(349));Y&127||Uo(r,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,fs(Go.bind(null,r,o,e),[e]),r.flags|=2048,cs(9,{destroy:void 0},Wo.bind(null,r,o,n,t),null),n},useId:function(){var e=Mo(),t=Rl.identifierPrefix;if(L){var n=Ai,r=ki;n=(r&~(1<<32-He(r)-1)).toString(32)+n,t=`_`+t+`R_`+n,n=bo++,0<\/script>`,o=o.removeChild(o.firstChild);break;case`select`:o=typeof r.is==`string`?s.createElement(`select`,{is:r.is}):s.createElement(`select`),r.multiple?o.multiple=!0:r.size&&(o.size=r.size);break;default:o=typeof r.is==`string`?s.createElement(a,{is:r.is}):s.createElement(a)}}o[P]=t,o[dt]=r;a:for(s=t.child;s!==null;){if(s.tag===5||s.tag===6)o.appendChild(s.stateNode);else if(s.tag!==4&&s.tag!==27&&s.child!==null){s.child.return=s,s=s.child;continue}if(s===t)break a;for(;s.sibling===null;){if(s.return===null||s.return===t)break a;s=s.return}s.sibling.return=s.return,s=s.sibling}t.stateNode=o;a:switch(Pd(o,a,r),a){case`button`:case`input`:case`select`:case`textarea`:r=!!r.autoFocus;break a;case`img`:r=!0;break a;default:r=!1}r&&Nc(t)}}return Rc(t),Pc(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&Nc(t);else{if(typeof r!=`string`&&t.stateNode===null)throw Error(i(166));if(e=fe.current,Ui(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,a=I,a!==null)switch(a.tag){case 27:case 5:r=a.memoizedProps}e[P]=t,e=!!(e.nodeValue===n||r!==null&&!0===r.suppressHydrationWarning||Md(e.nodeValue,n)),e||Bi(t,!0)}else e=Bd(e).createTextNode(r),e[P]=t,t.stateNode=e}return Rc(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=Ui(t),n!==null){if(e===null){if(!r)throw Error(i(318));if(e=t.memoizedState,e=e===null?null:e.dehydrated,!e)throw Error(i(557));e[P]=t}else Wi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Rc(t),e=!1}else n=Gi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?(fo(t),t):(fo(t),null);if(t.flags&128)throw Error(i(558))}return Rc(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(a=Ui(t),r!==null&&r.dehydrated!==null){if(e===null){if(!a)throw Error(i(318));if(a=t.memoizedState,a=a===null?null:a.dehydrated,!a)throw Error(i(317));a[P]=t}else Wi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Rc(t),a=!1}else a=Gi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=a),a=!0;if(!a)return t.flags&256?(fo(t),t):(fo(t),null)}return fo(t),t.flags&128?(t.lanes=n,t):(n=r!==null,e=e!==null&&e.memoizedState!==null,n&&(r=t.child,a=null,r.alternate!==null&&r.alternate.memoizedState!==null&&r.alternate.memoizedState.cachePool!==null&&(a=r.alternate.memoizedState.cachePool.pool),o=null,r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(o=r.memoizedState.cachePool.pool),o!==a&&(r.flags|=2048)),n!==e&&n&&(t.child.flags|=8192),Ic(t,t.updateQueue),Rc(t),null);case 4:return he(),e===null&&Sd(t.stateNode.containerInfo),Rc(t),null;case 10:return Zi(t.type),Rc(t),null;case 19:if(M(po),r=t.memoizedState,r===null)return Rc(t),null;if(a=(t.flags&128)!=0,o=r.rendering,o===null)if(a)Lc(r,!1);else{if(Wl!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(o=mo(e),o!==null){for(t.flags|=128,Lc(r,!1),e=o.updateQueue,t.updateQueue=e,Ic(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)F(n,e),n=n.sibling;return N(po,po.current&1|2),L&&ji(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&Ae()>tu&&(t.flags|=128,a=!0,Lc(r,!1),t.lanes=4194304)}else{if(!a)if(e=mo(o),e!==null){if(t.flags|=128,a=!0,e=e.updateQueue,t.updateQueue=e,Ic(t,e),Lc(r,!0),r.tail===null&&r.tailMode===`hidden`&&!o.alternate&&!L)return Rc(t),null}else 2*Ae()-r.renderingStartTime>tu&&n!==536870912&&(t.flags|=128,a=!0,Lc(r,!1),t.lanes=4194304);r.isBackwards?(o.sibling=t.child,t.child=o):(e=r.last,e===null?t.child=o:e.sibling=o,r.last=o)}return r.tail===null?(Rc(t),null):(e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=Ae(),e.sibling=null,n=po.current,N(po,a?n&1|2:n&1),L&&ji(t,r.treeForkCount),e);case 22:case 23:return fo(t),io(),r=t.memoizedState!==null,e===null?r&&(t.flags|=8192):e.memoizedState!==null!==r&&(t.flags|=8192),r?n&536870912&&!(t.flags&128)&&(Rc(t),t.subtreeFlags&6&&(t.flags|=8192)):Rc(t),n=t.updateQueue,n!==null&&Ic(t,n.retryQueue),n=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(n=e.memoizedState.cachePool.pool),r=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(r=t.memoizedState.cachePool.pool),r!==n&&(t.flags|=2048),e!==null&&M(ba),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),Zi(la),Rc(t),null;case 25:return null;case 30:return null}throw Error(i(156,t.tag))}function zc(e,t){switch(Pi(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Zi(la),he(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return _e(t),null;case 31:if(t.memoizedState!==null){if(fo(t),t.alternate===null)throw Error(i(340));Wi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(fo(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(i(340));Wi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return M(po),null;case 4:return he(),null;case 10:return Zi(t.type),null;case 22:case 23:return fo(t),io(),e!==null&&M(ba),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return Zi(la),null;case 25:return null;default:return null}}function Bc(e,t){switch(Pi(t),t.tag){case 3:Zi(la),he();break;case 26:case 27:case 5:_e(t);break;case 4:he();break;case 31:t.memoizedState!==null&&fo(t);break;case 13:fo(t);break;case 19:M(po);break;case 10:Zi(t.type);break;case 22:case 23:fo(t),io(),e!==null&&M(ba);break;case 24:Zi(la)}}function Vc(e,t){try{var n=t.updateQueue,r=n===null?null:n.lastEffect;if(r!==null){var i=r.next;n=i;do{if((n.tag&e)===e){r=void 0;var a=n.create,o=n.inst;r=a(),o.destroy=r}n=n.next}while(n!==i)}}catch(e){Z(t,t.return,e)}}function Hc(e,t,n){try{var r=t.updateQueue,i=r===null?null:r.lastEffect;if(i!==null){var a=i.next;r=a;do{if((r.tag&e)===e){var o=r.inst,s=o.destroy;if(s!==void 0){o.destroy=void 0,i=t;var c=n,l=s;try{l()}catch(e){Z(i,c,e)}}}r=r.next}while(r!==a)}}catch(e){Z(t,t.return,e)}}function Uc(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{$a(t,n)}catch(t){Z(e,e.return,t)}}}function Wc(e,t,n){n.props=qs(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){Z(e,t,n)}}function Gc(e,t){try{var n=e.ref;if(n!==null){switch(e.tag){case 26:case 27:case 5:var r=e.stateNode;break;case 30:r=e.stateNode;break;default:r=e.stateNode}typeof n==`function`?e.refCleanup=n(r):n.current=r}}catch(n){Z(e,t,n)}}function Kc(e,t){var n=e.ref,r=e.refCleanup;if(n!==null)if(typeof r==`function`)try{r()}catch(n){Z(e,t,n)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof n==`function`)try{n(null)}catch(n){Z(e,t,n)}else n.current=null}function qc(e){var t=e.type,n=e.memoizedProps,r=e.stateNode;try{a:switch(t){case`button`:case`input`:case`select`:case`textarea`:n.autoFocus&&r.focus();break a;case`img`:n.src?r.src=n.src:n.srcSet&&(r.srcset=n.srcSet)}}catch(t){Z(e,e.return,t)}}function Jc(e,t,n){try{var r=e.stateNode;Fd(r,e.type,n,t),r[dt]=t}catch(t){Z(e,e.return,t)}}function Yc(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&Zd(e.type)||e.tag===4}function Xc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||Yc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&Zd(e.type)||e.flags&2||e.child===null||e.tag===4)continue a;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Zc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?(n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n).insertBefore(e,t):(t=n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n,t.appendChild(e),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=rn));else if(r!==4&&(r===27&&Zd(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for(Zc(e,t,n),e=e.sibling;e!==null;)Zc(e,t,n),e=e.sibling}function Qc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(r===27&&Zd(e.type)&&(n=e.stateNode),e=e.child,e!==null))for(Qc(e,t,n),e=e.sibling;e!==null;)Qc(e,t,n),e=e.sibling}function $c(e){var t=e.stateNode,n=e.memoizedProps;try{for(var r=e.type,i=t.attributes;i.length;)t.removeAttributeNode(i[0]);Pd(t,r,n),t[P]=e,t[dt]=n}catch(t){Z(e,e.return,t)}}var el=!1,tl=!1,nl=!1,rl=typeof WeakSet==`function`?WeakSet:Set,il=null;function al(e,t){if(e=e.containerInfo,Rd=sp,e=jr(e),Mr(e)){if(`selectionStart`in e)var n={start:e.selectionStart,end:e.selectionEnd};else a:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var a=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break a}var s=0,c=-1,l=-1,u=0,d=0,f=e,p=null;b:for(;;){for(var m;f!==n||a!==0&&f.nodeType!==3||(c=s+a),f!==o||r!==0&&f.nodeType!==3||(l=s+r),f.nodeType===3&&(s+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break b;if(p===n&&++u===a&&(c=s),p===o&&++d===r&&(l=s),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}n=c===-1||l===-1?null:{start:c,end:l}}else n=null}n||={start:0,end:0}}else n=null;for(zd={focusedElem:e,selectionRange:n},sp=!1,il=t;il!==null;)if(t=il,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,il=e;else for(;il!==null;){switch(t=il,o=t.alternate,e=t.flags,t.tag){case 0:if(e&4&&(e=t.updateQueue,e=e===null?null:e.events,e!==null))for(n=0;n title`))),Pd(o,r,n),o[P]=e,Ct(o),r=o;break a;case`link`:var s=Vf(`link`,`href`,a).get(r+(n.href||``));if(s){for(var c=0;cg&&(o=g,g=h,h=o);var _=kr(s,h),v=kr(s,g);if(_&&v&&(p.rangeCount!==1||p.anchorNode!==_.node||p.anchorOffset!==_.offset||p.focusNode!==v.node||p.focusOffset!==v.offset)){var y=d.createRange();y.setStart(_.node,_.offset),p.removeAllRanges(),h>g?(p.addRange(y),p.extend(v.node,v.offset)):(y.setEnd(v.node,v.offset),p.addRange(y))}}}}for(d=[],p=s;p=p.parentNode;)p.nodeType===1&&d.push({element:p,left:p.scrollLeft,top:p.scrollTop});for(typeof s.focus==`function`&&s.focus(),s=0;sn?32:n,k.T=null,n=lu,lu=null;var o=au,s=su;if(iu=0,ou=au=null,su=0,q&6)throw Error(i(331));var c=q;if(q|=4,Nl(o.current),Tl(o,o.current,s,n),q=c,id(0,!1),Be&&typeof Be.onPostCommitFiberRoot==`function`)try{Be.onPostCommitFiberRoot(ze,o)}catch{}return!0}finally{A.p=a,k.T=r,Vu(e,t)}}function Wu(e,t,n){t=xi(n,t),t=$s(e.stateNode,t,2),e=Ka(e,t,2),e!==null&&(tt(e,2),rd(e))}function Z(e,t,n){if(e.tag===3)Wu(e,e,n);else for(;t!==null;){if(t.tag===3){Wu(t,e,n);break}else if(t.tag===1){var r=t.stateNode;if(typeof t.type.getDerivedStateFromError==`function`||typeof r.componentDidCatch==`function`&&(ru===null||!ru.has(r))){e=xi(n,e),n=U(2),r=Ka(t,n,2),r!==null&&(ec(n,r,t,e),tt(r,2),rd(r));break}}t=t.return}}function Gu(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new Ll;var i=new Set;r.set(t,i)}else i=r.get(t),i===void 0&&(i=new Set,r.set(t,i));i.has(n)||(Hl=!0,i.add(n),e=Ku.bind(null,e,t,n),t.then(e,e))}function Ku(e,t,n){var r=e.pingCache;r!==null&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,Rl===e&&(Y&n)===n&&(Wl===4||Wl===3&&(Y&62914560)===Y&&300>Ae()-$l?!(q&2)&&Su(e,0):ql|=n,Yl===Y&&(Yl=0)),rd(e)}function qu(e,t){t===0&&(t=$e()),e=si(e,t),e!==null&&(tt(e,t),rd(e))}function Ju(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),qu(e,n)}function Yu(e,t){var n=0;switch(e.tag){case 31:case 13:var r=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:r=e.stateNode;break;case 22:r=e.stateNode._retryCache;break;default:throw Error(i(314))}r!==null&&r.delete(t),qu(e,n)}function Xu(e,t){return Ee(e,t)}var Zu=null,Qu=null,$u=!1,ed=!1,td=!1,nd=0;function rd(e){e!==Qu&&e.next===null&&(Qu===null?Zu=Qu=e:Qu=Qu.next=e),ed=!0,$u||($u=!0,ud())}function id(e,t){if(!td&&ed){td=!0;do for(var n=!1,r=Zu;r!==null;){if(!t)if(e!==0){var i=r.pendingLanes;if(i===0)var a=0;else{var o=r.suspendedLanes,s=r.pingedLanes;a=(1<<31-He(42|e)+1)-1,a&=i&~(o&~s),a=a&201326741?a&201326741|1:a?a|2:0}a!==0&&(n=!0,ld(r,a))}else a=Y,a=Xe(r,r===Rl?a:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(a&3)||Ze(r,a)||(n=!0,ld(r,a));r=r.next}while(n);td=!1}}function ad(){od()}function od(){ed=$u=!1;var e=0;nd!==0&&Gd()&&(e=nd);for(var t=Ae(),n=null,r=Zu;r!==null;){var i=r.next,a=sd(r,t);a===0?(r.next=null,n===null?Zu=i:n.next=i,i===null&&(Qu=n)):(n=r,(e!==0||a&3)&&(ed=!0)),r=i}iu!==0&&iu!==5||id(e,!1),nd!==0&&(nd=0)}function sd(e,t){for(var n=e.suspendedLanes,r=e.pingedLanes,i=e.expirationTimes,a=e.pendingLanes&-62914561;0s)break;var u=c.transferSize,d=c.initiatorType;u&&Id(d)&&(c=c.responseEnd,o+=u*(c`u`?null:document;function xf(e,t,n){var r=bf;if(r&&typeof t==`string`&&t){var i=Ht(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),hf.has(i)||(hf.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),Pd(t,`link`,e),Ct(t),r.head.appendChild(t)))}}function Sf(e){_f.D(e),xf(`dns-prefetch`,e,null)}function Cf(e,t){_f.C(e,t),xf(`preconnect`,e,t)}function wf(e,t,n){_f.L(e,t,n);var r=bf;if(r&&e&&t){var i=`link[rel="preload"][as="`+Ht(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+Ht(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+Ht(n.imageSizes)+`"]`)):i+=`[href="`+Ht(e)+`"]`;var a=i;switch(t){case`style`:a=Af(e);break;case`script`:a=Pf(e)}mf.has(a)||(e=m({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),mf.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(jf(a))||t===`script`&&r.querySelector(Ff(a))||(t=r.createElement(`link`),Pd(t,`link`,e),Ct(t),r.head.appendChild(t)))}}function Tf(e,t){_f.m(e,t);var n=bf;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+Ht(r)+`"][href="`+Ht(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=Pf(e)}if(!mf.has(a)&&(e=m({rel:`modulepreload`,href:e},t),mf.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(Ff(a)))return}r=n.createElement(`link`),Pd(r,`link`,e),Ct(r),n.head.appendChild(r)}}}function Ef(e,t,n){_f.S(e,t,n);var r=bf;if(r&&e){var i=St(r).hoistableStyles,a=Af(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(jf(a)))s.loading=5;else{e=m({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=mf.get(a))&&Rf(e,n);var c=o=r.createElement(`link`);Ct(c),Pd(c,`link`,e),c._p=new Promise(function(e,t){c.onload=e,c.onerror=t}),c.addEventListener(`load`,function(){s.loading|=1}),c.addEventListener(`error`,function(){s.loading|=2}),s.loading|=4,Lf(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function Df(e,t){_f.X(e,t);var n=bf;if(n&&e){var r=St(n).hoistableScripts,i=Pf(e),a=r.get(i);a||(a=n.querySelector(Ff(i)),a||(e=m({src:e,async:!0},t),(t=mf.get(i))&&zf(e,t),a=n.createElement(`script`),Ct(a),Pd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Of(e,t){_f.M(e,t);var n=bf;if(n&&e){var r=St(n).hoistableScripts,i=Pf(e),a=r.get(i);a||(a=n.querySelector(Ff(i)),a||(e=m({src:e,async:!0,type:`module`},t),(t=mf.get(i))&&zf(e,t),a=n.createElement(`script`),Ct(a),Pd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function kf(e,t,n,r){var a=(a=fe.current)?gf(a):null;if(!a)throw Error(i(446));switch(e){case`meta`:case`title`:return null;case`style`:return typeof n.precedence==`string`&&typeof n.href==`string`?(t=Af(n.href),n=St(a).hoistableStyles,r=n.get(t),r||(r={type:`style`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};case`link`:if(n.rel===`stylesheet`&&typeof n.href==`string`&&typeof n.precedence==`string`){e=Af(n.href);var o=St(a).hoistableStyles,s=o.get(e);if(s||(a=a.ownerDocument||a,s={type:`stylesheet`,instance:null,count:0,state:{loading:0,preload:null}},o.set(e,s),(o=a.querySelector(jf(e)))&&!o._p&&(s.instance=o,s.state.loading=5),mf.has(e)||(n={rel:`preload`,as:`style`,href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},mf.set(e,n),o||Nf(a,e,n,s.state))),t&&r===null)throw Error(i(528,``));return s}if(t&&r!==null)throw Error(i(529,``));return null;case`script`:return t=n.async,n=n.src,typeof n==`string`&&t&&typeof t!=`function`&&typeof t!=`symbol`?(t=Pf(n),n=St(a).hoistableScripts,r=n.get(t),r||(r={type:`script`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};default:throw Error(i(444,e))}}function Af(e){return`href="`+Ht(e)+`"`}function jf(e){return`link[rel="stylesheet"][`+e+`]`}function Mf(e){return m({},e,{"data-precedence":e.precedence,precedence:null})}function Nf(e,t,n,r){e.querySelector(`link[rel="preload"][as="style"][`+t+`]`)?r.loading=1:(t=e.createElement(`link`),r.preload=t,t.addEventListener(`load`,function(){return r.loading|=1}),t.addEventListener(`error`,function(){return r.loading|=2}),Pd(t,`link`,n),Ct(t),e.head.appendChild(t))}function Pf(e){return`[src="`+Ht(e)+`"]`}function Ff(e){return`script[async]`+e}function If(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+Ht(n.href)+`"]`);if(r)return t.instance=r,Ct(r),r;var a=m({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),Ct(r),Pd(r,`style`,a),Lf(r,n.precedence,e),t.instance=r;case`stylesheet`:a=Af(n.href);var o=e.querySelector(jf(a));if(o)return t.state.loading|=4,t.instance=o,Ct(o),o;r=Mf(n),(a=mf.get(a))&&Rf(r,a),o=(e.ownerDocument||e).createElement(`link`),Ct(o);var s=o;return s._p=new Promise(function(e,t){s.onload=e,s.onerror=t}),Pd(o,`link`,r),t.state.loading|=4,Lf(o,n.precedence,e),t.instance=o;case`script`:return o=Pf(n.src),(a=e.querySelector(Ff(o)))?(t.instance=a,Ct(a),a):(r=n,(a=mf.get(o))&&(r=m({},n),zf(r,a)),e=e.ownerDocument||e,a=e.createElement(`script`),Ct(a),Pd(a,`link`,r),e.head.appendChild(a),t.instance=a);case`void`:return null;default:throw Error(i(443,t.type))}else t.type===`stylesheet`&&!(t.state.loading&4)&&(r=t.instance,t.state.loading|=4,Lf(r,n.precedence,e));return t.instance}function Lf(e,t,n){for(var r=n.querySelectorAll(`link[rel="stylesheet"][data-precedence],style[data-precedence]`),i=r.length?r[r.length-1]:null,a=i,o=0;o title`):null)}function Uf(e,t,n){if(n===1||t.itemProp!=null)return!1;switch(e){case`meta`:case`title`:return!0;case`style`:if(typeof t.precedence!=`string`||typeof t.href!=`string`||t.href===``)break;return!0;case`link`:if(typeof t.rel!=`string`||typeof t.href!=`string`||t.href===``||t.onLoad||t.onError)break;switch(t.rel){case`stylesheet`:return e=t.disabled,typeof t.precedence==`string`&&e==null;default:return!0}case`script`:if(t.async&&typeof t.async!=`function`&&typeof t.async!=`symbol`&&!t.onLoad&&!t.onError&&t.src&&typeof t.src==`string`)return!0}return!1}function Wf(e){return!(e.type===`stylesheet`&&!(e.state.loading&3))}function Gf(e,t,n,r){if(n.type===`stylesheet`&&(typeof r.media!=`string`||!1!==matchMedia(r.media).matches)&&!(n.state.loading&4)){if(n.instance===null){var i=Af(r.href),a=t.querySelector(jf(i));if(a){t=a._p,typeof t==`object`&&t&&typeof t.then==`function`&&(e.count++,e=Jf.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=a,Ct(a);return}a=t.ownerDocument||t,r=Mf(r),(i=mf.get(i))&&Rf(r,i),a=a.createElement(`link`),Ct(a);var o=a;o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),Pd(a,`link`,r),n.instance=a}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(n,t),(t=n.state.preload)&&!(n.state.loading&3)&&(e.count++,n=Jf.bind(e),t.addEventListener(`load`,n),t.addEventListener(`error`,n))}}var Kf=0;function qf(e,t){return e.stylesheets&&e.count===0&&Xf(e,e.stylesheets),0Kf?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(i)}}:null}function Jf(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Xf(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var Yf=null;function Xf(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,Yf=new Map,t.forEach(Zf,e),Yf=null,Jf.call(e))}function Zf(e,t){if(!(t.state.loading&4)){var n=Yf.get(e);if(n)var r=n.get(null);else{n=new Map,Yf.set(e,n);for(var i=e.querySelectorAll(`link[data-precedence],style[data-precedence]`),a=0;a{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=g()})),v=`modulepreload`,y=function(e){return`/admin/`+e},b={},x=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?import.meta.resolve(e):new URL(e,new URL(`../../../src/node/plugins/importAnalysisBuild.ts`,import.meta.url)).href}r=o(t.map(t=>{if(t=y(t,n),t=s(t),t in b)return;b[t]=!0;let r=t.endsWith(`.css`);for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}let i=document.createElement(`link`);if(i.rel=r?`stylesheet`:v,r||(i.as=`script`),i.crossOrigin=``,i.href=t,a&&i.setAttribute(`nonce`,a),document.head.appendChild(i),r)return new Promise((e,n)=>{i.addEventListener(`load`,e),i.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},S=l(d(),1),C=/^(?:[a-z][a-z0-9+.-]*:|[\\/]{2})/i,w=/^[\\/]{2}/;function T(e,t){return t+e.replace(/\\/g,`/`)}var ee=`popstate`;function te(e){return typeof e==`object`&&!!e&&`pathname`in e&&`search`in e&&`hash`in e&&`state`in e&&`key`in e}function ne(e={}){function t(e,t){let n=t.state?.masked,{pathname:r,search:i,hash:a}=n||e.location;return ie(``,{pathname:r,search:i,hash:a},t.state&&t.state.usr||null,t.state&&t.state.key||`default`,n?{pathname:e.location.pathname,search:e.location.search,hash:e.location.hash}:void 0)}function n(e,t){return typeof t==`string`?t:ae(t)}return k(t,n,null,e)}function E(e,t){if(e===!1||e==null)throw Error(t)}function D(e,t){if(!e){typeof console<`u`&&console.warn(t);try{throw Error(t)}catch{}}}function O(){return Math.random().toString(36).substring(2,10)}function re(e,t){return{usr:e.state,key:e.key,idx:t,masked:e.mask?{pathname:e.pathname,search:e.search,hash:e.hash}:void 0}}function ie(e,t,n=null,r,i){return{pathname:typeof e==`string`?e:e.pathname,search:``,hash:``,...typeof t==`string`?oe(t):t,state:n,key:t&&t.key||r||O(),mask:i}}function ae({pathname:e=`/`,search:t=``,hash:n=``}){return t&&t!==`?`&&(e+=t.charAt(0)===`?`?t:`?`+t),n&&n!==`#`&&(e+=n.charAt(0)===`#`?n:`#`+n),e}function oe(e){let t={};if(e){let n=e.indexOf(`#`);n>=0&&(t.hash=e.substring(n),e=e.substring(0,n));let r=e.indexOf(`?`);r>=0&&(t.search=e.substring(r),e=e.substring(0,r)),e&&(t.pathname=e)}return t}function k(e,t,n,r={}){let{window:i=document.defaultView,v5Compat:a=!1}=r,o=i.history,s=`POP`,c=null,l=u();l??(l=0,o.replaceState({...o.state,idx:l},``));function u(){return(o.state||{idx:null}).idx}function d(){s=`POP`;let e=u(),t=e==null?null:e-l;l=e,c&&c({action:s,location:h.location,delta:t})}function f(e,t){s=`PUSH`;let r=te(e)?e:ie(h.location,e,t);n&&n(r,e),l=u()+1;let d=re(r,l),f=h.createHref(r.mask||r);try{o.pushState(d,``,f)}catch(e){if(e instanceof DOMException&&e.name===`DataCloneError`)throw e;i.location.assign(f)}a&&c&&c({action:s,location:h.location,delta:1})}function p(e,t){s=`REPLACE`;let r=te(e)?e:ie(h.location,e,t);n&&n(r,e),l=u();let i=re(r,l),d=h.createHref(r.mask||r);o.replaceState(i,``,d),a&&c&&c({action:s,location:h.location,delta:0})}function m(e){return A(i,e)}let h={get action(){return s},get location(){return e(i,o)},listen(e){if(c)throw Error(`A history only accepts one active listener`);return i.addEventListener(ee,d),c=e,()=>{i.removeEventListener(ee,d),c=null}},createHref(e){return t(i,e)},createURL:m,encodeLocation(e){let t=m(e);return{pathname:t.pathname,search:t.search,hash:t.hash}},push:f,replace:p,go(e){return o.go(e)}};return h}function A(e,t,n=!1){let r=`http://localhost`;e&&(r=e.location.origin===`null`?e.location.href:e.location.origin),E(r,`No window.location.(origin|href) available to create URL`);let i=typeof t==`string`?t:ae(t);return i=i.replace(/ $/,`%20`),!n&&w.test(i)&&(i=r+i),new URL(i,r)}function se(e,t,n=`/`){return ce(e,t,n,!1)}function ce(e,t,n,r,i){let a=Te((typeof t==`string`?oe(t):t).pathname||`/`,n);if(a==null)return null;let o=i??j(e),s=null,c=we(a);for(let e=0;s==null&&e{let c={relativePath:s===void 0?e.path||``:s,caseSensitive:e.caseSensitive===!0,childrenIndex:a,route:e};if(c.relativePath.startsWith(`/`)){if(!c.relativePath.startsWith(r)&&o)return;E(c.relativePath.startsWith(r),`Absolute route path "${c.relativePath}" nested under path "${r}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),c.relativePath=c.relativePath.slice(r.length)}let l=Ne([r,c.relativePath]),u=n.concat(c);e.children&&e.children.length>0&&(E(e.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${l}".`),M(e.children,t,u,l,o)),!(e.path==null&&!e.index)&&t.push({path:l,score:ve(l,e.index),routesMeta:u.map((e,t)=>{let[n,r]=Ce(e.relativePath,e.caseSensitive,t===u.length-1);return{...e,matcher:n,compiledParams:r}})})};return e.forEach((e,t)=>{if(e.path===``||!e.path?.includes(`?`))a(e,t);else for(let n of N(e.path))a(e,t,!0,n)}),t}function N(e){let t=e.split(`/`);if(t.length===0)return[];let[n,...r]=t,i=n.endsWith(`?`),a=n.replace(/\?$/,``);if(r.length===0)return i?[a,``]:[a];let o=N(r.join(`/`)),s=[];return s.push(...o.map(e=>e===``?a:[a,e].join(`/`))),i&&s.push(...o),s.map(t=>e.startsWith(`/`)&&t===``?`/`:t)}function ue(e){e.sort((e,t)=>e.score===t.score?ye(e.routesMeta.map(e=>e.childrenIndex),t.routesMeta.map(e=>e.childrenIndex)):t.score-e.score)}var de=/^:[\w-]+$/,fe=3,pe=2,me=1,he=10,ge=-2,_e=e=>e===`*`;function ve(e,t){let n=e.split(`/`),r=n.length;return n.some(_e)&&(r+=ge),t&&(r+=pe),n.filter(e=>!_e(e)).reduce((e,t)=>e+(de.test(t)?fe:t===``?me:he),r)}function ye(e,t){return e.length===t.length&&e.slice(0,-1).every((e,n)=>e===t[n])?e[e.length-1]-t[t.length-1]:0}function be(e,t,n=!1){let{routesMeta:r}=e,i={},a=`/`,o=[];for(let e=0;e{if(t===`*`){let e=s[r]||``;o=a.slice(0,a.length-e.length).replace(/(.)\/+$/,`$1`)}let i=s[r];return n&&!i?e[t]=void 0:e[t]=(i||``).replace(/%2F/g,`/`),e},{}),pathname:a,pathnameBase:o,pattern:e}}function Ce(e,t=!1,n=!0){D(e===`*`||!e.endsWith(`*`)||e.endsWith(`/*`),`Route path "${e}" will be treated as if it were "${e.replace(/\*$/,`/*`)}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${e.replace(/\*$/,`/*`)}".`);let r=[],i=`^`+e.replace(/\/*\*?$/,``).replace(/^\/*/,`/`).replace(/[\\.*+^${}|()[\]]/g,`\\$&`).replace(/\/:([\w-]+)(\?)?/g,(e,t,n,i,a)=>{if(r.push({paramName:t,isOptional:n!=null}),n){let t=a.charAt(i+e.length);return t&&t!==`/`?`/([^\\/]*)`:`(?:/([^\\/]*))?`}return`/([^\\/]+)`}).replace(/\/([\w-]+)\?(\/|$)/g,`(/$1)?$2`);return e.endsWith(`*`)?(r.push({paramName:`*`}),i+=e===`*`||e===`/*`?`(.*)$`:`(?:\\/(.+)|\\/*)$`):n?i+=`\\/*$`:e!==``&&e!==`/`&&(i+=`(?:(?=\\/|$))`),[new RegExp(i,t?void 0:`i`),r]}function we(e){try{return e.split(`/`).map(e=>decodeURIComponent(e).replace(/\//g,`%2F`)).join(`/`)}catch(t){return D(!1,`The URL path "${e}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${t}).`),e}}function Te(e,t){if(t===`/`)return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let n=t.endsWith(`/`)?t.length-1:t.length,r=e.charAt(n);return r&&r!==`/`?null:e.slice(n)||`/`}function Ee(e,t=`/`){let{pathname:n,search:r=``,hash:i=``}=typeof e==`string`?oe(e):e,a;return n?(n=Me(n),a=n.startsWith(`/`)?De(n.substring(1),`/`):De(n,t)):a=t,{pathname:a,search:Ie(r),hash:Le(i)}}function De(e,t){let n=Pe(t).split(`/`);return e.split(`/`).forEach(e=>{e===`..`?n.length>1&&n.pop():e!==`.`&&n.push(e)}),n.length>1?n.join(`/`):`/`}function Oe(e,t,n,r){return`Cannot include a '${e}' character in a manually specified \`to.${t}\` field [${JSON.stringify(r)}]. Please separate it out to the \`to.${n}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function ke(e){return e.filter((e,t)=>t===0||e.route.path&&e.route.path.length>0)}function Ae(e){let t=ke(e);return t.map((e,n)=>n===t.length-1?e.pathname:e.pathnameBase)}function je(e,t,n,r=!1){let i;typeof e==`string`?i=oe(e):(i={...e},E(!i.pathname||!i.pathname.includes(`?`),Oe(`?`,`pathname`,`search`,i)),E(!i.pathname||!i.pathname.includes(`#`),Oe(`#`,`pathname`,`hash`,i)),E(!i.search||!i.search.includes(`#`),Oe(`#`,`search`,`hash`,i)));let a=e===``||i.pathname===``,o=a?`/`:i.pathname,s;if(o==null)s=n;else{let e=t.length-1;if(!r&&o.startsWith(`..`)){let t=o.split(`/`);for(;t[0]===`..`;)t.shift(),--e;i.pathname=t.join(`/`)}s=e>=0?t[e]:`/`}let c=Ee(i,s),l=o&&o!==`/`&&o.endsWith(`/`),u=(a||o===`.`)&&n.endsWith(`/`);return!c.pathname.endsWith(`/`)&&(l||u)&&(c.pathname+=`/`),c}var Me=e=>e.replace(/[\\/]{2,}/g,`/`),Ne=e=>Me(e.join(`/`)),Pe=e=>e.replace(/\/+$/,``),Fe=e=>Pe(e).replace(/^\/*/,`/`),Ie=e=>!e||e===`?`?``:e.startsWith(`?`)?e:`?`+e,Le=e=>!e||e===`#`?``:e.startsWith(`#`)?e:`#`+e,Re=class{constructor(e,t,n,r=!1){this.status=e,this.statusText=t||``,this.internal=r,n instanceof Error?(this.data=n.toString(),this.error=n):this.data=n}};function ze(e){return e!=null&&typeof e.status==`number`&&typeof e.statusText==`string`&&typeof e.internal==`boolean`&&`data`in e}function Be(e){return Ne(e.map(e=>e.route.path).filter(Boolean))||`/`}var Ve=typeof window<`u`&&window.document!==void 0&&window.document.createElement!==void 0;function He(e,t){let n=e;if(typeof n!=`string`||!C.test(n))return{absoluteURL:void 0,isExternal:!1,to:n};let r=n,i=!1;if(Ve)try{let e=new URL(window.location.href),r=w.test(n)?new URL(T(n,e.protocol)):new URL(n),a=Te(r.pathname,t);r.origin===e.origin&&a!=null?n=a+r.search+r.hash:i=!0}catch{D(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:r,isExternal:i,to:n}}Object.getOwnPropertyNames(Object.prototype).sort().join(`\0`);var Ue=[`POST`,`PUT`,`PATCH`,`DELETE`];new Set(Ue);var We=[`GET`,...Ue];new Set(We);var Ge=[`about:`,`blob:`,`chrome:`,`chrome-untrusted:`,`content:`,`data:`,`devtools:`,`file:`,`filesystem:`,`javascript:`];function Ke(e){try{return Ge.includes(new URL(e).protocol)}catch{return!1}}var qe=S.createContext(null);qe.displayName=`DataRouter`;var Je=S.createContext(null);Je.displayName=`DataRouterState`;var Ye=S.createContext(!1);function Xe(){return S.useContext(Ye)}var Ze=S.createContext({isTransitioning:!1});Ze.displayName=`ViewTransition`;var Qe=S.createContext(new Map);Qe.displayName=`Fetchers`;var $e=S.createContext(null);$e.displayName=`Await`;var et=S.createContext(null);et.displayName=`Navigation`;var tt=S.createContext(null);tt.displayName=`Location`;var nt=S.createContext({outlet:null,matches:[],isDataRoute:!1});nt.displayName=`Route`;var rt=S.createContext(null);rt.displayName=`RouteError`;var it=`REACT_ROUTER_ERROR`,at=`REDIRECT`,ot=`ROUTE_ERROR_RESPONSE`;function st(e){if(e.startsWith(`${it}:${at}:{`))try{let t=JSON.parse(e.slice(28));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`&&typeof t.location==`string`&&typeof t.reloadDocument==`boolean`&&typeof t.replace==`boolean`)return t}catch{}}function ct(e){if(e.startsWith(`${it}:${ot}:{`))try{let t=JSON.parse(e.slice(40));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`)return new Re(t.status,t.statusText,t.data)}catch{}}function lt(e,{relative:t}={}){E(ut(),`useHref() may be used only in the context of a component.`);let{basename:n,navigator:r}=S.useContext(et),{hash:i,pathname:a,search:o}=_t(e,{relative:t}),s=a;return n!==`/`&&(s=a===`/`?n:Ne([n,a])),r.createHref({pathname:s,search:o,hash:i})}function ut(){return S.useContext(tt)!=null}function P(){return E(ut(),`useLocation() may be used only in the context of a component.`),S.useContext(tt).location}var dt=`You should call navigate() in a React.useEffect(), not when your component is first rendered.`;function ft(e){S.useContext(et).static||S.useLayoutEffect(e)}function pt(){let{isDataRoute:e}=S.useContext(nt);return e?It():mt()}function mt(){E(ut(),`useNavigate() may be used only in the context of a component.`);let e=S.useContext(qe),{basename:t,navigator:n}=S.useContext(et),{matches:r}=S.useContext(nt),{pathname:i}=P(),a=JSON.stringify(Ae(r)),o=S.useRef(!1);return ft(()=>{o.current=!0}),S.useCallback((r,s={})=>{if(D(o.current,dt),!o.current)return;if(typeof r==`number`){n.go(r);return}let c=je(r,JSON.parse(a),i,s.relative===`path`);e==null&&t!==`/`&&(c.pathname=c.pathname===`/`?t:Ne([t,c.pathname])),(s.replace?n.replace:n.push)(c,s.state,s)},[t,n,a,i,e])}var ht=S.createContext(null);function gt(e){let t=S.useContext(nt).outlet;return S.useMemo(()=>t&&S.createElement(ht.Provider,{value:e},t),[t,e])}function _t(e,{relative:t}={}){let{matches:n}=S.useContext(nt),{pathname:r}=P(),i=JSON.stringify(Ae(n));return S.useMemo(()=>je(e,JSON.parse(i),r,t===`path`),[e,i,r,t])}function vt(e,t){return yt(e,t)}function yt(e,t,n){E(ut(),`useRoutes() may be used only in the context of a component.`);let{navigator:r}=S.useContext(et),{matches:i}=S.useContext(nt),a=i[i.length-1],o=a?a.params:{},s=a?a.pathname:`/`,c=a?a.pathnameBase:`/`,l=a&&a.route;{let e=l&&l.path||``;Rt(s,!l||e.endsWith(`*`)||e.endsWith(`*?`),`You rendered descendant (or called \`useRoutes()\`) at "${s}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. + +Please change the parent to .`)}let u=P(),d;if(t){let e=typeof t==`string`?oe(t):t;E(c===`/`||e.pathname?.startsWith(c),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${c}" but pathname "${e.pathname}" was given in the \`location\` prop.`),d=e}else d=u;let f=d.pathname||`/`,p=f;if(c!==`/`){let e=c.replace(/^\//,``).split(`/`);p=`/`+f.replace(/^\//,``).split(`/`).slice(e.length).join(`/`)}let m=n&&n.state.matches.length?n.state.matches.map(e=>Object.assign(e,{route:n.manifest[e.route.id]||e.route})):se(e,{pathname:p});D(l||m!=null,`No routes matched location "${d.pathname}${d.search}${d.hash}" `),D(m==null||m[m.length-1].route.element!==void 0||m[m.length-1].route.Component!==void 0||m[m.length-1].route.lazy!==void 0,`Matched leaf route at location "${d.pathname}${d.search}${d.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let h=Et(m&&m.map(e=>Object.assign({},e,{params:Object.assign({},o,e.params),pathname:Ne([c,r.encodeLocation?r.encodeLocation(e.pathname.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathname]),pathnameBase:e.pathnameBase===`/`?c:Ne([c,r.encodeLocation?r.encodeLocation(e.pathnameBase.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathnameBase])})),i,n);return t&&h?S.createElement(tt.Provider,{value:{location:{pathname:`/`,search:``,hash:``,state:null,key:`default`,mask:void 0,...d},navigationType:`POP`}},h):h}function bt(){let e=Ft(),t=ze(e)?`${e.status} ${e.statusText}`:e instanceof Error?e.message:JSON.stringify(e),n=e instanceof Error?e.stack:null,r=`rgba(200,200,200, 0.5)`,i={padding:`0.5rem`,backgroundColor:r},a={padding:`2px 4px`,backgroundColor:r},o=null;return console.error(`Error handled by React Router default ErrorBoundary:`,e),o=S.createElement(S.Fragment,null,S.createElement(`p`,null,`💿 Hey developer 👋`),S.createElement(`p`,null,`You can provide a way better UX than this when your app throws errors by providing your own `,S.createElement(`code`,{style:a},`ErrorBoundary`),` or`,` `,S.createElement(`code`,{style:a},`errorElement`),` prop on your route.`)),S.createElement(S.Fragment,null,S.createElement(`h2`,null,`Unexpected Application Error!`),S.createElement(`h3`,{style:{fontStyle:`italic`}},t),n?S.createElement(`pre`,{style:i},n):null,o)}var xt=S.createElement(bt,null),St=class extends S.Component{constructor(e){super(e),this.state={location:e.location,revalidation:e.revalidation,error:e.error}}static getDerivedStateFromError(e){return{error:e}}static getDerivedStateFromProps(e,t){return t.location!==e.location||t.revalidation!==`idle`&&e.revalidation===`idle`?{error:e.error,location:e.location,revalidation:e.revalidation}:{error:e.error===void 0?t.error:e.error,location:t.location,revalidation:e.revalidation||t.revalidation}}componentDidCatch(e,t){this.props.onError?this.props.onError(e,t):console.error(`React Router caught the following error during render`,e)}render(){let e=this.state.error;if(this.context&&typeof e==`object`&&e&&`digest`in e&&typeof e.digest==`string`){let t=ct(e.digest);t&&(e=t)}let t=e===void 0?this.props.children:S.createElement(nt.Provider,{value:this.props.routeContext},S.createElement(rt.Provider,{value:e,children:this.props.component}));return this.context?S.createElement(wt,{error:e},t):t}};St.contextType=Ye;var Ct=new WeakMap;function wt({children:e,error:t}){let{basename:n}=S.useContext(et);if(typeof t==`object`&&t&&`digest`in t&&typeof t.digest==`string`){let e=st(t.digest);if(e){let r=Ct.get(t);if(r)throw r;let i=He(e.location,n),a=i.absoluteURL||i.to;if(Ke(a))throw Error(`Invalid redirect location`);if(Ve&&!Ct.get(t))if(i.isExternal||e.reloadDocument)window.location.href=a;else{let n=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(i.to,{replace:e.replace}));throw Ct.set(t,n),n}return S.createElement(`meta`,{httpEquiv:`refresh`,content:`0;url=${a}`})}}return e}function Tt({routeContext:e,match:t,children:n}){let r=S.useContext(qe);return r&&r.static&&r.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=t.route.id),S.createElement(nt.Provider,{value:e},n)}function Et(e,t=[],n){let r=n?.state;if(e==null){if(!r)return null;if(r.errors)e=r.matches;else if(t.length===0&&!r.initialized&&r.matches.length>0)e=r.matches;else return null}let i=e,a=r?.errors;if(a!=null){let e=i.findIndex(e=>e.route.id&&a?.[e.route.id]!==void 0);E(e>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(a).join(`,`)}`),i=i.slice(0,Math.min(i.length,e+1))}let o=!1,s=-1;if(n&&r){o=r.renderFallback;for(let e=0;e=0?i.slice(0,s+1):[i[0]];break}}}}let c=n?.onError,l=r&&c?(e,t)=>{c(e,{location:r.location,params:r.matches?.[0]?.params??{},pattern:Be(r.matches),errorInfo:t})}:void 0;return i.reduceRight((e,n,c)=>{let u,d=!1,f=null,p=null;r&&(u=a&&n.route.id?a[n.route.id]:void 0,f=n.route.errorElement||xt,o&&(s<0&&c===0?(Rt(`route-fallback`,!1,"No `HydrateFallback` element provided to render during initial hydration"),d=!0,p=null):s===c&&(d=!0,p=n.route.hydrateFallbackElement||null)));let m=t.concat(i.slice(0,c+1)),h=()=>{let t;return t=u?f:d?p:n.route.Component?S.createElement(n.route.Component,null):n.route.element?n.route.element:e,S.createElement(Tt,{match:n,routeContext:{outlet:e,matches:m,isDataRoute:r!=null},children:t})};return r&&(n.route.ErrorBoundary||n.route.errorElement||c===0)?S.createElement(St,{location:r.location,revalidation:r.revalidation,component:f,error:u,children:h(),routeContext:{outlet:null,matches:m,isDataRoute:!0},onError:l}):h()},null)}function Dt(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function Ot(e){let t=S.useContext(qe);return E(t,Dt(e)),t}function kt(e){let t=S.useContext(Je);return E(t,Dt(e)),t}function At(e){let t=S.useContext(nt);return E(t,Dt(e)),t}function jt(e){let t=At(e),n=t.matches[t.matches.length-1];return E(n.route.id,`${e} can only be used on routes that contain a unique "id"`),n.route.id}function Mt(){return jt(`useRouteId`)}function Nt(){let e=kt(`useNavigation`);return S.useMemo(()=>{let{matches:t,historyAction:n,...r}=e.navigation;return r},[e.navigation])}function Pt(){let{matches:e,loaderData:t}=kt(`useMatches`);return S.useMemo(()=>e.map(e=>le(e,t)),[e,t])}function Ft(){let e=S.useContext(rt),t=kt(`useRouteError`),n=jt(`useRouteError`);return e===void 0?t.errors?.[n]:e}function It(){let{router:e}=Ot(`useNavigate`),t=jt(`useNavigate`),n=S.useRef(!1);return ft(()=>{n.current=!0}),S.useCallback(async(r,i={})=>{D(n.current,dt),n.current&&(typeof r==`number`?await e.navigate(r):await e.navigate(r,{fromRouteId:t,...i}))},[e,t])}var Lt={};function Rt(e,t,n){!t&&!Lt[e]&&(Lt[e]=!0,D(!1,n))}S.memo(zt);function zt({routes:e,manifest:t,future:n,state:r,isStatic:i,onError:a}){return yt(e,void 0,{manifest:t,state:r,isStatic:i,onError:a,future:n})}function Bt({to:e,replace:t,state:n,relative:r}){E(ut(),` may be used only in the context of a component.`);let{static:i}=S.useContext(et);D(!i,` must not be used on the initial render in a . This is a no-op, but you should modify your code so the is only ever rendered in response to some user interaction or state change.`);let{matches:a}=S.useContext(nt),{pathname:o}=P(),s=pt(),c=je(e,Ae(a),o,r===`path`),l=JSON.stringify(c);return S.useEffect(()=>{s(JSON.parse(l),{replace:t,state:n,relative:r})},[s,l,r,t,n]),null}function Vt(e){return gt(e.context)}function Ht(e){E(!1,`A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .`)}function Ut({basename:e=`/`,children:t=null,location:n,navigationType:r=`POP`,navigator:i,static:a=!1,useTransitions:o}){E(!ut(),`You cannot render a inside another . You should never have more than one in your app.`);let s=e.replace(/^\/*/,`/`),c=S.useMemo(()=>({basename:s,navigator:i,static:a,useTransitions:o,future:{}}),[s,i,a,o]);typeof n==`string`&&(n=oe(n));let{pathname:l=`/`,search:u=``,hash:d=``,state:f=null,key:p=`default`,mask:m}=n,h=S.useMemo(()=>{let e=Te(l,s);return e==null?null:{location:{pathname:e,search:u,hash:d,state:f,key:p,mask:m},navigationType:r}},[s,l,u,d,f,p,r,m]);return D(h!=null,` is not able to match the URL "${l}${u}${d}" because it does not start with the basename, so the won't render anything.`),h==null?null:S.createElement(et.Provider,{value:c},S.createElement(tt.Provider,{children:t,value:h}))}function Wt({children:e,location:t}){return vt(Gt(e),t)}S.Component;function Gt(e,t=[]){let n=[];return S.Children.forEach(e,(e,r)=>{if(!S.isValidElement(e))return;let i=[...t,r];if(e.type===S.Fragment){n.push.apply(n,Gt(e.props.children,i));return}E(e.type===Ht,`[${typeof e.type==`string`?e.type:e.type.name}] is not a component. All component children of must be a or `),E(!e.props.index||!e.props.children,`An index route cannot have child routes.`);let a={id:e.props.id||i.join(`-`),caseSensitive:e.props.caseSensitive,element:e.props.element,Component:e.props.Component,index:e.props.index,path:e.props.path,middleware:e.props.middleware,loader:e.props.loader,action:e.props.action,hydrateFallbackElement:e.props.hydrateFallbackElement,HydrateFallback:e.props.HydrateFallback,errorElement:e.props.errorElement,ErrorBoundary:e.props.ErrorBoundary,hasErrorBoundary:e.props.hasErrorBoundary===!0||e.props.ErrorBoundary!=null||e.props.errorElement!=null,shouldRevalidate:e.props.shouldRevalidate,handle:e.props.handle,lazy:e.props.lazy};e.props.children&&(a.children=Gt(e.props.children,i)),n.push(a)}),n}var Kt=`get`,qt=`application/x-www-form-urlencoded`;function Jt(e){return typeof HTMLElement<`u`&&e instanceof HTMLElement}function Yt(e){return Jt(e)&&e.tagName.toLowerCase()===`button`}function Xt(e){return Jt(e)&&e.tagName.toLowerCase()===`form`}function Zt(e){return Jt(e)&&e.tagName.toLowerCase()===`input`}function Qt(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function $t(e,t){return e.button===0&&(!t||t===`_self`)&&!Qt(e)}var en=null;function tn(){if(en===null)try{new FormData(document.createElement(`form`),0),en=!1}catch{en=!0}return en}var nn=new Set([`application/x-www-form-urlencoded`,`multipart/form-data`,`text/plain`]);function rn(e){return e!=null&&!nn.has(e)?(D(!1,`"${e}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${qt}"`),null):e}function an(e,t){let n,r,i,a,o;if(Xt(e)){let o=e.getAttribute(`action`);r=o?Te(o,t):null,n=e.getAttribute(`method`)||Kt,i=rn(e.getAttribute(`enctype`))||qt,a=new FormData(e)}else if(Yt(e)||Zt(e)&&(e.type===`submit`||e.type===`image`)){let o=e.form;if(o==null)throw Error(`Cannot submit a + + +
+ +
+ + ) +} diff --git a/pets-be/web/admin/src/components/Pager.tsx b/pets-be/web/admin/src/components/Pager.tsx new file mode 100644 index 0000000..e3b868f --- /dev/null +++ b/pets-be/web/admin/src/components/Pager.tsx @@ -0,0 +1,30 @@ +import { Button } from '@/components/ui/button' + +export function Pager({ + page, + totalPages, + total, + onChange, +}: { + page: number + totalPages: number + total: number + onChange: (p: number) => void +}) { + return ( +
+ 共 {total} 条 +
+ + + {page} / {totalPages} + + +
+
+ ) +} diff --git a/pets-be/web/admin/src/components/ui/badge.tsx b/pets-be/web/admin/src/components/ui/badge.tsx new file mode 100644 index 0000000..6a7cad6 --- /dev/null +++ b/pets-be/web/admin/src/components/ui/badge.tsx @@ -0,0 +1,28 @@ +import { cva, type VariantProps } from 'class-variance-authority' +import type * as React from 'react' +import { cn } from '@/lib/utils' + +const badgeVariants = cva( + 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold', + { + variants: { + variant: { + default: 'border-transparent bg-primary/15 text-primary', + secondary: 'border-transparent bg-secondary text-secondary-foreground', + destructive: 'border-transparent bg-destructive/15 text-destructive', + outline: 'text-foreground', + success: 'border-transparent bg-emerald-100 text-emerald-700', + muted: 'border-transparent bg-muted text-muted-foreground', + }, + }, + defaultVariants: { variant: 'default' }, + }, +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +export function Badge({ className, variant, ...props }: BadgeProps) { + return
+} diff --git a/pets-be/web/admin/src/components/ui/button.tsx b/pets-be/web/admin/src/components/ui/button.tsx new file mode 100644 index 0000000..4c3373d --- /dev/null +++ b/pets-be/web/admin/src/components/ui/button.tsx @@ -0,0 +1,43 @@ +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' +import { cn } from '@/lib/utils' + +const buttonVariants = cva( + 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50', + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground hover:bg-primary/90 shadow', + destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', + outline: 'border border-input bg-card hover:bg-accent hover:text-accent-foreground', + secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-9 px-4 py-2', + sm: 'h-8 rounded-md px-3 text-xs', + lg: 'h-10 rounded-md px-8', + icon: 'h-9 w-9', + }, + }, + defaultVariants: { variant: 'default', size: 'default' }, + }, +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button' + return + }, +) +Button.displayName = 'Button' + +export { Button, buttonVariants } diff --git a/pets-be/web/admin/src/components/ui/card.tsx b/pets-be/web/admin/src/components/ui/card.tsx new file mode 100644 index 0000000..d935caa --- /dev/null +++ b/pets-be/web/admin/src/components/ui/card.tsx @@ -0,0 +1,30 @@ +import * as React from 'react' +import { cn } from '@/lib/utils' + +const Card = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +) +Card.displayName = 'Card' + +const CardHeader = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +) +CardHeader.displayName = 'CardHeader' + +const CardTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +) +CardTitle.displayName = 'CardTitle' + +const CardContent = React.forwardRef>( + ({ className, ...props }, ref) =>
, +) +CardContent.displayName = 'CardContent' + +export { Card, CardHeader, CardTitle, CardContent } diff --git a/pets-be/web/admin/src/components/ui/dialog.tsx b/pets-be/web/admin/src/components/ui/dialog.tsx new file mode 100644 index 0000000..1bbb15d --- /dev/null +++ b/pets-be/web/admin/src/components/ui/dialog.tsx @@ -0,0 +1,41 @@ +import * as React from 'react' +import * as DialogPrimitive from '@radix-ui/react-dialog' +import { X } from 'lucide-react' +import { cn } from '@/lib/utils' + +const Dialog = DialogPrimitive.Root +const DialogTrigger = DialogPrimitive.Trigger +const DialogClose = DialogPrimitive.Close + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + + + +)) +DialogContent.displayName = 'DialogContent' + +function DialogHeader({ className, ...props }: React.HTMLAttributes) { + return
+} + +function DialogTitle({ className, ...props }: React.HTMLAttributes) { + return

+} + +export { Dialog, DialogTrigger, DialogClose, DialogContent, DialogHeader, DialogTitle } diff --git a/pets-be/web/admin/src/components/ui/input.tsx b/pets-be/web/admin/src/components/ui/input.tsx new file mode 100644 index 0000000..ec95b16 --- /dev/null +++ b/pets-be/web/admin/src/components/ui/input.tsx @@ -0,0 +1,19 @@ +import * as React from 'react' +import { cn } from '@/lib/utils' + +const Input = React.forwardRef>( + ({ className, type, ...props }, ref) => ( + + ), +) +Input.displayName = 'Input' + +export { Input } diff --git a/pets-be/web/admin/src/components/ui/label.tsx b/pets-be/web/admin/src/components/ui/label.tsx new file mode 100644 index 0000000..6dce284 --- /dev/null +++ b/pets-be/web/admin/src/components/ui/label.tsx @@ -0,0 +1,13 @@ +import * as React from 'react' +import * as LabelPrimitive from '@radix-ui/react-label' +import { cn } from '@/lib/utils' + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = 'Label' + +export { Label } diff --git a/pets-be/web/admin/src/components/ui/table.tsx b/pets-be/web/admin/src/components/ui/table.tsx new file mode 100644 index 0000000..8594b43 --- /dev/null +++ b/pets-be/web/admin/src/components/ui/table.tsx @@ -0,0 +1,46 @@ +import * as React from 'react' +import { cn } from '@/lib/utils' + +const Table = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ + + ), +) +Table.displayName = 'Table' + +const TableHeader = React.forwardRef>( + ({ className, ...props }, ref) => , +) +TableHeader.displayName = 'TableHeader' + +const TableBody = React.forwardRef>( + ({ className, ...props }, ref) => ( + + ), +) +TableBody.displayName = 'TableBody' + +const TableRow = React.forwardRef>( + ({ className, ...props }, ref) => ( + + ), +) +TableRow.displayName = 'TableRow' + +const TableHead = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +) +TableHead.displayName = 'TableHead' + +const TableCell = React.forwardRef>( + ({ className, ...props }, ref) => ( + + ), +) +TableCell.displayName = 'TableCell' + +export { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } diff --git a/pets-be/web/admin/src/index.css b/pets-be/web/admin/src/index.css new file mode 100644 index 0000000..703d023 --- /dev/null +++ b/pets-be/web/admin/src/index.css @@ -0,0 +1,37 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 40 33% 98%; + --foreground: 24 10% 15%; + --card: 0 0% 100%; + --card-foreground: 24 10% 15%; + --primary: 33 90% 62%; + --primary-foreground: 0 0% 100%; + --secondary: 40 30% 94%; + --secondary-foreground: 24 10% 25%; + --muted: 40 20% 94%; + --muted-foreground: 25 8% 45%; + --accent: 40 40% 92%; + --accent-foreground: 24 10% 20%; + --destructive: 5 78% 57%; + --destructive-foreground: 0 0% 100%; + --border: 33 25% 88%; + --input: 33 25% 88%; + --ring: 33 90% 62%; + --radius: 0.6rem; + } + + * { + border-color: hsl(var(--border)); + } + body { + margin: 0; + background: hsl(var(--background)); + color: hsl(var(--foreground)); + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', + 'Microsoft YaHei', sans-serif; + } +} diff --git a/pets-be/web/admin/src/lib/api.ts b/pets-be/web/admin/src/lib/api.ts new file mode 100644 index 0000000..a806eef --- /dev/null +++ b/pets-be/web/admin/src/lib/api.ts @@ -0,0 +1,72 @@ +import axios from 'axios' + +const TOKEN_KEY = 'pets_admin_token' + +export function getToken() { + return localStorage.getItem(TOKEN_KEY) || '' +} +export function setToken(t: string) { + localStorage.setItem(TOKEN_KEY, t) +} +export function clearToken() { + localStorage.removeItem(TOKEN_KEY) +} + +const http = axios.create({ baseURL: '/api' }) + +http.interceptors.request.use((cfg) => { + const t = getToken() + if (t) cfg.headers.Authorization = `Bearer ${t}` + return cfg +}) + +// 统一响应 {code,message,data}:成功返回 data,失败抛错 +http.interceptors.response.use( + (resp) => { + const body = resp.data + if (body && typeof body === 'object' && 'code' in body) { + if (body.code === 0) return body.data + if (body.code === 40100) { + clearToken() + if (location.pathname !== '/admin/login') location.href = '/admin/login' + } + return Promise.reject(new Error(body.message || '请求失败')) + } + return body + }, + (err) => Promise.reject(err), +) + +export interface Page { + list: T[] + total: number + page: number + page_size: number +} + +export const api = { + login: (username: string, password: string) => + http.post('/admin/login', { username, password }), + me: () => http.get('/admin/me'), + stats: () => http.get('/admin/stats'), + + users: (params: any) => http.get>('/admin/users', { params }), + setUserDisabled: (id: number, disabled: boolean) => + http.put(`/admin/users/${id}/disabled`, { disabled }), + + pets: (params: any) => http.get>('/admin/pets', { params }), + + posts: (params: any) => http.get>('/admin/posts', { params }), + setPostStatus: (id: number, status: string) => http.put(`/admin/posts/${id}/status`, { status }), + + comments: (params: any) => http.get>('/admin/comments', { params }), + deleteComment: (id: number) => http.delete(`/admin/comments/${id}`), + + articles: (params: any) => http.get>('/admin/articles', { params }), + saveArticle: (a: any) => http.post('/admin/articles', a), + deleteArticle: (id: number) => http.delete(`/admin/articles/${id}`), + + memberships: (params: any) => http.get>('/admin/memberships', { params }), +} + +export default http diff --git a/pets-be/web/admin/src/lib/usePaged.ts b/pets-be/web/admin/src/lib/usePaged.ts new file mode 100644 index 0000000..95c7fec --- /dev/null +++ b/pets-be/web/admin/src/lib/usePaged.ts @@ -0,0 +1,33 @@ +import { useCallback, useEffect, useState } from 'react' +import type { Page } from '@/lib/api' + +export function usePaged( + fetcher: (params: Record) => Promise>, + extra: Record = {}, +) { + const [page, setPage] = useState(1) + const [pageSize] = useState(10) + const [data, setData] = useState | null>(null) + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + + const extraKey = JSON.stringify(extra) + + const load = useCallback(() => { + setLoading(true) + setError('') + fetcher({ page, page_size: pageSize, ...extra }) + .then(setData) + .catch((e) => setError(e.message || '加载失败')) + .finally(() => setLoading(false)) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [page, pageSize, extraKey]) + + useEffect(() => { + load() + }, [load]) + + const totalPages = data ? Math.max(1, Math.ceil(data.total / pageSize)) : 1 + + return { page, setPage, pageSize, data, loading, error, reload: load, totalPages } +} diff --git a/pets-be/web/admin/src/lib/utils.ts b/pets-be/web/admin/src/lib/utils.ts new file mode 100644 index 0000000..fed2fe9 --- /dev/null +++ b/pets-be/web/admin/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from 'clsx' +import { twMerge } from 'tailwind-merge' + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/pets-be/web/admin/src/main.tsx b/pets-be/web/admin/src/main.tsx new file mode 100644 index 0000000..4a1bcf6 --- /dev/null +++ b/pets-be/web/admin/src/main.tsx @@ -0,0 +1,13 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { BrowserRouter } from 'react-router-dom' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + + + , +) diff --git a/pets-be/web/admin/src/pages/Articles.tsx b/pets-be/web/admin/src/pages/Articles.tsx new file mode 100644 index 0000000..c0f9259 --- /dev/null +++ b/pets-be/web/admin/src/pages/Articles.tsx @@ -0,0 +1,147 @@ +import { useState } from 'react' +import { api } from '@/lib/api' +import { usePaged } from '@/lib/usePaged' +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Card, CardContent } from '@/components/ui/card' +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' +import { Pager } from '@/components/Pager' + +const empty = { + id: 0, icon: '📘', title: '', description: '', content: '', + category: '', related_sheet_type: '', published: true, +} + +export default function Articles() { + const { page, setPage, data, totalPages, reload } = usePaged(api.articles) + const [open, setOpen] = useState(false) + const [form, setForm] = useState(empty) + + function edit(a: any) { + setForm({ ...a }) + setOpen(true) + } + function create() { + setForm({ ...empty }) + setOpen(true) + } + async function save() { + await api.saveArticle(form) + setOpen(false) + reload() + } + async function del(id: number) { + await api.deleteArticle(id) + reload() + } + + const set = (k: string) => (e: any) => setForm({ ...form, [k]: e.target.value }) + + return ( +
+
+

文章管理

+ +
+ + + + + + ID + 标题 + 分类 + 关联弹层 + 状态 + 操作 + + + + {data?.list?.map((a) => ( + + {a.id} + + {a.icon} + {a.title} + + {a.category || '—'} + {a.related_sheet_type || '—'} + + {a.published ? 已发布 : 草稿} + + + + + + + ))} + +
+ +
+
+ + + + + {form.id ? '编辑文章' : '新建文章'} + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + + + + + 记录便便 + 选择状态并补充备注,几秒完成。 + + 正常 + 软便 + 拉稀 + + + + + + + + + 记录饮食 + + + + 正常 + 偏少 + 不吃 + + + + + + + 异常观察 + 如实描述异常,AI 会给出观察建议与就医提示(非诊断)。 + + 呕吐 + 拉稀 + 精神差 + 皮肤红 + + + 刚刚 + 半天 + 1天以上 + + + 正常 + 一般 + 明显变差 + + + + + + + AI 观察建议 + ⚠️ 风险等级:{{riskData ? riskData.risk_level : '中'}} + 可能原因:{{riskData ? riskData.causes : '可能与换粮、应激或消化不适有关。不要把这里写成诊断结论。'}} + 建议:{{riskData ? riskData.suggestion : '继续观察精神、食欲和排便;暂停新食物;记录呕吐或腹泻次数。'}} + 建议就医:{{riskData ? riskData.seek_care : '如果持续超过 24 小时,或伴随便血、精神明显变差、频繁呕吐,建议尽快就医。'}} + + + + + + + + + 就医前摘要 + 宠物:{{pet.name}} +阶段:{{pet.stage}} +当前体重:{{pet.weight}} +近期重点:疫苗期、换粮观察、排便记录 +建议携带:排便/呕吐照片、饮食变化、疫苗驱虫记录。 + + + + + + 记录用药 + + + + + + + + 记一笔消费 + + + 食品 + 医疗 + 用品 + + + + + + + + 疫苗提醒 + 为 {{pet.name}} 设置疫苗提醒。接种前后避免洗澡和长途出行。 + + + {{dateVals.vaccine}} + + + + + + + 驱虫提醒 + 设置下次驱虫日期,到点提醒你。 + + + {{dateVals.deworm}} + + + + + + + 成长照片 + 上传成长照片,用于宠物档案与成长报告。 + + {{pet.emoji}} + {{pet.name}} 的成长照片 + 模拟上传预览 + + + + + + + 提醒中心 + {{item.title}}{{item.when}} + 暂无提醒 + + + + + + + 问问 AI + + {{item.text}} + + + + 猫咪吐了一次 + 多久驱虫 + 换粮软便 + + + + + + + + + + + {{pet.name}} 的成长报告 + {{pet.emoji}} + {{pet.age}}|{{pet.weight}}|{{pet.stage}} + ✓ 完成任务 {{poster ? poster.tasks_completed : 0}} 项 +✓ 体重记录 {{poster ? poster.weight_records : 0}} 次 +✓ 疫苗记录 {{poster ? poster.vaccine_records : 0}} 次 +✓ 高风险异常 {{poster ? poster.high_risk_count : 0}} 次 + 健康状态:{{poster ? poster.headline : '稳定成长'}} + 生成自:毛孩子计划 + + + + + + + 本周成长报告 + {{reportDetail ? reportDetail.summary : '正在汇总本周数据…'}} + 下周重点:{{reportDetail.next_week_focus}} + + + + + + 毛孩子计划 Pro + 解锁 365 天计划、PDF 健康档案、月度报告、多宠物管理与年度账单。 + + {{item}} + + + + + + + + {{innerType === 'editPet' ? '编辑档案' : '添加毛孩子'}} + + + 猫猫 + 狗狗 + + + 男孩 + 女孩 + 不确定 + + + + {{addBirthday || '选择日期'}} + + + + + + 刚到家 + 幼年期 + 成年期 + 老年期 + + + + + + + + 发布图文 + 选择发布身份,分享你的养宠日常。 + + {{pet.name}} + 匿名宠友 + 官方笔记 + + + + + 晒宠 + 求助 + 经验 + 避坑 + + + + + + + 评论 + {{item.author_name}}{{item.content}} + 还没有评论,来抢沙发~ + + + + + + + + + 分享帖子 + 转发给微信好友、微信群,或生成一张分享卡片。 + + + + + + diff --git a/pets-fe/components/bottom-sheet/bottom-sheet.wxss b/pets-fe/components/bottom-sheet/bottom-sheet.wxss new file mode 100644 index 0000000..872eaaa --- /dev/null +++ b/pets-fe/components/bottom-sheet/bottom-sheet.wxss @@ -0,0 +1,36 @@ +.overlay{ + position:fixed;inset:0;z-index:200; + background:rgba(26,18,12,.30); + opacity:0;pointer-events:none;transition:.2s ease; + backdrop-filter:blur(6rpx);-webkit-backdrop-filter:blur(6rpx); +} +.overlay.show{opacity:1;pointer-events:auto} +.sheet{ + position:absolute;left:0;right:0;bottom:0; + background:#fff;border-radius:60rpx 60rpx 0 0; + padding:32rpx 0 0; + transform:translateY(106%);transition:.24s ease; + box-shadow:0 -40rpx 100rpx rgba(0,0,0,.16); +} +.overlay.show .sheet{transform:translateY(0)} +.sheet-scroll{ + box-sizing:border-box; + max-height:78vh; + padding:0 36rpx calc(48rpx + env(safe-area-inset-bottom)); +} +.sheet-scroll::-webkit-scrollbar,.plan-chat::-webkit-scrollbar{width:0;height:0;display:none} +.sheetbar{width:88rpx;height:10rpx;border-radius:999rpx;background:#E1D8CF;margin:0 auto 24rpx} + +.sheet-h3{font-size:40rpx;font-weight:800;margin-bottom:20rpx} +.sheet-p{font-size:28rpx;color:#62584F;line-height:1.65;margin-bottom:24rpx} +.sheet-p.pre,.poster-list.pre{white-space:pre-line} +.poster-h3{font-size:40rpx;font-weight:800;margin-bottom:20rpx} +.bold{font-weight:900} + +.ai-input-row{display:flex;gap:16rpx;align-items:center} +.ai-input-row .input{flex:1} +.ai-send{height:88rpx;line-height:88rpx;padding:0 28rpx} + +/* 弹层内的日期选择器展示 */ +.picker-box{color:var(--text)} +.del-btn{background:var(--red-soft);color:var(--red)} diff --git a/pets-fe/components/fab/fab.js b/pets-fe/components/fab/fab.js new file mode 100644 index 0000000..f8e3323 --- /dev/null +++ b/pets-fe/components/fab/fab.js @@ -0,0 +1,8 @@ +Component({ + options: { addGlobalClass: true }, + methods: { + onTap() { + this.triggerEvent('tap'); + } + } +}); diff --git a/pets-fe/components/fab/fab.json b/pets-fe/components/fab/fab.json new file mode 100644 index 0000000..a89ef4d --- /dev/null +++ b/pets-fe/components/fab/fab.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} +} diff --git a/pets-fe/components/fab/fab.wxml b/pets-fe/components/fab/fab.wxml new file mode 100644 index 0000000..7e54a8b --- /dev/null +++ b/pets-fe/components/fab/fab.wxml @@ -0,0 +1 @@ +AI diff --git a/pets-fe/components/fab/fab.wxss b/pets-fe/components/fab/fab.wxss new file mode 100644 index 0000000..87ab330 --- /dev/null +++ b/pets-fe/components/fab/fab.wxss @@ -0,0 +1,10 @@ +.fab{ + position:fixed;right:36rpx;bottom:calc(190rpx + env(safe-area-inset-bottom)); + width:116rpx;height:116rpx;border-radius:44rpx; + background:linear-gradient(135deg,#78A6E8,#9FC0F5); + color:#fff;font-weight:900;font-size:40rpx; + display:flex;align-items:center;justify-content:center; + box-shadow:0 28rpx 60rpx rgba(120,166,232,.32); + z-index:60; +} +.fab:active{transform:scale(.96)} diff --git a/pets-fe/components/nav-bar/nav-bar.js b/pets-fe/components/nav-bar/nav-bar.js new file mode 100644 index 0000000..7131b98 --- /dev/null +++ b/pets-fe/components/nav-bar/nav-bar.js @@ -0,0 +1,31 @@ +const app = getApp(); + +Component({ + options: { addGlobalClass: true }, + properties: { + title: { type: String, value: '毛孩子计划' }, + showBack: { type: Boolean, value: false } + }, + data: { + statusBarHeight: 20, + navBarHeight: 44 + }, + lifetimes: { + attached() { + this.setData({ + statusBarHeight: app.globalData.statusBarHeight, + navBarHeight: app.globalData.navBarHeight + }); + } + }, + methods: { + onBack() { + const pages = getCurrentPages(); + if (pages.length > 1) { + wx.navigateBack(); + } else { + wx.switchTab({ url: '/pages/home/home' }); + } + } + } +}); diff --git a/pets-fe/components/nav-bar/nav-bar.json b/pets-fe/components/nav-bar/nav-bar.json new file mode 100644 index 0000000..a89ef4d --- /dev/null +++ b/pets-fe/components/nav-bar/nav-bar.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} +} diff --git a/pets-fe/components/nav-bar/nav-bar.wxml b/pets-fe/components/nav-bar/nav-bar.wxml new file mode 100644 index 0000000..813387d --- /dev/null +++ b/pets-fe/components/nav-bar/nav-bar.wxml @@ -0,0 +1,8 @@ + + + + {{title}} + + + + diff --git a/pets-fe/components/nav-bar/nav-bar.wxss b/pets-fe/components/nav-bar/nav-bar.wxss new file mode 100644 index 0000000..b1320d4 --- /dev/null +++ b/pets-fe/components/nav-bar/nav-bar.wxss @@ -0,0 +1,19 @@ +:host{display:block} +.nav-wrap{ + position:fixed;top:0;left:0;right:0;z-index:100; + background:rgba(248,246,242,.82); + backdrop-filter:blur(28rpx); + -webkit-backdrop-filter:blur(28rpx); +} +.nav-inner{ + position:relative;display:flex;align-items:center;justify-content:center; +} +.nav-title{ + font-size:32rpx;font-weight:900;color:var(--text); + max-width:60%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; +} +.nav-back{ + position:absolute;left:20rpx;top:50%;transform:translateY(-50%); + width:64rpx;height:64rpx;display:flex;align-items:center;justify-content:center; + font-size:52rpx;line-height:1;color:var(--text); +} diff --git a/pets-fe/components/pet-switch/pet-switch.js b/pets-fe/components/pet-switch/pet-switch.js new file mode 100644 index 0000000..076f6e7 --- /dev/null +++ b/pets-fe/components/pet-switch/pet-switch.js @@ -0,0 +1,34 @@ +const store = require('../../utils/store.js'); + +Component({ + options: { addGlobalClass: true }, + data: { + pets: [], + currentId: 0, + }, + lifetimes: { + attached() { + this.refresh(); + this._unsub = store.subscribe(() => this.refresh()); + }, + detached() { + if (this._unsub) this._unsub(); + }, + }, + methods: { + refresh() { + this.setData({ + pets: store.getPets(), + currentId: store.currentPetId() || 0, + }); + }, + onPick(e) { + const id = e.currentTarget.dataset.id; + store.switchPet(id); + this.triggerEvent('change', { id }); + }, + onAdd() { + this.triggerEvent('add'); + }, + }, +}); diff --git a/pets-fe/components/pet-switch/pet-switch.json b/pets-fe/components/pet-switch/pet-switch.json new file mode 100644 index 0000000..a89ef4d --- /dev/null +++ b/pets-fe/components/pet-switch/pet-switch.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} +} diff --git a/pets-fe/components/pet-switch/pet-switch.wxml b/pets-fe/components/pet-switch/pet-switch.wxml new file mode 100644 index 0000000..feea1fb --- /dev/null +++ b/pets-fe/components/pet-switch/pet-switch.wxml @@ -0,0 +1,10 @@ + + + {{item.emoji}}{{item.name}} + + + 添加 + + diff --git a/pets-fe/components/pet-switch/pet-switch.wxss b/pets-fe/components/pet-switch/pet-switch.wxss new file mode 100644 index 0000000..6c71022 --- /dev/null +++ b/pets-fe/components/pet-switch/pet-switch.wxss @@ -0,0 +1,5 @@ +.pet-switch{ + display:block;white-space:nowrap;width:100%; +} +.pet-switch .pet-chip{margin-right:20rpx} +.pet-switch .pet-chip:last-child{margin-right:0} diff --git a/pets-fe/custom-tab-bar/index.js b/pets-fe/custom-tab-bar/index.js new file mode 100644 index 0000000..fb6b191 --- /dev/null +++ b/pets-fe/custom-tab-bar/index.js @@ -0,0 +1,20 @@ +Component({ + data: { + selected: 0, + hidden: false, + list: [ + { pagePath: '/pages/home/home', text: '首页', icon: '🏠' }, + { pagePath: '/pages/plan/plan', text: '计划', icon: '📅' }, + { pagePath: '/pages/record/record', text: '记录', icon: '✍️' }, + { pagePath: '/pages/report/report', text: '报告', icon: '📊' }, + { pagePath: '/pages/community/community', text: '社区', icon: '🐾' } + ] + }, + methods: { + switchTab(e) { + const idx = e.currentTarget.dataset.index; + const path = this.data.list[idx].pagePath; + wx.switchTab({ url: path }); + } + } +}); diff --git a/pets-fe/custom-tab-bar/index.json b/pets-fe/custom-tab-bar/index.json new file mode 100644 index 0000000..a89ef4d --- /dev/null +++ b/pets-fe/custom-tab-bar/index.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} +} diff --git a/pets-fe/custom-tab-bar/index.wxml b/pets-fe/custom-tab-bar/index.wxml new file mode 100644 index 0000000..0fcc290 --- /dev/null +++ b/pets-fe/custom-tab-bar/index.wxml @@ -0,0 +1,7 @@ + diff --git a/pets-fe/custom-tab-bar/index.wxss b/pets-fe/custom-tab-bar/index.wxss new file mode 100644 index 0000000..40fcb52 --- /dev/null +++ b/pets-fe/custom-tab-bar/index.wxss @@ -0,0 +1,16 @@ +.tabbar{ + position:fixed;left:0;right:0;bottom:0;z-index:50; + height:calc(156rpx + env(safe-area-inset-bottom)); + padding-bottom:calc(16rpx + env(safe-area-inset-bottom)); + background:rgba(255,255,255,.94); + border-top:1rpx solid #EEE6DC; + display:flex; + backdrop-filter:blur(32rpx); + -webkit-backdrop-filter:blur(32rpx); +} +.tab{ + flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8rpx; + color:#8D8277;font-size:24rpx; +} +.tab-icon{font-size:42rpx;line-height:1} +.tab.active{color:#DD8420;font-weight:900} diff --git a/pets-fe/pages/community/community.js b/pets-fe/pages/community/community.js new file mode 100644 index 0000000..4e2c34d --- /dev/null +++ b/pets-fe/pages/community/community.js @@ -0,0 +1,94 @@ +const store = require('../../utils/store.js'); +const api = require('../../utils/api.js'); + +const TAG_CLASS = { 求助: 'warn', 经验: 'blue', 精选: 'purple', 避坑: 'red', 晒宠: '' }; + +function mapPost(p) { + const tags = p.tags || []; + const tag = tags[0] || ''; + return { + id: p.id, + author_name: p.author_name, + author_emoji: p.author_emoji || '🐾', + content: p.content, + images: p.images || [], + like_count: p.like_count, + comment_count: p.comment_count, + liked: false, + tag, + tagClass: TAG_CLASS[tag] || '', + }; +} + +Page({ + data: { + pet: {}, + feedTabs: ['推荐', '关注', '新手求助', '晒宠', '经验'], + feedIdx: 0, + posts: [], + sheetShow: false, + sheetType: '', + sheetPostId: 0, + }, + onLoad() { + this._unsub = store.subscribe((pet) => this.setData({ pet })); + }, + onUnload() { + if (this._unsub) this._unsub(); + }, + onShow() { + if (typeof this.getTabBar === 'function' && this.getTabBar()) { + this.getTabBar().setData({ selected: 4 }); + } + store + .ready() + .then(() => { + this.setData({ pet: store.getPet() }); + this.loadPosts(); + }) + .catch((e) => wx.showToast({ title: e.message || '加载失败', icon: 'none' })); + }, + loadPosts() { + const tab = this.data.feedTabs[this.data.feedIdx]; + api + .listPosts(tab, 1) + .then((page) => this.setData({ posts: (page.list || []).map(mapPost) })) + .catch(() => {}); + }, + switchFeedTab(e) { + this.setData({ feedIdx: e.currentTarget.dataset.index }); + this.loadPosts(); + }, + likePost(e) { + const i = e.currentTarget.dataset.index; + const post = this.data.posts[i]; + api + .likePost(post.id) + .then((res) => { + this.setData({ [`posts[${i}].like_count`]: res.like_count, [`posts[${i}].liked`]: true }); + }) + .catch(() => {}); + }, + openComments(e) { + this.setData({ sheetPostId: e.currentTarget.dataset.id, sheetType: 'comments', sheetShow: true }); + }, + openSheet(e) { + this.setData({ sheetType: e.currentTarget.dataset.type, sheetPostId: 0, sheetShow: true }); + }, + closeSheet() { + this.setData({ sheetShow: false }); + }, + onPosted() { + this.setData({ sheetShow: false }); + this.loadPosts(); + }, + onCommented() { + this.loadPosts(); + }, + goLearn() { + wx.navigateTo({ url: '/pages/learn/learn' }); + }, + onFab() { + this.setData({ sheetType: 'ai', sheetPostId: 0, sheetShow: true }); + }, +}); diff --git a/pets-fe/pages/community/community.json b/pets-fe/pages/community/community.json new file mode 100644 index 0000000..24ea6ea --- /dev/null +++ b/pets-fe/pages/community/community.json @@ -0,0 +1,7 @@ +{ + "usingComponents": { + "nav-bar": "/components/nav-bar/nav-bar", + "bottom-sheet": "/components/bottom-sheet/bottom-sheet", + "fab": "/components/fab/fab" + } +} diff --git a/pets-fe/pages/community/community.wxml b/pets-fe/pages/community/community.wxml new file mode 100644 index 0000000..abf79c0 --- /dev/null +++ b/pets-fe/pages/community/community.wxml @@ -0,0 +1,44 @@ + + + + + + 宠友圈 + 和宠友交流、晒宠、求助与经验分享。 + + + + {{item}} + + + + {{pet.emoji}} + 分享一下今天和 {{pet.name}} 的日常... + + + + + + {{item.author_emoji}} + {{item.author_name}}宠友 + {{item.tag}} + + {{item.content}} + + {{img}} + + + {{item.liked ? '♥' : '♡'}} {{item.like_count}} + 💬 {{item.comment_count}} + ↗ 分享 + + + + 还没有帖子,来发第一条吧 🐾 + + + + + diff --git a/pets-fe/pages/community/community.wxss b/pets-fe/pages/community/community.wxss new file mode 100644 index 0000000..42e37a4 --- /dev/null +++ b/pets-fe/pages/community/community.wxss @@ -0,0 +1,48 @@ +.page-body{padding:8rpx 40rpx calc(210rpx + env(safe-area-inset-bottom))} +.pt-h2{font-size:46rpx;letter-spacing:-.6rpx;font-weight:800} +.pt-p{color:var(--muted);font-size:26rpx;margin-top:10rpx} + +.feed-tabs{white-space:nowrap;padding:0 0 24rpx} +.feed-tab{ + display:inline-block;border:1rpx solid var(--line);background:#fff;border-radius:999rpx; + height:68rpx;line-height:68rpx;padding:0 26rpx;color:var(--muted);font-size:26rpx;font-weight:900; + box-shadow:0 12rpx 32rpx rgba(70,48,25,.05);margin-right:16rpx; +} +.feed-tab.active{color:var(--primary-dark);background:#FFF4E4;border-color:#FFD39A} + +.compose-card{ + display:flex;gap:20rpx;align-items:center;background:#fff;border-radius:44rpx; + box-shadow:var(--shadow);padding:24rpx;margin-bottom:28rpx; +} +.compose-input{ + flex:1;height:76rpx;line-height:76rpx;border-radius:999rpx;background:#FBFAF8; + border:1rpx solid var(--line);color:var(--muted);padding:0 26rpx;font-size:26rpx; + overflow:hidden;white-space:nowrap;text-overflow:ellipsis; +} + +.post-card{background:#fff;border-radius:48rpx;box-shadow:var(--shadow);padding:30rpx;margin-bottom:28rpx} +.post-head{display:flex;align-items:center;gap:20rpx;margin-bottom:22rpx} +.post-avatar{ + width:84rpx;height:84rpx;border-radius:32rpx;background:#FFF0D8; + display:flex;align-items:center;justify-content:center;font-size:44rpx;flex:none; +} +.post-user{flex:1} +.pu-b{font-size:28rpx;display:block;font-weight:700} +.pu-s{font-size:24rpx;color:var(--muted)} +.post-content{font-size:28rpx;line-height:1.62;color:#443D37;margin-bottom:24rpx} + +.photo-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12rpx;margin-bottom:24rpx} +.photo-tile{ + aspect-ratio:1;border-radius:32rpx;background:linear-gradient(135deg,#FFE6BA,#FFF8ED); + display:flex;align-items:center;justify-content:center;font-size:60rpx; +} + +.post-actions{display:flex;justify-content:space-around;border-top:1rpx solid var(--line);padding-top:20rpx} +.pa-btn{color:var(--muted);font-weight:900;font-size:26rpx} +.pa-btn.liked{color:var(--red)} + +.empty-hint{text-align:center;color:var(--muted);font-size:26rpx;padding:80rpx 0} + +/* 页面不自身滚动,改由 page-scroll 承载滚动(隐藏滚动条)*/ +page{height:100vh;overflow:hidden;display:flex;flex-direction:column} +.page-scroll{flex:1;min-height:0} diff --git a/pets-fe/pages/home/home.js b/pets-fe/pages/home/home.js new file mode 100644 index 0000000..222394a --- /dev/null +++ b/pets-fe/pages/home/home.js @@ -0,0 +1,130 @@ +const store = require('../../utils/store.js'); +const api = require('../../utils/api.js'); + +function mapTask(t) { + return { + id: t.id, + title: t.title, + sub: t.description, + priority: t.priority, + done: t.done, + sheet: t.sheet_type, + }; +} + +Page({ + data: { + pet: {}, + summary: { greeting: '', insights: [], week: [], advice: '', health_pct: 0, health_status: '正常' }, + tasks: [], + firstArticle: null, + sheetShow: false, + sheetType: '', + }, + onLoad() { + this._unsub = store.subscribe((pet) => { + this.setData({ pet }); + this.loadAll(); + }); + }, + onUnload() { + if (this._unsub) this._unsub(); + }, + onShow() { + if (typeof this.getTabBar === 'function' && this.getTabBar()) { + this.getTabBar().setData({ selected: 0 }); + } + store + .ready() + .then(() => { + this.setData({ pet: store.getPet() }); + this.loadAll(); + }) + .catch((e) => wx.showToast({ title: e.message || '加载失败', icon: 'none' })); + }, + loadAll() { + this.loadSummary(); + this.loadTasks(); + if (!this.data.firstArticle) { + api + .listArticles() + .then((list) => { + if (list && list[0]) { + this.setData({ firstArticle: { icon: list[0].icon, title: list[0].title, desc: list[0].description } }); + } + }) + .catch(() => {}); + } + }, + loadSummary() { + const id = store.currentPetId(); + if (!id) return; + api + .homeSummary(id) + .then((summary) => this.setData({ summary })) + .catch(() => {}); + }, + loadTasks() { + const id = store.currentPetId(); + if (!id) return; + api + .getTasks(id) + .then((tasks) => this.setData({ tasks: (tasks || []).map(mapTask) })) + .catch(() => {}); + }, + onTapTask(e) { + const i = e.currentTarget.dataset.index; + const t = this.data.tasks[i]; + if (t.sheet) { + this.openSheetType(t.sheet); + return; + } + api + .toggleTask(t.id) + .then((res) => { + this.setData({ [`tasks[${i}].done`]: res.done }); + this.loadSummary(); + }) + .catch(() => {}); + }, + completeTasks() { + const id = store.currentPetId(); + if (!id) return; + api + .completeAllTasks(id) + .then((tasks) => { + this.setData({ tasks: (tasks || []).map(mapTask) }); + this.loadSummary(); + }) + .catch(() => {}); + }, + openSheet(e) { + this.openSheetType(e.currentTarget.dataset.type); + }, + openSheetType(type) { + this.setData({ sheetType: type, sheetShow: true }); + }, + closeSheet() { + this.setData({ sheetShow: false }); + this.loadAll(); + }, + onSheetSave() {}, + onAddPet() { + this.openSheetType('addPet'); + }, + onEditPet() { + this.openSheetType('editPet'); + }, + onFab() { + this.openSheetType('ai'); + }, + goProfile() { + wx.navigateTo({ url: '/pages/profile/profile' }); + }, + goLearn() { + wx.navigateTo({ url: '/pages/learn/learn' }); + }, + goRecord() { + wx.switchTab({ url: '/pages/record/record' }); + }, +}); diff --git a/pets-fe/pages/home/home.json b/pets-fe/pages/home/home.json new file mode 100644 index 0000000..075a8b2 --- /dev/null +++ b/pets-fe/pages/home/home.json @@ -0,0 +1,8 @@ +{ + "usingComponents": { + "nav-bar": "/components/nav-bar/nav-bar", + "pet-switch": "/components/pet-switch/pet-switch", + "bottom-sheet": "/components/bottom-sheet/bottom-sheet", + "fab": "/components/fab/fab" + } +} diff --git a/pets-fe/pages/home/home.wxml b/pets-fe/pages/home/home.wxml new file mode 100644 index 0000000..eac90be --- /dev/null +++ b/pets-fe/pages/home/home.wxml @@ -0,0 +1,93 @@ + + + + + + + {{summary.greeting || '你好'}} + 今天也要照顾好 {{pet.name}} 🐾 + + + 🔔 + + + + + + + + + {{pet.emoji}} + + {{pet.name}} + {{pet.age}}|{{pet.weight}}|{{pet.stage}} + + {{summary.health_status || '正常'}} + + 本周健康完成度{{summary.health_pct}}% + + + {{item.bold}}{{item.text}} + + + + + + {{item.weekday}}{{item.day}} + + + + + + 今日任务 + 全部完成 + + + + + {{item.title}}{{item.sub}} + {{item.priority}} + + + + + + + 快速记录 + 更多 + + + ⚖️体重 + 💩便便 + 🍽️饮食 + 🤒异常 + 💰消费 + 💊用药 + 📷照片 + 💉疫苗 + + + + + + AI 今日建议 + 问问 AI + + {{summary.advice || '正在生成今日建议…'}} + + + + + 新手内容 + 查看 + + + {{firstArticle.icon}} + {{firstArticle.title}}{{firstArticle.desc}} + + + + + + + diff --git a/pets-fe/pages/home/home.wxss b/pets-fe/pages/home/home.wxss new file mode 100644 index 0000000..ff58450 --- /dev/null +++ b/pets-fe/pages/home/home.wxss @@ -0,0 +1,59 @@ +.page-body{padding:8rpx 36rpx calc(210rpx + env(safe-area-inset-bottom))} + +.top-row{display:flex;align-items:center;justify-content:space-between;margin:4rpx 0 28rpx} +.greet-h2{font-size:44rpx;line-height:1.15;letter-spacing:-.8rpx;font-weight:800} +.greet-p{font-size:26rpx;color:var(--muted);margin-top:10rpx} +.icon-stack{display:flex;gap:18rpx} +.icon-btn{ + width:80rpx;height:80rpx;border-radius:50%;background:#fff;box-shadow:var(--shadow); + font-size:36rpx;display:flex;align-items:center;justify-content:center; +} + +.hero-card{ + background: + radial-gradient(circle at 95% 16%, rgba(245,169,71,.25), transparent 30%), + linear-gradient(135deg,#FFFFFF,#FFF5E6); + min-height:340rpx; +} +.pet-row{display:flex;align-items:center;gap:26rpx;margin-bottom:28rpx} +.pet-main{flex:1} +.pet-name-b{font-size:42rpx;letter-spacing:-.4rpx;font-weight:800} +.pet-meta{color:var(--muted);font-size:26rpx;margin-top:8rpx} + +.insight-row{white-space:nowrap;margin-top:24rpx} +.insight{ + display:inline-block;min-width:224rpx;border-radius:32rpx; + background:rgba(255,255,255,.72);border:1rpx solid rgba(255,255,255,.9); + padding:20rpx;font-size:24rpx;margin-right:16rpx;vertical-align:top; +} +.insight-b{display:block;font-size:28rpx;margin-bottom:8rpx;font-weight:900} + +.date-strip{display:grid;grid-template-columns:repeat(7,1fr);gap:14rpx;margin-bottom:28rpx} +.day{ + background:#fff;border-radius:32rpx;min-height:116rpx;text-align:center;padding:16rpx 8rpx; + border:1rpx solid var(--line);color:var(--muted);font-size:22rpx; +} +.day-b{display:block;color:var(--text);font-size:30rpx;margin-top:8rpx;font-weight:700} +.day.active{border-color:#FFD39A;background:#FFF4E4;color:var(--primary-dark)} +.day.has-dot::after{content:"";display:block;width:10rpx;height:10rpx;border-radius:50%;background:var(--green);margin:8rpx auto 0} + +.tt-b{font-size:28rpx;font-weight:700} +.tt-p{font-size:24rpx;color:var(--muted);margin-top:8rpx;line-height:1.45} + +.quick-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:18rpx} +.quick{ + background:#FBFAF8;border-radius:32rpx;min-height:132rpx; + display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12rpx; + font-weight:900;color:var(--text);font-size:24rpx; +} +.q-ic{font-size:44rpx} + +.ai-banner{background:linear-gradient(135deg,#F2FAF6,#FFFFFF)} +.ai-text{color:#59635D;font-size:28rpx;line-height:1.6} + +.ac-b{font-size:30rpx;font-weight:700} +.ac-p{color:var(--muted);font-size:24rpx;line-height:1.45;margin-top:10rpx} + +/* 页面不自身滚动,改由 page-scroll 承载滚动(隐藏滚动条)*/ +page{height:100vh;overflow:hidden;display:flex;flex-direction:column} +.page-scroll{flex:1;min-height:0} diff --git a/pets-fe/pages/learn/learn.js b/pets-fe/pages/learn/learn.js new file mode 100644 index 0000000..e96c6c7 --- /dev/null +++ b/pets-fe/pages/learn/learn.js @@ -0,0 +1,35 @@ +const store = require('../../utils/store.js'); +const api = require('../../utils/api.js'); + +Page({ + data: { + articles: [], + sheetShow: false, + sheetType: '', + }, + onLoad() { + store + .ready() + .then(() => api.listArticles()) + .then((list) => { + this.setData({ + articles: (list || []).map((a) => ({ + icon: a.icon, + title: a.title, + desc: a.description, + type: a.related_sheet_type || 'vaccine', + })), + }); + }) + .catch((e) => wx.showToast({ title: e.message || '加载失败', icon: 'none' })); + }, + openSheet(e) { + this.setData({ sheetType: e.currentTarget.dataset.type, sheetShow: true }); + }, + closeSheet() { + this.setData({ sheetShow: false }); + }, + onFab() { + this.setData({ sheetType: 'ai', sheetShow: true }); + }, +}); diff --git a/pets-fe/pages/learn/learn.json b/pets-fe/pages/learn/learn.json new file mode 100644 index 0000000..24ea6ea --- /dev/null +++ b/pets-fe/pages/learn/learn.json @@ -0,0 +1,7 @@ +{ + "usingComponents": { + "nav-bar": "/components/nav-bar/nav-bar", + "bottom-sheet": "/components/bottom-sheet/bottom-sheet", + "fab": "/components/fab/fab" + } +} diff --git a/pets-fe/pages/learn/learn.wxml b/pets-fe/pages/learn/learn.wxml new file mode 100644 index 0000000..1afaa4d --- /dev/null +++ b/pets-fe/pages/learn/learn.wxml @@ -0,0 +1,18 @@ + + + + + + 新手养宠知识 + 新手养宠知识,看完顺手生成你的计划。 + + + + {{item.icon}} + {{item.title}}{{item.desc}} + + + + + + diff --git a/pets-fe/pages/learn/learn.wxss b/pets-fe/pages/learn/learn.wxss new file mode 100644 index 0000000..b646141 --- /dev/null +++ b/pets-fe/pages/learn/learn.wxss @@ -0,0 +1,9 @@ +.page-body{padding:8rpx 40rpx calc(60rpx + env(safe-area-inset-bottom))} +.pt-h2{font-size:46rpx;letter-spacing:-.6rpx;font-weight:800} +.pt-p{color:var(--muted);font-size:26rpx;margin-top:10rpx} +.ac-b{font-size:30rpx;font-weight:700} +.ac-p{color:var(--muted);font-size:24rpx;line-height:1.45;margin-top:10rpx} + +/* 页面不自身滚动,改由 page-scroll 承载滚动(隐藏滚动条)*/ +page{height:100vh;overflow:hidden;display:flex;flex-direction:column} +.page-scroll{flex:1;min-height:0} diff --git a/pets-fe/pages/onboarding/onboarding.js b/pets-fe/pages/onboarding/onboarding.js new file mode 100644 index 0000000..705b11a --- /dev/null +++ b/pets-fe/pages/onboarding/onboarding.js @@ -0,0 +1,101 @@ +const store = require('../../utils/store.js'); +const api = require('../../utils/api.js'); + +const GENDERS = ['男孩', '女孩', '不确定']; + +function ageFromBirthday(bdayStr) { + if (!bdayStr) return ''; + const b = new Date(bdayStr); + const now = new Date(); + let months = (now.getFullYear() - b.getFullYear()) * 12 + (now.getMonth() - b.getMonth()); + if (months < 0) months = 0; + return months < 24 ? months + '个月' : Math.floor(months / 12) + '岁'; +} + +Page({ + data: { + step: 0, + petType: '猫猫', + petEmoji: '🐱', + petName: '', + birthday: '', + genderIdx: 0, + weight: '', + stage: '幼年期', + goals: [ + { label: '不知道每天该做什么', on: true }, + { label: '疫苗驱虫怕忘记', on: true }, + { label: '想记录体重和健康', on: false }, + { label: '担心生病不会判断', on: false }, + { label: '想控制养宠开销', on: false }, + { label: '想生成成长报告', on: false }, + ], + submitting: false, + }, + onLoad() { + // 已有宠物则直接进首页 + store + .ready() + .then(() => { + if (store.getPets().length > 0) { + wx.switchTab({ url: '/pages/home/home' }); + } + }) + .catch(() => {}); + }, + nextStep() { + this.setData({ step: Math.min(this.data.step + 1, 5) }); + }, + choosePet(e) { + const { type, emoji } = e.currentTarget.dataset; + this.setData({ petType: type, petEmoji: emoji }); + }, + onName(e) { this.setData({ petName: e.detail.value }); }, + onWeight(e) { this.setData({ weight: e.detail.value }); }, + onBirthday(e) { this.setData({ birthday: e.detail.value }); }, + selectGender(e) { this.setData({ genderIdx: Number(e.currentTarget.dataset.index) }); }, + // 先随便看看:不建数据,直接进首页浏览(无宠物时首页显示空态) + skipBrowse() { wx.switchTab({ url: '/pages/home/home' }); }, + savePetNext() { + if (!(this.data.petName || '').trim()) { + wx.showToast({ title: '给它起个名字吧', icon: 'none' }); + return; + } + this.nextStep(); + }, + chooseStage(e) { + this.setData({ stage: e.currentTarget.dataset.stage }); + }, + toggleGoal(e) { + const i = e.currentTarget.dataset.index; + this.setData({ [`goals[${i}].on`]: !this.data.goals[i].on }); + }, + async submit() { + if (this.data.submitting) return; + this.setData({ submitting: true }); + wx.showLoading({ title: '生成计划中...' }); + const body = { + name: (this.data.petName || '').trim(), + emoji: this.data.petEmoji, + type: this.data.petType, + gender: GENDERS[this.data.genderIdx], + birthday: this.data.birthday, + weight: this.data.weight, + stage: this.data.stage, + age: ageFromBirthday(this.data.birthday), + goals: this.data.goals.filter((g) => g.on).map((g) => g.label), + }; + try { + await store.ready(); + await api.onboarding(body); + await store.loadPets(); + wx.hideLoading(); + wx.switchTab({ url: '/pages/home/home' }); + } catch (e) { + wx.hideLoading(); + wx.showToast({ title: e.message || '创建失败', icon: 'none' }); + this.setData({ submitting: false }); + } + }, + finishOnboard() { this.submit(); }, +}); diff --git a/pets-fe/pages/onboarding/onboarding.json b/pets-fe/pages/onboarding/onboarding.json new file mode 100644 index 0000000..9cfbe54 --- /dev/null +++ b/pets-fe/pages/onboarding/onboarding.json @@ -0,0 +1,6 @@ +{ + "usingComponents": { + "nav-bar": "/components/nav-bar/nav-bar" + }, + "navigationBarTitleText": "毛孩子计划" +} diff --git a/pets-fe/pages/onboarding/onboarding.wxml b/pets-fe/pages/onboarding/onboarding.wxml new file mode 100644 index 0000000..96e7901 --- /dev/null +++ b/pets-fe/pages/onboarding/onboarding.wxml @@ -0,0 +1,101 @@ + + + + + + + + + 毛孩子计划 + 新手养宠的 AI 日历、健康记录和成长报告。先生成计划,再每天照着做。 + + + 30天新手计划 + 8类快速记录 + AI观察建议 + + + 先随便看看 + + + + + 你的毛孩子是? + 目前支持猫和狗,选一个开始。 + + + 🐱猫猫幼猫、成年猫、老年猫 + + + 🐶狗狗幼犬、成年犬、老年犬 + + + + + + + + 告诉我一点它的信息 + 先填基础信息,之后可随时补充。 + + + + + + {{birthday || '选择日期'}} + + + 男孩 + 女孩 + 不确定 + + + + + + + + + + 它现在处于哪个阶段? + 阶段会决定今日任务、提醒和 AI 建议。 + + + 幼年期疫苗、驱虫、体重增长、行为训练 + + 刚到家 0-30 天适应环境、饮食稳定、基础照护 + + 成年期体重、饮食、日常健康管理 + + 老年期慢病、体检、用药和护理 + + + + + + + 你最想解决什么? + 多选,我们会据此为你定制计划。 + + + {{item.on ? '✓' : ''}}{{item.label}} + + + + + + + + 正在生成专属养宠计划 + 正在根据它的信息生成专属计划… + + 已识别:{{stage}} + 已生成:30 天新手计划 + 已安排:疫苗 / 驱虫提醒 + 已开启:体重、便便、饮食记录 + 已准备:首张成长报告卡片 + + + + + diff --git a/pets-fe/pages/onboarding/onboarding.wxss b/pets-fe/pages/onboarding/onboarding.wxss new file mode 100644 index 0000000..efdbd3d --- /dev/null +++ b/pets-fe/pages/onboarding/onboarding.wxss @@ -0,0 +1,55 @@ +page{height:100vh;overflow:hidden;display:flex;flex-direction:column} +.page-scroll{flex:1;min-height:0} +.onboard-page{ + padding:0 40rpx 56rpx; + min-height:100%; + background: + radial-gradient(circle at top right, rgba(245,169,71,.22), transparent 45%), + var(--bg); +} +.step{animation:fade .24s ease} +.skip-link{text-align:center;margin-top:28rpx;color:var(--muted);font-size:28rpx;padding:12rpx} +@keyframes fade{from{opacity:0;transform:translateY(16rpx)}to{opacity:1;transform:none}} + +.hero{text-align:center;padding:60rpx 0 32rpx} +.pet-logo{ + width:192rpx;height:192rpx;margin:0 auto 36rpx;border-radius:60rpx; + background:linear-gradient(135deg,#FFE0AB,#FDB35C); + display:flex;align-items:center;justify-content:center;font-size:96rpx; + box-shadow:0 32rpx 72rpx rgba(245,169,71,.26); +} +.hero-h1{font-size:64rpx;letter-spacing:-1.2rpx;font-weight:800} +.hero-sub{margin-top:20rpx;line-height:1.65;color:var(--muted);font-size:30rpx} + +.proof-row{display:grid;grid-template-columns:repeat(3,1fr);gap:16rpx;margin:40rpx 0} +.proof{background:#fff;border-radius:36rpx;padding:24rpx 16rpx;text-align:center;box-shadow:var(--shadow)} +.proof-b{display:block;font-size:32rpx;font-weight:800} +.proof-s{color:var(--muted);font-size:22rpx} + +.step-title{font-size:48rpx;letter-spacing:-.8rpx;line-height:1.24;margin:16rpx 0;font-weight:800} +.step-sub{color:var(--muted);line-height:1.6;font-size:28rpx;margin-bottom:32rpx} + +.choice-grid{display:grid;grid-template-columns:1fr 1fr;gap:24rpx;margin-bottom:36rpx} +.choice-card{ + background:#fff;border:4rpx solid transparent;border-radius:48rpx;min-height:264rpx; + padding:36rpx 28rpx;box-shadow:var(--shadow);transition:.18s ease; +} +.choice-card.selected{border-color:var(--primary);background:#FFF8EC} +.choice-card .animal{font-size:88rpx;margin-bottom:20rpx} +.choice-b{font-size:34rpx;font-weight:900} +.choice-s{display:block;color:var(--muted);font-size:24rpx;margin-top:12rpx} + +.form-card{background:#fff;border-radius:48rpx;padding:32rpx;box-shadow:var(--shadow);margin-bottom:36rpx} + +.stage-list{display:flex;flex-direction:column;gap:22rpx;margin-bottom:36rpx} +.stage-card{background:#fff;border-radius:40rpx;border:4rpx solid transparent;padding:30rpx;box-shadow:var(--shadow)} +.stage-card.selected{border-color:var(--green);background:var(--green-soft)} +.stage-b{font-size:32rpx;font-weight:900} +.stage-p{color:var(--muted);font-size:24rpx;margin-top:10rpx} + +.gen-card{margin-top:32rpx;background:#fff;border-radius:56rpx;padding:48rpx;box-shadow:var(--shadow2)} +.gen-line{display:flex;align-items:center;gap:20rpx;padding:20rpx 0;color:#5F554D;font-size:28rpx} +.tick{ + width:48rpx;height:48rpx;border-radius:50%;background:var(--green);color:#fff; + display:flex;align-items:center;justify-content:center;font-size:28rpx;font-weight:900;flex:none; +} diff --git a/pets-fe/pages/plan/plan.js b/pets-fe/pages/plan/plan.js new file mode 100644 index 0000000..74b759a --- /dev/null +++ b/pets-fe/pages/plan/plan.js @@ -0,0 +1,133 @@ +const store = require('../../utils/store.js'); +const api = require('../../utils/api.js'); + +function buildCells(year, month, taskedDays, today) { + const first = new Date(year, month - 1, 1); + const lead = (first.getDay() + 6) % 7; // 周一为一周起点 + const daysInMonth = new Date(year, month, 0).getDate(); + const set = {}; + (taskedDays || []).forEach((d) => (set[d] = true)); + const cells = []; + for (let i = 0; i < lead; i++) cells.push({ day: 0 }); + for (let d = 1; d <= daysInMonth; d++) { + cells.push({ day: d, tasked: !!set[d], today: d === today }); + } + return cells; +} + +Page({ + data: { + pet: {}, + planView: 'timeline', + plan: null, + calCells: [], + calLabel: '', + calLoaded: false, + aiInput: '', + aiPlan: null, + aiBusy: false, + sheetShow: false, + sheetType: '', + }, + onLoad() { + this._unsub = store.subscribe((pet) => { + this.setData({ pet, calLoaded: false }); + this.loadTimeline(); + if (this.data.planView === 'calendar') this.loadCalendar(); + }); + }, + onUnload() { + if (this._unsub) this._unsub(); + }, + onShow() { + if (typeof this.getTabBar === 'function' && this.getTabBar()) { + this.getTabBar().setData({ selected: 1 }); + } + store + .ready() + .then(() => { + this.setData({ pet: store.getPet() }); + this.loadTimeline(); + }) + .catch((e) => wx.showToast({ title: e.message || '加载失败', icon: 'none' })); + }, + loadTimeline() { + const id = store.currentPetId(); + if (!id) return; + api + .getPlan(id) + .then((plan) => this.setData({ plan })) + .catch(() => this.setData({ plan: null })); + }, + loadCalendar() { + const id = store.currentPetId(); + if (!id) return; + api + .planCalendar(id) + .then((res) => { + this.setData({ + calCells: buildCells(res.year, res.month, res.tasked_days, res.today), + calLabel: `${res.year} 年 ${res.month} 月`, + calLoaded: true, + }); + }) + .catch(() => {}); + }, + switchPlanView(e) { + const view = e.currentTarget.dataset.view; + this.setData({ planView: view }); + if (view === 'calendar' && !this.data.calLoaded) this.loadCalendar(); + }, + // 计划明细勾选 + toggleTimelineTask(e) { + const id = e.currentTarget.dataset.id; + api.togglePlanTask(id).then(() => this.loadTimeline()).catch(() => {}); + }, + // AI 计划 + onAiInput(e) { + this.setData({ aiInput: e.detail.value }); + }, + genAIPlan() { + const id = store.currentPetId(); + const input = (this.data.aiInput || '').trim(); + if (!id || !input) { + wx.showToast({ title: '先描述一下情况', icon: 'none' }); + return; + } + this.setData({ aiBusy: true }); + wx.showLoading({ title: 'AI 生成中...' }); + api + .createAIPlan(id, input) + .then((plan) => { + wx.hideLoading(); + this.setData({ aiPlan: plan, aiBusy: false }); + }) + .catch((err) => { + wx.hideLoading(); + this.setData({ aiBusy: false }); + wx.showToast({ title: err.message || '生成失败', icon: 'none' }); + }); + }, + applyAIPlan() { + const p = this.data.aiPlan; + if (!p) return; + api + .applyAIPlan(p.id) + .then(() => wx.showToast({ title: '已加入我的计划', icon: 'success' })) + .catch((err) => wx.showToast({ title: err.message || '加入失败', icon: 'none' })); + }, + openSheet(e) { + const type = e.currentTarget.dataset.type; + if (!type) return; // 无关联动作的计划项只可勾选,不弹层 + this.setData({ sheetType: type, sheetShow: true }); + }, + closeSheet() { + this.setData({ sheetShow: false }); + }, + onAddPet() { + this.setData({ sheetType: 'addPet', sheetShow: true }); + }, + onFab() { + this.setData({ sheetType: 'ai', sheetShow: true }); + }, +}); diff --git a/pets-fe/pages/plan/plan.json b/pets-fe/pages/plan/plan.json new file mode 100644 index 0000000..075a8b2 --- /dev/null +++ b/pets-fe/pages/plan/plan.json @@ -0,0 +1,8 @@ +{ + "usingComponents": { + "nav-bar": "/components/nav-bar/nav-bar", + "pet-switch": "/components/pet-switch/pet-switch", + "bottom-sheet": "/components/bottom-sheet/bottom-sheet", + "fab": "/components/fab/fab" + } +} diff --git a/pets-fe/pages/plan/plan.wxml b/pets-fe/pages/plan/plan.wxml new file mode 100644 index 0000000..1a7bb87 --- /dev/null +++ b/pets-fe/pages/plan/plan.wxml @@ -0,0 +1,85 @@ + + + + + + 养宠计划 + 按宠物查看路线图、日历与 AI 计划。 + + + + + + 路线图 + 日历 + AI 计划 + + + + + + + {{pet.name}}的 30 天计划 + {{plan.completion_pct || 0}}% + + 完成度{{plan.completion_pct || 0}}% + + + + + {{item.day_label}} + + {{item.done ? '✓' : ''}} + + {{item.title}}{{item.description}} + + + + 暂无计划 + + + + + + + {{calLabel}}提醒 + + + {{item.day || ''}} + + + + + + + + AI 计划模式Plan + 描述 {{pet.name}} 的近况(阶段、近期变化、担心的问题),AI 会提取关键信息并生成可执行计划。 + + + + + + 已提取信息 + + 阶段{{aiPlan.extracted.stage}} + 风险{{aiPlan.extracted.risk}} + 重点{{aiPlan.extracted.priority}} + 提醒{{aiPlan.extracted.reminder}} + + 生成的计划 + + + + {{item.day_label}}· {{item.title}}{{item.description}} + + + + + + + + + + diff --git a/pets-fe/pages/plan/plan.wxss b/pets-fe/pages/plan/plan.wxss new file mode 100644 index 0000000..57b8e10 --- /dev/null +++ b/pets-fe/pages/plan/plan.wxss @@ -0,0 +1,54 @@ +.page-body{padding:8rpx 40rpx calc(210rpx + env(safe-area-inset-bottom))} +.pt-h2{font-size:46rpx;letter-spacing:-.6rpx;font-weight:800} +.pt-p{color:var(--muted);font-size:26rpx;margin-top:10rpx} +.tt-b{font-size:28rpx;font-weight:700} +.tt-p{font-size:24rpx;color:var(--muted);margin-top:8rpx;line-height:1.45} +.ac-b{font-size:30rpx;font-weight:700} +.ac-p{color:var(--muted);font-size:24rpx;line-height:1.45;margin-top:10rpx} + +.inline-tabs{display:flex;padding:8rpx;gap:8rpx;border-radius:32rpx;background:#EFE8DE;margin-bottom:28rpx} +.it-btn{ + flex:1;height:68rpx;line-height:68rpx;text-align:center;border-radius:24rpx; + color:var(--muted);font-weight:900;font-size:26rpx; +} +.it-btn.active{background:#fff;color:var(--primary-dark);box-shadow:0 12rpx 28rpx rgba(0,0,0,.06)} + +.timeline{padding-left:36rpx;position:relative} +.timeline::before{content:"";position:absolute;left:12rpx;top:12rpx;bottom:16rpx;width:4rpx;background:#E8DED0} +.time-block{position:relative;margin-bottom:28rpx} +.time-label{font-weight:900;color:var(--primary-dark);font-size:26rpx;margin-bottom:16rpx;position:relative} +.time-label::before{ + content:"";position:absolute;left:-34rpx;top:8rpx;width:24rpx;height:24rpx;border-radius:50%; + background:var(--primary);border:6rpx solid #fff;box-shadow:0 0 0 4rpx #F5D5A8; +} +.plan-task{background:#fff;border-radius:36rpx;padding:26rpx;box-shadow:var(--shadow);border:1rpx solid transparent;display:flex;gap:18rpx;align-items:flex-start} +.plan-task.done{opacity:.55} +.pk-check{ + width:44rpx;height:44rpx;border-radius:50%;border:4rpx solid #DED1C4;flex:none;margin-top:2rpx; + display:flex;align-items:center;justify-content:center;color:#fff;font-size:24rpx;font-weight:900; +} +.plan-task.done .pk-check{background:var(--green);border-color:var(--green)} +.pk-body{flex:1} +.pk-b{font-size:28rpx;font-weight:700} +.pk-p{color:var(--muted);font-size:24rpx;line-height:1.45;margin-top:10rpx} +.ai-plan-tip{color:var(--muted);font-size:26rpx;line-height:1.6;margin-bottom:16rpx} + +.calendar{background:#fff;border-radius:44rpx;padding:28rpx;box-shadow:var(--shadow)} +.cal-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:20rpx;font-weight:900;font-size:30rpx} +.cal-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:12rpx;text-align:center} +.cal-w{font-size:22rpx;color:var(--muted);padding:8rpx 0} +.cal-cell{ + height:76rpx;border-radius:24rpx;display:flex;align-items:center;justify-content:center; + font-weight:800;font-size:26rpx;background:#FBFAF8;position:relative; +} +.cal-cell.blank{background:transparent} +.cal-cell.today{background:#FFF3DF;color:var(--primary-dark)} +.cal-cell.tasked::after{content:"";position:absolute;bottom:10rpx;width:10rpx;height:10rpx;border-radius:50%;background:var(--green)} + +.extracted{display:grid;grid-template-columns:1fr 1fr;gap:16rpx;margin:24rpx 0} +.extract{background:#FBFAF8;border-radius:30rpx;padding:20rpx;font-size:24rpx} +.ex-b{display:block;font-size:26rpx;margin-bottom:6rpx;font-weight:700} + +/* 页面不自身滚动,改由 page-scroll 承载滚动(隐藏滚动条)*/ +page{height:100vh;overflow:hidden;display:flex;flex-direction:column} +.page-scroll{flex:1;min-height:0} diff --git a/pets-fe/pages/profile/profile.js b/pets-fe/pages/profile/profile.js new file mode 100644 index 0000000..adc54e2 --- /dev/null +++ b/pets-fe/pages/profile/profile.js @@ -0,0 +1,55 @@ +const store = require('../../utils/store.js'); +const api = require('../../utils/api.js'); + +function completion(pet) { + if (!pet) return 0; + const fields = ['name', 'emoji', 'type', 'gender', 'birthday', 'weight', 'stage', 'age', 'color', 'breed']; + const filled = fields.filter((f) => pet[f]).length; + return Math.round((filled / fields.length) * 100); +} + +Page({ + data: { + pet: {}, + user: {}, + summary: { pets: 0, records: 0, reminders: 0, pro_status: 'none' }, + profilePct: 0, + sheetShow: false, + sheetType: '', + }, + onLoad() { + this._unsub = store.subscribe((pet) => this.setData({ pet, profilePct: completion(pet) })); + }, + onUnload() { + if (this._unsub) this._unsub(); + }, + onShow() { + store + .ready() + .then(() => { + const pet = store.getPet(); + this.setData({ pet, user: store.getUser() || {}, profilePct: completion(pet) }); + this.loadSummary(); + }) + .catch((e) => wx.showToast({ title: e.message || '加载失败', icon: 'none' })); + }, + loadSummary() { + api.userSummary().then((summary) => this.setData({ summary })).catch(() => {}); + }, + openSheet(e) { + this.setData({ sheetType: e.currentTarget.dataset.type, sheetShow: true }); + }, + onAddPet() { + this.setData({ sheetType: 'addPet', sheetShow: true }); + }, + onEditPet() { + this.setData({ sheetType: 'editPet', sheetShow: true }); + }, + closeSheet() { + this.setData({ sheetShow: false }); + this.loadSummary(); + }, + onFab() { + this.setData({ sheetType: 'ai', sheetShow: true }); + }, +}); diff --git a/pets-fe/pages/profile/profile.json b/pets-fe/pages/profile/profile.json new file mode 100644 index 0000000..24ea6ea --- /dev/null +++ b/pets-fe/pages/profile/profile.json @@ -0,0 +1,7 @@ +{ + "usingComponents": { + "nav-bar": "/components/nav-bar/nav-bar", + "bottom-sheet": "/components/bottom-sheet/bottom-sheet", + "fab": "/components/fab/fab" + } +} diff --git a/pets-fe/pages/profile/profile.wxml b/pets-fe/pages/profile/profile.wxml new file mode 100644 index 0000000..2514d7b --- /dev/null +++ b/pets-fe/pages/profile/profile.wxml @@ -0,0 +1,33 @@ + + + + + + {{pet.emoji}} + {{pet.name}} + {{pet.age}}|{{pet.weight}}|{{pet.stage}} + {{pet.breed}}{{pet.breed && pet.color ? ' · ' : ''}}{{pet.color}} + + + + + + 宠物档案(编辑)已完善 {{profilePct}}% › + 提醒设置 {{summary.reminders}} 条 + 健康记录 {{summary.records}} 条 + 多宠物管理 {{summary.pets}} 只 + + + + 会员权益 {{summary.pro_status === 'active' ? '已开通' : '未开通'}} + 数据导出 PDF + 意见反馈 去填写 + 关于我们 v0.2 + + + + + + + + diff --git a/pets-fe/pages/profile/profile.wxss b/pets-fe/pages/profile/profile.wxss new file mode 100644 index 0000000..8ac7d2a --- /dev/null +++ b/pets-fe/pages/profile/profile.wxss @@ -0,0 +1,18 @@ +.page-body{padding:8rpx 40rpx calc(60rpx + env(safe-area-inset-bottom))} + +.profile-header{text-align:center;padding:24rpx 0 32rpx} +.ph-avatar{margin:0 auto 20rpx;width:152rpx;height:152rpx;font-size:76rpx;border-radius:56rpx} +.ph-name{font-size:44rpx;font-weight:800} +.ph-meta{font-size:26rpx;margin-top:10rpx} + +.profile-list{background:#fff;border-radius:48rpx;box-shadow:var(--shadow);overflow:hidden;margin-bottom:28rpx} +.profile-row{ + display:flex;align-items:center;justify-content:space-between;min-height:104rpx;padding:0 32rpx; + border-bottom:1rpx solid var(--line);font-size:28rpx;font-weight:800; +} +.profile-row:last-child{border-bottom:0} +.pr-val{color:var(--muted);font-weight:700;font-size:26rpx} + +/* 页面不自身滚动,改由 page-scroll 承载滚动(隐藏滚动条)*/ +page{height:100vh;overflow:hidden;display:flex;flex-direction:column} +.page-scroll{flex:1;min-height:0} diff --git a/pets-fe/pages/record/record.js b/pets-fe/pages/record/record.js new file mode 100644 index 0000000..f5b1434 --- /dev/null +++ b/pets-fe/pages/record/record.js @@ -0,0 +1,79 @@ +const store = require('../../utils/store.js'); +const api = require('../../utils/api.js'); + +function pad(n) { + return n < 10 ? '0' + n : '' + n; +} +function fmtTime(iso) { + if (!iso) return ''; + const d = new Date(iso); + const now = new Date(); + const hm = pad(d.getHours()) + ':' + pad(d.getMinutes()); + if (d.toDateString() === now.toDateString()) return '今天 ' + hm; + const y = new Date(now.getTime() - 86400000); + if (d.toDateString() === y.toDateString()) return '昨天 ' + hm; + return d.getMonth() + 1 + '月' + d.getDate() + '日'; +} +function mapRecord(r) { + return { + icon: r.icon || '✍️', + title: r.title, + desc: fmtTime(r.occurred_at) + (r.description ? '|' + r.description : ''), + }; +} + +Page({ + data: { + pet: {}, + timeline: [], + sheetShow: false, + sheetType: '', + }, + onLoad() { + this._unsub = store.subscribe((pet) => { + this.setData({ pet }); + this.loadRecords(); + }); + }, + onUnload() { + if (this._unsub) this._unsub(); + }, + onShow() { + if (typeof this.getTabBar === 'function' && this.getTabBar()) { + this.getTabBar().setData({ selected: 2 }); + } + store + .ready() + .then(() => { + this.setData({ pet: store.getPet() }); + this.loadRecords(); + }) + .catch((e) => wx.showToast({ title: e.message || '加载失败', icon: 'none' })); + }, + loadRecords() { + const id = store.currentPetId(); + if (!id) return; + api + .getRecords(id) + .then((records) => this.setData({ timeline: (records || []).map(mapRecord) })) + .catch(() => {}); + }, + openSheet(e) { + this.openSheetType(e.currentTarget.dataset.type); + }, + openSheetType(type) { + this.setData({ sheetType: type, sheetShow: true }); + }, + closeSheet() { + this.setData({ sheetShow: false }); + }, + onSheetSaved() { + this.loadRecords(); + }, + onAddPet() { + this.openSheetType('addPet'); + }, + onFab() { + this.openSheetType('ai'); + }, +}); diff --git a/pets-fe/pages/record/record.json b/pets-fe/pages/record/record.json new file mode 100644 index 0000000..075a8b2 --- /dev/null +++ b/pets-fe/pages/record/record.json @@ -0,0 +1,8 @@ +{ + "usingComponents": { + "nav-bar": "/components/nav-bar/nav-bar", + "pet-switch": "/components/pet-switch/pet-switch", + "bottom-sheet": "/components/bottom-sheet/bottom-sheet", + "fab": "/components/fab/fab" + } +} diff --git a/pets-fe/pages/record/record.wxml b/pets-fe/pages/record/record.wxml new file mode 100644 index 0000000..6e97c71 --- /dev/null +++ b/pets-fe/pages/record/record.wxml @@ -0,0 +1,44 @@ + + + + + + 快速记录 + 先选择宠物,再记录体重、饮食、异常和消费。 + + + + + + ⚖️体重 + 🍽️饮食 + 💩便便 + 🤒异常 + 💊用药 + 💰消费 + 💉疫苗 + 🛡️驱虫 + 📷照片 + + + + 体重趋势记录 + + + + + + + 健康时间轴摘要 + + + {{item.icon}} + {{item.title}}{{item.desc}} + + + + + + + + diff --git a/pets-fe/pages/record/record.wxss b/pets-fe/pages/record/record.wxss new file mode 100644 index 0000000..3435896 --- /dev/null +++ b/pets-fe/pages/record/record.wxss @@ -0,0 +1,32 @@ +.page-body{padding:8rpx 40rpx calc(210rpx + env(safe-area-inset-bottom))} +.pt-h2{font-size:46rpx;letter-spacing:-.6rpx;font-weight:800} +.pt-p{color:var(--muted);font-size:26rpx;margin-top:10rpx} + +.record-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:24rpx;margin-bottom:28rpx} +.record-item{ + height:184rpx;background:#fff;border-radius:44rpx;box-shadow:var(--shadow); + display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16rpx; + font-weight:900;color:var(--text);font-size:28rpx; +} +.ri-ic{font-size:56rpx} + +.mini-chart{ + height:256rpx;border-radius:36rpx;margin-top:24rpx;position:relative;overflow:hidden; + background: + linear-gradient(180deg,rgba(115,190,157,.16),rgba(115,190,157,0)), + repeating-linear-gradient(0deg,#fff,#fff 60rpx,#EFE8DE 62rpx); +} +.chart-line{position:absolute;left:36rpx;right:36rpx;top:44rpx;bottom:44rpx;width:auto;height:auto} + +.health-timeline{display:flex;flex-direction:column;gap:20rpx} +.health-event{display:flex;gap:20rpx;background:#FBFAF8;border-radius:32rpx;padding:24rpx} +.event-dot{ + width:68rpx;height:68rpx;border-radius:26rpx;flex:none; + display:flex;align-items:center;justify-content:center;background:#FFF0D7;font-size:34rpx; +} +.he-b{font-size:28rpx;font-weight:700} +.he-p{color:var(--muted);font-size:24rpx;line-height:1.45;margin-top:6rpx} + +/* 页面不自身滚动,改由 page-scroll 承载滚动(隐藏滚动条)*/ +page{height:100vh;overflow:hidden;display:flex;flex-direction:column} +.page-scroll{flex:1;min-height:0} diff --git a/pets-fe/pages/report/report.js b/pets-fe/pages/report/report.js new file mode 100644 index 0000000..4dee6eb --- /dev/null +++ b/pets-fe/pages/report/report.js @@ -0,0 +1,62 @@ +const store = require('../../utils/store.js'); +const api = require('../../utils/api.js'); + +Page({ + data: { + pet: {}, + report: null, + gainText: '+0.0', + bill: null, + summary: null, + sheetShow: false, + sheetType: '', + }, + onLoad() { + this._unsub = store.subscribe((pet) => { + this.setData({ pet }); + this.loadData(); + }); + }, + onUnload() { + if (this._unsub) this._unsub(); + }, + onShow() { + if (typeof this.getTabBar === 'function' && this.getTabBar()) { + this.getTabBar().setData({ selected: 3 }); + } + store + .ready() + .then(() => { + this.setData({ pet: store.getPet() }); + this.loadData(); + }) + .catch((e) => wx.showToast({ title: e.message || '加载失败', icon: 'none' })); + }, + loadData() { + const id = store.currentPetId(); + if (!id) return; + Promise.all([api.weeklyReport(id), api.getBill(id, 'month'), api.healthSummary(id)]) + .then(([report, bill, summary]) => { + const g = report.weight_gain || 0; + this.setData({ + report, + gainText: (g >= 0 ? '+' : '') + g.toFixed(1), + bill, + summary, + }); + }) + .catch(() => {}); + }, + openSheet(e) { + this.setData({ sheetType: e.currentTarget.dataset.type, sheetShow: true }); + }, + closeSheet() { + this.setData({ sheetShow: false }); + }, + onAddPet() { + this.setData({ sheetType: 'addPet', sheetShow: true }); + }, + onFab() { + this.setData({ sheetType: 'ai', sheetShow: true }); + }, +}); diff --git a/pets-fe/pages/report/report.json b/pets-fe/pages/report/report.json new file mode 100644 index 0000000..075a8b2 --- /dev/null +++ b/pets-fe/pages/report/report.json @@ -0,0 +1,8 @@ +{ + "usingComponents": { + "nav-bar": "/components/nav-bar/nav-bar", + "pet-switch": "/components/pet-switch/pet-switch", + "bottom-sheet": "/components/bottom-sheet/bottom-sheet", + "fab": "/components/fab/fab" + } +} diff --git a/pets-fe/pages/report/report.wxml b/pets-fe/pages/report/report.wxml new file mode 100644 index 0000000..5e175f6 --- /dev/null +++ b/pets-fe/pages/report/report.wxml @@ -0,0 +1,59 @@ + + + + + + 成长报告 + 每只宠物的成长报告、账单与健康摘要。 + + + + + + 本周成长报告{{report.health_status || '稳定成长'}} + {{report.summary || '暂无本周数据'}} + + {{report.tasks_completed || 0}}完成任务 + {{gainText}}kg 增长 + {{report.high_risk_count || 0}}高风险异常 + + + + + + + + + 健康摘要导出 + 疫苗进度{{summary.vaccine_progress || '—'}} + 驱虫状态{{summary.deworm_status || '—'}} + 体重趋势{{summary.weight_trend || '—'}} + 异常记录{{summary.anomaly_count || 0}} 次 + + + + 养宠账单记一笔 + + ¥{{bill.total || 0}}本月花费 + ¥{{bill.max_single || 0}}最大支出 + {{bill.categories.length || 0}}消费类别 + + + + {{item.category}} + + ¥{{item.amount}} + + + + + + Pro 会员 + 解锁 365 天计划、PDF 健康档案、月度报告、多宠物管理与年度账单。 + + + + + + + diff --git a/pets-fe/pages/report/report.wxss b/pets-fe/pages/report/report.wxss new file mode 100644 index 0000000..3a8f708 --- /dev/null +++ b/pets-fe/pages/report/report.wxss @@ -0,0 +1,19 @@ +.page-body{padding:8rpx 40rpx calc(210rpx + env(safe-area-inset-bottom))} +.pt-h2{font-size:46rpx;letter-spacing:-.6rpx;font-weight:800} +.pt-p{color:var(--muted);font-size:26rpx;margin-top:10rpx} + +.report-card{background:linear-gradient(135deg,#FFFFFF,#F2FAF6)} +.rc-text{color:var(--muted);font-size:28rpx;line-height:1.6} +.stat-b{display:block;font-size:36rpx;margin-bottom:8rpx;font-weight:800} +.stat-s{font-size:22rpx;color:var(--muted)} + +.bill-bars{display:flex;flex-direction:column;gap:20rpx;margin-top:24rpx} +.bill-row{display:grid;grid-template-columns:88rpx 1fr 108rpx;gap:16rpx;align-items:center;font-size:24rpx} +.bill-cat{color:var(--text)} +.bill-amt{font-weight:900;text-align:right} +.mini-bar{height:18rpx;border-radius:999rpx;background:#F0E7DD;overflow:hidden} +.mini-bar-fill{display:block;height:100%;border-radius:999rpx;background:linear-gradient(90deg,var(--blue),#A9C8F6)} + +/* 页面不自身滚动,改由 page-scroll 承载滚动(隐藏滚动条)*/ +page{height:100vh;overflow:hidden;display:flex;flex-direction:column} +.page-scroll{flex:1;min-height:0} diff --git a/pets-fe/preview.html b/pets-fe/preview.html new file mode 100644 index 0000000..24c6290 --- /dev/null +++ b/pets-fe/preview.html @@ -0,0 +1,2385 @@ + + + + + + 毛孩子计划 Pro UI - 微信小程序交互原型 + + + +
+
+
+
9:41●●● 5G ▰
+
+ + 毛孩子计划 +
··· ○
+
+ + +
+
+
+ +

毛孩子计划

+

新手养宠的 AI 日历、健康记录和成长报告。
先生成计划,再每天照着做。

+
+
+
30天新手计划
+
8类快速记录
+
AI观察建议
+
+ + +
+ +
+

你的毛孩子是?

+

第一版建议只支持猫狗,容易做深,不要一开始做全品类。

+
+
+
🐱
猫猫幼猫、成年猫、老年猫 +
+
+
🐶
狗狗幼犬、成年犬、老年犬 +
+
+ +
+ +
+

告诉我一点它的信息

+

这里越轻越好,先让用户完成建档,再逐步补充信息。

+
+
+
+
+
+
+ +
+ +
+

它现在处于哪个阶段?

+

阶段会决定今日任务、提醒和 AI 建议。

+
+
幼年期

疫苗、驱虫、体重增长、行为训练

+
刚到家 0-30 天

适应环境、饮食稳定、基础照护

+
成年期

体重、饮食、日常健康管理

+
老年期

慢病、体检、用药和护理

+
+ +
+ +
+

你最想解决什么?

+

多选。这个信息适合后续用于推荐计划模板和付费能力。

+
+
不知道每天该做什么
+
疫苗驱虫怕忘记
+
想记录体重和健康
+
担心生病不会判断
+
想控制养宠开销
+
想生成成长报告
+
+ +
+ +
+

正在生成专属养宠计划

+

真实产品中可做成 2 秒的 AI 生成动画,增强完成感。

+
+
已识别:幼年期
+
已生成:30 天新手计划
+
已安排:疫苗 / 驱虫提醒
+
已开启:体重、便便、饮食记录
+
已准备:首张成长报告卡片
+
+ +
+
+ + +
+
+
+

早上好,Aiden

+

今天也要照顾好 奶球 🐾

+
+
+ + +
+
+ +
+ + + +
+ +
+
+
🐱
+
+ 奶球 +

4个月|2.8kg幼年期

+
+ 正常 +
+
本周健康完成度68%
+
+
+
5天后第 2 针疫苗
+
+0.2kg本周体重
+
2天连续记录
+
+
+ +
+
29
+
30
+
1
+
2
+
3
+
4
+
5
+
+ +
+
+

今日任务

+ +
+
+
记录一次体重

幼年期建议每周至少记录 2 次

重要
+
观察饮水和排便

换粮、应激都可能影响排便状态

+
检查疫苗预约

第 2 针疫苗还有 5 天

+
+
+ +
+
+

快速记录

+ +
+
+ + + + + + + + +
+
+ +
+
+

AI 今日建议

+ +
+

奶球处于快速成长阶段,本周建议重点记录体重和排便。若出现连续呕吐、精神明显变差或便血,应尽快就医。

+
+ +
+
+

新手内容

+ +
+
+
💉
+
幼猫疫苗和驱虫怎么安排?

把内容引流和计划生成打通,适合冷启动获客。

+
+
+
+ + +
+
+

养宠计划

+

计划页也要按宠物切换,避免多宠家庭计划混在一起。

+
+ +
+ + + +
+ +
+ + + +
+ +
+
+
+

奶球的 30 天计划

+ 12/30 +
+
完成度40%
+
+
+
+
今天
观察排便状态

记录颜色、形态、次数,发现软便可连续观察。

+
明天
检查疫苗预约

距离下一针还有 5 天,提前确认医院和时间。

+
第 7 天
体重趋势检查

幼年期每周称重,观察是否稳定增长。

+
第 14 天
复盘饮食与便便

如果近期换粮,建议把换粮过程和异常记录合并查看。

+
+
+ +
+
+
2026 年 7 月
+
+ +
29
30
1
2
3
4
5
+
6
7
8
9
10
11
12
+
13
14
15
16
17
18
19
+
20
21
22
23
24
25
26
+
+
+
+

7月2日任务

3项
+
+
记录体重

预计 1 分钟完成

+
观察排便

建议拍照留存

+
+
+
+ +
+
+

AI 计划模式

Plan
+
+
我会先提取关键信息,再生成可执行计划。你可以审核后确认。
+
奶球 4 个月,最近刚换粮,担心软便,也快打第二针疫苗。
+
我提取到了这些信息,请确认是否准确。
+
+
+
阶段幼年期 / 疫苗期
+
风险换粮软便
+
重点排便观察
+
提醒第二针疫苗
+
+
+ + +
+
+
+

推荐计划模板

+
+
🍽️
+
7 天换粮观察计划

每日记录饮食、便便、精神状态,自动生成复盘。

+
+
+
💉
+
疫苗前后护理计划

含洗澡避坑、异常观察、就医摘要。

+
+
+
+
+ + +
+
+

快速记录

+

先选择宠物,再记录体重、饮食、异常和消费。

+
+ +
+ + + +
+ +
+ + + + + + + + + +
+ +
+

体重趋势

+
+
+ + + + +
+
+
+ +
+

健康时间轴

+
+
⚖️
体重 2.8kg

今天 09:12|较上次 +0.2kg

+
💩
排便正常

昨天 20:18|颜色和形态正常

+
💰
猫粮 ¥168

7月1日|食品类支出

+
+
+
+ + +
+
+

成长报告

+

不同宠物的报告、账单和健康摘要要完全分开。

+
+ +
+ + + +
+ +
+

本周成长报告

稳定成长
+

本周 奶球 完成 6 项任务,体重 +0.2kg,无高风险异常记录。

+
+
6完成任务
+
+0.2kg 增长
+
0高风险异常
+
+
+ + +
+
+ +
+

健康摘要

+
疫苗进度2/3,即将到期
+
驱虫状态下次 7月15日
+
体重趋势稳定增长
+
异常记录低风险 1 次
+
+ +
+

养宠账单

+
+
¥326本月花费
+
¥168最大支出
+
3消费类别
+
+
+
食品
¥168
+
用品
¥96
+
医疗
¥62
+
+
+ +
+

Pro 会员

变现点
+

365 天计划、PDF 健康档案、月度报告、多宠物管理和年度账单都适合做付费权益。

+ +
+
+ + + +
+
+

宠友圈

+

社区适合做交流、晒宠和内容增长,但不要压过工具主线。

+
+ +
+ + + + + +
+ +
+
🐱
+
分享一下今天和 奶球 的日常...
+ +
+ +
+
+
🐱
+
奶球的铲屎官幼猫新手|刚刚
+ 求助 +
+
刚换粮第 2 天有一点软便,精神和食欲都正常。大家一般会观察几天?我已经在记录里保存了便便状态。
+
+
🍽️
+
💩
+
🐱
+
+
+ + + +
+
+ +
+
+
🐶
+
布丁今天不拆家幼犬训练|12 分钟前
+ 经验 +
+
分享一个幼犬到家 7 天适应计划:先固定睡觉区域,再固定喂食时间,最后做短时间独处训练。不要一开始就全天陪着。
+
+
🐶
+
🏠
+
🧸
+
+
+ + + +
+
+ +
+
+
🐾
+
毛孩子计划官方精选内容|今天
+ 精选 +
+
新手帖:猫咪软便不一定马上就医,但要记录持续时间、精神状态、食欲和是否便血。可以从异常记录里生成就医前摘要。
+
+ + + +
+
+
+ + +
+
+

新手养宠知识

+

内容页负责冷启动获客,结尾引导生成计划。

+
+
💉
幼猫疫苗和驱虫怎么安排?

适合搜索流量和社群传播,引导生成提醒。

+
💩
猫咪软便要不要去医院?

接异常记录和 AI 观察建议。

+
🍽️
7 天换粮计划怎么做?

接计划模板,适合一次性付费。

+
💰
一个月养猫大概花多少钱?

接养宠账本和年度账单。

+
+ + +
+
+
🐱
+

奶球

+

4个月|2.8kg幼年期

+
+
+
宠物档案 已完善 72%
+
提醒设置 已开启
+
健康记录 12 条
+
多宠物管理 Pro
+
+
+
会员权益 未开通
+
数据导出 PDF
+
意见反馈 去填写
+
关于我们 v0.2
+
+ +
+ + + + + +
+
+
+
+
+ + +
+ + + + \ No newline at end of file diff --git a/pets-fe/project.config.json b/pets-fe/project.config.json new file mode 100644 index 0000000..e64b764 --- /dev/null +++ b/pets-fe/project.config.json @@ -0,0 +1,53 @@ +{ + "description": "毛孩子计划 - 新手养宠小程序", + "packOptions": { + "ignore": [], + "include": [] + }, + "setting": { + "urlCheck": false, + "es6": true, + "enhance": true, + "postcss": true, + "preloadBackgroundData": false, + "minified": false, + "newFeature": true, + "coverView": true, + "nodeModules": false, + "autoAudits": false, + "showShadowRootInWxmlPanel": true, + "scopeDataCheck": false, + "uglifyFileName": false, + "checkInvalidKey": true, + "checkSiteMap": true, + "uploadWithSourceMap": true, + "compileHotReLoad": false, + "babelSetting": { + "ignore": [], + "disablePlugins": [], + "outputPath": "" + }, + "compileWorklet": false, + "packNpmManually": false, + "packNpmRelationList": [], + "minifyWXSS": true, + "minifyWXML": true, + "localPlugins": false, + "disableUseStrict": false, + "useCompilerPlugins": false, + "condition": false, + "swc": false, + "disableSWC": true + }, + "compileType": "miniprogram", + "libVersion": "3.5.0", + "appid": "wx52dfc635739a9c19", + "projectname": "pets-fe", + "condition": {}, + "editorSetting": { + "tabIndent": "insertSpaces", + "tabSize": 2 + }, + "miniprogramRoot": "./", + "simulatorPluginLibVersion": {} +} \ No newline at end of file diff --git a/pets-fe/project.private.config.json b/pets-fe/project.private.config.json new file mode 100644 index 0000000..937cea3 --- /dev/null +++ b/pets-fe/project.private.config.json @@ -0,0 +1,22 @@ +{ + "libVersion": "3.13.0", + "projectname": "pets-fe", + "condition": {}, + "setting": { + "urlCheck": false, + "coverView": true, + "lazyloadPlaceholderEnable": false, + "skylineRenderEnable": false, + "preloadBackgroundData": false, + "autoAudits": false, + "useApiHook": true, + "showShadowRootInWxmlPanel": true, + "useStaticServer": false, + "useLanDebug": false, + "showES6CompileOption": false, + "compileHotReLoad": true, + "checkInvalidKey": true, + "ignoreDevUnusedFiles": true, + "bigPackageSizeSupport": false + } +} \ No newline at end of file diff --git a/pets-fe/sitemap.json b/pets-fe/sitemap.json new file mode 100644 index 0000000..292c4ed --- /dev/null +++ b/pets-fe/sitemap.json @@ -0,0 +1,4 @@ +{ + "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", + "rules": [{ "action": "allow", "page": "*" }] +} diff --git a/pets-fe/utils/api.js b/pets-fe/utils/api.js new file mode 100644 index 0000000..1c3c5e6 --- /dev/null +++ b/pets-fe/utils/api.js @@ -0,0 +1,99 @@ +const { request, uploadFile, setToken } = require('./request.js'); + +// wx.login 取 code +function wxLogin() { + return new Promise((resolve, reject) => { + wx.login({ + success: (r) => (r.code ? resolve(r.code) : reject(new Error('wx.login 未返回 code'))), + fail: reject, + }); + }); +} + +// 登录:优先微信 code2session,失败回退开发态 Mock 登录(后端 dev_login=true) +async function login() { + try { + const code = await wxLogin(); + const data = await request({ url: '/api/auth/wechat', method: 'POST', data: { code } }); + setToken(data.token); + return data; + } catch (e) { + console.warn('[api] 微信登录失败,回退 Mock 登录:', e && e.message); + const data = await request({ url: '/api/auth/login', method: 'POST', data: { nickname: '开发用户' } }); + setToken(data.token); + return data; + } +} + +const api = { + login, + uploadFile, + + // 用户 + getProfile: () => request({ url: '/api/user/profile' }), + updateProfile: (body) => request({ url: '/api/user/profile', method: 'PUT', data: body }), + + // 用户 + userSummary: () => request({ url: '/api/user/summary' }), + + // 宠物 + getPets: () => request({ url: '/api/pets' }), + homeSummary: (id) => request({ url: `/api/pets/${id}/home-summary` }), + getPet: (id) => request({ url: `/api/pets/${id}` }), + createPet: (body) => request({ url: '/api/pets', method: 'POST', data: body }), + updatePet: (id, body) => request({ url: `/api/pets/${id}`, method: 'PUT', data: body }), + deletePet: (id) => request({ url: `/api/pets/${id}`, method: 'DELETE' }), + onboarding: (body) => request({ url: '/api/onboarding', method: 'POST', data: body }), + + // 记录 + getRecords: (id, type) => + request({ url: `/api/pets/${id}/records${type ? `?type=${type}` : ''}` }), + createRecord: (id, body) => request({ url: `/api/pets/${id}/records`, method: 'POST', data: body }), + weightTrend: (id) => request({ url: `/api/pets/${id}/records/weight-trend` }), + + // 任务 + getTasks: (id) => request({ url: `/api/pets/${id}/tasks` }), + toggleTask: (taskId) => request({ url: `/api/tasks/${taskId}/toggle`, method: 'POST' }), + completeAllTasks: (id) => request({ url: `/api/pets/${id}/tasks/complete-all`, method: 'POST' }), + + // 计划 + getPlan: (id) => request({ url: `/api/pets/${id}/plan` }), + planCalendar: (id, month) => + request({ url: `/api/pets/${id}/plan/calendar${month ? `?month=${month}` : ''}` }), + createAIPlan: (id, input) => request({ url: `/api/pets/${id}/ai-plan`, method: 'POST', data: { input } }), + applyAIPlan: (planId) => request({ url: `/api/ai-plan/${planId}/apply`, method: 'POST' }), + togglePlanTask: (taskId) => request({ url: `/api/plan-tasks/${taskId}/toggle`, method: 'POST' }), + + // 提醒 + getReminders: (id) => request({ url: `/api/pets/${id}/reminders` }), + + // 报告 + weeklyReport: (id) => request({ url: `/api/pets/${id}/report/weekly` }), + getBill: (id, period) => request({ url: `/api/pets/${id}/bill?period=${period || 'month'}` }), + healthSummary: (id) => request({ url: `/api/pets/${id}/health-summary` }), + getPoster: (id) => request({ url: `/api/pets/${id}/poster` }), + + // 社区 + listPosts: (tab, page) => + request({ url: `/api/posts?tab=${encodeURIComponent(tab || '')}&page=${page || 1}&page_size=10` }), + createPost: (body) => request({ url: '/api/posts', method: 'POST', data: body }), + likePost: (id) => request({ url: `/api/posts/${id}/like`, method: 'POST' }), + listComments: (id) => request({ url: `/api/posts/${id}/comments` }), + createComment: (id, content) => + request({ url: `/api/posts/${id}/comments`, method: 'POST', data: { content } }), + + // 文章 + listArticles: () => request({ url: '/api/articles' }), + getArticle: (id) => request({ url: `/api/articles/${id}` }), + + // Pro + getPro: () => request({ url: '/api/pro' }), + activatePro: () => request({ url: '/api/pro/activate', method: 'POST' }), + + // AI + aiChat: (body) => request({ url: '/api/ai/chat', method: 'POST', data: body }), + assessSymptom: (id, body) => + request({ url: `/api/pets/${id}/ai/assess-symptom`, method: 'POST', data: body }), +}; + +module.exports = api; diff --git a/pets-fe/utils/config.js b/pets-fe/utils/config.js new file mode 100644 index 0000000..0884c9d --- /dev/null +++ b/pets-fe/utils/config.js @@ -0,0 +1,5 @@ +// 后端地址。本地联调:微信开发者工具需在「详情 → 本地设置」勾选「不校验合法域名」。 +// 上线时改为你的 https 域名,并在微信公众平台配置 request 合法域名。 +const BASE_URL = 'http://192.168.31.4:8080'; + +module.exports = { BASE_URL }; diff --git a/pets-fe/utils/request.js b/pets-fe/utils/request.js new file mode 100644 index 0000000..0c15602 --- /dev/null +++ b/pets-fe/utils/request.js @@ -0,0 +1,69 @@ +const { BASE_URL } = require('./config.js'); + +const TOKEN_KEY = 'pets_token'; + +function getToken() { + return wx.getStorageSync(TOKEN_KEY) || ''; +} +function setToken(t) { + wx.setStorageSync(TOKEN_KEY, t); +} +function clearToken() { + wx.removeStorageSync(TOKEN_KEY); +} + +// 统一请求:注入 token,解析 {code,message,data},成功返回 data,失败 reject(Error) +function request({ url, method = 'GET', data, header = {} }) { + return new Promise((resolve, reject) => { + const token = getToken(); + wx.request({ + url: BASE_URL + url, + method, + data, + header: Object.assign( + { 'Content-Type': 'application/json' }, + token ? { Authorization: 'Bearer ' + token } : {}, + header, + ), + success(res) { + const body = res.data; + if (body && typeof body === 'object' && 'code' in body) { + if (body.code === 0) return resolve(body.data); + if (body.code === 40100) clearToken(); + return reject(new Error(body.message || '请求失败')); + } + resolve(body); + }, + fail(err) { + reject(new Error((err && err.errMsg) || '网络错误')); + }, + }); + }); +} + +// 文件上传(multipart) +function uploadFile(filePath) { + return new Promise((resolve, reject) => { + const token = getToken(); + wx.uploadFile({ + url: BASE_URL + '/api/upload', + filePath, + name: 'file', + header: token ? { Authorization: 'Bearer ' + token } : {}, + success(res) { + try { + const body = JSON.parse(res.data); + if (body.code === 0) return resolve(body.data); + reject(new Error(body.message || '上传失败')); + } catch (e) { + reject(new Error('上传响应解析失败')); + } + }, + fail(err) { + reject(new Error((err && err.errMsg) || '上传失败')); + }, + }); + }); +} + +module.exports = { request, uploadFile, getToken, setToken, clearToken }; diff --git a/pets-fe/utils/store.js b/pets-fe/utils/store.js new file mode 100644 index 0000000..a37413a --- /dev/null +++ b/pets-fe/utils/store.js @@ -0,0 +1,100 @@ +// 全局状态:登录、宠物列表、当前宠物;服务端驱动 + 发布订阅 +const api = require('./api.js'); + +const state = { + user: null, + pets: [], + currentId: 0, +}; + +const listeners = new Set(); +let bootstrapPromise = null; + +function getPets() { + return state.pets; +} +function getPet() { + return state.pets.find((p) => p.id === state.currentId) || state.pets[0] || {}; +} +function currentPetId() { + const p = getPet(); + return p && p.id; +} + +function notify() { + const pet = getPet(); + listeners.forEach((fn) => { + try { + fn(pet); + } catch (e) { + /* noop */ + } + }); +} + +function subscribe(fn) { + listeners.add(fn); + return () => listeners.delete(fn); +} + +// 登录 + 拉宠物列表(只跑一次) +function ready() { + if (!bootstrapPromise) { + bootstrapPromise = (async () => { + const res = await api.login(); + state.user = res.user; + await loadPets(); + })().catch((e) => { + bootstrapPromise = null; // 允许重试 + throw e; + }); + } + return bootstrapPromise; +} + +async function loadPets() { + const pets = await api.getPets(); + state.pets = pets || []; + if (!state.currentId && state.pets[0]) { + state.currentId = state.pets[0].id; + } + // 当前宠物被删或不存在时回退第一只 + if (!state.pets.find((p) => p.id === state.currentId) && state.pets[0]) { + state.currentId = state.pets[0].id; + } + notify(); + return state.pets; +} + +function switchPet(id) { + if (state.currentId === id) return; + state.currentId = id; + notify(); +} + +// 本地更新当前宠物字段(服务端已改动后同步 UI) +function patchCurrent(patch) { + const pet = getPet(); + Object.assign(pet, patch); + notify(); +} + +function setUser(u) { + state.user = u; +} +function getUser() { + return state.user; +} + +module.exports = { + ready, + loadPets, + getPets, + getPet, + currentPetId, + switchPet, + patchCurrent, + subscribe, + setUser, + getUser, +};