ExpressTemplateForm.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. <template>
  2. <Dialog :title="dialogTitle" v-model="dialogVisible" width="80%">
  3. <el-form
  4. ref="formRef"
  5. :model="formData"
  6. :rules="formRules"
  7. label-width="80px"
  8. v-loading="formLoading"
  9. >
  10. <el-form-item label="模板名称" prop="name">
  11. <el-input v-model="formData.name" placeholder="请输入模板名称" />
  12. </el-form-item>
  13. <el-form-item label="计费方式" prop="chargeMode">
  14. <el-radio-group v-model="formData.chargeMode" @change="changeChargeMode">
  15. <el-radio
  16. v-for="dict in getIntDictOptions(DICT_TYPE.EXPRESS_CHARGE_MODE)"
  17. :key="dict.value"
  18. :label="dict.value"
  19. >
  20. {{ dict.label }}
  21. </el-radio>
  22. </el-radio-group>
  23. </el-form-item>
  24. <el-form-item label="运费" prop="templateCharge">
  25. <el-table border style="width: 100%" :data="formData.templateCharge">
  26. <el-table-column align="center" label="区域" width="180">
  27. <template #default="{ row }">
  28. <!-- 区域数据太多,用赖加载方式,要不然性能有问题 -->
  29. <el-tree-select
  30. v-model="row.areaIds"
  31. lazy
  32. :load="loadChargeArea"
  33. :props="defaultProps"
  34. multiple
  35. node-key="id"
  36. check-strictly
  37. show-checkbox
  38. check-on-click-node
  39. :render-after-expand="false"
  40. :cache-data="areaCache"
  41. />
  42. </template>
  43. </el-table-column>
  44. <el-table-column
  45. align="center"
  46. :label="columnTitle.startCountTitle"
  47. width="180"
  48. prop="startCount"
  49. >
  50. <template #default="{ row }">
  51. <el-input-number v-model="row.startCount" :min="1" />
  52. </template>
  53. </el-table-column>
  54. <el-table-column width="180" align="center" label="运费(元)" prop="startPrice">
  55. <template #default="{ row }">
  56. <el-input-number v-model="row.startPrice" :min="1" />
  57. </template>
  58. </el-table-column>
  59. <el-table-column
  60. width="180"
  61. align="center"
  62. :label="columnTitle.extraCountTitle"
  63. prop="extraCount"
  64. >
  65. <template #default="{ row }">
  66. <el-input-number v-model="row.extraCount" :min="1" />
  67. </template>
  68. </el-table-column>
  69. <el-table-column width="180" align="center" label="续费(元)" prop="extraPrice">
  70. <template #default="{ row }">
  71. <el-input-number v-model="row.extraPrice" :min="1" />
  72. </template>
  73. </el-table-column>
  74. <el-table-column label="操作" align="center">
  75. <template #default="scope">
  76. <el-button link type="danger" @click="deleteChargeArea(scope.$index)">
  77. 删除
  78. </el-button>
  79. </template>
  80. </el-table-column>
  81. </el-table>
  82. </el-form-item>
  83. <el-form-item>
  84. <el-button type="primary" plain @click="addChargeArea()">
  85. <Icon icon="ep:plus" class="mr-5px" /> 添加区域
  86. </el-button>
  87. </el-form-item>
  88. <el-form-item label="包邮区域" prop="templateFree">
  89. <el-table border style="width: 100%" :data="formData.templateFree">
  90. <el-table-column align="center" label="区域">
  91. <template #default="{ row }">
  92. <!-- 区域数据太多,用赖加载方式,要不然性能有问题 -->
  93. <el-tree-select
  94. v-model="row.areaIds"
  95. multiple
  96. lazy
  97. :load="loadFreeArea"
  98. :props="defaultProps"
  99. node-key="id"
  100. check-strictly
  101. show-checkbox
  102. check-on-click-node
  103. :render-after-expand="true"
  104. :cache-data="areaCache"
  105. />
  106. </template>
  107. </el-table-column>
  108. <el-table-column align="center" :label="columnTitle.freeCountTitle" prop="freeCount">
  109. <template #default="{ row }">
  110. <el-input-number v-model="row.freeCount" :min="1" />
  111. </template>
  112. </el-table-column>
  113. <el-table-column align="center" label="包邮金额(元)" prop="freePrice">
  114. <template #default="{ row }">
  115. <el-input-number v-model="row.freePrice" :min="1" />
  116. </template>
  117. </el-table-column>
  118. <el-table-column label="操作" align="center">
  119. <template #default="scope">
  120. <el-button link type="danger" @click="deleteFreeArea(scope.$index)"> 删除 </el-button>
  121. </template>
  122. </el-table-column>
  123. </el-table>
  124. </el-form-item>
  125. <el-form-item>
  126. <el-button type="primary" plain @click="addFreeArea()">
  127. <Icon icon="ep:plus" class="mr-5px" /> 添加区域
  128. </el-button>
  129. </el-form-item>
  130. <el-form-item label="排序" prop="sort">
  131. <el-input-number v-model="formData.sort" controls-position="right" :min="0" />
  132. </el-form-item>
  133. </el-form>
  134. <template #footer>
  135. <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
  136. <el-button @click="dialogVisible = false">取 消</el-button>
  137. </template>
  138. </Dialog>
  139. </template>
  140. <script setup lang="ts">
  141. import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
  142. import * as DeliveryExpressTemplateApi from '@/api/mall/trade/delivery/expressTemplate'
  143. import { defaultProps } from '@/utils/tree'
  144. import { yuanToFen, fenToYuan } from '@/utils'
  145. import { getChildrenArea, getAreaListByIds } from '@/api/system/area'
  146. import { cloneDeep } from 'lodash-es'
  147. const { t } = useI18n() // 国际化
  148. const message = useMessage() // 消息弹窗
  149. const dialogVisible = ref(false) // 弹窗的是否展示
  150. const dialogTitle = ref('') // 弹窗的标题
  151. const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
  152. const formType = ref('') // 表单的类型:create - 新增;update - 修改
  153. const formData = ref({
  154. id: undefined,
  155. name: '',
  156. chargeMode: 1,
  157. sort: 0,
  158. templateCharge: [],
  159. templateFree: []
  160. })
  161. const columnTitleMap = new Map()
  162. const columnTitle = ref({
  163. startCountTitle: '首件',
  164. extraCountTitle: '续件',
  165. freeCountTitle: '包邮件数'
  166. })
  167. const formRules = reactive({
  168. name: [{ required: true, message: '模板名称不能为空', trigger: 'blur' }],
  169. chargeMode: [{ required: true, message: '配送计费方式不能为空', trigger: 'blur' }],
  170. sort: [{ required: true, message: '分类排序不能为空', trigger: 'blur' }]
  171. })
  172. const formRef = ref() // 表单 Ref
  173. const areaCache = ref([]) // 由于区域节点懒加载,已选区域节点需要缓存展示
  174. // TODO @jason:配送的时候,只允许选择省市级别,不允许选择区;如果这样的话,是不是打开弹窗,直接把城市都请求过来;
  175. // TODO @jaosn:因为只有省市两级,感觉就不用特殊做全国逻辑;选择全国,就默认把子节点都选择上;另外,选择父节点,要把子节点选中哈;
  176. /** 打开弹窗 */
  177. const open = async (type: string, id?: number) => {
  178. dialogVisible.value = true
  179. dialogTitle.value = t('action.' + type)
  180. formType.value = type
  181. resetForm()
  182. try {
  183. // 修改时,设置数据
  184. if (id) {
  185. formLoading.value = true
  186. formData.value = await DeliveryExpressTemplateApi.getDeliveryExpressTemplate(id)
  187. columnTitle.value = columnTitleMap.get(formData.value.chargeMode)
  188. const chargeAreaIds = []
  189. const freeAreaIds = []
  190. formData.value.templateCharge.forEach((item) => {
  191. for (let i = 0; i < item.areaIds.length; i++) {
  192. if (!chargeAreaIds.includes(item.areaIds[i])) {
  193. chargeAreaIds.push(item.areaIds[i])
  194. }
  195. }
  196. //前端价格以元展示
  197. item.startPrice = fenToYuan(item.startPrice)
  198. item.extraPrice = fenToYuan(item.extraPrice)
  199. })
  200. formData.value.templateFree.forEach((item) => {
  201. for (let i = 0; i < item.areaIds.length; i++) {
  202. if (!chargeAreaIds.includes(item.areaIds[i]) && !freeAreaIds.includes(item.areaIds[i])) {
  203. freeAreaIds.push(item.areaIds[i])
  204. }
  205. }
  206. item.freePrice = fenToYuan(item.freePrice)
  207. })
  208. // 已选的区域节点
  209. const areaIds = chargeAreaIds.concat(freeAreaIds)
  210. // 区域节点,懒加载方式。已选节点需要缓存展示
  211. areaCache.value = await getAreaListByIds(areaIds.join(','))
  212. }
  213. } finally {
  214. formLoading.value = false
  215. }
  216. }
  217. defineExpose({ open }) // 提供 open 方法,用于打开弹窗
  218. /** 提交表单 */
  219. const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
  220. const submitForm = async () => {
  221. // 校验表单
  222. if (!formRef) return
  223. const valid = await formRef.value.validate()
  224. if (!valid) return
  225. // 提交请求
  226. formLoading.value = true
  227. try {
  228. const data = formData.value as DeliveryExpressTemplateApi.DeliveryExpressTemplateVO
  229. // 前端价格以元展示,提交到后端。用分计算
  230. // TODO @jason:不能直接这样改,要复制出来改。不然后端操作失败,数据已经被改了
  231. data.templateCharge.forEach((item) => {
  232. item.startPrice = yuanToFen(item.startPrice)
  233. item.extraPrice = yuanToFen(item.extraPrice)
  234. })
  235. data.templateFree.forEach((item) => {
  236. item.freePrice = yuanToFen(item.freePrice)
  237. })
  238. if (formType.value === 'create') {
  239. await DeliveryExpressTemplateApi.createDeliveryExpressTemplate(data)
  240. message.success(t('common.createSuccess'))
  241. } else {
  242. await DeliveryExpressTemplateApi.updateDeliveryExpressTemplate(data)
  243. message.success(t('common.updateSuccess'))
  244. }
  245. dialogVisible.value = false
  246. // 发送操作成功的事件
  247. emit('success')
  248. } finally {
  249. formLoading.value = false
  250. }
  251. }
  252. /** 重置表单 */
  253. const resetForm = () => {
  254. formData.value = {
  255. id: undefined,
  256. name: '',
  257. chargeMode: 1,
  258. templateCharge: [
  259. {
  260. areaIds: [1],
  261. startCount: 2,
  262. startPrice: 5,
  263. extraCount: 5,
  264. extraPrice: 10
  265. }
  266. ],
  267. templateFree: [],
  268. sort: 0
  269. }
  270. columnTitle.value = columnTitleMap.get(1)
  271. formRef.value?.resetFields()
  272. }
  273. /** 配送计费方法改变 */
  274. const changeChargeMode = (chargeMode: number) => {
  275. columnTitle.value = columnTitleMap.get(chargeMode)
  276. }
  277. const defaultArea = [{ id: 1, name: '全国', disabled: false }]
  278. /** 初始化数据 */
  279. // TODO @jason:是不是不用写这样一个初始化方法,columnTitleMap 直接就可以了呀
  280. // const columnTitleMap = {
  281. // '1': {
  282. // startCountTitle: '首件',
  283. // extraCountTitle: '续件',
  284. // freeCountTitle: '包邮件数'
  285. // },
  286. // '2': {
  287. // startCountTitle: '首件重量(kg)',
  288. // extraCountTitle: '续件重量(kg)',
  289. // freeCountTitle: '包邮重量(kg)'
  290. // },
  291. // '3': {
  292. // startCountTitle: '首件体积(m³)',
  293. // extraCountTitle: '续件体积(m³)',
  294. // freeCountTitle: '包邮体积(m³)'
  295. // }
  296. // }
  297. const initData = async () => {
  298. // TODO 从服务端全量加载数据, 后面看懒加载是不是可以从前端获取数据。 目前从后端获取数据
  299. // formLoading.value = true
  300. // try {
  301. // const data = await getAreaTree()
  302. // areaTree = data
  303. // console.log('areaTree', areaTree)
  304. // } finally {
  305. // formLoading.value = false
  306. // }
  307. // 表头标题和计费方式的映射
  308. columnTitleMap.set(1, {
  309. startCountTitle: '首件',
  310. extraCountTitle: '续件',
  311. freeCountTitle: '包邮件数'
  312. })
  313. columnTitleMap.set(2, {
  314. startCountTitle: '首件重量(kg)',
  315. extraCountTitle: '续件重量(kg)',
  316. freeCountTitle: '包邮重量(kg)'
  317. })
  318. columnTitleMap.set(3, {
  319. startCountTitle: '首件体积(m³)',
  320. extraCountTitle: '续件体积(m³)',
  321. freeCountTitle: '包邮体积(m³)'
  322. })
  323. }
  324. /** 懒加载运费区域树 */
  325. const loadChargeArea = async (node, resolve) => {
  326. //已选区域需要禁止再次选择
  327. const areaIds = []
  328. formData.value.templateCharge.forEach((item) => {
  329. if (item.areaIds.length > 0) {
  330. item.areaIds.forEach((areaId) => areaIds.push(areaId))
  331. }
  332. })
  333. if (node.isLeaf) return resolve([])
  334. const length = node.data.length
  335. if (length === 0) {
  336. const data = cloneDeep(defaultArea)
  337. const item = data[0]
  338. if (areaIds.includes(item.id)) {
  339. // TODO 禁止选中的区域有些问题, 导致修改时候不能重新选择 不知道如何处理。 暂时注释掉 @芋艿 有空瞅瞅
  340. // TODO @jason:先不做这个功能哈。
  341. //item.disabled = true
  342. }
  343. resolve(data)
  344. } else {
  345. const id = node.data.id
  346. const data = await getChildrenArea(id)
  347. data.forEach((item) => {
  348. if (areaIds.includes(item.id)) {
  349. //item.disabled = true
  350. }
  351. })
  352. resolve(data)
  353. }
  354. }
  355. /** 懒加载包邮区域树 */
  356. const loadFreeArea = async (node, resolve) => {
  357. if (node.isLeaf) return resolve([])
  358. //已选区域需要禁止再次选择
  359. const areaIds = []
  360. formData.value.templateFree.forEach((item) => {
  361. if (item.areaIds.length > 0) {
  362. item.areaIds.forEach((areaId) => areaIds.push(areaId))
  363. }
  364. })
  365. const length = node.data.length
  366. if (length === 0) {
  367. // 为空,从全国开始选择。全国 id == 1
  368. const data = cloneDeep(defaultArea)
  369. const item = data[0]
  370. if (areaIds.includes(item.id)) {
  371. //item.disabled = true
  372. }
  373. resolve(data)
  374. } else {
  375. const id = node.data.id
  376. const data = await getChildrenArea(id)
  377. // 已选区域需要禁止再次选择
  378. data.forEach((item) => {
  379. if (areaIds.includes(item.id)) {
  380. // TODO 禁止选中的区域有些问题, 导致修改时候不能重新选择 不知道如何处理。 暂时注释掉 @芋艿 有空瞅瞅
  381. // TODO @jason:先不做这个功能哈。
  382. //item.disabled = true
  383. }
  384. })
  385. resolve(data)
  386. }
  387. }
  388. /** 添加计费区域 */
  389. const addChargeArea = () => {
  390. const data = formData.value
  391. data.templateCharge.push({
  392. areaIds: [],
  393. startCount: 1,
  394. startPrice: 1,
  395. extraCount: 1,
  396. extraPrice: 1
  397. })
  398. }
  399. /** 删除计费区域 */
  400. const deleteChargeArea = (index) => {
  401. const data = formData.value
  402. data.templateCharge.splice(index, 1)
  403. }
  404. /** 添加包邮区域 */
  405. const addFreeArea = () => {
  406. const data = formData.value
  407. data.templateFree.push({
  408. areaIds: [],
  409. freeCount: 1,
  410. freePrice: 1
  411. })
  412. }
  413. /** 删除包邮区域 */
  414. const deleteFreeArea = (index) => {
  415. const data = formData.value
  416. data.templateFree.splice(index, 1)
  417. }
  418. /** 初始化 **/
  419. onMounted(() => {
  420. initData()
  421. })
  422. </script>