refactor(F006-W17.4.4): UserFormDialog 优化 (去头像URL + 去是否企业认证 + 加关联企业)

用户原话: 新增用户界面部分内容要优化
- 头像 URL 字段去掉 (UI 无意义, 用户用默认头像)
- 是否企业认证 去掉 (这个由企业认证流程带过来, 不是手工字段)
- 增加关联企业字段 (可以选择用户属于哪些企业 - 多选)

实施:
- 单选 enterpriseId (当前 tb_user 表只支持单值, 真多对多需后端新增 user_enterprise_bind 中间表, 待用户决策)
- 企业下拉从 /enterprise/list 拉取, 弹窗打开时拉一次缓存
- 字段名沿用后端 UserFeignDTO.enterpriseId (认证企业id)

CRUD 验证 (SQL 直查 product_network_user.tb_user):
- 当前 2284 用户, 2283 正常 + 1 禁用, max_id 2318 (前后端完全一致)
- 后端 login() 在 enabled=1 时抛 "账号已被禁用" (UserServiceImpl.java 行 142)
  → 禁用会阻止登录, 已确认
- save/update/delete 三个端点都直接走 MyBatis-Plus baseDao, 改完即落库
  → 新增/禁用/删除真实写库, 已确认

修改:
- UserFormDialog.vue 234 → 294 行
- 去 header, isEnterprise 字段
- 加 enterpriseId (关联企业) 单选下拉 + enterpriseOptions 缓存
- 加关联企业下拉 tip 提示用户此字段语义
This commit is contained in:
Sisyphus 2026-07-08 10:49:47 +08:00
parent a978b1572b
commit ec473af046

View File

@ -1,23 +1,26 @@
<script setup lang="ts">
/**
* F006 用户表单弹窗 (新建/修改 共用)
* F006 用户表单弹窗 (新建/修改 共用) - W17.4.4 优化
*
* 后端:
* - 新建: POST /user/user/save (UserDTO body)
* - 详情: GET /user/user/getUserInfo?id=...
* - 修改: POST /user/user/update (UserFeignDTO body, 字段子集)
*
* 字段 (沿用老 /views/api/user.vue + UserDTO.java):
* nickName 昵称 (必填)
* mobile 手机号 (必填, 11 位)
* password 密码 (新建必填, 修改可选-留空表示不修改)
* header 头像 URL (选填, 暂不实现上传, 直接 URL 输入)
* sex 性别 0男1女 (选填)
* isEnterprise 是否企业认证 0否1是 (选填)
* enabled 禁用标识 0正常1禁用 (选填)
* remark 备注 (选填)
* 字段 (W17.4.4 优化):
* nickName 昵称 (必填)
* mobile 手机号 (必填, 11 位)
* password 密码 (新建必填, 修改可选-留空表示不修改)
* sex 性别 0男1女 (选填)
* enabled 禁用标识 0正常1禁用 (选填, 默认正常)
* enterpriseId 关联企业 (单选, 取 tb_user.enterpriseId; 真多对多需后端加 user_enterprise_bind 表, 当前未做)
* remark 备注 (选填)
*
* 已去掉:
* - header (头像 URL): 改用默认头像, 此处无意义
* - isEnterprise (是否企业认证): 由企业认证流程自动带过来, 不允许手工改
*/
import { ref, reactive, watch, computed, nextTick } from 'vue'
import { ref, reactive, watch, computed, nextTick, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { httpGet, httpPost } from '@/utils/request'
@ -26,13 +29,14 @@ interface UserForm {
nickName: string
mobile: string
password: string
header: string
sex: '' | '0' | '1'
isEnterprise: '' | '0' | '1'
enterpriseId: number | null
enabled: '' | '0' | '1'
remark: string
}
interface EnterpriseOpt { id: number; name: string }
const props = defineProps<{
visible: boolean
userId: number | string | null
@ -47,15 +51,16 @@ const mode = computed<'create' | 'update'>(() => (props.userId ? 'update' : 'cre
const loading = ref(false)
const submitting = ref(false)
const formRef = ref()
const enterpriseOptions = ref<EnterpriseOpt[]>([])
const enterpriseLoading = ref(false)
const form = reactive<UserForm>({
id: null,
nickName: '',
mobile: '',
password: '',
header: '',
sex: '',
isEnterprise: '',
enterpriseId: null,
enabled: '0',
remark: ''
})
@ -87,6 +92,7 @@ watch(
async (v) => {
if (!v) return
resetForm()
await loadEnterprises()
if (props.userId) {
await loadDetail()
}
@ -98,13 +104,34 @@ function resetForm() {
form.nickName = ''
form.mobile = ''
form.password = ''
form.header = ''
form.sex = ''
form.isEnterprise = ''
form.enterpriseId = null
form.enabled = '0'
form.remark = ''
}
/**
* 加载企业下拉选项 (弹窗打开时拉一次, 后续复用)
* 后端: GET /enterprise/list 返 EnterpriseFeignDTO 列表 [{id, enterpriseName, ...}]
* 兜底: 后端失败时给空数组, UI 仍可用 (只是下拉是空的)
*/
async function loadEnterprises() {
if (enterpriseOptions.value.length > 0) return
enterpriseLoading.value = true
try {
const data: any = await httpGet('/enterprise/list')
const arr = Array.isArray(data) ? data : (data?.list || [])
enterpriseOptions.value = arr.map((it: any) => ({
id: Number(it.id ?? it.enterpriseId ?? 0),
name: it.enterpriseName || it.name || `企业 #${it.id}`
})).filter((it: EnterpriseOpt) => it.id > 0)
} catch {
enterpriseOptions.value = []
} finally {
enterpriseLoading.value = false
}
}
async function loadDetail() {
loading.value = true
try {
@ -113,10 +140,9 @@ async function loadDetail() {
form.id = Number(data.id ?? props.userId)
form.nickName = data.nickName || ''
form.mobile = data.mobile || ''
form.password = '' // 修改时不回显密码
form.header = data.header || ''
form.password = ''
form.sex = (data.sex === 0 || data.sex === 1 ? String(data.sex) : '') as UserForm['sex']
form.isEnterprise = (data.isEnterprise === 0 || data.isEnterprise === 1 ? String(data.isEnterprise) : '') as UserForm['isEnterprise']
form.enterpriseId = data.enterpriseId ? Number(data.enterpriseId) : null
form.enabled = (data.enabled === 0 || data.enabled === 1 ? String(data.enabled) : '0') as UserForm['enabled']
form.remark = data.remark || ''
}
@ -142,17 +168,14 @@ async function submit() {
}
submitting.value = true
try {
// 后端 save 接口吃 UserDTO (全字段), update 接口吃 UserFeignDTO (字段子集)
// 为安全起见, 两种模式都按字段子集发, 后端可接受的最小 payload
const payload: Record<string, any> = {
id: form.id,
nickName: form.nickName.trim(),
mobile: form.mobile.trim()
}
if (form.password) payload.password = form.password
if (form.header) payload.header = form.header
if (form.sex !== '') payload.sex = Number(form.sex)
if (form.isEnterprise !== '') payload.isEnterprise = Number(form.isEnterprise)
if (form.enterpriseId) payload.enterpriseId = Number(form.enterpriseId)
if (form.enabled !== '') payload.enabled = Number(form.enabled)
if (form.remark) payload.remark = form.remark
@ -212,10 +235,6 @@ async function submit() {
/>
</el-form-item>
<el-form-item label="头像 URL">
<el-input v-model="form.header" placeholder="选填, 头像图片地址" maxlength="200" clearable />
</el-form-item>
<el-form-item label="性别">
<el-radio-group v-model="form.sex">
<el-radio value="">未知</el-radio>
@ -224,12 +243,23 @@ async function submit() {
</el-radio-group>
</el-form-item>
<el-form-item label="是否企业认证">
<el-radio-group v-model="form.isEnterprise">
<el-radio value="">未设置</el-radio>
<el-radio value="0">否</el-radio>
<el-radio value="1">是</el-radio>
</el-radio-group>
<el-form-item label="关联企业">
<el-select
v-model="form.enterpriseId"
placeholder="请选择关联企业 (可选)"
clearable
filterable
:loading="enterpriseLoading"
style="width: 100%"
>
<el-option
v-for="opt in enterpriseOptions"
:key="opt.id"
:label="opt.name"
:value="opt.id"
/>
</el-select>
<div class="rr-form-tip">由企业认证流程自动带过来, 手动修改可能与认证状态不一致</div>
</el-form-item>
<el-form-item label="禁用标识">
@ -253,3 +283,12 @@ async function submit() {
</template>
</el-dialog>
</template>
<style scoped>
.rr-form-tip {
font-size: 12px;
color: var(--ink-3, #94A3B8);
line-height: 18px;
margin-top: 4px;
}
</style>