浏览代码

Add user management functionality including user list, details, creation, updating, and deletion. Update layout and routing to integrate user management into the application.

yb 3 周之前
父节点
当前提交
35cf7178cf
共有 4 个文件被更改,包括 418 次插入1 次删除
  1. 57 0
      src/api/user.ts
  2. 6 1
      src/layout/index.vue
  3. 6 0
      src/router/index.ts
  4. 349 0
      src/views/user/index.vue

+ 57 - 0
src/api/user.ts

@@ -0,0 +1,57 @@
+import { get, post, put, del } from '@/utils/request'
+import type { ApiResponse, PageResult } from '@/types'
+
+// 用户类型
+export interface User {
+  id: string
+  username: string
+  role: 'admin' | 'operator' | 'viewer'
+  createdAt?: string
+  updatedAt?: string
+}
+
+// 用户列表查询参数 (matches backend schema)
+export interface UserQueryParams {
+  page?: number
+  pageSize?: number
+  search?: string
+  role?: 'admin' | 'operator' | 'viewer'
+  status?: 'active' | 'disabled'
+}
+
+// 创建/更新用户参数
+export interface UserForm {
+  username: string
+  password?: string
+  role: 'admin' | 'operator' | 'viewer'
+}
+
+// 获取用户列表
+export function listUsers(params?: UserQueryParams): Promise<ApiResponse<PageResult<User>>> {
+  return get('/users', params)
+}
+
+// 获取用户详情
+export function getUser(id: string): Promise<ApiResponse<User>> {
+  return get(`/users/${id}`)
+}
+
+// 创建用户
+export function createUser(data: UserForm): Promise<ApiResponse<User>> {
+  return post('/users', data)
+}
+
+// 更新用户
+export function updateUser(id: string, data: Partial<UserForm>): Promise<ApiResponse<User>> {
+  return put(`/users/${id}`, data)
+}
+
+// 删除用户
+export function deleteUser(id: string): Promise<ApiResponse<null>> {
+  return del(`/users/${id}`)
+}
+
+// 重置用户密码
+export function resetPassword(id: string, password: string): Promise<ApiResponse<null>> {
+  return post(`/users/${id}/reset-password`, { password })
+}

+ 6 - 1
src/layout/index.vue

@@ -19,6 +19,11 @@
           <template #title>摄像头管理</template>
         </el-menu-item>
 
+        <el-menu-item index="/user">
+          <el-icon><UserFilled /></el-icon>
+          <template #title>用户管理</template>
+        </el-menu-item>
+
         <el-sub-menu index="/stream">
           <template #title>
             <el-icon><Film /></el-icon>
@@ -87,7 +92,7 @@
 <script setup lang="ts">
 import { computed, onMounted } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
-import { VideoCamera, Fold, Expand, ArrowDown, Monitor, Film, VideoCameraFilled, Setting } from '@element-plus/icons-vue'
+import { VideoCamera, Fold, Expand, ArrowDown, Monitor, Film, VideoCameraFilled, Setting, UserFilled } from '@element-plus/icons-vue'
 import { useAppStore } from '@/store/app'
 import { useUserStore } from '@/store/user'
 

+ 6 - 0
src/router/index.ts

@@ -56,6 +56,12 @@ const routes: RouteRecordRaw[] = [
         name: 'StreamConfig',
         component: () => import('@/views/stream/config.vue'),
         meta: { title: 'Stream 配置', icon: 'Setting' }
+      },
+      {
+        path: 'user',
+        name: 'User',
+        component: () => import('@/views/user/index.vue'),
+        meta: { title: '用户管理', icon: 'User' }
       }
     ]
   },

+ 349 - 0
src/views/user/index.vue

@@ -0,0 +1,349 @@
+<template>
+  <div class="page-container">
+    <!-- 搜索区域 -->
+    <div class="search-form">
+      <el-form :model="queryParams" inline>
+        <el-form-item label="搜索">
+          <el-input v-model="queryParams.search" placeholder="请输入用户名" clearable @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item label="角色">
+          <el-select v-model="queryParams.role" placeholder="请选择角色" clearable>
+            <el-option label="管理员" value="admin" />
+            <el-option label="操作员" value="operator" />
+            <el-option label="观察者" value="viewer" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button>
+          <el-button :icon="Refresh" @click="resetQuery">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <!-- 操作按钮 -->
+    <div class="table-actions">
+      <el-button type="primary" :icon="Plus" @click="handleAdd">新增用户</el-button>
+      <el-button type="success" :icon="Refresh" @click="getList">刷新列表</el-button>
+    </div>
+
+    <!-- 数据表格 -->
+    <el-table v-loading="loading" :data="userList" border>
+      <el-table-column type="index" label="序号" width="60" align="center" />
+      <el-table-column prop="username" label="用户名" min-width="150" />
+      <el-table-column prop="role" label="角色" width="120" align="center">
+        <template #default="{ row }">
+          <el-tag :type="getRoleTagType(row.role)">{{ getRoleLabel(row.role) }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column prop="createdAt" label="创建时间" width="180" align="center">
+        <template #default="{ row }">
+          {{ formatTime(row.createdAt) }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="updatedAt" label="更新时间" width="180" align="center">
+        <template #default="{ row }">
+          {{ formatTime(row.updatedAt) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="220" align="center" fixed="right">
+        <template #default="{ row }">
+          <el-button type="primary" link :icon="Edit" @click="handleEdit(row)">编辑</el-button>
+          <el-button type="warning" link :icon="Key" @click="handleResetPassword(row)">重置密码</el-button>
+          <el-button type="danger" link :icon="Delete" @click="handleDelete(row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页 -->
+    <el-pagination
+      v-model:current-page="queryParams.page"
+      v-model:page-size="queryParams.pageSize"
+      :page-sizes="[10, 20, 50, 100]"
+      :total="total"
+      layout="total, sizes, prev, pager, next, jumper"
+      class="pagination"
+      @size-change="getList"
+      @current-change="getList"
+    />
+
+    <!-- 新增/编辑弹窗 -->
+    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px" destroy-on-close>
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="用户名" prop="username">
+          <el-input v-model="form.username" placeholder="请输入用户名" :disabled="isEdit" />
+        </el-form-item>
+        <el-form-item v-if="!isEdit" label="密码" prop="password">
+          <el-input v-model="form.password" type="password" placeholder="请输入密码" show-password />
+        </el-form-item>
+        <el-form-item label="角色" prop="role">
+          <el-select v-model="form.role" placeholder="请选择角色" style="width: 100%">
+            <el-option label="管理员" value="admin" />
+            <el-option label="操作员" value="operator" />
+            <el-option label="观察者" value="viewer" />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 重置密码弹窗 -->
+    <el-dialog v-model="resetDialogVisible" title="重置密码" width="400px" destroy-on-close>
+      <el-form ref="resetFormRef" :model="resetForm" :rules="resetRules" label-width="80px">
+        <el-form-item label="新密码" prop="password">
+          <el-input v-model="resetForm.password" type="password" placeholder="请输入新密码" show-password />
+        </el-form-item>
+        <el-form-item label="确认密码" prop="confirmPassword">
+          <el-input v-model="resetForm.confirmPassword" type="password" placeholder="请再次输入密码" show-password />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="resetDialogVisible = false">取消</el-button>
+        <el-button type="primary" :loading="resetLoading" @click="handleResetSubmit">确定</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, computed, onMounted } from 'vue'
+import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
+import { Search, Refresh, Plus, Edit, Delete, Key } from '@element-plus/icons-vue'
+import { listUsers, createUser, updateUser, deleteUser, resetPassword, type User, type UserForm } from '@/api/user'
+
+const loading = ref(false)
+const submitLoading = ref(false)
+const resetLoading = ref(false)
+const userList = ref<User[]>([])
+const total = ref(0)
+const dialogVisible = ref(false)
+const resetDialogVisible = ref(false)
+const formRef = ref<FormInstance>()
+const resetFormRef = ref<FormInstance>()
+const currentUserId = ref('')
+
+const queryParams = reactive({
+  page: 1,
+  pageSize: 10,
+  search: '',
+  role: '' as '' | 'admin' | 'operator' | 'viewer'
+})
+
+const form = reactive<UserForm & { id?: string }>({
+  username: '',
+  password: '',
+  role: 'viewer'
+})
+
+const resetForm = reactive({
+  password: '',
+  confirmPassword: ''
+})
+
+const isEdit = computed(() => !!form.id)
+const dialogTitle = computed(() => isEdit.value ? '编辑用户' : '新增用户')
+
+const rules: FormRules = {
+  username: [
+    { required: true, message: '请输入用户名', trigger: 'blur' },
+    { min: 3, max: 20, message: '用户名长度为3-20个字符', trigger: 'blur' }
+  ],
+  password: [
+    { required: true, message: '请输入密码', trigger: 'blur' },
+    { min: 6, max: 20, message: '密码长度为6-20个字符', trigger: 'blur' }
+  ],
+  role: [{ required: true, message: '请选择角色', trigger: 'change' }]
+}
+
+const validateConfirmPassword = (_rule: any, value: string, callback: Function) => {
+  if (value !== resetForm.password) {
+    callback(new Error('两次输入的密码不一致'))
+  } else {
+    callback()
+  }
+}
+
+const resetRules: FormRules = {
+  password: [
+    { required: true, message: '请输入新密码', trigger: 'blur' },
+    { min: 6, max: 20, message: '密码长度为6-20个字符', trigger: 'blur' }
+  ],
+  confirmPassword: [
+    { required: true, message: '请再次输入密码', trigger: 'blur' },
+    { validator: validateConfirmPassword, trigger: 'blur' }
+  ]
+}
+
+function getRoleLabel(role: string) {
+  const map: Record<string, string> = {
+    admin: '管理员',
+    operator: '操作员',
+    viewer: '观察者'
+  }
+  return map[role] || role
+}
+
+function getRoleTagType(role: string) {
+  const map: Record<string, '' | 'success' | 'warning' | 'info' | 'danger'> = {
+    admin: 'danger',
+    operator: 'warning',
+    viewer: 'info'
+  }
+  return map[role] || 'info'
+}
+
+function formatTime(time?: string) {
+  if (!time) return '-'
+  return new Date(time).toLocaleString('zh-CN')
+}
+
+async function getList() {
+  loading.value = true
+  try {
+    // Filter out empty strings to avoid validation errors
+    const params: any = {
+      page: queryParams.page,
+      pageSize: queryParams.pageSize
+    }
+    if (queryParams.search) params.search = queryParams.search
+    if (queryParams.role) params.role = queryParams.role
+
+    const res = await listUsers(params)
+    if (res.code === 200) {
+      userList.value = res.data?.rows || res.rows || []
+      total.value = res.data?.total || res.total || 0
+    }
+  } finally {
+    loading.value = false
+  }
+}
+
+function handleQuery() {
+  queryParams.page = 1
+  getList()
+}
+
+function resetQuery() {
+  queryParams.page = 1
+  queryParams.search = ''
+  queryParams.role = ''
+  getList()
+}
+
+function handleAdd() {
+  Object.assign(form, {
+    id: undefined,
+    username: '',
+    password: '',
+    role: 'viewer'
+  })
+  dialogVisible.value = true
+}
+
+function handleEdit(row: User) {
+  Object.assign(form, {
+    id: row.id,
+    username: row.username,
+    password: '',
+    role: row.role
+  })
+  dialogVisible.value = true
+}
+
+function handleResetPassword(row: User) {
+  currentUserId.value = row.id
+  resetForm.password = ''
+  resetForm.confirmPassword = ''
+  resetDialogVisible.value = true
+}
+
+async function handleDelete(row: User) {
+  try {
+    await ElMessageBox.confirm(`确定要删除用户 "${row.username}" 吗?`, '提示', {
+      type: 'warning'
+    })
+    const res = await deleteUser(row.id)
+    if (res.code === 200) {
+      ElMessage.success('删除成功')
+      getList()
+    }
+  } catch (error) {
+    if (error !== 'cancel') {
+      console.error('删除失败', error)
+    }
+  }
+}
+
+async function handleSubmit() {
+  if (!formRef.value) return
+
+  await formRef.value.validate(async (valid) => {
+    if (valid) {
+      submitLoading.value = true
+      try {
+        let res
+        if (form.id) {
+          const { password, ...updateData } = form
+          res = await updateUser(form.id, updateData)
+        } else {
+          res = await createUser(form)
+        }
+        if (res.code === 200) {
+          ElMessage.success(form.id ? '修改成功' : '新增成功')
+          dialogVisible.value = false
+          getList()
+        }
+      } finally {
+        submitLoading.value = false
+      }
+    }
+  })
+}
+
+async function handleResetSubmit() {
+  if (!resetFormRef.value) return
+
+  await resetFormRef.value.validate(async (valid) => {
+    if (valid) {
+      resetLoading.value = true
+      try {
+        const res = await resetPassword(currentUserId.value, resetForm.password)
+        if (res.code === 200) {
+          ElMessage.success('密码重置成功')
+          resetDialogVisible.value = false
+        }
+      } finally {
+        resetLoading.value = false
+      }
+    }
+  })
+}
+
+onMounted(() => {
+  getList()
+})
+</script>
+
+<style lang="scss" scoped>
+.page-container {
+  padding: 20px;
+}
+
+.search-form {
+  margin-bottom: 20px;
+  padding: 20px;
+  background-color: #fff;
+  border-radius: 4px;
+}
+
+.table-actions {
+  margin-bottom: 15px;
+}
+
+.pagination {
+  margin-top: 20px;
+  justify-content: flex-end;
+}
+</style>