工作台界面-初版
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user