Files
admin-ui/src/views/system/tenant/component/editTenant.vue

467 lines
14 KiB
Vue
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>
<div class="system-edit-tenant-container">
<el-dialog :title="(ruleForm.id !== 0 ? '修改' : '添加') + '租户'" v-model="isShowDialog" width="769px">
<el-form ref="formRef" :model="ruleForm" :rules="rules" size="default" label-width="90px">
<el-row :gutter="35">
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="租户名称" prop="tenantName">
<el-input v-model="ruleForm.tenantName" placeholder="请输入租户名称" clearable></el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="租户类型" prop="tenantType">
<el-select v-model="ruleForm.tenantType" placeholder="请选择租户类型" clearable class="w100">
<el-option label="普通类型" :value="1"></el-option>
<el-option label="代理类型" :value="2"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="联系人" prop="userNickname">
<el-input v-model="ruleForm.userNickname" placeholder="请输入联系人" clearable></el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="电话" prop="mobile">
<el-input v-model="ruleForm.mobile" placeholder="请输入电话" clearable></el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="所属城市" prop="cityMergerName">
<el-cascader
v-model="ruleForm.cityMergerName"
:options="cityOptions"
placeholder="请选择省市"
clearable
class="w100"
></el-cascader>
</el-form-item>
</el-col>
<!-- 新增时显示账号和密码 -->
<template v-if="ruleForm.id === 0">
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="租户账号" prop="userName">
<el-input v-model="ruleForm.userName" placeholder="字母或数字6位以上" clearable></el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="密码" prop="userPassword">
<el-input v-model="ruleForm.userPassword" placeholder="请输入密码" type="password" show-password clearable @input="checkPasswordStrength"></el-input>
<div class="password-strength" v-if="ruleForm.userPassword">
强度: <span :class="passwordStrengthClass">{{ passwordStrengthText }}</span>
</div>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="确认密码" prop="confirmPassword">
<el-input v-model="ruleForm.confirmPassword" placeholder="请再次输入密码" type="password" show-password clearable></el-input>
</el-form-item>
</el-col>
</template>
<!-- 修改时只显示账号不可改 -->
<template v-else>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="租户账号">
<el-input v-model="ruleForm.userName" disabled></el-input>
</el-form-item>
</el-col>
</template>
<el-col :span="24" class="mb20">
<el-form-item label="营业执照" prop="businessLicense">
<div class="upload-container">
<el-upload
class="avatar-uploader"
:http-request="handleImageUpload"
:show-file-list="false"
:before-upload="beforeAvatarUpload"
accept="image/*"
>
<img v-if="imagePreview" :src="imagePreview" class="avatar" />
<div v-else class="upload-placeholder">
<el-icon class="avatar-uploader-icon"><Plus /></el-icon>
<span class="upload-text">点击上传营业执照</span>
</div>
</el-upload>
<el-button v-if="imagePreview" type="danger" text size="small" style="margin-left: 10px" @click="removeImage">删除</el-button>
<div class="upload-tip">支持 jpgpng 格式文件大小不超过 2MB</div>
</div>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="onCancel" size="default"> </el-button>
<el-button type="primary" @click="onSubmit" size="default">{{ ruleForm.id !== 0 ? '修 改' : '添 加' }}</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, unref, computed, toRefs } from 'vue';
import { ElMessage, UploadProps } from 'element-plus';
import { Plus } from '@element-plus/icons-vue';
import { addTenant, editTenant } from '/@/api/system/tenant';
import { pcTextArr,provinceAndCityData } from 'element-china-area-data';
import { getUpFileUrl } from '/@/utils/gfast';
import { uploadAssetImage } from '/@/api/assets/asset';
// 定义父组件传递的事件
const emit = defineEmits(['getTenantList']);
const formRef = ref<HTMLElement | null>(null);
// 省市数据(使用 element-china-area-data
const cityOptions = ref<any[]>([]);
// 图片上传相关
const fileAddressPrefix = ref('');
const imagePreview = ref('');
const initCityData = () => {
const data = JSON.parse(JSON.stringify(provinceAndCityData));
cityOptions.value = data.map((item: any) => {
// 处理直辖市:北京(110000), 天津(120000), 上海(310000), 重庆(500000)
if (['110000', '120000', '310000', '500000'].includes(item.value) || ['北京市', '天津市', '上海市', '重庆市'].includes(item.label)) {
delete item.children;
}
return item;
});
};
initCityData();
const state = reactive({
isShowDialog: false,
passwordStrength: 0,
ruleForm: {
id: 0,
tenantName: '',
tenantType: '' as number | '',
userNickname: '',
mobile: '',
cityMergerName: [] as string[],
userName: '',
userPassword: '',
confirmPassword: '',
businessLicense: '',
},
rules: {
tenantName: [{ required: true, message: '请输入租户名称', trigger: 'blur' }],
tenantType: [{ required: true, message: '请选择租户类型', trigger: 'change' }],
userNickname: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
mobile: [
{ required: true, message: '请输入电话', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
],
cityMergerName: [{ required: true, message: '请选择所属城市', trigger: 'change' }],
userName: [
{ required: true, message: '请输入租户账号', trigger: 'blur' },
{ pattern: /^[a-zA-Z0-9]{6,}$/, message: '账号必须为字母或数字且长度至少6位', trigger: 'blur' }
],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
confirmPassword: [
{ required: true, message: '请再次输入密码', trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
if (value !== state.ruleForm.userPassword) {
callback(new Error('两次输入密码不一致'));
} else {
callback();
}
},
trigger: 'blur'
}
],
businessLicense: [{ required: true, message: '请上传营业执照', trigger: 'change' }]
},
});
// 解构 state 以便在模板中使用
const { isShowDialog, ruleForm, rules, passwordStrength } = toRefs(state);
// 密码强度检测
const checkPasswordStrength = () => {
const pwd = state.ruleForm.userPassword;
let strength = 0;
if (pwd.length >= 6) strength++;
if (/[A-Z]/.test(pwd)) strength++;
if (/[a-z]/.test(pwd)) strength++;
if (/[0-9]/.test(pwd)) strength++;
if (/[^A-Za-z0-9]/.test(pwd)) strength++;
state.passwordStrength = strength;
};
const passwordStrengthText = computed(() => {
const s = state.passwordStrength;
if (s < 2) return '弱';
if (s < 4) return '中';
return '强';
});
const passwordStrengthClass = computed(() => {
const s = state.passwordStrength;
if (s < 2) return 'strength-weak';
if (s < 4) return 'strength-medium';
return 'strength-strong';
});
// 打开弹窗
const openDialog = (row?: any) => {
resetForm();
if (row) {
// 处理城市回显:根据 cityCode 转换为级联选择器需要的数组格式
// cityCode 是6位代码如 "220100",级联选择器需要 ["22", "2201"] 格式
let cityArray: string[] = [];
const storedCode = row.cityCode;
if (storedCode && typeof storedCode === 'string') {
// 判断是否为直辖市代码 (11xxxx, 12xxxx, 31xxxx, 50xxxx)
if (/^(11|12|31|50)/.test(storedCode)) {
// 直辖市回显为省级代码2位
cityArray = [storedCode.substring(0, 2)];
} else {
// 普通省市,回显为 [省代码(2位), 市代码(4位)]
if (storedCode.length >= 4) {
const provinceCode = storedCode.substring(0, 2);
const cityCode = storedCode.substring(0, 4);
cityArray = [provinceCode, cityCode];
}
}
}
state.ruleForm = {
id: row.id,
tenantName: row.tenantName,
tenantType: row.tenantType,
userNickname: row.userNickname,
mobile: row.mobile,
cityMergerName: cityArray,
userName: row.userName,
userPassword: '', // 修改时不显示密码
confirmPassword: '',
businessLicense: row.businessLicense,
};
// 图片预览回显
if (row.businessLicense) {
// 设置 fileAddressPrefix如果后端返回了
if (row.fileAddressPrefix) {
fileAddressPrefix.value = row.fileAddressPrefix;
}
imagePreview.value = formatImageUrl(row.businessLicense);
}
}
state.isShowDialog = true;
};
const closeDialog = () => {
state.isShowDialog = false;
};
const onCancel = () => {
closeDialog();
};
const onSubmit = () => {
const formWrap = unref(formRef) as any;
if (!formWrap) return;
formWrap.validate((valid: boolean) => {
if (valid) {
// 处理城市数据提交:取数组最后一位
const submitForm: any = { ...state.ruleForm };
if (Array.isArray(submitForm.cityMergerName) && submitForm.cityMergerName.length > 0) {
let lastCode = String(submitForm.cityMergerName[submitForm.cityMergerName.length - 1]);
// 特殊处理直辖市:如果选中的是省级代码,转换为市级代码 (市辖区)
const municipalityMap: Record<string, string> = {
'11': '110100', '110000': '110100', // 北京
'12': '120100', '120000': '120100', // 天津
'31': '310100', '310000': '310100', // 上海
'50': '500100', '500000': '500100', // 重庆
};
if (municipalityMap[lastCode]) {
lastCode = municipalityMap[lastCode];
} else if (lastCode.length === 4) {
// 普通省市如果是4位代码补齐为6位
lastCode = lastCode + '00';
}
submitForm.cityCode = lastCode;
} else {
submitForm.cityCode = '';
}
// 图片已通过 uploadAssetImage 接口上传businessLicense 存储的是相对路径
// 直接提交 JSON 对象
if (state.ruleForm.id === 0) {
addTenant(submitForm).then(() => {
ElMessage.success('添加成功');
closeDialog();
emit('getTenantList');
});
} else {
// 修改时不能修改密码和账号
delete submitForm.userPassword;
delete submitForm.confirmPassword;
delete submitForm.userName;
editTenant(submitForm).then(() => {
ElMessage.success('修改成功');
closeDialog();
emit('getTenantList');
});
}
}
});
};
const resetForm = () => {
state.ruleForm = {
id: 0,
tenantName: '',
tenantType: '',
userNickname: '',
mobile: '',
cityMergerName: [],
userName: '',
userPassword: '',
confirmPassword: '',
businessLicense: '',
};
state.passwordStrength = 0;
imagePreview.value = '';
};
// 格式化图片 URL
const formatImageUrl = (url?: string) => {
if (!url) return '';
if (/^https?:\/\//i.test(url)) return url;
if (/^blob:/i.test(url)) return url;
return `${fileAddressPrefix.value || ''}${url}`;
};
// 上传图片并返回URL
const uploadImage = async (file: File): Promise<string> => {
const res: any = await uploadAssetImage(file);
// 获取并设置 fileAddressPrefix
if (res.fileAddressPrefix) {
fileAddressPrefix.value = res.fileAddressPrefix;
} else if (res.data && typeof res.data === 'object' && res.data.fileAddressPrefix) {
fileAddressPrefix.value = res.data.fileAddressPrefix;
}
// 获取 fileURL
if (res.fileURL) return res.fileURL;
if (res.data && typeof res.data === 'object') {
if (res.data.fileURL) return res.data.fileURL;
if (res.data.url) return res.data.url;
}
if (typeof res.data === 'string') return res.data;
return '';
};
// 处理图片上传
const handleImageUpload = async (options: any) => {
try {
const url = await uploadImage(options.file);
if (url) {
state.ruleForm.businessLicense = url;
imagePreview.value = formatImageUrl(url);
}
} catch (error) {
ElMessage.error('图片上传失败');
}
};
// 删除图片
const removeImage = () => {
state.ruleForm.businessLicense = '';
imagePreview.value = '';
};
const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
if (rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/png') {
ElMessage.error('图片格式必须为 JPG/PNG!');
return false;
} else if (rawFile.size / 1024 / 1024 > 2) {
ElMessage.error('图片大小不能超过 2MB!');
return false;
}
return true;
};
// 暴露变量
defineExpose({
openDialog,
});
</script>
<style scoped lang="scss">
.system-edit-tenant-container {
.avatar-uploader .avatar {
width: 178px;
height: 178px;
display: block;
object-fit: cover;
border-radius: 6px;
}
.upload-container {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.upload-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 178px;
height: 178px;
color: #8c939d;
background-color: #f5f7fa;
.el-icon {
font-size: 28px;
margin-bottom: 8px;
}
.upload-text {
font-size: 12px;
color: #8c939d;
}
}
.upload-tip {
margin-top: 8px;
font-size: 12px;
color: #909399;
line-height: 1.5;
}
}
.avatar-uploader .el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
}
.avatar-uploader .el-upload:hover {
border-color: var(--el-color-primary);
.upload-placeholder {
color: var(--el-color-primary);
.upload-text {
color: var(--el-color-primary);
}
}
}
.password-strength {
font-size: 12px;
margin-top: 5px;
.strength-weak { color: #f56c6c; }
.strength-medium { color: #e6a23c; }
.strength-strong { color: #67c23a; }
}
</style>