管理后台前后端逻辑、UI美化

This commit is contained in:
Cheng Zhou
2026-01-16 13:50:08 +08:00
parent 05c1f9579a
commit 7fdcfdaadd
82 changed files with 3210 additions and 549 deletions
+162 -27
View File
@@ -4,36 +4,54 @@
<div class="header">
<div>
<h3>{{ TEXT.modules.adminProjects.title }}</h3>
<p class="sub">{{ TEXT.modules.adminProjects.subtitle }}</p>
</div>
<el-button type="primary" @click="openCreate">{{ TEXT.common.actions.add }}{{ TEXT.modules.adminProjects.projectLabel }}</el-button>
</div>
<el-table :data="projects" v-loading="loading" stripe>
<el-table-column prop="code" :label="TEXT.modules.adminProjects.projectCode" width="140">
<el-table-column prop="name" :label="TEXT.common.fields.projectName" min-width="300">
<template #default="scope">
<el-link type="primary" @click="goDetail(scope.row)">{{ scope.row.code }}</el-link>
<el-link type="primary" @click="goDetail(scope.row)" class="font-medium">{{ scope.row.name }}</el-link>
</template>
</el-table-column>
<el-table-column prop="name" :label="TEXT.common.fields.projectName" min-width="200">
<el-table-column prop="status" :label="TEXT.common.fields.status" min-width="120" align="center">
<template #default="scope">
<el-link type="primary" @click="goDetail(scope.row)">{{ scope.row.name }}</el-link>
<el-tag :type="statusTag(scope.row.status)" effect="plain" round>{{ statusLabel(scope.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="sponsor" :label="TEXT.common.fields.sponsor" min-width="160" />
<el-table-column prop="status" :label="TEXT.common.fields.status" width="120">
<el-table-column prop="is_locked" label="锁定状态" min-width="100" align="center">
<template #default="scope">
<el-tag :type="statusTag(scope.row.status)">{{ statusLabel(scope.row.status) }}</el-tag>
<el-tag v-if="scope.row.is_locked" type="warning" effect="plain" round>已锁定</el-tag>
<el-tag v-else type="success" effect="plain" round>未锁定</el-tag>
</template>
</el-table-column>
<el-table-column prop="created_at" :label="TEXT.modules.adminUsers.createdAt" width="200">
<template #default="scope">{{ displayDateTime(scope.row.created_at) }}</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" width="320" fixed="right">
<el-table-column prop="created_at" :label="TEXT.modules.adminUsers.createdAt" min-width="180" align="center">
<template #default="scope">
<el-button link type="primary" size="small" @click="openEdit(scope.row)">{{ TEXT.common.actions.edit }}</el-button>
<el-button link type="primary" size="small" @click="goMembers(scope.row)">{{ TEXT.modules.adminProjects.members }}</el-button>
<el-button link type="primary" size="small" @click="goSites(scope.row)">{{ TEXT.modules.adminProjects.sites }}</el-button>
<el-button link type="success" size="small" @click="enterStudy(scope.row)">{{ TEXT.modules.adminProjects.enter }}</el-button>
<span class="text-gray-500">{{ displayDateTime(scope.row.created_at) }}</span>
</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" min-width="280" align="center">
<template #default="scope">
<el-tooltip :content="TEXT.modules.adminProjects.members" placement="top">
<el-button link type="primary" :icon="User" class="action-btn" @click="goMembers(scope.row)" />
</el-tooltip>
<el-tooltip :content="TEXT.modules.adminProjects.sites" placement="top">
<el-button link type="primary" :icon="OfficeBuilding" class="action-btn" @click="goSites(scope.row)" />
</el-tooltip>
<el-tooltip :content="TEXT.modules.adminProjects.enter" placement="top">
<el-button link type="success" :icon="ArrowRight" class="action-btn enter-btn" @click="enterStudy(scope.row)" />
</el-tooltip>
<el-tooltip v-if="!scope.row.is_locked" content="锁定项目" placement="top">
<el-button
link
type="info"
:icon="Lock"
class="action-btn"
@click="handleLockToggle(scope.row)"
/>
</el-tooltip>
<el-tooltip :content="TEXT.common.actions.delete" placement="top">
<el-button link type="danger" :icon="Delete" class="action-btn" @click="handleDelete(scope.row)" />
</el-tooltip>
</template>
</el-table-column>
</el-table>
@@ -45,8 +63,9 @@
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import { fetchStudies } from "../../api/studies";
import { ElMessage, ElMessageBox } from "element-plus";
import { User, OfficeBuilding, ArrowRight, Delete, Lock } from "@element-plus/icons-vue";
import { fetchStudies, deleteStudy, lockStudy } from "../../api/studies";
import type { Study } from "../../types/api";
import ProjectForm from "./ProjectForm.vue";
import { useStudyStore } from "../../store/study";
@@ -78,11 +97,6 @@ const openCreate = () => {
formVisible.value = true;
};
const openEdit = (row: Study) => {
editingProject.value = row;
formVisible.value = true;
};
const goMembers = (row: Study) => {
router.push(`/admin/projects/${row.id}/members`);
};
@@ -91,6 +105,79 @@ const goSites = (row: Study) => {
router.push(`/admin/projects/${row.id}/sites`);
};
const handleDelete = async (study: Study) => {
try {
// 第一次确认
await ElMessageBox.confirm(
`确定要删除项目\"${study.name}\"吗?该操作将永久删除项目及其所有关联数据(成员、站点等),且不可恢复!`,
TEXT.common.actions.delete,
{
confirmButtonText: "继续",
cancelButtonText: TEXT.common.actions.cancel,
type: "error",
dangerouslyUseHTMLString: false,
}
);
// 第二次确认:要求输入项目名称
const { value } = await ElMessageBox.prompt(
`请输入项目名称 "${study.name}" 以确认删除`,
"删除确认",
{
confirmButtonText: TEXT.common.actions.confirm,
cancelButtonText: TEXT.common.actions.cancel,
inputPattern: new RegExp(`^${study.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`),
inputErrorMessage: "项目名称不匹配",
inputPlaceholder: "请输入项目名称",
type: "error",
}
);
if (value !== study.name) {
ElMessage.warning("项目名称不匹配,已取消删除");
return;
}
await deleteStudy(study.id);
ElMessage.success("项目已删除");
await loadProjects();
} catch (err: any) {
if (err !== "cancel") {
const errorMsg = err?.response?.data?.detail || err?.message || "删除项目失败";
console.error("删除项目错误:", err);
ElMessage.error(errorMsg);
}
}
};
const handleLockToggle = async (study: Study) => {
// 锁定后不可解锁
if (study.is_locked) {
ElMessage.warning("项目已锁定,不可解锁");
return;
}
try {
await ElMessageBox.confirm(
`确定要锁定项目\"${study.name}\"吗?锁定后项目内数据将无法编辑,且不可解锁!`,
"锁定项目",
{
confirmButtonText: TEXT.common.actions.confirm,
cancelButtonText: TEXT.common.actions.cancel,
type: "warning",
}
);
await lockStudy(study.id);
ElMessage.success("项目已锁定");
await loadProjects();
} catch (err) {
if (err !== "cancel") {
ElMessage.error("锁定项目失败");
}
}
};
const enterStudy = (row: Study) => {
studyStore.setCurrentStudy({ ...row, role_in_study: "PM" } as Study);
router.push("/project/overview");
@@ -117,16 +204,64 @@ onMounted(() => {
<style scoped>
.page {
padding: 16px;
padding: 24px;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
margin-bottom: 24px;
}
.header h3 {
font-size: 20px;
font-weight: 600;
color: #1f2937;
margin: 0;
}
.sub {
color: #666;
margin-top: 4px;
color: #6b7280;
margin-top: 8px;
font-size: 14px;
}
/* 表格样式美化 */
:deep(.el-table) {
--el-table-header-bg-color: #f9fafb;
--el-table-header-text-color: #374151;
border-radius: 8px;
overflow: hidden;
}
:deep(.el-table th.el-table__cell) {
background-color: #f9fafb;
font-weight: 600;
height: 50px;
}
/* 增加单元格间距 */
:deep(.el-table .el-table__cell) {
padding: 16px 0;
}
/* 操作按钮美化 */
.action-btn {
font-size: 20px; /* 图标变更大 */
padding: 6px;
margin: 0 4px;
transition: all 0.3s;
}
.action-btn:hover {
transform: scale(1.1);
background-color: #f3f4f6;
border-radius: 4px;
}
.enter-btn {
font-size: 20px;
font-weight: bold;
}
</style>