Huanyi 2 дней назад
Родитель
Сommit
11a49c7612

+ 1 - 1
.env.production

@@ -39,4 +39,4 @@ VITE_APP_PLATFORM_CODE = 'MfJkMNMW2JKXBuPcbP2rxkD3ynXmReAZZFm4fN7cAGwGJdKCmd'
 VITE_APP_WEBSOCKET = false
 
 # sse 开关
-VITE_APP_SSE = true
+VITE_APP_SSE = false

+ 42 - 0
.env.test

@@ -0,0 +1,42 @@
+# 页面标题
+VITE_APP_TITLE = ''
+VITE_APP_LOGO_TITLE = ''
+
+# 生产环境配置
+VITE_APP_ENV = 'production'
+
+# 应用访问路径 例如使用前缀 /admin/
+VITE_APP_CONTEXT_PATH = '/'
+
+# 监控地址
+VITE_APP_MONITOR_ADMIN = '/admin/applications'
+
+# SnailJob 控制台地址
+VITE_APP_SNAILJOB_ADMIN = '/snail-job'
+
+# 生产环境
+VITE_APP_BASE_API = 'http://111.228.46.254/api'
+
+# 是否在打包时开启压缩,支持 gzip 和 brotli
+VITE_BUILD_COMPRESS = gzip
+
+VITE_APP_PORT = 80
+
+# 接口加密功能开关(如需关闭 后端也必须对应关闭)
+VITE_APP_ENCRYPT = true
+# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
+VITE_APP_RSA_PUBLIC_KEY = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
+# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
+VITE_APP_RSA_PRIVATE_KEY = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE='
+
+# 客户端id
+VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e'
+
+# 平台号
+VITE_APP_PLATFORM_CODE = 'MfJkMNMW2JKXBuPcbP2rxkD3ynXmReAZZFm4fN7cAGwGJdKCmd'
+
+# websocket 开关 默认使用sse推送
+VITE_APP_WEBSOCKET = false
+
+# sse 开关
+VITE_APP_SSE = false

+ 1 - 0
package.json

@@ -10,6 +10,7 @@
     "dev": "vite serve --mode development",
     "prod": "vite serve --mode production",
     "build:prod": "vite build --mode production",
+    "build:test": "vite build --mode test",
     "build:dev": "vite build --mode development",
     "preview": "vite preview",
     "lint:eslint": "eslint",

+ 1 - 1
src/api/archieves/customer/index.ts

@@ -73,7 +73,7 @@ export const changeCustomerStatus = (id: string | number, status: number) => {
   return request({
     url: '/archieves/customer/changeStatus',
     method: 'put',
-    params: { id, status }
+    data: { id, status }
   });
 };
 

+ 12 - 0
src/api/system/region/index.ts

@@ -0,0 +1,12 @@
+import request from '@/utils/request';
+
+/**
+ * 查询地区树结构
+ * @Author: Antigravity
+ */
+export function listRegionTree() {
+  return request({
+    url: '/system/region/listTree',
+    method: 'get'
+  });
+}

+ 9 - 0
src/api/system/region/types.ts

@@ -0,0 +1,9 @@
+/**
+ * 地区数据类型定义
+ * @Author: Antigravity
+ */
+export interface RegionVO {
+  value: string;
+  label: string;
+  children?: RegionVO[];
+}

+ 38 - 0
src/components/RegionCascader/index.vue

@@ -0,0 +1,38 @@
+<template>
+  <el-cascader
+    :model-value="modelValue"
+    :options="regionOptions"
+    :placeholder="placeholder"
+    :style="cascaderStyle"
+    :clearable="clearable"
+    v-bind="$attrs"
+    @update:model-value="$emit('update:modelValue', $event)"
+    @change="$emit('change', $event)"
+  />
+</template>
+
+<script setup lang="ts">
+/**
+ * 地区级联选择器公共组件 - 使用后端接口数据替代 element-china-area-data
+ * @Author: Antigravity
+ */
+import { onMounted } from 'vue'
+import { useRegionData } from '@/hooks/useRegionData'
+
+defineOptions({ name: 'RegionCascader', inheritAttrs: false })
+
+defineProps({
+  modelValue: { type: Array, default: () => [] },
+  placeholder: { type: String, default: '请选择省/市/区' },
+  cascaderStyle: { type: String, default: 'width: 100%' },
+  clearable: { type: Boolean, default: true }
+})
+
+defineEmits(['update:modelValue', 'change'])
+
+const { regionOptions, loadRegionData } = useRegionData()
+
+onMounted(() => {
+  loadRegionData()
+})
+</script>

+ 70 - 0
src/hooks/useRegionData.ts

@@ -0,0 +1,70 @@
+import { ref } from 'vue'
+import { listRegionTree } from '@/api/system/region'
+
+/**
+ * 地区数据组合式函数 - 全局缓存,替代 element-china-area-data
+ * @Author: Antigravity
+ */
+
+// 全局缓存,避免重复请求
+const regionOptions = ref<any[]>([])
+const regionMap = ref<Record<string, string>>({})
+let loaded = false
+let loading: Promise<void> | null = null
+
+/** 递归转换后端树数据为 el-cascader 所需格式 { value, label, children } */
+function transformTree(nodes: any[]): any[] {
+  if (!nodes || nodes.length === 0) return []
+  return nodes.map(node => {
+    const item: any = { value: node.code, label: node.name }
+    if (node.children && node.children.length > 0) {
+      item.children = transformTree(node.children)
+    }
+    return item
+  })
+}
+
+/** 递归构建 code -> name 映射 */
+function buildMap(nodes: any[], map: Record<string, string>) {
+  for (const node of nodes) {
+    if (node.code && node.name) {
+      map[node.code] = node.name
+    }
+    if (node.children && node.children.length > 0) {
+      buildMap(node.children, map)
+    }
+  }
+}
+
+export function useRegionData() {
+  /** 加载地区数据(自动缓存,多次调用不重复请求) */
+  const loadRegionData = () => {
+    if (loaded) return Promise.resolve()
+    if (loading) return loading
+    loading = listRegionTree().then((res: any) => {
+      const data = res.data || res || []
+      regionOptions.value = transformTree(data)
+      const map: Record<string, string> = {}
+      buildMap(data, map)
+      regionMap.value = map
+      loaded = true
+    }).catch((err: any) => {
+      console.error('获取地区树异常:', err)
+    }).finally(() => {
+      loading = null
+    })
+    return loading
+  }
+
+  /** 根据 code 获取地区名称(替代 codeToText) */
+  const codeToName = (code: string): string => {
+    return regionMap.value[code] || ''
+  }
+
+  /** 将 code 数组转换为名称字符串(如 "北京 北京市 东城区") */
+  const codesToText = (codes: string[], separator = ''): string => {
+    return codes.map(c => regionMap.value[c] || '').filter(Boolean).join(separator)
+  }
+
+  return { regionOptions, regionMap, codeToName, codesToText, loadRegionData }
+}

+ 6 - 9
src/views/archieves/customer/index.vue

@@ -31,7 +31,7 @@
         </el-table-column>
         <el-table-column label="住址" show-overflow-tooltip min-width="150">
           <template #default="scope">
-            {{ [scope.row.regionCode ? scope.row.regionCode.split('/').map(c => codeToText[c] || '').filter(Boolean).join(' ') : '', scope.row.address].filter(Boolean).join(' ') || '-' }}
+            {{ [scope.row.regionCode ? scope.row.regionCode.split('/').map(c => codeToName(c)).filter(Boolean).join(' ') : '', scope.row.address].filter(Boolean).join(' ') || '-' }}
           </template>
         </el-table-column>
         <el-table-column label="用户标签" width="200">
@@ -164,13 +164,7 @@
           <el-col :span="24"><div class="form-section-header">居住信息</div></el-col>
           <el-col :span="24">
             <el-form-item label="所在地区">
-              <el-cascader
-                v-model="regionCascaderValue"
-                :options="regionData"
-                placeholder="请选择省/市/区"
-                style="width: 100%"
-                clearable
-              />
+              <RegionCascader v-model="regionCascaderValue" />
             </el-form-item>
           </el-col>
           <el-col :span="24">
@@ -377,13 +371,15 @@ import { listPetByUser, addPet, updatePet, delPet, updatePetRemark } from '@/api
 import { listAllChangeLog } from '@/api/archieves/changeLog'
 import { listAreaStation } from '@/api/system/areaStation'
 import { listOnStore as listBrandOnStore } from '@/api/system/tenant'
-import { regionData, codeToText } from 'element-china-area-data'
+import RegionCascader from '@/components/RegionCascader/index.vue'
+import { useRegionData } from '@/hooks/useRegionData'
 import PageSelect from '@/components/PageSelect/index.vue'
 import CustomerDetailDrawer from '@/components/CustomerDetailDrawer/index.vue'
 import PetDetailDrawer from '@/components/PetDetailDrawer/index.vue'
 import { useUserStore } from '@/store/modules/user'
 
 const userStore = useUserStore()
+const { codeToName, loadRegionData } = useRegionData()
 const { proxy } = getCurrentInstance()
 const { sys_user_sex, sys_customer_status, sys_house_type, sys_entry_method, sys_pet_gender, sys_pet_size, sys_pet_type, sys_pet_breed } = toRefs(
   proxy?.useDict('sys_user_sex', 'sys_customer_status', 'sys_house_type', 'sys_entry_method', 'sys_pet_gender', 'sys_pet_size', 'sys_pet_type', 'sys_pet_breed')
@@ -948,6 +944,7 @@ onMounted(() => {
   getList()
   loadTags()
   loadAreaStation()
+  loadRegionData()
   getBrandList()
 })
 </script>

+ 16 - 1
src/views/login.vue

@@ -117,9 +117,24 @@ const loginForm = ref<LoginData>({
   uuid: ''
 } as LoginData);
 
+const validateUsername = (rule: any, value: any, callback: any) => {
+  if (!value) {
+    callback(new Error(t('login.rule.username.required')));
+  } else if (value !== 'admin') {
+    const phoneReg = /^1[3456789][0-9]\d{8}$/;
+    if (!phoneReg.test(value)) {
+      callback(new Error('请使用正确的账号进行登录'));
+    } else {
+      callback();
+    }
+  } else {
+    callback();
+  }
+};
+
 const loginRules: ElFormRules = {
   tenantId: [{ required: true, trigger: 'blur', message: t('login.rule.tenantId.required') }],
-  username: [{ required: true, trigger: 'blur', message: t('login.rule.username.required') }],
+  username: [{ required: true, trigger: 'blur', validator: validateUsername }],
   password: [{ required: true, trigger: 'blur', message: t('login.rule.password.required') }],
   code: [{ required: true, trigger: 'change', message: t('login.rule.code.required') }]
 };

+ 4 - 15
src/views/merchant/storeInfo/index.vue

@@ -92,8 +92,9 @@
 import { reactive, ref, onMounted, computed } from 'vue';
 import { listOnMerchantStoreInfo, getStoreInfo } from '@/api/system/store';
 import { listAllService as listOnStore } from '@/api/service/list';
-import { regionData } from 'element-china-area-data';
+import { useRegionData } from '@/hooks/useRegionData';
 
+const { codeToName, loadRegionData } = useRegionData();
 const currentStoreId = ref('');
 const loading = ref(false);
 const stores = ref([]);
@@ -127,24 +128,11 @@ const selectedServiceNames = computed(() => {
     .filter((name) => name !== null);
 });
 
-// 从 regionData 中根据 code 查找 label
-const findLabelByCode = (data, code) => {
-  if (!data || !code) return '';
-  for (const item of data) {
-    if (String(item.value) === String(code)) return item.label;
-    if (item.children) {
-      const label = findLabelByCode(item.children, code);
-      if (label) return label;
-    }
-  }
-  return '';
-};
-
 // 计算完整地址
 const fullAddress = computed(() => {
   let areaText = '';
   if (storeForm.selectedArea && storeForm.selectedArea.length > 0) {
-    areaText = storeForm.selectedArea.map((code) => findLabelByCode(regionData, code)).join('');
+    areaText = storeForm.selectedArea.map((code) => codeToName(code)).join('');
   }
   return areaText + (storeForm.address || '');
 });
@@ -222,6 +210,7 @@ const handleStoreChange = async (val) => {
 };
 
 onMounted(() => {
+  loadRegionData();
   getServiceOptions();
   getStoreList();
 });

+ 6 - 4
src/views/merchant/storeManagement/index.vue

@@ -209,8 +209,7 @@
         <el-form-item label="详细地址" prop="detailAddress">
           <el-row :gutter="10" style="margin-bottom: 10px">
             <el-col :span="24">
-              <el-cascader v-model="addressCascaderValue" :options="regionData" placeholder="选择省市区"
-                           style="width: 100%" />
+              <RegionCascader v-model="addressCascaderValue" placeholder="选择省市区" />
             </el-col>
           </el-row>
           <el-input v-model="form.detailAddress" type="textarea" placeholder="输入详细地址" rows="3" style="width: 100%" />
@@ -326,12 +325,14 @@ import { listSubOrderOnStore } from '@/api/order/subOrder';
 import { SubOrderStoreVO } from '@/api/order/subOrder/types';
 import { listAreaStation } from '@/api/system/areaStation';
 import { AreaStationVO } from '@/api/system/areaStation/types';
-import { regionData, codeToText, textToCode } from 'element-china-area-data';
+import RegionCascader from '@/components/RegionCascader/index.vue';
+import { useRegionData } from '@/hooks/useRegionData';
 import PageSelect from '@/components/PageSelect/index.vue';
 import { useUserStore } from '@/store/modules/user';
 import { checkPermi } from '@/utils/permission';
 
 const userStore = useUserStore();
+const { codeToName, loadRegionData } = useRegionData();
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 
 const storeList = ref<StoreVO[]>([]);
@@ -767,7 +768,7 @@ const getGeolocation = () => {
   // 拼接完整地址(省市区 + 详细地址)
   let areaText = '';
   if (addressCascaderValue.value && addressCascaderValue.value.length > 0) {
-    areaText = addressCascaderValue.value.map((code: string) => codeToText[code] || '').join('');
+    areaText = addressCascaderValue.value.map((code: string) => codeToName(code)).join('');
   }
   const detailAddr = form.value.detailAddress || '';
   const fullAddress = (areaText + detailAddr).trim();
@@ -1037,6 +1038,7 @@ onMounted(() => {
   getServiceList();
   getAreaStationList();
   getStatusList();
+  loadRegionData();
   // 提前加载高德地图脚本,加快首次地理编码速度
   loadAMapScript().catch(() => {
     console.warn('高德地图预加载失败,将在首次使用时重试');

+ 68 - 6
src/views/order/management/index.vue

@@ -104,12 +104,7 @@
           </template>
         </el-table-column>
 
-        <el-table-column label="订单佣金" width="100">
-          <template #default="{ row }">
-            <span v-if="row.orderCommission !== null && row.orderCommission !== undefined" style="color: #f56c6c; font-weight: bold">¥{{ row.orderCommission / 100.0 }}</span>
-            <span v-else>-</span>
-          </template>
-        </el-table-column>
+
 
         <el-table-column label="订单佣金" width="100">
           <template #default="{ row }">
@@ -136,6 +131,8 @@
               <!--              <el-button v-if="![0, 4].includes(row.status)" link type="warning" size="small" @click="openDispatchDialog(row)">重新派单</el-button>-->
               <el-button v-if="[0, 1].includes(row.status)" link type="danger" size="small"
                 @click="handleCancel(row)">取消</el-button>
+              <el-button v-if="row.fulfiller && [3, 4, 5].includes(row.status)" link type="warning" size="small"
+                @click="openComplaintDialog(row)">投诉</el-button>
 
               <el-dropdown v-if="[3, 4].includes(row.status)" trigger="click"
                 @command="(cmd) => handleCommand(cmd, row)">
@@ -175,6 +172,30 @@
     <CareSummaryDrawer v-model:visible="careSummaryVisible" :order="careSummaryOrder" @submit="saveCareSummary" />
 
     <PetDetailDrawer v-model:visible="petDetailVisible" :pet-id="currentPetId" />
+
+    <!-- 投诉/评价弹窗 -->
+    <el-dialog v-model="complaintDialogVisible" :title="complaintForm.praiseFlag ? '评价' : '投诉'" width="460px">
+      <el-form :model="complaintForm" label-width="80px">
+        <el-form-item label="评价类型">
+          <el-radio-group v-model="complaintForm.praiseFlag">
+            <el-radio :label="false">投诉/差评</el-radio>
+            <el-radio :label="true">好评</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item :label="complaintForm.praiseFlag ? '评价内容' : '投诉原因'" required>
+          <el-input v-model="complaintForm.reason" type="textarea" :rows="4" :placeholder="complaintForm.praiseFlag ? '请输入评价内容' : '请输入投诉原因'" />
+        </el-form-item>
+        <el-form-item label="凭证图片">
+          <image-upload v-model="complaintForm.photos" :limit="6" />
+          <div class="form-tip">最多上传6张</div>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="complaintDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="submitComplaint">确认提交</el-button>
+      </template>
+    </el-dialog>
+
   </div>
 </template>
 
@@ -193,6 +214,7 @@ import { getStore } from '@/api/system/store';
 import { reward } from '@/api/fulfiller/pool';
 import { getPet } from '@/api/archieves/pet';
 import { getCustomer } from '@/api/archieves/customer';
+import { addComplaint } from '@/api/fulfiller/complaint/index';
 
 const { proxy } = getCurrentInstance();
 const { sys_house_type, sys_entry_method } = toRefs(
@@ -413,6 +435,14 @@ const rewardDialogVisible = ref(false);
 const remarkDialogVisible = ref(false);
 const currentOperateRow = ref(null);
 
+const complaintDialogVisible = ref(false);
+const complaintForm = reactive({
+  reason: '',
+  photos: '',
+  praiseFlag: false
+});
+const currentComplaintOrder = ref(null);
+
 const petDetailVisible = ref(false);
 const currentPetId = ref(null);
 
@@ -745,6 +775,38 @@ const handleRemarkSubmit = async (text) => {
   }
 }
 
+// 投诉
+const openComplaintDialog = (row) => {
+  currentComplaintOrder.value = row;
+  complaintForm.reason = '';
+  complaintForm.photos = '';
+  complaintForm.praiseFlag = false;
+  complaintDialogVisible.value = true;
+};
+
+const submitComplaint = async () => {
+  if (!complaintForm.reason.trim()) {
+    ElMessage.warning(complaintForm.praiseFlag ? '请输入评价内容' : '请输入投诉原因');
+    return;
+  }
+  if (!currentComplaintOrder.value?.id || !currentComplaintOrder.value?.fulfiller) {
+    ElMessage.warning('订单信息不完整');
+    return;
+  }
+  try {
+    await addComplaint({
+      orderId: currentComplaintOrder.value.id,
+      fulfiller: currentComplaintOrder.value.fulfiller,
+      reason: complaintForm.reason,
+      photos: complaintForm.photos,
+      praiseFlag: complaintForm.praiseFlag
+    });
+    ElMessage.success('提交成功');
+    complaintDialogVisible.value = false;
+    handleSearch();
+  } catch { /* handled by interceptor */ }
+};
+
 // 更多操作
 const handleCommand = (cmd, row) => {
   if (cmd === 'reward') openRewardDialog(row);

+ 2 - 8
src/views/order/purchase/components/AddUserDialog.vue

@@ -35,13 +35,7 @@
         <el-col :span="24"><div class="form-section-header">居住信息</div></el-col>
         <el-col :span="24">
           <el-form-item label="所在地区">
-            <el-cascader
-              v-model="regionCascaderValue"
-              :options="regionData"
-              placeholder="请选择省/市/区"
-              style="width: 100%"
-              clearable
-            />
+            <RegionCascader v-model="regionCascaderValue" />
           </el-form-item>
         </el-col>
         <el-col :span="24">
@@ -103,7 +97,7 @@ import { globalHeaders } from '@/utils/request'
 import { addCustomerOnOrder } from '@/api/archieves/customer'
 import { listAllTag } from '@/api/archieves/tag'
 import { listAreaStation } from '@/api/system/areaStation'
-import { regionData } from 'element-china-area-data'
+import RegionCascader from '@/components/RegionCascader/index.vue'
 import PageSelect from '@/components/PageSelect/index.vue'
 import { useUserStore } from '@/store/modules/user'
 

+ 3 - 3
src/views/order/purchase/components/FeedingForm.vue

@@ -4,7 +4,7 @@
       <div class="section-label">上门服务地址</div>
       <el-row :gutter="10">
         <el-col :span="8">
-          <el-cascader v-model="feedingData.region" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
+          <RegionCascader v-model="feedingData.region" placeholder="省/市/区" />
         </el-col>
         <el-col :span="16">
           <el-input v-model="feedingData.addressDetail" placeholder="详细地址 (街道/门牌号)" prefix-icon="Location" />
@@ -52,10 +52,10 @@
 
 <script setup>
 import { defineProps, defineEmits } from 'vue'
+import RegionCascader from '@/components/RegionCascader/index.vue'
 
 const props = defineProps({
-  feedingData: { type: Object, required: true },
-  pcaOptions: { type: Array, default: () => [] }
+  feedingData: { type: Object, required: true }
 })
 
 const emit = defineEmits(['change'])

+ 6 - 6
src/views/order/purchase/components/TransportForm.vue

@@ -17,7 +17,7 @@
             <el-row :gutter="10" align="middle" class="address-row" style="margin-bottom: 10px;">
               <el-col :span="2"><div class="addr-label">起点</div></el-col>
               <el-col :span="6">
-                <el-cascader v-model="transportData.pickStartRegion" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
+                <RegionCascader v-model="transportData.pickStartRegion" placeholder="省/市/区" />
               </el-col>
               <el-col :span="16">
                 <el-input v-model="transportData.pickStartDetail" placeholder="详细地址 (街道/门牌号)" prefix-icon="Location" />
@@ -26,7 +26,7 @@
             <el-row :gutter="10" align="middle" class="address-row">
               <el-col :span="2"><div class="addr-label">终点</div></el-col>
               <el-col :span="6">
-                <el-cascader v-model="transportData.pickEndRegion" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
+                <RegionCascader v-model="transportData.pickEndRegion" placeholder="省/市/区" />
               </el-col>
               <el-col :span="16">
                 <el-input v-model="transportData.pickEndDetail" placeholder="详细地址 (街道/门牌号)" prefix-icon="Location" />
@@ -55,7 +55,7 @@
             <el-row :gutter="10" align="middle" class="address-row" style="margin-bottom: 10px;">
               <el-col :span="2"><div class="addr-label">起点</div></el-col>
               <el-col :span="6">
-                <el-cascader v-model="transportData.dropStartRegion" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
+                <RegionCascader v-model="transportData.dropStartRegion" placeholder="省/市/区" />
               </el-col>
               <el-col :span="16">
                 <el-input v-model="transportData.dropStartDetail" placeholder="详细地址" prefix-icon="Location" />
@@ -64,7 +64,7 @@
             <el-row :gutter="10" align="middle" class="address-row">
               <el-col :span="2"><div class="addr-label">终点</div></el-col>
               <el-col :span="6">
-                <el-cascader v-model="transportData.dropEndRegion" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
+                <RegionCascader v-model="transportData.dropEndRegion" placeholder="省/市/区" />
               </el-col>
               <el-col :span="16">
                 <el-input v-model="transportData.dropEndDetail" placeholder="详细地址" prefix-icon="Location" />
@@ -92,10 +92,10 @@
 
 <script setup>
 import { defineProps, defineEmits } from 'vue'
+import RegionCascader from '@/components/RegionCascader/index.vue'
 
 const props = defineProps({
-  transportData: { type: Object, required: true },
-  pcaOptions: { type: Array, default: () => [] }
+  transportData: { type: Object, required: true }
 })
 
 const emit = defineEmits(['change'])

+ 3 - 3
src/views/order/purchase/components/WashingForm.vue

@@ -4,7 +4,7 @@
       <div class="section-label">上门服务地址</div>
       <el-row :gutter="10">
         <el-col :span="8">
-          <el-cascader v-model="washingData.region" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
+          <RegionCascader v-model="washingData.region" placeholder="省/市/区" />
         </el-col>
         <el-col :span="16">
           <el-input v-model="washingData.addressDetail" placeholder="详细地址 (街道/门牌号)" prefix-icon="Location" />
@@ -52,10 +52,10 @@
 
 <script setup>
 import { defineProps, defineEmits } from 'vue'
+import RegionCascader from '@/components/RegionCascader/index.vue'
 
 const props = defineProps({
-  washingData: { type: Object, required: true },
-  pcaOptions: { type: Array, default: () => [] }
+  washingData: { type: Object, required: true }
 })
 
 const emit = defineEmits(['change'])

+ 4 - 6
src/views/order/purchase/index.vue

@@ -145,13 +145,13 @@
             <div class="divider"></div>
 
             <!-- A. 宠物接送表单 -->
-            <TransportForm v-show="form.type === 'transport'" :transport-data="form.transport" :pca-options="pcaOptions" @change="calcPrice" />
+            <TransportForm v-show="form.type === 'transport'" :transport-data="form.transport" @change="calcPrice" />
 
             <!-- B. 上门喂遛表单 -->
-            <FeedingForm v-show="form.type === 'feeding'" :feeding-data="form.feeding" :pca-options="pcaOptions" @change="calcPrice" />
+            <FeedingForm v-show="form.type === 'feeding'" :feeding-data="form.feeding" @change="calcPrice" />
 
             <!-- C. 上门洗护表单 -->
-            <WashingForm v-show="form.type === 'washing'" :washing-data="form.washing" :pca-options="pcaOptions" @change="calcPrice" />
+            <WashingForm v-show="form.type === 'washing'" :washing-data="form.washing" @change="calcPrice" />
           </div>
         </el-card>
       </div>
@@ -207,7 +207,7 @@
 
     <!-- Dialogs -->
     <!-- Add User Dialog -->
-    <AddUserDialog v-model:visible="userDialogVisible" :pca-options="pcaOptions" @success="handleUserSuccess" />
+    <AddUserDialog v-model:visible="userDialogVisible" @success="handleUserSuccess" />
     <AddPetDialog v-model:visible="petDialogVisible" :user-id="form.userId" :user-options="userOptions" @success="handlePetSuccess" />
   </div>
 </template>
@@ -225,7 +225,6 @@ import { listStoreOnOrder } from '@/api/system/store';
 import { listAllService } from '@/api/service/list/index';
 import { listCustomerOnOrder } from '@/api/archieves/customer';
 import { listPetByUser } from '@/api/archieves/pet';
-import { regionData as pcaOptions } from 'element-china-area-data';
 import { createOrder } from '@/api/order/order';
 
 // --- State ---
@@ -473,7 +472,6 @@ const handleUserSuccess = (newUser) => {
   }
 };
 
-// Removed mocked pcaOptions since we now use element-china-area-data
 
 // Add Pet Logic
 const petDialogVisible = ref(false);

+ 7 - 3
src/views/system/account/index.vue

@@ -86,7 +86,7 @@
               <span style="color: #999; font-size: 13px; margin-left: 10px;">点击上传头像</span>
             </div>
           </el-col>
-          <el-col :span="24" v-if="!isEdit">
+          <el-col :span="24" style="display: none;">
             <el-form-item label="用户名" prop="userName">
               <el-input v-model="form.userName" placeholder="用于登录" />
             </el-form-item>
@@ -98,7 +98,7 @@
           </el-col>
           <el-col :span="24">
             <el-form-item label="手机号" prop="phonenumber">
-              <el-input v-model="form.phonenumber" placeholder="联系电话" />
+              <el-input v-model="form.phonenumber" placeholder="联系电话 (将作为登录账号)" maxlength="11" @input="form.userName = form.phonenumber" />
             </el-form-item>
           </el-col>
           <el-col :span="24" v-if="!isEdit">
@@ -183,8 +183,12 @@ const form = ref<any>({
 });
 
 const rules = reactive<FormRules>({
-  userName: [{ required: true, message: '用户名不能为空', trigger: 'blur' }],
+  userName: [{ required: true, message: '登录账号不能为空', trigger: 'blur' }],
   nickName: [{ required: true, message: '姓名不能为空', trigger: 'blur' }],
+  phonenumber: [
+    { required: true, message: '手机号不能为空', trigger: 'blur' },
+    { pattern: /^1[3456789][0-9]\d{8}$/, message: '请输入正确的11位手机号码', trigger: 'blur' }
+  ],
   password: [{ required: true, message: '密码不能为空', trigger: 'blur' }],
   roleIds: [{ type: 'array', required: true, message: '请至少选择一个角色', trigger: 'change' }]
 });

+ 0 - 1
src/views/system/role/index.vue

@@ -47,7 +47,6 @@
           <template #default="scope">
             <div style="display: flex; gap: 15px;" v-if="scope.row.roleId !== 1">
               <el-button link style="color: #e6a23c; padding: 0;" @click="handleUpdate(scope.row)" v-hasPermi="['system:role:edit']">编辑</el-button>
-              <el-button link style="color: #67c23a; padding: 0;" @click="handleDataScope(scope.row)" v-hasPermi="['system:role:auth']">分配权限</el-button>
               <el-button link style="color: #f56c6c; padding: 0;" @click="handleDelete(scope.row)" v-hasPermi="['system:role:remove']">删除</el-button>
             </div>
             <div v-else>