Selaa lähdekoodia

下单、派单、取消基本完成

Huanyi 1 kuukausi sitten
vanhempi
sitoutus
aeff068b61

+ 12 - 0
src/api/fulfiller/pool/index.ts

@@ -2,6 +2,7 @@ import request from '@/utils/request';
 import { AxiosPromise } from 'axios';
 import {
   FlfFulfillerVO, FlfFulfillerForm, FlfFulfillerQuery,
+  FlfFulfillerOnOrderVO, FlfFulfillerOnOrderQuery,
   FlfRewardForm, FlfAdjustPointsForm, FlfAdjustBalanceForm,
   FlfPointsLogVO, FlfBalanceLogVO, FlfRewardLogVO
 } from './types';
@@ -17,6 +18,17 @@ export const listFulfiller = (query?: FlfFulfillerQuery): AxiosPromise<FlfFulfil
   });
 };
 
+/**
+ * 下单派单时查询履约者(分页)
+ */
+export const pageFulfillerOnOrder = (query?: FlfFulfillerOnOrderQuery): AxiosPromise<{ total: number; rows: FlfFulfillerOnOrderVO[] }> => {
+  return request({
+    url: '/fulfiller/fulfiller/pageOnOrder',
+    method: 'get',
+    params: query
+  });
+};
+
 /**
  * 查询履约者详细
  */

+ 14 - 0
src/api/fulfiller/pool/types.ts

@@ -87,6 +87,20 @@ export interface FlfFulfillerQuery extends PageQuery {
   workType?: string;
 }
 
+export interface FlfFulfillerOnOrderVO {
+  id: string | number;
+  name?: string;
+  avatar?: string;
+  phone?: string;
+  tags?: Array<string | number>;
+}
+
+export interface FlfFulfillerOnOrderQuery {
+  content?: string;
+  pageNum?: number;
+  pageSize?: number;
+}
+
 /**
  * 奖惩操作
  */

+ 40 - 0
src/api/order/subOrder/index.ts

@@ -0,0 +1,40 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { SubOrderVO, SubOrderQuery } from './types';
+
+/**
+ * 查询子订单列表
+ * @param query
+ * @returns {*}
+ */
+export const listSubOrder = (query?: SubOrderQuery): AxiosPromise<{ total: number, rows: SubOrderVO[] }> => {
+    return request({
+        url: '/order/subOrder/list',
+        method: 'get',
+        params: query
+    });
+};
+
+export const dispatchSubOrder = (data: { orderId: string | number; fulfiller: string | number; price: number; }) => {
+    return request({
+        url: '/order/subOrder/dispatch',
+        method: 'put',
+        data
+    });
+};
+
+export const getSubOrderInfo = (id: string | number): AxiosPromise<SubOrderVO> => {
+    return request({
+        url: '/order/subOrder/getInfo',
+        method: 'get',
+        params: { id }
+    });
+};
+
+export const cancelSubOrder = (data: { orderId: string | number; }) => {
+    return request({
+        url: '/order/subOrder/cancel',
+        method: 'put',
+        data
+    });
+};

+ 34 - 0
src/api/order/subOrder/types.ts

@@ -0,0 +1,34 @@
+export interface SubOrderQuery {
+    status?: number | string;
+    service?: number | string;
+    content?: string;
+    pageNum: number;
+    pageSize: number;
+}
+
+export interface SubOrderVO {
+    id: number;
+    code: string;
+    service: number;
+    pet: number;
+    petName: string;
+    petType: string;
+    customer: number;
+    customerName: string;
+    site: number;
+    store: number;
+    storeName: string;
+    platformId?: number;
+    placer: number;
+    placerUsername: string;
+    createTime: string;
+    status: number;
+    fulfiller: number;
+    fulfillerName: string;
+    price: number;
+    // 以下为可能需要用到的扩充字段以兼顾页面展现
+    type?: string;
+    transportType?: string;
+    splitType?: string;
+    detail?: any;
+}

+ 3 - 0
src/types/components.d.ts

@@ -51,9 +51,12 @@ declare module 'vue' {
     ElRadio: typeof import('element-plus/es')['ElRadio']
     ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
     ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
+    ElResult: typeof import('element-plus/es')['ElResult']
     ElRow: typeof import('element-plus/es')['ElRow']
     ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
     ElSelect: typeof import('element-plus/es')['ElSelect']
+    ElStep: typeof import('element-plus/es')['ElStep']
+    ElSteps: typeof import('element-plus/es')['ElSteps']
     ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
     ElSwitch: typeof import('element-plus/es')['ElSwitch']
     ElTable: typeof import('element-plus/es')['ElTable']

+ 247 - 0
src/views/order/orderList/components/CareSummaryDrawer.vue

@@ -0,0 +1,247 @@
+<template>
+    <el-drawer
+        v-model="drawerVisible"
+        title="宠物护理工作小结"
+        direction="rtl"
+        size="750px"
+        destroy-on-close
+        class="care-summary-drawer"
+    >
+        <div class="care-summary-container" v-if="order">
+             <!-- Pet Header -->
+             <div class="summary-header">
+                  <div class="avatar-wrapper">
+                      <el-avatar :size="80" :src="order.petAvatar" shape="circle" class="pet-summary-avatar">{{ order.petName?.charAt(0) }}</el-avatar>
+                  </div>
+                  <div class="pet-summary-info">
+                       <div class="summary-name-row">
+                           <span class="name">{{ order.petName }}</span>
+                           <div class="tags-group">
+                               <el-tag :type="order.petGender==='male'?'':'danger'" effect="light" round>
+                                   <el-icon><component :is="order.petGender==='male'?'Male':'Female'" /></el-icon>
+                                   {{ order.petAge }}
+                               </el-tag>
+                               <el-tag v-for="tag in (order.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">{{ order.petBreed || '未知' }}</span>
+                           </div>
+                           <div class="divider-v"></div>
+                           <div class="info-item">
+                               <span class="lbl">体重</span>
+                               <span class="val">{{ order.petWeight }}</span>
+                           </div>
+                           <div class="divider-v"></div>
+                           <div class="info-item">
+                               <span class="lbl">主人</span>
+                               <span class="val">{{ order.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="性格关键词">{{ order.petPersonality }}</el-descriptions-item>
+                      <el-descriptions-item label="健康状况">
+                          <el-tag :type="order.healthStatus==='健康'?'success':'danger'" effect="light" size="small">{{ order.healthStatus }}</el-tag>
+                      </el-descriptions-item>
+                      <el-descriptions-item label="疫苗情况">
+                          <div class="flex-align">
+                              <span style="color:#67c23a; margin-right:8px;" v-if="order.vaccineImg"><el-icon><CircleCheckFilled /></el-icon> 已接种</span>
+                              <span v-else style="color:#909399;">未接种</span>
+                              <el-image 
+                                v-if="order.vaccineImg"
+                                style="width: 24px; height: 24px; border-radius:4px; vertical-align:middle; cursor:zoom-in;"
+                                :src="order.vaccineImg"
+                                :preview-src-list="[order.vaccineImg]"
+                                :preview-teleported="true"
+                              />
+                          </div>
+                      </el-descriptions-item>
+                      <el-descriptions-item label="过敏史">
+                          <span :style="{color: order.allergy ? '#f56c6c' : 'inherit'}">{{ order.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="到家时间">{{ order.homeTime }}</el-descriptions-item>
+                      <el-descriptions-item label="房屋类型">{{ order.houseType }}</el-descriptions-item>
+                      <el-descriptions-item label="入户方式" :span="2">
+                          <span style="font-weight:bold;">{{ order.entryMethod }}</span> 
+                          <span style="margin-left:8px; color:#909399;">({{ order.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">{{ order.fulfillerName || '当前履约者' }}</span>
+                     </div>
+                     <div class="f-row">
+                         <span class="lbl">提交时间</span>
+                         <span class="val">{{ order.summaryTime || '2024-02-04 17:00' }}</span>
+                     </div>
+                 </div>
+                 <div class="footer-action">
+                     <el-button size="large" @click="drawerVisible = false">关闭</el-button>
+                 </div>
+             </div>
+        </div>
+    </el-drawer>
+</template>
+
+<script setup>
+import { ref, computed, watch } from 'vue'
+
+const props = defineProps({
+    visible: Boolean,
+    order: Object
+})
+const emit = defineEmits(['update:visible', 'submit'])
+
+const drawerVisible = computed({
+    get: () => props.visible,
+    set: (val) => emit('update:visible', val)
+})
+
+const careSummaryText = ref('')
+const isEditingSummary = ref(false)
+
+watch(() => props.visible, (val) => {
+    if (val && props.order) {
+        isEditingSummary.value = false
+        if (!props.order.careSummary) {
+            careSummaryText.value = `1. 精神/身体状态:${props.order.petName}精神状态良好,愿意互动。
+2. 进食/饮水:食欲正常,饮水适当,已清洗碗具。
+3. 排泄情况:排便正常,颜色形状正常,已清理。
+4. 卫生情况:猫砂盆/地面已清理干净,无异味。
+5. 互动情况:陪玩了20分钟,${props.order.petName}很开心。
+6. 特殊情况/备注:无特殊异常。`
+        } else {
+            careSummaryText.value = props.order.careSummary
+        }
+    }
+})
+
+const saveCareSummary = () => {
+    emit('submit', careSummaryText.value)
+    isEditingSummary.value = false
+}
+</script>
+
+<style scoped>
+/* 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; }
+</style>

+ 555 - 0
src/views/order/orderList/components/DispatchDialog.vue

@@ -0,0 +1,555 @@
+<template>
+    <el-dialog v-model="dialogVisible" 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="order">
+                <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="order.typeCode">
+                            {{ getShortType(order.typeCode) }}
+                        </div>
+                    </div>
+                    <div class="card-main">
+                        <template v-if="order.typeCode === 'transport'">
+                            <div class="row-addr" :title="order.toAddress || order.pickAddr || order.dropAddr">
+                                <span class="tag home">址</span> {{ order.toAddress || order.pickAddr || order.dropAddr
+                                }}
+                            </div>
+                        </template>
+                        <template v-else>
+                            <div class="row-addr" :title="order.address">
+                                <span class="tag home">址</span> {{ order.address }}
+                            </div>
+                        </template>
+                        <div class="row-time" style="margin-top: 4px;">
+                            <el-icon>
+                                <Clock />
+                            </el-icon> {{ order.time }}
+                            <span class="days-tag" v-if="order.daysLater">{{ order.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>
+                    <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.phone || '--' }}</span>
+                            </div>
+                        </div>
+
+                        <div class="row-2 categories-row"
+                            style="margin-top: 6px; display:flex; gap:4px; flex-wrap:wrap;">
+                            <el-tag v-for="cat in (currentRider.tags || [])" :key="cat" size="small"
+                                :type="getTagType(cat)" effect="plain">{{ getTagText(cat) }}</el-tag>
+                        </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 class="rider-scroll">
+                        <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>
+                                <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.phone || '--' }}</span>
+                                        </div>
+                                    </div>
+
+                                    <div class="row-2 categories-row"
+                                        style="margin-top: 6px; display:flex; gap:4px; flex-wrap:wrap;">
+                                        <el-tag v-for="cat in (rider.tags || [])" :key="cat" size="small"
+                                            :type="getTagType(cat)" effect="plain">{{ getTagText(cat) }}</el-tag>
+                                    </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 class="rider-pagination" style="margin-top: 20px;">
+                    <el-pagination v-model:current-page="pageNum" v-model:page-size="pageSize"
+                        :page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next" :total="total"
+                        @current-change="loadRiders" @size-change="handlePageSizeChange" />
+                </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="dialogVisible = false">取消</el-button>
+                    <el-button type="primary" :disabled="!canSubmit" @click="handleDispatchSubmit">确认派单</el-button>
+                </div>
+            </div>
+        </div>
+    </el-dialog>
+</template>
+
+<script setup>
+import { ref, computed, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+import { pageFulfillerOnOrder } from '@/api/fulfiller/pool'
+import { listAllTag } from '@/api/fulfiller/tag'
+
+const props = defineProps({
+    visible: Boolean,
+    order: Object
+})
+const emit = defineEmits(['update:visible', 'submit'])
+
+const dialogVisible = computed({
+    get: () => props.visible,
+    set: (val) => emit('update:visible', val)
+})
+
+const ridersList = ref([])
+const total = ref(0)
+const pageNum = ref(1)
+const pageSize = ref(10)
+
+const allTags = ref([])
+const tagMap = computed(() => {
+    const map = {}
+    for (const t of (allTags.value || [])) {
+        if (t && t.id !== undefined && t.id !== null) map[t.id] = t
+    }
+    return map
+})
+
+const currentRider = ref(null)
+const dispatchSearchQuery = ref('')
+const selectedRiderId = ref(null)
+const dispatchFee = ref(0)
+
+const loadAllTags = async () => {
+    if (allTags.value && allTags.value.length > 0) return
+    try {
+        const res = await listAllTag({ category: 'fulfiller' })
+        allTags.value = res?.data || []
+    } catch {
+        allTags.value = []
+    }
+}
+
+const loadRiders = async () => {
+    try {
+        const res = await pageFulfillerOnOrder({
+            content: dispatchSearchQuery.value || undefined,
+            pageNum: pageNum.value,
+            pageSize: pageSize.value
+        })
+        ridersList.value = res?.rows || []
+        total.value = res?.total || 0
+
+        if (props.order?.riderId) {
+            currentRider.value = ridersList.value.find(r => r.id === props.order.riderId) || null
+        }
+    } catch {
+        ridersList.value = []
+        total.value = 0
+    }
+}
+
+const handlePageSizeChange = (size) => {
+    pageSize.value = size
+    pageNum.value = 1
+    loadRiders()
+}
+
+watch(() => props.visible, (val) => {
+    if (val && props.order) {
+        currentRider.value = null
+        dispatchSearchQuery.value = ''
+        selectedRiderId.value = null
+        dispatchFee.value = 0
+        pageNum.value = 1
+        loadAllTags()
+        loadRiders()
+    }
+})
+
+const getTagText = (tagId) => {
+    const t = tagMap.value?.[tagId]
+    return t?.name || String(tagId)
+}
+
+const getTagType = (tagId) => {
+    const t = tagMap.value?.[tagId]
+    const type = t?.colorType
+    if (type === 'success' || type === 'warning' || type === 'danger' || type === 'info') return type
+    return ''
+}
+
+watch(dispatchSearchQuery, () => {
+    pageNum.value = 1
+    loadRiders()
+})
+
+const getShortType = (code) => {
+    const map = { 'transport': '接送', 'feeding': '喂遛', 'washing': '洗护' }
+    return map[code] || '订单'
+}
+
+const filteredDispatchRiders = computed(() => {
+    return ridersList.value || []
+})
+
+const canSubmit = computed(() => {
+    return !!selectedRiderId.value && !!dispatchFee.value
+})
+
+const handleDispatchSubmit = () => {
+    if (!selectedRiderId.value) {
+        ElMessage.warning('请选择履约者')
+        return
+    }
+    if (!dispatchFee.value) {
+        ElMessage.warning('请输入服务费用')
+        return
+    }
+    const rider = ridersList.value.find(r => r.id === selectedRiderId.value)
+    emit('submit', {
+        riderId: rider.id,
+        riderName: rider.name,
+        fee: dispatchFee.value
+    })
+    dialogVisible.value = false
+}
+</script>
+
+<style scoped>
+/* Dispatch Dialog Styles */
+.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-scroll {
+    max-height: 45vh;
+}
+
+.rider-grid {
+    display: grid;
+    grid-template-columns: repeat(2, 1fr);
+    gap: 12px;
+    padding-right: 10px;
+}
+
+.rider-pagination {
+    margin-top: 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;
+}
+</style>

+ 954 - 0
src/views/order/orderList/components/OrderDetailDrawer.vue

@@ -0,0 +1,954 @@
+<template>
+    <el-drawer v-model="drawerVisible" title="订单详情" direction="rtl" size="60%" class="order-detail-drawer">
+        <div class="detail-container" v-if="order">
+            <!-- 1. Header Status -->
+            <div class="detail-header">
+                <div class="left-head">
+                    <span class="order-no">{{ order.orderNo }}</span>
+                    <el-tag :type="getStatusTag(order.status)" effect="dark" class="status-tag">{{
+                        getStatusName(order.status) }}</el-tag>
+                    <el-tag effect="plain" class="type-tag"
+                        :type="order.type === 'transport' ? '' : (order.type === 'feeding' ? 'warning' : 'danger')">
+                        {{ getTypeName(order.type) }}
+                    </el-tag>
+                </div>
+                <div class="right-head">
+                    <!-- Action Buttons Group -->
+                    <div class="detail-actions">
+                        <template v-if="[0, 1, 2].includes(order.status)">
+                            <el-button type="success" icon="Bicycle" @click="emit('dispatch', order)">立即派单</el-button>
+                        </template>
+
+                        <template v-if="order.status === 0">
+                            <el-button type="danger" plain icon="CircleClose"
+                                @click="emit('cancel', order)">取消订单</el-button>
+                        </template>
+
+                        <template v-if="order.status === 3">
+                            <el-button type="primary" icon="CircleCheck"
+                                @click="emit('command', 'complete', order)">确认完成</el-button>
+                        </template>
+
+                        <template v-if="[3, 4].includes(order.status)">
+                            <el-button icon="Notebook" @click="emit('care-summary', order)">护理小结</el-button>
+                        </template>
+
+                        <el-dropdown trigger="click" @command="(cmd) => emit('command', cmd, order)"
+                            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="[5, 4].includes(order.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 -->
+                    <div class="info-section pet-section">
+                        <div class="sec-header">
+                            <span class="label">宠物档案</span>
+                            <el-tag size="small" effect="plain">{{ order.petBreed }}</el-tag>
+                        </div>
+                        <div class="pet-basic-row">
+                            <el-avatar :size="50" :src="order.petAvatar" shape="square" class="pet-avatar-lg">{{
+                                (order.petName || '').charAt(0) }}</el-avatar>
+                            <div class="pet-names">
+                                <div class="b-name">{{ order.petName }}
+                                    <el-icon v-if="order.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">{{ order.petAge || '未知年龄' }}</el-tag>
+                                    <el-tag size="small" type="info">{{ order.petWeight || '未知体重' }}</el-tag>
+                                </div>
+                            </div>
+                        </div>
+                        <el-descriptions :column="2" size="small" class="pet-desc" border>
+                            <el-descriptions-item label="绝育状态">{{ order.petSterilized ? '已绝育' : '未绝育'
+                            }}</el-descriptions-item>
+                            <el-descriptions-item label="疫苗状态"><span style="color:#67c23a">{{ order.petVaccine || '未知'
+                            }}</span></el-descriptions-item>
+                            <el-descriptions-item label="性格特点">{{ order.petCharacter || '温顺' }}</el-descriptions-item>
+                            <el-descriptions-item label="健康状况">{{ order.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="order.userAvatar">{{ (order.userName || '').charAt(0)
+                                }}</el-avatar>
+                                <div class="u-info">
+                                    <div class="nm">{{ order.userName }}</div>
+                                    <div class="ph">{{ order.contactPhone }}</div>
+                                </div>
+                            </div>
+                            <div class="addr-box">
+                                <div class="addr-label">服务地址</div>
+                                <div class="addr-txt">{{ order.city }}{{ order.district }} {{ order.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">
+                            <div class="section-block">
+                                <div class="sec-title-bar">基础业务信息</div>
+                                <el-descriptions :column="3" border size="default" class="custom-desc">
+                                    <el-descriptions-item label="系统单号">{{ order.orderNo }}</el-descriptions-item>
+                                    <el-descriptions-item label="服务类型">
+                                        {{ getTypeName(order.type) }}
+                                        <el-tag size="small" v-if="order.type === 'transport'" style="margin-left:5px"
+                                            effect="light">{{ getTransportModeName(order.transportType) }}</el-tag>
+                                    </el-descriptions-item>
+
+                                    <el-descriptions-item label="归属门店">{{ order.merchantName }}
+                                        ({{ Number(order.platformId) === 1 ? '门店下单' : '平台代下单' }})</el-descriptions-item>
+                                    <el-descriptions-item label="宠主信息">{{ order.userName }} / {{ order.contactPhone
+                                    }}</el-descriptions-item>
+                                    <el-descriptions-item label="服务费用" label-class-name="money-label">
+                                        <span style="color:#f56c6c; font-weight:bold;">¥ {{ order.fulfillerFee }}</span>
+                                    </el-descriptions-item>
+
+                                    <el-descriptions-item label="预约时间">{{ getServiceTimeRange(order.serviceTime)
+                                    }}</el-descriptions-item>
+                                    <el-descriptions-item label="团购套餐">{{ order.groupBuyPackage || '未使用团购套餐'
+                                    }}</el-descriptions-item>
+                                    <el-descriptions-item label="创建时间">{{ order.createTime }}</el-descriptions-item>
+
+                                    <el-descriptions-item label="订单备注" :span="3">
+                                        {{ order.remark || '暂无备注' }}
+                                    </el-descriptions-item>
+                                </el-descriptions>
+                            </div>
+
+                            <div v-if="order.type === 'transport'" class="section-block transport-split-block">
+                                <div class="sec-title-bar">接送任务详情</div>
+                                <div class="transport-one">
+                                    <div class="t-row">
+                                        <el-tag size="small" effect="plain" class="sub-badge">{{
+                                            getTransportLabel(order.subOrderType) }}</el-tag>
+                                        <span class="time">{{ order.serviceTime }}</span>
+                                    </div>
+                                    <div class="t-row">
+                                        <span class="t-k">起点</span>
+                                        <span class="t-v">{{ order.detail?.fromAddress || order.detail?.pickAddr || '--'
+                                            }}</span>
+                                    </div>
+                                    <div class="t-row">
+                                        <span class="t-k">终点</span>
+                                        <span class="t-v">{{ order.detail?.toAddress || order.detail?.dropAddr ||
+                                            order.toAddress ||
+                                            '--' }}</span>
+                                    </div>
+                                    <div class="t-row sub">
+                                        <span class="t-v">{{ order.contact || order.userName || '--' }} {{
+                                            order.contactPhoneNumber
+                                            || order.contactPhone || '--' }}</span>
+                                    </div>
+                                </div>
+                            </div>
+
+                            <div v-if="['feeding', 'washing'].includes(order.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">{{ order.detail.area || order.address
+                                    }}</el-descriptions-item>
+                                    <el-descriptions-item label="服务套餐">{{ order.detail.packageName
+                                    }}</el-descriptions-item>
+                                    <el-descriptions-item label="特殊要求">{{ order.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="order.fulfillerName" class="fulfiller-card">
+                                <div class="f-left">
+                                    <el-avatar :size="60" :src="order.fulfillerAvatar">{{ order.fulfillerName.charAt(0)
+                                    }}</el-avatar>
+                                </div>
+                                <div class="f-right">
+                                    <div class="f-row1">
+                                        <span class="f-name">{{ order.fulfillerName }}</span>
+                                        <el-tag size="small" type="primary" effect="plain" round>Lv1 普通</el-tag>
+                                    </div>
+                                    <div class="f-row2">
+                                        <span>联系电话:{{ order.fulfillerPhone || '138****0000' }}</span>
+                                        <span class="sep">|</span>
+                                        <span>归属区域:{{ order.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>{{ order.createTime }}</span>
+                                        <span><span style="color:#909399;">接单时间:</span>{{ order.detail?.receiveTime ||
+                                            order.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">
+                            <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 (order.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>
+
+                                <el-timeline-item
+                                    v-if="(!order.orderLogs || order.orderLogs.length === 0) && order.timeline"
+                                    v-for="(log, idx) in order.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>
+</template>
+
+<script setup>
+import { ref, computed, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+import { getPet } from '@/api/archieves/pet'
+import { getCustomer } from '@/api/archieves/customer'
+
+const props = defineProps({
+    visible: Boolean,
+    order: Object
+})
+
+const emit = defineEmits(['update:visible', 'dispatch', 'cancel', 'command', 'care-summary'])
+
+const drawerVisible = computed({
+    get: () => props.visible,
+    set: (val) => emit('update:visible', val)
+})
+
+const orderDetail = ref(null)
+const order = computed(() => orderDetail.value || props.order)
+
+const loadSeq = ref(0)
+
+const loadPetAndCustomer = async (order) => {
+    const seq = ++loadSeq.value
+    const next = { ...(order || {}) }
+
+    const petId = next?.pet || next?.petId
+    if (petId) {
+        try {
+            const res = await getPet(petId)
+            const pet = res?.data
+            if (pet) {
+                next.petName = pet.name ?? next.petName
+                next.petAvatar = pet.avatarUrl ?? next.petAvatar
+                next.petGender = pet.gender ?? next.petGender
+                next.petAge = (pet.age !== undefined && pet.age !== null) ? `${pet.age}岁` : next.petAge
+                next.petWeight = (pet.weight !== undefined && pet.weight !== null) ? `${pet.weight}kg` : next.petWeight
+                next.petBreed = pet.breed ?? next.petBreed
+                next.petSterilized = (pet.isSterilized !== undefined && pet.isSterilized !== null)
+                    ? (Number(pet.isSterilized) === 1)
+                    : next.petSterilized
+                next.petVaccine = pet.vaccineStatus ?? next.petVaccine
+                next.petCharacter = pet.personality ?? next.petCharacter
+                next.petHealth = pet.healthStatus ?? next.petHealth
+            }
+        } catch {
+        }
+    }
+
+    const customerId = next?.customer || next?.customerId
+    if (customerId) {
+        try {
+            const res = await getCustomer(customerId)
+            const customer = res?.data
+            if (customer) {
+                next.userName = customer.name ?? next.userName
+                next.userAvatar = customer.avatarUrl ?? next.userAvatar
+                next.contactPhone = customer.phone ?? next.contactPhone
+                next.city = customer.areaName ?? next.city
+                next.address = customer.address ?? next.address
+            }
+        } catch {
+        }
+    }
+
+    if (seq !== loadSeq.value) return
+    orderDetail.value = next
+}
+
+watch(() => props.order, (val) => {
+    if (!val) {
+        orderDetail.value = null
+        return
+    }
+    loadPetAndCustomer(val)
+}, { immediate: true, deep: true })
+
+const activeDetailTab = ref('basic')
+
+const getStatusName = (status) => {
+    const map = { 0: '待派单', 1: '待接单', 2: '服务中', 3: '待商家确认', 4: '已完成', 5: '已取消' }
+    return map[status] || '未知'
+}
+const getStatusTag = (status) => {
+    const map = { 0: 'danger', 1: 'warning', 2: 'primary', 3: 'warning', 4: 'success', 5: 'info' }
+    return map[status] || 'info'
+}
+const getTypeName = (type) => {
+    const map = { transport: '宠物接送', feeding: '上门喂遛', washing: '上门洗护' }
+    return map[type]
+}
+const getTransportModeName = (type) => {
+    const map = { round: '往返接送', pick: '单程接(到店)', drop: '单程送(回家)' }
+    return map[type] || '接送服务'
+}
+
+const getTransportLabel = (t) => {
+    if (t === 0 || t === '0') return '接'
+    if (t === 1 || t === '1') return '送'
+    if (t === 2 || t === '2') return '单程接'
+    if (t === 3 || t === '3') return '单程送'
+    return '接送'
+}
+const getServiceTimeRange = (timeStr) => {
+    if (!timeStr) return '--'
+    try {
+        if (timeStr.length < 16) return timeStr
+        let timePart = timeStr.substring(11, 16)
+        let [hh, mm] = timePart.split(':').map(Number)
+        let endH = hh + 2
+        if (endH >= 24) endH -= 24
+        let endHStr = endH.toString().padStart(2, '0')
+        return `${timeStr}-${endHStr}:${mm.toString().padStart(2, '0')}`
+    } catch (e) {
+        return timeStr
+    }
+}
+
+const currentOrderSteps = computed(() => {
+    if (!props.order) return { active: 0, steps: [] }
+    const steps = [
+        { title: '商户下单', status: 'created', time: '' },
+        { title: '运营派单', status: 'dispatched', time: '' },
+        { title: '履约接单', status: 'accepted', time: '' },
+        { title: '服务中', status: 'serving', time: '' },
+        { title: '待商家确认', status: 'confirming', time: '' },
+        { title: '已完成', status: 'completed', time: '' }
+    ]
+    const logs = props.order.orderLogs || []
+    const status = props.order.status
+    let active = 0
+    const findTime = (keyword) => {
+        const log = logs.find(l => l.title.includes(keyword) || l.content.includes(keyword))
+        return log ? log.time : ''
+    }
+    steps[0].time = props.order.createTime || findTime('下单') || findTime('创建')
+    if (steps[0].time) active = 1
+    if ([0].includes(status)) {
+        steps[1].time = findTime('派单') || steps[0].time
+    } else {
+        steps[1].time = findTime('派单') || ''
+    }
+    if ([1, 2, 3, 4].includes(status)) active = 2
+    steps[2].time = findTime('接单')
+    if ([1].includes(status)) {
+        steps[2].title = '待履约者接单'
+    } else if ([2, 3, 4].includes(status)) {
+        steps[2].title = '履约者已接单'
+        active = 3
+    }
+    steps[3].time = findTime('到达') || findTime('出发')
+    if ([2].includes(status)) {
+        steps[3].title = '服务进行中'
+    } else if ([3, 4].includes(status)) {
+        steps[3].title = '服务已完成'
+        active = 4
+    }
+    steps[4].time = findTime('等待商家确认') || findTime('待验收')
+    if ([3].includes(status)) {
+        steps[4].title = '待商家确认'
+    } else if ([4].includes(status)) {
+        steps[4].title = '商家已确认'
+        active = 5
+    }
+    if (status === 4) {
+        steps[5].time = findTime('完成')
+        active = 6
+    }
+    if (status === 5) {
+        return {
+            active: 1,
+            steps: [
+                { title: '商户下单', time: steps[0].time },
+                { title: '已取消', time: findTime('取消') || '订单已取消' }
+            ]
+        }
+    }
+    return { active, steps }
+})
+
+const serviceProgressSteps = computed(() => {
+    const order = props.order
+    if (!order) return []
+    if ([0, 1, 5].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 = []
+    steps.push({
+        title: '已接单', time: `${datePart} 09:30`, icon: 'Bicycle', color: '#ff9900',
+        desc: `履约者 ${order.fulfillerName || '当前履约者'} 已确认接单,准备前往服务地点`, media: []
+    })
+    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) {
+        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' }
+            ]
+        })
+        if ([3, 4].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 {
+        steps.push({
+            title: '开始服务', time: `${datePart} 10:00`, icon: 'VideoPlay', color: '#ff9900',
+            desc: '已确认宠物状态,开始进行服务视频录制',
+            media: [{ type: 'image', url: 'https://fuss10.elemecdn.com/0/6f/e35ff375812e6b0020b6b4e8f9583jpeg.jpeg' }]
+        })
+        if ([3, 4].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' }
+                ]
+            })
+        }
+    }
+    if ([3, 4].includes(order.status)) {
+        steps.push({ title: '待商家确认', time: `${datePart} 10:55`, icon: 'Clock', color: '#ff9900', desc: '履约者已提交完成信息,等待商家确认订单', media: [] })
+    }
+    if (order.status === 4) {
+        steps.push({ title: '订单完成', time: `${datePart} 11:00`, icon: 'Select', color: '#67C23A', desc: '用户/商家已确认,服务圆满结束', media: [] })
+    }
+    return steps
+})
+
+const handleExportLogs = () => {
+    const logs = props.order.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_${props.order.orderNo}.csv`
+    link.click()
+    URL.revokeObjectURL(url)
+    ElMessage.success('导出成功')
+}
+</script>
+
+<style scoped>
+/* Detail Styles */
+.order-detail-drawer :deep(.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;
+}
+
+.detail-actions {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+}
+
+.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;
+}
+
+.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;
+}
+
+/* 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;
+}
+
+/* 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;
+}
+
+.empty-state {
+    padding: 40px 0;
+    text-align: center;
+}
+
+/* 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;
+}
+
+/* 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;
+}
+
+.transport-one {
+    border: 1px solid #ebeef5;
+    border-radius: 6px;
+    background: #fff;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.02);
+    padding: 14px 16px;
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+}
+
+.transport-one .t-row {
+    display: flex;
+    align-items: baseline;
+    gap: 10px;
+}
+
+.transport-one .t-row .time {
+    font-size: 13px;
+    font-weight: bold;
+    color: #f56c6c;
+}
+
+.transport-one .t-k {
+    width: 40px;
+    color: #909399;
+    font-size: 13px;
+}
+
+.transport-one .t-v {
+    color: #303133;
+    font-size: 14px;
+    line-height: 1.4;
+}
+
+.transport-one .t-row.sub .t-v {
+    color: #909399;
+    font-size: 13px;
+}
+
+/* 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>

+ 47 - 0
src/views/order/orderList/components/RemarkDialog.vue

@@ -0,0 +1,47 @@
+<template>
+    <el-dialog v-model="dialogVisible" title="订单备注" width="500px">
+        <div style="margin-bottom:10px; font-size:13px; color:#909399;">
+            <span v-if="order">订单号:{{ order.orderNo }}</span>
+        </div>
+        <el-input 
+            v-model="remarkForm" 
+            type="textarea" 
+            :rows="5" 
+            placeholder="请输入订单备注信息..." 
+        />
+        <template #footer>
+            <span class="dialog-footer">
+                <el-button @click="dialogVisible = false">取消</el-button>
+                <el-button type="primary" @click="handleSubmit">保存备注</el-button>
+            </span>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup>
+import { ref, computed, watch } from 'vue'
+
+const props = defineProps({
+    visible: Boolean,
+    order: Object
+})
+const emit = defineEmits(['update:visible', 'submit'])
+
+const dialogVisible = computed({
+    get: () => props.visible,
+    set: (val) => emit('update:visible', val)
+})
+
+const remarkForm = ref('')
+
+watch(() => props.visible, (val) => {
+    if (val && props.order) {
+        remarkForm.value = props.order.remark || ''
+    }
+})
+
+const handleSubmit = () => {
+    emit('submit', remarkForm.value)
+    dialogVisible.value = false
+}
+</script>

+ 99 - 0
src/views/order/orderList/components/RewardDialog.vue

@@ -0,0 +1,99 @@
+<template>
+    <el-dialog v-model="dialogVisible" title="奖惩操作" width="500px">
+        <div v-if="order" 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;">{{ order.fulfillerName || '未指派' }}</span></div>
+                 <div style="font-size: 13px; margin-top: 4px;">订单号:{{ order.orderNo }}</div> 
+                 <div style="font-size: 13px; margin-top: 4px; display:flex; align-items:center; gap:6px;">
+                     服务类型:
+                     <el-tag :type="getTypeTag(order.type)" size="small">{{ getTypeName(order.type) }}</el-tag>
+                     <el-tag v-if="order.type === 'transport' && order.transportType === 'round'" size="small" effect="plain" type="warning">往返</el-tag>
+                     <el-tag v-if="order.splitType === 'pick'" size="small" effect="dark" color="#409eff" style="border:none; color:white;">接</el-tag>
+                     <el-tag v-if="order.splitType === 'drop'" size="small" effect="dark" color="#67c23a" style="border:none; color:white;">送</el-tag>
+                     <el-tag v-if="order.type === 'transport' && order.transportType === 'pick' && !order.splitType" size="small" effect="plain">单程接</el-tag>
+                     <el-tag v-if="order.type === 'transport' && order.transportType === 'drop' && !order.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="dialogVisible = false">取消</el-button>
+            <el-button type="primary" @click="handleSubmit">确认执行</el-button>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup>
+import { reactive, computed, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+
+const props = defineProps({
+    visible: Boolean,
+    order: Object
+})
+const emit = defineEmits(['update:visible', 'submit'])
+
+const dialogVisible = computed({
+    get: () => props.visible,
+    set: (val) => emit('update:visible', val)
+})
+
+const rewardForm = reactive({
+    type: 'reward',
+    item: 'points',
+    value: 10,
+    reason: ''
+})
+
+watch(() => props.visible, (val) => {
+    if (val) {
+        rewardForm.type = 'reward'
+        rewardForm.item = 'points'
+        rewardForm.value = 10
+        rewardForm.reason = ''
+    }
+})
+
+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 handleSubmit = () => {
+    if(!rewardForm.reason) {
+        ElMessage.warning('请输入奖惩原因')
+        return
+    }
+    emit('submit', rewardForm)
+    dialogVisible.value = false
+}
+</script>

+ 750 - 0
src/views/order/orderList/index.vue

@@ -0,0 +1,750 @@
+<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.service" size="default" @change="handleSearch">
+                            <el-radio-button label="">全部类型</el-radio-button>
+                            <el-radio-button v-for="item in serviceOptions" :key="item.id" :label="item.id">{{ item.name
+                                }}</el-radio-button>
+                        </el-radio-group>
+                        <el-input v-model="filters.content" 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-change="handleStatusTabChange">
+                    <el-tab-pane label="全部订单" name="" />
+                    <el-tab-pane label="待派单" name="0" />
+                    <el-tab-pane label="待接单" name="1" />
+                    <el-tab-pane label="服务中" name="2" />
+                    <el-tab-pane label="待商家确认" name="3" />
+                    <el-tab-pane label="已完成" name="4" />
+                    <el-tab-pane label="已取消" name="5" />
+                </el-tabs>
+            </template>
+
+            <el-table :data="tableData" style="width: 100%" v-loading="loading"
+                :header-cell-style="{ background: '#f5f7fa' }">
+                <el-table-column prop="code" label="订单号" width="170" fixed="left" />
+
+                <el-table-column label="服务类型" width="190">
+                    <template #default="{ row }">
+                        <div class="service-type-cell">
+                            <el-tag>{{ getServiceName(row.service) }}</el-tag>
+                            <el-tag v-if="getServiceModeTag(row)" class="sub-tag" type="warning" effect="plain">{{
+                                getServiceModeTag(row) }}</el-tag>
+                            <el-tag v-if="getServiceOrderTypeTag(row)" class="sub-tag"
+                                :type="getServiceOrderTypeTag(row).type" effect="dark">{{
+                                    getServiceOrderTypeTag(row).label }}</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" class="avatar-type">{{ row.petName?.charAt(0) }}</el-avatar>
+                            <div class="pet-detail">
+                                <div class="pet-name">
+                                    {{ row.petName }}
+                                </div>
+                                <div class="pet-breed">{{ row.petBreed }}</div>
+                            </div>
+                        </div>
+                    </template>
+                </el-table-column>
+
+                <el-table-column label="所属用户" width="120" prop="customerName">
+                    <template #default="{ row }">
+                        <span style="font-weight: 500">{{ row.customerName }}</span>
+                    </template>
+                </el-table-column>
+
+                <el-table-column label="城市/区域" width="140">
+                    <template #default="{ row }">
+                        <div>{{ getCityDistrictText(row).city || '-' }}</div>
+                        <div class="sub-text">{{ getCityDistrictText(row).district || '-' }}</div>
+                    </template>
+                </el-table-column>
+
+                <el-table-column label="商户/下单人" min-width="160">
+                    <template #default="{ row }">
+                        <div class="merchant-info">
+                            <div>{{ row.storeName }}</div>
+                            <div class="sub-text" v-if="row.placerUsername">{{ row.placerUsername }}</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 label="订单状态" width="100">
+                    <template #default="{ row }">
+                        <div class="status-cell">
+                            <div class="status-dot" :class="getStatusClass(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.price !== null && row.price !== undefined">¥{{
+                                row.price }}</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="row.status === 0" link type="success" size="small"
+                                @click="openDispatchDialog(row)">派单</el-button>
+                            <el-button v-if="[1, 2].includes(row.status)" link type="warning" size="small"
+                                @click="openDispatchDialog(row)">重新派单</el-button>
+                            <el-button v-if="[0, 1].includes(row.status)" link type="danger" size="small"
+                                @click="handleCancel(row)">取消</el-button>
+
+                            <el-dropdown v-if="[2, 3, 4].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 === 3"
+                                            command="complete">确认完成</el-dropdown-item>
+                                        <el-dropdown-item v-if="[3, 4].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>
+
+        <!-- 组件 -->
+        <OrderDetailDrawer v-model:visible="detailVisible" :order="currentOrder" @dispatch="openDispatchDialog"
+            @cancel="handleCancel" @command="handleCommand" @care-summary="openCareSummary" />
+
+        <DispatchDialog v-model:visible="dispatchDialogVisible" :order="currentDispatchOrder"
+            @submit="handleDispatchSubmit" />
+
+        <CareSummaryDrawer v-model:visible="careSummaryVisible" :order="careSummaryOrder" @submit="saveCareSummary" />
+
+        <RewardDialog v-model:visible="rewardDialogVisible" :order="currentOperateRow" @submit="handleRewardSubmit" />
+
+        <RemarkDialog v-model:visible="remarkDialogVisible" :order="currentOperateRow" @submit="handleRemarkSubmit" />
+
+    </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, nextTick } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import OrderDetailDrawer from './components/OrderDetailDrawer.vue'
+import DispatchDialog from './components/DispatchDialog.vue'
+import CareSummaryDrawer from './components/CareSummaryDrawer.vue'
+import RewardDialog from './components/RewardDialog.vue'
+import RemarkDialog from './components/RemarkDialog.vue'
+import { listOnStore as listServiceOnStore } from '@/api/service/list/index'
+import { listSubOrder } from '@/api/order/subOrder/index'
+import { dispatchSubOrder } from '@/api/order/subOrder/index'
+import { getSubOrderInfo } from '@/api/order/subOrder/index'
+import { cancelSubOrder } from '@/api/order/subOrder/index'
+import { listOnStore as listAreaStationOnStore } from '@/api/system/areaStation'
+import { getStore } from '@/api/system/store'
+
+const loading = ref(false)
+
+const filters = reactive({
+    service: '',
+    status: '',
+    content: ''
+})
+
+const pagination = reactive({
+    current: 1,
+    size: 10,
+    total: 100
+})
+
+const tableData = ref([])
+const serviceOptions = ref([])
+const areaStationList = ref([])
+const areaStationMap = ref({})
+const storeMap = ref({})
+
+onMounted(() => {
+    getServiceList()
+    getAreaStationList()
+    handleSearch()
+})
+
+const getServiceList = () => {
+    listServiceOnStore().then(res => {
+        serviceOptions.value = res.data || []
+    })
+}
+
+const getAreaStationList = () => {
+    listAreaStationOnStore().then(res => {
+        const list = res.data || []
+        areaStationList.value = list
+        const map = {}
+        for (const item of list) {
+            if (item && item.id !== undefined && item.id !== null) map[item.id] = item
+        }
+        areaStationMap.value = map
+    })
+}
+
+const getCityDistrictText = (row) => {
+    if (!row) return { city: '', district: '' }
+    const map = areaStationMap.value || {}
+    let stationId = row.site
+    if (!map[stationId] && row.store) {
+        const store = (storeMap.value || {})[row.store]
+        if (store?.site) stationId = store.site
+    }
+    if (!stationId) return { city: '', district: '' }
+
+    const station = map[stationId]
+    if (!station) return { city: '', district: '' }
+
+    const parent = station.parentId ? map[station.parentId] : undefined
+    if (!parent) return { city: station.name || '', district: '' }
+
+    if (parent.type === 0) return { city: parent.name || '', district: '' }
+    if (parent.type === 1) {
+        const city = parent.parentId ? map[parent.parentId] : undefined
+        return { city: city?.name || '', district: parent.name || '' }
+    }
+
+    return { city: '', district: parent.name || '' }
+}
+
+const handleStatusTabChange = async () => {
+    pagination.current = 1
+    await nextTick()
+    handleSearch()
+}
+
+const handleSearch = () => {
+    loading.value = true
+    listSubOrder({
+        pageNum: pagination.current,
+        pageSize: pagination.size,
+        service: filters.service !== '' ? filters.service : undefined,
+        status: filters.status !== '' ? Number(filters.status) : undefined,
+        content: filters.content || undefined
+    }).then(res => {
+        tableData.value = res.rows || []
+        pagination.total = res.total || 0
+        loadStoresForRows(tableData.value)
+        loading.value = false
+    }).catch(() => {
+        loading.value = false
+    })
+}
+
+const loadStoresForRows = async (rows) => {
+    const map = storeMap.value || {}
+    const ids = Array.from(new Set((rows || []).map(r => r?.store).filter(Boolean)))
+    const missing = ids.filter(id => map[id] === undefined)
+    if (missing.length === 0) return
+    await Promise.all(missing.map(async (id) => {
+        try {
+            const res = await getStore(id)
+            if (res?.data) map[id] = res.data
+        } catch {
+            map[id] = null
+        }
+    }))
+    storeMap.value = { ...map }
+}
+
+const handleSizeChange = (val) => {
+    pagination.size = val
+    handleSearch()
+}
+const handleCurrentChange = (val) => {
+    pagination.current = val
+    handleSearch()
+}
+
+const getServiceName = (serviceId) => {
+    const item = serviceOptions.value.find(i => i.id === serviceId)
+    return item ? item.name : '未知服务'
+}
+
+const getServiceModeTag = (row) => {
+    const t = row?.type
+    if (t === 0 || t === '0' || t === 1 || t === '1') return '往返'
+    return ''
+}
+
+const getServiceOrderTypeTag = (row) => {
+    const t = row?.type
+    if (t === 0 || t === '0') return { label: '接', type: 'primary' }
+    if (t === 1 || t === '1') return { label: '送', type: 'success' }
+    if (t === 2 || t === '2') return { label: '单程接', type: 'primary' }
+    if (t === 3 || t === '3') return { label: '单程送', type: 'success' }
+    return null
+}
+
+const getStatusName = (status) => {
+    const map = { 0: '待派单', 1: '待接单', 2: '服务中', 3: '待商家确认', 4: '已完成', 5: '已取消' }
+    return map[status] || '未知'
+}
+
+const getStatusClass = (status) => {
+    const map = { 0: 'pending_dispatch', 1: 'pending_accept', 2: 'serving', 3: 'pending_confirm', 4: 'completed', 5: 'cancelled' }
+    return map[status] || 'pending_dispatch'
+}
+
+// 弹窗状态管理
+const detailVisible = ref(false)
+const currentOrder = ref(null)
+
+const dispatchDialogVisible = ref(false)
+const currentDispatchOrder = ref(null)
+
+const careSummaryVisible = ref(false)
+const careSummaryOrder = ref(null)
+
+const rewardDialogVisible = ref(false)
+const remarkDialogVisible = ref(false)
+const currentOperateRow = ref(null)
+
+// 详情
+const handleDetail = async (row) => {
+    const typeName = getServiceName(row?.service)
+    const isTransport = row?.mode === 1 || row?.mode === '1'
+    const typeCode = isTransport ? 'transport' : (typeName?.includes('喂') || typeName?.includes('遛') ? 'feeding' : 'washing')
+    currentOrder.value = {
+        ...row,
+        orderNo: row?.code || row?.orderCode || row?.orderNo || row?.orderNumber || row?.no || '',
+        type: row?.typeCode || row?.type || typeCode,
+        serviceItem: getServiceName(row?.service) || row?.serviceName || row?.service || '',
+        userAvatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
+        address: '某小区5号楼2单元101',
+        groupBuyPackage: '',
+        transportType: row.splitType || row.transportType,
+        detail: {
+            ...row.detail,
+            pickTime: '2024-02-05 09:30',
+            pickAddr: row.detail?.pickAddr || '北京市朝阳区某小区5号楼2单元101',
+            pickContact: '李先生',
+            pickPhone: '13812345678',
+            dropTime: '2024-02-05 18:30',
+            dropAddr: row.detail?.dropAddr || '北京市朝阳区某小区5号楼2单元101',
+            dropContact: '李先生',
+            dropPhone: '13812345678',
+            packageName: row.detail?.packageName || '精细洗护套餐A',
+            petStatus: '胆小,需安抚',
+            area: '北京市朝阳区某小区5号楼2单元101'
+        },
+        petGender: 'male',
+        petAge: '2岁',
+        petWeight: '15kg',
+        petVaccine: '已接种',
+        petSterilized: true,
+        petCharacter: '活泼好动,喜欢球类玩具',
+        petHealth: '健康良好',
+        fulfillerAvatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
+        fulfillerPhone: '13812345678',
+        fulfillerStation: '朝阳服务站',
+        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' }
+        ]
+    }
+
+    try {
+        const res = await getSubOrderInfo(row?.id)
+        const info = res?.data?.data || res?.data
+        if (info) {
+            currentOrder.value = {
+                ...(currentOrder.value || {}),
+                id: info.id,
+                orderNo: info.code || currentOrder.value?.orderNo,
+                code: info.code,
+                subOrderType: info.type,
+                status: info.status ?? currentOrder.value?.status,
+                mode: info.mode ?? currentOrder.value?.mode,
+                type: (info.mode === 1 || info.mode === '1') ? 'transport' : currentOrder.value?.type,
+                transportType: (info.mode === 1 || info.mode === '1') ? 'round' : currentOrder.value?.transportType,
+                serviceTime: info.serviceTime || currentOrder.value?.serviceTime,
+                endServiceTime: info.endServiceTime || currentOrder.value?.endServiceTime,
+                contact: info.contact || currentOrder.value?.contact,
+                contactPhoneNumber: info.contactPhoneNumber || currentOrder.value?.contactPhoneNumber,
+                toAddress: info.toAddress || currentOrder.value?.toAddress,
+                address: info.toAddress || currentOrder.value?.address,
+                price: (info.price !== undefined && info.price !== null) ? (Number(info.price) / 100) : currentOrder.value?.price,
+                fulfillerFee: (info.price !== undefined && info.price !== null) ? (Number(info.price) / 100) : currentOrder.value?.fulfillerFee,
+                merchantName: info.storeName || currentOrder.value?.merchantName,
+                platformId: info.platformId ?? currentOrder.value?.platformId,
+                groupBuyPackage: info.groupPurchasePackageName || currentOrder.value?.groupBuyPackage,
+                fulfiller: info.fulfiller ?? currentOrder.value?.fulfiller,
+                detail: {
+                    ...(currentOrder.value?.detail || {}),
+                    pickTime: info.serviceTime || currentOrder.value?.detail?.pickTime,
+                    pickAddr: info.fromAddress || currentOrder.value?.detail?.pickAddr,
+                    pickContact: info.contact || currentOrder.value?.detail?.pickContact,
+                    pickPhone: info.contactPhoneNumber || currentOrder.value?.detail?.pickPhone,
+                    dropTime: info.endServiceTime || currentOrder.value?.detail?.dropTime,
+                    dropAddr: info.toAddress || currentOrder.value?.detail?.dropAddr,
+                    dropContact: info.contact || currentOrder.value?.detail?.dropContact,
+                    dropPhone: info.contactPhoneNumber || currentOrder.value?.detail?.dropPhone,
+                    fromAddress: info.fromAddress || currentOrder.value?.detail?.fromAddress,
+                    toAddress: info.toAddress || currentOrder.value?.detail?.toAddress,
+                    area: info.toAddress || currentOrder.value?.detail?.area,
+                    fromCode: info.fromCode || currentOrder.value?.detail?.fromCode,
+                    toCode: info.toCode || currentOrder.value?.detail?.toCode
+                }
+            }
+        }
+    } catch {
+    }
+    detailVisible.value = true
+}
+
+// 取消订单
+const handleCancel = (row) => {
+    ElMessageBox.confirm('确认取消该订单吗?', '提示', { type: 'warning' })
+        .then(() => {
+            cancelSubOrder({ orderId: row?.id }).then(() => {
+                ElMessage.success('订单已取消')
+                handleSearch()
+            })
+        })
+}
+
+// 派单
+const openDispatchDialog = (row) => {
+    const typeName = getServiceName(row?.service)
+    const isTransport = row?.mode === 1 || row?.mode === '1'
+    const typeCode = isTransport ? 'transport' : (typeName?.includes('喂') || typeName?.includes('遛') ? 'feeding' : 'washing')
+
+    const t = row?.subOrderType ?? row?.type
+    const transportType = (t === 0 || t === '0' || t === 1 || t === '1') ? 'round' : ((t === 2 || t === '2') ? 'pick' : ((t === 3 || t === '3') ? 'drop' : (row?.splitType || row?.transportType)))
+
+    const toAddress = row?.toAddress || ''
+    const pickAddr = isTransport ? toAddress : ''
+    const dropAddr = isTransport ? toAddress : ''
+    const address = isTransport ? '' : toAddress
+
+    const orderObj = {
+        id: row.id,
+        typeCode,
+        transportType,
+        time: row.serviceTime || row.appointTime || row.createTime,
+        status: row.status,
+        address,
+        pickAddr,
+        dropAddr,
+        riderId: row.riderId || row.fulfiller || null
+    }
+    currentDispatchOrder.value = orderObj
+    dispatchDialogVisible.value = true
+}
+
+const handleDispatchSubmit = (payload) => {
+    if (!currentDispatchOrder.value) return
+    const priceFen = Math.round(Number(payload.fee || 0) * 100)
+    dispatchSubOrder({
+        orderId: currentDispatchOrder.value.id,
+        fulfiller: payload.riderId,
+        price: priceFen
+    }).then(() => {
+        ElMessage.success('派单成功')
+        const row = tableData.value.find(r => r.id === currentDispatchOrder.value.id)
+        if (row) {
+            row.status = 1
+            row.fulfillerName = payload.riderName || 'Unknown'
+            row.price = payload.fee
+        }
+        handleSearch()
+    })
+}
+
+// 护理小结
+const openCareSummary = (row) => {
+    careSummaryOrder.value = {
+        ...row,
+        petAge: '3岁',
+        petGender: 'male',
+        petTags: ['易过敏', '胆小'],
+        petWeight: '30 kg',
+        petPersonality: '活泼,超级粘人,喜欢玩球',
+        homeTime: '2023-01-01',
+        houseType: '电梯',
+        entryMethod: '密码开门',
+        entryDetail: '密码: 123456 (仅限服务期间使用)',
+        healthStatus: '健康',
+        vaccineImg: 'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg',
+        allergy: '海鲜'
+    }
+    careSummaryVisible.value = true
+}
+
+const saveCareSummary = (text) => {
+    if (careSummaryOrder.value) {
+        careSummaryOrder.value.careSummary = text
+        ElMessage.success('护理小结已保存')
+        handleSearch()
+    }
+}
+
+// 奖惩
+const openRewardDialog = (row) => {
+    currentOperateRow.value = row
+    rewardDialogVisible.value = true
+}
+const handleRewardSubmit = (form) => {
+    ElMessage.success(`操作成功:${form.type === 'reward' ? '奖励' : '惩罚'}已执行`)
+}
+
+// 备注
+const openRemarkDialog = (row) => {
+    currentOperateRow.value = row
+    remarkDialogVisible.value = true
+}
+const handleRemarkSubmit = (text) => {
+    if (currentOperateRow.value) {
+        currentOperateRow.value.remark = text
+        ElMessage.success('备注已更新')
+    }
+}
+
+// 更多操作
+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 = 4
+                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;
+}
+
+.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;
+}
+
+.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;
+}
+
+.el-dropdown-link {
+    cursor: pointer;
+    color: #409eff;
+    font-size: 12px;
+    display: flex;
+    align-items: center;
+    line-height: 1;
+    height: 24px;
+}
+</style>

+ 392 - 125
src/views/order/purchase/index.vue

@@ -21,63 +21,48 @@
                         <span>服务门店 (平台代下单)</span>
                       </div>
                     </template>
-                    <PageSelect
-                      v-model="form.merchantId"
-                      placeholder="请选择商户门店"
-                      size="large"
-                      style="width: 100%"
-                      :options="merchantOptions"
-                      :total="storeTotal"
-                      :page-size="5"
-                      @page-change="handleStorePageChange"
-                      @update:modelValue="handleStoreChange"
-                    />
+                    <PageSelect v-model="form.merchantId" placeholder="请选择商户门店" size="large" style="width: 100%"
+                      :options="merchantOptions" :total="storeTotal" :page-size="5" @page-change="handleStorePageChange"
+                      @update:modelValue="handleStoreChange" />
                   </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;">
+                      <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>
+                        <el-button type="primary" plain size="small" @click="openAddUser" icon="Plus"
+                          style="margin-left: 15px;">添加用户</el-button>
                       </div>
                     </template>
-                    <PageSelect
-                      v-model="form.userId"
-                      placeholder="搜索姓名/手机号"
-                      size="large"
-                      style="width: 100%"
-                      :options="userSelectOptions"
-                      :total="userTotal"
-                      :page-size="5"
-                      :filter-method="searchUser"
-                      @page-change="handleUserPageChange"
-                      @update:modelValue="handleUserChange"
-                    />
+                    <PageSelect v-model="form.userId" placeholder="搜索姓名/手机号" size="large" style="width: 100%"
+                      :options="userSelectOptions" :total="userTotal" :page-size="5" :filter-method="searchUser"
+                      @page-change="handleUserPageChange" @update:modelValue="handleUserChange" />
                   </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 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 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>
+                    <el-icon :size="24">
+                      <Plus />
+                    </el-icon>
                     <span style="font-size: 15px; font-weight: bold;">新增宠物</span>
                   </div>
                 </div>
@@ -88,26 +73,30 @@
 
         <!-- 2. 服务类型选择 -->
         <div class="type-selection" v-if="form.merchantId">
-          <div
-            v-for="item in availableServices"
-            :key="item.id"
-            class="type-card"
+          <div v-for="item in availableServices" :key="item.id" class="type-card"
             :class="[getServiceType(item.name), { active: form.serviceId === item.id }]"
-            @click="handleServiceChange(item)"
-          >
+            @click="handleServiceChange(item)">
             <div class="icon-box">
-              <img v-if="item.icon && (item.icon.startsWith('http') || item.icon.startsWith('//') || item.icon.startsWith('/profile'))" :src="item.icon" class="service-icon-img" />
-              <el-icon v-else-if="item.icon"><component :is="item.icon" /></el-icon>
-              <el-icon v-else><component :is="getServiceIcon(item.name)" /></el-icon>
+              <img
+                v-if="item.icon && (item.icon.startsWith('http') || item.icon.startsWith('//') || item.icon.startsWith('/profile'))"
+                :src="item.icon" class="service-icon-img" />
+              <el-icon v-else-if="item.icon">
+                <component :is="item.icon" />
+              </el-icon>
+              <el-icon v-else>
+                <component :is="getServiceIcon(item.name)" />
+              </el-icon>
             </div>
             <div class="text">
               <div class="type-name">{{ item.name }}</div>
               <div class="type-desc">{{ item.remark }}</div>
             </div>
           </div>
-          <div v-if="availableServices.length === 0" style="grid-column: 1 / -1; color: #909399; text-align: center; padding: 20px;">该门店暂无可选服务</div>
+          <div v-if="availableServices.length === 0"
+            style="grid-column: 1 / -1; color: #909399; text-align: center; padding: 20px;">该门店暂无可选服务</div>
         </div>
-        <div v-else style="color: #909399; margin: 20px 0; padding: 20px; text-align: center; background: #fff; border-radius: 8px;">
+        <div v-else
+          style="color: #909399; margin: 20px 0; padding: 20px; text-align: center; background: #fff; border-radius: 8px;">
           请先在上一步中选择服务门店
         </div>
 
@@ -129,13 +118,16 @@
             <div class="divider"></div>
 
             <!-- A. 宠物接送表单 -->
-            <TransportForm v-show="form.type === 'transport'" :transport-data="form.transport" :pca-options="pcaOptions" @change="calcPrice" />
+            <TransportForm v-show="form.type === 'transport'" :transport-data="form.transport" :pca-options="pcaOptions"
+              @change="calcPrice" />
 
             <!-- B. 上门喂遛表单 -->
-            <FeedingForm v-show="form.type === 'feeding'" :feeding-data="form.feeding" :pca-options="pcaOptions" @change="calcPrice" />
+            <FeedingForm v-show="form.type === 'feeding'" :feeding-data="form.feeding" :pca-options="pcaOptions"
+              @change="calcPrice" />
 
             <!-- C. 上门洗护表单 -->
-            <WashingForm v-show="form.type === 'washing'" :washing-data="form.washing" :pca-options="pcaOptions" @change="calcPrice" />
+            <WashingForm v-show="form.type === 'washing'" :washing-data="form.washing" :pca-options="pcaOptions"
+              @change="calcPrice" />
 
           </div>
         </el-card>
@@ -174,9 +166,12 @@
 
               <!-- 接送预览 -->
               <div v-if="form.type === 'transport'" class="preview-detail">
-                <div>{{ form.transport.subType === 'round' ? '往返接送' : (form.transport.subType === 'pick' ? '单程接' : '单程送') }}</div>
+                <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 class="minor" v-if="form.transport.subType !== 'pick'">送: {{ form.transport.dropTime ?
+                  formatTime(form.transport.dropTime) : '未选' }}</div>
               </div>
             </div>
 
@@ -195,7 +190,8 @@
     <!-- Dialogs -->
     <!-- Add User Dialog -->
     <AddUserDialog v-model:visible="userDialogVisible" :pca-options="pcaOptions" @success="handleUserSuccess" />
-    <AddPetDialog v-model:visible="petDialogVisible" :user-id="form.userId" :user-options="userOptions" @success="handlePetSuccess" />
+    <AddPetDialog v-model:visible="petDialogVisible" :user-id="form.userId" :user-options="userOptions"
+      @success="handlePetSuccess" />
 
   </div>
 </template>
@@ -333,7 +329,7 @@ const handleStorePageChange = (page) => {
 const handleStoreChange = (val) => {
   const store = stores.value.find(s => s.id === val)
   if (store && store.services) {
-    if(!store.services.includes(form.serviceId)) {
+    if (!store.services.includes(form.serviceId)) {
       form.serviceId = ''
       form.type = ''
       form.mode = undefined
@@ -362,7 +358,8 @@ const getServiceIcon = (name) => {
 const handleServiceChange = (item) => {
   form.serviceId = item.id
   form.mode = item.mode
-  const sysType = getServiceType(item.name)
+  const isRouteMode = item.mode === 1 || item.mode === '1'
+  const sysType = isRouteMode ? 'transport' : getServiceType(item.name)
   form.type = sysType
   calcPrice(sysType)
 }
@@ -383,7 +380,7 @@ const calcPrice = (type) => {
 
   // Always use Base Logic for "Order Value", regardless of package
   if (type === 'transport') {
-    if(data.subType === 'round') {
+    if (data.subType === 'round') {
       data.pickPrice = base
       data.dropPrice = base
     } else if (data.subType === 'pick') {
@@ -407,7 +404,7 @@ const handleUserSuccess = (newUser) => {
   // 重新获取列表
   userQuery.pageNum = 1
   fetchUsers()
-  
+
   if (newUser && newUser.id) {
     // 后端如果直接返回了用户信息或主键,尝试将其加入列表中并选中
     const exists = userOptions.value.find(u => u.id === newUser.id)
@@ -479,7 +476,7 @@ const selectedPkgName = computed(() => {
 
 
 const canSubmit = computed(() => {
-  if(!form.merchantId || !form.userId || !form.petId || !form.serviceId) return false
+  if (!form.merchantId || !form.userId || !form.petId || !form.serviceId) return false
   return true
 })
 
@@ -512,19 +509,49 @@ const handleUserChange = (val) => {
 }
 const getStepTitle = (mode, type) => {
   if (mode === 1 || mode === '1') return '填写接送路线与时间'
-  if (mode === 0 || mode === '0') return '选择套餐与服务的细则'
-  // 兼容兜底方案(当后端数据未返回 mode 时)
-  const map = { transport: '填写接送路线与时间', feeding: '选择套餐与服务的细则', washing: '选择套餐与服务的细则' }
-  return map[type] || ''
+  return '选择套餐与服务的细则'
 }
 const getTypeName = (type) => {
   const map = { transport: '宠物接送', feeding: '上门喂遛', washing: '上门洗护' }
   return map[type]
 }
 const formatTime = (time) => {
-  if(!time) return ''
+  if (!time) return ''
   const d = new Date(time)
-  return `${d.getMonth()+1}-${d.getDate()} ${d.getHours()}:${d.getMinutes() < 10 ? '0'+d.getMinutes() : d.getMinutes()}`
+  return `${d.getMonth() + 1}-${d.getDate()} ${d.getHours()}:${d.getMinutes() < 10 ? '0' + d.getMinutes() : d.getMinutes()}`
+}
+
+const resetForm = () => {
+  form.merchantId = ''
+  form.userId = ''
+  form.petId = ''
+  form.serviceId = ''
+  form.type = ''
+  form.mode = undefined
+  form.groupBuyPackage = ''
+
+  form.transport = {
+    pkgId: '',
+    price: 0,
+    pickPrice: 35,
+    dropPrice: 35,
+    subType: 'round',
+    pickStartRegion: [], pickStartDetail: '', pickEndRegion: [], pickEndDetail: '', pickContact: '', pickPhone: '', pickTime: '',
+    dropStartRegion: [], dropStartDetail: '', dropEndRegion: [], dropEndDetail: '', dropContact: '', dropPhone: '', dropTime: ''
+  }
+  form.feeding = {
+    pkgId: '', price: 68,
+    appointments: [{ startTime: '', endTime: '' }],
+    region: [], addressDetail: '',
+    count: 1, dates: [], area: '', itemLoc: '', cleanLoc: '', foodAmount: '', other: ''
+  }
+  form.washing = {
+    pkgId: '', price: 88,
+    appointments: [{ startTime: '', endTime: '' }],
+    region: [], addressDetail: '',
+    time: '', petStatus: '', cleanLoc: '', toolLoc: '', other: ''
+  }
+  currentPets.value = []
 }
 
 const handleSubmit = async () => {
@@ -537,7 +564,7 @@ const handleSubmit = async () => {
 
     let subOrders = []
     const baseMode = form.mode || 0
-    
+
     // 获取默认客户联系方式
     const userObj = userOptions.value.find(u => u.id === form.userId)
     const defaultContact = userObj?.name || ''
@@ -545,10 +572,10 @@ const handleSubmit = async () => {
 
     if (form.type === 'transport') {
       const td = form.transport
-      const createTransportSubOrder = (time, startRegion, startDetail, endRegion, endDetail, contact, phone) => {
+      const createTransportSubOrder = (orderType, time, startRegion, startDetail, endRegion, endDetail, contact, phone) => {
         return {
           mode: baseMode,
-          type: 0,
+          type: orderType,
           contact: contact || defaultContact,
           contactPhoneNumber: phone || defaultPhone,
           serviceTime: time || '',
@@ -563,12 +590,14 @@ const handleSubmit = async () => {
       // 接送单:往返算两个,接/送分别算一个
       if (td.subType === 'round' || td.subType === 'pick') {
         subOrders.push(createTransportSubOrder(
+          td.subType === 'round' ? 0 : 2,
           td.pickTime, td.pickStartRegion, td.pickStartDetail, td.pickEndRegion, td.pickEndDetail, td.pickContact, td.pickPhone
         ))
       }
-      
+
       if (td.subType === 'round' || td.subType === 'drop') {
         subOrders.push(createTransportSubOrder(
+          td.subType === 'round' ? 1 : 3,
           td.dropTime, td.dropStartRegion, td.dropStartDetail, td.dropEndRegion, td.dropEndDetail, td.dropContact, td.dropPhone
         ))
       }
@@ -577,11 +606,10 @@ const handleSubmit = async () => {
       const hd = form[form.type]
       let code = hd.region && hd.region.length > 0 ? hd.region[hd.region.length - 1] : ''
       let address = hd.addressDetail || ''
-      
+
       const createHomeSubOrder = (startTime, endTime) => {
         return {
           mode: baseMode,
-          type: 0,
           contact: defaultContact,
           contactPhoneNumber: defaultPhone,
           serviceTime: startTime || hd.time || '',
@@ -604,7 +632,7 @@ const handleSubmit = async () => {
 
     const payload = {
       store: form.merchantId,
-      storeSite: form.merchantId, // 若后端有区分storeSite可以稍作调整,此处暂时一样
+      storeSite: storeObj.site,
       customer: form.userId,
       pet: form.petId,
       groupPurchasePackageName: form.groupBuyPackage || '',
@@ -616,10 +644,12 @@ const handleSubmit = async () => {
 
     const res = await createOrder(payload)
     if (res && res.code === 200) {
-      ElMessage.success('下单成功!订单号:' + (res.data || res.msg || 'ORD' + new Date().getTime()))
+      ElMessage.success('下单成功')
+      resetForm()
     } else {
       // 如果没有抛异常,走这里
-      ElMessage.success('下单成功!订单号:' + (res?.data || res?.msg || 'ORD' + new Date().getTime()))
+      ElMessage.success('下单成功')
+      resetForm()
     }
   } catch (error) {
     console.error('Create order error: ', error)
@@ -637,19 +667,57 @@ onMounted(() => {
 </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; }
+.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; }
+.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;
+}
 
-.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;
+  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;
 }
-.base-form .el-form-item { margin-bottom: 18px; }
 
 /* Pet Selection */
 /* Pet Selection */
@@ -659,6 +727,7 @@ onMounted(() => {
   gap: 15px;
   width: 100%;
 }
+
 .pet-card {
   border: 1px solid #8D9095;
   border-radius: 8px;
@@ -672,16 +741,19 @@ onMounted(() => {
   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);
+  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;
@@ -696,8 +768,19 @@ onMounted(() => {
   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-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;
@@ -711,6 +794,7 @@ onMounted(() => {
   height: auto;
   min-height: 70px;
 }
+
 .pet-card.add-card:hover {
   border-color: #303133;
   color: #303133;
@@ -719,30 +803,103 @@ onMounted(() => {
 }
 
 /* Type Selection */
-.type-selection { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; }
+.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);
+  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: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.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;
+  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;
 }
-.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-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;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+  overflow: hidden;
+}
 
-.type-name { font-weight: bold; font-size: 16px; color: #303133; margin-bottom: 4px; }
-.type-desc { font-size: 12px; color: #909399; margin-bottom: 4px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
-.type-price { font-size: 14px; color: #f56c6c; font-weight: bold; }
+.type-price {
+  font-size: 14px;
+  color: #f56c6c;
+  font-weight: bold;
+}
 
 /* Custom Backend Icon Img */
 .service-icon-img {
@@ -753,33 +910,143 @@ onMounted(() => {
 }
 
 /* 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; }
+.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;
+  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: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; }
 
-.divider { height: 1px; background: #EBEEF5; margin: 15px 0; }
+.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;
+}
+
+.divider {
+  height: 1px;
+  background: #EBEEF5;
+  margin: 15px 0;
+}
 
 /* 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; }
+.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>

+ 1 - 1
vite.config.ts

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