zjk
2026-04-20 78b21957f5c26a4f2a822287f712a6a3bde397e0
feat: 实现自动刷新列表和优化改派弹窗
1个文件已删除
1个文件已添加
6个文件已修改
2211 ■■■■■ 已修改文件
src/assets/pricingManagement/refresh.svg 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/accountPermission/permissionGroup/form.vue 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/transaction/order/orderManagement/components/CloseOrderModal.vue 124 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/transaction/order/orderManagement/components/ReassignModal.vue 463 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/transaction/order/orderManagement/components/TableModal.vue 372 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/transaction/order/orderManagement/index.vue 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/whitelistManagement/whitelist/components/WhitelistModal.vue 217 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/whitelistManagement/whitelist/detail.vue 980 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/pricingManagement/refresh.svg
New file
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="20" height="20" viewBox="0 0 20 20"><rect x="0" y="0" width="20" height="20" rx="0" fill="#FFFFFF" fill-opacity="0"/><g><path d="M15.244305,15.529691680541992C16.636475,14.149884680541993,17.5,12.227533380541992,17.5,10.103079780541993C17.5,5.910315480541993,14.135309,2.500030517581992,9.9707899,2.500030517581992C9.5168405,2.500030517581992,9.071764,2.540669441541992,8.6390858,2.6186924005419923L8.8609142,3.848852180541992C9.2207422,3.783966080541992,9.5915866,3.750030480541992,9.9707899,3.750030480541992C13.43245,3.750030480541992,16.25,6.588101380541993,16.25,10.103079780541993C16.25,11.883513480541993,15.526778,13.491476680541993,14.361572,14.644706680541992L14.361572,12.404691680541992L13.111572,12.404691680541992L13.111572,16.154691680541994C13.111572,16.49986968054199,13.391394,16.779691680541994,13.736572,16.779691680541994L17.486572000000002,16.779691680541994L17.486572000000002,15.529691680541992L15.244305,15.529691680541992ZM4.7556951000000005,4.470121380541992L2.514648438,4.470121380541992L2.514648438,3.2201213805419924L6.2646484000000004,3.2201213805419924C6.6098266,3.2201213805419924,6.8896484,3.499943380541992,6.8896484,3.8451213805419924L6.8896484,7.595121380541992L5.6396484000000004,7.595121380541992L5.6396484000000004,5.353899280541992C4.4737275,6.507213580541992,3.75,8.115678280541992,3.75,9.896733280541993C3.75,13.411711680541993,6.5675497,16.24978268054199,10.0292101,16.24978268054199C10.4084134,16.24978268054199,10.7792578,16.215846680541993,11.1390858,16.150960680541992L11.3609142,17.38112068054199C10.928236,17.45914368054199,10.4831595,17.49978268054199,10.0292101,17.49978268054199C5.8646902999999995,17.49978268054199,2.5,14.089497680541992,2.5,9.896733280541993C2.5,7.772280180541992,3.36352563,5.849929080541992,4.7556951000000005,4.470121380541992Z" fill-rule="evenodd" fill="#0068FF" fill-opacity="1"/></g></svg>
src/views/system/accountPermission/permissionGroup/form.vue
@@ -491,7 +491,7 @@
</script>
<template>
  <div class="w-full h-full relative viewContent roboTaxi" :class="mode=='view' && 'content'">
  <div class="w-full h-full relative viewContent roboTaxi" :class="mode == 'view' && 'content'">
    <div class="p-12px">
      <NForm
        ref="formRef"
@@ -709,7 +709,7 @@
              </NTabPane>
            </NTabs>
            <div v-else class="add-tree-container" :class="mode=='view' && 'tree-container'">
            <div v-else class="add-tree-container" :class="mode == 'view' && 'tree-container'">
              <NTree
                block-line
                cascade
@@ -777,7 +777,7 @@
  }
}
.viewContent{
  height: calc(100vh - 76px) !important;
    width: 100%;
  overflow-y: auto;
@@ -912,6 +912,18 @@
  padding: 12px;
  border-radius: 4px;
  &::-webkit-scrollbar {
    width: 6px;
    height: 6px;
  }
  &::-webkit-scrollbar-thumb {
    background-color: rgba(0, 0, 0, 0.2);
    border-radius: 3px;
  }
  &::-webkit-scrollbar-track {
    background: transparent;
  }
  .n-tree {
    height: 100%;
    .n-empty {
@@ -924,5 +936,18 @@
}
.add-tree-container{
  height: calc(100vh - 374px);
  overflow-y: auto;
  &::-webkit-scrollbar {
    width: 6px;
    height: 6px;
  }
  &::-webkit-scrollbar-thumb {
    background-color: rgba(0, 0, 0, 0.2);
    border-radius: 3px;
  }
  &::-webkit-scrollbar-track {
    background: transparent;
  }
}
</style>
src/views/transaction/order/orderManagement/components/CloseOrderModal.vue
@@ -55,100 +55,38 @@
</script>
<template>
  <n-modal v-model:show="modalVisible" :mask-closable="false">
    <n-card
      :bordered="false"
      :segmented="{ content: true }"
      style="width: 480px; border-radius: 8px; display: flex; flex-direction: column;"
      content-style="padding: 24px; display: flex; flex-direction: column; gap: 24px;"
      footer-style="padding: 16px 24px; border-top: 1px solid rgba(23, 31, 38, 0.08);"
    >
      <template #header>
        <span class="modal-title">{{ modalName }}</span>
      </template>
      <template #header-extra>
        <div class="modal-close" @click="closeModal">
          ×
        </div>
      </template>
      <div class="modal-content">
        <div class="form-row">
          <span class="form-label">是否豁免费用:</span>
          <n-radio-group v-model:value="formModel.isWaiveFee" name="isWaiveFeeGroup">
            <n-space :size="32">
              <n-radio :value="1">
                是
              </n-radio>
              <n-radio :value="0">
                否
              </n-radio>
            </n-space>
          </n-radio-group>
        </div>
  <n-modal
    v-model:show="modalVisible" :mask-closable="false" preset="card" :title="modalName" class="w-400px"
    :segmented="{
      content: true,
      action: true,
    }"
  >
    <div class="modal-content">
      <div class="form-row">
        <span class="form-label">是否豁免费用:</span>
        <n-radio-group v-model:value="formModel.isWaiveFee" name="isWaiveFeeGroup">
          <n-space :size="16">
            <n-radio :value="1">
              是
            </n-radio>
            <n-radio :value="0">
              否
            </n-radio>
          </n-space>
        </n-radio-group>
      </div>
    </div>
      <template #footer>
        <div class="footer-actions">
          <n-button class="footer-btn" @click="closeModal">
            取消
          </n-button>
          <n-button type="primary" class="footer-btn" @click="submitModal">
            确定
          </n-button>
        </div>
      </template>
    </n-card>
    <template #action>
      <n-space justify="end">
        <NButton @click="closeModal">
          取消
        </NButton>
        <NButton type="primary" @click="submitModal">
          确定
        </NButton>
      </n-space>
    </template>
  </n-modal>
</template>
<style scoped lang="scss">
.modal-title {
  font-size: 16px;
  font-weight: 700;
  color: #171f26;
}
.modal-close {
  font-size: 18px;
  line-height: 1;
  color: rgba(23, 31, 38, 0.6);
  cursor: pointer;
}
.modal-content {
  padding: 12px 0 24px;
}
.form-row {
  display: flex;
  align-items: center;
}
.form-label {
  font-size: 14px;
  color: #171f26;
  margin-right: 24px;
  margin-left: 12px;
}
.footer-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}
.footer-btn {
  width: 84px;
  height: 32px;
}
:deep(.n-card-header__main) {
  font-size: 16px;
}
:deep(.n-card-header) {
  padding: 16px 24px;
  border-bottom: 1px solid rgba(23, 31, 38, 0.08);
}
</style>
src/views/transaction/order/orderManagement/components/ReassignModal.vue
File was deleted
src/views/transaction/order/orderManagement/components/TableModal.vue
@@ -1,13 +1,25 @@
<script setup lang="ts">
<script setup lang="tsx">
import type { DataTableColumns, DataTableRowKey } from 'naive-ui'
import { useBoolean } from '@/hooks'
import { getAvailableVehicleList, reassignOrder } from '@/service/api/transaction'
import refreshIcon from '@/assets/pricingManagement/refresh.svg'
interface Props {
  modalName?: string
}
interface VehicleItem {
  id: number
  vehicleCode: string
  plateNumber: string
  vehicleModel: string
  fleetName: string
  estimatedArrivalTime: string
  estimatedDistance: string
}
const {
  modalName = '',
  modalName = '订单改派',
} = defineProps<Props>()
const emit = defineEmits<{
@@ -17,20 +29,127 @@
}>()
const { bool: modalVisible, setTrue: showModal, setFalse: hiddenModal } = useBoolean(false)
const { bool: submitLoading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
const formDefault = {
  id: '',
  vehicleId: '',
  orderId: '',
  vehicleId: null as number | null,
  reason: '',
}
const searchDefault = {
  plateNumber: '',
  fleetName: '',
}
const formModel = ref({ ...formDefault })
const searchModel = ref({ ...searchDefault })
const checkedRowKeys = ref<DataTableRowKey[]>([])
const vehicleList = ref<VehicleItem[]>([])
const tableData = ref<VehicleItem[]>([])
const columns = computed<DataTableColumns<VehicleItem>>(() => [
  {
    type: 'selection',
    width: 56,
  },
  {
    title: '序号',
    key: 'index',
    width: 60,
    render: (_row, index) => index + 1,
    resizable: true,
  },
  {
    title: '车辆编码',
    key: 'vehicleCode',
    minWidth: 120,
    ellipsis: true,
    resizable: true,
  },
  {
    title: '车牌号',
    key: 'plateNumber',
    minWidth: 120,
    ellipsis: true,
    resizable: true,
  },
  {
    title: '型号',
    key: 'vehicleModel',
    minWidth: 80,
    ellipsis: true,
    resizable: true,
  },
  {
    title: '预计到达出发点时间',
    key: 'estimatedArrivalTime',
    minWidth: 200,
    ellipsis: true,
    resizable: true,
  },
  {
    title: '预计到达出发点距离',
    key: 'estimatedDistance',
    minWidth: 200,
    ellipsis: true,
  },
])
function rowKey(row: VehicleItem) {
  return row.id
}
function handleRowClick(row: VehicleItem) {
  checkedRowKeys.value = [row.id]
  formModel.value.vehicleId = row.id
}
function getRowProps(row: VehicleItem) {
  return {
    onClick: () => handleRowClick(row),
  }
}
function applyFilters() {
  const plateNumber = searchModel.value.plateNumber.trim()
  const fleetName = searchModel.value.fleetName.trim()
  tableData.value = vehicleList.value.filter((item) => {
    const matchPlateNumber = !plateNumber || item.plateNumber.includes(plateNumber)
    const matchFleetName = !fleetName || item.fleetName.includes(fleetName)
    return matchPlateNumber && matchFleetName
  })
}
async function getList(orderId: number) {
  const res = await getAvailableVehicleList({ orderId, orderType: 1 })
  if (res.code === 200) {
    vehicleList.value = res.data.map((item: any) => ({
      id: item.id,
      vehicleCode: item.vehicleCode || '-',
      plateNumber: item.plateNumber || '-',
      vehicleModel: item.vehicleModel || '-',
      fleetName: item.fleetName || '-',
      estimatedArrivalTime: item.estimatedArrivalTime || '-',
      estimatedDistance: item.estimatedDistance || '-',
    }))
  }
}
async function openModal(data: any) {
  emit('open')
  formModel.value = {
    ...formDefault,
    orderId: data?.id ?? '',
  }
  searchModel.value = { ...searchDefault }
  checkedRowKeys.value = []
  vehicleList.value = []
  tableData.value = []
  await getList(data.id)
  applyFilters()
  showModal()
  formModel.value = { ...data }
}
function closeModal() {
@@ -39,19 +158,49 @@
  emit('close')
}
defineExpose({
  openModal,
})
function handleSearch() {
  applyFilters()
}
const formRef = ref()
function handleReset() {
  searchModel.value = { ...searchDefault }
  applyFilters()
}
function handleRefreshRecommend() {
  if (formModel.value.orderId) {
    getList(Number(formModel.value.orderId)).then(() => {
      checkedRowKeys.value = []
      formModel.value.vehicleId = null
      applyFilters()
    })
  }
}
function handleCheck(rowKeys: DataTableRowKey[]) {
  const selectedKey = rowKeys.length > 0 ? Number(rowKeys[rowKeys.length - 1]) : null
  checkedRowKeys.value = selectedKey === null ? [] : [selectedKey]
  formModel.value.vehicleId = selectedKey
}
async function submitModal() {
  await formRef.value?.validate()
  if (!formModel.value.vehicleId) {
    window.$message.warning('请选择改派车辆')
    return
  }
  if (!formModel.value.reason.trim()) {
    window.$message.warning('请输入改派原因')
    return
  }
  startLoading()
  reassignOrder({
    orderId: formModel.value.id,
    orderId: formModel.value.orderId,
    vehicleId: formModel.value.vehicleId,
    // reason: formModel.value.reason, // 接口可能需要传递原因,如果是请放开
  }).then((res: any) => {
    if (res.code === 0) {
    if (res.code === 200) {
      window.$message.success('改派成功')
      emit('refresh')
      closeModal()
@@ -60,68 +209,179 @@
    endLoading()
  })
}
const isElectronicAreaIdOpen = ref(false)
const rules = {
  vehicleId: {
    required: true,
    message: '请输入名称',
    trigger: 'blur',
  },
}
const options = ref()
async function getList(orderId: number) {
  const res = await getAvailableVehicleList({orderId,orderType:1})
  if (res.code === 200) {
    options.value = res.data.map((item: any) => ({
      label: item.plateNumber,
      value: item.id,
    }))
  }
}
defineExpose({
  openModal,
})
</script>
<template>
  <n-modal
    v-model:show="modalVisible" :mask-closable="false" preset="card" :title="modalName" class="w-400px"
    v-model:show="modalVisible" :mask-closable="false" preset="card" :title="modalName" class="w-1000px"
    :segmented="{
      content: true,
      action: true,
    }"
  >
    <n-form ref="formRef" :rules="rules" label-placement="top" :model="formModel" require-mark-placement="left">
      <n-form-item :span="1" label="改派车辆" path="vehicleId">
        <n-select v-model:value="formModel.vehicleId" :options="options" @update:show="(val: any) => isElectronicAreaIdOpen = val" >
        <template #arrow>
          <img
            src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e120f81c-9f15-4e1a-9938-22252898edf9.png"
            :class="`arrow-icon ${isElectronicAreaIdOpen ? '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 class="modal-content">
      <div class="search-wrapper">
        <div class="search-item">
          <div class="search-label">
            车牌号
          </div>
        </template>
      </n-select>
      </n-form-item>
    </n-form>
          <n-input v-model:value="searchModel.plateNumber" placeholder="请输入" clearable />
        </div>
        <div class="search-item">
          <div class="search-label">
            车队
          </div>
          <n-input v-model:value="searchModel.fleetName" placeholder="请输入" clearable />
        </div>
        <div class="search-actions">
          <n-button class="action-btn" @click="handleReset">
            重置
          </n-button>
          <n-button type="primary" class="action-btn" @click="handleSearch">
            查询
          </n-button>
        </div>
      </div>
      <div class="table-section">
        <div class="table-wrapper">
          <n-data-table
            v-model:checked-row-keys="checkedRowKeys"
            flex-height
            style="height: 100%"
            :columns="columns"
            :data="tableData"
            :bordered="false"
            :pagination="false"
            :row-key="rowKey"
            :scroll-x="836"
            :row-props="getRowProps"
            @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"
                  alt="no-data"
                >
              </div>
            </template>
          </n-data-table>
        </div>
        <div class="refresh-action" @click="handleRefreshRecommend">
          <img :src="refreshIcon" alt="refresh-icon" class="refresh-icon">
          <span>刷新推荐车辆</span>
        </div>
      </div>
      <div class="reason-section">
        <div class="reason-label">
          改派原因
        </div>
        <n-input
          v-model:value="formModel.reason"
          type="textarea"
          placeholder="请输入"
          :maxlength="200"
          show-count
          :autosize="{ minRows: 3, maxRows: 3 }"
        />
      </div>
    </div>
    <template #action>
      <n-space justify="end">
        <NButton @click="closeModal">
          取消
        </NButton>
        <NButton type="primary" :loading="submitLoading" @click="submitModal">
          确认
          确定
        </NButton>
      </n-space>
    </template>
  </n-modal>
</template>
<style scoped lang="scss">
.modal-content {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
  overflow: hidden;
}
.search-wrapper {
  display: flex;
  align-items: flex-end;
  gap: 24px;
  flex-shrink: 0;
}
.search-item {
  width: 300px;
  flex-shrink: 0;
}
.search-label,
.reason-label {
  margin-bottom: 8px;
  font-size: 13px;
  color: #171f26;
}
.search-actions {
  display: flex;
  gap: 8px;
  margin-left: auto;
}
.action-btn,
.footer-btn {
  width: 84px;
  height: 32px;
}
.table-section {
  display: flex;
  flex: 1;
  flex-direction: column;
  min-height: 0;
  margin-top: 16px;
}
.table-wrapper {
  flex: 1;
  min-height: 0;
  border-radius: 4px;
  overflow: hidden;
}
.refresh-action {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  width: fit-content;
  margin-top: 8px;
  color: #0068ff;
  font-weight: 500;
  cursor: pointer;
  flex-shrink: 0;
}
.refresh-icon {
  width: 20px;
  height: 20px;
}
.reason-section {
  margin-top: 16px;
  flex-shrink: 0;
}
</style>
src/views/transaction/order/orderManagement/index.vue
@@ -1,4 +1,5 @@
<script setup lang="tsx">
import { computed, onMounted, onUnmounted, ref } from 'vue'
import type { DataTableColumns, DataTableRowKey } from 'naive-ui'
import { useBoolean } from '@/hooks'
import { cancelAndCloseOrder, deleteOrder, exportOrderExcel, getFleetList, getOrderList } from '@/service/api/transaction'
@@ -7,8 +8,8 @@
import SearchPanel from '@/components/custom/SearchPanel.vue'
import dayjs from 'dayjs'
import { downloadFile } from '@/utils'
// import TableModal from './components/TableModal.vue'
import ReassignModal from './components/ReassignModal.vue'
import TableModal from './components/TableModal.vue'
// import ReassignModal from './components/ReassignModal.vue'
import CloseOrderModal from './components/CloseOrderModal.vue'
import Modal from './components/Modal.vue'
@@ -621,10 +622,21 @@
  return row.id
}
let timer: any = null
onMounted(() => {
  getList()
  fetchCascaderData()
  fetchFleetList()
  timer = setInterval(() => {
    getList()
  }, 6000)
})
onUnmounted(() => {
  if (timer) {
    clearInterval(timer)
  }
})
function handleCheck(rowKeys: DataTableRowKey[]) {
@@ -670,6 +682,9 @@
            <NButton @click="handleExport">
              批量导出订单
            </NButton>
            <span style="color: rgba(23, 31, 38, 0.6);font-weight: 400; margin-left: 8px;">
              订单列表每60S自动刷新一次
            </span>
          </div>
        </n-flex>
      </template>
@@ -692,8 +707,8 @@
        />
      </div>
    </n-card>
    <!-- <TableModal ref="modalRef" modal-name="改派" @refresh="handleSearch" /> -->
    <ReassignModal ref="modalRef" modal-name="订单改派" @refresh="handleSearch" />
    <TableModal ref="modalRef" modal-name="改派" @refresh="handleSearch" />
    <!-- <ReassignModal ref="modalRef" modal-name="订单改派" @refresh="handleSearch" /> -->
    <CloseOrderModal ref="closeOrderModalRef" modal-name="关闭订单" @refresh="handleSearch" />
    <Modal ref="modalRef1" modal-name="改价" @refresh="handleSearch" />
  </div>
src/views/userContent/whitelistManagement/whitelist/components/WhitelistModal.vue
@@ -11,194 +11,200 @@
  NSwitch,
  useDialog,
  useMessage,
} from "naive-ui";
import type { FormInst, FormRules } from "naive-ui";
import { addWhitelist, editWhitelist } from "@/service/api/whitelist";
} from 'naive-ui'
import type { FormInst, FormRules } from 'naive-ui'
import { addWhitelist, editWhitelist } from '@/service/api/whitelist'
interface Props {
  visible?: boolean;
  mode?: "add" | "edit";
  rowData?: any;
  visible?: boolean
  mode?: 'add' | 'edit'
  rowData?: any
}
interface Emits {
  (e: "update:visible", visible: boolean): void;
  (e: "success"): void;
  (e: 'update:visible', visible: boolean): void
  (e: 'success'): void
}
const props = withDefaults(defineProps<Props>(), {
  visible: false,
  mode: "add",
  mode: 'add',
  rowData: null,
});
})
const emit = defineEmits<Emits>();
const message = useMessage();
const dialog = useDialog();
const emit = defineEmits<Emits>()
const message = useMessage()
const dialog = useDialog()
interface FormModel {
  phones: string;
  account: string;
  nickName: string;
  status: boolean;
  isPriorityDispatch: boolean;
  expireDate: string | null;
  maxOrderCount: number | null;
  phones: string
  account: string
  nickName: string
  status: boolean
  isPriorityDispatch: boolean
  expireDate: string | null
  maxOrderCount: number | null
}
function createDefaultFormModel(): FormModel {
  return {
    phones: "",
    account: "",
    nickName: "",
    phones: '',
    account: '',
    nickName: '',
    status: true,
    isPriorityDispatch: true,
    expireDate: null,
    maxOrderCount: null,
  };
  }
}
function formatDateValue(value: unknown) {
  if (value === null || value === undefined || value === "") return null;
  if (value === null || value === undefined || value === '')
    return null
  if (typeof value === "string" && /^\d{4}\/\d{2}\/\d{2}$/.test(value.trim()))
    return value.trim();
  if (typeof value === 'string' && /^\d{4}\/\d{2}\/\d{2}$/.test(value.trim()))
    return value.trim()
  const date = new Date(value as string | number | Date);
  if (Number.isNaN(date.getTime())) return null;
  const date = new Date(value as string | number | Date)
  if (Number.isNaN(date.getTime()))
    return null
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, "0");
  const day = String(date.getDate()).padStart(2, "0");
  return `${year}/${month}/${day}`;
  const year = date.getFullYear()
  const month = String(date.getMonth() + 1).padStart(2, '0')
  const day = String(date.getDate()).padStart(2, '0')
  return `${year}/${month}/${day}`
}
function toOptionalNumber(value: unknown) {
  if (value === null || value === undefined || value === "") return null;
  if (value === null || value === undefined || value === '')
    return null
  const n = Number(value);
  return Number.isFinite(n) ? n : null;
  const n = Number(value)
  return Number.isFinite(n) ? n : null
}
const formRef = ref<FormInst | null>(null);
const formModel = ref<FormModel>(createDefaultFormModel());
const formRef = ref<FormInst | null>(null)
const formModel = ref<FormModel>(createDefaultFormModel())
// 表单验证规则
const rules: FormRules = {
  phones: [
    {
      required: true,
      message: "请输入白名单账号",
      trigger: ["blur", "change"],
      message: '请输入白名单账号',
      trigger: ['blur', 'change'],
    },
  ],
  account: [
    {
      required: true,
      message: "请输入白名单账号",
      trigger: ["blur", "change"],
      message: '请输入白名单账号',
      trigger: ['blur', 'change'],
    },
  ],
};
}
function parseAccountsInput(input: string) {
  return input
    .split(/[,\uFF0C\s]+/g)
    .map((s) => s.trim())
    .filter(Boolean);
    .map(s => s.trim())
    .filter(Boolean)
}
watch(
  () => props.visible,
  (val) => {
    if (val) {
      if (props.mode === "edit" && props.rowData) {
      if (props.mode === 'edit' && props.rowData) {
        formModel.value = {
          ...createDefaultFormModel(),
          account: props.rowData.account,
          nickName: props.rowData.nickName,
          status: props.rowData.status === 1 || props.rowData.status === true,
          isPriorityDispatch:
            props.rowData.isPriorityDispatch === 1 ||
            props.rowData.isPriorityDispatch === true,
            props.rowData.isPriorityDispatch === 1
            || props.rowData.isPriorityDispatch === true,
          expireDate: formatDateValue(
            props.rowData.expireDate ??
              props.rowData.effectiveDate ??
              props.rowData.validityDate ??
              props.rowData.expireTime
            props.rowData.expireDate
            ?? props.rowData.effectiveDate
            ?? props.rowData.validityDate
            ?? props.rowData.expireTime,
          ),
          maxOrderCount: toOptionalNumber(
            props.rowData.maxOrderCount ??
              props.rowData.maxOrderNum ??
              props.rowData.orderLimitCount
            props.rowData.maxOrderCount
            ?? props.rowData.maxOrderNum
            ?? props.rowData.orderLimitCount,
          ),
        };
      } else {
        formModel.value = createDefaultFormModel();
        }
      }
      else {
        formModel.value = createDefaultFormModel()
      }
    }
  }
);
  },
)
function closeModal() {
  emit("update:visible", false);
  emit('update:visible', false)
}
function handleCancel() {
  handleRequestClose();
  handleRequestClose()
}
function hasUnsavedInput() {
  if (props.mode !== "add") return false;
  if (props.mode !== 'add')
    return false
  return (
    parseAccountsInput(formModel.value.phones).length > 0 ||
    !!formModel.value.expireDate ||
    formModel.value.maxOrderCount !== null ||
    formModel.value.isPriorityDispatch !==
      createDefaultFormModel().isPriorityDispatch
  );
    parseAccountsInput(formModel.value.phones).length > 0
    || !!formModel.value.expireDate
    || formModel.value.maxOrderCount !== null
    || formModel.value.isPriorityDispatch
    !== createDefaultFormModel().isPriorityDispatch
  )
}
function handleRequestClose() {
  if (!hasUnsavedInput()) {
    closeModal();
    return;
    closeModal()
    return
  }
  dialog.warning({
    title: "确认关闭?",
    content: "当前已输入内容,关闭后将丢失,确定关闭吗?",
    positiveText: "确定关闭",
    negativeText: "继续编辑",
    title: '确认关闭?',
    content: '当前已输入内容,关闭后将丢失,确定关闭吗?',
    positiveText: '确定关闭',
    negativeText: '继续编辑',
    maskClosable: false,
    autoFocus: false,
    onPositiveClick: () => {
      closeModal();
      closeModal()
    },
  });
  })
}
async function handleSave() {
  try {
    await formRef.value?.validate();
    await formRef.value?.validate()
    if (props.mode === "add") {
    if (props.mode === 'add') {
      // 新增逻辑
      const accounts = parseAccountsInput(formModel.value.phones);
      const accounts = parseAccountsInput(formModel.value.phones)
      const res: any = await addWhitelist({
        account: accounts.join(","),
        account: accounts.join(','),
        status: formModel.value.status ? 1 : 0,
        isPriorityDispatch: formModel.value.isPriorityDispatch ? 1 : 0,
        expireDate: formModel.value.expireDate || undefined,
        maxOrderCount: formModel.value.maxOrderCount ?? undefined,
      });
      })
      if (res.code === 200) {
        message.success(`${res.msg}`);
        emit("success");
        closeModal();
        message.success(`${res.msg}`)
        emit('success')
        closeModal()
      }
    } else {
    }
    else {
      // 编辑逻辑
      const res = await editWhitelist({
        id: props.rowData.id,
@@ -208,15 +214,16 @@
        isPriorityDispatch: formModel.value.isPriorityDispatch ? 1 : 0,
        expireDate: formModel.value.expireDate || undefined,
        maxOrderCount: formModel.value.maxOrderCount ?? undefined,
      });
      })
      if (res.code === 200) {
        message.success("编辑白名单成功");
        emit("success");
        closeModal();
        message.success('编辑白名单成功')
        emit('success')
        closeModal()
      }
    }
  } catch (error) {
    console.error("表单验证失败:", error);
  }
  catch (error) {
    console.error('表单验证失败:', error)
  }
}
</script>
@@ -228,7 +235,7 @@
    preset="card"
    :title="mode === 'add' ? '新增白名单' : '编辑白名单'"
    class="whitelist-modal"
    :style="{ width: '600px' }"
    :style="{ width: '400px' }"
    :segmented="{
      content: true,
      action: true,
@@ -274,20 +281,16 @@
        v-if="mode === 'edit'"
        style="display: flex; align-items: center; margin-bottom: 16px"
      >
        <span style="margin-right: 12px; font-weight: 500; width: 100px"
          >状态</span
        >
        <span style="margin-right: 12px; font-weight: 500; width: 100px">状态</span>
        <NSwitch v-model:value="formModel.status" />
      </div>
      <div style="display: flex; align-items: center">
        <span style="margin-right: 12px; font-weight: 500; width: 100px"
          >是否优先派单</span
        >
        <span style="margin-right: 12px; font-weight: 500; width: 100px">是否优先派单</span>
        <NSwitch v-model:value="formModel.isPriorityDispatch" />
      </div>
      <div class="flex align-center justify-between mt-16px">
      <div class="flex align-center gap-12px mt-16px">
        <NFormItem label="有效期">
          <div class="form-item-content">
            <NDatePicker
@@ -300,7 +303,9 @@
              :actions="[]"
              style="width: 100%"
            />
            <div class="form-hint">不设置则默认永久有效</div>
            <div class="form-hint">
              不设置则默认永久有效
            </div>
          </div>
        </NFormItem>
@@ -313,7 +318,9 @@
              placeholder="请输入最大下单数量"
              style="width: 100%"
            />
            <div class="form-hint">不设置则默认不限制下单数量</div>
            <div class="form-hint">
              不设置则默认不限制下单数量
            </div>
          </div>
        </NFormItem>
      </div>
@@ -353,12 +360,12 @@
}
.form-item-content {
  width: 100%;
  width: 170px;
}
.form-hint {
  margin-top: 6px;
  color: rgba(23, 31, 38, 0.45);
  margin-top: 4px;
  color: rgba(23, 31, 38, 0.6);
  font-size: 12px;
  line-height: 16px;
}
src/views/userContent/whitelistManagement/whitelist/detail.vue
@@ -1,9 +1,8 @@
<script setup lang="tsx">
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import SearchPanel from '@/components/custom/SearchPanel.vue'
import {
  NButton,
  NCard,
  NCascader,
  NDataTable,
  NDatePicker,
@@ -12,29 +11,31 @@
  NGi,
  NInput,
  NSelect,
  NSpace,
  NTabPane,
  NTabs,
  NTag,
} from 'naive-ui'
import type { DataTableColumns } from 'naive-ui'
import { useRouter } from 'vue-router'
import Pagination from '@/components/common/Pagination.vue'
import SearchPanel from '@/components/custom/SearchPanel.vue'
import { getFleetList } from '@/service/api/transaction'
import { getRegionCascaderNoArea } from '@/service/api/operations'
const router = useRouter()
import dayjs from 'dayjs'
// 当前激活的 Tab
const activeTab = ref('basicInfo')
const activeTab = ref('activityRecord')
const router = useRouter()
// 基础信息数据 (模拟)
const basicInfoData = ref({
  userName: '名称名称名称',
  whitelistAccount: '19282837281',
  userName: '用户名称用户名称用户名称',
  whitelistAccount: '18898765432',
  status: '正常',
  isPriorityDispatch: '否',
  expireDate: '2026年04月09日',
  maxOrderCount: '100',
  isPriorityDispatch: '是',
  expireDate: '2025/09/09',
  maxOrderCount: '1000',
  creator: '杨佩芳',
  createTime: '2025/09/09 11:09:09',
})
// 活动参与记录 - 搜索条件
@@ -45,10 +46,10 @@
})
// 状态选项
const useStatusOptions = [
  { label: '可使用', value: 1 },
  { label: '已使用', value: 2 },
  { label: '已到期', value: 3 },
const useStatusOptions: { type: 'info' | 'success' | 'default', label: string, value: number }[] = [
  { label: '可使用', value: 1, type: 'info' },
  { label: '已使用', value: 2, type: 'success' },
  { label: '已到期', value: 3, type: 'default' },
]
// 下拉框展开状态
@@ -150,42 +151,79 @@
const activityPage = ref({ pageNum: 1, pageSize: 10 })
const selectedRowKeys = ref<number[]>([])
// 活动参与记录 - 搜索项配置
const activityFilterItems = computed(() => [
  {
    label: '领取时间',
    path: 'timeRange',
    render: () => <NDatePicker v-model:value={activitySearchModel.value.timeRange} type="daterange" clearable />,
  },
  {
    label: '所属活动名称',
    path: 'activityName',
    render: () => <NInput v-model:value={activitySearchModel.value.activityName} placeholder="请输入" clearable />,
  },
  {
    label: '状态',
    path: 'useStatus',
    render: () => (
      <NSelect
        v-model:value={activitySearchModel.value.useStatus}
        options={useStatusOptions}
        clearable
        placeholder="请选择"
        onUpdate:show={(value: boolean) => isUseStatusSelectOpen.value = value}
      >
        {{
          arrow: () => (
            <img
              src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e120f81c-9f15-4e1a-9938-22252898edf9.png"
              class={`arrow-icon ${isUseStatusSelectOpen.value ? 'arrow-icon--open' : ''}`}
              style={{ width: '16px', height: '16px' }}
              alt="arrow"
            />
          ),
          empty: () => (
            <div class="flex-center">
              <img
                src="/src/assets/element-img/no-data.png"
                style={{ width: '50%' }}
              />
            </div>
          ),
        }}
      </NSelect>
    ),
  },
])
// 活动参与记录 - 表格列定义
const activityColumns = [
  { type: 'selection' as const, width: 50 },
  { title: '领取时间', key: 'receiveTime', align: 'left' as const, width: 180 },
  { title: '领取优惠券名称', key: 'couponName', align: 'left' as const, width: 200 },
  { title: '所属活动名称', key: 'activityName', align: 'left' as const, width: 220 },
  { title: '领取金额', key: 'amount', align: 'left' as const, width: 120 },
  { title: '到期时间', key: 'expireTime', align: 'left' as const, width: 180 },
const activityColumns = computed<DataTableColumns<any>>(() => [
  { type: 'selection', width: 50 },
  { title: '领取时间', key: 'receiveTime', width: 180, resizable: true },
  { title: '领取优惠券名称', key: 'couponName', width: 200, resizable: true },
  { title: '所属活动名称', key: 'activityName', width: 220, resizable: true },
  { title: '领取金额', key: 'amount', width: 120, resizable: true },
  { title: '到期时间', key: 'expireTime', width: 180, resizable: true },
  {
    title: '状态',
    key: 'status',
    align: 'left' as const,
    width: 120,
    render: (row: any) => {
      const statusMap: Record<number, { type: 'info' | 'default', text: string, color: string, bgColor: string }> = {
        1: { type: 'info', text: '可使用', color: '#0068FF', bgColor: '#E6F4FF' },
        2: { type: 'default', text: '已使用', color: 'rgba(23, 31, 38, 0.45)', bgColor: 'rgba(23, 31, 38, 0.04)' },
        3: { type: 'default', text: '已到期', color: 'rgba(23, 31, 38, 0.45)', bgColor: 'rgba(23, 31, 38, 0.04)' },
      }
      const status = statusMap[row.status]
      const status = useStatusOptions[row.status]
      return status
        ? (
            <NTag
              bordered={false}
              style={{
                color: status.color,
                backgroundColor: status.bgColor,
              }}
              type={status.type}
            >
              {status.text}
              {status.label}
            </NTag>
          )
        : '-'
    },
  },
]
])
// 搜索操作
function handleSearchActivity() {
@@ -208,36 +246,36 @@
// 订单记录 - 搜索条件
const orderSearchModel = ref({
  orderNumber: '',
  userPhone: '',
  vehicleNumber: '',
  nickName: '',
  plateNumber: '',
  fleetId: null,
  channel: null,
  cityId: null,
  orderSource: null,
  cityCode: null,
  status: null,
  orderTime: null as [number, number] | null,
})
// 下拉框展开状态
// 模拟选项和状态
const isOrderFleetOpen = ref(false)
const isOrderChannelOpen = ref(false)
const isOrderSourceOpen = ref(false)
const isOrderCityOpen = ref(false)
const isOrderStatusOpen = ref(false)
// 模拟选项
const fleetOptions = ref<any[]>([])
const channelOptions = [{ label: '小鹏小程序', value: 1 }]
const cityOptions = ref<any[]>([])
const orderStatusOptions = [
  { 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' },
  { label: '待接单', value: 1 },
  { label: '待出发', value: 2 },
  { label: '待到达预约地点', value: 3 },
  { label: '待乘客上车', value: 4 },
  { label: '乘客已上车', value: 5 },
  { label: '服务中', value: 6 },
  { label: '待客下车', value: 7 },
  { label: '待支付', value: 8 },
  { label: '待评价', value: 9 },
  { label: '已完成', value: 10 },
  { label: '已取消', value: 0 },
]
// 搜索项配置
@@ -245,17 +283,17 @@
  {
    label: '订单号',
    path: 'orderNumber',
    render: () => <NInput v-model:value={orderSearchModel.value.orderNumber} placeholder="请输入" clearable />,
    render: () => <NInput v-model:value={orderSearchModel.value.orderNumber} placeholder="请输入订单编号" clearable />,
  },
  {
    label: '用户',
    path: 'userPhone',
    render: () => <NInput v-model:value={orderSearchModel.value.userPhone} placeholder="请输入" clearable />,
    path: 'nickName',
    render: () => <NInput v-model:value={orderSearchModel.value.nickName} placeholder="请输入用户昵称" clearable />,
  },
  {
    label: '车辆',
    path: 'vehicleNumber',
    render: () => <NInput v-model:value={orderSearchModel.value.vehicleNumber} placeholder="请输入" clearable />,
    path: 'plateNumber',
    render: () => <NInput v-model:value={orderSearchModel.value.plateNumber} placeholder="请输入车牌号" clearable />,
  },
  {
    label: '车队',
@@ -291,20 +329,20 @@
  },
  {
    label: '渠道',
    path: 'channel',
    path: 'orderSource',
    render: () => (
      <NSelect
        v-model:value={orderSearchModel.value.channel}
        v-model:value={orderSearchModel.value.orderSource}
        options={channelOptions}
        clearable
        placeholder="请选择"
        onUpdate:show={(value: boolean) => isOrderChannelOpen.value = value}
        onUpdate:show={(value: boolean) => isOrderSourceOpen.value = value}
      >
        {{
          arrow: () => (
            <img
              src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e120f81c-9f15-4e1a-9938-22252898edf9.png"
              class={`arrow-icon ${isOrderChannelOpen.value ? 'arrow-icon--open' : ''}`}
              class={`arrow-icon ${isOrderSourceOpen.value ? 'arrow-icon--open' : ''}`}
              style={{ width: '16px', height: '16px' }}
              alt="arrow"
            />
@@ -323,10 +361,10 @@
  },
  {
    label: '城市',
    path: 'cityId',
    path: 'cityCode',
    render: () => (
      <NCascader
        v-model:value={orderSearchModel.value.cityId}
        v-model:value={orderSearchModel.value.cityCode}
        options={cityOptions.value}
        value-field="code"
        label-field="name"
@@ -380,216 +418,267 @@
      </NSelect>
    ),
  },
  {
    label: '订单时间',
    path: 'orderTime',
    render: () => (
      <NDatePicker
        v-model:value={orderSearchModel.value.orderTime}
        type="daterange"
        clearable
        start-placeholder="开始时间"
        end-placeholder="结束时间"
        style={{ width: '260px' }}
        actions={[]}
        close-on-select={true}
      >
        {{
          separator: () => <span>至</span>,
        }}
      </NDatePicker>
    ),
  },
])
const orderRecords = ref([
  {
    id: 1,
    orderNumber: '121314',
    orderTime: '2022.07.05 12:09',
    orderSource: '运维小程序',
    rideTime: '2022.07.05 12:09',
    userName: '张三',
    userPhone: '18380591471',
    startLocation: '成都天府新谷',
    orderTime: '2021/07/29 10:59',
    couponName: '优惠券名称名称',
    startLocation: '小鹏汽车大厦',
    endLocation: '天府广场',
    distance: '3.1 km',
    estimatedAmount: '¥ 88.00',
    actualAmount: '¥ 0.00',
    channelName: '小鹏小程序',
    status: 1, // 待接单
  },
  {
    id: 2,
    orderNumber: '121314',
    orderTime: '2022.07.05 12:09',
    orderSource: '运维小程序',
    rideTime: '2022.07.05 12:09',
    userName: '张三',
    userPhone: '18380591471',
    startLocation: '成都天府新谷',
    orderTime: '2021/07/29 10:59',
    couponName: '优惠券名称名称',
    startLocation: '小鹏汽车大厦',
    endLocation: '天府广场',
    status: 2, // 待出发
    distance: '3.1 km',
    estimatedAmount: '¥ 88.00',
    actualAmount: '¥ 0.00',
    channelName: '小鹏小程序',
    status: 2,
  },
  {
    id: 3,
    orderNumber: '121314',
    orderTime: '2022.07.05 12:09',
    orderSource: '运维小程序',
    rideTime: '2022.07.05 12:09',
    userName: '张三',
    userPhone: '18380591471',
    startLocation: '成都天府新谷',
    orderTime: '2021/07/29 10:59',
    couponName: '优惠券名称名称',
    startLocation: '小鹏汽车大厦',
    endLocation: '天府广场',
    status: 3, // 待到达预约地
    distance: '3.1 km',
    estimatedAmount: '¥ 88.00',
    actualAmount: '¥ 0.00',
    channelName: '小鹏小程序',
    status: 10,
  },
  {
    id: 4,
    orderNumber: '121314',
    orderTime: '2022.07.05 12:09',
    orderSource: '运维小程序',
    rideTime: '2022.07.05 12:09',
    userName: '张三',
    userPhone: '18380591471',
    startLocation: '成都天府新谷',
    orderTime: '2021/07/29 10:59',
    couponName: '优惠券名称名称',
    startLocation: '小鹏汽车大厦',
    endLocation: '天府广场',
    status: 4, // 待乘客上车
    distance: '3.1 km',
    estimatedAmount: '¥ 88.00',
    actualAmount: '¥ 0.00',
    channelName: '小鹏小程序',
    status: 0,
  },
  {
    id: 5,
    orderNumber: '121314',
    orderTime: '2022.07.05 12:09',
    orderSource: '运维小程序',
    rideTime: '2022.07.05 12:09',
    userName: '张三',
    userPhone: '18380591471',
    startLocation: '成都天府新谷',
    orderTime: '2021/07/29 10:59',
    couponName: '优惠券名称名称',
    startLocation: '小鹏汽车大厦',
    endLocation: '天府广场',
    status: 6, // 服务中 (关闭订单/详情/删除)
  },
  {
    id: 6,
    orderNumber: '121314',
    orderTime: '2022.07.05 12:09',
    orderSource: '运维小程序',
    rideTime: '2022.07.05 12:09',
    userName: '张三',
    userPhone: '18380591471',
    startLocation: '成都天府新谷',
    endLocation: '天府广场',
    status: 8, // 待支付
  },
  {
    id: 7,
    orderNumber: '121314',
    orderTime: '2022.07.05 12:09',
    orderSource: '运维小程序',
    rideTime: '2022.07.05 12:09',
    userName: '张三',
    userPhone: '18380591471',
    startLocation: '成都天府新谷',
    endLocation: '天府广场',
    status: 9, // 待评价
  },
  {
    id: 8,
    orderNumber: '121314',
    orderTime: '2022.07.05 12:09',
    orderSource: '运维小程序',
    rideTime: '2022.07.05 12:09',
    userName: '张三',
    userPhone: '18380591471',
    startLocation: '成都天府新谷',
    endLocation: '天府广场',
    status: 6, // 服务中
  },
  {
    id: 9,
    orderNumber: '121314',
    orderTime: '2022.07.05 12:09',
    orderSource: '运维小程序',
    rideTime: '2022.07.05 12:09',
    userName: '张三',
    userPhone: '18380591471',
    startLocation: '成都天府新谷',
    endLocation: '天府广场',
    status: 6, // 服务中
  },
  {
    id: 10,
    orderNumber: '121314',
    orderTime: '2022.07.05 12:09',
    orderSource: '运维小程序',
    rideTime: '2022.07.05 12:09',
    userName: '张三',
    userPhone: '18380591471',
    startLocation: '成都天府新谷',
    endLocation: '天府广场',
    status: 6, // 服务中
    distance: '3.1 km',
    estimatedAmount: '¥ 88.00',
    actualAmount: '¥ 0.00',
    channelName: '小鹏小程序',
    status: 10,
  },
])
const orderPage = ref({ pageNum: 1, pageSize: 10 })
const selectedOrderRowKeys = ref<number[]>([])
// 订单状态
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 orderColumns = [
  { type: 'selection' as const, width: 50 },
  { title: '订单编号', key: 'orderNumber', align: 'left' as const, width: 100 },
  { title: '下单时间', key: 'orderTime', align: 'left' as const, width: 140 },
  { title: '订单来源', key: 'orderSource', align: 'left' as const, width: 120 },
  { title: '乘车时间', key: 'rideTime', align: 'left' as const, width: 140 },
  { title: '下单用户昵称', key: 'userName', align: 'left' as const, width: 120 },
  { title: '下单用户手机', key: 'userPhone', align: 'left' as const, width: 120 },
  { title: '起点', key: 'startLocation', align: 'left' as const, width: 140 },
  { title: '终点', key: 'endLocation', align: 'left' as const, width: 140 },
const orderColumns = computed<DataTableColumns<any>>(() => [
  {
    type: 'selection',
    width: 40,
    fixed: 'left',
  },
  {
    title: '订单编号',
    key: 'orderNumber',
    width: 150,
    resizable: true,
    fixed: 'left',
  },
  {
    title: '下单时间',
    key: 'createTime',
    width: 160,
    resizable: true,
    render: row => <span>{row.createTime ? dayjs(row.createTime).format('YYYY/MM/DD HH:mm') : '-'}</span>,
  },
  {
    title: '订单来源',
    key: 'orderSource',
    width: 100,
    resizable: true,
    render(row) {
      return row.orderSource === 1 ? '小鹏小程序' : '-'
    },
  },
  {
    title: '下单用户昵称',
    key: 'nickName',
    width: 120,
    resizable: true,
  },
  {
    title: '下单用户手机',
    key: 'phoneNumber',
    width: 120,
    resizable: true,
  },
  {
    title: '起点',
    key: 'startPointName',
    width: 160,
    resizable: true,
    ellipsis: {
      tooltip: true,
    },
  },
  {
    title: '终点',
    key: 'endPointName',
    width: 160,
    resizable: true,
    ellipsis: {
      tooltip: true,
    },
  },
  {
    title: '状态',
    key: 'status',
    align: 'left' as const,
    width: 120,
    render: (row: any) => {
      const statusMap: Record<number, { type: 'info' | 'success' | 'warning' | 'default', text: string, color: string, bgColor: string }> = {
        1: { type: 'info', text: '待接单', color: '#0068FF', bgColor: '#E6F4FF' },
        2: { type: 'success', text: '待出发', color: '#06B752', bgColor: '#E6F8ED' },
        3: { type: 'warning', text: '待到达预约地', color: '#F29B13', bgColor: '#FEF5E7' },
        4: { type: 'info', text: '待乘客上车', color: '#0068FF', bgColor: '#E6F4FF' },
        6: { type: 'success', text: '服务中', color: '#06B752', bgColor: '#E6F8ED' },
        8: { type: 'warning', text: '待支付', color: '#F29B13', bgColor: '#FEF5E7' },
        9: { type: 'default', text: '待评价', color: '#9013FE', bgColor: '#F4E7FE' },
        0: { type: 'default', text: '已取消', color: 'rgba(23, 31, 38, 0.45)', bgColor: 'rgba(23, 31, 38, 0.04)' },
      }
      const status = statusMap[row.status]
      return status
        ? (
            <NTag
              bordered={false}
              style={{
                color: status.color,
                backgroundColor: status.bgColor,
              }}
            >
              {status.text}
            </NTag>
          )
        : '-'
    resizable: true,
    render(row) {
      const option = statusOptions.find(item => item.value === row.status)
      return (
        <n-tag
          type={option?.type || 'default'}
          bordered={false}
          style={row.status === 9
            ? {
                color: '#764DFF',
                backgroundColor: '#F6F0FF',
              }
            : {}}
        >
          {option?.label || ''}
        </n-tag>
      )
    },
  },
  {
    title: '操作',
    key: 'actions',
    align: 'left' as const,
    width: 180,
    fixed: 'right' as const,
    render: (row: any) => {
      // 根据状态判断按钮
      // 待接单(1)、待出发(2)、待到达预约地(3)、待乘客上车(4): 取消订单、改派、详情、删除 (注: 设计图里待接单没有改派,但为简化先通用渲染)
      // 服务中(6): 关闭订单、详情、删除
      // 待支付(8): 改价、详情、删除
      // 待评价(9): 详情、删除
      const actions = []
      if ([1, 2, 3, 4].includes(row.status)) {
        actions.push(<span style={{ color: '#0068FF', cursor: 'pointer' }}>取消订单</span>)
        if (row.status !== 1) {
          actions.push(<span style={{ color: '#0068FF', cursor: 'pointer' }}>改派</span>)
        }
      }
      else if (row.status === 6) {
        actions.push(<span style={{ color: '#0068FF', cursor: 'pointer' }}>关闭订单</span>)
      }
      else if (row.status === 8) {
        actions.push(<span style={{ color: '#0068FF', cursor: 'pointer' }}>改价</span>)
      }
      actions.push(<span style={{ color: '#0068FF', cursor: 'pointer' }}>详情</span>)
      actions.push(<span style={{ color: '#0068FF', cursor: 'pointer' }}>删除</span>)
    width: 80,
    fixed: 'right',
    render: (row) => {
      return (
        <div class="flex justify-start gap-3">
          {actions}
        </div>
        <NButton
          text
          v-permission="60111"
          type="primary"
          onClick={() => {
            router.push({
              name: 'orderDetail',
              query: {
                orderId: row.id!,
              },
            })
          }}
        >
          详情
        </NButton>
      )
    },
  },
]
])
// 订单搜索操作
function handleSearchOrder() {
@@ -599,12 +688,13 @@
function handleResetOrder() {
  orderSearchModel.value = {
    orderNumber: '',
    userPhone: '',
    vehicleNumber: '',
    nickName: '',
    plateNumber: '',
    fleetId: null,
    channel: null,
    cityId: null,
    orderSource: null,
    cityCode: null,
    status: null,
    orderTime: null,
  }
}
@@ -665,203 +755,83 @@
<template>
  <div class="user-detail-page roboTaxi w-full h-full">
    <div class="flex flex-col gap-12px flex-1">
      <!-- 页面标题区 -->
      <NCard :bordered="false" class="page-header-card">
        <div class="page-header">
    <div class="flex flex-col gap-12px flex-1 h-full">
      <!-- 基础信息区 -->
      <div class="page-header-card" style="flex: none; padding-bottom: 24px; background: white; padding: 24px;">
        <div class="page-header" style="margin-bottom: 24px;">
          <div class="title-left">
            <div class="blue-bar" />
            <span class="title-text">用户详情</span>
            <span class="title-text">基础信息</span>
          </div>
          <!-- <NButton @click="handleBack">返回</NButton> -->
        </div>
        <NTabs v-model:value="activeTab" type="line" animated class="info-list flex-1">
          <!-- 基础信息 -->
          <NTabPane name="basicInfo" tab="基础信息" class="h-full flex-1">
            <div class="flex flex-col h-full basic-info-container">
              <div class="info-row">
                <div class="info-item">
                  <span class="info-label">用户名称:</span>
                  <span class="info-value">{{ basicInfoData.userName || '-' }}</span>
                </div>
                <div class="info-item">
                  <span class="info-label">白名单账号:</span>
                  <span class="info-value">{{ basicInfoData.whitelistAccount || '-' }}</span>
                </div>
                <div class="info-item">
                  <span class="info-label">状态:</span>
                  <span class="info-value">{{ basicInfoData.status || '-' }}</span>
                </div>
              </div>
              <div class="info-row">
                <div class="info-item">
                  <span class="info-label">是否优先派单:</span>
                  <span class="info-value">{{ basicInfoData.isPriorityDispatch || '-' }}</span>
                </div>
                <div class="info-item">
                  <span class="info-label">有效期:</span>
                  <span class="info-value">{{ basicInfoData.expireDate || '-' }}</span>
                </div>
                <div class="info-item">
                  <span class="info-label">最大下单数量:</span>
                  <span class="info-value">{{ basicInfoData.maxOrderCount || '-' }}</span>
                </div>
              </div>
        <div class="basic-info-container">
          <div class="info-row">
            <div class="info-item">
              <span class="info-label">用户名称:</span>
              <span class="info-value">{{ basicInfoData.userName || '-' }}</span>
            </div>
          </NTabPane>
            <div class="info-item">
              <span class="info-label">白名单账号:</span>
              <span class="info-value">{{ basicInfoData.whitelistAccount || '-' }}</span>
            </div>
            <div class="info-item">
              <span class="info-label">状态:</span>
              <span class="info-value">
                <NTag v-if="basicInfoData.status === '正常'" type="info" :bordered="false">
                  {{ basicInfoData.status || '-' }}
                </NTag>
                <span v-else>{{ basicInfoData.status || '-' }}</span>
              </span>
            </div>
          </div>
          <div class="info-row">
            <div class="info-item">
              <span class="info-label">是否优先派单:</span>
              <span class="info-value">{{ basicInfoData.isPriorityDispatch || '-' }}</span>
            </div>
            <div class="info-item">
              <span class="info-label">有效期:</span>
              <span class="info-value">{{ basicInfoData.expireDate || '-' }}</span>
            </div>
            <div class="info-item">
              <span class="info-label">最大下单数量:</span>
              <span class="info-value">{{ basicInfoData.maxOrderCount || '-' }}</span>
            </div>
          </div>
          <div class="info-row">
            <div class="info-item">
              <span class="info-label">创建人:</span>
              <span class="info-value">{{ basicInfoData.creator || '-' }}</span>
            </div>
            <div class="info-item">
              <span class="info-label">创建时间:</span>
              <span class="info-value">{{ basicInfoData.createTime || '-' }}</span>
            </div>
            <div class="info-item" />
          </div>
        </div>
      </div>
      <!-- Tab 标签页和内容区 -->
      <div class="info-list flex-1">
        <NTabs v-model:value="activeTab" type="line" animated class="flex-1 h-full">
          <!-- 活动参与记录 -->
          <NTabPane name="activityRecord" tab="活动参与记录" class="h-full flex-1">
            <div class="flex flex-col h-full">
            <div class="flex flex-col h-full px-12px">
              <!-- 搜索区域 -->
              <div class="search-form-area">
                <NForm
                  inline
                  label-placement="left"
                  :show-feedback="false"
                  style="width: 100%"
                >
                  <div class="search-wrapper">
                    <div class="search-fields">
                      <NFormItem label="领取时间:">
                        <NDatePicker
                          v-model:value="activitySearchModel.timeRange"
                          type="daterange"
                          clearable
                          start-placeholder="开始时间"
                          end-placeholder="结束时间"
                          style="width: 260px"
                          :actions="[]"
                          :close-on-select="true"
                        >
                          <template #separator>
                            <span>至</span>
                          </template>
                        </NDatePicker>
                      </NFormItem>
                      <NFormItem label="所属活动:">
                        <NInput
                          v-model:value="activitySearchModel.activityName"
                          placeholder="请输入活动名称"
                          style="width: 220px"
                        />
                      </NFormItem>
                      <NFormItem label="使用状态:">
                        <NSelect
                          v-model:value="activitySearchModel.useStatus"
                          :options="useStatusOptions"
                          clearable
                          placeholder="请选择使用状态"
                          style="width: 200px"
                          @update:show="(value: boolean) => isUseStatusSelectOpen = value"
                        >
                          <template #arrow>
                            <img
                              src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e120f81c-9f15-4e1a-9938-22252898edf9.png"
                              class="arrow-icon"
                              :class="{ 'arrow-icon--open': isUseStatusSelectOpen }"
                              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%"
                              >
                            </div>
                          </template>
                        </NSelect>
                      </NFormItem>
                    </div>
                    <div class="search-actions">
                      <NButton
                        class="action-btn"
                        @click="handleResetActivity"
                      >
                        重置
                      </NButton>
                      <NButton
                        type="primary"
                        class="action-btn"
                        @click="handleSearchActivity"
                      >
                        查询
                      </NButton>
                    </div>
                  </div>
                </NForm>
              </div>
              <!-- 批量操作区 -->
              <div class="table-actions">
                <NButton @click="handleBatchDelete">
                  批量删除
                </NButton>
              </div>
              <!-- 数据表格 -->
              <NDataTable
                v-model:checked-row-keys="selectedRowKeys"
                :columns="activityColumns"
                :data="activityRecords"
                :row-key="(row) => row.id"
                class="flex-1"
                flex-height
                striped
                :bordered="false"
              >
                <template #empty>
                  <div class="flex-center">
                    <img
                      src="/src/assets/element-img/no-data.png"
                      style="width: 123px; height: 125px"
                    >
                  </div>
                </template>
              </NDataTable>
              <!-- 分页器 -->
              <Pagination
                style="margin-top: 16px; margin-bottom: 16px;"
                :count="1000"
                :page="activityPage.pageNum"
                :page-size="activityPage.pageSize"
                @change="handleActivityPageChange"
              />
            </div>
          </NTabPane>
          <!-- 订单记录 -->
          <NTabPane name="orderRecord" tab="订单记录" class="h-full flex-1">
            <div class="flex flex-col h-full">
              <!-- 搜索区域 -->
              <div class="search-form-area">
                <NForm
                  inline
                  label-placement="left"
                  :show-feedback="false"
                  style="width: 100%"
                >
                <NForm inline label-placement="left" :show-feedback="false" style="width: 100%">
                  <SearchPanel>
                    <NGi v-for="item in orderFilterItems" :key="item.path">
                    <NGi v-for="item in activityFilterItems" :key="item.path" class="n-grid-item">
                      <NFormItem :label="item.label" :path="item.path">
                        <component :is="item.render" />
                      </NFormItem>
                    </NGi>
                    <template #action>
                      <NButton
                        class="action-btn"
                        @click="handleResetOrder"
                      >
                      <NButton @click="handleResetActivity">
                        重置
                      </NButton>
                      <NButton
                        type="primary"
                        class="action-btn ml-12px"
                        @click="handleSearchOrder"
                      >
                      <NButton type="primary" @click="handleSearchActivity">
                        查询
                      </NButton>
                    </template>
@@ -870,7 +840,63 @@
              </div>
              <!-- 批量操作区 -->
              <div class="table-actions">
              <div class="gap-2 items-center table-actions">
                <span v-if="selectedRowKeys.length > 0" style="color: rgba(23, 31, 38, 0.6); font-weight: 400">
                  已选择 {{ selectedRowKeys.length }} 条记录
                </span>
                <NButton v-permission="30121" :disabled="selectedRowKeys.length === 0" @click="handleBatchDelete">
                  批量删除
                </NButton>
              </div>
              <!-- 数据表格 -->
              <NDataTable
                v-model:checked-row-keys="selectedRowKeys" :columns="activityColumns" :data="activityRecords"
                :row-key="(row) => row.id" class="flex-1" flex-height striped :bordered="false"
              >
                <template #empty>
                  <div class="flex-center">
                    <img src="/src/assets/element-img/no-data.png" style="width: 123px; height: 125px">
                  </div>
                </template>
              </NDataTable>
              <!-- 分页器 -->
              <Pagination
                style="margin-top: 16px;" :count="1000" :page="activityPage.pageNum"
                :page-size="activityPage.pageSize" @change="handleActivityPageChange"
              />
            </div>
          </NTabPane>
          <!-- 订单记录 -->
          <NTabPane name="orderRecord" tab="订单记录" class="h-full flex-1">
            <div class="flex flex-col h-full px-12px">
              <!-- 搜索区域 -->
              <div class="search-form-area">
                <NForm inline label-placement="left" :show-feedback="false" style="width: 100%">
                  <SearchPanel>
                    <NGi v-for="item in orderFilterItems" :key="item.path" class="n-grid-item">
                      <NFormItem :label="item.label" :path="item.path">
                        <component :is="item.render" />
                      </NFormItem>
                    </NGi>
                    <template #action>
                      <NButton @click="handleResetOrder">
                        重置
                      </NButton>
                      <NButton type="primary" @click="handleSearchOrder">
                        查询
                      </NButton>
                    </template>
                  </SearchPanel>
                </NForm>
              </div>
              <!-- 批量操作区 -->
              <div class="gap-2 items-center table-actions">
                <span v-if="selectedOrderRowKeys.length > 0" style="color: rgba(23, 31, 38, 0.6);font-weight: 400;">
                  {{ $t('accountManagement.messages.selectedRecords', { count: selectedOrderRowKeys.length }) }}
                </span>
                <NButton @click="handleBatchExportOrder">
                  批量导出订单
                </NButton>
@@ -878,43 +904,25 @@
              <!-- 数据表格 -->
              <NDataTable
                v-model:checked-row-keys="selectedOrderRowKeys"
                :columns="orderColumns"
                :data="orderRecords"
                :row-key="(row) => row.id"
                class="flex-1"
                flex-height
                striped
                :scroll-x="1600"
                :bordered="false"
                v-model:checked-row-keys="selectedOrderRowKeys" :columns="orderColumns" :data="orderRecords"
                :row-key="(row) => row.id" class="flex-1" flex-height striped :scroll-x="1160" :bordered="false"
              >
                <template #empty>
                  <div class="flex-center">
                    <img
                      src="/src/assets/element-img/no-data.png"
                      style="width: 123px; height: 125px"
                    >
                    <img src="/src/assets/element-img/no-data.png" style="width: 123px; height: 125px">
                  </div>
                </template>
              </NDataTable>
              <!-- 分页器 -->
              <Pagination
                style="margin-top: 16px; margin-bottom: 16px;"
                :count="1000"
                :page="orderPage.pageNum"
                :page-size="orderPage.pageSize"
                @change="handleOrderPageChange"
                style="margin-top: 16px;" :count="1000" :page="orderPage.pageNum"
                :page-size="orderPage.pageSize" @change="handleOrderPageChange"
              />
            </div>
          </NTabPane>
        </NTabs>
      </NCard>
      <!-- Tab 标签页和内容区 -->
      <!-- <NCard :bordered="false" class="info-list flex-1">
      </NCard> -->
      </div>
    </div>
  </div>
</template>
@@ -937,13 +945,10 @@
}
.page-header-card {
  border-radius: 4px;
  flex: 1;
  :deep(.n-card__content) {
    padding: 16px 24px;
    display: flex;
    flex-direction: column;
  }
  border-radius: 6px;
  background-color: #fff;
  padding: 24px;
  box-sizing: border-box;
}
.page-header {
@@ -956,8 +961,8 @@
    align-items: center;
    .blue-bar {
      width: 4px;
      height: 16px;
      width: 3px;
      height: 14px;
      background-color: #0068ff;
      border-radius: 2px;
      margin-right: 8px;
@@ -973,26 +978,28 @@
}
.info-list {
  border-radius: 4px;
  border-radius: 6px;
  display: flex;
  flex-direction: column;
  :deep(.n-card__content) {
    padding: 0 24px;
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
  }
  background-color: #fff;
  padding-bottom: 12px;
  box-sizing: border-box;
  :deep(.n-tabs) {
    display: flex;
    flex-direction: column;
    flex: 1;
    .n-tabs-nav-scroll-content {
      box-sizing: border-box;
      padding-left: 16px;
    }
    .n-tabs-nav.n-tabs-nav--line-type.n-tabs-nav--top .n-tabs-nav-scroll-content {
      border-bottom: 1px solid rgba(23, 31, 38, 0.1);
    }
    .n-tabs-nav {
      padding: 12px 0 0;
      border-bottom: 1px solid rgba(23, 31, 38, 0.08);
      .n-tabs-tab {
        padding-bottom: 12px;
@@ -1007,44 +1014,33 @@
      flex: 1;
      min-height: 0;
    }
    .n-tabs-tab__label {
      font-weight: 600;
    }
    .n-tabs-bar {
      margin-left: 16px;
    }
  }
}
.search-form-area {
  padding: 12px 0 12px;
  .search-wrapper {
    display: flex;
    width: 100%;
    justify-content: space-between;
    align-items: flex-start;
    .search-fields {
      display: flex;
      flex-wrap: wrap;
      gap: 0 24px;
    }
    .search-actions {
      display: flex;
      gap: 8px;
      white-space: nowrap;
    }
  }
  width: 100%;
}
.table-actions {
  margin-bottom: 16px;
  margin-bottom: 12px;
  display: flex;
  justify-content: flex-start;
}
.basic-info-container {
  padding: 10px 0 24px;
  .info-row {
    display: flex;
    margin-bottom: 24px;
    margin-bottom: 16px;
    &:last-child {
      margin-bottom: 0;
@@ -1059,7 +1055,6 @@
    .info-label {
      color: rgba(23, 31, 38, 0.6);
      // width: 100px; /* 根据设计图和长文本保持标签宽度一致 */
      flex-shrink: 0;
    }
@@ -1071,32 +1066,7 @@
  }
}
:deep(.n-tabs-pane-wrapper){
  flex:1;
:deep(.n-tabs-pane-wrapper) {
  flex: 1;
}
:deep(.n-data-table) {
  .n-data-table-th {
    background-color: #f7f8f9 !important;
    font-weight: 500;
    color: rgba(23, 31, 38, 0.8);
  }
  .n-data-table-td {
    color: #171f26;
  }
}
.action-btn {
  padding: 7px 20px;
  border-radius: 4px;
}
/* .expand-btn {
  display: inline-flex;
  align-items: center;
  color: #0068FF;
  cursor: pointer;
  font-size: 14px;
  user-select: none;
} */
</style>