diff --git a/backend/app/api/v1/studies.py b/backend/app/api/v1/studies.py index 029246a0..2b505a69 100644 --- a/backend/app/api/v1/studies.py +++ b/backend/app/api/v1/studies.py @@ -5,7 +5,9 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.core.deps import get_current_user, get_db_session, require_roles, require_study_member, require_study_roles from app.crud import study as study_crud +from app.schemas.common import PaginatedResponse from app.schemas.study import StudyCreate, StudyRead, StudyUpdate +from app.utils.pagination import paginate router = APIRouter() @@ -23,18 +25,20 @@ async def create_study( return study -@router.get("/", response_model=list[StudyRead]) +@router.get("/", response_model=PaginatedResponse[StudyRead]) async def list_studies( skip: int = 0, limit: int = 100, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), -) -> list[StudyRead]: +) -> PaginatedResponse[StudyRead]: if current_user.role == "ADMIN": studies = await study_crud.list_studies(db, skip=skip, limit=limit) + total = await study_crud.list_studies(db, skip=0, limit=10_000_000) else: studies = await study_crud.list_studies_for_user(db, current_user.id, skip=skip, limit=limit) - return list(studies) + total = await study_crud.list_studies_for_user(db, current_user.id, skip=0, limit=10_000_000) + return paginate(list(studies), total=len(total)) @router.get("/{study_id}", response_model=StudyRead, dependencies=[Depends(require_study_member())]) diff --git a/frontend/src/api/studies.ts b/frontend/src/api/studies.ts new file mode 100644 index 00000000..8d7aad66 --- /dev/null +++ b/frontend/src/api/studies.ts @@ -0,0 +1,4 @@ +import { apiGet } from "./axios"; +import type { ApiListResponse, Study } from "../types/api"; + +export const fetchStudies = () => apiGet>("/api/v1/studies"); diff --git a/frontend/src/components/Layout.vue b/frontend/src/components/Layout.vue index 9e76054a..67bfc64e 100644 --- a/frontend/src/components/Layout.vue +++ b/frontend/src/components/Layout.vue @@ -1,35 +1,44 @@ diff --git a/frontend/src/components/StudySelector.vue b/frontend/src/components/StudySelector.vue new file mode 100644 index 00000000..d73af61b --- /dev/null +++ b/frontend/src/components/StudySelector.vue @@ -0,0 +1,32 @@ + + + diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 18e568a5..b2748e98 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -5,10 +5,16 @@ import "element-plus/dist/index.css"; import App from "./App.vue"; import router from "./router"; +import { useStudyStore } from "./store/study"; const app = createApp(App); -app.use(createPinia()); +const pinia = createPinia(); +app.use(pinia); app.use(router); app.use(ElementPlus); +// 初始化项目上下文 +const studyStore = useStudyStore(); +studyStore.loadCurrentStudy(); + app.mount("#app"); diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index aaa4168b..2009224d 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -1,8 +1,11 @@ import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router"; import { useAuthStore } from "../store/auth"; +import { useStudyStore } from "../store/study"; import Layout from "../components/Layout.vue"; import Home from "../views/Home.vue"; import Login from "../views/Login.vue"; +import StudyList from "../views/StudyList.vue"; +import StudyHome from "../views/StudyHome.vue"; const routes: RouteRecordRaw[] = [ { @@ -22,6 +25,24 @@ const routes: RouteRecordRaw[] = [ component: Home, meta: { title: "首页" }, }, + { + path: "study/home", + name: "StudyHome", + component: StudyHome, + meta: { title: "项目首页", requiresStudy: true }, + }, + ], + }, + { + path: "/studies", + component: Layout, + children: [ + { + path: "", + name: "StudyList", + component: StudyList, + meta: { title: "项目列表" }, + }, ], }, ]; @@ -33,13 +54,18 @@ const router = createRouter({ router.beforeEach((to, _from, next) => { const auth = useAuthStore(); + const studyStore = useStudyStore(); const token = auth.token; if (!to.meta.public && !token) { next({ path: "/login" }); return; } if (to.path === "/login" && token) { - next({ path: "/" }); + next({ path: studyStore.currentStudy ? "/study/home" : "/home" }); + return; + } + if (to.path.startsWith("/study") && !studyStore.currentStudy) { + next({ path: "/studies" }); return; } next(); diff --git a/frontend/src/store/auth.ts b/frontend/src/store/auth.ts index d8ac7c36..5a2cda79 100644 --- a/frontend/src/store/auth.ts +++ b/frontend/src/store/auth.ts @@ -3,6 +3,7 @@ import { ref } from "vue"; import { login as apiLogin, fetchMe } from "../api/auth"; import { setToken, clearToken, getToken } from "../utils/auth"; import type { UserInfo } from "../types/api"; +import { useStudyStore } from "./study"; export const useAuthStore = defineStore("auth", () => { const token = ref(getToken()); @@ -31,6 +32,8 @@ export const useAuthStore = defineStore("auth", () => { token.value = null; user.value = null; clearToken(); + const studyStore = useStudyStore(); + studyStore.clearCurrentStudy(); }; return { diff --git a/frontend/src/store/study.ts b/frontend/src/store/study.ts new file mode 100644 index 00000000..79f14c20 --- /dev/null +++ b/frontend/src/store/study.ts @@ -0,0 +1,37 @@ +import { defineStore } from "pinia"; +import { ref } from "vue"; +import type { Study } from "../types/api"; + +const STUDY_KEY = "ctms_current_study"; + +export const useStudyStore = defineStore("study", () => { + const currentStudy = ref(null); + + const setCurrentStudy = (study: Study) => { + currentStudy.value = study; + localStorage.setItem(STUDY_KEY, JSON.stringify(study)); + }; + + const loadCurrentStudy = () => { + const raw = localStorage.getItem(STUDY_KEY); + if (raw) { + try { + currentStudy.value = JSON.parse(raw) as Study; + } catch { + currentStudy.value = null; + } + } + }; + + const clearCurrentStudy = () => { + currentStudy.value = null; + localStorage.removeItem(STUDY_KEY); + }; + + return { + currentStudy, + setCurrentStudy, + loadCurrentStudy, + clearCurrentStudy, + }; +}); diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index 57d04d66..53d67624 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -25,3 +25,10 @@ export interface UserInfo { } export type UserMeResponse = UserInfo; + +export interface Study { + id: string; + code: string; + name: string; + status: string; +} diff --git a/frontend/src/views/StudyHome.vue b/frontend/src/views/StudyHome.vue new file mode 100644 index 00000000..4d5fcd5b --- /dev/null +++ b/frontend/src/views/StudyHome.vue @@ -0,0 +1,24 @@ + + + + + diff --git a/frontend/src/views/StudyList.vue b/frontend/src/views/StudyList.vue new file mode 100644 index 00000000..4918464b --- /dev/null +++ b/frontend/src/views/StudyList.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/pg_data/base/16384/24576 b/pg_data/base/16384/24576 index 21a89554..c2462649 100644 Binary files a/pg_data/base/16384/24576 and b/pg_data/base/16384/24576 differ diff --git a/pg_data/base/16384/24584 b/pg_data/base/16384/24584 index 7d19d116..d879719c 100644 Binary files a/pg_data/base/16384/24584 and b/pg_data/base/16384/24584 differ diff --git a/pg_data/base/16384/24590 b/pg_data/base/16384/24590 index 309adec8..0373bc7d 100644 Binary files a/pg_data/base/16384/24590 and b/pg_data/base/16384/24590 differ diff --git a/pg_data/base/16384/24597 b/pg_data/base/16384/24597 index 5d1f5b3a..cfca9ff6 100644 Binary files a/pg_data/base/16384/24597 and b/pg_data/base/16384/24597 differ diff --git a/pg_data/global/pg_control b/pg_data/global/pg_control index 3d19d35b..068631ab 100644 Binary files a/pg_data/global/pg_control and b/pg_data/global/pg_control differ diff --git a/pg_data/pg_subtrans/0000 b/pg_data/pg_subtrans/0000 index ffe9102f..6d17cf9d 100644 Binary files a/pg_data/pg_subtrans/0000 and b/pg_data/pg_subtrans/0000 differ diff --git a/pg_data/pg_wal/000000010000000000000001 b/pg_data/pg_wal/000000010000000000000001 index 4a2f36e2..be8a5c5f 100644 Binary files a/pg_data/pg_wal/000000010000000000000001 and b/pg_data/pg_wal/000000010000000000000001 differ diff --git a/pg_data/pg_xact/0000 b/pg_data/pg_xact/0000 index 0f1be393..87e4e3ae 100644 Binary files a/pg_data/pg_xact/0000 and b/pg_data/pg_xact/0000 differ