AddUserDialog.vue 11 KB

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