<script setup lang="tsx">
|
import { computed, h, onMounted, onUnmounted, ref, watch } from 'vue'
|
import { useRoute, useRouter } from 'vue-router'
|
import SelectPointModal from '@/views/testDemo/specialPointManagement/components/SelectPointModal.vue'
|
import VehicleSelectModal from '@/views/assetManagement/fleetManagement/components/VehicleSelectModal.vue'
|
import type { DataTableColumns } from 'naive-ui'
|
import dayjs from 'dayjs'
|
import { useBoolean } from '@/hooks'
|
import {
|
addTestOrder,
|
addTestPlan,
|
batchCancelTestPlan,
|
batchRetryTestPlan,
|
batchStartTestPlan,
|
cancelAndCloseTestOrder,
|
changeTestOrderAmount,
|
deleteTestOrderById,
|
editTestPlan,
|
fetchBatchProgress,
|
fetchTestOrderPageList,
|
fetchTestPlanDetail,
|
fetchVehicleProgress,
|
reassignTestOrderById,
|
} from '@/service/api/copyText'
|
import { getAvailableVehicleList } from '@/service/api/transaction'
|
|
const router = useRouter()
|
const route = useRoute()
|
const message = useMessage()
|
const dialog = useDialog()
|
|
interface TestPoint {
|
id: number
|
startTestPointId: number | null
|
startTestPointName: string
|
startTestPointAddress: string
|
endTestPointId: number | null
|
endTestPointName: string
|
endTestPointAddress: string
|
stopFlag: 0 | 1
|
testOrderType: 1 | 2 | null
|
}
|
|
interface TestOrderRow {
|
id: number
|
orderType: number
|
orderNumber: string
|
createTime: string
|
orderSource: number
|
startAddress: string
|
endAddress: string
|
plateNumber: string
|
fleetName: string
|
status: number
|
}
|
|
interface Vehicle {
|
id: number
|
vehicleCode: string
|
plateNumber: string
|
vehicleModel: string
|
fleetName: string
|
}
|
|
const planName = ref('')
|
const testPoints = ref<TestPoint[]>([])
|
const orders = ref<TestOrderRow[]>([])
|
const orderCount = ref(0)
|
const orderPageParams = ref({
|
pageNum: 1,
|
pageSize: 10,
|
})
|
|
const showPointSelectModal = ref(false)
|
const showVehicleModal = ref(false)
|
const vehicles = ref<Vehicle[]>([])
|
|
const orderSearchModel = ref({
|
orderNumber: '',
|
plateNumber: '',
|
status: null as number | null,
|
orderTimeRange: null as [number, number] | null,
|
})
|
const isOrderStatusOpen = ref(false)
|
const orderTableScrollX = 1440
|
|
const showOrderModal = ref(false)
|
const currentTestPointIndex = ref(-1)
|
const orderModalData = ref({
|
orderType: '测试订单',
|
isPark: true,
|
})
|
|
const isEdit = computed(() => route.query.mode === 'edit')
|
const isDetail = computed(() => route.query.mode === 'detail')
|
const isReadOnly = computed(() => isDetail.value)
|
const planId = ref(String(route.query.id || ''))
|
|
const initialSnapshot = ref('')
|
|
function buildSnapshot() {
|
return JSON.stringify({
|
planName: planName.value.trim(),
|
testPoints: testPoints.value.map(p => ({
|
startTestPointId: p.startTestPointId,
|
endTestPointId: p.endTestPointId,
|
stopFlag: p.stopFlag,
|
testOrderType: p.testOrderType,
|
})),
|
vehicles: vehicles.value.map(v => v.id),
|
})
|
}
|
|
function setInitialSnapshot() {
|
initialSnapshot.value = buildSnapshot()
|
}
|
|
function resolvePlanId(data: any) {
|
const id = data?.id ?? data?.planId ?? data?.testPlanId ?? data
|
if (id === null || id === undefined || id === '')
|
return ''
|
return String(id)
|
}
|
|
async function initCreatePlanId() {
|
const res = await addTestPlan()
|
if (res?.code !== 200) {
|
message.error(res?.message || '初始化测试计划失败')
|
return
|
}
|
const nextPlanId = resolvePlanId(res.data)
|
if (!nextPlanId) {
|
message.error('初始化测试计划失败')
|
return
|
}
|
planId.value = nextPlanId
|
}
|
|
|
function handleAddPoint() {
|
if (isReadOnly.value)
|
return
|
const newPoint: TestPoint = {
|
id: Date.now(),
|
startTestPointId: null,
|
startTestPointName: '',
|
startTestPointAddress: '',
|
endTestPointId: null,
|
endTestPointName: '',
|
endTestPointAddress: '',
|
stopFlag: 1,
|
testOrderType: null,
|
}
|
testPoints.value.push(newPoint)
|
}
|
|
const selectedPointIndex = ref(-1)
|
const selectedPointType = ref('')
|
|
function handleSelectPickupPoint(index: number) {
|
if (isReadOnly.value)
|
return
|
selectedPointIndex.value = index
|
selectedPointType.value = 'pickup'
|
showPointSelectModal.value = true
|
}
|
|
function handleSelectDropoffPoint(index: number) {
|
if (isReadOnly.value)
|
return
|
selectedPointIndex.value = index
|
selectedPointType.value = 'dropoff'
|
showPointSelectModal.value = true
|
}
|
|
const existingPointsForPointModal = computed(() => {
|
const index = selectedPointIndex.value
|
if (index < 0)
|
return []
|
const row = testPoints.value[index]
|
if (!row)
|
return []
|
if (selectedPointType.value === 'pickup' && row.startTestPointId) {
|
return [{
|
id: row.startTestPointId,
|
pointName: row.startTestPointName,
|
pointAddress: row.startTestPointAddress,
|
}]
|
}
|
if (selectedPointType.value === 'dropoff' && row.endTestPointId) {
|
return [{
|
id: row.endTestPointId,
|
pointName: row.endTestPointName,
|
pointAddress: row.endTestPointAddress,
|
}]
|
}
|
return []
|
})
|
|
function handleConfirmPointSelect(points: any[]) {
|
console.log('points',JSON.parse(JSON.stringify(points)))
|
console.log('selectedPointIndex.value',selectedPointIndex.value)
|
console.log('testPoints.value',testPoints.value)
|
const point = points[0]
|
if (point && selectedPointIndex.value !== -1) {
|
const currentPoint = testPoints.value[selectedPointIndex.value]
|
if (currentPoint) {
|
if (selectedPointType.value === 'pickup' && currentPoint.endTestPointId && String(currentPoint.endTestPointId) === String(point.id)) {
|
message.warning('同一行上车点和下车点不能相同')
|
return
|
}
|
if (selectedPointType.value === 'dropoff' && currentPoint.startTestPointId && String(currentPoint.startTestPointId) === String(point.id)) {
|
message.warning('同一行上车点和下车点不能相同')
|
return
|
}
|
if (selectedPointType.value === 'pickup') {
|
currentPoint.startTestPointId = point.id
|
currentPoint.startTestPointName = point.pointName || point.name || ''
|
currentPoint.startTestPointAddress = point.pointAddress || point.address || ''
|
}
|
if (selectedPointType.value === 'dropoff') {
|
currentPoint.endTestPointId = point.id
|
currentPoint.endTestPointName = point.pointName || point.name || ''
|
currentPoint.endTestPointAddress = point.pointAddress || point.address || ''
|
}
|
}
|
testPoints.value = [...testPoints.value]
|
}
|
selectedPointIndex.value = -1
|
selectedPointType.value = ''
|
showPointSelectModal.value = false
|
}
|
|
function handleGenerateOrder(index: number) {
|
if (isReadOnly.value)
|
return
|
if (vehicles.value.length === 0) {
|
message.warning('请先选择车辆')
|
return
|
}
|
const testPoint = testPoints.value[index]
|
if (!testPoint.startTestPointId || !testPoint.endTestPointId) {
|
message.warning('请先选择上车点和下车点')
|
return
|
}
|
if (String(testPoint.startTestPointId) === String(testPoint.endTestPointId)) {
|
message.warning('同一行上车点和下车点不能相同')
|
return
|
}
|
currentTestPointIndex.value = index
|
orderModalData.value = {
|
orderType: testPoint.testOrderType === 2 ? '巡游订单' : '测试订单',
|
isPark: testPoint.stopFlag === 1,
|
}
|
showOrderModal.value = true
|
}
|
|
async function handleConfirmGenerateOrder() {
|
if (currentTestPointIndex.value === -1)
|
return
|
|
const testPoint = testPoints.value[currentTestPointIndex.value]
|
if (!testPoint)
|
return
|
if (!planId.value) {
|
message.warning('测试计划初始化中,请稍后再试')
|
return
|
}
|
const orderTypeValue = orderModalData.value.orderType === '巡游订单' ? 2 : 1
|
const stopFlag = orderTypeValue === 1 ? 1 : (orderModalData.value.isPark ? 1 : 0)
|
try {
|
const res = await addTestOrder({
|
endTestPointId: String(testPoint.endTestPointId || ''),
|
orderType: orderTypeValue,
|
startTestPointId: String(testPoint.startTestPointId || ''),
|
stopFlag,
|
testPlanId: planId.value || '',
|
vehicleIds: vehicles.value.map(v => v.id),
|
})
|
if (res?.code === 200) {
|
testPoint.testOrderType = orderTypeValue
|
testPoint.stopFlag = stopFlag as 0 | 1
|
showOrderModal.value = false
|
message.success('订单生成成功')
|
getOrderList()
|
return
|
}
|
message.error(res?.message || '订单生成失败')
|
}
|
catch {
|
message.error('订单生成失败,请稍后重试')
|
}
|
}
|
|
function handleRemovePoint(index: number) {
|
if (isReadOnly.value)
|
return
|
testPoints.value.splice(index, 1)
|
}
|
|
function handleResetOrderSearch() {
|
orderSearchModel.value = {
|
orderNumber: '',
|
plateNumber: '',
|
status: null,
|
orderTimeRange: null,
|
}
|
orderPageParams.value.pageNum = 1
|
getOrderList()
|
}
|
|
function handleSearchOrders() {
|
orderPageParams.value.pageNum = 1
|
getOrderList()
|
}
|
|
const { bool: orderLoading, setTrue: startOrderLoading, setFalse: endOrderLoading } = useBoolean(false)
|
|
const statusOptions = [
|
{ label: '待接单', value: 1, type: 'primary' },
|
{ label: '待出发', value: 2, type: 'success' },
|
{ label: '接驾中', value: 3, type: 'warning' },
|
{ label: '待客上车', value: 4, type: 'primary' },
|
{ label: '乘客已上车', value: 5, type: 'success' },
|
{ label: '服务中', value: 6, type: 'success' },
|
{ label: '待客下车', value: 7, type: 'warning' },
|
{ label: '待付款', value: 8, type: 'warning' },
|
{ label: '待评价', value: 9, type: 'default' },
|
{ label: '已完成', value: 10, type: 'success' },
|
{ label: '已取消', value: 0, type: 'default' },
|
]
|
const orderStatusOptions = statusOptions.map(item => ({ label: item.label, value: item.value }))
|
|
function getOrderTypeLabel(type: number) {
|
return type === 2 ? '巡游订单' : type === 1 ? '测试订单' : '-'
|
}
|
|
function getOrderSourceLabel(source: number) {
|
return source === 1 ? '系统自动创建' : '-'
|
}
|
|
function getOrderStatusLabel(status: number) {
|
return statusOptions.find(item => item.value === status)?.label || '-'
|
}
|
|
async function getOrderList() {
|
startOrderLoading()
|
try {
|
const orderNumber = orderSearchModel.value.orderNumber?.trim()
|
const plateNumber = orderSearchModel.value.plateNumber?.trim()
|
const range = orderSearchModel.value.orderTimeRange
|
const startTime = range?.[0] ? dayjs(range[0]).format('YYYY-MM-DD') : undefined
|
const endTime = range?.[1] ? dayjs(range[1]).format('YYYY-MM-DD') : undefined
|
const res = await fetchTestOrderPageList({
|
...orderPageParams.value,
|
orderNumber: orderNumber || undefined,
|
plateNumber: plateNumber || undefined,
|
status: orderSearchModel.value.status ?? undefined,
|
startTime,
|
endTime,
|
testPlanId: planId.value || undefined,
|
})
|
console.log('res',res)
|
if (res.code === 200) {
|
orders.value = res.data?.records || []
|
orderCount.value = res.data?.total || 0
|
return
|
}
|
orders.value = []
|
orderCount.value = 0
|
}
|
finally {
|
endOrderLoading()
|
}
|
}
|
|
function changeOrderPage(page: number, size: number) {
|
orderPageParams.value.pageNum = page
|
orderPageParams.value.pageSize = size
|
getOrderList()
|
}
|
|
function handleCancelOrder(order: TestOrderRow) {
|
dialog.warning({
|
title: '确认取消订单?',
|
content: '',
|
positiveText: '确定',
|
negativeText: '取消',
|
maskClosable: false,
|
autoFocus: false,
|
onPositiveClick: async () => {
|
const res = await cancelAndCloseTestOrder({ orderId: order.id, type: 1 })
|
if (res.code === 200) {
|
message.success('取消订单成功')
|
getOrderList()
|
}
|
},
|
})
|
}
|
|
function handleDeleteOrder(order: TestOrderRow) {
|
dialog.warning({
|
title: '确认删除订单?',
|
content: '',
|
positiveText: '确定',
|
negativeText: '取消',
|
maskClosable: false,
|
autoFocus: false,
|
onPositiveClick: async () => {
|
const res = await deleteTestOrderById(order.id)
|
if (res.code === 200) {
|
message.success('删除订单成功')
|
getOrderList()
|
}
|
},
|
})
|
}
|
|
const showReassignModal = ref(false)
|
const isReassignVehicleOpen = ref(false)
|
const reassignVehicleOptions = ref<{ label: string, value: number }[]>([])
|
const reassignForm = ref({
|
orderId: 0,
|
vehicleId: null as number | null,
|
reassignReason: '',
|
})
|
|
async function fetchReassignVehicleOptions(orderId: number) {
|
const res = await getAvailableVehicleList({orderId,orderType:2})
|
if (res.code === 200) {
|
reassignVehicleOptions.value = (res.data || []).map((item: any) => ({
|
label: item.plateNumber || item.vehicleCode || `车辆${item.id}`,
|
value: item.id,
|
}))
|
return
|
}
|
reassignVehicleOptions.value = []
|
}
|
|
async function handleReassignOrder(order: TestOrderRow) {
|
reassignForm.value = {
|
orderId: order.id,
|
vehicleId: null,
|
reassignReason: '',
|
}
|
await fetchReassignVehicleOptions(order.id)
|
showReassignModal.value = true
|
}
|
|
async function handleConfirmReassign() {
|
if (!reassignForm.value.vehicleId || !reassignForm.value.reassignReason.trim()) {
|
message.warning('请选择车辆并填写改派原因')
|
return
|
}
|
const res = await reassignTestOrderById({
|
orderId: reassignForm.value.orderId,
|
vehicleId: reassignForm.value.vehicleId,
|
reassignReason: reassignForm.value.reassignReason.trim(),
|
})
|
if (res.code === 200) {
|
message.success('改派成功')
|
showReassignModal.value = false
|
getOrderList()
|
}
|
}
|
|
function handleCloseOrder(order: TestOrderRow) {
|
dialog.warning({
|
title: '确认关闭订单?',
|
content: '',
|
positiveText: '确定',
|
negativeText: '取消',
|
maskClosable: false,
|
autoFocus: false,
|
onPositiveClick: async () => {
|
const res = await cancelAndCloseTestOrder({ orderId: order.id, type: 2 })
|
if (res.code === 200) {
|
message.success('关闭订单成功')
|
getOrderList()
|
}
|
},
|
})
|
}
|
|
function handleOrderDetail(order: TestOrderRow) {
|
if (order.orderType === 2) {
|
router.push({ name: 'patrolPlanOrderDetail', query: { orderId: String(order.id), type: 'cruise' } })
|
return
|
}
|
router.push({ name: 'orderDetail', query: { orderId: String(order.id), type: 'test' } })
|
}
|
|
const showChangeAmountModal = ref(false)
|
const changeAmountForm = ref({
|
id: 0,
|
amount: null as number | null,
|
})
|
|
function handleChangeAmount(order: TestOrderRow) {
|
changeAmountForm.value = { id: order.id, amount: null }
|
showChangeAmountModal.value = true
|
}
|
|
async function handleConfirmChangeAmount() {
|
if (changeAmountForm.value.amount === null) {
|
message.warning('请输入订单金额')
|
return
|
}
|
const res = await changeTestOrderAmount({ id: changeAmountForm.value.id, amount: changeAmountForm.value.amount })
|
if (res.code === 200) {
|
message.success('改价成功')
|
showChangeAmountModal.value = false
|
getOrderList()
|
}
|
}
|
|
const orderColumns = computed<DataTableColumns<TestOrderRow>>(() => [
|
{
|
title: '订单类型',
|
key: 'orderType',
|
width: 100,
|
align: 'left',
|
render: (row) => getOrderTypeLabel(row.orderType),
|
},
|
{
|
title: '订单编号',
|
key: 'orderNumber',
|
width: 180,
|
align: 'left',
|
},
|
{
|
title: '下单时间',
|
key: 'createTime',
|
width: 160,
|
align: 'left',
|
render: row => <span>{row.createTime ? dayjs(row.createTime).format('YYYY/MM/DD HH:mm') : '-'}</span>,
|
},
|
{
|
title: '订单来源',
|
key: 'orderSource',
|
width: 100,
|
align: 'left',
|
render: row => <span>{getOrderSourceLabel(row.orderSource)}</span>,
|
},
|
{
|
title: '起点',
|
key: 'startAddress',
|
width: 160,
|
align: 'left',
|
},
|
{
|
title: '终点',
|
key: 'endAddress',
|
width: 160,
|
align: 'left',
|
},
|
{
|
title: '接单车辆',
|
key: 'plateNumber',
|
width: 120,
|
align: 'left',
|
},
|
{
|
title: '归属车队',
|
key: 'fleetName',
|
width: 120,
|
align: 'left',
|
},
|
{
|
title: '状态',
|
key: 'status',
|
width: 100,
|
align: 'left',
|
render: (row: TestOrderRow) => {
|
const option = statusOptions.find(item => item.value == row.status)
|
console.log('option', option,'row.status',row.status)
|
return <n-tag type={option?.type || 'default'} size="small">{option?.label || '-'}</n-tag>
|
},
|
},
|
{
|
title: '操作',
|
key: 'actions',
|
width: 240,
|
align: 'left',
|
fixed: 'right' as const,
|
render: (row: TestOrderRow) => {
|
return (
|
<n-space size={2}>
|
{(row.status === 1 || row.status === 2 || row.status === 3 || row.status === 4) && (
|
<n-button text type="primary" size="small" onClick={() => handleCancelOrder(row)}>
|
取消订单
|
</n-button>
|
)}
|
{row.status === 6 && (
|
<n-button text type="primary" size="small" onClick={() => handleCloseOrder(row)}>
|
关闭订单
|
</n-button>
|
)}
|
{row.status === 8 && (
|
<n-button text type="primary" size="small" onClick={() => handleChangeAmount(row)}>
|
改价
|
</n-button>
|
)}
|
{(row.status === 2 || row.status === 3 || row.status === 4) && (
|
<n-button text type="primary" size="small" onClick={() => handleReassignOrder(row)}>
|
改派
|
</n-button>
|
)}
|
<n-button text type="primary" size="small" onClick={() => handleOrderDetail(row)}>
|
详情
|
</n-button>
|
{row.status === 0 && (
|
<n-button text type="error" size="small" onClick={() => handleDeleteOrder(row)}>
|
删除
|
</n-button>
|
)}
|
</n-space>
|
)
|
},
|
},
|
])
|
|
const vehicleTableScrollX = 680
|
const vehicleColumns = computed<DataTableColumns<Vehicle>>(() => [
|
{
|
title: '序号',
|
key: 'index',
|
width: 60,
|
align: 'left',
|
render: (_row, index) => index + 1,
|
},
|
{
|
title: '车辆编码',
|
key: 'vehicleCode',
|
width: 160,
|
align: 'left',
|
},
|
{
|
title: '车牌号',
|
key: 'plateNumber',
|
width: 120,
|
align: 'left',
|
},
|
{
|
title: '型号',
|
key: 'vehicleModel',
|
width: 120,
|
align: 'left',
|
},
|
{
|
title: '所属车队',
|
key: 'fleetName',
|
width: 140,
|
align: 'left',
|
},
|
{
|
title: '操作',
|
key: 'actions',
|
width: 80,
|
align: 'left',
|
render: (row: Vehicle) => (
|
<n-button
|
text
|
type="primary"
|
size="small"
|
disabled={isReadOnly.value}
|
onClick={() => handleRemoveVehicle(row)}
|
>
|
移除
|
</n-button>
|
),
|
},
|
])
|
|
const testPointTableScrollX = 960
|
const testPointColumns = computed<DataTableColumns<TestPoint>>(() => [
|
{
|
title: '排序',
|
key: 'index',
|
width: 60,
|
align: 'left',
|
render: (_row, index) => index + 1,
|
},
|
{
|
title: '上车点',
|
key: 'pickupPoint',
|
width: 200,
|
align: 'left',
|
render: (row: TestPoint) => row.startTestPointName || '-',
|
},
|
{
|
title: '下车点',
|
key: 'dropoffPoint',
|
width: 200,
|
align: 'left',
|
render: (row: TestPoint) => row.endTestPointName || '-',
|
},
|
{
|
title: '是否停车',
|
key: 'isPark',
|
width: 100,
|
align: 'left',
|
render: (row: TestPoint) => (row.stopFlag === 1 ? '是' : '否'),
|
},
|
{
|
title: '订单类型',
|
key: 'orderType',
|
width: 120,
|
align: 'left',
|
render: (row: TestPoint) => row.testOrderType == 2 ? '巡游订单' : row.testOrderType == 1 ? '测试订单' : '-',
|
},
|
{
|
title: '操作',
|
key: 'actions',
|
width: 280,
|
align: 'left',
|
render: (_row, index) => (
|
<n-space size={2}>
|
<n-button
|
text
|
type="primary"
|
size="small"
|
disabled={isReadOnly.value}
|
onClick={() => handleSelectPickupPoint(index)}
|
>
|
选择上车点
|
</n-button>
|
<n-button
|
text
|
type="primary"
|
size="small"
|
disabled={isReadOnly.value}
|
onClick={() => handleSelectDropoffPoint(index)}
|
>
|
选择下车点
|
</n-button>
|
<n-button
|
text
|
type="primary"
|
size="small"
|
disabled={isReadOnly.value}
|
onClick={() => handleGenerateOrder(index)}
|
>
|
生成订单
|
</n-button>
|
<n-button
|
text
|
type="error"
|
size="small"
|
disabled={isReadOnly.value}
|
onClick={() => handleRemovePoint(index)}
|
>
|
移除
|
</n-button>
|
</n-space>
|
),
|
},
|
])
|
|
function handleResetVehicles() {
|
vehicles.value = []
|
}
|
|
function handleSelectVehicle() {
|
showVehicleModal.value = true
|
}
|
|
function handleConfirmVehicleSelect(selectedVehicles: any[]) {
|
vehicles.value = selectedVehicles.map(v => ({
|
id: v.id,
|
vehicleCode: v.vehicleCode,
|
plateNumber: v.plateNumber,
|
vehicleModel: v.vehicleModel,
|
fleetName: v.fleetName,
|
}))
|
}
|
|
function handleRemoveVehicle(vehicle: Vehicle) {
|
vehicles.value = vehicles.value.filter(v => v.id !== vehicle.id)
|
}
|
|
function handleCancel() {
|
if (buildSnapshot() === initialSnapshot.value) {
|
router.back()
|
return
|
}
|
|
dialog.warning({
|
title: '确认离开?',
|
content: '当前已填写/选择内容,离开后将丢失,确定离开吗?',
|
positiveText: '确定离开',
|
negativeText: '继续编辑',
|
maskClosable: false,
|
autoFocus: false,
|
onPositiveClick: () => {
|
router.back()
|
},
|
})
|
}
|
|
async function getDetail() {
|
if (!planId.value)
|
return
|
|
const res = await fetchTestPlanDetail(planId.value)
|
if (res.code === 200) {
|
planName.value = res.data?.planName || ''
|
const pointList = (res.data?.pointList || []).slice()
|
testPoints.value = pointList.map((p: any, index: number) => ({
|
id: Date.now() + index,
|
startTestPointId: p.startTestPointId,
|
startTestPointName: p.startTestPointName || '',
|
startTestPointAddress: p.startTestPointAddress || '',
|
endTestPointId: p.endTestPointId,
|
endTestPointName: p.endTestPointName || '',
|
endTestPointAddress: p.endTestPointAddress || '',
|
stopFlag: p.stopFlag === 1 ? 1 : 0,
|
testOrderType: p.testOrderType || null,
|
}))
|
vehicles.value = (res.data?.vehicleList || []).map((v: any) => ({
|
id: v.vehicleId,
|
vehicleCode: v.vehicleCode,
|
plateNumber: v.plateNumber,
|
vehicleModel: v.vehicleModel,
|
fleetName: v.fleetName,
|
}))
|
setInitialSnapshot()
|
getOrderList()
|
return
|
}
|
planName.value = ''
|
testPoints.value = []
|
vehicles.value = []
|
setInitialSnapshot()
|
}
|
|
async function handleSave() {
|
if (isReadOnly.value) {
|
router.back()
|
return
|
}
|
if (!planId.value) {
|
message.warning('测试计划初始化中,请稍后再试')
|
return
|
}
|
const name = planName.value.trim()
|
if (!name) {
|
message.warning('请输入计划名称')
|
return
|
}
|
if (testPoints.value.length === 0) {
|
message.warning('请至少添加一个测试点位')
|
return
|
}
|
const invalidPointIndex = testPoints.value.findIndex((p) => {
|
const startId = String(p.startTestPointId || '').trim()
|
const endId = String(p.endTestPointId || '').trim()
|
return !startId || !endId
|
})
|
if (invalidPointIndex > -1) {
|
message.warning(`第${invalidPointIndex + 1}行请先选择上车点和下车点`)
|
return
|
}
|
const samePointIndex = testPoints.value.findIndex((p) => String(p.startTestPointId) === String(p.endTestPointId))
|
if (samePointIndex > -1) {
|
message.warning(`第${samePointIndex + 1}行上车点和下车点不能相同`)
|
return
|
}
|
const payload = {
|
id: planId.value,
|
planName: name,
|
pointList: testPoints.value.map(p => ({
|
startTestPointId: p.startTestPointId || 0,
|
endTestPointId: p.endTestPointId || 0,
|
stopFlag: p.stopFlag,
|
testOrderType: p.testOrderType || 1,
|
})),
|
vehicleIdList: vehicles.value.map(v => v.id),
|
}
|
const res = await editTestPlan(payload)
|
if (res.code === 200) {
|
message.success('保存成功')
|
window.sessionStorage.setItem('testPlanListNeedRefresh', '1')
|
router.back()
|
}
|
}
|
|
// ==================== 批量测试相关 ====================
|
const lapCount = ref(1)
|
const batchProgress = ref<any>(null)
|
const vehicleProgressList = ref<any[]>([])
|
let batchPollTimer: ReturnType<typeof setInterval> | null = null
|
|
const batchStatusMap: Record<string, { label: string, type: 'default' | 'success' | 'warning' | 'error' | 'info' }> = {
|
RUNNING: { label: '运行中', type: 'warning' },
|
SUCCESS: { label: '成功', type: 'success' },
|
PARTIAL_SUCCESS: { label: '部分成功', type: 'warning' },
|
FAILED: { label: '失败', type: 'error' },
|
}
|
|
function getBatchStatusLabel(status: string) {
|
return batchStatusMap[status]?.label || status || '-'
|
}
|
|
function getBatchStatusType(status: string) {
|
return batchStatusMap[status]?.type || 'default'
|
}
|
|
function getVehicleProgressStatusLabel(status: string) {
|
const map: Record<string, { label: string, type: 'default' | 'success' | 'warning' | 'error' | 'info' }> = {
|
RUNNING: { label: '运行中', type: 'warning' },
|
COMPLETED: { label: '已完成', type: 'success' },
|
ERROR: { label: '异常', type: 'error' },
|
CANCELLED: { label: '已取消', type: 'default' },
|
PENDING: { label: '待执行', type: 'info' },
|
}
|
return map[status]?.label || status || '-'
|
}
|
|
function getVehicleProgressStatusType(status: string) {
|
const map: Record<string, 'default' | 'success' | 'warning' | 'error' | 'info'> = {
|
RUNNING: 'warning',
|
COMPLETED: 'success',
|
ERROR: 'error',
|
CANCELLED: 'default',
|
PENDING: 'info',
|
}
|
return map[status] || 'default'
|
}
|
|
const batchProgressPercent = computed(() => {
|
if (!batchProgress.value || !batchProgress.value.totalCount) return 0
|
return Math.round((batchProgress.value.completedCount / batchProgress.value.totalCount) * 100)
|
})
|
|
const isBatchRunning = computed(() => batchProgress.value?.status === 'RUNNING')
|
|
async function getBatchProgress() {
|
if (!planId.value) return
|
const res = await fetchBatchProgress(planId.value)
|
if (res.code === 200 && res.data) {
|
batchProgress.value = res.data
|
}
|
}
|
|
async function getVehicleProgressList() {
|
if (!batchProgress.value || !batchProgress.value.batchId) return
|
const res = await fetchVehicleProgress(batchProgress.value.batchId)
|
if (res.code === 200) {
|
vehicleProgressList.value = res.data || []
|
}
|
}
|
|
async function refreshBatchData() {
|
await getBatchProgress()
|
await getVehicleProgressList()
|
}
|
|
function startBatchPolling() {
|
stopBatchPolling()
|
batchPollTimer = setInterval(() => {
|
refreshBatchData()
|
}, 5000)
|
}
|
|
function stopBatchPolling() {
|
if (batchPollTimer) {
|
clearInterval(batchPollTimer)
|
batchPollTimer = null
|
}
|
}
|
|
async function handleBatchStart() {
|
if (!planId.value) {
|
message.warning('测试计划初始化中,请稍后再试')
|
return
|
}
|
if (vehicles.value.length === 0) {
|
message.warning('请先选择车辆')
|
return
|
}
|
if (testPoints.value.length === 0) {
|
message.warning('请至少添加一个测试点位')
|
return
|
}
|
if (!lapCount.value || lapCount.value < 1) {
|
message.warning('请输入有效的圈数(至少1圈)')
|
return
|
}
|
|
dialog.create({
|
title: '确认启动批量测试?',
|
content: `将为 ${vehicles.value.length} 辆车按 ${testPoints.value.length} 个点位组合生成链式巡游订单,每车跑 ${lapCount.value} 圈`,
|
positiveText: '确定',
|
negativeText: '取消',
|
maskClosable: false,
|
autoFocus: false,
|
onPositiveClick: async () => {
|
const res = await batchStartTestPlan({ testPlanId: planId.value, lapCount: lapCount.value })
|
if (res.code === 200) {
|
message.success('批量测试已启动')
|
await refreshBatchData()
|
startBatchPolling()
|
}
|
else {
|
message.error(res?.message || '启动失败')
|
}
|
},
|
})
|
}
|
|
async function handleBatchRetry() {
|
if (!planId.value) return
|
const res = await batchRetryTestPlan(planId.value)
|
if (res.code === 200) {
|
message.success('重试已提交')
|
await refreshBatchData()
|
if (isBatchRunning.value) startBatchPolling()
|
}
|
else {
|
message.error(res?.message || '重试失败')
|
}
|
}
|
|
async function handleBatchCancel() {
|
if (!planId.value) return
|
dialog.warning({
|
title: '确认取消批量测试?',
|
content: '取消后未完成的订单将不再执行',
|
positiveText: '确定',
|
negativeText: '取消',
|
maskClosable: false,
|
autoFocus: false,
|
onPositiveClick: async () => {
|
const res = await batchCancelTestPlan(planId.value)
|
if (res.code === 200) {
|
message.success('已取消')
|
stopBatchPolling()
|
await refreshBatchData()
|
}
|
else {
|
message.error(res?.message || '取消失败')
|
}
|
},
|
})
|
}
|
|
const vehicleProgressTableScrollX = 960
|
const vehicleProgressColumns = computed<DataTableColumns<any>>(() => [
|
{
|
title: '序号',
|
key: 'index',
|
width: 60,
|
align: 'left',
|
render: (_row, index) => index + 1,
|
},
|
{
|
title: '车牌号',
|
key: 'plateNumber',
|
width: 120,
|
align: 'left',
|
},
|
{
|
title: '当前圈数/总圈数',
|
key: 'currentLap',
|
width: 140,
|
align: 'left',
|
render: row => `${row.currentLap || 0}/${row.totalLaps || '-'}`,
|
},
|
{
|
title: '当前段/总段数',
|
key: 'currentSegment',
|
width: 140,
|
align: 'left',
|
render: row => `${row.currentSegment || 0}/${row.totalSegments || '-'}`,
|
},
|
{
|
title: '当前订单ID',
|
key: 'currentOrderId',
|
width: 140,
|
align: 'left',
|
render: row => row.currentOrderId || '-',
|
},
|
{
|
title: '状态',
|
key: 'status',
|
width: 100,
|
align: 'left',
|
render: row => <n-tag type={getVehicleProgressStatusType(row.status)} size="small">{getVehicleProgressStatusLabel(row.status)}</n-tag>,
|
},
|
{
|
title: '错误信息',
|
key: 'errorMsg',
|
width: 200,
|
align: 'left',
|
ellipsis: true,
|
render: row => row.errorMsg || '-',
|
},
|
])
|
|
// 监听批次状态,自动停止轮询
|
watch(() => batchProgress.value?.status, (status) => {
|
if (status && status !== 'RUNNING') {
|
stopBatchPolling()
|
}
|
})
|
|
onMounted(() => {
|
if (isEdit.value || isDetail.value) {
|
planId.value = String(route.query.id || '')
|
getDetail()
|
getOrderList()
|
refreshBatchData()
|
}
|
else {
|
initCreatePlanId()
|
setInitialSnapshot()
|
}
|
})
|
|
onUnmounted(() => {
|
stopBatchPolling()
|
})
|
|
watch(() => route.query.id, (id) => {
|
if (isEdit.value || isDetail.value) {
|
planId.value = String(id || '')
|
}
|
})
|
|
watch(() => planId.value, () => {
|
if (isEdit.value || isDetail.value) {
|
getDetail()
|
getOrderList()
|
refreshBatchData()
|
}
|
})
|
</script>
|
|
<template>
|
<div class="h-full flex flex-col gap-4">
|
<n-card style="border-radius: 6px" content-style="padding: 16px 24px;">
|
<div class="text-lg font-bold">
|
{{ isDetail ? "测试计划详情" : isEdit ? "编辑测试计划" : "新增测试计划" }}
|
</div>
|
</n-card>
|
|
<n-card style="border-radius: 6px">
|
<div class="mb-4 flex items-center">
|
<div class="mr-2 bg-blue-500" style="width: 3px; height: 14px" />
|
<span class="font-bold" style="font-size: 16px">基础信息</span>
|
</div>
|
<div class="flex items-center gap-2">
|
<div style="font-size: 13px">
|
<span style="color: #ff4d4f">*</span>计划名称
|
</div>
|
<n-input
|
v-model:value="planName"
|
class="flex-1"
|
placeholder="请输入"
|
clearable
|
style="width: 400px"
|
maxlength="30"
|
show-count
|
:disabled="isReadOnly"
|
/>
|
</div>
|
</n-card>
|
|
<n-card style="border-radius: 6px" content-style="padding: 24px;">
|
<div class="mb-4 flex items-center">
|
<div class="mr-2 bg-blue-500" style="width: 3px; height: 14px" />
|
<span class="font-bold" style="font-size: 16px">测试车辆</span>
|
</div>
|
<div class="mb-4 flex items-center gap-2">
|
<div class="text-sm">测试车辆</div>
|
<div class="flex gap-2">
|
<n-button
|
style="
|
height: 28px;
|
border: 1px solid #7abdff;
|
background-color: #e6f4ff;
|
color: #0068ff;
|
"
|
@click="handleSelectVehicle"
|
:disabled="isReadOnly"
|
>
|
选择
|
</n-button>
|
<n-button style="height: 28px" @click="handleResetVehicles" :disabled="isReadOnly">
|
重置
|
</n-button>
|
</div>
|
</div>
|
|
<n-data-table
|
:columns="vehicleColumns"
|
:data="vehicles"
|
:bordered="false"
|
:scroll-x="vehicleTableScrollX"
|
:header-height="18"
|
:row-height="18"
|
:row-key="row => row.id"
|
/>
|
</n-card>
|
|
<n-card style="border-radius: 6px" content-style="padding: 24px;">
|
<div class="mb-4 flex items-center justify-between">
|
<div class="flex items-center">
|
<div class="mr-2 bg-blue-500" style="width: 3px; height: 14px" />
|
<span class="font-bold" style="font-size: 16px">测试点位信息</span>
|
</div>
|
<n-button
|
style="
|
height: 28px;
|
border: 1px solid #7abdff;
|
background-color: #e6f4ff;
|
color: #0068ff;
|
"
|
@click="handleAddPoint"
|
:disabled="isReadOnly"
|
>
|
添加
|
</n-button>
|
</div>
|
|
<n-data-table
|
:columns="testPointColumns"
|
:data="testPoints"
|
:bordered="false"
|
:scroll-x="testPointTableScrollX"
|
:header-height="18"
|
:row-height="18"
|
:row-key="row => row.id"
|
/>
|
</n-card>
|
|
<n-card
|
style="border-radius: 6px; margin-bottom: 80px; min-height: 400px"
|
content-style="display: flex; flex-direction: column; padding: 24px;"
|
>
|
<div class="mb-4 flex items-center">
|
<div class="mr-2 bg-blue-500" style="width: 3px; height: 14px" />
|
<span class="font-bold" style="font-size: 16px">订单列表</span>
|
</div>
|
|
<div class="mb-4 flex flex-wrap gap-4">
|
<div class="flex items-center gap-2">
|
<div class="text-sm" style="width: 70px">订单号</div>
|
<n-input
|
v-model:value="orderSearchModel.orderNumber"
|
placeholder="请输入订单号"
|
clearable
|
style="width: 200px"
|
/>
|
</div>
|
<div class="flex items-center gap-2">
|
<div class="text-sm" style="width: 70px">车辆</div>
|
<n-input
|
v-model:value="orderSearchModel.plateNumber"
|
placeholder="请输入车辆"
|
clearable
|
style="width: 200px"
|
/>
|
</div>
|
<div class="flex items-center gap-2">
|
<div class="text-sm" style="width: 70px">订单状态</div>
|
<n-select
|
v-model:value="orderSearchModel.status"
|
placeholder="请选择订单状态"
|
clearable
|
style="width: 150px"
|
:options="orderStatusOptions"
|
@update:show="(val) => isOrderStatusOpen = val"
|
>
|
<template #arrow>
|
<img
|
src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e120f81c-9f15-4e1a-9938-22252898edf9.png"
|
:class="['arrow-icon', isOrderStatusOpen ? 'arrow-icon--open' : '']"
|
style="width: 16px; height: 16px"
|
alt="arrow"
|
>
|
</template>
|
<template #empty>
|
<div class="flex-center">
|
<img
|
src="/src/assets/element-img/no-data.png"
|
style="width: 50%"
|
alt="no-data"
|
/>
|
</div>
|
</template>
|
</n-select>
|
</div>
|
<div class="flex items-center gap-2">
|
<div class="text-sm" style="width: 70px">下单时间</div>
|
<n-date-picker
|
format="yyyy/MM/dd"
|
v-model:value="orderSearchModel.orderTimeRange"
|
type="daterange"
|
clearable
|
:actions="[]"
|
:close-on-select="true"
|
start-placeholder="开始时间"
|
end-placeholder="结束时间"
|
style="width: 300px"
|
/>
|
</div>
|
<div class="flex gap-2">
|
<n-button type="primary" @click="handleSearchOrders">
|
查询
|
</n-button>
|
<n-button @click="handleResetOrderSearch">
|
重置
|
</n-button>
|
</div>
|
</div>
|
|
<div class="flex-1 overflow-auto">
|
<n-data-table
|
:columns="orderColumns"
|
:data="orders"
|
:bordered="false"
|
:scroll-x="orderTableScrollX"
|
:header-height="18"
|
:row-height="18"
|
:loading="orderLoading"
|
>
|
<template #empty>
|
<div class="flex-center">
|
<img src="/src/assets/element-img/no-data.png" style="width:123px;height:125px">
|
</div>
|
</template>
|
</n-data-table>
|
</div>
|
<Pagination
|
:count="orderCount"
|
:page="orderPageParams.pageNum"
|
:page-size="orderPageParams.pageSize"
|
class="robotaxi-pagination mt-12px"
|
@change="changeOrderPage"
|
/>
|
</n-card>
|
|
<!-- 批量测试区域:详情/编辑模式下显示 -->
|
<template v-if="isDetail || isEdit">
|
<n-card style="border-radius: 6px" content-style="padding: 24px;">
|
<div class="mb-4 flex items-center justify-between">
|
<div class="flex items-center">
|
<div class="mr-2 bg-blue-500" style="width: 3px; height: 14px" />
|
<span class="font-bold" style="font-size: 16px">批量测试</span>
|
</div>
|
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-2">
|
<span class="text-sm">圈数:</span>
|
<n-input-number
|
v-model:value="lapCount"
|
:min="1"
|
:max="100"
|
style="width: 100px"
|
:disabled="isBatchRunning"
|
/>
|
</div>
|
<n-button
|
type="primary"
|
@click="handleBatchStart"
|
:disabled="isBatchRunning"
|
>
|
启动批量测试
|
</n-button>
|
<n-button
|
@click="handleBatchRetry"
|
:disabled="!batchProgress || batchProgress.errorCount === 0"
|
>
|
重试失败
|
</n-button>
|
<n-button
|
type="error"
|
@click="handleBatchCancel"
|
:disabled="!isBatchRunning"
|
>
|
取消
|
</n-button>
|
</div>
|
</div>
|
</n-card>
|
|
<!-- 批次进度卡片 -->
|
<n-card v-if="batchProgress" style="border-radius: 6px" content-style="padding: 24px;">
|
<div class="mb-4 flex items-center">
|
<div class="mr-2 bg-blue-500" style="width: 3px; height: 14px" />
|
<span class="font-bold" style="font-size: 16px">批次进度</span>
|
</div>
|
<div class="flex items-center gap-6 mb-4">
|
<n-tag :type="getBatchStatusType(batchProgress.status)" size="medium">
|
{{ getBatchStatusLabel(batchProgress.status) }}
|
</n-tag>
|
<span class="text-sm text-gray-500" v-if="batchProgress.startTime">
|
开始:{{ dayjs(batchProgress.startTime).format('YYYY/MM/DD HH:mm:ss') }}
|
</span>
|
<span class="text-sm text-gray-500" v-if="batchProgress.endTime">
|
结束:{{ dayjs(batchProgress.endTime).format('YYYY/MM/DD HH:mm:ss') }}
|
</span>
|
</div>
|
<n-progress
|
type="line"
|
:percentage="batchProgressPercent"
|
:status="batchProgress.status === 'FAILED' ? 'error' : batchProgress.status === 'SUCCESS' ? 'success' : 'default'"
|
:height="20"
|
:show-indicator="true"
|
class="mb-4"
|
/>
|
<div class="flex gap-8 text-sm">
|
<div>总订单数:<span class="font-bold">{{ batchProgress.totalCount || 0 }}</span></div>
|
<div>已创建:<span class="font-bold text-blue-600">{{ batchProgress.createdCount || 0 }}</span></div>
|
<div>已完成:<span class="font-bold text-green-600">{{ batchProgress.completedCount || 0 }}</span></div>
|
<div>异常数:<span class="font-bold" :class="batchProgress.errorCount > 0 ? 'text-red-600' : ''">{{ batchProgress.errorCount || 0 }}</span></div>
|
</div>
|
</n-card>
|
|
<!-- 车辆进度卡片 -->
|
<n-card v-if="batchProgress" style="border-radius: 6px; margin-bottom: 80px" content-style="padding: 24px;">
|
<div class="mb-4 flex items-center">
|
<div class="mr-2 bg-blue-500" style="width: 3px; height: 14px" />
|
<span class="font-bold" style="font-size: 16px">车辆进度</span>
|
</div>
|
<n-data-table
|
:columns="vehicleProgressColumns"
|
:data="vehicleProgressList"
|
:bordered="false"
|
:scroll-x="vehicleProgressTableScrollX"
|
:header-height="18"
|
:row-height="18"
|
>
|
<template #empty>
|
<div class="flex-center">
|
<img src="/src/assets/element-img/no-data.png" style="width:123px;height:125px">
|
</div>
|
</template>
|
</n-data-table>
|
</n-card>
|
</template>
|
|
<div
|
class="absolute bottom-0 left-0 right-0 flex justify-end gap-3 bg-white px-6 py-4"
|
style="box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1); z-index: 100"
|
>
|
<n-button style="width: 84px; height: 32px" @click="handleCancel">
|
{{ isDetail ? "返回" : "取消" }}
|
</n-button>
|
<n-button
|
v-if="!isDetail"
|
type="primary"
|
style="width: 84px; height: 32px"
|
@click="handleSave"
|
>
|
保存
|
</n-button>
|
</div>
|
|
<SelectPointModal
|
v-model:show="showPointSelectModal"
|
:existing-points="existingPointsForPointModal"
|
@confirm="handleConfirmPointSelect"
|
/>
|
<VehicleSelectModal
|
v-model:visible="showVehicleModal"
|
:existing-vehicles="vehicles"
|
@confirm="handleConfirmVehicleSelect"
|
/>
|
|
<n-modal
|
v-model:show="showOrderModal"
|
title="生成订单"
|
preset="card"
|
size="small"
|
style="width: 400px"
|
:segmented="{
|
content: true,
|
action: true,
|
}"
|
:mask-closable="false"
|
>
|
<div class="p-4">
|
<div class="mb-4">
|
<div class="text-sm mb-2">订单类型:</div>
|
<n-radio-group v-model:value="orderModalData.orderType">
|
<n-radio value="测试订单"> 测试订单 </n-radio>
|
<n-radio value="巡游订单"> 巡游订单 </n-radio>
|
</n-radio-group>
|
</div>
|
<div class="mb-4">
|
<div class="text-sm mb-2">是否停车:</div>
|
<n-radio-group
|
v-model:value="orderModalData.isPark"
|
:disabled="orderModalData.orderType === '测试订单'"
|
>
|
<n-radio :value="true"> 是 </n-radio>
|
<n-radio :value="false"> 否 </n-radio>
|
</n-radio-group>
|
<div
|
v-if="orderModalData.orderType === '测试订单'"
|
class="text-sm text-red-500 mt-1"
|
>
|
测试订单默认停车,不可修改
|
</div>
|
</div>
|
</div>
|
<template #footer>
|
<div class="flex justify-end gap-2">
|
<n-button @click="showOrderModal = false"> 取消 </n-button>
|
<n-button type="primary" @click="handleConfirmGenerateOrder">
|
确认
|
</n-button>
|
</div>
|
</template>
|
</n-modal>
|
|
<n-modal
|
v-model:show="showChangeAmountModal"
|
title="订单改价"
|
preset="card"
|
size="small"
|
style="width: 400px"
|
:segmented="{
|
content: true,
|
action: true,
|
}"
|
:bordered="false"
|
:mask-closable="false"
|
>
|
<div class="p-4">
|
<div class="text-sm mb-2">订单金额:</div>
|
<n-input-number
|
v-model:value="changeAmountForm.amount"
|
:precision="2"
|
min="0"
|
placeholder="请输入金额"
|
style="width: 100%"
|
/>
|
</div>
|
<template #footer>
|
<div class="flex justify-end gap-2">
|
<n-button @click="showChangeAmountModal = false"> 取消 </n-button>
|
<n-button type="primary" @click="handleConfirmChangeAmount">
|
确认
|
</n-button>
|
</div>
|
</template>
|
</n-modal>
|
|
<n-modal
|
v-model:show="showReassignModal"
|
title="改派订单"
|
preset="card"
|
size="small"
|
style="width: 400px"
|
:segmented="{
|
content: true,
|
action: true,
|
}"
|
:bordered="false"
|
:mask-closable="false"
|
>
|
<div class="p-4">
|
<div class="text-sm mb-2">改派车辆:</div>
|
<n-select
|
v-model:value="reassignForm.vehicleId"
|
placeholder="请选择改派车辆"
|
:options="reassignVehicleOptions"
|
clearable
|
style="width: 100%"
|
@update:show="(val) => isReassignVehicleOpen = val"
|
>
|
<template #arrow>
|
<img
|
src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e120f81c-9f15-4e1a-9938-22252898edf9.png"
|
:class="['arrow-icon', isReassignVehicleOpen ? 'arrow-icon--open' : '']"
|
style="width: 16px; height: 16px"
|
alt="arrow"
|
>
|
</template>
|
<template #empty>
|
<div class="flex-center">
|
<img
|
src="/src/assets/element-img/no-data.png"
|
style="width: 50%"
|
alt="no-data"
|
/>
|
</div>
|
</template>
|
</n-select>
|
<div class="text-sm mb-2 mt-4">改派原因:</div>
|
<n-input
|
v-model:value="reassignForm.reassignReason"
|
type="textarea"
|
placeholder="请输入改派原因"
|
maxlength="200"
|
show-count
|
/>
|
</div>
|
<template #footer>
|
<div class="flex justify-end gap-2">
|
<n-button @click="showReassignModal = false"> 取消 </n-button>
|
<n-button type="primary" @click="handleConfirmReassign">
|
确认
|
</n-button>
|
</div>
|
</template>
|
</n-modal>
|
|
</div>
|
</template>
|
|
<style scoped></style>
|