ai-coding
2026-08-07 a3107a8f7b7fce1f2b92bc99a6daed7047b8dffc
feat(vehicle-order): 新增车辆模式切换并区分意见反馈与投诉筛选-lianjd

Co-Authored-By: 连建达 <lianjd@xiaopeng.com>
5个文件已修改
362 ■■■■■ 已修改文件
src/service/api/serviceCenter.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/service/api/vehicleConvoy.ts 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/assetManagement/vehicleManagement/vehicleState/index.vue 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/transaction/order/evaluationRecord/FeedbackTab.vue 209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/transaction/order/evaluationRecord/index.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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)
}
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)
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">
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>
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>