26-7-31-1
This commit is contained in:
@@ -0,0 +1,840 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<!-- 统计 -->
|
||||
<el-row :gutter="12" class="stat-row">
|
||||
<el-col :xs="12" :sm="8" :md="4" v-for="c in statCards" :key="c.key">
|
||||
<div class="stat-card" :class="[{ active: c.active }, c.key]" @click="applyStat(c)">
|
||||
<div class="stat-label">{{ c.label }}</div>
|
||||
<div class="stat-value">{{ c.value }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-card shadow="never" class="panel">
|
||||
<template #header>
|
||||
<div class="header">
|
||||
<div>
|
||||
<div class="title">用户管理</div>
|
||||
<div class="sub">账号、角色、头像与启用状态维护</div>
|
||||
</div>
|
||||
<el-button type="primary" @click="openForm()">
|
||||
<el-icon class="btn-icon"><Plus /></el-icon>
|
||||
新增用户
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="filter-bar">
|
||||
<el-radio-group v-model="filters.role" size="default" @change="reload">
|
||||
<el-radio-button value="">全部角色</el-radio-button>
|
||||
<el-radio-button value="ADMIN">管理员</el-radio-button>
|
||||
<el-radio-button value="DOCTOR">医生</el-radio-button>
|
||||
<el-radio-button value="RADIOLOGIST">影像医师</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-radio-group v-model="filters.enabled" size="default" @change="reload">
|
||||
<el-radio-button value="">全部状态</el-radio-button>
|
||||
<el-radio-button value="true">启用</el-radio-button>
|
||||
<el-radio-button value="false">停用</el-radio-button>
|
||||
</el-radio-group>
|
||||
<div class="filter-right">
|
||||
<el-input
|
||||
v-model="filters.keyword"
|
||||
placeholder="账号 / 姓名 / 科室 / 电话"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
@clear="reload"
|
||||
@keyup.enter="reload"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button @click="reload">查询</el-button>
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table :data="rows" v-loading="loading" stripe empty-text="暂无用户" class="user-table">
|
||||
<el-table-column prop="id" label="ID" width="64" />
|
||||
<el-table-column label="用户" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div class="user-cell">
|
||||
<el-avatar :size="40" class="row-avatar" :src="row.avatar || undefined">
|
||||
{{ (row.realName || row.username || 'U').slice(0, 1) }}
|
||||
</el-avatar>
|
||||
<div class="user-meta">
|
||||
<div class="name-line">
|
||||
<span class="name">{{ row.realName || '—' }}</span>
|
||||
<el-tag v-if="isSelf(row)" size="small" type="warning" effect="plain">当前</el-tag>
|
||||
</div>
|
||||
<div class="username">@{{ row.username }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="角色" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="roleTagType(row.role)" effect="light" round>
|
||||
{{ roleLabel(row.role) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="department" label="科室" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.department || '—' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
:model-value="row.enabled !== false"
|
||||
:disabled="isSelf(row) && row.enabled !== false"
|
||||
inline-prompt
|
||||
active-text="启"
|
||||
inactive-text="停"
|
||||
@change="(v) => onSwitchEnabled(row, v)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="phone" label="电话" min-width="120">
|
||||
<template #default="{ row }">{{ row.phone || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="email" label="邮箱" min-width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.email || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="190" fixed="right" align="left">
|
||||
<template #default="{ row }">
|
||||
<div class="ops">
|
||||
<el-button link type="primary" class="op-btn" @click="openForm(row)">编辑</el-button>
|
||||
<el-button link type="success" class="op-btn" @click="openAvatar(row)">头像</el-button>
|
||||
<el-dropdown trigger="click" class="op-drop" @command="(cmd) => onMore(row, cmd)">
|
||||
<el-button link type="info" class="op-btn">更多</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="detail">查看详情</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="row.enabled === false"
|
||||
command="enable"
|
||||
>
|
||||
启用账号
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-else
|
||||
command="disable"
|
||||
:disabled="isSelf(row)"
|
||||
>
|
||||
停用账号
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
command="delete"
|
||||
divided
|
||||
:disabled="isSelf(row)"
|
||||
>
|
||||
<span class="danger">删除用户</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
v-model:current-page="pager.page"
|
||||
v-model:page-size="pager.size"
|
||||
:total="pager.total"
|
||||
layout="total, prev, pager, next, sizes"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
class="pager"
|
||||
@current-change="loadPage"
|
||||
@size-change="reload"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增 / 编辑 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="form.id ? '编辑用户' : '新增用户'"
|
||||
width="560px"
|
||||
destroy-on-close
|
||||
align-center
|
||||
append-to-body
|
||||
class="form-dialog"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px">
|
||||
<el-form-item label="账号" prop="username">
|
||||
<el-input
|
||||
v-model="form.username"
|
||||
:disabled="!!form.id"
|
||||
maxlength="50"
|
||||
placeholder="登录用户名"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="密码" :prop="form.id ? undefined : 'password'">
|
||||
<el-input
|
||||
v-model="form.password"
|
||||
:placeholder="form.id ? '留空则不修改密码' : '至少 6 位'"
|
||||
show-password
|
||||
maxlength="64"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="姓名" prop="realName">
|
||||
<el-input v-model="form.realName" placeholder="真实姓名" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="角色" prop="role">
|
||||
<el-select v-model="form.role" style="width: 100%">
|
||||
<el-option label="医生" value="DOCTOR" />
|
||||
<el-option label="影像医师" value="RADIOLOGIST" />
|
||||
<el-option label="管理员" value="ADMIN" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="科室">
|
||||
<el-select
|
||||
v-model="form.department"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
clearable
|
||||
placeholder="选择或输入科室"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option v-for="d in departments" :key="d" :label="d" :value="d" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="电话">
|
||||
<el-input v-model="form.phone" maxlength="20" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="邮箱">
|
||||
<el-input v-model="form.email" maxlength="80" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="状态">
|
||||
<el-switch
|
||||
v-model="form.enabled"
|
||||
:disabled="isSelfForm"
|
||||
active-text="启用"
|
||||
inactive-text="停用"
|
||||
/>
|
||||
<span v-if="isSelfForm" class="form-tip">不能停用当前登录账号</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="save">保 存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 头像 -->
|
||||
<el-dialog v-model="avatarVisible" title="设置用户头像" width="420px" destroy-on-close align-center append-to-body>
|
||||
<div class="avatar-panel">
|
||||
<el-avatar :size="96" class="row-avatar preview" :src="avatarPreview || avatarTarget?.avatar || undefined">
|
||||
{{ (avatarTarget?.realName || avatarTarget?.username || 'U').slice(0, 1) }}
|
||||
</el-avatar>
|
||||
<div class="avatar-user">
|
||||
{{ avatarTarget?.realName }}
|
||||
<span class="uname">@{{ avatarTarget?.username }}</span>
|
||||
</div>
|
||||
<el-upload
|
||||
:show-file-list="false"
|
||||
:auto-upload="false"
|
||||
accept="image/png,image/jpeg,image/jpg,image/gif,image/webp"
|
||||
:on-change="onAvatarPick"
|
||||
>
|
||||
<el-button type="primary" plain>选择图片</el-button>
|
||||
</el-upload>
|
||||
<div class="avatar-tip">支持 jpg / png / gif / webp,不超过 2MB</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="avatarVisible = false">取 消</el-button>
|
||||
<el-button type="primary" :loading="avatarSaving" :disabled="!avatarFile" @click="submitAvatar">
|
||||
上 传
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 详情 -->
|
||||
<el-drawer v-model="detailVisible" title="用户详情" size="400px" destroy-on-close append-to-body>
|
||||
<template v-if="detail">
|
||||
<div class="detail-head">
|
||||
<el-avatar :size="72" class="row-avatar" :src="detail.avatar || undefined">
|
||||
{{ (detail.realName || detail.username || 'U').slice(0, 1) }}
|
||||
</el-avatar>
|
||||
<div>
|
||||
<div class="detail-name">
|
||||
{{ detail.realName }}
|
||||
<el-tag v-if="isSelf(detail)" size="small" type="warning" effect="plain">当前</el-tag>
|
||||
</div>
|
||||
<div class="detail-user">@{{ detail.username }}</div>
|
||||
<el-tag :type="roleTagType(detail.role)" size="small" effect="light">
|
||||
{{ roleLabel(detail.role) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<el-descriptions :column="1" border class="detail-desc">
|
||||
<el-descriptions-item label="用户 ID">{{ detail.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="科室">{{ detail.department || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="电话">{{ detail.phone || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="邮箱">{{ detail.email || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="detail.enabled === false ? 'danger' : 'success'" size="small">
|
||||
{{ detail.enabled === false ? '已停用' : '启用' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div class="detail-actions">
|
||||
<el-button type="primary" @click="openForm(detail); detailVisible = false">编辑</el-button>
|
||||
<el-button @click="openAvatar(detail); detailVisible = false">改头像</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Search } from '@element-plus/icons-vue'
|
||||
import {
|
||||
listUsers,
|
||||
getUserStats,
|
||||
createUser,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
setUserEnabled,
|
||||
uploadUserAvatar
|
||||
} from '@/api/user'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { roleLabel, roleTagType } from '@/utils/labels'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const rows = ref([])
|
||||
const loading = ref(false)
|
||||
const pager = reactive({ page: 1, size: 10, total: 0 })
|
||||
const filters = reactive({ keyword: '', role: '', enabled: '' })
|
||||
const stats = ref({
|
||||
total: 0,
|
||||
adminCount: 0,
|
||||
doctorCount: 0,
|
||||
radiologistCount: 0,
|
||||
enabledCount: 0,
|
||||
disabledCount: 0
|
||||
})
|
||||
|
||||
const departments = ['内科', '外科', '影像科', '儿科', '妇科', '骨科', '急诊科', '信息科', '心内科', '神经内科']
|
||||
|
||||
const isSelf = (row) =>
|
||||
!!row &&
|
||||
(row.id === userStore.userInfo?.id || row.username === userStore.userInfo?.username)
|
||||
|
||||
const isSelfForm = computed(() => form.id && form.id === userStore.userInfo?.id)
|
||||
|
||||
const statCards = computed(() => [
|
||||
{
|
||||
key: 'total',
|
||||
label: '全部用户',
|
||||
value: stats.value.total ?? 0,
|
||||
active: !filters.role && !filters.enabled,
|
||||
clear: true
|
||||
},
|
||||
{
|
||||
key: 'admin',
|
||||
label: '管理员',
|
||||
value: stats.value.adminCount ?? 0,
|
||||
active: filters.role === 'ADMIN',
|
||||
role: 'ADMIN'
|
||||
},
|
||||
{
|
||||
key: 'doctor',
|
||||
label: '医生',
|
||||
value: stats.value.doctorCount ?? 0,
|
||||
active: filters.role === 'DOCTOR',
|
||||
role: 'DOCTOR'
|
||||
},
|
||||
{
|
||||
key: 'radio',
|
||||
label: '影像医师',
|
||||
value: stats.value.radiologistCount ?? 0,
|
||||
active: filters.role === 'RADIOLOGIST',
|
||||
role: 'RADIOLOGIST'
|
||||
},
|
||||
{
|
||||
key: 'on',
|
||||
label: '已启用',
|
||||
value: stats.value.enabledCount ?? 0,
|
||||
active: filters.enabled === 'true',
|
||||
enabled: 'true'
|
||||
},
|
||||
{
|
||||
key: 'off',
|
||||
label: '已停用',
|
||||
value: stats.value.disabledCount ?? 0,
|
||||
active: filters.enabled === 'false',
|
||||
enabled: 'false'
|
||||
}
|
||||
])
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const saving = ref(false)
|
||||
const formRef = ref()
|
||||
const emptyForm = () => ({
|
||||
id: null,
|
||||
username: '',
|
||||
password: '',
|
||||
realName: '',
|
||||
role: 'DOCTOR',
|
||||
department: '内科',
|
||||
phone: '',
|
||||
email: '',
|
||||
enabled: true
|
||||
})
|
||||
const form = reactive(emptyForm())
|
||||
const rules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入账号', trigger: 'blur' },
|
||||
{ min: 3, max: 50, message: '账号长度 3-50', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, message: '密码至少 6 位', trigger: 'blur' }
|
||||
],
|
||||
realName: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
||||
role: [{ required: true, message: '请选择角色', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const avatarVisible = ref(false)
|
||||
const avatarSaving = ref(false)
|
||||
const avatarTarget = ref(null)
|
||||
const avatarFile = ref(null)
|
||||
const avatarPreview = ref('')
|
||||
|
||||
const detailVisible = ref(false)
|
||||
const detail = ref(null)
|
||||
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
stats.value = (await getUserStats()) || stats.value
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
const loadPage = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
let enabled
|
||||
if (filters.enabled === 'true') enabled = true
|
||||
else if (filters.enabled === 'false') enabled = false
|
||||
const data = await listUsers({
|
||||
page: pager.page - 1,
|
||||
size: pager.size,
|
||||
keyword: filters.keyword || undefined,
|
||||
role: filters.role || undefined,
|
||||
enabled
|
||||
})
|
||||
rows.value = data?.content || []
|
||||
pager.total = data?.totalElements || 0
|
||||
} catch {
|
||||
rows.value = []
|
||||
pager.total = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const reload = () => {
|
||||
pager.page = 1
|
||||
loadPage()
|
||||
loadStats()
|
||||
}
|
||||
|
||||
const resetFilters = () => {
|
||||
filters.keyword = ''
|
||||
filters.role = ''
|
||||
filters.enabled = ''
|
||||
reload()
|
||||
}
|
||||
|
||||
const applyStat = (c) => {
|
||||
if (c.clear) {
|
||||
resetFilters()
|
||||
return
|
||||
}
|
||||
if (c.role) {
|
||||
filters.role = c.role
|
||||
filters.enabled = ''
|
||||
}
|
||||
if (c.enabled !== undefined) {
|
||||
filters.enabled = c.enabled
|
||||
filters.role = ''
|
||||
}
|
||||
reload()
|
||||
}
|
||||
|
||||
const openForm = (row) => {
|
||||
if (row) {
|
||||
Object.assign(form, {
|
||||
id: row.id,
|
||||
username: row.username,
|
||||
password: '',
|
||||
realName: row.realName || '',
|
||||
role: row.role || 'DOCTOR',
|
||||
department: row.department || '',
|
||||
phone: row.phone || '',
|
||||
email: row.email || '',
|
||||
enabled: row.enabled !== false
|
||||
})
|
||||
} else {
|
||||
Object.assign(form, emptyForm())
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
await formRef.value.validate()
|
||||
if (isSelfForm.value && form.enabled === false) {
|
||||
ElMessage.warning('不能停用当前登录账号')
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
const payload = {
|
||||
username: form.username,
|
||||
realName: form.realName,
|
||||
role: form.role,
|
||||
department: form.department,
|
||||
phone: form.phone,
|
||||
email: form.email,
|
||||
enabled: form.enabled,
|
||||
password: form.password || undefined
|
||||
}
|
||||
if (form.id && !payload.password) delete payload.password
|
||||
if (form.id) await updateUser(form.id, payload)
|
||||
else await createUser(payload)
|
||||
ElMessage.success('保存成功')
|
||||
dialogVisible.value = false
|
||||
reload()
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openAvatar = (row) => {
|
||||
avatarTarget.value = row
|
||||
avatarFile.value = null
|
||||
if (avatarPreview.value) URL.revokeObjectURL(avatarPreview.value)
|
||||
avatarPreview.value = ''
|
||||
avatarVisible.value = true
|
||||
}
|
||||
|
||||
const onAvatarPick = (file) => {
|
||||
const raw = file.raw
|
||||
if (!raw) return
|
||||
if (!raw.type?.startsWith('image/')) {
|
||||
ElMessage.warning('请选择图片文件')
|
||||
return
|
||||
}
|
||||
if (raw.size > 2 * 1024 * 1024) {
|
||||
ElMessage.warning('图片不能超过 2MB')
|
||||
return
|
||||
}
|
||||
avatarFile.value = raw
|
||||
if (avatarPreview.value) URL.revokeObjectURL(avatarPreview.value)
|
||||
avatarPreview.value = URL.createObjectURL(raw)
|
||||
}
|
||||
|
||||
const submitAvatar = async () => {
|
||||
if (!avatarFile.value || !avatarTarget.value) return
|
||||
avatarSaving.value = true
|
||||
try {
|
||||
const user = await uploadUserAvatar(avatarTarget.value.id, avatarFile.value)
|
||||
ElMessage.success('头像已更新')
|
||||
avatarVisible.value = false
|
||||
if (userStore.userInfo?.id === user.id) {
|
||||
userStore.setUserInfo({ ...userStore.userInfo, ...user })
|
||||
}
|
||||
loadPage()
|
||||
} finally {
|
||||
avatarSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const onSwitchEnabled = async (row, next) => {
|
||||
if (isSelf(row) && !next) {
|
||||
ElMessage.warning('不能停用当前登录账号')
|
||||
return
|
||||
}
|
||||
const action = next ? '启用' : '停用'
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定${action}账号「${row.username}」?`, '确认', { type: 'warning' })
|
||||
await setUserEnabled(row.id, next)
|
||||
ElMessage.success(`已${action}`)
|
||||
reload()
|
||||
} catch {
|
||||
/* 取消 */
|
||||
}
|
||||
}
|
||||
|
||||
const onMore = async (row, cmd) => {
|
||||
if (cmd === 'detail') {
|
||||
detail.value = { ...row }
|
||||
detailVisible.value = true
|
||||
return
|
||||
}
|
||||
if (cmd === 'enable') {
|
||||
await onSwitchEnabled(row, true)
|
||||
return
|
||||
}
|
||||
if (cmd === 'disable') {
|
||||
await onSwitchEnabled(row, false)
|
||||
return
|
||||
}
|
||||
if (cmd === 'delete') {
|
||||
await remove(row)
|
||||
}
|
||||
}
|
||||
|
||||
const remove = async (row) => {
|
||||
if (isSelf(row)) {
|
||||
ElMessage.warning('不能删除当前登录账号')
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm(
|
||||
`确定删除用户「${row.realName || row.username}」?此操作不可恢复。`,
|
||||
'删除确认',
|
||||
{ type: 'warning', confirmButtonText: '删除' }
|
||||
)
|
||||
await deleteUser(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
reload()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadPage()
|
||||
loadStats()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stat-row {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.stat-card {
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
border-radius: 12px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid #eef2f7;
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(15, 61, 102, 0.08);
|
||||
}
|
||||
.stat-card.active {
|
||||
border-color: #409eff;
|
||||
box-shadow: 0 0 0 1px rgba(64, 158, 255, 0.2);
|
||||
}
|
||||
.stat-card.total { border-top: 3px solid #1e6091; }
|
||||
.stat-card.admin { border-top: 3px solid #f56c6c; }
|
||||
.stat-card.doctor { border-top: 3px solid #67c23a; }
|
||||
.stat-card.radio { border-top: 3px solid #e6a23c; }
|
||||
.stat-card.on { border-top: 3px solid #409eff; }
|
||||
.stat-card.off { border-top: 3px solid #909399; }
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
.stat-value {
|
||||
margin-top: 4px;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.panel {
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
.sub {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.btn-icon {
|
||||
margin-right: 4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.filter-right {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-left: auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.user-meta {
|
||||
min-width: 0;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.name-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.name {
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
}
|
||||
.username {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.row-avatar {
|
||||
background: linear-gradient(135deg, #1e6091, #38bdf8);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.danger {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
/* 操作列:统一水平/垂直对齐,避免 dropdown 把「更多」顶高 */
|
||||
.ops {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
height: 32px;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.ops :deep(.op-btn) {
|
||||
margin: 0 !important;
|
||||
padding: 0 6px !important;
|
||||
height: 28px !important;
|
||||
line-height: 28px !important;
|
||||
font-size: 13px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.ops :deep(.op-drop) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.ops :deep(.el-dropdown) {
|
||||
line-height: 28px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.pager {
|
||||
margin-top: 16px;
|
||||
justify-content: flex-end;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
margin-left: 10px;
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.avatar-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.avatar-panel .preview {
|
||||
font-size: 32px;
|
||||
}
|
||||
.avatar-user {
|
||||
font-size: 15px;
|
||||
color: #0f172a;
|
||||
font-weight: 600;
|
||||
}
|
||||
.avatar-user .uname {
|
||||
margin-left: 6px;
|
||||
font-weight: 400;
|
||||
color: #94a3b8;
|
||||
font-size: 13px;
|
||||
}
|
||||
.avatar-tip {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.detail-head {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.detail-name {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.detail-user {
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.detail-desc {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.detail-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user