优化后台界面与无操作退出体验

This commit is contained in:
Cheng Zhou
2026-06-10 15:20:06 +08:00
parent ea3f19e241
commit 84e55af8fe
39 changed files with 793 additions and 608 deletions
+87 -25
View File
@@ -65,7 +65,6 @@
clearable
class="filter-input-comp"
:prefix-icon="Search"
@clear="applyFilters"
@keyup.enter="applyFilters"
/>
</div>
@@ -77,9 +76,6 @@
<el-option :label="TEXT.enums.userStatus.DISABLED" value="DISABLED" />
</el-select>
</div>
<div class="filter-item-form">
<el-button @click="applyFilters" :icon="Refresh">{{ TEXT.common.actions.search }}</el-button>
</div>
<div class="filter-spacer"></div>
<el-button type="primary" :icon="Plus" @click="openCreate">{{ TEXT.common.actions.add }}{{ TEXT.modules.adminUsers.userLabel }}</el-button>
</div>
@@ -88,7 +84,7 @@
<!-- 用户表格 -->
<div class="unified-section user-table-section">
<el-table :data="users" v-loading="loading" class="user-table" style="width: 100%" table-layout="fixed">
<el-table-column :label="TEXT.common.fields.name" min-width="200">
<el-table-column :label="TEXT.common.fields.name" width="360">
<template #default="scope">
<div class="user-cell">
<div class="user-avatar" :class="'avatar--' + (scope.row.status || '').toLowerCase()">
@@ -102,40 +98,42 @@
</template>
</el-table-column>
<el-table-column prop="clinical_department" :label="TEXT.modules.adminUsers.clinicalDepartmentLabel" show-overflow-tooltip />
<el-table-column :label="TEXT.common.fields.status" width="100">
<el-table-column :label="TEXT.common.fields.status">
<template #default="scope">
<span class="status-dot" :class="'dot--' + (scope.row.status || '').toLowerCase()"></span>
{{ statusLabel(scope.row.status) }}
</template>
</el-table-column>
<el-table-column prop="created_at" :label="TEXT.modules.adminUsers.createdAt" width="160" show-overflow-tooltip>
<el-table-column prop="created_at" :label="TEXT.modules.adminUsers.createdAt" show-overflow-tooltip>
<template #default="scope">
<span class="text-muted">{{ displayDateTime(scope.row.created_at) }}</span>
</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" width="180" align="center" fixed="right">
<el-table-column :label="TEXT.common.labels.actions" align="center">
<template #default="scope">
<div class="action-row">
<el-tooltip :content="TEXT.common.actions.edit" placement="top">
<el-button link type="primary" :icon="Edit" @click="openEdit(scope.row)" />
<el-button link type="primary" :icon="Edit" class="action-btn" @click="openEdit(scope.row)" />
</el-tooltip>
<el-tooltip :content="scope.row.status === 'ACTIVE' ? TEXT.common.actions.disable : TEXT.common.actions.enable" placement="top">
<el-button
link
:type="scope.row.status === 'ACTIVE' ? 'warning' : 'success'"
:icon="scope.row.status === 'ACTIVE' ? Lock : Unlock"
class="action-btn"
:disabled="isLastAdmin(scope.row)"
@click="toggleStatus(scope.row)"
/>
</el-tooltip>
<el-tooltip :content="TEXT.modules.adminUsers.resetPassword" placement="top">
<el-button link type="warning" :icon="Key" @click="openReset(scope.row)" />
<el-button link type="warning" :icon="Key" class="action-btn" @click="openReset(scope.row)" />
</el-tooltip>
<el-tooltip :content="TEXT.common.actions.delete" placement="top">
<el-button
link
type="danger"
:icon="Delete"
class="action-btn"
:disabled="scope.row.id === auth.user?.id"
@click="onDelete(scope.row)"
/>
@@ -164,9 +162,9 @@
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Edit, Delete, Key, Lock, Unlock, Search, Plus, Refresh } from "@element-plus/icons-vue";
import { Edit, Delete, Key, Lock, Unlock, Search, Plus } from "@element-plus/icons-vue";
import { fetchUsers, updateUser, deleteUser } from "../../api/users";
import { fetchStudies } from "../../api/studies";
import { listMembers } from "../../api/members";
@@ -191,6 +189,7 @@ const statusFilter = ref("");
const editingUser = ref<UserInfo | null>(null);
const resetUser = ref<UserInfo | null>(null);
const auth = useAuthStore();
let keywordSearchTimer: ReturnType<typeof setTimeout> | null = null;
const activeCount = computed(() => allUsers.value.filter(u => u.status === 'ACTIVE').length);
const pendingCount = computed(() => allUsers.value.filter(u => u.status === 'PENDING').length);
@@ -251,10 +250,30 @@ const loadUsers = async () => {
};
const applyFilters = () => {
clearKeywordSearchTimer();
page.value = 1;
loadUsers();
};
const clearKeywordSearchTimer = () => {
if (!keywordSearchTimer) return;
clearTimeout(keywordSearchTimer);
keywordSearchTimer = null;
};
const scheduleKeywordSearch = () => {
clearKeywordSearchTimer();
keywordSearchTimer = setTimeout(() => {
keywordSearchTimer = null;
page.value = 1;
loadUsers();
}, 300);
};
watch(searchKeyword, () => {
scheduleKeywordSearch();
});
const onPageSizeChange = (size: number) => {
pageSize.value = size;
page.value = 1;
@@ -360,23 +379,27 @@ const findUserProjects = async (userId: string) => {
onMounted(() => {
loadUsers();
});
onBeforeUnmount(() => {
clearKeywordSearchTimer();
});
</script>
<style scoped>
.stats-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
padding: 16px;
gap: 10px;
padding: 10px 16px;
border-bottom: 1px solid var(--unified-shell-divider);
}
.stat-card {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
border-radius: 12px;
gap: 10px;
padding: 9px 12px;
border-radius: 10px;
background: var(--ctms-neutral-100);
transition: var(--ctms-transition);
}
@@ -387,9 +410,9 @@ onMounted(() => {
}
.stat-icon {
width: 38px;
height: 38px;
border-radius: 10px;
width: 32px;
height: 32px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
@@ -397,8 +420,8 @@ onMounted(() => {
}
.stat-icon svg {
width: 20px;
height: 20px;
width: 18px;
height: 18px;
}
.stat-card--total .stat-icon {
@@ -427,14 +450,14 @@ onMounted(() => {
}
.stat-value {
font-size: 22px;
font-size: 20px;
font-weight: 700;
line-height: 1.2;
color: var(--ctms-text-main);
}
.stat-label {
font-size: 12px;
font-size: 11px;
color: var(--ctms-text-secondary);
margin-top: 2px;
}
@@ -559,10 +582,49 @@ onMounted(() => {
.action-row {
display: inline-flex;
align-items: center;
gap: 6px;
justify-content: center;
gap: 4px;
padding: 2px;
border-radius: 8px;
background: rgba(248, 250, 252, 0.72);
box-shadow: inset 0 0 0 1px rgba(148, 163, 184, 0.14);
white-space: nowrap;
}
.action-btn {
width: 28px;
height: 28px;
min-height: 28px;
padding: 0;
margin: 0;
border-radius: 7px;
font-size: 15px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.94), rgba(248, 250, 252, 0.82));
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06), inset 0 0 0 1px rgba(148, 163, 184, 0.16);
transition: transform 120ms ease, box-shadow 120ms ease, background-color 120ms ease;
}
.action-row :deep(.el-button + .el-button) {
margin-left: 0;
}
.action-row :deep(.el-button .el-icon) {
width: 15px;
height: 15px;
}
.action-btn:hover {
transform: translateY(-1px);
background: #fff;
box-shadow: 0 4px 10px rgba(15, 23, 42, 0.1), inset 0 0 0 1px rgba(148, 163, 184, 0.22);
}
.action-btn.is-disabled {
transform: none;
opacity: 0.45;
box-shadow: inset 0 0 0 1px rgba(148, 163, 184, 0.12);
}
/* 表格区域 */
.user-table-section {
padding: 0;