feat: user management page (search/table/pagination/dialog/delete/reset-pwd)
占位数据源 src/api/user.ts(字段对齐后端 User),页面跑通 CRUD 模式,后续接后端只换数据源。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
246a305df5
commit
5549b2e75c
27
components.d.ts
vendored
27
components.d.ts
vendored
@ -14,22 +14,34 @@ declare module 'vue' {
|
|||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
|
ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
|
||||||
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
|
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
|
||||||
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
|
ElCard: typeof import('element-plus/es')['ElCard']
|
||||||
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
||||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||||
|
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||||
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||||
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||||
|
ElForm: typeof import('element-plus/es')['ElForm']
|
||||||
|
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||||
|
ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||||
|
ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
|
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||||
|
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
ElSlider: typeof import('element-plus/es')['ElSlider']
|
ElSlider: typeof import('element-plus/es')['ElSlider']
|
||||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||||
|
ElTable: typeof import('element-plus/es')['ElTable']
|
||||||
|
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||||
|
ElTag: typeof import('element-plus/es')['ElTag']
|
||||||
Icon: typeof import('./src/components/icon/icon.vue')['default']
|
Icon: typeof import('./src/components/icon/icon.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
@ -42,28 +54,43 @@ declare module 'vue' {
|
|||||||
TySidebar: typeof import('./src/components/ty-sidebar.vue')['default']
|
TySidebar: typeof import('./src/components/ty-sidebar.vue')['default']
|
||||||
TyTabs: typeof import('./src/components/ty-tabs.vue')['default']
|
TyTabs: typeof import('./src/components/ty-tabs.vue')['default']
|
||||||
}
|
}
|
||||||
|
export interface GlobalDirectives {
|
||||||
|
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For TSX support
|
// For TSX support
|
||||||
declare global {
|
declare global {
|
||||||
const ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
|
const ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
|
||||||
const ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
|
const ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
|
||||||
|
const ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
|
const ElCard: typeof import('element-plus/es')['ElCard']
|
||||||
const ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
const ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
||||||
const ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
const ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||||
|
const ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||||
const ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
const ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||||
const ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
const ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||||
const ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
const ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||||
const ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
const ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||||
|
const ElForm: typeof import('element-plus/es')['ElForm']
|
||||||
|
const ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||||
|
const ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
const ElMenu: typeof import('element-plus/es')['ElMenu']
|
const ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||||
const ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
const ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||||
|
const ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
|
const ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||||
const ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
const ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||||
const ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
const ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||||
const ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
const ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||||
|
const ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
const ElSlider: typeof import('element-plus/es')['ElSlider']
|
const ElSlider: typeof import('element-plus/es')['ElSlider']
|
||||||
const ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
const ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||||
const ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
const ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||||
|
const ElTable: typeof import('element-plus/es')['ElTable']
|
||||||
|
const ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||||
const ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
const ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||||
const ElTabs: typeof import('element-plus/es')['ElTabs']
|
const ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||||
|
const ElTag: typeof import('element-plus/es')['ElTag']
|
||||||
const Icon: typeof import('./src/components/icon/icon.vue')['default']
|
const Icon: typeof import('./src/components/icon/icon.vue')['default']
|
||||||
const RouterLink: typeof import('vue-router')['RouterLink']
|
const RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
const RouterView: typeof import('vue-router')['RouterView']
|
const RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
|||||||
139
src/api/user.ts
Normal file
139
src/api/user.ts
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
/**
|
||||||
|
* 用户管理数据源。
|
||||||
|
* TODO: 对接后端后,把下面 mock 实现替换为 http 调用(字段与返回形状保持不变)。
|
||||||
|
* 后端 User 实体字段:id/tenantCode/username/nickname/email/phone/status/orgCode/createdAt
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type UserStatus = 'enabled' | 'disabled' | 'locked'
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: string
|
||||||
|
tenantCode: string
|
||||||
|
username: string
|
||||||
|
nickname: string | null
|
||||||
|
email: string | null
|
||||||
|
phone: string | null
|
||||||
|
status: UserStatus
|
||||||
|
orgCode: string | null
|
||||||
|
createdAt: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserListQuery {
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
username?: string
|
||||||
|
nickname?: string
|
||||||
|
phone?: string
|
||||||
|
status?: UserStatus | ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserListResult {
|
||||||
|
list: User[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserUpsertData {
|
||||||
|
username: string
|
||||||
|
nickname?: string
|
||||||
|
email?: string
|
||||||
|
phone?: string
|
||||||
|
status?: UserStatus
|
||||||
|
orgCode?: string | null
|
||||||
|
password?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 占位组织(后续接后端换真实组织数据)。 */
|
||||||
|
export const mockOrgs: { code: string; name: string }[] = [
|
||||||
|
{ code: 'touyun', name: '投云总部' },
|
||||||
|
{ code: 'rd', name: '研发部' },
|
||||||
|
{ code: 'ops', name: '运维部' },
|
||||||
|
{ code: 'sales', name: '销售部' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
const day = 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
function mockUser(id: string, username: string, nickname: string, orgCode: string, status: UserStatus, daysAgo: number): User {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
tenantCode: 'touyun',
|
||||||
|
username,
|
||||||
|
nickname,
|
||||||
|
email: `${username}@example.com`,
|
||||||
|
phone: `1380000${String(1000 + Number(id)).slice(-4)}`,
|
||||||
|
status,
|
||||||
|
orgCode,
|
||||||
|
createdAt: now - daysAgo * day,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mockUsers: User[] = [
|
||||||
|
mockUser('1', 'touyun', '超级管理员', 'touyun', 'enabled', 30),
|
||||||
|
mockUser('2', 'zhangsan', '张三', 'rd', 'enabled', 20),
|
||||||
|
mockUser('3', 'lisi', '李四', 'rd', 'disabled', 18),
|
||||||
|
mockUser('4', 'wangwu', '王五', 'ops', 'locked', 15),
|
||||||
|
mockUser('5', 'zhaoliu', '赵六', 'sales', 'enabled', 12),
|
||||||
|
mockUser('6', 'sunqi', '孙七', 'ops', 'enabled', 9),
|
||||||
|
mockUser('7', 'zhouba', '周八', 'rd', 'enabled', 7),
|
||||||
|
mockUser('8', 'wujiu', '吴九', 'sales', 'disabled', 5),
|
||||||
|
mockUser('9', 'zhengshi', '郑十', 'rd', 'enabled', 3),
|
||||||
|
mockUser('10', 'chenyi', '陈一', 'sales', 'enabled', 1),
|
||||||
|
]
|
||||||
|
|
||||||
|
function uuid(): string {
|
||||||
|
return crypto.randomUUID()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function orgName(code: string | null): string {
|
||||||
|
if (!code) return '-'
|
||||||
|
return mockOrgs.find((o) => o.code === code)?.name ?? code
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listUsers(query: UserListQuery): Promise<UserListResult> {
|
||||||
|
const { page, pageSize, username, nickname, phone, status } = query
|
||||||
|
const filtered = mockUsers.filter((u) => {
|
||||||
|
if (username && !u.username.includes(username)) return false
|
||||||
|
if (nickname && !(u.nickname ?? '').includes(nickname)) return false
|
||||||
|
if (phone && !(u.phone ?? '').includes(phone)) return false
|
||||||
|
if (status && u.status !== status) return false
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
const total = filtered.length
|
||||||
|
const start = (page - 1) * pageSize
|
||||||
|
return { list: filtered.slice(start, start + pageSize), total, page, pageSize }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createUser(data: UserUpsertData): Promise<User> {
|
||||||
|
const user: User = {
|
||||||
|
id: uuid(),
|
||||||
|
tenantCode: 'touyun',
|
||||||
|
username: data.username,
|
||||||
|
nickname: data.nickname ?? null,
|
||||||
|
email: data.email ?? null,
|
||||||
|
phone: data.phone ?? null,
|
||||||
|
status: data.status ?? 'enabled',
|
||||||
|
orgCode: data.orgCode ?? null,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
}
|
||||||
|
mockUsers.unshift(user)
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateUser(id: string, data: Partial<UserUpsertData>): Promise<User> {
|
||||||
|
const user = mockUsers.find((u) => u.id === id)
|
||||||
|
if (!user) throw new Error('用户不存在')
|
||||||
|
Object.assign(user, data)
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteUser(id: string): Promise<null> {
|
||||||
|
mockUsers = mockUsers.filter((u) => u.id !== id)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resetPassword(_id: string, _password: string): Promise<null> {
|
||||||
|
// mock:无真实密码存储,仅占位
|
||||||
|
return null
|
||||||
|
}
|
||||||
295
src/pages/system/user/index.vue
Normal file
295
src/pages/system/user/index.vue
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-user">
|
||||||
|
<el-card class="page-user__search" shadow="never">
|
||||||
|
<el-form :inline="true" :model="query" @submit.prevent>
|
||||||
|
<el-form-item label="用户名">
|
||||||
|
<el-input v-model="query.username" placeholder="请输入用户名" clearable @keyup.enter="handleSearch" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="昵称">
|
||||||
|
<el-input v-model="query.nickname" placeholder="请输入昵称" clearable @keyup.enter="handleSearch" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="手机">
|
||||||
|
<el-input v-model="query.phone" placeholder="请输入手机号" clearable @keyup.enter="handleSearch" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-select v-model="query.status" placeholder="全部" clearable style="width: 120px">
|
||||||
|
<el-option label="启用" value="enabled" />
|
||||||
|
<el-option label="停用" value="disabled" />
|
||||||
|
<el-option label="锁定" value="locked" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||||
|
<el-button @click="handleReset">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card shadow="never">
|
||||||
|
<div class="page-user__toolbar">
|
||||||
|
<el-button type="primary" @click="openCreate">新增</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="list" border>
|
||||||
|
<el-table-column prop="username" label="用户名" min-width="110" />
|
||||||
|
<el-table-column prop="nickname" label="昵称" min-width="100" />
|
||||||
|
<el-table-column prop="email" label="邮箱" min-width="160" />
|
||||||
|
<el-table-column prop="phone" label="手机" min-width="120" />
|
||||||
|
<el-table-column label="状态" width="80" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="STATUS_MAP[row.status as UserStatus].type" size="small">
|
||||||
|
{{ STATUS_MAP[row.status as UserStatus].label }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="所属组织" min-width="110">
|
||||||
|
<template #default="{ row }">{{ orgName(row.orgCode) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="创建时间" width="160">
|
||||||
|
<template #default="{ row }">{{ formatDateTime(row.createdAt) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="200" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
|
||||||
|
<el-button link type="warning" @click="openResetPwd(row)">重置密码</el-button>
|
||||||
|
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="query.page"
|
||||||
|
v-model:page-size="query.pageSize"
|
||||||
|
class="page-user__pagination"
|
||||||
|
:total="total"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@current-change="fetchList"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-dialog v-model="dialog.visible" :title="dialog.isEdit ? '编辑用户' : '新增用户'" width="480px" 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" :disabled="dialog.isEdit" placeholder="请输入用户名" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="!dialog.isEdit" label="密码" prop="password">
|
||||||
|
<el-input v-model="form.password" type="password" show-password placeholder="请输入初始密码" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="昵称" prop="nickname">
|
||||||
|
<el-input v-model="form.nickname" placeholder="请输入昵称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="邮箱" prop="email">
|
||||||
|
<el-input v-model="form.email" placeholder="请输入邮箱" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="手机" prop="phone">
|
||||||
|
<el-input v-model="form.phone" placeholder="请输入手机号" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-select v-model="form.status" style="width: 100%">
|
||||||
|
<el-option label="启用" value="enabled" />
|
||||||
|
<el-option label="停用" value="disabled" />
|
||||||
|
<el-option label="锁定" value="locked" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="所属组织" prop="orgCode">
|
||||||
|
<el-select v-model="form.orgCode" placeholder="请选择组织" clearable style="width: 100%">
|
||||||
|
<el-option v-for="o in mockOrgs" :key="o.code" :label="o.name" :value="o.code" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialog.visible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog v-model="resetPwd.visible" title="重置密码" width="420px" destroy-on-close>
|
||||||
|
<el-form label-width="80px">
|
||||||
|
<el-form-item label="用户名">
|
||||||
|
<span>{{ resetPwd.username }}</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="新密码">
|
||||||
|
<el-input v-model="resetPwd.password" type="password" show-password placeholder="请输入新密码" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="resetPwd.visible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="submitResetPwd">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, reactive, ref } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
import {
|
||||||
|
createUser,
|
||||||
|
deleteUser,
|
||||||
|
listUsers,
|
||||||
|
mockOrgs,
|
||||||
|
orgName,
|
||||||
|
resetPassword,
|
||||||
|
updateUser,
|
||||||
|
type User,
|
||||||
|
type UserStatus,
|
||||||
|
} from '@/api/user'
|
||||||
|
|
||||||
|
const STATUS_MAP: Record<UserStatus, { label: string; type: 'success' | 'info' | 'danger' }> = {
|
||||||
|
enabled: { label: '启用', type: 'success' },
|
||||||
|
disabled: { label: '停用', type: 'info' },
|
||||||
|
locked: { label: '锁定', type: 'danger' },
|
||||||
|
}
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const list = ref<User[]>([])
|
||||||
|
const total = ref(0)
|
||||||
|
const query = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
username: '',
|
||||||
|
nickname: '',
|
||||||
|
phone: '',
|
||||||
|
status: '' as UserStatus | '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const dialog = reactive({ visible: false, isEdit: false })
|
||||||
|
const form = reactive({
|
||||||
|
id: '',
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
nickname: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
status: 'enabled' as UserStatus,
|
||||||
|
orgCode: null as string | null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const resetPwd = reactive({ visible: false, id: '', username: '', password: '' })
|
||||||
|
|
||||||
|
const rules: FormRules = {
|
||||||
|
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||||
|
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(ms: number): string {
|
||||||
|
const d = new Date(ms)
|
||||||
|
const pad = (n: number) => String(n).padStart(2, '0')
|
||||||
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchList(): Promise<void> {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const result = await listUsers({ ...query, status: query.status })
|
||||||
|
list.value = result.list
|
||||||
|
total.value = result.total
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSearch(): void {
|
||||||
|
query.page = 1
|
||||||
|
fetchList()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReset(): void {
|
||||||
|
query.username = ''
|
||||||
|
query.nickname = ''
|
||||||
|
query.phone = ''
|
||||||
|
query.status = ''
|
||||||
|
handleSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSizeChange(): void {
|
||||||
|
query.page = 1
|
||||||
|
fetchList()
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreate(): void {
|
||||||
|
dialog.visible = true
|
||||||
|
dialog.isEdit = false
|
||||||
|
Object.assign(form, { id: '', username: '', password: '', nickname: '', email: '', phone: '', status: 'enabled', orgCode: null })
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEdit(row: User): void {
|
||||||
|
dialog.visible = true
|
||||||
|
dialog.isEdit = true
|
||||||
|
Object.assign(form, { id: row.id, username: row.username, password: '', nickname: row.nickname ?? '', email: row.email ?? '', phone: row.phone ?? '', status: row.status, orgCode: row.orgCode })
|
||||||
|
}
|
||||||
|
|
||||||
|
function openResetPwd(row: User): void {
|
||||||
|
resetPwd.visible = true
|
||||||
|
resetPwd.id = row.id
|
||||||
|
resetPwd.username = row.username
|
||||||
|
resetPwd.password = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitForm(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await formRef.value?.validate()
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (dialog.isEdit) {
|
||||||
|
await updateUser(form.id, {
|
||||||
|
nickname: form.nickname,
|
||||||
|
email: form.email,
|
||||||
|
phone: form.phone,
|
||||||
|
status: form.status,
|
||||||
|
orgCode: form.orgCode,
|
||||||
|
})
|
||||||
|
ElMessage.success('更新成功')
|
||||||
|
} else {
|
||||||
|
await createUser({
|
||||||
|
username: form.username,
|
||||||
|
password: form.password,
|
||||||
|
nickname: form.nickname,
|
||||||
|
email: form.email,
|
||||||
|
phone: form.phone,
|
||||||
|
status: form.status,
|
||||||
|
orgCode: form.orgCode,
|
||||||
|
})
|
||||||
|
ElMessage.success('新增成功')
|
||||||
|
}
|
||||||
|
dialog.visible = false
|
||||||
|
fetchList()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete(row: User): Promise<void> {
|
||||||
|
await ElMessageBox.confirm(`确认删除用户「${row.username}」?`, '提示', { type: 'warning' })
|
||||||
|
await deleteUser(row.id)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
fetchList()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitResetPwd(): Promise<void> {
|
||||||
|
await resetPassword(resetPwd.id, resetPwd.password)
|
||||||
|
ElMessage.success('密码已重置')
|
||||||
|
resetPwd.visible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(fetchList)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.page-user {
|
||||||
|
&__search {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__toolbar {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__pagination {
|
||||||
|
margin-top: 12px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
File diff suppressed because one or more lines are too long
13
typed-router.d.ts
vendored
13
typed-router.d.ts
vendored
@ -44,6 +44,13 @@ declare module 'vue-router/auto-routes' {
|
|||||||
Record<never, never>,
|
Record<never, never>,
|
||||||
| never
|
| never
|
||||||
>,
|
>,
|
||||||
|
'/system/user/': RouteRecordInfo<
|
||||||
|
'/system/user/',
|
||||||
|
'/system/user',
|
||||||
|
Record<never, never>,
|
||||||
|
Record<never, never>,
|
||||||
|
| never
|
||||||
|
>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -75,6 +82,12 @@ declare module 'vue-router/auto-routes' {
|
|||||||
views:
|
views:
|
||||||
| never
|
| never
|
||||||
}
|
}
|
||||||
|
'src/pages/system/user/index.vue': {
|
||||||
|
routes:
|
||||||
|
| '/system/user/'
|
||||||
|
views:
|
||||||
|
| never
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user