Bladeren bron

完成导出功能

Huanyi 2 weken geleden
bovenliggende
commit
1d084f2dc2

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

@@ -160,3 +160,16 @@ export const listRewardLog = (fulfillerId: string | number, query?: PageQuery):
     params: { fulfillerId, ...query }
   });
 };
+
+/**
+ * 导出履约者列表为Excel
+ * @param data 查询参数(与列表查询同步)
+ */
+export const exportFulfiller = (data: FlfFulfillerQuery) => {
+  return request({
+    url: '/fulfiller/fulfiller/export',
+    method: 'post',
+    data,
+    responseType: 'blob'
+  });
+};

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

@@ -121,3 +121,16 @@ export const listSubOrderOnStore = (query: any): AxiosPromise<SubOrderVO[]> => {
         params: query
     });
 };
+
+/**
+ * 导出子订单列表为Excel
+ * @param data 查询参数(status、service、content)
+ */
+export const exportSubOrder = (data: { status?: number; service?: number; content?: string }) => {
+    return request({
+        url: '/order/subOrder/export',
+        method: 'post',
+        data,
+        responseType: 'blob'
+    });
+};

+ 20 - 3
src/views/fulfiller/pool/index.vue

@@ -8,7 +8,8 @@
             <el-tag type="info" effect="plain" style="margin-left: 10px;">共 {{ total }} 人</el-tag>
           </div>
           <div class="right-panel">
-            <el-button type="primary" icon="Plus" style="margin-right: 15px" @click="handleCreate" v-hasPermi="['fulfiller:pool:add']">新增履约者</el-button>
+            <el-button type="success" icon="Download" @click="handleExport" v-hasPermi="['fulfiller:pool:exportExcel']">导出Excel</el-button>
+            <el-button type="primary" icon="Plus" style="margin-right: 16px" @click="handleCreate" v-hasPermi="['fulfiller:pool:add']">新增履约者</el-button>
             <el-input v-model="searchKey" placeholder="搜索姓名/手机号/身份证" class="search-input" prefix-icon="Search" clearable
               @keyup.enter="handleSearch" @clear="handleSearch" />
             <el-cascader
@@ -690,12 +691,12 @@
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, computed, onMounted } from 'vue'
+import { ref, reactive, computed, onMounted, getCurrentInstance } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import {
   listFulfiller, getFulfiller, addFulfiller, updateFulfiller,
   changeStatus, resetPwd, reward, adjustPoints, adjustBalance,
-  listPointsLog, listBalanceLog, listRewardLog
+  listPointsLog, listBalanceLog, listRewardLog, exportFulfiller
 } from '@/api/fulfiller/pool'
 import { addViolation, listViolationByFulfiller } from '@/api/fulfiller/violation'
 import type { FlfViolationVO } from '@/api/fulfiller/violation/types'
@@ -716,6 +717,9 @@ import ImageUpload from '@/components/ImageUpload/index.vue'
 import { listAllLevelRights, addLevelRights, updateLevelRights, delLevelRights, changeLevelRightsStatus } from '@/api/fulfiller/levelRights';
 import { listAllLevelConfig, addLevelConfig, updateLevelConfig, delLevelConfig } from '@/api/fulfiller/levelConfig';
 
+// 获取全局实例,用于调用 proxy.download
+const { proxy } = getCurrentInstance() as any
+
 const loading = ref(false)
 const searchKey = ref('')
 const activeTab = ref('all')
@@ -796,6 +800,19 @@ const getList = async () => {
   }
 }
 
+/** 导出为Excel */
+const handleExport = () => {
+  proxy?.download(
+    'fulfiller/fulfiller/export',
+    {
+      status: activeTab.value === 'all' ? undefined : activeTab.value,
+      keyword: searchKey.value || undefined,
+      stationId: queryParams.stationId || undefined
+    },
+    '履约者列表.xlsx'
+  )
+}
+
 /** 加载全部标签(选择器用) */
 const loadAllTags = async () => {
   try {

+ 28 - 20
src/views/order/orderList/components/OrderDetailDrawer.vue

@@ -89,9 +89,9 @@
                         </div>
                         <el-descriptions :column="2" size="small" class="pet-desc" border>
                             <el-descriptions-item label="宠物品种">{{ order.petBreed || '未知'
-                                }}</el-descriptions-item>
+                            }}</el-descriptions-item>
                             <el-descriptions-item label="疫苗状态"><span style="color:#67c23a">{{ order.petVaccine || '未知'
-                                    }}</span></el-descriptions-item>
+                            }}</span></el-descriptions-item>
                             <el-descriptions-item label="性格特点">{{ order.petCharacter || '温顺' }}</el-descriptions-item>
                             <el-descriptions-item label="健康状况">{{ order.petHealth || '健康' }}</el-descriptions-item>
                         </el-descriptions>
@@ -105,7 +105,7 @@
                         <div class="user-content">
                             <div class="u-row">
                                 <el-avatar :size="40" :src="order.userAvatar">{{ (order.userName || '').charAt(0)
-                                    }}</el-avatar>
+                                }}</el-avatar>
                                 <div class="u-info">
                                     <div class="nm">{{ order.userName }}</div>
                                     <div class="ph">{{ order.contactPhone }}</div>
@@ -138,15 +138,15 @@
                                     <el-descriptions-item label="归属门店">{{ order.merchantName }}
                                         ({{ Number(order.platformId) === 1 ? '门店下单' : '平台代下单' }})</el-descriptions-item>
                                     <el-descriptions-item label="宠主信息">{{ order.userName }} / {{ order.contactPhone
-                                        }}</el-descriptions-item>
+                                    }}</el-descriptions-item>
                                     <el-descriptions-item label="服务费用" label-class-name="money-label">
                                         <span style="color:#f56c6c; font-weight:bold;">¥ {{ order.fulfillerFee }}</span>
                                     </el-descriptions-item>
 
                                     <el-descriptions-item label="预约时间">{{ getServiceTimeRange(order.serviceTime)
-                                        }}</el-descriptions-item>
+                                    }}</el-descriptions-item>
                                     <el-descriptions-item label="团购套餐">{{ order.groupBuyPackage || '未使用团购套餐'
-                                        }}</el-descriptions-item>
+                                    }}</el-descriptions-item>
                                     <el-descriptions-item label="创建时间">{{ order.createTime }}</el-descriptions-item>
 
                                     <el-descriptions-item label="订单备注" :span="3">
@@ -166,7 +166,7 @@
                                     <div class="t-row">
                                         <span class="t-k">起点</span>
                                         <span class="t-v">{{ order.detail?.fromAddress || order.detail?.pickAddr || '--'
-                                        }}</span>
+                                            }}</span>
                                     </div>
                                     <div class="t-row">
                                         <span class="t-k">终点</span>
@@ -186,7 +186,7 @@
                                 <div class="sec-title-bar">服务执行要求</div>
                                 <el-descriptions :column="2" border size="default" class="custom-desc">
                                     <el-descriptions-item label="服务地址" :span="2">{{ order.detail.area || order.address
-                                        }}</el-descriptions-item>
+                                    }}</el-descriptions-item>
                                 </el-descriptions>
                             </div>
                         </div>
@@ -198,7 +198,7 @@
                             <div v-if="order.fulfillerName" class="fulfiller-card">
                                 <div class="f-left">
                                     <el-avatar :size="60" :src="order.fulfillerAvatar">{{ order.fulfillerName.charAt(0)
-                                        }}</el-avatar>
+                                    }}</el-avatar>
                                 </div>
                                 <div class="f-right">
                                     <div class="f-row1">
@@ -243,14 +243,18 @@
                                             <div v-for="(item, i) in step.media" :key="i" class="media-item">
                                                 <!-- 图片类型 -->
                                                 <el-image v-if="item.type === 'image'" :src="item.url"
-                                                    :preview-src-list="step.media.filter(m => m.type === 'image').map(m => m.url)" fit="cover"
-                                                    class="p-img" :preview-teleported="true" />
+                                                    :preview-src-list="step.media.filter(m => m.type === 'image').map(m => m.url)"
+                                                    fit="cover" class="p-img" :preview-teleported="true" />
 
                                                 <!-- 视频类型:由于后端没给第一帧,这里采用 video 标签 preload 方式尝试展示 -->
-                                                <div v-else-if="item.type === 'video'" class="p-video-box" @click="openVideoPreview(item.url)">
-                                                    <video :src="item.url" preload="metadata" class="p-img p-video"></video>
+                                                <div v-else-if="item.type === 'video'" class="p-video-box"
+                                                    @click="openVideoPreview(item.url)">
+                                                    <video :src="item.url" preload="metadata"
+                                                        class="p-img p-video"></video>
                                                     <div class="play-icon-overlay">
-                                                        <el-icon><VideoPlay /></el-icon>
+                                                        <el-icon>
+                                                            <VideoPlay />
+                                                        </el-icon>
                                                     </div>
                                                 </div>
                                             </div>
@@ -266,11 +270,13 @@
                         <div class="tab-pane-content">
                             <div style="display: flex; justify-content: flex-end; margin-bottom: 15px;">
                                 <el-button type="primary" size="small" icon="Download"
+                                    v-hasPermi="['order:orderList:queryExportExcel']"
                                     @click="handleExportLogs">导出日志Excel</el-button>
                             </div>
                             <el-timeline>
-                                <el-timeline-item v-for="(log, index) in (orderLogs || [])" :key="index" :timestamp="log.createTime || log.time || ''"
-                                    :type="'primary'" :icon="undefined" placement="top">
+                                <el-timeline-item v-for="(log, index) in (orderLogs || [])" :key="index"
+                                    :timestamp="log.createTime || log.time || ''" :type="'primary'" :icon="undefined"
+                                    placement="top">
                                     <div class="log-card">
                                         <div class="l-tit">{{ log.title }}</div>
                                         <div class="l-txt">{{ log.content }}</div>
@@ -304,8 +310,10 @@
 
     <!-- 视频播放弹窗 -->
     <el-dialog v-model="videoPreview.visible" title="视频播放" width="800px" append-to-body @closed="videoPreview.url = ''">
-        <div style="width: 100%; display: flex; justify-content: center; background: #000; border-radius: 4px; overflow: hidden;">
-            <video v-if="videoPreview.url" :src="videoPreview.url" controls autoplay style="max-width: 100%; max-height: 70vh;"></video>
+        <div
+            style="width: 100%; display: flex; justify-content: center; background: #000; border-radius: 4px; overflow: hidden;">
+            <video v-if="videoPreview.url" :src="videoPreview.url" controls autoplay
+                style="max-width: 100%; max-height: 70vh;"></video>
         </div>
     </el-dialog>
 </template>
@@ -872,7 +880,7 @@ const handleExportLogs = () => {
     top: 50%;
     left: 50%;
     transform: translate(-50%, -50%);
-    background: rgba(0,0,0,0.4);
+    background: rgba(0, 0, 0, 0.4);
     color: #fff;
     width: 32px;
     height: 32px;
@@ -885,7 +893,7 @@ const handleExportLogs = () => {
 }
 
 .p-video-box:hover .play-icon-overlay {
-    background: rgba(0,0,0,0.6);
+    background: rgba(0, 0, 0, 0.6);
     transform: translate(-50%, -50%) scale(1.1);
 }
 

+ 28 - 2
src/views/order/orderList/index.vue

@@ -3,7 +3,9 @@
     <el-card shadow="never" class="table-card">
       <template #header>
         <div class="card-header">
-          <span class="title">订单列表</span>
+          <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>
@@ -13,6 +15,7 @@
             <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>
 
@@ -183,7 +186,7 @@
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, onMounted, nextTick } from 'vue';
+import { ref, reactive, onMounted, nextTick, getCurrentInstance } from 'vue';
 import { useRouter } from 'vue-router';
 import { ElMessage, ElMessageBox } from 'element-plus';
 import fulfillerEnums from '@/json/fulfiller.json';
@@ -200,9 +203,13 @@ import { cancelSubOrder } 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 { 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';
 import { addComplaint } from '@/api/fulfiller/complaint';
@@ -753,6 +760,19 @@ const handleCommand = (cmd, row) => {
     });
   }
 };
+
+/** 导出为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>
@@ -766,6 +786,12 @@ const handleCommand = (cmd, row) => {
   align-items: center;
 }
 
+.left-panel {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
 .title {
   font-weight: bold;
   font-size: 18px;

+ 17 - 14
src/views/order/purchase/index.vue

@@ -33,14 +33,16 @@
                         style="display:flex; justify-content:space-between; align-items:center; width:100%; height: 24px;">
                         <span>宠主用户</span>
                         <el-button type="primary" plain size="small" @click="openAddUser" icon="Plus"
-                          style="margin-left: 15px;" v-hasPermi="['order:purchase:addCustomer']" :disabled="!form.merchantId">添加用户</el-button>
+                          style="margin-left: 15px;" v-hasPermi="['order:purchase:addCustomer']"
+                          :disabled="!form.merchantId">添加用户</el-button>
                       </div>
                     </template>
-                    <PageSelect v-model="form.userId" :placeholder="form.merchantId ? '搜索姓名/手机号' : '请先选择服务门店'" size="large" style="width: 100%"
-                      :options="userSelectOptions" :total="userTotal" :page-size="5" :filter-method="searchUser"
-                      :loading="userLoading" :popper-class="!userQuery.content ? 'hide-search-popper' : ''"
-                      :disabled="!form.merchantId"
-                      @page-change="handleUserPageChange" @visible-change="handleUserVisibleChange" @update:modelValue="handleUserChange" />
+                    <PageSelect v-model="form.userId" :placeholder="form.merchantId ? '搜索姓名/手机号' : '请先选择服务门店'"
+                      size="large" style="width: 100%" :options="userSelectOptions" :total="userTotal" :page-size="5"
+                      :filter-method="searchUser" :loading="userLoading"
+                      :popper-class="!userQuery.content ? 'hide-search-popper' : ''" :disabled="!form.merchantId"
+                      @page-change="handleUserPageChange" @visible-change="handleUserVisibleChange"
+                      @update:modelValue="handleUserChange" />
                   </el-form-item>
                 </el-col>
               </el-row>
@@ -187,7 +189,8 @@
 
     <!-- Dialogs -->
     <!-- Add User Dialog -->
-    <AddUserDialog v-model:visible="userDialogVisible" :pca-options="pcaOptions" :tenant-id="currentTenantId" @success="handleUserSuccess" />
+    <AddUserDialog v-model:visible="userDialogVisible" :pca-options="pcaOptions" :tenant-id="currentTenantId"
+      @success="handleUserSuccess" />
     <AddPetDialog v-model:visible="petDialogVisible" :user-id="form.userId" :user-options="userOptions"
       @success="handlePetSuccess" />
 
@@ -364,7 +367,7 @@ const handleServiceChange = (item) => {
   form.serviceId = item.id
   form.mode = item.mode
   const isRouteMode = item.mode === 1 || item.mode === '1'
-  
+
   if (isRouteMode) {
     form.type = 'transport'
   } else {
@@ -372,7 +375,7 @@ const handleServiceChange = (item) => {
     // 如果 mode=0 但识别为 transport,强制改为 feeding 作为兜底服务单
     form.type = t === 'transport' ? 'feeding' : t
   }
-  
+
   calcPrice(form.type)
 }
 
@@ -501,11 +504,11 @@ const currentTenantId = computed(() => {
 const fetchUsers = () => {
   if (!form.merchantId) return
   userLoading.value = true
-  
+
   // 关联当前选中门店的租户ID
   const store = stores.value.find(s => s.id === form.merchantId)
   userQuery.tenantId = store ? store.tenantId : undefined
-  
+
   listCustomerOnOrder(userQuery).then(res => {
     userOptions.value = res.rows || []
     userTotal.value = res.total || 0
@@ -620,7 +623,7 @@ const validateForm = () => {
     if (!data.region || data.region.length === 0) return '请选择服务区域';
     if (!data.addressDetail) return '请填写服务详细地址';
     if (!data.appointments || data.appointments.length === 0) return '请至少添加一个预约时间';
-    
+
     for (let i = 0; i < data.appointments.length; i++) {
       if (!data.appointments[i].startTime) {
         return `请选择第 ${i + 1} 个预约的开始时间`;
@@ -650,7 +653,7 @@ const handleSubmit = async () => {
     }
 
     let subOrders = []
-    const baseMode = form.mode || 0
+    const baseMode = form.mode
 
     // 获取默认客户联系方式
     const userObj = userOptions.value.find(u => u.id === form.userId)
@@ -724,7 +727,7 @@ const handleSubmit = async () => {
       pet: form.petId,
       groupPurchasePackageName: form.groupBuyPackage || '',
       service: form.serviceId,
-      remark: "", 
+      remark: "",
       tenantId: storeObj.tenantId || "",
       subOrders: subOrders
     }