feat: initialize Ballet Island 微信小程序 with basic structure and functionality

- Add project configuration for WeChat mini program.
- Create skills lock file for Taroify integration.
- Set up app configuration and styles.
- Implement main app component and index page with welcome message.
- Add health check service to verify backend readiness.
- Configure TypeScript settings and global types.
This commit is contained in:
yuxuanhui
2026-09-28 13:41:48 +08:00
commit 4b42b55928
32 changed files with 12054 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
---
name: taroify
description: >
Use when a task involves Taroify (@taroify/core, @taroify/icons,
@taroify/hooks, or @taroify/commerce), including writing Taro React UI,
selecting components, checking props or subcomponents, debugging usage,
finding examples, or customizing themes.
allowed-tools:
- Bash(taroify *)
- Bash(npx -y @taroify/cli *)
- Bash(which taroify)
---
# Taroify CLI
Use the bundled offline Taroify Catalog before writing or changing Taroify code. It contains
component and Hook APIs, complete Markdown documentation, focused examples, full Taro Demo pages,
and theme variables.
## Setup
Check for a global binary, otherwise use `npx`:
```bash
which taroify || echo "use npx -y @taroify/cli <command>"
```
Always pass `--format json` when consuming output programmatically.
## Workflow
1. If the correct component is unknown, search by scenario:
```bash
taroify search 上传 --format json
taroify list --category form --format json
```
2. Before writing code, inspect the real API:
```bash
taroify info Button --format json
taroify info Button.Group --format json
```
3. Get a focused example, or request the full Taro page when imports and surrounding context matter:
```bash
taroify demo Button --format json
taroify demo Button demo1 --format json
taroify demo Button --full --format json
```
4. Read the full document only when the structured API and examples are insufficient:
```bash
taroify doc Button --format json
```
5. Query theme variables before customizing component styles:
```bash
taroify token --format json
taroify token Button --format json
```
## Rules
- Query before writing; never guess Taroify component names, Props, enum values, events, or
subcomponents.
- Treat Taroify Props and inherited Taro native capabilities separately. Platform support is
determined by the target Taro adapter.
- Use public imports from `@taroify/core`, `@taroify/icons`, `@taroify/hooks`, or
`@taroify/commerce`.
- Prefer `ConfigProvider` keys or returned CSS variables over internal selectors and hardcoded
component values.
- Do not repeat a tool call with identical arguments.
- Taroify examples use Taro React semantics; do not replace Taro components with Web-only DOM
elements unless the user explicitly targets H5.
## MCP
When `taroify_*` MCP tools are available, prefer them over shell commands. They expose the same
offline Catalog through six read-only tools and also provide document/demo resources.
+13
View File
@@ -0,0 +1,13 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
[*.go]
indent_style = tab
+14
View File
@@ -0,0 +1,14 @@
.DS_Store
node_modules/
dist/
bin/
.cache/
.swc/
coverage/
*.log
.env
.env.*
!.env.example
project.private.config.json
*.tsbuildinfo
+11
View File
@@ -0,0 +1,11 @@
# 项目约定
本仓库包含两个独立项目,按目录分别管理代码、依赖、配置与运行命令。
- `miniprogram/`:Taro React 小程序。Node 配置、`package.json`、pnpm 锁文件、`node_modules/` 和 Taroify skill 均归属此目录。涉及小程序时,先读取 [小程序约定](miniprogram/AGENTS.md),在该目录执行 pnpm 命令。
- `backend/`:Go + PostgreSQL 后端。Go 模块、Dockerfile、Compose、环境配置及开发脚本均归属此目录。使用 Go、Docker 和 shell 命令开发,不依赖 Node.js。
根目录仅保留版本控制、说明文档和通用仓库约定,不建立根级 Node 包或 pnpm workspace。
修改小程序后,在 `miniprogram/` 执行 `pnpm typecheck` 和 `pnpm build`。修改后端后,在 `backend/` 执行 `go vet ./...`、`go test -race ./...` 和 `go build -o bin/api ./cmd/server`;部署配置变更另需检查 Compose 和镜像构建。仅报告实际执行的验证。
+116
View File
@@ -0,0 +1,116 @@
# Ballet Island
同一 Git 仓库中的两个独立项目,分别安装依赖、运行与构建。当前提供项目骨架、欢迎页和服务健康检查,业务需求后续补充。
## 目录
```text
miniprogram/ Taro 4.2.1 + React 18 + TypeScript + Taroify 1.0.6
src/ 小程序源码
config/ 小程序构建配置
package.json 小程序依赖与命令
pnpm-lock.yaml 小程序依赖锁文件
node_modules/ 小程序本地依赖(不提交)
.nvmrc Node 版本
.agents/skills/taroify/ Taroify Agent Skill
backend/ Go 1.26 + PostgreSQL 18
cmd/server/ 服务入口、数据库连接与优雅退出
internal/httpapi/ HTTP 路由与测试
go.mod / go.sum Go 模块与依赖
Dockerfile 后端镜像构建
compose.yaml API 与数据库编排
.env.example 后端环境变量示例
scripts/dev.sh 本机 Go 开发启动脚本
```
小程序的 Node、pnpm 配置和依赖均位于 `miniprogram/`。后端通过 Go、Docker 和 shell 独立运行。根目录保留 Git、说明文档和通用仓库约定。
## 小程序开发
需要 Node.js 22.12+(`miniprogram/.nvmrc` 指定 22.22.1)、pnpm 9.15.0,以及具有该小程序开发权限的微信开发者工具。
从仓库根目录进入小程序项目:
```sh
cd miniprogram
pnpm install --frozen-lockfile
pnpm dev
```
微信开发者工具导入 `miniprogram/`,AppID 为 `wx6dfbf1021db8aee0`,编译目录为 `dist/`。首页“检查连接”请求后端 `/readyz`,同时检查数据库是否可用。
默认 API 为 `http://127.0.0.1:8080`。使用本地 HTTP 地址时,在开发者工具的本地设置中开启“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”,仅用于本地调试。真机需使用能够访问的接口地址,并按微信要求配置域名。
在 `miniprogram/` 中通过构建时环境变量指定接口地址,修改后重启构建:
```sh
TARO_APP_API_BASE_URL=https://api.example.com pnpm dev
TARO_APP_API_BASE_URL=https://api.example.com pnpm build
```
`TARO_APP_API_BASE_URL` 是公开接口地址,不自动读取后端 `.env`;未设置时使用本机默认地址。
## 后端开发
需要 Go 1.26 或更高版本,以及 Docker Compose。仅使用 Docker 运行后端时无需本机 Go。
另开终端,从仓库根目录进入后端项目:
```sh
cd backend
test -f .env || cp .env.example .env
docker compose up -d --wait postgres
./scripts/dev.sh
```
`backend/.env` 已被 Git 忽略。启动脚本读取其中的 PostgreSQL 标准 `PG*` 变量与 `HTTP_ADDR`,先构建再启动 Go 服务;修改代码后需重启命令。脚本只应读取可信的本地配置,`.env` 使用 shell 赋值语法,含空格或 `$` 等特殊字符的值须用单引号包裹。直接运行 Go 或二进制时需自行注入环境变量。
后端全程无需 Node.js 或 pnpm。
## Docker 运行与部署
以下命令在 `backend/` 中执行:
```sh
docker compose up -d --build --wait
curl http://127.0.0.1:8080/healthz
curl http://127.0.0.1:8080/readyz
docker compose logs -f
docker compose down
```
Compose 等待数据库健康后启动 API,并等待两个服务就绪。API 容器使用非 root 用户;数据库写入命名卷,`docker compose down` 保留数据。Compose 项目名固定为 `ballet-island`,沿用现有数据库卷。
默认仅向宿主机回环地址开放 API 和数据库。部署到服务器时,可让 HTTPS 反向代理连接 `127.0.0.1:${API_PORT}`;如确需其他机器直接访问 API,可在 `backend/.env` 调整 `API_BIND`。数据库端口始终只绑定本机。Compose 内部使用 `PGHOST=postgres`、`PGPORT=5432`,宿主机数据库端口由 `.env` 的 `PGPORT` 控制。
`API_PORT` 控制 Docker API 的宿主机端口,`HTTP_ADDR` 控制本机 Go 服务的监听地址;修改后应同步小程序的 API 地址。本机 Go 服务与 Docker API 不应同时占用同一端口。
正式上线需配置 HTTPS、微信 request 合法域名、环境密码与数据库备份。小程序由微信开发者工具发布,Docker 部署后端和数据库。
PostgreSQL 18 数据卷按[官方镜像说明](https://hub.docker.com/_/postgres)挂载到 `/var/lib/postgresql`。已有数据库卷的密码不会因修改 `.env` 自动更新;需按数据库流程修改,不要为更新密码删除数据卷。
## 验证与构建
在 `miniprogram/` 中:
```sh
pnpm typecheck
pnpm build
```
在 `backend/` 中:
```sh
go vet ./...
go test -race ./...
go build -o bin/api ./cmd/server
```
产物分别在 `miniprogram/dist/` 和 `backend/bin/api`,均不进入版本控制。
| 接口 | 用途 | 响应 |
| --- | --- | --- |
| `GET /healthz` | HTTP 服务存活检查 | `200 {"status":"ok"}` |
| `GET /readyz` | 实际检查 PostgreSQL 连接 | 可用时 200,不可用时 `503 {"status":"unavailable"}` |
Go 服务启动时检查数据库连接,收到 `SIGINT` / `SIGTERM` 时停止接收请求并关闭连接池。
+6
View File
@@ -0,0 +1,6 @@
.git
.env*
bin
coverage
*.log
+13
View File
@@ -0,0 +1,13 @@
# Local development only. Set a strong password for a deployed environment.
PGHOST=127.0.0.1
PGPORT=5432
PGDATABASE=ballet_island
PGUSER=ballet_island
PGPASSWORD=change-me-for-local-development
PGSSLMODE=disable
# Host Go process address; Docker always listens on :8080 inside its container.
HTTP_ADDR=127.0.0.1:8080
API_PORT=8080
API_BIND=127.0.0.1
+14
View File
@@ -0,0 +1,14 @@
FROM golang:1.26-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/api ./cmd/server
FROM alpine:3.23
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /out/api /usr/local/bin/api
USER 65532:65532
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/api"]
+84
View File
@@ -0,0 +1,84 @@
package main
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"ballet-island/backend/internal/httpapi"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)))
if err := run(); err != nil {
slog.Error("server stopped", "error", err)
os.Exit(1)
}
}
func run() error {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
for _, key := range []string{"PGHOST", "PGDATABASE", "PGUSER", "PGPASSWORD"} {
if os.Getenv(key) == "" {
return fmt.Errorf("%s is required", key)
}
}
// Use libpq environment variables so passwords need no URL escaping.
pool, err := pgxpool.New(ctx, "")
if err != nil {
return fmt.Errorf("configure database: %w", err)
}
defer pool.Close()
startupCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
err = pool.Ping(startupCtx)
cancel()
if err != nil {
return fmt.Errorf("connect database: %w", err)
}
addr := os.Getenv("HTTP_ADDR")
if addr == "" {
addr = "127.0.0.1:8080"
}
server := &http.Server{
Addr: addr,
Handler: httpapi.NewHandler(pool.Ping),
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
serverErr := make(chan error, 1)
go func() {
slog.Info("server starting", "address", addr)
serverErr <- server.ListenAndServe()
}()
select {
case err := <-serverErr:
if !errors.Is(err, http.ErrServerClosed) {
return err
}
case <-ctx.Done():
slog.Info("server shutting down")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
_ = server.Close()
return fmt.Errorf("shutdown server: %w", err)
}
}
return nil
}
+49
View File
@@ -0,0 +1,49 @@
name: ballet-island
services:
postgres:
image: postgres:18-alpine
restart: unless-stopped
environment:
POSTGRES_DB: ${PGDATABASE:?Set PGDATABASE in .env}
POSTGRES_USER: ${PGUSER:?Set PGUSER in .env}
POSTGRES_PASSWORD: ${PGPASSWORD:?Set PGPASSWORD in .env}
ports:
- "127.0.0.1:${PGPORT:-5432}:5432"
volumes:
# PostgreSQL 18 stores its versioned data directory under this mount.
- postgres_data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U \"$$POSTGRES_USER\" -d \"$$POSTGRES_DB\""]
interval: 5s
timeout: 3s
retries: 10
start_period: 10s
api:
build:
context: .
restart: unless-stopped
environment:
HTTP_ADDR: :8080
PGHOST: postgres
PGPORT: 5432
PGDATABASE: ${PGDATABASE:?Set PGDATABASE in .env}
PGUSER: ${PGUSER:?Set PGUSER in .env}
PGPASSWORD: ${PGPASSWORD:?Set PGPASSWORD in .env}
PGSSLMODE: disable
ports:
- "${API_BIND:-127.0.0.1}:${API_PORT:-8080}:8080"
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-q", "-T", "3", "-O", "/dev/null", "http://127.0.0.1:8080/readyz"]
interval: 10s
timeout: 5s
retries: 3
start_period: 10s
stop_grace_period: 15s
volumes:
postgres_data:
+13
View File
@@ -0,0 +1,13 @@
module ballet-island/backend
go 1.26.0
require github.com/jackc/pgx/v5 v5.11.0
require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/text v0.29.0 // indirect
)
+26
View File
@@ -0,0 +1,26 @@
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/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-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.11.0 h1:IzBBtyK9AHqf98cctWFifYSci2hgQR/cd56wB4p+ogg=
github.com/jackc/pgx/v5 v5.11.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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=
+34
View File
@@ -0,0 +1,34 @@
package httpapi
import (
"context"
"encoding/json"
"net/http"
"time"
)
// NewHandler returns health endpoints. ping must honor its context deadline.
func NewHandler(ping func(context.Context) error) http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
writeStatus(w, http.StatusOK, "ok")
})
mux.HandleFunc("GET /readyz", func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
if err := ping(ctx); err != nil {
// Do not expose database connection details through a public endpoint.
writeStatus(w, http.StatusServiceUnavailable, "unavailable")
return
}
writeStatus(w, http.StatusOK, "ok")
})
return mux
}
func writeStatus(w http.ResponseWriter, code int, status string) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(code)
_ = json.NewEncoder(w).Encode(map[string]string{"status": status})
}
+67
View File
@@ -0,0 +1,67 @@
package httpapi
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestHealthRemainsLiveWithoutDatabase(t *testing.T) {
handler := NewHandler(func(context.Context) error {
t.Fatal("liveness must not query the database")
return nil
})
response := httptest.NewRecorder()
handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/healthz", nil))
if response.Code != http.StatusOK || response.Body.String() != "{\"status\":\"ok\"}\n" {
t.Fatalf("unexpected response: %d %s", response.Code, response.Body.String())
}
}
func TestReadinessReflectsDatabaseState(t *testing.T) {
for _, tc := range []struct {
name string
err error
code int
body string
}{
{"ready", nil, http.StatusOK, "{\"status\":\"ok\"}\n"},
{"unavailable", errors.New("private database details"), http.StatusServiceUnavailable, "{\"status\":\"unavailable\"}\n"},
} {
t.Run(tc.name, func(t *testing.T) {
called := false
handler := NewHandler(func(ctx context.Context) error {
called = true
deadline, ok := ctx.Deadline()
if !ok || time.Until(deadline) > 2*time.Second {
t.Fatal("database check must have a bounded deadline")
}
return tc.err
})
response := httptest.NewRecorder()
handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/readyz", nil))
if !called || response.Code != tc.code || response.Body.String() != tc.body {
t.Fatalf("unexpected response: %d %s", response.Code, response.Body.String())
}
if !strings.HasPrefix(response.Header().Get("Content-Type"), "application/json") {
t.Fatal("health response must be JSON")
}
})
}
}
func TestReadinessPreservesRequestCancellation(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
handler := NewHandler(func(ctx context.Context) error { return ctx.Err() })
response := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/readyz", nil).WithContext(ctx)
handler.ServeHTTP(response, request)
if response.Code != http.StatusServiceUnavailable {
t.Fatalf("got %d, want 503", response.Code)
}
}
+20
View File
@@ -0,0 +1,20 @@
#!/bin/sh
set -eu
backend_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
cd "$backend_dir"
if [ ! -f .env ]; then
printf '%s\n' '缺少 backend/.env,请先从 .env.example 复制并配置。' >&2
exit 1
fi
# Only source trusted local configuration; .env uses shell assignment syntax.
set -a
. ./.env
set +a
go build -o bin/api ./cmd/server
# Replace the shell so shutdown signals reach the Go process directly.
exec ./bin/api
+1
View File
@@ -0,0 +1 @@
22.22.1
+34
View File
@@ -0,0 +1,34 @@
# 小程序约定
本目录是独立的 Node 项目。安装依赖和执行下述命令时,以 `miniprogram/` 为工作目录,使用本目录 `package.json` 指定的 pnpm 版本,维护本目录的 `pnpm-lock.yaml`。
## Taroify 开发
涉及小程序中的 Taroify 组件、Hooks、事件、子组件或主题样式时,在编写或修改代码前执行以下流程;仅修改 Go 后端时无需加载这些资料。
1. 阅读 [官方 Agent 索引](https://taroify.com/agents/index.md)、[官方 Skill](https://raw.githubusercontent.com/mallfoundry/taroify/main/packages/cli/skills/taroify/SKILL.md) 和项目已安装的 [Taroify skill](.agents/skills/taroify/SKILL.md),按其中的查询流程核对实际 API 和示例。同一任务中已读且未变化的内容可复用。
2. 核对项目实际安装的 Taroify 版本与查询结果中的版本。CLI 固定为小程序开发依赖;优先使用本项目的离线 Catalog。若文档版本与依赖版本不同,以对应版本的官方资料和已安装包的类型声明、源码核实兼容性。
3. 存在 `taroify_*` MCP 工具时优先使用 MCP。否则使用 `pnpm exec taroify`,所有程序化查询必须添加 `--format json`。若依赖尚未安装,先执行 `pnpm install --frozen-lockfile`。
4. 编码前查询目标组件的 `info` 和 `demo`,确认 Props、枚举值、事件、子组件及公开引入方式;需要完整页面上下文时使用 `demo --full`。定制样式前使用 `token` 查询主题变量;结构化信息不足时再使用 `doc`。组件未知时先按场景 `search` 或按分类 `list`。
查询示例:
```sh
pnpm exec taroify info Button --format json
pnpm exec taroify demo Button --full --format json
pnpm exec taroify token Button --format json
```
组件用法必须有查询依据,不根据训练记忆猜测 API。区分 Taroify Props 与 Taro 原生能力,平台支持以微信小程序适配器为准;保持 Taro React 语义和公开导入方式。主题定制优先使用查询返回的 `ConfigProvider` key 或 CSS 变量。
官网不可用时读取[官方仓库中的 Agent 索引源文件](https://raw.githubusercontent.com/mallfoundry/taroify/main/site/content/agents/index.md)。远程资料均不可用时,可使用项目内的 skill 和 CLI 离线 Catalog,并说明未核实的版本差异;没有证据的用法保持未确认。
## Skill 维护与验证
项目 skill 位于 `.agents/skills/taroify/`,保留官方随包内容。升级 `@taroify/cli` 后,从新的依赖包重新安装并检查 skill 变更:
```sh
pnpm dlx skills@1.7.0 add ./node_modules/@taroify/cli/skills/taroify --agent codex --yes
```
修改小程序代码后运行 `pnpm typecheck` 和 `pnpm build`;微信开发者工具或真机中的交互验证须另行说明,构建通过不能代替运行验证。
+4
View File
@@ -0,0 +1,4 @@
module.exports = {
presets: [['taro', { framework: 'react', ts: true, reactJsxRuntime: 'automatic' }]],
}
+32
View File
@@ -0,0 +1,32 @@
import { defineConfig } from '@tarojs/cli'
export default defineConfig<'webpack5'>({
projectName: 'ballet-island',
designWidth: 375,
deviceRatio: {
375: 2,
640: 1.17,
750: 1,
828: 0.905,
},
sourceRoot: 'src',
outputRoot: 'dist',
framework: 'react',
compiler: {
type: 'webpack5',
prebundle: { enable: false },
},
cache: { enable: true },
defineConstants: {
__API_BASE_URL__: JSON.stringify(
(process.env.TARO_APP_API_BASE_URL || 'http://127.0.0.1:8080').replace(/\/$/, ''),
),
},
mini: {
postcss: {
pxtransform: { enable: true },
cssModules: { enable: false },
},
},
})
+45
View File
@@ -0,0 +1,45 @@
{
"name": "@ballet-island/miniprogram",
"version": "0.1.0",
"private": true,
"packageManager": "pnpm@9.15.0",
"engines": {
"node": ">=22.12.0 <23",
"pnpm": ">=9 <10"
},
"scripts": {
"dev": "taro build --type weapp --watch",
"build": "taro build --type weapp",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@babel/runtime": "^7.28.4",
"@taroify/core": "1.0.6",
"@tarojs/components": "4.2.1",
"@tarojs/helper": "4.2.1",
"@tarojs/react": "4.2.1",
"@tarojs/runtime": "4.2.1",
"@tarojs/shared": "4.2.1",
"@tarojs/taro": "4.2.1",
"react": "18.3.1",
"react-dom": "18.3.1"
},
"devDependencies": {
"@babel/core": "^7.28.4",
"@babel/preset-react": "^7.27.1",
"@taroify/cli": "1.0.6",
"@tarojs/cli": "4.2.1",
"@tarojs/plugin-framework-react": "4.2.1",
"@tarojs/plugin-platform-weapp": "4.2.1",
"@tarojs/taro-loader": "4.2.1",
"@tarojs/webpack5-runner": "4.2.1",
"@types/node": "^22.0.0",
"@types/react": "^18.3.0",
"babel-preset-taro": "4.2.1",
"less": "^4.2.0",
"postcss": "^8.5.6",
"sass": "1.105.0",
"typescript": "5.9.3",
"webpack": "5.91.0"
}
}
+11235
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
{
"description": "Ballet Island 微信小程序",
"miniprogramRoot": "dist/",
"projectname": "ballet-island",
"appid": "wx6dfbf1021db8aee0",
"compileType": "miniprogram",
"setting": {
"es6": false,
"minified": true,
"postcss": false,
"urlCheck": true
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"version": 1,
"skills": {
"taroify": {
"source": "./node_modules/@taroify/cli/skills/taroify",
"sourceType": "local",
"computedHash": "c1a0f9254edcf2d10ba8d0d6373cbb14a29bf054c8683e5a5855d37aa692b1b8"
}
}
}
+10
View File
@@ -0,0 +1,10 @@
export default defineAppConfig({
pages: ['pages/index/index'],
window: {
navigationBarTitleText: 'Ballet Island',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
backgroundColor: '#f7f8fa',
},
})
+7
View File
@@ -0,0 +1,7 @@
page {
background: #f7f8fa;
color: #323233;
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', sans-serif;
font-size: 16px;
}
+7
View File
@@ -0,0 +1,7 @@
import type { PropsWithChildren } from 'react'
import './app.scss'
export default function App({ children }: PropsWithChildren) {
return children
}
@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: 'Ballet Island',
})
+21
View File
@@ -0,0 +1,21 @@
.welcome {
margin: 32px 20px;
padding: 28px 24px;
border-radius: 16px;
background: #fff;
&__title {
display: block;
margin-bottom: 12px;
font-size: 28px;
font-weight: 600;
}
&__message {
display: block;
margin-bottom: 28px;
color: #646566;
line-height: 1.6;
}
}
+34
View File
@@ -0,0 +1,34 @@
import { useState } from 'react'
import { Text, View } from '@tarojs/components'
import Button from '@taroify/core/button'
import '@taroify/core/button/style'
import { checkServiceReady } from '../../services/health'
import './index.scss'
export default function Index() {
const [loading, setLoading] = useState(false)
const [message, setMessage] = useState('欢迎来到 Ballet Island')
async function handleCheck() {
setLoading(true)
try {
await checkServiceReady()
setMessage('连接成功,服务已就绪')
} catch {
setMessage('暂时无法连接,请稍后重试')
} finally {
setLoading(false)
}
}
return (
<View className="welcome">
<Text className="welcome__title">Ballet Island</Text>
<Text className="welcome__message">{message}</Text>
<Button color="primary" block loading={loading} disabled={loading} onClick={handleCheck}>
检查连接
</Button>
</View>
)
}
+14
View File
@@ -0,0 +1,14 @@
import Taro from '@tarojs/taro'
export async function checkServiceReady(): Promise<void> {
const response = await Taro.request<{ status: string }>({
url: `${__API_BASE_URL__}/readyz`,
method: 'GET',
timeout: 5000,
})
if (response.statusCode !== 200 || response.data?.status !== 'ok') {
throw new Error('服务暂未就绪')
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"types": ["node", "@tarojs/taro"]
},
"include": ["src", "config", "types"]
}
+4
View File
@@ -0,0 +1,4 @@
declare const __API_BASE_URL__: string
declare module '*.scss'