管理后台前后端逻辑、UI美化
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user