占位数据源 src/api/user.ts(字段对齐后端 User),页面跑通 CRUD 模式,后续接后端只换数据源。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
296 lines
9.7 KiB
Vue
296 lines
9.7 KiB
Vue
<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>
|