feat: 新增车型管理和点位现场信息模块并优化现有功能
| | |
| | | activeMenu: 'businessOperations/pointManagement/map', |
| | | }, |
| | | { |
| | | name: 'siteInfoList', |
| | | path: '/businessOperations/siteInfoList', |
| | | title: '点位现场信息', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:list-view', |
| | | menuType: 'page', |
| | | componentPath: '/businessOperations/siteInfo/index.vue', |
| | | id: 4062, |
| | | keepAlive: true, |
| | | pid: 4, |
| | | }, |
| | | { |
| | | name: 'siteInfoDetail', |
| | | path: '/businessOperations/siteInfoList/detail', |
| | | title: '点位现场情况', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:list-view', |
| | | menuType: 'page', |
| | | componentPath: '/businessOperations/siteInfo/detail.vue', |
| | | id: 4063, |
| | | keepAlive: false, |
| | | hide: true, |
| | | activeMenu: '/businessOperations/siteInfoList', |
| | | pid: 4, |
| | | }, |
| | | { |
| | | name: 'cruise', |
| | | path: '/businessOperations/cruise', |
| | | title: '巡游', |
| | |
| | | pid: 5, |
| | | }, |
| | | { |
| | | name: 'vehicleModel', |
| | | path: '/assetManagement/vehicleModel', |
| | | title: '车型管理', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:car', |
| | | menuType: 'page', |
| | | componentPath: '/assetManagement/vehicleModel/index.vue', |
| | | id: 504, |
| | | keepAlive: false, |
| | | pid: 5, |
| | | }, |
| | | { |
| | | name: 'fleetManagementDetail', |
| | | path: '/assetManagement/fleetManagement/detail', |
| | | title: '车队详情', |
| | |
| | | vehicleCode: '', // 车辆编码 |
| | | vin: '', // 车辆VIN码 |
| | | plateNumber: '', // 车牌号 |
| | | model: '', // 型号 |
| | | model: null, // 型号 |
| | | vehicleColor: '', // 车辆颜色 |
| | | vehiclePhotos: [] as UploadFileInfo[], // 车辆照片 |
| | | purchaseSource: '', // 采购来源 |
| | |
| | | // 运营区域选项 |
| | | const operationAreaOptions = ref<{ label: string, value: string | number }[]>([]) |
| | | const isOperationAreaSelectOpen = ref(false) |
| | | |
| | | // 车辆型号选项 |
| | | const modelOptions = ref<{ label: string, value: string | number }[]>([]) |
| | | const isModelSelectOpen = ref(false) |
| | | |
| | | // 获取车辆型号列表(预留接口) |
| | | async function getVehicleModelList() { |
| | | try { |
| | | // const { data } = await fetchVehicleModelList() // TODO: 对接获取车辆型号接口 |
| | | // modelOptions.value = data.map((item: any) => ({ |
| | | // label: item.modelName, |
| | | // value: item.modelId |
| | | // })) |
| | | } |
| | | catch (error) { |
| | | console.error('获取型号列表失败:', error) |
| | | } |
| | | } |
| | | |
| | | // 获取运营区域列表 |
| | | async function getOperationAreaList() { |
| | |
| | | |
| | | onMounted(() => { |
| | | getOperationAreaList() |
| | | getVehicleModelList() |
| | | }) |
| | | |
| | | // 表单验证规则 |
| | |
| | | { required: true, type: 'array', message: '请上传车辆照片', trigger: ['change'] }, |
| | | ], |
| | | model: [ |
| | | { required: true, message: '请输入型号', trigger: ['blur', 'input'] }, |
| | | { min: 1, max: 50, message: '型号长度为1-50位', trigger: ['blur', 'input'] }, |
| | | { required: true, message: '请选择型号', trigger: ['blur', 'change'] }, |
| | | ], |
| | | purchaseSource: [ |
| | | { required: true, message: '请输入采购来源', trigger: ['blur', 'input'] }, |
| | |
| | | <!-- 型号 --> |
| | | <n-gi> |
| | | <n-form-item label="型号" path="model"> |
| | | <n-input |
| | | <n-select |
| | | v-model:value="formModel.model" |
| | | placeholder="请输入" |
| | | :options="modelOptions" |
| | | placeholder="请选择" |
| | | clearable |
| | | /> |
| | | @update:show="isModelSelectOpen = $event" |
| | | > |
| | | <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': isModelSelectOpen, |
| | | }" |
| | | style="width: 16px; height: 16px" |
| | | alt="arrow" |
| | | > |
| | | </template> |
| | | <template #empty> |
| | | <div class="flex-center"> |
| | | <img |
| | | :src="noDataIcon" |
| | | style="width: 50%" |
| | | alt="no-data" |
| | | > |
| | | </div> |
| | | </template> |
| | | </n-select> |
| | | </n-form-item> |
| | | </n-gi> |
| | | |
| New file |
| | |
| | | <script setup lang="ts"> |
| | | import { ref } from 'vue' |
| | | import type { FormInst, FormRules } from 'naive-ui' |
| | | import { NButton, NForm, NFormItem, NInput, NInputNumber, NModal, NSpace, useMessage } from 'naive-ui' |
| | | |
| | | const emit = defineEmits<{ |
| | | refresh: [] |
| | | }>() |
| | | |
| | | const message = useMessage() |
| | | const visible = ref(false) |
| | | const loading = ref(false) |
| | | const isEdit = ref(false) |
| | | const formRef = ref<FormInst | null>(null) |
| | | |
| | | const defaultModel = { |
| | | id: undefined as string | undefined, |
| | | modelName: '', |
| | | seats: null as number | null, |
| | | } |
| | | |
| | | const formModel = ref({ ...defaultModel }) |
| | | |
| | | const rules: FormRules = { |
| | | modelName: { |
| | | required: true, |
| | | message: '请输入车型名称', |
| | | trigger: ['blur', 'input'], |
| | | }, |
| | | seats: { |
| | | required: true, |
| | | type: 'number', |
| | | message: '请输入座位数', |
| | | trigger: ['blur', 'change'], |
| | | }, |
| | | } |
| | | |
| | | function openModal(row?: any) { |
| | | isEdit.value = !!row |
| | | if (row) { |
| | | formModel.value = { ...row } |
| | | } |
| | | else { |
| | | formModel.value = { ...defaultModel } |
| | | } |
| | | visible.value = true |
| | | } |
| | | |
| | | function handleCancel() { |
| | | visible.value = false |
| | | } |
| | | |
| | | function handleSubmit() { |
| | | formRef.value?.validate((errors) => { |
| | | if (!errors) { |
| | | loading.value = true |
| | | // 模拟提交 |
| | | setTimeout(() => { |
| | | loading.value = false |
| | | message.success(isEdit.value ? '编辑成功' : '新增成功') |
| | | visible.value = false |
| | | emit('refresh') |
| | | }, 500) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | defineExpose({ |
| | | openModal, |
| | | }) |
| | | </script> |
| | | |
| | | <template> |
| | | <NModal |
| | | v-model:show="visible" |
| | | preset="card" |
| | | :title="isEdit ? '编辑车型' : '新增车型'" |
| | | style="width: 400px; border-radius: 8px;" |
| | | :segmented="{ content: true }" |
| | | content-style="padding: 24px;" |
| | | footer-style="padding: 16px 24px;" |
| | | > |
| | | <NForm |
| | | ref="formRef" |
| | | :model="formModel" |
| | | :rules="rules" |
| | | label-placement="top" |
| | | require-mark-placement="left" |
| | | > |
| | | <NFormItem label="车型名称" path="modelName"> |
| | | <NInput |
| | | v-model:value="formModel.modelName" |
| | | placeholder="请输入" |
| | | clearable |
| | | /> |
| | | </NFormItem> |
| | | <NFormItem label="座位数" path="seats"> |
| | | <NInputNumber |
| | | v-model:value="formModel.seats" |
| | | placeholder="请输入" |
| | | clearable |
| | | :min="1" |
| | | :show-button="false" |
| | | style="width: 100%" |
| | | /> |
| | | </NFormItem> |
| | | </NForm> |
| | | |
| | | <template #footer> |
| | | <NSpace justify="end"> |
| | | <NButton @click="handleCancel"> |
| | | 取消 |
| | | </NButton> |
| | | <NButton |
| | | type="primary" |
| | | :loading="loading" |
| | | @click="handleSubmit" |
| | | > |
| | | 保存 |
| | | </NButton> |
| | | </NSpace> |
| | | </template> |
| | | </NModal> |
| | | </template> |
| New file |
| | |
| | | <script setup lang="tsx"> |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { computed, onMounted, reactive, ref } from 'vue' |
| | | import { NButton, NSpace } from 'naive-ui' |
| | | import { useBoolean } from '@/hooks' |
| | | import SearchPanel from '@/components/custom/SearchPanel.vue' |
| | | import Pagination from '@/components/common/Pagination.vue' |
| | | import AddOrEditModal from './components/AddOrEditModal.vue' |
| | | |
| | | const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false) |
| | | const dialog = useDialog() |
| | | const addOrEditModalRef = ref() |
| | | |
| | | const initialModel = { |
| | | modelName: '', |
| | | } |
| | | const model = ref({ ...initialModel }) |
| | | const pagination = reactive({ |
| | | page: 1, |
| | | pageSize: 10, |
| | | }) |
| | | |
| | | function handleResetSearch() { |
| | | model.value = { ...initialModel } |
| | | pagination.page = 1 |
| | | pagination.pageSize = 10 |
| | | handleSearch() |
| | | } |
| | | |
| | | function handleSearch() { |
| | | pagination.page = 1 |
| | | getList() |
| | | } |
| | | |
| | | const filterItems = computed(() => [ |
| | | { |
| | | label: '车型名称', |
| | | path: 'modelName', |
| | | render: () => <n-input v-model:value={model.value.modelName} placeholder="请输入" clearable />, |
| | | }, |
| | | ]) |
| | | |
| | | const count = ref(0) |
| | | const listData = ref<any[]>([]) |
| | | |
| | | // 表格列定义 |
| | | const columns = computed<DataTableColumns<any>>(() => [ |
| | | { |
| | | title: '序号', |
| | | key: 'index', |
| | | width: 60, |
| | | resizable: true, |
| | | render: (_, index) => { |
| | | return (pagination.page - 1) * pagination.pageSize + index + 1 |
| | | }, |
| | | }, |
| | | { |
| | | title: '创建时间', |
| | | key: 'createTime', |
| | | width: 160, |
| | | resizable: true, |
| | | }, |
| | | { |
| | | title: '车型名称', |
| | | key: 'modelName', |
| | | width: 300, |
| | | resizable: true, |
| | | }, |
| | | { |
| | | title: '座位数', |
| | | key: 'seats', |
| | | width: 100, |
| | | resizable: true, |
| | | }, |
| | | { |
| | | title: '操作', |
| | | key: 'actions', |
| | | width: 100, |
| | | fixed: 'right', |
| | | render: (row) => { |
| | | return ( |
| | | <NSpace wrap={false} size={8}> |
| | | <NButton |
| | | text |
| | | type="primary" |
| | | onClick={() => handleEdit(row)} |
| | | > |
| | | 编辑 |
| | | </NButton> |
| | | <NButton |
| | | text |
| | | type="primary" |
| | | onClick={() => handleDelete(row)} |
| | | > |
| | | 删除 |
| | | </NButton> |
| | | </NSpace> |
| | | ) |
| | | }, |
| | | }, |
| | | ]) |
| | | |
| | | async function getList() { |
| | | startLoading() |
| | | // Mock data simulation |
| | | setTimeout(() => { |
| | | const mockData = Array.from({ length: pagination.pageSize }).map((_, index) => { |
| | | const idx = (pagination.page - 1) * pagination.pageSize + index + 1 |
| | | return { |
| | | id: `mock-id-${idx}`, |
| | | createTime: '2025-05-07 14:53', |
| | | modelName: idx === 1 ? 'Robotaxi R1' : 'Robotaxi X1', |
| | | seats: idx === 1 ? 4 : (idx === 2 ? 6 : 4), |
| | | } |
| | | }) |
| | | count.value = 1000 |
| | | listData.value = mockData |
| | | endLoading() |
| | | }, 500) |
| | | } |
| | | |
| | | function handleAdd() { |
| | | addOrEditModalRef.value?.openModal() |
| | | } |
| | | |
| | | function handleEdit(row: any) { |
| | | addOrEditModalRef.value?.openModal(row) |
| | | } |
| | | |
| | | function handleDelete(row: any) { |
| | | dialog.create({ |
| | | title: '是否确认删除', |
| | | content: `确认删除车型 ${row.modelName} 吗?`, |
| | | positiveText: '确定', |
| | | negativeText: '取消', |
| | | onPositiveClick: () => { |
| | | window.$message?.success('删除成功') |
| | | getList() |
| | | }, |
| | | }) |
| | | } |
| | | |
| | | function changePage(page: number, pageSize: number) { |
| | | pagination.page = page |
| | | pagination.pageSize = pageSize |
| | | getList() |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getList() |
| | | }) |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="roboTaxi w-full h-full"> |
| | | <div class="flex-1 flex flex-col"> |
| | | <div class="roboTaxi-form-card mb-12px"> |
| | | <n-form |
| | | :model="model" |
| | | label-placement="left" |
| | | inline |
| | | :show-feedback="false" |
| | | > |
| | | <SearchPanel :default-collapsed="false"> |
| | | <n-gi v-for="item in filterItems" :key="item.path"> |
| | | <n-form-item :label="item.label" :path="item.path"> |
| | | <component :is="item.render" /> |
| | | </n-form-item> |
| | | </n-gi> |
| | | <template #action> |
| | | <NButton @click="handleResetSearch"> |
| | | 重置 |
| | | </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 |
| | | type="primary" |
| | | secondary |
| | | @click="handleAdd" |
| | | > |
| | | 新增车型 |
| | | </NButton> |
| | | </n-flex> |
| | | </template> |
| | | <div class="flex-1 flex flex-col h-full"> |
| | | <n-data-table |
| | | class="flex-1" |
| | | :columns="columns" |
| | | :data="listData" |
| | | :loading="loading" |
| | | :row-key="(row: any) => row.id" |
| | | flex-height |
| | | :bordered="false" |
| | | :header-height="18" |
| | | :row-height="18" |
| | | > |
| | | <template #empty> |
| | | <div class="flex-center"> |
| | | <img |
| | | src="/src/assets/element-img/no-data.png" |
| | | style="width: 123px; height: 125px" |
| | | > |
| | | </div> |
| | | </template> |
| | | </n-data-table> |
| | | <Pagination |
| | | :count="count" |
| | | :page="pagination.page" |
| | | :page-size="pagination.pageSize" |
| | | class="robotaxi-pagination mt-12px" |
| | | @change="changePage" |
| | | /> |
| | | </div> |
| | | </n-card> |
| | | </div> |
| | | |
| | | <AddOrEditModal ref="addOrEditModalRef" @refresh="getList" /> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped lang="scss"> |
| | | </style> |
| | |
| | | { |
| | | type: 'selection', |
| | | width: columnWidths.selection, |
| | | align: 'left', |
| | | align: 'center', |
| | | }, |
| | | { |
| | | title: '点位ID', |
| | |
| | | positiveText: '确定', |
| | | negativeText: '取消', |
| | | maskClosable: false, |
| | | autoFocus: false, |
| | | autoFocus: false, |
| | | onPositiveClick: async () => { |
| | | try { |
| | | const res = await deletePoint(row.id) |
| | |
| | | positiveText: '确定', |
| | | negativeText: '取消', |
| | | maskClosable: false, |
| | | autoFocus: false, |
| | | autoFocus: false, |
| | | onPositiveClick: async () => { |
| | | try { |
| | | const res = await deletePoint(row.id) |
| | |
| | | positiveText: '确定', |
| | | negativeText: '取消', |
| | | maskClosable: false, |
| | | autoFocus: false, |
| | | autoFocus: false, |
| | | onPositiveClick: async () => { |
| | | try { |
| | | const res = await enableDisablePoint({ |
| New file |
| | |
| | | <script setup lang="tsx"> |
| | | import { onMounted, ref } from 'vue' |
| | | import { NCard, NSpin, NTag } from 'naive-ui' |
| | | import { useBoolean } from '@/hooks' |
| | | |
| | | const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false) |
| | | |
| | | const detailData = ref<any>({}) |
| | | |
| | | async function getDetail() { |
| | | startLoading() |
| | | try { |
| | | // 模拟网络延迟 |
| | | await new Promise(resolve => setTimeout(resolve, 500)) |
| | | // 模拟 Mock 数据 |
| | | detailData.value = { |
| | | pointNumber: '00000000000000000000', |
| | | pointName: '各种名称各种名称各种名称', |
| | | statusText: '可接单', |
| | | reporter: '张三', |
| | | role: '网格员', |
| | | fleet: '分队名称111', |
| | | reportTime: '2026-03-23 22:03:17', |
| | | sourceChannel: '异常', |
| | | abnormalDesc: '描述内容的容描述内容的容描述内容的容描述内容的容描述内容的容描述内容的容', |
| | | photos: [ |
| | | 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png', |
| | | 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png', |
| | | ], |
| | | } |
| | | } |
| | | finally { |
| | | endLoading() |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getDetail() |
| | | }) |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="w-full h-full p-16px bg-[#f5f7fa]"> |
| | | <NSpin :show="loading"> |
| | | <NCard :bordered="false" class="rounded-8px shadow-sm min-h-[calc(100vh-140px)]"> |
| | | <!-- 点位信息 --> |
| | | <div class="mb-32px"> |
| | | <div class="flex items-center mb-16px"> |
| | | <div class="w-4px h-16px bg-[#1890FF] rounded-sm mr-8px" /> |
| | | <span class="text-16px font-bold text-[#1F2D3D]">点位信息</span> |
| | | </div> |
| | | <div class="grid grid-cols-3 gap-x-24px gap-y-16px pl-12px"> |
| | | <div class="flex items-center"> |
| | | <span class="text-[#909399] w-80px">点位ID:</span> |
| | | <span class="text-[#1F2D3D]">{{ detailData.pointNumber || '-' }}</span> |
| | | </div> |
| | | <div class="flex items-center"> |
| | | <span class="text-[#909399] w-80px">点位名称:</span> |
| | | <span class="text-[#1F2D3D]">{{ detailData.pointName || '-' }}</span> |
| | | </div> |
| | | <div class="flex items-center"> |
| | | <span class="text-[#909399] w-80px">点位状态:</span> |
| | | <NTag |
| | | v-if="detailData.statusText" |
| | | :bordered="false" |
| | | style="border-radius: 4px; padding: 0 8px; height: 22px; font-size: 12px; color: #0068FF; background-color: rgba(0, 104, 255, 0.12);" |
| | | > |
| | | {{ detailData.statusText }} |
| | | </NTag> |
| | | <span v-else class="text-[#1F2D3D]">-</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 上报信息 --> |
| | | <div> |
| | | <div class="flex items-center mb-16px"> |
| | | <div class="w-4px h-16px bg-[#1890FF] rounded-sm mr-8px" /> |
| | | <span class="text-16px font-bold text-[#1F2D3D]">上报信息</span> |
| | | </div> |
| | | <div class="grid grid-cols-3 gap-x-24px gap-y-24px pl-12px"> |
| | | <div class="flex items-start"> |
| | | <span class="text-[#909399] w-80px">上报人:</span> |
| | | <span class="text-[#1F2D3D]">{{ detailData.reporter || '-' }}</span> |
| | | </div> |
| | | <div class="flex items-start"> |
| | | <span class="text-[#909399] w-80px">角色:</span> |
| | | <span class="text-[#1F2D3D]">{{ detailData.role || '-' }}</span> |
| | | </div> |
| | | <div class="flex items-start"> |
| | | <span class="text-[#909399] w-80px">所属分队:</span> |
| | | <span class="text-[#1F2D3D]">{{ detailData.fleet || '-' }}</span> |
| | | </div> |
| | | <div class="flex items-start"> |
| | | <span class="text-[#909399] w-80px">上报时间:</span> |
| | | <span class="text-[#1F2D3D]">{{ detailData.reportTime || '-' }}</span> |
| | | </div> |
| | | <div class="flex items-start"> |
| | | <span class="text-[#909399] w-80px">现场情况:</span> |
| | | <span class="text-[#1F2D3D]">{{ detailData.sourceChannel || '-' }}</span> |
| | | </div> |
| | | <!-- 空位占位符 --> |
| | | <div /> |
| | | |
| | | <!-- 跨列说明 --> |
| | | <div class="col-span-3 flex items-start"> |
| | | <span class="text-[#909399] w-100px shrink-0">现场情况描述:</span> |
| | | <span class="text-[#1F2D3D] leading-relaxed"> |
| | | {{ detailData.abnormalDesc || '-' }} |
| | | </span> |
| | | </div> |
| | | |
| | | <!-- 图片 --> |
| | | <div class="col-span-3 flex items-start"> |
| | | <span class="text-[#909399] w-80px shrink-0">现场照片:</span> |
| | | <div class="flex flex-wrap gap-12px"> |
| | | <template v-if="detailData.photos && detailData.photos.length"> |
| | | <img |
| | | v-for="(img, idx) in detailData.photos" |
| | | :key="idx" |
| | | :src="img" |
| | | class="w-64px h-64px rounded-4px object-cover border border-[#EBEEF5]" |
| | | > |
| | | </template> |
| | | <span v-else class="text-[#1F2D3D]">-</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </NCard> |
| | | </NSpin> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped lang="scss"> |
| | | </style> |
| New file |
| | |
| | | <script setup lang="tsx"> |
| | | import type { DataTableColumns, FormInst } from 'naive-ui' |
| | | import { useBoolean } from '@/hooks' |
| | | import { NButton, NDatePicker, NInput, NSelect, NSpace } from 'naive-ui' |
| | | import SearchPanel from '@/components/custom/SearchPanel.vue' |
| | | import { computed, onMounted, reactive, ref } from 'vue' |
| | | import { useRouter } from 'vue-router' |
| | | import { useI18n } from 'vue-i18n' |
| | | import dayjs from 'dayjs' |
| | | |
| | | const router = useRouter() |
| | | const message = useMessage() |
| | | const dialog = useDialog() |
| | | |
| | | const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false) |
| | | const { t } = useI18n() |
| | | |
| | | const initialModel = { |
| | | pointNumber: '', |
| | | pointName: '', |
| | | status: null, |
| | | siteCondition: null, |
| | | reporter: '', |
| | | reportTime: null, |
| | | } |
| | | |
| | | const model = ref({ ...initialModel }) |
| | | |
| | | const isStatusSelectOpen = ref(false) |
| | | const isConditionSelectOpen = ref(false) |
| | | |
| | | const formRef = ref<FormInst | null>() |
| | | |
| | | function getStatusClass(status: string) { |
| | | switch (status) { |
| | | case '待测试': |
| | | return 'wait-test' |
| | | case '测试中': |
| | | return 'testing' |
| | | case '测试不通过': |
| | | case '失效': |
| | | return 'test-fail' |
| | | case '可运营': |
| | | return 'test-pass' |
| | | case '待生效': |
| | | return 'test-take-effect' |
| | | case '启用中': |
| | | return 'test-starting' |
| | | case '已停用': |
| | | return 'test-stop' |
| | | default: |
| | | return '' |
| | | } |
| | | } |
| | | |
| | | const statusOptions = [ |
| | | { label: '待测试', value: 1 }, |
| | | { label: '测试中', value: 2 }, |
| | | { label: '测试不通过', value: 3 }, |
| | | { label: '可运营', value: 4 }, |
| | | { label: '待生效', value: 5 }, |
| | | { label: '启用中', value: 6 }, |
| | | { label: '已停用', value: 7 }, |
| | | { label: '失效', value: 8 }, |
| | | ] |
| | | |
| | | const conditionOptions = [ |
| | | { label: '正常', value: 1 }, |
| | | { label: '异常', value: 0 }, |
| | | ] |
| | | |
| | | function getPointStatusText(operationStatus: number, verificationStatus?: number) { |
| | | const v = Number(verificationStatus) |
| | | const o = Number(operationStatus) |
| | | |
| | | if (v === 1) |
| | | return '待测试' |
| | | if (v === 2) |
| | | return '测试中' |
| | | if (v === 4) |
| | | return '测试不通过' |
| | | if (v === 3) |
| | | return '可运营' |
| | | if (v === 5) { |
| | | if (o === 1) |
| | | return '待生效' |
| | | if (o === 2) |
| | | return '启用中' |
| | | if (o === 3) |
| | | return '已停用' |
| | | if (o === 4) |
| | | return '已失效' |
| | | } |
| | | return '待测试' |
| | | } |
| | | |
| | | const columns = computed<DataTableColumns<any>>(() => [ |
| | | { |
| | | type: 'selection', |
| | | width: 50, |
| | | align: 'center', |
| | | }, |
| | | { |
| | | title: '点位ID', |
| | | key: 'pointNumber', |
| | | width: 140, |
| | | align: 'left', |
| | | resizable: true, |
| | | ellipsis: true, |
| | | }, |
| | | { |
| | | title: '点位名称', |
| | | key: 'pointName', |
| | | width: 180, |
| | | align: 'left', |
| | | resizable: true, |
| | | ellipsis: true, |
| | | render(row) { |
| | | return ( |
| | | <div |
| | | class="cursor-pointer text-ellipsis whitespace-nowrap w-full overflow-hidden text-[#0068FF]" |
| | | onClick={() => handleToDetail(row)} |
| | | > |
| | | {row.pointName} |
| | | </div> |
| | | ) |
| | | }, |
| | | }, |
| | | { |
| | | title: '状态', |
| | | key: 'status', |
| | | width: 100, |
| | | resizable: true, |
| | | render(row) { |
| | | const statusText = getPointStatusText(row.operationStatus, row.verificationStatus) |
| | | const statusClass = getStatusClass(statusText) |
| | | return ( |
| | | <div class={`status-tag ${statusClass}`}> |
| | | {statusText} |
| | | </div> |
| | | ) |
| | | }, |
| | | }, |
| | | { |
| | | title: '现场情况', |
| | | key: 'siteCondition', |
| | | width: 100, |
| | | align: 'left', |
| | | ellipsis: true, |
| | | resizable: true, |
| | | render(row) { |
| | | const isNormal = row.siteCondition === 1 |
| | | return ( |
| | | <span style={{ color: isNormal ? '#19B144' : '#E02716' }}> |
| | | {isNormal ? '正常' : '异常'} |
| | | </span> |
| | | ) |
| | | }, |
| | | }, |
| | | { |
| | | title: '异常情况说明', |
| | | key: 'abnormalDesc', |
| | | width: 220, |
| | | align: 'left', |
| | | ellipsis: true, |
| | | resizable: true, |
| | | render(row) { |
| | | return row.abnormalDesc || '-' |
| | | }, |
| | | }, |
| | | { |
| | | title: '上报人', |
| | | key: 'reporter', |
| | | width: 120, |
| | | align: 'left', |
| | | ellipsis: true, |
| | | resizable: true, |
| | | }, |
| | | { |
| | | title: '上报时间', |
| | | key: 'reportTime', |
| | | width: 160, |
| | | align: 'left', |
| | | ellipsis: true, |
| | | render(row) { |
| | | return row.reportTime ? dayjs(row.reportTime).format('YYYY/MM/DD HH:mm') || '-' : '-' |
| | | }, |
| | | }, |
| | | { |
| | | title: '操作', |
| | | key: 'actions', |
| | | width: 90, |
| | | align: 'left', |
| | | fixed: 'right', |
| | | render: (row) => { |
| | | return ( |
| | | <NSpace wrap={false}> |
| | | <NButton |
| | | text |
| | | type="primary" |
| | | onClick={() => handleToDetail(row)} |
| | | > |
| | | 详情 |
| | | </NButton> |
| | | <NButton |
| | | text |
| | | type="primary" |
| | | onClick={() => handleDelete(row)} |
| | | > |
| | | 删除 |
| | | </NButton> |
| | | </NSpace> |
| | | ) |
| | | }, |
| | | }, |
| | | ]) |
| | | |
| | | const filterItems = computed(() => [ |
| | | { |
| | | label: '点位ID', |
| | | path: 'pointNumber', |
| | | render: () => <NInput v-model:value={model.value.pointNumber} placeholder="请输入" clearable />, |
| | | }, |
| | | { |
| | | label: '点位名称', |
| | | path: 'pointName', |
| | | render: () => <NInput v-model:value={model.value.pointName} placeholder="请输入" clearable />, |
| | | }, |
| | | { |
| | | label: '点位状态', |
| | | path: 'status', |
| | | render: () => ( |
| | | <NSelect |
| | | v-model:value={model.value.status} |
| | | options={statusOptions} |
| | | placeholder="请选择" |
| | | clearable |
| | | onUpdate:show={(value: boolean) => isStatusSelectOpen.value = value} |
| | | > |
| | | {{ |
| | | arrow: () => ( |
| | | <img |
| | | src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e120f81c-9f15-4e1a-9938-22252898edf9.png" |
| | | class={`arrow-icon ${isStatusSelectOpen.value ? 'arrow-icon--open' : ''}`} |
| | | style={{ width: '16px', height: '16px' }} |
| | | alt="arrow" |
| | | /> |
| | | ), |
| | | }} |
| | | </NSelect> |
| | | ), |
| | | }, |
| | | { |
| | | label: '现场情况', |
| | | path: 'siteCondition', |
| | | render: () => ( |
| | | <NSelect |
| | | v-model:value={model.value.siteCondition} |
| | | options={conditionOptions} |
| | | placeholder="请选择" |
| | | clearable |
| | | onUpdate:show={(value: boolean) => isConditionSelectOpen.value = value} |
| | | > |
| | | {{ |
| | | arrow: () => ( |
| | | <img |
| | | src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e120f81c-9f15-4e1a-9938-22252898edf9.png" |
| | | class={`arrow-icon ${isConditionSelectOpen.value ? 'arrow-icon--open' : ''}`} |
| | | style={{ width: '16px', height: '16px' }} |
| | | alt="arrow" |
| | | /> |
| | | ), |
| | | }} |
| | | </NSelect> |
| | | ), |
| | | }, |
| | | { |
| | | label: '上报人', |
| | | path: 'reporter', |
| | | render: () => <NInput v-model:value={model.value.reporter} placeholder="请输入" clearable />, |
| | | }, |
| | | { |
| | | label: '上报时间', |
| | | path: 'reportTime', |
| | | render: () => <NDatePicker v-model:value={model.value.reportTime} type="daterange" format="yyyy/MM/dd" clearable actions={[]} close-on-select={true} start-placeholder="开始时间" end-placeholder="结束时间" separator="-" />, |
| | | }, |
| | | ]) |
| | | |
| | | const listData = ref<any>([]) |
| | | const checkedRowKeys = ref<Array<string | number>>([]) |
| | | |
| | | const pagination = reactive({ |
| | | page: 1, |
| | | pageSize: 10, |
| | | itemCount: 0, |
| | | showSizePicker: true, |
| | | pageSizes: [10, 20, 50, 100], |
| | | prefix: ({ itemCount }: { itemCount: number | undefined }) => `共 ${itemCount || 0} 条`, |
| | | onChange: (page: number) => { |
| | | pagination.page = page |
| | | getList() |
| | | }, |
| | | onUpdatePageSize: (pageSize: number) => { |
| | | pagination.pageSize = pageSize |
| | | pagination.page = 1 |
| | | getList() |
| | | }, |
| | | }) |
| | | |
| | | function handleSearch() { |
| | | pagination.page = 1 |
| | | getList() |
| | | } |
| | | |
| | | function handleResetSearch() { |
| | | model.value = { ...initialModel } |
| | | handleSearch() |
| | | } |
| | | |
| | | async function getList() { |
| | | startLoading() |
| | | try { |
| | | // Mock API Call |
| | | await new Promise(resolve => setTimeout(resolve, 500)) |
| | | |
| | | const mockData = [] |
| | | const statuses = ['待测试', '测试中', '测试不通过', '可运营', '待生效', '启用中', '已停用', '已停用', '已停用', '失效'] |
| | | const reporters = ['刘晓燕', '刘晓燕', '王明', '王明', '张三', '张三', '王明明', '王明明', '王珂', '王珂'] |
| | | const conditions = [1, 1, 0, 0, 0, 0, 0, 0, 0, 0] // 1: 正常, 0: 异常 |
| | | |
| | | for (let i = 0; i < pagination.pageSize; i++) { |
| | | const index = (pagination.page - 1) * pagination.pageSize + i |
| | | if (index >= 1000) |
| | | break |
| | | |
| | | mockData.push({ |
| | | id: `mock-id-${index}`, |
| | | pointNumber: 'XP676543457', |
| | | pointName: '国贸大厦北门停车点', |
| | | statusText: statuses[i % 10], |
| | | siteCondition: conditions[i % 10], |
| | | abnormalDesc: '异常情况说明内容内容内容内容内容内容内容...', |
| | | reporter: reporters[i % 10], |
| | | reportTime: '2021-07-29T10:59:00', |
| | | }) |
| | | } |
| | | |
| | | listData.value = mockData |
| | | pagination.itemCount = 1000 |
| | | } |
| | | finally { |
| | | endLoading() |
| | | } |
| | | } |
| | | |
| | | function handleToDetail(row: any) { |
| | | router.push({ name: 'siteInfoDetail', query: { id: row.id } }) |
| | | } |
| | | |
| | | function handleDelete(row: any) { |
| | | dialog.warning({ |
| | | title: '提示', |
| | | content: '确认删除该点位现场信息吗?', |
| | | positiveText: '确定', |
| | | negativeText: '取消', |
| | | onPositiveClick: () => { |
| | | console.warn(row) |
| | | message.success('删除成功') |
| | | getList() |
| | | }, |
| | | }) |
| | | } |
| | | |
| | | async function handleBatchExport() { |
| | | if (checkedRowKeys.value.length === 0) { |
| | | message.warning('请先选择记录') |
| | | return |
| | | } |
| | | message.success('导出成功') |
| | | } |
| | | |
| | | function handleCheck(rowKeys: Array<string | number>) { |
| | | checkedRowKeys.value = rowKeys |
| | | } |
| | | |
| | | function changePage(page: number, size: number) { |
| | | pagination.page = page |
| | | pagination.pageSize = size |
| | | getList() |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getList() |
| | | }) |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="roboTaxi w-full h-full px-[12px] py-[12px]"> |
| | | <div class="roboTaxi-form-card mb-12px"> |
| | | <n-form ref="formRef" :model="model" label-placement="left" inline :show-feedback="false"> |
| | | <SearchPanel> |
| | | <n-gi v-for="item in filterItems" :key="item.path"> |
| | | <n-form-item :label="item.label" :path="item.path"> |
| | | <component :is="item.render" /> |
| | | </n-form-item> |
| | | </n-gi> |
| | | <template #action> |
| | | <NButton @click="handleResetSearch"> |
| | | {{ t('common.reset') }} |
| | | </NButton> |
| | | <NButton type="primary" @click="handleSearch"> |
| | | {{ t('common.search') }} |
| | | </NButton> |
| | | </template> |
| | | </SearchPanel> |
| | | </n-form> |
| | | </div> |
| | | |
| | | <n-card |
| | | class="flex-1 robotaxi-table-card point-management-list" |
| | | content-style="display: flex; flex-direction: column; flex: 1; min-height: 0; min-width: 0;" |
| | | > |
| | | <template #header> |
| | | <n-flex justify="space-between" align="center"> |
| | | <n-flex align="center"> |
| | | <span v-if="checkedRowKeys.length > 0" style="color: rgba(23, 31, 38, 0.6);font-weight: 400;">已选择{{ checkedRowKeys.length }}条记录</span> |
| | | <NButton :disabled="checkedRowKeys.length === 0" @click="handleBatchExport"> |
| | | 批量导出 |
| | | </NButton> |
| | | </n-flex> |
| | | </n-flex> |
| | | </template> |
| | | |
| | | <div class="flex-1 flex flex-col h-full" style="min-width: 0; overflow: hidden;"> |
| | | <n-data-table |
| | | class="flex-1" |
| | | :columns="columns" |
| | | :data="listData" |
| | | :loading="loading" |
| | | flex-height |
| | | :bordered="false" |
| | | :header-height="18" |
| | | :row-height="18" |
| | | :scroll-x="1160" |
| | | :row-key="(row) => row.id" |
| | | @update:checked-row-keys="handleCheck" |
| | | > |
| | | <template #empty> |
| | | <div class="flex-center"> |
| | | <img src="/src/assets/element-img/no-data.png" style="width:123px;height:125px" @error="(e: any) => { e.target.src = 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png' }"> |
| | | </div> |
| | | </template> |
| | | </n-data-table> |
| | | <Pagination |
| | | :count="pagination.itemCount" |
| | | :page="pagination.page" |
| | | :page-size="pagination.pageSize" |
| | | class="robotaxi-pagination mt-12px" |
| | | @change="changePage" |
| | | /> |
| | | </div> |
| | | </n-card> |
| | | </div> |
| | | </template> |
| | | |
| | | <style lang="scss"> |
| | | .point-management-list { |
| | | .n-data-table-th { |
| | | background-color: #fafafc; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .status-tag { |
| | | display: inline-block; |
| | | padding: 2px 8px; |
| | | border-radius: 4px; |
| | | font-family: PingFang SC; |
| | | font-size: 12px; |
| | | font-weight: normal; |
| | | line-height: 16px; |
| | | text-align: center; |
| | | margin: 0 4px; |
| | | |
| | | // 待测试 |
| | | &.wait-test { |
| | | background: #FFF6EA; |
| | | color: #FFAA2A; |
| | | } |
| | | |
| | | // 测试中 |
| | | &.testing { |
| | | background: #E6F4FF; |
| | | color: #0068FF; |
| | | } |
| | | |
| | | // 测试失败---运营失效 |
| | | &.test-fail { |
| | | background: #FFF3F1; |
| | | color: #E02716; |
| | | } |
| | | |
| | | // 可运营 |
| | | &.test-pass { |
| | | color: #0068FF; |
| | | background: transparent; |
| | | border: 1px dashed #0068FF; |
| | | } |
| | | |
| | | // 待生效--待启用 |
| | | &.test-take-effect { |
| | | background: #E6F4FF; |
| | | color: #0068FF; |
| | | } |
| | | |
| | | //启用中 |
| | | &.test-starting { |
| | | background: #E9F9ED; |
| | | color: #19B144; |
| | | } |
| | | |
| | | // 已停用 |
| | | &.test-stop { |
| | | background: #E8E9E9; |
| | | color: #171F26; |
| | | } |
| | | |
| | | // 失效 |
| | | &.test-invalid { |
| | | background: #E8E9E9; |
| | | color: #171F26; |
| | | } |
| | | } |
| | | } |
| | | </style> |
| | |
| | | <script setup lang="ts"> |
| | | import { ref, computed } from 'vue' |
| | | import { computed, ref } from 'vue' |
| | | import type { FormInst, FormRules } from 'naive-ui' |
| | | |
| | | interface FormModel { |
| | | shiftName: string |
| | | shiftColor: string |
| | | |
| | | // 第一行 |
| | | firstPunchStartTime: string | null |
| | | firstPunchEndTime: string | null |
| | | |
| | | // 第二行 |
| | | startTime: string | null |
| | | secondPunchStartTime: string | null |
| | | secondPunchEndTime: string | null |
| | | isSecondPunchCrossDay: boolean // 是否跨天 |
| | | |
| | | // 第三行 |
| | | endTime: string | null |
| | | thirdPunchStartTime: string | null |
| | | thirdPunchEndTime: string | null |
| | | |
| | | isRest: boolean |
| | | restStartTime: string | null |
| | | restEndTime: string | null |
| | |
| | | |
| | | const emit = defineEmits<{ |
| | | 'update:show': [value: boolean] |
| | | confirm: [data: FormModel] |
| | | 'confirm': [data: FormModel] |
| | | }>() |
| | | |
| | | const show = computed({ |
| | | get: () => props.show, |
| | | set: (value) => emit('update:show', value) |
| | | set: value => emit('update:show', value), |
| | | }) |
| | | |
| | | const formRef = ref<FormInst | null>(null) |
| | | const formModel = ref<FormModel>({ |
| | | shiftName: '', |
| | | shiftColor: '', |
| | | firstPunchStartTime: null, |
| | | firstPunchEndTime: null, |
| | | startTime: null, |
| | | secondPunchStartTime: null, |
| | | secondPunchEndTime: null, |
| | | isSecondPunchCrossDay: false, |
| | | endTime: null, |
| | | thirdPunchStartTime: null, |
| | | thirdPunchEndTime: null, |
| | | isRest: false, |
| | | restStartTime: null, |
| | | restEndTime: null |
| | | restEndTime: null, |
| | | }) |
| | | |
| | | const rules: FormRules = { |
| | | shiftName: [ |
| | | { required: true, message: '请输入班次名称', trigger: 'blur' } |
| | | { required: true, message: '请输入班次名称', trigger: 'blur' }, |
| | | ], |
| | | // shiftColor: [ |
| | | // { required: true, message: '请选择班次颜色', trigger: 'change' } |
| | | // ], |
| | | firstPunchStartTime: [ |
| | | { required: true, message: '请选择最早打卡时间', trigger: 'change' }, |
| | | ], |
| | | firstPunchEndTime: [ |
| | | { required: true, message: '请选择最晚打卡时间', trigger: 'change' }, |
| | | ], |
| | | startTime: [ |
| | | { required: true, message: '请选择开始时间', trigger: 'change' } |
| | | { required: true, message: '请选择开始时间', trigger: 'change' }, |
| | | ], |
| | | secondPunchStartTime: [ |
| | | { required: true, message: '请选择最早打卡时间', trigger: 'change' }, |
| | | ], |
| | | secondPunchEndTime: [ |
| | | { required: true, message: '请选择最晚打卡时间', trigger: 'change' }, |
| | | ], |
| | | endTime: [ |
| | | { required: true, message: '请选择结束时间', trigger: 'change' }, |
| | | { |
| | | validator: (_rule: any, value: any) => { |
| | | if (formModel.value.startTime && value) { |
| | | const start = new Date(`2000-01-01 ${formModel.value.startTime}`) |
| | | const end = new Date(`2000-01-01 ${value}`) |
| | | if (end <= start) { |
| | | return new Error('结束时间必须大于开始时间') |
| | | } |
| | | } |
| | | return true |
| | | }, |
| | | trigger: ['change', 'blur'] |
| | | } |
| | | // 这里因为可能有跨天逻辑,原有的简单结束必须大于开始时间的校验可能不适用,暂时移除强制结束>开始 |
| | | ], |
| | | thirdPunchStartTime: [ |
| | | { required: true, message: '请选择最早打卡时间', trigger: 'change' }, |
| | | ], |
| | | restStartTime: [ |
| | | { |
| | |
| | | } |
| | | return true |
| | | }, |
| | | trigger: 'change' |
| | | } |
| | | trigger: 'change', |
| | | }, |
| | | ], |
| | | restEndTime: [ |
| | | { |
| | |
| | | } |
| | | return true |
| | | }, |
| | | trigger: ['change', 'blur'] |
| | | } |
| | | ] |
| | | trigger: ['change', 'blur'], |
| | | }, |
| | | ], |
| | | } |
| | | |
| | | const handleCancel = () => { |
| | | function handleCancel() { |
| | | show.value = false |
| | | resetForm() |
| | | } |
| | | |
| | | const handleConfirm = async () => { |
| | | async function handleConfirm() { |
| | | try { |
| | | await formRef.value?.validate() |
| | | emit('confirm', formModel.value) |
| | | show.value = false |
| | | resetForm() |
| | | } catch (error) { |
| | | console.log('验证失败', error) |
| | | } |
| | | catch (error) { |
| | | console.warn('验证失败', error) |
| | | } |
| | | } |
| | | |
| | | const resetForm = () => { |
| | | function resetForm() { |
| | | formModel.value = { |
| | | shiftName: '', |
| | | shiftColor: '', |
| | | firstPunchStartTime: null, |
| | | firstPunchEndTime: null, |
| | | startTime: null, |
| | | secondPunchStartTime: null, |
| | | secondPunchEndTime: null, |
| | | isSecondPunchCrossDay: false, |
| | | endTime: null, |
| | | thirdPunchStartTime: null, |
| | | thirdPunchEndTime: null, |
| | | isRest: false, |
| | | restStartTime: null, |
| | | restEndTime: null |
| | | restEndTime: null, |
| | | } |
| | | } |
| | | |
| | | const openEdit = (data: any) => { |
| | | function openEdit(data: any) { |
| | | formModel.value = { ...data } |
| | | } |
| | | function closeTimePicker() { |
| | |
| | | :mask-closable="false" |
| | | preset="card" |
| | | :title="mode === 'edit' ? '编辑班次' : '新增班次'" |
| | | class="w-400px" |
| | | style="width: 800px;" |
| | | :segmented="{ |
| | | content: true, |
| | | action: true, |
| | | }"> |
| | | }" |
| | | > |
| | | <n-form |
| | | ref="formRef" |
| | | :model="formModel" |
| | |
| | | require-mark-placement="left" |
| | | show-require-mark |
| | | > |
| | | <div class="flex gap-8px flex-col"> |
| | | <n-form-item label="班次" path="shiftName"> |
| | | <n-input v-model:value="formModel.shiftName" placeholder="请输入" clearable /> |
| | | </n-form-item> |
| | | <!-- Row 1 --> |
| | | <div class="grid grid-cols-3 gap-x-24px mb-16px"> |
| | | <n-form-item label="班次" path="shiftName"> |
| | | <n-input v-model:value="formModel.shiftName" placeholder="请输入" clearable /> |
| | | </n-form-item> |
| | | </div> |
| | | |
| | | <!-- <n-form-item label="班次色号" path="color"> |
| | | <div class="flex gap-2"> |
| | | <div |
| | | v-for="color in colorOptions" |
| | | :key="color.value" |
| | | class="w-12 h-12 rounded cursor-pointer relative" |
| | | :style="{ backgroundColor: color.value, border: formModel.shiftColor === color.value ? '2px solid #0068FF' : '1px solid #e5e7eb' }" |
| | | @click="formModel.shiftColor = color.value" |
| | | > |
| | | <div |
| | | v-if="formModel.shiftColor === color.value" |
| | | class="absolute top-0 right-0 w-4 h-4 bg-blue-500 rounded-bl flex items-center justify-center" |
| | | > |
| | | <span class="text-white text-xs">✓</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </n-form-item> --> |
| | | <!-- Row 2 --> |
| | | <div class="grid grid-cols-3 gap-x-24px mb-16px"> |
| | | <n-form-item label="开始时间" path="startTime"> |
| | | <n-time-picker |
| | | v-model:formatted-value="formModel.startTime" |
| | | format="HH:mm" value-format="HH:mm" |
| | | :actions="['now']" |
| | | placeholder="--:--" |
| | | clearable style="width: 100%" @update:value="closeTimePicker" |
| | | /> |
| | | </n-form-item> |
| | | <n-form-item label="最早打卡时间" path="secondPunchStartTime"> |
| | | <n-time-picker |
| | | v-model:formatted-value="formModel.secondPunchStartTime" |
| | | format="HH:mm" value-format="HH:mm" |
| | | :actions="['now']" |
| | | placeholder="--:--" |
| | | clearable style="width: 100%" @update:value="closeTimePicker" |
| | | /> |
| | | </n-form-item> |
| | | <n-form-item label="最晚打卡时间" path="secondPunchEndTime"> |
| | | <n-time-picker |
| | | v-model:formatted-value="formModel.secondPunchEndTime" |
| | | format="HH:mm" value-format="HH:mm" |
| | | :actions="['now']" |
| | | placeholder="--:--" |
| | | clearable style="width: 100%" @update:value="closeTimePicker" |
| | | /> |
| | | </n-form-item> |
| | | </div> |
| | | |
| | | <n-form-item label="开始时间" path="startTime"> |
| | | <n-time-picker v-model:formatted-value="formModel.startTime" |
| | | format="HH:mm" value-format="HH:mm" |
| | | :actions="['now']" |
| | | @update:value="() => { closeTimePicker();}" |
| | | placeholder="--:--" clearable style="width: 100%" /> |
| | | </n-form-item> |
| | | <!-- Row 3 --> |
| | | <div class="grid grid-cols-3 gap-x-24px mb-16px"> |
| | | <n-form-item label="结束时间" path="endTime"> |
| | | <n-time-picker |
| | | v-model:formatted-value="formModel.endTime" |
| | | format="HH:mm" value-format="HH:mm" |
| | | :actions="['now']" |
| | | placeholder="--:--" |
| | | clearable style="width: 100%" @update:value="closeTimePicker" |
| | | /> |
| | | </n-form-item> |
| | | <n-form-item label="最早打卡时间" path="thirdPunchStartTime"> |
| | | <n-time-picker |
| | | v-model:formatted-value="formModel.thirdPunchStartTime" |
| | | format="HH:mm" value-format="HH:mm" |
| | | :actions="['now']" |
| | | placeholder="--:--" |
| | | clearable style="width: 100%" @update:value="closeTimePicker" |
| | | /> |
| | | </n-form-item> |
| | | <n-form-item path="secondPunchEndTime"> |
| | | <template #label> |
| | | 最晚打卡时间 |
| | | </template> |
| | | <n-time-picker |
| | | v-model:formatted-value="formModel.secondPunchEndTime" |
| | | format="HH:mm" value-format="HH:mm" |
| | | :actions="['now']" |
| | | placeholder="--:--" |
| | | clearable style="width: 100%" @update:value="closeTimePicker" |
| | | /> |
| | | </n-form-item> |
| | | <n-form-item label="是否跨天" path="isRest" label-placement="left" class="mb-16px"> |
| | | <n-switch v-model:value="formModel.isRest" /> |
| | | </n-form-item> |
| | | </div> |
| | | |
| | | <n-form-item label="结束时间" path="endTime"> |
| | | <n-time-picker v-model:formatted-value="formModel.endTime" |
| | | format="HH:mm" value-format="HH:mm" |
| | | :actions="['now']" |
| | | @update:value="() => { closeTimePicker();}" |
| | | placeholder="--:--" clearable style="width: 100%" /> |
| | | </n-form-item> |
| | | |
| | | <n-form-item label="是否中途休息" path="isRest" label-placement="left"> |
| | | <!-- Rest Section --> |
| | | <n-form-item label="是否中途休息" path="isRest" label-placement="left" class="mb-16px"> |
| | | <n-switch v-model:value="formModel.isRest" /> |
| | | </n-form-item> |
| | | |
| | | <template v-if="formModel.isRest"> |
| | | <div v-if="formModel.isRest" class="grid grid-cols-3 gap-x-24px"> |
| | | <n-form-item label="开始休息时间" path="restStartTime"> |
| | | <n-time-picker v-model:formatted-value="formModel.restStartTime" |
| | | format="HH:mm" value-format="HH:mm" |
| | | :actions="['now']" |
| | | @update:value="() => { closeTimePicker();}" |
| | | placeholder="--:--" clearable style="width: 100%" /> |
| | | <n-time-picker |
| | | v-model:formatted-value="formModel.restStartTime" |
| | | format="HH:mm" value-format="HH:mm" |
| | | :actions="['now']" |
| | | placeholder="--:--" |
| | | clearable style="width: 100%" @update:value="closeTimePicker" |
| | | /> |
| | | </n-form-item> |
| | | |
| | | <n-form-item label="结束时间" path="restEndTime"> |
| | | <n-time-picker v-model:formatted-value="formModel.restEndTime" |
| | | format="HH:mm" value-format="HH:mm" |
| | | :actions="['now']" |
| | | @update:value="() => { closeTimePicker();}" |
| | | placeholder="--:--" clearable style="width: 100%" /> |
| | | <n-form-item label="结束休息时间" path="restEndTime"> |
| | | <n-time-picker |
| | | v-model:formatted-value="formModel.restEndTime" |
| | | format="HH:mm" value-format="HH:mm" |
| | | :actions="['now']" |
| | | placeholder="--:--" |
| | | clearable style="width: 100%" @update:value="closeTimePicker" |
| | | /> |
| | | </n-form-item> |
| | | </template> |
| | | </div> |
| | | </n-form> |
| | | |
| | | <template #action> |
| | | <n-space justify="end"> |
| | | <n-button @click="handleCancel">取消</n-button> |
| | | <n-button type="primary" @click="handleConfirm">确定</n-button> |
| | | <n-button @click="handleCancel"> |
| | | 取消 |
| | | </n-button> |
| | | <n-button type="primary" @click="handleConfirm"> |
| | | 确定 |
| | | </n-button> |
| | | </n-space> |
| | | </template> |
| | | </n-modal> |