- 支付配置: privateKeyPath改为privateKey,支持textarea粘贴和pem文件上传
- 审计日志: 优化列表展示(head字段/result解析)和详情弹窗
- 橘视厅调用日志: 接口路径截断显示/JSON格式化/耗时颜色分级
- 第三方设备列表: 新增来源/WiFi MAC/UID/心跳时间列,状态改为已激活/未激活
- 菜单/角色/用户详情: 统一用 data.{entity} 提取业务对象
- 设备权益: 去除双层data嵌套,适配后端统一响应结构
332 lines
11 KiB
Vue
Executable File
332 lines
11 KiB
Vue
Executable File
<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>
|