feat(vehicle-order): 新增车辆模式切换并区分意见反馈与投诉筛选-lianjd
Co-Authored-By: 连建达 <lianjd@xiaopeng.com>
| | |
| | | orderNo?: string |
| | | createTimeStart?: string |
| | | createTimeEnd?: string |
| | | hasFeedbackType?: boolean |
| | | }) { |
| | | return request.Post<Service.ResponseResult<any>>('/operation/api/v1/userFeedBack/pageList', data) |
| | | } |
| | |
| | | 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) |
| | |
| | | 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) |
| | |
| | | { |
| | | 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 |
| | |
| | | |
| | | 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>, |
| | |
| | | } |
| | | |
| | | 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 || '操作失败') |
| | | } |
| | | } |
| | | |
| | |
| | | <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"> |
| | |
| | | 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() |
| | | |
| | |
| | | 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: '序号', |
| | |
| | | ) |
| | | }, |
| | | }, |
| | | { |
| | | ...(props.hasFeedbackType !== false ? [{ |
| | | title: '反馈渠道', |
| | | key: 'feedbackChannelText', |
| | | width: 120, |
| | |
| | | } |
| | | return tagsNode |
| | | }, |
| | | }, |
| | | }] : []), |
| | | { |
| | | title: '问题描述', |
| | | key: 'feedbackContent', |
| | |
| | | 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 |
| | |
| | | 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, |
| | |
| | | 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 |
| | | } |
| | |
| | | } |
| | | |
| | | onMounted(() => { |
| | | loadFeedbackTypeOptions() |
| | | // 投诉tab无需加载问题类型选项 |
| | | if (props.hasFeedbackType !== false) { |
| | | loadFeedbackTypeOptions() |
| | | } |
| | | getList() |
| | | }) |
| | | </script> |
| | |
| | | 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> |
| | |
| | | <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> |