| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004 |
- <template>
- <div class="page-container">
- <el-card shadow="never" class="table-card">
- <template #header>
- <div class="card-header">
- <div class="left-panel">
- <span class="title">订单列表</span>
- </div>
- <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>
- <el-button type="success" icon="Download" @click="handleExport"
- v-hasPermi="['order:orderList:export']">导出Excel</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-tab-pane label="已拒绝" name="6" />
- <el-tab-pane label="已关闭" name="7" />
- </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 prop="createTime" label="预约服务时间" width="165" sortable>
- <template #default="{ row }">
- <span class="time-text">{{ row.serviceTime }}</span>
- </template>
- </el-table-column>
- <el-table-column label="订单状态" width="100">
- <template #default="{ row }">
- <div class="status-cell">
- <div class="status-dot" :class="getStatusClass(row.status)"></div>
- <span>{{ getStatusName(row.status) }}</span>
- </div>
- </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="120">
- <template #default="{ row }">
- <span v-if="row.fulfillerName" style="font-weight: 500; color: #333;">{{ row.fulfillerName }}</span>
- <span v-else class="text-gray">暂未指派</span>
- </template>
- </el-table-column>
- <el-table-column label="履约电话" width="130">
- <template #default="{ row }">
- <div v-if="row.fulfillerPhone" style="display: flex; align-items: center; gap: 4px;">
- <el-icon>
- <Phone />
- </el-icon>
- <span>{{ row.fulfillerPhone }}</span>
- </div>
- <span v-else>-</span>
- </template>
- </el-table-column>
- <el-table-column label="履约佣金" width="100">
- <template #default="{ row }">
- <span v-if="row.fulfillmentCommission !== null && row.fulfillmentCommission !== undefined"
- style="color: #f56c6c; font-size: 14px; font-weight: bold;">
- ¥{{ (row.fulfillmentCommission / 100).toFixed(2) }}
- </span>
- <span v-else>-</span>
- </template>
- </el-table-column>
- <el-table-column label="操作" width="240" fixed="right">
- <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 === 2 && row.status !== 7" 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, 7].includes(row.status)" link type="warning" size="small"
- @click="openDispatchDialog(row)" v-hasPermi="['order:orderList:redispatch']">重新派单</el-button>
- <el-button v-if="[0, 1].includes(row.status)" link type="danger" size="small" @click="handleCancel(row)"
- v-hasPermi="['order:orderList:cancel']">取消</el-button>
- <el-button v-if="![4, 5, 6, 7].includes(row.status)" link type="info" size="small"
- @click="handleClose(row)" v-hasPermi="['order:orderList:close']">关闭</el-button>
- <el-dropdown v-if="[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 === 4 && getServiceMode(row.service) == 0" command="care_summary"
- v-hasPermi="['order:orderList:nursingSummary']">护理小结</el-dropdown-item>
- <el-dropdown-item command="reward" v-hasPermi="['order:orderList:reward']">奖惩</el-dropdown-item>
- <el-dropdown-item command="remark" v-hasPermi="['order:orderList: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" @success="handleSearch" />
- <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" />
- <!-- 开通服务弹窗 -->
- <el-dialog v-model="activateDialogVisible" title="开通服务" width="400px" append-to-body>
- <el-form :model="activateForm" label-width="100px">
- <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-item label="服务说明">
- <el-input v-model="activateForm.serviceSpecification" type="textarea" placeholder="请输入服务说明(选填)" :rows="3"
- 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>
- <script setup lang="ts">
- import { ref, reactive, onMounted, onUnmounted, nextTick, getCurrentInstance } from 'vue';
- import { useRouter, useRoute } 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/index.vue';
- import CareSummaryDrawer from './components/CareSummaryDrawer.vue';
- import RewardDialog from './components/RewardDialog.vue';
- import RemarkDialog from './components/RemarkDialog.vue';
- import { useIntervalRefresh } from '@/hooks/useIntervalRefresh';
- import { listAllService } from '@/api/service/list/index';
- import { listSubOrder } from '@/api/order/subOrder/index';
- import { getSubOrderInfo } from '@/api/order/subOrder/index';
- import { cancelSubOrder } from '@/api/order/subOrder/index';
- import { closeSubOrder } from '@/api/order/subOrder/index';
- 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';
- // 获取全局实例,用于调用 proxy.download
- const { proxy } = getCurrentInstance() as any;
- import { getPet } from '@/api/archieves/pet';
- import { getCustomer } from '@/api/archieves/customer';
- const route = useRoute();
- 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();
- if (route.query.id) {
- handleDetail({ id: route.query.id as any });
- }
- });
- // 监听路由参数变化,实现从消息直达详情 @Author: Antigravity
- watch(() => route.query.id, (newId) => {
- if (newId) {
- handleDetail({ id: newId as any });
- }
- });
- // 统一数据轮询 @Author: Antigravity
- useIntervalRefresh(() => handleSearch(true), 5000);
- const getServiceList = () => {
- listAllService().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 = (isPolling: any = false) => {
- const isAutoPoll = isPolling === true;
- if (!isAutoPoll) {
- 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);
- if (!isAutoPoll) loading.value = false;
- })
- .catch(() => {
- if (!isAutoPoll) 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 getServiceMode = (serviceId) => {
- const item = serviceOptions.value.find((i) => i.id === serviceId);
- return item ? item.mode : null;
- };
- 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' as const };
- if (t === 1 || t === '1') return { label: '送', type: 'success' as const };
- if (t === 2 || t === '2') return { label: '单程接', type: 'primary' as const };
- if (t === 3 || t === '3') return { label: '单程送', type: 'success' as const };
- return null;
- };
- const getStatusName = (status) => {
- const map = { 0: '待派单', 1: '待接单', 2: '待服务', 3: '服务中', 4: '已完成', 5: '已取消', 6: '已拒绝', 7: '已关闭' };
- return map[status] || '未知';
- };
- const getStatusClass = (status) => {
- const map = { 0: 'pending', 1: 'waiting', 2: 'waiting', 3: 'processing', 4: 'completed', 5: 'cancelled', 6: 'rejected', 7: 'closed' };
- return map[status] || '';
- };
- const getFulfillerStatusText = (status) => {
- const statusMap = {
- resting: '休息',
- busy: '接单中',
- disabled: '禁用'
- };
- return statusMap[status] || status;
- };
- const getFulfillerStatusType = (status) => {
- const typeMap = {
- resting: 'info',
- busy: 'success',
- disabled: 'danger'
- };
- return typeMap[status] || 'info';
- };
- // 弹窗状态管理
- 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 activateDialogVisible = ref(false);
- const activateLoading = ref(false);
- const activateForm = reactive({
- id: 0,
- fulfillmentCommission: 0,
- serviceSpecification: ''
- });
- const openActivateDialog = (row) => {
- activateForm.id = row.id;
- activateForm.fulfillmentCommission = row.fulfillmentCommission !== undefined && row.fulfillmentCommission !== null ? row.fulfillmentCommission / 100 : 0;
- activateForm.serviceSpecification = '';
- activateDialogVisible.value = true;
- };
- const submitActivate = async () => {
- if (!activateForm.fulfillmentCommission) {
- ElMessage.warning('请填写履约佣金');
- return;
- }
- activateLoading.value = true;
- try {
- await activateSubOrder({
- id: activateForm.id,
- fulfillmentCommission: Math.round(Number(activateForm.fulfillmentCommission) * 100),
- serviceSpecification: activateForm.serviceSpecification || undefined
- });
- ElMessage.success('开通服务成功');
- activateDialogVisible.value = false;
- handleSearch();
- } catch (error) {
- // interceptor handled
- } finally {
- activateLoading.value = false;
- }
- };
- // 详情
- 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,
- 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: '',
- address: '',
- groupBuyPackage: '',
- transportType: row.splitType || row.transportType,
- detail: {
- ...row.detail,
- pickTime: '',
- pickAddr: row.detail?.pickAddr || '',
- pickContact: '',
- pickPhone: '',
- dropTime: '',
- dropAddr: row.detail?.dropAddr || '',
- dropContact: '',
- dropPhone: '',
- packageName: row.detail?.packageName || '',
- petStatus: '',
- area: ''
- },
- petGender: '',
- petAge: '',
- petWeight: '',
- petVaccine: '',
- petSterilized: undefined,
- petCharacter: '',
- petHealth: '',
- fulfillerAvatar: '',
- fulfillerPhone: '',
- fulfillerStation: '',
- orderLogs: []
- };
- 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 ?? getServiceMode(info.service) ?? 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.mode === 1 || info.mode === '1'
- ? 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,
- 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.area || info.address || info.toAddress || currentOrder.value?.detail?.area,
- packageName: info.packageName || info.servicePackageName || info.package || currentOrder.value?.detail?.packageName,
- petStatus: info.petStatus || info.specialRequirement || info.requirement || currentOrder.value?.detail?.petStatus,
- fromCode: info.fromCode || currentOrder.value?.detail?.fromCode,
- toCode: info.toCode || currentOrder.value?.detail?.toCode
- }
- };
- }
- } catch { }
- detailVisible.value = true;
- };
- // 取消订单
- const handleCancel = (row: any) => {
- ElMessageBox.prompt('请输入订单取消原因', '订单确认取消', {
- confirmButtonText: '确认取消',
- cancelButtonText: '暂不取消',
- inputPlaceholder: '请输入取消原因(必填)',
- inputValidator: (value) => {
- if (!value || !value.trim()) {
- return '取消原因不能为空';
- }
- return true;
- },
- type: 'warning'
- }).then(({ value }) => {
- cancelSubOrder({ orderId: row?.id, reason: value }).then(() => {
- ElMessage.success('订单已成功取消');
- handleSearch();
- });
- }).catch(() => {
- // 用户取消输入
- });
- };
- const handleClose = (row: any) => {
- const statusName = getStatusName(row.status);
- ElMessageBox.confirm(
- `<div style="line-height: 1.8;">
- <p style="color: #f56c6c; font-weight: bold; margin-bottom: 10px;">⚠️ 一旦关闭,该订单将无法进行操作!</p>
- <p><strong>订单号:</strong>${row.code}</p>
- <p><strong>服务类型:</strong>${getServiceName(row.service)}</p>
- <p><strong>宠物名称:</strong>${row.petName || '-'}</p>
- <p><strong>当前状态:</strong>${statusName}</p>
- </div>`,
- '确认关闭订单',
- {
- confirmButtonText: '确认关闭',
- cancelButtonText: '取消',
- type: 'warning',
- dangerouslyUseHTMLString: true,
- }
- ).then(() => {
- closeSubOrder({ orderId: row?.id }).then(() => {
- ElMessage.success('订单已成功关闭');
- handleSearch();
- });
- }).catch(() => {
- // 用户取消
- });
- };
- // 派单
- const openDispatchDialog = (row) => {
- currentDispatchOrder.value = row;
- dispatchDialogVisible.value = true;
- };
- // 护理小结
- const openCareSummary = async (row) => {
- const orderData = {
- ...row,
- careSummary: row.nursingSummary || undefined
- };
- // 获取宠物信息
- const petId = row?.pet || row?.petId;
- if (petId) {
- try {
- const petRes = await getPet(petId);
- const pet = petRes?.data;
- if (pet) {
- orderData.petAge = pet.age ? `${pet.age}岁` : '未知';
- orderData.petGender = pet.gender || 'male';
- orderData.petTags = pet.tags || [];
- orderData.petWeight = pet.weight ? `${pet.weight} kg` : '未知';
- orderData.petPersonality = pet.personality || '未知';
- orderData.healthStatus = pet.healthStatus || '未知';
- orderData.vaccineImg = pet.vaccineImg || '';
- orderData.allergy = pet.allergy || '无';
- orderData.petBreed = pet.breed || '未知';
- }
- } catch { }
- }
- // 获取客户信息(宠物主人)
- const customerId = row?.customer || row?.customerId;
- if (customerId) {
- try {
- const customerRes = await getCustomer(customerId);
- const customer = customerRes?.data;
- if (customer) {
- orderData.userName = customer.name || orderData.userName;
- orderData.homeTime = customer.homeTime || '未知';
- orderData.houseType = customer.houseType || '未知';
- orderData.entryMethod = customer.entryMethod || '未知';
- orderData.entryDetail = customer.entryMethod === 'password'
- ? (customer.entryPassword ? `密码: ${customer.entryPassword}` : '未设置密码')
- : (customer.keyLocation || '未设置');
- }
- } catch { }
- }
- orderData.summaryTime = row.nursingSummaryTime || orderData.summaryTime;
- careSummaryOrder.value = orderData;
- careSummaryVisible.value = true;
- };
- const saveCareSummary = async (text) => {
- if (!careSummaryOrder.value?.id) {
- ElMessage.warning('订单信息不存在');
- return;
- }
- try {
- await nursingSummarySubOrder({
- orderId: careSummaryOrder.value.id,
- content: text
- });
- if (careSummaryOrder.value) {
- careSummaryOrder.value.careSummary = text;
- careSummaryOrder.value.nursingSummary = text;
- }
- ElMessage.success('护理小结已保存');
- handleSearch();
- } catch {
- // Error handled by interceptor
- }
- };
- // 奖惩
- const openRewardDialog = (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 =
- typeof t === 'number' ? (t === 0 ? 'pick' : t === 1 ? 'drop' : t === 2 ? 'round' : undefined) :
- (t === 'pick' || t === 'drop' || t === 'round' ? t : undefined);
- currentOperateRow.value = {
- ...row,
- orderNo: row?.code || row?.orderNo,
- type: typeCode,
- transportType,
- splitType: row?.splitType
- };
- rewardDialogVisible.value = true;
- };
- const handleRewardSubmit = async (form) => {
- if (!currentOperateRow.value?.fulfiller) {
- ElMessage.warning('当前订单未指派履约者,无法执行奖惩操作');
- return;
- }
- try {
- // 针对余额(balance)目标,将元转为分
- const submitAmount = form.item === 'balance' ? Math.round(form.value * 100) : form.value;
- await reward({
- fulfillerId: currentOperateRow.value.fulfiller,
- type: form.type,
- target: form.item,
- amount: submitAmount,
- reason: form.reason,
- bizType: form.type === fulfillerEnums.ActionType.ADD ? 'order_reward' : 'order_punish',
- orderId: currentOperateRow.value.id
- } as any);
- ElMessage.success(`操作成功:${form.type === fulfillerEnums.ActionType.ADD ? '增加' : '减少'}已执行`);
- handleSearch();
- } catch {
- // Error handled by interceptor
- }
- };
- // 备注
- const openRemarkDialog = (row) => {
- currentOperateRow.value = {
- ...row,
- orderNo: row?.code || row?.orderNo
- };
- remarkDialogVisible.value = true;
- };
- const handleRemarkSubmit = async (text) => {
- if (!currentOperateRow.value?.id) {
- ElMessage.warning('订单信息不存在');
- return;
- }
- try {
- await remarkSubOrder({ orderId: currentOperateRow.value.id, remark: text });
- ElMessage.success('备注添加成功');
- remarkDialogVisible.value = false;
- handleSearch();
- } catch { /* handled by interceptor */ }
- };
- // 更多操作
- 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(async () => {
- try {
- await confirmSubOrder({ id: row.id });
- ElMessage.success('订单已确认完成');
- handleSearch();
- } catch {
- // Error handled by interceptor
- }
- }).catch(() => { });
- }
- if (cmd === 'delete') {
- ElMessageBox.confirm('确认删除该订单吗?此操作不可恢复', '警告', { type: 'error' }).then(() => {
- tableData.value = tableData.value.filter((item) => item.id !== row.id);
- ElMessage.success('订单已删除');
- });
- }
- };
- /** 导出为Excel */
- const handleExport = () => {
- proxy?.download(
- 'order/subOrder/export',
- {
- status: filters.status !== '' ? Number(filters.status) : undefined,
- service: filters.service !== '' ? filters.service : undefined,
- content: filters.content || undefined
- },
- '订单列表.xlsx'
- );
- };
- </script>
- <style scoped>
- .page-container {
- padding: 20px;
- }
- .card-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- .left-panel {
- display: flex;
- align-items: center;
- gap: 12px;
- }
- .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;
- }
- .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>
|