This commit is contained in:
huangjin
2026-06-29 17:34:59 +08:00
commit fe3ad20fe2
271 changed files with 51767 additions and 0 deletions

View File

@@ -0,0 +1,288 @@
<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>