Files
sc-lktx-mp/mp-sc-admin/src/pages/Users.vue
huangjin fe3ad20fe2 'init'
2026-06-29 17:34:59 +08:00

289 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>