工作台界面-初版

This commit is contained in:
Cheng Zhou
2025-12-17 22:37:51 +08:00
parent c50ca05099
commit ad4fa6ae7a
13 changed files with 685 additions and 75 deletions
@@ -0,0 +1,281 @@
<template>
<div class="page">
<el-card class="mb-12">
<div class="header">
<div>
<h2>我的工作台</h2>
<div class="sub">基于当前角色为你聚合最需要处理的事项</div>
</div>
<el-tag type="info">{{ roleLabel }}</el-tag>
</div>
<div v-if="!currentStudyId" class="empty-tip">
请选择一个项目后查看工作台内容
</div>
</el-card>
<template v-if="currentStudyId">
<el-row :gutter="12">
<el-col :span="12">
<SectionCard
title="今日待办"
:items="todayList"
:loading="loading"
empty-text="暂无需要你处理的事项"
@more="goMore(todayMorePath)"
/>
</el-col>
<el-col :span="12">
<SectionCard
title="已逾期"
:items="overdueList"
:loading="loading"
empty-text="暂无需要你处理的事项"
@more="goMore(overdueMorePath)"
/>
</el-col>
</el-row>
<el-row :gutter="12" class="mt-12">
<el-col :span="12">
<SectionCard
title="需要我处理的事项"
:items="actionList"
:loading="loading"
empty-text="暂无需要你处理的事项"
@more="goMore(actionMorePath)"
/>
</el-col>
<el-col :span="12">
<el-card shadow="never" class="section-card">
<div class="section-header">
<span>快捷入口</span>
<el-button type="primary" link @click="goMore('/study/home')">进入项目</el-button>
</div>
<div class="quick-list">
<el-button v-for="qa in quickActions" :key="qa.path" type="primary" plain size="small" @click="goMore(qa.path)">
{{ qa.label }}
</el-button>
</div>
<div v-if="!quickActions.length" class="empty">{{ emptyText }}</div>
</el-card>
</el-col>
</el-row>
<el-row v-if="showPersonalTodo" class="mt-12">
<el-col :span="24">
<PersonalTodo :storage-key="todoStorageKey" />
</el-col>
</el-row>
</template>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, defineAsyncComponent } from "vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import { useAuthStore } from "../../store/auth";
import { useStudyStore } from "../../store/study";
import { fetchTasks } from "../../api/tasks";
import { fetchAes } from "../../api/aes";
import { fetchFinanceItems } from "../../api/finance";
import { fetchImpTransactions } from "../../api/impTransactions";
interface WorkItem {
title: string;
subtitle?: string;
status?: string;
overdue?: boolean;
path: string;
}
const SectionCard = defineAsyncComponent(() => import("./components/SectionCard.vue"));
const PersonalTodo = defineAsyncComponent(() => import("./components/PersonalTodo.vue"));
const auth = useAuthStore();
const study = useStudyStore();
const router = useRouter();
const loading = ref(false);
const todayList = ref<WorkItem[]>([]);
const overdueList = ref<WorkItem[]>([]);
const actionList = ref<WorkItem[]>([]);
const currentStudyId = computed(() => study.currentStudy?.id || "");
const todayStr = computed(() => new Date().toISOString().slice(0, 10));
const roleLabel = computed(() => auth.user?.role || "-");
const emptyText = "暂无需要你处理的事项";
const showPersonalTodo = computed(() => role.value !== "ADMIN");
const todoStorageKey = computed(() => `workbench_todos_${auth.user?.id || "guest"}`);
const role = computed(() => auth.user?.role || "");
const quickActions = computed(() => {
if (!currentStudyId.value) return [];
if (role.value === "CRA")
return [
{ label: "新建 AE", path: "/study/aes" },
{ label: "我的任务", path: "/study/tasks" },
];
if (role.value === "PM")
return [
{ label: "派发任务", path: "/study/tasks" },
{ label: "发公告", path: "/study/faq" },
{ label: "查看 CRA 列表", path: "/study/tasks" },
];
if (role.value === "PV") return [{ label: "AE 列表", path: "/study/aes" }];
if (role.value === "IMP") return [{ label: "药品台账", path: "/study/imp/transactions" }];
return [];
});
const todayMorePath = computed(() => (role.value === "CRA" ? "/study/tasks" : "/study/tasks"));
const overdueMorePath = computed(() => (role.value === "CRA" ? "/study/tasks" : "/study/tasks"));
const actionMorePath = computed(() => {
if (role.value === "IMP") return "/study/imp/transactions";
if (role.value === "PV") return "/study/aes";
if (role.value === "PM" || role.value === "ADMIN") return "/study/finance";
return "/study/aes";
});
const isOverdue = (d?: string | null) => {
if (!d) return false;
return new Date(d) < new Date(todayStr.value);
};
const toTaskItem = (task: any): WorkItem => ({
title: task.title || "任务",
subtitle: study.currentStudy?.name || "",
status: task.due_date || task.status,
overdue: isOverdue(task.due_date),
path: "/study/tasks",
});
const toAeItem = (ae: any): WorkItem => ({
title: ae.term || "不良事件",
subtitle: study.currentStudy?.name || "",
status: ae.status,
overdue: false,
path: `/study/aes/${ae.id}`,
});
const toFinanceItem = (fi: any): WorkItem => ({
title: fi.title || "费用",
subtitle: study.currentStudy?.name || "",
status: fi.status,
overdue: false,
path: `/study/finance/${fi.id}`,
});
const toImpItem = (tx: any): WorkItem => ({
title: tx.type || "药品记录",
subtitle: study.currentStudy?.name || "",
status: tx.status || "",
overdue: false,
path: "/study/imp/transactions",
});
const loadData = async () => {
if (!currentStudyId.value) return;
loading.value = true;
try {
const studyId = currentStudyId.value;
const tasksReq = fetchTasks(studyId, { limit: 200, assignee_id: auth.user?.id });
const aesReq = fetchAes(studyId, { status: "NEW", limit: 50 });
const financeReq = fetchFinanceItems(studyId, { status: "SUBMITTED", limit: 50 });
const impReq = fetchImpTransactions ? fetchImpTransactions(studyId, { limit: 50 }) : Promise.resolve({ data: [] });
const [tasksRes, aesRes, financeRes, impRes] = await Promise.allSettled([tasksReq, aesReq, financeReq, impReq]);
const tasks = tasksRes.status === "fulfilled" ? (tasksRes.value.data.items || tasksRes.value.data || []) : [];
const aes = aesRes.status === "fulfilled" ? (aesRes.value.data.items || aesRes.value.data || []) : [];
const finances =
financeRes.status === "fulfilled" ? (financeRes.value.data.items || financeRes.value.data || []) : [];
const impTx = impRes.status === "fulfilled" ? (impRes.value.data.items || impRes.value.data || []) : [];
buildSections(tasks, aes, finances, impTx);
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || "工作台数据加载失败");
} finally {
loading.value = false;
}
};
const buildSections = (tasks: any[], aes: any[], finances: any[], impTx: any[]) => {
todayList.value = [];
overdueList.value = [];
actionList.value = [];
if (role.value === "CRA") {
todayList.value = tasks.filter((t) => t.due_date === todayStr.value).slice(0, 5).map(toTaskItem);
overdueList.value = tasks.filter((t) => isOverdue(t.due_date)).slice(0, 5).map(toTaskItem);
actionList.value = aes.slice(0, 5).map(toAeItem);
} else if (role.value === "PM") {
todayList.value = tasks.filter((t) => t.due_date === todayStr.value).slice(0, 5).map(toTaskItem);
overdueList.value = tasks.filter((t) => isOverdue(t.due_date)).slice(0, 5).map(toTaskItem);
const financePending = finances.slice(0, 3).map(toFinanceItem);
const aePending = aes.slice(0, 2).map(toAeItem);
actionList.value = [...financePending, ...aePending].slice(0, 5);
} else if (role.value === "PV") {
const aeOpen = aes.filter((a) => a.status !== "CLOSED");
todayList.value = aeOpen.slice(0, 5).map(toAeItem);
overdueList.value = aeOpen.filter((a) => isOverdue(a.updated_at)).slice(0, 5).map(toAeItem);
actionList.value = aeOpen.slice(0, 5).map(toAeItem);
} else if (role.value === "IMP") {
actionList.value = impTx.slice(0, 5).map(toImpItem);
todayList.value = [];
overdueList.value = [];
}
};
const goMore = (path: string) => {
if (!path) return;
router.push(path);
};
onMounted(async () => {
if (!auth.user && auth.token) {
await auth.fetchMe().catch(() => {});
}
loadData();
});
</script>
<style scoped>
.page {
padding: 16px;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
}
.sub {
color: #666;
font-size: 13px;
}
.empty-tip {
padding: 12px 0;
color: #888;
}
.section-card {
min-height: 220px;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.quick-list {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.empty {
color: #888;
}
.mt-12 {
margin-top: 12px;
}
.mb-12 {
margin-bottom: 12px;
}
</style>