Explorar o código

为订单管理和后续档案管理做准备

Huanyi hai 1 mes
pai
achega
94c7934f3f

+ 6 - 6
src/views/login.vue

@@ -5,12 +5,12 @@
         <h3 class="title">{{ title }}</h3>
         <lang-select />
       </div>
-      <el-form-item v-if="tenantEnabled" prop="tenantId">
-        <el-select v-model="loginForm.tenantId" filterable :placeholder="proxy.$t('login.selectPlaceholder')" style="width: 100%">
-          <el-option v-for="item in tenantList" :key="item.tenantId" :label="item.companyName" :value="item.tenantId"></el-option>
-          <template #prefix><svg-icon icon-class="company" class="el-input__icon input-icon" /></template>
-        </el-select>
-      </el-form-item>
+<!--      <el-form-item v-if="tenantEnabled" prop="tenantId">-->
+<!--        <el-select v-model="loginForm.tenantId" filterable :placeholder="proxy.$t('login.selectPlaceholder')" style="width: 100%">-->
+<!--          <el-option v-for="item in tenantList" :key="item.tenantId" :label="item.companyName" :value="item.tenantId"></el-option>-->
+<!--          <template #prefix><svg-icon icon-class="company" class="el-input__icon input-icon" /></template>-->
+<!--        </el-select>-->
+<!--      </el-form-item>-->
       <el-form-item prop="username">
         <el-input v-model="loginForm.username" type="text" size="large" auto-complete="off" :placeholder="proxy.$t('login.username')">
           <template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>

+ 57 - 22
src/views/merchant/storeManagement/index.vue

@@ -146,12 +146,7 @@
                       :total="tenantCategoriesTotal" :pageSize="10" placeholder="请选择商户分类"
                       @page-change="handleTenantCategoriesPageChange" @visible-change="handleTenantCategoriesVisibleChange" />
         </el-form-item>
-        <el-form-item label="所属品牌" prop="tenantId">
-          <PageSelect v-model="form.tenantId"
-                      :options="brandList.map(item => ({ value: item.tenantId, label: item.name }))" :total="brandTotal"
-                      :pageSize="10" placeholder="请选择所属品牌" @page-change="handleBrandPageChange"
-                      @visible-change="handleBrandSelectVisibleChange" />
-        </el-form-item>
+
         <el-form-item label="营业时间" prop="startBusinessTime">
           <el-row :gutter="10">
             <el-col :span="10">
@@ -282,7 +277,9 @@ import { listOnStore as listAreaStationOnStore } from '@/api/system/areaStation'
 import { SysAreaStationOnStoreVo } from '@/api/system/areaStation/types';
 import { regionData, codeToText, textToCode } from 'element-china-area-data';
 import PageSelect from '@/components/PageSelect/index.vue';
+import { useUserStore } from '@/store/modules/user';
 
+const userStore = useUserStore();
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 
 const storeList = ref<StoreVO[]>([]);
@@ -310,9 +307,9 @@ const handleSearchAreaChange = (value: any[]) => {
     const areaId = value[value.length - 1];
     queryParams.value.area = areaId;
     searchSiteOptions.value = areaStationList.value
-      .filter((item: any) => item.type === 2 && String(item.parentId) === String(areaId))
+      .filter((item: any) => String(item.type) === '2' && String(item.parentId) === String(areaId))
       .map((item: any) => ({
-        value: item.id,
+        value: String(item.id),
         label: item.name
       }));
   } else {
@@ -431,9 +428,7 @@ const data = reactive<PageData<StoreForm, SysStorePageBo>>({
     validity: [
       { required: true, message: "有效期至不能为空", trigger: "blur" }
     ],
-    tenantId: [
-      { required: true, message: "租户编号不能为空", trigger: "change" }
-    ],
+
     regionId: [
       { required: true, message: "所在区域不能为空", trigger: "change" }
     ],
@@ -499,6 +494,9 @@ const handleSelectionChange = (selection: StoreVO[]) => {
 /** 新增按钮操作 */
 const handleAdd = () => {
   reset();
+  if (userStore.tenantId) {
+    form.value.tenantId = userStore.tenantId;
+  }
   dialog.visible = true;
   dialog.title = "添加门店管理";
 }
@@ -510,11 +508,45 @@ const handleUpdate = async (row?: StoreVO) => {
   const res = await getStore(_id);
   Object.assign(form.value, res.data);
 
+  // 确保数据加载完
+  if (areaStationList.value.length === 0) {
+    await getAreaStationList();
+  }
+
   if (res.data.areaCode) {
     if (Array.isArray(res.data.areaCode)) {
-      addressCascaderValue.value = res.data.areaCode;
+      addressCascaderValue.value = res.data.areaCode.map(String);
     } else if (typeof res.data.areaCode === 'string') {
-      addressCascaderValue.value = res.data.areaCode.split(',');
+      addressCascaderValue.value = res.data.areaCode.split(',').map(String);
+    }
+  }
+
+  if (res.data.site) {
+    form.value.site = String(res.data.site);
+    const siteData = areaStationList.value.find((item: any) => String(item.id) === String(res.data.site));
+    if (siteData) {
+      const regionId = siteData.parentId;
+      form.value.regionId = String(regionId);
+      
+      const path: any[] = [];
+      let currentId = regionId;
+      while (currentId && String(currentId) !== '0') {
+        path.unshift(String(currentId));
+        const currentArea = areaStationList.value.find((item: any) => String(item.id) === String(currentId));
+        if (currentArea) {
+          currentId = currentArea.parentId;
+        } else {
+          break;
+        }
+      }
+      regionValue.value = path;
+      
+      siteOptions.value = areaStationList.value
+        .filter((item: any) => String(item.type) === '2' && String(item.parentId) === String(regionId))
+        .map((item: any) => ({
+          value: String(item.id),
+          label: item.name
+        }));
     }
   }
 
@@ -627,7 +659,7 @@ const getAreaStationList = async () => {
     areaStationList.value = data;
 
     // 分离所在区域数据(type为0或1)
-    const areaData = data.filter((item: any) => item.type === 0 || item.type === 1);
+    const areaData = data.filter((item: any) => String(item.type) === '0' || String(item.type) === '1');
     // 构建树形结构
     areaOptions.value = buildTree(areaData, 0);
 
@@ -642,11 +674,14 @@ const getAreaStationList = async () => {
 const buildTree = (data: any[], parentId: any): any[] => {
   return data
     .filter(item => String(item.parentId) === String(parentId))
-    .map(item => ({
-      value: item.id,
-      label: item.name,
-      children: buildTree(data, item.id)
-    }));
+    .map(item => {
+      const children = buildTree(data, item.id);
+      return {
+        value: String(item.id),
+        label: item.name,
+        children: children.length > 0 ? children : undefined
+      };
+    });
 };
 
 /** 处理所在区域选择变化 */
@@ -658,12 +693,12 @@ const handleAreaChange = (value: any[]) => {
     // 获取最后一级的id
     const areaId = value[value.length - 1];
     // 更新regionId
-    form.value.regionId = areaId;
+    form.value.regionId = String(areaId);
     // 过滤出parentId等于areaId的站点
     siteOptions.value = areaStationList.value
-      .filter((item: any) => item.type === 2 && String(item.parentId) === String(areaId))
+      .filter((item: any) => String(item.type) === '2' && String(item.parentId) === String(areaId))
       .map((item: any) => ({
-        value: item.id,
+        value: String(item.id),
         label: item.name
       }));
   } else {

+ 232 - 0
src/views/order/purchase/components/AddPetDialog.vue

@@ -0,0 +1,232 @@
+<template>
+  <el-dialog :model-value="visible" @update:model-value="$emit('update:visible', $event)" title="宠物档案详情" width="800px" top="10vh" class="pet-profile-dialog">
+    <el-tabs v-model="activePetTab" class="pet-tabs">
+      <el-tab-pane label="基本信息" name="basic">
+        <div class="pet-form-content">
+          <!-- Avatar Upload -->
+          <div class="avatar-col">
+            <el-upload
+              class="avatar-uploader"
+              action="#"
+              :show-file-list="false"
+              :auto-upload="false"
+              :on-change="handleAvatarChange"
+            >
+              <img v-if="petForm.avatar" :src="petForm.avatar" class="avatar" />
+              <el-icon v-else class="avatar-uploader-icon" :size="28" color="#8c939d"><Plus /></el-icon>
+            </el-upload>
+            <div style="font-size:12px; color:#999; margin-top:8px; text-align:center">点击上传头像</div>
+          </div>
+
+          <!-- Form Fields -->
+          <el-form :model="petForm" label-width="80px" class="inner-form">
+            <el-row :gutter="20">
+              <el-col :span="12">
+                <el-form-item label="宠物姓名" required>
+                  <el-input v-model="petForm.name" placeholder="请输入" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="所属主人" required>
+                  <el-select :model-value="userId" disabled placeholder="选择主人" style="width:100%">
+                    <el-option v-for="u in userOptions" :key="u.id" :label="u.name" :value="u.id" />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+            </el-row>
+
+            <el-row :gutter="20">
+              <el-col :span="12">
+                <el-form-item label="性别">
+                  <el-radio-group v-model="petForm.gender">
+                    <el-radio label="MM">公</el-radio>
+                    <el-radio label="GG">母</el-radio>
+                  </el-radio-group>
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="品种">
+                  <el-select v-model="petForm.breed" placeholder="请选择品种" style="width:100%">
+                    <el-option label="金毛" value="金毛" />
+                    <el-option label="布偶" value="布偶" />
+                    <el-option label="边牧" value="边牧" />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+            </el-row>
+
+            <el-row :gutter="20">
+              <el-col :span="12">
+                <el-form-item label="体型">
+                  <el-select v-model="petForm.bodyType" placeholder="选择体型" style="width:100%">
+                    <el-option label="小型" value="small" />
+                    <el-option label="中型" value="medium" />
+                    <el-option label="大型" value="large" />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="体重(kg)">
+                  <el-row :gutter="10">
+                    <el-col :span="12"><el-input-number v-model="petForm.weight" :min="0" :step="0.1" :controls="false" style="width:100%" /></el-col>
+                    <el-col :span="12"></el-col>
+                  </el-row>
+                </el-form-item>
+              </el-col>
+            </el-row>
+
+            <el-row :gutter="20">
+              <el-col :span="12">
+                <el-form-item label="年龄(岁)">
+                  <el-input-number v-model="petForm.age" :min="0" style="width:100%" />
+                </el-form-item>
+              </el-col>
+            </el-row>
+
+            <el-form-item label="性格关键词">
+              <el-input v-model="petForm.keywords" placeholder="如:活泼、粘人" />
+            </el-form-item>
+
+            <el-form-item label="萌宠性格">
+              <el-input v-model="petForm.desc" type="textarea" placeholder="详细描述" :rows="2" />
+            </el-form-item>
+
+            <el-form-item label="宠物标签">
+              <el-select v-model="petForm.tags" multiple placeholder="选择标签" style="width:100%">
+                <el-option label="绝育" value="1" />
+                <el-option label="疫苗齐全" value="2" />
+              </el-select>
+            </el-form-item>
+
+          </el-form>
+        </div>
+      </el-tab-pane>
+      <el-tab-pane label="家庭信息" name="family">
+        <el-form :model="petForm" label-width="120px">
+          <el-form-item label="新来家庭时间">
+            <el-date-picker v-model="petForm.arrivalTime" type="date" placeholder="选择日期" style="width: 100%" />
+          </el-form-item>
+          <el-form-item label="家庭房屋类型">
+            <el-radio-group v-model="petForm.houseType">
+              <el-radio label="stairs">楼梯</el-radio>
+              <el-radio label="elevator">电梯</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="入门方式">
+            <el-radio-group v-model="petForm.entryMethod">
+              <el-radio label="password">密码开门</el-radio>
+              <el-radio label="key">钥匙开门</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="密码" v-if="petForm.entryMethod === 'password'">
+            <el-input v-model="petForm.entryPassword" placeholder="请输入门锁密码" />
+          </el-form-item>
+          <el-form-item label="钥匙位置" v-if="petForm.entryMethod === 'key'">
+            <el-input v-model="petForm.keyLocation" placeholder="请输入钥匙存放位置" />
+          </el-form-item>
+        </el-form>
+      </el-tab-pane>
+      <el-tab-pane label="健康状况" name="health">
+        <el-form :model="petForm" label-width="120px">
+          <el-form-item label="健康状态">
+            <el-radio-group v-model="petForm.healthStatus">
+              <el-radio label="健康">健康</el-radio>
+              <el-radio label="亚健康">亚健康</el-radio>
+              <el-radio label="疾病">疾病</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="是否有攻击倾向">
+            <el-switch v-model="petForm.aggression" active-text="是" inactive-text="否" />
+          </el-form-item>
+          <el-form-item label="疫苗情况">
+            <el-input v-model="petForm.vaccine" type="textarea" placeholder="记录疫苗接种情况" />
+          </el-form-item>
+          <el-form-item label="既往病史">
+            <el-input v-model="petForm.medicalHistory" type="textarea" placeholder="如有病史请记录" />
+          </el-form-item>
+          <el-form-item label="过敏史">
+            <el-input v-model="petForm.allergies" type="textarea" placeholder="如有过敏源请记录" />
+          </el-form-item>
+        </el-form>
+      </el-tab-pane>
+    </el-tabs>
+    <template #footer>
+      <el-button @click="$emit('update:visible', false)">取消</el-button>
+      <el-button type="primary" @click="submit">保存</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup>
+import { ref, reactive, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+
+const props = defineProps({
+  visible: { type: Boolean, default: false },
+  userId: { type: [String, Number], default: '' },
+  userOptions: { type: Array, default: () => [] }
+})
+
+const emit = defineEmits(['update:visible', 'success'])
+
+const activePetTab = ref('basic')
+
+const petForm = reactive({
+  name: '', breed: '', gender: 'MM', avatar: '',
+  bodyType: 'small', weight: 0, age: 0, keywords: '', desc: '', tags: [],
+
+  // Family
+  arrivalTime: '', houseType: 'stairs', entryMethod: 'key', entryPassword: '', keyLocation: '',
+  // Health
+  healthStatus: '健康', aggression: false, vaccine: '', medicalHistory: '', allergies: ''
+})
+
+watch(() => props.visible, (newVal) => {
+  if (newVal) {
+    activePetTab.value = 'basic'
+    Object.assign(petForm, {
+      name: '', breed: '', gender: 'MM', avatar: '',
+      bodyType: 'small', weight: 0, age: 0, keywords: '', desc: '', tags: [],
+      arrivalTime: '', houseType: 'stairs', entryMethod: 'key', entryPassword: '', keyLocation: '',
+      healthStatus: '健康', aggression: false, vaccine: '', medicalHistory: '', allergies: ''
+    })
+  }
+})
+
+const handleAvatarChange = (uploadFile) => {
+  // Mock upload: create local URL
+  petForm.avatar = URL.createObjectURL(uploadFile.raw)
+}
+
+const submit = () => {
+  if (!petForm.name || !petForm.breed) {
+    ElMessage.warning('请补全宠物必填信息')
+    return
+  }
+  const newPet = {
+    id: Date.now(),
+    name: petForm.name,
+    breed: petForm.breed,
+    avatar: petForm.avatar
+  }
+  emit('success', newPet)
+  emit('update:visible', false)
+}
+</script>
+
+<style scoped>
+.pet-form-content { display: flex; gap: 20px; }
+.avatar-col { width: 120px; display: flex; flex-direction: column; align-items: center; padding-top: 10px; }
+.avatar-uploader { display: inline-block; }
+.avatar-uploader:deep(.el-upload) {
+  border: 1px dashed #d9d9d9; border-radius: 6px; cursor: pointer; position: relative; overflow: hidden;
+  transition: var(--el-transition-duration-fast);
+}
+.avatar-uploader:deep(.el-upload:hover) { border-color: var(--el-color-primary); }
+.avatar-uploader-icon {
+  font-size: 28px; color: #8c939d; width: 100px; height: 100px; text-align: center; border: 1px dashed #d9d9d9;
+  border-radius: 50%; display: flex; align-items: center; justify-content: center;
+}
+.avatar { width: 100px; height: 100px; display: block; border-radius: 50%; object-fit: cover; }
+.inner-form { flex: 1; }
+</style>

+ 164 - 0
src/views/order/purchase/components/AddUserDialog.vue

@@ -0,0 +1,164 @@
+<template>
+  <el-dialog :model-value="visible" @update:model-value="$emit('update:visible', $event)" title="新增用户" width="700px" destroy-on-close append-to-body class="add-user-dialog">
+    <el-form :model="userForm" label-width="90px" class="user-form">
+      <div style="display: flex; justify-content: center; align-items: center; gap: 20px; margin-bottom: 30px;">
+        <el-upload action="#" :show-file-list="false" :auto-upload="false" :on-change="handleUserAvatarChange">
+          <el-avatar :size="80" :src="userForm.avatar || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'" style="cursor: pointer; border: 2px solid #e4e7ed;" />
+        </el-upload>
+        <el-button type="primary" link @click="">点击修改头像</el-button>
+      </div>
+
+      <div class="form-section-header">基本资料</div>
+      <el-row :gutter="30">
+        <el-col :span="12">
+          <el-form-item label="录入来源">
+            <el-select v-model="userForm.source" style="width: 100%" filterable allow-create default-first-option>
+              <el-option label="平台录入" value="平台录入" />
+              <el-option label="萌它宠物连锁录入" value="萌它宠物连锁录入" />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="所属区域">
+            <el-select v-model="userForm.area" style="width: 100%" filterable allow-create default-first-option placeholder="请选择或输入">
+              <el-option label="朝阳区" value="朝阳区" />
+              <el-option label="海淀区" value="海淀区" />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="姓名" required><el-input v-model="userForm.name" placeholder="请输入姓名" /></el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="电话" required><el-input v-model="userForm.phone" placeholder="请输入电话" /></el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="性别">
+            <el-radio-group v-model="userForm.gender">
+              <el-radio label="男">男</el-radio>
+              <el-radio label="女">女</el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <div class="form-section-header">居住信息</div>
+      <el-row :gutter="30">
+        <el-col :span="24">
+          <el-form-item label="所在地区">
+            <el-cascader v-model="userForm.region" :options="pcaOptions" placeholder="请选择省/市/区" style="width: 100%" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="24">
+          <el-form-item label="详细住址"><el-input v-model="userForm.detailAddress" placeholder="请输入街道/门牌号" /></el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="房屋类型">
+            <el-radio-group v-model="userForm.houseType">
+              <el-radio label="stairs">楼梯</el-radio>
+              <el-radio label="elevator">电梯</el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="入门方式">
+            <el-radio-group v-model="userForm.entryMethod">
+              <el-radio label="password">密码开门</el-radio>
+              <el-radio label="key">钥匙开门</el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12" v-if="userForm.entryMethod === 'password'">
+          <el-form-item label="开门密码">
+            <el-input v-model="userForm.entryPassword" placeholder="请输入密码" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12" v-if="userForm.entryMethod === 'key'">
+          <el-form-item label="钥匙位置">
+            <el-input v-model="userForm.keyLocation" placeholder="如:地毯下" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <div class="form-section-header">其他</div>
+      <el-row :gutter="30">
+        <el-col :span="24">
+          <el-form-item label="用户标签">
+            <el-select v-model="userSelectedTagIds" multiple placeholder="选择标签" style="width: 100%">
+              <el-option v-for="tag in allUserTags" :key="tag.id" :label="tag.name" :value="tag.id">
+                <el-tag :type="tag.type" effect="light" size="small">{{ tag.name }}</el-tag>
+              </el-option>
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="24">
+          <el-form-item label="备注说明"><el-input type="textarea" v-model="userForm.remark" rows="3" /></el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+    <template #footer>
+      <div style="text-align: center; margin-top: 20px;">
+        <el-button @click="$emit('update:visible', false)" size="large" style="width: 120px;">取消</el-button>
+        <el-button type="primary" @click="submit" size="large" style="width: 120px;">保存</el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup>
+import { reactive, ref, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+
+const props = defineProps({
+  visible: { type: Boolean, default: false },
+  pcaOptions: { type: Array, default: () => [] }
+})
+
+const emit = defineEmits(['update:visible', 'success'])
+
+const userSelectedTagIds = ref([])
+const allUserTags = [
+  { id: 1, name: '优质客户', type: 'success' },
+  { id: 2, name: '潜在流失', type: 'warning' },
+  { id: 3, name: '黑名单', type: 'danger' }
+]
+
+const userForm = reactive({
+  id: null, avatar: '', name: '', phone: '', gender: '男', address: '', detailAddress: '', region: [], remark: '',
+  houseType: 'elevator', entryMethod: 'password', entryPassword: '', keyLocation: '',
+  source: '平台录入', area: ''
+})
+
+watch(() => props.visible, (newVal) => {
+  if (newVal) {
+    userSelectedTagIds.value = []
+    Object.assign(userForm, {
+      id: null, avatar: '', name: '', phone: '', gender: '男', address: '', detailAddress: '', region: [], remark: '',
+      houseType: 'elevator', entryMethod: 'password', entryPassword: '', keyLocation: '',
+      source: '平台录入', area: ''
+    })
+  }
+})
+
+const handleUserAvatarChange = (uploadFile) => {
+  userForm.avatar = URL.createObjectURL(uploadFile.raw)
+}
+
+const submit = () => {
+  if (!userForm.name || !userForm.phone) {
+    ElMessage.warning('请补全用户必填信息')
+    return
+  }
+  const newUser = {
+    id: Date.now(),
+    name: userForm.name,
+    phone: userForm.phone
+  }
+  emit('success', newUser)
+  emit('update:visible', false)
+}
+</script>
+
+<style scoped>
+.form-section-header { font-weight: bold; margin-bottom: 20px; font-size: 15px; color: #303133; }
+</style>

+ 86 - 0
src/views/order/purchase/components/FeedingForm.vue

@@ -0,0 +1,86 @@
+<template>
+  <div class="business-form">
+    <div style="margin-bottom: 20px;">
+      <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%" />
+        </el-col>
+        <el-col :span="16">
+          <el-input v-model="feedingData.addressDetail" placeholder="详细地址 (街道/门牌号)" prefix-icon="Location" />
+        </el-col>
+      </el-row>
+    </div>
+
+    <div style="margin-bottom: 20px;">
+      <div class="section-label" style="display:flex; align-items:center; margin-bottom:10px;">
+        预约服务时间
+        <el-tag type="info" size="small" style="margin-left:10px;">共 {{ feedingData.appointments.length }} 次</el-tag>
+      </div>
+      <div v-for="(item, index) in feedingData.appointments" :key="index" style="display:flex; align-items:center; margin-bottom:10px;">
+        <span style="width:30px; color:#999; font-size:12px; font-weight:bold;">{{ index + 1 }}.</span>
+        <el-date-picker
+          v-model="item.startTime"
+          type="datetime"
+          placeholder="开始时间"
+          style="width: 200px; margin-right: 5px;"
+          format="YYYY-MM-DD HH:mm"
+        />
+        <span style="margin:0 5px; color:#999;">~</span>
+        <el-date-picker
+          v-model="item.endTime"
+          type="datetime"
+          placeholder="结束时间 (可选)"
+          style="width: 200px; margin-right: 15px;"
+          format="YYYY-MM-DD HH:mm"
+        />
+
+        <div style="display:flex; gap:8px; margin-left:5px;">
+          <el-button v-if="index === feedingData.appointments.length - 1" type="primary" circle size="small" icon="Plus" @click="addAppointment" />
+          <el-button v-if="feedingData.appointments.length > 1" type="danger" circle size="small" icon="Minus" @click="removeAppointment(index)" plain />
+        </div>
+      </div>
+    </div>
+
+    <div class="remark-section">
+      <div class="section-label">家庭服务及宠物档案备注</div>
+      <el-row :gutter="15">
+        <el-col :span="12"><el-input v-model="feedingData.area" placeholder="宠物活动区域" /></el-col>
+        <el-col :span="12"><el-input v-model="feedingData.itemLoc" placeholder="物品存放位置" /></el-col>
+        <el-col :span="12" style="margin-top:10px"><el-input v-model="feedingData.cleanLoc" placeholder="清洗位置" /></el-col>
+        <el-col :span="12" style="margin-top:10px"><el-input v-model="feedingData.foodAmount" placeholder="喂食量标准" /></el-col>
+        <el-col :span="24" style="margin-top:10px"><el-input v-model="feedingData.other" type="textarea" :rows="2" placeholder="其他注意事项" /></el-col>
+      </el-row>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { defineProps, defineEmits } from 'vue'
+
+const props = defineProps({
+  feedingData: { type: Object, required: true },
+  pcaOptions: { type: Array, default: () => [] }
+})
+
+const emit = defineEmits(['change'])
+
+const addAppointment = () => {
+  props.feedingData.appointments.push({ startTime: '', endTime: '' })
+  props.feedingData.count = props.feedingData.appointments.length
+  emit('change', 'feeding')
+}
+
+const removeAppointment = (index) => {
+  if (props.feedingData.appointments.length <= 1) return
+  props.feedingData.appointments.splice(index, 1)
+  props.feedingData.count = props.feedingData.appointments.length
+  emit('change', 'feeding')
+}
+</script>
+
+<style scoped>
+.business-form { padding-top: 5px; }
+.remark-section { background: #fdfdfd; border: 1px dashed #dcdfe6; padding: 15px; border-radius: 6px; margin-top: 20px; }
+.section-label { font-size: 13px; font-weight: bold; color: #606266; margin-bottom: 12px; }
+</style>

+ 95 - 0
src/views/order/purchase/components/TransportForm.vue

@@ -0,0 +1,95 @@
+<template>
+  <div class="business-form">
+    <el-form-item label="接送模式">
+      <el-radio-group v-model="transportData.subType" size="large" @change="$emit('change', 'transport')">
+        <el-radio-button label="round">往返接送</el-radio-button>
+        <el-radio-button label="pick">单程接 (到店)</el-radio-button>
+        <el-radio-button label="drop">单程送 (回家)</el-radio-button>
+      </el-radio-group>
+    </el-form-item>
+
+    <div class="route-box">
+      <!-- 接宠段 -->
+      <div class="route-segment" v-if="['round', 'pick'].includes(transportData.subType)">
+        <div class="seg-badge start">接</div>
+        <div class="seg-content">
+          <el-row :gutter="10">
+            <el-col :span="8">
+              <el-cascader v-model="transportData.pickRegion" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
+            </el-col>
+            <el-col :span="16">
+              <el-input v-model="transportData.pickDetail" placeholder="详细地址 (街道/门牌号)" prefix-icon="Location" />
+            </el-col>
+          </el-row>
+          <el-row :gutter="10">
+            <el-col :span="12"><el-input v-model="transportData.pickContact" placeholder="联系人" /></el-col>
+            <el-col :span="12"><el-input v-model="transportData.pickPhone" placeholder="电话" /></el-col>
+          </el-row>
+          <el-row :gutter="10">
+            <el-col :span="24">
+              <el-date-picker v-model="transportData.pickTime" type="datetime" placeholder="选择接宠时间" style="width: 100%" />
+            </el-col>
+          </el-row>
+        </div>
+      </div>
+
+      <!-- 门店中转标识 -->
+      <div class="route-connector">
+        <div class="line"></div>
+        <div class="store-node"><el-icon><Shop /></el-icon> 服务门店</div>
+        <div class="line"></div>
+      </div>
+
+      <!-- 送回段 -->
+      <div class="route-segment" v-if="['round', 'drop'].includes(transportData.subType)">
+        <div class="seg-badge end">送</div>
+        <div class="seg-content">
+          <el-row :gutter="10">
+            <el-col :span="8">
+              <el-cascader v-model="transportData.dropRegion" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
+            </el-col>
+            <el-col :span="16">
+              <el-input v-model="transportData.dropDetail" placeholder="详细地址" prefix-icon="Location" />
+            </el-col>
+          </el-row>
+          <el-row :gutter="10">
+            <el-col :span="12"><el-input v-model="transportData.dropContact" placeholder="联系人" /></el-col>
+            <el-col :span="12"><el-input v-model="transportData.dropPhone" placeholder="电话" /></el-col>
+          </el-row>
+          <el-row :gutter="10">
+            <el-col :span="24">
+              <el-date-picker v-model="transportData.dropTime" type="datetime" placeholder="预计送回时间 (可选)" style="width: 100%" />
+            </el-col>
+          </el-row>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { defineProps, defineEmits } from 'vue'
+
+const props = defineProps({
+  transportData: { type: Object, required: true },
+  pcaOptions: { type: Array, default: () => [] }
+})
+
+const emit = defineEmits(['change'])
+</script>
+
+<style scoped>
+.business-form { padding-top: 5px; }
+.route-box { background: #f9f9f9; padding: 20px; border-radius: 8px; border: 1px solid #EBEEF5; }
+.route-segment { display: flex; gap: 15px; }
+.seg-badge {
+  width: 32px; height: 32px; background: #409eff; color: white; border-radius: 8px;
+  text-align: center; line-height: 32px; font-weight: bold; flex-shrink: 0;
+}
+.seg-badge.end { background: #67c23a; }
+.seg-content { flex: 1; display: flex; flex-direction: column; gap: 10px; }
+
+.route-connector { display: flex; align-items: center; justify-content: center; margin: 15px 0; gap: 10px; color: #909399; font-size: 12px; }
+.route-connector .line { height: 1px; width: 80px; background: #dcdfe6; }
+.route-connector .store-node { background: white; padding: 4px 12px; border-radius: 20px; border: 1px solid #dcdfe6; display: flex; align-items: center; gap: 5px; }
+</style>

+ 90 - 0
src/views/order/purchase/components/WashingForm.vue

@@ -0,0 +1,90 @@
+<template>
+  <div class="business-form">
+    <div style="margin-bottom: 20px;">
+      <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%" />
+        </el-col>
+        <el-col :span="16">
+          <el-input v-model="washingData.addressDetail" placeholder="详细地址 (街道/门牌号)" prefix-icon="Location" />
+        </el-col>
+      </el-row>
+    </div>
+
+    <div style="margin-bottom: 20px;">
+      <div class="section-label" style="display:flex; align-items:center; margin-bottom:10px;">
+        预约服务时间
+        <el-tag type="info" size="small" style="margin-left:10px;">共 {{ washingData.appointments.length }} 次</el-tag>
+      </div>
+      <div v-for="(item, index) in washingData.appointments" :key="index" style="display:flex; align-items:center; margin-bottom:10px;">
+        <span style="width:30px; color:#999; font-size:12px; font-weight:bold;">{{ index + 1 }}.</span>
+        <el-date-picker
+          v-model="item.startTime"
+          type="datetime"
+          placeholder="开始时间"
+          style="width: 200px; margin-right: 5px;"
+          format="YYYY-MM-DD HH:mm"
+        />
+        <span style="margin:0 5px; color:#999;">~</span>
+        <el-date-picker
+          v-model="item.endTime"
+          type="datetime"
+          placeholder="结束时间 (可选)"
+          style="width: 200px; margin-right: 15px;"
+          format="YYYY-MM-DD HH:mm"
+        />
+
+        <div style="display:flex; gap:8px; margin-left:5px;">
+          <el-button v-if="index === washingData.appointments.length - 1" type="primary" circle size="small" icon="Plus" @click="addAppointment" />
+          <el-button v-if="washingData.appointments.length > 1" type="danger" circle size="small" icon="Minus" @click="removeAppointment(index)" plain />
+        </div>
+      </div>
+    </div>
+
+    <div class="remark-section">
+      <div class="section-label">服务备注及宠物状态</div>
+      <el-row :gutter="15">
+        <el-col :span="8">
+          <el-select v-model="washingData.petStatus" placeholder="宠物应激状态" style="width:100%">
+            <el-option label="性格温顺" value="calm" />
+            <el-option label="胆小怕人" value="shy" />
+            <el-option label="容易应激" value="stress" />
+            <el-option label="有攻击性" value="aggressive" />
+          </el-select>
+        </el-col>
+        <el-col :span="8"><el-input v-model="washingData.cleanLoc" placeholder="清洗位置" /></el-col>
+        <el-col :span="8"><el-input v-model="washingData.toolLoc" placeholder="工具/水源位置" /></el-col>
+        <el-col :span="24" style="margin-top:10px"><el-input v-model="washingData.other" type="textarea" :rows="2" placeholder="其他注意事项" /></el-col>
+      </el-row>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { defineProps, defineEmits } from 'vue'
+
+const props = defineProps({
+  washingData: { type: Object, required: true },
+  pcaOptions: { type: Array, default: () => [] }
+})
+
+const emit = defineEmits(['change'])
+
+const addAppointment = () => {
+  props.washingData.appointments.push({ startTime: '', endTime: '' })
+  emit('change', 'washing')
+}
+
+const removeAppointment = (index) => {
+  if (props.washingData.appointments.length <= 1) return
+  props.washingData.appointments.splice(index, 1)
+  emit('change', 'washing')
+}
+</script>
+
+<style scoped>
+.business-form { padding-top: 5px; }
+.remark-section { background: #fdfdfd; border: 1px dashed #dcdfe6; padding: 15px; border-radius: 6px; margin-top: 20px; }
+.section-label { font-size: 13px; font-weight: bold; color: #606266; margin-bottom: 12px; }
+</style>

+ 20 - 606
src/views/order/purchase/index.vue

@@ -114,193 +114,13 @@
             <div class="divider"></div>
 
             <!-- A. 宠物接送表单 -->
-            <div v-show="form.type === 'transport'" class="business-form">
-              <el-form-item label="接送模式">
-                <el-radio-group v-model="form.transport.subType" size="large" @change="calcPrice('transport')">
-                  <el-radio-button label="round">往返接送</el-radio-button>
-                  <el-radio-button label="pick">单程接 (到店)</el-radio-button>
-                  <el-radio-button label="drop">单程送 (回家)</el-radio-button>
-                </el-radio-group>
-              </el-form-item>
-
-              <div class="route-box">
-                <!-- 接宠段 -->
-                <div class="route-segment" v-if="['round', 'pick'].includes(form.transport.subType)">
-                  <div class="seg-badge start">接</div>
-                  <div class="seg-content">
-                    <el-row :gutter="10">
-                      <el-col :span="8">
-                        <el-cascader v-model="form.transport.pickRegion" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
-                      </el-col>
-                      <el-col :span="16">
-                        <el-input v-model="form.transport.pickDetail" placeholder="详细地址 (街道/门牌号)" prefix-icon="Location" />
-                      </el-col>
-                    </el-row>
-                    <el-row :gutter="10">
-                      <el-col :span="12"><el-input v-model="form.transport.pickContact" placeholder="联系人" /></el-col>
-                      <el-col :span="12"><el-input v-model="form.transport.pickPhone" placeholder="电话" /></el-col>
-                    </el-row>
-                    <el-row :gutter="10">
-                      <el-col :span="24">
-                        <el-date-picker v-model="form.transport.pickTime" type="datetime" placeholder="选择接宠时间" style="width: 100%" />
-                      </el-col>
-                    </el-row>
-                  </div>
-                </div>
-
-                <!-- 门店中转标识 -->
-                <div class="route-connector">
-                  <div class="line"></div>
-                  <div class="store-node"><el-icon><Shop /></el-icon> 服务门店</div>
-                  <div class="line"></div>
-                </div>
-
-                <!-- 送回段 -->
-                <div class="route-segment" v-if="['round', 'drop'].includes(form.transport.subType)">
-                  <div class="seg-badge end">送</div>
-                  <div class="seg-content">
-                    <el-row :gutter="10">
-                      <el-col :span="8">
-                        <el-cascader v-model="form.transport.dropRegion" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
-                      </el-col>
-                      <el-col :span="16">
-                        <el-input v-model="form.transport.dropDetail" placeholder="详细地址" prefix-icon="Location" />
-                      </el-col>
-                    </el-row>
-                    <el-row :gutter="10">
-                      <el-col :span="12"><el-input v-model="form.transport.dropContact" placeholder="联系人" /></el-col>
-                      <el-col :span="12"><el-input v-model="form.transport.dropPhone" placeholder="电话" /></el-col>
-                    </el-row>
-                    <el-row :gutter="10">
-                      <el-col :span="24">
-                        <el-date-picker v-model="form.transport.dropTime" type="datetime" placeholder="预计送回时间 (可选)" style="width: 100%" />
-                      </el-col>
-                    </el-row>
-                  </div>
-                </div>
-              </div>
-            </div>
+            <TransportForm v-show="form.type === 'transport'" :transport-data="form.transport" :pca-options="pcaOptions" @change="calcPrice" />
 
             <!-- B. 上门喂遛表单 -->
-            <div v-show="form.type === 'feeding'" class="business-form">
-
-              <div style="margin-bottom: 20px;">
-                <div class="section-label">上门服务地址</div>
-                <el-row :gutter="10">
-                  <el-col :span="8">
-                    <el-cascader v-model="form.feeding.region" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
-                  </el-col>
-                  <el-col :span="16">
-                    <el-input v-model="form.feeding.addressDetail" placeholder="详细地址 (街道/门牌号)" prefix-icon="Location" />
-                  </el-col>
-                </el-row>
-              </div>
-
-              <div style="margin-bottom: 20px;">
-                <div class="section-label" style="display:flex; align-items:center; margin-bottom:10px;">
-                  预约服务时间
-                  <el-tag type="info" size="small" style="margin-left:10px;">共 {{ form.feeding.appointments.length }} 次</el-tag>
-                </div>
-                <div v-for="(item, index) in form.feeding.appointments" :key="index" style="display:flex; align-items:center; margin-bottom:10px;">
-                  <span style="width:30px; color:#999; font-size:12px; font-weight:bold;">{{ index + 1 }}.</span>
-                  <el-date-picker
-                    v-model="item.startTime"
-                    type="datetime"
-                    placeholder="开始时间"
-                    style="width: 200px; margin-right: 5px;"
-                    format="YYYY-MM-DD HH:mm"
-                  />
-                  <span style="margin:0 5px; color:#999;">~</span>
-                  <el-date-picker
-                    v-model="item.endTime"
-                    type="datetime"
-                    placeholder="结束时间 (可选)"
-                    style="width: 200px; margin-right: 15px;"
-                    format="YYYY-MM-DD HH:mm"
-                  />
-
-                  <div style="display:flex; gap:8px; margin-left:5px;">
-                    <el-button v-if="index === form.feeding.appointments.length - 1" type="primary" circle size="small" icon="Plus" @click="addAppointment('feeding')" />
-                    <el-button v-if="form.feeding.appointments.length > 1" type="danger" circle size="small" icon="Minus" @click="removeAppointment('feeding', index)" plain />
-                  </div>
-                </div>
-              </div>
-
-              <div class="remark-section">
-                <div class="section-label">家庭服务及宠物档案备注</div>
-                <el-row :gutter="15">
-                  <el-col :span="12"><el-input v-model="form.feeding.area" placeholder="宠物活动区域" /></el-col>
-                  <el-col :span="12"><el-input v-model="form.feeding.itemLoc" placeholder="物品存放位置" /></el-col>
-                  <el-col :span="12" style="margin-top:10px"><el-input v-model="form.feeding.cleanLoc" placeholder="清洗位置" /></el-col>
-                  <el-col :span="12" style="margin-top:10px"><el-input v-model="form.feeding.foodAmount" placeholder="喂食量标准" /></el-col>
-                  <el-col :span="24" style="margin-top:10px"><el-input v-model="form.feeding.other" type="textarea" :rows="2" placeholder="其他注意事项" /></el-col>
-                </el-row>
-              </div>
-            </div>
+            <FeedingForm v-show="form.type === 'feeding'" :feeding-data="form.feeding" :pca-options="pcaOptions" @change="calcPrice" />
 
             <!-- C. 上门洗护表单 -->
-            <div v-show="form.type === 'washing'" class="business-form">
-
-              <div style="margin-bottom: 20px;">
-                <div class="section-label">上门服务地址</div>
-                <el-row :gutter="10">
-                  <el-col :span="8">
-                    <el-cascader v-model="form.washing.region" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
-                  </el-col>
-                  <el-col :span="16">
-                    <el-input v-model="form.washing.addressDetail" placeholder="详细地址 (街道/门牌号)" prefix-icon="Location" />
-                  </el-col>
-                </el-row>
-              </div>
-
-              <div style="margin-bottom: 20px;">
-                <div class="section-label" style="display:flex; align-items:center; margin-bottom:10px;">
-                  预约服务时间
-                  <el-tag type="info" size="small" style="margin-left:10px;">共 {{ form.washing.appointments.length }} 次</el-tag>
-                </div>
-                <div v-for="(item, index) in form.washing.appointments" :key="index" style="display:flex; align-items:center; margin-bottom:10px;">
-                  <span style="width:30px; color:#999; font-size:12px; font-weight:bold;">{{ index + 1 }}.</span>
-                  <el-date-picker
-                    v-model="item.startTime"
-                    type="datetime"
-                    placeholder="开始时间"
-                    style="width: 200px; margin-right: 5px;"
-                    format="YYYY-MM-DD HH:mm"
-                  />
-                  <span style="margin:0 5px; color:#999;">~</span>
-                  <el-date-picker
-                    v-model="item.endTime"
-                    type="datetime"
-                    placeholder="结束时间 (可选)"
-                    style="width: 200px; margin-right: 15px;"
-                    format="YYYY-MM-DD HH:mm"
-                  />
-
-                  <div style="display:flex; gap:8px; margin-left:5px;">
-                    <el-button v-if="index === form.washing.appointments.length - 1" type="primary" circle size="small" icon="Plus" @click="addAppointment('washing')" />
-                    <el-button v-if="form.washing.appointments.length > 1" type="danger" circle size="small" icon="Minus" @click="removeAppointment('washing', index)" plain />
-                  </div>
-                </div>
-              </div>
-
-              <div class="remark-section">
-                <div class="section-label">服务备注及宠物状态</div>
-                <el-row :gutter="15">
-                  <el-col :span="8">
-                    <el-select v-model="form.washing.petStatus" placeholder="宠物应激状态" style="width:100%">
-                      <el-option label="性格温顺" value="calm" />
-                      <!-- ... options ... -->
-                      <el-option label="胆小怕人" value="shy" />
-                      <el-option label="容易应激" value="stress" />
-                      <el-option label="有攻击性" value="aggressive" />
-                    </el-select>
-                  </el-col>
-                  <el-col :span="8"><el-input v-model="form.washing.cleanLoc" placeholder="清洗位置" /></el-col>
-                  <el-col :span="8"><el-input v-model="form.washing.toolLoc" placeholder="工具/水源位置" /></el-col>
-                  <el-col :span="24" style="margin-top:10px"><el-input v-model="form.washing.other" type="textarea" :rows="2" placeholder="其他注意事项" /></el-col>
-                </el-row>
-              </div>
-            </div>
+            <WashingForm v-show="form.type === 'washing'" :washing-data="form.washing" :pca-options="pcaOptions" @change="calcPrice" />
 
           </div>
         </el-card>
@@ -359,267 +179,8 @@
 
     <!-- Dialogs -->
     <!-- Add User Dialog -->
-    <el-dialog v-model="userDialogVisible" title="新增用户" width="700px" destroy-on-close append-to-body class="add-user-dialog">
-      <el-form :model="userForm" label-width="90px" class="user-form">
-
-        <div style="display: flex; justify-content: center; align-items: center; gap: 20px; margin-bottom: 30px;">
-          <el-upload action="#" :show-file-list="false" :auto-upload="false" :on-change="handleUserAvatarChange">
-            <el-avatar :size="80" :src="userForm.avatar || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'" style="cursor: pointer; border: 2px solid #e4e7ed;" />
-          </el-upload>
-          <el-button type="primary" link @click="">点击修改头像</el-button>
-        </div>
-
-        <div class="form-section-header">基本资料</div>
-        <el-row :gutter="30">
-          <el-col :span="12">
-            <el-form-item label="录入来源">
-              <el-select v-model="userForm.source" style="width: 100%" filterable allow-create default-first-option>
-                <el-option label="平台录入" value="平台录入" />
-                <el-option label="萌它宠物连锁录入" value="萌它宠物连锁录入" />
-              </el-select>
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="所属区域">
-              <el-select v-model="userForm.area" style="width: 100%" filterable allow-create default-first-option placeholder="请选择或输入">
-                <el-option label="朝阳区" value="朝阳区" />
-                <el-option label="海淀区" value="海淀区" />
-              </el-select>
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="姓名" required><el-input v-model="userForm.name" placeholder="请输入姓名" /></el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="电话" required><el-input v-model="userForm.phone" placeholder="请输入电话" /></el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="性别">
-              <el-radio-group v-model="userForm.gender">
-                <el-radio label="男">男</el-radio>
-                <el-radio label="女">女</el-radio>
-              </el-radio-group>
-            </el-form-item>
-          </el-col>
-        </el-row>
-
-        <div class="form-section-header">居住信息</div>
-        <el-row :gutter="30">
-          <el-col :span="24">
-            <el-form-item label="所在地区">
-              <el-cascader v-model="userForm.region" :options="pcaOptions" placeholder="请选择省/市/区" style="width: 100%" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="24">
-            <el-form-item label="详细住址"><el-input v-model="userForm.detailAddress" placeholder="请输入街道/门牌号" /></el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="房屋类型">
-              <el-radio-group v-model="userForm.houseType">
-                <el-radio label="stairs">楼梯</el-radio>
-                <el-radio label="elevator">电梯</el-radio>
-              </el-radio-group>
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="入门方式">
-              <el-radio-group v-model="userForm.entryMethod">
-                <el-radio label="password">密码开门</el-radio>
-                <el-radio label="key">钥匙开门</el-radio>
-              </el-radio-group>
-            </el-form-item>
-          </el-col>
-          <el-col :span="12" v-if="userForm.entryMethod === 'password'">
-            <el-form-item label="开门密码">
-              <el-input v-model="userForm.entryPassword" placeholder="请输入密码" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12" v-if="userForm.entryMethod === 'key'">
-            <el-form-item label="钥匙位置">
-              <el-input v-model="userForm.keyLocation" placeholder="如:地毯下" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-
-        <div class="form-section-header">其他</div>
-        <el-row :gutter="30">
-          <el-col :span="24">
-            <el-form-item label="用户标签">
-              <el-select v-model="userSelectedTagIds" multiple placeholder="选择标签" style="width: 100%">
-                <el-option v-for="tag in allUserTags" :key="tag.id" :label="tag.name" :value="tag.id">
-                  <el-tag :type="tag.type" effect="light" size="small">{{ tag.name }}</el-tag>
-                </el-option>
-              </el-select>
-            </el-form-item>
-          </el-col>
-          <el-col :span="24">
-            <el-form-item label="备注说明"><el-input type="textarea" v-model="userForm.remark" rows="3" /></el-form-item>
-          </el-col>
-        </el-row>
-      </el-form>
-      <template #footer>
-        <div style="text-align: center; margin-top: 20px;">
-          <el-button @click="userDialogVisible = false" size="large" style="width: 120px;">取消</el-button>
-          <el-button type="primary" @click="submitUser" size="large" style="width: 120px;">保存</el-button>
-        </div>
-      </template>
-    </el-dialog>
-    <el-dialog v-model="petDialogVisible" title="宠物档案详情" width="800px" top="10vh" class="pet-profile-dialog">
-      <el-tabs v-model="activePetTab" class="pet-tabs">
-        <el-tab-pane label="基本信息" name="basic">
-          <div class="pet-form-content">
-            <!-- Avatar Upload -->
-            <div class="avatar-col">
-              <el-upload
-                class="avatar-uploader"
-                action="#"
-                :show-file-list="false"
-                :auto-upload="false"
-                :on-change="handleAvatarChange"
-              >
-                <img v-if="petForm.avatar" :src="petForm.avatar" class="avatar" />
-                <el-icon v-else class="avatar-uploader-icon" :size="28" color="#8c939d"><Plus /></el-icon>
-              </el-upload>
-              <div style="font-size:12px; color:#999; margin-top:8px; text-align:center">点击上传头像</div>
-            </div>
-
-            <!-- Form Fields -->
-            <el-form :model="petForm" label-width="80px" class="inner-form">
-              <el-row :gutter="20">
-                <el-col :span="12">
-                  <el-form-item label="宠物姓名" required>
-                    <el-input v-model="petForm.name" placeholder="请输入" />
-                  </el-form-item>
-                </el-col>
-                <el-col :span="12">
-                  <el-form-item label="所属主人" required>
-                    <el-select v-model="form.userId" disabled placeholder="选择主人" style="width:100%">
-                      <el-option v-for="u in userOptions" :key="u.id" :label="u.name" :value="u.id" />
-                    </el-select>
-                  </el-form-item>
-                </el-col>
-              </el-row>
-
-              <el-row :gutter="20">
-                <el-col :span="12">
-                  <el-form-item label="性别">
-                    <el-radio-group v-model="petForm.gender">
-                      <el-radio label="MM">公</el-radio>
-                      <el-radio label="GG">母</el-radio>
-                    </el-radio-group>
-                  </el-form-item>
-                </el-col>
-                <el-col :span="12">
-                  <el-form-item label="品种">
-                    <el-select v-model="petForm.breed" placeholder="请选择品种" style="width:100%">
-                      <el-option label="金毛" value="金毛" />
-                      <el-option label="布偶" value="布偶" />
-                      <el-option label="边牧" value="边牧" />
-                    </el-select>
-                  </el-form-item>
-                </el-col>
-              </el-row>
-
-              <el-row :gutter="20">
-                <el-col :span="12">
-                  <el-form-item label="体型">
-                    <el-select v-model="petForm.bodyType" placeholder="选择体型" style="width:100%">
-                      <el-option label="小型" value="small" />
-                      <el-option label="中型" value="medium" />
-                      <el-option label="大型" value="large" />
-                    </el-select>
-                  </el-form-item>
-                </el-col>
-                <el-col :span="12">
-                  <el-form-item label="体重(kg)">
-                    <el-row :gutter="10">
-                      <el-col :span="12"><el-input-number v-model="petForm.weight" :min="0" :step="0.1" controls="false" style="width:100%" /></el-col>
-                      <el-col :span="12"></el-col>
-                    </el-row>
-                  </el-form-item>
-                </el-col>
-              </el-row>
-
-              <el-row :gutter="20">
-                <el-col :span="12">
-                  <el-form-item label="年龄(岁)">
-                    <el-input-number v-model="petForm.age" :min="0" style="width:100%" />
-                  </el-form-item>
-                </el-col>
-              </el-row>
-
-              <el-form-item label="性格关键词">
-                <el-input v-model="petForm.keywords" placeholder="如:活泼、粘人" />
-              </el-form-item>
-
-              <el-form-item label="萌宠性格">
-                <el-input v-model="petForm.desc" type="textarea" placeholder="详细描述" :rows="2" />
-              </el-form-item>
-
-              <el-form-item label="宠物标签">
-                <el-select v-model="petForm.tags" multiple placeholder="选择标签" style="width:100%">
-                  <el-option label="绝育" value="1" />
-                  <el-option label="疫苗齐全" value="2" />
-                </el-select>
-              </el-form-item>
-
-            </el-form>
-          </div>
-        </el-tab-pane>
-        <el-tab-pane label="家庭信息" name="family">
-          <el-form :model="petForm" label-width="120px">
-            <el-form-item label="新来家庭时间">
-              <el-date-picker v-model="petForm.arrivalTime" type="date" placeholder="选择日期" style="width: 100%" />
-            </el-form-item>
-            <el-form-item label="家庭房屋类型">
-              <el-radio-group v-model="petForm.houseType">
-                <el-radio label="stairs">楼梯</el-radio>
-                <el-radio label="elevator">电梯</el-radio>
-              </el-radio-group>
-            </el-form-item>
-            <el-form-item label="入门方式">
-              <el-radio-group v-model="petForm.entryMethod">
-                <el-radio label="password">密码开门</el-radio>
-                <el-radio label="key">钥匙开门</el-radio>
-              </el-radio-group>
-            </el-form-item>
-            <el-form-item label="密码" v-if="petForm.entryMethod === 'password'">
-              <el-input v-model="petForm.entryPassword" placeholder="请输入门锁密码" />
-            </el-form-item>
-            <el-form-item label="钥匙位置" v-if="petForm.entryMethod === 'key'">
-              <el-input v-model="petForm.keyLocation" placeholder="请输入钥匙存放位置" />
-            </el-form-item>
-          </el-form>
-        </el-tab-pane>
-        <el-tab-pane label="健康状况" name="health">
-          <el-form :model="petForm" label-width="120px">
-            <el-form-item label="健康状态">
-              <el-radio-group v-model="petForm.healthStatus">
-                <el-radio label="健康">健康</el-radio>
-                <el-radio label="亚健康">亚健康</el-radio>
-                <el-radio label="疾病">疾病</el-radio>
-              </el-radio-group>
-            </el-form-item>
-            <el-form-item label="是否有攻击倾向">
-              <el-switch v-model="petForm.aggression" active-text="是" inactive-text="否" />
-            </el-form-item>
-            <el-form-item label="疫苗情况">
-              <el-input v-model="petForm.vaccine" type="textarea" placeholder="记录疫苗接种情况" />
-            </el-form-item>
-            <el-form-item label="既往病史">
-              <el-input v-model="petForm.medicalHistory" type="textarea" placeholder="如有病史请记录" />
-            </el-form-item>
-            <el-form-item label="过敏史">
-              <el-input v-model="petForm.allergies" type="textarea" placeholder="如有过敏源请记录" />
-            </el-form-item>
-          </el-form>
-        </el-tab-pane>
-      </el-tabs>
-      <template #footer>
-        <el-button @click="petDialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="submitPet">保存</el-button>
-      </template>
-    </el-dialog>
+    <AddUserDialog v-model:visible="userDialogVisible" :pca-options="pcaOptions" @success="handleUserSuccess" />
+    <AddPetDialog v-model:visible="petDialogVisible" :user-id="form.userId" :user-options="userOptions" @success="handlePetSuccess" />
 
   </div>
 </template>
@@ -627,6 +188,11 @@
 <script setup>
 import { ref, reactive, computed, onMounted, watch } from 'vue'
 import { ElMessage } from 'element-plus'
+import TransportForm from './components/TransportForm.vue'
+import FeedingForm from './components/FeedingForm.vue'
+import WashingForm from './components/WashingForm.vue'
+import AddUserDialog from './components/AddUserDialog.vue'
+import AddPetDialog from './components/AddPetDialog.vue'
 
 // --- Mock Data ---
 const merchants = ref([
@@ -769,33 +335,16 @@ const calcPrice = (type) => {
   }
 }
 
-// Appointment Logic
-const addAppointment = (type) => {
-  form[type].appointments.push({ startTime: '', endTime: '' })
-  if(type === 'feeding') {
-    form.feeding.count = form.feeding.appointments.length
-    calcPrice('feeding')
-  }
-}
-
-const removeAppointment = (type, index) => {
-  if (form[type].appointments.length <= 1) return
-  form[type].appointments.splice(index, 1)
-  if(type === 'feeding') {
-    form.feeding.count = form.feeding.appointments.length
-    calcPrice('feeding')
-  }
-}
-
 // Add User Logic
 const userDialogVisible = ref(false)
-const userSelectedTagIds = ref([])
-
-const allUserTags = [
-  { id: 1, name: '优质客户', type: 'success' },
-  { id: 2, name: '潜在流失', type: 'warning' },
-  { id: 3, name: '黑名单', type: 'danger' }
-]
+const openAddUser = () => { userDialogVisible.value = true }
+const handleUserSuccess = (newUser) => {
+  userOptions.value.push(newUser)
+  form.userId = newUser.id
+  currentPets.value = []
+  form.petId = ''
+  ElMessage.success('用户添加成功并已选中')
+}
 
 const pcaOptions = [
   {
@@ -812,92 +361,13 @@ const pcaOptions = [
   }
 ]
 
-const userForm = reactive({
-  id: null, avatar: '', name: '', phone: '', gender: '男', address: '', detailAddress: '', region: [], remark: '',
-  houseType: 'elevator', entryMethod: 'password', entryPassword: '', keyLocation: '',
-  source: '平台录入', area: ''
-})
-
-const openAddUser = () => {
-  userSelectedTagIds.value = []
-  Object.assign(userForm, {
-    id: null, avatar: '', name: '', phone: '', gender: '男', address: '', detailAddress: '', region: [], remark: '',
-    houseType: 'elevator', entryMethod: 'password', entryPassword: '', keyLocation: '',
-    source: '平台录入', area: ''
-  })
-  userDialogVisible.value = true
-}
-
-const handleUserAvatarChange = (uploadFile) => {
-  userForm.avatar = URL.createObjectURL(uploadFile.raw)
-}
-
-const submitUser = () => {
-  if(!userForm.name || !userForm.phone) {
-    ElMessage.warning('请补全用户必填信息')
-    return
-  }
-  const newUser = {
-    id: Date.now(),
-    name: userForm.name,
-    phone: userForm.phone
-  }
-  userOptions.value.push(newUser)
-  form.userId = newUser.id
-
-  // Clear pets for new user
-  currentPets.value = []
-  form.petId = ''
-
-  userDialogVisible.value = false
-  ElMessage.success('用户添加成功并已选中')
-}
-
 // Add Pet Logic
 const petDialogVisible = ref(false)
-const activePetTab = ref('basic')
-
-const petForm = reactive({
-  name: '', breed: '', gender: 'MM', avatar: '',
-  bodyType: 'small', weight: 0, age: 0, keywords: '', desc: '', tags: [],
-
-  // Family
-  arrivalTime: '', houseType: 'stairs', entryMethod: 'key', entryPassword: '', keyLocation: '',
-  // Health
-  healthStatus: '健康', aggression: false, vaccine: '', medicalHistory: '', allergies: ''
-})
-
-const openAddPet = () => {
-  activePetTab.value = 'basic'
-  Object.assign(petForm, {
-    name: '', breed: '', gender: 'MM', avatar: '',
-    bodyType: 'small', weight: 0, age: 0, keywords: '', desc: '', tags: [],
-    arrivalTime: '', houseType: 'stairs', entryMethod: 'key', entryPassword: '', keyLocation: '',
-    healthStatus: '健康', aggression: false, vaccine: '', medicalHistory: '', allergies: ''
-  })
-  petDialogVisible.value = true
-}
-
-const handleAvatarChange = (uploadFile) => {
-  // Mock upload: create local URL
-  petForm.avatar = URL.createObjectURL(uploadFile.raw)
-}
-
-const submitPet = () => {
-  if(!petForm.name || !petForm.breed) {
-    ElMessage.warning('请补全宠物必填信息')
-    return
-  }
-  const newPet = {
-    id: Date.now(),
-    name: petForm.name,
-    breed: petForm.breed,
-    avatar: petForm.avatar
-  }
+const openAddPet = () => { petDialogVisible.value = true }
+const handlePetSuccess = (newPet) => {
   if(!currentPets.value) currentPets.value = []
   currentPets.value.push(newPet)
   form.petId = newPet.id
-  petDialogVisible.value = false
   ElMessage.success('宠物添加成功')
 }
 
@@ -1033,44 +503,6 @@ onMounted(() => {
   transform: translateY(-2px);
 }
 
-/* Dialog Styles */
-.pet-form-content { display: flex; gap: 20px; }
-.avatar-col { width: 120px; display: flex; flex-direction: column; align-items: center; padding-top: 10px; }
-.avatar-uploader {
-  display: inline-block;
-}
-.avatar-uploader .el-upload {
-  border: 1px dashed #d9d9d9;
-  border-radius: 6px;
-  cursor: pointer;
-  position: relative;
-  overflow: hidden;
-  transition: var(--el-transition-duration-fast);
-}
-.avatar-uploader .el-upload:hover {
-  border-color: var(--el-color-primary);
-}
-.avatar-uploader-icon {
-  font-size: 28px;
-  color: #8c939d;
-  width: 100px;
-  height: 100px;
-  text-align: center;
-  border: 1px dashed #d9d9d9;
-  border-radius: 50%;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-.avatar {
-  width: 100px;
-  height: 100px;
-  display: block;
-  border-radius: 50%;
-  object-fit: cover;
-}
-.inner-form { flex: 1; }
-
 /* Type Selection */
 .type-selection { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; }
 .type-card {
@@ -1109,25 +541,7 @@ onMounted(() => {
 .pkg-select-card .pkg-name { font-weight: bold; font-size: 14px; color: #303133; }
 .pkg-select-card .pkg-desc { font-size: 12px; color: #909399; margin-top: 2px; }
 
-/* Business Form */
-.business-form { padding-top: 5px; }
-.route-box { background: #f9f9f9; padding: 20px; border-radius: 8px; border: 1px solid #EBEEF5; }
-.route-segment { display: flex; gap: 15px; }
-.seg-badge {
-  width: 32px; height: 32px; background: #409eff; color: white; border-radius: 8px;
-  text-align: center; line-height: 32px; font-weight: bold; flex-shrink: 0;
-}
-.seg-badge.end { background: #67c23a; }
-.seg-content { flex: 1; display: flex; flex-direction: column; gap: 10px; }
-
-.route-connector { display: flex; align-items: center; justify-content: center; margin: 15px 0; gap: 10px; color: #909399; font-size: 12px; }
-.route-connector .line { height: 1px; width: 80px; background: #dcdfe6; }
-.route-connector .store-node { background: white; padding: 4px 12px; border-radius: 20px; border: 1px solid #dcdfe6; display: flex; align-items: center; gap: 5px; }
-
 .divider { height: 1px; background: #EBEEF5; margin: 15px 0; }
-.remark-section { background: #fdfdfd; border: 1px dashed #dcdfe6; padding: 15px; border-radius: 6px; margin-top: 20px; }
-.section-label { font-size: 13px; font-weight: bold; color: #606266; margin-bottom: 12px; }
-.tip { font-size: 12px; color: #e6a23c; margin-top: 4px; }
 
 /* Sidebar */
 .summary-sidebar { width: 320px; flex-shrink: 0; }

+ 3 - 3
src/views/system/role/index.vue

@@ -112,9 +112,9 @@
         <el-form-item label="角色名称">
           <el-input v-model="form.roleName" :disabled="true" />
         </el-form-item>
-        <el-form-item label="权限字符">
-          <el-input v-model="form.roleKey" :disabled="true" />
-        </el-form-item>
+<!--        <el-form-item label="权限字符">-->
+<!--          <el-input v-model="form.roleKey" :disabled="true" />-->
+<!--        </el-form-item>-->
         <el-form-item label="权限范围">
           <el-select v-model="form.dataScope" @change="dataScopeSelectChange">
             <el-option v-for="item in dataScopeOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>

+ 1 - 1
vite.config.ts

@@ -24,7 +24,7 @@ export default defineConfig(({ mode, command }) => {
       // open: true,
       proxy: {
         [env.VITE_APP_BASE_API]: {
-          target: 'http://localhost:8080',
+          target: 'http://192.168.1.118:8080',
           changeOrigin: true,
           ws: true,
           rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')