feat: scaffold FastAPI backend and Vue 3 frontend

Backend (backend/):
- FastAPI app with layered app/{api,core,crud,db,models,schemas} structure
- TiDB connection via SQLAlchemy. The URL is built with URL.create rather
  than string formatting: the password contains '@', which would otherwise be
  parsed as the user/host separator and silently truncate the credential.
- Alembic environment wired to Base.metadata and the app settings, so
  backend/.env stays the single source of truth for credentials
- /api/health endpoint reporting database reachability
- models/ and crud/ are intentionally empty: no model classes are defined and
  no tables are created, at import time or otherwise

Frontend (frontend/):
- Vue 3 + Vite + TypeScript scaffold (create-vue, --bare)
- axios instance with a normalized error shape
- vue-router with a home route and a catch-all 404
- pinia + pinia-plugin-persistedstate; the app store persists selected keys
- Element Plus with its icons registered globally
- dev proxy forwards /api to the FastAPI service on port 8000

The database paper_doc was created in TiDB out of band; no table exists yet.
This commit is contained in:
2026-09-17 12:51:37 +08:00
parent 4438b04ff1
commit 5128e1551c
43 changed files with 3321 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
import axios, { AxiosError, type AxiosInstance } from 'axios'
/**
* Shared axios instance.
*
* `baseURL` defaults to the same-origin `/api` prefix, which Vite proxies to
* the FastAPI service in development (see vite.config.ts). Point
* `VITE_API_BASE_URL` at an absolute URL to bypass the proxy.
*/
export const http: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL ?? '/api',
timeout: 15000,
headers: {
'Content-Type': 'application/json',
},
})
/** Normalized failure shape, so views never have to inspect AxiosError. */
export interface ApiError {
message: string
status?: number
detail?: unknown
}
function normalizeError(error: AxiosError): ApiError {
const status = error.response?.status
const payload = error.response?.data as { detail?: unknown } | undefined
let message: string
if (payload?.detail && typeof payload.detail === 'string') {
message = payload.detail
} else if (error.code === 'ECONNABORTED') {
message = 'The request timed out.'
} else if (!error.response) {
message = 'Could not reach the API. Is the backend running on port 8000?'
} else {
message = error.message
}
return { message, status, detail: payload?.detail }
}
http.interceptors.response.use(
(response) => response,
(error: AxiosError) => Promise.reject(normalizeError(error)),
)
export default http
+15
View File
@@ -0,0 +1,15 @@
import http from './client'
/** Mirrors `app.schemas.health.HealthResponse` on the backend. */
export interface HealthResponse {
status: string
app: string
database: string
database_target: string
}
/** Probe the backend, including its TiDB connection. */
export async function fetchHealth(): Promise<HealthResponse> {
const { data } = await http.get<HealthResponse>('/health')
return data
}