用户反馈: 关联企业的下拉框是空的 排查: 1. nginx-vue.conf 加 /enterprise/ location (W17.4.5) ✅ 2. gateway.json 加 enterprise 路由 + 推 nacos ✅ 3. 但 gateway 启动时 bootstrap.yml 没订阅 gateway.json dataId (bootstrap.yml 只订阅 datasource.yaml + common.yaml + gateway-server.yaml/-dev.yaml) gateway.json 仅供运维手工管理, 实际不参与路由 4. /enterprise/list 在 user-server 也 404 (jar 是旧版本, ApiEnterpriseController @RequestMapping 路径未生效) 修法: 前端从 /user/user/page 拉用户列表聚合 (enterpriseId, enterpriseName) 去重 - 端点 100% 可用 (已在 W11 验收过 2284 用户) - 字段 enterpriseId + enterpriseName 已在 user 表 - 聚合 Map<id, name> 去重, 按 id 排序 - 弹窗打开时拉一次缓存, 后续复用 nginx /enterprise/ location + gateway.json 路由保留: future-proof, 等后端 jar 重打包后会自动生效 前端聚合方式短期可行, 但仅用于下拉选项场景; 如要真正查询某企业的所有用户等场景, 仍需后端打 jar 暴露 /enterprise/page
290 lines
8.5 KiB
Vue
290 lines
8.5 KiB
Vue
<script setup lang="ts">
|
|
/**
|
|
* F006 用户表单弹窗 (新建/修改 共用) - W17.4.4 优化
|
|
*
|
|
* 后端:
|
|
* - 新建: POST /user/user/save (UserDTO body)
|
|
* - 详情: GET /user/user/getUserInfo?id=...
|
|
* - 修改: POST /user/user/update (UserFeignDTO body, 字段子集)
|
|
*
|
|
* 字段 (W17.4.4 优化):
|
|
* nickName 昵称 (必填)
|
|
* mobile 手机号 (必填, 11 位)
|
|
* password 密码 (新建必填, 修改可选-留空表示不修改)
|
|
* sex 性别 0男1女 (选填)
|
|
* enabled 禁用标识 0正常1禁用 (选填, 默认正常)
|
|
* enterpriseId 关联企业 (单选, 取 tb_user.enterpriseId; 真多对多需后端加 user_enterprise_bind 表, 当前未做)
|
|
*
|
|
* 已去掉:
|
|
* - header (头像 URL): 改用默认头像, 此处无意义
|
|
* - isEnterprise (是否企业认证): 由企业认证流程自动带过来, 不允许手工改
|
|
*/
|
|
import { ref, reactive, watch, computed, nextTick, onMounted } from 'vue'
|
|
import { ElMessage } from 'element-plus'
|
|
import { httpGet, httpPost } from '@/utils/request'
|
|
|
|
interface UserForm {
|
|
id: number | null
|
|
nickName: string
|
|
mobile: string
|
|
password: string
|
|
sex: '' | '0' | '1'
|
|
enterpriseId: number | null
|
|
enabled: '' | '0' | '1'
|
|
}
|
|
|
|
interface EnterpriseOpt { id: number; name: string }
|
|
|
|
const props = defineProps<{
|
|
visible: boolean
|
|
userId: number | string | null
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'update:visible', v: boolean): void
|
|
(e: 'success'): void
|
|
}>()
|
|
|
|
const mode = computed<'create' | 'update'>(() => (props.userId ? 'update' : 'create'))
|
|
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: '',
|
|
sex: '',
|
|
enterpriseId: null,
|
|
enabled: '0',
|
|
})
|
|
|
|
const rules = {
|
|
nickName: [{ required: true, message: '请输入用户昵称', trigger: 'blur' }],
|
|
mobile: [
|
|
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
|
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }
|
|
],
|
|
password: [
|
|
{
|
|
validator(_: any, value: string, cb: (err?: Error) => void) {
|
|
if (mode.value === 'create' && !value) {
|
|
return cb(new Error('新建用户必须输入密码'))
|
|
}
|
|
if (value && (value.length < 6 || value.length > 20)) {
|
|
return cb(new Error('密码长度 6-20 位'))
|
|
}
|
|
cb()
|
|
},
|
|
trigger: 'blur'
|
|
}
|
|
]
|
|
}
|
|
|
|
watch(
|
|
() => props.visible,
|
|
async (v) => {
|
|
if (!v) return
|
|
resetForm()
|
|
await loadEnterprises()
|
|
if (props.userId) {
|
|
await loadDetail()
|
|
}
|
|
}
|
|
)
|
|
|
|
function resetForm() {
|
|
form.id = null
|
|
form.nickName = ''
|
|
form.mobile = ''
|
|
form.password = ''
|
|
form.sex = ''
|
|
form.enterpriseId = null
|
|
form.enabled = '0'
|
|
}
|
|
|
|
/**
|
|
* 加载企业下拉选项 (弹窗打开时拉一次, 后续复用)
|
|
*
|
|
* 实现: 从 /user/user/page 用户列表聚合 (enterpriseId, enterpriseName) 去重
|
|
* 原因: gateway.json 当前没配 /enterprise/** 路由, /enterprise/list 返 404;
|
|
* 而 user-server jar 内 ApiEnterpriseController 也没正确暴露路径
|
|
* (推测 jar 是旧版本编译, controller 路径没生效)
|
|
* 替代: 用户表里有 enterpriseId + enterpriseName 字段, 直接聚合即可
|
|
* 兜底: 后端失败时给空数组
|
|
*/
|
|
async function loadEnterprises() {
|
|
if (enterpriseOptions.value.length > 0) return
|
|
enterpriseLoading.value = true
|
|
try {
|
|
const data: any = await httpGet('/user/user/page', { page: 1, limit: 10000 })
|
|
const arr = Array.isArray(data?.list) ? data.list : (Array.isArray(data) ? data : [])
|
|
const seen = new Map<number, string>()
|
|
for (const u of arr) {
|
|
const eid = Number(u.enterpriseId ?? 0)
|
|
if (eid > 0 && !seen.has(eid)) {
|
|
seen.set(eid, u.enterpriseName || `企业 #${eid}`)
|
|
}
|
|
}
|
|
enterpriseOptions.value = Array.from(seen.entries())
|
|
.map(([id, name]) => ({ id, name }))
|
|
.sort((a, b) => a.id - b.id)
|
|
} catch {
|
|
enterpriseOptions.value = []
|
|
} finally {
|
|
enterpriseLoading.value = false
|
|
}
|
|
}
|
|
|
|
async function loadDetail() {
|
|
loading.value = true
|
|
try {
|
|
const data: any = await httpGet('/user/user/getUserInfo', { id: props.userId })
|
|
if (data) {
|
|
form.id = Number(data.id ?? props.userId)
|
|
form.nickName = data.nickName || ''
|
|
form.mobile = data.mobile || ''
|
|
form.password = ''
|
|
form.sex = (data.sex === 0 || data.sex === 1 ? String(data.sex) : '') as UserForm['sex']
|
|
form.enterpriseId = data.enterpriseId ? Number(data.enterpriseId) : null
|
|
form.enabled = (data.enabled === 0 || data.enabled === 1 ? String(data.enabled) : '0') as UserForm['enabled']
|
|
}
|
|
await nextTick()
|
|
formRef.value?.clearValidate()
|
|
} catch (e: any) {
|
|
ElMessage.error(e?.msg || e?.message || '加载用户详情失败')
|
|
close()
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function close() {
|
|
emit('update:visible', false)
|
|
}
|
|
|
|
async function submit() {
|
|
try {
|
|
await formRef.value?.validate()
|
|
} catch {
|
|
return
|
|
}
|
|
submitting.value = true
|
|
try {
|
|
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.sex !== '') payload.sex = Number(form.sex)
|
|
if (form.enterpriseId) payload.enterpriseId = Number(form.enterpriseId)
|
|
if (form.enabled !== '') payload.enabled = Number(form.enabled)
|
|
|
|
if (mode.value === 'create') {
|
|
await httpPost('/user/user/save', payload)
|
|
ElMessage.success('新建成功')
|
|
} else {
|
|
await httpPost('/user/user/update', payload)
|
|
ElMessage.success('修改成功')
|
|
}
|
|
emit('success')
|
|
close()
|
|
} catch (e: any) {
|
|
ElMessage.error(e?.msg || e?.message || (mode.value === 'create' ? '新建失败' : '修改失败'))
|
|
} finally {
|
|
submitting.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<el-dialog
|
|
:model-value="visible"
|
|
:title="mode === 'create' ? '新建用户' : '修改用户'"
|
|
width="560"
|
|
:style="{ borderRadius: '12px', overflow: 'hidden' }"
|
|
align-center
|
|
destroy-on-close
|
|
:close-on-click-modal="false"
|
|
@update:model-value="(v: boolean) => emit('update:visible', v)"
|
|
@closed="resetForm"
|
|
>
|
|
<div v-loading="loading">
|
|
<el-form
|
|
ref="formRef"
|
|
:model="form"
|
|
:rules="rules"
|
|
label-width="100px"
|
|
label-position="right"
|
|
>
|
|
<el-form-item label="用户昵称" prop="nickName">
|
|
<el-input v-model="form.nickName" placeholder="请输入用户昵称" maxlength="50" show-word-limit clearable />
|
|
</el-form-item>
|
|
|
|
<el-form-item label="手机号" prop="mobile">
|
|
<el-input v-model="form.mobile" placeholder="请输入手机号" maxlength="11" clearable />
|
|
</el-form-item>
|
|
|
|
<el-form-item label="登录密码" prop="password">
|
|
<el-input
|
|
v-model="form.password"
|
|
type="password"
|
|
show-password
|
|
:placeholder="mode === 'create' ? '请输入密码 (6-20 位)' : '留空表示不修改密码'"
|
|
maxlength="20"
|
|
clearable
|
|
/>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="性别">
|
|
<el-radio-group v-model="form.sex">
|
|
<el-radio value="">未知</el-radio>
|
|
<el-radio value="0">男</el-radio>
|
|
<el-radio value="1">女</el-radio>
|
|
</el-radio-group>
|
|
</el-form-item>
|
|
|
|
<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>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="禁用标识">
|
|
<el-radio-group v-model="form.enabled">
|
|
<el-radio value="0">正常</el-radio>
|
|
<el-radio value="1">禁用</el-radio>
|
|
</el-radio-group>
|
|
</el-form-item>
|
|
|
|
</el-form>
|
|
</div>
|
|
|
|
<template #footer>
|
|
<el-button @click="close">取消</el-button>
|
|
<el-button type="primary" :loading="submitting" @click="submit">
|
|
{{ mode === 'create' ? '确认新建' : '确认修改' }}
|
|
</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
</template>
|
|
|
|
<style scoped>
|
|
</style>
|