289 lines
10 KiB
Vue
289 lines
10 KiB
Vue
<template>
|
||
<div>
|
||
<h1 class="text-xl font-bold text-gray-800 mb-4">用户管理</h1>
|
||
<el-card shadow="never" class="mb-3">
|
||
<el-row :gutter="12" align="middle">
|
||
<el-col :span="7">
|
||
<el-input v-model="keyword" placeholder="搜索姓名/昵称/手机号" clearable @clear="fetchData" @keyup.enter="fetchData" />
|
||
</el-col>
|
||
<el-col :span="4">
|
||
<el-select v-model="roleFilter" placeholder="角色" clearable class="w-full" @change="fetchData">
|
||
<el-option label="全部" value="" />
|
||
<el-option label="访客" value="visitor" />
|
||
<el-option label="员工" value="employee" />
|
||
<el-option label="保安" value="guard" />
|
||
</el-select>
|
||
</el-col>
|
||
<el-col :span="4">
|
||
<el-select v-model="statusFilter" placeholder="状态" clearable class="w-full" @change="fetchData">
|
||
<el-option label="全部" value="" />
|
||
<el-option label="正常" :value="1" />
|
||
<el-option label="禁用" :value="0" />
|
||
</el-select>
|
||
</el-col>
|
||
<el-col :span="2">
|
||
<el-button type="primary" @click="fetchData">搜索</el-button>
|
||
</el-col>
|
||
</el-row>
|
||
</el-card>
|
||
<div class="bg-white rounded-xl shadow-sm border overflow-hidden">
|
||
<el-table :data="users" stripe class="w-full" v-loading="loading">
|
||
<el-table-column prop="id" label="ID" width="60" />
|
||
<el-table-column label="头像" width="60">
|
||
<template #default="{ row }">
|
||
<el-avatar v-if="row.avatar_url" :src="row.avatar_url" :size="32" />
|
||
<el-avatar v-else :size="32">{{ (row.real_name || row.nickname || '?').charAt(0) }}</el-avatar>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="姓名">
|
||
<template #default="{ row }">{{ row.real_name || row.nickname || '未命名' }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="phone" label="手机号" width="140" />
|
||
<el-table-column label="角色" width="100">
|
||
<template #default="{ row }">
|
||
<el-tag v-if="row.role === 'employee'" type="success" size="small">员工</el-tag>
|
||
<el-tag v-else-if="row.role === 'guard'" type="warning" size="small">保安</el-tag>
|
||
<el-tag v-else size="small" type="info">{{ row.role_name || '访客' }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" width="80">
|
||
<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="所属部门" min-width="120">
|
||
<template #default="{ row }">
|
||
<span class="text-gray-600">{{ getUserDept(row.id) || '未分配' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="100" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-button type="primary" link size="small" @click="openEdit(row)">编辑</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<!-- 分页 -->
|
||
<div v-if="total > 0" class="p-4 flex justify-center">
|
||
<el-pagination
|
||
v-model:current-page="page"
|
||
:page-size="pageSize"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
:total="total"
|
||
layout="total,sizes,prev,pager,next"
|
||
@current-change="fetchData"
|
||
@size-change="handleSizeChange"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 编辑对话框 -->
|
||
<el-dialog v-model="dialogVisible" title="编辑用户" width="500px" :close-on-click-modal="false" @closed="onDialogClosed">
|
||
<el-form v-if="editForm" ref="formRef" :model="editForm" label-width="100px">
|
||
<el-form-item label="用户ID">
|
||
<span class="text-gray-500">{{ editForm.id }}</span>
|
||
</el-form-item>
|
||
<el-form-item label="头像">
|
||
<el-avatar v-if="editForm.avatar_url" :src="editForm.avatar_url" :size="48" />
|
||
<el-avatar v-else :size="48">{{ (editForm.real_name || editForm.nickname || '?').charAt(0) }}</el-avatar>
|
||
</el-form-item>
|
||
<el-form-item label="真实姓名" prop="real_name">
|
||
<el-input v-model="editForm.real_name" placeholder="输入真实姓名" />
|
||
</el-form-item>
|
||
<el-form-item label="昵称" prop="nickname">
|
||
<el-input v-model="editForm.nickname" placeholder="输入微信昵称" />
|
||
</el-form-item>
|
||
<el-form-item label="手机号" prop="phone">
|
||
<el-input v-model="editForm.phone" placeholder="输入手机号" maxlength="11" />
|
||
</el-form-item>
|
||
<el-form-item label="状态">
|
||
<el-switch v-model="editForm.status" :active-value="1" :inactive-value="0" active-text="正常" inactive-text="禁用" />
|
||
</el-form-item>
|
||
<el-form-item label="角色">
|
||
<el-radio-group v-model="editForm.role">
|
||
<el-radio value="visitor">访客</el-radio>
|
||
<el-radio value="employee">员工</el-radio>
|
||
<el-radio value="guard">保安</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<el-form-item label="职位">
|
||
<el-input v-model="editForm.position" placeholder="如:前端工程师" />
|
||
</el-form-item>
|
||
<el-form-item label="所属部门">
|
||
<el-select v-model="editForm.department_id" filterable clearable placeholder="请选择部门" class="w-full">
|
||
<el-option
|
||
v-for="d in departments"
|
||
:key="d.id"
|
||
:label="d.name"
|
||
:value="d.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="dialogVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="saving" @click="handleSave">保存</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, onMounted } from 'vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import { getUsers, updateUser, listAllDepartments, setEmployeeDepartment, clearEmployeeDepartment, getEmployeeDepartments } from '@/api'
|
||
|
||
const users = ref<any[]>([])
|
||
const keyword = ref('')
|
||
const roleFilter = ref('')
|
||
const statusFilter = ref('')
|
||
const page = ref(1)
|
||
const pageSize = ref(10)
|
||
const total = ref(0)
|
||
const loading = ref(false)
|
||
|
||
// 编辑对话框
|
||
const dialogVisible = ref(false)
|
||
const saving = ref(false)
|
||
const editForm = ref<any>(null)
|
||
|
||
// 部门列表(用于下拉选择)
|
||
const departments = ref<any[]>([])
|
||
|
||
// 员工-部门关联映射:userId -> { department_id, department_name }
|
||
const deptMap = ref<Record<number, { department_id: number; department_name: string }>>({})
|
||
|
||
function getUserDept(userId: number): string {
|
||
return deptMap.value[userId]?.department_name || ''
|
||
}
|
||
|
||
function getUserDeptId(userId: number): number | null {
|
||
return deptMap.value[userId]?.department_id ?? null
|
||
}
|
||
|
||
function getUserPosition(userId: number): string {
|
||
return deptMap.value[userId]?.position || ''
|
||
}
|
||
|
||
async function loadDepartments() {
|
||
try {
|
||
const res: any = await listAllDepartments()
|
||
departments.value = res?.data || []
|
||
} catch {
|
||
// 部门列表非关键,静默失败
|
||
}
|
||
}
|
||
|
||
async function loadDeptMap() {
|
||
try {
|
||
const res: any = await getEmployeeDepartments()
|
||
const list: any[] = res?.data?.list || res?.data || []
|
||
const map: Record<number, { department_id: number; department_name: string; position: string }> = {}
|
||
for (const item of list) {
|
||
map[item.user_id] = {
|
||
department_id: item.department_id,
|
||
department_name: item.department_name,
|
||
position: item.position || '',
|
||
}
|
||
}
|
||
deptMap.value = map
|
||
} catch {
|
||
deptMap.value = {}
|
||
}
|
||
}
|
||
|
||
async function fetchData() {
|
||
loading.value = true
|
||
try {
|
||
const params: Record<string, any> = { page: page.value, page_size: pageSize.value }
|
||
if (keyword.value) params.keyword = keyword.value
|
||
if (roleFilter.value) params.type = roleFilter.value
|
||
if (statusFilter.value !== '') params.status = statusFilter.value
|
||
|
||
const [userRes] = await Promise.all([
|
||
getUsers(params),
|
||
loadDeptMap(),
|
||
])
|
||
users.value = (userRes as any)?.data?.list || []
|
||
total.value = (userRes as any)?.data?.total || 0
|
||
} catch {
|
||
ElMessage.error('加载用户列表失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function handleSizeChange(val: number) {
|
||
pageSize.value = val
|
||
page.value = 1
|
||
fetchData()
|
||
}
|
||
|
||
function openEdit(row: any) {
|
||
editForm.value = {
|
||
id: row.id,
|
||
real_name: row.real_name || '',
|
||
nickname: row.nickname || '',
|
||
phone: row.phone || '',
|
||
avatar_url: row.avatar_url || '',
|
||
status: row.status,
|
||
role: row.role || 'visitor',
|
||
position: getUserPosition(row.id),
|
||
_origDeptId: getUserDeptId(row.id),
|
||
_origPosition: getUserPosition(row.id),
|
||
department_id: getUserDeptId(row.id),
|
||
}
|
||
dialogVisible.value = true
|
||
}
|
||
|
||
function onDialogClosed() {
|
||
// 对话框关闭动画结束后清理
|
||
}
|
||
|
||
async function handleSave() {
|
||
if (!editForm.value) return
|
||
saving.value = true
|
||
try {
|
||
// 1. 更新用户基本信息(不含部门)
|
||
await updateUser(editForm.value.id, {
|
||
real_name: editForm.value.real_name,
|
||
nickname: editForm.value.nickname,
|
||
phone: editForm.value.phone,
|
||
status: editForm.value.status,
|
||
role: editForm.value.role,
|
||
})
|
||
|
||
// 2. 部门/职位变更走关联表接口
|
||
const newDeptId = editForm.value.department_id
|
||
const origDeptId = editForm.value._origDeptId
|
||
const newPos = editForm.value.position
|
||
const origPos = editForm.value._origPosition
|
||
const deptChanged = newDeptId !== origDeptId
|
||
const posChanged = newPos !== origPos
|
||
|
||
if (deptChanged || posChanged) {
|
||
if (newDeptId) {
|
||
await setEmployeeDepartment(editForm.value.id, newDeptId, newPos)
|
||
} else {
|
||
// 无部门:清除关联(职位也不存了)
|
||
await clearEmployeeDepartment(editForm.value.id)
|
||
}
|
||
}
|
||
|
||
ElMessage.success('保存成功')
|
||
dialogVisible.value = false
|
||
page.value = 1
|
||
await fetchData()
|
||
} catch (err: any) {
|
||
ElMessage.error(err.message || '保存失败')
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
fetchData()
|
||
loadDepartments()
|
||
})
|
||
</script>
|