Step F2:项目列表 & 当前项目上下文
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
import { apiGet } from "./axios";
|
||||
import type { ApiListResponse, Study } from "../types/api";
|
||||
|
||||
export const fetchStudies = () => apiGet<ApiListResponse<Study>>("/api/v1/studies");
|
||||
@@ -1,35 +1,44 @@
|
||||
<template>
|
||||
<el-container class="layout">
|
||||
<el-header class="header">
|
||||
<div class="logo">CTMS</div>
|
||||
<div class="user-info">
|
||||
<span>{{ auth.user?.username }}</span>
|
||||
<el-tag size="small" class="role">{{ auth.user?.role }}</el-tag>
|
||||
<el-button size="small" type="danger" @click="onLogout">退出登录</el-button>
|
||||
</div>
|
||||
</el-header>
|
||||
<el-container>
|
||||
<el-aside width="200px" class="aside">
|
||||
<el-menu router default-active="/home">
|
||||
<el-menu-item index="/home">首页</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
<el-main>
|
||||
<router-view />
|
||||
</el-main>
|
||||
</el-container>
|
||||
<el-container class="layout">
|
||||
<el-header class="header">
|
||||
<div class="logo">CTMS</div>
|
||||
<div class="current-study" v-if="study.currentStudy">
|
||||
<el-tag type="success" size="small">{{ study.currentStudy.name }}</el-tag>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<StudySelector />
|
||||
<span>{{ auth.user?.username }}</span>
|
||||
<el-tag size="small" class="role">{{ auth.user?.role }}</el-tag>
|
||||
<el-button size="small" type="danger" @click="onLogout">退出登录</el-button>
|
||||
</div>
|
||||
</el-header>
|
||||
<el-container>
|
||||
<el-aside width="200px" class="aside" v-if="study.currentStudy">
|
||||
<el-menu router :default-active="$route.path">
|
||||
<el-menu-item index="/home">首页</el-menu-item>
|
||||
<el-menu-item index="/study/home">项目首页</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
<el-main>
|
||||
<router-view />
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from "vue-router";
|
||||
import { useAuthStore } from "../store/auth";
|
||||
import { useStudyStore } from "../store/study";
|
||||
import StudySelector from "./StudySelector.vue";
|
||||
|
||||
const auth = useAuthStore();
|
||||
const study = useStudyStore();
|
||||
const router = useRouter();
|
||||
|
||||
const onLogout = () => {
|
||||
auth.logout();
|
||||
study.clearCurrentStudy();
|
||||
router.push("/login");
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<el-dropdown>
|
||||
<span class="el-dropdown-link">
|
||||
<el-tag size="small" type="info">
|
||||
{{ study.currentStudy?.name || "未选择项目" }}
|
||||
</el-tag>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="goSelect">切换项目</el-dropdown-item>
|
||||
<el-dropdown-item @click="clearStudy">清除当前项目</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from "vue-router";
|
||||
import { useStudyStore } from "../store/study";
|
||||
|
||||
const router = useRouter();
|
||||
const study = useStudyStore();
|
||||
|
||||
const goSelect = () => {
|
||||
router.push("/studies");
|
||||
};
|
||||
|
||||
const clearStudy = () => {
|
||||
study.clearCurrentStudy();
|
||||
router.push("/studies");
|
||||
};
|
||||
</script>
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<string | null>(getToken());
|
||||
@@ -31,6 +32,8 @@ export const useAuthStore = defineStore("auth", () => {
|
||||
token.value = null;
|
||||
user.value = null;
|
||||
clearToken();
|
||||
const studyStore = useStudyStore();
|
||||
studyStore.clearCurrentStudy();
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -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<Study | null>(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,
|
||||
};
|
||||
});
|
||||
@@ -25,3 +25,10 @@ export interface UserInfo {
|
||||
}
|
||||
|
||||
export type UserMeResponse = UserInfo;
|
||||
|
||||
export interface Study {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="study-home" v-if="study.currentStudy">
|
||||
<el-card>
|
||||
<h2>项目首页</h2>
|
||||
<p>项目名称:{{ study.currentStudy.name }}</p>
|
||||
<p>项目编号:{{ study.currentStudy.code }}</p>
|
||||
</el-card>
|
||||
</div>
|
||||
<div v-else class="study-home">
|
||||
<el-alert type="info" title="请先选择一个项目" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useStudyStore } from "../store/study";
|
||||
|
||||
const study = useStudyStore();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.study-home {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div class="study-list">
|
||||
<el-card>
|
||||
<div class="header">
|
||||
<h3>项目列表</h3>
|
||||
</div>
|
||||
<el-table :data="studies" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="code" label="项目编号" width="160" />
|
||||
<el-table-column prop="name" label="项目名称" />
|
||||
<el-table-column prop="status" label="状态" width="120" />
|
||||
<el-table-column label="操作" width="140">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" size="small" @click="enterStudy(scope.row)">进入项目</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { fetchStudies } from "../api/studies";
|
||||
import { useStudyStore } from "../store/study";
|
||||
import type { Study } from "../types/api";
|
||||
|
||||
const studies = ref<Study[]>([]);
|
||||
const loading = ref(false);
|
||||
const router = useRouter();
|
||||
const studyStore = useStudyStore();
|
||||
|
||||
const loadStudies = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await fetchStudies();
|
||||
studies.value = data.items;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const enterStudy = (study: Study) => {
|
||||
studyStore.setCurrentStudy(study);
|
||||
router.push("/study/home");
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadStudies();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.study-list {
|
||||
padding: 16px;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user