Browse Source

申诉功能基本完成

Huanyi 1 week ago
parent
commit
741a5b78b2

+ 1 - 0
src/api/order/order/types.ts

@@ -20,5 +20,6 @@ export interface CreateOrderDTO {
     service: number | string;
     remark?: string;
     tenantId?: string;
+    orderCommission?: number | string;
     subOrders: CreateSubOrderDTO[];
 }

+ 9 - 1
src/api/order/subOrder/index.ts

@@ -26,7 +26,7 @@ export const listSubOrder = (query?: SubOrderQuery): AxiosPromise<{ total: numbe
     });
 };
 
-export const dispatchSubOrder = (data: { orderId: string | number; fulfiller: string | number; fulfillmentCommission: number; }) => {
+export const dispatchSubOrder = (data: { orderId: string | number; fulfiller: string | number; fulfillmentCommission: number; orderCommission: number; }) => {
     return request({
         url: '/order/subOrder/dispatch',
         method: 'put',
@@ -134,3 +134,11 @@ export const exportSubOrder = (data: { status?: number; service?: number; conten
         responseType: 'blob'
     });
 };
+
+export const activateSubOrder = (data: { id: string | number; service: string | number; fulfillmentCommission: number; }) => {
+    return request({
+        url: '/order/subOrder/activate',
+        method: 'put',
+        data
+    });
+};

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

@@ -32,6 +32,7 @@ export interface SubOrderVO {
     fulfillerName: string;
     fulfillerGender?: string;
     fulfillerStatus?: 'resting' | 'busy' | 'disabled';
+    orderCommission?: number;
     fulfillmentCommission: number;
     // 以下为可能需要用到的扩充字段以兼顾页面展现
     type?: string;

+ 74 - 0
src/api/order/subOrderAppeal/index.ts

@@ -0,0 +1,74 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { SubOrderAppealVO, SubOrderAppealQuery, SubOrderAppealForm, SubOrderAppealAudit } from './types';
+
+/**
+ * 查询子订单申诉列表
+ * @param query 查询对象
+ */
+export function listSubOrderAppeal(query: SubOrderAppealQuery): AxiosPromise<SubOrderAppealVO[]> {
+  return request({
+    url: '/order/subOrderAppeal/list',
+    method: 'get',
+    params: query
+  });
+}
+
+/**
+ * 获取子订单申诉详细信息
+ * @param id 主键
+ */
+export function getSubOrderAppeal(id: string | number): AxiosPromise<SubOrderAppealVO> {
+  return request({
+    url: '/order/subOrderAppeal/' + id,
+    method: 'get'
+  });
+}
+
+/**
+ * 新增子订单申诉
+ * @param data 申诉表单对象
+ */
+export function addSubOrderAppeal(data: SubOrderAppealForm) {
+  return request({
+    url: '/order/subOrderAppeal',
+    method: 'post',
+    data: data
+  });
+}
+
+/**
+ * 修改子订单申诉
+ * @param data 申诉表单对象
+ */
+export function updateSubOrderAppeal(data: SubOrderAppealForm) {
+  return request({
+    url: '/order/subOrderAppeal',
+    method: 'put',
+    data: data
+  });
+}
+
+/**
+ * 删除子订单申诉
+ * @param ids 主键串
+ */
+export function delSubOrderAppeal(ids: string | number | (string | number)[]) {
+  return request({
+    url: '/order/subOrderAppeal/' + ids,
+    method: 'delete'
+  });
+}
+
+/**
+ * 审核子订单申诉
+ * @param data 审核对象
+ */
+export function auditSubOrderAppeal(data: SubOrderAppealAudit) {
+  return request({
+    url: '/order/subOrderAppeal/audit',
+    method: 'put',
+    data: data
+  });
+}
+

+ 151 - 0
src/api/order/subOrderAppeal/types.ts

@@ -0,0 +1,151 @@
+export interface SubOrderAppealVO {
+  /**
+   * 序号
+   */
+  id: string | number;
+
+  /**
+   * 所属订单
+   */
+  orderId: string | number;
+
+  /**
+   * 订单编码
+   */
+  orderCode: string;
+
+  /**
+   * 实际服务
+   */
+  service: string | number;
+
+  /**
+   * 图片
+   */
+  photos: string;
+
+  /**
+   * 图片链接
+   */
+  photoUrls: string;
+
+  /**
+   * 履约佣金
+   */
+  fulfillmentCommission: number;
+
+  /**
+   * 申诉理由
+   */
+  reason: string;
+
+  /**
+   * 履约者 ID
+   */
+  fulfiller: string | number;
+
+  /**
+   * 履约者名称
+   */
+  fulfillerName: string;
+
+  /**
+   * 审核状态
+   */
+  auditStatus: number;
+
+  /**
+   * 驳回理由
+   */
+  rejectReason: string;
+
+  /**
+   * 审核人
+   */
+  auditor: string | number;
+
+  /**
+   * 审核人名称
+   */
+  auditorName: string;
+
+  /**
+   * 审核时间
+   */
+  auditTime: string;
+
+  /**
+   * 创建时间
+   */
+  createTime: string;
+}
+
+export interface SubOrderAppealQuery extends PageQuery {
+  /**
+   * 所属订单编码
+   */
+  orderCode?: string;
+
+  /**
+   * 实际服务
+   */
+  service?: string | number;
+
+  /**
+   * 申诉理由
+   */
+  reason?: string;
+
+  /**
+   * 履约者名称
+   */
+  fulfillerName?: string;
+}
+
+export interface SubOrderAppealForm {
+  /**
+   * 序号
+   */
+  id?: string | number;
+
+  /**
+   * 所属订单编码
+   */
+  orderCode?: string;
+
+  /**
+   * 实际服务
+   */
+  service?: string | number;
+
+  /**
+   * 图片
+   */
+  photos?: string;
+
+  /**
+   * 履约佣金
+   */
+  fulfillmentCommission?: number;
+
+  /**
+   * 申诉理由
+   */
+  reason?: string;
+
+  /**
+   * 履约者 ID
+   */
+  fulfiller?: string | number;
+
+  /**
+   * 履约者名称
+   */
+  fulfillerName?: string;
+}
+
+export interface SubOrderAppealAudit {
+  id: string | number;
+  result: number;
+  reason?: string;
+}

+ 1 - 0
src/assets/icons/svg/appeal-management.svg

@@ -0,0 +1 @@
+<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1024" height="1024"><path d="M896 832H128c-17.7 0-32 14.3-32 32s14.3 32 32 32h768c17.7 0 32-14.3 32-32s-14.3-32-32-32zM324.3 646.1L169.8 491.6c-12.5-12.5-12.5-32.8 0-45.3l160.4-160.4c12.5-12.5 32.8-12.5 45.3 0l154.5 154.5c12.5 12.5 12.5 32.8 0 45.3L369.6 646.1c-12.5 12.5-32.8 12.5-45.3 0z m574.4-486.2c-25-25-65.5-25-90.5 0L512 456.1l90.5 90.5 296.2-296.2c25-25.1 25-65.6 0-90.5zM453.1 515.1l-63.6-63.6-212.1 212.1 63.6 63.6z"></path></svg>

+ 710 - 0
src/components/DispatchDialog/index.vue

@@ -0,0 +1,710 @@
+<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.pickAddr">
+                                <span class="tag pick">取</span> {{ order.pickAddr }}
+                            </div>
+                            <div class="row-addr" :title="order.dropAddr">
+                                <span class="tag drop">送</span> {{ 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 class="card-right" style="display: flex; align-items: center; gap: 10px; padding-left: 20px;">
+                        <el-button type="primary" size="small" plain round icon="User" @click="openCustomerDetail" :loading="orderInfoLoading">用户档案</el-button>
+                        <el-button type="success" size="small" plain round @click="openPetDetail" :loading="orderInfoLoading" style="margin-left: 0;">宠物档案</el-button>
+                    </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>
+                                <dict-tag :options="sys_user_sex" :value="currentRider.gender" />
+                                <el-tag v-if="currentRider.status" size="small" :type="getStatusType(currentRider.status)" effect="plain">
+                                    {{ getStatusText(currentRider.status) }}
+                                </el-tag>
+                            </div>
+                        </div>
+
+                        <div class="row-2 categories-row"
+                            style="margin-top: 6px; display:flex; gap:4px; flex-wrap:wrap;">
+                            <el-tag v-for="typeId in (currentRider.serviceTypes ? String(currentRider.serviceTypes).split(',') : [])" :key="typeId" size="small"
+                                type="primary" effect="plain">{{ getServiceTypeText(typeId) }}</el-tag>
+                        </div>
+                        <div class="row-3 time-row" style="margin-top: 4px;">
+                            <span class="last-time">下一单: {{ currentRider.nextOrderTime || '-' }}</span>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <!-- Middle: Rider Selection -->
+            <div class="dispatch-rider-select">
+                <div class="select-header">
+                    <span class="tit">选择履约者</span>
+                    <el-input v-model="dispatchSearchQuery" placeholder="搜索履约者姓名/手机号" prefix-icon="Search" clearable
+                        style="width: 240px" />
+                </div>
+
+                <div class="rider-grid-wrapper">
+                    <el-scrollbar 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>
+                                            <dict-tag :options="sys_user_sex" :value="rider.gender" />
+                                        </div>
+                                        <el-tag v-if="rider.status" size="small" :type="getStatusType(rider.status)" effect="plain">
+                                            {{ getStatusText(rider.status) }}
+                                        </el-tag>
+                                    </div>
+
+                                    <div class="row-2 categories-row"
+                                        style="margin-top: 6px; display:flex; gap:4px; flex-wrap:wrap;">
+                                        <el-tag v-for="typeId in (rider.serviceTypes ? String(rider.serviceTypes).split(',') : [])" :key="typeId" size="small"
+                                            type="primary" effect="plain">{{ getServiceTypeText(typeId) }}</el-tag>
+                                    </div>
+                                    <div class="row-3 time-row" style="margin-top: 4px">
+                                        <span class="last-time">下一单: {{ rider.nextOrderTime || '-' }}</span>
+                                    </div>
+                                </div>
+
+                                <!-- Selected Check -->
+                                <div class="selected-mark" v-if="selectedRiderId === rider.id">
+                                    <el-icon>
+                                        <Check />
+                                    </el-icon>
+                                </div>
+                            </div>
+
+                            <div v-if="filteredDispatchRiders.length === 0" class="empty-text">暂无符合条件的履约者</div>
+                        </div>
+                    </el-scrollbar>
+                </div>
+
+                <div 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>
+
+            <div class="dispatch-footer">
+                <div class="fee-inputs" style="display: flex; align-items: center;">
+                    <div class="fee-input">
+                        <span class="label">履约佣金:</span>
+                        <el-input-number v-model="dispatchFee" :min="0" :precision="2" :step="10" placeholder="请输入"
+                            style="width: 130px;" />
+                        <span class="unit">元</span>
+                    </div>
+                    <div class="fee-input" style="margin-left: 20px;">
+                        <span class="label">订单佣金:</span>
+                        <el-input-number v-model="orderCommission" :min="0" :precision="2" :step="10" placeholder="请输入"
+                            style="width: 130px;" />
+                        <span class="unit">元</span>
+                    </div>
+                </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>
+
+    <CustomerDetailDrawer v-model:visible="customerDialogVisible" :customer-id="customerId" :area-station-list="areaStationList" />
+    <PetDetailDrawer v-model:visible="petDialogVisible" :pet-id="petId" />
+</template>
+
+<script setup>
+import { ref, computed, watch, getCurrentInstance, toRefs } from 'vue'
+import { ElMessage } from 'element-plus'
+import { pageFulfillerOnOrder } from '@/api/fulfiller/pool'
+import { listAllTag } from '@/api/fulfiller/tag'
+import { getSubOrderInfo } from '@/api/order/subOrder/index'
+import { listAllService } from '@/api/service/list/index'
+import { listAreaStation } from '@/api/system/areaStation/index'
+import CustomerDetailDrawer from '@/components/CustomerDetailDrawer/index.vue'
+import PetDetailDrawer from '@/components/PetDetailDrawer/index.vue'
+
+const props = defineProps({
+    visible: Boolean,
+    order: Object
+})
+const emit = defineEmits(['update:visible', 'submit'])
+
+const { proxy } = getCurrentInstance();
+const { sys_user_sex } = toRefs(proxy.useDict('sys_user_sex'));
+
+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 orderCommission = ref(0)
+
+const customerDialogVisible = ref(false)
+const petDialogVisible = ref(false)
+const customerId = ref(null)
+const petId = ref(null)
+const orderInfoLoading = ref(false)
+
+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 serviceOptions = ref([])
+const loadServiceOptions = async () => {
+    if (serviceOptions.value.length > 0) return
+    try {
+        const res = await listAllService()
+        serviceOptions.value = res?.data || []
+    } catch { /* ignore */ }
+}
+
+const areaStationList = ref([])
+const loadAreaStationList = async () => {
+    if (areaStationList.value.length > 0) return
+    try {
+        const res = await listAreaStation()
+        areaStationList.value = res.data || []
+    } catch { /* ignore */ }
+}
+
+const getServiceTypeText = (id) => {
+    const s = serviceOptions.value.find(item => String(item.id) === String(id))
+    return s ? s.name : String(id)
+}
+
+const loadRiders = async () => {
+    try {
+        const res = await pageFulfillerOnOrder({
+            content: dispatchSearchQuery.value || undefined,
+            pageNum: pageNum.value,
+            pageSize: pageSize.value,
+            service: props.order?.service
+        })
+        const list = res?.rows || []
+        ridersList.value = list.map(r => ({
+            ...r,
+            nextOrderTime: r.nextOrderTime || '-',
+            gender: r.gender ?? r.sex
+        }))
+        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
+        // price 单位为分,转成元显示
+        dispatchFee.value = props.order?.fulfillmentCommission ? Number((props.order.fulfillmentCommission / 100).toFixed(2)) : 0
+        orderCommission.value = props.order?.orderCommission ? Number((props.order.orderCommission / 100).toFixed(2)) : 0
+        if (props.order?.riderId) {
+            currentRider.value = {
+                id: props.order.riderId,
+                gender: props.order.riderGender ?? props.order.riderSex
+            }
+        }
+        pageNum.value = 1
+        loadAllTags()
+        loadServiceOptions()
+        loadAreaStationList()
+        loadRiders()
+
+        // 获取订单详细信息
+        customerId.value = null
+        petId.value = null
+        orderInfoLoading.value = true
+        getSubOrderInfo(props.order.id).then((res) => {
+            if(res.data) {
+                // 如果 usrCustomer / usrPet 是对象则取其 id,如果是 ID 直接取
+                customerId.value = res.data.usrCustomer?.id || res.data.usrCustomer
+                petId.value = res.data.usrPet?.id || res.data.usrPet
+                
+                // 接到详情后,把真实的金额放进去(后端金额单位为分)
+                if (res.data.fulfillmentCommission !== undefined && res.data.fulfillmentCommission !== null) {
+                    dispatchFee.value = Number((res.data.fulfillmentCommission / 100).toFixed(2))
+                }
+                if (res.data.orderCommission !== undefined && res.data.orderCommission !== null) {
+                    orderCommission.value = Number((res.data.orderCommission / 100).toFixed(2))
+                }
+
+                // 如果已经有履约者且不是在列表中找到的,从详情中补全性别
+                if (props.order?.riderId && !currentRider.value) {
+                    currentRider.value = {
+                        id: props.order.riderId,
+                        name: res.data.fulfillerName,
+                        gender: res.data.fulfillerGender ?? res.data.fulfillerSex,
+                        status: res.data.fulfillerStatus
+                    }
+                } else if (currentRider.value && (res.data.fulfillerGender || res.data.fulfillerSex)) {
+                    currentRider.value.gender = res.data.fulfillerGender ?? res.data.fulfillerSex
+                }
+            }
+        }).catch((e) => {
+            console.error('获取订单详细信息失败', e)
+        }).finally(() => {
+            orderInfoLoading.value = false
+        })
+    }
+})
+
+const openCustomerDetail = () => {
+    if (!customerId.value) {
+        ElMessage.warning('未能获取到用户信息')
+        return
+    }
+    customerDialogVisible.value = true
+}
+
+const openPetDetail = () => {
+    if (!petId.value) {
+        ElMessage.warning('未能获取到宠物信息')
+        return
+    }
+    petDialogVisible.value = true
+}
+
+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 getStatusText = (status) => {
+    const statusMap = {
+        resting: '休息',
+        busy: '接单中',
+        disabled: '禁用'
+    }
+    return statusMap[status] || status
+}
+
+const getStatusType = (status) => {
+    const typeMap = {
+        resting: 'info',
+        busy: 'success',
+        disabled: 'danger'
+    }
+    return typeMap[status] || 'info'
+}
+
+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,
+        orderCommission: orderCommission.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>

+ 16 - 0
src/json/subOrderAppeal.json

@@ -0,0 +1,16 @@
+{
+  "AuditStatus": {
+    "0": {
+      "label": "未审核",
+      "tagType": "info"
+    },
+    "1": {
+      "label": "通过",
+      "tagType": "success"
+    },
+    "2": {
+      "label": "驳回",
+      "tagType": "danger"
+    }
+  }
+}

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

@@ -13,6 +13,7 @@ declare module 'vue' {
     Breadcrumb: typeof import('./../components/Breadcrumb/index.vue')['default']
     CustomerDetailDrawer: typeof import('./../components/CustomerDetailDrawer/index.vue')['default']
     DictTag: typeof import('./../components/DictTag/index.vue')['default']
+    DispatchDialog: typeof import('./../components/DispatchDialog/index.vue')['default']
     Editor: typeof import('./../components/Editor/index.vue')['default']
     ElAlert: typeof import('element-plus/es')['ElAlert']
     ElAutocomplete: typeof import('element-plus/es')['ElAutocomplete']

+ 418 - 0
src/views/order/appeal/index.vue

@@ -0,0 +1,418 @@
+<template>
+  <div class="app-container">
+    <div class="header-container box-card">
+      <div class="left-title">
+        <span class="title-text">订单申诉管理</span>
+      </div>
+      <div class="right-search">
+        <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
+          <el-form-item prop="orderCode">
+            <el-input v-model="queryParams.orderCode" placeholder="订单号" clearable style="width: 200px"
+              @keyup.enter="handleQuery">
+              <template #prefix>
+                <el-icon>
+                  <Search />
+                </el-icon>
+              </template>
+            </el-input>
+          </el-form-item>
+          <el-form-item prop="fulfillerName">
+            <el-input v-model="queryParams.fulfillerName" placeholder="履约者姓名" clearable style="width: 150px"
+              @keyup.enter="handleQuery">
+              <template #prefix>
+                <el-icon>
+                  <Search />
+                </el-icon>
+              </template>
+            </el-input>
+          </el-form-item>
+          <el-form-item prop="service">
+            <el-select v-model="queryParams.service" placeholder="实际服务" clearable style="width: 160px"
+              @change="handleQuery">
+              <el-option v-for="item in serviceOptions" :key="item.id" :label="item.name" :value="item.id" />
+            </el-select>
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="handleQuery">查询</el-button>
+            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+    </div>
+
+    <div class="content-container box-card">
+      <el-table v-loading="loading" :data="subOrderAppealList"
+        :header-cell-style="{ background: '#f8f9fa', color: '#606266', fontWeight: 'bold' }">
+        <el-table-column label="关联订单号" align="left" prop="orderCode" min-width="150" />
+        <el-table-column label="实际服务" align="center" prop="serviceName" min-width="120" />
+        <el-table-column label="履约者" align="center" prop="fulfillerName" min-width="100" />
+        <el-table-column label="上报图片" align="center" prop="photoUrls" min-width="120">
+          <template #default="scope">
+            <div style="display: flex; gap: 5px; justify-content: center; flex-wrap: wrap;">
+              <template v-if="scope.row.photoUrls">
+                <image-preview v-for="(url, index) in scope.row.photoUrls.split(',')" :key="index" :src="url"
+                  :width="40" :height="40" />
+              </template>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="履约佣金(元)" align="right" prop="fulfillmentCommission" width="120">
+          <template #default="scope">
+            <span>{{ (scope.row.fulfillmentCommission / 100).toFixed(2) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="提交内容" align="left" prop="reason" :show-overflow-tooltip="true" min-width="200" />
+        <el-table-column label="提交时间" align="center" prop="createTime" width="180" sortable>
+          <template #default="scope">
+            <span>{{ parseTime(scope.row.createTime) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="审核状态" align="center" prop="auditStatus" min-width="100">
+          <template #default="scope">
+            <el-tag v-if="scope.row.auditStatus !== undefined"
+              :type="subOrderAppealDict.AuditStatus[scope.row.auditStatus]?.tagType || 'info'">
+              {{ subOrderAppealDict.AuditStatus[scope.row.auditStatus]?.label || scope.row.auditStatus }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" width="160" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-button v-if="scope.row.auditStatus === 0 && checkPermi(['order:appeal:audit'])" link type="primary"
+              @click="handleAudit(scope.row)">审核</el-button>
+            <el-button link type="danger" v-hasPermi="['order:appeal:remove']"
+              @click="handleDelete(scope.row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
+        v-model:limit="queryParams.pageSize" @pagination="getList" />
+    </div>
+
+    <!-- 删除确认详情展示 -->
+    <el-dialog title="确认删除申诉" v-model="openView" width="600px" append-to-body custom-class="detail-dialog">
+      <div class="detail-container">
+        <div class="detail-item">
+          <span class="label">订单号:</span>
+          <span class="value">{{ form.orderCode }}</span>
+        </div>
+        <div class="detail-item">
+          <span class="label">实际服务:</span>
+          <span class="value">{{ form.serviceName }}</span>
+        </div>
+        <div class="detail-item">
+          <span class="label">履约者:</span>
+          <span class="value">{{ form.fulfillerName }}</span>
+        </div>
+        <div class="detail-item">
+          <span class="label">履约佣金:</span>
+          <span class="value highlighting">{{ (form.fulfillmentCommission / 100).toFixed(2) }} 元</span>
+        </div>
+        <div class="detail-item full-width">
+          <span class="label">提交时间:</span>
+          <span class="value">{{ parseTime(form.createTime) }}</span>
+        </div>
+        <div class="detail-item full-width">
+          <span class="label">申诉理由:</span>
+          <div class="reason-content">{{ form.reason }}</div>
+        </div>
+        <div class="detail-item full-width">
+          <span class="label">图片展示:</span>
+          <div class="photo-list" v-if="form.photoUrls">
+            <image-preview v-for="(url, index) in form.photoUrls.split(',')" :key="index" :src="url" :width="120"
+              :height="120" style="margin-right: 10px; margin-bottom: 10px;" />
+          </div>
+        </div>
+        <div class="detail-item">
+          <span class="label">审核状态:</span>
+          <span class="value">
+            <el-tag v-if="form.auditStatus !== undefined"
+              :type="subOrderAppealDict.AuditStatus[form.auditStatus]?.tagType || 'info'">
+              {{ subOrderAppealDict.AuditStatus[form.auditStatus]?.label || form.auditStatus }}
+            </el-tag>
+          </span>
+        </div>
+        <div class="detail-item">
+          <span class="label">审核人:</span>
+          <span class="value">{{ form.auditorName || '-' }}</span>
+        </div>
+        <div class="detail-item">
+          <span class="label">审核时间:</span>
+          <span class="value">{{ parseTime(form.auditTime) || '-' }}</span>
+        </div>
+        <div class="detail-item full-width" v-if="form.rejectReason">
+          <span class="label">驳回理由:</span>
+          <div class="reason-content">{{ form.rejectReason }}</div>
+        </div>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="danger" @click="submitDelete">确认删除</el-button>
+          <el-button @click="openView = false">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 审核对话框 -->
+    <el-dialog title="审核操作" v-model="openAudit" width="500px" append-to-body>
+      <el-form :model="auditForm" :rules="auditRules" ref="auditRef" label-width="100px">
+        <el-form-item label="审核结果" prop="result">
+          <el-radio-group v-model="auditForm.result">
+            <el-radio :label="1">通过</el-radio>
+            <el-radio :label="2">驳回</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="驳回原因" prop="reason" v-if="auditForm.result === 2">
+          <el-input v-model="auditForm.reason" type="textarea" placeholder="请输入驳回原因" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitAudit">确 定</el-button>
+          <el-button @click="openAudit = false">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="SubOrderAppeal">
+import { listSubOrderAppeal, delSubOrderAppeal, auditSubOrderAppeal } from "@/api/order/subOrderAppeal";
+import { listAllService } from "@/api/service/list";
+import { parseTime } from "@/utils/ruoyi";
+import { checkPermi } from "@/utils/permission";
+import subOrderAppealDict from "@/json/subOrderAppeal.json";
+
+const { proxy } = getCurrentInstance();
+
+const subOrderAppealList = ref([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const total = ref(0);
+const openView = ref(false);
+const openAudit = ref(false);
+const serviceOptions = ref([]);
+
+const auditForm = ref({
+  id: undefined,
+  result: 1,
+  reason: undefined
+});
+
+const auditRules = ref({
+  result: [{ required: true, message: "请选择审核结果", trigger: "change" }],
+  reason: [{ required: true, message: "请输入驳回原因", trigger: "blur" }]
+});
+
+const data = reactive({
+  form: {},
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    orderCode: undefined, // 改为 orderCode
+    fulfillerName: undefined, // 履约者名称
+    service: undefined,
+    reason: undefined,
+  },
+});
+
+const { queryParams, form } = toRefs(data);
+
+/** 查询服务列表用于下拉框 */
+function getServiceList() {
+  listAllService().then(response => {
+    serviceOptions.value = response.data;
+    // 数据记载后重新渲染列表,确保 serviceName 正确映射
+    getList();
+  });
+}
+
+/** 查询列表 */
+function getList() {
+  loading.value = true;
+  listSubOrderAppeal(queryParams.value).then(response => {
+    subOrderAppealList.value = response.rows.map(item => {
+      // 匹配服务名称
+      const serviceObj = serviceOptions.value.find(s => s.id === item.service);
+      return {
+        ...item,
+        serviceName: serviceObj ? serviceObj.name : item.service
+      };
+    });
+    total.value = response.total;
+    loading.value = false;
+  });
+}
+
+/** 搜索按钮操作 */
+function handleQuery() {
+  queryParams.value.pageNum = 1;
+  getList();
+}
+
+/** 重置按钮操作 */
+function resetQuery() {
+  proxy.resetForm("queryRef");
+  handleQuery();
+}
+
+/** 删除获取详情操作 */
+function handleDelete(row) {
+  openView.value = true;
+  form.value = { ...row };
+}
+
+/** 确认删除请求 */
+function submitDelete() {
+  delSubOrderAppeal(form.value.id).then(() => {
+    proxy.$modal.msgSuccess("删除成功");
+    openView.value = false;
+    getList();
+  });
+}
+
+/** 审核按钮操作 */
+function handleAudit(row) {
+  auditForm.value = {
+    id: row.id,
+    result: 1,
+    reason: undefined
+  };
+  openAudit.value = true;
+  if (proxy.$refs["auditRef"]) {
+    proxy.$refs["auditRef"].resetFields();
+  }
+}
+
+/** 提交审核 */
+function submitAudit() {
+  proxy.$refs["auditRef"].validate(valid => {
+    if (valid) {
+      auditSubOrderAppeal(auditForm.value).then(response => {
+        proxy.$modal.msgSuccess("审核成功");
+        openAudit.value = false;
+        getList();
+      });
+    }
+  });
+}
+
+// 初始化加载
+getServiceList();
+</script>
+
+<style lang="scss" scoped>
+.app-container {
+  padding: 20px;
+  background-color: #f5f7fa;
+  min-height: calc(100vh - 84px);
+}
+
+.box-card {
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+  margin-bottom: 20px;
+  padding: 20px;
+}
+
+.header-container {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 15px 25px;
+
+  .left-title {
+    .title-text {
+      font-size: 18px;
+      font-weight: 600;
+      color: #303133;
+    }
+  }
+
+  .right-search {
+    :deep(.el-form-item) {
+      margin-bottom: 0;
+      margin-right: 12px;
+    }
+
+    :deep(.el-input__wrapper) {
+      border-radius: 20px;
+      padding-left: 15px;
+    }
+
+    :deep(.el-select .el-input__wrapper) {
+      border-radius: 20px;
+    }
+  }
+}
+
+.content-container {
+  padding: 0;
+  overflow: hidden;
+
+  :deep(.el-table) {
+    --el-table-header-bg-color: #f8f9fa;
+    border-radius: 8px 8px 0 0;
+  }
+}
+
+.detail-container {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 20px;
+  padding: 10px;
+
+  .detail-item {
+    width: 45%;
+    display: flex;
+    align-items: center;
+
+    &.full-width {
+      width: 100%;
+      align-items: flex-start;
+      flex-direction: column;
+    }
+
+    .label {
+      color: #909399;
+      font-weight: bold;
+      margin-right: 10px;
+      white-space: nowrap;
+    }
+
+    .value {
+      color: #303133;
+
+      &.highlighting {
+        color: #f56c6c;
+        font-weight: bold;
+      }
+    }
+
+    .reason-content {
+      margin-top: 8px;
+      padding: 12px;
+      background: #f8f9fa;
+      border-radius: 4px;
+      width: 100%;
+      color: #606266;
+      line-height: 1.6;
+    }
+
+    .photo-list {
+      margin-top: 10px;
+    }
+  }
+}
+
+:deep(.el-button--primary) {
+  border-radius: 20px;
+  padding: 8px 20px;
+}
+
+:deep(.dialog-footer) {
+  text-align: center;
+  padding-top: 20px;
+}
+</style>

+ 20 - 7
src/views/order/dispatch/components/DispatchDialog.vue

@@ -133,13 +133,20 @@
                 </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 class="fee-inputs" style="display: flex; align-items: center;">
+                    <div class="fee-input">
+                        <span class="label">履约佣金:</span>
+                        <el-input-number v-model="dispatchFee" :min="0" :precision="2" :step="10" placeholder="请输入"
+                            style="width: 130px;" />
+                        <span class="unit">元</span>
+                    </div>
+                    <div class="fee-input" style="margin-left: 20px;">
+                        <span class="label">订单佣金:</span>
+                        <el-input-number v-model="orderCommission" :min="0" :precision="2" :step="10" placeholder="请输入"
+                            style="width: 130px;" />
+                        <span class="unit">元</span>
+                    </div>
                 </div>
                 <div class="btns">
                     <el-button @click="dialogVisible = false">取消</el-button>
@@ -200,6 +207,7 @@ const currentRider = ref(null)
 const dispatchSearchQuery = ref('')
 const selectedRiderId = ref(null)
 const dispatchFee = ref(0)
+const orderCommission = ref(0)
 
 const customerDialogVisible = ref(false)
 const petDialogVisible = ref(false)
@@ -272,6 +280,7 @@ watch(() => props.visible, (val) => {
         selectedRiderId.value = null
         // price 单位为分,转成元显示
         dispatchFee.value = props.order?.fulfillmentCommission ? Number((props.order.fulfillmentCommission / 100).toFixed(2)) : 0
+        orderCommission.value = props.order?.orderCommission ? Number((props.order.orderCommission / 100).toFixed(2)) : 0
         if (props.order?.riderId) {
             currentRider.value = {
                 id: props.order.riderId,
@@ -297,6 +306,9 @@ watch(() => props.visible, (val) => {
                 if (res.data.fulfillmentCommission !== undefined && res.data.fulfillmentCommission !== null) {
                     dispatchFee.value = Number((res.data.fulfillmentCommission / 100).toFixed(2))
                 }
+                if (res.data.orderCommission !== undefined && res.data.orderCommission !== null) {
+                    orderCommission.value = Number((res.data.orderCommission / 100).toFixed(2))
+                }
 
                 // 如果已经有履约者且不是在列表中找到的,从详情中补全性别
                 if (props.order?.riderId && !currentRider.value) {
@@ -395,7 +407,8 @@ const handleDispatchSubmit = () => {
     emit('submit', {
         riderId: rider.id,
         riderName: rider.name,
-        fee: dispatchFee.value
+        fee: dispatchFee.value,
+        orderCommission: orderCommission.value
     })
     dialogVisible.value = false
 }

+ 8 - 4
src/views/order/dispatch/index.vue

@@ -96,7 +96,7 @@ import { getFulfillerGps } from '@/api/fulfiller/fulfiller/index'
 import OrderListPanel from './components/OrderListPanel.vue';
 import RiderListPanel from './components/RiderListPanel.vue';
 import RiderOrdersDialog from './components/RiderOrdersDialog.vue';
-import DispatchDialog from './components/DispatchDialog.vue';
+import DispatchDialog from '@/components/DispatchDialog/index.vue';
 
 // Mock Data
 import dispatchMockData from '@/mock/dispatch.json';
@@ -542,7 +542,9 @@ const openDispatchDialog = (order) => {
     dropAddr,
     service: order.service,
     riderId: order.riderId || order.fulfiller || null,
-    riderGender: order.fulfillerGender
+    riderGender: order.fulfillerGender,
+    fulfillmentCommission: order.fulfillmentCommission,
+    orderCommission: order.orderCommission
   };
   currentDispatchOrder.value = orderObj;
   dispatchDialogVisible.value = true;
@@ -551,11 +553,13 @@ const openDispatchDialog = (order) => {
 const handleDispatchSubmit = async (data) => {
   if (!currentDispatchOrder.value) return;
   try {
-    const priceFen = Math.round(Number(data.fee || 0) * 100);
+    const fulfillmentCommissionFen = Math.round(Number(data.fee || 0) * 100);
+    const orderCommissionFen = Math.round(Number(data.orderCommission || 0) * 100);
     await dispatchSubOrder({
       orderId: currentDispatchOrder.value.id,
       fulfiller: data.riderId,
-      fulfillmentCommission: priceFen
+      fulfillmentCommission: fulfillmentCommissionFen,
+      orderCommission: orderCommissionFen
     });
     ElMessage.success('派单成功');
     dispatchDialogVisible.value = false;

+ 20 - 7
src/views/order/orderList/components/DispatchDialog.vue

@@ -133,13 +133,20 @@
                 </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 class="fee-inputs" style="display: flex; align-items: center;">
+                    <div class="fee-input">
+                        <span class="label">履约佣金:</span>
+                        <el-input-number v-model="dispatchFee" :min="0" :precision="2" :step="10" placeholder="请输入"
+                            style="width: 130px;" />
+                        <span class="unit">元</span>
+                    </div>
+                    <div class="fee-input" style="margin-left: 20px;">
+                        <span class="label">订单佣金:</span>
+                        <el-input-number v-model="orderCommission" :min="0" :precision="2" :step="10" placeholder="请输入"
+                            style="width: 130px;" />
+                        <span class="unit">元</span>
+                    </div>
                 </div>
                 <div class="btns">
                     <el-button @click="dialogVisible = false">取消</el-button>
@@ -196,6 +203,7 @@ const currentRider = ref(null)
 const dispatchSearchQuery = ref('')
 const selectedRiderId = ref(null)
 const dispatchFee = ref(0)
+const orderCommission = ref(0)
 
 const customerDialogVisible = ref(false)
 const petDialogVisible = ref(false)
@@ -274,6 +282,7 @@ watch(() => props.visible, (val) => {
         selectedRiderId.value = null
         // price 单位为分,转成元显示
         dispatchFee.value = props.order?.fulfillmentCommission ? Number((props.order.fulfillmentCommission / 100).toFixed(2)) : 0
+        orderCommission.value = props.order?.orderCommission ? Number((props.order.orderCommission / 100).toFixed(2)) : 0
         if (props.order?.riderId) {
             currentRider.value = {
                 id: props.order.riderId,
@@ -300,6 +309,9 @@ watch(() => props.visible, (val) => {
                 if (res.data.fulfillmentCommission !== undefined && res.data.fulfillmentCommission !== null) {
                     dispatchFee.value = Number((res.data.fulfillmentCommission / 100).toFixed(2))
                 }
+                if (res.data.orderCommission !== undefined && res.data.orderCommission !== null) {
+                    orderCommission.value = Number((res.data.orderCommission / 100).toFixed(2))
+                }
 
                 // 如果已经有履约者且不是在列表中找到的,从详情中补全性别
                 if (props.order?.riderId && !currentRider.value) {
@@ -398,7 +410,8 @@ const handleDispatchSubmit = () => {
     emit('submit', {
         riderId: rider.id,
         riderName: rider.name,
-        fee: dispatchFee.value
+        fee: dispatchFee.value,
+        orderCommission: orderCommission.value
     })
     dialogVisible.value = false
 }

+ 13 - 10
src/views/order/orderList/components/OrderDetailDrawer.vue

@@ -82,18 +82,18 @@
                                     </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>
+                                    <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.petBreed || '未知'
+                            <el-descriptions-item label="宠物品种">{{ order.petBreed || '-'
                             }}</el-descriptions-item>
-                            <el-descriptions-item label="疫苗状态"><span style="color:#67c23a">{{ order.petVaccine || '未知'
+                            <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-item label="性格特点">{{ order.petPersonality || order.petCharacter || '-' }}</el-descriptions-item>
+                            <el-descriptions-item label="健康状况">{{ order.petHealth || '-' }}</el-descriptions-item>
                         </el-descriptions>
                     </div>
 
@@ -139,7 +139,7 @@
                                         ({{ 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">
+                                    <el-descriptions-item label="履约佣金" label-class-name="money-label">
                                         <span style="color:#f56c6c; font-weight:bold;">¥ {{ order.fulfillerFee }}</span>
                                     </el-descriptions-item>
 
@@ -148,6 +148,9 @@
                                     <el-descriptions-item label="团购套餐">{{ order.groupBuyPackage || '未使用团购套餐'
                                     }}</el-descriptions-item>
                                     <el-descriptions-item label="创建时间">{{ order.createTime }}</el-descriptions-item>
+                                    <el-descriptions-item label="订单佣金" label-class-name="money-label">
+                                        <span style="color:#f56c6c; font-weight:bold;">¥ {{ order.orderCommission || 0 }}</span>
+                                    </el-descriptions-item>
 
                                     <el-descriptions-item label="订单备注" :span="3">
                                         {{ order.remark || '暂无备注' }}
@@ -206,9 +209,9 @@
                                         <el-tag size="small" type="primary" effect="plain" round>Lv1 普通</el-tag>
                                     </div>
                                     <div class="f-row2">
-                                        <span>联系电话:{{ order.fulfillerPhone || '138****0000' }}</span>
+                                        <span>联系电话:{{ order.fulfillerPhone || '-' }}</span>
                                         <span class="sep">|</span>
-                                        <span>归属区域:{{ order.fulfillerStation || '朝阳一站' }}</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;">
@@ -414,7 +417,7 @@ const loadPetAndCustomer = async (order) => {
                     ? (Number(pet.isSterilized) === 1)
                     : next.petSterilized
                 next.petVaccine = pet.vaccineStatus ?? next.petVaccine
-                next.petCharacter = pet.personality ?? next.petCharacter
+                next.petPersonality = pet.personality ?? next.petPersonality
                 next.petHealth = pet.healthStatus ?? next.petHealth
             }
         } catch {

+ 106 - 34
src/views/order/orderList/index.vue

@@ -105,6 +105,15 @@
           </template>
         </el-table-column>
 
+        <el-table-column label="订单佣金" width="100">
+          <template #default="{ row }">
+            <span v-if="row.orderCommission !== null && row.orderCommission !== undefined"
+              style="color: #f56c6c; font-weight: bold;">¥{{ row.orderCommission / 100.0
+              }}</span>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+
         <el-table-column label="履约信息" width="140">
           <template #default="{ row }">
             <div v-if="row.fulfillerName" class="fulfiller-info">
@@ -120,6 +129,7 @@
           <template #default="{ row }">
             <div class="op-cell">
               <el-button link type="primary" size="small" @click="handleDetail(row)" v-hasPermi="['order:orderList:query']">详情</el-button>
+              <el-button v-if="row.serviceFlag === false" link type="success" size="small" @click="openActivateDialog(row)" v-hasPermi="['order:orderList:activate']">开通服务</el-button>
               <el-button v-if="row.status === 0" link type="success" size="small"
                 @click="openDispatchDialog(row)" v-hasPermi="['order:orderList:dispatch']">派单</el-button>
               <el-button v-if="![0, 4].includes(row.status)" link type="warning" size="small"
@@ -182,6 +192,24 @@
         <el-button type="primary" @click="submitComplaint">确认</el-button>
       </template>
     </el-dialog>
+
+    <!-- 开通服务弹窗 -->
+    <el-dialog v-model="activateDialogVisible" title="开通服务" width="400px" append-to-body>
+      <el-form :model="activateForm" label-width="80px">
+        <el-form-item label="新的服务" required>
+          <el-select v-model="activateForm.service" placeholder="请选择新的服务" style="width: 100%">
+            <el-option v-for="item in serviceOptions" :key="item.id" :label="item.name" :value="item.id" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="履约佣金" required>
+          <el-input-number v-model="activateForm.fulfillmentCommission" :min="0" :precision="2" :step="1" placeholder="请输入履约佣金(元)" style="width: 100%" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="activateDialogVisible = false">取消</el-button>
+        <el-button type="primary" :loading="activateLoading" @click="submitActivate">确认</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
@@ -191,7 +219,7 @@ import { useRouter } from 'vue-router';
 import { ElMessage, ElMessageBox } from 'element-plus';
 import fulfillerEnums from '@/json/fulfiller.json';
 import OrderDetailDrawer from './components/OrderDetailDrawer.vue';
-import DispatchDialog from './components/DispatchDialog.vue';
+import DispatchDialog from '@/components/DispatchDialog/index.vue';
 import CareSummaryDrawer from './components/CareSummaryDrawer.vue';
 import RewardDialog from './components/RewardDialog.vue';
 import RemarkDialog from './components/RemarkDialog.vue';
@@ -204,6 +232,7 @@ import { remarkSubOrder } from '@/api/order/subOrder/index';
 import { confirmSubOrder } from '@/api/order/subOrder/index';
 import { nursingSummarySubOrder } from '@/api/order/subOrder/index';
 import { exportSubOrder } from '@/api/order/subOrder/index';
+import { activateSubOrder } from '@/api/order/subOrder/index';
 import { listAreaStation as listAreaStationOnStore } from '@/api/system/areaStation';
 import { getStore } from '@/api/system/store';
 import { reward } from '@/api/fulfiller/pool';
@@ -347,12 +376,14 @@ const getServiceMode = (serviceId) => {
 };
 
 const getServiceModeTag = (row) => {
+  if (getServiceMode(row.service) !== 1) return '';
   const t = row?.type;
   if (t === 0 || t === '0' || t === 1 || t === '1') return '往返';
   return '';
 };
 
 const getServiceOrderTypeTag = (row) => {
+  if (getServiceMode(row.service) !== 1) return null;
   const t = row?.type;
   if (t === 0 || t === '0') return { label: '接', type: 'primary' };
   if (t === 1 || t === '1') return { label: '送', type: 'success' };
@@ -406,6 +437,44 @@ const complaintForm = reactive({ reason: '' });
 const currentComplaintOrder = ref(null);
 const currentOperateRow = ref(null);
 
+// 开通服务相关
+const activateDialogVisible = ref(false);
+const activateLoading = ref(false);
+const activateForm = reactive({
+  id: 0,
+  service: '',
+  fulfillmentCommission: 0
+});
+
+const openActivateDialog = (row) => {
+  activateForm.id = row.id;
+  activateForm.service = row.service;
+  activateForm.fulfillmentCommission = row.fulfillmentCommission !== undefined && row.fulfillmentCommission !== null ? row.fulfillmentCommission / 100 : 0;
+  activateDialogVisible.value = true;
+};
+
+const submitActivate = async () => {
+  if (!activateForm.service) {
+    ElMessage.warning('请选择服务');
+    return;
+  }
+  activateLoading.value = true;
+  try {
+    await activateSubOrder({
+      id: activateForm.id,
+      service: activateForm.service,
+      fulfillmentCommission: Math.round(Number(activateForm.fulfillmentCommission) * 100)
+    });
+    ElMessage.success('开通服务成功');
+    activateDialogVisible.value = false;
+    handleSearch();
+  } catch (error) {
+    // interceptor handled
+  } finally {
+    activateLoading.value = false;
+  }
+};
+
 // 详情
 const handleDetail = async (row) => {
   const typeName = getServiceName(row?.service);
@@ -413,43 +482,40 @@ const handleDetail = async (row) => {
   const typeCode = isTransport ? 'transport' : typeName?.includes('喂') || typeName?.includes('遛') ? 'feeding' : 'washing';
   currentOrder.value = {
     ...row,
+    orderCommission: row.orderCommission !== undefined && row.orderCommission !== null ? row.orderCommission / 100.0 : undefined,
+    fulfillerFee: row.fulfillmentCommission !== undefined && row.fulfillmentCommission !== null ? row.fulfillmentCommission / 100.0 : undefined,
     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',
+    userAvatar: '',
+    address: '',
     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'
+      pickTime: '',
+      pickAddr: row.detail?.pickAddr || '',
+      pickContact: '',
+      pickPhone: '',
+      dropTime: '',
+      dropAddr: row.detail?.dropAddr || '',
+      dropContact: '',
+      dropPhone: '',
+      packageName: row.detail?.packageName || '',
+      petStatus: '',
+      area: ''
     },
-    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' }
-    ]
+    petGender: '',
+    petAge: '',
+    petWeight: '',
+    petVaccine: '',
+    petSterilized: undefined,
+    petCharacter: '',
+    petHealth: '',
+    fulfillerAvatar: '',
+    fulfillerPhone: '',
+    fulfillerStation: '',
+    orderLogs: []
   };
 
   try {
@@ -476,6 +542,7 @@ const handleDetail = async (row) => {
             ? info.toAddress || currentOrder.value?.address
             : info.address || info.toAddress || currentOrder.value?.address,
         fulfillmentCommission: info.fulfillmentCommission !== undefined && info.fulfillmentCommission !== null ? Number(info.fulfillmentCommission) / 100 : currentOrder.value?.fulfillmentCommission,
+        orderCommission: info.orderCommission !== undefined && info.orderCommission !== null ? Number(info.orderCommission) / 100 : currentOrder.value?.orderCommission,
         fulfillerFee: info.fulfillmentCommission !== undefined && info.fulfillmentCommission !== null ? Number(info.fulfillmentCommission) / 100 : currentOrder.value?.fulfillerFee,
         merchantName: info.storeName || currentOrder.value?.merchantName,
         platformId: info.platformId ?? currentOrder.value?.platformId,
@@ -547,7 +614,9 @@ const openDispatchDialog = (row) => {
     dropAddr,
     service: row.service,
     riderId: row.riderId || row.fulfiller || null,
-    riderGender: row.fulfillerGender
+    riderGender: row.fulfillerGender,
+    fulfillmentCommission: row.fulfillmentCommission,
+    orderCommission: row.orderCommission
   };
   currentDispatchOrder.value = orderObj;
   dispatchDialogVisible.value = true;
@@ -555,18 +624,21 @@ const openDispatchDialog = (row) => {
 
 const handleDispatchSubmit = (payload) => {
   if (!currentDispatchOrder.value) return;
-  const priceFen = Math.round(Number(payload.fee || 0) * 100);
+  const fulfillmentCommissionFen = Math.round(Number(payload.fee || 0) * 100);
+  const orderCommissionFen = Math.round(Number(payload.orderCommission || 0) * 100);
   dispatchSubOrder({
     orderId: currentDispatchOrder.value.id,
     fulfiller: payload.riderId,
-    fulfillmentCommission: priceFen
+    fulfillmentCommission: fulfillmentCommissionFen,
+    orderCommission: orderCommissionFen
   }).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.fulfillmentCommission = payload.fee;
+      row.fulfillmentCommission = payload.fee * 100; // 后端期望的分
+      row.orderCommission = orderCommissionFen;
     }
     handleSearch();
   });

+ 16 - 3
src/views/order/purchase/index.vue

@@ -110,9 +110,19 @@
 
           <div class="card-body">
             <!-- 服务套餐信息 -->
-            <el-form-item label="团购套餐">
-              <el-input v-model="form.groupBuyPackage" placeholder="请输入团购套餐名称 (选填)" clearable />
-            </el-form-item>
+            <el-row :gutter="20">
+              <el-col :span="12">
+                <el-form-item label="团购套餐">
+                  <el-input v-model="form.groupBuyPackage" placeholder="请输入团购套餐名称 (选填)" clearable />
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="订单佣金 (元)">
+                  <el-input-number v-model="form.orderCommission" :min="0" :precision="2" :step="1" placeholder="佣金金额"
+                    style="width: 100%" />
+                </el-form-item>
+              </el-col>
+            </el-row>
 
             <div class="divider"></div>
 
@@ -248,6 +258,7 @@ const form = reactive({
   type: '',
   mode: undefined,
   groupBuyPackage: '',
+  orderCommission: 0.00,
 
   // Sub Forms Data
   transport: {
@@ -565,6 +576,7 @@ const resetForm = () => {
   form.type = ''
   form.mode = undefined
   form.groupBuyPackage = ''
+  form.orderCommission = 0.00
 
   form.transport = {
     pkgId: '',
@@ -729,6 +741,7 @@ const handleSubmit = async () => {
       service: form.serviceId,
       remark: "",
       tenantId: storeObj.tenantId || "",
+      orderCommission: Math.round((form.orderCommission || 0) * 100), // 订单佣金,单位:分
       subOrders: subOrders
     }