324 lines
14 KiB
Vue
324 lines
14 KiB
Vue
<template>
|
|
<!--
|
|
1:1 抄 doctor/Submissions.vue
|
|
容器: <div class="page-card">
|
|
面包屑: <div class="breadcrumb">首页 / 人员管理</div>
|
|
筛选栏: <el-form inline :model="q" class="filter-form">
|
|
工具栏: <div class="toolbar">
|
|
列表列 (按原型 sponsor-people.html left 排序):
|
|
复选框 / 姓名 / 手机号 / 工作单位 / 部门 / 职务 / 角色(角色+账号类型已合并) / 状态 / 操作
|
|
-->
|
|
<div class="page-card">
|
|
<div class="breadcrumb">首页 / 人员管理</div>
|
|
|
|
<el-form inline :model="q" class="filter-form">
|
|
<el-form-item label="姓名">
|
|
<el-input v-model="q.name" placeholder="输入姓名" clearable style="width: 160px" />
|
|
</el-form-item>
|
|
<el-form-item label="手机号">
|
|
<el-input v-model="q.phone" placeholder="输入手机号" clearable maxlength="11" style="width: 180px" />
|
|
</el-form-item>
|
|
<el-form-item label="工作单位">
|
|
<el-input v-model="q.orgName" placeholder="输入所属公司" clearable style="width: 180px" />
|
|
</el-form-item>
|
|
<el-form-item label="部门">
|
|
<el-input v-model="q.department" placeholder="输入部门" clearable style="width: 140px" />
|
|
</el-form-item>
|
|
<el-form-item label="状态">
|
|
<el-select v-model="q.status" placeholder="全部状态" clearable style="width: 140px">
|
|
<el-option label="正常" value="0" />
|
|
<el-option label="禁用" value="1" />
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item>
|
|
<el-button type="primary" @click="load">查询</el-button>
|
|
<el-button @click="reset">重置</el-button>
|
|
</el-form-item>
|
|
</el-form>
|
|
|
|
<div class="toolbar">
|
|
<el-button type="primary" @click="onCreate">新建人员</el-button>
|
|
<el-button @click="onImport">批量导入</el-button>
|
|
<el-button :disabled="!selected.length || hasSelfInSelection" @click="onBatchRemove">批量删除</el-button>
|
|
</div>
|
|
|
|
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSelectionChange">
|
|
<el-table-column type="selection" width="48" :selectable="(row) => row.userId !== store.user?.userId" />
|
|
<el-table-column prop="account" label="账号" width="120" show-overflow-tooltip />
|
|
<el-table-column prop="name" label="姓名" width="100" />
|
|
<el-table-column prop="phone" label="手机号" width="130" />
|
|
<el-table-column prop="orgName" label="所属公司" min-width="180" show-overflow-tooltip />
|
|
<el-table-column prop="department" label="部门" width="120" />
|
|
<el-table-column prop="position" label="职务" width="120" />
|
|
<el-table-column label="角色" width="110">
|
|
<template #default="{ row }">
|
|
<!-- 角色与账号类型合并: 由 account_type + unitType 推导 (MAIN=管理员, SUB=监察员) -->
|
|
<el-tag :type="accountTypeRoleTagType(row.accountType)" disable-transitions>
|
|
{{ accountTypeRoleLabel(row.unitType, row.accountType) || '—' }}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="状态" width="80">
|
|
<template #default="{ row }">
|
|
<el-tag :type="row.status === '0' ? 'success' : 'danger'" disable-transitions>
|
|
{{ row.status === '0' ? '正常' : '禁用' }}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="createTime" label="创建时间" width="170" />
|
|
<el-table-column label="操作" width="240" fixed="right">
|
|
<template #default="{ row }">
|
|
<el-button link type="primary" @click="onView(row)">查看</el-button>
|
|
<el-button link type="primary" @click="onEdit(row)">编辑</el-button>
|
|
<!-- 本人不显示禁用/恢复按钮 (避免主账号把自己禁用导致登录不上) -->
|
|
<el-button v-if="row.userId !== store.user?.userId" link :type="row.status === '0' ? 'warning' : 'success'" @click="onToggleStatus(row)">
|
|
{{ row.status === '0' ? '禁用' : '恢复' }}
|
|
</el-button>
|
|
<!-- 本人不显示删除按钮 (避免主账号误删自己的 biz_person) -->
|
|
<el-button v-if="row.userId !== store.user?.userId" link type="danger" @click="onRemoveOne(row)">删除</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
|
|
<el-pagination
|
|
v-model:current-page="page.pageNum"
|
|
v-model:page-size="page.pageSize"
|
|
:total="page.total"
|
|
:page-sizes="[10, 20, 50]"
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|
@current-change="load"
|
|
@size-change="load"
|
|
style="margin-top: 12px; text-align: right;"
|
|
/>
|
|
|
|
<!-- 批量导入 dialog (后端 ExcelUtil 自动生成中文列头 .xlsx) -->
|
|
<el-dialog v-model="importVisible" title="批量导入 (Excel)" width="520px">
|
|
<el-form label-width="80px">
|
|
<el-form-item label="选择文件">
|
|
<!-- 对齐 manager/admin/executor: el-upload :auto-upload=false 自管 file, footer 按钮触发提交 -->
|
|
<el-upload ref="uploadRef" :auto-upload="false" :limit="1" accept=".xls,.xlsx"
|
|
:on-change="onFileChange" :on-remove="onFileRemove">
|
|
<el-button>选择文件</el-button>
|
|
</el-upload>
|
|
</el-form-item>
|
|
<el-form-item>
|
|
<span class="hint-link" @click="downloadTemplate">批量导入模板下载 (.xlsx)</span>
|
|
</el-form-item>
|
|
</el-form>
|
|
<template #footer>
|
|
<el-button @click="importVisible = false">关闭</el-button>
|
|
<el-button type="primary" :loading="importing" :disabled="!importFile" @click="submitImport">开始导入</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
|
|
<!-- 导入结果 dialog (用 table 展示每行成功/失败明细) -->
|
|
<el-dialog v-model="resultVisible" :title="`导入结果 (成功 ${resultOk}/${resultTotal})`" width="780px">
|
|
<el-table :data="resultRows" stripe border max-height="480">
|
|
<el-table-column prop="rowNo" label="行号" width="80" align="center" />
|
|
<el-table-column prop="name" label="姓名" width="120" />
|
|
<el-table-column prop="phone" label="手机号" width="130" />
|
|
<el-table-column label="状态" width="90" align="center">
|
|
<template #default="{ row }">
|
|
<el-tag :type="row.ok ? 'success' : 'danger'" disable-transitions size="small">
|
|
{{ row.ok ? '成功' : '失败' }}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="message" label="错误信息" show-overflow-tooltip />
|
|
</el-table>
|
|
<template #footer>
|
|
<el-button type="primary" @click="resultVisible = false">关闭</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { reactive, ref, computed } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
import { bizUpdate, bizDelete } from '@/api/public'
|
|
import { listSponsorPerson } from '@/api/business/person'
|
|
import { useUserStore } from '@/store/user'
|
|
import { accountTypeRoleLabel, accountTypeRoleTagType } from '@/utils/roleMap'
|
|
import request from '@/utils/request'
|
|
|
|
const router = useRouter()
|
|
const store = useUserStore()
|
|
|
|
// 批量操作禁用判断: 选了主账号自己 (跟"操作列禁用/删除隐藏"是同一条规则, 防止误删)
|
|
const hasSelfInSelection = computed(() => selected.value.some(r => r.userId === store.user?.userId))
|
|
|
|
// sponsor 端只看自己团队 (主账号+子账号) 创建的人, 走专属接口 /business/person/sponsorList (后端强制隔离)
|
|
const q = reactive({
|
|
name: '', phone: '', orgName: '', department: '', status: ''
|
|
})
|
|
const page = reactive({ pageNum: 1, pageSize: 20, total: 0 })
|
|
const rows = ref([])
|
|
const loading = ref(false)
|
|
const selected = ref([])
|
|
|
|
async function load() {
|
|
loading.value = true
|
|
try {
|
|
const res = await listSponsorPerson({ ...q, pageNum: page.pageNum, pageSize: page.pageSize })
|
|
rows.value = res.rows || []
|
|
page.total = res.total || 0
|
|
} catch (e) {
|
|
rows.value = []; page.total = 0
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function reset() {
|
|
q.name = ''
|
|
q.phone = ''
|
|
q.orgName = ''
|
|
q.department = ''
|
|
q.status = ''
|
|
page.pageNum = 1
|
|
load()
|
|
}
|
|
|
|
function onSelectionChange(arr) { selected.value = arr }
|
|
|
|
function onCreate() {
|
|
router.push({ path: '/sponsor/people/new' })
|
|
}
|
|
function onView(row) {
|
|
router.push({ path: `/sponsor/people/detail/${row.personId}` })
|
|
}
|
|
function onEdit(row) {
|
|
router.push({ path: `/sponsor/people/edit/${row.personId}` })
|
|
}
|
|
|
|
async function onToggleStatus(row) {
|
|
const newStatus = row.status === '0' ? '1' : '0'
|
|
const label = newStatus === '0' ? '恢复' : '禁用'
|
|
try {
|
|
await ElMessageBox.confirm(`确定要${label}「${row.name}」吗?`, '确认', { type: 'warning' })
|
|
} catch { return }
|
|
try {
|
|
// 必须带上 userId: BizPersonServiceImpl.updateByPrimaryKey 仅在 userId != null 时
|
|
// 才同步 status 到 sys_user, 否则只是白更新 biz_person (而 biz_person.status 已删除)
|
|
await bizUpdate('person', { personId: row.personId, userId: row.userId, status: newStatus })
|
|
ElMessage.success('已' + label)
|
|
load()
|
|
} catch (e) {
|
|
ElMessage.error(e?.msg || '操作失败')
|
|
}
|
|
}
|
|
|
|
async function onRemoveOne(row) {
|
|
try {
|
|
await ElMessageBox.confirm(`确定删除「${row.name}」? 该操作不可恢复`, '删除确认', { type: 'warning' })
|
|
} catch { return }
|
|
try {
|
|
await bizDelete('person', row.personId)
|
|
ElMessage.success('删除成功')
|
|
load()
|
|
} catch (e) {
|
|
ElMessage.error(e?.msg || '删除失败')
|
|
}
|
|
}
|
|
|
|
async function onBatchRemove() {
|
|
if (!selected.value.length) { ElMessage.warning('请先勾选要删除的人员'); return }
|
|
if (hasSelfInSelection.value) { ElMessage.warning('选中的包含您本人, 不能删除'); return }
|
|
try {
|
|
await ElMessageBox.confirm(`确定删除选中的 ${selected.value.length} 条? 该操作不可恢复`, '批量删除', { type: 'warning' })
|
|
} catch { return }
|
|
let ok = 0
|
|
for (const r of selected.value) {
|
|
try { await bizDelete('person', r.personId); ok++ } catch {}
|
|
}
|
|
ElMessage.success(`已删除 ${ok} 条`)
|
|
selected.value = []
|
|
load()
|
|
}
|
|
|
|
// 批量导入 (Excel)
|
|
const importVisible = ref(false)
|
|
const uploadRef = ref()
|
|
const importFile = ref(null) // el-upload 选中的原始 File 对象 (file.raw)
|
|
const importing = ref(false)
|
|
// 导入结果 (dialog + table)
|
|
const resultVisible = ref(false)
|
|
const resultRows = ref([])
|
|
const resultOk = ref(0)
|
|
const resultTotal = ref(0)
|
|
function onImport() {
|
|
importFile.value = null
|
|
// 关闭后下次再打开 el-upload 内部 fileList 可能残留, 用 ref 清掉
|
|
uploadRef.value?.clearFiles()
|
|
importVisible.value = true
|
|
}
|
|
function onFileChange(file) {
|
|
importFile.value = file.raw
|
|
}
|
|
function onFileRemove() {
|
|
importFile.value = null
|
|
}
|
|
async function downloadTemplate() {
|
|
// 后端 ExcelUtil<BizPersonImportVO>.importTemplateExcel 自动生成中文列头 .xlsx
|
|
const res = await request.get('/business/person/sponsorImportTemplate', { responseType: 'blob' })
|
|
const blob = new Blob([res.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
|
|
const url = URL.createObjectURL(blob)
|
|
const a = document.createElement('a')
|
|
a.href = url
|
|
// 优先用后端 Content-Disposition 的 filename* (utf-8) 字段
|
|
const dispo = res.headers?.['content-disposition'] || res.headers?.['Content-Disposition'] || ''
|
|
const m = /filename\*=UTF-8''([^;]+)/i.exec(dispo)
|
|
a.download = m ? decodeURIComponent(m[1]) : 'sponsor人员批量导入模板.xlsx'
|
|
a.click()
|
|
URL.revokeObjectURL(url)
|
|
ElMessage.success('模板下载完成')
|
|
}
|
|
async function submitImport() {
|
|
if (!importFile.value) { ElMessage.warning('请先选择文件'); return }
|
|
if (!/\.(xlsx|xls)$/i.test(importFile.value.name)) { ElMessage.warning('只支持 .xlsx / .xls 文件'); return }
|
|
importing.value = true
|
|
try {
|
|
const form = new FormData()
|
|
form.append('file', importFile.value)
|
|
const res = await request.post('/business/person/sponsorImport', form, {
|
|
headers: { 'Content-Type': 'multipart/form-data' }
|
|
})
|
|
const data = res.data || res
|
|
resultOk.value = data.ok || 0
|
|
resultTotal.value = data.total || 0
|
|
resultRows.value = data.results || []
|
|
resultVisible.value = true
|
|
importVisible.value = false
|
|
importFile.value = null
|
|
uploadRef.value?.clearFiles()
|
|
load()
|
|
} catch (e) {
|
|
ElMessage.error(e?.msg || '导入失败')
|
|
} finally {
|
|
importing.value = false
|
|
}
|
|
}
|
|
|
|
load()
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* 1:1 抄 doctor/Submissions.vue */
|
|
.page-card { background: #fff; padding: 16px 20px; border-radius: 6px; border: 1px solid #f0f0f0; }
|
|
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
|
.filter-form { display: flex; flex-wrap: wrap; gap: 0 16px; padding-bottom: 12px; border-bottom: 1px solid #f0f0f0; margin-bottom: 12px; }
|
|
.toolbar { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
|
|
|
|
.hint-link { color: var(--brand-primary); cursor: pointer; font-size: 13px; }
|
|
.hint-link:hover { color: var(--brand-primary-deep); }
|
|
|
|
:deep(.el-button--primary) { background: var(--brand-primary); border-color: var(--brand-primary); border-radius: 4px; }
|
|
:deep(.el-button--primary:hover) { background: var(--brand-primary-deep); border-color: var(--brand-primary-deep); }
|
|
:deep(.el-table .el-button) { border-radius: 4px; }
|
|
:deep(.el-table .el-button.is-link) { color: var(--brand-primary); background: transparent; border: none; }
|
|
:deep(.el-table .el-button.is-link:hover) { color: var(--brand-primary-deep); background: transparent; }
|
|
:deep(.el-table .el-button.is-link.is-disabled) { color: #c0c4cc; background: transparent; }
|
|
</style>
|