refactor(executor): 对齐 sponsor 人员管理模式 + 修 3 个 bug
后端:
- BizAuthController.registerExecutor step 6 同步建 biz_person (主账号自己=管理员, name=unitName, role='admin', unit_type='executor'), 让"人员管理"能看见自己 (跟 registerSponsor 同模式, 走 BizPersonMapper 直插, 绕过 SUB 子账号逻辑)
- BizPersonMapper.xml selectExecutorList 加 OR u.user_id=主账号 (主账号自己能查到), + countAdminByOrgId (校验公司唯一管理员)
- SysUserMapper.xml 手机号正则 1[3-9] → 1[0-9] (允许 10 开头测试号段, 跟 sponsor 一致)
前端 executor/NewPerson.vue 1:1 对齐 sponsor/NewPerson.vue:
- el-card form-card 包裹 + el-row :gutter=12 两列布局
- 用户名/初始密码 行 (v-if=!isEdit)
- 状态改用 el-radio-group, 值 0=正常 1=禁用
- 修了状态值反向 bug: 原 executor 正常→"1" 禁用→"0" 跟 DB/UserStatus enum 相反, 编辑时反向生效 (BizPersonServiceImpl.insert 硬编码 status='0' 掩盖了 bug)
- role 统一 el-select + admin 选项 :disabled=true (管理员只能注册时建)
- 编辑模式 el-select :disabled=true (不能改角色)
- phone 正则 /^1[0-9]\d{9}$/, userName 3-30 模式, password 6-30, department maxlength=100
- 新建成功 ElMessageBox.alert 展示子账号+密码
- 重复手机号走 warning 黄 (isDuplicateError)
- confirmCancel 取消确认
- loadMyOrg 自动带出主账号公司
前端 executor/People.vue:
- 状态列对齐 sponsor: row.status === '0' ? '正常' : '禁用' (修原 || '正常' fallback bug 导致显示原始 '0' + 颜色永远红的两个 bug)
- 批量导入 完整实装 (原 downloadTemplate/submitImport 都是 stub, 只弹提示):
- downloadTemplate GET /business/person/executorImportTemplate + blob 下载 + 解析 Content-Disposition 文件名
- submitImport POST /business/person/executorImport + FormData + 导入结果 dialog (行号/姓名/手机号/成功+错误信息)
- el-upload 加 :on-change=:on-remove 自管 file, footer 改 "关闭/开始导入" 带 loading/disabled
- 新增 onImport 先 clearFiles 再开 dialog
This commit is contained in:
@@ -1,161 +1,165 @@
|
||||
<template>
|
||||
<!--
|
||||
角色: 🟨 执行方 新建/编辑人员 (executor/people/new, executor/people/edit/:id)
|
||||
结构 1:1 对齐 proto new-person.html (但用 page-card 整页包裹, 不用 main.content):
|
||||
page-card (整页面板)
|
||||
page-header (标题区: h1 + 副标题)
|
||||
h1.page-title
|
||||
p.page-sub
|
||||
el-form.card
|
||||
7 字段 form-row
|
||||
form-actions (保存/取消)
|
||||
1:1 抄 sponsor/NewPerson.vue 结构, 仅 role/unitType/orgType 字段值改为 executor
|
||||
表单布局: el-card shadow="never" form-card + el-row :gutter="12" el-col :span="12" 两列
|
||||
编辑/新建复用: 通过 route.params.id 区分
|
||||
-->
|
||||
<div class="page-card new-person">
|
||||
<!-- 面包屑 -->
|
||||
<div class="breadcrumb">
|
||||
<a href="javascript:void(0)" @click="goHome">首页</a>
|
||||
<span class="sep">/</span>
|
||||
<a href="javascript:void(0)" @click="goBack">人员管理</a>
|
||||
<span class="sep">/</span>
|
||||
<span>{{ isEdit ? '编辑人员' : '新建人员' }}</span>
|
||||
<a @click="goBack">人员管理</a> > {{ isEdit ? '编辑人员' : '新建人员' }}
|
||||
</div>
|
||||
|
||||
<!-- 表单 (1 个 page-card 内, 无大标题) -->
|
||||
<el-form ref="formRef" :model="form" :rules="rules" class="person-form" label-width="100px">
|
||||
<el-form-item label="用户名" prop="userName">
|
||||
<el-input v-model="form.userName" placeholder="请输入登录用户名" maxlength="50" />
|
||||
</el-form-item>
|
||||
<el-form-item label="姓名" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入姓名" maxlength="20" />
|
||||
</el-form-item>
|
||||
<!-- 表单卡片 -->
|
||||
<el-card v-loading="loadingDetail" shadow="never" class="form-card nested">
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
|
||||
<el-row v-if="!isEdit" :gutter="12">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="用户名" prop="userName">
|
||||
<el-input v-model="form.userName" placeholder="请输入子账号登录用户名" maxlength="30" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="初始密码" prop="password">
|
||||
<el-input v-model="form.password" placeholder="请输入初始密码 (≥6 位)" type="password" show-password maxlength="30" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="form.phone" placeholder="请输入手机号" maxlength="11" />
|
||||
</el-form-item>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="姓名" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入姓名" maxlength="50" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="form.phone" placeholder="请输入手机号" maxlength="11" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="所属公司" prop="orgName">
|
||||
<el-input v-model="form.orgName" :readonly="!!myOrg" placeholder="请输入所属公司" maxlength="200">
|
||||
<template #append v-if="myOrg">
|
||||
<el-tooltip content="主账号公司, 注册时已自动关联" placement="top">
|
||||
<el-icon><Lock /></el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="所属公司" prop="orgName">
|
||||
<el-input v-model="form.orgName" :readonly="!!myOrg" placeholder="请输入所属公司" maxlength="200">
|
||||
<template #append v-if="myOrg">
|
||||
<el-tooltip content="主账号公司, 注册时已自动关联" placement="top">
|
||||
<el-icon><Lock /></el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="部门" prop="department">
|
||||
<el-input v-model="form.department" placeholder="请输入部门" maxlength="100" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="部门" prop="department">
|
||||
<el-input v-model="form.department" placeholder="请输入部门" maxlength="50" />
|
||||
</el-form-item>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="职务" prop="position">
|
||||
<el-input v-model="form.position" placeholder="请输入职务" maxlength="50" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="角色" prop="role">
|
||||
<!-- 统一 el-select + 管理员禁用 + 编辑模式置灰 (与 sponsor/NewPerson 一致) -->
|
||||
<el-select v-model="form.role" placeholder="请选择" style="width:100%" :disabled="isEdit">
|
||||
<el-option label="会议执行" value="meetingExecutor" />
|
||||
<el-option label="管理员" value="admin" :disabled="true" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="职务" prop="position">
|
||||
<el-input v-model="form.position" placeholder="请输入职务" maxlength="50" />
|
||||
</el-form-item>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12">
|
||||
<!-- 状态用 radio 不用 select: 与 sponsor 一致, 状态值 0=正常 1=禁用 (UserStatus enum) -->
|
||||
<!-- 修了原 executor 状态值反向 bug (原: 正常→"1" 禁用→"0", 跟 DB 约定相反) -->
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio value="0">正常</el-radio>
|
||||
<el-radio value="1">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="角色" prop="role">
|
||||
<el-select v-model="form.role" placeholder="请选择角色" style="width:100%">
|
||||
<el-option label="会议执行" value="meetingExecutor" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="form.status" placeholder="请选择" style="width:100%">
|
||||
<el-option label="正常" value="1" />
|
||||
<el-option label="禁用" value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 按钮区 (在表单内底部) -->
|
||||
<div class="form-actions">
|
||||
<el-button type="primary" :loading="saving" @click="onSave">保存</el-button>
|
||||
<el-button @click="goBack">取消</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
<!-- 操作按钮 -->
|
||||
<div class="form-actions">
|
||||
<el-button type="primary" :loading="saving" @click="onSave">保存</el-button>
|
||||
<el-button @click="confirmCancel">取消</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { reactive, ref, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { bizGet, bizAdd, bizUpdate } from '@/api/public'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { bizAdd, bizUpdate, bizGet } from '@/api/public'
|
||||
import request from '@/utils/request'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { Lock } from '@element-plus/icons-vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const formRef = ref(null)
|
||||
const saving = ref(false)
|
||||
const loadingDetail = ref(false)
|
||||
const isEdit = computed(() => !!route.params.id)
|
||||
const personId = computed(() => route.params.id || null)
|
||||
|
||||
const formRef = ref()
|
||||
const saving = ref(false)
|
||||
// 主账号关联的公司 (注册时建, 按 user_id 反查)
|
||||
const myOrg = ref(null)
|
||||
|
||||
const form = reactive({
|
||||
userName: '',
|
||||
personId: null,
|
||||
name: '',
|
||||
phone: '',
|
||||
orgName: '',
|
||||
department: '',
|
||||
position: '',
|
||||
role: 'meetingExecutor',
|
||||
status: '1',
|
||||
unitType: 'executor'
|
||||
unitType: 'executor',
|
||||
status: '0',
|
||||
// 子账号登录信息 (后端会创建 sys_user SUB, parent_user_id=主账号, 字段名跟后端 @JsonProperty 对齐)
|
||||
userName: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
// 8 字段全必填 + 手机号正则 (与 sponsor/NewPerson 一致)
|
||||
// 注: userName/password 只在新建时校验, 编辑时不传这俩字段
|
||||
const rules = {
|
||||
userName: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
||||
phone: [
|
||||
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
||||
phone: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ pattern: /^1\d{10}$/, message: '手机号格式错误', trigger: 'blur' }
|
||||
{ pattern: /^1[0-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' },
|
||||
],
|
||||
orgName: [{ required: true, message: '请输入所属公司', trigger: 'blur' }],
|
||||
department:[{ required: true, message: '请输入部门', trigger: 'blur' }],
|
||||
position: [{ required: true, message: '请输入职务', trigger: 'blur' }],
|
||||
role: [{ required: true, message: '请选择角色', trigger: 'change' }],
|
||||
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
|
||||
userName: [
|
||||
{ required: true, message: '请输入子账号登录用户名', trigger: 'blur' },
|
||||
{ pattern: /^[a-zA-Z][a-zA-Z0-9_]{2,29}$/, message: '用户名以字母开头,3-30 位字母数字下划线', trigger: 'blur' },
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入初始密码', trigger: 'blur' },
|
||||
{ min: 6, max: 30, message: '密码长度 6-30 位', trigger: 'blur' },
|
||||
],
|
||||
orgName: [{ required: true, message: '请输入所属公司', trigger: 'blur' }],
|
||||
department: [{ required: true, message: '请输入部门', trigger: 'blur' }],
|
||||
position: [{ required: true, message: '请输入职务', trigger: 'blur' }],
|
||||
role: [{ required: true, message: '请选择角色', trigger: 'change' }],
|
||||
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
|
||||
}
|
||||
|
||||
async function loadDetail() {
|
||||
if (!isEdit.value) return
|
||||
try {
|
||||
const r = await bizGet('person', personId.value)
|
||||
Object.assign(form, r.data || {})
|
||||
form.status = String(form.status)
|
||||
} catch (e) {
|
||||
ElMessage.error('加载人员详情失败')
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
try { await formRef.value.validate() } catch { return }
|
||||
saving.value = true
|
||||
try {
|
||||
const payload = { ...form, unitType: 'executor' }
|
||||
if (isEdit.value) {
|
||||
await bizUpdate('person', { personId: personId.value, ...payload })
|
||||
ElMessage.success('修改成功')
|
||||
} else {
|
||||
await bizAdd('person', payload)
|
||||
ElMessage.success('新建成功')
|
||||
}
|
||||
goBack()
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.msg || '保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() { router.push('/executor/people') }
|
||||
|
||||
function goHome() {
|
||||
try { if (window.parent && window.parent.goHome) window.parent.goHome() } catch (e) {}
|
||||
router.push('/executor/overview')
|
||||
function goBack() {
|
||||
router.push({ path: '/executor/people', query: { _t: Date.now() } })
|
||||
}
|
||||
|
||||
// 加载主账号关联的公司 (注册时建, 按 user_id + orgType 反查, 一用户一类型一公司)
|
||||
@@ -171,7 +175,6 @@ async function loadMyOrg() {
|
||||
const rows = (res?.data?.rows) || res?.rows || []
|
||||
if (rows.length) {
|
||||
myOrg.value = rows[0]
|
||||
// 默认带出, 提交时也直接发这个 orgId (不再依赖 orgName 字符串匹配)
|
||||
form.orgName = rows[0].orgName || ''
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -179,6 +182,96 @@ async function loadMyOrg() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDetail() {
|
||||
if (!personId.value) return
|
||||
loadingDetail.value = true
|
||||
try {
|
||||
const res = await bizGet('person', personId.value)
|
||||
if (res?.code === 200 && res.data) {
|
||||
const d = res.data
|
||||
Object.assign(form, {
|
||||
personId: d.personId,
|
||||
name: d.name || '',
|
||||
phone: d.phone || '',
|
||||
orgName: d.orgName || '',
|
||||
department: d.department || '',
|
||||
position: d.position || '',
|
||||
role: d.role || '',
|
||||
unitType: d.unitType || 'executor',
|
||||
status: d.status != null ? String(d.status) : '0',
|
||||
})
|
||||
} else {
|
||||
ElMessage.error(res?.msg || '加载失败')
|
||||
goBack()
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.msg || '加载失败')
|
||||
goBack()
|
||||
} finally {
|
||||
loadingDetail.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function confirmCancel() {
|
||||
const hasContent = form.name || form.phone || form.orgName || form.department || form.position || form.role
|
||||
if (!isEdit.value && !hasContent) { goBack(); return }
|
||||
ElMessageBox.confirm('确定取消?未保存的内容将丢失', '提示', { type: 'warning' })
|
||||
.then(() => goBack())
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
// 业务"重复/已存在"提示用 warning 黄 (memory 规则)
|
||||
function isDuplicateError(msg) {
|
||||
return /已存在|重复|duplicate|uk_person_phone/i.test(msg || '')
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch {
|
||||
ElMessage.warning('请填写必填项')
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
let payload = { ...form }
|
||||
let res
|
||||
if (isEdit.value) {
|
||||
delete payload.userName // 编辑时不改登录账号
|
||||
delete payload.password // 编辑时不改密码
|
||||
res = await bizUpdate('person', payload)
|
||||
} else {
|
||||
payload.unitType = 'executor'
|
||||
res = await bizAdd('person', payload)
|
||||
}
|
||||
if (res?.code === 200) {
|
||||
if (!isEdit.value && res.data) {
|
||||
// 新建成功: 提示主账号刚创建的子账号信息
|
||||
const u = res.data
|
||||
ElMessageBox.alert(
|
||||
`子账号已创建\n账号: ${u.userName || form.userName}\n初始密码: ${u.password || form.password}\n请记录后告知使用人 (密码仅本次显示)`,
|
||||
'创建成功',
|
||||
{ type: 'success', confirmButtonText: '我已记录, 返回' }
|
||||
).then(() => goBack())
|
||||
.catch(() => goBack())
|
||||
} else {
|
||||
ElMessage.success(isEdit.value ? '修改成功' : '新建成功')
|
||||
goBack()
|
||||
}
|
||||
} else {
|
||||
const msg = res?.msg || '保存失败'
|
||||
if (isDuplicateError(msg)) ElMessage.warning('该手机号已存在,请更换')
|
||||
else ElMessage.error(msg)
|
||||
}
|
||||
} catch (e) {
|
||||
const msg = e?.msg || e?.message || '保存失败'
|
||||
if (isDuplicateError(msg)) ElMessage.warning('该手机号已存在,请更换')
|
||||
else ElMessage.error(msg)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (isEdit.value) loadDetail()
|
||||
else loadMyOrg() // 新建时默认带出主账号公司
|
||||
@@ -186,25 +279,20 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 整页面板 (与 People.vue 风格一致) */
|
||||
.executor-new-person { padding: 16px; }
|
||||
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 20px; }
|
||||
.breadcrumb a { color: var(--brand-primary); text-decoration: none; cursor: pointer; }
|
||||
.breadcrumb a:hover { text-decoration: underline; }
|
||||
.breadcrumb .sep { margin: 0 6px; color: #bfbfbf; }
|
||||
/* 1:1 抄 sponsor/NewPerson.vue (page-card / form-card nested / form-actions 风格) */
|
||||
.new-person { max-width: 1200px; }
|
||||
.page-card { background: #fff; padding: 16px 20px; border-radius: 6px; border: 1px solid #f0f0f0; }
|
||||
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 16px; }
|
||||
.breadcrumb a { color: var(--brand-primary); cursor: pointer; text-decoration: none; }
|
||||
.breadcrumb a:hover { color: var(--brand-primary-deep, var(--brand-primary)); }
|
||||
|
||||
/* 表单 */
|
||||
.person-form { max-width: 720px; }
|
||||
.person-form :deep(.el-form-item) { margin-bottom: 18px; }
|
||||
.person-form :deep(.el-form-item__label) { font-size: 14px; color: #595959; }
|
||||
.form-card.nested { background: transparent; border: none; box-shadow: none; }
|
||||
:deep(.form-card .el-card__body) { padding: 24px 28px; }
|
||||
|
||||
/* 按钮区 */
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
margin-top: 32px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
.form-actions { margin-top: 16px; display: flex; gap: 8px; }
|
||||
|
||||
: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, var(--brand-primary)); border-color: var(--brand-primary-deep, var(--brand-primary)); }
|
||||
:deep(.el-button) { border-radius: 4px; }
|
||||
:deep(.el-input__wrapper), :deep(.el-textarea__inner) { border-radius: 4px; }
|
||||
</style>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<!-- ========== 批量按钮 (top=203: 新建人员 / 批量导入) ========== -->
|
||||
<div class="batch-bar">
|
||||
<el-button type="primary" @click="goNew">新建人员</el-button>
|
||||
<el-button @click="importOpen = true">批量导入</el-button>
|
||||
<el-button @click="onImport">批量导入</el-button>
|
||||
<span class="filter-tip">*批量导入模板与列表表项一致</span>
|
||||
</div>
|
||||
|
||||
@@ -34,17 +34,22 @@
|
||||
<el-tag size="small" :type="row.role === 'meetingExecutor' ? 'warning' : 'info'">{{ translatePersonRole(row.role) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="row.status === '正常' ? 'success' : 'danger'">{{ row.status || '正常' }}</el-tag>
|
||||
<el-tag :type="row.status === '0' ? 'success' : 'danger'" disable-transitions>
|
||||
{{ row.status === '0' ? '正常' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" link type="primary" @click="onView(row)">查看</el-button>
|
||||
<el-button size="small" link type="primary" @click="goEdit(row)">编辑</el-button>
|
||||
<el-button v-if="(row.status || '正常') === '正常'" size="small" link type="danger" @click="onToggleStatus(row, '禁用')">禁用</el-button>
|
||||
<el-button v-else size="small" link type="success" @click="onToggleStatus(row, '恢复')">恢复</el-button>
|
||||
<!-- 本人不显示禁用/恢复按钮 (跟 sponsor 一样, 避免主账号把自己禁用) -->
|
||||
<template v-if="row.userId !== store.user?.userId">
|
||||
<el-button v-if="(row.status || '正常') === '正常'" size="small" link type="danger" @click="onToggleStatus(row, '禁用')">禁用</el-button>
|
||||
<el-button v-else size="small" link type="success" @click="onToggleStatus(row, '恢复')">恢复</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -58,21 +63,42 @@
|
||||
<!-- (旧的 formOpen dialog 已删) -->
|
||||
|
||||
<!-- ========== Dialog: 批量导入 ========== -->
|
||||
<el-dialog v-model="importOpen" title="批量导入" width="520px">
|
||||
<el-form label-width="100px">
|
||||
<el-dialog v-model="importOpen" title="批量导入 (Excel)" width="520px">
|
||||
<el-form label-width="80px">
|
||||
<el-form-item label="选择文件">
|
||||
<el-upload ref="uploadRef" :auto-upload="false" :limit="1" accept=".xls,.xlsx">
|
||||
<!-- 对齐 manager/admin/sponsor: 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 label="">
|
||||
<el-button link type="primary" @click="downloadTemplate">批量导入模板下载</el-button>
|
||||
<el-form-item>
|
||||
<span class="hint-link" @click="downloadTemplate">批量导入模板下载 (.xlsx)</span>
|
||||
</el-form-item>
|
||||
<el-form-item><span class="filter-tip">*批量导入模板xls与列表表项一致</span></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="importOpen = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitImport">确定</el-button>
|
||||
<el-button @click="importOpen = 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>
|
||||
|
||||
@@ -97,10 +123,13 @@ import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { bizUpdate } from '@/api/public'
|
||||
import { listExecutorPerson } from '@/api/business/person'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { translatePersonRole } from '@/utils/roleMap'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useUserStore()
|
||||
|
||||
const list = ref([])
|
||||
const loading = ref(false)
|
||||
@@ -111,6 +140,15 @@ const importOpen = ref(false)
|
||||
const view = ref({})
|
||||
const viewOpen = 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)
|
||||
|
||||
async function loadList() {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -142,14 +180,58 @@ async function onToggleStatus(row, action) {
|
||||
} catch (e) { if (e !== 'cancel') ElMessage.error(e?.msg || `${action}失败`) }
|
||||
}
|
||||
|
||||
function downloadTemplate() {
|
||||
ElMessage.info('模板下载中...')
|
||||
// 批量导入 (Excel) - 对齐 sponsor/SponsorPeople.vue
|
||||
function onImport() {
|
||||
importFile.value = null
|
||||
// 关闭后下次再打开 el-upload 内部 fileList 可能残留, 用 ref 清掉
|
||||
uploadRef.value?.clearFiles()
|
||||
importOpen.value = true
|
||||
}
|
||||
|
||||
function submitImport() {
|
||||
ElMessage.success('导入成功')
|
||||
importOpen.value = false
|
||||
loadList()
|
||||
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/executorImportTemplate', { 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]) : 'executor人员批量导入模板.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/executorImport', 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
|
||||
importOpen.value = false
|
||||
importFile.value = null
|
||||
uploadRef.value?.clearFiles()
|
||||
loadList()
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.msg || '导入失败')
|
||||
} finally {
|
||||
importing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadList)
|
||||
@@ -160,6 +242,8 @@ onMounted(loadList)
|
||||
.page-sub { font-size: 13px; color: #909399; margin-bottom: 16px; }
|
||||
.batch-bar { margin-bottom: 12px; display: flex; gap: 8px; align-items: center; }
|
||||
.filter-tip { color: #909399; font-size: 12px; }
|
||||
.hint-link { color: var(--brand-primary); cursor: pointer; font-size: 13px; }
|
||||
.hint-link:hover { color: var(--brand-primary-deep, var(--brand-primary)); }
|
||||
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
|
||||
.filter-form { margin-bottom: 12px; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user