zjk
2026-06-03 d9bb7aeca3754223d14e0303434eccaf712d12ef
refactor(api,pages): 替换Mock接口并优化多页面交互与UI

将财务账单模块的Mock数据与模拟接口替换为真实后端API请求
优化交易策略页面时间选择器,选择后自动关闭弹窗
重构财务账单页面:调整表格列配置、修复手动推送弹窗加载状态
全面重构巡逻点位管理页面,新增标签页、搜索过滤、分页及编辑删除功能
4个文件已修改
552 ■■■■■ 已修改文件
src/service/api/financeBill.ts 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/cruise/patrol-points/index.vue 336 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/financeBill/index.vue 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/transaction/transactionStrategy/index.vue 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/service/api/financeBill.ts
@@ -1,4 +1,4 @@
import dayjs from 'dayjs'
import { request } from '@/service/http'
export interface FinanceBillRecord {
  id: number
@@ -8,90 +8,14 @@
  returnResult: string
}
export interface FinanceBillLogRecord {
  id: number
  operatorName: string
  operatorAccount: string
  operationType: number
  operationContent: string
  operationIp: string
  operationTime: string
}
const operatorNames = ['蔡文婷', '黄芸欢', '王思龙']
const financeBillResultTemplates = [
  {
    codeResult: 'success' as const,
    returnResult: '返回结果返回结果返回结果返回结果返回结果返回结果返回结果返回结果返回结果返回结果...',
  },
  {
    codeResult: 'param_error' as const,
    returnResult: '参数校验失败,请检查账单批次号、账期和推送目标后重试。',
  },
  {
    codeResult: 'repeat_push' as const,
    returnResult: '重复事件,同一账单在短时间内已发起过一次推送,系统已自动拦截。',
  },
]
// TODO: 联调时将以下 mock 实现替换为真实接口请求。
const financeBillMockList: FinanceBillRecord[] = Array.from({ length: 28 }, (_, index) => {
  const result = financeBillResultTemplates[index % financeBillResultTemplates.length]
  return {
    id: index + 1,
    operationTime: dayjs('2026-09-09 11:09:08').subtract(index, 'minute').format('YYYY-MM-DD HH:mm:ss'),
    operatorName: operatorNames[index % operatorNames.length],
    codeResult: result.codeResult,
    returnResult: result.returnResult,
  }
})
const financeBillOperationLogMockList: FinanceBillLogRecord[] = Array.from({ length: 18 }, (_, index) => ({
  id: index + 1,
  operatorName: operatorNames[index % operatorNames.length],
  operatorAccount: `finance_${index + 1}`,
  operationType: index % 2 === 0 ? 2 : 1,
  operationContent: index % 2 === 0 ? '执行财务账单手动推送' : '查看财务账单详情',
  operationIp: `10.10.10.${index + 10}`,
  operationTime: dayjs('2026-09-09 11:09:08').subtract(index * 3, 'minute').format('YYYY-MM-DD HH:mm:ss'),
}))
function paginateList<T>(list: T[], pageNum = 1, pageSize = 10) {
  const startIndex = (pageNum - 1) * pageSize
  return {
    records: list.slice(startIndex, startIndex + pageSize),
    total: list.length,
  }
}
export function fetchFinanceBillPage(params: { pageNum?: number, pageSize?: number }) {
  const { pageNum = 1, pageSize = 10 } = params || {}
  return Promise.resolve({
    code: 200,
    data: paginateList(financeBillMockList, pageNum, pageSize),
  })
  return request.Post<Service.ResponseResult<any>>('/order/api/v1/order/manualPush/batchDetail', params)
}
export function fetchFinanceBillDetail(id: number | string) {
  const target = financeBillMockList.find(item => String(item.id) === String(id)) || null
  return Promise.resolve({
    code: 200,
    data: target,
  })
  return request.Get<Service.ResponseResult<any>>('/order/api/v1/order/manualPush/detail', { params: { id } })
}
export function manualPushFinanceBill() {
  return Promise.resolve({
    code: 200,
    msg: '手动推送成功',
  })
}
export function fetchFinanceBillOperationLogPage(params: { pageNum?: number, pageSize?: number }) {
  const { pageNum = 1, pageSize = 10 } = params || {}
  return Promise.resolve({
    code: 200,
    data: paginateList(financeBillOperationLogMockList, pageNum, pageSize),
  })
  return request.Post<Service.ResponseResult<any>>('/order/api/v1/order/manualPush')
}
src/views/businessOperations/cruise/patrol-points/index.vue
@@ -1,23 +1,111 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
<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 { 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 message = useMessage()
const showSelectModal = ref(false)
const currentSelectType = ref<'charge' | 'park'>('charge')
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 currentExistingIds = computed(() => {
  const list = currentSelectType.value === 'charge' ? chargePoints.value : parkPoints.value
  return list.map(p => p.pointId)
const model = ref({
  pointName: '',
  pointAddress: '',
})
const pageParams = ref({
  pageNum: 1,
  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,
    ellipsis: { tooltip: true },
  },
  {
    title: '详细地址',
    key: 'pointAddress',
    minWidth: 280,
    ellipsis: { tooltip: true },
  },
  {
    title: '关联车辆数',
    key: 'relatedVehicleCount',
    width: 140,
    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),
      }, '移除'),
    ],
  },
])
const filterItems = computed(() => [
  {
    label: '点位名称',
    path: 'pointName',
    render: () => <n-input v-model:value={model.value.pointName} placeholder="请输入" clearable />,
  },
  {
    label: '详细地址',
    path: 'pointAddress',
    render: () => <n-input v-model:value={model.value.pointAddress} placeholder="请输入" clearable />,
  },
])
function getRelatedVehicleCount(row: any) {
  return row.relatedVehicleCount ?? row.bindVehicleCount ?? row.vehicleCount ?? 0
}
async function getList() {
  startLoading()
@@ -33,6 +121,7 @@
function handleSelectPoint(type: 'charge' | 'park') {
  currentSelectType.value = type
  editingPointId.value = null
  showSelectModal.value = true
}
@@ -41,7 +130,27 @@
    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))
@@ -55,13 +164,38 @@
  }
}
function handleRemovePoint(type: 'charge' | 'park', index: number) {
function handleRemovePoint(type: 'charge' | 'park', pointId: number | string) {
  if (type === 'charge') {
    chargePoints.value.splice(index, 1)
    chargePoints.value = chargePoints.value.filter(item => String(item.pointId) !== String(pointId))
  }
  else {
    parkPoints.value.splice(index, 1)
    parkPoints.value = parkPoints.value.filter(item => String(item.pointId) !== String(pointId))
  }
}
function handleEditPoint(row: any) {
  currentSelectType.value = activeTab.value
  editingPointId.value = row.pointId
  showSelectModal.value = true
}
function handleSearch() {
  pageParams.value.pageNum = 1
}
function handleResetSearch() {
  model.value.pointName = ''
  model.value.pointAddress = ''
  pageParams.value.pageNum = 1
}
function handlePageChange(page: number, pageSize: number) {
  pageParams.value.pageNum = page
  pageParams.value.pageSize = pageSize
}
function handleOpenOperationLog() {
  showOperationLogModal.value = true
}
async function handleSave() {
@@ -91,117 +225,103 @@
onMounted(() => {
  getList()
})
watch(activeTab, () => {
  handleResetSearch()
})
</script>
<template>
  <div class="h-full roboTaxi">
    <n-card class="w-full full-content" style="border-radius: 6px" content-style="padding:24px;">
      <div class="mb-6 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="h-full roboTaxi flex flex-col">
    <n-tabs v-model:value="activeTab" type="line" animated class="special-point-tabs">
      <n-tab-pane name="park" tab="收车点位" />
      <n-tab-pane name="charge" tab="充电点位" />
    </n-tabs>
      <div class="mb-4">
        <div class="flex items-center justify-between">
          <span>充电点位</span>
          <NButton
            secondary
            type="primary"
            @click="handleSelectPoint('charge')"
          >
            选择点位
          </NButton>
        </div>
      </div>
    <div class="roboTaxi-form-card mb-12px">
      <n-form :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>
            <n-button @click="handleResetSearch">
              重置
            </n-button>
            <n-button type="primary" @click="handleSearch">
              查询
            </n-button>
          </template>
        </SearchPanel>
      </n-form>
    </div>
      <div class="mb-2 flex bg-gray-100 font-bold px-4 py-3 font-bold">
        <div class="flex-1 text-sm text-gray-700">
          点位名称
        </div>
        <div class="flex-1 text-sm text-gray-700">
          详细地址
        </div>
        <div class="w-24 text-sm text-gray-700">
          操作
        </div>
      </div>
      <div v-for="(point, index) in chargePoints" :key="index" class="mb-2 flex items-center bg-white px-4 py-3" style="border-bottom: 1px solid rgba(23, 31, 38, 0.1)">
        <div class="flex-1 text-sm pr-4">
          {{ point.pointName }}
        </div>
        <div class="flex-1 text-sm text-gray-600 pr-4">
          {{ point.pointAddress }}
        </div>
        <div class="w-24">
          <n-popconfirm @positive-click="handleRemovePoint('charge', index)">
            <template #trigger>
              <n-button text type="primary" size="small">
                移除
              </n-button>
            </template>
            确定移除该点位吗?
          </n-popconfirm>
        </div>
      </div>
    <n-card
      class="flex-1 robotaxi-table-card"
      content-style="display: flex; flex-direction: column; flex: 1; min-height: 0; min-width: 0;"
    >
      <template #header>
        <n-flex justify="space-between">
          <n-button @click="handleOpenOperationLog">
            操作日志
          </n-button>
          <n-button secondary type="primary" @click="handleSelectPoint(activeTab)">
            新增
          </n-button>
        </n-flex>
      </template>
      <div class="mt-6 mb-4">
        <div class="flex items-center justify-between">
          <span>收车点位</span>
          <NButton
            secondary
            type="primary"
            @click="handleSelectPoint('park')"
          >
            选择点位
          </NButton>
        </div>
      </div>
      <div class="mb-2 flex bg-gray-100 px-4 py-3 font-bold">
        <div class="flex-1 text-sm text-gray-700">
          点位名称
        </div>
        <div class="flex-1 text-sm text-gray-700">
          详细地址
        </div>
        <div class="w-24 text-sm text-gray-700">
          操作
        </div>
      </div>
      <div v-for="(point, index) in parkPoints" :key="index" class="mb-2 flex items-center bg-white px-4 py-3" style="border-bottom: 1px solid rgba(23, 31, 38, 0.1)">
        <div class="flex-1 text-sm pr-4">
          {{ point.pointName }}
        </div>
        <div class="flex-1 text-sm text-gray-600 pr-4">
          {{ point.pointAddress }}
        </div>
        <div class="w-24">
          <n-popconfirm @positive-click="handleRemovePoint('park', index)">
            <template #trigger>
              <n-button text type="primary" size="small">
                移除
              </n-button>
            </template>
            确定移除该点位吗?
          </n-popconfirm>
        </div>
      <div class="flex-1 flex flex-col h-full" style="min-width: 0; overflow: hidden">
        <n-data-table
          class="flex-1"
          :columns="columns"
          :data="pagedList"
          :loading="loading"
          flex-height
          :bordered="false"
          :scroll-x="980"
          :header-height="18"
          :row-height="18"
          :row-key="(row) => row.pointId"
        >
          <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="filteredList.length"
          :page="pageParams.pageNum"
          :page-size="pageParams.pageSize"
          class="robotaxi-pagination mt-12px"
          @change="handlePageChange"
        />
      </div>
    </n-card>
    <div v-permission="40711" class="absolute bottom-0 left-0 right-0 flex justify-end gap-3 bg-white py-16px px-24px">
      <n-button style="width: 84px; height: 32px" @click="getList">取消</n-button>
      <n-button type="primary" style="width: 84px; height: 32px" :loading="loading" @click="handleSave">
        保存
      </n-button>
    </div>
    <SelectPointModal v-model:show="showSelectModal" :existing-ids="currentExistingIds" @confirm="handleConfirmSelect" />
    <SelectPointModal
      v-model:show="showSelectModal"
      :existing-points="currentExistingPoints"
      @confirm="handleConfirmSelect"
    />
    <OperationLogModal v-model:show="showOperationLogModal" />
  </div>
</template>
<style scoped lang="scss">
.full-content{
  height: calc(100vh - 164px);
:deep(.n-tabs-nav-scroll-content) {
  border-color: transparent !important;
}
.table-action {
  color: #0068ff;
  cursor: pointer;
}
.table-action:hover {
  opacity: 0.8;
}
</style>
src/views/system/financeBill/index.vue
@@ -3,23 +3,25 @@
import { computed, onMounted, ref } from 'vue'
import { NButton, NTag, useDialog, useMessage } from 'naive-ui'
import dayjs from 'dayjs'
import OperationLogModal from '@/components/custom/OperationLogModal.vue'
import Pagination from '@/components/common/Pagination.vue'
import {
  fetchFinanceBillDetail,
  fetchFinanceBillOperationLogPage,
  fetchFinanceBillPage,
  manualPushFinanceBill,
} from '@/service/api/financeBill'
type CodeResultType = 'success' | 'param_error' | 'repeat_push'
interface FinanceBillRow {
  id: number
  id: number | string
  batchNo: string
  orderId: number | string
  orderNumber: string
  operationTime: string
  operatorName: string
  codeResult: CodeResultType
  returnResult: string
  operatorAccount: string
  responseCode: number | string | null
  codeResult: string
  responseJson: string
  createTime: string
}
const dialog = useDialog()
@@ -33,15 +35,21 @@
  pageSize: 10,
})
const showOperationLogModal = ref(false)
const showDetailModal = ref(false)
const detailLoading = ref(false)
const detailData = ref<FinanceBillRow | null>(null)
const codeResultConfigMap: Record<CodeResultType, { label: string, type: 'success' | 'error' | 'warning' }> = {
  success: { label: '接收成功', type: 'success' },
  param_error: { label: '参数错误', type: 'error' },
  repeat_push: { label: '重复事件', type: 'warning' },
const codeResultTagMap: Record<string, { label: string, type: 'success' | 'error' | 'warning' }> = {
  接收成功: { label: '接收成功', type: 'success' },
  参数错误: { label: '参数错误', type: 'error' },
  重复事件: { label: '重复事件', type: 'warning' },
}
function getCodeResultTagConfig(codeResult?: string) {
  if (!codeResult)
    return null
  return codeResultTagMap[codeResult] || null
}
const columns = computed<DataTableColumns<FinanceBillRow>>(() => [
@@ -49,41 +57,51 @@
    title: '操作时间',
    key: 'operationTime',
    width: 180,
    resizable: true,
    render: row => row.operationTime ? dayjs(row.operationTime).format('YYYY/MM/DD HH:mm:ss') : '-',
  },
  {
    title: '操作人',
    key: 'operatorName',
    width: 140,
    width: 160,
    ellipsis: true,
    render: row => row.operatorName || '-',
    resizable: true,
    render: row => row.operatorName || row.operatorAccount || '-',
  },
  {
    title: 'code结果',
    key: 'codeResult',
    width: 140,
    width: 160,
    resizable: true,
    ellipsis: {
      tooltip: true,
    },
    render: (row) => {
      const config = codeResultConfigMap[row.codeResult]
      const config = getCodeResultTagConfig(row.codeResult)
      if (!config)
        return row.codeResult || '-'
      return (
        <NTag bordered={false} type={config?.type || 'default'}>
          {config?.label || '-'}
        <NTag bordered={false} type={config.type}>
          {config.label}
        </NTag>
      )
    },
  },
  {
    title: '返回结果',
    key: 'returnResult',
    minWidth: 420,
    key: 'responseJson',
    width: 420,
    resizable: true,
    ellipsis: {
      tooltip: true,
    },
    render: row => row.returnResult || '-',
    render: row => row.responseJson || '-',
  },
  {
    title: '操作',
    key: 'actions',
    width: 100,
    width: 60,
    fixed: 'right',
    render: row => (
      <NButton text type="primary" onClick={() => handleOpenDetail(row.id)}>
@@ -121,21 +139,25 @@
  }
}
function handleOpenOperationLog() {
  showOperationLogModal.value = true
}
function handleManualPush() {
  dialog.warning({
  const dialogReactive = dialog.warning({
    title: '确认手动推送',
    content: '确认立即执行财务账单手动推送吗?',
    positiveText: '确认',
    negativeText: '取消',
    maskClosable: false,
    autoFocus: false,
    onPositiveClick: async () => {
      const res = await manualPushFinanceBill()
      if (res.code === 200) {
        message.success(res.msg || '手动推送成功')
        getList()
      dialogReactive.loading = true
      try {
        const res = await manualPushFinanceBill()
        if (res.code === 200) {
          message.success(res.msg || '手动推送成功')
          getList()
        }
      }
      finally {
        dialogReactive.loading = false
      }
    },
  })
@@ -161,10 +183,7 @@
  <div class="finance-bill-page roboTaxi">
    <n-card class="flex-1 robotaxi-table-card" :bordered="false">
      <template #header>
        <div class="flex items-center justify-between">
          <NButton @click="handleOpenOperationLog">
            操作日志
          </NButton>
        <div class="flex items-center justify-end">
          <NButton type="primary" secondary @click="handleManualPush">
            手动推送
          </NButton>
@@ -198,17 +217,6 @@
      </div>
    </n-card>
    <OperationLogModal
      v-model:show="showOperationLogModal"
      title="操作日志"
      :show-export="false"
      :operation-type-options="[
        { label: '手动推送', value: 1 },
        { label: '查看详情', value: 2 },
      ]"
      :request-list="fetchFinanceBillOperationLogPage"
    />
    <n-modal
      v-model:show="showDetailModal" :mask-closable="false" preset="card" title="推送详情" class="w-400px"
      :auto-focus="false" :segmented="{
@@ -224,7 +232,7 @@
              操作人
            </div>
            <div class="detail-value detail-value--strong">
              {{ detailData?.operatorName || '-' }}
              {{ detailData?.operatorName || detailData?.operatorAccount || '-' }}
            </div>
          </div>
@@ -242,10 +250,14 @@
              code结果
            </div>
            <div class="detail-value">
              <NTag v-if="detailData" :bordered="false" :type="codeResultConfigMap[detailData.codeResult]?.type || 'default'">
                {{ codeResultConfigMap[detailData.codeResult]?.label || '-' }}
              <NTag
                v-if="getCodeResultTagConfig(detailData?.codeResult)"
                :bordered="false"
                :type="getCodeResultTagConfig(detailData?.codeResult)?.type || 'default'"
              >
                {{ getCodeResultTagConfig(detailData?.codeResult)?.label || '-' }}
              </NTag>
              <span v-else>-</span>
              <span v-else>{{ detailData?.codeResult || '-' }}</span>
            </div>
          </div>
@@ -254,7 +266,7 @@
              返回结果
            </div>
            <div class="detail-value detail-value--multiline">
              {{ detailData?.returnResult || '-' }}
              {{ detailData?.responseJson || '-' }}
            </div>
          </div>
        </div>
src/views/transaction/transactionStrategy/index.vue
@@ -515,6 +515,12 @@
  }
}
function closeTimePicker() {
  if (document.activeElement) {
    (document.activeElement as HTMLElement).blur()
  }
}
onMounted(() => {
  getDetail()
  getCityListData()
@@ -755,10 +761,14 @@
                        format="HH:mm"
                        value-format="HH:mm"
                        clearable
                        close-on-select
                        :actions="['now']"
                        class="w-140px"
                        placeholder="开始时间"
                        @update:value="
                          () => {
                            closeTimePicker();
                          }
                        "
                      />
                      <span class="mx-8px text-[#171F2660]">至</span>
                      <n-time-picker
@@ -766,10 +776,14 @@
                        format="HH:mm"
                        value-format="HH:mm"
                        clearable
                        close-on-select
                        :actions="['now']"
                        class="w-140px"
                        placeholder="结束时间"
                        @update:value="
                          () => {
                            closeTimePicker();
                          }
                        "
                      />
                      <icon-park-outline-delete class="special-time-delete-btn" @click="handleDeleteSpecialTimeRule(rule)" />
                    </n-form-item>