AddUserDialog.vue 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. <template>
  2. <el-dialog :model-value="visible" @update:model-value="$emit('update:visible', $event)" title="新增用户" width="700px" destroy-on-close append-to-body>
  3. <el-form :model="form" label-width="90px" class="user-form">
  4. <el-row :gutter="20">
  5. <el-col :span="24" style="text-align: center; margin-bottom: 25px;">
  6. <el-upload action="#" :show-file-list="false" :auto-upload="false" :on-change="handleUserUploadFile">
  7. <el-avatar :size="80" :src="userAvatarDisplayUrl || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'" class="upload-avatar" />
  8. <div style="margin-top: 8px; font-size: 12px; color: #409EFF;">点击修改头像</div>
  9. </el-upload>
  10. </el-col>
  11. <el-col :span="24"><div class="form-section-header">基本资料</div></el-col>
  12. <!-- 录入来源不在表单中展示,自动使用当前登录用户的 tenantId,提交时静默传递 -->
  13. <el-col :span="24">
  14. <el-form-item label="所属站点">
  15. <el-cascader v-model="formAreaValue" :options="areaTreeOptions" placeholder="请选择站点"
  16. :props="{ checkStrictly: true, value: 'value', label: 'label' }"
  17. style="width: 100%" clearable @change="handleFormAreaChange" />
  18. </el-form-item>
  19. </el-col>
  20. <el-col :span="12">
  21. <el-form-item label="姓名" required><el-input v-model="form.name" placeholder="请输入姓名" /></el-form-item>
  22. </el-col>
  23. <el-col :span="12">
  24. <el-form-item label="电话" required><el-input v-model="form.phone" placeholder="请输入电话" /></el-form-item>
  25. </el-col>
  26. <el-col :span="12">
  27. <el-form-item label="性别">
  28. <el-select v-model="form.gender" placeholder="请选择">
  29. <el-option v-for="dict in sys_user_sex" :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
  30. </el-select>
  31. </el-form-item>
  32. </el-col>
  33. <el-col :span="24"><div class="form-section-header">居住信息</div></el-col>
  34. <el-col :span="24">
  35. <el-form-item label="所在地区">
  36. <el-cascader
  37. v-model="regionCascaderValue"
  38. :options="regionData"
  39. placeholder="请选择省/市/区"
  40. style="width: 100%"
  41. clearable
  42. />
  43. </el-form-item>
  44. </el-col>
  45. <el-col :span="24">
  46. <el-form-item label="详细住址"><el-input v-model="form.address" placeholder="请输入街道/门牌号" /></el-form-item>
  47. </el-col>
  48. <el-col :span="12">
  49. <el-form-item label="房屋类型">
  50. <el-radio-group v-model="form.houseType">
  51. <el-radio v-for="dict in sys_house_type" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
  52. </el-radio-group>
  53. </el-form-item>
  54. </el-col>
  55. <el-col :span="12">
  56. <el-form-item label="入门方式">
  57. <el-radio-group v-model="form.entryMethod">
  58. <el-radio v-for="dict in sys_entry_method" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
  59. </el-radio-group>
  60. </el-form-item>
  61. </el-col>
  62. <el-col :span="12" v-if="form.entryMethod === 'password'">
  63. <el-form-item label="开门密码">
  64. <el-input v-model="form.entryPassword" placeholder="请输入密码" />
  65. </el-form-item>
  66. </el-col>
  67. <el-col :span="12" v-if="form.entryMethod === 'key'">
  68. <el-form-item label="钥匙位置">
  69. <el-input v-model="form.keyLocation" placeholder="如:地毯下" />
  70. </el-form-item>
  71. </el-col>
  72. <el-col :span="24"><div class="form-section-header">其他</div></el-col>
  73. <el-col :span="24">
  74. <el-form-item label="用户标签">
  75. <el-select v-model="selectedTagIds" multiple placeholder="选择标签" style="width: 100%">
  76. <el-option v-for="tag in allUserTags" :key="tag.id" :label="tag.name" :value="tag.id">
  77. <el-tag :type="tag.colorType || 'info'" effect="light" size="small">{{ tag.name }}</el-tag>
  78. </el-option>
  79. </el-select>
  80. </el-form-item>
  81. </el-col>
  82. <el-col :span="24">
  83. <el-form-item label="备注说明"><el-input type="textarea" v-model="form.remark" rows="3" /></el-form-item>
  84. </el-col>
  85. </el-row>
  86. </el-form>
  87. <template #footer>
  88. <div style="text-align: center; margin-top: 20px;">
  89. <el-button @click="$emit('update:visible', false)" size="large" style="width: 120px;">取消</el-button>
  90. <el-button type="primary" :loading="submitLoading" @click="saveUser" size="large" style="width: 120px;">保存</el-button>
  91. </div>
  92. </template>
  93. </el-dialog>
  94. </template>
  95. <script setup>
  96. import { ref, reactive, computed, onMounted, watch, getCurrentInstance, toRefs } from 'vue'
  97. import { ElMessage } from 'element-plus'
  98. import { globalHeaders } from '@/utils/request'
  99. import { addCustomerOnOrder } from '@/api/archieves/customer'
  100. import { listAllTag } from '@/api/archieves/tag'
  101. import { listAreaStation } from '@/api/system/areaStation'
  102. import { regionData } from 'element-china-area-data'
  103. import PageSelect from '@/components/PageSelect/index.vue'
  104. import { useUserStore } from '@/store/modules/user'
  105. const props = defineProps({
  106. visible: { type: Boolean, default: false }
  107. })
  108. const emit = defineEmits(['update:visible', 'success'])
  109. const { proxy } = getCurrentInstance()
  110. const { sys_user_sex, sys_house_type, sys_entry_method } = toRefs(
  111. proxy?.useDict('sys_user_sex', 'sys_house_type', 'sys_entry_method')
  112. )
  113. const submitLoading = ref(false)
  114. // 获取当前登录用户的 tenantId
  115. const userStore = useUserStore()
  116. const allNodes = ref([])
  117. const allUserTags = ref([])
  118. const formAreaValue = ref([])
  119. const regionCascaderValue = ref([])
  120. const selectedTagIds = ref([])
  121. const userAvatarDisplayUrl = ref('')
  122. const baseUrl = import.meta.env.VITE_APP_BASE_API
  123. const uploadUrl = baseUrl + '/resource/oss/upload'
  124. const form = reactive({
  125. name: '',
  126. phone: '',
  127. avatar: undefined,
  128. gender: undefined,
  129. birthday: '',
  130. idCard: '',
  131. areaId: undefined,
  132. stationId: undefined,
  133. regionCode: '',
  134. region: [],
  135. address: '',
  136. houseType: '',
  137. entryMethod: '',
  138. entryPassword: '',
  139. keyLocation: '',
  140. // 录入来源默认为当前登录用户的 tenantId
  141. tenantId: userStore.tenantId,
  142. emergencyContact: '',
  143. emergencyPhone: '',
  144. memberLevel: 0,
  145. status: 0,
  146. remark: '',
  147. tagIds: []
  148. })
  149. watch(() => props.visible, (val) => {
  150. if (val) {
  151. resetForm()
  152. }
  153. })
  154. const resetForm = () => {
  155. submitLoading.value = false
  156. selectedTagIds.value = []
  157. userAvatarDisplayUrl.value = ''
  158. formAreaValue.value = []
  159. regionCascaderValue.value = []
  160. Object.assign(form, {
  161. name: '', phone: '', avatar: undefined, gender: undefined, birthday: '', idCard: '',
  162. areaId: undefined, stationId: undefined, regionCode: '', region: [], address: '',
  163. houseType: '', entryMethod: '', entryPassword: '', keyLocation: '',
  164. // 重置时依然保留当前 tenantId 作为录入来源
  165. tenantId: userStore.tenantId,
  166. emergencyContact: '', emergencyPhone: '', memberLevel: 0, status: 0, remark: '', tagIds: []
  167. })
  168. }
  169. const areaTreeOptions = computed(() => {
  170. const buildTree = (data, parentId) => {
  171. return data
  172. .filter(item => String(item.parentId) === String(parentId))
  173. .map(item => {
  174. const children = buildTree(data, item.id)
  175. const node = { value: item.id, label: item.name }
  176. if (children.length > 0) node.children = children
  177. return node
  178. })
  179. }
  180. const areaData = allNodes.value
  181. return buildTree(areaData, 0)
  182. })
  183. const handleFormAreaChange = (value) => {
  184. if (value && value.length > 0) {
  185. const lastId = value[value.length - 1]
  186. const node = allNodes.value.find(n => String(n.id) === String(lastId))
  187. if (node) {
  188. if (String(node.type) === '2') {
  189. form.stationId = lastId
  190. form.areaId = node.parentId
  191. } else {
  192. form.areaId = lastId
  193. form.stationId = undefined
  194. }
  195. }
  196. } else {
  197. form.areaId = undefined
  198. form.stationId = undefined
  199. }
  200. }
  201. // 品牌列表相关逻辑已移除,录入来源改为自动使用当前用户 tenantId
  202. const loadTags = () => {
  203. listAllTag({ category: 'customer', status: 0 }).then((res) => {
  204. allUserTags.value = res.data || []
  205. })
  206. }
  207. const loadAreaStation = () => {
  208. listAreaStation().then((res) => {
  209. allNodes.value = res.data || []
  210. })
  211. }
  212. const handleUserUploadFile = async (file) => {
  213. const formData = new FormData()
  214. formData.append('file', file.raw)
  215. try {
  216. const headers = globalHeaders()
  217. const res = await fetch(uploadUrl, {
  218. method: 'POST',
  219. headers: {
  220. 'Authorization': headers.Authorization,
  221. 'clientid': headers.clientid
  222. },
  223. body: formData
  224. })
  225. const result = await res.json()
  226. if (result.code === 200) {
  227. form.avatar = result.data.ossId
  228. userAvatarDisplayUrl.value = result.data.url
  229. } else {
  230. ElMessage.error(result.msg || '头像上传失败')
  231. }
  232. } catch (e) {
  233. ElMessage.error('头像上传失败')
  234. }
  235. }
  236. const saveUser = () => {
  237. if (!form.name) return ElMessage.warning('请输入姓名')
  238. if (!form.phone) return ElMessage.warning('请输入电话')
  239. submitLoading.value = true
  240. form.tagIds = selectedTagIds.value
  241. if (regionCascaderValue.value && regionCascaderValue.value.length > 0) {
  242. form.regionCode = regionCascaderValue.value.join('/')
  243. } else {
  244. form.regionCode = ''
  245. }
  246. addCustomerOnOrder(form).then(res => {
  247. emit('success', res.data)
  248. emit('update:visible', false)
  249. }).finally(() => {
  250. submitLoading.value = false
  251. })
  252. }
  253. onMounted(() => {
  254. loadAreaStation()
  255. loadTags()
  256. })
  257. </script>
  258. <style scoped>
  259. .form-section-header { font-weight: bold; margin-bottom: 20px; font-size: 15px; color: #303133; }
  260. .upload-avatar { cursor: pointer; border: 2px solid #e4e7ed; transition: border-color 0.2s; border-radius: 50%; }
  261. .upload-avatar:hover { border-color: #409EFF; }
  262. </style>