Huanyi пре 1 месец
родитељ
комит
889af8a7dd

+ 3 - 0
src/api/system/user/types.ts

@@ -37,6 +37,7 @@ export interface UserVO extends BaseEntity {
   phonenumber: string;
   phonenumber: string;
   sex: string;
   sex: string;
   avatar: string;
   avatar: string;
+  avatarUrl?: string;
   status: string;
   status: string;
   delFlag: string;
   delFlag: string;
   loginIp: string;
   loginIp: string;
@@ -69,6 +70,7 @@ export interface UserForm {
   postIds: string[];
   postIds: string[];
   roleIds: string[];
   roleIds: string[];
   storeIds?: number[];
   storeIds?: number[];
+  avatar?: any;
 }
 }
 
 
 export interface UserInfoVO {
 export interface UserInfoVO {
@@ -77,6 +79,7 @@ export interface UserInfoVO {
   roleIds: string[];
   roleIds: string[];
   posts: PostVO[];
   posts: PostVO[];
   postIds: string[];
   postIds: string[];
+  storeIds?: number[];
   roleGroup: string;
   roleGroup: string;
   postGroup: string;
   postGroup: string;
 }
 }

+ 104 - 0
src/components/PermiSelect/index.vue

@@ -0,0 +1,104 @@
+<template>
+  <div class="tree-border permi-select">
+    <el-tree
+      ref="treeRef"
+      :data="treeData"
+      show-checkbox
+      default-expand-all
+      node-key="id"
+      :props="{ label: 'name', children: 'children' }"
+      :check-strictly="true"
+      empty-text="暂无数据"
+      @check="handleCheck"
+    />
+  </div>
+</template>
+
+<script setup lang="ts" name="PermiSelect">
+import { ref, computed, watch, nextTick, PropType } from 'vue';
+import type { ElTree } from 'element-plus';
+
+const props = defineProps({
+  modelValue: {
+    type: Array as PropType<number[]>,
+    default: () => []
+  },
+  options: {
+    type: Array as PropType<any[]>,
+    default: () => []
+  }
+});
+
+const emit = defineEmits(['update:modelValue']);
+
+const treeRef = ref<InstanceType<typeof ElTree>>();
+
+const treeData = computed(() => {
+  return [
+    {
+      id: 0,
+      name: '全部门店',
+      children: props.options
+    }
+  ];
+});
+
+watch(
+  () => props.modelValue,
+  (newVal) => {
+    nextTick(() => {
+      if (!treeRef.value) return;
+      if (!newVal || newVal.length === 0) {
+        treeRef.value.setCheckedKeys([]);
+        const rootNode = treeRef.value.getNode(0);
+        if (rootNode) rootNode.indeterminate = false;
+      } else if (newVal.includes(0)) {
+        const allKeys = [0, ...props.options.map(o => o.id)];
+        treeRef.value.setCheckedKeys(allKeys);
+        const rootNode = treeRef.value.getNode(0);
+        if (rootNode) rootNode.indeterminate = false;
+      } else {
+        treeRef.value.setCheckedKeys(newVal);
+        const rootNode = treeRef.value.getNode(0);
+        if (rootNode) rootNode.indeterminate = newVal.length > 0;
+      }
+    });
+  },
+  { immediate: true, deep: true }
+);
+
+const handleCheck = (data: any, info: any) => {
+  if (!treeRef.value) return;
+  const isChecked = info.checkedKeys.includes(data.id);
+  
+  let newValue: number[] = [];
+  
+  if (data.id === 0) {
+    if (isChecked) {
+      newValue = [0];
+    } else {
+      newValue = [];
+    }
+  } else {
+    // 获取当前树中选中的子节点,不包含 0
+    let currentCheckedChildren = props.options
+      .map(o => o.id)
+      .filter(id => info.checkedKeys.includes(id));
+      
+    newValue = currentCheckedChildren;
+  }
+  
+  emit('update:modelValue', newValue);
+};
+</script>
+
+<style scoped>
+.permi-select {
+  width: 100%;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  padding: 5px;
+  max-height: 200px;
+  overflow-y: auto;
+}
+</style>

+ 1 - 1
src/store/modules/user.ts

@@ -38,7 +38,7 @@ export const useUserStore = defineStore('user', () => {
     if (res) {
     if (res) {
       const data = res.data;
       const data = res.data;
       const user = data.user;
       const user = data.user;
-      const profile = user.avatar == '' || user.avatar == null ? defAva : user.avatar;
+      const profile = user.avatarUrl == '' || user.avatarUrl == null ? defAva : user.avatarUrl;
 
 
       if (data.roles && data.roles.length > 0) {
       if (data.roles && data.roles.length > 0) {
         // 验证返回的roles是否是一个非空数组
         // 验证返回的roles是否是一个非空数组

+ 2108 - 0
src/views/order/management/index.vue

@@ -0,0 +1,2108 @@
+<template>
+  <div class="page-container">
+    <el-card shadow="never" class="table-card">
+      <template #header>
+        <div class="card-header">
+          <span class="title">订单列表</span>
+          <div class="right-panel">
+            <el-radio-group v-model="filters.orderType" size="default" @change="handleSearch">
+              <el-radio-button label="">全部类型</el-radio-button>
+              <el-radio-button label="transport">宠物接送</el-radio-button>
+              <el-radio-button label="feeding">上门喂遛</el-radio-button>
+              <el-radio-button label="washing">上门洗护</el-radio-button>
+            </el-radio-group>
+            <el-input
+              v-model="filters.keyword"
+              placeholder="订单号/商户/宠主/手机号"
+              class="search-input"
+              prefix-icon="Search"
+              clearable
+              @clear="handleSearch"
+              @keyup.enter="handleSearch"
+            />
+            <el-button type="primary" icon="Search" @click="handleSearch">查询</el-button>
+          </div>
+        </div>
+
+        <el-tabs v-model="filters.status" class="status-tabs" @tab-click="handleSearch">
+          <el-tab-pane label="全部订单" name="" />
+          <el-tab-pane label="待派单" name="pending_dispatch" />
+          <el-tab-pane label="待接单" name="pending_accept" />
+          <el-tab-pane label="服务中" name="serving" />
+          <el-tab-pane label="待商家确认" name="pending_confirm" />
+          <el-tab-pane label="已完成" name="completed" />
+          <el-tab-pane label="已取消" name="cancelled" />
+        </el-tabs>
+      </template>
+
+      <el-table :data="tableData" style="width: 100%" v-loading="loading" :header-cell-style="{ background: '#f5f7fa' }">
+        <el-table-column prop="orderNo" label="订单号" width="170" fixed="left" />
+
+        <el-table-column label="服务类型" width="190">
+          <template #default="{ row }">
+            <div class="service-type-cell">
+              <el-tag :type="getTypeTag(row.type)">{{ getTypeName(row.type) }}</el-tag>
+              <!-- 接送细分标签 -->
+              <el-tag v-if="row.type === 'transport' && row.transportType === 'round'" size="small" effect="plain" type="warning" class="sub-tag">往返</el-tag>
+              <!-- Split Tags -->
+              <el-tag v-if="row.splitType === 'pick'" size="small" effect="dark" color="#409eff" style="border:none; color:white;" class="sub-tag">接</el-tag>
+              <el-tag v-if="row.splitType === 'drop'" size="small" effect="dark" color="#67c23a" style="border:none; color:white;" class="sub-tag">送</el-tag>
+
+              <el-tag v-if="row.type === 'transport' && row.transportType === 'pick' && !row.splitType" size="small" effect="plain" class="sub-tag">单程接</el-tag>
+              <el-tag v-if="row.type === 'transport' && row.transportType === 'drop' && !row.splitType" size="small" effect="plain" type="success" class="sub-tag">单程送</el-tag>
+            </div>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="宠物信息" min-width="150">
+          <template #default="{ row }">
+            <div class="pet-info">
+              <el-avatar :size="30" :src="row.petAvatar" :class="'avatar-' + row.type">{{ row.petName?.charAt(0) }}</el-avatar>
+              <div class="pet-detail">
+                <div class="pet-name">
+                  {{ row.petName }}
+                  <span class="pet-breed">{{ row.petBreed }}</span>
+                </div>
+              </div>
+            </div>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="所属用户" width="120" prop="userName">
+          <template #default="{ row }">
+            <span style="font-weight: 500">{{ row.userName }}</span>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="城市/区域" width="140">
+          <template #default="{ row }">
+            <div>{{ row.city }}</div>
+            <div class="sub-text">{{ row.district }}</div>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="接单门店/下单人" min-width="160">
+          <template #default="{ row }">
+            <div class="merchant-info">
+              <div>{{ row.merchantName }}</div>
+              <div class="sub-text">{{ row.contactPhone }}</div>
+            </div>
+          </template>
+        </el-table-column>
+
+        <el-table-column prop="createTime" label="下单时间" width="165" sortable>
+          <template #default="{ row }">
+            <span class="time-text">{{ row.createTime }}</span>
+          </template>
+        </el-table-column>
+
+        <el-table-column prop="serviceTime" label="预约服务时间" width="165" sortable>
+          <template #default="{ row }">
+            <span class="time-text">{{ row.serviceTime }}</span>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="订单状态" width="100">
+          <template #default="{ row }">
+            <div class="status-cell">
+              <div class="status-dot" :class="row.status"></div>
+              <span>{{ getStatusName(row.status) }}</span>
+            </div>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="履约信息" width="140">
+          <template #default="{ row }">
+            <div v-if="row.fulfillerName" class="fulfiller-info">
+              <span class="fulfiller-name">{{ row.fulfillerName }}</span>
+              <span class="fulfiller-fee" v-if="row.fulfillerFee">¥{{ row.fulfillerFee }}</span>
+            </div>
+            <span v-else class="text-gray">暂未指派</span>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="操作" width="200" fixed="right">
+          <template #default="{ row }">
+            <div class="op-cell">
+              <el-button link type="primary" size="small" @click="handleDetail(row)">详情</el-button>
+              <el-button v-if="['pending_dispatch', 'pending_accept'].includes(row.status)" link type="danger" size="small" @click="handleCancel(row)">取消</el-button>
+
+              <el-dropdown v-if="['serving', 'pending_confirm', 'completed'].includes(row.status)" trigger="click" @command="(cmd) => handleCommand(cmd, row)">
+                    <span class="el-dropdown-link">
+                        更多<el-icon class="el-icon--right"><ArrowDown /></el-icon>
+                    </span>
+                <template #dropdown>
+                  <el-dropdown-menu>
+                    <el-dropdown-item v-if="row.status === 'pending_confirm'" command="complete">确认完成</el-dropdown-item>
+                    <el-dropdown-item v-if="['pending_confirm', 'completed'].includes(row.status)" command="care_summary">护理小结</el-dropdown-item>
+                    <el-dropdown-item command="reward">奖惩</el-dropdown-item>
+                    <el-dropdown-item command="remark">备注</el-dropdown-item>
+                  </el-dropdown-menu>
+                </template>
+              </el-dropdown>
+            </div>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <div class="pagination-container">
+        <el-pagination
+          v-model:current-page="pagination.current"
+          v-model:page-size="pagination.size"
+          :page-sizes="[10, 20, 50, 100]"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="pagination.total"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+        />
+      </div>
+    </el-card>
+
+    <!-- 订单详情侧滑栏 -->
+    <el-drawer
+      v-model="detailVisible"
+      title="订单详情"
+      direction="rtl"
+      size="60%"
+      class="order-detail-drawer"
+    >
+      <div class="detail-container" v-if="currentOrder">
+        <!-- 1. Header Status -->
+        <!-- 1. Header Status -->
+        <div class="detail-header">
+          <div class="left-head">
+            <span class="order-no">{{ currentOrder.orderNo }}</span>
+            <el-tag :type="getStatusTag(currentOrder.status)" effect="dark" class="status-tag">{{ getStatusName(currentOrder.status) }}</el-tag>
+            <el-tag effect="plain" class="type-tag" :type="currentOrder.type === 'transport' ? '' : (currentOrder.type === 'feeding' ? 'warning' : 'danger')">
+              {{ getTypeName(currentOrder.type) }}
+            </el-tag>
+          </div>
+          <div class="right-head">
+            <!-- Action Buttons Group -->
+            <div class="detail-actions">
+              <template v-if="currentOrder.status === 'pending_dispatch'">
+                <el-button type="danger" plain icon="CircleClose" @click="handleCancel(currentOrder)">取消订单</el-button>
+              </template>
+
+              <template v-if="currentOrder.status === 'pending_confirm'">
+                <el-button type="primary" icon="CircleCheck" @click="handleCommand('complete', currentOrder)">确认完成</el-button>
+              </template>
+
+              <template v-if="['pending_confirm', 'completed'].includes(currentOrder.status)">
+                <el-button icon="Notebook" @click="openCareSummary(currentOrder)">护理小结</el-button>
+              </template>
+
+              <el-dropdown trigger="click" @command="(cmd) => handleCommand(cmd, currentOrder)" style="margin-left: 12px;">
+                <el-button icon="More">更多操作</el-button>
+                <template #dropdown>
+                  <el-dropdown-menu>
+                    <el-dropdown-item command="reward" icon="Trophy">奖惩操作</el-dropdown-item>
+                    <el-dropdown-item command="remark" icon="EditPen">订单备注</el-dropdown-item>
+                    <el-dropdown-item command="delete" v-if="['cancelled', 'completed'].includes(currentOrder.status)" divided icon="Delete" style="color: #f56c6c;">删除订单</el-dropdown-item>
+                  </el-dropdown-menu>
+                </template>
+              </el-dropdown>
+            </div>
+          </div>
+        </div>
+
+        <div class="detail-scroll-area">
+          <!-- 2. Progress Section -->
+          <div class="progress-section">
+            <el-steps :active="currentOrderSteps.active" finish-status="success" align-center class="custom-steps">
+              <el-step
+                v-for="(step, index) in currentOrderSteps.steps"
+                :key="index"
+                :title="step.title"
+                :description="step.time"
+              />
+            </el-steps>
+          </div>
+
+          <!-- 3. Top Info: Pet & User -->
+          <div class="top-info-row">
+            <!-- Left: Pet Info (Enhanced) -->
+            <div class="info-section pet-section">
+              <div class="sec-header">
+                <span class="label">宠物档案</span>
+                <el-tag size="small" effect="plain">{{ currentOrder.petBreed }}</el-tag>
+              </div>
+              <div class="pet-basic-row">
+                <el-avatar :size="50" :src="currentOrder.petAvatar" shape="square" class="pet-avatar-lg">{{ (currentOrder.petName||'').charAt(0) }}</el-avatar>
+                <div class="pet-names">
+                  <div class="b-name">{{ currentOrder.petName }}
+                    <el-icon v-if="currentOrder.petGender==='male'" color="#409eff"><Male /></el-icon>
+                    <el-icon v-else color="#f56c6c"><Female /></el-icon>
+                  </div>
+                  <div class="b-tags">
+                    <el-tag size="small" type="info">{{ currentOrder.petAge || '未知年龄' }}</el-tag>
+                    <el-tag size="small" type="info">{{ currentOrder.petWeight || '未知体重' }}</el-tag>
+                  </div>
+                </div>
+              </div>
+              <el-descriptions :column="2" size="small" class="pet-desc" border>
+                <el-descriptions-item label="绝育状态">{{ currentOrder.petSterilized ? '已绝育' : '未绝育' }}</el-descriptions-item>
+                <el-descriptions-item label="疫苗状态"><span style="color:#67c23a">{{ currentOrder.petVaccine || '未知' }}</span></el-descriptions-item>
+                <el-descriptions-item label="性格特点">{{ currentOrder.petCharacter || '温顺' }}</el-descriptions-item>
+                <el-descriptions-item label="健康状况">{{ currentOrder.petHealth || '健康' }}</el-descriptions-item>
+              </el-descriptions>
+            </div>
+
+            <!-- Right: User Info -->
+            <div class="info-section user-section">
+              <div class="sec-header">
+                <span class="label">用户信息</span>
+              </div>
+              <div class="user-content">
+                <div class="u-row">
+                  <el-avatar :size="40" :src="currentOrder.userAvatar">{{ (currentOrder.userName||'').charAt(0) }}</el-avatar>
+                  <div class="u-info">
+                    <div class="nm">{{ currentOrder.userName }}</div>
+                    <div class="ph">{{ currentOrder.contactPhone }}</div>
+                  </div>
+                </div>
+                <div class="addr-box">
+                  <div class="addr-label">服务地址</div>
+                  <div class="addr-txt">{{ currentOrder.city }}{{ currentOrder.district }} {{ currentOrder.address || ''}}</div>
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <!-- 4. Bottom Tabs -->
+          <el-tabs v-model="activeDetailTab" class="detail-tabs type-card">
+
+            <!-- Tab 1: Basic Info -->
+            <el-tab-pane label="订单基础信息" name="basic">
+              <div class="tab-pane-content">
+                <!-- A. General Order Info -->
+                <div class="section-block">
+                  <div class="sec-title-bar">基础业务信息</div>
+                  <el-descriptions :column="3" border size="default" class="custom-desc">
+                    <el-descriptions-item label="系统单号">{{ currentOrder.orderNo }}</el-descriptions-item>
+                    <el-descriptions-item label="服务类型">
+                      {{ getTypeName(currentOrder.type) }}
+                      <el-tag size="small" v-if="currentOrder.type==='transport'" style="margin-left:5px" effect="light">{{ getTransportModeName(currentOrder.transportType) }}</el-tag>
+                    </el-descriptions-item>
+
+                    <el-descriptions-item label="归属门店">{{ currentOrder.merchantName }} (平台代下单)</el-descriptions-item>
+                    <el-descriptions-item label="宠主信息">{{ currentOrder.userName }} / {{ currentOrder.contactPhone }}</el-descriptions-item>
+                    <el-descriptions-item label="服务费用" label-class-name="money-label">
+                      <span style="color:#f56c6c; font-weight:bold;">¥ {{ currentOrder.fulfillerFee }}</span>
+                    </el-descriptions-item>
+
+                    <el-descriptions-item label="预约时间">{{ getServiceTimeRange(currentOrder.serviceTime) }}</el-descriptions-item>
+                    <el-descriptions-item label="团购套餐">{{ currentOrder.groupBuyPackage || '未使用团购套餐' }}</el-descriptions-item>
+                    <el-descriptions-item label="创建时间">{{ currentOrder.createTime }}</el-descriptions-item>
+
+                    <el-descriptions-item label="订单备注" :span="3">
+                      {{ currentOrder.remark || '暂无备注' }}
+                    </el-descriptions-item>
+                  </el-descriptions>
+                </div>
+
+                <!-- B. Transport Specifics (Single Display) -->
+                <div v-if="currentOrder.type === 'transport'" class="section-block transport-split-block">
+                  <div class="sec-title-bar">接送任务详情</div>
+                  <div class="transport-grid">
+                    <!-- Pick Up Info (Only if pick or round) -->
+                    <div class="transport-card pick-card" v-if="['round', 'pick'].includes(currentOrder.transportType)">
+                      <div class="t-header">
+                        <div class="left-badges">
+                          <el-tag :type="currentOrder.type === 'feeding' ? 'warning' : ''" effect="dark">{{ getTypeName(currentOrder.type) }}</el-tag>
+                          <el-tag size="small" effect="plain" class="sub-badge">接</el-tag>
+                        </div>
+                        <span class="time">{{ currentOrder.detail.pickTime || currentOrder.serviceTime }}</span>
+                      </div>
+                      <div class="t-body">
+                        <div class="row"><el-icon><Location /></el-icon> <span class="addr">{{ currentOrder.detail.pickAddr }}</span></div>
+                        <div class="row sub"><el-icon><User /></el-icon> {{ currentOrder.detail.pickContact || currentOrder.userName }}  <el-icon><Phone /></el-icon> {{ currentOrder.detail.pickPhone || currentOrder.contactPhone }}</div>
+                      </div>
+                    </div>
+                    <!-- Drop Off Info (Only if drop or round) -->
+                    <div class="transport-card drop-card" v-if="['round', 'drop'].includes(currentOrder.transportType)">
+                      <div class="t-header">
+                        <div class="left-badges">
+                          <el-tag :type="currentOrder.type === 'feeding' ? 'warning' : 'success'" effect="dark">{{ getTypeName(currentOrder.type) }}</el-tag>
+                          <el-tag size="small" effect="plain" class="sub-badge">送</el-tag>
+                        </div>
+                        <span class="time">{{ currentOrder.detail.dropTime || '待定' }}</span>
+                      </div>
+                      <div class="t-body">
+                        <div class="row"><el-icon><Location /></el-icon> <span class="addr">{{ currentOrder.detail.dropAddr }}</span></div>
+                        <div class="row sub"><el-icon><User /></el-icon> {{ currentOrder.detail.dropContact || currentOrder.userName }}  <el-icon><Phone /></el-icon> {{ currentOrder.detail.dropPhone || currentOrder.contactPhone }}</div>
+                      </div>
+                    </div>
+                  </div>
+                </div>
+
+                <!-- C. Other Service Specifics -->
+                <div v-if="['feeding', 'washing'].includes(currentOrder.type)" class="section-block">
+                  <div class="sec-title-bar">服务执行要求</div>
+                  <el-descriptions :column="2" border size="default" class="custom-desc">
+                    <el-descriptions-item label="服务地址" :span="2">{{ currentOrder.detail.area || currentOrder.address }}</el-descriptions-item>
+                    <el-descriptions-item label="服务套餐">{{ currentOrder.detail.packageName }}</el-descriptions-item>
+                    <el-descriptions-item label="特殊要求">{{ currentOrder.detail.petStatus || '无' }}</el-descriptions-item>
+                  </el-descriptions>
+                </div>
+              </div>
+            </el-tab-pane>
+
+            <!-- Tab 2: Fulfiller Info -->
+            <el-tab-pane label="指派履约者" name="fulfiller">
+              <div class="tab-pane-content">
+                <div v-if="currentOrder.fulfillerName" class="fulfiller-card">
+                  <div class="f-left">
+                    <el-avatar :size="60" :src="currentOrder.fulfillerAvatar">{{ currentOrder.fulfillerName.charAt(0) }}</el-avatar>
+                  </div>
+                  <div class="f-right">
+                    <div class="f-row1">
+                      <span class="f-name">{{ currentOrder.fulfillerName }}</span>
+                      <el-tag size="small" type="primary" effect="plain" round>Lv1 普通</el-tag>
+                    </div>
+                    <div class="f-row2">
+                      <span>联系电话:{{ currentOrder.fulfillerPhone || '138****0000' }}</span>
+                      <span class="sep">|</span>
+                      <span>归属区域:{{ currentOrder.fulfillerStation || '朝阳一站' }}</span>
+                    </div>
+                    <div class="f-row3" style="margin-top: 8px; font-size: 13px; color: #606266; background: #f9fafe; padding: 8px; border-radius: 4px; display: flex; gap: 20px;">
+                      <span><span style="color:#909399;">指派时间:</span>{{ currentOrder.createTime }}</span>
+                      <span><span style="color:#909399;">接单时间:</span>{{ currentOrder.detail?.receiveTime || currentOrder.serviceTime }}</span>
+                    </div>
+                  </div>
+                </div>
+                <div v-else class="empty-state">
+                  <el-result icon="info" title="暂无履约者" sub-title="该订单尚未指派履约人员"></el-result>
+                </div>
+              </div>
+            </el-tab-pane>
+
+            <!-- Tab 3: Service Progress -->
+            <el-tab-pane label="服务进度" name="service">
+              <div class="tab-pane-content">
+                <!-- Empty State for Pending Accept -->
+                <div v-if="serviceProgressSteps.length === 0" class="empty-progress" style="padding:40px; text-align:center; color:#909399;">
+                  <el-result icon="info" title="待接单" sub-title="履约者接单后将在此记录服务进度"></el-result>
+                </div>
+
+                <el-timeline style="padding: 10px 20px;" v-else>
+                  <el-timeline-item
+                    v-for="(step, index) in serviceProgressSteps"
+                    :key="index"
+                    :timestamp="step.time"
+                    placement="top"
+                    :color="step.color"
+                    :icon="step.icon"
+                    size="large"
+                  >
+                    <div class="progress-card">
+                      <h4 class="p-title">{{ step.title }}</h4>
+                      <p class="p-desc">{{ step.desc }}</p>
+                      <div class="p-media" v-if="step.media && step.media.length">
+                        <div v-for="(item, i) in step.media" :key="i" class="media-item">
+                          <el-image
+                            v-if="item.type === 'image'"
+                            :src="item.url"
+                            :preview-src-list="step.media.map(m=>m.url)"
+                            fit="cover"
+                            class="p-img"
+                            :preview-teleported="true"
+                          />
+                        </div>
+                      </div>
+                    </div>
+                  </el-timeline-item>
+                </el-timeline>
+              </div>
+            </el-tab-pane>
+
+            <!-- Tab 4: Logs -->
+            <el-tab-pane label="订单日志" name="logs">
+              <div class="tab-pane-content">
+                <div style="display: flex; justify-content: flex-end; margin-bottom: 15px;">
+                  <el-button type="primary" size="small" icon="Download" @click="handleExportLogs">导出日志Excel</el-button>
+                </div>
+                <el-timeline>
+                  <el-timeline-item
+                    v-for="(log, index) in (currentOrder.orderLogs || [])"
+                    :key="index"
+                    :timestamp="log.time"
+                    :type="log.type || 'primary'"
+                    :icon="log.icon"
+                    placement="top"
+                  >
+                    <div class="log-card">
+                      <div class="l-tit">{{ log.title }}</div>
+                      <div class="l-txt">{{ log.content }}</div>
+                    </div>
+                  </el-timeline-item>
+
+                  <!-- Fallback Legacy Mock -->
+                  <el-timeline-item
+                    v-if="(!currentOrder.orderLogs || currentOrder.orderLogs.length === 0) && currentOrder.timeline"
+                    v-for="(log, idx) in currentOrder.timeline"
+                    :key="'old-'+idx"
+                    :timestamp="log.time"
+                    :type="log.type"
+                  >
+                    {{ log.content }}
+                  </el-timeline-item>
+                </el-timeline>
+              </div>
+            </el-tab-pane>
+          </el-tabs>
+
+        </div>
+      </div>
+    </el-drawer>
+
+    <!-- 派单弹窗 (Enhanced) -->
+    <el-dialog
+      v-model="dispatchDialogVisible"
+      title="派单调度"
+      width="900px"
+      top="5vh"
+      destroy-on-close
+      append-to-body
+    >
+      <div class="dispatch-dialog-content">
+        <!-- Top: Order Info (OrderDispatch Style) -->
+        <div class="dispatch-order-info" v-if="currentDispatchOrder">
+          <div class="list-card order-card" style="margin:0; box-shadow:none; cursor:default; border:none;">
+            <div class="card-left">
+              <div class="type-tag" :class="currentDispatchOrder.typeCode">
+                {{ getShortType(currentDispatchOrder.typeCode) }}
+              </div>
+            </div>
+            <div class="card-main">
+              <template v-if="currentDispatchOrder.typeCode === 'transport'">
+                <div class="row-addr" v-if="['round', 'pick'].includes(currentDispatchOrder.transportType)" :title="currentDispatchOrder.pickAddr">
+                  <span class="tag pick">取</span> {{ currentDispatchOrder.pickAddr }}
+                </div>
+                <div class="row-addr" v-if="['round', 'drop'].includes(currentDispatchOrder.transportType)" :title="currentDispatchOrder.dropAddr">
+                  <span class="tag drop">送</span> {{ currentDispatchOrder.dropAddr }}
+                </div>
+              </template>
+              <template v-else>
+                <div class="row-addr" :title="currentDispatchOrder.address">
+                  <span class="tag home">址</span> {{ currentDispatchOrder.address }}
+                </div>
+              </template>
+              <div class="row-time" style="margin-top: 4px;">
+                <el-icon><Clock /></el-icon> {{ currentDispatchOrder.time }}
+                <span class="days-tag" v-if="currentDispatchOrder.daysLater">{{ currentDispatchOrder.daysLater }}</span>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <!-- Current Rider Info (If Exists) -->
+        <div class="current-rider-section" v-if="currentRider">
+          <div class="select-header" style="margin-bottom:8px;">
+            <span class="tit">当前派单履约者</span>
+          </div>
+          <div class="list-card rider-card" style="margin-bottom: 20px; border: 1px solid #e4e7ed; background:#fafafa; cursor:default;">
+            <div class="card-left relative">
+              <el-avatar :src="currentRider.avatar" :size="40" />
+              <div class="dot" :class="currentRider.status"></div>
+            </div>
+            <div class="card-main">
+              <div class="row-1" style="justify-content: space-between; align-items: flex-start; display: flex;">
+                <div style="display:flex; align-items:baseline; gap:8px;">
+                  <span class="r-name">{{ currentRider.name }}</span>
+                  <span class="r-phone">{{ currentRider.maskPhone }}</span>
+                </div>
+                <div class="status-right">
+                  <span class="status-badge" :class="currentRider.status">{{ getRiderStatusText(currentRider.status) }}</span>
+                </div>
+              </div>
+
+              <div class="row-2 categories-row" style="margin-top: 6px; display:flex; gap:4px; flex-wrap:wrap;">
+                <span v-for="cat in currentRider.categories" :key="cat" class="cat-tag" :class="getCategoryClass(cat)">{{ cat }}</span>
+              </div>
+
+              <div class="row-3 time-row" style="margin-top: 4px;">
+                <span class="last-time">下一单: {{ (currentRider.status === 'offline' || currentRider.status === 'disabled') ? '--' : currentRider.lastServiceTime }}</span>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <!-- Middle: Rider Selection -->
+        <div class="dispatch-rider-select">
+          <div class="select-header">
+            <span class="tit">选择履约者 (下一单时间由近及远排序)</span>
+            <el-input
+              v-model="dispatchSearchQuery"
+              placeholder="搜索履约者姓名/手机号"
+              prefix-icon="Search"
+              clearable
+              style="width: 240px"
+            />
+          </div>
+
+          <div class="rider-grid-wrapper">
+            <el-scrollbar height="400px">
+              <div class="rider-grid">
+                <div
+                  v-for="rider in filteredDispatchRiders"
+                  :key="rider.id"
+                  class="list-card rider-card select-card"
+                  :class="{active: selectedRiderId === rider.id}"
+                  @click="selectedRiderId = rider.id"
+                >
+                  <!-- Reusing Rider Card Layout -->
+                  <div class="card-left relative">
+                    <el-avatar :src="rider.avatar" :size="40" />
+                    <div class="dot" :class="rider.status"></div>
+                  </div>
+                  <div class="card-main">
+                    <div class="row-1" style="justify-content: space-between; align-items: flex-start; display: flex;">
+                      <div style="display:flex; align-items:baseline; gap:8px;">
+                        <span class="r-name">{{ rider.name }}</span>
+                        <span class="r-phone">{{ rider.maskPhone }}</span>
+                      </div>
+                      <div class="status-right">
+                        <span class="status-badge" :class="rider.status">{{ getRiderStatusText(rider.status) }}</span>
+                      </div>
+                    </div>
+
+                    <div class="row-2 categories-row" style="margin-top: 6px; display:flex; gap:4px; flex-wrap:wrap;">
+                      <span v-for="cat in rider.categories" :key="cat" class="cat-tag" :class="getCategoryClass(cat)">{{ cat }}</span>
+                    </div>
+
+                    <div class="row-3 time-row" style="margin-top: 4px;">
+                      <span class="last-time">下一单: {{ (rider.status === 'offline' || rider.status === 'disabled') ? '--' : rider.lastServiceTime }}</span>
+                    </div>
+                  </div>
+
+                  <!-- Selected Check -->
+                  <div class="selected-mark" v-if="selectedRiderId === rider.id">
+                    <el-icon><Check /></el-icon>
+                  </div>
+                </div>
+
+                <div v-if="filteredDispatchRiders.length === 0" class="empty-text">暂无符合条件的履约者</div>
+              </div>
+            </el-scrollbar>
+          </div>
+        </div>
+
+        <!-- Bottom: Fee & Submit -->
+        <div class="dispatch-footer">
+          <div class="fee-input">
+            <span class="label">服务费用:</span>
+            <el-input-number v-model="dispatchFee" :min="0" :precision="2" :step="10" placeholder="请输入" style="width: 140px;" />
+            <span class="unit">元</span>
+          </div>
+          <div class="btns">
+            <el-button @click="dispatchDialogVisible = false">取消</el-button>
+            <el-button type="primary" @click="handleDispatchSubmit">确认派单</el-button>
+          </div>
+        </div>
+      </div>
+    </el-dialog>
+
+    <!-- 护理小结侧滑栏 -->
+    <el-drawer
+      v-model="careSummaryVisible"
+      title="宠物护理工作小结"
+      direction="rtl"
+      size="750px"
+      destroy-on-close
+      class="care-summary-drawer"
+    >
+      <div class="care-summary-container" v-if="careSummaryOrder">
+        <!-- Pet Header -->
+        <div class="summary-header">
+          <div class="avatar-wrapper">
+            <el-avatar :size="80" :src="careSummaryOrder.petAvatar" shape="circle" class="pet-summary-avatar">{{ careSummaryOrder.petName?.charAt(0) }}</el-avatar>
+          </div>
+          <div class="pet-summary-info">
+            <div class="summary-name-row">
+              <span class="name">{{ careSummaryOrder.petName }}</span>
+              <div class="tags-group">
+                <el-tag :type="careSummaryOrder.petGender==='male'?'':'danger'" effect="light" round>
+                  <el-icon><component :is="careSummaryOrder.petGender==='male'?'Male':'Female'" /></el-icon>
+                  {{ careSummaryOrder.petAge }}
+                </el-tag>
+                <el-tag v-for="tag in (careSummaryOrder.petTags||[])" :key="tag" type="warning" effect="plain" round>{{ tag }}</el-tag>
+              </div>
+            </div>
+            <div class="summary-sub-row">
+              <div class="info-item">
+                <span class="lbl">品种</span>
+                <span class="val">{{ careSummaryOrder.petBreed || '未知' }}</span>
+              </div>
+              <div class="divider-v"></div>
+              <div class="info-item">
+                <span class="lbl">体重</span>
+                <span class="val">{{ careSummaryOrder.petWeight }}</span>
+              </div>
+              <div class="divider-v"></div>
+              <div class="info-item">
+                <span class="lbl">主人</span>
+                <span class="val">{{ careSummaryOrder.userName || '未知' }}</span>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <!-- Info Groups -->
+        <div class="summary-section">
+          <div class="sec-title">
+            <span class="icon-box"><el-icon><List /></el-icon></span>
+            基本信息
+          </div>
+          <el-descriptions :column="2" border class="spacious-desc">
+            <el-descriptions-item label="性格关键词">{{ careSummaryOrder.petPersonality }}</el-descriptions-item>
+            <el-descriptions-item label="健康状况">
+              <el-tag :type="careSummaryOrder.healthStatus==='健康'?'success':'danger'" effect="light" size="small">{{ careSummaryOrder.healthStatus }}</el-tag>
+            </el-descriptions-item>
+            <el-descriptions-item label="疫苗情况">
+              <div class="flex-align">
+                <span style="color:#67c23a; margin-right:8px;" v-if="careSummaryOrder.vaccineImg"><el-icon><CircleCheckFilled /></el-icon> 已接种</span>
+                <span v-else style="color:#909399;">未接种</span>
+                <el-image
+                  v-if="careSummaryOrder.vaccineImg"
+                  style="width: 24px; height: 24px; border-radius:4px; vertical-align:middle; cursor:zoom-in;"
+                  :src="careSummaryOrder.vaccineImg"
+                  :preview-src-list="[careSummaryOrder.vaccineImg]"
+                  :preview-teleported="true"
+                />
+              </div>
+            </el-descriptions-item>
+            <el-descriptions-item label="过敏史">
+              <span :style="{color: careSummaryOrder.allergy ? '#f56c6c' : 'inherit'}">{{ careSummaryOrder.allergy || '无' }}</span>
+            </el-descriptions-item>
+          </el-descriptions>
+        </div>
+
+        <div class="summary-section">
+          <div class="sec-title">
+            <span class="icon-box text-blue"><el-icon><HomeFilled /></el-icon></span>
+            服务环境
+          </div>
+          <el-descriptions :column="2" border class="spacious-desc">
+            <el-descriptions-item label="到家时间">{{ careSummaryOrder.homeTime }}</el-descriptions-item>
+            <el-descriptions-item label="房屋类型">{{ careSummaryOrder.houseType }}</el-descriptions-item>
+            <el-descriptions-item label="入户方式" :span="2">
+              <span style="font-weight:bold;">{{ careSummaryOrder.entryMethod }}</span>
+              <span style="margin-left:8px; color:#909399;">({{ careSummaryOrder.entryDetail }})</span>
+            </el-descriptions-item>
+          </el-descriptions>
+        </div>
+
+        <!-- Service Log -->
+        <div class="summary-section main-log">
+          <div class="sec-title" style="border:none; padding-left:0; margin-bottom:16px;">
+            <div class="left">
+              <span class="icon-box text-orange"><el-icon><Notebook /></el-icon></span>
+              服务内容记录
+            </div>
+            <el-button v-if="!isEditingSummary" type="primary" link icon="Edit" @click="isEditingSummary = true">编辑</el-button>
+          </div>
+
+          <div v-if="isEditingSummary" class="edit-area">
+            <el-input
+              v-model="careSummaryText"
+              type="textarea"
+              :rows="12"
+              placeholder="请输入详细的护理服务小结..."
+              resize="none"
+            />
+            <div class="edit-actions">
+              <el-button @click="isEditingSummary = false">取消</el-button>
+              <el-button type="primary" @click="saveCareSummary">保存内容</el-button>
+            </div>
+          </div>
+          <div v-else class="log-content-box">
+            <pre class="log-text">{{ careSummaryText }}</pre>
+          </div>
+        </div>
+
+        <!-- Footer Info -->
+        <div class="summary-footer">
+          <div class="footer-info">
+            <div class="f-row">
+              <span class="lbl">护宠师</span>
+              <span class="val user-active">{{ careSummaryOrder.fulfillerName || '当前履约者' }}</span>
+            </div>
+            <div class="f-row">
+              <span class="lbl">提交时间</span>
+              <span class="val">{{ careSummaryOrder.summaryTime || '2024-02-04 17:00' }}</span>
+            </div>
+          </div>
+          <div class="footer-action">
+            <el-button size="large" @click="careSummaryVisible = false">关闭</el-button>
+          </div>
+        </div>
+      </div>
+    </el-drawer>
+
+    <!-- 奖惩弹窗 -->
+    <el-dialog v-model="rewardDialogVisible" title="奖惩操作" width="500px">
+      <div v-if="currentOperateRow" style="padding: 0 10px;">
+        <div style="margin-bottom: 20px; font-size: 14px; color: #606266; line-height: 1.6; background: #fdf6ec; padding: 10px; border-radius: 4px;">
+          <div>奖惩履约者:<span style="font-weight: bold; color: #303133;">{{ currentOperateRow.fulfillerName || '未指派' }}</span></div>
+          <div style="font-size: 13px; margin-top: 4px;">订单号:{{ currentOperateRow.orderNo }}</div>
+          <div style="font-size: 13px; margin-top: 4px; display:flex; align-items:center; gap:6px;">
+            服务类型:
+            <el-tag :type="getTypeTag(currentOperateRow.type)" size="small">{{ getTypeName(currentOperateRow.type) }}</el-tag>
+            <el-tag v-if="currentOperateRow.type === 'transport' && currentOperateRow.transportType === 'round'" size="small" effect="plain" type="warning">往返</el-tag>
+            <el-tag v-if="currentOperateRow.splitType === 'pick'" size="small" effect="dark" color="#409eff" style="border:none; color:white;">接</el-tag>
+            <el-tag v-if="currentOperateRow.splitType === 'drop'" size="small" effect="dark" color="#67c23a" style="border:none; color:white;">送</el-tag>
+            <el-tag v-if="currentOperateRow.type === 'transport' && currentOperateRow.transportType === 'pick' && !currentOperateRow.splitType" size="small" effect="plain">单程接</el-tag>
+            <el-tag v-if="currentOperateRow.type === 'transport' && currentOperateRow.transportType === 'drop' && !currentOperateRow.splitType" size="small" effect="plain" type="success">单程送</el-tag>
+          </div>
+        </div>
+
+        <el-form :model="rewardForm" label-width="80px">
+          <el-form-item label="操作类型">
+            <el-radio-group v-model="rewardForm.type">
+              <el-radio label="reward">奖励 (增加)</el-radio>
+              <el-radio label="punish">惩罚 (扣除)</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="调整项目">
+            <el-radio-group v-model="rewardForm.item">
+              <el-radio label="points">积分</el-radio>
+              <el-radio label="amount">金额 (元)</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="数额" required>
+            <el-input-number v-model="rewardForm.value" :min="1" :step="10" />
+          </el-form-item>
+          <el-form-item label="原因备注" required>
+            <el-input
+              v-model="rewardForm.reason"
+              type="textarea"
+              :rows="3"
+              placeholder="请输入奖惩原因..."
+            />
+          </el-form-item>
+        </el-form>
+      </div>
+      <template #footer>
+        <el-button @click="rewardDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="handleRewardSubmit">确认执行</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 备注弹窗 -->
+    <el-dialog v-model="remarkDialogVisible" title="订单备注" width="500px">
+      <div style="margin-bottom:10px; font-size:13px; color:#909399;">
+        <span v-if="currentOperateRow">订单号:{{ currentOperateRow.orderNo }}</span>
+      </div>
+      <el-input
+        v-model="remarkForm"
+        type="textarea"
+        :rows="5"
+        placeholder="请输入订单备注信息..."
+      />
+      <template #footer>
+            <span class="dialog-footer">
+                <el-button @click="remarkDialogVisible = false">取消</el-button>
+                <el-button type="primary" @click="handleRemarkSubmit">保存备注</el-button>
+            </span>
+      </template>
+    </el-dialog>
+
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, computed } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+
+const loading = ref(false)
+const detailVisible = ref(false)
+const dispatchVisible = ref(false)
+const currentOrder = ref(null)
+
+const filters = reactive({
+  orderType: '',
+  status: '',
+  keyword: ''
+})
+
+const pagination = reactive({
+  current: 1,
+  size: 10,
+  total: 100
+})
+
+const dispatchForm = reactive({
+  orderId: null,
+  fulfillerId: null,
+  fee: 0,
+  remark: ''
+})
+
+// Mocks
+const fulfillerOptions = ref([
+  { id: 101, name: '王大力', distance: '1.2km' },
+  { id: 102, name: '张小美', distance: '3.5km' },
+  { id: 103, name: '李建国', distance: '0.8km' }
+])
+
+const tableData = ref([])
+
+const mockData = [
+  // 宠物接送 - 待派单 (往返)
+  {
+    id: 1,
+    orderNo: 'ORD202402048801',
+    type: 'transport',
+    transportType: 'round', // 往返
+    petName: '旺财',
+    petBreed: '金毛',
+    petAvatar: '',
+    userName: '李先生',
+    city: '北京市',
+    district: '朝阳区',
+    createTime: '2024-02-04 09:30',
+    merchantName: '爱宠生活馆 (三里屯店)',
+    contactPhone: '13812345678',
+    serviceTime: '2024-02-05 10:00',
+    status: 'pending_dispatch',
+    fulfillerName: '',
+    fulfillerFee: 0,
+    detail: {
+      pickAddr: '朝阳区三里屯SOHO B座',
+      pickContact: '张先生',
+      pickPhone: '138xxxx',
+      pickTime: '10:00',
+      dropAddr: '朝阳区某某宠物医院',
+      dropContact: '前台',
+      dropPhone: '010-xxxx',
+    },
+    timeline: [{ time: '2024-02-04 09:30', content: '商户下单成功', type: 'primary' }]
+  },
+  // 宠物接送 - 待接单 (单程接)
+  {
+    id: 2,
+    orderNo: 'ORD202402048802',
+    type: 'transport',
+    transportType: 'pick', // 单程接
+    petName: 'Bella',
+    petBreed: '拉布拉多',
+    petAvatar: '',
+    userName: '赵女士',
+    city: '北京市',
+    district: '海淀区',
+    createTime: '2024-02-04 10:00',
+    merchantName: '爱宠生活馆 (国贸店)',
+    contactPhone: '13911112222',
+    serviceTime: '2024-02-05 14:00',
+    status: 'pending_accept',
+    fulfillerName: '王大力',
+    fulfillerFee: 35.00,
+    detail: {
+      pickAddr: '海淀区万柳书院',
+      pickContact: '李女士',
+      pickPhone: '139xxxx',
+      pickTime: '14:00',
+      dropAddr: '',
+    },
+    timeline: [
+      { time: '2024-02-04 10:00', content: '下单成功', type: 'info' },
+      { time: '2024-02-04 10:30', content: '已派单给 王大力', type: 'primary' }
+    ]
+  },
+  // 宠物接送 - 服务中 (单程送)
+  {
+    id: 3,
+    orderNo: 'ORD202402048803',
+    type: 'transport',
+    transportType: 'drop', // 单程送
+    petName: 'Cookie',
+    petBreed: '柯基',
+    petAvatar: '',
+    userName: '王先生',
+    city: '北京市',
+    district: '朝阳区',
+    createTime: '2024-02-04 12:00',
+    merchantName: '爱宠生活馆 (中关村店)',
+    contactPhone: '13612345678',
+    serviceTime: '2024-02-04 18:00',
+    status: 'serving',
+    fulfillerName: '张小美',
+    fulfillerFee: 40.00,
+    detail: {
+      pickAddr: '', // 单程送不需要接的详细起始点,实际业务中是已在店
+      dropAddr: '朝阳公园西门',
+      dropContact: '王先生',
+      dropPhone: '136xxxx',
+    },
+    timeline: [
+      { time: '2024-02-04 12:00', content: '下单成功', type: 'info' },
+      { time: '2024-02-04 17:50', content: '履约者出发', type: 'primary' }
+    ]
+  },
+  // 上门喂遛 - 待派单
+  {
+    id: 4,
+    orderNo: 'ORD202402048804',
+    type: 'feeding',
+    petName: '咪咪',
+    petBreed: '布偶猫',
+    petAvatar: '',
+    userName: '李女士',
+    city: '上海市',
+    district: '徐汇区',
+    createTime: '2024-02-04 12:00',
+    merchantName: '爱宠生活馆 (三里屯店)',
+    contactPhone: '13987654321',
+    serviceTime: '2024-02-06 12:00',
+    status: 'pending_dispatch',
+    fulfillerName: '',
+    fulfillerFee: 0,
+    detail: {
+      packageName: '春节上门喂猫7天套餐',
+      currentCount: 1,
+      totalCount: 7,
+      area: '客厅、阳台',
+      itemLoc: '厨房柜子里',
+      cleanLoc: '卫生间洗手台',
+      foodAmount: '每次一罐头 + 半碗粮',
+    },
+    timeline: [{ time: '2024-02-04 12:00', content: '订单创建', type: 'info' }]
+  },
+  // 上门喂遛 - 服务中
+  {
+    id: 5,
+    orderNo: 'ORD202402048805',
+    type: 'feeding',
+    petName: '大黄',
+    petBreed: '中华田园犬',
+    petAvatar: '',
+    userName: '张先生',
+    city: '杭州市',
+    district: '西湖区',
+    createTime: '2024-02-04 12:30',
+    merchantName: '爱宠生活馆 (国贸店)',
+    contactPhone: '13555555555',
+    serviceTime: '2024-02-04 14:00',
+    status: 'serving',
+    fulfillerName: '李建国',
+    fulfillerFee: 50.00,
+    detail: {
+      packageName: '上门遛狗1小时',
+      currentCount: 1,
+      totalCount: 1,
+      area: '小区公园',
+      itemLoc: '门口鞋柜处',
+      cleanLoc: '楼下垃圾桶',
+      foodAmount: '-',
+    },
+    timeline: [
+      { time: '2024-02-04 12:00', content: '张小美 已接单', type: 'primary' },
+      { time: '2024-02-04 13:55', content: '履约者到达并在门口打卡', type: 'success', media: ['https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg'] }
+    ]
+  },
+  // 上门洗护 - 待接单
+  {
+    id: 6,
+    orderNo: 'ORD202402048806',
+    type: 'washing',
+    petName: '豆豆',
+    petBreed: '泰迪',
+    petAvatar: '',
+    userName: '刘小姐',
+    city: '深圳市',
+    district: '南山区',
+    createTime: '2024-02-04 15:00',
+    merchantName: '爱宠生活馆 (三里屯店)',
+    contactPhone: '13666666666',
+    serviceTime: '2024-02-04 16:00',
+    status: 'pending_accept',
+    fulfillerName: '王大力',
+    fulfillerFee: 80.00,
+    detail: {
+      packageName: '精致洗护套餐',
+      petStatus: '胆小,怕吹风机',
+      area: '浴室',
+      cleanLoc: '浴室淋浴间',
+      toolLoc: '自带工具箱',
+    },
+    timeline: [
+      { time: '2024-02-04 15:00', content: '订单创建', type: 'info' },
+      { time: '2024-02-04 15:05', content: '派单给 王大力', type: 'primary' }
+    ]
+  },
+  // 上门洗护 - 已取消
+  {
+    id: 7,
+    orderNo: 'ORD202402047701',
+    type: 'washing',
+    petName: 'Snow',
+    petBreed: '萨摩耶',
+    petAvatar: '',
+    userName: 'Zhao',
+    city: '北京市',
+    district: '朝阳区',
+    createTime: '2024-02-02 10:00',
+    merchantName: '爱宠生活馆 (国贸店)',
+    contactPhone: '13777777777',
+    serviceTime: '2024-02-03 09:00',
+    status: 'cancelled',
+    fulfillerName: '',
+    fulfillerFee: 0,
+    detail: { packageName: '洗澡+美容', petStatus: '温顺' },
+    timeline: [
+      { time: '2024-02-02 10:00', content: '订单创建', type: 'info' },
+      { time: '2024-02-02 12:00', content: '用户取消订单', type: 'warning' }
+    ]
+  },
+  // 宠物接送 - 已完成 (往返)
+  {
+    id: 8,
+    orderNo: 'ORD202402038899',
+    type: 'transport',
+    transportType: 'round',
+    petName: 'Cooper',
+    petBreed: '法斗',
+    petAvatar: '',
+    userName: '孙女士',
+    city: '北京市',
+    district: '通州区',
+    createTime: '2024-02-03 09:00',
+    merchantName: '爱宠生活馆 (三里屯店)',
+    contactPhone: '13333333333',
+    serviceTime: '2024-02-03 10:00',
+    status: 'completed',
+    fulfillerName: '张小美',
+    fulfillerFee: 65.00,
+    detail: {
+      pickAddr: '朝阳大悦城',
+      dropAddr: '瑞鹏宠物医院',
+    },
+    timeline: [
+      { time: '2024-02-03 09:00', content: '订单创建', type: 'info' },
+      { time: '2024-02-03 11:30', content: '服务完成', type: 'success' }
+    ]
+  },
+  // 待商家确认
+  {
+    id: 99,
+    orderNo: 'ORD202402048999',
+    type: 'feeding',
+    petName: '小黑',
+    petBreed: '拉布拉多',
+    petAvatar: '',
+    userName: '周先生',
+    city: '北京市',
+    district: '海淀区',
+    createTime: '2024-02-04 14:00',
+    merchantName: '爱宠生活馆 (三里屯店)',
+    contactPhone: '13811112222',
+    serviceTime: '2024-02-04 16:00',
+    status: 'pending_confirm',
+    fulfillerName: '赵铁柱',
+    fulfillerFee: 45.00,
+    detail: {
+      packageName: '上门喂遛',
+      area: '小区内部',
+    },
+    timeline: [
+      { time: '2024-02-04 14:00', content: '订单创建', type: 'info' },
+      { time: '2024-02-04 14:05', content: '赵铁柱 已接单', type: 'primary' },
+      { time: '2024-02-04 16:30', content: '履约者完成服务,等待商家确认', type: 'warning' }
+    ]
+  }
+]
+
+onMounted(() => {
+  handleSearch()
+})
+
+const handleSearch = () => {
+  loading.value = true
+  setTimeout(() => {
+    let res = mockData
+    if(filters.orderType) res = res.filter(i => i.type === filters.orderType)
+    if(filters.status) res = res.filter(i => i.status === filters.status)
+    if(filters.keyword) {
+      const k = filters.keyword
+      res = res.filter(i => i.orderNo.includes(k) || i.merchantName.includes(k) || i.petName.includes(k))
+    }
+
+    // Split Round type orders
+    let processedData = []
+    res.forEach(item => {
+      if (item.type === 'transport' && item.transportType === 'round') {
+        // Split into Pick (接) and Drop (送)
+        // We clone the item. Ideally IDs should be unique for keys, but for display it's ok or append suffix.
+        let pickObj = { ...item, id: item.id + '_1', splitType: 'pick' }
+        let dropObj = { ...item, id: item.id + '_2', splitType: 'drop' }
+        processedData.push(pickObj)
+        processedData.push(dropObj)
+      } else {
+        processedData.push(item)
+      }
+    })
+
+    tableData.value = processedData
+    pagination.total = processedData.length
+    loading.value = false
+  }, 500)
+}
+
+const handleSizeChange = (val) => { console.log(val) }
+const handleCurrentChange = (val) => { console.log(val) }
+
+const getTypeTag = (type) => {
+  const map = { transport: '', feeding: 'warning', washing: 'success' }
+  return map[type]
+}
+const getTypeName = (type) => {
+  const map = { transport: '宠物接送', feeding: '上门喂遛', washing: '上门洗护' }
+  return map[type]
+}
+const getStatusName = (status) => {
+  const map = { pending_dispatch: '待派单', pending_accept: '待接单', serving: '服务中', pending_confirm: '待商家确认', completed: '已完成', cancelled: '已取消' }
+  return map[status]
+}
+
+const getStatusTag = (status) => {
+  const map = {
+    pending_dispatch: 'danger',
+    pending_accept: 'warning',
+    serving: 'primary',
+    pending_confirm: 'warning',
+    completed: 'success',
+    cancelled: 'info'
+  }
+  return map[status] || 'info'
+}
+
+const getStepActive = (status) => {
+  // Kept for table list if needed, but detail now uses computed steps below
+  if(status === 'pending_dispatch') return 1
+  if(status === 'pending_accept') return 2
+  if(status === 'serving') return 3
+  if(status === 'pending_confirm') return 4
+  if(status === 'completed') return 6
+  if(status === 'cancelled') return 0
+  return 0
+}
+
+const getTransportModeName = (type) => {
+  const map = { round: '往返接送', pick: '单程接(到店)', drop: '单程送(回家)' }
+  return map[type] || '接送服务'
+}
+
+// Generate dynamic steps with times for detail view
+const currentOrderSteps = computed(() => {
+  if(!currentOrder.value) return { active: 0, steps: [] }
+
+  // Base timeline nodes
+  const steps = [
+    { title: '商户下单', status: 'created', time: '' },
+    { title: '运营派单', status: 'dispatched', time: '' },
+    { title: '履约接单', status: 'accepted', time: '' },
+    { title: '服务中', status: 'serving', time: '' }, // Includes arrival/start
+    { title: '待商家确认', status: 'confirming', time: '' }, // Finished service
+    { title: '已完成', status: 'completed', time: '' }
+  ]
+
+  const logs = currentOrder.value.orderLogs || []
+  const status = currentOrder.value.status
+
+  let active = 0
+
+  // Map logs to steps time
+  // This is a simple mapper, in real app would match specific log types/codes
+  const findTime = (keyword) => {
+    const log = logs.find(l => l.title.includes(keyword) || l.content.includes(keyword))
+    return log ? log.time : ''
+  }
+
+  // 1. Created
+  steps[0].time = currentOrder.value.createTime || findTime('下单') || findTime('创建')
+  if (steps[0].time) active = 1
+
+  // 2. Dispatched
+  steps[1].time = findTime('派单') || (status !== 'pending_dispatch' ? steps[0].time : '') // Mock if passed
+  if (['pending_accept', 'serving', 'pending_confirm', 'completed'].includes(status)) active = 2
+
+  // 3. Accepted
+  steps[2].time = findTime('接单')
+  if (['pending_accept'].includes(status)) {
+    steps[2].title = '待履约者接单'
+  } else if (['serving', 'pending_confirm', 'completed'].includes(status)) {
+    steps[2].title = '履约者已接单'
+    active = 3
+  }
+
+  // 4. Serving (Arrival/Start)
+  steps[3].time = findTime('到达') || findTime('出发')
+  if (['serving'].includes(status)) {
+    steps[3].title = '服务进行中'
+  } else if (['pending_confirm', 'completed'].includes(status)) {
+    steps[3].title = '服务已完成'
+    active = 4
+  }
+
+  // 5. Confirming
+  steps[4].time = findTime('等待商家确认') || findTime('待验收')
+  if (['pending_confirm'].includes(status)) {
+    steps[4].title = '待商家确认'
+  } else if (['completed'].includes(status)) {
+    steps[4].title = '商家已确认'
+    active = 5
+  }
+
+  // 6. Completed
+  if (status === 'completed') {
+    steps[5].time = findTime('完成') // or calculate based on logic
+    active = 6
+  }
+
+  if (status === 'cancelled') {
+    // Handle cancelled state simply or insert a cancelled step
+    return {
+      active: 1,
+      steps: [
+        { title: '商户下单', time: steps[0].time },
+        { title: '已取消', time: findTime('取消') || '订单已取消' }
+      ]
+    }
+  }
+
+  return { active, steps }
+})
+
+const activeDetailTab = ref('basic')
+
+const handleDetail = (row) => {
+  // Inject rich mock detail data
+  currentOrder.value = {
+    ...row,
+    // Mock extended user info
+    userAvatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
+    userLevel: 'Lv3',
+    userRegDays: '210天',
+    userTotalSpend: '3580',
+    address: '某小区5号楼2单元101',
+    address: '某小区5号楼2单元101',
+    groupBuyPackage: '新客体验9.9元洗护套餐',
+    transportType: 'pick', // 模拟单程接
+    detail: {
+      ...row.detail,
+      pickTime: '2024-02-05 09:30',
+      pickAddr: '北京市朝阳区某小区5号楼2单元101',
+      pickContact: '李先生',
+      pickPhone: '13812345678',
+      dropTime: '2024-02-05 18:30',
+      dropAddr: '北京市朝阳区某小区5号楼2单元101',
+      dropContact: '李先生',
+      dropPhone: '13812345678',
+      packageName: '精细洗护套餐A',
+      petStatus: '胆小,需安抚',
+      area: '北京市朝阳区某小区5号楼2单元101'
+    },
+    petGender: 'male',
+    petAge: '2岁',
+    petWeight: '15kg',
+    petVaccine: '已接种',
+    petSterilized: true,
+    petCharacter: '活泼好动,喜欢球类玩具',
+    petHealth: '健康良好',
+    // Mock fulfiller info
+    fulfillerAvatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
+    fulfillerPhone: '13812345678',
+    fulfillerStation: '朝阳服务站',
+    fulfillerRating: 4.8,
+    // Mock order logs
+    orderLogs: [
+      { time: '2024-02-04 09:30', title: '订单创建', content: '商户提交订单', icon: 'Document' },
+      { time: '2024-02-04 10:00', title: '系统派单', content: '指派给 王大力', icon: 'Bicycle' },
+      { time: '2024-02-04 10:05', title: '接单成功', content: '履约者已确认接单', icon: 'CircleCheck' },
+      { time: '2024-02-04 13:55', title: '到达服务点', content: '履约者已打卡', icon: 'Location' }
+    ]
+  }
+  activeDetailTab.value = 'basic'
+  detailVisible.value = true
+}
+
+
+// --- Dispatch Dialog Logic (Copied from OrderDispatch) ---
+const ridersList = ref([
+  {
+    id: 101, name: '王大力', station: '朝阳站', phone: '13800138000', maskPhone: '138****8000',
+    status: 'online', categories: ['接送', '喂遛'], lastServiceTime: '2024-02-07 11:00',
+    avatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
+    pendingCount: 0, todoCount: 2, lng: 116.460, lat: 39.920
+  },
+  {
+    id: 102, name: '李小龙', station: '海淀站', phone: '13912345678', maskPhone: '139****5678',
+    status: 'online', categories: ['接送', '洗护'], lastServiceTime: '2024-02-07 10:30',
+    avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
+    pendingCount: 1, todoCount: 3, lng: 116.450, lat: 39.915
+  },
+  {
+    id: 103, name: '张小美', station: '望京站', phone: '13666666666', maskPhone: '136****6666',
+    status: 'online', categories: ['喂遛'], lastServiceTime: '2024-02-07 10:45',
+    avatar: '',
+    pendingCount: 0, todoCount: 0, lng: 116.470, lat: 39.930
+  },
+  {
+    id: 104, name: '赵铁柱', station: '通州站', phone: '13555555555', maskPhone: '135****5555',
+    status: 'offline', categories: ['接送'], lastServiceTime: '2024-02-06 18:00',
+    avatar: '',
+    pendingCount: 0, todoCount: 0, lng: 116.440, lat: 39.910
+  },
+  {
+    id: 105, name: '孙悟空', station: '花果山', phone: '13888888888', maskPhone: '138****8888',
+    status: 'online', categories: ['接送', '喂遛', '洗护'], lastServiceTime: '2024-02-07 09:30',
+    avatar: '',
+    pendingCount: 0, todoCount: 1, lng: 116.480, lat: 39.925
+  }
+])
+
+const serviceProgressSteps = computed(() => {
+  const order = currentOrder.value
+  if (!order) return []
+
+  // 1. Pending / Waiting for Rider: No progress yet
+  // Strict requirement: "待接单时应该为空"
+  if (['pending', 'pending_dispatch', 'pending_accept', 'cancelled'].includes(order.status)) {
+    return []
+  }
+
+  const baseTime = order.serviceTime || '2024-02-10 10:00'
+  const datePart = baseTime.split(' ')[0]
+  const isTransport = order.type === 'transport'
+
+  let steps = []
+
+  // --- Step 1: Accepted (Start for Serving+) ---
+  steps.push({
+    title: '已接单',
+    time: `${datePart} 09:30`,
+    icon: 'Bicycle',
+    color: '#ff9900', // Active color
+    desc: `履约者 ${order.fulfillerName || '当前履约者'} 已确认接单,准备前往服务地点`,
+    media: []
+  })
+
+  // --- Step 2: Arrived (Assume arrived if serving) ---
+  steps.push({
+    title: '到达打卡',
+    time: `${datePart} 09:50`,
+    icon: 'Location',
+    color: '#ff9900',
+    desc: '已到达指定位置,打卡确认',
+    media: [
+      { type: 'image', url: 'https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg' }
+    ]
+  })
+
+  if (isTransport) {
+    // Transport Flow
+    // --- Step 3: Depart (Active in Serving) ---
+    steps.push({
+      title: '确认出发',
+      time: `${datePart} 10:10`,
+      icon: 'Van',
+      color: '#ff9900',
+      desc: '接到宠物,状态良好,开始运输。备注:宠物很乖,已放入航空箱。',
+      media: [
+        { type: 'image', url: 'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg' },
+        { type: 'image', url: 'https://fuss10.elemecdn.com/a/3f/3302e58f9a181d2509f3dc0fa68b0jpeg.jpeg' }
+      ]
+    })
+
+    // --- Step 4: Deliver (Only if Confirming or Completed) ---
+    if (['pending_confirm', 'completed'].includes(order.status)) {
+      steps.push({
+        title: '送达打卡',
+        time: `${datePart} 10:50`,
+        icon: 'Place',
+        color: '#ff9900',
+        desc: '宠物已安全送达目的地,等待商家验收',
+        media: [
+          { type: 'image', url: 'https://fuss10.elemecdn.com/d/e6/c4d93a3805b3ce3f323f7974e6f78jpeg.jpeg' }
+        ]
+      })
+    }
+  } else {
+    // Service Flow
+    // --- Step 3: Start Service (Active in Serving) ---
+    steps.push({
+      title: '开始服务',
+      time: `${datePart} 10:00`,
+      icon: 'VideoPlay',
+      color: '#ff9900',
+      desc: '已确认宠物状态,开始进行服务视频录制',
+      media: [
+        { type: 'image', url: 'https://fuss10.elemecdn.com/0/6f/e35ff375812e6b0020b6b4e8f9583jpeg.jpeg' }
+      ]
+    })
+
+    // --- Step 4: End Service (Only if Confirming or Completed) ---
+    if (['pending_confirm', 'completed'].includes(order.status)) {
+      steps.push({
+        title: '服务结束',
+        time: `${datePart} 10:50`,
+        icon: 'VideoPause',
+        color: '#ff9900',
+        desc: '服务项目已全部完成,清理现场完毕。备注:狗狗今天很配合,完成了梳毛和喂食。',
+        media: [
+          { type: 'image', url: 'https://fuss10.elemecdn.com/3/28/bbf893f792f03a54408b3b7a7ebf0jpeg.jpeg' },
+          { type: 'image', url: 'https://fuss10.elemecdn.com/2/11/6535bcfb26e4c79b48ddde44f4b6fjpeg.jpeg' }
+        ]
+      })
+    }
+  }
+
+  // --- Step 5: Wait Confirm (Only if Confirming or Completed) ---
+  if (['pending_confirm', 'completed'].includes(order.status)) {
+    steps.push({
+      title: '待商家确认',
+      time: `${datePart} 10:55`,
+      icon: 'Clock',
+      color: '#ff9900',
+      desc: '履约者已提交完成信息,等待商家确认订单',
+      media: []
+    })
+  }
+
+  // --- Step 6: Completed (Only if Completed) ---
+  if (order.status === 'completed') {
+    steps.push({
+      title: '订单完成',
+      time: `${datePart} 11:00`,
+      icon: 'Select',
+      color: '#67C23A',
+      desc: '用户/商家已确认,服务圆满结束',
+      media: []
+    })
+  }
+
+  return steps
+})
+
+const dispatchDialogVisible = ref(false)
+const currentDispatchOrder = ref(null)
+const currentRider = ref(null)
+const dispatchSearchQuery = ref('')
+const selectedRiderId = ref(null)
+const dispatchFee = ref(0) // Default fee
+
+const openDispatchDialog = (row) => {
+  // Map table row to order card structure if needed, or use row directly if fields match
+  // OrderDispatch uses: pickAddr, dropAddr, address, typeCode, daysLater, time
+  // OrderList row has: detail.pickAddr/dropAddr, type, serviceTime (as time), etc.
+  // We need to adapt the row to 'currentDispatchOrder' structure expected by dialog template
+
+  // Construct dispatch order object
+  let orderObj = {
+    id: row.id,
+    typeCode: row.type,
+    transportType: row.splitType || row.transportType, // Add transportType
+    time: row.serviceTime,
+    status: row.status,
+    daysLater: getDaysLater(row.serviceTime), // Need helper
+    address: '', // generic address for feeding/washing
+    pickAddr: '',
+    dropAddr: '',
+    riderId: (row.status === 'pending_accept' || row.status === 'serving') ? getRiderIdByName(row.fulfillerName) : null // Mock finding rider ID
+  }
+
+  if (row.type === 'transport') {
+    orderObj.pickAddr = row.detail.pickAddr
+    orderObj.dropAddr = row.detail.dropAddr
+  } else {
+    // Feeding/Washing address isn't in main row usually, let's assume city+district or from detail if we had it
+    // row.city + row.district is available.
+    // We will just use row.city + row.district for display in card for now, or add address to mock data details.
+    // Actually, OrderList mock has detailed addresses implicit in description?
+    // Let's use City + District + "详细地址" placeholder or row.detail.address if strictly needed.
+    // For visual, let's just use City+District.
+    orderObj.address = row.city + row.district
+  }
+
+  currentDispatchOrder.value = orderObj
+
+  // Set Current Rider Logic
+  if (orderObj.riderId) {
+    currentRider.value = ridersList.value.find(r => r.id === orderObj.riderId) || null
+  } else {
+    currentRider.value = null
+  }
+
+  dispatchDialogVisible.value = true
+  dispatchSearchQuery.value = ''
+  selectedRiderId.value = null
+  dispatchFee.value = 0
+}
+
+const handleDispatchSubmit = () => {
+  if (!selectedRiderId.value) {
+    ElMessage.warning('请选择履约者')
+    return
+  }
+  if (!dispatchFee.value) {
+    ElMessage.warning('请输入服务费用')
+    return
+  }
+  dispatchDialogVisible.value = false
+  ElMessage.success('派单成功')
+
+  // Update local list
+  if (currentDispatchOrder.value) {
+    const row = tableData.value.find(r => r.id === currentDispatchOrder.value.id)
+    if (row) {
+      row.status = 'pending_accept'
+      const rider = ridersList.value.find(r => r.id === selectedRiderId.value)
+      row.fulfillerName = rider ? rider.name : 'Unknown'
+      row.fulfillerFee = dispatchFee.value
+    }
+  }
+}
+
+const handleExportLogs = () => {
+  const logs = currentOrder.value.orderLogs || []
+  if (logs.length === 0) {
+    ElMessage.warning('暂无日志可导出')
+    return
+  }
+
+  let csvContent = "时间,类型,标题,内容\n"
+  logs.forEach(log => {
+    const time = log.time || ''
+    const type = log.type || ''
+    const title = (log.title || '').replace(/"/g, '""')
+    const content = (log.content || '').replace(/"/g, '""')
+    csvContent += `${time},${type},"${title}","${content}"\n`
+  })
+
+  const blob = new Blob(["\uFEFF" + csvContent], { type: 'text/csv;charset=utf-8;' })
+  const url = URL.createObjectURL(blob)
+  const link = document.createElement("a")
+  link.href = url
+  link.download = `OrderLogs_${currentOrder.value.orderNo}.csv`
+  link.click()
+  URL.revokeObjectURL(url)
+
+  ElMessage.success('导出成功')
+}
+
+// Helpers
+const getShortType = (code) => {
+  const map = { 'transport': '接送', 'feeding': '喂遛', 'washing': '洗护' }
+  return map[code] || '订单'
+}
+const getRiderStatusText = (status) => {
+  const map = { 'online': '接单中', 'busy': '接单中', 'offline': '休息中', 'disabled': '禁用' }
+  return map[status]
+}
+const getCategoryClass = (cat) => {
+  const map = { '接送': 'cat-transport', '喂遛': 'cat-feeding', '洗护': 'cat-washing' }
+  return map[cat] || ''
+}
+
+const getServiceTimeRange = (timeStr) => {
+  if (!timeStr) return '--'
+  try {
+    // Assume format YYYY-MM-DD HH:mm
+    if (timeStr.length < 16) return timeStr
+
+    let timePart = timeStr.substring(11, 16)
+    let [hh, mm] = timePart.split(':').map(Number)
+    let endH = hh + 2 // Assume 2 hours duration
+    if (endH >= 24) endH -= 24
+
+    let endHStr = endH.toString().padStart(2, '0')
+    return `${timeStr}-${endHStr}:${mm.toString().padStart(2, '0')}`
+  } catch (e) {
+    return timeStr
+  }
+}
+// Mock helper to get days later text
+const getDaysLater = (dateStr) => {
+  // Simple mock logic
+  if (dateStr.includes('02-07')) return '今天'
+  if (dateStr.includes('02-08')) return '明天'
+  return ''
+}
+// Mock helper to find rider ID by name (since table has name only)
+const getRiderIdByName = (name) => {
+  const rider = ridersList.value.find(r => r.name === name)
+  return rider ? rider.id : null
+}
+const filteredDispatchRiders = computed(() => {
+  let result = ridersList.value.filter(r => r.status === 'online' || r.status === 'busy')
+  if (dispatchSearchQuery.value) {
+    const q = dispatchSearchQuery.value.toLowerCase()
+    result = result.filter(r => r.name.includes(q) || r.phone.includes(q))
+  }
+  result.sort((a,b) => {
+    return a.lastServiceTime.localeCompare(b.lastServiceTime)
+  })
+  return result
+})
+
+
+const handleCancel = (row) => {
+  ElMessageBox.confirm('确认取消该订单吗?', '提示', { type: 'warning' })
+    .then(() => {
+      ElMessage.success('订单已取消')
+      handleSearch()
+    })
+}
+
+const careSummaryVisible = ref(false)
+const careSummaryOrder = ref(null)
+const careSummaryText = ref('')
+const isEditingSummary = ref(false)
+
+const openCareSummary = (row) => {
+  // Inject rich mock data for the detailed profile view
+  careSummaryOrder.value = {
+    ...row,
+    petAge: '3岁',
+    petGender: 'male',
+    petTags: ['易过敏', '胆小'],
+    petWeight: '30 kg',
+    petSize: '大型',
+    petPersonality: '活泼,超级粘人,喜欢玩球',
+    homeTime: '2023-01-01',
+    houseType: '电梯',
+    entryMethod: '密码开门',
+    entryDetail: '密码: 123456 (仅限服务期间使用)',
+    healthStatus: '健康',
+    aggression: '无',
+    vaccineImg: 'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg',
+    medicalHistory: '无',
+    allergy: '海鲜'
+  }
+
+  isEditingSummary.value = false
+  // Mock default summary if empty
+  if (!row.careSummary) {
+    careSummaryText.value = `1. 精神/身体状态:${row.petName}精神状态良好,愿意互动。
+2. 进食/饮水:食欲正常,饮水适当,已清洗碗具。
+3. 排泄情况:排便正常,颜色形状正常,已清理。
+4. 卫生情况:猫砂盆/地面已清理干净,无异味。
+5. 互动情况:陪玩了20分钟,${row.petName}很开心。
+6. 特殊情况/备注:无特殊异常。`
+  } else {
+    careSummaryText.value = row.careSummary
+  }
+  careSummaryVisible.value = true
+}
+
+const saveCareSummary = () => {
+  if(careSummaryOrder.value) {
+    careSummaryOrder.value.careSummary = careSummaryText.value
+    // Update time if not exists or update it on save? Usually maintain original submission time unless updated.
+    // Let's set a mock time if missing.
+    if (!careSummaryOrder.value.summaryTime) {
+      careSummaryOrder.value.summaryTime = '2024-02-04 17:00' // Mock
+    }
+    ElMessage.success('护理小结已保存')
+    isEditingSummary.value = false
+  }
+}
+
+// Reward / Punish Logic
+const rewardDialogVisible = ref(false)
+const remarkDialogVisible = ref(false)
+const currentOperateRow = ref(null)
+
+const rewardForm = reactive({
+  type: 'reward', // reward | punish
+  item: 'points', // points | amount
+  value: 10,
+  reason: ''
+})
+
+const remarkForm = ref('')
+
+const openRewardDialog = (row) => {
+  currentOperateRow.value = row
+  // Reset form
+  rewardForm.type = 'reward'
+  rewardForm.item = 'points'
+  rewardForm.value = 10
+  rewardForm.reason = ''
+  rewardDialogVisible.value = true
+}
+
+const handleRewardSubmit = () => {
+  if(!rewardForm.reason) {
+    ElMessage.warning('请输入奖惩原因')
+    return
+  }
+  ElMessage.success(`操作成功:${rewardForm.type === 'reward' ? '奖励' : '惩罚'}已执行`)
+  rewardDialogVisible.value = false
+}
+
+const openRemarkDialog = (row) => {
+  currentOperateRow.value = row
+  remarkForm.value = row.remark || ''
+  remarkDialogVisible.value = true
+}
+
+const handleRemarkSubmit = () => {
+  if(currentOperateRow.value) {
+    currentOperateRow.value.remark = remarkForm.value
+    ElMessage.success('备注已更新')
+    remarkDialogVisible.value = false
+  }
+}
+
+const handleCommand = (cmd, row) => {
+  if(cmd === 'reward') openRewardDialog(row)
+  if(cmd === 'remark') openRemarkDialog(row)
+  if(cmd === 'care_summary') openCareSummary(row)
+
+  if(cmd === 'complete') {
+    ElMessageBox.confirm('确认将该订单手动标记为完成吗?', '提示', { type: 'warning' })
+      .then(() => {
+        row.status = 'completed'
+        ElMessage.success('订单已标记完成')
+      })
+  }
+
+  if(cmd === 'delete') {
+    ElMessageBox.confirm('确认删除该订单吗?此操作不可恢复', '警告', { type: 'error' })
+      .then(() => {
+        tableData.value = tableData.value.filter(item => item.id !== row.id)
+        ElMessage.success('订单已删除')
+      })
+  }
+}
+
+</script>
+
+<style scoped>
+.page-container { padding: 20px; }
+.card-header { display: flex; justify-content: space-between; align-items: center; }
+.title { font-weight: bold; font-size: 18px; }
+.right-panel { display: flex; gap: 10px; align-items: center; }
+.search-input { width: 220px; }
+.status-tabs { margin-top: 10px; margin-bottom: -10px; }
+.pagination-container { display: flex; justify-content: flex-end; margin-top: 20px; }
+
+/* Table Content Styles */
+.service-type-cell { display: flex; flex-direction: row; gap: 4px; align-items: center; } /* Changed to row */
+.sub-tag { font-size: 11px; height: 20px; padding: 0 5px; }
+
+.pet-info { display: flex; align-items: center; gap: 10px; }
+.pet-info .el-avatar { background: #e0eaff; color: #409eff; font-weight: bold; flex-shrink: 0; }
+.pet-info .avatar-feeding { background: #fdf6ec; color: #e6a23c; }
+.pet-info .avatar-washing { background: #f0f9eb; color: #67c23a; }
+
+.pet-detail { display: flex; flex-direction: column; line-height: 1.4; }
+.pet-name { font-weight: bold; font-size: 14px; color: #303133; }
+.pet-breed { color: #909399; font-weight: normal; font-size: 12px; margin-left: 4px; }
+
+.merchant-info { display: flex; flex-direction: column; line-height: 1.4; }
+.sub-text { font-size: 12px; color: #999; }
+.text-gray { color: #ccc; font-style: italic; }
+.time-text { font-size: 13px; color: #606266; }
+
+.status-cell { display: flex; align-items: center; }
+.status-dot { width: 6px; height: 6px; border-radius: 50%; margin-right: 6px; background-color: #909399; }
+.status-dot.pending_dispatch { background-color: #f56c6c; box-shadow: 0 0 4px rgba(245, 108, 108, 0.4); }
+.status-dot.pending_accept { background-color: #e6a23c; }
+.status-dot.serving { background-color: #409eff; }
+.status-dot.pending_confirm { background-color: #bf24e8; }
+.status-dot.completed { background-color: #67c23a; }
+.status-dot.cancelled { background-color: #909399; }
+
+.fulfiller-info { display: flex; flex-direction: column; }
+.fulfiller-name { font-weight: 500; color: #333; }
+.fulfiller-fee { font-size: 12px; color: #e6a23c; }
+
+.op-cell { display: flex; align-items: center; gap: 8px; } /* Added flex for operation column */
+.el-dropdown-link { cursor: pointer; color: #409eff; font-size: 12px; display: flex; align-items: center; line-height: 1; height: 24px;} /* Ensure alignment */
+
+/* Detail Styles */
+.detail-content { padding: 0 10px; }
+.order-detail-drawer .el-drawer__body { padding: 0 !important; }
+.detail-container { height: 100%; display: flex; flex-direction: column; background: #f5f7fa; }
+
+.detail-header {
+  background: #fff; padding: 20px 24px; border-bottom: 1px solid #ebeef5;
+  display: flex; justify-content: space-between; align-items: center;
+}
+.left-head { display: flex; align-items: center; gap: 12px; }
+.order-no { font-size: 20px; font-weight: bold; color: #303133; }
+.type-tag { font-weight: normal; }
+.crt-time { font-size: 13px; color: #909399; }
+
+.detail-scroll-area { flex: 1; overflow-y: auto; padding: 20px 24px; }
+
+/* Progress */
+.progress-section {
+  background: #fff; padding: 30px 20px 20px; border-radius: 8px;
+  margin-bottom: 20px; box-shadow: 0 1px 4px rgba(0,0,0,0.05);
+}
+.custom-steps :deep(.el-step__title) { font-size: 13px; }
+
+/* Top Info Row */
+.top-info-row { display: flex; gap: 20px; margin-bottom: 20px; align-items: stretch; }
+.info-section { flex: 1; background: #fff; border-radius: 8px; padding: 15px; box-shadow: 0 1px 4px rgba(0,0,0,0.05); }
+
+.sec-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #f2f2f2; }
+.sec-header .label { font-weight: bold; font-size: 15px; color: #303133; border-left: 3px solid #409eff; padding-left: 8px; }
+
+/* Pet Section */
+.pet-basic-row { display: flex; gap: 15px; margin-bottom: 15px; align-items: center; }
+.pet-avatar-lg { border-radius: 8px; background: #ecf5ff; color: #409eff; font-size: 20px; font-weight: bold; }
+.pet-names { display: flex; flex-direction: column; gap: 6px; }
+.b-name { font-size: 18px; font-weight: bold; display: flex; align-items: center; gap: 6px; }
+.b-tags { display: flex; gap: 5px; }
+.pet-desc :deep(.el-descriptions__label) { width: 70px; }
+
+/* User Section */
+.u-row { display: flex; gap: 12px; align-items: center; margin-bottom: 12px; }
+.u-info .nm { font-weight: bold; font-size: 15px; color: #303133; display: flex; align-items: center; gap: 6px; }
+.vip-badge { background: linear-gradient(90deg, #f3d19e, #e6a23c); color: #fff; font-size: 10px; padding: 0 5px; border-radius: 10px; height: 16px; line-height: 16px; font-weight: bold; }
+.u-info .ph { font-size: 13px; color: #909399; margin-top: 2px; }
+.addr-box { background: #fdf6ec; padding: 8px 10px; border-radius: 4px; margin-bottom: 10px; }
+.addr-label { font-size: 12px; color: #e6a23c; margin-bottom: 2px; font-weight: bold; }
+.addr-txt { font-size: 13px; color: #606266; line-height: 1.4; }
+.stat-row { font-size: 12px; color: #909399; display: flex; gap: 10px; }
+
+/* Tabs */
+.detail-tabs { background: #fff; padding: 10px 20px; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.05); min-height: 400px; }
+.tab-pane-content { padding: 15px 0; }
+.order-desc :deep(.el-descriptions__label) { width: 90px; font-weight: bold; color: #606266; }
+
+/* Fulfiller Card inside Tab */
+.fulfiller-card { display: flex; align-items: center; gap: 20px; padding: 20px; background: #fff; border: 1px solid #ebeef5; border-radius: 8px; }
+.f-right { flex: 1; display: flex; flex-direction: column; gap: 8px; }
+.f-row1 { display: flex; align-items: center; gap: 10px; }
+.f-name { font-size: 18px; font-weight: bold; color: #303133; }
+.f-row2 { font-size: 13px; color: #606266; display: flex; gap: 10px; }
+.sep { color: #e4e7ed; }
+.f-row3 { display: flex; align-items: center; gap: 8px; }
+.score-text { font-weight: bold; color: #ff9900; }
+.empty-state { padding: 40px 0; text-align: center; }
+
+/* Service Block */
+.service-block { margin-bottom: 25px; }
+.block-title { font-weight: bold; font-size: 15px; margin-bottom: 15px; padding-left: 8px; border-left: 4px solid #409eff; }
+
+/* New Dispatch Styles */
+/* Dispatch Dialog Styles from OrderDispatch */
+.list-card {
+  background: #fff; border: 1px solid #ebeef5; border-radius: 8px;
+  padding: 12px; margin-bottom: 10px;
+  display: flex; align-items: stretch; gap: 12px; transition: all 0.2s;
+  cursor: pointer;
+}
+.list-card:hover { border-color: #c6e2ff; box-shadow: 0 2px 8px rgba(0,0,0,0.05); }
+
+.card-left { flex-shrink: 0; display: flex; align-items: center; }
+.order-card .type-tag {
+  width: 40px; height: 40px; border-radius: 8px; color: #fff;
+  display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: bold;
+}
+.type-tag.transport { background: #e6a23c; }
+.type-tag.feeding { background: #67c23a; }
+.type-tag.washing { background: #409eff; }
+
+.card-main { flex: 1; overflow: hidden; display: flex; flex-direction: column; justify-content: center; gap: 4px; }
+.row-addr {
+  font-size: 13px; color: #303133;
+  display: flex; align-items: center; gap: 4px;
+  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
+  line-height: 1.5;
+}
+.row-addr .tag {
+  font-size: 11px; color: #fff; padding: 1px 4px; border-radius: 4px; flex-shrink: 0; transform: scale(0.9);
+}
+.tag.pick { background: #409eff; }
+.tag.drop { background: #e6a23c; }
+.tag.home { background: #67c23a; }
+
+.row-time {
+  font-size: 12px; color: #909399; display: flex; align-items: center; gap: 4px;
+}
+.days-tag {
+  color: #f56c6c; background: #fef0f0; padding: 0 4px; border-radius: 4px; font-size: 11px; border: 1px solid #fde2e2;
+  transform: scale(0.95);
+}
+
+.dispatch-order-info {
+  background: #f5f7fa; padding: 10px; border-radius: 4px; margin-bottom: 20px; border:1px solid #e4e7ed;
+  display: block;
+}
+.dispatch-rider-select .select-header {
+  display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;
+}
+.dispatch-rider-select .tit { font-weight: bold; font-size: 14px; }
+
+.rider-grid {
+  display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; padding-right: 10px;
+}
+.rider-card.select-card {
+  cursor: pointer; border: 1px solid #dcdfe6; position: relative; transition: all 0.2s;
+  margin-bottom: 0;
+}
+.rider-card.select-card:hover { border-color: #409eff; }
+.rider-card.select-card.active {
+  border-color: #409eff; background-color: #ecf5ff;
+}
+.selected-mark {
+  position: absolute; top: 0; right: 0;
+  background: #409eff; color: #fff; border-bottom-left-radius: 6px;
+  width: 20px; height: 20px; display: flex; align-items: center; justify-content: center;
+  font-size: 12px;
+}
+
+.rider-card .card-left .dot {
+  position: absolute; bottom: 0; right: 0; width: 10px; height: 10px; border-radius: 50%; border: 2px solid #fff;
+}
+.dot.online { background: #67c23a; }
+.dot.busy { background: #409eff; }
+.dot.offline { background: #909399; }
+
+.r-name { font-weight: bold; font-size: 14px; color: #303133; margin-right: 8px; }
+.r-phone { font-size: 12px; color: #909399; }
+.status-badge { font-size: 11px; padding: 2px 6px; border-radius: 4px; display: inline-block; font-weight: bold; }
+.status-badge.online { background: #f0f9eb; color: #67c23a; }
+.status-badge.busy { background: #ecf5ff; color: #409eff; }
+.status-badge.offline { background: #f4f4f5; color: #909399; }
+
+.cat-tag {
+  background: #f4f4f5; color: #909399; font-size: 10px;
+  padding: 1px 4px; border-radius: 2px;
+  margin-right: 4px;
+}
+.cat-tag.cat-transport { background: #e6f7ff; color: #1890ff; border: 1px solid #91d5ff; }
+.cat-tag.cat-feeding { background: #f6ffed; color: #52c41a; border: 1px solid #b7eb8f; }
+.cat-tag.cat-washing { background: #fff0f6; color: #eb2f96; border: 1px solid #ffadd2; }
+
+.last-time { font-size: 11px; color: #999; }
+.empty-text { text-align: center; color: #909399; padding: 20px; width: 100%; grid-column: span 2; }
+.dispatch-footer {
+  margin-top: 20px; padding-top: 20px; border-top: 1px solid #ebeef5;
+  display: flex; justify-content: space-between; align-items: center;
+}
+.dispatch-footer .fee-input { display: flex; align-items: center; gap: 8px; font-size: 14px; }
+
+
+/* Enhanced Care Summary Styles */
+.care-summary-drawer :deep(.el-drawer__header) { margin-bottom: 0; padding: 20px 24px; border-bottom: 1px solid #f0f0f0; }
+.care-summary-drawer :deep(.el-drawer__body) { padding: 0; overflow-y: auto; background: #fff; }
+
+.care-summary-container { padding: 32px 40px; }
+
+/* 1. Header */
+.summary-header { display: flex; gap: 24px; align-items: flex-start; margin-bottom: 32px; padding-bottom: 24px; border-bottom: 1px dashed #e4e7ed; }
+.avatar-wrapper { border: 4px solid #f2f6fc; border-radius: 50%; }
+.pet-summary-info { flex: 1; display:flex; flex-direction:column; gap:12px; padding-top: 4px; }
+
+.summary-name-row { display: flex; align-items: center; gap: 16px; margin-bottom: 4px; }
+.summary-name-row .name { font-size: 24px; font-weight: 800; color: #303133; letter-spacing: 0.5px; }
+.tags-group { display: flex; gap: 8px; align-items: center; }
+
+.summary-sub-row { display: flex; align-items: center; background: #f9fafe; padding: 10px 16px; border-radius: 8px; align-self: flex-start; }
+.info-item { display: flex; flex-direction: column; gap: 2px; }
+.info-item .lbl { font-size: 11px; color: #909399; text-transform: uppercase; }
+.info-item .val { font-size: 14px; font-weight: bold; color: #606266; }
+.divider-v { width: 1px; height: 24px; background: #ebeef5; margin: 0 16px; }
+
+/* 2. Sections */
+.summary-section { margin-bottom: 40px; }
+.sec-title {
+  font-size: 16px; font-weight: 700; color: #303133; margin-bottom: 16px;
+  display:flex; align-items:center; gap:8px;
+  justify-content: space-between;
+}
+.sec-title .left { display: flex; align-items: center; gap: 8px; }
+.icon-box {
+  width: 28px; height: 28px; background: #ecf5ff; color: #409eff; border-radius: 6px;
+  display: flex; align-items: center; justify-content: center; font-size: 16px;
+}
+.icon-box.text-blue { background: #ecf5ff; color: #409eff; }
+.icon-box.text-orange { background: #fdf6ec; color: #e6a23c; }
+
+/* 3. Descriptions */
+.spacious-desc :deep(.el-descriptions__cell) { padding: 12px 16px!important; }
+.spacious-desc :deep(.el-descriptions__label) { width: 100px; color: #606266; font-weight: 500; background: #fafafa; }
+.flex-align { display: flex; align-items: center; }
+
+/* 4. Log Area */
+.main-log { background: #fff; }
+.log-content-box {
+  background: #fff;
+  border: 1px solid #ebeef5; border-radius: 8px;
+  padding: 24px;
+  box-shadow: 0 2px 12px rgba(0,0,0,0.02);
+  position: relative;
+}
+.log-content-box::before {
+  content: ''; position: absolute; top: 0; left: 0; width: 4px; height: 100%; background: #e6a23c; border-top-left-radius: 8px; border-bottom-left-radius: 8px;
+}
+.log-text {
+  white-space: pre-wrap; font-family: 'Inter', system-ui, sans-serif; margin: 0; line-height: 1.8; font-size: 15px; color: #303133; text-align: justify;
+}
+.edit-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 12px; }
+
+/* 5. Footer */
+.summary-footer {
+  margin-top: 60px; padding-top: 24px; border-top: 1px solid #ebeef5;
+  display: flex; justify-content: space-between; align-items: center;
+}
+.footer-info { display: flex; gap: 32px; }
+.f-row { display: flex; flex-direction: column; gap: 4px; }
+.f-row .lbl { font-size: 12px; color: #909399; }
+.f-row .val { font-size: 15px; font-weight: 600; color: #303133; }
+.f-row .val.user-active { color: #409eff; }
+
+
+/* Progress Card Styles */
+.progress-card {
+  background: #f8fcfb; border-radius: 8px; padding: 12px; border: 1px solid #ebeef5;
+}
+.p-title { margin: 0 0 8px; font-size: 15px; font-weight: bold; color: #303133; }
+.p-desc { margin: 0 0 12px; color: #606266; font-size: 13px; line-height: 1.5; }
+.p-media { display: flex; gap: 8px; flex-wrap: wrap; }
+.media-item { display: inline-block; }
+.p-img { width: 80px; height: 80px; border-radius: 4px; border: 1px solid #e4e7ed; cursor: pointer; }
+
+/* Route Graph */
+.route-graph { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; background: #f9f9f9; padding: 20px; border-radius: 6px; }
+.route-node { display: flex; gap: 10px; align-items: flex-start; background: #fff; padding: 12px; border-radius: 6px; box-shadow: 0 2px 6px rgba(0,0,0,0.04); flex: 1; min-width: 200px; }
+.node-icon { width: 30px; height: 30px; border-radius: 50%; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; flex-shrink: 0; }
+.node-icon.pick { background: #409eff; }
+.node-icon.drop { background: #67c23a; }
+
+/* New Transport Split Styles */
+.transport-split-block { margin-top: 20px; }
+.transport-grid { display: flex; gap: 20px; }
+.transport-card {
+  flex: 1; border: 1px solid #ebeef5; border-radius: 6px; overflow: hidden;
+  background: #fff; box-shadow: 0 2px 8px rgba(0,0,0,0.02);
+}
+.transport-card .t-header {
+  background: #f5f7fa; padding: 10px 15px; border-bottom: 1px solid #ebeef5;
+  display: flex; justify-content: space-between; align-items: center;
+}
+.transport-card .t-header .time { font-size: 13px; font-weight: bold; color: #f56c6c; }
+.transport-card .t-body { padding: 15px; display: flex; flex-direction: column; gap: 10px; }
+.transport-card .row { display: flex; align-items: flex-start; gap: 8px; font-size: 14px; color: #303133; line-height: 1.4; }
+.transport-card .row.sub { color: #909399; font-size: 13px; margin-top: 4px; }
+.transport-card .row .el-icon { margin-top: 3px; }
+.node-icon.shop { background: #e6a23c; font-size: 16px; }
+.node-content { display: flex; flex-direction: column; line-height: 1.4; flex: 1; }
+.addr-t { font-weight: bold; font-size: 14px; color: #303133; margin-bottom: 4px; }
+.contact-t { font-size: 12px; color: #909399; }
+.time-t { font-size: 12px; color: #f56c6c; margin-top: 4px; font-weight: 500; }
+.route-arrow-lg { color: #c0c4cc; font-size: 20px; }
+
+/* Logs */
+.log-card { background: #f4f4f5; padding: 10px 15px; border-radius: 4px; position: relative; top: -5px; width: 100%; }
+.l-tit { font-weight: bold; font-size: 14px; margin-bottom: 4px; color: #303133; }
+.l-txt { font-size: 13px; color: #606266; line-height: 1.5; }
+</style>

+ 1148 - 0
src/views/order/purchase/index.vue

@@ -0,0 +1,1148 @@
+<template>
+  <div class="page-container">
+    <div class="create-layout">
+
+      <!-- 左侧:下单填写区 -->
+      <div class="form-container">
+        <!-- 1. 服务类型选择 -->
+        <div class="type-selection">
+          <div
+            v-for="item in serviceList"
+            :key="item.type"
+            class="type-card"
+            :class="[item.type, { active: form.type === item.type }]"
+            @click="handleTypeChange(item.type)"
+          >
+            <div class="icon-box"><el-icon><component :is="item.icon" /></el-icon></div>
+            <div class="text">
+              <div class="type-name">{{ item.name }}</div>
+              <div class="type-desc">{{ item.desc }}</div>
+            </div>
+          </div>
+        </div>
+
+        <!-- 2. 基础信息:门店与宠主 -->
+        <el-card shadow="never" class="section-card">
+          <template #header>
+            <div class="card-title">
+              <span class="step-num">02</span> 基础信息
+            </div>
+          </template>
+          <div class="card-body">
+            <el-form label-position="top" class="base-form">
+              <el-row :gutter="20">
+                <el-col :span="12">
+                  <el-form-item>
+                    <template #label>
+                      <div style="display:flex; align-items:center; height: 24px;">
+                        <span>服务门店 (平台代下单)</span>
+                      </div>
+                    </template>
+                    <el-select v-model="form.merchantId" placeholder="请选择商户门店" size="large" style="width: 100%" filterable>
+                      <el-option v-for="m in merchants" :key="m.id" :label="m.name" :value="m.id" />
+                    </el-select>
+                  </el-form-item>
+                </el-col>
+                <el-col :span="12">
+                  <el-form-item>
+                    <template #label>
+                      <div style="display:flex; justify-content:space-between; align-items:center; width:100%; height: 24px;">
+                        <span>宠主用户</span>
+                        <el-button type="primary" plain size="small" @click="openAddUser" icon="Plus" style="margin-left: 15px;">添加用户</el-button>
+                      </div>
+                    </template>
+                    <el-select
+                      v-model="form.userId"
+                      placeholder="搜索手机号/姓名"
+                      size="large"
+                      style="width: 100%"
+                      filterable
+                      remote
+                      :remote-method="searchUser"
+                      :loading="userLoading"
+                      @change="handleUserChange"
+                    >
+                      <el-option v-for="u in userOptions" :key="u.id" :label="u.name + ' - ' + u.phone" :value="u.id" />
+                    </el-select>
+                  </el-form-item>
+                </el-col>
+              </el-row>
+
+              <el-form-item label="选择宠物" v-if="form.userId">
+                <div class="pet-select-row">
+                  <div
+                    v-for="p in currentPets"
+                    :key="p.id"
+                    class="pet-card"
+                    :class="{ active: form.petId === p.id }"
+                    @click="form.petId = p.id"
+                  >
+                    <el-avatar :size="48" :src="p.avatar" shape="square" style="border-radius: 6px;">{{ p.name.charAt(0) }}</el-avatar>
+                    <div class="pet-info">
+                      <div class="name">{{ p.name }}</div>
+                      <div class="sub">{{ p.breed }}</div>
+                    </div>
+                    <div class="check-mark" v-if="form.petId === p.id"><el-icon><Check /></el-icon></div>
+                  </div>
+
+                  <!-- Add Button Card (Last Item in Grid) -->
+                  <div class="pet-card add-card" @click="openAddPet">
+                    <el-icon :size="24"><Plus /></el-icon>
+                    <span style="font-size: 15px; font-weight: bold;">新增宠物</span>
+                  </div>
+                </div>
+              </el-form-item>
+            </el-form>
+          </div>
+        </el-card>
+
+        <!-- 3. 业务详情表单 -->
+        <el-card shadow="never" class="section-card form-card" v-if="form.type">
+          <template #header>
+            <div class="card-title">
+              <span class="step-num">03</span>
+              {{ getStepTitle(form.type) }}
+            </div>
+          </template>
+
+          <div class="card-body">
+            <!-- 服务套餐信息 -->
+            <el-form-item label="团购套餐">
+              <el-input v-model="form.groupBuyPackage" placeholder="请输入团购套餐名称 (选填)" clearable />
+            </el-form-item>
+
+            <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>
+
+            <!-- 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>
+
+            <!-- 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>
+
+          </div>
+        </el-card>
+      </div>
+
+      <!-- 右侧:收银台概览 -->
+      <div class="summary-sidebar">
+        <div class="summary-panel">
+          <div class="summary-header">订单概览</div>
+
+          <div class="summary-content">
+            <div class="row" v-if="selectedMerchantName">
+              <span class="label">服务门店</span>
+              <span class="value">{{ selectedMerchantName }}</span>
+            </div>
+            <div class="row" v-if="selectedUserName">
+              <span class="label">客户</span>
+              <span class="value">{{ selectedUserName }}</span>
+            </div>
+            <div class="row" v-if="selectedPetName">
+              <span class="label">服务对象</span>
+              <span class="value action-text">{{ selectedPetName }} ({{ selectedPetBreed }})</span>
+            </div>
+            <div class="divider"></div>
+
+            <div class="service-preview" v-if="form.type">
+              <div class="preview-title">{{ getTypeName(form.type) }}</div>
+
+              <!-- 套餐显示 -->
+              <div class="preview-detail" v-if="selectedPkgName">
+                <div style="font-weight:bold; color:#409eff">{{ selectedPkgName }}</div>
+              </div>
+              <div class="preview-detail" v-else>
+                <div style="color:#e6a23c">非服务套餐 (单次)</div>
+              </div>
+
+              <!-- 接送预览 -->
+              <div v-if="form.type === 'transport'" class="preview-detail">
+                <div>{{ form.transport.subType === 'round' ? '往返接送' : (form.transport.subType === 'pick' ? '单程接' : '单程送') }}</div>
+                <div class="minor">接: {{ form.transport.pickTime ? formatTime(form.transport.pickTime) : '未选时间' }}</div>
+                <div class="minor" v-if="form.transport.subType !== 'pick'">送: {{ form.transport.dropTime ? formatTime(form.transport.dropTime) : '未选' }}</div>
+              </div>
+            </div>
+
+          </div>
+
+          <div class="summary-footer">
+            <el-button type="primary" size="large" class="submit-btn" :disabled="!canSubmit" @click="handleSubmit">
+              立即下单
+            </el-button>
+          </div>
+        </div>
+      </div>
+
+    </div>
+
+    <!-- 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>
+
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed, onMounted, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+
+// --- Mock Data ---
+const merchants = ref([
+  { id: 1, name: '萌它宠物三里屯店' },
+  { id: 2, name: '宠爱国际动物医院' }
+])
+const userOptions = ref([
+  { id: 101, name: '张三', phone: '13812345678' },
+  { id: 102, name: '李四', phone: '13987654321' }
+])
+const mockPets = {
+  101: [
+    { id: 1, name: '旺财', breed: '金毛', avatar: '', region: ['北京市', '市辖区', '朝阳区'], address: '三里屯SOHO A座 1001' },
+    { id: 2, name: '咪咪', breed: '布偶', avatar: '', region: ['北京市', '市辖区', '朝阳区'], address: '三里屯SOHO A座 1001' }
+  ],
+  102: [
+    { id: 3, name: '奥利奥', breed: '边牧', avatar: '', region: ['上海市', '市辖区', '浦东新区'], address: '陆家嘴一号院 5-502' }
+  ]
+}
+
+const serviceList = [
+  { type: 'transport', name: '宠物接送', icon: 'Van', desc: '专车接送 · 全程监护', basePrice: 35 },
+  { type: 'feeding', name: '上门喂遛', icon: 'Food', desc: '喂食添水 · 陪玩遛狗', basePrice: 68 },
+  { type: 'washing', name: '上门洗护', icon: 'Soap', desc: '专业设备 · 深度清洁', basePrice: 88 }
+]
+
+const allPackages = [
+  { id: 10, type: 'transport', name: '包月接送套餐', price: 0 },
+  { id: 11, type: 'feeding', name: '基础喂猫套餐', price: 0 },
+  { id: 12, type: 'feeding', name: '深度陪玩套餐', price: 0 },
+  { id: 13, type: 'washing', name: '精致洗护+美容', price: 0 },
+  { id: 14, type: 'washing', name: '除菌药浴套餐', price: 0 },
+]
+
+// --- State ---
+const userLoading = ref(false)
+const currentPets = ref([])
+
+const form = reactive({
+  merchantId: '',
+  userId: '',
+  petId: '',
+  type: 'transport',
+  groupBuyPackage: '',
+
+  // Sub Forms Data
+  transport: {
+    pkgId: '',
+    price: 0,
+    pickPrice: 35,
+    dropPrice: 35,
+    subType: 'round',
+    pickRegion: [], pickDetail: '', pickContact: '', pickPhone: '', pickTime: '',
+    dropRegion: [], dropDetail: '', dropContact: '', dropPhone: '', dropTime: ''
+  },
+  feeding: {
+    pkgId: '', price: 68,
+    appointments: [{ startTime: '', endTime: '' }],
+    region: [], addressDetail: '',
+    count: 1, dates: [], area: '', itemLoc: '', cleanLoc: '', foodAmount: '', other: ''
+  },
+  washing: {
+    pkgId: '', price: 88,
+    appointments: [{ startTime: '', endTime: '' }],
+    region: [], addressDetail: '',
+    time: '', petStatus: '', cleanLoc: '', toolLoc: '', other: ''
+  }
+})
+
+// Address Autofill Watcher
+watch(() => form.petId, (newId) => {
+  if (!newId) return
+  const pet = currentPets.value.find(p => p.id === newId)
+  if (!pet) return
+
+  const user = userOptions.value.find(u => u.id === form.userId)
+
+  // Fill Transport
+  form.transport.pickRegion = pet.region || []
+  form.transport.pickDetail = pet.address || ''
+  form.transport.pickContact = user?.name || ''
+  form.transport.pickPhone = user?.phone || ''
+
+  form.transport.dropRegion = pet.region || []
+  form.transport.dropDetail = pet.address || ''
+  form.transport.dropContact = user?.name || ''
+  form.transport.dropPhone = user?.phone || ''
+
+  // Fill Feeding
+  form.feeding.region = pet.region || []
+  form.feeding.addressDetail = pet.address || ''
+
+  // Fill Washing
+  form.washing.region = pet.region || []
+  form.washing.addressDetail = pet.address || ''
+})
+
+// Current Active Data Helper
+const activeData = computed(() => {
+  return form[form.type]
+})
+
+// --- Logic ---
+
+const handleTypeChange = (type) => {
+  form.type = type
+  calcPrice(type)
+}
+
+const currentPackages = computed(() => {
+  return allPackages.filter(p => p.type === form.type)
+})
+
+const handlePkgSelect = (id) => {
+  activeData.value.pkgId = id
+  // Price calculation should remain same (base price), just payable changes
+  calcPrice(form.type)
+}
+
+const calcPrice = (type) => {
+  const data = form[type]
+  const base = serviceList.find(s => s.type === type)?.basePrice || 0
+
+  // Always use Base Logic for "Order Value", regardless of package
+  if (type === 'transport') {
+    if(data.subType === 'round') {
+      data.pickPrice = base
+      data.dropPrice = base
+    } else if (data.subType === 'pick') {
+      data.pickPrice = base
+      data.dropPrice = 0
+    } else if (data.subType === 'drop') {
+      data.pickPrice = 0
+      data.dropPrice = base
+    }
+  } else if (type === 'feeding') {
+    data.price = base * data.count
+  } else if (type === 'washing') {
+    data.price = base
+  }
+}
+
+// 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 pcaOptions = [
+  {
+    value: '北京市', label: '北京市',
+    children: [
+      { value: '市辖区', label: '市辖区', children: [ { value: '朝阳区', label: '朝阳区' }, { value: '海淀区', label: '海淀区' } ] }
+    ]
+  },
+  {
+    value: '上海市', label: '上海市',
+    children: [
+      { value: '市辖区', label: '市辖区', children: [ { value: '浦东新区', label: '浦东新区' }, { value: '徐汇区', label: '徐汇区' } ] }
+    ]
+  }
+]
+
+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
+  }
+  if(!currentPets.value) currentPets.value = []
+  currentPets.value.push(newPet)
+  form.petId = newPet.id
+  petDialogVisible.value = false
+  ElMessage.success('宠物添加成功')
+}
+
+
+// --- Computed Helpers ---
+const selectedMerchantName = computed(() => merchants.value.find(m => m.id === form.merchantId)?.name)
+const selectedUserName = computed(() => userOptions.value.find(u => u.id === form.userId)?.name)
+const selectedPet = computed(() => currentPets.value.find(p => p.id === form.petId))
+const selectedPetName = computed(() => selectedPet.value?.name)
+const selectedPetBreed = computed(() => selectedPet.value?.breed)
+
+const selectedPkgName = computed(() => {
+  const pkgId = activeData.value.pkgId
+  return allPackages.find(p => p.id === pkgId)?.name || ''
+})
+
+
+
+const canSubmit = computed(() => {
+  if(!form.merchantId || !form.userId || !form.petId) return false
+  return true
+})
+
+// --- Methods ---
+const searchUser = (query) => { /* Mock */ }
+const handleUserChange = (val) => {
+  currentPets.value = mockPets[val] || []
+  form.petId = ''
+}
+const getStepTitle = (type) => {
+  const map = { transport: '填写接送路线与时间', feeding: '选择套餐与服务的细则', washing: '选择套餐与服务的细则' }
+  return map[type]
+}
+const getTypeName = (type) => {
+  const map = { transport: '宠物接送', feeding: '上门喂遛', washing: '上门洗护' }
+  return map[type]
+}
+const formatTime = (time) => {
+  if(!time) return ''
+  const d = new Date(time)
+  return `${d.getMonth()+1}-${d.getDate()} ${d.getHours()}:${d.getMinutes() < 10 ? '0'+d.getMinutes() : d.getMinutes()}`
+}
+
+const handleSubmit = () => {
+  ElMessage.success('下单成功!订单号:ORD20248888')
+}
+
+// Initialize
+onMounted(() => {
+  calcPrice('transport')
+})
+</script>
+
+<style scoped>
+.page-container { padding: 20px; background-color: #f0f2f5; min-height: 100vh; }
+.create-layout { display: flex; gap: 20px; align-items: flex-start; max-width: 1400px; margin: 0 auto; }
+
+/* Left Content */
+.form-container { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 20px; }
+
+.section-card { border-radius: 8px; border: none; }
+.card-title { font-size: 16px; font-weight: bold; color: #303133; display: flex; align-items: center; gap: 10px; }
+.step-num {
+  background: #e6f7ff; color: #1890ff; width: 28px; height: 28px; border-radius: 50%;
+  text-align: center; line-height: 28px; font-family: Impact, sans-serif;
+}
+.base-form .el-form-item { margin-bottom: 18px; }
+
+/* Pet Selection */
+/* Pet Selection */
+.pet-select-row {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+  gap: 15px;
+  width: 100%;
+}
+.pet-card {
+  border: 1px solid #8D9095;
+  border-radius: 8px;
+  padding: 12px 15px;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  position: relative;
+  transition: all 0.2s ease-in-out;
+  background: #fff;
+  min-height: 70px;
+}
+.pet-card:hover {
+  border-color: #303133;
+  transform: translateY(-2px);
+  box-shadow: 0 4px 12px rgba(0,0,0,0.08);
+}
+.pet-card.active {
+  border-color: #409eff;
+  background-color: #fff;
+  box-shadow: 0 0 0 1px #409eff inset;
+}
+.check-mark {
+  position: absolute;
+  right: 0;
+  top: 0;
+  background: #409eff;
+  color: white;
+  width: 28px;
+  height: 18px;
+  border-radius: 0 8px 0 12px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 12px;
+}
+.pet-info .name { font-weight: bold; font-size: 15px; color: #303133; margin-bottom: 2px; }
+.pet-info .sub { font-size: 12px; color: #606266; line-height: 1.2; }
+
+.pet-card.add-card {
+  border: 1px solid #8D9095;
+  justify-content: center;
+  align-items: center;
+  color: #303133;
+  flex-direction: row;
+  gap: 8px;
+  background: #fff;
+  box-shadow: none;
+  height: auto;
+  min-height: 70px;
+}
+.pet-card.add-card:hover {
+  border-color: #303133;
+  color: #303133;
+  background: #f9f9f9;
+  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 {
+  background: white; border-radius: 8px; padding: 20px; cursor: pointer; position: relative;
+  display: flex; align-items: center; gap: 15px; transition: all 0.2s;
+  border: 2px solid transparent; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+}
+.type-card:hover { transform: translateY(-2px); box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.1); }
+.type-card.active { border-color: #409eff; background-color: #f0f9ff; }
+.type-card .icon-box {
+  width: 48px; height: 48px; border-radius: 12px; background: #f2f3f5;
+  display: flex; align-items: center; justify-content: center; font-size: 24px; color: #606266;
+}
+.type-card.active .icon-box { background: #409eff; color: white; }
+/* Colors */
+.type-card.transport.active .icon-box { background: #409eff; }
+.type-card.transport.active { border-color: #409eff; background-color: #f0f9ff; }
+.type-card.feeding.active .icon-box { background: #e6a23c; }
+.type-card.feeding.active { border-color: #e6a23c; background-color: #fdf6ec; }
+.type-card.washing.active .icon-box { background: #67c23a; }
+.type-card.washing.active { border-color: #67c23a; background-color: #f0f9eb; }
+
+.type-name { font-weight: bold; font-size: 16px; color: #303133; margin-bottom: 4px; }
+.type-desc { font-size: 12px; color: #909399; margin-bottom: 4px; }
+.type-price { font-size: 14px; color: #f56c6c; font-weight: bold; }
+
+/* Package Selection Grid */
+.form-section-title { font-weight: bold; margin-bottom: 12px; font-size: 14px; }
+.package-selection-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 20px; }
+.pkg-select-card {
+  border: 1px solid #dcdfe6; border-radius: 8px; padding: 10px 15px; cursor: pointer; position: relative;
+  background: #fff; transition: all 0.2s; min-height: 56px; display: flex; flex-direction: column; justify-content: center;
+}
+.pkg-select-card:hover { border-color: #409eff; }
+.pkg-select-card.active { border-color: #409eff; background-color: #ecf5ff; }
+.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; }
+.summary-panel { background: white; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.05); position: sticky; top: 20px; }
+.summary-header { background: #304156; color: white; padding: 15px 20px; font-weight: bold; font-size: 16px; border-radius: 8px 8px 0 0; }
+.summary-content { padding: 20px; }
+.row { display: flex; justify-content: space-between; margin-bottom: 12px; font-size: 14px; }
+.row .label { color: #909399; }
+.row .value { color: #303133; font-weight: 500; }
+.preview-title { font-weight: bold; margin-bottom: 8px; color: #333; }
+.preview-detail { background: #f8f9fa; padding: 10px; border-radius: 4px; font-size: 13px; margin-bottom: 8px; }
+.preview-detail .minor { color: #999; font-size: 12px; margin-top: 2px; }
+.placeholder { color: #C0C4CC; text-align: center; padding: 20px 0; font-size: 13px; font-style: italic; }
+
+
+.summary-footer { background: #f9f9fc; padding: 15px 20px; border-top: 1px solid #ebeef5; text-align: center; border-radius: 0 0 8px 8px; }
+.submit-btn { width: 100%; font-weight: bold; border-radius: 22px; }
+</style>

+ 42 - 28
src/views/system/account/index.vue

@@ -17,7 +17,7 @@
       <el-table :data="tableData" style="width: 100%" v-loading="loading" :header-cell-style="{ color: '#909399', fontWeight: 'normal', backgroundColor: '#fff', borderBottom: '1px solid #ebeef5' }">
       <el-table :data="tableData" style="width: 100%" v-loading="loading" :header-cell-style="{ color: '#909399', fontWeight: 'normal', backgroundColor: '#fff', borderBottom: '1px solid #ebeef5' }">
         <el-table-column prop="avatar" label="头像" width="80" align="center">
         <el-table-column prop="avatar" label="头像" width="80" align="center">
           <template #default="scope">
           <template #default="scope">
-            <el-avatar :size="40" :src="scope.row.avatar || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'" />
+            <el-avatar :size="40" :src="scope.row.avatarUrl || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'" />
           </template>
           </template>
         </el-table-column>
         </el-table-column>
         <el-table-column prop="userName" label="用户名" width="120" />
         <el-table-column prop="userName" label="用户名" width="120" />
@@ -81,8 +81,14 @@
       <el-form ref="userFormRef" :model="form" :rules="rules" label-width="90px">
       <el-form ref="userFormRef" :model="form" :rules="rules" label-width="90px">
         <el-row>
         <el-row>
           <el-col :span="24">
           <el-col :span="24">
+            <div style="display: flex; justify-content: center; align-items: center; margin-bottom: 20px;">
+              <image-upload v-model="form.avatar" :limit="1" :is-show-tip="false" class="circular-upload" />
+              <span style="color: #999; font-size: 13px; margin-left: 10px;">点击上传头像</span>
+            </div>
+          </el-col>
+          <el-col :span="24" v-if="!isEdit">
             <el-form-item label="用户名" prop="userName">
             <el-form-item label="用户名" prop="userName">
-              <el-input v-model="form.userName" placeholder="用于登录" :disabled="isEdit" />
+              <el-input v-model="form.userName" placeholder="用于登录" />
             </el-form-item>
             </el-form-item>
           </el-col>
           </el-col>
           <el-col :span="24">
           <el-col :span="24">
@@ -100,7 +106,7 @@
               <el-input v-model="form.password" type="password" show-password placeholder="密码" />
               <el-input v-model="form.password" type="password" show-password placeholder="密码" />
             </el-form-item>
             </el-form-item>
           </el-col>
           </el-col>
-          <el-col :span="24" v-if="form.userId == null || form.userId != useUserStore().userId">
+          <el-col :span="24">
             <el-form-item label="分配角色" prop="roleIds">
             <el-form-item label="分配角色" prop="roleIds">
               <el-select v-model="form.roleIds" multiple placeholder="请选择角色" style="width: 100%">
               <el-select v-model="form.roleIds" multiple placeholder="请选择角色" style="width: 100%">
                 <el-option
                 <el-option
@@ -112,22 +118,9 @@
               </el-select>
               </el-select>
             </el-form-item>
             </el-form-item>
           </el-col>
           </el-col>
-          <el-col :span="24" v-if="form.userId == null || form.userId != useUserStore().userId">
+          <el-col :span="24">
             <el-form-item label="管理门店">
             <el-form-item label="管理门店">
-              <el-select
-                v-model="form.storeIds"
-                multiple
-                collapse-tags
-                placeholder="请选择管理门店"
-                style="width: 100%"
-              >
-                <el-option
-                  v-for="store in storeOptions"
-                  :key="store.id"
-                  :label="store.name"
-                  :value="store.id"
-                />
-              </el-select>
+              <permi-select v-model="form.storeIds" :options="storeOptions" />
             </el-form-item>
             </el-form-item>
           </el-col>
           </el-col>
           <el-col :span="24">
           <el-col :span="24">
@@ -151,7 +144,7 @@
 </template>
 </template>
 
 
 <script setup lang="ts" name="Account">
 <script setup lang="ts" name="Account">
-import { ref, reactive, onMounted, getCurrentInstance, ComponentInternalInstance } from 'vue';
+import { ref, reactive, computed, onMounted, nextTick, getCurrentInstance, ComponentInternalInstance } from 'vue';
 import { ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus';
 import { ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus';
 import userApi from '@/api/system/user';
 import userApi from '@/api/system/user';
 import { listOnUser } from '@/api/system/store';
 import { listOnUser } from '@/api/system/store';
@@ -185,7 +178,8 @@ const form = ref<any>({
   roleIds: [],
   roleIds: [],
   storeIds: [],
   storeIds: [],
   postIds: [],
   postIds: [],
-  status: '0'
+  status: '0',
+  avatar: ''
 });
 });
 
 
 const rules = reactive<FormRules>({
 const rules = reactive<FormRules>({
@@ -247,7 +241,8 @@ const resetForm = () => {
     roleIds: [],
     roleIds: [],
     storeIds: [],
     storeIds: [],
     postIds: [],
     postIds: [],
-    status: '0'
+    status: '0',
+    avatar: ''
   };
   };
   userFormRef.value?.resetFields();
   userFormRef.value?.resetFields();
   userFormRef.value?.clearValidate();
   userFormRef.value?.clearValidate();
@@ -270,6 +265,16 @@ const handleEdit = async (row: any) => {
   form.value.roleIds = data.roleIds || [];
   form.value.roleIds = data.roleIds || [];
   form.value.storeIds = data.storeIds || (data.user && data.user.storeIds) || [];
   form.value.storeIds = data.storeIds || (data.user && data.user.storeIds) || [];
   form.value.password = '';
   form.value.password = '';
+  
+  // 处理头像回显
+  if (data.user.avatar && data.user.avatarUrl) {
+    form.value.avatar = [{ ossId: data.user.avatar, url: data.user.avatarUrl }];
+  } else if (data.user.avatar) {
+    form.value.avatar = data.user.avatar;
+  } else {
+    form.value.avatar = '';
+  }
+  
   dialogVisible.value = true;
   dialogVisible.value = true;
 };
 };
 
 
@@ -306,14 +311,14 @@ const saveAccount = () => {
     if (valid) {
     if (valid) {
       buttonLoading.value = true;
       buttonLoading.value = true;
       try {
       try {
-        if (form.value.userId) {
-          if (form.value.userId === useUserStore().userId) {
-            form.value.roleIds = null as any;
-            form.value.storeIds = null as any;
-          }
-          await userApi.updateUser(form.value);
+        const formData = { ...form.value };
+        if (Array.isArray(formData.avatar)) {
+          formData.avatar = formData.avatar.map((item: any) => item.ossId).join(',');
+        }
+        if (formData.userId) {
+          await userApi.updateUser(formData);
         } else {
         } else {
-          await userApi.addUser(form.value);
+          await userApi.addUser(formData);
         }
         }
         ElMessage.success('保存成功');
         ElMessage.success('保存成功');
         dialogVisible.value = false;
         dialogVisible.value = false;
@@ -351,4 +356,13 @@ onMounted(() => {
   display: flex;
   display: flex;
   justify-content: flex-end;
   justify-content: flex-end;
 }
 }
+:deep(.circular-upload .el-upload--picture-card),
+:deep(.circular-upload .el-upload-list--picture-card .el-upload-list__item) {
+  width: 100px;
+  height: 100px;
+  border-radius: 50%;
+}
+:deep(.circular-upload .el-upload-list__item-actions) {
+  border-radius: 50%;
+}
 </style>
 </style>

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

@@ -71,17 +71,6 @@
         <el-form-item label="角色名称" prop="roleName">
         <el-form-item label="角色名称" prop="roleName">
           <el-input v-model="form.roleName" placeholder="请输入角色名称" />
           <el-input v-model="form.roleName" placeholder="请输入角色名称" />
         </el-form-item>
         </el-form-item>
-        <el-form-item prop="roleKey">
-          <template #label>
-            <span>
-              <el-tooltip content="控制器中定义的权限字符,如:@SaCheckRole('admin')" placement="top">
-                <el-icon><question-filled /></el-icon>
-              </el-tooltip>
-              权限字符
-            </span>
-          </template>
-          <el-input v-model="form.roleKey" placeholder="请输入权限字符" />
-        </el-form-item>
         <el-form-item label="角色顺序" prop="roleSort">
         <el-form-item label="角色顺序" prop="roleSort">
           <el-input-number v-model="form.roleSort" controls-position="right" :min="0" />
           <el-input-number v-model="form.roleSort" controls-position="right" :min="0" />
         </el-form-item>
         </el-form-item>
@@ -225,7 +214,6 @@ const data = reactive<PageData<RoleForm, RoleQuery>>({
   },
   },
   rules: {
   rules: {
     roleName: [{ required: true, message: '角色名称不能为空', trigger: 'blur' }],
     roleName: [{ required: true, message: '角色名称不能为空', trigger: 'blur' }],
-    roleKey: [{ required: true, message: '权限字符不能为空', trigger: 'blur' }],
     roleSort: [{ required: true, message: '角色顺序不能为空', trigger: 'blur' }]
     roleSort: [{ required: true, message: '角色顺序不能为空', trigger: 'blur' }]
   }
   }
 });
 });

+ 32 - 2
src/views/system/user/index.vue

@@ -149,6 +149,11 @@
     <el-dialog ref="formDialogRef" v-model="dialog.visible" :title="dialog.title" width="600px" append-to-body @close="closeDialog">
     <el-dialog ref="formDialogRef" v-model="dialog.visible" :title="dialog.title" width="600px" append-to-body @close="closeDialog">
       <el-form ref="userFormRef" :model="form" :rules="rules" label-width="80px">
       <el-form ref="userFormRef" :model="form" :rules="rules" label-width="80px">
         <el-row>
         <el-row>
+          <el-col :span="24">
+            <div style="display: flex; justify-content: center; align-items: center; margin-bottom: 20px;">
+              <image-upload v-model="form.avatar" :limit="1" :is-show-tip="false" class="circular-upload" />
+            </div>
+          </el-col>
           <el-col :span="12">
           <el-col :span="12">
             <el-form-item label="用户昵称" prop="nickName">
             <el-form-item label="用户昵称" prop="nickName">
               <el-input v-model="form.nickName" placeholder="请输入用户昵称" maxlength="30" />
               <el-input v-model="form.nickName" placeholder="请输入用户昵称" maxlength="30" />
@@ -366,7 +371,8 @@ const initFormData: UserForm = {
   status: '0',
   status: '0',
   remark: '',
   remark: '',
   postIds: [],
   postIds: [],
-  roleIds: []
+  roleIds: [],
+  avatar: ''
 };
 };
 
 
 const initData: PageData<UserForm, UserQuery> = {
 const initData: PageData<UserForm, UserQuery> = {
@@ -616,17 +622,29 @@ const handleUpdate = async (row?: UserForm) => {
   Object.assign(form.value, data.user);
   Object.assign(form.value, data.user);
   postOptions.value = data.posts;
   postOptions.value = data.posts;
   roleOptions.value = Array.from(
   roleOptions.value = Array.from(
-    new Map([...data.roles, ...data.user.roles].map(role => [role.roleId, role])).values()
+    new Map([...data.roles, ...(data.user.roles || [])].map(role => [role.roleId, role])).values()
   );
   );
   form.value.postIds = data.postIds;
   form.value.postIds = data.postIds;
   form.value.roleIds = data.roleIds;
   form.value.roleIds = data.roleIds;
   form.value.password = '';
   form.value.password = '';
+  
+  // 处理头像回显
+  if (data.user.avatar && data.user.avatarUrl) {
+    form.value.avatar = [{ ossId: data.user.avatar, url: data.user.avatarUrl }];
+  } else if (data.user.avatar) {
+    form.value.avatar = data.user.avatar;
+  } else {
+    form.value.avatar = '';
+  }
 };
 };
 
 
 /** 提交按钮 */
 /** 提交按钮 */
 const submitForm = () => {
 const submitForm = () => {
   userFormRef.value?.validate(async (valid: boolean) => {
   userFormRef.value?.validate(async (valid: boolean) => {
     if (valid) {
     if (valid) {
+      if (Array.isArray(form.value.avatar)) {
+        form.value.avatar = form.value.avatar.map((item: any) => item.ossId).join(',');
+      }
       if (form.value.userId) {
       if (form.value.userId) {
         // 自己编辑自己的情况下 不允许编辑角色部门岗位
         // 自己编辑自己的情况下 不允许编辑角色部门岗位
         if (form.value.userId == useUserStore().userId) {
         if (form.value.userId == useUserStore().userId) {
@@ -677,3 +695,15 @@ async function handleDeptChange(value: number | string) {
   form.value.postIds = [];
   form.value.postIds = [];
 }
 }
 </script>
 </script>
+
+<style scoped>
+:deep(.circular-upload .el-upload--picture-card),
+:deep(.circular-upload .el-upload-list--picture-card .el-upload-list__item) {
+  width: 100px;
+  height: 100px;
+  border-radius: 50%;
+}
+:deep(.circular-upload .el-upload-list__item-actions) {
+  border-radius: 50%;
+}
+</style>