Quellcode durchsuchen

申诉功能基本完成

Huanyi vor 2 Wochen
Ursprung
Commit
0fd9fedaad
5 geänderte Dateien mit 533 neuen und 20 gelöschten Zeilen
  1. 13 0
      api/order/subOrderAppeal.js
  2. 2 1
      pages.json
  3. 3 3
      pages/home/index.vue
  4. 482 0
      pages/orders/appeal/index.vue
  5. 33 16
      pages/orders/index.vue

+ 13 - 0
api/order/subOrderAppeal.js

@@ -0,0 +1,13 @@
+import request from '@/utils/request';
+
+/**
+ * 提交申诉(增改服务项)
+ * @param {Object} data 
+ */
+export function addSubOrderAppeal(data) {
+  return request({
+    url: '/order/subOrderAppeal',
+    method: 'POST',
+    data: data
+  });
+}

+ 2 - 1
pages.json

@@ -246,8 +246,9 @@
 			}
 		},
 		{
-			"path": "pages/mine/settings/about/agreement-detail/index",
+			"path": "pages/orders/appeal/index",
 			"style": {
+				"navigationBarTitleText": "增改服务项",
 				"navigationStyle": "custom"
 			}
 		}

+ 3 - 3
pages/home/index.vue

@@ -58,7 +58,7 @@
                 </view>
                 <view class="divider"></view>
                 <view class="stat-item">
-                    <text class="num">{{ (orderStats.price / 100).toFixed(2) }}</text>
+                    <text class="num">{{ (orderStats.fulfillmentCommission / 100).toFixed(2) }}</text>
                     <text class="label">服务总得</text>
                 </view>
             </view>
@@ -494,7 +494,7 @@ export default {
         async loadOrderStats() {
             try {
                 const res = await getOrderCount()
-                this.orderStats = res.data || { total: 0, reject: 0, completed: 0, price: 0 }
+                this.orderStats = res.data || { total: 0, reject: 0, completed: 0, fulfillmentCommission: 0 }
             } catch (err) {
                 console.error('获取订单统计失败:', err)
             }
@@ -728,7 +728,7 @@ export default {
                 type: isRoundTrip ? 1 : item.service,
                 typeText: serviceText,
                 typeIcon: serviceIcon,
-                price: (item.price / 100).toFixed(2),
+                price: (item.fulfillmentCommission / 100).toFixed(2),
                 timeLabel: '服务时间',
                 time: item.serviceTime,
                 petAvatar: item.petAvatar || '/static/dog.png',

+ 482 - 0
pages/orders/appeal/index.vue

@@ -0,0 +1,482 @@
+<template>
+  <view class="appeal-container">
+    <!-- 自定义头部 -->
+    <view class="custom-header">
+      <view class="header-left" @click="navBack">
+        <image class="back-icon" src="/static/icons/chevron_right_dark.svg" style="transform: rotate(180deg);"></image>
+      </view>
+      <text class="header-title">增改服务项</text>
+      <view class="header-right"></view>
+    </view>
+    <view class="header-placeholder"></view>
+
+    <!-- 顶部提示 -->
+    <view class="banner-tip">
+      <view class="tip-content">
+        <text class="tip-title">服务变更申请</text>
+        <text class="tip-desc">如需在服务过程中增加或修改服务内容,请在此提交申请并上传相关凭证。</text>
+      </view>
+      <image class="banner-img" src="/static/icons/service-classification.svg"></image>
+    </view>
+
+    <view class="form-wrapper">
+      <!-- 选择服务项 -->
+      <view class="form-section">
+        <view class="section-label">
+          <text class="label-text">变更服务项</text>
+          <text class="required">*</text>
+        </view>
+        <picker @change="onServiceChange" :value="serviceIndex" :range="serviceOptions" range-key="name">
+          <view class="picker-box">
+            <text class="picker-value" :class="{ 'placeholder': serviceIndex === -1 }">
+              {{ serviceIndex === -1 ? '请选择需要变更的服务项' : serviceOptions[serviceIndex].name }}
+            </text>
+            <image class="arrow-icon" src="/static/icons/nav_arrow.svg"></image>
+          </view>
+        </picker>
+      </view>
+
+      <!-- 上传凭证 -->
+      <view class="form-section">
+        <view class="section-label">
+          <text class="label-text">图片凭证</text>
+          <text class="required">*</text>
+          <text class="label-sub">(最多9张)</text>
+        </view>
+        <view class="upload-grid">
+          <view class="upload-item" v-for="(img, index) in imageList" :key="index" @click="previewImage(index)">
+            <image :src="img" class="uploaded-img" mode="aspectFill"></image>
+            <view class="delete-btn" @click.stop="deleteImage(index)">×</view>
+          </view>
+          <view class="upload-btn" v-if="imageList.length < 9" @click="chooseImage">
+            <text class="plus">+</text>
+            <text class="upload-text">上传凭证</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 变更说明 -->
+      <view class="form-section">
+        <view class="section-label">
+          <text class="label-text">变更说明</text>
+          <text class="label-sub">(选填)</text>
+        </view>
+        <view class="textarea-box">
+          <textarea 
+            class="content-textarea" 
+            v-model="description" 
+            placeholder="请详细描述具体的变更内容或原因..." 
+            maxlength="500" 
+            auto-height 
+          />
+          <text class="word-count">{{ description.length }}/500</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 底部确认按钮 -->
+    <view class="bottom-action">
+      <button class="confirm-btn" :class="{ 'disabled': !isReady }" @click="submitAppeal">确认提交</button>
+    </view>
+  </view>
+</template>
+
+<script>
+import { listAllService } from '@/api/service/list'
+import { uploadFile } from '@/api/fulfiller/app'
+import { addSubOrderAppeal } from '@/api/order/subOrderAppeal'
+
+export default {
+  data() {
+    return {
+      orderId: '',
+      serviceOptions: [],
+      serviceIndex: -1,
+      imageList: [],
+      imageOssIds: [],
+      description: ''
+    }
+  },
+  computed: {
+    isReady() {
+      // 必须要选服务且上传了至少一张图片
+      return this.serviceIndex !== -1 && this.imageList.length > 0;
+    }
+  },
+  async onLoad(options) {
+    if (options.id) {
+      this.orderId = options.id;
+    }
+    await this.fetchServices();
+  },
+  methods: {
+    // 补齐一个简单的时间格式化工具
+    formatNow() {
+      const now = new Date();
+      const pad = (n) => String(n).padStart(2, '0');
+      const year = now.getFullYear();
+      const month = pad(now.getMonth() + 1);
+      const day = pad(now.getDate());
+      const hours = pad(now.getHours());
+      const minutes = pad(now.getMinutes());
+      const seconds = pad(now.getSeconds());
+      return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
+    },
+    async fetchServices() {
+      try {
+        const res = await listAllService();
+        this.serviceOptions = res.data || [];
+      } catch (err) {
+        console.error('获取服务项失败:', err);
+      }
+    },
+    navBack() {
+      uni.navigateBack({ delta: 1 });
+    },
+    onServiceChange(e) {
+      this.serviceIndex = e.detail.value;
+    },
+    chooseImage() {
+      uni.chooseImage({
+        count: 9 - this.imageList.length,
+        sizeType: ['compressed'],
+        sourceType: ['album', 'camera'],
+        success: async (res) => {
+          for (const path of res.tempFilePaths) {
+            this.imageList.push(path);
+            try {
+              uni.showLoading({ title: '正在上传...', mask: true });
+              const uploadRes = await uploadFile(path);
+              if (uploadRes && uploadRes.data && uploadRes.data.ossId) {
+                this.imageOssIds.push(uploadRes.data.ossId);
+              }
+              uni.hideLoading();
+            } catch (err) {
+              uni.hideLoading();
+              console.error('上传凭证失败:', err);
+              uni.showToast({ title: '上传失败', icon: 'none' });
+            }
+          }
+        }
+      });
+    },
+    deleteImage(index) {
+      this.imageList.splice(index, 1);
+      this.imageOssIds.splice(index, 1);
+    },
+    previewImage(index) {
+      uni.previewImage({
+        urls: this.imageList,
+        current: index
+      });
+    },
+    async submitAppeal() {
+      // 再次检查校验条件
+      if (!this.isReady) {
+        if (this.serviceIndex === -1) {
+          uni.showToast({ title: '请先选择服务项', icon: 'none' });
+        } else if (this.imageList.length === 0) {
+          uni.showToast({ title: '请上传至少一张凭证', icon: 'none' });
+        }
+        return;
+      }
+
+      uni.showLoading({ title: '提交中...', mask: true });
+      
+      try {
+        const selectedService = this.serviceOptions[this.serviceIndex];
+        const nowStr = this.formatNow();
+
+        // 构造完整 JSON 入参
+        const data = {
+          "id": 0,
+          "orderId": this.orderId, // 保留原始类型(通常为 string 或 number)
+          "service": selectedService.id,
+          "photos": this.imageOssIds.join(','),
+          "fulfillmentCommission": selectedService.fulfillmentCommission || 0,
+          "reason": this.description || '无详细备注',
+          "createDept": 0,
+          "createBy": 0,
+          "createTime": nowStr,
+          "updateBy": 0,
+          "updateTime": nowStr,
+          "params": {}
+        };
+        
+        console.log('即将发起 API 请求:', data);
+        const res = await addSubOrderAppeal(data);
+        
+        uni.hideLoading();
+        if (res.code === 200 || res.msg === '操作成功') {
+            uni.showToast({ title: '提交成功,请等待后续处理', icon: 'none', duration: 2000 });
+            setTimeout(() => {
+              uni.navigateBack({ delta: 1 });
+            }, 2000);
+        } else {
+            uni.showToast({ title: res.msg || '提交异常,请稍后重试', icon: 'none', duration: 2000 });
+        }
+      } catch (err) {
+        uni.hideLoading();
+        console.error('提交失败详情:', err);
+        uni.showToast({ title: '网络请求失败,请检查网络链接', icon: 'none', duration: 2000 });
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+page {
+  background-color: #F8F9FB;
+}
+
+.appeal-container {
+  min-height: 100vh;
+  padding-bottom: 180rpx;
+}
+
+/* 导航栏样式 */
+.custom-header {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 88rpx;
+  padding-top: var(--status-bar-height);
+  background-color: #fff;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding-left: 30rpx;
+  padding-right: 30rpx;
+  box-sizing: content-box;
+  z-index: 100;
+}
+
+.header-placeholder {
+  height: calc(88rpx + var(--status-bar-height));
+}
+
+.back-icon {
+  width: 44rpx;
+  height: 44rpx;
+}
+
+.header-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+}
+
+.header-right {
+  width: 44rpx;
+}
+
+/* Banner 提示 */
+.banner-tip {
+  background: linear-gradient(135deg, #FF9800 0%, #FF5722 100%);
+  margin: 30rpx;
+  border-radius: 20rpx;
+  padding: 30rpx;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  box-shadow: 0 10rpx 20rpx rgba(255, 87, 34, 0.2);
+}
+
+.tip-content {
+  flex: 1;
+}
+
+.tip-title {
+  color: #fff;
+  font-size: 32rpx;
+  font-weight: bold;
+  display: block;
+  margin-bottom: 10rpx;
+}
+
+.tip-desc {
+  color: rgba(255, 255, 255, 0.9);
+  font-size: 24rpx;
+}
+
+.banner-img {
+  width: 100rpx;
+  height: 100rpx;
+  opacity: 0.8;
+}
+
+/* 表单容器 */
+.form-wrapper {
+  margin: 0 30rpx;
+}
+
+.form-section {
+  background-color: #fff;
+  border-radius: 20rpx;
+  padding: 30rpx;
+  margin-bottom: 24rpx;
+  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.02);
+}
+
+.section-label {
+  display: flex;
+  align-items: center;
+  margin-bottom: 24rpx;
+}
+
+.label-text {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+}
+
+.required {
+  color: #FF5722;
+  margin-left: 6rpx;
+}
+
+.label-sub {
+  font-size: 24rpx;
+  color: #999;
+  margin-left: 10rpx;
+}
+
+/* 下拉选择框 */
+.picker-box {
+  background-color: #F8F9FA;
+  height: 96rpx;
+  padding: 0 30rpx;
+  border-radius: 12rpx;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  border: 1px solid #EEEEEE;
+}
+
+.picker-value {
+  font-size: 28rpx;
+  color: #333;
+}
+
+.picker-value.placeholder {
+  color: #999;
+}
+
+.arrow-icon {
+  width: 24rpx;
+  height: 24rpx;
+  opacity: 0.3;
+}
+
+/* 图片上传网格 */
+.upload-grid {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 18rpx;
+}
+
+.upload-item, .upload-btn {
+  width: calc((100% - 36rpx) / 3);
+  height: 200rpx;
+  border-radius: 12rpx;
+  overflow: hidden;
+  position: relative;
+}
+
+.uploaded-img {
+  width: 100%;
+  height: 100%;
+}
+
+.delete-btn {
+  position: absolute;
+  top: 10rpx;
+  right: 10rpx;
+  background-color: rgba(0, 0, 0, 0.5);
+  color: #fff;
+  width: 36rpx;
+  height: 36rpx;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 24rpx;
+}
+
+.upload-btn {
+  background-color: #F8F9FA;
+  border: 2rpx dashed #E0E0E0;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+
+.plus {
+  font-size: 60rpx;
+  color: #CCCCCC;
+  line-height: 1;
+}
+
+.upload-text {
+  font-size: 22rpx;
+  color: #999;
+  margin-top: 8rpx;
+}
+
+/* 文本域 */
+.textarea-box {
+  background-color: #F8F9FA;
+  border-radius: 12rpx;
+  padding: 24rpx;
+  border: 1px solid #EEEEEE;
+}
+
+.content-textarea {
+  width: 100%;
+  min-height: 200rpx;
+  font-size: 28rpx;
+  color: #333;
+  line-height: 1.5;
+}
+
+.word-count {
+  text-align: right;
+  display: block;
+  font-size: 22rpx;
+  color: #BBB;
+  margin-top: 10rpx;
+}
+
+/* 底部按钮 */
+.bottom-action {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  background-color: #fff;
+  padding: 30rpx 40rpx;
+  padding-bottom: calc(30rpx + env(safe-area-inset-bottom));
+  box-shadow: 0 -10rpx 30rpx rgba(0, 0, 0, 0.05);
+  z-index: 99;
+}
+
+.confirm-btn {
+  background: linear-gradient(90deg, #FF9800 0%, #FF5722 100%);
+  color: #fff;
+  height: 88rpx;
+  line-height: 88rpx;
+  border-radius: 44rpx;
+  font-size: 32rpx;
+  font-weight: bold;
+  box-shadow: 0 8rpx 20rpx rgba(255, 87, 34, 0.3);
+}
+
+.confirm-btn.disabled {
+  background: #E0E0E0;
+  box-shadow: none;
+  color: #fff;
+}
+</style>

+ 33 - 16
pages/orders/index.vue

@@ -75,7 +75,7 @@
 
         <!-- 订单列表 -->
         <view class="order-list">
-            <view class="order-card" v-for="(item, index) in orderList" :key="index" @click="goToDetail(item)">
+            <view class="order-card" v-for="(item, index) in orderList" :key="index" :class="{ 'disabled-card': !item.serviceFlag }">
                 <view class="card-header">
                     <view class="type-badge">
                         <image class="type-icon" :src="item.typeIcon"></image>
@@ -149,15 +149,16 @@
                     </view>
                 </view><!-- End of card-body -->
 
-                <!-- 按钮组 -->
+                <!-- 按钮组 (重新排版) -->
                 <view class="action-btns" v-if="['接单', '到达', '出发', '开始', '送达', '结束'].includes(item.statusText)">
-                    <view class="action-left">
-                        <button class="btn normal" @click.stop="doCall('customer', item)">拨号</button>
-                    </view>
-                    <view class="action-right">
+                    <view class="action-row">
                         <button class="btn normal danger" v-if="item.status === 2"
-                            @click.stop="handleCancelOrder(item)">取消</button>
+                            @click.stop="handleCancelOrder(item)">取消订单</button>
                         <button class="btn normal" @click.stop="reportAbnormal(item)">异常上报</button>
+                        <button class="btn normal" @click.stop="addOrUpdateService(item)">增改服务项</button>
+                    </view>
+                    <view class="action-row">
+                        <button class="btn normal" @click.stop="doCall('customer', item)">拨号</button>
                         <button class="btn primary" @click.stop="mainAction(item)">到达打卡</button>
                     </view>
                 </view>
@@ -405,7 +406,7 @@ export default {
                 typeText: serviceText,
                 typeIcon: serviceIcon,
                 statusText: statusText,
-                fulfillmentCommission: (order.price / 100).toFixed(2),
+                fulfillmentCommission: (order.fulfillmentCommission / 100).toFixed(2),
                 timeLabel: '服务时间',
                 time: order.serviceTime || '',
                 petAvatar: order.petAvatar || '/static/dog.png',
@@ -426,7 +427,8 @@ export default {
                 customerPhone: order.customerPhone || '',
                 endDistance: '0km',
                 serviceContent: order.remark || '',
-                remark: order.remark || ''
+                remark: order.remark || '',
+                serviceFlag: !!order.serviceFlag // 是否允许服务(点击跳转)
             }
         },
         getDisplayStatus(item) {
@@ -729,6 +731,10 @@ export default {
         mainAction(item) {
             uni.navigateTo({ url: `/pages/orders/detail/index?id=${item.id}` });
         },
+        addOrUpdateService(item) {
+            // 跳转到申诉(增改服务项)页面
+            uni.navigateTo({ url: `/pages/orders/appeal/index?id=${item.id}` });
+        },
         openRemarkInput() {
             this.remarkText = '';
             this.showRemarkInput = true;
@@ -1375,16 +1381,16 @@ page {
 
 .action-btns {
     display: flex;
-    justify-content: space-between;
-    margin-top: 15rpx;
-}
-
-.action-left {
-    display: flex;
+    flex-direction: column;
+    gap: 16rpx;
+    margin-top: 20rpx;
 }
 
-.action-right {
+.action-row {
     display: flex;
+    justify-content: space-between;
+    align-items: center;
+    width: 100%;
 }
 
 .btn {
@@ -1852,4 +1858,15 @@ page {
     z-index: 900;
     background: transparent;
 }
+.disabled-card {
+    opacity: 0.5; /* 降低透明度以示禁用 */
+    pointer-events: none; /* 禁用该卡片内背景的一切交互 */
+    filter: grayscale(80%); /* 增加灰度,使视觉效果更明显 */
+}
+
+.disabled-card .action-row {
+    pointer-events: auto; /* 允许按钮即使在置灰状态下也能点击操作 */
+}
+
+/* 即使使用了 pointer-events: none,外层的 @click 也会失效,为了保险我们在 JS 中也做了判断 */
 </style>