Files
guoju0808/ry-vue3/src/views/sponsor-people/SponsorPeople.vue
T
郭庆泰 5a0a892574 feat: 邀请函邮件发送 + 首次设密/密码为空 + 姓名单一可信源
邀请发送 (菜单暂隐藏, 待确认参数见 发送邀请-待确认参数.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) 入库
2026-09-10 20:40:49 +08:00

449 lines
16 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>
<!--
共享支持单位下人员列表页 (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">仅允许导入 xlsxlsx 格式文件</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>