邀请发送 (菜单暂隐藏, 待确认参数见 发送邀请-待确认参数.md): - 后端: BizInvite / BizInviteRecipient + Controller/Service/Mapper/XML - 邮件: InviteMailSender (spring-boot-starter-mail SMTP) + application*.yml 邮件配置 - 上传进度: UploadProgressRegistry + UploadProgressController - 前端: InviteList / InviteNew / InviteDetail / InviteView + api/business/invite.js - 原型: proto/html/components/invite-detail / new-invitation / send-invitation 登录/账号: - 首次设密: /getInfo 返回 isPasswordEmpty, SysProfileController 密码为空时跳过旧密码校验, ForcePasswordDialog 强制弹窗 - 姓名单一可信源 resolveDisplayName: doctor→biz_expert.name, sponsor/executor→biz_person.name, 其余回退 nick_name - OA compliance 门禁改为按手机号查 ecology 视图 (不再限定 manager/leader) 其它: - OSS zip 在线查看 (列清单+取单文件, 公开只读) + SecurityConfig permitAll - doctor 项目详情 ProjectDetail.vue - 数据库/测试/设计文档 (md) 入库
449 lines
16 KiB
Vue
449 lines
16 KiB
Vue
<template>
|
||
<!--
|
||
共享支持单位下人员列表页 (admin/manager 共用, 通过 route.path 区分角色)
|
||
- 入口: org 表格行内「人员管理」按钮 (router.push 带 orgId/orgName)
|
||
- 接口: bizList('person', q) / bizUpdate / bizAdd / sponsorImport / sponsorImportTemplate
|
||
- op 列: 查看 / 编辑 / 启用/禁用 (3 按钮, 共用, 不分角色)
|
||
-->
|
||
<div class="page-card sponsor-people">
|
||
<div class="breadcrumb">
|
||
首页 / 支持单位管理 / 人员管理{{ orgName ? ' (' + orgName + ')' : '' }}
|
||
</div>
|
||
|
||
<!-- ========== 筛选区 ========== -->
|
||
<el-form inline :model="q" class="filter-form" @keyup.enter="load">
|
||
<el-form-item label="姓名">
|
||
<el-input v-model="q.name" placeholder="姓名" clearable style="width:140px" />
|
||
</el-form-item>
|
||
<el-form-item label="手机号">
|
||
<el-input v-model="q.phone" placeholder="手机号" clearable style="width:140px" />
|
||
</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-input v-model="q.position" placeholder="职务" clearable style="width:140px" />
|
||
</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="batch-bar">
|
||
<el-button type="primary" @click="goNew">新建人员</el-button>
|
||
<el-button @click="openImport">批量导入</el-button>
|
||
</div>
|
||
|
||
<!-- ========== 表格 ========== -->
|
||
<el-table :data="rows" v-loading="loading" stripe border>
|
||
<el-table-column prop="name" label="姓名" min-width="100" />
|
||
<el-table-column prop="phone" label="手机号" min-width="130" />
|
||
<el-table-column prop="orgName" label="企业名称" min-width="180" show-overflow-tooltip />
|
||
<el-table-column prop="department" label="部门" min-width="100" />
|
||
<el-table-column prop="position" label="职务" min-width="100" />
|
||
<el-table-column label="状态" width="100" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="statusTagType(row.status)" disable-transitions>{{ row.status === '1' ? '禁用' : '正常' }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<!-- 管理员 switch: 仅 admin 可见, 指定机构管理员 (MAIN) -->
|
||
<el-table-column v-if="isAdmin" label="管理员" width="90" align="center">
|
||
<template #default="{ row }">
|
||
<el-switch
|
||
:model-value="row.accountType === 'MAIN'"
|
||
:disabled="switchingId === row.personId"
|
||
@change="onChangeAdmin(row)"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="140" fixed="right">
|
||
<template #default="{ row }"><div class="table-actions">
|
||
<!-- 查看: 保留为直链 (高频操作) -->
|
||
<el-link :underline="false" type="primary" @click="goView(row)">查看</el-link>
|
||
<!-- 更多: 编辑 + 启用/禁用 折叠到 el-dropdown -->
|
||
<el-dropdown trigger="click" @command="(cmd) => onMoreAction(cmd, row)">
|
||
<el-link :underline="false" type="primary" class="more-link">
|
||
更多<el-icon class="more-icon"><ArrowDown /></el-icon>
|
||
</el-link>
|
||
<template #dropdown>
|
||
<el-dropdown-menu>
|
||
<el-dropdown-item command="edit">编辑</el-dropdown-item>
|
||
<el-dropdown-item command="resetPwd" v-if="row.userId">重置密码</el-dropdown-item>
|
||
<el-dropdown-item :command="isDisabled(row) ? 'enable' : 'disable'">
|
||
{{ isDisabled(row) ? '启用' : '禁用' }}
|
||
</el-dropdown-item>
|
||
<!-- 删除 (软删除): 软删关联登录账号 sys_user.del_flag, 机构管理员(MAIN)需先更换管理员 -->
|
||
<el-dropdown-item command="delete" divided>删除</el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</template>
|
||
</el-dropdown>
|
||
</div></template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<div class="pager">
|
||
<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"
|
||
/>
|
||
</div>
|
||
|
||
<!-- ========== 批量导入 (沿用 executor-people 模式: drag + 模板下载在框外 + ImportResultDialog) ========== -->
|
||
<el-dialog v-model="importOpen" title="批量导入" width="400px" append-to-body :close-on-click-modal="false">
|
||
<el-upload
|
||
v-if="importOpen"
|
||
ref="uploadRef"
|
||
:limit="1"
|
||
accept=".xlsx, .xls"
|
||
:on-change="onFileChange"
|
||
:on-remove="onFileRemove"
|
||
:auto-upload="false"
|
||
drag
|
||
>
|
||
<el-icon class="el-icon--upload"><upload /></el-icon>
|
||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||
</el-upload>
|
||
<!-- 模板下载: 必须放在 el-upload 框外(下面), 否则点击会被 drag 区域吞掉, 触发的是文件选择器 -->
|
||
<div style="margin-top:10px;font-size:13px;text-align:center">
|
||
<span style="color:#909399;margin-right:8px">仅允许导入 xls、xlsx 格式文件</span>
|
||
<el-link type="primary" :underline="false" @click="downloadTemplate">下载模板</el-link>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="importOpen=false">取 消</el-button>
|
||
<el-button type="primary" :loading="importing" @click="submitImport">确 定</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
<ImportResultDialog v-model="importResultOpen" :result="importResult" />
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, onMounted, computed, watch } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { bizList, bizUpdate, bizDelete, importPerson, downloadImportTemplate, changePersonAdmin, resetPersonPassword } from '@/api/public'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { Upload, ArrowDown } from '@element-plus/icons-vue'
|
||
import ImportResultDialog from '@/components/ImportResultDialog.vue'
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
|
||
// 角色判定 + 路由基础路径 (admin/manager 共用页面, 但跳回各自 list)
|
||
const isAdmin = computed(() => route.path.startsWith('/admin/'))
|
||
const listBasePath = computed(() => isAdmin.value ? '/admin/sponsor-people' : '/manager/sponsor-people')
|
||
|
||
const orgId = computed(() => route.query.orgId ? Number(route.query.orgId) : null)
|
||
const orgName = computed(() => route.query.orgName || '')
|
||
|
||
const q = reactive({ name: '', phone: '', orgName: '', department: '', position: '' })
|
||
const rows = ref([])
|
||
const loading = ref(false)
|
||
const page = reactive({ pageNum: 1, pageSize: 20, total: 0 })
|
||
|
||
function statusTagType(s) {
|
||
// 统一: 0=正常(success), 1=禁用(danger)
|
||
return s === '1' ? 'danger' : 'success'
|
||
}
|
||
function isDisabled(row) {
|
||
return row.status === '1'
|
||
}
|
||
|
||
async function load() {
|
||
loading.value = true
|
||
try {
|
||
const params = {
|
||
...q,
|
||
orgType: 'sponsor',
|
||
...(orgId.value ? { orgId: orgId.value } : {}),
|
||
pageNum: page.pageNum,
|
||
pageSize: page.pageSize,
|
||
}
|
||
const { data } = await bizList('person', params)
|
||
rows.value = (data && data.rows) || []
|
||
page.total = (data && data.total) || 0
|
||
} catch { rows.value = []; page.total = 0 }
|
||
finally { loading.value = false }
|
||
}
|
||
function reset() {
|
||
Object.assign(q, { name: '', phone: '', orgName: '', department: '', position: '' })
|
||
page.pageNum = 1
|
||
load()
|
||
}
|
||
|
||
async function onToggleStatus(row) {
|
||
const disabled = isDisabled(row)
|
||
// sys_user.status 是 CHAR(1), 只能写 '0'/'1' (禁用='1', 启用='0')
|
||
// 必须带 userId: BizPersonServiceImpl.updateByPrimaryKey 仅在 userId != null 时
|
||
// 才同步 status 到 sys_user, 否则只更新 biz_person (而 biz_person.status 已删除)
|
||
const newStatus = disabled ? '0' : '1'
|
||
const label = disabled ? '启用' : '禁用'
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
`确定要${label}「${row.name}」吗?`,
|
||
'确认',
|
||
{ type: 'warning' }
|
||
)
|
||
} catch { return }
|
||
try {
|
||
await bizUpdate('person', { personId: row.personId, userId: row.userId, status: newStatus })
|
||
ElMessage.success('已' + label)
|
||
load()
|
||
} catch (e) { ElMessage.error(e?.msg || '操作失败') }
|
||
}
|
||
|
||
// ========== 更换机构管理员 (仅 admin) ==========
|
||
const switchingId = ref('')
|
||
async function onChangeAdmin(row) {
|
||
// switch 受控 (:model-value), 当前管理员 (MAIN) 的 switch 恒为开, 无法直接关掉
|
||
if (row.accountType === 'MAIN') { ElMessage.info('已是该机构管理员'); return }
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
`确定将「${row.name}」设为该机构管理员吗?`,
|
||
'更换管理员',
|
||
{ type: 'warning' }
|
||
)
|
||
} catch { return }
|
||
switchingId.value = row.personId
|
||
try {
|
||
await changePersonAdmin(row.personId)
|
||
ElMessage.success('已更换管理员')
|
||
load()
|
||
} catch (e) {
|
||
ElMessage.error(e?.msg || e?.message || '更换失败')
|
||
} finally {
|
||
switchingId.value = ''
|
||
}
|
||
}
|
||
|
||
// ========== 重置密码 ==========
|
||
async function onResetPwd(row) {
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
`确定重置「${row.name}」的登录密码为默认 123456 吗?`,
|
||
'重置密码',
|
||
{ type: 'warning' }
|
||
)
|
||
} catch { return }
|
||
try {
|
||
await resetPersonPassword(row.personId)
|
||
ElMessage.success('已重置为 123456')
|
||
} catch (e) {
|
||
ElMessage.error(e?.msg || e?.message || '重置失败')
|
||
}
|
||
}
|
||
|
||
// ========== 跳转 (按角色) ==========
|
||
function goNew() {
|
||
router.push({
|
||
path: listBasePath.value + '/new',
|
||
query: {
|
||
...(orgId.value ? { orgId: orgId.value } : {}),
|
||
...(orgName.value ? { orgName: orgName.value } : {})
|
||
}
|
||
})
|
||
}
|
||
function goEdit(row) {
|
||
router.push({
|
||
path: listBasePath.value + '/edit/' + row.personId,
|
||
query: {
|
||
...(orgId.value ? { orgId: orgId.value } : {}),
|
||
...(orgName.value ? { orgName: orgName.value } : {})
|
||
}
|
||
})
|
||
}
|
||
function goView(row) {
|
||
router.push({
|
||
path: listBasePath.value + '/view/' + row.personId,
|
||
query: {
|
||
...(orgId.value ? { orgId: orgId.value } : {}),
|
||
...(orgName.value ? { orgName: orgName.value } : {})
|
||
}
|
||
})
|
||
}
|
||
|
||
// 操作列 "更多" 下拉: 分发编辑/重置密码/启用/禁用/删除
|
||
function onMoreAction(cmd, row) {
|
||
if (cmd === 'edit') return goEdit(row)
|
||
if (cmd === 'resetPwd') return onResetPwd(row)
|
||
if (cmd === 'enable' || cmd === 'disable') return onToggleStatus(row)
|
||
if (cmd === 'delete') return onDelete(row)
|
||
}
|
||
|
||
// ========== 删除 (软删除) ==========
|
||
async function onDelete(row) {
|
||
// 机构管理员 (MAIN) 不能直接删除: 删了会导致机构无主账号, 需先更换管理员
|
||
if (row.accountType === 'MAIN') { ElMessage.warning('机构管理员不能删除,请先更换管理员'); return }
|
||
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 || e?.message || '删除失败')
|
||
}
|
||
}
|
||
|
||
// ========== 批量导入 ==========
|
||
const importOpen = ref(false)
|
||
const importing = ref(false)
|
||
const uploadRef = ref()
|
||
const importFile = ref(null)
|
||
const importResultOpen = ref(false)
|
||
const importResult = ref(null)
|
||
|
||
function openImport() { importFile.value = null; importResult.value = null; importOpen.value = true }
|
||
function onFileChange(file) { importFile.value = file.raw }
|
||
function onFileRemove() { importFile.value = null }
|
||
async function downloadTemplate() {
|
||
try {
|
||
const res = await downloadImportTemplate('sponsor')
|
||
const blob = new Blob([res.data], { type: res.data.type || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
|
||
const url = URL.createObjectURL(blob)
|
||
const a = document.createElement('a')
|
||
a.href = url
|
||
a.download = '支持单位人员导入模板.xlsx'
|
||
a.click()
|
||
URL.revokeObjectURL(url)
|
||
} catch { ElMessage.error('模板下载失败') }
|
||
}
|
||
async function submitImport() {
|
||
if (!importFile.value) { ElMessage.warning('请先选择文件'); return }
|
||
importing.value = true
|
||
try {
|
||
const res = await importPerson('sponsor', importFile.value, orgId.value)
|
||
const payload = res.data || res
|
||
const ok = payload.ok ?? 0
|
||
const failed = (payload.results || []).filter(r => !r.ok)
|
||
// 映射为 ImportResultDialog 期望的形状 { okNum, ngNum, ngList: [{rowNum, message}] }
|
||
importResult.value = {
|
||
okNum: ok,
|
||
ngNum: failed.length,
|
||
ngList: failed.map(f => ({ rowNum: f.rowNo, message: (f.name ? f.name + ': ' : '') + (f.message || '') }))
|
||
}
|
||
importOpen.value = false
|
||
importResultOpen.value = true
|
||
load()
|
||
} catch (e) {
|
||
ElMessage.error(e?.msg || '导入失败')
|
||
} finally {
|
||
importing.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(load)
|
||
// 从 SponsorPersonNew / SponsorPersonDetail 跳回来时刷新
|
||
watch(() => route.fullPath, () => {
|
||
if (route.path === listBasePath.value) load()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.sponsor-people { padding: 16px; }
|
||
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
||
.batch-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
|
||
.filter-tip { color: #909399; font-size: 12px; }
|
||
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
|
||
|
||
/* ========================================
|
||
移动端适配 (≤768px)
|
||
- 卡片 padding 收窄
|
||
- 工具栏横向滚动 (6 个按钮铺不下, 用 overflow-x)
|
||
- filter-form: 保持 label 左 + input 右横向布局
|
||
- 表格字号收紧
|
||
======================================== */
|
||
@media (max-width: 768px) {
|
||
/* 卡片 padding */
|
||
.page-card { padding: 12px !important; border-radius: 4px !important; }
|
||
.breadcrumb { font-size: 12px !important; margin-bottom: 8px !important; }
|
||
|
||
/* 工具栏: 横向滚动 (按钮多时不换行) */
|
||
.toolbar {
|
||
flex-wrap: nowrap !important;
|
||
overflow-x: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
padding-bottom: 6px;
|
||
margin-bottom: 8px !important;
|
||
scrollbar-width: thin;
|
||
}
|
||
.toolbar::-webkit-scrollbar { height: 4px; }
|
||
.toolbar::-webkit-scrollbar-thumb { background: var(--brand-slate-200); border-radius: 2px; }
|
||
.toolbar :deep(.action-btn),
|
||
.toolbar :deep(.el-button) {
|
||
flex-shrink: 0;
|
||
font-size: 12px !important;
|
||
padding: 0 10px !important;
|
||
height: 30px !important;
|
||
}
|
||
|
||
/* filter-form: label 与输入框横向 (label 左, input 右) */
|
||
.filter-form {
|
||
display: flex !important;
|
||
flex-direction: column !important;
|
||
align-items: stretch !important;
|
||
gap: 10px !important;
|
||
}
|
||
.filter-form :deep(.el-form-item) {
|
||
display: flex !important;
|
||
align-items: center !important;
|
||
margin-right: 0 !important;
|
||
margin-bottom: 0 !important;
|
||
}
|
||
.filter-form :deep(.el-form-item__label) {
|
||
float: none !important;
|
||
width: auto !important;
|
||
min-width: 80px !important;
|
||
text-align: right !important;
|
||
padding: 0 8px 0 0 !important;
|
||
font-size: 13px !important;
|
||
color: var(--el-text-color-regular) !important;
|
||
line-height: 32px !important;
|
||
height: 32px !important;
|
||
}
|
||
.filter-form :deep(.el-form-item__content) {
|
||
margin-left: 0 !important;
|
||
line-height: 32px !important;
|
||
flex: 1 !important;
|
||
min-width: 0 !important;
|
||
}
|
||
|
||
/* 强制所有控件全宽, 覆盖 inline 260px / 140px / 120px */
|
||
.filter-form :deep(.el-select),
|
||
.filter-form :deep(.el-input),
|
||
.filter-form :deep(.el-date-editor),
|
||
.filter-form :deep(.el-button),
|
||
.filter-form :deep(.el-cascader) {
|
||
width: 100% !important;
|
||
min-width: 0 !important;
|
||
margin-left: 0 !important;
|
||
margin-right: 0 !important;
|
||
display: block !important;
|
||
}
|
||
.filter-form :deep(.el-button + .el-button) {
|
||
margin-top: 8px !important;
|
||
}
|
||
|
||
/* 表格字号收紧 */
|
||
:deep(.el-table) { font-size: 12px !important; }
|
||
}
|
||
|
||
/* 操作列 "更多" 下拉样式 (桌面 + 移动端共用) */
|
||
.more-link { display: inline-flex; align-items: center; gap: 2px; }
|
||
.more-icon { font-size: 12px; }
|
||
</style>
|