<script setup lang="tsx">
|
import type { DataTableColumns } from 'naive-ui'
|
import { useBoolean } from '@/hooks'
|
import { NButton, NDatePicker, NInput, NSelect, NSpace } from 'naive-ui'
|
import SearchPanel from '@/components/custom/SearchPanel.vue'
|
import OperationLogModal from '@/components/custom/OperationLogModal.vue'
|
import { deletePoint, enableDisablePoint, exportPointExcel, fetchPointPageList } from '@/service/api/copyText'
|
import { downloadFile } from '@/utils'
|
import dayjs from 'dayjs'
|
|
const router = useRouter()
|
const message = useMessage()
|
const dialog = useDialog()
|
|
const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
|
const { t } = useI18n()
|
|
const initialModel = {
|
pointNumber: '',
|
pointName: '',
|
operationStatus: null,
|
operationStatusList: null,
|
parkingTimeLimit: '',
|
pointLonAndLat: '',
|
createBy: '',
|
createTime: null,
|
}
|
|
const model = ref({ ...initialModel })
|
|
const isStatusSelectOpen = ref(false)
|
|
function normalizePointLonAndLatInput(raw: unknown) {
|
const value = String(raw ?? '').trim()
|
if (!value)
|
return ''
|
|
const normalizedPunctuation = value.replace(/,/g, ',')
|
const numbers = normalizedPunctuation.match(/-?\d+(?:\.\d+)?/g)
|
|
if (numbers && numbers.length >= 2)
|
return `${numbers[0]},${numbers[1]}`
|
|
return normalizedPunctuation
|
.replace(/[\t\r\n]/g, '')
|
.trim()
|
}
|
|
function getPointStatusText(operationStatus: number, verificationStatus?: number) {
|
const v = Number(verificationStatus)
|
const o = Number(operationStatus)
|
|
if (v === 1)
|
return '待测试'
|
if (v === 2)
|
return '测试中'
|
if (v === 4)
|
return '测试不通过'
|
if (v === 3)
|
return '可运营'
|
if (v === 5) {
|
if (o === 1)
|
return '待生效'
|
if (o === 2)
|
return '启用中'
|
if (o === 3)
|
return '已停用'
|
if (o === 4)
|
return '已失效'
|
}
|
return '待测试'
|
}
|
|
function getStatusClass(status: string) {
|
switch (status) {
|
case '待测试':
|
return 'wait-test'
|
case '测试中':
|
return 'testing'
|
case '测试不通过':
|
case '已失效':
|
return 'test-fail'
|
case '可运营':
|
return 'test-pass'
|
case '待生效':
|
return 'test-take-effect'
|
case '启用中':
|
return 'test-starting'
|
case '已停用':
|
return 'test-stop'
|
default:
|
return ''
|
}
|
}
|
|
const statusMap: Record<number, { label: string, type: 'default' | 'success' | 'warning' | 'error' | 'info' | 'primary' }> = {
|
1: { label: '待测试', type: 'warning' },
|
2: { label: '测试中', type: 'info' },
|
3: { label: '测试不通过', type: 'error' },
|
4: { label: '可运营', type: 'info' },
|
5: { label: '待生效', type: 'warning' },
|
6: { label: '启用中', type: 'success' },
|
7: { label: '已停用', type: 'default' },
|
8: { label: '已失效', type: 'default' },
|
}
|
|
const columnWidths = {
|
selection: 48,
|
pointNumber: 160,
|
pointName: 240,
|
operationStatus: 140,
|
parkingTimeLimit: 140,
|
pointLon: 160,
|
pointLat: 160,
|
createBy: 160,
|
createTime: 200,
|
actions: 100,
|
} as const
|
|
const tableScrollX = Math.min(
|
1600,
|
Object.values(columnWidths).reduce((sum, width) => sum + width, 0),
|
)
|
|
const columns = computed<DataTableColumns<any>>(() => [
|
{
|
type: 'selection',
|
width: columnWidths.selection,
|
align: 'center',
|
},
|
{
|
title: '点位ID',
|
key: 'pointNumber',
|
width: columnWidths.pointNumber,
|
align: 'left',
|
resizable: true,
|
ellipsis: true,
|
},
|
{
|
title: '点位名称',
|
key: 'pointName',
|
width: columnWidths.pointName,
|
align: 'left',
|
resizable: true,
|
ellipsis: true,
|
render(row) {
|
return (
|
<div
|
class="cursor-pointer text-ellipsis whitespace-nowrap w-full overflow-hidden text-[#0068FF]"
|
onClick={() => handleToDetail(row, 'detail')}
|
>
|
{row.pointName}
|
</div>
|
)
|
},
|
},
|
{
|
title: '状态',
|
key: 'operationStatus',
|
width: columnWidths.operationStatus,
|
align: 'left',
|
resizable: true,
|
ellipsis: true,
|
render(row) {
|
const statusText = getPointStatusText(row.operationStatus, row.verificationStatus)
|
const statusClass = getStatusClass(statusText)
|
return (
|
<div class={`status-tag ${statusClass}`}>
|
{statusText}
|
</div>
|
)
|
},
|
},
|
{
|
title: '限停时长',
|
key: 'parkingTimeLimit',
|
width: columnWidths.parkingTimeLimit,
|
align: 'left',
|
ellipsis: true,
|
resizable: true,
|
render(row) {
|
return row.parkingTimeLimit ? `${row.parkingTimeLimit}分钟` : '-'
|
},
|
},
|
{
|
title: '经度',
|
key: 'pointLon',
|
width: columnWidths.pointLon,
|
align: 'left',
|
ellipsis: true,
|
resizable: true,
|
},
|
{
|
title: '纬度',
|
key: 'pointLat',
|
width: columnWidths.pointLat,
|
align: 'left',
|
ellipsis: true,
|
resizable: true,
|
},
|
{
|
title: '创建人',
|
key: 'createBy',
|
width: columnWidths.createBy,
|
align: 'left',
|
ellipsis: true,
|
resizable: true,
|
},
|
{
|
title: '创建时间',
|
key: 'createTime',
|
width: columnWidths.createTime,
|
align: 'left',
|
ellipsis: true,
|
resizable: true,
|
render(row) {
|
return row.createTime ? dayjs(row.createTime).format('YYYY/MM/DD HH:mm') || '-' : '-'
|
},
|
},
|
{
|
title: '操作',
|
key: 'actions',
|
width: columnWidths.actions,
|
align: 'left',
|
fixed: 'right',
|
render: (row) => {
|
if (row.verificationStatus === 5 && row.operationStatus === 2) {
|
return (
|
<NSpace>
|
<NButton
|
text
|
v-permission="923"
|
type="primary"
|
onClick={() => handleToDetail(row, 'edit')}
|
>
|
编辑
|
</NButton>
|
<NButton
|
text
|
v-permission="924"
|
type="primary"
|
onClick={() => handleEnableDisable(row)}
|
>
|
停用
|
</NButton>
|
</NSpace>
|
)
|
}
|
|
return (
|
<NSpace>
|
<NButton
|
text
|
v-permission="923"
|
type="primary"
|
onClick={() => handleToDetail(row, 'edit')}
|
>
|
编辑
|
</NButton>
|
<NButton
|
text
|
v-permission="924"
|
type="primary"
|
onClick={() => handleDelete(row)}
|
>
|
删除
|
</NButton>
|
</NSpace>
|
)
|
},
|
},
|
])
|
|
const statusOptions = Object.keys(statusMap).map(key => ({
|
label: statusMap[Number(key)].label,
|
value: Number(key),
|
}))
|
|
const filterItems = computed(() => [
|
{
|
label: '点位ID',
|
path: 'pointNumber',
|
render: () => <NInput v-model:value={model.value.pointNumber} placeholder="请输入" />,
|
},
|
{
|
label: '点位名称',
|
path: 'pointName',
|
render: () => <NInput v-model:value={model.value.pointName} placeholder="请输入" />,
|
},
|
{
|
label: '点位状态',
|
path: 'operationStatusList',
|
render: () => (
|
<NSelect
|
v-model:value={model.value.operationStatusList}
|
options={statusOptions}
|
placeholder="请选择"
|
clearable
|
multiple
|
max-tag-count="responsive"
|
onUpdate:show={(value: boolean) => isStatusSelectOpen.value = value}
|
>
|
{{
|
arrow: () => (
|
<img
|
src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e120f81c-9f15-4e1a-9938-22252898edf9.png"
|
class={`arrow-icon ${isStatusSelectOpen.value ? 'arrow-icon--open' : ''}`}
|
style={{ width: '16px', height: '16px' }}
|
alt="arrow"
|
/>
|
),
|
}}
|
</NSelect>
|
),
|
},
|
{
|
label: '限停时长',
|
path: 'parkingTimeLimit',
|
render: () => <NInput v-model:value={model.value.parkingTimeLimit} placeholder="请输入" />,
|
},
|
{
|
label: '经纬度',
|
path: 'pointLonAndLat',
|
render: () => <NInput v-model:value={model.value.pointLonAndLat} placeholder="请输入" />,
|
},
|
{
|
label: '创建人',
|
path: 'createBy',
|
render: () => <NInput v-model:value={model.value.createBy} placeholder="请输入" />,
|
},
|
{
|
label: '创建时间',
|
path: 'createTime',
|
render: () => <NDatePicker v-model:value={model.value.createTime} type="daterange" format="yyyy/MM/dd" clearable actions={[]} close-on-select={true} start-placeholder="开始时间" end-placeholder="结束时间" separator="-" />,
|
},
|
])
|
|
const listData = ref<any>([])
|
const checkedRowKeys = ref<Array<string | number>>([])
|
const showOperationLogModal = ref(false)
|
|
const pagination = reactive({
|
page: 1,
|
pageSize: 10,
|
itemCount: 0,
|
showSizePicker: true,
|
pageSizes: [10, 20, 50, 100],
|
prefix: ({ itemCount }: { itemCount: number | undefined }) => `共 ${itemCount || 0} 条`,
|
onChange: (page: number) => {
|
pagination.page = page
|
getList()
|
},
|
onUpdatePageSize: (pageSize: number) => {
|
pagination.pageSize = pageSize
|
pagination.page = 1
|
getList()
|
},
|
})
|
|
function handleSearch() {
|
pagination.page = 1
|
getList()
|
}
|
|
function handleResetSearch() {
|
model.value = { ...initialModel }
|
handleSearch()
|
}
|
|
async function getList() {
|
startLoading()
|
try {
|
const {
|
createTime,
|
operationStatus,
|
...rest
|
} = {
|
pageNum: pagination.page,
|
pageSize: pagination.pageSize,
|
...model.value,
|
pointLonAndLat: normalizePointLonAndLatInput(model.value.pointLonAndLat),
|
startTime: model.value.createTime ? dayjs(model.value.createTime[0]).startOf('day').format('YYYY-MM-DD HH:mm:ss') : undefined,
|
endTime: model.value.createTime ? dayjs(model.value.createTime[1]).endOf('day').format('YYYY-MM-DD HH:mm:ss') : undefined,
|
}
|
|
const res = await fetchPointPageList(rest)
|
if (res.code === 200 && res.data) {
|
listData.value = res.data.records || []
|
pagination.itemCount = res.data.total || 0
|
}
|
}
|
finally {
|
endLoading()
|
}
|
}
|
|
function handleToDetail(row: any, type: string) {
|
router.push({ name: 'testPointManagement', query: { pointId: row.id, type } })
|
}
|
|
function handleDelete(row: any) {
|
const statusText = getPointStatusText(row.operationStatus, row.verificationStatus)
|
if (['已停用', '已失效'].includes(statusText)) {
|
dialog.warning({
|
title: '提示',
|
content: '该点位置投入运营,删除后将隐藏所有关联历史位置记录,建议仅保持‘已停用’。是否确认删除?',
|
positiveText: '确定',
|
negativeText: '取消',
|
maskClosable: false,
|
autoFocus: false,
|
onPositiveClick: async () => {
|
try {
|
const res = await deletePoint(row.id)
|
if (res.code === 200) {
|
message.success('删除成功')
|
getList()
|
}
|
}
|
catch {
|
}
|
},
|
})
|
return
|
}
|
if (['待测试', '测试中', '测试不通过', '可运营', '待生效'].includes(statusText)) {
|
deletePoint(row.id).then((res) => {
|
if (res.code === 200) {
|
message.success('删除成功')
|
getList()
|
}
|
}).catch(() => {
|
})
|
return
|
}
|
dialog.warning({
|
title: '提示',
|
content: '删除后该点位及其历史位置记录将永久下线,确认删除吗?',
|
positiveText: '确定',
|
negativeText: '取消',
|
maskClosable: false,
|
autoFocus: false,
|
onPositiveClick: async () => {
|
try {
|
const res = await deletePoint(row.id)
|
if (res.code === 200) {
|
message.success('删除成功')
|
getList()
|
}
|
}
|
catch {
|
}
|
},
|
})
|
}
|
|
function handleEnableDisable(row: any) {
|
const isEnabled = row.verificationStatus === 5 && row.operationStatus === 2
|
const actionText = isEnabled ? '停用' : '启用'
|
const targetStatus = isEnabled ? 3 : 2
|
|
dialog.warning({
|
title: '提示',
|
content: `确认${actionText}该点位吗?`,
|
positiveText: '确定',
|
negativeText: '取消',
|
maskClosable: false,
|
autoFocus: false,
|
onPositiveClick: async () => {
|
try {
|
const res = await enableDisablePoint({
|
id: row.id,
|
operationStatus: targetStatus,
|
})
|
if (res.code === 200) {
|
message.success(`${actionText}成功`)
|
getList()
|
}
|
}
|
catch {
|
}
|
},
|
})
|
}
|
|
async function handleBatchExport() {
|
if (checkedRowKeys.value.length === 0) {
|
message.warning('请先选择记录')
|
return
|
}
|
try {
|
const res = await exportPointExcel({ pointIds: checkedRowKeys.value })
|
if (res instanceof Blob) {
|
downloadFile(res, '点位列表.xlsx')
|
message.success(`导出成功`)
|
}
|
else {
|
message.error('导出失败')
|
}
|
}
|
catch {
|
message.error('导出失败')
|
}
|
}
|
|
function handleMapMode() {
|
router.push({ name: 'testPointManagement' })
|
}
|
|
function handleOpenOperationLog() {
|
showOperationLogModal.value = true
|
}
|
|
function handleCheck(rowKeys: Array<string | number>) {
|
checkedRowKeys.value = rowKeys
|
}
|
|
function changePage(page: number, size: number) {
|
pagination.page = page
|
pagination.pageSize = size
|
getList()
|
}
|
|
onMounted(() => {
|
getList()
|
})
|
</script>
|
|
<template>
|
<div class="roboTaxi w-full h-full px-[12px] py-[12px]">
|
<div class="roboTaxi-form-card mb-12px">
|
<n-form
|
ref="formRef"
|
:model="model"
|
label-placement="left"
|
inline
|
:show-feedback="false"
|
>
|
<SearchPanel>
|
<n-gi v-for="item in filterItems" :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="handleResetSearch">
|
{{ t("common.reset") }}
|
</NButton>
|
<NButton type="primary" @click="handleSearch">
|
{{ t("common.search") }}
|
</NButton>
|
</template>
|
</SearchPanel>
|
</n-form>
|
</div>
|
|
<n-card
|
class="flex-1 robotaxi-table-card point-management-list"
|
content-style="display: flex; flex-direction: column; flex: 1; min-height: 0; min-width: 0;"
|
>
|
<template #header>
|
<n-flex justify="space-between" align="center">
|
<n-flex align="center">
|
<n-flex v-permission="921" align="center">
|
<span
|
v-if="checkedRowKeys.length > 0"
|
style="color: rgba(23, 31, 38, 0.6); font-weight: 400"
|
>已选择{{ checkedRowKeys.length }}条记录</span
|
>
|
<NButton
|
:disabled="checkedRowKeys.length === 0"
|
@click="handleBatchExport"
|
>
|
批量导出
|
</NButton>
|
</n-flex>
|
<NButton v-permission="2026051803" @click="handleOpenOperationLog"> 操作日志 </NButton>
|
</n-flex>
|
|
<n-flex>
|
<NButton
|
v-permission="922"
|
type="primary"
|
secondary
|
@click="handleMapMode"
|
>
|
点位地图
|
</NButton>
|
</n-flex>
|
</n-flex>
|
</template>
|
|
<div
|
class="flex-1 flex flex-col h-full"
|
style="min-width: 0; overflow: hidden"
|
>
|
<n-data-table
|
class="flex-1"
|
:columns="columns"
|
:data="listData"
|
:loading="loading"
|
flex-height
|
:bordered="false"
|
:header-height="18"
|
:row-height="18"
|
:scroll-x="tableScrollX"
|
:row-key="(row) => row.id"
|
@update:checked-row-keys="handleCheck"
|
>
|
<template #empty>
|
<div class="flex-center">
|
<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="pagination.itemCount"
|
:page="pagination.page"
|
:page-size="pagination.pageSize"
|
class="robotaxi-pagination mt-12px"
|
@change="changePage"
|
/>
|
</div>
|
</n-card>
|
<OperationLogModal
|
v-model:show="showOperationLogModal"
|
:business-type="44"
|
/>
|
</div>
|
</template>
|
|
<style lang="scss">
|
.point-management-list {
|
.n-data-table-th {
|
background-color: #fafafc;
|
font-weight: 500;
|
}
|
|
.status-tag {
|
display: inline-block;
|
padding: 2px 8px;
|
border-radius: 4px;
|
font-family: PingFang SC;
|
font-size: 12px;
|
font-weight: normal;
|
line-height: 16px;
|
text-align: center;
|
margin: 0 4px;
|
|
&.wait-test {
|
background: #fff6ea;
|
color: #ffaa2a;
|
}
|
|
&.testing {
|
background: #e6f4ff;
|
color: #0068ff;
|
}
|
|
&.test-fail {
|
background: #fff3f1;
|
color: #e02716;
|
}
|
|
&.test-pass {
|
color: #0068ff;
|
background: #e6f4ff;
|
}
|
|
&.test-take-effect {
|
background: #fff6ea;
|
color: #ffaa2a;
|
}
|
|
&.test-starting {
|
background: #e9f9ed;
|
color: #19b144;
|
}
|
|
&.test-stop {
|
background: #e8e9e9;
|
color: #171f26;
|
}
|
|
&.test-invalid {
|
background: #e8e9e9;
|
color: #171f26;
|
}
|
}
|
}
|
</style>
|