Przeglądaj źródła

feat(system): add user and role management features

- Introduced a new system management section in the layout with routes for user and role management.
- Implemented user management functionality with a searchable table, including user details and actions for editing, deleting, and resetting passwords.
- Added role management functionality with a comprehensive table for role details, including permissions and actions for editing and deleting roles.
- Enhanced the user interface for both user and role management with improved forms and pagination for better usability.
yb 3 dni temu
rodzic
commit
1339f2daf0

+ 10 - 1
src/layout/index.vue

@@ -246,7 +246,16 @@ const menuItems: MenuItem[] = [
   { path: '/webrtc', title: 'WebRTC 流', icon: 'mdi:wifi' },
   { path: '/monitor', title: '多视频监控', icon: 'mdi:video' },
   { path: '/machine', title: '机器管理', icon: 'mdi:monitor' },
-  { path: '/camera', title: '摄像头管理', icon: 'mdi:video' }
+  { path: '/camera', title: '摄像头管理', icon: 'mdi:video' },
+  {
+    path: '/system',
+    title: '系统管理',
+    icon: 'mdi:cog',
+    children: [
+      { path: '/system/user', title: '用户管理', icon: 'mdi:account' },
+      { path: '/system/role', title: '角色管理', icon: 'mdi:shield-account' }
+    ]
+  }
   // {
   //   path: '/demo',
   //   title: '视频测试',

+ 20 - 0
src/router/index.ts

@@ -185,6 +185,26 @@ const routes: RouteRecordRaw[] = [
         name: 'MTableDemo',
         component: () => import('@/views/test/m-table-demo.vue'),
         meta: { title: 'MTable 测试', icon: 'Grid' }
+      },
+      {
+        path: 'system',
+        name: 'System',
+        meta: { title: '系统管理', icon: 'Setting' },
+        redirect: '/system/user',
+        children: [
+          {
+            path: 'user',
+            name: 'SystemUser',
+            component: () => import('@/views/system/user/index.vue'),
+            meta: { title: '用户管理', icon: 'User' }
+          },
+          {
+            path: 'role',
+            name: 'SystemRole',
+            component: () => import('@/views/system/role/index.vue'),
+            meta: { title: '角色管理', icon: 'UserFilled' }
+          }
+        ]
       }
     ]
   },

+ 546 - 0
src/views/system/role/index.vue

@@ -0,0 +1,546 @@
+<template>
+  <div class="page-container">
+    <!-- 搜索表单 -->
+    <div class="search-form">
+      <el-form :model="searchForm" inline data-id="search-form">
+        <el-form-item>
+          <el-input
+            v-model.trim="searchForm.roleName"
+            :placeholder="t('角色名称')"
+            clearable
+            @keyup.enter="handleSearch"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-input
+            v-model.trim="searchForm.roleCode"
+            :placeholder="t('角色编码')"
+            clearable
+            @keyup.enter="handleSearch"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-select v-model="searchForm.status" :placeholder="t('状态')" clearable>
+            <el-option :label="t('全部')" value="" />
+            <el-option :label="t('启用')" value="enabled" />
+            <el-option :label="t('禁用')" value="disabled" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="handleSearch">
+            <Icon icon="mdi:magnify" width="16" height="16" style="margin-right: 4px" />
+            {{ t('查询') }}
+          </el-button>
+          <el-button type="info" @click="handleReset">
+            <Icon icon="mdi:refresh" width="16" height="16" style="margin-right: 4px" />
+            {{ t('重置') }}
+          </el-button>
+          <el-button type="primary" @click="handleAdd">
+            <Icon icon="mdi:plus" width="16" height="16" style="margin-right: 4px" />
+            {{ t('新增') }}
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <!-- 数据表格 -->
+    <div class="table-wrapper">
+      <el-table
+        ref="tableRef"
+        v-loading="loading"
+        :data="roleList"
+        stripe
+        size="default"
+        height="100%"
+        @sort-change="handleSortChange"
+      >
+        <el-table-column prop="id" :label="t('ID')" width="80" />
+        <el-table-column
+          prop="roleName"
+          :label="t('角色名称')"
+          min-width="120"
+          sortable="custom"
+          show-overflow-tooltip
+        />
+        <el-table-column prop="roleCode" :label="t('角色编码')" min-width="120" show-overflow-tooltip />
+        <el-table-column :label="t('用户数')" width="100" align="center">
+          <template #default="{ row }">
+            <el-tag type="info" size="small">{{ row.userCount }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column :label="t('状态')" width="100" align="center">
+          <template #default="{ row }">
+            <el-tag :type="row.status === 'enabled' ? 'success' : 'danger'" size="small">
+              {{ row.status === 'enabled' ? t('启用') : t('禁用') }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="description" :label="t('描述')" min-width="200" show-overflow-tooltip />
+        <el-table-column :label="t('创建时间')" min-width="160">
+          <template #default="{ row }">
+            {{ formatTime(row.createdAt) }}
+          </template>
+        </el-table-column>
+        <el-table-column :label="t('操作')" width="150" align="center" fixed="right">
+          <template #default="{ row }">
+            <el-button type="primary" link @click="handleEdit(row)">
+              <Icon icon="mdi:note-edit-outline" width="20" height="20" />
+            </el-button>
+            <el-button type="primary" link @click="handlePermission(row)">
+              <Icon icon="mdi:shield-key" width="20" height="20" />
+            </el-button>
+            <el-button type="danger" link :disabled="row.roleCode === 'admin'" @click="handleDelete(row)">
+              <Icon icon="mdi:delete" width="20" height="20" />
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+
+    <!-- 分页 -->
+    <div class="pagination-container">
+      <el-pagination
+        v-model:current-page="currentPage"
+        v-model:page-size="pageSize"
+        :page-sizes="[10, 15, 20, 50, 100]"
+        :total="total"
+        layout="total, sizes, prev, pager, next, jumper"
+        background
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+      />
+    </div>
+
+    <!-- 角色编辑抽屉 -->
+    <el-drawer
+      v-model="drawerVisible"
+      :title="isEdit ? t('编辑角色') : t('新增角色')"
+      direction="rtl"
+      size="500px"
+      :close-on-click-modal="false"
+      destroy-on-close
+    >
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
+        <el-form-item :label="t('角色名称')" prop="roleName">
+          <el-input v-model="form.roleName" :placeholder="t('请输入角色名称')" />
+        </el-form-item>
+        <el-form-item :label="t('角色编码')" prop="roleCode">
+          <el-input v-model="form.roleCode" :disabled="isEdit" :placeholder="t('请输入角色编码')" />
+        </el-form-item>
+        <el-form-item :label="t('排序')" prop="sort">
+          <el-input-number v-model="form.sort" :min="0" :max="999" />
+        </el-form-item>
+        <el-form-item :label="t('状态')" prop="status">
+          <el-radio-group v-model="form.status">
+            <el-radio value="enabled">{{ t('启用') }}</el-radio>
+            <el-radio value="disabled">{{ t('禁用') }}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item :label="t('描述')" prop="description">
+          <el-input v-model="form.description" type="textarea" :rows="3" :placeholder="t('请输入描述')" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="drawer-footer">
+          <el-button @click="drawerVisible = false">{{ t('取消') }}</el-button>
+          <el-button type="primary" :loading="submitting" @click="handleSubmit">
+            {{ isEdit ? t('更新') : t('添加') }}
+          </el-button>
+        </div>
+      </template>
+    </el-drawer>
+
+    <!-- 权限配置抽屉 -->
+    <el-drawer
+      v-model="permissionDrawerVisible"
+      :title="`${t('权限配置')} - ${currentRole?.roleName || ''}`"
+      direction="rtl"
+      size="500px"
+      :close-on-click-modal="false"
+      destroy-on-close
+    >
+      <el-tree
+        ref="treeRef"
+        :data="permissionTree"
+        show-checkbox
+        node-key="id"
+        :default-checked-keys="checkedPermissions"
+        :props="{ label: 'name', children: 'children' }"
+      />
+      <template #footer>
+        <div class="drawer-footer">
+          <el-button @click="permissionDrawerVisible = false">{{ t('取消') }}</el-button>
+          <el-button type="primary" :loading="permissionSubmitting" @click="handleSavePermission">
+            {{ t('保存') }}
+          </el-button>
+        </div>
+      </template>
+    </el-drawer>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted, computed } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Icon } from '@iconify/vue'
+import type { FormInstance, FormRules } from 'element-plus'
+import { useI18n } from 'vue-i18n'
+import { formatTime } from '@/utils/dayjs'
+
+const { t } = useI18n({ useScope: 'global' })
+
+// Mock 数据
+interface Role {
+  id: number
+  roleName: string
+  roleCode: string
+  userCount: number
+  status: 'enabled' | 'disabled'
+  description: string
+  sort: number
+  createdAt: string
+}
+
+interface Permission {
+  id: number
+  name: string
+  children?: Permission[]
+}
+
+const mockRoles: Role[] = [
+  {
+    id: 1,
+    roleName: '管理员',
+    roleCode: 'admin',
+    userCount: 1,
+    status: 'enabled',
+    description: '系统管理员,拥有所有权限',
+    sort: 0,
+    createdAt: '2024-01-01T10:00:00Z'
+  },
+  {
+    id: 2,
+    roleName: '操作员',
+    roleCode: 'operator',
+    userCount: 2,
+    status: 'enabled',
+    description: '可以操作设备和查看数据',
+    sort: 1,
+    createdAt: '2024-01-15T14:30:00Z'
+  },
+  {
+    id: 3,
+    roleName: '查看者',
+    roleCode: 'viewer',
+    userCount: 3,
+    status: 'enabled',
+    description: '只能查看数据,无法操作',
+    sort: 2,
+    createdAt: '2024-02-01T09:00:00Z'
+  },
+  {
+    id: 4,
+    roleName: '测试角色',
+    roleCode: 'test',
+    userCount: 0,
+    status: 'disabled',
+    description: '测试用角色',
+    sort: 99,
+    createdAt: '2024-02-15T16:00:00Z'
+  }
+]
+
+const mockPermissions: Permission[] = [
+  {
+    id: 1,
+    name: '仪表盘',
+    children: [{ id: 11, name: '查看仪表盘' }]
+  },
+  {
+    id: 2,
+    name: 'LSS 管理',
+    children: [
+      { id: 21, name: '查看 LSS 列表' },
+      { id: 22, name: '编辑 LSS' },
+      { id: 23, name: '删除 LSS' }
+    ]
+  },
+  {
+    id: 3,
+    name: '设备管理',
+    children: [
+      { id: 31, name: '查看设备列表' },
+      { id: 32, name: '添加设备' },
+      { id: 33, name: '编辑设备' },
+      { id: 34, name: '删除设备' }
+    ]
+  },
+  {
+    id: 4,
+    name: '系统管理',
+    children: [
+      { id: 41, name: '用户管理' },
+      { id: 42, name: '角色管理' }
+    ]
+  }
+]
+
+const loading = ref(false)
+const roleList = ref<Role[]>([])
+const tableRef = ref()
+const treeRef = ref()
+
+// 搜索表单
+const searchForm = reactive({
+  roleName: '',
+  roleCode: '',
+  status: '' as '' | 'enabled' | 'disabled'
+})
+
+// 分页
+const currentPage = ref(1)
+const pageSize = ref(15)
+const total = ref(0)
+
+// 排序
+const sortState = reactive({
+  sortBy: '',
+  sortDir: '' as 'ASC' | 'DESC' | ''
+})
+
+// 抽屉
+const drawerVisible = ref(false)
+const isEdit = ref(false)
+const submitting = ref(false)
+const formRef = ref<FormInstance>()
+const currentRole = ref<Role | null>(null)
+
+// 权限配置
+const permissionDrawerVisible = ref(false)
+const permissionSubmitting = ref(false)
+const permissionTree = ref<Permission[]>(mockPermissions)
+const checkedPermissions = ref<number[]>([])
+
+// 表单
+const form = reactive({
+  roleName: '',
+  roleCode: '',
+  sort: 0,
+  status: 'enabled' as 'enabled' | 'disabled',
+  description: ''
+})
+
+// 表单验证规则
+const rules = computed<FormRules>(() => ({
+  roleName: [{ required: true, message: t('请输入角色名称'), trigger: 'blur' }],
+  roleCode: [{ required: true, message: t('请输入角色编码'), trigger: 'blur' }]
+}))
+
+// 获取列表
+async function getList() {
+  loading.value = true
+  try {
+    await new Promise((resolve) => setTimeout(resolve, 300))
+
+    let filtered = [...mockRoles]
+
+    // 搜索过滤
+    if (searchForm.roleName) {
+      filtered = filtered.filter((r) => r.roleName.includes(searchForm.roleName))
+    }
+    if (searchForm.roleCode) {
+      filtered = filtered.filter((r) => r.roleCode.includes(searchForm.roleCode))
+    }
+    if (searchForm.status) {
+      filtered = filtered.filter((r) => r.status === searchForm.status)
+    }
+
+    // 排序
+    if (sortState.sortBy) {
+      filtered.sort((a, b) => {
+        const aVal = a[sortState.sortBy as keyof Role] as string
+        const bVal = b[sortState.sortBy as keyof Role] as string
+        return sortState.sortDir === 'ASC' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal)
+      })
+    }
+
+    total.value = filtered.length
+    const start = (currentPage.value - 1) * pageSize.value
+    roleList.value = filtered.slice(start, start + pageSize.value)
+  } finally {
+    loading.value = false
+  }
+}
+
+function handleSearch() {
+  currentPage.value = 1
+  getList()
+}
+
+function handleReset() {
+  searchForm.roleName = ''
+  searchForm.roleCode = ''
+  searchForm.status = ''
+  currentPage.value = 1
+  getList()
+}
+
+function handleSortChange({ prop, order }: { prop: string; order: 'ascending' | 'descending' | null }) {
+  sortState.sortBy = prop || ''
+  sortState.sortDir = order === 'ascending' ? 'ASC' : order === 'descending' ? 'DESC' : ''
+  getList()
+}
+
+function handleSizeChange(val: number) {
+  pageSize.value = val
+  currentPage.value = 1
+  getList()
+}
+
+function handleCurrentChange(val: number) {
+  currentPage.value = val
+  getList()
+}
+
+function resetForm() {
+  form.roleName = ''
+  form.roleCode = ''
+  form.sort = 0
+  form.status = 'enabled'
+  form.description = ''
+  formRef.value?.clearValidate()
+}
+
+function handleAdd() {
+  isEdit.value = false
+  currentRole.value = null
+  resetForm()
+  drawerVisible.value = true
+}
+
+function handleEdit(row: Role) {
+  isEdit.value = true
+  currentRole.value = row
+  form.roleName = row.roleName
+  form.roleCode = row.roleCode
+  form.sort = row.sort
+  form.status = row.status
+  form.description = row.description
+  drawerVisible.value = true
+}
+
+async function handleSubmit() {
+  if (!formRef.value) return
+
+  await formRef.value.validate(async (valid) => {
+    if (!valid) return
+
+    submitting.value = true
+    try {
+      await new Promise((resolve) => setTimeout(resolve, 500))
+      ElMessage.success(isEdit.value ? t('更新成功') : t('添加成功'))
+      drawerVisible.value = false
+      getList()
+    } finally {
+      submitting.value = false
+    }
+  })
+}
+
+async function handleDelete(row: Role) {
+  if (row.roleCode === 'admin') {
+    ElMessage.warning(t('管理员角色不能删除'))
+    return
+  }
+
+  try {
+    await ElMessageBox.confirm(
+      `你确定要删除这个角色吗?<br/><br/>角色名称:${row.roleName}<br/>角色编码:${row.roleCode}`,
+      t('提示'),
+      {
+        type: 'warning',
+        dangerouslyUseHTMLString: true
+      }
+    )
+    await new Promise((resolve) => setTimeout(resolve, 300))
+    ElMessage.success(t('删除成功'))
+    getList()
+  } catch {
+    // 取消
+  }
+}
+
+function handlePermission(row: Role) {
+  currentRole.value = row
+  // Mock 已选权限
+  if (row.roleCode === 'admin') {
+    checkedPermissions.value = [11, 21, 22, 23, 31, 32, 33, 34, 41, 42]
+  } else if (row.roleCode === 'operator') {
+    checkedPermissions.value = [11, 21, 22, 31, 32, 33]
+  } else if (row.roleCode === 'viewer') {
+    checkedPermissions.value = [11, 21, 31]
+  } else {
+    checkedPermissions.value = []
+  }
+  permissionDrawerVisible.value = true
+}
+
+async function handleSavePermission() {
+  permissionSubmitting.value = true
+  try {
+    await new Promise((resolve) => setTimeout(resolve, 500))
+    ElMessage.success(t('权限配置保存成功'))
+    permissionDrawerVisible.value = false
+  } finally {
+    permissionSubmitting.value = false
+  }
+}
+
+onMounted(() => {
+  getList()
+})
+</script>
+
+<style lang="scss" scoped>
+.page-container {
+  display: flex;
+  flex-direction: column;
+  box-sizing: border-box;
+}
+
+.search-form {
+  flex-shrink: 0;
+  margin-bottom: 16px;
+  padding: 16px 16px 4px 16px;
+  background: #f5f7fa;
+
+  :deep(.el-form-item) {
+    margin-bottom: 12px;
+    margin-right: 16px;
+  }
+
+  :deep(.el-input),
+  :deep(.el-select) {
+    width: 160px;
+  }
+}
+
+.table-wrapper {
+  flex: 1;
+  min-height: 0;
+  overflow: hidden;
+}
+
+.pagination-container {
+  flex-shrink: 0;
+  display: flex;
+  justify-content: flex-end;
+  padding-top: 16px;
+}
+
+.drawer-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+}
+</style>

+ 485 - 0
src/views/system/user/index.vue

@@ -0,0 +1,485 @@
+<template>
+  <div class="page-container">
+    <!-- 搜索表单 -->
+    <div class="search-form">
+      <el-form :model="searchForm" inline data-id="search-form">
+        <el-form-item>
+          <el-input
+            v-model.trim="searchForm.username"
+            :placeholder="t('用户名')"
+            clearable
+            @keyup.enter="handleSearch"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-input v-model.trim="searchForm.realName" :placeholder="t('姓名')" clearable @keyup.enter="handleSearch" />
+        </el-form-item>
+        <el-form-item>
+          <el-select v-model="searchForm.status" :placeholder="t('状态')" clearable>
+            <el-option :label="t('全部')" value="" />
+            <el-option :label="t('启用')" value="enabled" />
+            <el-option :label="t('禁用')" value="disabled" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="handleSearch">
+            <Icon icon="mdi:magnify" width="16" height="16" style="margin-right: 4px" />
+            {{ t('查询') }}
+          </el-button>
+          <el-button type="info" @click="handleReset">
+            <Icon icon="mdi:refresh" width="16" height="16" style="margin-right: 4px" />
+            {{ t('重置') }}
+          </el-button>
+          <el-button type="primary" @click="handleAdd">
+            <Icon icon="mdi:plus" width="16" height="16" style="margin-right: 4px" />
+            {{ t('新增') }}
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <!-- 数据表格 -->
+    <div class="table-wrapper">
+      <el-table
+        ref="tableRef"
+        v-loading="loading"
+        :data="userList"
+        stripe
+        size="default"
+        height="100%"
+        @sort-change="handleSortChange"
+      >
+        <el-table-column prop="id" :label="t('ID')" width="80" />
+        <el-table-column prop="username" :label="t('用户名')" min-width="120" sortable="custom" show-overflow-tooltip />
+        <el-table-column prop="realName" :label="t('姓名')" min-width="120" show-overflow-tooltip />
+        <el-table-column prop="email" :label="t('邮箱')" min-width="180" show-overflow-tooltip />
+        <el-table-column prop="phone" :label="t('手机号')" min-width="130" show-overflow-tooltip />
+        <el-table-column :label="t('角色')" min-width="120">
+          <template #default="{ row }">
+            <el-tag v-for="role in row.roles" :key="role" size="small" style="margin-right: 4px">
+              {{ role }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column :label="t('状态')" width="100" align="center">
+          <template #default="{ row }">
+            <el-tag :type="row.status === 'enabled' ? 'success' : 'danger'" size="small">
+              {{ row.status === 'enabled' ? t('启用') : t('禁用') }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column :label="t('创建时间')" min-width="160">
+          <template #default="{ row }">
+            {{ formatTime(row.createdAt) }}
+          </template>
+        </el-table-column>
+        <el-table-column :label="t('操作')" width="150" align="center" fixed="right">
+          <template #default="{ row }">
+            <el-button type="primary" link @click="handleEdit(row)">
+              <Icon icon="mdi:note-edit-outline" width="20" height="20" />
+            </el-button>
+            <el-button type="primary" link @click="handleResetPassword(row)">
+              <Icon icon="mdi:lock-reset" width="20" height="20" />
+            </el-button>
+            <el-button type="danger" link @click="handleDelete(row)">
+              <Icon icon="mdi:delete" width="20" height="20" />
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+
+    <!-- 分页 -->
+    <div class="pagination-container">
+      <el-pagination
+        v-model:current-page="currentPage"
+        v-model:page-size="pageSize"
+        :page-sizes="[10, 15, 20, 50, 100]"
+        :total="total"
+        layout="total, sizes, prev, pager, next, jumper"
+        background
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+      />
+    </div>
+
+    <!-- 用户编辑抽屉 -->
+    <el-drawer
+      v-model="drawerVisible"
+      :title="isEdit ? t('编辑用户') : t('新增用户')"
+      direction="rtl"
+      size="500px"
+      :close-on-click-modal="false"
+      destroy-on-close
+    >
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
+        <el-form-item :label="t('用户名')" prop="username">
+          <el-input v-model="form.username" :disabled="isEdit" :placeholder="t('请输入用户名')" />
+        </el-form-item>
+        <el-form-item :label="t('姓名')" prop="realName">
+          <el-input v-model="form.realName" :placeholder="t('请输入姓名')" />
+        </el-form-item>
+        <el-form-item :label="t('邮箱')" prop="email">
+          <el-input v-model="form.email" :placeholder="t('请输入邮箱')" />
+        </el-form-item>
+        <el-form-item :label="t('手机号')" prop="phone">
+          <el-input v-model="form.phone" :placeholder="t('请输入手机号')" />
+        </el-form-item>
+        <el-form-item :label="t('角色')" prop="roles">
+          <el-select v-model="form.roles" multiple :placeholder="t('请选择角色')" style="width: 100%">
+            <el-option v-for="role in roleOptions" :key="role.value" :label="role.label" :value="role.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item v-if="!isEdit" :label="t('密码')" prop="password">
+          <el-input v-model="form.password" type="password" show-password :placeholder="t('请输入密码')" />
+        </el-form-item>
+        <el-form-item :label="t('状态')" prop="status">
+          <el-radio-group v-model="form.status">
+            <el-radio value="enabled">{{ t('启用') }}</el-radio>
+            <el-radio value="disabled">{{ t('禁用') }}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item :label="t('备注')" prop="remark">
+          <el-input v-model="form.remark" type="textarea" :rows="3" :placeholder="t('请输入备注')" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="drawer-footer">
+          <el-button @click="drawerVisible = false">{{ t('取消') }}</el-button>
+          <el-button type="primary" :loading="submitting" @click="handleSubmit">
+            {{ isEdit ? t('更新') : t('添加') }}
+          </el-button>
+        </div>
+      </template>
+    </el-drawer>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted, computed } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Icon } from '@iconify/vue'
+import type { FormInstance, FormRules } from 'element-plus'
+import { useI18n } from 'vue-i18n'
+import { formatTime } from '@/utils/dayjs'
+
+const { t } = useI18n({ useScope: 'global' })
+
+// Mock 数据
+interface User {
+  id: number
+  username: string
+  realName: string
+  email: string
+  phone: string
+  roles: string[]
+  status: 'enabled' | 'disabled'
+  createdAt: string
+  remark?: string
+}
+
+const mockUsers: User[] = [
+  {
+    id: 1,
+    username: 'admin',
+    realName: '系统管理员',
+    email: 'admin@example.com',
+    phone: '13800138000',
+    roles: ['管理员'],
+    status: 'enabled',
+    createdAt: '2024-01-01T10:00:00Z'
+  },
+  {
+    id: 2,
+    username: 'operator',
+    realName: '张三',
+    email: 'zhangsan@example.com',
+    phone: '13800138001',
+    roles: ['操作员'],
+    status: 'enabled',
+    createdAt: '2024-01-15T14:30:00Z'
+  },
+  {
+    id: 3,
+    username: 'viewer',
+    realName: '李四',
+    email: 'lisi@example.com',
+    phone: '13800138002',
+    roles: ['查看者'],
+    status: 'enabled',
+    createdAt: '2024-02-01T09:00:00Z'
+  },
+  {
+    id: 4,
+    username: 'test',
+    realName: '王五',
+    email: 'wangwu@example.com',
+    phone: '13800138003',
+    roles: ['操作员', '查看者'],
+    status: 'disabled',
+    createdAt: '2024-02-15T16:00:00Z'
+  },
+  {
+    id: 5,
+    username: 'user001',
+    realName: '赵六',
+    email: 'zhaoliu@example.com',
+    phone: '13800138004',
+    roles: ['查看者'],
+    status: 'enabled',
+    createdAt: '2024-03-01T11:30:00Z'
+  }
+]
+
+const loading = ref(false)
+const userList = ref<User[]>([])
+const tableRef = ref()
+
+// 搜索表单
+const searchForm = reactive({
+  username: '',
+  realName: '',
+  status: '' as '' | 'enabled' | 'disabled'
+})
+
+// 分页
+const currentPage = ref(1)
+const pageSize = ref(15)
+const total = ref(0)
+
+// 排序
+const sortState = reactive({
+  sortBy: '',
+  sortDir: '' as 'ASC' | 'DESC' | ''
+})
+
+// 抽屉
+const drawerVisible = ref(false)
+const isEdit = ref(false)
+const submitting = ref(false)
+const formRef = ref<FormInstance>()
+const currentUser = ref<User | null>(null)
+
+// 角色选项
+const roleOptions = [
+  { label: '管理员', value: '管理员' },
+  { label: '操作员', value: '操作员' },
+  { label: '查看者', value: '查看者' }
+]
+
+// 表单
+const form = reactive({
+  username: '',
+  realName: '',
+  email: '',
+  phone: '',
+  roles: [] as string[],
+  password: '',
+  status: 'enabled' as 'enabled' | 'disabled',
+  remark: ''
+})
+
+// 表单验证规则
+const rules = computed<FormRules>(() => ({
+  username: [{ required: true, message: t('请输入用户名'), trigger: 'blur' }],
+  realName: [{ required: true, message: t('请输入姓名'), trigger: 'blur' }],
+  email: [{ type: 'email', message: t('请输入正确的邮箱'), trigger: 'blur' }],
+  roles: [{ required: true, message: t('请选择角色'), trigger: 'change' }],
+  password: [{ required: !isEdit.value, message: t('请输入密码'), trigger: 'blur' }]
+}))
+
+// 获取列表
+async function getList() {
+  loading.value = true
+  try {
+    // 模拟 API 延迟
+    await new Promise((resolve) => setTimeout(resolve, 300))
+
+    let filtered = [...mockUsers]
+
+    // 搜索过滤
+    if (searchForm.username) {
+      filtered = filtered.filter((u) => u.username.includes(searchForm.username))
+    }
+    if (searchForm.realName) {
+      filtered = filtered.filter((u) => u.realName.includes(searchForm.realName))
+    }
+    if (searchForm.status) {
+      filtered = filtered.filter((u) => u.status === searchForm.status)
+    }
+
+    // 排序
+    if (sortState.sortBy) {
+      filtered.sort((a, b) => {
+        const aVal = a[sortState.sortBy as keyof User] as string
+        const bVal = b[sortState.sortBy as keyof User] as string
+        return sortState.sortDir === 'ASC' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal)
+      })
+    }
+
+    total.value = filtered.length
+    const start = (currentPage.value - 1) * pageSize.value
+    userList.value = filtered.slice(start, start + pageSize.value)
+  } finally {
+    loading.value = false
+  }
+}
+
+function handleSearch() {
+  currentPage.value = 1
+  getList()
+}
+
+function handleReset() {
+  searchForm.username = ''
+  searchForm.realName = ''
+  searchForm.status = ''
+  currentPage.value = 1
+  getList()
+}
+
+function handleSortChange({ prop, order }: { prop: string; order: 'ascending' | 'descending' | null }) {
+  sortState.sortBy = prop || ''
+  sortState.sortDir = order === 'ascending' ? 'ASC' : order === 'descending' ? 'DESC' : ''
+  getList()
+}
+
+function handleSizeChange(val: number) {
+  pageSize.value = val
+  currentPage.value = 1
+  getList()
+}
+
+function handleCurrentChange(val: number) {
+  currentPage.value = val
+  getList()
+}
+
+function resetForm() {
+  form.username = ''
+  form.realName = ''
+  form.email = ''
+  form.phone = ''
+  form.roles = []
+  form.password = ''
+  form.status = 'enabled'
+  form.remark = ''
+  formRef.value?.clearValidate()
+}
+
+function handleAdd() {
+  isEdit.value = false
+  currentUser.value = null
+  resetForm()
+  drawerVisible.value = true
+}
+
+function handleEdit(row: User) {
+  isEdit.value = true
+  currentUser.value = row
+  form.username = row.username
+  form.realName = row.realName
+  form.email = row.email
+  form.phone = row.phone
+  form.roles = [...row.roles]
+  form.status = row.status
+  form.remark = row.remark || ''
+  drawerVisible.value = true
+}
+
+async function handleSubmit() {
+  if (!formRef.value) return
+
+  await formRef.value.validate(async (valid) => {
+    if (!valid) return
+
+    submitting.value = true
+    try {
+      await new Promise((resolve) => setTimeout(resolve, 500))
+      ElMessage.success(isEdit.value ? t('更新成功') : t('添加成功'))
+      drawerVisible.value = false
+      getList()
+    } finally {
+      submitting.value = false
+    }
+  })
+}
+
+async function handleDelete(row: User) {
+  try {
+    await ElMessageBox.confirm(
+      `你确定要删除这个用户吗?<br/><br/>用户名:${row.username}<br/>姓名:${row.realName}`,
+      t('提示'),
+      {
+        type: 'warning',
+        dangerouslyUseHTMLString: true
+      }
+    )
+    await new Promise((resolve) => setTimeout(resolve, 300))
+    ElMessage.success(t('删除成功'))
+    getList()
+  } catch {
+    // 取消
+  }
+}
+
+async function handleResetPassword(row: User) {
+  try {
+    await ElMessageBox.confirm(`确定要重置用户 "${row.username}" 的密码吗?`, t('提示'), {
+      type: 'warning'
+    })
+    await new Promise((resolve) => setTimeout(resolve, 300))
+    ElMessage.success(t('密码已重置为默认密码'))
+  } catch {
+    // 取消
+  }
+}
+
+onMounted(() => {
+  getList()
+})
+</script>
+
+<style lang="scss" scoped>
+.page-container {
+  display: flex;
+  flex-direction: column;
+  box-sizing: border-box;
+}
+
+.search-form {
+  flex-shrink: 0;
+  margin-bottom: 16px;
+  padding: 16px 16px 4px 16px;
+  background: #f5f7fa;
+
+  :deep(.el-form-item) {
+    margin-bottom: 12px;
+    margin-right: 16px;
+  }
+
+  :deep(.el-input),
+  :deep(.el-select) {
+    width: 160px;
+  }
+}
+
+.table-wrapper {
+  flex: 1;
+  min-height: 0;
+  overflow: hidden;
+}
+
+.pagination-container {
+  flex-shrink: 0;
+  display: flex;
+  justify-content: flex-end;
+  padding-top: 16px;
+}
+
+.drawer-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+}
+</style>