Ver código fonte

feat(shipping-template): 添加运费模板管理功能

- 实现运费模板相关API接口,包括获取、创建、更新、删除等操作
- 开发运费模板选择组件,支持业务类型和ID参数绑定
- 构建运费模板表单组件,包含模板信息、默认规则和特殊区域设置
- 集成省市区三级联动选择功能,支持精确地区配置
- 实现多种计费方式,包括按件数、重量、体积计算
- 添加特殊区域规则设置,可为不同地区配置独立运费规则
- 集成地区选择对话框,支持多层级地区批量选择和管理
runphp 3 semanas atrás
pai
commit
e1f6853154

+ 75 - 0
frontend/admin/api/index.js

@@ -0,0 +1,75 @@
+import request from '@/utils/request'
+
+/**
+ * 运费模板相关API
+ */
+
+// 获取省份列表
+export function getProvinces() {
+    return request({
+        url: '/limit_purchase/region/province',
+        method: 'get'
+    })
+}
+
+// 根据省份ID获取城市列表
+export function getCities(provinceId) {
+    return request({
+        url: `/limit_purchase/region/city/${provinceId}`,
+        method: 'get'
+    })
+}
+
+// 根据城市ID获取区县列表
+export function getAreas(cityId) {
+    return request({
+        url: `/limit_purchase/region/area/${cityId}`,
+        method: 'get'
+    })
+}
+
+// 获取运费模板列表
+export function getShippingTemplateList(bizType, bizId) {
+  return request({
+    url: '/shipping_template',
+    method: 'get',
+    params: {
+      biz_type: bizType,
+      biz_id: bizId
+    }
+  })
+}
+
+// 获取运费模板详情
+export function getShippingTemplateDetail(id) {
+  return request({
+    url: `/shipping_template/${id}`,
+    method: 'get'
+  })
+}
+
+// 新增运费模板
+export function createShippingTemplate(data) {
+  return request({
+    url: '/shipping_template',
+    method: 'post',
+      data: data
+  })
+}
+
+// 更新运费模板
+export function updateShippingTemplate(id, data) {
+  return request({
+    url: `/shipping_template/${id}`,
+    method: 'put',
+      data: data
+  })
+}
+
+// 删除运费模板
+export function deleteShippingTemplate(id) {
+  return request({
+    url: `/shipping_template/${id}`,
+    method: 'delete'
+  })
+}

+ 39 - 0
frontend/admin/index.js

@@ -0,0 +1,39 @@
+export default {
+    routes: {
+        path: '/shipping-template',
+        name: 'ShippingTemplateModule',
+        component: () => import('@/layout/index.vue'),
+        meta: {
+            title: '运费模板管理',
+            icon: 'Van',
+            permission: 'shipping_template'
+        },
+        children: [
+            {
+                path: 'list',
+                name: 'ShippingTemplateList',
+                component: () => import('./views/ShippingTemplateList.vue'),
+                meta: {
+                    title: '运费模板列表',
+                    icon: 'List'
+                }
+            }
+        ]
+    },
+
+    menus: [
+        {
+            path: '/shipping-template',
+            title: '运费模板管理',
+            icon: 'Van',
+            permission: 'shipping_template',
+            children: [
+                {
+                    path: '/shipping-template/list',
+                    title: '运费模板列表',
+                    icon: 'List'
+                }
+            ]
+        }
+    ]
+}

+ 97 - 0
frontend/admin/views/SelectShippingTemplate.vue

@@ -0,0 +1,97 @@
+<template>
+  <div class="shipping-template-component">
+    <!-- 选择模板 -->
+    <el-select 
+      v-model="selectedTemplateId" 
+      placeholder="请选择运费模板"
+      style="width: 100%"
+    >
+      <el-option
+        v-for="template in templateList"
+        :key="template.id"
+        :label="template.name"
+        :value="template.id"
+      />
+    </el-select>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+import { getShippingTemplateList } from '../api/index.js'
+
+// 定义组件props
+const props = defineProps({
+  // 业务类型
+  bizType: {
+    type: String,
+    default: ''
+  },
+  // 业务ID
+  bizId: {
+    type: [String, Number],
+    default: ''
+  },
+  // 当前模板ID(用于v-model绑定)
+  modelValue: {
+    type: [String, Number],
+    default: ''
+  }
+})
+
+// 定义组件事件
+const emit = defineEmits(['update:modelValue', 'select', 'cancel'])
+
+// 数据相关
+const templateList = ref([])
+const selectedTemplateId = ref(null)
+
+// 监听modelValue变化,更新选中值
+watch(() => props.modelValue, (newVal) => {
+  selectedTemplateId.value = newVal
+})
+
+// 组件挂载时获取模板列表
+onMounted(() => {
+  // 初始化选中值
+  selectedTemplateId.value = props.modelValue
+  fetchTemplateList()
+})
+
+// 监听bizType和bizId的变化,重新获取模板列表
+watch([() => props.bizType, () => props.bizId], () => {
+  fetchTemplateList()
+})
+
+// 监听selectedTemplateId变化,更新外部v-model绑定的值
+watch(selectedTemplateId, (newVal) => {
+  emit('update:modelValue', newVal)
+})
+
+// 获取模板列表
+const fetchTemplateList = async () => {
+  try {
+    const response = await getShippingTemplateList(props.bizType, props.bizId)
+    templateList.value = response.data || []
+    
+    // 如果没有传递modelValue,则使用列表中selected为true的项作为当前模板
+    if (!props.modelValue && templateList.value.length > 0) {
+      const selectedTemplate = templateList.value.find(template => template.selected)
+      if (selectedTemplate) {
+        selectedTemplateId.value = selectedTemplate.id
+      }
+    }
+  } catch (error) {
+    console.error('获取运费模板列表失败:', error)
+    ElMessage.error('获取运费模板列表失败')
+    templateList.value = []
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.shipping-template-component {
+  width: 100%;
+}
+</style>

+ 1655 - 0
frontend/admin/views/ShippingTemplateForm.vue

@@ -0,0 +1,1655 @@
+<template>
+  <div class="shipping-template-component">
+    <el-card class="box-card">
+      <!-- 头部 -->
+      <template #header>
+        <div class="card-header">
+          <el-icon><Van /></el-icon>
+          <span>{{ isEditMode ? '编辑运费模板' : '新建运费模板' }}</span>
+        </div>
+      </template>
+
+      <el-form label-width="120px">
+        <!-- 模板信息 -->
+        <el-card shadow="never" class="mb-4">
+          <template #header>
+            <div class="card-sub-header">
+              <span>模板信息</span>
+            </div>
+          </template>
+          
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item :rules="[{ required: true, message: '请输入模板名称', trigger: 'blur' }]" label="模板名称"
+                            prop="templateInfo.name">
+                <el-input 
+                  v-model="templateInfo.name" 
+                  placeholder="例如:全国配送模板"
+                />
+                <div class="form-item-tip">给模板起一个容易识别的名称</div>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item :rules="[{ required: true, message: '请选择计费方式', trigger: 'change' }]" label="计费方式"
+                            prop="templateInfo.calcMethod">
+                <el-select 
+                  v-model="templateInfo.calcMethod" 
+                  @change="handleCalcMethodChange"
+                  style="width: 100%"
+                  placeholder="请选择计费方式"
+                >
+                  <el-option label="按件数计算(标准化小商品如服饰)" value="piece" />
+                  <el-option label="按重量计算(重货如五金件)" value="weight" />
+                  <el-option label="按体积计算(轻抛货如泡沫箱、家具)" value="volume" />
+                </el-select>
+                <div class="form-item-tip">根据商品特性选择合适的计费方式</div>
+              </el-form-item>
+            </el-col>
+          </el-row>
+          
+          <!-- 单位选择 -->
+          <el-row :gutter="20" v-if="templateInfo.calcMethod !== 'piece'">
+            <el-col :span="12">
+              <el-form-item :rules="[{ required: true, message: '请选择计量单位', trigger: 'change' }]" label="计量单位"
+                            prop="templateInfo.unit">
+                <el-select 
+                  v-model="templateInfo.unit" 
+                  @change="handleUnitChange"
+                  style="width: 100%"
+                  placeholder="请选择计量单位"
+                >
+                  <el-option 
+                    v-for="unit in getAvailableUnits()" 
+                    :key="unit.value" 
+                    :label="unit.label" 
+                    :value="unit.value" 
+                  />
+                </el-select>
+                <div class="form-item-tip">选择合适的计量单位便于计算运费</div>
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </el-card>
+
+        <!-- 默认区设置 -->
+        <el-card shadow="never" class="mb-4">
+          <template #header>
+            <div class="flex justify-between items-center">
+              <span>默认计费规则</span>
+              <el-tag type="info">适用于未指定地区</el-tag>
+            </div>
+          </template>
+
+          <el-alert
+              class="mb-4"
+              description="这是适用于全国大部分地区的运费计算规则,针对特定区域可以在下方设置特殊区域规则"
+              show-icon
+              title="默认计费规则说明"
+              type="info"
+          />
+
+          <el-row :gutter="20">
+            <el-col :span="8">
+              <el-form-item :label="getFirstLabel()" :rules="[{ required: true, type: 'number', min: 0.001, message: getFirstLabel() + '必须大于0', trigger: 'blur' }]"
+                            prop="defaultRule.first">
+                <div class="input-with-unit">
+                  <el-input-number 
+                    v-model="defaultRule.first" 
+                    :min="getMinFirstValue()" 
+                    :step="getFirstStep()"
+                    controls-position="right"
+                    style="width: 100%"
+                  />
+                  <span class="unit-text">
+                    <span v-if="getCurrentDisplayUnit() === 'cm3'">
+                      cm<sup>3</sup>
+                    </span>
+                    <span v-else-if="getCurrentDisplayUnit() === 'm3'">
+                      m<sup>3</sup>
+                    </span>
+                    <span v-else>{{ getCurrentDisplayUnit() }}</span>
+                  </span>
+                </div>
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item :label="getFirstPriceLabel()" :rules="[{ required: true, type: 'number', min: 0, message: getFirstPriceLabel() + '不能为负数', trigger: 'blur' }]"
+                            prop="defaultRule.firstPrice">
+                <el-input-number 
+                  v-model="defaultRule.firstPrice" 
+                  :min="0" 
+                  :step="0.01"
+                  controls-position="right"
+                  style="width: 100%"
+                />
+                <div class="form-item-tip">首{{ getCurrentDisplayUnit() }}的费用</div>
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item :label="getNextPriceLabel()" :rules="[{ required: true, type: 'number', min: 0, message: getNextPriceLabel() + '不能为负数', trigger: 'blur' }]"
+                            prop="defaultRule.nextPrice">
+                <el-input-number 
+                  v-model="defaultRule.nextPrice" 
+                  :min="0" 
+                  :step="0.01"
+                  controls-position="right"
+                  style="width: 100%"
+                />
+                <div class="form-item-tip">超出首{{ getCurrentDisplayUnit() }}后每增加一个单位的费用</div>
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </el-card>
+
+        <!-- 特殊区设置 -->
+        <el-card shadow="never" class="mb-4">
+          <template #header>
+            <div class="flex justify-between items-center">
+              <span>特殊区域设置</span>
+              <el-button type="primary" @click="addSpecialArea" plain>
+                <el-icon><Plus /></el-icon>添加特殊区
+              </el-button>
+            </div>
+          </template>
+
+          <el-alert
+              class="mb-4"
+              description="可以为某些偏远或特殊地区设置独立的运费规则,这些规则会覆盖默认规则"
+              show-icon
+              title="特殊区域设置说明"
+              type="info"
+          />
+
+          <!-- 特殊区列表 -->
+          <div 
+            v-for="(area, index) in specialAreas" 
+            :key="index" 
+            class="special-area mb-4 p-4 border rounded"
+          >
+            <el-button 
+              @click="removeSpecialArea(index)" 
+              type="danger" 
+              :icon="Close"
+              circle 
+              size="small" 
+              class="float-right"
+            />
+            
+            <el-row :gutter="20" class="mb-4">
+              <el-col :span="12">
+                <el-form-item :prop="'specialAreas.' + index + '.regions'" :rules="{ type: 'array', required: true, message: '请选择至少一个地区', trigger: 'change' }"
+                              label="选择地区">
+                  <div class="region-tags mb-2">
+                    <el-tag
+                      v-for="(region, regionIndex) in area.regions" 
+                      :key="regionIndex"
+                      closable
+                      type="primary"
+                      class="mr-2 mb-2"
+                      @close="removeRegion(index, regionIndex)"
+                    >
+                      {{ formatRegionName(region) }}
+                    </el-tag>
+                  </div>
+                  <el-button @click="openRegionModal(index)" type="primary" link>
+                    <el-icon><Plus /></el-icon>添加地区
+                  </el-button>
+                  <div class="form-item-tip">点击添加地区,可选择省份、城市或区县</div>
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item :prop="'specialAreas.' + index + '.area_name'"
+                              :rules="{ required: true, message: '请输入区域名称', trigger: 'blur' }"
+                              label="区域名称">
+                  <el-input
+                      v-model="area.area_name"
+                    placeholder="例如:偏远地区"
+                  />
+                  <div class="form-item-tip">给这个特殊区域起一个名字,便于识别</div>
+                </el-form-item>
+              </el-col>
+            </el-row>
+
+            <!-- 特殊区域价格设置 -->
+            <el-row :gutter="20">
+              <el-col :span="8">
+                <el-form-item :label="getFirstLabel()" :prop="'specialAreas.' + index + '.first'"
+                              :rules="{ required: true, type: 'number', min: 0.001, message: getFirstLabel() + '必须大于0', trigger: 'blur' }">
+                  <div class="input-with-unit">
+                    <el-input-number 
+                      v-model="area.first" 
+                      :min="getMinFirstValue()" 
+                      :step="getFirstStep()"
+                      controls-position="right"
+                      style="width: 100%"
+                    />
+                    <span class="unit-text">
+                      <span v-if="getCurrentDisplayUnit() === 'cm3'">
+                        cm<sup>3</sup>
+                      </span>
+                      <span v-else-if="getCurrentDisplayUnit() === 'm3'">
+                        m<sup>3</sup>
+                      </span>
+                      <span v-else>{{ getCurrentDisplayUnit() }}</span>
+                    </span>
+                  </div>
+                </el-form-item>
+              </el-col>
+              <el-col :span="8">
+                <el-form-item :label="getFirstPriceLabel()" :prop="'specialAreas.' + index + '.firstPrice'"
+                              :rules="{ required: true, type: 'number', min: 0, message: getFirstPriceLabel() + '不能为负数', trigger: 'blur' }">
+                  <el-input-number 
+                    v-model="area.firstPrice" 
+                    :min="0" 
+                    :step="0.01"
+                    controls-position="right"
+                    style="width: 100%"
+                  />
+                  <div class="form-item-tip">首{{ getCurrentDisplayUnit() }}的费用</div>
+                </el-form-item>
+              </el-col>
+              <el-col :span="8">
+                <el-form-item :label="getNextPriceLabel()" :prop="'specialAreas.' + index + '.nextPrice'"
+                              :rules="{ required: true, type: 'number', min: 0, message: getNextPriceLabel() + '不能为负数', trigger: 'blur' }">
+                  <el-input-number 
+                    v-model="area.nextPrice" 
+                    :min="0" 
+                    :step="0.01"
+                    controls-position="right"
+                    style="width: 100%"
+                  />
+                  <div class="form-item-tip">超出首{{ getCurrentDisplayUnit() }}后每增加一个单位的费用</div>
+                </el-form-item>
+              </el-col>
+            </el-row>
+          </div>
+
+          <el-empty v-if="specialAreas.length === 0" description="暂无特殊区域,请点击上方按钮添加"/>
+        </el-card>
+
+        <!-- 操作按钮 -->
+        <div class="flex justify-end">
+          <el-button @click="cancel">取消</el-button>
+          <el-button :loading="props.loading" type="primary" @click="saveTemplate">
+            <el-icon><DocumentAdd /></el-icon>{{ isEditMode ? '保存模板' : '创建模板' }}
+          </el-button>
+        </div>
+      </el-form>
+    </el-card>
+
+    <!-- 地区选择对话框 -->
+    <el-dialog 
+      v-model="showRegionModal" 
+      :before-close="closeRegionModal"
+      title="选择地区"
+      width="800px"
+    >
+      <el-alert
+          class="mb-4"
+          description="可以选择省份、城市或区县级别,系统会自动处理层级关系"
+          show-icon
+          title="地区选择说明"
+          type="info"
+      />
+
+      <el-row :gutter="20">
+        <el-col :span="6">
+          <div class="region-selector-container">
+            <div class="selector-header">
+              <span>省份</span>
+              <div>
+                <el-button
+                    :disabled="!provinceList.length"
+                    link
+                    type="primary"
+                    @click="addAllProvinces"
+                >
+                  全部
+                </el-button>
+                <el-button
+                    :disabled="!selectedProvinces.length"
+                    link
+                    type="success"
+                    @click="addCurrentProvincesToSelected"
+                >
+                  添加
+                </el-button>
+              </div>
+            </div>
+            <el-scrollbar height="300px">
+              <div
+                  v-for="province in provinceList"
+                  :key="province.code"
+                  :class="['region-item', { active: isProvinceSelected(province.code) || selectedProvinceCode === province.code }]"
+                  @click="selectProvince(province)"
+              >
+                <el-checkbox
+                    :model-value="isProvinceSelected(province.code)"
+                    @change="toggleProvinceSelection(province)"
+                    @click.stop
+                />
+                {{ province.name }}
+              </div>
+            </el-scrollbar>
+          </div>
+        </el-col>
+
+        <el-col :span="6">
+          <div class="region-selector-container">
+            <div class="selector-header">
+              <span>城市</span>
+              <div>
+                <el-button
+                    v-if="selectedProvince"
+                    :disabled="!cityList.length"
+                    link
+                    type="primary"
+                    @click="addAllCities"
+                >
+                  全部
+                </el-button>
+                <el-button
+                    :disabled="!selectedCities.length"
+                    link
+                    type="success"
+                    @click="addCurrentCitiesToSelected"
+                >
+                  添加
+                </el-button>
+              </div>
+            </div>
+            <el-scrollbar height="300px">
+              <div
+                  v-for="city in cityList"
+                  :key="city.code"
+                  :class="['region-item', { active: isCitySelected(city.code) || selectedCityCode === city.code }]"
+                  @click="selectCity(city)"
+              >
+                <el-checkbox
+                    :disabled="isProvinceOfCitySelected(city)"
+                    :model-value="isCitySelected(city.code)"
+                    @change="toggleCitySelection(city)"
+                    @click.stop
+                />
+                {{ city.name }}
+              </div>
+            </el-scrollbar>
+          </div>
+        </el-col>
+
+        <el-col :span="6">
+          <div class="region-selector-container">
+            <div class="selector-header">
+              <span>区县</span>
+              <div>
+                <el-button
+                    v-if="selectedCity"
+                    :disabled="!areaList.length"
+                    link
+                    type="primary"
+                    @click="addAllAreas"
+                >
+                  全部
+                </el-button>
+                <el-button
+                    :disabled="!selectedAreas.length"
+                    link
+                    type="success"
+                    @click="addCurrentAreasToSelected"
+                >
+                  添加
+                </el-button>
+              </div>
+            </div>
+            <el-scrollbar height="300px">
+              <div
+                  v-for="area in areaList"
+                  :key="area.code"
+                  :class="['region-item', { active: isAreaSelected(area.code) }]"
+                  @click="toggleAreaSelection(area)"
+              >
+                <el-checkbox
+                    :disabled="isHigherLevelRegionSelected(area)"
+                    :model-value="isAreaSelected(area.code)"
+                    @change="toggleAreaSelection(area)"
+                    @click.stop
+                />
+                {{ area.name }}
+              </div>
+            </el-scrollbar>
+          </div>
+        </el-col>
+
+        <el-col :span="6">
+          <div class="region-selector-container">
+            <div class="selector-header">
+              <span>已选择</span>
+              <el-button
+                  :disabled="!selectedRegions.length"
+                  link
+                  type="danger"
+                  @click="clearSelectedRegions"
+              >
+                清空
+              </el-button>
+            </div>
+            <el-scrollbar height="300px">
+              <div
+                  v-for="(region, index) in selectedRegions"
+                  :key="index"
+                  class="selected-region-item"
+              >
+                <span>{{ formatRegionName(region) }}</span>
+                <el-icon @click.stop="removeSelectedRegion(index)">
+                  <Close/>
+                </el-icon>
+              </div>
+              <div v-if="!selectedRegions.length" class="no-data">
+                暂无选择
+              </div>
+            </el-scrollbar>
+          </div>
+        </el-col>
+      </el-row>
+      
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="closeRegionModal">取消</el-button>
+          <el-button type="primary" @click="confirmRegions">确认选择</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import {computed, nextTick, onMounted, reactive, ref, watch} from 'vue'
+import {ElMessage, ElMessageBox} from 'element-plus'
+import {Close, DocumentAdd, Plus, Van} from '@element-plus/icons-vue'
+import {getAreas, getCities, getProvinces} from '../api/index.js'
+
+// 定义组件props
+const props = defineProps({
+  // 模板数据,用于编辑模式
+  templateData: {
+    type: Object,
+    default: null
+  },
+  // 加载状态
+  loading: {
+    type: Boolean,
+    default: false
+  }
+})
+
+// 定义组件事件
+const emit = defineEmits(['save', 'cancel'])
+
+// 判断是否为编辑模式
+const isEditMode = computed(() => {
+  return props.templateData && props.templateData.templateInfo && props.templateData.templateInfo.id
+})
+
+// 省份数据
+const provinceData = ref<any[]>([])
+
+// 模板信息
+const templateInfo = reactive({
+  id: null,
+  name: '',
+  calcMethod: 'piece', // piece, weight, volume
+  unit: 'kg' // 默认单位为 kg
+})
+
+// 默认规则
+const defaultRule = reactive({
+  first: 1,
+  firstPrice: 10,
+  nextPrice: 5
+})
+
+// 特殊区域列表
+const specialAreas = ref<any[]>([])
+
+// 地区选择模态框相关
+const showRegionModal = ref(false)
+const currentAreaIndex = ref(-1)
+
+// 按省市区选择相关
+const provinceList = ref<any[]>([]) // 省份列表
+const cityList = ref<any[]>([]) // 城市列表
+const areaList = ref<any[]>([]) // 区县列表
+const selectedProvince = ref<any>(null) // 已选择的省份
+const selectedProvinceCode = ref<string>('') // 已选择的省份编码
+const selectedCity = ref<any>(null) // 已选择的城市
+const selectedCityCode = ref<string>('') // 已选择的城市编码
+const selectedProvinces = ref<any[]>([]) // 已选择的省份列表
+const selectedCities = ref<any[]>([]) // 已选择的城市列表
+const selectedAreas = ref<any[]>([]) // 已选择的区县列表
+const selectedRegions = ref<any[]>([]) // 已选择的地区列表
+
+// 在组件挂载时加载省份数据
+onMounted(async () => {
+  try {
+    // 获取省份数据
+    const res = await getProvinces()
+    provinceData.value = res.data || []
+    provinceList.value = res.data || []
+  } catch (error) {
+    console.error('加载省份数据失败:', error)
+    // 不再使用硬编码的默认省份列表
+    provinceData.value = []
+    provinceList.value = []
+  }
+})
+
+// 选择省份
+const selectProvince = async (province: any) => {
+  selectedProvince.value = province
+  selectedProvinceCode.value = province.code
+
+  try {
+    // 调用API获取城市列表
+    const res = await getCities(province.code)
+    cityList.value = res.data || []
+    areaList.value = []
+    selectedCity.value = null
+    selectedCityCode.value = ''
+    selectedCities.value = []
+    selectedAreas.value = []
+  } catch (error) {
+    console.error('获取城市列表失败:', error)
+    cityList.value = []
+    areaList.value = []
+    selectedCity.value = null
+    selectedCityCode.value = ''
+    selectedCities.value = []
+    selectedAreas.value = []
+  }
+}
+
+// 选择城市
+const selectCity = async (city: any) => {
+  selectedCity.value = city
+  selectedCityCode.value = city.code
+
+  try {
+    // 调用API获取区县列表
+    const res = await getAreas(city.code)
+    areaList.value = res.data || []
+    selectedAreas.value = []
+  } catch (error) {
+    console.error('获取区县列表失败:', error)
+    areaList.value = []
+    selectedAreas.value = []
+  }
+}
+
+// 切换城市选择
+const toggleCitySelection = (city: any) => {
+  const index = selectedCities.value.findIndex((c: any) => c.city_id === city.code)
+  if (index > -1) {
+    // 取消选择
+    selectedCities.value.splice(index, 1)
+    // 同时取消该城市下所有区县的选择
+    selectedAreas.value = selectedAreas.value.filter((area: any) => area.city_id !== city.city_id)
+  } else {
+    // 添加选择之前检查是否已选择了所属省份
+    const isProvinceAlreadySelected = selectedProvinces.value.some(
+        (province: any) => province.province_id === city.province_id
+    )
+
+    if (isProvinceAlreadySelected) {
+      // 获取省份名称用于提示
+      const province = selectedProvinces.value.find(
+          (p: any) => p.province_id === city.province_id
+      )
+      const provinceName = province ? province.province_name : '该省份'
+      ElMessage.warning(`您已选择了${provinceName},无需再单独选择其下属城市`)
+      return
+    }
+
+    // 添加选择,直接存储完整的6个字段
+    const province = provinceList.value.find((p: any) => p.code === city.province + '0000')
+    const provinceName = province ? province.name : '未知省份'
+
+    selectedCities.value.push({
+      area_id: null,
+      city_id: city.code,
+      area_name: null,
+      city_name: city.name,
+      province_id: city.province + '0000',
+      province_name: provinceName
+    })
+  }
+}
+
+// 切换区县选择
+const toggleAreaSelection = (area: any) => {
+  const index = selectedAreas.value.findIndex((a: any) => a.area_id === area.code)
+  if (index > -1) {
+    // 取消选择
+    selectedAreas.value.splice(index, 1)
+  } else {
+    // 添加选择之前检查是否已选择了所属城市或省份
+    const isCityAlreadySelected = selectedCities.value.some(
+        (city: any) => city.city_id === area.city_id
+    )
+
+    const isProvinceAlreadySelected = selectedProvinces.value.some(
+        (province: any) => province.province_id === area.province_id
+    )
+
+    if (isCityAlreadySelected) {
+      // 获取城市名称用于提示
+      const city = selectedCities.value.find(
+          (c: any) => c.city_id === area.city_id
+      )
+      const cityName = city ? city.city_name : '该城市'
+      ElMessage.warning(`您已选择了${cityName},无需再单独选择其下属区县`)
+      return
+    }
+
+    if (isProvinceAlreadySelected) {
+      // 获取省份名称用于提示
+      const province = selectedProvinces.value.find(
+          (p: any) => p.province_id === area.province + '0000'
+      )
+      const provinceName = province ? province.province_name : '该省份'
+      ElMessage.warning(`您已选择了${provinceName},无需再单独选择其下属区县`)
+      return
+    }
+
+    // 添加选择,直接存储完整的6个字段
+    const province = provinceList.value.find((p: any) => p.code === area.province + '0000')
+    const provinceName = province ? province.name : '未知省份'
+
+    const city = cityList.value.find((c: any) => c.code === area.province + area.city + '00')
+    const cityName = city ? city.name : '未知城市'
+    
+    selectedAreas.value.push({
+      area_id: area.code,
+      city_id: area.province + area.city + '00',
+      area_name: area.name,
+      city_name: cityName,
+      province_id: area.province + '0000',
+      province_name: provinceName
+    })
+  }
+}
+
+// 切换省份选择
+const toggleProvinceSelection = (province: any) => {
+  const index = selectedProvinces.value.findIndex((p: any) => p.province_id === province.code)
+  if (index > -1) {
+    // 取消选择
+    selectedProvinces.value.splice(index, 1)
+    // 同时取消该省份下所有城市和区县的选择
+    selectedCities.value = selectedCities.value.filter((city: any) => city.province_id !== province.province_id)
+    selectedAreas.value = selectedAreas.value.filter((area: any) => area.province_id !== province.province_id)
+  } else {
+    // 添加选择,直接存储完整的6个字段
+    selectedProvinces.value.push({
+      area_id: null,
+      city_id: null,
+      area_name: null,
+      city_name: null,
+      province_id: province.code,
+      province_name: province.name
+    })
+  }
+}
+
+// 判断城市是否被选中
+const isCitySelected = (cityCode: string) => {
+  return selectedCities.value.some((city: any) => city.city_id === cityCode)
+}
+
+// 判断区县是否被选中
+const isAreaSelected = (areaCode: string) => {
+  return selectedAreas.value.some((area: any) => area.area_id === areaCode)
+}
+
+// 判断省份是否被选中
+const isProvinceSelected = (provinceCode: string) => {
+  return selectedProvinces.value.some((province: any) => province.province_id === provinceCode)
+}
+
+// 判断城市的省份是否已经被选中
+const isProvinceOfCitySelected = (city: any) => {
+  return selectedProvinces.value.some((province: any) => province.province_id === city.province_id)
+}
+
+// 判断区县的上级(城市或省份)是否已经被选中
+const isHigherLevelRegionSelected = (area: any) => {
+  // 检查所属城市是否被选中
+  const isCitySelected = selectedCities.value.some((city: any) => city.city_id === area.city_id)
+  // 检查所属省份是否被选中
+  const isProvinceSelected = selectedProvinces.value.some((province: any) => province.province_id === area.province_id)
+  return isCitySelected || isProvinceSelected
+}
+
+// 添加所有城市
+const addAllCities = () => {
+  // 过滤掉那些所属省份已经被选中的城市
+  const citiesToAdd = cityList.value.filter((city: any) => !isProvinceOfCitySelected(city))
+  selectedCities.value = citiesToAdd.map(city => {
+    const province = provinceList.value.find((p: any) => p.code === city.province + '0000')
+    const provinceName = province ? province.name : '未知省份'
+
+    return {
+      area_id: null,
+      city_id: city.code,
+      area_name: null,
+      city_name: city.name,
+      province_id: city.province + '0000',
+      province_name: provinceName
+    }
+  })
+}
+
+// 添加所有区县
+const addAllAreas = () => {
+  // 过滤掉那些所属城市或省份已经被选中的区县
+  const areasToAdd = areaList.value.filter((area: any) => !isHigherLevelRegionSelected(area))
+  selectedAreas.value = areasToAdd.map(area => {
+    const province = provinceList.value.find((p: any) => p.code === area.province + '0000')
+    const provinceName = province ? province.name : '未知省份'
+
+    const city = cityList.value.find((c: any) => c.code === area.province + area.city + '00')
+    const cityName = city ? city.name : '未知城市'
+
+    return {
+      area_id: area.code,
+      city_id: area.province + area.city + '00',
+      area_name: area.name,
+      city_name: cityName,
+      province_id: area.province + '0000',
+      province_name: provinceName
+    }
+  })
+}
+
+// 添加所有省份
+const addAllProvinces = async () => {
+  // 检查是否已选择了省份的子级(城市或区县)
+  const hasCitySelected = selectedCities.value.some((city: any) => {
+    return selectedProvinces.value.some((province: any) => province.province_id === city.province_id)
+  })
+
+  const hasAreaSelected = selectedAreas.value.some((area: any) => {
+    return selectedProvinces.value.some((province: any) => province.province_id === area.province_id)
+  })
+
+  if (hasCitySelected || hasAreaSelected) {
+    ElMessage.warning('您已选择了省份的子级区域,无法再添加全部省份')
+    return
+  }
+
+  selectedProvinces.value = provinceList.value.map(province => ({
+    area_id: null,
+    city_id: null,
+    area_name: null,
+    city_name: null,
+    province_id: province.code,
+    province_name: province.name
+  }))
+
+  // 如果省份列表不为空,加载第一个省份的城市数据
+  if (provinceList.value.length > 0) {
+    const firstProvince = provinceList.value[0]
+    selectedProvince.value = firstProvince
+    selectedProvinceCode.value = firstProvince.code
+
+    try {
+      // 调用API获取城市列表
+      const res = await getCities(firstProvince.code)
+      cityList.value = res.data || []
+      areaList.value = []
+      selectedCity.value = null
+      selectedCityCode.value = ''
+      selectedCities.value = []
+      selectedAreas.value = []
+    } catch (error) {
+      console.error('获取城市列表失败:', error)
+      cityList.value = []
+      areaList.value = []
+      selectedCity.value = null
+      selectedCityCode.value = ''
+      selectedAreas.value = []
+    }
+  }
+}
+
+// 清空已选择的地区
+const clearSelectedRegions = () => {
+  selectedRegions.value = []
+}
+
+// 移除已选择的地区
+const removeSelectedRegion = (index: number) => {
+  selectedRegions.value.splice(index, 1)
+}
+
+// 格式化地区名称显示
+const formatRegionName = (region: any) => {
+  if (!region.city_id && !region.area_id) {
+    // 省级区域,只显示省份名称
+    return region.province_name || ''
+  } else if (region.city_id && !region.area_id) {
+    // 市级区域,显示"河北省/石家庄市"格式
+    return `${region.province_name || ''}/${region.city_name || ''}`
+  } else if (region.city_id && region.area_id) {
+    // 区县级区域,显示"河北省/石家庄市/长安区"格式
+    return `${region.province_name || ''}/${region.city_name || ''}/${region.area_name || ''}`
+  }
+  return region.province_name || ''
+}
+
+// 打开地区选择模态框
+const openRegionModal = (areaIndex: number) => {
+  currentAreaIndex.value = areaIndex
+  showRegionModal.value = true
+
+  // 初始化省市选择模式数据
+  selectedProvince.value = null
+  selectedProvinceCode.value = ''
+  selectedCity.value = null
+  selectedCityCode.value = ''
+  cityList.value = []
+  areaList.value = []
+  selectedProvinces.value = []
+  selectedCities.value = []
+  selectedAreas.value = []
+
+  // 从现有数据中恢复已选择的地区
+  const regions = [...specialAreas.value[areaIndex].regions]
+  selectedRegions.value = regions
+}
+
+// 关闭地区选择模态框
+const closeRegionModal = () => {
+  showRegionModal.value = false
+  currentAreaIndex.value = -1
+
+  // 重置省市选择模式数据
+  selectedProvince.value = null
+  selectedProvinceCode.value = ''
+  selectedCity.value = null
+  selectedCityCode.value = ''
+  cityList.value = []
+  areaList.value = []
+  selectedCities.value = []
+  selectedAreas.value = []
+  selectedRegions.value = []
+}
+
+// 确认选择地区
+const confirmRegions = () => {
+  if (currentAreaIndex.value >= 0) {
+    // 将当前所有选中的省份、城市、区县添加到已选择列表
+    const currentRegions = []
+
+    // 添加选中的省份
+    for (const province of selectedProvinces.value) {
+      currentRegions.push({
+        area_id: province.area_id,
+        city_id: province.city_id,
+        area_name: province.area_name,
+        city_name: province.city_name,
+        province_id: province.province_id,
+        province_name: province.province_name
+      })
+    }
+
+    // 添加选中的城市(过滤掉所属省份已经被选中的城市)
+    for (const city of selectedCities.value) {
+      const isProvinceSelected = selectedProvinces.value.some(
+          (province: any) => province.province_id === city.province_id
+      )
+
+      if (!isProvinceSelected) {
+        currentRegions.push({
+          area_id: city.area_id,
+          city_id: city.city_id,
+          area_name: city.area_name,
+          city_name: city.city_name,
+          province_id: city.province_id,
+          province_name: city.province_name
+        })
+      }
+    }
+
+    // 添加选中的区县(过滤掉所属城市或省份已经被选中的区县)
+    for (const area of selectedAreas.value) {
+      const isCitySelected = selectedCities.value.some(
+          (city: any) => city.city_id === area.city_id
+      )
+
+      const isProvinceSelected = selectedProvinces.value.some(
+          (province: any) => province.province_id === area.province_id
+      )
+
+      if (!isCitySelected && !isProvinceSelected) {
+        currentRegions.push({
+          area_id: area.area_id,
+          city_id: area.city_id,
+          area_name: area.area_name,
+          city_name: area.city_name,
+          province_id: area.province_id,
+          province_name: area.province_name
+        })
+      }
+    }
+
+    // 合并已存在的地区和新选择的地区
+    const allRegions = [...selectedRegions.value, ...currentRegions]
+
+    // 去重处理
+    const uniqueRegions = allRegions.filter((region, index, self) =>
+            index === self.findIndex(r =>
+                r.province_id === region.province_id &&
+                r.city_id === region.city_id &&
+                r.area_id === region.area_id
+            )
+    )
+
+    // 最后进行层级关系检查,确保不会同时存在父级和子级
+    const filteredRegions = []
+    for (const region of uniqueRegions) {
+      let shouldAdd = true
+
+      if (!region.city_id && !region.area_id) {
+        // 省级区域,检查是否已存在其子级
+        const hasChild = filteredRegions.some(r =>
+            (r.province_id === region.province_id && (r.city_id || r.area_id))
+        )
+        shouldAdd = !hasChild
+      } else if (region.city_id && !region.area_id) {
+        // 市级区域,检查是否已存在父级或子级
+        const hasParent = filteredRegions.some(r =>
+            r.province_id === region.province_id && !r.city_id && !r.area_id
+        )
+        const hasChild = filteredRegions.some(r =>
+            r.city_id === region.city_id && r.area_id
+        )
+        shouldAdd = !hasParent && !hasChild
+      } else if (region.city_id && region.area_id) {
+        // 区县级区域,检查是否已存在父级
+        const hasParentProvince = filteredRegions.some(r =>
+            r.province_id === region.province_id && !r.city_id && !r.area_id
+        )
+        const hasParentCity = filteredRegions.some(r =>
+            r.province_id === region.province_id && r.city_id === region.city_id && !r.area_id
+        )
+        shouldAdd = !hasParentProvince && !hasParentCity
+      }
+
+      if (shouldAdd) {
+        filteredRegions.push(region)
+      }
+    }
+
+    specialAreas.value[currentAreaIndex.value].regions = filteredRegions
+  }
+  closeRegionModal()
+}
+
+// 监听传入的模板数据变化
+watch(() => props.templateData, (newVal) => {
+  if (newVal) {
+    // 填充表单数据用于编辑
+    templateInfo.id = newVal.templateInfo?.id || null
+    templateInfo.name = newVal.templateInfo?.name || ''
+    templateInfo.calcMethod = newVal.templateInfo?.calcMethod || 'piece'
+    templateInfo.unit = newVal.templateInfo?.unit || 'kg'
+    
+    if (newVal.defaultRule) {
+      defaultRule.first = Number(newVal.defaultRule.first) || 1
+      defaultRule.firstPrice = Number(newVal.defaultRule.firstPrice) || 0
+      defaultRule.nextPrice = Number(newVal.defaultRule.nextPrice) || 0
+    }
+    
+    if (Array.isArray(newVal.specialAreas)) {
+      specialAreas.value = newVal.specialAreas.map((area: any) => {
+        // 确保每个区域的字段都正确处理,特别是处理可能为 null 的字段
+        const processedRegions = Array.isArray(area.regions)
+            ? area.regions.map((region: any) => ({
+              province_id: region.province_id || null,
+              province_name: region.province_name || '',
+              city_id: region.city_id || null,
+              city_name: region.city_name || null,
+              area_id: region.area_id || null,
+              area_name: region.area_name || null
+            })).filter((region: any) => region.province_id) // 过滤掉没有province_id的无效区域
+            : [];
+        
+        return {
+          area_name: area.area_name || area.name || '',
+          regions: processedRegions,
+          first: Number(area.first) || 1,
+          firstPrice: Number(area.first_price || area.firstPrice) || 0,
+          nextPrice: Number(area.next_price || area.nextPrice) || 0
+        }
+      })
+    } else {
+      specialAreas.value = []
+    }
+  } else {
+    // 如果没有传入数据,重置表单
+    // 为了避免引用错误,这里手动重置表单而不是调用resetForm函数
+    templateInfo.id = null
+    templateInfo.name = ''
+    templateInfo.calcMethod = 'piece'
+    templateInfo.unit = 'kg'
+    defaultRule.first = 1
+    defaultRule.firstPrice = 10
+    defaultRule.nextPrice = 5
+    specialAreas.value = []
+  }
+}, { immediate: true })
+
+// 可用单位选项
+const unitOptions = {
+  weight: [
+    { label: '克(g)', value: 'g' },
+    { label: '千克(kg)', value: 'kg' }
+  ],
+  volume: [
+    { label: '立方厘米(cm³)', value: 'cm3' },
+    { label: '立方米(m³)', value: 'm3' }
+  ]
+}
+
+// 获取可用单位选项
+const getAvailableUnits = () => {
+  if (templateInfo.calcMethod === 'weight') {
+    return unitOptions.weight
+  } else if (templateInfo.calcMethod === 'volume') {
+    return unitOptions.volume
+  }
+  return []
+}
+
+// 获取当前显示的单位
+const getCurrentDisplayUnit = () => {
+  // 如果是按件数计费,直接返回件
+  if (templateInfo.calcMethod === 'piece') {
+    return '件'
+  }
+  
+  // 返回用户选择的具体单位
+  return templateInfo.unit
+}
+
+// 获取首值标签
+const getFirstLabel = () => {
+  switch (templateInfo.calcMethod) {
+    case 'piece': return '首件'
+    case 'weight': return '首重'
+    case 'volume': return '首体积'
+    default: return '首件'
+  }
+}
+
+// 获取首价标签
+const getFirstPriceLabel = () => {
+  switch (templateInfo.calcMethod) {
+    case 'piece': return '首件费用(元)'
+    case 'weight': return '首重费用(元)'
+    case 'volume': return '首体积费用(元)'
+    default: return '首件费用(元)'
+  }
+}
+
+// 获取续价标签
+const getNextPriceLabel = () => {
+  switch (templateInfo.calcMethod) {
+    case 'piece': return '续件单价(元)'
+    case 'weight': return '续重单价(元)'
+    case 'volume': return '续体积单价(元)'
+    default: return '续件单价(元)'
+  }
+}
+
+// 获取首值的最小值
+const getMinFirstValue = () => {
+  switch (templateInfo.calcMethod) {
+    case 'piece': return 1
+    case 'weight': return templateInfo.unit === 'g' ? 100 : 0.1
+    case 'volume': return templateInfo.unit === 'cm3' ? 1000 : 0.001
+    default: return 1
+  }
+}
+
+// 获取首值的步长
+const getFirstStep = () => {
+  switch (templateInfo.calcMethod) {
+    case 'piece': return 1
+    case 'weight': return templateInfo.unit === 'g' ? 100 : 0.1
+    case 'volume': return templateInfo.unit === 'cm3' ? 1000 : 0.001
+    default: return 1
+  }
+}
+
+// 处理计费方式变更
+const handleCalcMethodChange = async () => {
+  // 当计费方式改变时,更新所有规则的首值、步长和单位
+  defaultRule.first = getMinFirstValue()
+  
+  // 更新所有特殊区域的首值
+  specialAreas.value.forEach(area => {
+    area.first = getMinFirstValue()
+  })
+  
+  // 强制更新DOM以确保单位正确显示
+  await nextTick()
+  console.log('计费方式已更改为:', templateInfo.calcMethod)
+}
+
+// 处理单位变化
+const handleUnitChange = () => {
+  // 更新默认规则的首值、步长
+  defaultRule.first = getMinFirstValue()
+  
+  // 更新所有特殊区域的首值
+  specialAreas.value.forEach(area => {
+    area.first = getMinFirstValue()
+  })
+  
+  console.log('单位已更改为:', templateInfo.unit)
+}
+
+// 添加特殊区域
+const addSpecialArea = () => {
+  specialAreas.value.push({
+    area_name: '',
+    regions: [],
+    first: getMinFirstValue(),
+    firstPrice: 15,
+    nextPrice: 8
+  })
+}
+
+// 删除特殊区域
+const removeSpecialArea = (index: number) => {
+  ElMessageBox.confirm('确定要删除这个特殊区域吗?', '提示', {
+    type: 'warning'
+  }).then(() => {
+    specialAreas.value.splice(index, 1)
+  }).catch(() => {
+    // 取消删除
+  })
+}
+
+// 删除地区
+const removeRegion = (areaIndex: number, regionIndex: number) => {
+  specialAreas.value[areaIndex].regions.splice(regionIndex, 1)
+}
+
+// 保存模板
+const saveTemplate = () => {
+  // 数据验证
+  if (!templateInfo.name) {
+    ElMessage.error('请输入模板名称')
+    return
+  }
+  
+  // 验证默认规则
+  if (defaultRule.first <= 0) {
+    ElMessage.error('默认规则' + getFirstLabel() + '必须大于0')
+    return
+  }
+  
+  if (defaultRule.firstPrice < 0) {
+    ElMessage.error('默认规则' + getFirstPriceLabel() + '不能为负数')
+    return
+  }
+  
+  if (defaultRule.nextPrice < 0) {
+    ElMessage.error('默认规则' + getNextPriceLabel() + '不能为负数')
+    return
+  }
+  
+  // 验证特殊区域
+  for (let i = 0; i < specialAreas.value.length; i++) {
+    const area = specialAreas.value[i]
+    if (!area.area_name) {
+      ElMessage.error(`第${i + 1}个特殊区域请输入区域名称`)
+      return
+    }
+    
+    if (area.regions.length === 0) {
+      ElMessage.error(`第${i + 1}个特殊区域请选择地区`)
+      return
+    }
+    
+    if (area.first <= 0) {
+      ElMessage.error(`第${i + 1}个特殊区域` + getFirstLabel() + '必须大于0')
+      return
+    }
+    
+    if (area.firstPrice < 0) {
+      ElMessage.error(`第${i + 1}个特殊区域` + getFirstPriceLabel() + '不能为负数')
+      return
+    }
+    
+    if (area.nextPrice < 0) {
+      ElMessage.error(`第${i + 1}个特殊区域` + getNextPriceLabel() + '不能为负数')
+      return
+    }
+  }
+
+  // 处理特殊区域数据,确保只包含有效的6字段结构
+  const processedSpecialAreas = specialAreas.value.map(area => {
+    // 过滤掉无效的区域数据并确保每个区域都有province_id
+    const validRegions = area.regions
+        .filter(region => region && region.province_id)
+        .map(region => ({
+          province_id: region.province_id,
+          province_name: region.province_name || '',
+          city_id: region.city_id || null,
+          city_name: region.city_name || null,
+          area_id: region.area_id || null,
+          area_name: region.area_name || null
+        }));
+
+    return {
+      area_name: area.area_name,
+      first: area.first,
+      firstPrice: area.firstPrice,
+      nextPrice: area.nextPrice,
+      regions: validRegions
+    };
+  });
+  
+  const templateData = {
+    templateInfo: { ...templateInfo },
+    defaultRule: { ...defaultRule },
+    specialAreas: processedSpecialAreas
+  }
+  
+  emit('save', templateData)
+  const message = isEditMode.value ? '模板保存成功' : '模板创建成功'
+  ElMessage.success(message)
+  console.log('保存模板数据:', templateData)
+}
+
+// 取消操作
+const cancel = () => {
+  ElMessageBox.confirm('确定要取消吗?未保存的数据将会丢失。', '提示', {
+    type: 'warning'
+  }).then(() => {
+    emit('cancel')
+    console.log('取消操作')
+  }).catch(() => {
+    // 取消操作
+  })
+}
+
+// 重置表单
+const resetForm = () => {
+  templateInfo.id = null
+  templateInfo.name = ''
+  templateInfo.calcMethod = 'piece'
+  templateInfo.unit = 'kg'
+  defaultRule.first = 1
+  defaultRule.firstPrice = 10
+  defaultRule.nextPrice = 5
+  specialAreas.value = []
+}
+
+// 添加当前选中的省份到已选择列表
+const addCurrentProvincesToSelected = () => {
+  const newRegions = [];
+
+  // 添加选中的省份
+  for (const province of selectedProvinces.value) {
+    newRegions.push({
+      area_id: province.area_id,
+      city_id: province.city_id,
+      area_name: province.area_name,
+      city_name: province.city_name,
+      province_id: province.province_id,
+      province_name: province.province_name
+    });
+  }
+
+  // 合并到已选择区域并去重
+  const allRegions = [...selectedRegions.value, ...newRegions];
+  const uniqueRegions = allRegions.filter((region, index, self) =>
+          index === self.findIndex(r =>
+              r.province_id === region.province_id &&
+              r.city_id === region.city_id &&
+              r.area_id === region.area_id
+          )
+  );
+
+  // 检查添加省份时是否存在下级区域(已选择的城市或区县)
+  const finalRegions = [];
+  for (const region of uniqueRegions) {
+    let shouldAdd = true;
+
+    if (!region.city_id && !region.area_id) {
+      // 对于省份,检查是否存在下级区域
+      const hasChild = uniqueRegions.some(r =>
+          (r.province_id === region.province_id && (r.city_id || r.area_id))
+      );
+
+      if (hasChild) {
+        ElMessage.warning(`您已选择了${region.province_name}的下级区域,无法再添加该省份`);
+        shouldAdd = false;
+      }
+    } else if (region.city_id && !region.area_id) {
+      // 对于城市,检查是否存在上级省份或下级区县
+      const hasParent = uniqueRegions.some(r =>
+          r.province_id === region.province_id && !r.city_id && !r.area_id
+      );
+      const hasChild = uniqueRegions.some(r =>
+          r.city_id === region.city_id && r.area_id
+      );
+
+      if (hasParent) {
+        ElMessage.warning(`您已选择了${region.province_name}的上级省份,无法再添加该城市`);
+        shouldAdd = false;
+      }
+
+      if (hasChild) {
+        ElMessage.warning(`您已选择了${region.city_name}的下级区县,无法再添加该城市`);
+        shouldAdd = false;
+      }
+    } else if (region.city_id && region.area_id) {
+      // 对于区县,检查是否存在上级省份或城市
+      const hasParentProvince = uniqueRegions.some(r =>
+          r.province_id === region.province_id && !r.city_id && !r.area_id
+      );
+      const hasParentCity = uniqueRegions.some(r =>
+          r.province_id === region.province_id && r.city_id === region.city_id && !r.area_id
+      );
+
+      if (hasParentProvince) {
+        ElMessage.warning(`您已选择了${region.province_name}的上级省份,无法再添加该区县`);
+        shouldAdd = false;
+      }
+
+      if (hasParentCity) {
+        ElMessage.warning(`您已选择了${region.city_name}的上级城市,无法再添加该区县`);
+        shouldAdd = false;
+      }
+    }
+
+    if (shouldAdd) {
+      finalRegions.push(region);
+    }
+  }
+
+  selectedRegions.value = finalRegions;
+
+  // 清空当前选中状态
+  selectedProvinces.value = [];
+}
+
+// 添加当前选中的城市到已选择列表
+const addCurrentCitiesToSelected = () => {
+  const newRegions = [];
+
+  // 添加选中的城市
+  for (const city of selectedCities.value) {
+    newRegions.push({
+      province_id: city.province_id,
+      province_name: city.province_name,
+      city_id: city.city_id,
+      city_name: city.city_name,
+      area_id: city.area_id,
+      area_name: city.area_name
+    });
+  }
+
+  // 合并到已选择区域并去重
+  const allRegions = [...selectedRegions.value, ...newRegions];
+  const uniqueRegions = allRegions.filter((region, index, self) =>
+          index === self.findIndex(r =>
+              r.province_id === region.province_id &&
+              r.city_id === region.city_id &&
+              r.area_id === region.area_id
+          )
+  );
+
+  // 检查添加城市时是否存在上级或下级区域
+  const finalRegions = [];
+  for (const region of uniqueRegions) {
+    let shouldAdd = true;
+
+    if (!region.city_id && !region.area_id) {
+      // 对于省份,检查是否存在下级区域
+      const hasChild = uniqueRegions.some(r =>
+          (r.province_id === region.province_id && (r.city_id || r.area_id))
+      );
+
+      if (hasChild) {
+        ElMessage.warning(`您已选择了${region.province_name}的下级区域,无法再添加该省份`);
+        shouldAdd = false;
+      }
+    } else if (region.city_id && !region.area_id) {
+      // 对于城市,检查是否存在上级省份或下级区县
+      const hasParent = uniqueRegions.some(r =>
+          r.province_id === region.province_id && !r.city_id && !r.area_id
+      );
+      const hasChild = uniqueRegions.some(r =>
+          r.city_id === region.city_id && r.area_id
+      );
+
+      if (hasParent) {
+        ElMessage.warning(`您已选择了${region.province_name}的上级省份,无法再添加该城市`);
+        shouldAdd = false;
+      }
+
+      if (hasChild) {
+        ElMessage.warning(`您已选择了${region.city_name}的下级区县,无法再添加该城市`);
+        shouldAdd = false;
+      }
+    } else if (region.city_id && region.area_id) {
+      // 对于区县,检查是否存在上级省份或城市
+      const hasParentProvince = uniqueRegions.some(r =>
+          r.province_id === region.province_id && !r.city_id && !r.area_id
+      );
+      const hasParentCity = uniqueRegions.some(r =>
+          r.province_id === region.province_id && r.city_id === region.city_id && !r.area_id
+      );
+
+      if (hasParentProvince) {
+        ElMessage.warning(`您已选择了${region.province_name}的上级省份,无法再添加该区县`);
+        shouldAdd = false;
+      }
+
+      if (hasParentCity) {
+        ElMessage.warning(`您已选择了${region.city_name}的上级城市,无法再添加该区县`);
+        shouldAdd = false;
+      }
+    }
+
+    if (shouldAdd) {
+      finalRegions.push(region);
+    }
+  }
+
+  selectedRegions.value = finalRegions;
+
+  // 清空当前选中状态
+  selectedCities.value = [];
+}
+
+// 添加当前选中的区县到已选择列表
+const addCurrentAreasToSelected = () => {
+  const newRegions = [];
+
+  // 添加选中的区县
+  for (const area of selectedAreas.value) {
+    // 检查要添加的区县是否存在上级(已选择的省份或城市)
+    const hasParentProvince = selectedProvinces.value.some(province => province.province_id === area.province_id);
+    const hasParentCity = selectedCities.value.some(city => city.city_id === area.city_id);
+
+    if (hasParentProvince) {
+      ElMessage.warning(`您已选择了${area.name}的上级省份,无法再添加该区县`);
+      continue; // 跳过这个区县,不添加
+    }
+
+    if (hasParentCity) {
+      ElMessage.warning(`您已选择了${area.name}的上级城市,无法再添加该区县`);
+      continue; // 跳过这个区县,不添加
+    }
+
+    newRegions.push({
+      area_id: area.area_id,
+      city_id: area.city_id,
+      area_name: area.area_name,
+      city_name: area.city_name,
+      province_id: area.province_id,
+      province_name: area.province_name
+    });
+  }
+
+  // 合并到已选择区域并去重
+  const allRegions = [...selectedRegions.value, ...newRegions];
+  const uniqueRegions = allRegions.filter((region, index, self) =>
+          index === self.findIndex(r =>
+              r.province_id === region.province_id &&
+              r.city_id === region.city_id &&
+              r.area_id === region.area_id
+          )
+  );
+
+  selectedRegions.value = uniqueRegions;
+
+  // 清空当前选中状态
+  selectedAreas.value = [];
+}
+</script>
+
+<style scoped lang="scss">
+.shipping-template-component {
+  .card-header {
+    font-size: 18px;
+    font-weight: bold;
+    color: #409eff;
+    display: flex;
+    align-items: center;
+    gap: 8px;
+  }
+
+  .card-sub-header {
+    font-size: 16px;
+    font-weight: bold;
+  }
+
+  .special-area {
+    border: 1px solid #dcdfe6;
+    border-radius: 4px;
+    transition: all 0.3s;
+
+    &:hover {
+      box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+    }
+  }
+
+  .region-tags {
+    min-height: 32px;
+  }
+
+  .province-checkbox-group {
+    max-height: 300px;
+    overflow-y: auto;
+  }
+
+  .mb-4 {
+    margin-bottom: 1rem;
+  }
+
+  :deep(.el-card__header) {
+    padding: 12px 20px;
+    border-bottom: 1px solid #ebeef5;
+  }
+
+  :deep(.el-form-item) {
+    margin-bottom: 18px;
+  }
+
+  :deep(.el-form-item__label) {
+    font-weight: normal;
+  }
+
+  .input-with-unit {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+
+    .unit-text {
+      white-space: nowrap;
+      color: #606266;
+      font-size: 14px;
+    }
+  }
+
+  .form-item-tip {
+    margin-top: 4px;
+    font-size: 12px;
+    color: #909399;
+    line-height: 1.4;
+  }
+
+  .region-selector-container {
+    border: 1px solid #dcdfe6;
+    border-radius: 4px;
+    height: 350px;
+    display: flex;
+    flex-direction: column;
+
+    .selector-header {
+      padding: 8px 12px;
+      border-bottom: 1px solid #dcdfe6;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      font-weight: 500;
+    }
+
+    .region-item {
+      padding: 8px 12px;
+      cursor: pointer;
+      display: flex;
+      align-items: center;
+      gap: 8px;
+
+      &:hover {
+        background-color: #f5f7fa;
+      }
+
+      &.active {
+        background-color: #ecf5ff;
+        color: #409eff;
+      }
+    }
+
+    .selected-region-item {
+      padding: 8px 12px;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+
+      &:hover {
+        background-color: #f5f7fa;
+      }
+    }
+
+    .no-data {
+      padding: 20px;
+      text-align: center;
+      color: #909399;
+    }
+  }
+
+  .dialog-footer {
+    text-align: right;
+  }
+}
+</style>

+ 381 - 0
frontend/admin/views/ShippingTemplateList.vue

@@ -0,0 +1,381 @@
+<template>
+  <div class="shipping-template-management">
+    <h2>运费模板管理</h2>
+    
+    <!-- 模式切换 -->
+    <el-radio-group v-model="currentMode" style="margin-bottom: 20px;">
+      <el-radio-button label="create">创建模板</el-radio-button>
+      <el-radio-button label="edit">编辑模板</el-radio-button>
+    </el-radio-group>
+    
+    <!-- 创建模板 -->
+    <shipping-template-form
+      v-if="currentMode === 'create'"
+      :key="formKey"
+      @save="handleSave"
+      @cancel="handleCancel"
+    />
+    
+    <!-- 编辑模板 -->
+    <div v-else-if="currentMode === 'edit'">
+      <!-- 模板选择 -->
+      <el-card shadow="never" class="mb-4">
+        <template #header>
+          <div class="card-sub-header">
+            <span>选择要编辑的模板</span>
+          </div>
+        </template>
+        
+        <el-form :model="selectedTemplateForm" label-width="120px">
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="选择模板">
+                <el-select 
+                  v-model="selectedTemplateForm.id" 
+                  placeholder="请选择要编辑的模板"
+                  style="width: 100%"
+                  @change="handleTemplateSelect"
+                >
+                  <el-option
+                    v-for="template in templateList"
+                    :key="template.id"
+                    :label="template.name"
+                    :value="template.id"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="操作">
+                <el-button 
+                  type="danger" 
+                  :disabled="!selectedTemplateForm.id"
+                  @click="handleDeleteTemplate"
+                >
+                  删除选中模板
+                </el-button>
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </el-form>
+      </el-card>
+      
+      <!-- 模板编辑表单 -->
+      <shipping-template-form
+        v-if="editTemplateData"
+        :key="formKey"
+        :template-data="editTemplateData"
+        :loading="loading"
+        @save="handleSave"
+        @cancel="handleCancel"
+      />
+      
+      <!-- 加载提示 -->
+      <div v-if="loading && !editTemplateData" class="loading-container">
+        <el-skeleton animated />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import {onMounted, reactive, ref, watch} from 'vue'
+import {ElMessage, ElMessageBox} from 'element-plus'
+import ShippingTemplateForm from './ShippingTemplateForm.vue'
+import {
+  createShippingTemplate,
+  deleteShippingTemplate,
+  getShippingTemplateDetail,
+  getShippingTemplateList,
+  updateShippingTemplate
+} from '../api/index.js'
+
+// 当前模式
+const currentMode = ref('create')
+const formKey = ref(0)
+const loading = ref(false)
+
+// 选中的模板表单
+const selectedTemplateForm = reactive({
+  id: null
+})
+
+// 编辑模板数据
+const editTemplateData = ref(null)
+
+// 模板列表
+const templateList = ref([])
+
+// 监听模式变化,当模式切换时更新key强制重新渲染组件
+watch(currentMode, (newMode, oldMode) => {
+  // 当在create和edit之间切换时,更新key值强制组件重新渲染
+  if ((newMode === 'create' || newMode === 'edit') && (oldMode === 'create' || oldMode === 'edit')) {
+    formKey.value += 1
+    // 重置编辑相关数据
+    if (newMode === 'edit') {
+      selectedTemplateForm.id = null
+      editTemplateData.value = null
+    }
+  }
+  
+  // 根据模式加载相应数据
+  loadDataForMode(newMode)
+})
+
+// 组件挂载时加载初始数据
+onMounted(() => {
+  loadDataForMode(currentMode.value)
+})
+
+// 根据当前模式加载数据
+const loadDataForMode = async (mode) => {
+  if (mode === 'edit') {
+    // 加载模板列表数据
+    await loadTemplateList()
+  }
+}
+
+// 加载模板列表数据
+const loadTemplateList = async () => {
+  loading.value = true
+  try {
+    // 这里需要传入实际的bizType和bizId参数
+    // 示例中使用默认值
+    const response = await getShippingTemplateList('product', 1)
+    if (response.code === 200) {
+      templateList.value = response.data.map(item => ({
+        id: item.id,
+        name: item.name
+      }))
+    } else {
+      throw new Error(response.msg || '获取模板列表失败')
+    }
+  } catch (error) {
+    console.error('加载模板列表失败:', error)
+    ElMessage.error('模板列表加载失败: ' + (error.message || '未知错误'))
+  } finally {
+    loading.value = false
+  }
+}
+
+// 处理模板选择
+const handleTemplateSelect = async (templateId) => {
+  if (!templateId) {
+    editTemplateData.value = null
+    return
+  }
+  
+  loading.value = true
+  try {
+    await loadEditTemplateData(templateId)
+  } catch (error) {
+    console.error('加载模板详情失败:', error)
+    ElMessage.error('模板详情加载失败: ' + (error.message || '未知错误'))
+    selectedTemplateForm.id = null
+  } finally {
+    loading.value = false
+  }
+}
+
+// 加载编辑模板数据
+const loadEditTemplateData = async (templateId) => {
+  try {
+    const response = await getShippingTemplateDetail(templateId)
+    if (response.code === 200) {
+      // 转换API返回的数据格式以适配组件
+      const templateData = response.data
+      editTemplateData.value = {
+        templateInfo: {
+          id: templateData.id,
+          name: templateData.name,
+          calcMethod: templateData.calc_method,
+          unit: templateData.unit
+        },
+        defaultRule: {
+          first: Number(templateData.default_rule.first) || 1,
+          firstPrice: Number(templateData.default_rule.first_price) || 0,
+          nextPrice: Number(templateData.default_rule.next_price) || 0
+        },
+        specialAreas: Array.isArray(templateData.special_areas) 
+          ? templateData.special_areas.map(area => ({
+              area_name: area.area_name || area.name || '',  // 优先使用area_name字段
+              regions: Array.isArray(area.regions) 
+                ? area.regions.map(region => ({
+                    province_id: region.province_id,
+                    province_name: region.province_name,
+                    city_id: region.city_id,
+                    city_name: region.city_name,
+                    area_id: region.area_id,
+                    area_name: region.area_name
+                  }))
+                : [],
+              first: Number(area.first) || 1,
+              firstPrice: Number(area.first_price) || 0,
+              nextPrice: Number(area.next_price) || 0
+            }))
+          : []
+      }
+    } else {
+      throw new Error(response.msg || '获取模板详情失败')
+    }
+  } catch (error) {
+    throw error
+  }
+}
+
+// 处理保存模板(创建或编辑)
+const handleSave = async (templateData: any) => {
+  try {
+    let response;
+    
+    // 准备提交数据
+    const submitData = {
+      name: templateData.templateInfo.name,
+      calc_method: templateData.templateInfo.calcMethod,
+      unit: templateData.templateInfo.unit,
+      sort: 100,
+      default_rule: {
+        first: templateData.defaultRule.first,
+        first_price: templateData.defaultRule.firstPrice,
+        next_price: templateData.defaultRule.nextPrice
+      },
+      special_areas: Array.isArray(templateData.specialAreas) 
+        ? templateData.specialAreas.map((area: any) => ({
+            area_name: area.area_name,
+            first: area.first,
+            first_price: area.firstPrice,
+            next_price: area.nextPrice,
+            regions: Array.isArray(area.regions)
+                ? area.regions
+                    .filter((region: any) => region && region.province_id)
+                    .map((region: any) => ({
+                      province_id: region.province_id || null,
+                      province_name: region.province_name || '',
+                      city_id: region.city_id || null,
+                      city_name: region.city_name || null,
+                      area_id: region.area_id || null,
+                      area_name: region.area_name || null
+                    }))
+              : []
+          }))
+        : []
+    }
+
+    // 调试日志,查看提交的数据结构
+    console.log('提交数据结构:', submitData);
+    console.log('templateData:', templateData);
+    
+    // 根据当前模式判断是创建还是更新模板
+    if (currentMode.value === 'edit' && templateData.templateInfo.id) {
+      // 更新模板
+      response = await updateShippingTemplate(templateData.templateInfo.id, submitData)
+    } else {
+      // 创建模板
+      response = await createShippingTemplate(submitData)
+    }
+
+    if (response.code === 200) {
+      const message = currentMode.value === 'edit' && templateData.templateInfo.id ? '模板更新成功' : '模板创建成功'
+      ElMessage.success(message)
+      
+      // 如果是创建模板,重置表单
+      if (currentMode.value === 'create') {
+        formKey.value += 1  // 通过更新key来重置表单
+      }
+      
+      // 如果是编辑模板,刷新模板列表
+      if (currentMode.value === 'edit') {
+        await loadTemplateList()
+      }
+      
+      console.log('保存模板:', templateData)
+    } else {
+      ElMessage.error(response.msg || '操作失败')
+    }
+  } catch (error) {
+    console.error('保存模板失败:', error)
+    ElMessage.error('操作失败: ' + (error.message || '未知错误'))
+  }
+}
+
+// 处理取消操作
+const handleCancel = () => {
+  ElMessage.info('操作已取消')
+  console.log('取消操作')
+  
+  // 重置选择
+  if (currentMode.value === 'edit') {
+    selectedTemplateForm.id = null
+    editTemplateData.value = null
+  } else if (currentMode.value === 'create') {
+    // 在创建模式下,重置表单
+    formKey.value += 1  // 通过更新key来重置表单
+  }
+}
+
+// 处理删除模板
+const handleDeleteTemplate = async () => {
+  if (!selectedTemplateForm.id) {
+    ElMessage.warning('请先选择要删除的模板')
+    return
+  }
+  
+  try {
+    // 获取选中模板的名称用于提示
+    const selectedTemplate = templateList.value.find(t => t.id === selectedTemplateForm.id)
+    const templateName = selectedTemplate ? selectedTemplate.name : '未知模板'
+    
+    await ElMessageBox.confirm(
+      `确定要删除运费模板"${templateName}"吗?此操作不可恢复!`,
+      '删除确认',
+      {
+        type: 'warning',
+        confirmButtonText: '确定删除',
+        cancelButtonText: '取消'
+      }
+    )
+    
+    const response = await deleteShippingTemplate(selectedTemplateForm.id)
+    
+    if (response.code === 200) {
+      ElMessage.success('模板删除成功')
+      // 重置选择
+      selectedTemplateForm.id = null
+      editTemplateData.value = null
+      // 重新加载模板列表
+      await loadTemplateList()
+    } else {
+      throw new Error(response.msg || '删除失败')
+    }
+  } catch (error) {
+    if (error !== 'cancel') {
+      console.error('删除模板失败:', error)
+      ElMessage.error('删除失败: ' + (error.message || '未知错误'))
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.shipping-template-management {
+  padding: 20px;
+  
+  h2 {
+    margin-bottom: 20px;
+    color: #303133;
+  }
+  
+  .card-sub-header {
+    font-size: 16px;
+    font-weight: bold;
+  }
+  
+  .loading-container {
+    padding: 20px;
+  }
+  
+  .mb-4 {
+    margin-bottom: 1rem;
+  }
+}
+</style>