| | |
| | | pid: 6, |
| | | }, |
| | | { |
| | | name: 'directServiceManagement', |
| | | path: '/transaction/directServiceManagement', |
| | | title: '定向服务管理', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:list-view', |
| | | menuType: 'page', |
| | | componentPath: '/transaction/directServiceManagement/index.vue', |
| | | id: 607, |
| | | keepAlive: true, |
| | | pid: 6, |
| | | }, |
| | | { |
| | | name: 'directServiceManagementAdd', |
| | | path: '/transaction/directServiceManagement/add', |
| | | title: '新增定向服务', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:list-view', |
| | | menuType: 'page', |
| | | componentPath: '/transaction/directServiceManagement/add.vue', |
| | | id: 6071, |
| | | keepAlive: false, |
| | | pid: 6, |
| | | hide: true, |
| | | activeMenu: '/transaction/directServiceManagement', |
| | | fullScreen: true, |
| | | }, |
| | | { |
| | | name: 'directServiceManagementEdit', |
| | | path: '/transaction/directServiceManagement/edit', |
| | | title: '编辑定向服务', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:list-view', |
| | | menuType: 'page', |
| | | componentPath: '/transaction/directServiceManagement/add.vue', |
| | | id: 6072, |
| | | keepAlive: false, |
| | | pid: 6, |
| | | hide: true, |
| | | activeMenu: '/transaction/directServiceManagement', |
| | | fullScreen: true, |
| | | }, |
| | | { |
| | | name: 'directServiceManagementDetail', |
| | | path: '/transaction/directServiceManagement/detail', |
| | | title: '定向服务详情', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:list-view', |
| | | menuType: 'page', |
| | | componentPath: '/transaction/directServiceManagement/detail.vue', |
| | | id: 6073, |
| | | keepAlive: false, |
| | | pid: 6, |
| | | hide: true, |
| | | activeMenu: '/transaction/directServiceManagement', |
| | | fullScreen: true, |
| | | }, |
| | | { |
| | | name: 'operations', |
| | | path: '/operations', |
| | | title: '运维执行', |
| New file |
| | |
| | | import { request } from '@/service/http' |
| | | |
| | | /** 获取定向服务列表 */ |
| | | export function getDirectServiceList(params: any) { |
| | | return request.Get<Service.ResponseResult<any>>('/api/direct-service/page', params) |
| | | } |
| | | |
| | | /** 添加定向服务 */ |
| | | export function addDirectService(params: any) { |
| | | return request.Post<Service.ResponseResult<any>>('/api/direct-service', params) |
| | | } |
| | | |
| | | /** 获取定向服务详情 */ |
| | | export function getDirectServiceDetail(id: any) { |
| | | return request.Get<Service.ResponseResult<any>>(`/api/direct-service/${id}`) |
| | | } |
| | | |
| | | /** 编辑定向服务 */ |
| | | export function editDirectService(id: any, params: any) { |
| | | return request.Put<Service.ResponseResult<any>>(`/api/direct-service/${id}`, params) |
| | | } |
| | | |
| | | /** 删除定向服务 */ |
| | | export function deleteDirectService(id: any) { |
| | | return request.Delete<Service.ResponseResult<any>>(`/api/direct-service/${id}`) |
| | | } |
| | | |
| | | /** 启用/禁用定向服务 */ |
| | | export function toggleDirectServiceStatus(id: any, status: any) { |
| | | return request.Put<Service.ResponseResult<any>>(`/api/direct-service/${id}/status`, { status }) |
| | | } |
| New file |
| | |
| | | <script setup lang="tsx"> |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { computed, onMounted, ref } from 'vue' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { NButton, NCascader, NCheckbox, NDataTable, NDatePicker, NForm, NGrid, NInput, NSpace, NTag, useDialog, useMessage } from 'naive-ui' |
| | | import { getRegionCascaderNoArea } from '@/service/api/operations' |
| | | import { useTabStore } from '@/store' |
| | | import { useBoolean } from '@/hooks' |
| | | import PriorityModal from './components/PriorityModal.vue' |
| | | import VehicleSelectModal from './components/VehicleSelectModal.vue' |
| | | import CruisePlanSelectModal from './components/CruisePlanSelectModal.vue' |
| | | import UserSelectModal from './components/UserSelectModal.vue' |
| | | |
| | | const tabStore = useTabStore() |
| | | const router = useRouter() |
| | | const route = useRoute() |
| | | const dialog = useDialog() |
| | | const message = useMessage() |
| | | |
| | | const isEdit = ref(false) |
| | | const id = ref<string | null>(null) |
| | | const loading = ref(false) |
| | | |
| | | const currentStep = ref(2) |
| | | |
| | | const formModel = ref({ |
| | | cityCode: null, |
| | | isPermanent: true, |
| | | startTime: null as number | null, |
| | | endTime: null as number | null, |
| | | remark: '', |
| | | }) |
| | | |
| | | // 表单校验规则 |
| | | const rules = { |
| | | cityCode: { |
| | | required: true, |
| | | message: '请选择城市', |
| | | trigger: ['blur', 'change'], |
| | | }, |
| | | } |
| | | |
| | | const formRef = ref<any>(null) |
| | | |
| | | const pcaOptions = ref<any[]>([]) |
| | | |
| | | function formatCascaderData(list: any[]) { |
| | | return list.map((item) => { |
| | | const newItem = { ...item } |
| | | if (newItem.children && newItem.children.length > 0) { |
| | | newItem.children = formatCascaderData(newItem.children) |
| | | } |
| | | else { |
| | | delete newItem.children |
| | | } |
| | | return newItem |
| | | }) |
| | | } |
| | | |
| | | async function fetchCascaderData() { |
| | | const res = await getRegionCascaderNoArea() |
| | | if (res.code === 200 && res.data) { |
| | | pcaOptions.value = formatCascaderData(res.data) |
| | | } |
| | | } |
| | | |
| | | async function fetchDetail() { |
| | | if (!id.value) |
| | | return |
| | | loading.value = true |
| | | try { |
| | | // const res = await getDirectServiceDetail(id.value) |
| | | // if (res.code === 200 && res.data) { |
| | | // const data = res.data |
| | | // formModel.value.cityCode = data.cityCode |
| | | // formModel.value.isPermanent = !!data.isPermanent |
| | | // if (data.startTime && data.endTime) { |
| | | // formModel.value.timeRange = [dayjs(data.startTime).valueOf(), dayjs(data.endTime).valueOf()] |
| | | // } |
| | | // formModel.value.boundVehicleCount = data.boundVehicleCount || 0 |
| | | // formModel.value.boundUserCount = data.boundUserCount || 0 |
| | | // formModel.value.status = data.status |
| | | // formModel.value.remark = data.remark || '' |
| | | // } |
| | | } |
| | | finally { |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | // 关联车辆 Mock 数据和表格配置 |
| | | const selectedVehicles = ref<any[]>([ |
| | | { |
| | | id: 1, |
| | | vehicleCode: 'S156165161651', |
| | | plateNo: '粤PXV7029', |
| | | model: 'Robotaxi', |
| | | fleet: '白云区1号', |
| | | cruisePlan: '广州巡游计划1111', |
| | | }, |
| | | { |
| | | id: 2, |
| | | vehicleCode: 'S156165161651', |
| | | plateNo: '粤PXV7029', |
| | | model: 'Robotaxi', |
| | | fleet: '白云区1号', |
| | | cruisePlan: '-', |
| | | }, |
| | | ]) |
| | | |
| | | const vehicleColumns = computed<DataTableColumns<any>>(() => [ |
| | | { title: '序号', key: 'index', width: 60, render: (_, index) => index + 1 }, |
| | | { title: '车辆编码', key: 'vehicleCode' }, |
| | | { title: '车牌号', key: 'plateNo' }, |
| | | { title: '型号', key: 'model' }, |
| | | { title: '所属车队', key: 'fleet' }, |
| | | { title: '关联巡游计划', key: 'cruisePlan' }, |
| | | { |
| | | title: '操作', |
| | | key: 'actions', |
| | | width: 140, |
| | | render: (row) => { |
| | | return ( |
| | | <div class="flex items-center gap-2"> |
| | | <NButton text type="primary" onClick={() => handleRemoveVehicle(row)}>移除</NButton> |
| | | <NButton text type="primary" onClick={() => handleOpenCruisePlanModal(row)}>设置巡游计划</NButton> |
| | | </div> |
| | | ) |
| | | }, |
| | | }, |
| | | ]) |
| | | |
| | | // 服务用户 Mock 数据和表格配置 |
| | | const selectedUsers = ref<any[]>([ |
| | | { id: 1, userName: '方一强', account: '13034179070', status: 1, priority: 100, desc: '优先级说明内容说明内容说明内容' }, |
| | | { id: 2, userName: '李雅惠', account: '13619910302', status: 0, priority: 80, desc: '优先级说明内容说明内容说明内容' }, |
| | | { id: 3, userName: '倪怡芳', account: '15392830508', status: 1, priority: 50, desc: '优先级说明内容说明内容说明内容' }, |
| | | { id: 4, userName: '-', account: '13818629999', status: 0, priority: 50, desc: '优先级说明内容说明内容说明内容' }, |
| | | { id: 5, userName: '-', account: '13654373011', status: 1, priority: 50, desc: '优先级说明内容说明内容说明内容' }, |
| | | { id: 6, userName: '谢彦文', account: '13396827528', status: 1, priority: 50, desc: '优先级说明内容说明内容说明内容' }, |
| | | { id: 7, userName: '-', account: '18878751881', status: 1, priority: 50, desc: '优先级说明内容说明内容说明内容' }, |
| | | { id: 8, userName: '-', account: '18878751881', status: 1, priority: 50, desc: '优先级说明内容说明内容说明内容' }, |
| | | { id: 9, userName: '-', account: '18878751881', status: 1, priority: 50, desc: '优先级说明内容说明内容说明内容' }, |
| | | { id: 10, userName: '-', account: '18878751881', status: 1, priority: 50, desc: '优先级说明内容说明内容说明内容' }, |
| | | { id: 11, userName: '-', account: '18878751881', status: 1, priority: 50, desc: '优先级说明内容说明内容说明内容' }, |
| | | ]) |
| | | |
| | | const userColumns = computed<DataTableColumns<any>>(() => [ |
| | | { title: '序号', key: 'index', width: 60, render: (_, index) => index + 1 }, |
| | | { title: '用户名称', key: 'userName' }, |
| | | { title: '白名单账号', key: 'account' }, |
| | | { |
| | | title: '状态', |
| | | key: 'status', |
| | | render: (row) => { |
| | | const isNormal = row.status === 1 |
| | | return ( |
| | | <NTag |
| | | round |
| | | bordered={false} |
| | | style={{ |
| | | borderRadius: '4px', |
| | | color: isNormal ? '#0068FF' : '#FF8C00', |
| | | backgroundColor: isNormal ? 'rgba(0, 104, 255, 0.1)' : 'rgba(255, 140, 0, 0.1)', |
| | | padding: '0 8px', |
| | | }} |
| | | > |
| | | {isNormal ? '正常' : '冻结'} |
| | | </NTag> |
| | | ) |
| | | }, |
| | | }, |
| | | { title: '用户优先级', key: 'priority' }, |
| | | { title: '优先级说明', key: 'desc' }, |
| | | { |
| | | title: '操作', |
| | | key: 'actions', |
| | | width: 130, |
| | | render: (row) => { |
| | | return ( |
| | | <div class="flex items-center gap-2"> |
| | | <NButton text type="primary" onClick={() => handleRemoveUser(row)}>移除</NButton> |
| | | <NButton text type="primary" onClick={() => handleOpenPriorityModal(row)}>设置优先级</NButton> |
| | | </div> |
| | | ) |
| | | }, |
| | | }, |
| | | ]) |
| | | |
| | | // 弹窗相关逻辑已抽离到单独组件 |
| | | const { bool: priorityModalVisible, setTrue: showPriorityModal } = useBoolean(false) |
| | | const { bool: vehicleSelectModalVisible, setTrue: showVehicleModal } = useBoolean(false) |
| | | const { bool: cruisePlanModalVisible, setTrue: showCruisePlanModal } = useBoolean(false) |
| | | const { bool: userSelectModalVisible, setTrue: showUserModal } = useBoolean(false) |
| | | |
| | | const currentEditVehicle = ref<any>(null) // null 表示批量设置,否则为单条设置 |
| | | const currentEditUser = ref<any>(null) |
| | | |
| | | function handleOpenVehicleSelectModal() { |
| | | showVehicleModal() |
| | | } |
| | | |
| | | function handleOpenUserSelectModal() { |
| | | showUserModal() |
| | | } |
| | | |
| | | function handleUserSelectConfirm(selectedUsersData: any[]) { |
| | | const newUsers = selectedUsersData.filter( |
| | | (u: any) => !selectedUsers.value?.some((existing: any) => existing.id === u.id), |
| | | ) |
| | | |
| | | if (newUsers.length > 0) { |
| | | selectedUsers.value = [...(selectedUsers.value || []), ...newUsers.map(u => ({ |
| | | ...u, |
| | | priority: 50, // 默认赋予优先级 50 |
| | | desc: '默认优先级', // 默认说明 |
| | | }))] |
| | | } |
| | | } |
| | | |
| | | function handleOpenCruisePlanModal(row?: any) { |
| | | currentEditVehicle.value = row || null |
| | | showCruisePlanModal() |
| | | } |
| | | |
| | | function handleCruisePlanConfirm(selectedPlan: any) { |
| | | if (currentEditVehicle.value) { |
| | | // 单条设置 |
| | | currentEditVehicle.value.cruisePlan = selectedPlan.planName |
| | | } |
| | | else { |
| | | // 批量设置 |
| | | selectedVehicles.value.forEach((v) => { |
| | | v.cruisePlan = selectedPlan.planName |
| | | }) |
| | | } |
| | | } |
| | | |
| | | function handleVehicleSelectConfirm(selectedVehiclesData: any[]) { |
| | | // Mock adding selected vehicles to the list |
| | | const newVehicles = selectedVehiclesData.filter( |
| | | (v: any) => !selectedVehicles.value?.some((existing: any) => existing.id === v.id), |
| | | ) |
| | | |
| | | if (newVehicles.length > 0) { |
| | | selectedVehicles.value = [...(selectedVehicles.value || []), ...newVehicles] |
| | | } |
| | | } |
| | | |
| | | function handleOpenPriorityModal(row: any) { |
| | | currentEditUser.value = row |
| | | showPriorityModal() |
| | | } |
| | | |
| | | function submitPriority(priority: number) { |
| | | if (currentEditUser.value) { |
| | | currentEditUser.value.priority = priority |
| | | } |
| | | } |
| | | |
| | | function handleRemoveVehicle(row: any) { |
| | | dialog.warning({ |
| | | title: '提示', |
| | | content: '确认要移除该车辆吗?', |
| | | positiveText: '确认', |
| | | negativeText: '取消', |
| | | onPositiveClick: () => { |
| | | selectedVehicles.value = selectedVehicles.value.filter(v => v.id !== row.id) |
| | | message.success('移除成功') |
| | | }, |
| | | }) |
| | | } |
| | | |
| | | function handleRemoveUser(row: any) { |
| | | dialog.warning({ |
| | | title: '提示', |
| | | content: '确认要移除该用户吗?', |
| | | positiveText: '确认', |
| | | negativeText: '取消', |
| | | onPositiveClick: () => { |
| | | selectedUsers.value = selectedUsers.value.filter(u => u.id !== row.id) |
| | | message.success('移除成功') |
| | | }, |
| | | }) |
| | | } |
| | | |
| | | function handleResetVehicles() { |
| | | if (!selectedVehicles.value.length) { |
| | | message.warning('暂无可重置的数据') |
| | | return |
| | | } |
| | | dialog.warning({ |
| | | title: '提示', |
| | | content: '确认要清空已关联的车辆吗?', |
| | | positiveText: '确认', |
| | | negativeText: '取消', |
| | | onPositiveClick: () => { |
| | | selectedVehicles.value = [] |
| | | message.success('重置成功') |
| | | }, |
| | | }) |
| | | } |
| | | |
| | | function handleResetUsers() { |
| | | if (!selectedUsers.value.length) { |
| | | message.warning('暂无可重置的数据') |
| | | return |
| | | } |
| | | dialog.warning({ |
| | | title: '提示', |
| | | content: '确认要清空已选择的用户吗?', |
| | | positiveText: '确认', |
| | | negativeText: '取消', |
| | | onPositiveClick: () => { |
| | | selectedUsers.value = [] |
| | | message.success('重置成功') |
| | | }, |
| | | }) |
| | | } |
| | | |
| | | function handlePrev() { |
| | | currentStep.value-- |
| | | } |
| | | |
| | | function handleNext() { |
| | | if (currentStep.value === 1) { |
| | | formRef.value?.validate((errors: any) => { |
| | | if (!errors) { |
| | | currentStep.value = 2 |
| | | } |
| | | }) |
| | | } |
| | | else if (currentStep.value === 2) { |
| | | currentStep.value = 3 |
| | | } |
| | | } |
| | | |
| | | function handleCancel() { |
| | | tabStore.closeTab(route.fullPath) |
| | | router.back() |
| | | } |
| | | |
| | | onMounted(() => { |
| | | fetchCascaderData() |
| | | if (route.query.id) { |
| | | isEdit.value = true |
| | | id.value = route.query.id as string |
| | | fetchDetail() |
| | | } |
| | | }) |
| | | </script> |
| | | |
| | | <template> |
| | | <div class=" w-full h-full relative content flex flex-col"> |
| | | <div class="p-12px main flex flex-col flex-1 overflow-hidden"> |
| | | <div class="box mb-12px shrink-0"> |
| | | <div class="text-xl"> |
| | | 定向服务配置 |
| | | </div> |
| | | </div> |
| | | <!-- 顶部步骤条 --> |
| | | <div class="box mb-12px shrink-0"> |
| | | <n-steps |
| | | :current="currentStep" status="process" class="justify-center mx-auto" |
| | | style="max-width: 600px; padding-top: 12px;" |
| | | > |
| | | <n-step title="基础信息" /> |
| | | <n-step title="关联车辆" /> |
| | | <n-step title="服务用户" /> |
| | | </n-steps> |
| | | </div> |
| | | |
| | | <!-- 基础信息卡片 --> |
| | | <div v-if="currentStep === 1" class="box flex-1 flex flex-col overflow-y-auto"> |
| | | <div class="head shrink-0"> |
| | | 基础信息 |
| | | </div> |
| | | |
| | | <n-spin :show="loading"> |
| | | <NForm ref="formRef" :model="formModel" :rules="rules" label-placement="top" class="custom-form"> |
| | | <!-- 第一行:城市和生效时间段 --> |
| | | <NGrid :cols="24" :x-gap="24" :y-gap="8"> |
| | | <n-form-item-gi :span="6" label="城市" path="cityCode" required> |
| | | <NCascader |
| | | v-model:value="formModel.cityCode" :options="pcaOptions" value-field="code" |
| | | label-field="name" check-strategy="child" clearable placeholder="请选择" |
| | | > |
| | | <template #arrow> |
| | | <img |
| | | src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e120f81c-9f15-4e1a-9938-22252898edf9.png" |
| | | class="arrow-icon" style="width: 16px; height: 16px" alt="arrow" |
| | | > |
| | | </template> |
| | | </NCascader> |
| | | </n-form-item-gi> |
| | | |
| | | <n-form-item-gi :span="12" label="生效时间段" path="startTime" required> |
| | | <div class="flex items-center gap-2 w-full"> |
| | | <NDatePicker |
| | | v-model:value="formModel.startTime" type="datetime" clearable |
| | | :disabled="formModel.isPermanent" placeholder="请选择" class="flex-1" |
| | | /> |
| | | <span class="text-gray-400 shrink-0">至</span> |
| | | <NDatePicker |
| | | v-model:value="formModel.endTime" type="datetime" clearable |
| | | :disabled="formModel.isPermanent" placeholder="请选择" class="flex-1" |
| | | /> |
| | | <NCheckbox v-model:checked="formModel.isPermanent" class="shrink-0 ml-2"> |
| | | 长期有效 |
| | | </NCheckbox> |
| | | </div> |
| | | </n-form-item-gi> |
| | | |
| | | <n-form-item-gi :span="18" label="备注" path="remark"> |
| | | <NInput |
| | | v-model:value="formModel.remark" type="textarea" placeholder="请输入" show-count :maxlength="200" |
| | | class="bg-gray-50/50" |
| | | /> |
| | | </n-form-item-gi> |
| | | </NGrid> |
| | | </NForm> |
| | | </n-spin> |
| | | </div> |
| | | <!-- 关联车辆卡片 --> |
| | | <div v-if="currentStep === 2" class="box flex-1 flex flex-col overflow-hidden"> |
| | | <div class="head shrink-0"> |
| | | 关联车辆 |
| | | </div> |
| | | <div class="mb-4 shrink-0 flex gap-3"> |
| | | <NButton type="primary" secondary @click="handleOpenVehicleSelectModal"> |
| | | 选择 |
| | | </NButton> |
| | | <NButton @click="handleResetVehicles"> |
| | | 重置 |
| | | </NButton> |
| | | <NButton @click="handleOpenCruisePlanModal()"> |
| | | 批量设置巡游计划 |
| | | </NButton> |
| | | </div> |
| | | <NDataTable |
| | | :columns="vehicleColumns" |
| | | :data="selectedVehicles" |
| | | :bordered="false" |
| | | :single-line="false" |
| | | flex-height |
| | | class="flex-1" |
| | | /> |
| | | </div> |
| | | |
| | | <!-- 服务用户卡片 --> |
| | | <div v-if="currentStep === 3" class="box flex-1 flex flex-col overflow-hidden"> |
| | | <div class="head shrink-0"> |
| | | 服务用户 |
| | | </div> |
| | | <div class="mb-4 shrink-0 flex gap-3"> |
| | | <NButton type="primary" secondary @click="handleOpenUserSelectModal"> |
| | | 选择 |
| | | </NButton> |
| | | <NButton @click="handleResetUsers"> |
| | | 重置 |
| | | </NButton> |
| | | </div> |
| | | <NDataTable |
| | | :columns="userColumns" |
| | | :data="selectedUsers" |
| | | :bordered="false" |
| | | :single-line="false" |
| | | flex-height |
| | | class="flex-1" |
| | | /> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 底部操作栏 --> |
| | | <div class="footer flex justify-end items-center shrink-0"> |
| | | <NSpace> |
| | | <NButton v-if="currentStep === 1" class="w-84px" @click="handleCancel"> |
| | | 取消 |
| | | </NButton> |
| | | <NButton v-if="currentStep > 1" class="w-84px" @click="handlePrev"> |
| | | 上一步 |
| | | </NButton> |
| | | <NButton type="primary" class="w-84px" @click="handleNext"> |
| | | {{ currentStep === 3 ? '完成' : '下一步' }} |
| | | </NButton> |
| | | </NSpace> |
| | | </div> |
| | | |
| | | <PriorityModal |
| | | v-model:visible="priorityModalVisible" |
| | | :priority="currentEditUser?.priority ?? null" |
| | | @confirm="submitPriority" |
| | | /> |
| | | |
| | | <VehicleSelectModal |
| | | v-model:visible="vehicleSelectModalVisible" |
| | | :existing-vehicles="selectedVehicles" |
| | | @confirm="handleVehicleSelectConfirm" |
| | | /> |
| | | |
| | | <CruisePlanSelectModal |
| | | v-model:visible="cruisePlanModalVisible" |
| | | @confirm="handleCruisePlanConfirm" |
| | | /> |
| | | |
| | | <UserSelectModal |
| | | v-model:visible="userSelectModalVisible" |
| | | :existing-users="selectedUsers" |
| | | @confirm="handleUserSelectConfirm" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped lang="scss"> |
| | | .roboTaxi-card { |
| | | height: calc(100vh - 76px); |
| | | width: 100%; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .n-button--secondary { |
| | | box-sizing: border-box; |
| | | color: #0068FF; |
| | | border: 1px solid #7ABDFF; |
| | | background-color: #E6F4FF; |
| | | } |
| | | |
| | | .box { |
| | | padding: 24px; |
| | | background-color: #fff; |
| | | border-radius: 6px; |
| | | width: 100%; |
| | | box-sizing: border-box; |
| | | --n-label-text-color: rgba(23, 31, 38, 0.8); |
| | | } |
| | | |
| | | .head { |
| | | font-family: PingFang SC; |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | line-height: 24px; |
| | | letter-spacing: normal; |
| | | color: #171f26; |
| | | position: relative; |
| | | text-indent: 11px; |
| | | margin-bottom: 16px; |
| | | |
| | | &::before { |
| | | content: ""; |
| | | position: absolute; |
| | | top: 50%; |
| | | left: 0; |
| | | transform: translateY(-50%); |
| | | width: 3px; |
| | | height: 14px; |
| | | z-index: 0; |
| | | border-radius: 3px; |
| | | background: #0068ff; |
| | | } |
| | | } |
| | | |
| | | .footer { |
| | | position: sticky; |
| | | bottom: 0; |
| | | left: 0; |
| | | width: 100%; |
| | | height: 64px; |
| | | background-color: #fff; |
| | | box-shadow: 0px 0px 5px 0px rgba(23, 31, 38, 0.05); |
| | | padding: 16px 24px; |
| | | } |
| | | </style> |
| New file |
| | |
| | | <script setup lang="tsx"> |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { useBoolean } from '@/hooks' |
| | | import { computed, ref, watch } from 'vue' |
| | | import { NButton, NCard, NDataTable, NInput, NModal } from 'naive-ui' |
| | | import Pagination from '@/components/common/Pagination.vue' |
| | | |
| | | interface CruisePlanItem { |
| | | id: number |
| | | planName: string |
| | | relatedPoints: number |
| | | relatedVehicles: number |
| | | [key: string]: any |
| | | } |
| | | |
| | | const emit = defineEmits<{ |
| | | confirm: [selectedPlan: CruisePlanItem] |
| | | }>() |
| | | const visible = defineModel<boolean>('visible', { default: false }) |
| | | const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false) |
| | | |
| | | // 搜索条件 |
| | | const searchModel = ref({ |
| | | planName: '', |
| | | }) |
| | | |
| | | // 选中的行(单选) |
| | | const checkedRowKeys = ref<number[]>([]) |
| | | |
| | | const listData = ref<CruisePlanItem[]>([]) |
| | | |
| | | // 缓存所有加载过的数据 |
| | | const allLoadedData = ref<Map<number, CruisePlanItem>>(new Map()) |
| | | |
| | | const count = ref(0) |
| | | const currentPage = ref(1) |
| | | const pageSize = ref(10) |
| | | |
| | | // 表格列定义 |
| | | const columns = computed<DataTableColumns<CruisePlanItem>>(() => [ |
| | | { |
| | | type: 'selection', |
| | | multiple: false, |
| | | width: 50, |
| | | }, |
| | | { |
| | | title: '序号', |
| | | key: 'index', |
| | | width: 80, |
| | | render: (row, index) => <span>{(currentPage.value - 1) * pageSize.value + index + 1}</span>, |
| | | }, |
| | | { |
| | | title: '计划名称', |
| | | key: 'planName', |
| | | ellipsis: true, |
| | | }, |
| | | { |
| | | title: '关联点位', |
| | | key: 'relatedPoints', |
| | | }, |
| | | { |
| | | title: '关联车辆数', |
| | | key: 'relatedVehicles', |
| | | }, |
| | | ]) |
| | | |
| | | // 获取巡游计划列表(纯Mock) |
| | | async function getPlanList() { |
| | | startLoading() |
| | | try { |
| | | // 模拟网络延迟 |
| | | await new Promise(resolve => setTimeout(resolve, 500)) |
| | | |
| | | // 生成Mock数据,总共假设50条 |
| | | const totalItems = 50 |
| | | const startIdx = (currentPage.value - 1) * pageSize.value |
| | | const currentList = [] |
| | | |
| | | const basePlanName = searchModel.value.planName ? `${searchModel.value.planName}巡游计划` : '广州白云区巡游计划' |
| | | |
| | | for (let i = 0; i < pageSize.value; i++) { |
| | | const idx = startIdx + i + 1 |
| | | if (idx > totalItems) |
| | | break |
| | | |
| | | const randomPoints = Math.floor(Math.random() * 10) + 1 |
| | | |
| | | currentList.push({ |
| | | id: idx, |
| | | planName: basePlanName, |
| | | relatedPoints: randomPoints, |
| | | relatedVehicles: randomPoints, // 简化的Mock,数量与点位相同 |
| | | }) |
| | | } |
| | | |
| | | listData.value = currentList |
| | | count.value = totalItems |
| | | |
| | | // 存入缓存 |
| | | listData.value.forEach((item) => { |
| | | allLoadedData.value.set(item.id, item) |
| | | }) |
| | | } |
| | | catch (error) { |
| | | console.error('获取巡游计划失败', error) |
| | | } |
| | | finally { |
| | | endLoading() |
| | | } |
| | | } |
| | | |
| | | // 搜索 |
| | | function handleSearch() { |
| | | currentPage.value = 1 |
| | | getPlanList() |
| | | } |
| | | |
| | | function resetSearchModel() { |
| | | searchModel.value = { |
| | | planName: '', |
| | | } |
| | | } |
| | | |
| | | // 重置 |
| | | function handleReset() { |
| | | resetSearchModel() |
| | | currentPage.value = 1 |
| | | pageSize.value = 10 |
| | | getPlanList() |
| | | } |
| | | |
| | | // 取消 |
| | | function handleCancel() { |
| | | visible.value = false |
| | | } |
| | | |
| | | // 确认 |
| | | function handleConfirm() { |
| | | if (checkedRowKeys.value.length === 0) { |
| | | window.$message?.warning('请选择一个巡游计划') |
| | | return |
| | | } |
| | | |
| | | // 单选 |
| | | const selectedId = checkedRowKeys.value[0] |
| | | const selectedPlan = allLoadedData.value.get(selectedId) |
| | | |
| | | if (selectedPlan) { |
| | | emit('confirm', selectedPlan) |
| | | visible.value = false |
| | | } |
| | | } |
| | | |
| | | // 分页改变 |
| | | function changePage(page: number, size: number) { |
| | | const isPageSizeChanged = size !== pageSize.value |
| | | pageSize.value = size |
| | | currentPage.value = isPageSizeChanged ? 1 : page |
| | | getPlanList() |
| | | } |
| | | |
| | | function handleRowClick(row: CruisePlanItem) { |
| | | checkedRowKeys.value = [row.id] |
| | | } |
| | | |
| | | function rowProps(row: CruisePlanItem) { |
| | | return { |
| | | style: 'cursor: pointer;', |
| | | onClick: () => handleRowClick(row), |
| | | } |
| | | } |
| | | |
| | | // 监听弹窗打开 |
| | | watch(visible, (val) => { |
| | | if (val) { |
| | | resetSearchModel() |
| | | currentPage.value = 1 |
| | | pageSize.value = 10 |
| | | checkedRowKeys.value = [] |
| | | allLoadedData.value.clear() |
| | | getPlanList() |
| | | } |
| | | }) |
| | | </script> |
| | | |
| | | <template> |
| | | <NModal v-model:show="visible"> |
| | | <NCard |
| | | style=" |
| | | width: 900px; |
| | | height: 70vh; |
| | | max-height: 700px; |
| | | border-radius: 8px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | " |
| | | :bordered="false" |
| | | :segmented="{ content: true }" |
| | | content-style="padding: 16px 24px; flex: 1; display: flex; flex-direction: column; overflow: hidden;" |
| | | footer-style="padding: 16px 24px; border-top: 1px solid rgba(46, 61, 77, 0.1); flex-shrink: 0;" |
| | | > |
| | | <template #header> |
| | | <span style="font-size: 16px; font-weight: bold; color: #1F1F1F;">设置巡游计划</span> |
| | | </template> |
| | | <template #header-extra> |
| | | <div text class="cursor-pointer" @click="handleCancel"> |
| | | <span style="font-size: 18px; width: 10px; height: 10px; color: #999;">×</span> |
| | | </div> |
| | | </template> |
| | | |
| | | <div class="flex flex-col h-full overflow-hidden"> |
| | | <!-- 搜索区域 --> |
| | | <div class="mb-3 flex items-end justify-between" style="flex-shrink: 0"> |
| | | <div class="w-300px"> |
| | | <div class="mb-2 text-sm text-[#1F1F1F]"> |
| | | 计划名称 |
| | | </div> |
| | | <NInput |
| | | v-model:value="searchModel.planName" |
| | | placeholder="请输入" |
| | | clearable |
| | | /> |
| | | </div> |
| | | <div class="flex gap-8px"> |
| | | <NButton style="width: 84px; height: 32px" @click="handleReset"> |
| | | 重置 |
| | | </NButton> |
| | | <NButton |
| | | type="primary" |
| | | style="width: 84px; height: 32px" |
| | | @click="handleSearch" |
| | | > |
| | | 查询 |
| | | </NButton> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 表格区域 --> |
| | | <div class="flex-1 overflow-hidden" style="min-height: 0"> |
| | | <NDataTable |
| | | v-model:checked-row-keys="checkedRowKeys" |
| | | style="height: 100%" |
| | | flex-height |
| | | :columns="columns" |
| | | :data="listData" |
| | | :loading="loading" |
| | | :bordered="false" |
| | | :row-key="(row: CruisePlanItem) => row.id" |
| | | :pagination="false" |
| | | :row-props="rowProps" |
| | | > |
| | | <template #empty> |
| | | <div class="flex-center"> |
| | | <img |
| | | src="/src/assets/element-img/no-data.png" |
| | | style="width: 123px; height: 125px" |
| | | > |
| | | </div> |
| | | </template> |
| | | </NDataTable> |
| | | </div> |
| | | <Pagination |
| | | :count="count" |
| | | :page="currentPage" |
| | | :page-size="pageSize" |
| | | style="margin-top: 12px; flex-shrink: 0" |
| | | @change="changePage" |
| | | /> |
| | | </div> |
| | | |
| | | <template #footer> |
| | | <div class="flex justify-end gap-3"> |
| | | <NButton class="footer-btn" @click="handleCancel"> |
| | | 取消 |
| | | </NButton> |
| | | <NButton type="primary" class="footer-btn" @click="handleConfirm"> |
| | | 保存 |
| | | </NButton> |
| | | </div> |
| | | </template> |
| | | </NCard> |
| | | </NModal> |
| | | </template> |
| | | |
| | | <style scoped lang="scss"> |
| | | .mb-4 { |
| | | margin-bottom: 1rem; |
| | | } |
| | | .flex { |
| | | display: flex; |
| | | } |
| | | .items-end { |
| | | align-items: flex-end; |
| | | } |
| | | .gap-4 { |
| | | gap: 1rem; |
| | | } |
| | | .gap-2 { |
| | | gap: 0.5rem; |
| | | } |
| | | .gap-3 { |
| | | gap: 0.75rem; |
| | | } |
| | | .flex-1 { |
| | | flex: 1 1 0%; |
| | | } |
| | | .justify-between { |
| | | justify-content: space-between; |
| | | } |
| | | .justify-end { |
| | | justify-content: flex-end; |
| | | } |
| | | .mb-2 { |
| | | margin-bottom: 0.5rem; |
| | | } |
| | | .text-sm { |
| | | font-size: 0.875rem; |
| | | line-height: 1.25rem; |
| | | } |
| | | |
| | | .footer-btn { |
| | | padding: 7px 29px; |
| | | height: auto; |
| | | font-size: 13px; |
| | | line-height: 18px; |
| | | } |
| | | </style> |
| New file |
| | |
| | | <script setup lang="ts"> |
| | | import type { FormInst, FormRules } from 'naive-ui' |
| | | import { NButton, NForm, NFormItem, NInputNumber, NModal, NSpace } from 'naive-ui' |
| | | |
| | | const props = defineProps<{ |
| | | visible: boolean |
| | | priority: number | null |
| | | }>() |
| | | |
| | | const emit = defineEmits<{ |
| | | 'update:visible': [value: boolean] |
| | | confirm: [value: number] |
| | | }>() |
| | | |
| | | const formRef = ref<FormInst>() |
| | | const formModel = ref({ |
| | | priority: null as number | null, |
| | | }) |
| | | |
| | | const rules: FormRules = { |
| | | priority: [ |
| | | { |
| | | required: true, |
| | | type: 'number', |
| | | message: '请输入优先权重值(1-100)', |
| | | trigger: ['blur', 'change'], |
| | | }, |
| | | { |
| | | validator: (_, value: number | null) => { |
| | | if (value === null || value === undefined) |
| | | return new Error('请输入优先权重值(1-100)') |
| | | if (!Number.isInteger(value) || value < 1 || value > 100) |
| | | return new Error('请输入1-100的整数') |
| | | return true |
| | | }, |
| | | trigger: ['blur', 'change'], |
| | | }, |
| | | ], |
| | | } |
| | | |
| | | function closeModal() { |
| | | emit('update:visible', false) |
| | | } |
| | | |
| | | async function handleConfirm() { |
| | | await formRef.value?.validate() |
| | | if (formModel.value.priority !== null) { |
| | | emit('confirm', formModel.value.priority) |
| | | closeModal() |
| | | } |
| | | } |
| | | |
| | | watch( |
| | | () => props.visible, |
| | | (visible) => { |
| | | if (visible) { |
| | | formModel.value.priority = props.priority |
| | | formRef.value?.restoreValidation() |
| | | } |
| | | }, |
| | | ) |
| | | </script> |
| | | |
| | | <template> |
| | | <NModal |
| | | :show="visible" |
| | | :mask-closable="false" |
| | | preset="card" |
| | | title="设置优先级" |
| | | class="w-400px" |
| | | :auto-focus="false" |
| | | :segmented="{ |
| | | content: true, |
| | | action: true, |
| | | }" |
| | | @close="closeModal" |
| | | > |
| | | <div class="pt-4 pb-2"> |
| | | <NForm ref="formRef" :model="formModel" :rules="rules" label-placement="top" label-width="100"> |
| | | <NFormItem label="优先权重值" path="priority"> |
| | | <div class="w-full"> |
| | | <NInputNumber |
| | | v-model:value="formModel.priority" |
| | | placeholder="请输入优先权重值(1-100)" |
| | | :min="1" |
| | | :max="100" |
| | | :precision="0" |
| | | :show-button="false" |
| | | class="w-full" |
| | | /> |
| | | <div class="text-[#0068FF] text-12px mt-2"> |
| | | 权重值越大,优先级越高 |
| | | </div> |
| | | </div> |
| | | </NFormItem> |
| | | </NForm> |
| | | </div> |
| | | <template #action> |
| | | <NSpace justify="end"> |
| | | <NButton @click="closeModal"> |
| | | 取消 |
| | | </NButton> |
| | | <NButton type="primary" @click="handleConfirm"> |
| | | 确认 |
| | | </NButton> |
| | | </NSpace> |
| | | </template> |
| | | </NModal> |
| | | </template> |
| New file |
| | |
| | | <script setup lang="tsx"> |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { useBoolean } from '@/hooks' |
| | | import { computed, ref, watch } from 'vue' |
| | | import { NButton, NCard, NDataTable, NInput, NModal, NSelect, NTag } from 'naive-ui' |
| | | import Pagination from '@/components/common/Pagination.vue' |
| | | |
| | | interface UserItem { |
| | | id: number |
| | | userName: string |
| | | account: string |
| | | status: number | string |
| | | priorityDispatch: boolean |
| | | creator: string |
| | | createTime: string |
| | | priority?: number |
| | | desc?: string |
| | | [key: string]: any |
| | | } |
| | | |
| | | interface Props { |
| | | existingUsers?: UserItem[] |
| | | } |
| | | |
| | | const props = withDefaults(defineProps<Props>(), { |
| | | existingUsers: () => [], |
| | | }) |
| | | |
| | | const emit = defineEmits<{ |
| | | confirm: [selectedUsers: UserItem[]] |
| | | }>() |
| | | const visible = defineModel<boolean>('visible', { default: false }) |
| | | const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false) |
| | | |
| | | // 搜索条件 |
| | | const searchModel = ref({ |
| | | userName: '', |
| | | account: '', |
| | | status: null as number | string | null, |
| | | }) |
| | | |
| | | const statusOptions = [ |
| | | { label: '全部', value: '' }, |
| | | { label: '正常', value: 1 }, |
| | | { label: '冻结', value: 0 }, |
| | | ] |
| | | |
| | | // 选中的行 |
| | | const checkedRowKeys = ref<number[]>([]) |
| | | |
| | | const listData = ref<UserItem[]>([]) |
| | | |
| | | // 缓存所有加载过的数据 |
| | | const allLoadedData = ref<Map<number, UserItem>>(new Map()) |
| | | |
| | | const count = ref(0) |
| | | const currentPage = ref(1) |
| | | const pageSize = ref(10) |
| | | |
| | | // 表格列定义 |
| | | const columns = computed<DataTableColumns<UserItem>>(() => [ |
| | | { |
| | | type: 'selection', |
| | | width: 50, |
| | | }, |
| | | { |
| | | title: '序号', |
| | | key: 'index', |
| | | width: 80, |
| | | render: (row, index) => <span>{(currentPage.value - 1) * pageSize.value + index + 1}</span>, |
| | | }, |
| | | { |
| | | title: '用户名称', |
| | | key: 'userName', |
| | | ellipsis: true, |
| | | }, |
| | | { |
| | | title: '白名单账号', |
| | | key: 'account', |
| | | ellipsis: true, |
| | | }, |
| | | { |
| | | title: '状态', |
| | | key: 'status', |
| | | render: (row) => { |
| | | const isNormal = row.status === 1 |
| | | return ( |
| | | <NTag |
| | | round |
| | | bordered={false} |
| | | style={{ |
| | | borderRadius: '4px', |
| | | color: isNormal ? '#0068FF' : '#FF8C00', |
| | | backgroundColor: isNormal ? 'rgba(0, 104, 255, 0.1)' : 'rgba(255, 140, 0, 0.1)', |
| | | padding: '0 8px', |
| | | }} |
| | | > |
| | | {isNormal ? '正常' : '冻结'} |
| | | </NTag> |
| | | ) |
| | | }, |
| | | }, |
| | | { |
| | | title: '优先派单', |
| | | key: 'priorityDispatch', |
| | | render: row => <span>{row.priorityDispatch ? '是' : '否'}</span>, |
| | | }, |
| | | { |
| | | title: '创建人', |
| | | key: 'creator', |
| | | ellipsis: true, |
| | | }, |
| | | { |
| | | title: '创建时间', |
| | | key: 'createTime', |
| | | ellipsis: true, |
| | | }, |
| | | ]) |
| | | |
| | | // 获取用户列表(纯Mock) |
| | | async function getUserList() { |
| | | startLoading() |
| | | try { |
| | | // 模拟网络延迟 |
| | | await new Promise(resolve => setTimeout(resolve, 500)) |
| | | |
| | | // 生成Mock数据,总共假设1000条 |
| | | const totalItems = 1000 |
| | | const startIdx = (currentPage.value - 1) * pageSize.value |
| | | const currentList = [] |
| | | |
| | | const mockNames = ['杨佩芳', '曹敏侍', '郭贯妃', '丁汉臻', '朱政廷', '方一强', '李雅惠', '倪怡芳', '谢彦文'] |
| | | |
| | | for (let i = 0; i < pageSize.value; i++) { |
| | | const idx = startIdx + i + 1 |
| | | if (idx > totalItems) |
| | | break |
| | | |
| | | const isNormal = idx % 2 !== 0 // 奇数正常,偶数冻结 |
| | | |
| | | currentList.push({ |
| | | id: idx, |
| | | userName: searchModel.value.userName || mockNames[i % mockNames.length], |
| | | account: searchModel.value.account || `13${Math.floor(Math.random() * 1000000000).toString().padStart(9, '0')}`, |
| | | status: Number(searchModel.value.status !== null && searchModel.value.status !== '' ? searchModel.value.status : (isNormal ? 1 : 0)), |
| | | priorityDispatch: idx % 3 !== 0, |
| | | creator: searchModel.value.userName || mockNames[i % mockNames.length], |
| | | createTime: '2021/07/29 10:59', |
| | | }) |
| | | } |
| | | |
| | | listData.value = currentList |
| | | count.value = totalItems |
| | | |
| | | // 存入缓存 |
| | | listData.value.forEach((item) => { |
| | | allLoadedData.value.set(item.id, item) |
| | | }) |
| | | } |
| | | catch (error) { |
| | | console.error('获取用户列表失败', error) |
| | | } |
| | | finally { |
| | | endLoading() |
| | | } |
| | | } |
| | | |
| | | // 搜索 |
| | | function handleSearch() { |
| | | currentPage.value = 1 |
| | | getUserList() |
| | | } |
| | | |
| | | function resetSearchModel() { |
| | | searchModel.value = { |
| | | userName: '', |
| | | account: '', |
| | | status: null, |
| | | } |
| | | } |
| | | |
| | | // 重置 |
| | | function handleReset() { |
| | | resetSearchModel() |
| | | currentPage.value = 1 |
| | | pageSize.value = 10 |
| | | getUserList() |
| | | } |
| | | |
| | | // 取消 |
| | | function handleCancel() { |
| | | visible.value = false |
| | | } |
| | | |
| | | // 确认 |
| | | function handleConfirm() { |
| | | if (checkedRowKeys.value.length === 0) { |
| | | window.$message?.warning('请至少选择一个用户') |
| | | return |
| | | } |
| | | |
| | | // 从缓存中获取选中的完整对象 |
| | | const selectedUsers: UserItem[] = [] |
| | | checkedRowKeys.value.forEach((id) => { |
| | | const item = allLoadedData.value.get(id) |
| | | if (item) { |
| | | selectedUsers.push(item) |
| | | } |
| | | }) |
| | | |
| | | emit('confirm', selectedUsers) |
| | | visible.value = false |
| | | } |
| | | |
| | | // 分页改变 |
| | | function changePage(page: number, size: number) { |
| | | const isPageSizeChanged = size !== pageSize.value |
| | | pageSize.value = size |
| | | currentPage.value = isPageSizeChanged ? 1 : page |
| | | getUserList() |
| | | } |
| | | |
| | | // 监听弹窗打开 |
| | | watch(visible, (val) => { |
| | | if (val) { |
| | | resetSearchModel() |
| | | currentPage.value = 1 |
| | | pageSize.value = 10 |
| | | checkedRowKeys.value = props.existingUsers.map(v => v.id) |
| | | // 清空并初始化 allLoadedData |
| | | allLoadedData.value.clear() |
| | | props.existingUsers.forEach((v) => { |
| | | allLoadedData.value.set(v.id, v) |
| | | }) |
| | | |
| | | getUserList() |
| | | } |
| | | else { |
| | | // 关闭时清空 |
| | | checkedRowKeys.value = [] |
| | | allLoadedData.value.clear() |
| | | } |
| | | }) |
| | | </script> |
| | | |
| | | <template> |
| | | <NModal v-model:show="visible"> |
| | | <NCard |
| | | style=" |
| | | width: 1000px; |
| | | height: 80vh; |
| | | max-height: 800px; |
| | | border-radius: 8px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | " |
| | | :bordered="false" |
| | | :segmented="{ content: true }" |
| | | content-style="padding: 16px 24px; flex: 1; display: flex; flex-direction: column; overflow: hidden;" |
| | | footer-style="padding: 16px 24px; border-top: 1px solid rgba(46, 61, 77, 0.1); flex-shrink: 0;" |
| | | > |
| | | <template #header> |
| | | <span style="font-size: 16px; font-weight: bold; color: #1F1F1F;">选择用户</span> |
| | | </template> |
| | | <template #header-extra> |
| | | <div text class="cursor-pointer" @click="handleCancel"> |
| | | <span style="font-size: 18px; width: 10px; height: 10px; color: #999;">×</span> |
| | | </div> |
| | | </template> |
| | | |
| | | <div class="flex flex-col h-full overflow-hidden"> |
| | | <!-- 搜索区域 --> |
| | | <div class="mb-3 flex items-end gap-24px" style="flex-shrink: 0; flex-wrap: wrap;"> |
| | | <div class="w-300px"> |
| | | <div class="mb-2 text-sm text-[#1F1F1F]"> |
| | | 用户名称 |
| | | </div> |
| | | <NInput |
| | | v-model:value="searchModel.userName" |
| | | placeholder="请输入" |
| | | clearable |
| | | /> |
| | | </div> |
| | | <div class="w-300px"> |
| | | <div class="mb-2 text-sm text-[#1F1F1F]"> |
| | | 白名单账号 |
| | | </div> |
| | | <NInput |
| | | v-model:value="searchModel.account" |
| | | placeholder="请输入" |
| | | clearable |
| | | /> |
| | | </div> |
| | | <div class="w-300px"> |
| | | <div class="mb-2 text-sm text-[#1F1F1F]"> |
| | | 状态 |
| | | </div> |
| | | <NSelect |
| | | v-model:value="searchModel.status" |
| | | :options="statusOptions" |
| | | placeholder="请搜索" |
| | | clearable |
| | | /> |
| | | </div> |
| | | <div class="flex gap-8px ml-auto"> |
| | | <NButton style="width: 84px; height: 32px" @click="handleReset"> |
| | | 重置 |
| | | </NButton> |
| | | <NButton |
| | | type="primary" |
| | | style="width: 84px; height: 32px" |
| | | @click="handleSearch" |
| | | > |
| | | 查询 |
| | | </NButton> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 表格区域 --> |
| | | <div class="flex-1 overflow-hidden" style="min-height: 0"> |
| | | <NDataTable |
| | | v-model:checked-row-keys="checkedRowKeys" |
| | | style="height: 100%" |
| | | flex-height |
| | | :columns="columns" |
| | | :data="listData" |
| | | :loading="loading" |
| | | :bordered="false" |
| | | :row-key="(row: UserItem) => row.id" |
| | | :pagination="false" |
| | | > |
| | | <template #empty> |
| | | <div class="flex-center"> |
| | | <img |
| | | src="/src/assets/element-img/no-data.png" |
| | | style="width: 123px; height: 125px" |
| | | > |
| | | </div> |
| | | </template> |
| | | </NDataTable> |
| | | </div> |
| | | <Pagination |
| | | :count="count" |
| | | :page="currentPage" |
| | | :page-size="pageSize" |
| | | style="margin-top: 12px; flex-shrink: 0" |
| | | @change="changePage" |
| | | /> |
| | | </div> |
| | | |
| | | <template #footer> |
| | | <div class="flex justify-end gap-3"> |
| | | <NButton class="footer-btn" @click="handleCancel"> |
| | | 取消 |
| | | </NButton> |
| | | <NButton type="primary" class="footer-btn" @click="handleConfirm"> |
| | | 确认 |
| | | </NButton> |
| | | </div> |
| | | </template> |
| | | </NCard> |
| | | </NModal> |
| | | </template> |
| | | |
| | | <style scoped lang="scss"> |
| | | .mb-4 { |
| | | margin-bottom: 1rem; |
| | | } |
| | | .flex { |
| | | display: flex; |
| | | } |
| | | .items-end { |
| | | align-items: flex-end; |
| | | } |
| | | .gap-4 { |
| | | gap: 1rem; |
| | | } |
| | | .gap-2 { |
| | | gap: 0.5rem; |
| | | } |
| | | .gap-3 { |
| | | gap: 0.75rem; |
| | | } |
| | | .flex-1 { |
| | | flex: 1 1 0%; |
| | | } |
| | | .justify-end { |
| | | justify-content: flex-end; |
| | | } |
| | | .mb-2 { |
| | | margin-bottom: 0.5rem; |
| | | } |
| | | .text-sm { |
| | | font-size: 0.875rem; |
| | | line-height: 1.25rem; |
| | | } |
| | | |
| | | .footer-btn { |
| | | padding: 7px 29px; |
| | | height: auto; |
| | | font-size: 13px; |
| | | line-height: 18px; |
| | | } |
| | | </style> |
| New file |
| | |
| | | <script setup lang="tsx"> |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { useBoolean } from '@/hooks' |
| | | import { computed, ref, watch } from 'vue' |
| | | import { NButton, NCard, NDataTable, NInput, NModal } from 'naive-ui' |
| | | import Pagination from '@/components/common/Pagination.vue' |
| | | |
| | | interface VehicleItem { |
| | | id: number |
| | | vehicleCode: string |
| | | plateNumber: string |
| | | vehicleModel: string |
| | | procurementSource?: string |
| | | fleetName: string |
| | | [key: string]: any |
| | | } |
| | | |
| | | interface Props { |
| | | existingVehicles?: VehicleItem[] |
| | | } |
| | | |
| | | const props = withDefaults(defineProps<Props>(), { |
| | | existingVehicles: () => [], |
| | | }) |
| | | |
| | | const emit = defineEmits<{ |
| | | confirm: [selectedVehicles: VehicleItem[]] |
| | | }>() |
| | | const visible = defineModel<boolean>('visible', { default: false }) |
| | | const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false) |
| | | |
| | | // 搜索条件 |
| | | const searchModel = ref({ |
| | | vehicleCode: '', |
| | | plateNumber: '', |
| | | fleetName: '', |
| | | }) |
| | | |
| | | // 选中的行 |
| | | const checkedRowKeys = ref<number[]>([]) |
| | | |
| | | const listData = ref<VehicleItem[]>([]) |
| | | |
| | | // 缓存所有加载过的数据,以便在 handleConfirm 时能找到不在当前页但在 checkedRowKeys 中的项 |
| | | const allLoadedData = ref<Map<number, VehicleItem>>(new Map()) |
| | | |
| | | const count = ref(0) |
| | | const currentPage = ref(1) |
| | | const pageSize = ref(10) |
| | | |
| | | // 表格列定义 |
| | | const columns = computed<DataTableColumns<VehicleItem>>(() => [ |
| | | { |
| | | type: 'selection', |
| | | width: 50, |
| | | }, |
| | | { |
| | | title: '序号', |
| | | align: 'center', |
| | | key: 'index', |
| | | width: 80, |
| | | render: (row, index) => <span>{(currentPage.value - 1) * pageSize.value + index + 1}</span>, |
| | | resizable: true, |
| | | }, |
| | | { |
| | | title: '车辆编号', |
| | | align: 'center', |
| | | key: 'vehicleCode', |
| | | ellipsis: true, |
| | | resizable: true, |
| | | }, |
| | | { |
| | | title: '车牌号', |
| | | align: 'center', |
| | | key: 'plateNumber', |
| | | ellipsis: true, |
| | | resizable: true, |
| | | }, |
| | | { |
| | | title: '型号', |
| | | align: 'center', |
| | | key: 'vehicleModel', |
| | | ellipsis: true, |
| | | resizable: true, |
| | | }, |
| | | { |
| | | title: '采购来源', |
| | | align: 'center', |
| | | key: 'procurementSource', |
| | | ellipsis: true, |
| | | resizable: true, |
| | | }, |
| | | { |
| | | title: '当前车队', |
| | | align: 'center', |
| | | key: 'fleetName', |
| | | ellipsis: true, |
| | | render: row => <span>{row.fleetName || '-'}</span>, |
| | | }, |
| | | ]) |
| | | |
| | | // 获取车辆列表(纯Mock) |
| | | async function getVehicleList() { |
| | | startLoading() |
| | | try { |
| | | // 模拟网络延迟 |
| | | await new Promise(resolve => setTimeout(resolve, 500)) |
| | | |
| | | // 生成Mock数据,总共假设50条 |
| | | const totalItems = 5 |
| | | const startIdx = (currentPage.value - 1) * pageSize.value |
| | | const currentList = [] |
| | | |
| | | for (let i = 0; i < pageSize.value; i++) { |
| | | const idx = startIdx + i + 1 |
| | | if (idx > totalItems) |
| | | break |
| | | |
| | | currentList.push({ |
| | | id: idx, |
| | | vehicleCode: '073 D 204 77', |
| | | plateNumber: '粤PXV7029', |
| | | vehicleModel: 'Robotaxi', |
| | | procurementSource: '自产', |
| | | fleetName: '王美丽的车队', |
| | | }) |
| | | } |
| | | |
| | | listData.value = currentList |
| | | count.value = totalItems |
| | | |
| | | // 存入缓存 |
| | | listData.value.forEach((item) => { |
| | | allLoadedData.value.set(item.id, item) |
| | | }) |
| | | } |
| | | catch (error) { |
| | | console.error('获取车辆列表失败', error) |
| | | } |
| | | finally { |
| | | endLoading() |
| | | } |
| | | } |
| | | |
| | | // 搜索 |
| | | function handleSearch() { |
| | | currentPage.value = 1 |
| | | getVehicleList() |
| | | } |
| | | |
| | | function resetSearchModel() { |
| | | searchModel.value = { |
| | | vehicleCode: '', |
| | | plateNumber: '', |
| | | fleetName: '', |
| | | } |
| | | } |
| | | |
| | | // 重置 |
| | | function handleReset() { |
| | | resetSearchModel() |
| | | currentPage.value = 1 |
| | | pageSize.value = 10 |
| | | getVehicleList() |
| | | } |
| | | |
| | | // 取消 |
| | | function handleCancel() { |
| | | visible.value = false |
| | | } |
| | | |
| | | // 确认 |
| | | function handleConfirm() { |
| | | if (checkedRowKeys.value.length === 0) { |
| | | window.$message?.warning('请至少选择一辆车辆') |
| | | return |
| | | } |
| | | |
| | | // 从缓存中获取选中的完整对象 |
| | | const selectedVehicles: VehicleItem[] = [] |
| | | checkedRowKeys.value.forEach((id) => { |
| | | const item = allLoadedData.value.get(id) |
| | | if (item) { |
| | | selectedVehicles.push(item) |
| | | } |
| | | }) |
| | | |
| | | emit('confirm', selectedVehicles) |
| | | visible.value = false |
| | | } |
| | | |
| | | // 分页改变 |
| | | function changePage(page: number, size: number) { |
| | | const isPageSizeChanged = size !== pageSize.value |
| | | pageSize.value = size |
| | | currentPage.value = isPageSizeChanged ? 1 : page |
| | | getVehicleList() |
| | | } |
| | | |
| | | // 监听弹窗打开 |
| | | watch(visible, (val) => { |
| | | if (val) { |
| | | resetSearchModel() |
| | | currentPage.value = 1 |
| | | pageSize.value = 10 |
| | | checkedRowKeys.value = props.existingVehicles.map(v => v.id) |
| | | // 清空并初始化 allLoadedData |
| | | allLoadedData.value.clear() |
| | | props.existingVehicles.forEach((v) => { |
| | | allLoadedData.value.set(v.id, v) |
| | | }) |
| | | |
| | | getVehicleList() |
| | | } |
| | | else { |
| | | // 关闭时清空 |
| | | checkedRowKeys.value = [] |
| | | allLoadedData.value.clear() |
| | | } |
| | | }) |
| | | </script> |
| | | |
| | | <template> |
| | | <NModal v-model:show="visible"> |
| | | <NCard |
| | | style=" |
| | | width: 1000px; |
| | | height: 80vh; |
| | | max-height: 800px; |
| | | border-radius: 8px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | " |
| | | :bordered="false" |
| | | :segmented="{ content: true }" |
| | | content-style="padding: 16px 24px; flex: 1; display: flex; flex-direction: column; overflow: hidden;" |
| | | footer-style="padding: 16px 24px; border-top: 1px solid rgba(46, 61, 77, 0.1); flex-shrink: 0;" |
| | | > |
| | | <template #header> |
| | | <span style="font-size: 16px; font-weight: bold">添加车辆</span> |
| | | </template> |
| | | <template #header-extra> |
| | | <div text class="cursor-pointer" @click="handleCancel"> |
| | | <span style="font-size: 18px; width: 10px; height: 10px">×</span> |
| | | </div> |
| | | </template> |
| | | |
| | | <div class="flex flex-col h-full overflow-hidden"> |
| | | <!-- 搜索区域 --> |
| | | <div class="mb-3 flex items-end gap-24px" style="flex-shrink: 0"> |
| | | <div class="w-300px"> |
| | | <div class="mb-2 text-sm text-[#1F1F1F]"> |
| | | 车牌号 |
| | | </div> |
| | | <NInput |
| | | v-model:value="searchModel.plateNumber" |
| | | placeholder="请输入" |
| | | clearable |
| | | /> |
| | | </div> |
| | | <div class="w-300px"> |
| | | <div class="mb-2 text-sm text-[#1F1F1F]"> |
| | | 车队 |
| | | </div> |
| | | <NInput |
| | | v-model:value="searchModel.fleetName" |
| | | placeholder="请输入" |
| | | clearable |
| | | /> |
| | | </div> |
| | | <div class="flex ml-auto gap-8px"> |
| | | <NButton style="width: 84px; height: 32px" @click="handleReset"> |
| | | 重置 |
| | | </NButton> |
| | | <NButton |
| | | type="primary" |
| | | style="width: 84px; height: 32px" |
| | | @click="handleSearch" |
| | | > |
| | | 查询 |
| | | </NButton> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 表格区域 --> |
| | | <div class="flex-1 overflow-hidden" style="min-height: 0"> |
| | | <NDataTable |
| | | v-model:checked-row-keys="checkedRowKeys" |
| | | style="height: 100%" |
| | | flex-height |
| | | :columns="columns" |
| | | :data="listData" |
| | | :loading="loading" |
| | | :bordered="false" |
| | | :row-key="(row: VehicleItem) => row.id" |
| | | :pagination="false" |
| | | > |
| | | <template #empty> |
| | | <div class="flex-center"> |
| | | <img |
| | | src="/src/assets/element-img/no-data.png" |
| | | style="width: 123px; height: 125px" |
| | | > |
| | | </div> |
| | | </template> |
| | | </NDataTable> |
| | | </div> |
| | | <Pagination |
| | | :count="count" |
| | | :page="currentPage" |
| | | :page-size="pageSize" |
| | | style="margin-top: 12px; flex-shrink: 0" |
| | | @change="changePage" |
| | | /> |
| | | </div> |
| | | |
| | | <template #footer> |
| | | <div class="flex justify-end gap-3"> |
| | | <NButton class="footer-btn" @click="handleCancel"> |
| | | 取消 |
| | | </NButton> |
| | | <NButton type="primary" class="footer-btn" @click="handleConfirm"> |
| | | 保存 |
| | | </NButton> |
| | | </div> |
| | | </template> |
| | | </NCard> |
| | | </NModal> |
| | | </template> |
| | | |
| | | <style scoped lang="scss"> |
| | | .mb-4 { |
| | | margin-bottom: 1rem; |
| | | } |
| | | .flex { |
| | | display: flex; |
| | | } |
| | | .items-end { |
| | | align-items: flex-end; |
| | | } |
| | | .gap-4 { |
| | | gap: 1rem; |
| | | } |
| | | .gap-2 { |
| | | gap: 0.5rem; |
| | | } |
| | | .gap-3 { |
| | | gap: 0.75rem; |
| | | } |
| | | .flex-1 { |
| | | flex: 1 1 0%; |
| | | } |
| | | .justify-end { |
| | | justify-content: flex-end; |
| | | } |
| | | .mb-2 { |
| | | margin-bottom: 0.5rem; |
| | | } |
| | | .text-sm { |
| | | font-size: 0.875rem; |
| | | line-height: 1.25rem; |
| | | } |
| | | |
| | | .footer-btn { |
| | | padding: 7px 29px; |
| | | height: auto; |
| | | font-size: 13px; |
| | | line-height: 18px; |
| | | } |
| | | </style> |
| New file |
| | |
| | | <script setup lang="tsx"> |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { computed, onMounted, ref } from 'vue' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { NButton, NCascader, NCheckbox, NDataTable, NDatePicker, NForm, NFormItem, NInput, NRadio, NRadioGroup, NSpace, NSpin, NTag } from 'naive-ui' |
| | | import { getRegionCascaderNoArea } from '@/service/api/operations' |
| | | |
| | | const router = useRouter() |
| | | const route = useRoute() |
| | | |
| | | const id = ref<string | null>(null) |
| | | const loading = ref(false) |
| | | |
| | | const formModel = ref({ |
| | | cityCode: '110000', // Mock data |
| | | isPermanent: true, |
| | | startTime: 1770454122000, |
| | | endTime: 1770454136000, |
| | | status: 1, // 1 启用, 0 禁用 |
| | | remark: '备注内容备注内容', |
| | | }) |
| | | |
| | | const pcaOptions = ref<any[]>([]) |
| | | |
| | | function formatCascaderData(list: any[]) { |
| | | return list.map((item) => { |
| | | const newItem = { ...item } |
| | | if (newItem.children && newItem.children.length > 0) { |
| | | newItem.children = formatCascaderData(newItem.children) |
| | | } |
| | | else { |
| | | delete newItem.children |
| | | } |
| | | return newItem |
| | | }) |
| | | } |
| | | |
| | | async function fetchCascaderData() { |
| | | const res = await getRegionCascaderNoArea() |
| | | if (res.code === 200 && res.data) { |
| | | pcaOptions.value = formatCascaderData(res.data) |
| | | } |
| | | } |
| | | |
| | | async function fetchDetail() { |
| | | if (!id.value) |
| | | return |
| | | loading.value = true |
| | | try { |
| | | // Mock data fetch |
| | | setTimeout(() => { |
| | | loading.value = false |
| | | }, 500) |
| | | } |
| | | finally { |
| | | // loading.value = false |
| | | } |
| | | } |
| | | |
| | | // 关联车辆 Mock 数据和表格配置 |
| | | const selectedVehicles = ref<any[]>([ |
| | | { |
| | | id: 1, |
| | | vehicleCode: 'S156165161651', |
| | | plateNo: '粤PXV7029', |
| | | model: 'Robotaxi', |
| | | fleet: '白云区1号', |
| | | cruisePlan: '广州巡游计划1111', |
| | | }, |
| | | { |
| | | id: 2, |
| | | vehicleCode: 'S156165161651', |
| | | plateNo: '粤PXV7029', |
| | | model: 'Robotaxi', |
| | | fleet: '白云区1号', |
| | | cruisePlan: '-', |
| | | }, |
| | | ]) |
| | | |
| | | const vehicleColumns = computed<DataTableColumns<any>>(() => [ |
| | | { title: '序号', key: 'index', width: 60, render: (_, index) => index + 1 }, |
| | | { title: '车辆编码', key: 'vehicleCode' }, |
| | | { title: '车牌号', key: 'plateNo' }, |
| | | { title: '型号', key: 'model' }, |
| | | { title: '所属车队', key: 'fleet' }, |
| | | { title: '关联巡游计划', key: 'cruisePlan' }, |
| | | ]) |
| | | |
| | | // 服务用户 Mock 数据和表格配置 |
| | | const selectedUsers = ref<any[]>([ |
| | | { id: 1, userName: '方一强', account: '13034179070', status: 1, priority: 100, desc: '优先级说明内容说明内容说明内容' }, |
| | | { id: 2, userName: '李雅惠', account: '13619910302', status: 0, priority: 80, desc: '优先级说明内容说明内容说明内容' }, |
| | | { id: 3, userName: '倪怡芳', account: '15392830508', status: 1, priority: 50, desc: '优先级说明内容说明内容说明内容' }, |
| | | { id: 4, userName: '-', account: '13818629999', status: 0, priority: 50, desc: '优先级说明内容说明内容说明内容' }, |
| | | { id: 5, userName: '-', account: '13654373011', status: 1, priority: 50, desc: '优先级说明内容说明内容说明内容' }, |
| | | { id: 6, userName: '谢彦文', account: '13396827528', status: 1, priority: 50, desc: '优先级说明内容说明内容说明内容' }, |
| | | { id: 7, userName: '-', account: '18878751881', status: 1, priority: 50, desc: '优先级说明内容说明内容说明内容' }, |
| | | ]) |
| | | |
| | | const userColumns = computed<DataTableColumns<any>>(() => [ |
| | | { title: '序号', key: 'index', width: 60, render: (_, index) => index + 1 }, |
| | | { title: '用户名称', key: 'userName' }, |
| | | { title: '白名单账号', key: 'account' }, |
| | | { |
| | | title: '状态', |
| | | key: 'status', |
| | | render: (row) => { |
| | | const isNormal = row.status === 1 |
| | | return ( |
| | | <NTag |
| | | round |
| | | bordered={false} |
| | | style={{ |
| | | borderRadius: '4px', |
| | | color: isNormal ? '#0068FF' : '#FF8C00', |
| | | backgroundColor: isNormal ? 'rgba(0, 104, 255, 0.1)' : 'rgba(255, 140, 0, 0.1)', |
| | | padding: '0 8px', |
| | | }} |
| | | > |
| | | {isNormal ? '正常' : '冻结'} |
| | | </NTag> |
| | | ) |
| | | }, |
| | | }, |
| | | { title: '用户优先级', key: 'priority' }, |
| | | { title: '优先级说明', key: 'desc' }, |
| | | ]) |
| | | |
| | | function handleEdit() { |
| | | router.push({ |
| | | name: 'directServiceManagementEdit', |
| | | query: { id: id.value }, |
| | | }) |
| | | } |
| | | |
| | | onMounted(() => { |
| | | fetchCascaderData() |
| | | if (route.query.id) { |
| | | id.value = route.query.id as string |
| | | fetchDetail() |
| | | } |
| | | }) |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="w-full h-full relative content flex flex-col"> |
| | | <div class="p-12px main flex flex-col flex-1 overflow-y-auto"> |
| | | <div class="box mb-12px shrink-0"> |
| | | <div class="text-xl"> |
| | | 定向服务详情 |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 基础信息卡片 --> |
| | | <div class="box shrink-0 mb-12px"> |
| | | <div class="head shrink-0"> |
| | | 基础信息 |
| | | </div> |
| | | |
| | | <NSpin :show="loading"> |
| | | <NForm :model="formModel" inline label-placement="left" class="custom-form" disabled> |
| | | <NGrid :cols="24" :x-gap="24" :y-gap="8"> |
| | | <n-form-item-gi :span="6" label="城市" path="cityCode" required> |
| | | <NCascader |
| | | v-model:value="formModel.cityCode" :options="pcaOptions" value-field="code" |
| | | label-field="name" check-strategy="child" clearable placeholder="请选择" |
| | | > |
| | | <template #arrow> |
| | | <img |
| | | src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e120f81c-9f15-4e1a-9938-22252898edf9.png" |
| | | class="arrow-icon" style="width: 16px; height: 16px" alt="arrow" |
| | | > |
| | | </template> |
| | | </NCascader> |
| | | </n-form-item-gi> |
| | | |
| | | <n-form-item-gi :span="12" label="生效时间段" path="startTime" required> |
| | | <div class="flex items-center gap-2"> |
| | | <NDatePicker |
| | | v-model:value="formModel.startTime" type="datetime" clearable |
| | | placeholder="请选择" style="width: 200px" |
| | | /> |
| | | <span class="text-gray-400">至</span> |
| | | <NDatePicker |
| | | v-model:value="formModel.endTime" type="datetime" clearable |
| | | placeholder="请选择" style="width: 200px" |
| | | /> |
| | | <NCheckbox v-model:checked="formModel.isPermanent" class="shrink-0 ml-2"> |
| | | 长期有效 |
| | | </NCheckbox> |
| | | </div> |
| | | </n-form-item-gi> |
| | | |
| | | <n-form-item-gi :span="4" label="状态" path="status" required> |
| | | <NRadioGroup v-model:value="formModel.status"> |
| | | <NSpace> |
| | | <NRadio :value="1"> |
| | | 启用 |
| | | </NRadio> |
| | | <NRadio :value="0"> |
| | | 禁用 |
| | | </NRadio> |
| | | </NSpace> |
| | | </NRadioGroup> |
| | | </n-form-item-gi> |
| | | <n-form-item-gi :span="24" label="备注" path="remark" label-placement="top" class="w-full"> |
| | | <NInput |
| | | v-model:value="formModel.remark" type="textarea" placeholder="请输入备注内容" show-count :maxlength="200" |
| | | class="bg-gray-50/50 w-full" |
| | | /> |
| | | </n-form-item-gi> |
| | | </NGrid> |
| | | </NForm> |
| | | </NSpin> |
| | | </div> |
| | | |
| | | <!-- 关联车辆卡片 --> |
| | | <div class="box shrink-0 mb-12px"> |
| | | <div class="head shrink-0"> |
| | | 关联车辆 |
| | | </div> |
| | | <NDataTable |
| | | :columns="vehicleColumns" |
| | | :data="selectedVehicles" |
| | | :bordered="false" |
| | | :single-line="false" |
| | | /> |
| | | </div> |
| | | |
| | | <!-- 服务用户卡片 --> |
| | | <div class="box shrink-0"> |
| | | <div class="head shrink-0"> |
| | | 服务用户 |
| | | </div> |
| | | <NDataTable |
| | | :columns="userColumns" |
| | | :data="selectedUsers" |
| | | :bordered="false" |
| | | :single-line="false" |
| | | /> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 底部操作栏 --> |
| | | <div class="footer flex justify-end items-center shrink-0"> |
| | | <NSpace> |
| | | <NButton type="primary" class="w-84px" @click="handleEdit"> |
| | | 编辑 |
| | | </NButton> |
| | | </NSpace> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped lang="scss"> |
| | | .box { |
| | | padding: 24px; |
| | | background-color: #fff; |
| | | border-radius: 6px; |
| | | width: 100%; |
| | | box-sizing: border-box; |
| | | --n-label-text-color: rgba(23, 31, 38, 0.8); |
| | | } |
| | | |
| | | .head { |
| | | font-family: PingFang SC; |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | line-height: 24px; |
| | | letter-spacing: normal; |
| | | color: #171f26; |
| | | position: relative; |
| | | text-indent: 11px; |
| | | margin-bottom: 16px; |
| | | |
| | | &::before { |
| | | content: ""; |
| | | position: absolute; |
| | | top: 50%; |
| | | left: 0; |
| | | transform: translateY(-50%); |
| | | width: 3px; |
| | | height: 14px; |
| | | z-index: 0; |
| | | border-radius: 3px; |
| | | background: #0068ff; |
| | | } |
| | | } |
| | | |
| | | .footer { |
| | | position: sticky; |
| | | bottom: 0; |
| | | left: 0; |
| | | width: 100%; |
| | | height: 64px; |
| | | background-color: #fff; |
| | | box-shadow: 0px 0px 5px 0px rgba(23, 31, 38, 0.05); |
| | | padding: 16px 24px; |
| | | } |
| | | </style> |
| New file |
| | |
| | | <script setup lang="tsx"> |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { computed, onMounted, ref } from 'vue' |
| | | import { useBoolean } from '@/hooks' |
| | | import { deleteDirectService, getDirectServiceList, toggleDirectServiceStatus } from '@/service/api/directService' |
| | | import { getRegionCascaderNoArea } from '@/service/api/operations' |
| | | import { NButton, NSwitch, NTag, useDialog, useMessage } from 'naive-ui' |
| | | import SearchPanel from '@/components/custom/SearchPanel.vue' |
| | | import dayjs from 'dayjs' |
| | | import { useRouter } from 'vue-router' |
| | | import Pagination from '@/components/common/Pagination.vue' |
| | | |
| | | const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false) |
| | | const message = useMessage() |
| | | const router = useRouter() |
| | | const dialog = useDialog() |
| | | |
| | | const initialModel = { |
| | | cityCode: null, |
| | | } |
| | | const model = ref({ ...initialModel }) |
| | | |
| | | const pageParams = ref({ |
| | | page: 1, |
| | | size: 10, |
| | | }) |
| | | |
| | | function handleResetSearch() { |
| | | model.value = { ...initialModel } |
| | | handleSearch() |
| | | } |
| | | |
| | | function handleSearch() { |
| | | pageParams.value.page = 1 |
| | | getList() |
| | | } |
| | | |
| | | const count = ref(0) |
| | | const listData = ref<any[]>([]) |
| | | |
| | | const isCityCodeIdOpen = ref(false) |
| | | const pcaOptions = ref<any[]>([]) |
| | | |
| | | // 获取城市数据 |
| | | function formatCascaderData(list: any[]) { |
| | | return list.map((item) => { |
| | | const newItem = { ...item } |
| | | if (newItem.children && newItem.children.length > 0) { |
| | | newItem.children = formatCascaderData(newItem.children) |
| | | } |
| | | else { |
| | | delete newItem.children |
| | | } |
| | | return newItem |
| | | }) |
| | | } |
| | | |
| | | async function fetchCascaderData() { |
| | | const res = await getRegionCascaderNoArea() |
| | | if (res.code === 200 && res.data) { |
| | | pcaOptions.value = formatCascaderData(res.data) |
| | | } |
| | | } |
| | | |
| | | // 搜索项 |
| | | const filterItems = computed(() => [ |
| | | { |
| | | label: '城市', |
| | | path: 'cityCode', |
| | | render: () => ( |
| | | <n-cascader |
| | | v-model:value={model.value.cityCode} |
| | | options={pcaOptions.value} |
| | | onUpdate:show={(val: any) => (isCityCodeIdOpen.value = val)} |
| | | value-field="code" |
| | | label-field="name" |
| | | check-strategy="child" |
| | | clearable |
| | | placeholder="请选择" |
| | | > |
| | | {{ |
| | | arrow: () => ( |
| | | <img |
| | | src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e120f81c-9f15-4e1a-9938-22252898edf9.png" |
| | | class={`arrow-icon ${isCityCodeIdOpen.value ? 'arrow-icon--open' : ''}`} |
| | | style={{ width: '16px', height: '16px' }} |
| | | alt="arrow" |
| | | /> |
| | | ), |
| | | }} |
| | | </n-cascader> |
| | | ), |
| | | }, |
| | | ]) |
| | | |
| | | // 获取列表数据 |
| | | async function getList() { |
| | | startLoading() |
| | | // Mock data simulation |
| | | setTimeout(() => { |
| | | const mockData = Array.from({ length: 10 }).map((_, index) => { |
| | | const isPermanent = index % 3 === 0 |
| | | const isEnabled = index % 4 !== 0 |
| | | |
| | | // City generation based on selection or random |
| | | let cityName = '成都市金牛区' |
| | | if (model.value.cityCode) { |
| | | // Simple mock to just show the selected code if it exists |
| | | cityName = `已选城市(${model.value.cityCode})` |
| | | } |
| | | else if (index > 7) { |
| | | cityName = '广州市' |
| | | } |
| | | |
| | | return { |
| | | id: `mock-id-${pageParams.value.page}-${index}`, |
| | | cityName, |
| | | isPermanent, |
| | | startTime: isPermanent ? null : '2026-03-20 09:00:00', |
| | | endTime: isPermanent ? null : '2026-03-20 16:00:00', |
| | | boundVehicleCount: 5, |
| | | boundUserCount: 5, |
| | | status: isEnabled ? 1 : 0, |
| | | remark: '备注内容备注内容备注内容备注内容', |
| | | addBy: 'admin', |
| | | addTime: '2026-03-20 16:42:23', |
| | | } |
| | | }) |
| | | |
| | | count.value = 1000 // Mock total count |
| | | listData.value = mockData |
| | | endLoading() |
| | | }, 500) |
| | | } |
| | | |
| | | // 分页变化 |
| | | function changePage(page: number, size: number) { |
| | | pageParams.value.page = page |
| | | pageParams.value.size = size |
| | | getList() |
| | | } |
| | | |
| | | // 表格列定义 |
| | | const columns = computed<DataTableColumns<any>>(() => [ |
| | | { |
| | | title: '序号', |
| | | key: 'index', |
| | | width: 60, |
| | | render: (_, index) => { |
| | | return (pageParams.value.page - 1) * pageParams.value.size + index + 1 |
| | | }, |
| | | }, |
| | | { |
| | | title: '城市', |
| | | key: 'cityName', |
| | | width: 120, |
| | | resizable: true, |
| | | }, |
| | | { |
| | | title: '生效时间段', |
| | | key: 'effectiveTime', |
| | | width: 245, |
| | | resizable: true, |
| | | render: (row) => { |
| | | if (row.isPermanent) |
| | | return '永久有效' |
| | | const start = row.startTime ? dayjs(row.startTime).format('YYYY/MM/DD HH:mm') : '' |
| | | const end = row.endTime ? dayjs(row.endTime).format('YYYY/MM/DD HH:mm') : '' |
| | | if (start && end) |
| | | return `${start} ~ ${end}` |
| | | return '-' |
| | | }, |
| | | }, |
| | | { |
| | | title: '绑定车辆数', |
| | | key: 'boundVehicleCount', |
| | | width: 100, |
| | | resizable: true, |
| | | }, |
| | | { |
| | | title: '绑定用户数', |
| | | key: 'boundUserCount', |
| | | width: 100, |
| | | resizable: true, |
| | | }, |
| | | { |
| | | title: '状态', |
| | | key: 'status', |
| | | width: 80, |
| | | resizable: true, |
| | | render: (row) => { |
| | | const isEnabled = row.status === 1 || row.status === '1' || row.status === true |
| | | const label = isEnabled ? '启用' : '禁用' |
| | | return ( |
| | | <NTag |
| | | round |
| | | bordered={false} |
| | | style={{ |
| | | borderRadius: '4px', |
| | | border: 'none', |
| | | color: isEnabled ? '#0BB52D' : '#666666', |
| | | backgroundColor: isEnabled ? '#DCF5DD' : 'rgba(23, 31, 38, 0.05)', |
| | | }} |
| | | > |
| | | {label} |
| | | </NTag> |
| | | ) |
| | | }, |
| | | }, |
| | | { |
| | | title: '备注', |
| | | key: 'remark', |
| | | width: 150, |
| | | ellipsis: { |
| | | tooltip: true, |
| | | }, |
| | | resizable: true, |
| | | }, |
| | | { |
| | | title: '添加时间', |
| | | key: 'addTime', |
| | | width: 160, |
| | | resizable: true, |
| | | render: row => row.addTime ? dayjs(row.addTime).format('YYYY/MM/DD HH:mm') : '', |
| | | }, |
| | | { |
| | | title: '操作', |
| | | key: 'actions', |
| | | width: 190, |
| | | fixed: 'right', |
| | | render: (row) => { |
| | | const isEnabled = row.status === 1 || row.status === '1' || row.status === true |
| | | return ( |
| | | <div class="flex items-center justify-start gap-2"> |
| | | <NButton |
| | | text |
| | | v-permission="60701" |
| | | type="primary" |
| | | onClick={() => handleDetail(row)} |
| | | > |
| | | 详情 |
| | | </NButton> |
| | | <NButton |
| | | text |
| | | v-permission="60702" |
| | | type="primary" |
| | | onClick={() => handleEdit(row)} |
| | | > |
| | | 编辑 |
| | | </NButton> |
| | | <NButton |
| | | text |
| | | v-permission="60703" |
| | | type="primary" |
| | | onClick={() => handleDelete(row)} |
| | | > |
| | | 删除 |
| | | </NButton> |
| | | <NSwitch |
| | | v-permission="60704" |
| | | value={isEnabled} |
| | | onUpdateValue={val => handleToggleStatus(row, val)} |
| | | > |
| | | {{ |
| | | checked: () => '启用', |
| | | unchecked: () => '禁用', |
| | | }} |
| | | </NSwitch> |
| | | </div> |
| | | ) |
| | | }, |
| | | }, |
| | | ]) |
| | | |
| | | // 详情 |
| | | function handleDetail(row: any) { |
| | | router.push({ |
| | | name: 'directServiceManagementDetail', |
| | | query: { id: row.id }, |
| | | }) |
| | | } |
| | | |
| | | // 新增/编辑 |
| | | function handleAdd() { |
| | | router.push({ name: 'directServiceManagementAdd' }) |
| | | } |
| | | |
| | | function handleEdit(row: any) { |
| | | router.push({ |
| | | name: 'directServiceManagementEdit', |
| | | query: { id: row.id }, |
| | | }) |
| | | } |
| | | |
| | | // 删除 |
| | | function handleDelete(row: any) { |
| | | dialog.create({ |
| | | title: '删除确认', |
| | | content: '确定要删除该定向服务吗?', |
| | | positiveText: '确定', |
| | | negativeText: '取消', |
| | | onPositiveClick: () => { |
| | | deleteDirectService(row.id).then((res) => { |
| | | if (res.code === 200) { |
| | | message.success('删除成功') |
| | | getList() |
| | | } |
| | | }) |
| | | }, |
| | | }) |
| | | } |
| | | |
| | | // 启用/禁用 |
| | | function handleToggleStatus(row: any, targetStatus: boolean) { |
| | | const statusValue = targetStatus ? 1 : 0 |
| | | const actionText = targetStatus ? '启用' : '禁用' |
| | | dialog.create({ |
| | | title: `${actionText}确认`, |
| | | content: `确定要${actionText}该定向服务吗?`, |
| | | positiveText: '确定', |
| | | negativeText: '取消', |
| | | onPositiveClick: () => { |
| | | toggleDirectServiceStatus(row.id, statusValue).then((res) => { |
| | | if (res.code === 200) { |
| | | message.success(`${actionText}成功`) |
| | | getList() |
| | | } |
| | | }) |
| | | }, |
| | | }) |
| | | } |
| | | |
| | | onMounted(() => { |
| | | fetchCascaderData() |
| | | getList() |
| | | }) |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="roboTaxi w-full h-full"> |
| | | <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" class="n-grid-item"> |
| | | <n-form-item :label="item.label" :path="item.path"> |
| | | <component :is="item.render" /> |
| | | </n-form-item> |
| | | </n-gi> |
| | | <template #action> |
| | | <NButton @click="handleResetSearch"> |
| | | 重置 |
| | | </NButton> |
| | | <NButton type="primary" @click="handleSearch"> |
| | | 查询 |
| | | </NButton> |
| | | </template> |
| | | </SearchPanel> |
| | | </n-form> |
| | | </div> |
| | | |
| | | <n-card class="flex-1 robotaxi-table-card"> |
| | | <template #header> |
| | | <n-flex justify="flex-end"> |
| | | <NButton |
| | | v-permission="60702" |
| | | secondary |
| | | type="primary" |
| | | @click="handleAdd" |
| | | > |
| | | 新增 |
| | | </NButton> |
| | | </n-flex> |
| | | </template> |
| | | <div class="flex-1 flex flex-col h-full"> |
| | | <n-data-table |
| | | class="flex-1" |
| | | :scroll-x="1180" |
| | | :columns="columns" |
| | | :data="listData" |
| | | :loading="loading" |
| | | flex-height |
| | | :bordered="false" |
| | | :header-height="40" |
| | | :row-height="50" |
| | | > |
| | | <template #empty> |
| | | <div class="flex-center" style="justify-content: 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="count" |
| | | :page="pageParams.page" |
| | | :page-size="pageParams.size" |
| | | class="robotaxi-pagination mt-12px" |
| | | @change="changePage" |
| | | /> |
| | | </div> |
| | | </n-card> |
| | | </div> |
| | | </template> |
| | |
| | | model.value = { ...initialModel } |
| | | handleSearch() |
| | | } |
| | | function handleSearch() { |
| | | pageParams.value.pageNum = 1 |
| | | getList() |
| | | } |
| | | const pageParams = ref({ |
| | | pageNum: 1, |
| | | pageSize: 10, |
| | | }) |
| | | |
| | | function handleSearch() { |
| | | pageParams.value.pageNum = 1 |
| | | getList() |
| | | } |
| | | |
| | | const isFleetIdOpen = ref(false) |
| | | const isCityCodeIdOpen = ref(false) |
| | | const isStatusOpen = ref(false) |
| | |
| | | return 220 |
| | | return 260 |
| | | }) |
| | | |
| | | // 订单状态 |
| | | 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 columns = computed<DataTableColumns<any>>(() => [ |
| | | { |
| | |
| | | value: 1, |
| | | }, |
| | | ] |
| | | // 订单状态 |
| | | 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 pcaOptions = ref<any[]>([]) |
| | | // 搜索项 |
| | | const filterItems = computed(() => [ |
| | |
| | | src="/src/assets/element-img/no-data.png" |
| | | style="width: 50%" |
| | | alt="no-data" |
| | | /> |
| | | > |
| | | </div> |
| | | </template> |
| | | </n-select> |
| | |
| | | </n-form-item> |
| | | </n-form> |
| | | <template #action> |
| | | <n-space justify="center"> |
| | | <n-space justify="end"> |
| | | <n-button @click="closeModal"> |
| | | 取消 |
| | | </n-button> |