ShippingTemplateForm.vue 53 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655
  1. <template>
  2. <div class="shipping-template-component">
  3. <el-card class="box-card">
  4. <!-- 头部 -->
  5. <template #header>
  6. <div class="card-header">
  7. <el-icon><Van /></el-icon>
  8. <span>{{ isEditMode ? '编辑运费模板' : '新建运费模板' }}</span>
  9. </div>
  10. </template>
  11. <el-form label-width="120px">
  12. <!-- 模板信息 -->
  13. <el-card shadow="never" class="mb-4">
  14. <template #header>
  15. <div class="card-sub-header">
  16. <span>模板信息</span>
  17. </div>
  18. </template>
  19. <el-row :gutter="20">
  20. <el-col :span="12">
  21. <el-form-item :rules="[{ required: true, message: '请输入模板名称', trigger: 'blur' }]" label="模板名称"
  22. prop="templateInfo.name">
  23. <el-input
  24. v-model="templateInfo.name"
  25. placeholder="例如:全国配送模板"
  26. />
  27. <div class="form-item-tip">给模板起一个容易识别的名称</div>
  28. </el-form-item>
  29. </el-col>
  30. <el-col :span="12">
  31. <el-form-item :rules="[{ required: true, message: '请选择计费方式', trigger: 'change' }]" label="计费方式"
  32. prop="templateInfo.calcMethod">
  33. <el-select
  34. v-model="templateInfo.calcMethod"
  35. @change="handleCalcMethodChange"
  36. style="width: 100%"
  37. placeholder="请选择计费方式"
  38. >
  39. <el-option label="按件数计算(标准化小商品如服饰)" value="piece" />
  40. <el-option label="按重量计算(重货如五金件)" value="weight" />
  41. <el-option label="按体积计算(轻抛货如泡沫箱、家具)" value="volume" />
  42. </el-select>
  43. <div class="form-item-tip">根据商品特性选择合适的计费方式</div>
  44. </el-form-item>
  45. </el-col>
  46. </el-row>
  47. <!-- 单位选择 -->
  48. <el-row :gutter="20" v-if="templateInfo.calcMethod !== 'piece'">
  49. <el-col :span="12">
  50. <el-form-item :rules="[{ required: true, message: '请选择计量单位', trigger: 'change' }]" label="计量单位"
  51. prop="templateInfo.unit">
  52. <el-select
  53. v-model="templateInfo.unit"
  54. @change="handleUnitChange"
  55. style="width: 100%"
  56. placeholder="请选择计量单位"
  57. >
  58. <el-option
  59. v-for="unit in getAvailableUnits()"
  60. :key="unit.value"
  61. :label="unit.label"
  62. :value="unit.value"
  63. />
  64. </el-select>
  65. <div class="form-item-tip">选择合适的计量单位便于计算运费</div>
  66. </el-form-item>
  67. </el-col>
  68. </el-row>
  69. </el-card>
  70. <!-- 默认区设置 -->
  71. <el-card shadow="never" class="mb-4">
  72. <template #header>
  73. <div class="flex justify-between items-center">
  74. <span>默认计费规则</span>
  75. <el-tag type="info">适用于未指定地区</el-tag>
  76. </div>
  77. </template>
  78. <el-alert
  79. class="mb-4"
  80. description="这是适用于全国大部分地区的运费计算规则,针对特定区域可以在下方设置特殊区域规则"
  81. show-icon
  82. title="默认计费规则说明"
  83. type="info"
  84. />
  85. <el-row :gutter="20">
  86. <el-col :span="8">
  87. <el-form-item :label="getFirstLabel()" :rules="[{ required: true, type: 'number', min: 0.001, message: getFirstLabel() + '必须大于0', trigger: 'blur' }]"
  88. prop="defaultRule.first">
  89. <div class="input-with-unit">
  90. <el-input-number
  91. v-model="defaultRule.first"
  92. :min="getMinFirstValue()"
  93. :step="getFirstStep()"
  94. controls-position="right"
  95. style="width: 100%"
  96. />
  97. <span class="unit-text">
  98. <span v-if="getCurrentDisplayUnit() === 'cm3'">
  99. cm<sup>3</sup>
  100. </span>
  101. <span v-else-if="getCurrentDisplayUnit() === 'm3'">
  102. m<sup>3</sup>
  103. </span>
  104. <span v-else>{{ getCurrentDisplayUnit() }}</span>
  105. </span>
  106. </div>
  107. </el-form-item>
  108. </el-col>
  109. <el-col :span="8">
  110. <el-form-item :label="getFirstPriceLabel()" :rules="[{ required: true, type: 'number', min: 0, message: getFirstPriceLabel() + '不能为负数', trigger: 'blur' }]"
  111. prop="defaultRule.firstPrice">
  112. <el-input-number
  113. v-model="defaultRule.firstPrice"
  114. :min="0"
  115. :step="0.01"
  116. controls-position="right"
  117. style="width: 100%"
  118. />
  119. <div class="form-item-tip">首{{ getCurrentDisplayUnit() }}的费用</div>
  120. </el-form-item>
  121. </el-col>
  122. <el-col :span="8">
  123. <el-form-item :label="getNextPriceLabel()" :rules="[{ required: true, type: 'number', min: 0, message: getNextPriceLabel() + '不能为负数', trigger: 'blur' }]"
  124. prop="defaultRule.nextPrice">
  125. <el-input-number
  126. v-model="defaultRule.nextPrice"
  127. :min="0"
  128. :step="0.01"
  129. controls-position="right"
  130. style="width: 100%"
  131. />
  132. <div class="form-item-tip">超出首{{ getCurrentDisplayUnit() }}后每增加一个单位的费用</div>
  133. </el-form-item>
  134. </el-col>
  135. </el-row>
  136. </el-card>
  137. <!-- 特殊区设置 -->
  138. <el-card shadow="never" class="mb-4">
  139. <template #header>
  140. <div class="flex justify-between items-center">
  141. <span>特殊区域设置</span>
  142. <el-button type="primary" @click="addSpecialArea" plain>
  143. <el-icon><Plus /></el-icon>添加特殊区
  144. </el-button>
  145. </div>
  146. </template>
  147. <el-alert
  148. class="mb-4"
  149. description="可以为某些偏远或特殊地区设置独立的运费规则,这些规则会覆盖默认规则"
  150. show-icon
  151. title="特殊区域设置说明"
  152. type="info"
  153. />
  154. <!-- 特殊区列表 -->
  155. <div
  156. v-for="(area, index) in specialAreas"
  157. :key="index"
  158. class="special-area mb-4 p-4 border rounded"
  159. >
  160. <el-button
  161. @click="removeSpecialArea(index)"
  162. type="danger"
  163. :icon="Close"
  164. circle
  165. size="small"
  166. class="float-right"
  167. />
  168. <el-row :gutter="20" class="mb-4">
  169. <el-col :span="12">
  170. <el-form-item :prop="'specialAreas.' + index + '.regions'" :rules="{ type: 'array', required: true, message: '请选择至少一个地区', trigger: 'change' }"
  171. label="选择地区">
  172. <div class="region-tags mb-2">
  173. <el-tag
  174. v-for="(region, regionIndex) in area.regions"
  175. :key="regionIndex"
  176. closable
  177. type="primary"
  178. class="mr-2 mb-2"
  179. @close="removeRegion(index, regionIndex)"
  180. >
  181. {{ formatRegionName(region) }}
  182. </el-tag>
  183. </div>
  184. <el-button @click="openRegionModal(index)" type="primary" link>
  185. <el-icon><Plus /></el-icon>添加地区
  186. </el-button>
  187. <div class="form-item-tip">点击添加地区,可选择省份、城市或区县</div>
  188. </el-form-item>
  189. </el-col>
  190. <el-col :span="12">
  191. <el-form-item :prop="'specialAreas.' + index + '.area_name'"
  192. :rules="{ required: true, message: '请输入区域名称', trigger: 'blur' }"
  193. label="区域名称">
  194. <el-input
  195. v-model="area.area_name"
  196. placeholder="例如:偏远地区"
  197. />
  198. <div class="form-item-tip">给这个特殊区域起一个名字,便于识别</div>
  199. </el-form-item>
  200. </el-col>
  201. </el-row>
  202. <!-- 特殊区域价格设置 -->
  203. <el-row :gutter="20">
  204. <el-col :span="8">
  205. <el-form-item :label="getFirstLabel()" :prop="'specialAreas.' + index + '.first'"
  206. :rules="{ required: true, type: 'number', min: 0.001, message: getFirstLabel() + '必须大于0', trigger: 'blur' }">
  207. <div class="input-with-unit">
  208. <el-input-number
  209. v-model="area.first"
  210. :min="getMinFirstValue()"
  211. :step="getFirstStep()"
  212. controls-position="right"
  213. style="width: 100%"
  214. />
  215. <span class="unit-text">
  216. <span v-if="getCurrentDisplayUnit() === 'cm3'">
  217. cm<sup>3</sup>
  218. </span>
  219. <span v-else-if="getCurrentDisplayUnit() === 'm3'">
  220. m<sup>3</sup>
  221. </span>
  222. <span v-else>{{ getCurrentDisplayUnit() }}</span>
  223. </span>
  224. </div>
  225. </el-form-item>
  226. </el-col>
  227. <el-col :span="8">
  228. <el-form-item :label="getFirstPriceLabel()" :prop="'specialAreas.' + index + '.firstPrice'"
  229. :rules="{ required: true, type: 'number', min: 0, message: getFirstPriceLabel() + '不能为负数', trigger: 'blur' }">
  230. <el-input-number
  231. v-model="area.firstPrice"
  232. :min="0"
  233. :step="0.01"
  234. controls-position="right"
  235. style="width: 100%"
  236. />
  237. <div class="form-item-tip">首{{ getCurrentDisplayUnit() }}的费用</div>
  238. </el-form-item>
  239. </el-col>
  240. <el-col :span="8">
  241. <el-form-item :label="getNextPriceLabel()" :prop="'specialAreas.' + index + '.nextPrice'"
  242. :rules="{ required: true, type: 'number', min: 0, message: getNextPriceLabel() + '不能为负数', trigger: 'blur' }">
  243. <el-input-number
  244. v-model="area.nextPrice"
  245. :min="0"
  246. :step="0.01"
  247. controls-position="right"
  248. style="width: 100%"
  249. />
  250. <div class="form-item-tip">超出首{{ getCurrentDisplayUnit() }}后每增加一个单位的费用</div>
  251. </el-form-item>
  252. </el-col>
  253. </el-row>
  254. </div>
  255. <el-empty v-if="specialAreas.length === 0" description="暂无特殊区域,请点击上方按钮添加"/>
  256. </el-card>
  257. <!-- 操作按钮 -->
  258. <div class="flex justify-end">
  259. <el-button @click="cancel">取消</el-button>
  260. <el-button :loading="props.loading" type="primary" @click="saveTemplate">
  261. <el-icon><DocumentAdd /></el-icon>{{ isEditMode ? '保存模板' : '创建模板' }}
  262. </el-button>
  263. </div>
  264. </el-form>
  265. </el-card>
  266. <!-- 地区选择对话框 -->
  267. <el-dialog
  268. v-model="showRegionModal"
  269. :before-close="closeRegionModal"
  270. title="选择地区"
  271. width="800px"
  272. >
  273. <el-alert
  274. class="mb-4"
  275. description="可以选择省份、城市或区县级别,系统会自动处理层级关系"
  276. show-icon
  277. title="地区选择说明"
  278. type="info"
  279. />
  280. <el-row :gutter="20">
  281. <el-col :span="6">
  282. <div class="region-selector-container">
  283. <div class="selector-header">
  284. <span>省份</span>
  285. <div>
  286. <el-button
  287. :disabled="!provinceList.length"
  288. link
  289. type="primary"
  290. @click="addAllProvinces"
  291. >
  292. 全部
  293. </el-button>
  294. <el-button
  295. :disabled="!selectedProvinces.length"
  296. link
  297. type="success"
  298. @click="addCurrentProvincesToSelected"
  299. >
  300. 添加
  301. </el-button>
  302. </div>
  303. </div>
  304. <el-scrollbar height="300px">
  305. <div
  306. v-for="province in provinceList"
  307. :key="province.code"
  308. :class="['region-item', { active: isProvinceSelected(province.code) || selectedProvinceCode === province.code }]"
  309. @click="selectProvince(province)"
  310. >
  311. <el-checkbox
  312. :model-value="isProvinceSelected(province.code)"
  313. @change="toggleProvinceSelection(province)"
  314. @click.stop
  315. />
  316. {{ province.name }}
  317. </div>
  318. </el-scrollbar>
  319. </div>
  320. </el-col>
  321. <el-col :span="6">
  322. <div class="region-selector-container">
  323. <div class="selector-header">
  324. <span>城市</span>
  325. <div>
  326. <el-button
  327. v-if="selectedProvince"
  328. :disabled="!cityList.length"
  329. link
  330. type="primary"
  331. @click="addAllCities"
  332. >
  333. 全部
  334. </el-button>
  335. <el-button
  336. :disabled="!selectedCities.length"
  337. link
  338. type="success"
  339. @click="addCurrentCitiesToSelected"
  340. >
  341. 添加
  342. </el-button>
  343. </div>
  344. </div>
  345. <el-scrollbar height="300px">
  346. <div
  347. v-for="city in cityList"
  348. :key="city.code"
  349. :class="['region-item', { active: isCitySelected(city.code) || selectedCityCode === city.code }]"
  350. @click="selectCity(city)"
  351. >
  352. <el-checkbox
  353. :disabled="isProvinceOfCitySelected(city)"
  354. :model-value="isCitySelected(city.code)"
  355. @change="toggleCitySelection(city)"
  356. @click.stop
  357. />
  358. {{ city.name }}
  359. </div>
  360. </el-scrollbar>
  361. </div>
  362. </el-col>
  363. <el-col :span="6">
  364. <div class="region-selector-container">
  365. <div class="selector-header">
  366. <span>区县</span>
  367. <div>
  368. <el-button
  369. v-if="selectedCity"
  370. :disabled="!areaList.length"
  371. link
  372. type="primary"
  373. @click="addAllAreas"
  374. >
  375. 全部
  376. </el-button>
  377. <el-button
  378. :disabled="!selectedAreas.length"
  379. link
  380. type="success"
  381. @click="addCurrentAreasToSelected"
  382. >
  383. 添加
  384. </el-button>
  385. </div>
  386. </div>
  387. <el-scrollbar height="300px">
  388. <div
  389. v-for="area in areaList"
  390. :key="area.code"
  391. :class="['region-item', { active: isAreaSelected(area.code) }]"
  392. @click="toggleAreaSelection(area)"
  393. >
  394. <el-checkbox
  395. :disabled="isHigherLevelRegionSelected(area)"
  396. :model-value="isAreaSelected(area.code)"
  397. @change="toggleAreaSelection(area)"
  398. @click.stop
  399. />
  400. {{ area.name }}
  401. </div>
  402. </el-scrollbar>
  403. </div>
  404. </el-col>
  405. <el-col :span="6">
  406. <div class="region-selector-container">
  407. <div class="selector-header">
  408. <span>已选择</span>
  409. <el-button
  410. :disabled="!selectedRegions.length"
  411. link
  412. type="danger"
  413. @click="clearSelectedRegions"
  414. >
  415. 清空
  416. </el-button>
  417. </div>
  418. <el-scrollbar height="300px">
  419. <div
  420. v-for="(region, index) in selectedRegions"
  421. :key="index"
  422. class="selected-region-item"
  423. >
  424. <span>{{ formatRegionName(region) }}</span>
  425. <el-icon @click.stop="removeSelectedRegion(index)">
  426. <Close/>
  427. </el-icon>
  428. </div>
  429. <div v-if="!selectedRegions.length" class="no-data">
  430. 暂无选择
  431. </div>
  432. </el-scrollbar>
  433. </div>
  434. </el-col>
  435. </el-row>
  436. <template #footer>
  437. <div class="dialog-footer">
  438. <el-button @click="closeRegionModal">取消</el-button>
  439. <el-button type="primary" @click="confirmRegions">确认选择</el-button>
  440. </div>
  441. </template>
  442. </el-dialog>
  443. </div>
  444. </template>
  445. <script setup lang="ts">
  446. import {computed, nextTick, onMounted, reactive, ref, watch} from 'vue'
  447. import {ElMessage, ElMessageBox} from 'element-plus'
  448. import {Close, DocumentAdd, Plus, Van} from '@element-plus/icons-vue'
  449. import {getAreas, getCities, getProvinces} from '../api/index.js'
  450. // 定义组件props
  451. const props = defineProps({
  452. // 模板数据,用于编辑模式
  453. templateData: {
  454. type: Object,
  455. default: null
  456. },
  457. // 加载状态
  458. loading: {
  459. type: Boolean,
  460. default: false
  461. }
  462. })
  463. // 定义组件事件
  464. const emit = defineEmits(['save', 'cancel'])
  465. // 判断是否为编辑模式
  466. const isEditMode = computed(() => {
  467. return props.templateData && props.templateData.templateInfo && props.templateData.templateInfo.id
  468. })
  469. // 省份数据
  470. const provinceData = ref<any[]>([])
  471. // 模板信息
  472. const templateInfo = reactive({
  473. id: null,
  474. name: '',
  475. calcMethod: 'piece', // piece, weight, volume
  476. unit: 'kg' // 默认单位为 kg
  477. })
  478. // 默认规则
  479. const defaultRule = reactive({
  480. first: 1,
  481. firstPrice: 10,
  482. nextPrice: 5
  483. })
  484. // 特殊区域列表
  485. const specialAreas = ref<any[]>([])
  486. // 地区选择模态框相关
  487. const showRegionModal = ref(false)
  488. const currentAreaIndex = ref(-1)
  489. // 按省市区选择相关
  490. const provinceList = ref<any[]>([]) // 省份列表
  491. const cityList = ref<any[]>([]) // 城市列表
  492. const areaList = ref<any[]>([]) // 区县列表
  493. const selectedProvince = ref<any>(null) // 已选择的省份
  494. const selectedProvinceCode = ref<string>('') // 已选择的省份编码
  495. const selectedCity = ref<any>(null) // 已选择的城市
  496. const selectedCityCode = ref<string>('') // 已选择的城市编码
  497. const selectedProvinces = ref<any[]>([]) // 已选择的省份列表
  498. const selectedCities = ref<any[]>([]) // 已选择的城市列表
  499. const selectedAreas = ref<any[]>([]) // 已选择的区县列表
  500. const selectedRegions = ref<any[]>([]) // 已选择的地区列表
  501. // 在组件挂载时加载省份数据
  502. onMounted(async () => {
  503. try {
  504. // 获取省份数据
  505. const res = await getProvinces()
  506. provinceData.value = res.data || []
  507. provinceList.value = res.data || []
  508. } catch (error) {
  509. console.error('加载省份数据失败:', error)
  510. // 不再使用硬编码的默认省份列表
  511. provinceData.value = []
  512. provinceList.value = []
  513. }
  514. })
  515. // 选择省份
  516. const selectProvince = async (province: any) => {
  517. selectedProvince.value = province
  518. selectedProvinceCode.value = province.code
  519. try {
  520. // 调用API获取城市列表
  521. const res = await getCities(province.code)
  522. cityList.value = res.data || []
  523. areaList.value = []
  524. selectedCity.value = null
  525. selectedCityCode.value = ''
  526. selectedCities.value = []
  527. selectedAreas.value = []
  528. } catch (error) {
  529. console.error('获取城市列表失败:', error)
  530. cityList.value = []
  531. areaList.value = []
  532. selectedCity.value = null
  533. selectedCityCode.value = ''
  534. selectedCities.value = []
  535. selectedAreas.value = []
  536. }
  537. }
  538. // 选择城市
  539. const selectCity = async (city: any) => {
  540. selectedCity.value = city
  541. selectedCityCode.value = city.code
  542. try {
  543. // 调用API获取区县列表
  544. const res = await getAreas(city.code)
  545. areaList.value = res.data || []
  546. selectedAreas.value = []
  547. } catch (error) {
  548. console.error('获取区县列表失败:', error)
  549. areaList.value = []
  550. selectedAreas.value = []
  551. }
  552. }
  553. // 切换城市选择
  554. const toggleCitySelection = (city: any) => {
  555. const index = selectedCities.value.findIndex((c: any) => c.city_id === city.code)
  556. if (index > -1) {
  557. // 取消选择
  558. selectedCities.value.splice(index, 1)
  559. // 同时取消该城市下所有区县的选择
  560. selectedAreas.value = selectedAreas.value.filter((area: any) => area.city_id !== city.city_id)
  561. } else {
  562. // 添加选择之前检查是否已选择了所属省份
  563. const isProvinceAlreadySelected = selectedProvinces.value.some(
  564. (province: any) => province.province_id === city.province_id
  565. )
  566. if (isProvinceAlreadySelected) {
  567. // 获取省份名称用于提示
  568. const province = selectedProvinces.value.find(
  569. (p: any) => p.province_id === city.province_id
  570. )
  571. const provinceName = province ? province.province_name : '该省份'
  572. ElMessage.warning(`您已选择了${provinceName},无需再单独选择其下属城市`)
  573. return
  574. }
  575. // 添加选择,直接存储完整的6个字段
  576. const province = provinceList.value.find((p: any) => p.code === city.province + '0000')
  577. const provinceName = province ? province.name : '未知省份'
  578. selectedCities.value.push({
  579. area_id: null,
  580. city_id: city.code,
  581. area_name: null,
  582. city_name: city.name,
  583. province_id: city.province + '0000',
  584. province_name: provinceName
  585. })
  586. }
  587. }
  588. // 切换区县选择
  589. const toggleAreaSelection = (area: any) => {
  590. const index = selectedAreas.value.findIndex((a: any) => a.area_id === area.code)
  591. if (index > -1) {
  592. // 取消选择
  593. selectedAreas.value.splice(index, 1)
  594. } else {
  595. // 添加选择之前检查是否已选择了所属城市或省份
  596. const isCityAlreadySelected = selectedCities.value.some(
  597. (city: any) => city.city_id === area.city_id
  598. )
  599. const isProvinceAlreadySelected = selectedProvinces.value.some(
  600. (province: any) => province.province_id === area.province_id
  601. )
  602. if (isCityAlreadySelected) {
  603. // 获取城市名称用于提示
  604. const city = selectedCities.value.find(
  605. (c: any) => c.city_id === area.city_id
  606. )
  607. const cityName = city ? city.city_name : '该城市'
  608. ElMessage.warning(`您已选择了${cityName},无需再单独选择其下属区县`)
  609. return
  610. }
  611. if (isProvinceAlreadySelected) {
  612. // 获取省份名称用于提示
  613. const province = selectedProvinces.value.find(
  614. (p: any) => p.province_id === area.province + '0000'
  615. )
  616. const provinceName = province ? province.province_name : '该省份'
  617. ElMessage.warning(`您已选择了${provinceName},无需再单独选择其下属区县`)
  618. return
  619. }
  620. // 添加选择,直接存储完整的6个字段
  621. const province = provinceList.value.find((p: any) => p.code === area.province + '0000')
  622. const provinceName = province ? province.name : '未知省份'
  623. const city = cityList.value.find((c: any) => c.code === area.province + area.city + '00')
  624. const cityName = city ? city.name : '未知城市'
  625. selectedAreas.value.push({
  626. area_id: area.code,
  627. city_id: area.province + area.city + '00',
  628. area_name: area.name,
  629. city_name: cityName,
  630. province_id: area.province + '0000',
  631. province_name: provinceName
  632. })
  633. }
  634. }
  635. // 切换省份选择
  636. const toggleProvinceSelection = (province: any) => {
  637. const index = selectedProvinces.value.findIndex((p: any) => p.province_id === province.code)
  638. if (index > -1) {
  639. // 取消选择
  640. selectedProvinces.value.splice(index, 1)
  641. // 同时取消该省份下所有城市和区县的选择
  642. selectedCities.value = selectedCities.value.filter((city: any) => city.province_id !== province.province_id)
  643. selectedAreas.value = selectedAreas.value.filter((area: any) => area.province_id !== province.province_id)
  644. } else {
  645. // 添加选择,直接存储完整的6个字段
  646. selectedProvinces.value.push({
  647. area_id: null,
  648. city_id: null,
  649. area_name: null,
  650. city_name: null,
  651. province_id: province.code,
  652. province_name: province.name
  653. })
  654. }
  655. }
  656. // 判断城市是否被选中
  657. const isCitySelected = (cityCode: string) => {
  658. return selectedCities.value.some((city: any) => city.city_id === cityCode)
  659. }
  660. // 判断区县是否被选中
  661. const isAreaSelected = (areaCode: string) => {
  662. return selectedAreas.value.some((area: any) => area.area_id === areaCode)
  663. }
  664. // 判断省份是否被选中
  665. const isProvinceSelected = (provinceCode: string) => {
  666. return selectedProvinces.value.some((province: any) => province.province_id === provinceCode)
  667. }
  668. // 判断城市的省份是否已经被选中
  669. const isProvinceOfCitySelected = (city: any) => {
  670. return selectedProvinces.value.some((province: any) => province.province_id === city.province_id)
  671. }
  672. // 判断区县的上级(城市或省份)是否已经被选中
  673. const isHigherLevelRegionSelected = (area: any) => {
  674. // 检查所属城市是否被选中
  675. const isCitySelected = selectedCities.value.some((city: any) => city.city_id === area.city_id)
  676. // 检查所属省份是否被选中
  677. const isProvinceSelected = selectedProvinces.value.some((province: any) => province.province_id === area.province_id)
  678. return isCitySelected || isProvinceSelected
  679. }
  680. // 添加所有城市
  681. const addAllCities = () => {
  682. // 过滤掉那些所属省份已经被选中的城市
  683. const citiesToAdd = cityList.value.filter((city: any) => !isProvinceOfCitySelected(city))
  684. selectedCities.value = citiesToAdd.map(city => {
  685. const province = provinceList.value.find((p: any) => p.code === city.province + '0000')
  686. const provinceName = province ? province.name : '未知省份'
  687. return {
  688. area_id: null,
  689. city_id: city.code,
  690. area_name: null,
  691. city_name: city.name,
  692. province_id: city.province + '0000',
  693. province_name: provinceName
  694. }
  695. })
  696. }
  697. // 添加所有区县
  698. const addAllAreas = () => {
  699. // 过滤掉那些所属城市或省份已经被选中的区县
  700. const areasToAdd = areaList.value.filter((area: any) => !isHigherLevelRegionSelected(area))
  701. selectedAreas.value = areasToAdd.map(area => {
  702. const province = provinceList.value.find((p: any) => p.code === area.province + '0000')
  703. const provinceName = province ? province.name : '未知省份'
  704. const city = cityList.value.find((c: any) => c.code === area.province + area.city + '00')
  705. const cityName = city ? city.name : '未知城市'
  706. return {
  707. area_id: area.code,
  708. city_id: area.province + area.city + '00',
  709. area_name: area.name,
  710. city_name: cityName,
  711. province_id: area.province + '0000',
  712. province_name: provinceName
  713. }
  714. })
  715. }
  716. // 添加所有省份
  717. const addAllProvinces = async () => {
  718. // 检查是否已选择了省份的子级(城市或区县)
  719. const hasCitySelected = selectedCities.value.some((city: any) => {
  720. return selectedProvinces.value.some((province: any) => province.province_id === city.province_id)
  721. })
  722. const hasAreaSelected = selectedAreas.value.some((area: any) => {
  723. return selectedProvinces.value.some((province: any) => province.province_id === area.province_id)
  724. })
  725. if (hasCitySelected || hasAreaSelected) {
  726. ElMessage.warning('您已选择了省份的子级区域,无法再添加全部省份')
  727. return
  728. }
  729. selectedProvinces.value = provinceList.value.map(province => ({
  730. area_id: null,
  731. city_id: null,
  732. area_name: null,
  733. city_name: null,
  734. province_id: province.code,
  735. province_name: province.name
  736. }))
  737. // 如果省份列表不为空,加载第一个省份的城市数据
  738. if (provinceList.value.length > 0) {
  739. const firstProvince = provinceList.value[0]
  740. selectedProvince.value = firstProvince
  741. selectedProvinceCode.value = firstProvince.code
  742. try {
  743. // 调用API获取城市列表
  744. const res = await getCities(firstProvince.code)
  745. cityList.value = res.data || []
  746. areaList.value = []
  747. selectedCity.value = null
  748. selectedCityCode.value = ''
  749. selectedCities.value = []
  750. selectedAreas.value = []
  751. } catch (error) {
  752. console.error('获取城市列表失败:', error)
  753. cityList.value = []
  754. areaList.value = []
  755. selectedCity.value = null
  756. selectedCityCode.value = ''
  757. selectedAreas.value = []
  758. }
  759. }
  760. }
  761. // 清空已选择的地区
  762. const clearSelectedRegions = () => {
  763. selectedRegions.value = []
  764. }
  765. // 移除已选择的地区
  766. const removeSelectedRegion = (index: number) => {
  767. selectedRegions.value.splice(index, 1)
  768. }
  769. // 格式化地区名称显示
  770. const formatRegionName = (region: any) => {
  771. if (!region.city_id && !region.area_id) {
  772. // 省级区域,只显示省份名称
  773. return region.province_name || ''
  774. } else if (region.city_id && !region.area_id) {
  775. // 市级区域,显示"河北省/石家庄市"格式
  776. return `${region.province_name || ''}/${region.city_name || ''}`
  777. } else if (region.city_id && region.area_id) {
  778. // 区县级区域,显示"河北省/石家庄市/长安区"格式
  779. return `${region.province_name || ''}/${region.city_name || ''}/${region.area_name || ''}`
  780. }
  781. return region.province_name || ''
  782. }
  783. // 打开地区选择模态框
  784. const openRegionModal = (areaIndex: number) => {
  785. currentAreaIndex.value = areaIndex
  786. showRegionModal.value = true
  787. // 初始化省市选择模式数据
  788. selectedProvince.value = null
  789. selectedProvinceCode.value = ''
  790. selectedCity.value = null
  791. selectedCityCode.value = ''
  792. cityList.value = []
  793. areaList.value = []
  794. selectedProvinces.value = []
  795. selectedCities.value = []
  796. selectedAreas.value = []
  797. // 从现有数据中恢复已选择的地区
  798. const regions = [...specialAreas.value[areaIndex].regions]
  799. selectedRegions.value = regions
  800. }
  801. // 关闭地区选择模态框
  802. const closeRegionModal = () => {
  803. showRegionModal.value = false
  804. currentAreaIndex.value = -1
  805. // 重置省市选择模式数据
  806. selectedProvince.value = null
  807. selectedProvinceCode.value = ''
  808. selectedCity.value = null
  809. selectedCityCode.value = ''
  810. cityList.value = []
  811. areaList.value = []
  812. selectedCities.value = []
  813. selectedAreas.value = []
  814. selectedRegions.value = []
  815. }
  816. // 确认选择地区
  817. const confirmRegions = () => {
  818. if (currentAreaIndex.value >= 0) {
  819. // 将当前所有选中的省份、城市、区县添加到已选择列表
  820. const currentRegions = []
  821. // 添加选中的省份
  822. for (const province of selectedProvinces.value) {
  823. currentRegions.push({
  824. area_id: province.area_id,
  825. city_id: province.city_id,
  826. area_name: province.area_name,
  827. city_name: province.city_name,
  828. province_id: province.province_id,
  829. province_name: province.province_name
  830. })
  831. }
  832. // 添加选中的城市(过滤掉所属省份已经被选中的城市)
  833. for (const city of selectedCities.value) {
  834. const isProvinceSelected = selectedProvinces.value.some(
  835. (province: any) => province.province_id === city.province_id
  836. )
  837. if (!isProvinceSelected) {
  838. currentRegions.push({
  839. area_id: city.area_id,
  840. city_id: city.city_id,
  841. area_name: city.area_name,
  842. city_name: city.city_name,
  843. province_id: city.province_id,
  844. province_name: city.province_name
  845. })
  846. }
  847. }
  848. // 添加选中的区县(过滤掉所属城市或省份已经被选中的区县)
  849. for (const area of selectedAreas.value) {
  850. const isCitySelected = selectedCities.value.some(
  851. (city: any) => city.city_id === area.city_id
  852. )
  853. const isProvinceSelected = selectedProvinces.value.some(
  854. (province: any) => province.province_id === area.province_id
  855. )
  856. if (!isCitySelected && !isProvinceSelected) {
  857. currentRegions.push({
  858. area_id: area.area_id,
  859. city_id: area.city_id,
  860. area_name: area.area_name,
  861. city_name: area.city_name,
  862. province_id: area.province_id,
  863. province_name: area.province_name
  864. })
  865. }
  866. }
  867. // 合并已存在的地区和新选择的地区
  868. const allRegions = [...selectedRegions.value, ...currentRegions]
  869. // 去重处理
  870. const uniqueRegions = allRegions.filter((region, index, self) =>
  871. index === self.findIndex(r =>
  872. r.province_id === region.province_id &&
  873. r.city_id === region.city_id &&
  874. r.area_id === region.area_id
  875. )
  876. )
  877. // 最后进行层级关系检查,确保不会同时存在父级和子级
  878. const filteredRegions = []
  879. for (const region of uniqueRegions) {
  880. let shouldAdd = true
  881. if (!region.city_id && !region.area_id) {
  882. // 省级区域,检查是否已存在其子级
  883. const hasChild = filteredRegions.some(r =>
  884. (r.province_id === region.province_id && (r.city_id || r.area_id))
  885. )
  886. shouldAdd = !hasChild
  887. } else if (region.city_id && !region.area_id) {
  888. // 市级区域,检查是否已存在父级或子级
  889. const hasParent = filteredRegions.some(r =>
  890. r.province_id === region.province_id && !r.city_id && !r.area_id
  891. )
  892. const hasChild = filteredRegions.some(r =>
  893. r.city_id === region.city_id && r.area_id
  894. )
  895. shouldAdd = !hasParent && !hasChild
  896. } else if (region.city_id && region.area_id) {
  897. // 区县级区域,检查是否已存在父级
  898. const hasParentProvince = filteredRegions.some(r =>
  899. r.province_id === region.province_id && !r.city_id && !r.area_id
  900. )
  901. const hasParentCity = filteredRegions.some(r =>
  902. r.province_id === region.province_id && r.city_id === region.city_id && !r.area_id
  903. )
  904. shouldAdd = !hasParentProvince && !hasParentCity
  905. }
  906. if (shouldAdd) {
  907. filteredRegions.push(region)
  908. }
  909. }
  910. specialAreas.value[currentAreaIndex.value].regions = filteredRegions
  911. }
  912. closeRegionModal()
  913. }
  914. // 监听传入的模板数据变化
  915. watch(() => props.templateData, (newVal) => {
  916. if (newVal) {
  917. // 填充表单数据用于编辑
  918. templateInfo.id = newVal.templateInfo?.id || null
  919. templateInfo.name = newVal.templateInfo?.name || ''
  920. templateInfo.calcMethod = newVal.templateInfo?.calcMethod || 'piece'
  921. templateInfo.unit = newVal.templateInfo?.unit || 'kg'
  922. if (newVal.defaultRule) {
  923. defaultRule.first = Number(newVal.defaultRule.first) || 1
  924. defaultRule.firstPrice = Number(newVal.defaultRule.firstPrice) || 0
  925. defaultRule.nextPrice = Number(newVal.defaultRule.nextPrice) || 0
  926. }
  927. if (Array.isArray(newVal.specialAreas)) {
  928. specialAreas.value = newVal.specialAreas.map((area: any) => {
  929. // 确保每个区域的字段都正确处理,特别是处理可能为 null 的字段
  930. const processedRegions = Array.isArray(area.regions)
  931. ? area.regions.map((region: any) => ({
  932. province_id: region.province_id || null,
  933. province_name: region.province_name || '',
  934. city_id: region.city_id || null,
  935. city_name: region.city_name || null,
  936. area_id: region.area_id || null,
  937. area_name: region.area_name || null
  938. })).filter((region: any) => region.province_id) // 过滤掉没有province_id的无效区域
  939. : [];
  940. return {
  941. area_name: area.area_name || area.name || '',
  942. regions: processedRegions,
  943. first: Number(area.first) || 1,
  944. firstPrice: Number(area.first_price || area.firstPrice) || 0,
  945. nextPrice: Number(area.next_price || area.nextPrice) || 0
  946. }
  947. })
  948. } else {
  949. specialAreas.value = []
  950. }
  951. } else {
  952. // 如果没有传入数据,重置表单
  953. // 为了避免引用错误,这里手动重置表单而不是调用resetForm函数
  954. templateInfo.id = null
  955. templateInfo.name = ''
  956. templateInfo.calcMethod = 'piece'
  957. templateInfo.unit = 'kg'
  958. defaultRule.first = 1
  959. defaultRule.firstPrice = 10
  960. defaultRule.nextPrice = 5
  961. specialAreas.value = []
  962. }
  963. }, { immediate: true })
  964. // 可用单位选项
  965. const unitOptions = {
  966. weight: [
  967. { label: '克(g)', value: 'g' },
  968. { label: '千克(kg)', value: 'kg' }
  969. ],
  970. volume: [
  971. { label: '立方厘米(cm³)', value: 'cm3' },
  972. { label: '立方米(m³)', value: 'm3' }
  973. ]
  974. }
  975. // 获取可用单位选项
  976. const getAvailableUnits = () => {
  977. if (templateInfo.calcMethod === 'weight') {
  978. return unitOptions.weight
  979. } else if (templateInfo.calcMethod === 'volume') {
  980. return unitOptions.volume
  981. }
  982. return []
  983. }
  984. // 获取当前显示的单位
  985. const getCurrentDisplayUnit = () => {
  986. // 如果是按件数计费,直接返回件
  987. if (templateInfo.calcMethod === 'piece') {
  988. return '件'
  989. }
  990. // 返回用户选择的具体单位
  991. return templateInfo.unit
  992. }
  993. // 获取首值标签
  994. const getFirstLabel = () => {
  995. switch (templateInfo.calcMethod) {
  996. case 'piece': return '首件'
  997. case 'weight': return '首重'
  998. case 'volume': return '首体积'
  999. default: return '首件'
  1000. }
  1001. }
  1002. // 获取首价标签
  1003. const getFirstPriceLabel = () => {
  1004. switch (templateInfo.calcMethod) {
  1005. case 'piece': return '首件费用(元)'
  1006. case 'weight': return '首重费用(元)'
  1007. case 'volume': return '首体积费用(元)'
  1008. default: return '首件费用(元)'
  1009. }
  1010. }
  1011. // 获取续价标签
  1012. const getNextPriceLabel = () => {
  1013. switch (templateInfo.calcMethod) {
  1014. case 'piece': return '续件单价(元)'
  1015. case 'weight': return '续重单价(元)'
  1016. case 'volume': return '续体积单价(元)'
  1017. default: return '续件单价(元)'
  1018. }
  1019. }
  1020. // 获取首值的最小值
  1021. const getMinFirstValue = () => {
  1022. switch (templateInfo.calcMethod) {
  1023. case 'piece': return 1
  1024. case 'weight': return templateInfo.unit === 'g' ? 100 : 0.1
  1025. case 'volume': return templateInfo.unit === 'cm3' ? 1000 : 0.001
  1026. default: return 1
  1027. }
  1028. }
  1029. // 获取首值的步长
  1030. const getFirstStep = () => {
  1031. switch (templateInfo.calcMethod) {
  1032. case 'piece': return 1
  1033. case 'weight': return templateInfo.unit === 'g' ? 100 : 0.1
  1034. case 'volume': return templateInfo.unit === 'cm3' ? 1000 : 0.001
  1035. default: return 1
  1036. }
  1037. }
  1038. // 处理计费方式变更
  1039. const handleCalcMethodChange = async () => {
  1040. // 当计费方式改变时,更新所有规则的首值、步长和单位
  1041. defaultRule.first = getMinFirstValue()
  1042. // 更新所有特殊区域的首值
  1043. specialAreas.value.forEach(area => {
  1044. area.first = getMinFirstValue()
  1045. })
  1046. // 强制更新DOM以确保单位正确显示
  1047. await nextTick()
  1048. console.log('计费方式已更改为:', templateInfo.calcMethod)
  1049. }
  1050. // 处理单位变化
  1051. const handleUnitChange = () => {
  1052. // 更新默认规则的首值、步长
  1053. defaultRule.first = getMinFirstValue()
  1054. // 更新所有特殊区域的首值
  1055. specialAreas.value.forEach(area => {
  1056. area.first = getMinFirstValue()
  1057. })
  1058. console.log('单位已更改为:', templateInfo.unit)
  1059. }
  1060. // 添加特殊区域
  1061. const addSpecialArea = () => {
  1062. specialAreas.value.push({
  1063. area_name: '',
  1064. regions: [],
  1065. first: getMinFirstValue(),
  1066. firstPrice: 15,
  1067. nextPrice: 8
  1068. })
  1069. }
  1070. // 删除特殊区域
  1071. const removeSpecialArea = (index: number) => {
  1072. ElMessageBox.confirm('确定要删除这个特殊区域吗?', '提示', {
  1073. type: 'warning'
  1074. }).then(() => {
  1075. specialAreas.value.splice(index, 1)
  1076. }).catch(() => {
  1077. // 取消删除
  1078. })
  1079. }
  1080. // 删除地区
  1081. const removeRegion = (areaIndex: number, regionIndex: number) => {
  1082. specialAreas.value[areaIndex].regions.splice(regionIndex, 1)
  1083. }
  1084. // 保存模板
  1085. const saveTemplate = () => {
  1086. // 数据验证
  1087. if (!templateInfo.name) {
  1088. ElMessage.error('请输入模板名称')
  1089. return
  1090. }
  1091. // 验证默认规则
  1092. if (defaultRule.first <= 0) {
  1093. ElMessage.error('默认规则' + getFirstLabel() + '必须大于0')
  1094. return
  1095. }
  1096. if (defaultRule.firstPrice < 0) {
  1097. ElMessage.error('默认规则' + getFirstPriceLabel() + '不能为负数')
  1098. return
  1099. }
  1100. if (defaultRule.nextPrice < 0) {
  1101. ElMessage.error('默认规则' + getNextPriceLabel() + '不能为负数')
  1102. return
  1103. }
  1104. // 验证特殊区域
  1105. for (let i = 0; i < specialAreas.value.length; i++) {
  1106. const area = specialAreas.value[i]
  1107. if (!area.area_name) {
  1108. ElMessage.error(`第${i + 1}个特殊区域请输入区域名称`)
  1109. return
  1110. }
  1111. if (area.regions.length === 0) {
  1112. ElMessage.error(`第${i + 1}个特殊区域请选择地区`)
  1113. return
  1114. }
  1115. if (area.first <= 0) {
  1116. ElMessage.error(`第${i + 1}个特殊区域` + getFirstLabel() + '必须大于0')
  1117. return
  1118. }
  1119. if (area.firstPrice < 0) {
  1120. ElMessage.error(`第${i + 1}个特殊区域` + getFirstPriceLabel() + '不能为负数')
  1121. return
  1122. }
  1123. if (area.nextPrice < 0) {
  1124. ElMessage.error(`第${i + 1}个特殊区域` + getNextPriceLabel() + '不能为负数')
  1125. return
  1126. }
  1127. }
  1128. // 处理特殊区域数据,确保只包含有效的6字段结构
  1129. const processedSpecialAreas = specialAreas.value.map(area => {
  1130. // 过滤掉无效的区域数据并确保每个区域都有province_id
  1131. const validRegions = area.regions
  1132. .filter(region => region && region.province_id)
  1133. .map(region => ({
  1134. province_id: region.province_id,
  1135. province_name: region.province_name || '',
  1136. city_id: region.city_id || null,
  1137. city_name: region.city_name || null,
  1138. area_id: region.area_id || null,
  1139. area_name: region.area_name || null
  1140. }));
  1141. return {
  1142. area_name: area.area_name,
  1143. first: area.first,
  1144. firstPrice: area.firstPrice,
  1145. nextPrice: area.nextPrice,
  1146. regions: validRegions
  1147. };
  1148. });
  1149. const templateData = {
  1150. templateInfo: { ...templateInfo },
  1151. defaultRule: { ...defaultRule },
  1152. specialAreas: processedSpecialAreas
  1153. }
  1154. emit('save', templateData)
  1155. const message = isEditMode.value ? '模板保存成功' : '模板创建成功'
  1156. ElMessage.success(message)
  1157. console.log('保存模板数据:', templateData)
  1158. }
  1159. // 取消操作
  1160. const cancel = () => {
  1161. ElMessageBox.confirm('确定要取消吗?未保存的数据将会丢失。', '提示', {
  1162. type: 'warning'
  1163. }).then(() => {
  1164. emit('cancel')
  1165. console.log('取消操作')
  1166. }).catch(() => {
  1167. // 取消操作
  1168. })
  1169. }
  1170. // 重置表单
  1171. const resetForm = () => {
  1172. templateInfo.id = null
  1173. templateInfo.name = ''
  1174. templateInfo.calcMethod = 'piece'
  1175. templateInfo.unit = 'kg'
  1176. defaultRule.first = 1
  1177. defaultRule.firstPrice = 10
  1178. defaultRule.nextPrice = 5
  1179. specialAreas.value = []
  1180. }
  1181. // 添加当前选中的省份到已选择列表
  1182. const addCurrentProvincesToSelected = () => {
  1183. const newRegions = [];
  1184. // 添加选中的省份
  1185. for (const province of selectedProvinces.value) {
  1186. newRegions.push({
  1187. area_id: province.area_id,
  1188. city_id: province.city_id,
  1189. area_name: province.area_name,
  1190. city_name: province.city_name,
  1191. province_id: province.province_id,
  1192. province_name: province.province_name
  1193. });
  1194. }
  1195. // 合并到已选择区域并去重
  1196. const allRegions = [...selectedRegions.value, ...newRegions];
  1197. const uniqueRegions = allRegions.filter((region, index, self) =>
  1198. index === self.findIndex(r =>
  1199. r.province_id === region.province_id &&
  1200. r.city_id === region.city_id &&
  1201. r.area_id === region.area_id
  1202. )
  1203. );
  1204. // 检查添加省份时是否存在下级区域(已选择的城市或区县)
  1205. const finalRegions = [];
  1206. for (const region of uniqueRegions) {
  1207. let shouldAdd = true;
  1208. if (!region.city_id && !region.area_id) {
  1209. // 对于省份,检查是否存在下级区域
  1210. const hasChild = uniqueRegions.some(r =>
  1211. (r.province_id === region.province_id && (r.city_id || r.area_id))
  1212. );
  1213. if (hasChild) {
  1214. ElMessage.warning(`您已选择了${region.province_name}的下级区域,无法再添加该省份`);
  1215. shouldAdd = false;
  1216. }
  1217. } else if (region.city_id && !region.area_id) {
  1218. // 对于城市,检查是否存在上级省份或下级区县
  1219. const hasParent = uniqueRegions.some(r =>
  1220. r.province_id === region.province_id && !r.city_id && !r.area_id
  1221. );
  1222. const hasChild = uniqueRegions.some(r =>
  1223. r.city_id === region.city_id && r.area_id
  1224. );
  1225. if (hasParent) {
  1226. ElMessage.warning(`您已选择了${region.province_name}的上级省份,无法再添加该城市`);
  1227. shouldAdd = false;
  1228. }
  1229. if (hasChild) {
  1230. ElMessage.warning(`您已选择了${region.city_name}的下级区县,无法再添加该城市`);
  1231. shouldAdd = false;
  1232. }
  1233. } else if (region.city_id && region.area_id) {
  1234. // 对于区县,检查是否存在上级省份或城市
  1235. const hasParentProvince = uniqueRegions.some(r =>
  1236. r.province_id === region.province_id && !r.city_id && !r.area_id
  1237. );
  1238. const hasParentCity = uniqueRegions.some(r =>
  1239. r.province_id === region.province_id && r.city_id === region.city_id && !r.area_id
  1240. );
  1241. if (hasParentProvince) {
  1242. ElMessage.warning(`您已选择了${region.province_name}的上级省份,无法再添加该区县`);
  1243. shouldAdd = false;
  1244. }
  1245. if (hasParentCity) {
  1246. ElMessage.warning(`您已选择了${region.city_name}的上级城市,无法再添加该区县`);
  1247. shouldAdd = false;
  1248. }
  1249. }
  1250. if (shouldAdd) {
  1251. finalRegions.push(region);
  1252. }
  1253. }
  1254. selectedRegions.value = finalRegions;
  1255. // 清空当前选中状态
  1256. selectedProvinces.value = [];
  1257. }
  1258. // 添加当前选中的城市到已选择列表
  1259. const addCurrentCitiesToSelected = () => {
  1260. const newRegions = [];
  1261. // 添加选中的城市
  1262. for (const city of selectedCities.value) {
  1263. newRegions.push({
  1264. province_id: city.province_id,
  1265. province_name: city.province_name,
  1266. city_id: city.city_id,
  1267. city_name: city.city_name,
  1268. area_id: city.area_id,
  1269. area_name: city.area_name
  1270. });
  1271. }
  1272. // 合并到已选择区域并去重
  1273. const allRegions = [...selectedRegions.value, ...newRegions];
  1274. const uniqueRegions = allRegions.filter((region, index, self) =>
  1275. index === self.findIndex(r =>
  1276. r.province_id === region.province_id &&
  1277. r.city_id === region.city_id &&
  1278. r.area_id === region.area_id
  1279. )
  1280. );
  1281. // 检查添加城市时是否存在上级或下级区域
  1282. const finalRegions = [];
  1283. for (const region of uniqueRegions) {
  1284. let shouldAdd = true;
  1285. if (!region.city_id && !region.area_id) {
  1286. // 对于省份,检查是否存在下级区域
  1287. const hasChild = uniqueRegions.some(r =>
  1288. (r.province_id === region.province_id && (r.city_id || r.area_id))
  1289. );
  1290. if (hasChild) {
  1291. ElMessage.warning(`您已选择了${region.province_name}的下级区域,无法再添加该省份`);
  1292. shouldAdd = false;
  1293. }
  1294. } else if (region.city_id && !region.area_id) {
  1295. // 对于城市,检查是否存在上级省份或下级区县
  1296. const hasParent = uniqueRegions.some(r =>
  1297. r.province_id === region.province_id && !r.city_id && !r.area_id
  1298. );
  1299. const hasChild = uniqueRegions.some(r =>
  1300. r.city_id === region.city_id && r.area_id
  1301. );
  1302. if (hasParent) {
  1303. ElMessage.warning(`您已选择了${region.province_name}的上级省份,无法再添加该城市`);
  1304. shouldAdd = false;
  1305. }
  1306. if (hasChild) {
  1307. ElMessage.warning(`您已选择了${region.city_name}的下级区县,无法再添加该城市`);
  1308. shouldAdd = false;
  1309. }
  1310. } else if (region.city_id && region.area_id) {
  1311. // 对于区县,检查是否存在上级省份或城市
  1312. const hasParentProvince = uniqueRegions.some(r =>
  1313. r.province_id === region.province_id && !r.city_id && !r.area_id
  1314. );
  1315. const hasParentCity = uniqueRegions.some(r =>
  1316. r.province_id === region.province_id && r.city_id === region.city_id && !r.area_id
  1317. );
  1318. if (hasParentProvince) {
  1319. ElMessage.warning(`您已选择了${region.province_name}的上级省份,无法再添加该区县`);
  1320. shouldAdd = false;
  1321. }
  1322. if (hasParentCity) {
  1323. ElMessage.warning(`您已选择了${region.city_name}的上级城市,无法再添加该区县`);
  1324. shouldAdd = false;
  1325. }
  1326. }
  1327. if (shouldAdd) {
  1328. finalRegions.push(region);
  1329. }
  1330. }
  1331. selectedRegions.value = finalRegions;
  1332. // 清空当前选中状态
  1333. selectedCities.value = [];
  1334. }
  1335. // 添加当前选中的区县到已选择列表
  1336. const addCurrentAreasToSelected = () => {
  1337. const newRegions = [];
  1338. // 添加选中的区县
  1339. for (const area of selectedAreas.value) {
  1340. // 检查要添加的区县是否存在上级(已选择的省份或城市)
  1341. const hasParentProvince = selectedProvinces.value.some(province => province.province_id === area.province_id);
  1342. const hasParentCity = selectedCities.value.some(city => city.city_id === area.city_id);
  1343. if (hasParentProvince) {
  1344. ElMessage.warning(`您已选择了${area.name}的上级省份,无法再添加该区县`);
  1345. continue; // 跳过这个区县,不添加
  1346. }
  1347. if (hasParentCity) {
  1348. ElMessage.warning(`您已选择了${area.name}的上级城市,无法再添加该区县`);
  1349. continue; // 跳过这个区县,不添加
  1350. }
  1351. newRegions.push({
  1352. area_id: area.area_id,
  1353. city_id: area.city_id,
  1354. area_name: area.area_name,
  1355. city_name: area.city_name,
  1356. province_id: area.province_id,
  1357. province_name: area.province_name
  1358. });
  1359. }
  1360. // 合并到已选择区域并去重
  1361. const allRegions = [...selectedRegions.value, ...newRegions];
  1362. const uniqueRegions = allRegions.filter((region, index, self) =>
  1363. index === self.findIndex(r =>
  1364. r.province_id === region.province_id &&
  1365. r.city_id === region.city_id &&
  1366. r.area_id === region.area_id
  1367. )
  1368. );
  1369. selectedRegions.value = uniqueRegions;
  1370. // 清空当前选中状态
  1371. selectedAreas.value = [];
  1372. }
  1373. </script>
  1374. <style scoped lang="scss">
  1375. .shipping-template-component {
  1376. .card-header {
  1377. font-size: 18px;
  1378. font-weight: bold;
  1379. color: #409eff;
  1380. display: flex;
  1381. align-items: center;
  1382. gap: 8px;
  1383. }
  1384. .card-sub-header {
  1385. font-size: 16px;
  1386. font-weight: bold;
  1387. }
  1388. .special-area {
  1389. border: 1px solid #dcdfe6;
  1390. border-radius: 4px;
  1391. transition: all 0.3s;
  1392. &:hover {
  1393. box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  1394. }
  1395. }
  1396. .region-tags {
  1397. min-height: 32px;
  1398. }
  1399. .province-checkbox-group {
  1400. max-height: 300px;
  1401. overflow-y: auto;
  1402. }
  1403. .mb-4 {
  1404. margin-bottom: 1rem;
  1405. }
  1406. :deep(.el-card__header) {
  1407. padding: 12px 20px;
  1408. border-bottom: 1px solid #ebeef5;
  1409. }
  1410. :deep(.el-form-item) {
  1411. margin-bottom: 18px;
  1412. }
  1413. :deep(.el-form-item__label) {
  1414. font-weight: normal;
  1415. }
  1416. .input-with-unit {
  1417. display: flex;
  1418. align-items: center;
  1419. gap: 8px;
  1420. .unit-text {
  1421. white-space: nowrap;
  1422. color: #606266;
  1423. font-size: 14px;
  1424. }
  1425. }
  1426. .form-item-tip {
  1427. margin-top: 4px;
  1428. font-size: 12px;
  1429. color: #909399;
  1430. line-height: 1.4;
  1431. }
  1432. .region-selector-container {
  1433. border: 1px solid #dcdfe6;
  1434. border-radius: 4px;
  1435. height: 350px;
  1436. display: flex;
  1437. flex-direction: column;
  1438. .selector-header {
  1439. padding: 8px 12px;
  1440. border-bottom: 1px solid #dcdfe6;
  1441. display: flex;
  1442. justify-content: space-between;
  1443. align-items: center;
  1444. font-weight: 500;
  1445. }
  1446. .region-item {
  1447. padding: 8px 12px;
  1448. cursor: pointer;
  1449. display: flex;
  1450. align-items: center;
  1451. gap: 8px;
  1452. &:hover {
  1453. background-color: #f5f7fa;
  1454. }
  1455. &.active {
  1456. background-color: #ecf5ff;
  1457. color: #409eff;
  1458. }
  1459. }
  1460. .selected-region-item {
  1461. padding: 8px 12px;
  1462. display: flex;
  1463. justify-content: space-between;
  1464. align-items: center;
  1465. &:hover {
  1466. background-color: #f5f7fa;
  1467. }
  1468. }
  1469. .no-data {
  1470. padding: 20px;
  1471. text-align: center;
  1472. color: #909399;
  1473. }
  1474. }
  1475. .dialog-footer {
  1476. text-align: right;
  1477. }
  1478. }
  1479. </style>