Files
as-partner-frontend/src/views/modules/sys/components/user-add-or-update.vue
freeboss e39b88d1db feat: 优化日志页面/支付配置/设备列表/菜单详情数据解析
- 支付配置: privateKeyPath改为privateKey,支持textarea粘贴和pem文件上传
- 审计日志: 优化列表展示(head字段/result解析)和详情弹窗
- 橘视厅调用日志: 接口路径截断显示/JSON格式化/耗时颜色分级
- 第三方设备列表: 新增来源/WiFi MAC/UID/心跳时间列,状态改为已激活/未激活
- 菜单/角色/用户详情: 统一用 data.{entity} 提取业务对象
- 设备权益: 去除双层data嵌套,适配后端统一响应结构
2026-08-11 16:07:10 +08:00

332 lines
11 KiB
Vue
Executable File
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>
<el-dialog
:title="!dataForm.id ? '添加用户' : '修改'"
:close-on-click-modal="false"
v-model="visible">
<el-form :model="dataForm" :rules="dataRule" ref="dataFormRef" @keyup.enter="dataFormSubmit()" label-width="100px">
<template v-if="!dataForm.id">
<el-form-item label="归属方式" prop="bindScopeType">
<el-radio-group v-model="dataForm.bindScopeType" @change="onBindScopeChange">
<el-radio value="channel_agent">渠道 / 代理商</el-radio>
<el-radio value="store">门店</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="dataForm.bindScopeType === 'channel_agent'"
label="所属组织"
prop="selectedOrgId">
<org-tree-select
v-model="dataForm.selectedOrgId"
:hide-node-types="orgTreeHideTypes"
height="320px"
@change="onOrgTreeChange" />
</el-form-item>
<el-form-item v-else label="所属门店" prop="storeOrgId">
<store-select
v-model="dataForm.storeOrgId"
width="100%"
@change="onStoreSelectChange" />
</el-form-item>
</template>
<el-form-item label="用户名" prop="userName">
<el-input v-model="dataForm.userName" placeholder="登录帐号"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password" :class="{ 'is-required': !dataForm.id }">
<el-input
v-model="dataForm.password"
type="password"
:placeholder="dataForm.id ? '不改请留空' : '密码'"
autocomplete="new-password"></el-input>
<div v-if="!dataForm.id" class="form-tip" style="margin-top: 4px;">最短8位,且须同时包含小写、大写、数字、特殊字符4种</div>
<div v-else class="form-tip" style="margin-top: 4px;">不修改密码请留空</div>
</el-form-item>
<el-form-item label="确认密码" prop="comfirmPassword" :class="{ 'is-required': !dataForm.id }">
<el-input
v-model="dataForm.comfirmPassword"
type="password"
:placeholder="dataForm.id ? '不改请留空' : '确认密码'"
autocomplete="new-password"></el-input>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="dataForm.email" placeholder="选填"></el-input>
</el-form-item>
<el-form-item label="手机号" prop="mobile">
<el-input v-model="dataForm.mobile" placeholder="选填"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="dataFormSubmit()">确定</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { reactive, ref, nextTick } from 'vue'
import { getUserInfo, saveUser, type UserInfo } from '@/api/sys'
import { isEmail, isMobile } from '@/utils/validate'
import { ElMessage, type FormInstance, type FormItemRule } from 'element-plus'
import OrgTreeSelect from '@/components/org-tree-select/index.vue'
import StoreSelect from '@/components/store-select/index.vue'
const emit = defineEmits<{
refreshDataList: []
}>()
interface OrgTreePayload {
orgId: string | number | null
channelOrgId?: string | number | null
agentOrgId?: string | number | null
}
/** 组织树仅展示渠道/代理商,不包含门店与平台根 */
const orgTreeHideTypes = ['PLATFORM', 'STORE']
const visible = ref<boolean>(false)
const dataFormRef = ref<FormInstance | null>(null)
const orgBindings = reactive<OrgTreePayload>({
orgId: null,
channelOrgId: null,
agentOrgId: null
})
interface DataForm {
id: number
bindScopeType: 'channel_agent' | 'store'
selectedOrgId: string | number | undefined
storeOrgId: string | number | undefined
userName: string
password: string
comfirmPassword: string
salt: string
email: string
mobile: string
status: number
}
const dataForm = reactive<DataForm>({
id: 0,
bindScopeType: 'channel_agent',
selectedOrgId: undefined,
storeOrgId: undefined,
userName: '',
password: '',
comfirmPassword: '',
salt: '',
email: '',
mobile: '',
status: 1
})
function onOrgTreeChange (payload: OrgTreePayload): void {
orgBindings.orgId = payload.orgId
orgBindings.channelOrgId = payload.channelOrgId
orgBindings.agentOrgId = payload.agentOrgId
nextTick(() => {
dataFormRef.value?.validateField('selectedOrgId')
})
}
function onBindScopeChange (): void {
dataForm.selectedOrgId = undefined
dataForm.storeOrgId = undefined
orgBindings.orgId = null
orgBindings.channelOrgId = null
orgBindings.agentOrgId = null
nextTick(() => {
dataFormRef.value?.clearValidate(['selectedOrgId', 'storeOrgId'])
})
}
function onStoreSelectChange (): void {
nextTick(() => {
dataFormRef.value?.validateField('storeOrgId')
})
}
function validatePasswordStrength (password: string): { valid: boolean; message?: string } {
if (!password || password.length < 8) return { valid: false, message: '密码长度不能少于8位' }
const hasLower = /[a-z]/.test(password)
const hasUpper = /[A-Z]/.test(password)
const hasDigit = /[0-9]/.test(password)
const hasSpecial = /[^A-Za-z0-9]/.test(password)
if (!hasLower) return { valid: false, message: '密码须包含小写字母' }
if (!hasUpper) return { valid: false, message: '密码须包含大写字母' }
if (!hasDigit) return { valid: false, message: '密码须包含数字' }
if (!hasSpecial) return { valid: false, message: '密码须包含特殊字符' }
return { valid: true }
}
const validatePassword: FormItemRule['validator'] = (_rule, value, callback) => {
const v = typeof value === 'string' ? value.trim() : ''
if (!dataForm.id && !v) {
callback(new Error('密码不能为空'))
} else if (!dataForm.id && v) {
const r = validatePasswordStrength(v)
if (!r.valid) return callback(new Error(r.message))
callback()
} else if (dataForm.id && v) {
const r = validatePasswordStrength(v)
if (!r.valid) return callback(new Error(r.message))
callback()
} else {
callback()
}
}
const validateComfirmPassword: FormItemRule['validator'] = (_rule, value, callback) => {
const confirm = typeof value === 'string' ? value.trim() : ''
const pwd = typeof dataForm.password === 'string' ? dataForm.password.trim() : ''
if (!dataForm.id && !confirm) {
callback(new Error('确认密码不能为空'))
} else if (!pwd && confirm) {
callback(new Error('请先填写密码'))
} else if (pwd !== confirm) {
callback(new Error('确认密码与密码输入不一致'))
} else {
callback()
}
}
const validateEmail: FormItemRule['validator'] = (_rule, value, callback) => {
if (!value || !String(value).trim()) {
return callback()
}
if (!isEmail(value as string)) {
callback(new Error('邮箱格式错误'))
} else {
callback()
}
}
const validateMobile: FormItemRule['validator'] = (_rule, value, callback) => {
if (!value || !String(value).trim()) {
return callback()
}
if (!isMobile(value as string)) {
callback(new Error('手机号格式错误'))
} else {
callback()
}
}
const validateChannelAgentOrg: FormItemRule['validator'] = (_rule, value, callback) => {
if (dataForm.id || dataForm.bindScopeType !== 'channel_agent') return callback()
if (value == null || value === '') {
return callback(new Error('请选择渠道或代理商'))
}
callback()
}
const validateStoreOrg: FormItemRule['validator'] = (_rule, value, callback) => {
if (dataForm.id || dataForm.bindScopeType !== 'store') return callback()
if (value == null || value === '') {
return callback(new Error('请选择门店'))
}
callback()
}
const dataRule: Record<string, FormItemRule[]> = {
selectedOrgId: [{ validator: validateChannelAgentOrg, trigger: 'change' }],
storeOrgId: [{ validator: validateStoreOrg, trigger: 'change' }],
userName: [{ required: true, message: '用户名不能为空', trigger: 'blur' }],
password: [{ validator: validatePassword, trigger: 'blur' }],
comfirmPassword: [{ validator: validateComfirmPassword, trigger: 'blur' }],
email: [{ validator: validateEmail, trigger: 'blur' }],
mobile: [{ validator: validateMobile, trigger: 'blur' }]
}
function init (id?: number | string | null): void {
dataForm.id = id ? Number(id) : 0
visible.value = true
nextTick(() => {
if (dataFormRef.value && dataFormRef.value.clearValidate) {
dataFormRef.value.clearValidate()
}
dataForm.bindScopeType = 'channel_agent'
dataForm.selectedOrgId = undefined
dataForm.storeOrgId = undefined
orgBindings.orgId = null
orgBindings.channelOrgId = null
orgBindings.agentOrgId = null
dataForm.userName = ''
dataForm.password = ''
dataForm.comfirmPassword = ''
dataForm.salt = ''
dataForm.email = ''
dataForm.mobile = ''
dataForm.status = 1
})
if (id !== null && id !== undefined && id !== '') {
getUserInfo(Number(id)).then((body) => {
if (!body || body.code !== 0) return
const user = body.data?.user
if (!user || typeof user !== 'object') return
dataForm.id = user.id
dataForm.userName = user.username || ''
dataForm.salt = user.salt || ''
dataForm.email = user.email || ''
dataForm.mobile = user.mobile || ''
dataForm.status = user.status != null ? Number(user.status) : 1
dataForm.password = ''
dataForm.comfirmPassword = ''
})
}
}
function dataFormSubmit (): void {
if (!dataFormRef.value) return
dataFormRef.value.validate((valid) => {
if (!valid) return
const pwdTrim = typeof dataForm.password === 'string' ? dataForm.password.trim() : ''
const payload: Record<string, any> = {
id: dataForm.id || undefined,
username: dataForm.userName,
salt: dataForm.salt,
email: dataForm.email,
mobile: dataForm.mobile,
status: dataForm.status
}
if (dataForm.id) {
if (pwdTrim) payload.password = pwdTrim
} else {
payload.password = pwdTrim
}
if (!dataForm.id) {
if (dataForm.bindScopeType === 'channel_agent') {
payload.orgId = orgBindings.orgId ?? dataForm.selectedOrgId
if (orgBindings.channelOrgId != null) {
payload.channelOrgId = orgBindings.channelOrgId
}
if (orgBindings.agentOrgId) {
payload.agentOrgId = orgBindings.agentOrgId
}
} else {
payload.orgId = dataForm.storeOrgId
}
}
saveUser(payload).then((res) => {
if (res && res.code === 0) {
ElMessage.success({
message: '操作成功',
duration: 1500,
onClose: () => {
visible.value = false
emit('refreshDataList')
}
})
} else {
ElMessage.error(res?.msg || '请求失败')
}
})
})
}
defineExpose({ init })
</script>