|
|
@@ -0,0 +1,494 @@
|
|
|
+<template>
|
|
|
+ <div class="page-container">
|
|
|
+ <!-- 搜索栏 -->
|
|
|
+ <el-card class="search-card" shadow="never">
|
|
|
+ <el-form :model="query" inline>
|
|
|
+ <el-form-item label="用户名/姓名">
|
|
|
+ <el-input
|
|
|
+ v-model="query.keyword"
|
|
|
+ placeholder="请输入用户名或姓名"
|
|
|
+ clearable
|
|
|
+ style="width: 200px"
|
|
|
+ @keyup.enter="handleSearch"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="角色">
|
|
|
+ <el-select v-model="query.adminRole" placeholder="全部" clearable style="width: 130px">
|
|
|
+ <el-option label="超级管理员" :value="1" />
|
|
|
+ <el-option label="运营人员" :value="2" />
|
|
|
+ <el-option label="只读用户" :value="3" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="状态">
|
|
|
+ <el-select v-model="query.status" placeholder="全部" clearable style="width: 100px">
|
|
|
+ <el-option label="启用" :value="1" />
|
|
|
+ <el-option label="禁用" :value="0" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
|
|
|
+ <el-button :icon="Refresh" @click="handleReset">重置</el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 表格卡片 -->
|
|
|
+ <el-card class="table-card" shadow="never">
|
|
|
+ <!-- 操作按钮 -->
|
|
|
+ <div class="toolbar">
|
|
|
+ <div class="toolbar-left">
|
|
|
+ <el-button type="primary" :icon="Plus" @click="handleAdd">新增管理员</el-button>
|
|
|
+ <el-button
|
|
|
+ type="danger"
|
|
|
+ :icon="Delete"
|
|
|
+ :disabled="selectedIds.length === 0"
|
|
|
+ @click="handleBatchDelete"
|
|
|
+ >
|
|
|
+ 批量删除 {{ selectedIds.length > 0 ? `(${selectedIds.length})` : '' }}
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 数据表格 -->
|
|
|
+ <el-table
|
|
|
+ v-loading="loading"
|
|
|
+ :data="tableData"
|
|
|
+ border
|
|
|
+ stripe
|
|
|
+ @selection-change="handleSelectionChange"
|
|
|
+ >
|
|
|
+ <el-table-column type="selection" width="50" align="center" />
|
|
|
+ <el-table-column label="序号" type="index" width="60" align="center" />
|
|
|
+ <el-table-column label="管理员ID" prop="id" width="100" align="center" />
|
|
|
+ <el-table-column label="用户名" prop="username" min-width="120" />
|
|
|
+ <el-table-column label="姓名" prop="nickname" min-width="120" />
|
|
|
+ <el-table-column label="角色" prop="adminRole" width="120" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag :type="roleTagType(row.adminRole)" size="small">
|
|
|
+ {{ roleLabel(row.adminRole) }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="状态" prop="status" width="90" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
|
|
|
+ {{ row.status === 1 ? '启用' : '禁用' }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="最后登录时间" prop="lastLoginTime" width="170" align="center">
|
|
|
+ <template #default="{ row }">{{ row.lastLoginTime || '—' }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="230" align="center" fixed="right">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button type="primary" link size="small" @click="handleEdit(row)">编辑</el-button>
|
|
|
+ <el-button
|
|
|
+ :type="row.status === 1 ? 'warning' : 'success'"
|
|
|
+ link
|
|
|
+ size="small"
|
|
|
+ @click="handleToggleStatus(row)"
|
|
|
+ >
|
|
|
+ {{ row.status === 1 ? '禁用' : '启用' }}
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ link
|
|
|
+ size="small"
|
|
|
+ :disabled="row.adminRole === 1"
|
|
|
+ @click="handlePermission(row)"
|
|
|
+ >
|
|
|
+ 权限配置
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ type="danger"
|
|
|
+ link
|
|
|
+ size="small"
|
|
|
+ :disabled="row.adminRole === 1"
|
|
|
+ @click="handleDelete(row)"
|
|
|
+ >
|
|
|
+ 删除
|
|
|
+ </el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <!-- 分页 -->
|
|
|
+ <div class="pagination">
|
|
|
+ <el-pagination
|
|
|
+ v-model:current-page="query.pageNum"
|
|
|
+ v-model:page-size="query.pageSize"
|
|
|
+ :page-sizes="[10, 20, 50]"
|
|
|
+ :total="total"
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
+ background
|
|
|
+ @size-change="loadData"
|
|
|
+ @current-change="loadData"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 新增/编辑对话框 -->
|
|
|
+ <el-dialog
|
|
|
+ v-model="formVisible"
|
|
|
+ :title="formMode === 'add' ? '新增管理员' : '编辑管理员'"
|
|
|
+ width="500px"
|
|
|
+ :close-on-click-modal="false"
|
|
|
+ @close="handleFormClose"
|
|
|
+ >
|
|
|
+ <el-form ref="formRef" :model="formData" :rules="formRules" label-width="90px">
|
|
|
+ <el-form-item label="用户名" prop="username">
|
|
|
+ <el-input v-model="formData.username" placeholder="4-20个字符,唯一" :disabled="formMode === 'edit'" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="密码" prop="password">
|
|
|
+ <el-input
|
|
|
+ v-model="formData.password"
|
|
|
+ type="password"
|
|
|
+ show-password
|
|
|
+ :placeholder="formMode === 'edit' ? '留空则不修改密码' : '8-20位,含字母和数字'"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="姓名" prop="nickname">
|
|
|
+ <el-input v-model="formData.nickname" placeholder="最长50字符" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="角色" prop="adminRole">
|
|
|
+ <el-select v-model="formData.adminRole" placeholder="请选择角色" style="width: 100%">
|
|
|
+ <el-option label="超级管理员" :value="1" />
|
|
|
+ <el-option label="运营人员" :value="2" />
|
|
|
+ <el-option label="只读用户" :value="3" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="状态" prop="status">
|
|
|
+ <el-radio-group v-model="formData.status">
|
|
|
+ <el-radio :label="1">启用</el-radio>
|
|
|
+ <el-radio :label="0">禁用</el-radio>
|
|
|
+ </el-radio-group>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="formVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 权限配置对话框 -->
|
|
|
+ <el-dialog
|
|
|
+ v-model="permissionVisible"
|
|
|
+ title="页面权限配置"
|
|
|
+ width="520px"
|
|
|
+ :close-on-click-modal="false"
|
|
|
+ >
|
|
|
+ <div class="permission-tip">
|
|
|
+ <el-alert
|
|
|
+ title="配置该管理员可访问的页面菜单,超级管理员默认拥有全部权限(不可配置)"
|
|
|
+ type="info"
|
|
|
+ :closable="false"
|
|
|
+ show-icon
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div class="permission-admin-name">
|
|
|
+ 当前管理员:<strong>{{ currentAdmin?.nickname }} ({{ currentAdmin?.username }})</strong>
|
|
|
+ </div>
|
|
|
+ <el-checkbox-group v-model="selectedRoutes" class="permission-checkboxes">
|
|
|
+ <el-row>
|
|
|
+ <el-col v-for="item in allMenuRoutes" :key="item.path" :span="12">
|
|
|
+ <el-checkbox :label="item.path" style="margin-bottom: 10px;">
|
|
|
+ {{ item.title }}
|
|
|
+ </el-checkbox>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-checkbox-group>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="permissionVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" :loading="permissionLoading" @click="handlePermissionSubmit">
|
|
|
+ 保存权限
|
|
|
+ </el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, reactive, onMounted } from 'vue'
|
|
|
+import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
+import { Search, Refresh, Plus, Delete } from '@element-plus/icons-vue'
|
|
|
+import { adminApi } from '@/api'
|
|
|
+
|
|
|
+// ======================== 可配置的菜单路由列表 ========================
|
|
|
+const allMenuRoutes = [
|
|
|
+ { path: '/user/profile', title: '用户管理' },
|
|
|
+ { path: '/acupoint', title: '穴位管理' },
|
|
|
+ { path: '/device', title: '设备管理' },
|
|
|
+ { path: '/moxibustion', title: '艾灸手法管理' },
|
|
|
+ { path: '/plan', title: '方案管理' },
|
|
|
+ { path: '/admin', title: '管理员管理' },
|
|
|
+ { path: '/log', title: '日志管理' },
|
|
|
+]
|
|
|
+
|
|
|
+// ======================== 列表 ========================
|
|
|
+const loading = ref(false)
|
|
|
+const tableData = ref([])
|
|
|
+const total = ref(0)
|
|
|
+const selectedIds = ref([])
|
|
|
+
|
|
|
+const query = reactive({
|
|
|
+ keyword: '',
|
|
|
+ adminRole: null,
|
|
|
+ status: null,
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 10
|
|
|
+})
|
|
|
+
|
|
|
+async function loadData() {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const params = {
|
|
|
+ username: query.keyword || undefined,
|
|
|
+ nickname: query.keyword || undefined,
|
|
|
+ adminRole: query.adminRole ?? undefined,
|
|
|
+ status: query.status ?? undefined,
|
|
|
+ pageNum: query.pageNum,
|
|
|
+ pageSize: query.pageSize
|
|
|
+ }
|
|
|
+ const res = await adminApi.getAdminPage(params)
|
|
|
+ tableData.value = res.data.records || []
|
|
|
+ total.value = res.data.total || 0
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function handleSearch() {
|
|
|
+ query.pageNum = 1
|
|
|
+ loadData()
|
|
|
+}
|
|
|
+
|
|
|
+function handleReset() {
|
|
|
+ query.keyword = ''
|
|
|
+ query.adminRole = null
|
|
|
+ query.status = null
|
|
|
+ query.pageNum = 1
|
|
|
+ loadData()
|
|
|
+}
|
|
|
+
|
|
|
+function handleSelectionChange(rows) {
|
|
|
+ selectedIds.value = rows.map(r => r.id)
|
|
|
+}
|
|
|
+
|
|
|
+// ======================== 角色标签 ========================
|
|
|
+function roleLabel(role) {
|
|
|
+ const map = { 1: '超级管理员', 2: '运营人员', 3: '只读用户' }
|
|
|
+ return map[role] || '未知'
|
|
|
+}
|
|
|
+
|
|
|
+function roleTagType(role) {
|
|
|
+ const map = { 1: 'danger', 2: 'primary', 3: 'info' }
|
|
|
+ return map[role] || 'default'
|
|
|
+}
|
|
|
+
|
|
|
+// ======================== 新增/编辑表单 ========================
|
|
|
+const formVisible = ref(false)
|
|
|
+const formMode = ref('add')
|
|
|
+const submitLoading = ref(false)
|
|
|
+const formRef = ref()
|
|
|
+
|
|
|
+const formData = reactive({
|
|
|
+ id: null,
|
|
|
+ username: '',
|
|
|
+ password: '',
|
|
|
+ nickname: '',
|
|
|
+ adminRole: null,
|
|
|
+ status: 1
|
|
|
+})
|
|
|
+
|
|
|
+const formRules = {
|
|
|
+ username: [
|
|
|
+ { required: true, message: '请输入用户名', trigger: 'blur' },
|
|
|
+ { min: 4, max: 20, message: '用户名长度4-20字符', trigger: 'blur' }
|
|
|
+ ],
|
|
|
+ password: [
|
|
|
+ {
|
|
|
+ validator: (rule, value, callback) => {
|
|
|
+ if (formMode.value === 'add' && !value) {
|
|
|
+ callback(new Error('请输入密码'))
|
|
|
+ } else if (value && (value.length < 8 || value.length > 20)) {
|
|
|
+ callback(new Error('密码长度8-20位'))
|
|
|
+ } else if (value && !/(?=.*[a-zA-Z])(?=.*\d)/.test(value)) {
|
|
|
+ callback(new Error('密码必须包含字母和数字'))
|
|
|
+ } else {
|
|
|
+ callback()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ trigger: 'blur'
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ nickname: [
|
|
|
+ { required: true, message: '请输入姓名', trigger: 'blur' },
|
|
|
+ { max: 50, message: '姓名最长50字符', trigger: 'blur' }
|
|
|
+ ],
|
|
|
+ adminRole: [{ required: true, message: '请选择角色', trigger: 'change' }],
|
|
|
+ status: [{ required: true, message: '请选择状态', trigger: 'change' }]
|
|
|
+}
|
|
|
+
|
|
|
+function handleAdd() {
|
|
|
+ formMode.value = 'add'
|
|
|
+ Object.assign(formData, { id: null, username: '', password: '', nickname: '', adminRole: null, status: 1 })
|
|
|
+ formVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+function handleEdit(row) {
|
|
|
+ formMode.value = 'edit'
|
|
|
+ Object.assign(formData, {
|
|
|
+ id: row.id,
|
|
|
+ username: row.username,
|
|
|
+ password: '',
|
|
|
+ nickname: row.nickname,
|
|
|
+ adminRole: row.adminRole,
|
|
|
+ status: row.status
|
|
|
+ })
|
|
|
+ formVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+function handleFormClose() {
|
|
|
+ formRef.value?.resetFields()
|
|
|
+}
|
|
|
+
|
|
|
+async function handleSubmit() {
|
|
|
+ await formRef.value?.validate()
|
|
|
+ submitLoading.value = true
|
|
|
+ try {
|
|
|
+ const payload = { ...formData }
|
|
|
+ if (formMode.value === 'add') {
|
|
|
+ await adminApi.addAdmin(payload)
|
|
|
+ ElMessage.success('新增管理员成功')
|
|
|
+ } else {
|
|
|
+ if (!payload.password) delete payload.password
|
|
|
+ await adminApi.updateAdmin(payload)
|
|
|
+ ElMessage.success('修改成功')
|
|
|
+ }
|
|
|
+ formVisible.value = false
|
|
|
+ loadData()
|
|
|
+ } finally {
|
|
|
+ submitLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ======================== 状态切换 ========================
|
|
|
+async function handleToggleStatus(row) {
|
|
|
+ const newStatus = row.status === 1 ? 0 : 1
|
|
|
+ const label = newStatus === 1 ? '启用' : '禁用'
|
|
|
+ await ElMessageBox.confirm(`确认${label}管理员【${row.nickname}】吗?`, '提示', {
|
|
|
+ confirmButtonText: '确认',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ })
|
|
|
+ await adminApi.updateAdminStatus(row.id, newStatus)
|
|
|
+ ElMessage.success(`${label}成功`)
|
|
|
+ loadData()
|
|
|
+}
|
|
|
+
|
|
|
+// ======================== 删除 ========================
|
|
|
+async function handleDelete(row) {
|
|
|
+ await ElMessageBox.confirm(`确认删除管理员【${row.nickname}】吗?此操作不可恢复`, '警告', {
|
|
|
+ confirmButtonText: '确认删除',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ })
|
|
|
+ await adminApi.deleteAdmin(row.id)
|
|
|
+ ElMessage.success('删除成功')
|
|
|
+ loadData()
|
|
|
+}
|
|
|
+
|
|
|
+async function handleBatchDelete() {
|
|
|
+ await ElMessageBox.confirm(`确认删除选中的 ${selectedIds.value.length} 条记录吗?`, '批量删除', {
|
|
|
+ confirmButtonText: '确认删除',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ })
|
|
|
+ await adminApi.batchDeleteAdmin(selectedIds.value)
|
|
|
+ ElMessage.success('批量删除成功')
|
|
|
+ selectedIds.value = []
|
|
|
+ loadData()
|
|
|
+}
|
|
|
+
|
|
|
+// ======================== 权限配置 ========================
|
|
|
+const permissionVisible = ref(false)
|
|
|
+const permissionLoading = ref(false)
|
|
|
+const currentAdmin = ref(null)
|
|
|
+const selectedRoutes = ref([])
|
|
|
+
|
|
|
+async function handlePermission(row) {
|
|
|
+ currentAdmin.value = row
|
|
|
+ const res = await adminApi.getAdminPermission(row.id)
|
|
|
+ const routePaths = res.data?.routePaths
|
|
|
+ if (routePaths) {
|
|
|
+ try {
|
|
|
+ selectedRoutes.value = JSON.parse(routePaths)
|
|
|
+ } catch {
|
|
|
+ selectedRoutes.value = []
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ selectedRoutes.value = []
|
|
|
+ }
|
|
|
+ permissionVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+async function handlePermissionSubmit() {
|
|
|
+ permissionLoading.value = true
|
|
|
+ try {
|
|
|
+ const routePathsStr = JSON.stringify(selectedRoutes.value)
|
|
|
+ await adminApi.updateAdminPermission(currentAdmin.value.id, routePathsStr)
|
|
|
+ ElMessage.success('权限配置保存成功')
|
|
|
+ permissionVisible.value = false
|
|
|
+ } finally {
|
|
|
+ permissionLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ======================== 初始化 ========================
|
|
|
+onMounted(() => {
|
|
|
+ loadData()
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.page-container {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.search-card :deep(.el-card__body) {
|
|
|
+ padding-bottom: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.toolbar {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.pagination {
|
|
|
+ margin-top: 16px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+}
|
|
|
+
|
|
|
+.permission-tip {
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.permission-admin-name {
|
|
|
+ margin-bottom: 16px;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #606266;
|
|
|
+}
|
|
|
+
|
|
|
+.permission-checkboxes {
|
|
|
+ max-height: 300px;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding: 8px;
|
|
|
+ border: 1px solid #ebeef5;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+</style>
|