Files
opencloud/AGENTS.md
2026-05-30 00:39:20 +08:00

153 lines
8.6 KiB
Markdown

# AGENTS.md
## Commands
- `npm run dev` — Vite dev server with HMR
- `npm run build``vue-tsc -b && vite build` (typecheck must pass or build aborts)
- `npx vue-tsc -b` — standalone typecheck (no script in package.json)
- No linter, formatter, or test runner exists
## Architecture
- **Vue 3 + Vite + Tailwind CSS + Vue Router + Pinia + Supabase + AMap (高德地图) + Naive UI**
- Path alias `@/``src/` (configured in both `vite.config.ts` and `tsconfig.app.json`)
- Supabase client singleton at `src/lib/supabase.ts` — throws at import if env vars missing
- AMap loaded lazily via `src/lib/amap.ts` with type declarations in `src/types/amap.d.ts`
- UI language is Chinese (zh-CN)
- All DB operations are direct `supabase.from(...)` calls from the browser — security depends on Supabase RLS policies
## Directory Map
| Path | Purpose |
| --- | --- |
| `src/lib/` | Singletons and utilities: supabase, amap, canvas patch, cloudTypes constants, cloudBadges canvas renderer, SEO meta builder |
| `src/stores/` | Pinia stores: auth, clouds (cloud_types cache), encyclopedia (collection + unlock tracking), profile (user pages + cloud CRUD) |
| `src/composables/` | Vue composables: `useUpload` (batch upload with thumbnail generation, EXIF extraction, badge unlocking) |
| `src/components/cloud/` | Cloud-related modals and widgets: ImageDetailModal, CloudEditModal, MapPickerModal, QuickUploadModal, MiniLocationMap |
| `src/components/layout/` | AppHeader (top nav bar with auth state) |
| `src/components/profile/` | ContributionHeatmap |
| `src/style.css` | Global visual system hooks: shared button/card utility classes, base typography, body background |
| `src/views/` | Route-level page components (see Routes below) |
| `src/types/` | TypeScript types: database models (`database.ts`), AMap declarations (`amap.d.ts`), router meta (`router.d.ts`) |
## Routes
| Path | View | Auth Required |
| --- | --- | :---: |
| `/` | MapView | No |
| `/login`, `/register`, `/auth/confirm`, `/auth/reset-password` | Auth views | No |
| `/upload` | UploadView | Yes |
| `/encyclopedia` | EncyclopediaView | No |
| `/encyclopedia/:id` | CloudTypeView | No |
| `/gallery` | GalleryView | No |
| `/community` | CommunityView | No |
| `/profile` | ProfileView (own) | Yes |
| `/profile/settings` | ProfileSettingsView | Yes |
| `/profile/:id` | ProfileView (public) | No |
| `/admin` | AdminView | Yes (admin) |
| `/403` | ForbiddenView | No |
| `/:pathMatch(.*)*` | NotFoundView | No |
- Route guards in `router/index.ts`: `requiresAuth` redirects to `/login`, `requiresAdmin` redirects to `/403`
- SEO meta tags applied per-route via `lib/seo.ts` in `router.afterEach`
## Auth Flow
- **`main.ts` initializes auth before mounting**: `authStore.initialize()` is called, which calls `getSession()` and subscribes to `onAuthStateChange`. The app only mounts after the initial session check completes.
- **Login sets `user.value` explicitly**: `login()` extracts `data.user` from `signInWithPassword` and assigns it to the store directly, rather than relying on `onAuthStateChange`.
- **Profile is auto-created by DB trigger** (`handle_new_user` on `auth.users`), not by the frontend.
- Auth error messages are translated to Chinese in the store.
- `register()` checks username uniqueness before calling `signUp()` — username is passed via `options.data.username`.
## Stores
- **auth** — User session, profile, login/register/logout, username/password update, password reset. `initialize()` must be called before app mounts.
- **clouds** — Simple cache of `cloud_types` table. Fetched once, shared across views.
- **encyclopedia** — Cloud types + user's collection (unlock state). Tracks `unlockPercent` for progress display. Depends on `authStore`.
- **profile** — User profile pages. Fetches profile + cloud list per-user. Supports update/delete/visibility-toggle with optimistic cache patching. `deleteClouds` also removes from Supabase Storage.
## Supabase
- Env var is `VITE_SUPABASE_PUBLISHABLE_KEY` (not `VITE_SUPABASE_ANON_KEY`), using Supabase's `sb_publishable_` key format.
- All tables have RLS enabled. Check `plan.md` section 10 for the full schema and RLS policies.
- Storage bucket `clouds` is public read, authenticated upload.
- Profile `role` field (`user`/`admin`) controls admin access — checked in route guard, not in JWT metadata.
- `user_collections` tracks encyclopedia unlocks with `first_cloud_id` FK to `clouds`.
## Gallery Pagination
- Page-based navigation (50 items/page), not infinite scroll.
- `resolveSearchFilters()` pre-fetches user IDs or cloud type IDs before the main query.
- `buildFilteredQuery()` returns a chainable query builder; `loadPage()` forks it into a `count` query and a `data` query that run in parallel via `Promise.all`.
- Search debounced at 250ms.
## Map Timeline
- Realtime mode: slider selects a minute of today, shows clouds captured within 2 hours before that time. Marker opacity decays with age.
- Archive mode: browse clouds by day or month. Toggling timeline controls closed auto-returns to realtime.
- Slider resets to current time each time the controls panel is opened.
## Upload Flow
- `useUpload` composable handles: file selection (drag/drop/click), client-side thumbnail generation (JPEG, max 640px edge, 0.72 quality), EXIF date extraction, coordinate blurring (2 decimal places), Supabase Storage upload (original + thumbnail), DB insert with `status: 'pending'`, badge unlock detection via `user_collections` upsert.
- `UploadView` is the full-page batch uploader. `QuickUploadModal` is a single-image shortcut from the map page.
## Build & Deploy
- **Vercel** with `vercel.json`: SPA rewrites, security headers (X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy). No CSP or HSTS configured.
- **SEO plugin** in `vite.config.ts`: generates `robots.txt` and `sitemap.xml` at build time. Disallows `/admin`, `/auth/`, `/login`, `/register`, `/upload`, `/profile/settings`.
- **`lib/canvas.ts`**: patches `HTMLCanvasElement.getContext('2d')` to always pass `willReadFrequently: true` — needed for the badge card renderer in `lib/cloudBadges.ts`.
## Environment Variables
Required (app won't start without):
- `VITE_SUPABASE_URL`
- `VITE_SUPABASE_PUBLISHABLE_KEY`
Required for map features:
- `VITE_AMAP_KEY`
Optional:
- `VITE_SITE_URL` — canonical URL for SEO meta and sitemap (falls back to Vercel env or hardcoded default)
Future (not in `.env`):
- `OPENAI_API_KEY`, `OPENWEATHERMAP_API_KEY`
## Naive UI
- Used for modals, buttons, inputs, tags, progress bars, alerts, dropdowns, empty states, skeletons, and message toasts.
- No SSR — pure client-side rendering.
- Custom CSS overrides via scoped `<style>` blocks for slider styling, transitions, and component tweaks.
- **Do not rely on default Naive UI primary/secondary colors for page CTAs or panel actions.** The project now uses shared global classes in `src/style.css` to keep buttons visually consistent with the sky-atlas theme.
- Auth buttons use `oc-primary-button` with semantic variants:
- `oc-primary-button--teal` — login / account-access actions
- `oc-primary-button--sky` — register / create / forward actions
- Panel and utility buttons use `oc-panel-button` with semantic variants:
- `oc-panel-button--neutral` — white card-style utility actions
- `oc-panel-button--sky` — primary panel actions
- `oc-panel-button--teal` — active toggles / confirm actions
- `oc-panel-button--danger` — destructive actions
- `oc-panel-button--amber` — admin/state-toggle actions where warning emphasis fits better than danger
- Card-like containers should prefer shared shadow classes instead of ad-hoc shadows:
- `oc-panel-card`
- `oc-panel-card-soft`
- `oc-empty-card`
- When styling `NButton`, prefer `type="default"` plus the shared class when a custom panel button variant is intended. Otherwise Naive UI theme variables may override white backgrounds or semantic colors.
## Visual Notes
- The site icon and header brand mark are a matching pixel-style cloud motif; avoid reintroducing emoji or unrelated icon styles for primary brand surfaces.
- Header action buttons use slight hover lift (`translate(-1px, -1px)`) with hard-offset shadow growth; new header-like actions should follow that interaction pattern.
- Heatmap view toggles in `ContributionHeatmap` intentionally use separate buttons with gap spacing instead of `NButtonGroup`, because grouped buttons visually collide once hard shadows and hover transforms are applied.
## MVP Constraints (from plan.md)
- No Supabase Realtime — refresh-based loading
- No OAuth — email/password only, email confirmation required
- No AI cloud identification — manual type selection
- AMap only (China-focused), no Mapbox fallback yet