| Icons | `@mdi/font` 7 | Material Design Icons set |
| Quality | ESLint 9 + Prettier 3 | Lint with `--fix` and Prettier over `src/` |
> All Vuetify labs components must be imported at the call site — only the core set is global.
---
## Core Features
-**Master Data CRUD** — Students, Subjects, Terms, Stories (each with its own `*.vue` view under `src/views/`)
-**Task Calendar** — FullCalendar-driven `TaskView.vue` with image attachments fetched as blobs
-**JWT Authentication** — Access + refresh tokens in `localStorage`; axios interceptor refreshes and retries once per request, second 401 triggers logout
-**Bilingual i18n** — English (source) and `zh-CN`, switched via `LanguageSwitcher.vue`
-**Layout system** — `App.vue` picks a layout from `route.meta.layout` (`'default'` for authenticated pages, `'none'` for `/login`)
| `src/api/index.js` | Shared axios `apiClient` (`baseURL: http://192.168.1.52:8001`, `timeout: 30s`, JSON headers) and the `apiEndpoints` table (currently stale for `terms`, missing for `task`/`story`) |
| `src/api/studentService.js` | CRUD for `/api/students/`; ships a `fetchAllPages()` helper that loops `page_size: 100` with a 100-page safety cap |
| `src/api/subjectService.js` | CRUD for `/api/subjects/` |
| `src/api/termService.js` | CRUD for `/api/terms/` |
| `src/api/storyService.js` | CRUD for `/api/stories/` (paths hardcoded; not present in `apiEndpoints`) |
| `src/api/taskService.js` | CRUD for tasks, including `getTaskImage(id)` which fetches images as `blob` with `Accept: */*` to avoid 406s; duplicates the `fetchAllPages()` helper verbatim from `studentService.js` |
| `src/api/README.md` | Working notes for the service layer |
---
## Conventions
- Plain JS modules exporting `async` functions that return `response.data`.
- Service functions log heavily to `console` (including sample data) when called.
- Base64 payloads are redacted in `studentService` — mirror that pattern when adding endpoints that return images/audio.
- DRF pagination responses (`{ count, next, previous, results }`) are normalized via the duplicated `fetchAllPages()` helper — if pagination logic changes, **update both**`studentService.js` and `taskService.js` (or factor it out).
| `src/components/AppLayout.vue` | Authenticated page chrome — composes `AppHeader` + `AppDrawer` + breadcrumbs + logout dialog; uses `100dvh` min-height so mobile full-bleed works on real devices |
| `src/components/AppHeader.vue` | Top app bar; renders the title, `LanguageSwitcher`, user info and triggers the logout dialog |
| `src/components/AppDrawer.vue` | Left navigation; resolves menu items from `components/constants/menuItems.js` |
| `src/components/AppFooter.vue` | Footer shown within the default layout |
| `src/components/LanguageSwitcher.vue` | Persists the user's locale choice via `setLocale(...)` into `localStorage('preferred-locale')` |
| `src/components/constants/menuItems.js` | `GLOBAL_MENU_ITEMS` with nested `children` for the **Master Data** group; matched against `route.meta.breadcrumbPath` to resolve icons in `AppLayout` |
| `src/components/constants/colors.js` | Shared color tokens for use across views |
---
## Conventions
- All authenticated views must keep `meta.layout: 'default'` in the router; the login route sets `meta.layout: 'none'` so `App.vue` renders a bare `<div>`.
- Breadcrumbs are **not** derived from the path. Each route declares `meta.breadcrumbPath` (an array of `{ key, to, disabled }` i18n keys); `AppLayout` resolves them and applies a fixed `home`/`master-data` icon, falling back to `meta.breadcrumb.icon` or matching `menuItems` for the leaf entry.
- Localized strings must come from `useI18n()` / `$t(...)` — never hardcoded English.
- Each view is paired with parallel translation keys under `src/locales/{en,zh-CN}/modules/*.js`.
- Add new authenticated pages with `meta.layout: 'default'` and `meta.requiresAuth: true`, and declare `meta.breadcrumbPath` so breadcrumbs render in `AppLayout`.
> Each authenticated route carries a `meta.breadcrumbPath` array (`{ key, to, disabled }`) that `AppLayout` resolves against the i18n bundles.
---
## Navigation Guard
-**LAN bypass** — `isLanAccess()` returns `true` → guard lets the navigation through without auth checks (used during local development when the device is on the same network as the backend).
-**Unauthenticated access** — when `to.meta.requiresAuth && !isAuthenticated`, redirect to `/login`.
-**Proactive refresh** — when the access token will expire within the threshold (default `60s`, see `isAccessTokenExpiringSoon`), the guard awaits `authStore.refreshAccessToken()` before navigating. Failure path cleans up tokens and redirects to `/login`.
-**Already authenticated** — visiting `/login` while logged in redirects to `/`.
-**Login-endpoint 401s** — handled separately by the axios interceptor; they are not retried via the refresh flow.
| `src/stores/auth.js` | The single production store. Defines `useAuthStore` (setup-style), persists tokens in `localStorage`, decodes JWT claims, exposes `isAuthenticated` / `isAccessTokenExpired` / `isAccessTokenExpiringSoon`, and wires the axios request/response interceptors exactly once (deduped via a module-level `refreshPromise`). |
| `src/stores/counter.js` | Boilerplate from the Vue scaffold; unused by the app. |
---
## Auth Store Surface
-**State** — `accessToken`, `refreshToken`, `user`, `interceptorsInitialized`, `isRefreshing` (`refreshPromise` is a module-level `let`).
-**Axios wiring** — `setAxiosAuthHeader(token)`, `initializeAuth()` (called once from `main.js`), and `refreshAccessToken()` (uses the module-level `refreshPromise` to dedupe concurrent refreshes; second 401 in a request → `logout()`).
-**Lifecycle** — `login(credentials)` navigates on success; `logout()` clears tokens and routes back to `/login`.
> Login-endpoint 401s are intentionally **not** passed through the retry flow — only requests that hit a regular endpoint (not `/api/token/`) get the refresh + retry treatment.
│ ├── index.js # Aggregates en/* into a single message object
│ ├── common/
│ │ ├── buttons.js
│ │ ├── messages.js
│ │ ├── status.js
│ │ ├── table.js
│ │ └── validation.js
│ ├── components/
│ │ └── app-header.js
│ └── modules/
│ ├── auth.js
│ ├── navigation.js
│ ├── settings.js
│ ├── story.js
│ ├── student.js
│ ├── subject.js
│ ├── task.js
│ └── term.js
└── zh-CN/
├── index.js
├── common/...
├── components/app-header.js
└── modules/...
```
---
## Conventions
-**English is the source locale.** Mirror every new key into `zh-CN` in the same shape.
- Use `$t(...)` in templates and `const { t } = useI18n()` inside `<script setup>`.
- Locale is persisted across reloads: `LanguageSwitcher.vue` and `setLocale()` write the user's choice to `localStorage('preferred-locale')`.
- Domain namespaces are organized as `modules/<domain>.js` (one file per feature view), with cross-cutting strings under `common/` and per-component texts under `components/`.
| `src/plugins/vuetify.js` | Creates the Vuetify plugin instance; imports `@mdi/font/css/materialdesignicons.css` for icons and `vuetify/styles` for base CSS; uses namespace imports (`vuetify/components`, `vuetify/directives`) to register everything globally. Declares a single `light` theme with custom `primary`/`secondary`/`accent`/`error`/`info`/`success`/`warning` colors. |
---
## Conventions
- All **core** Vuetify components and directives are registered globally via `import * as components from 'vuetify/components'` and `import * as directives from 'vuetify/directives'`.
-**Labs** components are intentionally **not** registered here — import them at the call site (e.g. `import { VSomeLabs } from 'vuetify/labs/VSomeLabs'`).
- Only the light theme is enabled (`defaultTheme: 'light'`). Dark mode is not currently exposed.
> The Vuetify plugin is installed in `src/main.js` together with `pinia`, `router`, and `i18n`.
| `src/utils/lanAccess.js` | Exports `isLanAccess()` — detected by inspecting `window.location.hostname` against private IPv4 ranges (e.g. `10.*`, `192.168.*`, `172.16–31.*`) so the router guard can skip the JWT requirement during local development. |
---
## Conventions
- Utilities are imported with the `@/` alias (see `vite.config.js`) — e.g. `import { isLanAccess } from '@/utils/lanAccess'`.
- Keep helpers pure and dependency-free; place anything stateful inside a Pinia store instead.
| `src/assets/images/login.jpg` | Background image for the login screen |
| `src/assets/images/loginAvatar.jpg` | Avatar overlay for the login screen |
| `src/styles/safe-area.css` | Global CSS for iOS Home Indicator / safe-area insets; imported once from `src/main.js` |
| `src/App.vue` | Root component — selects a layout (`default` vs `none`) from `route.meta.layout` and renders `<router-view />` inside it |
---
## Conventions
- Static imagery and styles that the app references with `@/assets/...` or `@/styles/...` live here.
- All global stylesheets should be imported from `main.js`; per-component scoped styles live alongside each `.vue` file.
- The favicon used in `public/favicon.ico` is duplicated as `dist/favicon.ico` after build (handled by Vite).
---
Home: [[01.00 student-app-frontend]] | **Location:**`C:\Projects\vue\student-app-frontend\src\assets` and `C:\Projects\vue\student-app-frontend\src\styles` and `C:\Projects\vue\student-app-frontend\src\App.vue`
| `vite.config.js` | Vite config — installs `vue` + `vite-plugin-vue-devtools` plugins, registers the `@` alias pointing at `./src`, and binds the dev server to `host: '0.0.0.0'`, `port: 5173` (so other devices on the LAN can reach the app). |
The frontend issues JSON requests to a Django REST Framework service running at `http://192.168.1.52:8001`. Authentication uses JWT — the access + refresh pair is obtained from `/api/token/` and `/api/token/refresh/`.
| `/api/tasks/` and task sub-routes | various | `taskService` (paths hardcoded — not in `apiEndpoints`) |
## Notable Behaviors
- DRF returns paginated lists as `{ count, next, previous, results }` — see `fetchAllPages()` in `studentService.js` and `taskService.js`.
- Task **images** are returned as binary. The frontend fetches them with `responseType: 'blob'` and `Accept: */*` (`getTaskImage`) — without that header the backend responds `406 Not Acceptable`.
## External Files in This Repo (Not Part of Vue Build)
The following live at the repo root but are not part of the Vue app:
-`swagger_schema.json` — the OpenAPI/Swagger schema for the backend; consult it before adding/changing API calls.
## Operational Notes
- The base URL is **hardcoded** — change `API_BASE_URL` in `src/api/index.js` when the backend moves (no env-based config exists).
- The `apiEndpoints` table in `src/api/index.js` is partially stale: `TERMS.UPDATE`/`DELETE` are still template strings (`/api/terms/{termId}/`) instead of functions, and `task`/`story` endpoints aren't listed. Prefer hardcoding paths in the service modules over trusting `apiEndpoints` until it's cleaned up.
---
Home: [[01.00 student-app-frontend]] | **Location:**`C:\Projects\vue\student-app-frontend\swagger_schema.json` (spec lives at the project root)