From cc54d80b3a97c941350dbd75331e874100432e64 Mon Sep 17 00:00:00 2001 From: Cheng Zhou Date: Tue, 16 Dec 2025 20:31:20 +0800 Subject: [PATCH] =?UTF-8?q?Step=20F2=EF=BC=9A=E9=A1=B9=E7=9B=AE=E5=88=97?= =?UTF-8?q?=E8=A1=A8=20&=20=E5=BD=93=E5=89=8D=E9=A1=B9=E7=9B=AE=E4=B8=8A?= =?UTF-8?q?=E4=B8=8B=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/v1/studies.py | 10 ++-- frontend/src/api/studies.ts | 4 ++ frontend/src/components/Layout.vue | 47 +++++++++------- frontend/src/components/StudySelector.vue | 32 +++++++++++ frontend/src/main.ts | 8 ++- frontend/src/router/index.ts | 28 +++++++++- frontend/src/store/auth.ts | 3 ++ frontend/src/store/study.ts | 37 +++++++++++++ frontend/src/types/api.ts | 7 +++ frontend/src/views/StudyHome.vue | 24 +++++++++ frontend/src/views/StudyList.vue | 63 ++++++++++++++++++++++ pg_data/base/16384/24576 | Bin 8192 -> 8192 bytes pg_data/base/16384/24584 | Bin 8192 -> 8192 bytes pg_data/base/16384/24590 | Bin 16384 -> 16384 bytes pg_data/base/16384/24597 | Bin 16384 -> 16384 bytes pg_data/global/pg_control | Bin 8192 -> 8192 bytes pg_data/pg_subtrans/0000 | Bin 8192 -> 8192 bytes pg_data/pg_wal/000000010000000000000001 | Bin 16777216 -> 16777216 bytes pg_data/pg_xact/0000 | Bin 8192 -> 8192 bytes 19 files changed, 239 insertions(+), 24 deletions(-) create mode 100644 frontend/src/api/studies.ts create mode 100644 frontend/src/components/StudySelector.vue create mode 100644 frontend/src/store/study.ts create mode 100644 frontend/src/views/StudyHome.vue create mode 100644 frontend/src/views/StudyList.vue 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 21a8955430b391f776f515f431ab8fe3635cd546..c24626497b3e01133bb30564b142818df5cc23e0 100644 GIT binary patch delta 21 bcmZp0XmDU-00Ie(jcj%Dj7ulC%2xvbG>-*y delta 21 bcmZp0XmDU-0D>QU8`J?sWJ diff --git a/pg_data/base/16384/24584 b/pg_data/base/16384/24584 index 7d19d1166b6aa51c9c08939f1b46f9db05dd0ff6..d879719cb02a84ad2cccbf4920a13f8b7e4f3245 100644 GIT binary patch delta 122 zcmZp0XmH?X0D=OU!;B!hhCyK>emx_8WWd;c%fFuJa12d<>aj;sTuy-_T zr(cG{_DMaVVJjkxnIs)UL;Q7(Q!J896Qy6YuX@_G;^o4f&u6cjd{#`HorTGAG5<@Z J&Fx~9oB(`gAtnF- delta 35 ncmZp0XmH?X0D=|FhZ#Y11w+R~{>+U{^F$|3DB9d1R>27Xo{b7q diff --git a/pg_data/base/16384/24590 b/pg_data/base/16384/24590 index 309adec81827962b113cf624491afcb1b920f3f6..0373bc7d810687e67ef9e09391139a81b23d801b 100644 GIT binary patch delta 59 zcmZo@U~Fh$+`yv1Jwg63BLfiBFwB_Dub{{@WA0`>g)CVSNd^f9?`YOezYK@%lX^nK ORzw&xZQfuTzytu~wGklz delta 38 scmZo@U~Fh$+`yv1y@Ks9BLfgrFl?C2ub{ZuL?J_VVt~x%Ew(;P0MG0S+5i9m diff --git a/pg_data/base/16384/24597 b/pg_data/base/16384/24597 index 5d1f5b3a8576839475102ea5d0f9173b449ff186..cfca9ff62d461ca44fa16b3353abc9c30753c64d 100644 GIT binary patch delta 60 zcmZo@U~Fh$+`ytBuAp?7kpT#57#!q2$g?Pb*begyHj680$_Yp^NH|D3hKBg-8mCw! MnI>)yv0K9k0GXZ*`2YX_ delta 42 vcmZo@U~Fh$+`ytB%)oh=kpT!Q7$W38$g?N_nVY2*H034+$ZU?WTfztc(ew%Y diff --git a/pg_data/global/pg_control b/pg_data/global/pg_control index 3d19d35b3f093d316c94a1854ffec612ec83f259..068631abf50ca9f01908205830d15d34a401c269 100644 GIT binary patch delta 87 zcmZp0XmFSyq4__+F_QrZHfS7X1kn{x8pMHNkodBRj_N`%#UKF?SjIfDY8gwO%x$C1 NhK%RsCOa_l0s#4B5ugA7 delta 76 zcmZp0XmFSyp}E}8F_QrZ90U$CBGVua41>g%PIOdfnF3TiaiYk?h6OB17kYngHe@_6 JH`#%a7XU**5ugA7 diff --git a/pg_data/pg_subtrans/0000 b/pg_data/pg_subtrans/0000 index ffe9102f574e5bb6271416606eb9e1aeb4880e07..6d17cf9d15fb9f4a2358a2d079f3b8c755d005fa 100644 GIT binary patch delta 14 VcmZp0XmHq^z`HX= delta 38 fcmZp0XmHrz!YdWP%m4;KP&x!khcRztTqFblcmV}k diff --git a/pg_data/pg_wal/000000010000000000000001 b/pg_data/pg_wal/000000010000000000000001 index 4a2f36e2618e8090fecb7cd462a8d83a201ecc17..be8a5c5f4bc99cdb3e6ac47275474902bccc92c0 100644 GIT binary patch delta 15894 zcmeIy`%_e97{KwvF32vVECISJ%C1BOmPQaJUKgW_7?(th0#V6ID+}yKNP+S-yige# z2Q#^(7+@)-v|^dG=Z9vRW}xYZ`mLF!rYxH1w=9y*;c%v%@h@oRd1u};`(B=Bp7%Xx zKMf5Hjs~L>h7CL6L@-HugG_ECi$&bdVzODnQkJot6|BTh4y(9>T<+v9?&cm= zlgAqFC7%KcDdIlXQp`F^SkL{G@&FG~MmZa(U?ZE@OeGJog@<{BN7>3Y9^-MIU^`Fp z6jfBSgQt0hXL*k2so@1`*-0HQ@)Gqlu#4R^0(*FwCib$A{T!f~S9q1zILPa?@CJu? zlU7vPILuod;cbr6P6wTI@eap0PB$lbmmYfQ<0S9#KBqWMKOgWRXE@6M9}(ajgPiAM zKH&nNa*-iEBY0LDbGb7Vvz>EkVa6|)Ls7nWUj9w3iqTTz#;jR=#k1U*F;P0EmbtW* zwyz7-(HJeol^XkB*zWP_A$7#1`MfPriqdUYl+LMDMIR66=jA9aMJbP}Q#X!xc%toQ zWS!!hsdm*grl`Z0gSq*>){)6jvnuoHQJjk1;ro2DKz(XWS;W4At^PnubIJOi(oCBt zr(|8>g3K+8E3-By{@m2}lKAIw~^KvNc$`J zO++C5%!LE?RFAdp(f1lB_|`f>w4y8=%}q8=@Pn;Rz4&E^JGy6@b%Zwc7rQpk)^=2# zI}j|0_bBeGiVPHoYehD7_={+#x7ucAubN{fQ1tAFX8bx}WM8IdKYgXc?Nyvsc7M{C zH{55G=5J3|s`S3MJzcGJ1ZTTlUqvXB*Wxobhv}J(rdF*AE>#O7HAkFYj6d{-^wq%H zNUb+a{cbQgJ>EFwL9ZFTNqo-x~s0Yw%*tpX1vQ} zJ%=&yB~R6_myj60y?nyanqc0wQG=pbAD27T+-H1T^@khNCfw$_{>(}qIiY7Vx{=Xr zR-8X1Zs%ylAN?VL`p&BZcT6Ryu>$pR#qS%MCQ7Xte3hTG>>j&yzF;vg}#S!w5JCN@{{suyUNGKADgd(9x zC=!ZWBdfi&v52Eoc#f3 z_Fkz}DkJsplu=fia?+JoL57MdsjP~ss;Mqh4K>wLTOD=PQ(psF8fv7mCYov{TXQY6 z)Jkh@wAD^~9dy)5XI&(8)lGLj^wdjlee~5&e*+9O$Y42!7%JB=!;LV~D0z}b8)K|- z#+zWGN%BoL#Z=QwH^WS`%r?hd^USxvLW?Z6#8S&Fx57%RthUBl>#VoIMw@K5#a7#F zx5G}m?6${V`=so5z(E{x*bzq^bKD6hopRb4XPtB21s4^#z?}_c<7PGo_Ok+=U#Z}mDk>Qt5}J5O1<~NN1uH5#aG{a_ruTPUw<-7vkLQy(n=D^ z{Pc+ULG&PI5EO)jAz?@u5{85!VMrJfhJ+zuNEi}^gdt%_7!rnrAz?@u5{85!VMrJf zhJ+zuNEi}^gdt%_7!rnrAz?@u5{85!VMrJfhJ+zuNEi}^gdt%_7!rnrAz?`Pze`w< HNdEo{ya6)N diff --git a/pg_data/pg_xact/0000 b/pg_data/pg_xact/0000 index 0f1be3937b2e2e18e69c73a1fcbd7d57835ea92b..87e4e3ae2fb8ec53c06d01abe863d43462762fc4 100644 GIT binary patch delta 15 WcmZp0XmFVDl96@eM}GN9jO+k04Fzfd delta 15 WcmZp0XmFVDl96%aM}GN9jO+j~?FD84