DeliveryExpressTemplateForm.vue 13 KB

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