From a3107a8f7b7fce1f2b92bc99a6daed7047b8dffc Mon Sep 17 00:00:00 2001
From: ai-coding <ai-coding@devgenie.local>
Date: 星期五, 07 八月 2026 11:18:54 +0800
Subject: [PATCH] feat(vehicle-order): 新增车辆模式切换并区分意见反馈与投诉筛选-lianjd

---
 src/views/transaction/order/evaluationRecord/FeedbackTab.vue       |  209 +++++++++++++++--------------
 src/views/transaction/order/evaluationRecord/index.vue             |    7 
 src/views/assetManagement/vehicleManagement/vehicleState/index.vue |  125 ++++++++++++++---
 src/service/api/serviceCenter.ts                                   |    1 
 src/service/api/vehicleConvoy.ts                                   |   20 ++
 5 files changed, 237 insertions(+), 125 deletions(-)

diff --git a/src/service/api/serviceCenter.ts b/src/service/api/serviceCenter.ts
index dfa4146..7e09446 100644
--- a/src/service/api/serviceCenter.ts
+++ b/src/service/api/serviceCenter.ts
@@ -75,6 +75,7 @@
   orderNo?: string
   createTimeStart?: string
   createTimeEnd?: string
+  hasFeedbackType?: boolean
 }) {
   return request.Post<Service.ResponseResult<any>>('/operation/api/v1/userFeedBack/pageList', data)
 }
diff --git a/src/service/api/vehicleConvoy.ts b/src/service/api/vehicleConvoy.ts
index 2279a13..0df81d0 100644
--- a/src/service/api/vehicleConvoy.ts
+++ b/src/service/api/vehicleConvoy.ts
@@ -37,6 +37,26 @@
   return request.Post<Service.ResponseResult<any>>(`/vehicleConvoy/api/v1/vehicle/updateAcceptOrTest?id=${id}&acceptOrTest=${acceptOrTest}`)
 }
 
+// 出车接单(切运营接单模式)
+export function enterRtMode(id: string | number) {
+  return request.Post<Service.ResponseResult<any>>(`/vehicleConvoy/api/v1/vehicle/enterRtMode?vehicleId=${id}`)
+}
+
+// 收车(切非接单模式)
+export function exitRtMode(id: string | number) {
+  return request.Post<Service.ResponseResult<any>>(`/vehicleConvoy/api/v1/vehicle/exitRtMode?vehicleId=${id}`)
+}
+
+// 进入运维模式
+export function enterMaintenanceMode(id: string | number) {
+  return request.Post<Service.ResponseResult<any>>(`/vehicleConvoy/api/v1/vehicle/enterMaintenanceMode?vehicleId=${id}`)
+}
+
+// 退出运维模式
+export function exitMaintenanceMode(id: string | number) {
+  return request.Post<Service.ResponseResult<any>>(`/vehicleConvoy/api/v1/vehicle/exitMaintenanceMode?vehicleId=${id}`)
+}
+
 // 通过车辆id查询年检分页列表
 export function queryTaskRoutineByVehicleId(data: any) {
   return request.Post<Service.ResponseResult<any>>('/vehicleConvoy/api/v1/vehicle/queryTaskRoutineByVehicleId', data)
diff --git a/src/views/assetManagement/vehicleManagement/vehicleState/index.vue b/src/views/assetManagement/vehicleManagement/vehicleState/index.vue
index 0488e44..683df37 100644
--- a/src/views/assetManagement/vehicleManagement/vehicleState/index.vue
+++ b/src/views/assetManagement/vehicleManagement/vehicleState/index.vue
@@ -8,11 +8,14 @@
 import { useBoolean } from '@/hooks'
 import { onlyAllowNumberAndLetter } from '@/utils'
 import {
+  enterMaintenanceMode,
+  enterRtMode,
+  exitMaintenanceMode,
+  exitRtMode,
   operationPageList,
   resetVehicleState,
   updateAcceptOrTest,
   updateAcceptOrTestStatus,
-  updateOperatingStatus,
 } from '@/service/api/vehicleConvoy'
 
 const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
@@ -250,13 +253,14 @@
   {
     title: '操作',
     key: 'actions',
-    width: 300,
+    width: 360,
     fixed: 'right',
     render: (row) => {
-      // 收车/发车: operatingStatus 3=已收车,1=运营中(发车后)
-      const isParked = Number(row.operatingStatus) === 3
-      const dispatchLabel = isParked ? '发车' : '收车'
-      const nextOperatingStatus = isParked ? 1 : 3
+      // operatingStatus: 3=已收车(枢纽), 2=维修中(运维), 1/4/5=运营中
+      const os = Number(row.operatingStatus)
+      const isParked = os === 3
+      const isMaintenance = os === 2
+      const isOperating = os === 1 || os === 4 || os === 5
 
       // 开始/停止接单: 1=开始 2=停止
       const isAcceptOn = Number(row.acceptOrTestStatus) === 1
@@ -270,24 +274,57 @@
 
       return (
         <NSpace size={12} wrap={false} align="center">
+          {/* 重置state - 所有状态都显示 */}
           <NPopconfirm onPositiveClick={() => doResetState(row)}>
             {{
               trigger: () => <NButton text v-permission="50141" type="primary">重置state</NButton>,
               default: () => '确认将该车辆 state 重置为空闲(0)?',
             }}
           </NPopconfirm>
-          <NPopconfirm onPositiveClick={() => doUpdateOperatingStatus(row, nextOperatingStatus)}>
-            {{
-              trigger: () => <NButton text v-permission="50142" type="primary">{dispatchLabel}</NButton>,
-              default: () => `确认${dispatchLabel}该车辆?`,
-            }}
-          </NPopconfirm>
-          <NPopconfirm onPositiveClick={() => doUpdateAcceptOrTestStatus(row, nextAcceptStatus)}>
-            {{
-              trigger: () => <NButton text v-permission="50143" type="primary">{acceptStatusLabel}</NButton>,
-              default: () => `确认${acceptStatusLabel}?`,
-            }}
-          </NPopconfirm>
+          {/* 收车状态(3): 发车 + 进入运维模式 */}
+          {isParked && (
+            <NPopconfirm onPositiveClick={() => doEnterRtMode(row)}>
+              {{
+                trigger: () => <NButton text v-permission="50142" type="primary">发车</NButton>,
+                default: () => '确认发车该车辆?',
+              }}
+            </NPopconfirm>
+          )}
+          {isParked && (
+            <NPopconfirm onPositiveClick={() => doEnterMaintenanceMode(row)}>
+              {{
+                trigger: () => <NButton text v-permission="50145" type="primary">进入运维模式</NButton>,
+                default: () => '确认进入运维模式?',
+              }}
+            </NPopconfirm>
+          )}
+          {/* 运维模式(2): 退出运维模式 */}
+          {isMaintenance && (
+            <NPopconfirm onPositiveClick={() => doExitMaintenanceMode(row)}>
+              {{
+                trigger: () => <NButton text v-permission="50146" type="primary">退出运维模式</NButton>,
+                default: () => '确认退出运维模式?',
+              }}
+            </NPopconfirm>
+          )}
+          {/* 运营状态(1/4/5): 收车 + 开始/停止接单 */}
+          {isOperating && (
+            <NPopconfirm onPositiveClick={() => doExitRtMode(row)}>
+              {{
+                trigger: () => <NButton text v-permission="50142" type="primary">收车</NButton>,
+                default: () => '确认收车该车辆?',
+              }}
+            </NPopconfirm>
+          )}
+          {isOperating && (
+            <NPopconfirm onPositiveClick={() => doUpdateAcceptOrTestStatus(row, nextAcceptStatus)}>
+              {{
+                trigger: () => <NButton text v-permission="50143" type="primary">{acceptStatusLabel}</NButton>,
+                default: () => `确认${acceptStatusLabel}?`,
+              }}
+            </NPopconfirm>
+          )}
+          {/* 切换为测试/接单 - 所有状态都显示 */}
           <NPopconfirm onPositiveClick={() => doUpdateAcceptOrTest(row, nextAccept)}>
             {{
               trigger: () => <NButton text v-permission="50144" type="primary">{modeLabel}</NButton>,
@@ -318,34 +355,72 @@
 }
 
 async function doResetState(row: any) {
-  const { code } = await resetVehicleState(row.id)
+  const { code, msg } = await resetVehicleState(row.id)
   if (code === 200) {
     window.$message.success('已重置')
     getList()
+  } else {
+    window.$message.error(msg || '操作失败')
   }
 }
 
-async function doUpdateOperatingStatus(row: any, next: number) {
-  const { code } = await updateOperatingStatus(row.id, next)
+async function doEnterRtMode(row: any) {
+  const { code, msg } = await enterRtMode(row.id)
   if (code === 200) {
-    window.$message.success('操作成功')
+    window.$message.success('发车成功')
     getList()
+  } else {
+    window.$message.error(msg || '操作失败')
+  }
+}
+
+async function doExitRtMode(row: any) {
+  const { code, msg } = await exitRtMode(row.id)
+  if (code === 200) {
+    window.$message.success('收车成功')
+    getList()
+  } else {
+    window.$message.error(msg || '操作失败')
+  }
+}
+
+async function doEnterMaintenanceMode(row: any) {
+  const { code, msg } = await enterMaintenanceMode(row.id)
+  if (code === 200) {
+    window.$message.success('已进入运维模式')
+    getList()
+  } else {
+    window.$message.error(msg || '操作失败')
+  }
+}
+
+async function doExitMaintenanceMode(row: any) {
+  const { code, msg } = await exitMaintenanceMode(row.id)
+  if (code === 200) {
+    window.$message.success('已退出运维模式')
+    getList()
+  } else {
+    window.$message.error(msg || '操作失败')
   }
 }
 
 async function doUpdateAcceptOrTestStatus(row: any, next: number) {
-  const { code } = await updateAcceptOrTestStatus(row.id, next)
+  const { code, msg } = await updateAcceptOrTestStatus(row.id, next)
   if (code === 200) {
     window.$message.success('操作成功')
     getList()
+  } else {
+    window.$message.error(msg || '操作失败')
   }
 }
 
 async function doUpdateAcceptOrTest(row: any, next: number) {
-  const { code } = await updateAcceptOrTest(row.id, next)
+  const { code, msg } = await updateAcceptOrTest(row.id, next)
   if (code === 200) {
     window.$message.success('操作成功')
     getList()
+  } else {
+    window.$message.error(msg || '操作失败')
   }
 }
 
@@ -378,7 +453,7 @@
         <template #header>
           <span style="font-weight: 600; font-size: 15px;">⚠️ 谨慎操作</span>
         </template>
-        当前页所有操作(重置 state、收车/发车、开始/停止接单、接单/测试切换)<b>直接修改数据库字段,后端未做业务校验与状态机约束</b>,可能造成车辆状态不一致、订单异常或调度故障。请仅在确认风险的情况下使用。
+        当前页操作(重置 state、发车/收车、进入/退出运维模式、开始/停止接单、接单/测试切换)均经过后端状态机校验与 SWITCHING 互斥保护,失败时会返回具体原因。
       </n-alert>
 
       <div class="roboTaxi-form-card mb-12px">
diff --git a/src/views/transaction/order/evaluationRecord/FeedbackTab.vue b/src/views/transaction/order/evaluationRecord/FeedbackTab.vue
index 5812a38..bd6c557 100644
--- a/src/views/transaction/order/evaluationRecord/FeedbackTab.vue
+++ b/src/views/transaction/order/evaluationRecord/FeedbackTab.vue
@@ -12,6 +12,8 @@
 import AttachmentPreviewModal from '@/components/custom/AttachmentPreviewModal.vue'
 import { fetchFeedbackPageList, fetchAllFeedbackTypes, exportFeedbackList } from '@/service/api/serviceCenter'
 
+const props = defineProps<{ hasFeedbackType?: boolean }>()
+
 const router = useRouter()
 const { hasPermission } = usePermission()
 
@@ -69,7 +71,8 @@
 const pagination = ref({ page: 1, pageSize: 10 })
 const count = ref(0)
 
-const columns = computed<DataTableColumns<any>>(() => [
+const columns = computed<DataTableColumns<any>>(() => {
+  const cols: DataTableColumns<any> = [
   { type: 'selection' },
   {
     title: '序号',
@@ -107,7 +110,7 @@
       )
     },
   },
-  {
+  ...(props.hasFeedbackType !== false ? [{
     title: '反馈渠道',
     key: 'feedbackChannelText',
     width: 120,
@@ -158,7 +161,7 @@
       }
       return tagsNode
     },
-  },
+  }] : []),
   {
     title: '问题描述',
     key: 'feedbackContent',
@@ -215,100 +218,105 @@
     resizable: true,
     render: row => row.createTime ? dayjs(row.createTime).format('YYYY/MM/DD HH:mm') : '-',
   },
-])
+  ]
+  return cols
+})
 
-const filterItems = computed(() => [
-  {
-    label: '用户昵称/姓名',
-    path: 'nickName',
-    render: () => <n-input v-model:value={searchForm.value.nickName} placeholder="请输入" clearable />,
-  },
-  {
-    label: '用户手机号',
-    path: 'phone',
-    render: () => <n-input v-model:value={searchForm.value.phone} placeholder="请输入完整或后四位" clearable />,
-  },
-  {
-    label: '反馈渠道',
-    path: 'feedbackChannel',
-    render: () => (
-      <n-select
-        v-model:value={searchForm.value.feedbackChannel}
-        options={channelOptions}
-        placeholder="请选择"
-        clearable
-        onUpdate:show={(val: boolean) => isChannelOpen.value = val}
-      >
-        {{
-          arrow: () => (
-            <img
-              src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e120f81c-9f15-4e1a-9938-22252898edf9.png"
-              class={`arrow-icon ${isChannelOpen.value ? 'arrow-icon--open' : ''}`}
-              style={{ width: '16px', height: '16px' }}
-              alt="arrow"
-            />
-          ),
-          empty: () => <div class="flex-center"><img src={noDataImg} style={{ width: '50%' }} /></div>,
-        }}
-      </n-select>
-    ),
-  },
-  {
-    label: '问题类型',
-    path: 'feedbackTypeIds',
-    render: () => (
-      <n-select
-        v-model:value={searchForm.value.feedbackTypeIds}
-        options={feedbackTypeOptions.value}
-        multiple
-        filterable
-        max-tag-count="responsive"
-        clearable
-        placeholder="请选择"
-        onUpdate:show={(val: boolean) => isTypeOpen.value = val}
-      >
-        {{
-          arrow: () => (
-            <img
-              src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e120f81c-9f15-4e1a-9938-22252898edf9.png"
-              class={`arrow-icon ${isTypeOpen.value ? 'arrow-icon--open' : ''}`}
-              style={{ width: '16px', height: '16px' }}
-              alt="arrow"
-            />
-          ),
-          empty: () => <div class="flex-center"><img src={noDataImg} style={{ width: '50%' }} /></div>,
-        }}
-      </n-select>
-    ),
-  },
-  {
-    label: '问题描述',
-    path: 'feedbackContent',
-    render: () => <n-input v-model:value={searchForm.value.feedbackContent} placeholder="请输入关键字" clearable />,
-  },
-  {
-    label: '关联订单号',
-    path: 'orderNo',
-    render: () => <n-input v-model:value={searchForm.value.orderNo} allow-input={onlyAllowNumberAndLetter} placeholder="请输入" clearable />,
-  },
-  {
-    label: '评价时间',
-    path: 'createTimeRange',
-    render: () => (
-      <n-date-picker
-        v-model:value={searchForm.value.createTimeRange}
-        type="daterange"
-        format="yyyy/MM/dd"
-        clearable
-        actions={[]}
-        close-on-select={true}
-        start-placeholder="开始时间"
-        end-placeholder="结束时间"
-        separator="-"
-      />
-    ),
-  },
-])
+const filterItems = computed(() => {
+  const items = [
+    {
+      label: '用户昵称/姓名',
+      path: 'nickName',
+      render: () => <n-input v-model:value={searchForm.value.nickName} placeholder="请输入" clearable />,
+    },
+    {
+      label: '用户手机号',
+      path: 'phone',
+      render: () => <n-input v-model:value={searchForm.value.phone} placeholder="请输入完整或后四位" clearable />,
+    },
+    ...(props.hasFeedbackType !== false ? [{
+      label: '反馈渠道',
+      path: 'feedbackChannel',
+      render: () => (
+        <n-select
+          v-model:value={searchForm.value.feedbackChannel}
+          options={channelOptions}
+          placeholder="请选择"
+          clearable
+          onUpdate:show={(val: boolean) => isChannelOpen.value = val}
+        >
+          {{
+            arrow: () => (
+              <img
+                src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e120f81c-9f15-4e1a-9938-22252898edf9.png"
+                class={`arrow-icon ${isChannelOpen.value ? 'arrow-icon--open' : ''}`}
+                style={{ width: '16px', height: '16px' }}
+                alt="arrow"
+              />
+            ),
+            empty: () => <div class="flex-center"><img src={noDataImg} style={{ width: '50%' }} /></div>,
+          }}
+        </n-select>
+      ),
+    },
+    {
+      label: '问题类型',
+      path: 'feedbackTypeIds',
+      render: () => (
+        <n-select
+          v-model:value={searchForm.value.feedbackTypeIds}
+          options={feedbackTypeOptions.value}
+          multiple
+          filterable
+          max-tag-count="responsive"
+          clearable
+          placeholder="请选择"
+          onUpdate:show={(val: boolean) => isTypeOpen.value = val}
+        >
+          {{
+            arrow: () => (
+              <img
+                src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e120f81c-9f15-4e1a-9938-22252898edf9.png"
+                class={`arrow-icon ${isTypeOpen.value ? 'arrow-icon--open' : ''}`}
+                style={{ width: '16px', height: '16px' }}
+                alt="arrow"
+              />
+            ),
+            empty: () => <div class="flex-center"><img src={noDataImg} style={{ width: '50%' }} /></div>,
+          }}
+        </n-select>
+      ),
+    }] : []),
+    {
+      label: '问题描述',
+      path: 'feedbackContent',
+      render: () => <n-input v-model:value={searchForm.value.feedbackContent} placeholder="请输入关键字" clearable />,
+    },
+    {
+      label: '关联订单号',
+      path: 'orderNo',
+      render: () => <n-input v-model:value={searchForm.value.orderNo} allow-input={onlyAllowNumberAndLetter} placeholder="请输入" clearable />,
+    },
+    {
+      label: '评价时间',
+      path: 'createTimeRange',
+      render: () => (
+        <n-date-picker
+          v-model:value={searchForm.value.createTimeRange}
+          type="daterange"
+          format="yyyy/MM/dd"
+          clearable
+          actions={[]}
+          close-on-select={true}
+          start-placeholder="开始时间"
+          end-placeholder="结束时间"
+          separator="-"
+        />
+      ),
+    },
+  ]
+  return items
+})
 
 function handleSearch() {
   pagination.value.page = 1
@@ -332,6 +340,7 @@
     feedbackTypeIds: f.feedbackTypeIds.length ? f.feedbackTypeIds.join(',') : undefined,
     feedbackContent: f.feedbackContent.trim() || undefined,
     orderNo: f.orderNo.trim() || undefined,
+    hasFeedbackType: props.hasFeedbackType,
     createTimeStart: f.createTimeRange?.[0]
       ? dayjs(f.createTimeRange[0]).startOf('day').format('YYYY-MM-DD HH:mm:ss')
       : undefined,
@@ -368,7 +377,8 @@
   try {
     const params = buildQueryParams(true)
     const res = await exportFeedbackList(params)
-    downloadFile(res, `RT运营管理后台_意见反馈列表_${dayjs().format('YYYY_MM_DD')}.CSV`)
+    const exportName = props.hasFeedbackType === false ? '投诉列表' : '意见反馈列表'
+    downloadFile(res, `RT运营管理后台_${exportName}_${dayjs().format('YYYY_MM_DD')}.CSV`)
   } finally {
     exporting.value = false
   }
@@ -381,7 +391,10 @@
 }
 
 onMounted(() => {
-  loadFeedbackTypeOptions()
+  // 投诉tab无需加载问题类型选项
+  if (props.hasFeedbackType !== false) {
+    loadFeedbackTypeOptions()
+  }
   getList()
 })
 </script>
diff --git a/src/views/transaction/order/evaluationRecord/index.vue b/src/views/transaction/order/evaluationRecord/index.vue
index 4e41401..7152568 100644
--- a/src/views/transaction/order/evaluationRecord/index.vue
+++ b/src/views/transaction/order/evaluationRecord/index.vue
@@ -3,7 +3,7 @@
 import TripEvaluationTab from './TripEvaluationTab.vue'
 import FeedbackTab from './FeedbackTab.vue'
 
-const activeTab = ref<'tripEvaluation' | 'feedback'>('tripEvaluation')
+const activeTab = ref<'tripEvaluation' | 'feedback' | 'complaint'>('tripEvaluation')
 </script>
 
 <template>
@@ -19,7 +19,10 @@
         <TripEvaluationTab />
       </n-tab-pane>
       <n-tab-pane name="feedback" tab="意见反馈" class="h-full flex-1">
-        <FeedbackTab />
+        <FeedbackTab :has-feedback-type="true" />
+      </n-tab-pane>
+      <n-tab-pane name="complaint" tab="投诉" class="h-full flex-1">
+        <FeedbackTab :has-feedback-type="false" />
       </n-tab-pane>
     </n-tabs>
   </div>

--
Gitblit v1.7.1