feat(businessOperations/cruise): 新增特殊点位管理功能并优化巡逻点位列表
新增特殊点位的新增与编辑页面,支持点位选择与关联车辆管理。
重构巡逻点位列表页面,替换原弹窗选择逻辑为路由跳转,新增删除确认弹窗,优化表格样式与后端分页逻辑。
更新点位相关API接口,将getSpecialPoints改为POST请求并添加分页参数,调整setSpecialPoint参数结构,新增deleteSpecialPoint和getSpecialPointDetail接口,修复类型空格问题。
优化SelectPointModal组件,支持单选模式与点击行选择功能。
新增对应的路由配置。
| | |
| | | pid: 407, |
| | | }, |
| | | { |
| | | name: 'patrolPointsAdd', |
| | | path: '/businessOperations/cruise/patrol-points/add', |
| | | title: '新增特殊点位', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:list-view', |
| | | menuType: 'page', |
| | | componentPath: '/businessOperations/cruise/patrol-points/add.vue', |
| | | id: 40711, |
| | | keepAlive: false, |
| | | pid: 407, |
| | | hide: true, |
| | | fullScreen: true, |
| | | activeMenu: '/businessOperations/cruise/patrol-points', |
| | | }, |
| | | { |
| | | name: 'patrolPlan', |
| | | path: '/businessOperations/cruise/parade-plan', |
| | | title: '巡游计划', |
| | |
| | | return request.Post<Service.ResponseResult<any>>('/map/api/v1/point/enableDisable', data) |
| | | } |
| | | |
| | | export function fetchPointDetail(id: number|string) { |
| | | export function fetchPointDetail(id: number | string) { |
| | | return request.Get<Service.ResponseResult<Entity.Point>>('/map/api/v1/point/queryById', { params: { id } }) |
| | | } |
| | | |
| | |
| | | return method |
| | | } |
| | | |
| | | export function getSpecialPoints() { |
| | | return request.Get<Service.ResponseResult<any>>('/operation/api/v1/point/listSpecialPoint') |
| | | export function getSpecialPoints(params: any) { |
| | | return request.Post<Service.ResponseResult<any>>('/operation/api/v1/point/listSpecialPoint', params) |
| | | } |
| | | |
| | | export function setSpecialPoint(data: { chargePoints: number[], parkingPoints: number[] }) { |
| | | export function setSpecialPoint(data: { |
| | | pointId: number | string |
| | | pointsType: number |
| | | vehicleIds: Array<number | string> |
| | | }) { |
| | | return request.Post<Service.ResponseResult<any>>('/operation/api/v1/point/setSpecialPoint', data) |
| | | } |
| | | |
| | | export function deleteSpecialPoint(data: { |
| | | pointId: number | string |
| | | pointsType: number |
| | | }) { |
| | | return request.Post<Service.ResponseResult<any>>('/operation/api/v1/point/deleteSpecialPoint', data) |
| | | } |
| | | |
| | | export function getSpecialPointDetail(data: { |
| | | pointId: number | string |
| | | }) { |
| | | return request.Post<Service.ResponseResult<any>>('/operation/api/v1/point/getSpecialPointDetail', data) |
| | | } |
| New file |
| | |
| | | <script setup lang="tsx"> |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { computed, onMounted, ref } from 'vue' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { NButton, NTag, useMessage } from 'naive-ui' |
| | | import SearchPanel from '@/components/custom/SearchPanel.vue' |
| | | import Pagination from '@/components/common/Pagination.vue' |
| | | import SelectPointModal from './components/SelectPointModal.vue' |
| | | import VehicleSelectModal from '@/views/assetManagement/fleetManagement/components/VehicleSelectModal.vue' |
| | | import { getSpecialPointDetail, getSpecialPoints, setSpecialPoint } from '@/service/api/point' |
| | | import { useTabStore } from '@/store' |
| | | |
| | | interface PointItem { |
| | | id: number | string |
| | | pointName: string |
| | | pointAddress: string |
| | | } |
| | | |
| | | interface VehicleItem { |
| | | id: number | string |
| | | vehicleCode: string |
| | | plateNumber: string |
| | | vehicleModelName?: string |
| | | vehicleModel?: string |
| | | procurementSource?: string |
| | | fleetName?: string |
| | | bindPointName?: string |
| | | specialPointName?: string |
| | | parkingPointName?: string |
| | | chargePointName?: string |
| | | bindStatus?: number |
| | | pointBindStatus?: number |
| | | [key: string]: any |
| | | } |
| | | |
| | | const router = useRouter() |
| | | const route = useRoute() |
| | | const tabStore = useTabStore() |
| | | const message = useMessage() |
| | | |
| | | const showPointModal = ref(false) |
| | | const showVehicleModal = ref(false) |
| | | const saving = ref(false) |
| | | |
| | | const selectedPoint = ref<PointItem | null>(null) |
| | | const selectedVehicles = ref<VehicleItem[]>([]) |
| | | |
| | | const vehicleSearchModel = ref({ |
| | | plateNumber: '', |
| | | vehicleCode: '', |
| | | }) |
| | | |
| | | const vehiclePageParams = ref({ |
| | | pageNum: 1, |
| | | pageSize: 10, |
| | | }) |
| | | |
| | | const pageMode = computed<'add' | 'edit'>(() => route.query.mode === 'edit' ? 'edit' : 'add') |
| | | const editingPointId = computed<string>(() => String(route.query.pointId || route.query.id || '')) |
| | | const pointType = computed<'charge' | 'park'>(() => route.query.type === 'charge' ? 'charge' : 'park') |
| | | const pointTypeText = computed(() => pointType.value === 'charge' ? '充电点位' : '收车点位') |
| | | |
| | | const selectedPointData = computed(() => selectedPoint.value ? [selectedPoint.value] : []) |
| | | const existingPointForModal = computed(() => { |
| | | if (!selectedPoint.value) |
| | | return [] |
| | | |
| | | return [{ |
| | | id: selectedPoint.value.id, |
| | | pointName: selectedPoint.value.pointName, |
| | | pointAddress: selectedPoint.value.pointAddress, |
| | | }] |
| | | }) |
| | | |
| | | const filteredVehicleList = computed(() => { |
| | | return selectedVehicles.value.filter((item) => { |
| | | const matchPlateNumber = !vehicleSearchModel.value.plateNumber || String(item.plateNumber || '').includes(vehicleSearchModel.value.plateNumber.trim()) |
| | | const matchVehicleCode = !vehicleSearchModel.value.vehicleCode || String(item.vehicleCode || '').includes(vehicleSearchModel.value.vehicleCode.trim()) |
| | | return matchPlateNumber && matchVehicleCode |
| | | }) |
| | | }) |
| | | |
| | | const pagedVehicleList = computed(() => { |
| | | const startIndex = (vehiclePageParams.value.pageNum - 1) * vehiclePageParams.value.pageSize |
| | | return filteredVehicleList.value.slice(startIndex, startIndex + vehiclePageParams.value.pageSize) |
| | | }) |
| | | |
| | | const pointColumns = computed<DataTableColumns<PointItem>>(() => [ |
| | | { |
| | | title: '点位名称', |
| | | key: 'pointName', |
| | | width: 260, |
| | | resizable: true, |
| | | ellipsis: { tooltip: true }, |
| | | }, |
| | | { |
| | | title: '详细地址', |
| | | key: 'pointAddress', |
| | | width: 420, |
| | | resizable: true, |
| | | ellipsis: { tooltip: true }, |
| | | }, |
| | | { |
| | | title: '操作', |
| | | key: 'actions', |
| | | width: 100, |
| | | render: row => ( |
| | | <NButton text type="primary" onClick={() => handleOpenPointModal()}> |
| | | 更换 |
| | | </NButton> |
| | | ), |
| | | }, |
| | | ]) |
| | | |
| | | const vehicleColumns = computed<DataTableColumns<VehicleItem>>(() => [ |
| | | { |
| | | title: '序号', |
| | | key: 'index', |
| | | width: 70, |
| | | render: (row, index) => <span>{(vehiclePageParams.value.pageNum - 1) * vehiclePageParams.value.pageSize + index + 1}</span>, |
| | | }, |
| | | { |
| | | title: '车辆编号', |
| | | key: 'vehicleCode', |
| | | minWidth: 150, |
| | | ellipsis: { tooltip: true }, |
| | | render: row => row.vehicleCode || '-', |
| | | }, |
| | | { |
| | | title: '车牌号', |
| | | key: 'plateNumber', |
| | | minWidth: 140, |
| | | ellipsis: { tooltip: true }, |
| | | render: row => row.plateNumber || '-', |
| | | }, |
| | | { |
| | | title: '型号', |
| | | key: 'vehicleModelName', |
| | | minWidth: 120, |
| | | ellipsis: { tooltip: true }, |
| | | render: row => row.vehicleModelName || row.vehicleModel || '-', |
| | | }, |
| | | { |
| | | title: '采购来源', |
| | | key: 'procurementSource', |
| | | minWidth: 120, |
| | | ellipsis: { tooltip: true }, |
| | | render: row => row.procurementSource || '-', |
| | | }, |
| | | { |
| | | title: '当前车队', |
| | | key: 'fleetName', |
| | | minWidth: 160, |
| | | ellipsis: { tooltip: true }, |
| | | render: row => row.fleetName || '-', |
| | | }, |
| | | { |
| | | title: '关联状态', |
| | | key: 'bindStatus', |
| | | width: 140, |
| | | render: (row) => { |
| | | const conflictText = getVehicleConflictText(row) |
| | | if (conflictText) { |
| | | return ( |
| | | <NTag bordered={false} type="warning"> |
| | | 存在冲突 |
| | | </NTag> |
| | | ) |
| | | } |
| | | |
| | | return ( |
| | | <NTag bordered={false} type="success"> |
| | | 正常 |
| | | </NTag> |
| | | ) |
| | | }, |
| | | }, |
| | | { |
| | | title: '操作', |
| | | key: 'actions', |
| | | width: 80, |
| | | render: row => ( |
| | | <NButton text type="primary" onClick={() => handleRemoveVehicle(row.id)}> |
| | | 移除 |
| | | </NButton> |
| | | ), |
| | | }, |
| | | ]) |
| | | |
| | | const vehicleFilterItems = computed(() => [ |
| | | { |
| | | label: '车牌号', |
| | | path: 'plateNumber', |
| | | render: () => <n-input v-model:value={vehicleSearchModel.value.plateNumber} placeholder="请输入" clearable />, |
| | | }, |
| | | { |
| | | label: '车辆编号', |
| | | path: 'vehicleCode', |
| | | render: () => <n-input v-model:value={vehicleSearchModel.value.vehicleCode} placeholder="请输入" clearable />, |
| | | }, |
| | | ]) |
| | | |
| | | const isSaveDisabled = computed(() => !selectedPoint.value || saving.value) |
| | | |
| | | function getVehicleConflictText(vehicle: VehicleItem) { |
| | | const currentPointId = selectedPoint.value ? String(selectedPoint.value.id) : '' |
| | | const conflictName = vehicle.bindPointName |
| | | || vehicle.specialPointName |
| | | || vehicle.parkingPointName |
| | | || vehicle.chargePointName |
| | | const conflictPointId = vehicle.bindPointId ? String(vehicle.bindPointId) : '' |
| | | |
| | | if (currentPointId && conflictPointId && conflictPointId === currentPointId) |
| | | return '' |
| | | |
| | | if (selectedPoint.value && conflictName && conflictName === selectedPoint.value.pointName) |
| | | return '' |
| | | |
| | | if (conflictName) |
| | | return `已关联${conflictName}` |
| | | |
| | | if (vehicle.bindStatus === 1 || vehicle.pointBindStatus === 1) |
| | | return '已关联其他点位' |
| | | |
| | | return '' |
| | | } |
| | | |
| | | function getPointsTypeValue(type: 'charge' | 'park') { |
| | | return type === 'charge' ? 1 : 2 |
| | | } |
| | | |
| | | async function fetchExistingPointIds(type: 'charge' | 'park') { |
| | | const res = await getSpecialPoints({ |
| | | pageNum: 1, |
| | | pageSize: 9999, |
| | | pointName: '', |
| | | pointAddress: '', |
| | | pointsType: getPointsTypeValue(type), |
| | | }) |
| | | |
| | | return (res.data?.records || []) |
| | | .map((item: any) => String(item.pointId)) |
| | | .filter(id => pageMode.value !== 'edit' || id !== editingPointId.value) |
| | | } |
| | | |
| | | async function handleConfirmPointSelect(points: any[]) { |
| | | const targetPoint = points[0] |
| | | if (!targetPoint) |
| | | return |
| | | |
| | | const existingPointIds = await fetchExistingPointIds(pointType.value) |
| | | if (existingPointIds.includes(String(targetPoint.id))) { |
| | | message.warning(`该${pointTypeText.value}已存在,请重新选择`) |
| | | return |
| | | } |
| | | |
| | | selectedPoint.value = { |
| | | id: targetPoint.id, |
| | | pointName: targetPoint.pointName || targetPoint.name || '', |
| | | pointAddress: targetPoint.pointAddress || targetPoint.address || '', |
| | | } |
| | | } |
| | | |
| | | function handleOpenPointModal() { |
| | | showPointModal.value = true |
| | | } |
| | | |
| | | function handleVehicleSearch() { |
| | | vehiclePageParams.value.pageNum = 1 |
| | | } |
| | | |
| | | function handleResetVehicleSearch() { |
| | | vehicleSearchModel.value.plateNumber = '' |
| | | vehicleSearchModel.value.vehicleCode = '' |
| | | vehiclePageParams.value.pageNum = 1 |
| | | } |
| | | |
| | | function handleVehiclePageChange(page: number, pageSize: number) { |
| | | vehiclePageParams.value.pageNum = page |
| | | vehiclePageParams.value.pageSize = pageSize |
| | | } |
| | | |
| | | function handleOpenVehicleModal() { |
| | | showVehicleModal.value = true |
| | | } |
| | | |
| | | function handleConfirmVehicleSelect(vehicles: VehicleItem[]) { |
| | | const vehicleMap = new Map<string, VehicleItem>() |
| | | selectedVehicles.value.forEach((item) => { |
| | | vehicleMap.set(String(item.id), item) |
| | | }) |
| | | vehicles.forEach((item) => { |
| | | vehicleMap.set(String(item.id), item) |
| | | }) |
| | | selectedVehicles.value = Array.from(vehicleMap.values()) |
| | | vehiclePageParams.value.pageNum = 1 |
| | | } |
| | | |
| | | function handleRemoveVehicle(vehicleId: number | string) { |
| | | selectedVehicles.value = selectedVehicles.value.filter(item => String(item.id) !== String(vehicleId)) |
| | | } |
| | | |
| | | function handleCancel() { |
| | | tabStore.closeTab(route.fullPath) |
| | | router.push({ name: 'patrolPoints' }) |
| | | } |
| | | |
| | | function normalizePointData(data: any): PointItem | null { |
| | | const pointId = data?.pointId ?? data?.id ?? data?.specialPointId |
| | | if (pointId === null || pointId === undefined || pointId === '') |
| | | return null |
| | | |
| | | return { |
| | | id: pointId, |
| | | pointName: data?.pointName || data?.name || '', |
| | | pointAddress: data?.pointAddress || data?.address || '', |
| | | } |
| | | } |
| | | |
| | | function normalizeVehicleItem(vehicle: any): VehicleItem { |
| | | return { |
| | | ...vehicle, |
| | | id: vehicle?.id ?? vehicle?.vehicleId ?? vehicle?.carId, |
| | | vehicleCode: vehicle?.vehicleCode || vehicle?.carCode || '', |
| | | plateNumber: vehicle?.plateNumber || '', |
| | | vehicleModelName: vehicle?.vehicleModelName || vehicle?.vehicleModel || '', |
| | | procurementSource: vehicle?.procurementSource || '', |
| | | fleetName: vehicle?.fleetName || '', |
| | | bindPointName: vehicle?.bindPointName || vehicle?.specialPointName || '', |
| | | specialPointName: vehicle?.specialPointName || '', |
| | | parkingPointName: vehicle?.parkingPointName || '', |
| | | chargePointName: vehicle?.chargePointName || '', |
| | | bindStatus: vehicle?.bindStatus, |
| | | pointBindStatus: vehicle?.pointBindStatus, |
| | | bindPointId: vehicle?.bindPointId ?? vehicle?.specialPointId ?? vehicle?.parkingPointId ?? vehicle?.chargePointId, |
| | | } |
| | | } |
| | | |
| | | function getDetailVehicleList(data: any) { |
| | | const vehicleList = data?.vehicleList |
| | | || data?.bindVehicleList |
| | | || data?.specialPointVehicleList |
| | | || data?.vehicles |
| | | || [] |
| | | |
| | | return Array.isArray(vehicleList) ? vehicleList.map(normalizeVehicleItem) : [] |
| | | } |
| | | |
| | | async function fetchDetail() { |
| | | if (pageMode.value !== 'edit' || !editingPointId.value) |
| | | return |
| | | |
| | | const res = await getSpecialPointDetail({ pointId: editingPointId.value }) |
| | | if (res.code !== 200 || !res.data) { |
| | | return |
| | | } |
| | | |
| | | selectedPoint.value = normalizePointData(res.data) |
| | | selectedVehicles.value = getDetailVehicleList(res.data) |
| | | } |
| | | |
| | | async function handleSave() { |
| | | if (!selectedPoint.value) { |
| | | message.warning(`请选择${pointTypeText.value}`) |
| | | return |
| | | } |
| | | |
| | | const conflictVehicle = selectedVehicles.value.find(item => getVehicleConflictText(item)) |
| | | if (conflictVehicle) { |
| | | message.warning('关联车辆存在已绑定其他点位的冲突,请调整后再保存') |
| | | return |
| | | } |
| | | |
| | | saving.value = true |
| | | try { |
| | | const payload = { |
| | | pointId: selectedPoint.value.id, |
| | | pointsType: getPointsTypeValue(pointType.value), |
| | | vehicleIds: selectedVehicles.value.map(item => item.id), |
| | | } |
| | | |
| | | const res = await setSpecialPoint(payload) |
| | | if (res.code !== 200) { |
| | | message.error(res.msg || res.data || '保存失败') |
| | | return |
| | | } |
| | | |
| | | message.success('保存成功') |
| | | window.sessionStorage.setItem('cruisePatrolPointNeedRefresh', '1') |
| | | tabStore.closeTab(route.fullPath) |
| | | router.push({ name: 'patrolPoints' }) |
| | | } |
| | | finally { |
| | | saving.value = false |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | fetchDetail() |
| | | }) |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="special-point-add-page roboTaxi"> |
| | | <div class="p-12px h-full flex flex-col gap-12px"> |
| | | <n-card class="content-card" :bordered="false"> |
| | | <div class="flex justify-between items-center mb-12px"> |
| | | <div class="section-title-wrap"> |
| | | <div class="section-title-bar" /> |
| | | <span class="section-title">特殊点位信息</span> |
| | | </div> |
| | | |
| | | <NButton type="primary" secondary @click="handleOpenPointModal"> |
| | | 选择点位 |
| | | </NButton> |
| | | </div> |
| | | |
| | | <n-data-table |
| | | :columns="pointColumns" |
| | | :data="selectedPointData" |
| | | :bordered="false" |
| | | :pagination="false" |
| | | :style="selectedPointData.length === 0 ? { minHeight: '140px' } : {}" |
| | | :row-key="(row: PointItem) => row.id" |
| | | > |
| | | <template #empty> |
| | | <div class="empty-wrap"> |
| | | <img src="/src/assets/element-img/no-data.png" style="width:123px;height:125px" @error="(e: any) => { e.target.src = 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png' }"> |
| | | </div> |
| | | </template> |
| | | </n-data-table> |
| | | </n-card> |
| | | |
| | | <n-card class="content-card flex-1" :bordered="false" content-style="display:flex;flex-direction:column;flex:1;min-height:0;"> |
| | | <div class="section-title-wrap mb-12px"> |
| | | <div class="section-title-bar" /> |
| | | <span class="section-title">关联车辆列表</span> |
| | | </div> |
| | | |
| | | <div class="vehicle-search-card"> |
| | | <n-form :model="vehicleSearchModel" label-placement="left" inline :show-feedback="false"> |
| | | <SearchPanel> |
| | | <n-gi v-for="item in vehicleFilterItems" :key="item.path"> |
| | | <n-form-item :label="item.label" :path="item.path"> |
| | | <component :is="item.render" /> |
| | | </n-form-item> |
| | | </n-gi> |
| | | <template #action> |
| | | <NButton @click="handleResetVehicleSearch"> |
| | | 重置 |
| | | </NButton> |
| | | <NButton type="primary" @click="handleVehicleSearch"> |
| | | 查询 |
| | | </NButton> |
| | | </template> |
| | | </SearchPanel> |
| | | </n-form> |
| | | </div> |
| | | |
| | | <div class="vehicle-toolbar"> |
| | | <NButton type="primary" secondary @click="handleOpenVehicleModal"> |
| | | 添加关联车辆 |
| | | </NButton> |
| | | </div> |
| | | |
| | | <div class="flex-1 flex flex-col h-full" style="min-width: 0; overflow: hidden"> |
| | | <n-data-table |
| | | class="flex-1" |
| | | :columns="vehicleColumns" |
| | | :data="pagedVehicleList" |
| | | :bordered="false" |
| | | :pagination="false" |
| | | flex-height |
| | | :row-key="(row: VehicleItem) => row.id" |
| | | :scroll-x="1100" |
| | | > |
| | | <template #empty> |
| | | <div class="empty-wrap"> |
| | | <img src="/src/assets/element-img/no-data.png" style="width:123px;height:125px" @error="(e: any) => { e.target.src = 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png' }"> |
| | | </div> |
| | | </template> |
| | | </n-data-table> |
| | | |
| | | <Pagination |
| | | :count="filteredVehicleList.length" |
| | | :page="vehiclePageParams.pageNum" |
| | | :page-size="vehiclePageParams.pageSize" |
| | | class="robotaxi-pagination mt-12px" |
| | | @change="handleVehiclePageChange" |
| | | /> |
| | | </div> |
| | | </n-card> |
| | | </div> |
| | | |
| | | <div class="page-footer"> |
| | | <NButton class="w-84px" @click="handleCancel"> |
| | | 取消 |
| | | </NButton> |
| | | <NButton type="primary" class="w-84px" :loading="saving" :disabled="isSaveDisabled" @click="handleSave"> |
| | | 保存 |
| | | </NButton> |
| | | </div> |
| | | |
| | | <SelectPointModal |
| | | v-model:show="showPointModal" |
| | | :existing-points="existingPointForModal" |
| | | :max-select-count="1" |
| | | @confirm="handleConfirmPointSelect" |
| | | /> |
| | | |
| | | <VehicleSelectModal |
| | | v-model:visible="showVehicleModal" |
| | | :existing-vehicles="selectedVehicles" |
| | | @confirm="handleConfirmVehicleSelect" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped lang="scss"> |
| | | .special-point-add-page { |
| | | height: calc(100vh - 76px); |
| | | } |
| | | |
| | | .page-title-card, |
| | | .content-card { |
| | | border-radius: 6px; |
| | | } |
| | | |
| | | .page-title { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #171f26; |
| | | } |
| | | |
| | | .section-title-wrap { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .section-title-bar { |
| | | width: 3px; |
| | | height: 14px; |
| | | margin-right: 8px; |
| | | border-radius: 3px; |
| | | background: #0068ff; |
| | | } |
| | | |
| | | .section-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #171f26; |
| | | } |
| | | |
| | | .point-toolbar, |
| | | .vehicle-toolbar { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .vehicle-search-card { |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .empty-wrap { |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | padding: 16px 0; |
| | | } |
| | | |
| | | .page-footer { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | align-items: center; |
| | | gap: 12px; |
| | | padding: 16px 24px; |
| | | background: #fff; |
| | | border-radius: 6px; |
| | | } |
| | | </style> |
| | |
| | | <script setup lang="ts"> |
| | | import { ref, h, watch } from "vue"; |
| | | import { useMessage } from "naive-ui"; |
| | | import { fetchPointPageList } from "@/service/api/point"; |
| | | import { useBoolean } from "@/hooks"; |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { computed, h, ref, watch } from 'vue' |
| | | import { useMessage } from 'naive-ui' |
| | | import { fetchPointPageList } from '@/service/api/point' |
| | | import { useBoolean } from '@/hooks' |
| | | |
| | | interface Point { |
| | | id: number; |
| | | name?: string; |
| | | pointAddress?: string; |
| | | address?: string; |
| | | type?: string; |
| | | status?: string; |
| | | [key: string]: any; |
| | | id: number |
| | | name?: string |
| | | pointAddress?: string |
| | | address?: string |
| | | type?: string |
| | | status?: string |
| | | [key: string]: any |
| | | } |
| | | |
| | | const props = defineProps<{ |
| | | show: boolean; |
| | | existingPoints?: Point[]; |
| | | }>(); |
| | | show: boolean |
| | | existingPoints?: Point[] |
| | | maxSelectCount?: number |
| | | }>() |
| | | |
| | | const emit = defineEmits<{ |
| | | "update:show": [value: boolean]; |
| | | confirm: [points: Point[]]; |
| | | }>(); |
| | | 'update:show': [value: boolean] |
| | | 'confirm': [points: Point[]] |
| | | }>() |
| | | |
| | | const { |
| | | bool: loading, |
| | | setTrue: startLoading, |
| | | setFalse: endLoading, |
| | | } = useBoolean(false); |
| | | const message = useMessage(); |
| | | } = useBoolean(false) |
| | | const message = useMessage() |
| | | |
| | | const model = ref({ |
| | | pointName: "", |
| | | }); |
| | | const selectedPoints = ref<number[]>([]); |
| | | const allSelectedMap = ref<Map<number, Point>>(new Map()); |
| | | pointName: '', |
| | | }) |
| | | const selectedPoints = ref<number[]>([]) |
| | | const allSelectedMap = ref<Map<number, Point>>(new Map()) |
| | | const isSingleSelect = computed(() => props.maxSelectCount === 1) |
| | | |
| | | const listData = ref<any[]>([]); |
| | | const listData = ref<any[]>([]) |
| | | const pagination = ref({ |
| | | page: 1, |
| | | pageSize: 10, |
| | | itemCount: 0, |
| | | }); |
| | | }) |
| | | |
| | | async function getList() { |
| | | startLoading(); |
| | | startLoading() |
| | | try { |
| | | const params = { |
| | | pageNum: pagination.value.page, |
| | | pageSize: pagination.value.pageSize, |
| | | pointName: model.value.pointName, |
| | | operationStatusList: [6], |
| | | }; |
| | | const { data } = await fetchPointPageList(params); |
| | | listData.value = data.records || []; |
| | | } |
| | | const { data } = await fetchPointPageList(params) |
| | | listData.value = data.records || [] |
| | | |
| | | // 更新 map 中的数据,确保有最新的信息(可选) |
| | | // listData.value.forEach(item => { |
| | |
| | | // } |
| | | // }) |
| | | |
| | | pagination.value.itemCount = data.total || 0; |
| | | } finally { |
| | | endLoading(); |
| | | pagination.value.itemCount = data.total || 0 |
| | | } |
| | | finally { |
| | | endLoading() |
| | | } |
| | | } |
| | | |
| | | function handlePageChange(page: number, size: number) { |
| | | pagination.value.page = page; |
| | | pagination.value.pageSize = size; |
| | | getList(); |
| | | pagination.value.page = page |
| | | pagination.value.pageSize = size |
| | | getList() |
| | | } |
| | | |
| | | const handleSearch = () => { |
| | | pagination.value.page = 1; |
| | | getList(); |
| | | }; |
| | | function handleSearch() { |
| | | pagination.value.page = 1 |
| | | getList() |
| | | } |
| | | |
| | | const handleReset = () => { |
| | | model.value.pointName = ""; |
| | | function handleReset() { |
| | | model.value.pointName = '' |
| | | // 重置时只清空搜索条件,不应该清空已选?或者清空所有? |
| | | // 通常重置按钮是在搜索栏旁边,应该只重置搜索条件。 |
| | | // 原代码:selectedPoints.value = [],这会清空已选。根据用户习惯,重置搜索通常不清除已选。 |
| | |
| | | // 假设这里只是重置搜索条件。 |
| | | // 原代码:model.value.pointName = ''; selectedPoints.value = []; handleSearch(); |
| | | // 这里我改为只重置搜索。 |
| | | handleSearch(); |
| | | }; |
| | | handleSearch() |
| | | } |
| | | |
| | | // 专门的清空选择按钮?或者保留原逻辑? |
| | | // 为了安全,我们把“重置”改为“重置搜索”,不清除已选。 |
| | | const handleResetSearch = () => { |
| | | model.value.pointName = ""; |
| | | handleSearch(); |
| | | }; |
| | | function handleResetSearch() { |
| | | model.value.pointName = '' |
| | | handleSearch() |
| | | } |
| | | |
| | | const handleCancel = () => { |
| | | emit("update:show", false); |
| | | }; |
| | | function handleCancel() { |
| | | emit('update:show', false) |
| | | } |
| | | |
| | | const handleConfirm = () => { |
| | | const result: Point[] = []; |
| | | function handleConfirm() { |
| | | if (props.maxSelectCount && selectedPoints.value.length > props.maxSelectCount) { |
| | | message.warning(`最多只能选择${props.maxSelectCount}个点位`) |
| | | return |
| | | } |
| | | |
| | | const result: Point[] = [] |
| | | selectedPoints.value.forEach((id) => { |
| | | if (allSelectedMap.value.has(id)) { |
| | | result.push(allSelectedMap.value.get(id)!); |
| | | result.push(allSelectedMap.value.get(id)!) |
| | | } |
| | | }); |
| | | emit("confirm", result); |
| | | emit("update:show", false); |
| | | }; |
| | | }) |
| | | emit('confirm', result) |
| | | emit('update:show', false) |
| | | } |
| | | |
| | | function handleUpdateChecked(keys: number[]) { |
| | | selectedPoints.value = keys; |
| | | selectedPoints.value = isSingleSelect.value && keys.length > 1 |
| | | ? [keys[keys.length - 1]] |
| | | : keys |
| | | |
| | | // 遍历当前页数据,如果有被选中的,存入 map |
| | | listData.value.forEach((item) => { |
| | | if (keys.includes(item.id)) { |
| | | if (selectedPoints.value.includes(item.id)) { |
| | | // 如果 map 中已有该 id,尝试合并属性以保留额外信息(如 isCharge 等) |
| | | // 优先使用新数据覆盖,但保留新数据中没有的旧字段 |
| | | const old = allSelectedMap.value.get(item.id); |
| | | const old = allSelectedMap.value.get(item.id) |
| | | if (old) { |
| | | allSelectedMap.value.set(item.id, { ...old, ...item }); |
| | | } else { |
| | | allSelectedMap.value.set(item.id, item); |
| | | allSelectedMap.value.set(item.id, { ...old, ...item }) |
| | | } |
| | | else { |
| | | allSelectedMap.value.set(item.id, item) |
| | | } |
| | | } |
| | | }); |
| | | }) |
| | | |
| | | // 注意:我们不需要从 map 中删除未被选中的,因为 keys 才是最终的 source of truth。 |
| | | // 只要 confirm 时遍历 keys 去 map 取即可。 |
| | |
| | | // 但我们的 props 改为了 existingPoints: Point[],所以初始化时 map 是全的。 |
| | | } |
| | | |
| | | function handleRowClick(row: Point) { |
| | | if (isSingleSelect.value) { |
| | | handleUpdateChecked([row.id]) |
| | | return |
| | | } |
| | | |
| | | const nextKeys = selectedPoints.value.includes(row.id) |
| | | ? selectedPoints.value.filter(id => id !== row.id) |
| | | : [...selectedPoints.value, row.id] |
| | | handleUpdateChecked(nextKeys) |
| | | } |
| | | |
| | | function rowProps(row: Point) { |
| | | return { |
| | | style: 'cursor: pointer;', |
| | | onClick: () => handleRowClick(row), |
| | | } |
| | | } |
| | | |
| | | watch( |
| | | () => props.show, |
| | | (newVal) => { |
| | | if (newVal) { |
| | | model.value.pointName = ""; |
| | | model.value.pointName = '' |
| | | // 初始化 |
| | | selectedPoints.value = props.existingPoints |
| | | ? props.existingPoints.map((p) => p.id) |
| | | : []; |
| | | allSelectedMap.value.clear(); |
| | | ? props.existingPoints.map(p => p.id) |
| | | : [] |
| | | allSelectedMap.value.clear() |
| | | if (props.existingPoints) { |
| | | props.existingPoints.forEach((p) => { |
| | | allSelectedMap.value.set(p.id, p); |
| | | }); |
| | | allSelectedMap.value.set(p.id, p) |
| | | }) |
| | | } |
| | | |
| | | pagination.value.page = 1; |
| | | getList(); |
| | | pagination.value.page = 1 |
| | | getList() |
| | | } |
| | | } |
| | | ); |
| | | }, |
| | | ) |
| | | |
| | | const columns = [ |
| | | { type: "selection" }, |
| | | const columns = computed<DataTableColumns<Point>>(() => [ |
| | | { |
| | | title: "序号", |
| | | key: "index", |
| | | type: 'selection', |
| | | multiple: !isSingleSelect.value, |
| | | width: 50, |
| | | }, |
| | | { |
| | | title: '序号', |
| | | key: 'index', |
| | | width: 120, |
| | | render: (row, index) => { |
| | | return ( |
| | | (pagination.value.page - 1) * pagination.value.pageSize + index + 1 |
| | | ); |
| | | ) |
| | | }, |
| | | }, |
| | | { title: "点位名称", key: "pointName", minWidth: 150 }, |
| | | { title: '点位名称', key: 'pointName', minWidth: 150 }, |
| | | { |
| | | title: "详细地址", |
| | | key: "pointAddress", |
| | | title: '详细地址', |
| | | key: 'pointAddress', |
| | | minWidth: 200, |
| | | ellipsis: { tooltip: true }, |
| | | }, |
| | | { |
| | | title: "状态", |
| | | key: "operationStatus", |
| | | title: '状态', |
| | | key: 'operationStatus', |
| | | width: 100, |
| | | render: () => { |
| | | return h( |
| | | "div", |
| | | 'div', |
| | | { |
| | | class: "status-tag test-starting", |
| | | class: 'status-tag test-starting', |
| | | }, |
| | | "启用中" |
| | | ); |
| | | '启用中', |
| | | ) |
| | | }, |
| | | }, |
| | | ]; |
| | | ]) |
| | | </script> |
| | | |
| | | <template> |
| | |
| | | <div class="flex flex-col h-full overflow-hidden"> |
| | | <div class="mb-4 flex items-end gap-4" style="flex-shrink: 0"> |
| | | <div class="flex-1"> |
| | | <div class="mb-2 text-sm">点位名称</div> |
| | | <div class="mb-2 text-sm"> |
| | | 点位名称 |
| | | </div> |
| | | <n-input |
| | | v-model:value="model.pointName" |
| | | placeholder="请输入" |
| | | clearable |
| | | /> |
| | | </div> |
| | | <div class="flex-1"></div> |
| | | <div class="flex-1" /> |
| | | <div class="flex gap-2"> |
| | | <n-button style="width: 84px; height: 32px" @click="handleReset" |
| | | >重置</n-button |
| | | > |
| | | <n-button style="width: 84px; height: 32px" @click="handleReset"> |
| | | 重置 |
| | | </n-button> |
| | | <n-button |
| | | type="primary" |
| | | style="width: 84px; height: 32px" |
| | | @click="handleSearch" |
| | | >查询</n-button |
| | | > |
| | | 查询 |
| | | </n-button> |
| | | </div> |
| | | </div> |
| | | <div class="flex-1 overflow-hidden" style="min-height: 0"> |
| | | <n-data-table |
| | | :checked-row-keys="selectedPoints" |
| | | @update:checked-row-keys="handleUpdateChecked" |
| | | v-model:checked-row-keys="selectedPoints" |
| | | :columns="columns" |
| | | :data="listData" |
| | | flex-height |
| | | style="height: 100%" |
| | | style="height: 100%" |
| | | :loading="loading" |
| | | :row-key="(row: Point) => row.id" |
| | | :row-props="rowProps" |
| | | @update:checked-row-keys="handleUpdateChecked" |
| | | > |
| | | <template #empty> |
| | | <div class="flex-center"> |
| | |
| | | src="/src/assets/element-img/no-data.png" |
| | | style="width: 123px; height: 125px" |
| | | @error="(e: any) => { e.target.src = 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png' }" |
| | | /> |
| | | > |
| | | </div> |
| | | </template> |
| | | </n-data-table> |
| | | </div> |
| | | <Pagination |
| | | style="margin-top: 12px; flex-shrink: 0" |
| | | :count="pagination.itemCount" |
| | | :page="pagination.page" |
| | | :page-size="pagination.pageSize" |
| | | @change="handlePageChange" |
| | | /> |
| | | <Pagination |
| | | style="margin-top: 12px; flex-shrink: 0" |
| | | :count="pagination.itemCount" |
| | | :page="pagination.page" |
| | | :page-size="pagination.pageSize" |
| | | @change="handlePageChange" |
| | | /> |
| | | </div> |
| | | |
| | | <template #footer> |
| | | <div class="flex justify-end gap-3"> |
| | | <n-button style="width: 84px; height: 32px" @click="handleCancel" |
| | | >取消</n-button |
| | | > |
| | | <n-button style="width: 84px; height: 32px" @click="handleCancel"> |
| | | 取消 |
| | | </n-button> |
| | | <n-button |
| | | type="primary" |
| | | style="width: 84px; height: 32px" |
| | | @click="handleConfirm" |
| | | >保存</n-button |
| | | > |
| | | 保存 |
| | | </n-button> |
| | | </div> |
| | | </template> |
| | | </n-card> |
| | |
| | | <script setup lang="tsx"> |
| | | import { computed, h, onMounted, ref, watch } from 'vue' |
| | | import { useMessage } from 'naive-ui' |
| | | import SelectPointModal from './components/SelectPointModal.vue' |
| | | import { getSpecialPoints, setSpecialPoint } from '@/service/api/point' |
| | | import { computed, onActivated, onMounted, ref, watch } from 'vue' |
| | | import { useRouter } from 'vue-router' |
| | | import { useDialog, useMessage } from 'naive-ui' |
| | | import { deleteSpecialPoint, getSpecialPoints } from '@/service/api/point' |
| | | import { useBoolean } from '@/hooks' |
| | | import SearchPanel from '@/components/custom/SearchPanel.vue' |
| | | import Pagination from '@/components/common/Pagination.vue' |
| | | import OperationLogModal from '@/components/custom/OperationLogModal.vue' |
| | | |
| | | const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false) |
| | | const router = useRouter() |
| | | const dialog = useDialog() |
| | | const message = useMessage() |
| | | |
| | | const showSelectModal = ref(false) |
| | | const showOperationLogModal = ref(false) |
| | | const activeTab = ref<'park' | 'charge'>('park') |
| | | const currentSelectType = ref<'charge' | 'park'>('park') |
| | | const editingPointId = ref<number | string | null>(null) |
| | | |
| | | const chargePoints = ref<any[]>([]) |
| | | const parkPoints = ref<any[]>([]) |
| | | const total = ref(0) |
| | | const model = ref({ |
| | | pointName: '', |
| | | pointAddress: '', |
| | |
| | | pageSize: 10, |
| | | }) |
| | | |
| | | const currentExistingPoints = computed(() => { |
| | | if (editingPointId.value !== null) |
| | | return [] |
| | | |
| | | return currentTabList.value.map(item => ({ |
| | | id: item.pointId, |
| | | pointName: item.pointName, |
| | | pointAddress: item.pointAddress, |
| | | })) |
| | | }) |
| | | |
| | | const currentTabList = computed(() => activeTab.value === 'charge' ? chargePoints.value : parkPoints.value) |
| | | |
| | | const filteredList = computed(() => { |
| | | return currentTabList.value.filter((item) => { |
| | | const matchPointName = !model.value.pointName || String(item.pointName || '').includes(model.value.pointName.trim()) |
| | | const matchPointAddress = !model.value.pointAddress || String(item.pointAddress || '').includes(model.value.pointAddress.trim()) |
| | | return matchPointName && matchPointAddress |
| | | }) |
| | | }) |
| | | |
| | | const pagedList = computed(() => { |
| | | const startIndex = (pageParams.value.pageNum - 1) * pageParams.value.pageSize |
| | | return filteredList.value.slice(startIndex, startIndex + pageParams.value.pageSize) |
| | | }) |
| | | |
| | | const columns = computed(() => [ |
| | | { |
| | | title: '点位名称', |
| | | key: 'pointName', |
| | | minWidth: 220, |
| | | width: 220, |
| | | resizable: true, |
| | | ellipsis: { tooltip: true }, |
| | | }, |
| | | { |
| | | title: '详细地址', |
| | | key: 'pointAddress', |
| | | minWidth: 280, |
| | | width: 280, |
| | | resizable: true, |
| | | ellipsis: { tooltip: true }, |
| | | }, |
| | | { |
| | | title: '关联车辆数', |
| | | key: 'relatedVehicleCount', |
| | | width: 140, |
| | | resizable: true, |
| | | render: (row: any) => getRelatedVehicleCount(row), |
| | | }, |
| | | { |
| | | title: '操作', |
| | | key: 'actions', |
| | | width: 120, |
| | | render: (row: any) => [ |
| | | h('span', { |
| | | class: 'table-action', |
| | | onClick: () => handleEditPoint(row), |
| | | }, '编辑'), |
| | | h('span', { |
| | | class: 'table-action ml-16px', |
| | | onClick: () => handleRemovePoint(activeTab.value, row.pointId), |
| | | }, '移除'), |
| | | ], |
| | | render: (row: any) => ( |
| | | <div class="flex items-center"> |
| | | <n-button text type="primary" onClick={() => handleEditPoint(row)}> |
| | | 编辑 |
| | | </n-button> |
| | | <n-button text type="primary" class="ml-12px" onClick={() => handleRemovePoint(activeTab.value, row.pointId)}> |
| | | 移除 |
| | | </n-button> |
| | | </div> |
| | | ), |
| | | }, |
| | | ]) |
| | | |
| | |
| | | ]) |
| | | |
| | | function getRelatedVehicleCount(row: any) { |
| | | return row.relatedVehicleCount ?? row.bindVehicleCount ?? row.vehicleCount ?? 0 |
| | | return row.relatedVehicleCount ?? row.bindVehicleCount ?? row.vehicleCount ?? row.bindCount ?? 0 |
| | | } |
| | | |
| | | function getPointsType() { |
| | | return activeTab.value === 'charge' ? 1 : 2 |
| | | } |
| | | |
| | | async function getList() { |
| | | startLoading() |
| | | try { |
| | | const { data } = await getSpecialPoints() |
| | | chargePoints.value = data?.chargePoints || [] |
| | | parkPoints.value = data?.parkingPoints || [] |
| | | const { data } = await getSpecialPoints({ |
| | | pageNum: pageParams.value.pageNum, |
| | | pageSize: pageParams.value.pageSize, |
| | | pointName: model.value.pointName.trim(), |
| | | pointAddress: model.value.pointAddress.trim(), |
| | | pointsType: getPointsType(), |
| | | }) |
| | | const records = Array.isArray(data?.records) ? data.records : [] |
| | | total.value = data?.total || 0 |
| | | pageParams.value.pageNum = data?.current || pageParams.value.pageNum |
| | | pageParams.value.pageSize = data?.size || pageParams.value.pageSize |
| | | |
| | | if (activeTab.value === 'charge') |
| | | chargePoints.value = records |
| | | else |
| | | parkPoints.value = records |
| | | } |
| | | finally { |
| | | endLoading() |
| | | } |
| | | } |
| | | |
| | | function handleSelectPoint(type: 'charge' | 'park') { |
| | | currentSelectType.value = type |
| | | editingPointId.value = null |
| | | showSelectModal.value = true |
| | | } |
| | | |
| | | function handleConfirmSelect(points: any[]) { |
| | | const selectedPoints = points.map(p => ({ |
| | | pointId: p.id, |
| | | pointName: p.pointName, |
| | | pointAddress: p.pointAddress, |
| | | relatedVehicleCount: p.relatedVehicleCount ?? p.bindVehicleCount ?? p.vehicleCount ?? 0, |
| | | })) |
| | | |
| | | if (editingPointId.value !== null) { |
| | | const targetPoint = selectedPoints[0] |
| | | if (!targetPoint) |
| | | return |
| | | |
| | | const targetList = currentSelectType.value === 'charge' ? chargePoints.value : parkPoints.value |
| | | const currentIndex = targetList.findIndex(item => String(item.pointId) === String(editingPointId.value)) |
| | | const duplicateIndex = targetList.findIndex(item => String(item.pointId) === String(targetPoint.pointId)) |
| | | if (duplicateIndex !== -1 && duplicateIndex !== currentIndex) { |
| | | message.warning('该点位已存在,请重新选择') |
| | | return |
| | | } |
| | | if (currentIndex !== -1) |
| | | targetList.splice(currentIndex, 1, targetPoint) |
| | | |
| | | editingPointId.value = null |
| | | return |
| | | } |
| | | |
| | | if (currentSelectType.value === 'charge') { |
| | | const existingIds = new Set(chargePoints.value.map(p => p.pointId)) |
| | | const newPoints = selectedPoints.filter(p => !existingIds.has(p.pointId)) |
| | | chargePoints.value.push(...newPoints) |
| | | } |
| | | else { |
| | | const existingIds = new Set(parkPoints.value.map(p => p.pointId)) |
| | | const newPoints = selectedPoints.filter(p => !existingIds.has(p.pointId)) |
| | | parkPoints.value.push(...newPoints) |
| | | } |
| | | } |
| | | |
| | | function handleRemovePoint(type: 'charge' | 'park', pointId: number | string) { |
| | | if (type === 'charge') { |
| | | chargePoints.value = chargePoints.value.filter(item => String(item.pointId) !== String(pointId)) |
| | | } |
| | | else { |
| | | parkPoints.value = parkPoints.value.filter(item => String(item.pointId) !== String(pointId)) |
| | | } |
| | | dialog.warning({ |
| | | title: '删除确认', |
| | | content: '确认移除该特殊点位吗?', |
| | | positiveText: '确认', |
| | | negativeText: '取消', |
| | | onPositiveClick: async () => { |
| | | const res = await deleteSpecialPoint({ |
| | | pointId, |
| | | pointsType: type === 'charge' ? 1 : 2, |
| | | }) |
| | | |
| | | if (res.code !== 200) { |
| | | return |
| | | } |
| | | message.success('移除成功') |
| | | getList() |
| | | }, |
| | | }) |
| | | } |
| | | |
| | | function handleEditPoint(row: any) { |
| | | currentSelectType.value = activeTab.value |
| | | editingPointId.value = row.pointId |
| | | showSelectModal.value = true |
| | | router.push({ |
| | | name: 'patrolPointsAdd', |
| | | query: { |
| | | type: activeTab.value, |
| | | mode: 'edit', |
| | | pointId: row.pointId, |
| | | }, |
| | | }) |
| | | } |
| | | |
| | | function handleSearch() { |
| | | pageParams.value.pageNum = 1 |
| | | getList() |
| | | } |
| | | |
| | | function handleResetSearch() { |
| | | model.value.pointName = '' |
| | | model.value.pointAddress = '' |
| | | pageParams.value.pageNum = 1 |
| | | getList() |
| | | } |
| | | |
| | | function handlePageChange(page: number, pageSize: number) { |
| | | pageParams.value.pageNum = page |
| | | pageParams.value.pageSize = pageSize |
| | | getList() |
| | | } |
| | | |
| | | function handleOpenOperationLog() { |
| | | showOperationLogModal.value = true |
| | | } |
| | | |
| | | async function handleSave() { |
| | | if (!chargePoints.value.length || !parkPoints.value.length) { |
| | | message.warning('请先选择充电点位和收车点位') |
| | | return |
| | | } |
| | | startLoading() |
| | | try { |
| | | const params = { |
| | | chargePoints: chargePoints.value.map(p => p.pointId), |
| | | parkingPoints: parkPoints.value.map(p => p.pointId), |
| | | } |
| | | const { code, data } = await setSpecialPoint(params) |
| | | if (code !== 200) { |
| | | message.error(data || '保存失败') |
| | | return |
| | | } |
| | | message.success('保存成功') |
| | | getList() |
| | | } |
| | | finally { |
| | | endLoading() |
| | | } |
| | | function handleAdd() { |
| | | router.push({ |
| | | name: 'patrolPointsAdd', |
| | | query: { |
| | | type: activeTab.value, |
| | | }, |
| | | }) |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getList() |
| | | }) |
| | | |
| | | onActivated(() => { |
| | | if (window.sessionStorage.getItem('cruisePatrolPointNeedRefresh') === '1') { |
| | | window.sessionStorage.removeItem('cruisePatrolPointNeedRefresh') |
| | | getList() |
| | | } |
| | | }) |
| | | |
| | | watch(activeTab, () => { |
| | | handleResetSearch() |
| | | pageParams.value.pageNum = 1 |
| | | model.value.pointName = '' |
| | | model.value.pointAddress = '' |
| | | getList() |
| | | }) |
| | | </script> |
| | | |
| | |
| | | <n-button @click="handleOpenOperationLog"> |
| | | 操作日志 |
| | | </n-button> |
| | | <n-button secondary type="primary" @click="handleSelectPoint(activeTab)"> |
| | | <n-button secondary type="primary" @click="handleAdd"> |
| | | 新增 |
| | | </n-button> |
| | | </n-flex> |
| | |
| | | <n-data-table |
| | | class="flex-1" |
| | | :columns="columns" |
| | | :data="pagedList" |
| | | :data="currentTabList" |
| | | :loading="loading" |
| | | flex-height |
| | | :bordered="false" |
| | |
| | | </template> |
| | | </n-data-table> |
| | | <Pagination |
| | | :count="filteredList.length" |
| | | :count="total" |
| | | :page="pageParams.pageNum" |
| | | :page-size="pageParams.pageSize" |
| | | class="robotaxi-pagination mt-12px" |
| | |
| | | /> |
| | | </div> |
| | | </n-card> |
| | | |
| | | <SelectPointModal |
| | | v-model:show="showSelectModal" |
| | | :existing-points="currentExistingPoints" |
| | | @confirm="handleConfirmSelect" |
| | | /> |
| | | <OperationLogModal v-model:show="showOperationLogModal" /> |
| | | </div> |
| | | </template> |
| | |
| | | <style scoped lang="scss"> |
| | | :deep(.n-tabs-nav-scroll-content) { |
| | | border-color: transparent !important; |
| | | } |
| | | |
| | | .table-action { |
| | | color: #0068ff; |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .table-action:hover { |
| | | opacity: 0.8; |
| | | } |
| | | </style> |