feat(users): implement user and permission management

This commit is contained in:
yelan
2026-08-27 16:48:41 +08:00
parent 332fb53cfa
commit ac8eff5286
5 changed files with 574 additions and 15 deletions

View File

@@ -0,0 +1,81 @@
<script setup lang="ts">
import type { RoleSummary } from '../types'
defineProps<{
rows: RoleSummary[]
}>()
</script>
<template>
<div class="role-cards">
<article v-for="role in rows" :key="role.id" class="role-card">
<h3>
<span class="role-color" :style="{ backgroundColor: role.color }" aria-hidden="true" />
{{ role.name }}
</h3>
<p>{{ role.description }}</p>
<span class="role-count">{{ role.userCount }} </span>
</article>
</div>
</template>
<style scoped>
.role-cards {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
margin-bottom: 16px;
}
.role-card {
padding: 13px 14px;
background: #fff;
border: 1px solid var(--line);
border-radius: 10px;
}
.role-card h3 {
display: flex;
gap: 7px;
align-items: center;
margin: 0;
color: var(--ink);
font-size: 13.5px;
}
.role-color {
width: 10px;
height: 10px;
flex-shrink: 0;
border-radius: 3px;
}
.role-card p {
min-height: 35px;
margin: 6px 0;
color: var(--mut);
font-size: 11.5px;
line-height: 1.5;
}
.role-count {
display: inline-block;
padding: 2px 9px;
color: var(--brand);
font-size: 11px;
background: var(--brand-l);
border-radius: 20px;
}
@media (max-width: 1200px) {
.role-cards {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 600px) {
.role-cards {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,182 @@
<script setup lang="ts">
import type { UserStatus, UserTableRow } from '../types'
defineProps<{
rows: UserTableRow[]
roleColors: Record<string, string>
}>()
const emit = defineEmits<{
edit: [row: UserTableRow]
resetPassword: [row: UserTableRow]
}>()
const statusLabels: Record<UserStatus, string> = {
enabled: '启用',
disabled: '停用',
}
</script>
<template>
<div class="table-scroll">
<table class="user-table">
<thead>
<tr>
<th>用户</th>
<th>工号</th>
<th>院区</th>
<th>科室</th>
<th>角色</th>
<th>数据范围</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="row in rows" :key="row.id">
<td>
<div class="user-cell">
<span
class="user-avatar"
:style="{ backgroundColor: roleColors[row.role] ?? '#8a9aa3' }"
aria-hidden="true"
>
{{ row.name.slice(0, 1) }}
</span>
<strong>{{ row.name }}</strong>
</div>
</td>
<td>{{ row.employeeNo }}</td>
<td>{{ row.campus }}</td>
<td>{{ row.department }}</td>
<td>
<span
class="role-tag"
:style="{
color: roleColors[row.role] ?? '#8a9aa3',
backgroundColor: `${roleColors[row.role] ?? '#8a9aa3'}22`,
}"
>
{{ row.role }}
</span>
</td>
<td>{{ row.dataScope }}</td>
<td>
<span class="status-tag" :class="'status-' + row.status">
{{ statusLabels[row.status] }}
</span>
</td>
<td class="actions-cell">
<button type="button" class="table-button" @click="emit('edit', row)">编辑</button>
<button type="button" class="table-button" @click="emit('resetPassword', row)">
重置密码
</button>
</td>
</tr>
<tr v-if="!rows.length">
<td colspan="8" class="empty-cell">没有找到匹配的用户</td>
</tr>
</tbody>
</table>
</div>
</template>
<style scoped>
.table-scroll {
width: 100%;
overflow-x: auto;
}
.user-table {
width: 100%;
min-width: 1040px;
font-size: 13px;
border-collapse: collapse;
}
.user-table th {
padding: 9px 12px;
color: var(--mut);
font-size: 12.5px;
font-weight: 700;
text-align: left;
white-space: nowrap;
background: #f5f9fa;
border-bottom: 1px solid var(--line);
}
.user-table td {
padding: 9px 12px;
vertical-align: middle;
border-bottom: 1px solid #eef4f6;
}
.user-table tbody tr:hover td {
background: #f7fbfc;
}
.user-cell {
display: flex;
gap: 9px;
align-items: center;
}
.user-avatar {
display: grid;
flex-shrink: 0;
width: 30px;
height: 30px;
color: #fff;
font-size: 12px;
font-weight: 700;
place-items: center;
border-radius: 50%;
}
.role-tag,
.status-tag {
display: inline-block;
padding: 2px 10px;
font-size: 11.5px;
font-weight: 700;
white-space: nowrap;
border-radius: 20px;
}
.status-enabled {
color: var(--ok);
background: var(--ok-l);
}
.status-disabled {
color: #8a9aa3;
background: #eceef0;
}
.actions-cell {
white-space: nowrap;
}
.table-button {
padding: 4px 10px;
color: var(--brand);
font-size: 12px;
background: #fff;
border: 1px solid var(--brand);
border-radius: 5px;
}
.table-button + .table-button {
margin-left: 6px;
}
.table-button:hover {
background: var(--brand-l);
}
.empty-cell {
padding: 40px 12px !important;
color: var(--mut);
text-align: center;
}
</style>

View File

@@ -1,12 +1,196 @@
<script setup lang="ts"> <script setup lang="ts">
import PagePlaceholder from '@/components/PagePlaceholder.vue' import { computed, onMounted, ref } from 'vue'
import { ElMessage } from 'element-plus'
import RoleSummaryCards from './components/RoleSummaryCards.vue'
import UserTable from './components/UserTable.vue'
import { roleColors, roleSummaries, users } from './mock'
import type { RoleSummary, UserTableRow } from './types'
const loading = ref(true)
const keyword = ref('')
const roleRows = ref<RoleSummary[]>([])
const userRows = ref<UserTableRow[]>([])
const visibleUsers = computed(() => {
const normalizedKeyword = keyword.value.trim().toLowerCase()
return userRows.value.filter((user) => {
const matchesCampus = user.campus === '本部院区'
const matchesKeyword =
!normalizedKeyword ||
[user.name, user.employeeNo].join(' ').toLowerCase().includes(normalizedKeyword)
return matchesCampus && matchesKeyword
})
})
async function loadUsers() {
loading.value = true
await Promise.resolve()
roleRows.value = roleSummaries.map((role) => ({ ...role }))
userRows.value = users.map((user) => ({ ...user }))
loading.value = false
}
function showAddUserMessage() {
ElMessage.info('原型演示:新增用户')
}
function showImportMessage() {
ElMessage.info('原型演示:批量导入')
}
function editUser(user: UserTableRow) {
ElMessage.info(`原型演示:编辑用户《${user.name}`)
}
function resetPassword(user: UserTableRow) {
ElMessage.success(`已重置${user.name}的密码`)
}
onMounted(loadUsers)
</script> </script>
<template> <template>
<PagePlaceholder <section class="users-page">
group="管理" <div v-if="loading" class="loading-card" aria-label="正在加载用户与角色">
title="用户与权限" <span v-for="index in 4" :key="index" />
icon="◉" </div>
description="用户与权限页面占位,待根据 Web 端需求实现。"
<template v-else>
<RoleSummaryCards :rows="roleRows" />
<section class="page-card">
<div class="card-heading">
<h2>用户列表</h2>
<span class="heading-spacer" />
<input
v-model="keyword"
class="user-search"
type="search"
placeholder="搜索姓名 / 工号"
aria-label="搜索姓名或工号"
/> />
<button type="button" class="table-button" @click="showAddUserMessage">新增用户</button>
<button type="button" class="table-button" @click="showImportMessage">批量导入</button>
</div>
<UserTable
:rows="visibleUsers"
:role-colors="roleColors"
@edit="editUser"
@reset-password="resetPassword"
/>
</section>
</template>
</section>
</template> </template>
<style scoped>
.users-page {
width: 100%;
max-width: 1440px;
margin: 0 auto;
}
.page-card {
padding: 16px 18px;
background: var(--card);
border-radius: var(--r);
box-shadow: var(--sh);
}
.card-heading {
display: flex;
gap: 10px;
align-items: center;
margin-bottom: 14px;
}
.card-heading h2 {
margin: 0;
color: var(--ink);
font-size: 14.5px;
line-height: 1.4;
}
.heading-spacer {
flex: 1;
}
.user-search {
width: 180px;
padding: 6px 10px;
color: var(--ink);
font-size: 12.5px;
background: #fff;
border: 1px solid var(--line);
border-radius: 6px;
}
.user-search:focus {
outline: none;
border-color: var(--brand);
}
.table-button {
padding: 4px 10px;
color: var(--brand);
font-size: 12px;
background: #fff;
border: 1px solid var(--brand);
border-radius: 5px;
}
.table-button:hover {
background: var(--brand-l);
}
.loading-card {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
}
.loading-card span {
display: block;
height: 116px;
background: linear-gradient(90deg, #edf3f5 25%, #f7fafb 37%, #edf3f5 63%);
background-size: 400% 100%;
border-radius: 10px;
box-shadow: var(--sh);
animation: skeleton-shimmer 1.4s ease infinite;
}
@keyframes skeleton-shimmer {
0% {
background-position: 100% 0;
}
100% {
background-position: -100% 0;
}
}
@media (max-width: 900px) {
.card-heading {
align-items: flex-start;
flex-wrap: wrap;
}
.heading-spacer {
display: none;
}
.user-search {
flex: 1;
min-width: 180px;
}
}
@media (max-width: 600px) {
.loading-card {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,109 @@
import type { RoleSummary, UserTableRow } from './types'
export const roleSummaries: RoleSummary[] = [
{
id: 'system-admin',
name: '系统管理员',
color: '#6b5bd2',
description: '系统配置、用户与权限管理、审计日志',
userCount: 2,
},
{
id: 'medical-management',
name: '医务管理',
color: '#0e6e8c',
description: '全院文档库维护、报表分析、任务监督',
userCount: 4,
},
{
id: 'department-signer',
name: '科室签署员',
color: '#0f9d6c',
description: '本科室文档发起签署、手写板 / 短信操作',
userCount: 23,
},
{
id: 'audit-viewer',
name: '查询审计员',
color: '#d9821f',
description: '只读查询报表与审计留痕,不可发起',
userCount: 3,
},
]
export const users: UserTableRow[] = [
{
id: 'user-001',
name: '张文静',
employeeNo: '10234',
campus: '本部院区',
department: '医务处',
role: '医务管理',
dataScope: '全院',
status: 'enabled',
},
{
id: 'user-002',
name: '刘明辉',
employeeNo: '10301',
campus: '本部院区',
department: '放射科',
role: '科室签署员',
dataScope: '本科室',
status: 'enabled',
},
{
id: 'user-003',
name: '王芳',
employeeNo: '10455',
campus: '本部院区',
department: '心内科',
role: '科室签署员',
dataScope: '本科室',
status: 'enabled',
},
{
id: 'user-004',
name: '李国栋',
employeeNo: '10512',
campus: '东院区',
department: '骨科',
role: '科室签署员',
dataScope: '本科室',
status: 'enabled',
},
{
id: 'user-005',
name: '陈晓云',
employeeNo: '10188',
campus: '本部院区',
department: '信息科',
role: '系统管理员',
dataScope: '全院',
status: 'enabled',
},
{
id: 'user-006',
name: '赵雪梅',
employeeNo: '10602',
campus: '东院区',
department: '健康管理中心',
role: '科室签署员',
dataScope: '本科室',
status: 'disabled',
},
{
id: 'user-007',
name: '孙立',
employeeNo: '10733',
campus: '本部院区',
department: '纪检监察室',
role: '查询审计员',
dataScope: '全院只读',
status: 'enabled',
},
]
export const roleColors = Object.fromEntries(
roleSummaries.map((role) => [role.name, role.color]),
) as Record<string, string>

View File

@@ -1,17 +1,20 @@
export type UserFilterStatus = 'all' | 'enabled' | 'disabled' export type UserStatus = 'enabled' | 'disabled'
export interface UserFilterForm { export interface RoleSummary {
keyword: string id: string
department: string name: string
status: UserFilterStatus color: string
description: string
userCount: number
} }
export interface UserTableRow { export interface UserTableRow {
id: string id: string
name: string name: string
account: string employeeNo: string
campus: string
department: string department: string
role: string role: string
status: UserFilterStatus dataScope: string
lastLoginAt: string status: UserStatus
} }