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