Files
guoju0808/ry-vue3/src/views/sponsor/SponsorPeople.vue
T

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>