| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423 |
- <template>
- <el-dialog :model-value="visible" @update:model-value="$emit('update:visible', $event)" :title="dialogTitle"
- width="700px" destroy-on-close append-to-body class="add-customer-dialog">
- <div class="dialog-body">
- <!-- 头像区 -->
- <div class="avatar-section">
- <el-upload action="#" :show-file-list="false" :auto-upload="false" :on-change="handleUploadFile">
- <el-avatar :size="88"
- :src="avatarDisplayUrl || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'"
- class="avatar-preview" />
- <div class="avatar-tip">点击修改头像</div>
- </el-upload>
- </div>
- <el-form :model="form" label-width="90px" label-position="top" class="customer-form">
- <!-- 基本资料 -->
- <div class="section">
- <div class="section-title">基本资料</div>
- <el-row :gutter="24">
- <el-col :span="12" v-if="showBrand">
- <el-form-item label="所属品牌" required>
- <PageSelect v-model="form.tenantId"
- :options="brandList.map(item => ({ value: item.tenantId, label: item.name }))" :total="brandTotal"
- :pageSize="10" placeholder="请选择所属品牌" @page-change="handleBrandPageChange"
- @visible-change="handleBrandVisibleChange" />
- </el-form-item>
- </el-col>
- <el-col :span="showBrand ? 12 : 24">
- <el-form-item label="所属站点" required>
- <el-cascader v-model="formAreaValue" :options="areaTreeOptions" :props="{ value: 'id', label: 'name' }"
- placeholder="请选择站点" style="width: 100%" clearable @change="handleFormAreaChange" />
- </el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="姓名" required><el-input v-model="form.name" placeholder="请输入姓名" /></el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="电话" required><el-input v-model="form.phone" placeholder="请输入电话" /></el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="性别">
- <el-select v-model="form.gender" placeholder="请选择">
- <el-option v-for="dict in sys_user_sex" :key="dict.value" :label="dict.label"
- :value="parseInt(dict.value)" />
- </el-select>
- </el-form-item>
- </el-col>
- </el-row>
- </div>
- <!-- 居住信息 -->
- <div class="section">
- <div class="section-title">居住信息</div>
- <el-row :gutter="24">
- <el-col :span="12">
- <el-form-item label="所在地区">
- <RegionCascader v-model="regionCascaderValue" />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="详细住址" required>
- <el-input v-model="form.address" placeholder="请输入街道/门牌号" />
- </el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="房屋类型">
- <el-select v-model="form.houseType" placeholder="请选择">
- <el-option v-for="dict in sys_house_type" :key="dict.value" :label="dict.label" :value="dict.value" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="入门方式" required>
- <el-select v-model="form.entryMethod" placeholder="请选择">
- <el-option v-for="dict in sys_entry_method" :key="dict.value" :label="dict.label"
- :value="dict.value" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="8" v-if="form.entryMethod === 'password'">
- <el-form-item label="开门密码" required>
- <el-input v-model="form.entryPassword" placeholder="请输入密码" />
- </el-form-item>
- </el-col>
- <el-col :span="8" v-if="form.entryMethod === 'key'">
- <el-form-item label="钥匙位置" required>
- <el-input v-model="form.keyLocation" placeholder="如:地毯下" />
- </el-form-item>
- </el-col>
- </el-row>
- </div>
- <!-- 其他 -->
- <div class="section">
- <div class="section-title">其他</div>
- <el-row :gutter="24">
- <el-col :span="12">
- <el-form-item label="用户标签">
- <el-select v-model="selectedTagIds" multiple placeholder="选择标签" collapse-tags collapse-tags-tooltip>
- <el-option v-for="tag in allUserTags" :key="tag.id" :label="tag.name" :value="tag.id">
- <el-tag :type="tag.colorType || 'info'" effect="light" size="small">{{ tag.name }}</el-tag>
- </el-option>
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="备注说明">
- <el-input type="textarea" v-model="form.remark" :rows="2" placeholder="备注信息" />
- </el-form-item>
- </el-col>
- </el-row>
- </div>
- </el-form>
- </div>
- <template #footer>
- <div class="dialog-footer">
- <el-button @click="$emit('update:visible', false)" size="large">取消</el-button>
- <el-button type="primary" :loading="submitLoading" @click="saveData" size="large">保存</el-button>
- </div>
- </template>
- </el-dialog>
- </template>
- <script setup>
- import { ref, reactive, computed, watch, onMounted, getCurrentInstance, toRefs } from 'vue'
- import { ElMessage } from 'element-plus'
- import { globalHeaders } from '@/utils/request'
- import { addCustomer, updateCustomer, addCustomerOnOrder } from '@/api/archieves/customer'
- import { listAllTag } from '@/api/archieves/tag'
- import { listAreaStation } from '@/api/system/areaStation'
- import { listOnStore as listBrandOnStore } from '@/api/system/tenant'
- import RegionCascader from '@/components/RegionCascader/index.vue'
- import PageSelect from '@/components/PageSelect/index.vue'
- import { useUserStore } from '@/store/modules/user'
- const userStore = useUserStore()
- const props = defineProps({
- visible: { type: Boolean, default: false },
- customerData: { type: Object, default: null },
- showBrand: { type: Boolean, default: false },
- stationId: { type: [String, Number], default: undefined },
- tenantId: { type: [String, Number], default: undefined },
- orderMode: { type: Boolean, default: false }
- })
- const emit = defineEmits(['update:visible', 'success'])
- const { proxy } = getCurrentInstance()
- const { sys_user_sex, sys_house_type, sys_entry_method } = toRefs(
- proxy?.useDict('sys_user_sex', 'sys_house_type', 'sys_entry_method')
- )
- const isEdit = ref(false)
- const dialogTitle = computed(() => {
- if (props.orderMode) return '新增用户'
- return isEdit.value ? '编辑用户' : '新增用户'
- })
- const submitLoading = ref(false)
- const allNodes = ref([])
- const allUserTags = ref([])
- const formAreaValue = ref([])
- const regionCascaderValue = ref([])
- const selectedTagIds = ref([])
- const avatarDisplayUrl = ref('')
- const brandList = ref([])
- const brandTotal = ref(0)
- const baseUrl = import.meta.env.VITE_APP_BASE_API
- const uploadUrl = baseUrl + '/resource/oss/upload'
- function emptyForm() {
- return {
- id: undefined, name: '', phone: '', avatar: undefined, gender: undefined,
- birthday: '', idCard: '', areaId: undefined, stationId: undefined,
- regionCode: '', region: [], address: '',
- houseType: '', entryMethod: '', entryPassword: '', keyLocation: '',
- source: '', emergencyContact: '', emergencyPhone: '',
- memberLevel: 0, status: 0, remark: '', tagIds: [], tenantId: undefined
- }
- }
- const form = reactive(emptyForm())
- watch(() => props.visible, (val) => {
- if (val) {
- resetForm()
- loadTags()
- }
- })
- const resetForm = () => {
- submitLoading.value = false
- selectedTagIds.value = []
- avatarDisplayUrl.value = ''
- formAreaValue.value = []
- regionCascaderValue.value = []
- Object.assign(form, emptyForm())
- if (props.customerData) {
- isEdit.value = true
- const data = props.customerData
- Object.assign(form, {
- id: data.id, name: data.name, phone: data.phone, avatar: data.avatar, gender: data.gender,
- birthday: data.birthday, idCard: data.idCard, areaId: data.areaId, stationId: data.stationId,
- regionCode: data.regionCode, region: data.regionCode ? data.regionCode.split('/') : [],
- address: data.address, houseType: data.houseType, entryMethod: data.entryMethod,
- entryPassword: data.entryPassword, keyLocation: data.keyLocation, source: data.source,
- emergencyContact: data.emergencyContact, emergencyPhone: data.emergencyPhone,
- memberLevel: data.memberLevel, status: data.status, remark: data.remark, tagIds: [],
- tenantId: data.tenantId
- })
- avatarDisplayUrl.value = data.avatarUrl || ''
- if (data.stationId || data.areaId) {
- formAreaValue.value = getStationPath(data.stationId || data.areaId)
- }
- regionCascaderValue.value = data.regionCode ? data.regionCode.split('/') : []
- selectedTagIds.value = data.tags ? data.tags.map(t => t.id) : []
- } else {
- isEdit.value = false
- form.tenantId = props.tenantId || undefined
- if (props.stationId && allNodes.value.length > 0) {
- const path = getStationPath(props.stationId)
- if (path.length > 0) {
- formAreaValue.value = path
- handleFormAreaChange(path)
- }
- }
- }
- }
- const areaTreeOptions = computed(() => {
- const buildTree = (data, parentId) => {
- return data
- .filter(item => String(item.parentId) === String(parentId))
- .map(item => {
- const children = buildTree(data, item.id)
- const node = { id: item.id, name: item.name }
- if (children.length > 0) node.children = children
- return node
- })
- }
- return buildTree(allNodes.value, 0)
- })
- const getStationPath = (id) => {
- if (!id) return []
- const path = []
- let currentId = String(id)
- let maxDepth = 10
- while (currentId && maxDepth-- > 0) {
- const node = allNodes.value.find(n => String(n.id) === currentId)
- if (!node) break
- path.unshift(node.id)
- currentId = node.parentId != null ? String(node.parentId) : null
- }
- return path
- }
- const handleFormAreaChange = (value) => {
- if (value && value.length > 0) {
- const lastId = value[value.length - 1]
- const node = allNodes.value.find(item => item.id === lastId)
- form.stationId = lastId
- form.areaId = node ? node.parentId : undefined
- } else {
- form.stationId = undefined
- form.areaId = undefined
- }
- }
- const loadTags = () => {
- listAllTag({ category: 'customer', status: 0 }).then((res) => {
- allUserTags.value = res.data || []
- }).catch(() => { })
- }
- const loadAreaStation = () => {
- listAreaStation().then((res) => {
- allNodes.value = res.data || []
- })
- }
- const getBrandList = async (pageNum = 1) => {
- const res = await listBrandOnStore({ pageNum, pageSize: 10 })
- if (res.code === 200) {
- brandList.value = res.rows || []
- brandTotal.value = res.total || 0
- }
- }
- const handleBrandPageChange = (page) => {
- getBrandList(Number(page))
- }
- const handleBrandVisibleChange = (visible) => {
- if (visible) {
- getBrandList(1)
- }
- }
- const handleUploadFile = async (file) => {
- const fd = new FormData()
- fd.append('file', file.raw)
- try {
- const headers = globalHeaders()
- const res = await fetch(uploadUrl, {
- method: 'POST',
- headers: { 'Authorization': headers.Authorization, 'clientid': headers.clientid },
- body: fd
- })
- const result = await res.json()
- if (result.code === 200) {
- form.avatar = result.data.ossId
- avatarDisplayUrl.value = result.data.url
- } else {
- ElMessage.error(result.msg || '头像上传失败')
- }
- } catch (e) {
- ElMessage.error('头像上传失败')
- }
- }
- const saveData = () => {
- if (props.showBrand && !form.tenantId) return ElMessage.warning('请选择所属品牌')
- if (!form.name) return ElMessage.warning('请输入姓名')
- if (!form.phone) return ElMessage.warning('请输入电话')
- if (!form.stationId) return ElMessage.warning('请选择所属站点')
- if (!form.address) return ElMessage.warning('请输入详细住址')
- if (!form.entryMethod) return ElMessage.warning('请选择入门方式')
- if (form.entryMethod === 'password' && !form.entryPassword) return ElMessage.warning('请输入开门密码')
- if (form.entryMethod === 'key' && !form.keyLocation) return ElMessage.warning('请输入钥匙存放位置')
- submitLoading.value = true
- form.tagIds = selectedTagIds.value
- if (regionCascaderValue.value && regionCascaderValue.value.length > 0) {
- form.regionCode = regionCascaderValue.value.join('/')
- } else {
- form.regionCode = ''
- }
- let api
- if (props.orderMode) {
- api = addCustomerOnOrder(form)
- } else {
- api = isEdit.value ? updateCustomer(form) : addCustomer(form)
- }
- api.then((res) => {
- ElMessage.success(isEdit.value ? '更新成功' : '保存成功')
- emit('success', res.data)
- emit('update:visible', false)
- }).finally(() => {
- submitLoading.value = false
- })
- }
- onMounted(() => {
- loadAreaStation()
- })
- </script>
- <style scoped>
- .add-customer-dialog :deep(.el-dialog__body) {
- padding: 0 24px 0;
- max-height: 62vh;
- overflow-y: auto;
- }
- .dialog-body {
- padding-top: 8px;
- }
- .avatar-section {
- display: flex;
- justify-content: center;
- margin-bottom: 20px;
- }
- .avatar-preview {
- box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
- border: 3px solid #fff;
- outline: 1px solid #e8e8e8;
- cursor: pointer;
- }
- .avatar-tip {
- margin-top: 8px;
- font-size: 12px;
- color: #409eff;
- text-align: center;
- }
- .customer-form {
- padding-bottom: 10px;
- }
- .section {
- margin-bottom: 8px;
- padding: 16px 20px 4px;
- background: #fafbfc;
- border-radius: 8px;
- border: 1px solid #f0f0f0;
- }
- .section-title {
- font-size: 14px;
- font-weight: 600;
- color: #303133;
- margin-bottom: 14px;
- padding-left: 10px;
- border-left: 3px solid #409eff;
- }
- .dialog-footer {
- display: flex;
- justify-content: flex-end;
- gap: 12px;
- padding-top: 8px;
- }
- </style>
|