Merge branch 'main' of http://120.76.84.145:10101/gitblit/r/H5/xp-robotaxi-web
| | |
| | | } |
| | | |
| | | /** |
| | | * 下载导入模板 |
| | | * @returns |
| | | */ |
| | | export function getVehicleImportTemplate() { |
| | | const method = request.Post<Service.ResponseResult<any>>('/vehicleConvoy/api/v1/vehicle/importTemplate', { responseType: 'blob' }) |
| | | method.meta = { ...method.meta, isBlob: true } |
| | | return method |
| | | } |
| | | |
| | | /** |
| | | * 删除车辆 |
| | | * @param id |
| | | * @returns |
| | |
| | | const rsaPublicKey = method.meta?.rsaPublicKey || '' // 从meta中获取RSA公钥 |
| | | const secretKey = method.meta?.secretKey || '' // 从meta中获取签名密钥 |
| | | const sensitiveFields = method.meta?.sensitiveFields || [] // 从meta中获取敏感字段 |
| | | console.log('secretKey', secretKey) |
| | | console.log('method.meta', method.meta, 'method.data', method.data) |
| | | // console.log('secretKey', secretKey) |
| | | // console.log('method.meta', method.meta, 'method.data', method.data) |
| | | |
| | | // 获取请求参数:GET请求使用params,其他请求使用data |
| | | const requestData = method.type === 'GET' ? (method.config.params || {}) : (method.data || {}) |
| | |
| | | } |
| | | // 使用包含时间戳的对象生成签名 |
| | | const sign = generateSign(paramsWithTimestamp, secretKey, timestamp) |
| | | console.log('method', method) |
| | | // console.log('method', method) |
| | | |
| | | // 根据请求类型设置参数 |
| | | // if (method.type === 'GET') { |
| | |
| | | // 重置当前存储库 |
| | | this.$reset() |
| | | // 重定向到登录页 |
| | | if (route.meta.requiresAuth) { |
| | | router.push({ |
| | | name: 'login', |
| | | query: { |
| | | redirect: route.fullPath, |
| | | }, |
| | | }) |
| | | } |
| | | const redirect = route.meta.requiresAuth && route.name !== 'not-found' ? route.fullPath : undefined |
| | | router.push({ |
| | | name: 'login', |
| | | query: { |
| | | redirect, |
| | | }, |
| | | }) |
| | | }, |
| | | clearAuthStorage() { |
| | | local.remove('accessToken') |
| | |
| | | export function createRoutes(routes: AppRoute.RowRoute[]) { |
| | | const { hasPermission } = usePermission() |
| | | |
| | | console.log('Creating routes with input:', routes) |
| | | // console.log('Creating routes with input:', routes) |
| | | |
| | | // Structure the meta field |
| | | |
| | |
| | | |
| | | let resultRouter = standardizedRoutes(routes) |
| | | |
| | | console.log('After standardization:', resultRouter) |
| | | // console.log('After standardization:', resultRouter) |
| | | |
| | | // Route permission filtering |
| | | resultRouter = resultRouter.filter(i => { |
| | | if (i.meta?.requiresAuth === false) |
| | | return true |
| | | if (i.id) { |
| | | return hasPermission(i.id) |
| | | } |
| | | return hasPermission(i.meta.roles) |
| | | }) |
| | | // 正式解开 |
| | | console.log('After permission filtering:', resultRouter) |
| | | // console.log('After permission filtering:', resultRouter) |
| | | |
| | | // Generate routes, no need to import files for those with redirect |
| | | const modules = import.meta.glob('@/views/**/*.vue') |
| | | console.log('Available modules:', Object.keys(modules)) |
| | | // console.log('Available modules:', Object.keys(modules)) |
| | | |
| | | resultRouter = resultRouter.map((item: AppRoute.Route) => { |
| | | if (item.componentPath && !item.redirect) { |
| | | const componentPath = `/src/views${item.componentPath}` |
| | | console.log(`Looking for component at: ${componentPath}`) |
| | | // console.log(`Looking for component at: ${componentPath}`) |
| | | if (modules[componentPath]) { |
| | | item.component = modules[componentPath] |
| | | console.log(`Component found for: ${item.path}`) |
| | | // console.log(`Component found for: ${item.path}`) |
| | | } else { |
| | | console.warn(`Component not found for: ${componentPath}`) |
| | | // console.warn(`Component not found for: ${componentPath}`) |
| | | } |
| | | } |
| | | return item |
| | |
| | | |
| | | // Insert the processed route into the root route |
| | | appRootRoute.children = resultRouter as unknown as RouteRecordRaw[] |
| | | console.log('Final app root route:', appRootRoute) |
| | | // console.log('Final app root route:', appRootRoute) |
| | | return appRootRoute |
| | | } |
| | | |
| | |
| | | |
| | | // Route permission filtering |
| | | resultRouter = resultRouter.filter(i => { |
| | | if (i.meta?.requiresAuth === false) |
| | | return true |
| | | // 优先检查路由 ID 是否在权限列表中 |
| | | if (i.id) { |
| | | return hasPermission(i.id) |
| | |
| | | if (import.meta.env.VITE_ROUTE_LOAD_MODE === 'dynamic') { |
| | | try { |
| | | // Get user's route |
| | | const authStore = useAuthStore() |
| | | const result = await fetchUserRoutes({ |
| | | id: 1, |
| | | id: authStore.userInfo?.id || 1, |
| | | }) |
| | | |
| | | if (!result.isSuccess || !result.data) { |
| | | throw new Error('Failed to fetch user routes') |
| | | } |
| | | |
| | | return result.data |
| | | const publicRoutes = staticRoutes.filter(route => route.requiresAuth === false) |
| | | const userRoutes = result.data |
| | | const mergedRoutes = [...publicRoutes.filter(p => !userRoutes.some(u => u.name === p.name)), ...userRoutes] |
| | | |
| | | return mergedRoutes |
| | | } |
| | | catch (error) { |
| | | console.error('Failed to initialize route info:', error) |
| | |
| | | const sortedParams = Object.keys(params) |
| | | .filter(key => params[key] !== undefined && params[key] !== null && params[key] !== '') |
| | | .sort() |
| | | console.log('sortedParams', sortedParams) |
| | | // console.log('sortedParams', sortedParams) |
| | | |
| | | const paramsList = sortedParams.map(key => `${key}=${String(params[key])}`) |
| | | const paramsStr = [...paramsList, `timestamp=${String(timestamp)}`].join('&') |
| | | |
| | | console.log('paramsStr paramsStr', paramsStr) |
| | | console.log('secretKey secretKey', secretKey) |
| | | // console.log('paramsStr paramsStr', paramsStr) |
| | | // console.log('secretKey secretKey', secretKey) |
| | | const hmac = CryptoJS.HmacSHA256(paramsStr, secretKey) |
| | | console.log('hmac hmac', hmac) |
| | | console.log('CryptoJS.enc.Base64.stringify(hmac)', CryptoJS.enc.Base64.stringify(hmac)) |
| | | // console.log('hmac hmac', hmac) |
| | | // console.log('CryptoJS.enc.Base64.stringify(hmac)', CryptoJS.enc.Base64.stringify(hmac)) |
| | | return CryptoJS.enc.Base64.stringify(hmac) |
| | | } |
| | | |
| | |
| | | <script setup lang="tsx"> |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { NAvatar, NButton, NSpace } from 'naive-ui' |
| | | import { NAvatar, NButton, NSpace, NSelect } from 'naive-ui' |
| | | import { useBoolean } from '@/hooks' |
| | | import { fetchOperationsUserList } from '@/service/api/fleet' |
| | | import { fetchOperationsUserList, fetchRoleList } from '@/service/api/fleet' |
| | | |
| | | interface StaffItem { |
| | | id: number |
| | |
| | | phoneNumber: '', |
| | | roleId: undefined as number | undefined, |
| | | }) |
| | | |
| | | // 角色下拉选项 |
| | | const roleOptions = ref<{ label: string; value: number }[]>([]) |
| | | |
| | | // 选中的行 |
| | | const checkedRowKeys = ref<number[]>([]) |
| | |
| | | resizable: true, |
| | | }, |
| | | { |
| | | title: '岗位', |
| | | title: '角色', |
| | | align: 'center', |
| | | key: 'roleName', |
| | | ellipsis: true, |
| | |
| | | window.$message?.error('获取人员列表失败') |
| | | } finally { |
| | | endLoading() |
| | | } |
| | | } |
| | | |
| | | // 获取角色列表 |
| | | async function getRoleList() { |
| | | try { |
| | | const res = await fetchRoleList() |
| | | if (res.code === 200 && res.data) { |
| | | roleOptions.value = res.data.map((item: any) => ({ |
| | | label: item.roleName, |
| | | value: item.id |
| | | })) |
| | | } |
| | | } catch (error) { |
| | | console.error('获取角色列表失败', error) |
| | | } |
| | | } |
| | | |
| | |
| | | watch(visible, (val) => { |
| | | if (val) { |
| | | getStaffList() |
| | | getRoleList() |
| | | } |
| | | }) |
| | | </script> |
| | |
| | | <div class="mb-2 text-sm">联系电话</div> |
| | | <n-input v-model:value="searchModel.phoneNumber" placeholder="请输入" clearable /> |
| | | </div> |
| | | <div class="flex-1"> |
| | | <div class="mb-2 text-sm">角色</div> |
| | | <n-select v-model:value="searchModel.roleId" :options="roleOptions" placeholder="请选择" clearable /> |
| | | </div> |
| | | <div class="flex gap-2"> |
| | | <n-button style="width: 84px; height: 32px" @click="handleReset">重置</n-button> |
| | | <n-button type="primary" style="width: 84px; height: 32px" @click="handleSearch">搜索</n-button> |
| | |
| | | ellipsis: true, |
| | | resizable: true, |
| | | }, |
| | | { |
| | | { |
| | | title: '车辆颜色', |
| | | align: 'left', |
| | | key: 'vehicleColor', |
| | | ellipsis: true, |
| | | resizable: true, |
| | | }, |
| | | // { |
| | | // title: '采购来源', |
| | | // align: 'left', |
| | | // key: 'procurementSource', |
| | | // ellipsis: true, |
| | | // resizable: true, |
| | | // render: (row) => <span>{row.procurementSource || '-'}</span>, |
| | | // }, |
| | | // { |
| | | // title: '运营区域', |
| | | // align: 'left', |
| | | // key: 'locationName', |
| | | // ellipsis: true, |
| | | // resizable: true, |
| | | // render: (row) => <span>{row.locationName || '-'}</span>, |
| | | // }, |
| | | { |
| | | title: '采购来源', |
| | | align: 'left', |
| | | key: 'procurementSource', |
| | | ellipsis: true, |
| | | resizable: true, |
| | | render: row => <span>{row.procurementSource || '-'}</span>, |
| | | }, |
| | | { |
| | | title: '运营区域', |
| | | align: 'left', |
| | | key: 'electronicName', |
| | | ellipsis: true, |
| | | resizable: true, |
| | | render: row => <span>{row.electronicName || '-'}</span>, |
| | | }, |
| | | { |
| | | title: '状态', |
| | | align: 'left', |
| | |
| | | </n-gi> |
| | | <n-gi> |
| | | <div class="info-item"> |
| | | <span class="label">所属区域:</span> |
| | | <span class="label">打卡地址:</span> |
| | | <span class="value">{{ fleetInfo.locationName || "-" }}</span> |
| | | </div> |
| | | </n-gi> |
| | |
| | | fleetName: row.fleetName, |
| | | operationsUserName: row.operationsUserName, |
| | | phoneNumber: row.phoneNumber, |
| | | |
| | | electronicName: row.electronicName |
| | | } |
| | | fleetFormModalVisible.value = true |
| | |
| | | <!-- Footer Actions (Fixed at Bottom via Flexbox Layout) --> |
| | | <div class=" bg-white px-6 py-4 flex justify-end gap-3 border-t border-gray-100 flex-shrink-0 z-50"> |
| | | <n-button>取消</n-button> |
| | | <n-button type="primary" @click="handleSave">保存</n-button> |
| | | <n-button type="primary" @click="handleSave" v-permission="5031">保存</n-button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | |
| | | |
| | | const router = useRouter() |
| | | |
| | | const formRef = ref<FormInst | null>(null) |
| | | const vehicleFormRef = ref<FormInst | null>(null) |
| | | const completeInfoFormRef = ref<FormInst | null>(null) |
| | | |
| | | type UploadField = 'vehiclePhotos' | 'inspectionFiles' | 'insuranceFiles' | 'onlineCarLicenseFiles' |
| | | |
| | | // 表单数据 |
| | | const formModel = ref({ |
| | |
| | | // 完善信息验证 |
| | | inspectionExpireDate: [ |
| | | { |
| | | required: true, |
| | | type: 'number', |
| | | message: '请选择年检到期时间', |
| | | trigger: ['blur', 'change'], |
| | | }, |
| | | { |
| | | validator: (rule, value) => { |
| | | if (value && value <= Date.now()) { |
| | | return new Error('年检到期时间必须大于当前时间') |
| | |
| | | ], |
| | | insuranceCompany: [ |
| | | { |
| | | required: true, |
| | | message: '请输入保险公司', |
| | | required: false, |
| | | message: '', |
| | | trigger: ['blur', 'input'], |
| | | }, |
| | | ], |
| | | insuranceExpireDate: [ |
| | | { |
| | | required: true, |
| | | type: 'number', |
| | | message: '请选择保险到期时间', |
| | | trigger: ['blur', 'change'], |
| | | }, |
| | | { |
| | | validator: (rule, value) => { |
| | | if (value && value <= Date.now()) { |
| | |
| | | trigger: ['blur', 'change'], |
| | | }, |
| | | ], |
| | | onlineCarLicenseExpireDate: [ |
| | | { |
| | | validator: (rule, value) => { |
| | | if (value && value <= Date.now()) { |
| | | return new Error('网约车运营资质到期时间必须大于当前时间') |
| | | } |
| | | return true |
| | | }, |
| | | trigger: ['blur', 'change'], |
| | | }, |
| | | ], |
| | | insuranceFiles: [ |
| | | { |
| | | validator: (rule, value: UploadFileInfo[]) => { |
| | | const hasInsuranceInfo = Boolean( |
| | | formModel.value.insuranceCompany |
| | | || formModel.value.insurancePhone |
| | | || formModel.value.insuranceExpireDate, |
| | | ) |
| | | if (hasInsuranceInfo && (!value || value.length === 0)) { |
| | | return new Error('填写了保险信息后,必须上传保险资料') |
| | | } |
| | | return true |
| | | }, |
| | | trigger: ['blur', 'change'], |
| | | }, |
| | | ], |
| | | onlineCarLicenseFiles: [ |
| | | { |
| | | validator: (rule, value: UploadFileInfo[]) => { |
| | | if (formModel.value.onlineCarLicenseExpireDate && (!value || value.length === 0)) { |
| | | return new Error('设置了网约车运营到期时间后,必须上传网约车运营资质') |
| | | } |
| | | return true |
| | | }, |
| | | trigger: ['blur', 'change'], |
| | | }, |
| | | ], |
| | | insurancePhone: [ |
| | | { |
| | | required: true, |
| | | message: '请输入保险电话', |
| | | trigger: ['blur', 'input'], |
| | | }, |
| | | { |
| | | pattern: /^1[3-9]\d{9}$/, |
| | | message: '请输入正确的手机号码', |
| | | validator: (rule, value) => { |
| | | if (!value) |
| | | return true |
| | | if (!/^1[3-9]\d{9}$/.test(String(value))) { |
| | | return new Error('请输入正确的手机号码') |
| | | } |
| | | return true |
| | | }, |
| | | trigger: ['blur', 'input'], |
| | | }, |
| | | ], |
| | | inspectionFiles: [ |
| | | { |
| | | required: true, |
| | | type: 'array', |
| | | message: '请上传年检资料附件', |
| | | trigger: ['blur', 'change'], |
| | | }, |
| | | { |
| | | validator: (rule, value: UploadFileInfo[]) => { |
| | | // 如果设置了年检到期时间,必须上传年检资料 |
| | |
| | | return beforeUploadCheck(data, { allowedTypes: ['image/jpeg', 'image/png', 'image/jpg'] }) |
| | | } |
| | | // 自定义上传请求 |
| | | async function customRequest({ file, onFinish, onError, onProgress }: any, field: keyof typeof formModel.value) { |
| | | async function customRequest({ file, onFinish, onError, onProgress }: any, field: UploadField) { |
| | | const res = await customUploadRequest({ file, onFinish, onError, onProgress }) |
| | | if (res) { |
| | | // 不需要手动添加文件,v-model:file-list 会自动管理 |
| | |
| | | } |
| | | |
| | | // 删除文件 |
| | | function handleRemove(data: { file: UploadFileInfo }, field: keyof typeof formModel.value) { |
| | | const list = formModel.value[field] as UploadFileInfo[] |
| | | formModel.value[field] = list.filter(item => item.id !== data.file.id) as any |
| | | function handleRemove(data: { file: UploadFileInfo }, field: UploadField) { |
| | | const list = formModel.value[field] |
| | | formModel.value[field] = list.filter(item => item.id !== data.file.id) |
| | | } |
| | | |
| | | // 删除指定文件 |
| | | function removeFile(index: number, field: keyof typeof formModel.value) { |
| | | const list = formModel.value[field] as UploadFileInfo[] |
| | | function removeFile(index: number, field: UploadField) { |
| | | const list = formModel.value[field] |
| | | list.splice(index, 1) |
| | | } |
| | | |
| | |
| | | |
| | | // 处理保存 |
| | | async function handleSave() { |
| | | await formRef.value?.validate(async (errors) => { |
| | | if (!errors) { |
| | | try { |
| | | saving.value = true |
| | | try { |
| | | await vehicleFormRef.value?.validate() |
| | | await completeInfoFormRef.value?.validate() |
| | | } |
| | | catch { |
| | | window.$message.error('请完善必填信息') |
| | | return |
| | | } |
| | | |
| | | const params: any = { |
| | | vehicleCode: formModel.value.vehicleCode, |
| | | vehicleVin: formModel.value.vin, |
| | | plateNumber: formModel.value.plateNumber, |
| | | vehicleColor: formModel.value.vehicleColor, |
| | | vehicleModel: formModel.value.model, |
| | | procurementSource: formModel.value.purchaseSource, |
| | | remarks: formModel.value.remark, |
| | | electronicId:formModel.value.operationArea, |
| | | annualInspectionDate: formModel.value.inspectionExpireDate ? dayjs(formModel.value.inspectionExpireDate).format('YYYY-MM-DD HH:mm:ss') : null, |
| | | insuranceCompany: formModel.value.insuranceCompany, |
| | | insuranceEndTime: formModel.value.insuranceExpireDate ? dayjs(formModel.value.insuranceExpireDate).format('YYYY-MM-DD HH:mm:ss') : null, |
| | | qualificationExpireTime: formModel.value.onlineCarLicenseExpireDate ? dayjs(formModel.value.onlineCarLicenseExpireDate).format('YYYY-MM-DD HH:mm:ss') : null, |
| | | insurancePhone: formModel.value.insurancePhone, |
| | | } |
| | | // 处理附件 |
| | | const mapFiles = (files: UploadFileInfo[]) => { |
| | | return JSON.stringify(files.map(f => ({ |
| | | id: f.id, |
| | | name: f.name, |
| | | url: 'https://haitunyingyu.obs.cn-southwest-2.myhuaweicloud.com/admin/8bbba1ccafda4456bf7e3b2054d78541.jpg' || f.url, |
| | | }))) |
| | | } |
| | | try { |
| | | saving.value = true |
| | | |
| | | if (formModel.value.vehiclePhotos.length > 0) { |
| | | const photo = formModel.value.vehiclePhotos[0] |
| | | params.vehiclePictures = JSON.stringify({ |
| | | id: photo.id, |
| | | name: photo.name, |
| | | url: 'https://haitunyingyu.obs.cn-southwest-2.myhuaweicloud.com/admin/8bbba1ccafda4456bf7e3b2054d78541.jpg' || photo.url, |
| | | }) |
| | | } |
| | | params.inspectionAttachments = mapFiles(formModel.value.inspectionFiles) |
| | | params.policyInformation = mapFiles(formModel.value.insuranceFiles) |
| | | params.operationQualification = mapFiles(formModel.value.onlineCarLicenseFiles) |
| | | |
| | | const { code } = await addVehicle(params) |
| | | if (code === 200) { |
| | | window.$message.success('新增车辆成功') |
| | | // 返回列表页 |
| | | router.go(-1) |
| | | } |
| | | } |
| | | catch (error) { |
| | | // 错误已经在 request 拦截器中处理 |
| | | } |
| | | finally { |
| | | saving.value = false |
| | | } |
| | | const params: any = { |
| | | vehicleCode: formModel.value.vehicleCode, |
| | | vehicleVin: formModel.value.vin, |
| | | plateNumber: formModel.value.plateNumber, |
| | | vehicleColor: formModel.value.vehicleColor, |
| | | vehicleModel: formModel.value.model, |
| | | procurementSource: formModel.value.purchaseSource, |
| | | remarks: formModel.value.remark, |
| | | electronicId: formModel.value.operationArea, |
| | | annualInspectionDate: formModel.value.inspectionExpireDate ? dayjs(formModel.value.inspectionExpireDate).format('YYYY-MM-DD HH:mm:ss') : null, |
| | | insuranceCompany: formModel.value.insuranceCompany, |
| | | insuranceEndTime: formModel.value.insuranceExpireDate ? dayjs(formModel.value.insuranceExpireDate).format('YYYY-MM-DD HH:mm:ss') : null, |
| | | qualificationExpireTime: formModel.value.onlineCarLicenseExpireDate ? dayjs(formModel.value.onlineCarLicenseExpireDate).format('YYYY-MM-DD HH:mm:ss') : null, |
| | | insurancePhone: formModel.value.insurancePhone, |
| | | } |
| | | else { |
| | | window.$message.error('请完善必填信息') |
| | | // 处理附件 |
| | | const mapFiles = (files: UploadFileInfo[]) => { |
| | | return JSON.stringify(files.map(f => ({ |
| | | id: f.id, |
| | | name: f.name, |
| | | url: 'https://haitunyingyu.obs.cn-southwest-2.myhuaweicloud.com/admin/8bbba1ccafda4456bf7e3b2054d78541.jpg' || f.url, |
| | | }))) |
| | | } |
| | | }) |
| | | |
| | | if (formModel.value.vehiclePhotos.length > 0) { |
| | | const photo = formModel.value.vehiclePhotos[0] |
| | | params.vehiclePictures = JSON.stringify({ |
| | | id: photo.id, |
| | | name: photo.name, |
| | | url: 'https://haitunyingyu.obs.cn-southwest-2.myhuaweicloud.com/admin/8bbba1ccafda4456bf7e3b2054d78541.jpg' || photo.url, |
| | | }) |
| | | } |
| | | params.inspectionAttachments = mapFiles(formModel.value.inspectionFiles) |
| | | params.policyInformation = mapFiles(formModel.value.insuranceFiles) |
| | | params.operationQualification = mapFiles(formModel.value.onlineCarLicenseFiles) |
| | | |
| | | const { code } = await addVehicle(params) |
| | | if (code === 200) { |
| | | window.$message.success('新增车辆成功') |
| | | // 返回列表页 |
| | | router.go(-1) |
| | | } |
| | | } |
| | | catch (error) { |
| | | // 错误已经在 request 拦截器中处理 |
| | | } |
| | | finally { |
| | | saving.value = false |
| | | } |
| | | } |
| | | |
| | | // 处理返回 |
| | |
| | | // 监听年检/保险到期时间变化,触发附件验证 |
| | | watch([ |
| | | () => formModel.value.inspectionExpireDate, |
| | | () => formModel.value.insuranceCompany, |
| | | () => formModel.value.insuranceExpireDate, |
| | | () => formModel.value.insurancePhone, |
| | | () => formModel.value.onlineCarLicenseExpireDate, |
| | | ], () => { |
| | | formRef.value?.validate( |
| | | (errors) => { |
| | | // 验证完成 |
| | | }, |
| | | completeInfoFormRef.value?.validate( |
| | | () => {}, |
| | | (rule) => { |
| | | return rule?.key === 'inspectionFiles' |
| | | return ['inspectionFiles', 'insuranceFiles', 'onlineCarLicenseFiles'].includes(String(rule?.key)) |
| | | }, |
| | | ) |
| | | }) |
| | |
| | | </div> |
| | | </div> |
| | | <n-form |
| | | ref="formRef" :model="formModel" :rules="formRules" label-placement="left" :show-feedback="false" |
| | | ref="vehicleFormRef" :model="formModel" :rules="formRules" label-placement="left" |
| | | in-line require-mark-placement="left" |
| | | > |
| | | <div class="vehicle-info-layout"> |
| | |
| | | |
| | | <!-- 右侧:表单 --> |
| | | <div class="form-section"> |
| | | <n-grid :cols="3" :x-gap="24" :y-gap="16"> |
| | | <n-grid :cols="3" :x-gap="24" :y-gap="8"> |
| | | <!-- 车辆VIN码 --> |
| | | <n-gi> |
| | | <n-form-item label="车辆VIN码" path="vin"> |
| | |
| | | </div> |
| | | </div> |
| | | <n-form |
| | | ref="formRef" :model="formModel" :rules="formRules" label-placement="left" :show-feedback="false" |
| | | ref="completeInfoFormRef" :model="formModel" :rules="formRules" label-placement="left" |
| | | in-line require-mark-placement="left" |
| | | > |
| | | <!-- 年检到期时间 - 单独一行 --> |
| | |
| | | |
| | | /* 完善信息表单布局 */ |
| | | .form-row { |
| | | margin-bottom: 16px; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .form-row-group { |
| | | display: flex; |
| | | justify-content: flex-start; |
| | | gap: 24px; |
| | | margin-bottom: 16px; |
| | | margin-bottom:8px; |
| | | } |
| | | |
| | | .form-item-inline { |
| | |
| | | import dayjs from 'dayjs' |
| | | import { useBoolean } from '@/hooks' |
| | | import { downloadFile } from '@/utils' |
| | | import { batchOperation, batchSetElectronic, deleteVehicle, exportVehicleExcel, getVehiclePageList, importVehicleExcel } from '@/service/api/vehicle' |
| | | import { batchOperation, batchSetElectronic, deleteVehicle, exportVehicleExcel, getVehicleImportTemplate, getVehiclePageList, importVehicleExcel } from '@/service/api/vehicle' |
| | | import { fetchElectronicFenceAreaList } from '@/service/api/point' |
| | | import SearchPanel from '@/components/custom/SearchPanel.vue' |
| | | import CompleteVehicleInfoModal from './components/CompleteVehicleInfoModal.vue' |
| | | import EditVehicleModal from './components/EditVehicleModal.vue' |
| | | import ConfirmDialog from '@/components/ConfirmDialog/index.vue' |
| | | |
| | | const router = useRouter() |
| | | const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false) |
| | |
| | | const vehicleStatusOptions = [ |
| | | { label: '待完善信息', value: '1' }, |
| | | { label: '待运营', value: '2' }, |
| | | { label: '保险过期', value: '3' }, |
| | | { label: '保险/年检过期', value: '3' }, |
| | | ] |
| | | |
| | | const filterItems = computed(() => [ |
| | |
| | | const statusMap: Record<string, { type: 'default' | 'success' | 'info' | 'warning' | 'error', text: string }> = { |
| | | 1: { type: 'warning', text: '待完善信息' }, |
| | | 2: { type: 'info', text: '待运营' }, |
| | | 3: { type: 'error', text: '保险过期' }, |
| | | 3: { type: 'error', text: '保险/年检过期' }, |
| | | } |
| | | |
| | | // 运营区域选项 |
| | |
| | | const editVehicle = ref<any>(null) |
| | | const editMode = ref<'add' | 'edit'>('edit') |
| | | |
| | | // 通用二次确认对话框 |
| | | const showConfirmDialog = ref(false) |
| | | const confirmDialogConfig = ref({ |
| | | title: '', |
| | | description: '', |
| | | cancelText: '取消', |
| | | confirmText: '确定', |
| | | onConfirm: async () => {}, |
| | | onCancel: () => { |
| | | showConfirmDialog.value = false |
| | | }, |
| | | }) |
| | | // 表单验证规则 |
| | | const areaFormRules = { |
| | | operationArea: { |
| | |
| | | return |
| | | } |
| | | const idStr = selectedVehicles.value.map(item => item.id).join(',') |
| | | const res = await batchOperation(idStr) |
| | | if (res && (res as any).code === 200) { |
| | | window.$message.success('批量运营成功') |
| | | selectedVehicles.value = [] |
| | | checkedRowKeys.value = [] |
| | | getVehicleList() |
| | | confirmDialogConfig.value = { |
| | | title: '确认开始运营', |
| | | description: `确认对选中的 ${selectedVehicles.value.length} 辆车辆执行开始运营吗?`, |
| | | cancelText: '取消', |
| | | confirmText: '确定', |
| | | onConfirm: async () => { |
| | | const res = await batchOperation(idStr) |
| | | if (res && (res as any).code === 200) { |
| | | window.$message.success('批量运营成功') |
| | | selectedVehicles.value = [] |
| | | checkedRowKeys.value = [] |
| | | getVehicleList() |
| | | } else { |
| | | window.$message.error((res as any).message || '批量运营失败') |
| | | } |
| | | showConfirmDialog.value = false |
| | | }, |
| | | onCancel: () => { |
| | | showConfirmDialog.value = false |
| | | }, |
| | | } |
| | | showConfirmDialog.value = true |
| | | } |
| | | |
| | | // 下载模板 |
| | | async function handleDownloadTemplate() { |
| | | try { |
| | | const response = await getVehicleImportTemplate() |
| | | if (response instanceof Blob) { |
| | | downloadFile(response, `车辆导入模板_${new Date().getTime()}.xlsx`) |
| | | window.$message.success('下载成功') |
| | | } |
| | | else { |
| | | window.$message.error('下载失败:接口未返回正确的文件模板') |
| | | } |
| | | } |
| | | catch (error) { |
| | | console.error('下载模板失败:', error) |
| | | } |
| | | } |
| | | |
| | |
| | | window.$message.warning('车辆信息不完整,无法开始运营') |
| | | return |
| | | } |
| | | const res = await batchOperation(String(row.id)) |
| | | if (res && (res as any).code === 200) { |
| | | window.$message.success(`开始运营: ${row.plateNumber}`) |
| | | getVehicleList() |
| | | confirmDialogConfig.value = { |
| | | title: '确认开始运营', |
| | | description: `确认对车辆 ${row.plateNumber || row.vehicleCode || row.id} 执行开始运营吗?`, |
| | | cancelText: '取消', |
| | | confirmText: '确定', |
| | | onConfirm: async () => { |
| | | const res = await batchOperation(String(row.id)) |
| | | if (res && (res as any).code === 200) { |
| | | window.$message.success(`开始运营: ${row.plateNumber}`) |
| | | getVehicleList() |
| | | } else { |
| | | window.$message.error((res as any).message || '开始运营失败') |
| | | } |
| | | showConfirmDialog.value = false |
| | | }, |
| | | onCancel: () => { |
| | | showConfirmDialog.value = false |
| | | }, |
| | | } |
| | | showConfirmDialog.value = true |
| | | } |
| | | |
| | | function handleUpdateInfo(row: any) { |
| | |
| | | } |
| | | |
| | | function handleDetail(row: any) { |
| | | const payload = window.btoa(encodeURIComponent(JSON.stringify(row))) |
| | | router.push({ |
| | | name: 'operatingVehicleDetail', |
| | | query: { |
| | | id: row.id, |
| | | payload, |
| | | type: 'inventory', |
| | | }, |
| | | }) |
| | | } |
| | |
| | | </NButton> |
| | | </NSpace> |
| | | <NSpace> |
| | | <NButton v-permission="50114" @click="handleDownloadTemplate"> |
| | | 下载导入模板 |
| | | </NButton> |
| | | <NButton v-permission="50114" @click="handleImport"> |
| | | 导入 |
| | | </NButton> |
| | |
| | | </n-card> |
| | | </NSpace> |
| | | |
| | | <!-- 通用确认弹窗 --> |
| | | <ConfirmDialog |
| | | v-model:visible="showConfirmDialog" |
| | | :title="confirmDialogConfig.title" |
| | | :description="confirmDialogConfig.description" |
| | | :cancel-text="confirmDialogConfig.cancelText" |
| | | :confirm-text="confirmDialogConfig.confirmText" |
| | | @confirm="confirmDialogConfig.onConfirm" |
| | | @cancel="confirmDialogConfig.onCancel" |
| | | /> |
| | | |
| | | <!-- 批量设置运营区域弹窗 --> |
| | | <n-modal |
| | | v-model:show="showAreaModal" |
| New file |
| | |
| | | acceptOrTest integer($int32) |
| | | 当前模式 1接单 2测试 |
| | | |
| | | acceptOrTestStatus integer($int32) |
| | | 模式状态 1开始接单/开始测试 2停止接单/停止测试 |
| | | |
| | | annualInspectionDate string($date-time) |
| | | 年检到期时间 |
| | | |
| | | createBy string |
| | | 记录创建人,前端忽略 |
| | | |
| | | createTime string($date-time) |
| | | 记录创建时间,前端忽略 |
| | | |
| | | currentElectricity integer($int32) |
| | | 当前电量 |
| | | |
| | | deleted boolean |
| | | 是否删除 0未删除 1已删除 |
| | | |
| | | electronicId integer($int64) |
| | | 围栏id |
| | | |
| | | electronicName string |
| | | 运营区域 |
| | | |
| | | fleetId integer($int64) |
| | | 车队id |
| | | |
| | | id integer($int64) |
| | | 自增id |
| | | |
| | | inspectionAttachments string |
| | | 年检资料附件 |
| | | |
| | | insuranceCompany string |
| | | 保险公司 |
| | | |
| | | insuranceEndTime string($date-time) |
| | | 保险到期时间 |
| | | |
| | | insurancePhone string |
| | | 保险电话 |
| | | |
| | | mileageRange number($float) |
| | | 续航公里 |
| | | |
| | | operatingStatus integer($int32) |
| | | 运营状态 1运营中 2维修中 3已收车 4车辆异常 |
| | | |
| | | operationQualification string |
| | | 运营资质 |
| | | |
| | | plateNumber string |
| | | 车牌号 |
| | | |
| | | policyInformation string |
| | | 保单资料 |
| | | |
| | | procurementSource string |
| | | 采购来源 |
| | | |
| | | qualificationExpireTime string($date-time) |
| | | 运营资质到期时间 |
| | | |
| | | remarks string |
| | | 备注 |
| | | |
| | | state integer($int32) |
| | | 车辆接单状态 1接单中 0空闲 |
| | | |
| | | status integer($int32) |
| | | 车辆状态 1=待完善 2=待运营 3=保险/年检过期 |
| | | |
| | | updateBy string |
| | | 记录修改人,前端忽略 |
| | | |
| | | updateTime string($date-time) |
| | | 记录修改时间,前端忽略 |
| | | |
| | | vehicleCode string |
| | | 车辆编码 |
| | | |
| | | vehicleColor string |
| | | 车辆颜色 |
| | | |
| | | vehicleModel string |
| | | 型号 |
| | | |
| | | vehiclePictures string |
| | | 车辆图片 |
| | | |
| | | vehicleType integer($int32) |
| | | 车辆类型 1=在库 2=运营 |
| | | |
| | | vehicleVin string |
| | | 车辆vin码 |
| | | |
| | | warehousingTime string($date-time) |
| | | 入库时间 |
| | | |
| | | xpuId string |
| | | 车辆xpuid |
| | |
| | | import TaskDetailModal from './components/TaskDetailModal.vue' |
| | | import Pagination from '@/components/common/Pagination.vue' |
| | | import dayjs from 'dayjs' |
| | | import { getVehicleById } from '@/service/api/vehicle' |
| | | import { deleteChargingRecordById, deleteTaskById, queryChargingByVehicleId, queryDetectionByVehicleId, queryPreparationByVehicleId, queryRepairByVehicleId, queryTaskByVehicleId, queryTaskRoutineByVehicleId } from '@/service/api/vehicleConvoy' |
| | | import { parseFileJson } from '@/utils/common' |
| | | |
| | |
| | | const router = useRouter() |
| | | const { t } = useI18n() |
| | | |
| | | const payload = computed(() => route.query.payload as string | undefined) |
| | | const detailId = computed(() => route.query.id as string | number | undefined) |
| | | const detailType = computed(() => route.query.type as string | undefined) |
| | | |
| | | const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false) |
| | | |
| | |
| | | vehicleModel: '', |
| | | color: '', |
| | | purchaseSource: '', |
| | | operatingArea: '', |
| | | vehicleStatus: 'operating', |
| | | electronicName: '', |
| | | status: 1, |
| | | operatingStatus: 1, |
| | | abnormalStatus: 'normal', |
| | | inspectionExpireDate: '', |
| | |
| | | remark: '', |
| | | vehicleImage: '', |
| | | needCompleteInfo: false, |
| | | inspectionOrInsuranceExpired: false, |
| | | isSecondHand: false, |
| | | }) |
| | | |
| | | // 车辆状态映射 |
| | | const vehicleStatusMap: Record<string, { type: 'default' | 'success' | 'info' | 'warning' | 'error', text: string }> = { |
| | | operating: { type: 'info', text: '运营' }, |
| | | inventory: { type: 'default', text: '在库' }, |
| | | recalled: { type: 'info', text: '已收车' }, |
| | | scrapped: { type: 'error', text: '已报废' }, |
| | | const vehicleStatusMap: Record<number, { type: 'default' | 'success' | 'info' | 'warning' | 'error', text: string }> = { |
| | | 1: { type: 'error', text: '待完善信息' }, |
| | | 2: { type: 'success', text: '待运营' }, |
| | | 3: { type: 'warning', text: '保险/年检过期' }, |
| | | } |
| | | |
| | | const operatingStatusMap: Record<number, { type: 'default' | 'success' | 'info' | 'warning' | 'error', text: string }> = { |
| | |
| | | 4: { type: 'error', text: '车辆异常' }, |
| | | } |
| | | |
| | | // 车辆异常状态映射 |
| | | const abnormalStatusMap: Record<string, { type: 'default' | 'success' | 'info' | 'warning' | 'error', text: string }> = { |
| | | normal: { type: 'success', text: '正常' }, |
| | | repairing: { type: 'error', text: '维修中' }, |
| | | malfunction: { type: 'error', text: '故障' }, |
| | | accident: { type: 'error', text: '事故' }, |
| | | } |
| | | const vehicleTag = computed(() => { |
| | | if (detailType.value === 'operating') { |
| | | return { text: '运营', cls: 'status--operating' } |
| | | } |
| | | const m = vehicleStatusMap[vehicleDetail.value.status] |
| | | return { text: m?.text || '-', cls: `status--${m?.type || 'default'}` } |
| | | }) |
| | | |
| | | const operatingTag = computed(() => { |
| | | if (detailType.value === 'inventory') { |
| | | return { text: '在库', cls: 'status--inventory' } |
| | | } |
| | | const m = operatingStatusMap[vehicleDetail.value.operatingStatus] |
| | | return { text: m?.text || '-', cls: `status--${m?.type || 'default'}` } |
| | | }) |
| | | |
| | | // 年检记录搜索条件 |
| | | const inspectionSearchModel = ref({ |
| | |
| | | async function loadVehicleDetail() { |
| | | startLoading() |
| | | try { |
| | | if (payload.value) { |
| | | const jsonStr = decodeURIComponent(window.atob(String(payload.value))) |
| | | const row = JSON.parse(jsonStr) |
| | | |
| | | console.log('111111111111',row) |
| | | const imgs = parseFileJson(row.vehiclePictures) |
| | | if (!detailId.value) { |
| | | window.$message.error('缺少车辆ID,无法加载详情') |
| | | return |
| | | } |
| | | const { data } = await getVehicleById(detailId.value) |
| | | if (data) { |
| | | const imgs = parseFileJson(data.vehiclePictures) |
| | | vehicleDetail.value = { |
| | | id: row.id, |
| | | id: data.id, |
| | | vehicleImage: imgs.length > 0 ? imgs[0].url : '', |
| | | vehicleCode: row.vehicleCode || '', |
| | | plateNumber: row.plateNumber || '', |
| | | vinCode: row.vehicleVin || '', |
| | | vehicleModel: row.vehicleModel || '', |
| | | color: row.vehicleColor || '', |
| | | purchaseSource: row.procurementSource || '', |
| | | operatingArea: row.electronicName || '', |
| | | vehicleStatus: row.vehicleType === 2 ? 'operating' : 'inventory', |
| | | operatingStatus: row.operatingStatus || 1, |
| | | vehicleCode: data.vehicleCode || '', |
| | | plateNumber: data.plateNumber || '', |
| | | vinCode: data.vehicleVin || '', |
| | | vehicleModel: data.vehicleModel || '', |
| | | color: data.vehicleColor || '', |
| | | purchaseSource: data.procurementSource || '', |
| | | electronicId: data.electronicId, |
| | | electronicName: data.electronicName || '', |
| | | status: data.status ?? vehicleDetail.value.status, |
| | | operatingStatus: data.operatingStatus || vehicleDetail.value.operatingStatus, |
| | | abnormalStatus: 'normal', |
| | | inspectionExpireDate: row.annualInspectionDate || '', |
| | | insuranceExpireDate: row.insuranceEndTime || '', |
| | | entryTime: row.warehousingTime || '', |
| | | remark: row.remarks || '', |
| | | inspectionExpireDate: data.annualInspectionDate || '', |
| | | insuranceExpireDate: data.insuranceEndTime || '', |
| | | entryTime: data.warehousingTime || '', |
| | | remark: data.remarks || '', |
| | | needCompleteInfo: false, |
| | | inspectionOrInsuranceExpired: false, |
| | | isSecondHand: false, |
| | | } |
| | | // 状态直接使用接口返回的 data.status 与 data.operatingStatus |
| | | } |
| | | if (vehicleDetail.value.id) { |
| | | await Promise.all([ |
| | |
| | | router.back() |
| | | } |
| | | |
| | | // 查看容器备/信息 |
| | | function handleViewContainer() { |
| | | window.$message.info('查看容器备/信息') |
| | | } |
| | | |
| | | // 已收车 |
| | | function handleRecall() { |
| | | window.$dialog.warning({ |
| | |
| | | |
| | | // 查看运营区域 |
| | | function handleViewArea() { |
| | | window.$message.info('查看运营区域') |
| | | router.push({ |
| | | path: '/businessOperations/geofenceManagement/detail', |
| | | query: { |
| | | type:'detail', |
| | | id: vehicleDetail.value.electronicId, |
| | | }, |
| | | }) |
| | | } |
| | | |
| | | // 搜索年检记录 |
| | |
| | | <div class="plate-number"> |
| | | {{ vehicleDetail.plateNumber }} |
| | | </div> |
| | | <NTag v-if="vehicleDetail.needCompleteInfo" type="warning" size="small" class="tag-info"> |
| | | 待完善信息 |
| | | </NTag> |
| | | <NTag v-if="vehicleDetail.inspectionOrInsuranceExpired" type="error" size="small" class="tag-info"> |
| | | 年检/保险已过期 |
| | | </NTag> |
| | | <NTag :type="vehicleStatusMap[vehicleDetail.vehicleStatus]?.type" size="small" class="tag-info"> |
| | | {{ vehicleStatusMap[vehicleDetail.vehicleStatus]?.text }} |
| | | </NTag> |
| | | <span class="status-chip" :class="vehicleTag.cls">{{ vehicleTag.text }}</span> |
| | | <span class="status-chip" :class="operatingTag.cls">{{ operatingTag.text }}</span> |
| | | </div> |
| | | <div class="basic-info-row"> |
| | | <span class="basic-label">{{ vehicleDetail.color || '白色' }}</span> |
| | |
| | | </div> |
| | | <div class="detail-item"> |
| | | <span class="detail-label">运营区域:</span> |
| | | <span class="detail-value">{{ vehicleDetail.operatingArea || '-' }}</span> |
| | | <span class="detail-value area-ellipsis">{{ vehicleDetail.electronicName || '-' }}</span> |
| | | <NButton |
| | | v-if="vehicleDetail.operatingArea" text type="primary" size="small" |
| | | v-if="vehicleDetail.electronicName" text type="primary" size="small" |
| | | style="padding: 0; margin-left: 4px;" @click="handleViewArea" |
| | | > |
| | | 查看 |
| | |
| | | |
| | | .detail-value { |
| | | color: rgba(23, 31, 38, 1); |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | max-width: 300px; |
| | | } |
| | | |
| | | .expire-warning { |
| | |
| | | } |
| | | } |
| | | |
| | | .status-chip { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | height: 20px; |
| | | line-height: 22px; |
| | | padding: 0 8px; |
| | | border-radius: 4px; |
| | | font-size: 12px; |
| | | margin-left: 8px; |
| | | } |
| | | .status--default { |
| | | background: rgba(230, 244, 255, 1); |
| | | color:rgba(0, 104, 255, 1) |
| | | } |
| | | .status--info { |
| | | background:rgba(220, 245, 221, 1); |
| | | color: rgba(11, 181, 45, 1) |
| | | } |
| | | .status--warning { |
| | | background: rgba(255, 236, 230, 1); |
| | | color:rgba(224, 39, 22, 1)} |
| | | .status--error { |
| | | background: rgba(255, 248, 230, 1); |
| | | color:rgba(255, 144, 0, 1) |
| | | } |
| | | .status--success { |
| | | background: rgba(230, 244, 255, 1); |
| | | color:rgba(0, 104, 255, 1) |
| | | } |
| | | .status--operating { |
| | | background: rgba(230, 244, 255, 1); |
| | | color:rgba(0, 104, 255, 1) |
| | | } |
| | | .status--inventory { |
| | | background: rgba(230, 244, 255, 1); |
| | | color:rgba(0, 104, 255, 1) |
| | | } |
| | | |
| | | .search-form-card { |
| | | border-radius: 4px; |
| | | margin-bottom: 12px; |
| | |
| | | <script setup lang="tsx"> |
| | | import type { DataTableColumns, FormInst } from 'naive-ui' |
| | | import { NButton, NPopconfirm, NSpace, NTag } from 'naive-ui' |
| | | import ConfirmDialog from '@/components/ConfirmDialog/index.vue' |
| | | import { useRouter } from 'vue-router' |
| | | import { useBoolean } from '@/hooks' |
| | | import { downloadFile } from '@/utils' |
| | |
| | | |
| | | function handleResetSearch() { |
| | | model.value = { ...initialModel } |
| | | pageParams.value.pageNum = 1 |
| | | getOperatingVehicleList() |
| | | } |
| | | |
| | | const formRef = ref<FormInst | null>() |
| | |
| | | const count = ref(0) |
| | | const listData = ref<any[]>([]) |
| | | const selectedRowKeys = ref<number[]>([]) |
| | | |
| | | const showConfirmDialog = ref(false) |
| | | const confirmDialogConfig = ref({ |
| | | title: '', |
| | | description: '', |
| | | cancelText: '取消', |
| | | confirmText: '确定', |
| | | onConfirm: async () => {}, |
| | | onCancel: () => { showConfirmDialog.value = false }, |
| | | }) |
| | | |
| | | const statusMap: Record<number, { type: 'default' | 'success' | 'info' | 'warning' | 'error', text: string }> = { |
| | | 1: { type: 'success', text: '运营中' }, |
| | |
| | | window.$message.warning('请先选择要转为在库车辆的数据') |
| | | return |
| | | } |
| | | window.$dialog.warning({ |
| | | confirmDialogConfig.value = { |
| | | title: '批量转为在库车辆', |
| | | content: `确认将选中的 ${selectedRowKeys.value.length} 辆车转为在库车辆吗?`, |
| | | positiveText: '确定', |
| | | negativeText: '取消', |
| | | onPositiveClick: async () => { |
| | | description: `确认将选中的 ${selectedRowKeys.value.length} 辆车转为在库车辆吗?`, |
| | | cancelText: '取消', |
| | | confirmText: '确定', |
| | | onConfirm: async () => { |
| | | const { code } = await batchStock({ idStr: selectedRowKeys.value.join(',') }) |
| | | if (code==200) { |
| | | window.$message.success(`已成功将 ${selectedRowKeys.value.length} 辆车转为在库车辆`) |
| | | selectedRowKeys.value = [] |
| | | getOperatingVehicleList() |
| | | } |
| | | showConfirmDialog.value = false |
| | | }, |
| | | }) |
| | | onCancel: () => { showConfirmDialog.value = false }, |
| | | } |
| | | showConfirmDialog.value = true |
| | | } |
| | | |
| | | function handleConvertToInventory(row: any) { |
| | | window.$dialog.warning({ |
| | | confirmDialogConfig.value = { |
| | | title: '转为在库车辆', |
| | | content: `确认将车辆 ${row.plateNumber} 转为在库车辆吗?`, |
| | | positiveText: '确定', |
| | | negativeText: '取消', |
| | | onPositiveClick: async () => { |
| | | description: `确认将车辆 ${row.plateNumber} 转为在库车辆吗?`, |
| | | cancelText: '取消', |
| | | confirmText: '确定', |
| | | onConfirm: async () => { |
| | | const { code } = await batchStock({ idStr: String(row.id) }) |
| | | if (code==200) { |
| | | window.$message.success(`车辆 ${row.plateNumber} 已转为在库车辆`) |
| | | getOperatingVehicleList() |
| | | } |
| | | showConfirmDialog.value = false |
| | | }, |
| | | }) |
| | | onCancel: () => { showConfirmDialog.value = false }, |
| | | } |
| | | showConfirmDialog.value = true |
| | | } |
| | | |
| | | function handleViewDetail(row: any) { |
| | | const payload = window.btoa(encodeURIComponent(JSON.stringify(row))) |
| | | router.push({ |
| | | name: 'operatingVehicleDetail', |
| | | query: { |
| | | id: row.id, |
| | | payload, |
| | | type: 'operating', |
| | | }, |
| | | }) |
| | | } |
| | |
| | | :header-height="18" |
| | | :row-height="18" |
| | | /> |
| | | <ConfirmDialog |
| | | v-model:visible="showConfirmDialog" |
| | | :title="confirmDialogConfig.title" |
| | | :description="confirmDialogConfig.description" |
| | | :cancel-text="confirmDialogConfig.cancelText" |
| | | :confirm-text="confirmDialogConfig.confirmText" |
| | | @confirm="confirmDialogConfig.onConfirm" |
| | | @cancel="confirmDialogConfig.onCancel" |
| | | /> |
| | | <Pagination :count="count" @change="changePage" /> |
| | | </NSpace> |
| | | </n-card> |
| | |
| | | drawDisallowedAreaPolygon(id) |
| | | return |
| | | } |
| | | |
| | | // 检查禁行区域是否在运营区域内 |
| | | if (!fence) { |
| | | message.warning('请先绘制运营区域') |
| | | map.remove(e.obj) |
| | | drawDisallowedAreaPolygon(id) |
| | | return |
| | | } |
| | | |
| | | const operationPath = fence.getPath() |
| | | if (!isDisallowedAreaInOperationArea(e.obj.getPath(), operationPath)) { |
| | | message.warning('禁行区域必须在运营区域内') |
| | | map.remove(e.obj) |
| | | drawDisallowedAreaPolygon(id) |
| | | return |
| | | } |
| | | |
| | | const disallowedArea = disallowedAreaList.value.find((sub: any) => sub.id === id) |
| | | disallowedArea.fence = e.obj |
| | | disallowedArea.area = area |
| | |
| | | e.target.setPath(path); |
| | | } |
| | | }); |
| | | /** 拖拽/调整时校验面积 **/ |
| | | /** 拖拽/调整时校验面积和是否在运营区域内 **/ |
| | | fenceEditor.on('adjust', (e: any) => { |
| | | const path = e.target.getPath() |
| | | const area = calcPolygonArea(path) |
| | | |
| | | // 检查面积限制 |
| | | if (area > MAX_AREA) { |
| | | message.warning(`禁行区域面积不能超过${MAX_AREA}平方公里`) |
| | | e.target.setPath(disallowedArea.lastValidPath) |
| | | closeFenceEditor() |
| | | handleDisallowedAreaEdit(disallowedArea) |
| | | } else { |
| | | disallowedArea.lastValidPath = path |
| | | disallowedArea.area = area |
| | | return |
| | | } |
| | | |
| | | // 检查是否在运营区域内 |
| | | if (fence) { |
| | | const operationPath = fence.getPath() |
| | | if (!isDisallowedAreaInOperationArea(path, operationPath)) { |
| | | message.warning('禁行区域必须在运营区域内') |
| | | e.target.setPath(disallowedArea.lastValidPath) |
| | | closeFenceEditor() |
| | | handleDisallowedAreaEdit(disallowedArea) |
| | | return |
| | | } |
| | | } |
| | | |
| | | disallowedArea.lastValidPath = path |
| | | disallowedArea.area = area |
| | | }) |
| | | |
| | | /** 编辑完成后最终校验(兜底) **/ |
| | | fenceEditor.on('end', (e: any) => { |
| | | const path = e.target.getPath() |
| | | const area = calcPolygonArea(path) |
| | | |
| | | // 检查面积限制 |
| | | if (area > MAX_AREA) { |
| | | message.warning(`禁行区域面积不能超过${MAX_AREA}平方公里`) |
| | | e.target.setPath(disallowedArea.lastValidPath) |
| | | closeFenceEditor() |
| | | handleDisallowedAreaEdit(disallowedArea) |
| | | } else { |
| | | disallowedArea.lastValidPath = path |
| | | disallowedArea.area = area |
| | | return |
| | | } |
| | | |
| | | // 检查是否在运营区域内 |
| | | if (fence) { |
| | | const operationPath = fence.getPath() |
| | | if (!isDisallowedAreaInOperationArea(path, operationPath)) { |
| | | message.warning('禁行区域必须在运营区域内') |
| | | e.target.setPath(disallowedArea.lastValidPath) |
| | | closeFenceEditor() |
| | | handleDisallowedAreaEdit(disallowedArea) |
| | | return |
| | | } |
| | | } |
| | | |
| | | disallowedArea.lastValidPath = path |
| | | disallowedArea.area = area |
| | | }) |
| | | } |
| | | |
| | |
| | | ) |
| | | } |
| | | |
| | | // 判断禁行区域是否完全在运营区域内 |
| | | function isDisallowedAreaInOperationArea(disallowedPath: any, operationPath: any): boolean { |
| | | // 使用高德地图的几何工具来判断禁行区域是否完全在运营区域内 |
| | | return AMap.GeometryUtil.isRingInRing( |
| | | disallowedPath, |
| | | operationPath |
| | | ) |
| | | } |
| | | |
| | | // ==================== 图层切换 ==================== |
| | | const layerOptions = reactive({ |
| | | isActive: false, // 默认为关闭状态 |
| | |
| | | // 居中定位 |
| | | const centerMap = () => { |
| | | if (map) { |
| | | map.setCenter(centerPoint); |
| | | // 如果是详情或编辑模式 |
| | | if (modalType.value === 'detail' || modalType.value === 'edit') { |
| | | map.setFitView([fence], false, [220, 220, 220, 220]) |
| | | } else { |
| | | map.setCenter(centerPoint); |
| | | map.setZoom(14); |
| | | } |
| | | } |
| | | } |
| | | // 放大 |
| | |
| | | effectTime: { |
| | | type: 'number', |
| | | required: true, |
| | | message: '请选择生效时间', |
| | | trigger: ['blur', 'change'], |
| | | validator: (_rule: any, value: any) => { |
| | | // 验证不能为空 |
| | | if (value == null) { |
| | | return new Error('请选择生效时间') |
| | | } |
| | | // 如果是立即生效,则跳过时间比较验证 |
| | | if (value === IMMEDIATE_EFFECT) { |
| | | return true |
| | | } |
| | | // 如果是具体时间值,则验证不能早于当前时间 |
| | | if (typeof value === 'number') { |
| | | // 计算当前时间加一分钟作为最小允许时间 |
| | | const now = new Date(); |
| | | const minAllowedDateTime = new Date( |
| | | now.getFullYear(), |
| | | now.getMonth(), |
| | | now.getDate(), |
| | | now.getHours(), |
| | | now.getMinutes() + 1, // 当前时间加一分钟 |
| | | 0, // 秒数设为0 |
| | | 0 // 毫秒设为0 |
| | | ); |
| | | if (value < minAllowedDateTime.getTime()) { |
| | | return new Error('生效时间不能早于当前时间') |
| | | } |
| | | } |
| | | return true |
| | | }, |
| | | } as FormItemRule, |
| | | failureTime: { |
| | | type: 'number', |
| | | required: true, |
| | | message: '请选择失效时间', |
| | | trigger: ['blur', 'change'], |
| | | validator: (_rule: any, value: any) => { |
| | | // 验证不能为空 |
| | | if (value == null) { |
| | | return new Error('请选择失效时间') |
| | | } |
| | | // 如果是永不失效,则跳过时间比较验证 |
| | | if (value === NEVER_EXPIRE) { |
| | | return true |
| | | } |
| | | // 如果生效时间是立即生效(IMMEDIATE_EFFECT),则失效时间需要大于当前时间 |
| | | if (formModel.value.effectTime === IMMEDIATE_EFFECT && typeof value === 'number') { |
| | | const currentTime = Date.now() |
| | | if (value <= currentTime) { |
| | | return new Error('失效时间不能小于当前时间') |
| | | } |
| | | return true |
| | | } |
| | | // 如果生效时间和失效时间都是具体时间,则失效时间需要大于生效时间 |
| | | if (typeof formModel.value.effectTime === 'number' && typeof value === 'number') { |
| | | if (value <= formModel.value.effectTime) { |
| | | return new Error('失效时间必须大于生效时间') |
| | | } |
| | | } |
| | | return true |
| | | }, |
| | | } as FormItemRule, |
| | | } |
| | | |
| | |
| | | disallowedAreaList.value.forEach((sub: any) => { |
| | | sub.isActive = false |
| | | }) |
| | | const id = disallowedAreaList.value.length + 1 |
| | | const id = 'temp' + disallowedAreaList.value.length + 1 |
| | | disallowedAreaList.value.push({ |
| | | areaName: `禁行区${id}`, |
| | | areaName: `禁行区${disallowedAreaList.value.length + 1}`, |
| | | areaStatus: 1, |
| | | isActive: true, |
| | | edit: false, |
| | |
| | | /** 保存草稿/发布围栏 */ |
| | | const handleAddFence = (type: number) => { |
| | | fencePoints = fence.getPath().map((p: any) => [p.lng, p.lat]) |
| | | if (disallowedAreaList.value.some((item: any) => item.fence === null)) { |
| | | message.error('请先绘制禁行区') |
| | | return |
| | | } |
| | | if (isPolygonSelfIntersect(fencePoints)) { |
| | | message.error('围栏存在自相交,请调整形状') |
| | | return |
| | |
| | | message.error(`${disallowedAreaIntersect.name} 与运营区相交,请调整形状`) |
| | | return |
| | | } |
| | | // 禁行区不能在运营区外 |
| | | const disallowedAreaOutside = disallowedAreaList.value.find((item: any) => { |
| | | const points = item.fence.getPath().map((p: any) => [p.lng, p.lat]) |
| | | return !isPolygonContained(points, fencePoints) |
| | | }) |
| | | |
| | | if (disallowedAreaOutside) { |
| | | message.error(`禁行区 ${disallowedAreaOutside.areaName} 必须在运营区范围内`) |
| | | return |
| | | } |
| | | formRef.value.validate().then(() => { |
| | | // 验证运营策略时间段 |
| | | for (const item of groupList.value) { |
| | | for (const sub of item.time) { |
| | | if (sub.startTime && sub.endTime) { |
| | | // 比较开始时间和结束时间 |
| | | if (sub.startTime >= sub.endTime) { |
| | | message.error('运营策略时间段中,结束时间不能小于或等于开始时间') |
| | | return |
| | | } |
| | | } else if (!sub.startTime || !sub.endTime) { |
| | | message.error('运营策略时间段中,开始时间和结束时间不能为空') |
| | | return |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 策略组 |
| | | const operationStrategy = groupList.value.map((item: any) => ({ |
| | | name: item.name, |
| | |
| | | areaType: 1, |
| | | fencePoints: fencePoints.map((item: any) => (item[0] + '_' + item[1])).join(','), |
| | | area: calcPolygonArea(fencePoints).toFixed(2), |
| | | id: fenceDetail.value.electronicFenceAreas?.find((item: any) => item.areaType === 1)?.id ?? null, |
| | | }] |
| | | disallowedAreaList.value.forEach((item: any) => { |
| | | electronicFenceAreas.push({ |
| | |
| | | areaType: 2, |
| | | fencePoints: item.fence.getPath().map((p: any) => (p.lng + '_' + p.lat)).join(','), |
| | | area: item.area.toFixed(2), |
| | | id: item.id.startsWith('temp') ? null : item.id, |
| | | }) |
| | | }) |
| | | // 组装请求参数 |
| | | const params = { |
| | | status: modalType.value !== 'add' && formModel.value.status !== 0 && formModel.value.status !== 1 ? (formModel.value.areaStatus === 1 ? 2 : 3 ) : type, |
| | | // status: type === 0 ? type : formModel.value.areaStatus === 1 ? 1 : 3 , // 0: 草稿 1: 发布 2: 启用 3: 停用 |
| | | cityCode: 510100, |
| | | cityCode: 0, |
| | | // cityCode: 440100, |
| | | area: calcPolygonArea(fencePoints).toFixed(2), |
| | | electronicName: formModel.value.electronicName, |
| | |
| | | infoWindow = new AMap.InfoWindow({ |
| | | isCustom: true, |
| | | content: '<div class="map-info"><div class="map-info-name">' + item.areaName + '</div><img class="map-info-icon" src="/src/assets/geofence/down.png" alt="图标"></div>', |
| | | autoMove: false |
| | | }); |
| | | |
| | | // 打开信息窗口 |
| | |
| | | <template> |
| | | <div class="w-full h-full geofence-detail-page" style="position: relative"> |
| | | <!-- 地图容器 --> |
| | | <div ref="domRef" class="w-full h-full" /> |
| | | <div ref="domRef" class="w-full h-full" :style="drawerVisible ? 'width: calc(100% - 280px);' : ''" /> |
| | | <!-- 提示 --> |
| | | <div class="tips" v-if="modalType === 'add'"> |
| | | {{ !isActiceDisallowedArea ? '请在地图上【非运营区域】绘制多边形区域,双击闭合' : '请在地图上【蓝色运营区域】绘制多边形区域,双击闭合' }} |
| | |
| | | <img src="../../../assets/geofence/foldUp.png" class="icon" @click="toggleDrawer" v-else> |
| | | </div> |
| | | <div class="drawer-content"> |
| | | <div class="drawer-head" v-if="(modalType === 'edit' || modalType === 'detail') && showDrawerHead"> |
| | | <!-- <div class="drawer-head" v-if="(modalType === 'edit' || modalType === 'detail') && showDrawerHead"> |
| | | <div class="title" v-if="fenceDetail">{{ fenceDetail?.electronicName || '' }} |
| | | <NTag style="font-weight: 500;" |
| | | :type="fenceDetail?.status === 0 || fenceDetail?.status === 3 ? 'default' : fenceDetail?.status === 2 ? 'success' : 'warning'" |
| | |
| | | <div class="drawer-head" v-if="modalType === 'add' && showDrawerHead"> |
| | | <div class="title">围栏详情</div> |
| | | <img src="../../../assets/geofence/close2.png" class="w-24px h-24px ml-auto cursor-pointer" @click="onClose"> |
| | | </div> |
| | | </div> --> |
| | | <div class="drawer-main" ref="mainScrollRef"> |
| | | <!-- <div class="main-head" v-if="!showDrawerHead"> --> |
| | | <div class="main-head"> |
| | | <div class="flex items-center gap-4px mb-4px" v-if="modalType === 'add'"> |
| | | <div class="main-head" :class="{ 'sticky-top': showDrawerHead }"> |
| | | <div class="flex items-center gap-4px mb-16px" v-if="modalType === 'add'"> |
| | | <div class="title">围栏详情</div> |
| | | <n-switch v-model:value="formModel.areaStatus" :checked-value="1" :unchecked-value="2"> |
| | | <template #checked> |
| | |
| | | @click="onClose"> |
| | | </div> |
| | | <div class="flex items-center gap-4px mb-4px" v-else> |
| | | <div class="title" v-if="fenceDetail"> |
| | | <div class="title items-center" v-if="fenceDetail"> |
| | | {{ fenceDetail.electronicName || '' }} |
| | | <NTag style="font-weight: 500;" |
| | | :type="fenceDetail.status === 0 || fenceDetail.status === 3 ? 'default' : fenceDetail.status === 2 ? 'success' : 'warning'" |
| | |
| | | </NTag> |
| | | <n-switch v-model:value="formModel.areaStatus" :checked-value="1" :unchecked-value="2" :disabled="fenceDetail.status === 0 || fenceDetail.status === 1" class="text-13px" v-if="modalType !== 'detail'"> |
| | | <template #checked> |
| | | 启用 |
| | | </template> |
| | | 启用 |
| | | </template> |
| | | <template #unchecked> |
| | | 停用 |
| | | </template> |
| | | 停用 |
| | | </template> |
| | | </n-switch> |
| | | </div> |
| | | </div> |
| | | <div class="head-des" v-if="modalType !== 'add' && fenceDetail">围栏ID: {{ fenceDetail.electronicNumber || '-' }}</div> |
| | | <div class="head-des" v-if="modalType !== 'add' && fenceDetail">覆盖面积: {{ fenceDetail.area || '-' }}km²</div> |
| | | </div> |
| | | <div class="flex flex-col gap-4px px-12px pb-16px" v-if="modalType !== 'add' && fenceDetail"> |
| | | <div class="head-des">围栏ID: {{ fenceDetail.electronicNumber || '-' }}</div> |
| | | <div class="head-des">覆盖面积: {{ fenceDetail.area || '-' }}km²</div> |
| | | </div> |
| | | <div class="line"></div> |
| | | <div v-if="modalType === 'detail' && fenceDetail"> |
| | | <div class="gap-16px flex flex-col"> |
| | | <div class="gap-16px flex flex-col px-12px py-16px"> |
| | | <!-- 基本信息 --> |
| | | <div class="box flex flex-col"> |
| | | <div class="box-title mb-8px">基本信息</div> |
| | | <div class="box-item mb-8px"> |
| | | <div class="label">区域类型</div> |
| | | <div class="value">{{regionTypeOptions.find(item => item.value === fenceDetail.areaType)?.label || '' |
| | | <div class="value">{{regionTypeOptions.find(item => item.value === fenceDetail.electronicFenceAreas?.[0].areaType)?.label || '' |
| | | }}</div> |
| | | </div> |
| | | <div class="box-item"> |
| | |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <n-form ref="formRef" :rules="rules" :model="formModel" require-mark-placement="left" v-else> |
| | | <n-form class="px-12px py-16px" ref="formRef" :rules="rules" :model="formModel" require-mark-placement="left" v-else> |
| | | <div class="gap-16px flex flex-col"> |
| | | <!-- 基本信息 --> |
| | | <div class="box flex flex-col"> |
| | |
| | | left: 50%; |
| | | transform: translateX(-50%); |
| | | } |
| | | .n-pro-layout--full-content { |
| | | .geofence-detail-page { |
| | | margin-top: -76px; |
| | | height: 100vh; |
| | | } |
| | | } |
| | | </style> |
| | | <style lang="scss" scoped> |
| | | @use './index.scss'; |
| | | .main-head { |
| | | // padding: 16px; |
| | | padding: 16px 12px; |
| | | padding-bottom: 0; |
| | | background: white; |
| | | z-index: 10; |
| | | |
| | | &.sticky-top { |
| | | position: sticky; |
| | | top: 0; |
| | | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
| | | } |
| | | } |
| | | |
| | | .main-head.sticky-top { |
| | | position: sticky; |
| | | top: 0; |
| | | z-index: 10; |
| | | background: white; |
| | | // padding: 16px; |
| | | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .tips { |
| | | position: absolute; |
| | |
| | | background-color: #fff; |
| | | display: flex; |
| | | flex-direction: column; |
| | | padding: 16px 12px; |
| | | // padding: 16px 12px; |
| | | // gap: 16px; |
| | | scrollbar-width: none; |
| | | /* Firefox */ |
| | |
| | | |
| | | .line { |
| | | border-top: 1px solid rgba(23, 31, 38, 0.15); |
| | | width: 100%; |
| | | margin: 16px 0; |
| | | width: calc(100% - 24px); |
| | | margin: 0 12px; |
| | | } |
| | | |
| | | .box { |
| | |
| | | const columns = computed<DataTableColumns<any>>(() => [ |
| | | { |
| | | title: '围栏ID', |
| | | key: 'id', |
| | | key: 'electronicNumber', |
| | | resizable: true, |
| | | ellipsis: true |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | title: '围栏名称', |
| | | key: 'electronicName', |
| | | resizable: true, |
| | | minWidth: 150, |
| | | ellipsis: true, |
| | | render(row) { |
| | | return ( |
| | |
| | | key: 'operatingAreaCount', |
| | | resizable: true, |
| | | ellipsis: true, |
| | | minWidth: 180, |
| | | render(row) { |
| | | return ( |
| | | <div class='cursor-pointer text-ellipsis whitespace-nowrap w-full overflow-hidden'> |
| | |
| | | { |
| | | title: '状态', |
| | | key: 'status', |
| | | ellipsis: true, |
| | | resizable: true, |
| | | minWidth: 80, |
| | | render(row) { |
| | | return h( |
| | | NTag, |
| | |
| | | title: '更新人', |
| | | key: 'updateBy', |
| | | ellipsis: true, |
| | | resizable: true |
| | | resizable: true, |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | title: '更新时间', |
| | |
| | | ellipsis: true, |
| | | resizable: true, |
| | | sorter: true, |
| | | minWidth: 200, |
| | | renderSorterIcon({ order }) { |
| | | let src = sortDefault |
| | | if (order === 'ascend') src = sortAsc |
| | |
| | | title: '创建人', |
| | | key: 'createBy', |
| | | ellipsis: true, |
| | | resizable: true |
| | | resizable: true, |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | title: '创建时间', |
| | |
| | | ellipsis: true, |
| | | sorter: true, |
| | | resizable: true, |
| | | minWidth: 200, |
| | | renderSorterIcon({ order }) { |
| | | let src = sortDefault |
| | | if (order === 'ascend') src = sortAsc |
| | |
| | | label: '围栏状态', |
| | | path: 'status', |
| | | render: () => <n-select v-model:value={model.value.status} options={options} clearable />, |
| | | // render: () => <n-select v-model:value={model.value.status} options={options} clearable multiple/>, |
| | | }, |
| | | { |
| | | label: '更新人', |
| | |
| | | flex-direction: column; |
| | | background: #fff; |
| | | position: relative; |
| | | transition: width 0.3s ease; |
| | | |
| | | &.is-collapsed { |
| | | width: 0; |
| | | overflow: visible; |
| | | border: none; |
| | | background: transparent; |
| | | |
| | | // 隐藏除按钮外的所有内容 |
| | | > *:not(.collapse-btn) { |
| | | display: none; |
| | | } |
| | | |
| | | .collapse-btn { |
| | | .collapse-icon { |
| | | transform: rotate(180deg); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .point-info { |
| | |
| | | import { createPoint, deletePoint, enableDisablePoint, fetchElectronicFenceAreaList, fetchPointList, updatePoint } from '@/service/api/point' |
| | | import PointStatusSteps from '@/components/PointStatusSteps/custom.vue' |
| | | import ConfirmDialog from '@/components/ConfirmDialog/index.vue' |
| | | |
| | | // 定义地图图标在线链接 |
| | | const MARK_ICON_RED = 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/36c926b7-1e6d-4310-86c2-a71b69dc49b7.png' |
| | | const MARK_ICON_GREEN = 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/8f048a0f-3c36-4b6e-ae14-77d85bc919bf.png' |
| | | const MARK_ICON_BLUE = 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e7f49805-c8fd-42aa-9c92-af48d3ec8eec.png' |
| | | |
| | | const router = useRouter() |
| | | |
| | | const { VITE_MAP_KEY, VITE_MAP_SECURITY } = import.meta.env |
| | |
| | | name: '', |
| | | address: '', |
| | | fullAddress: '', |
| | | city: '', // 城市名称 |
| | | lon: '', |
| | | lat: '', |
| | | }) |
| | |
| | | |
| | | // 点位详情面板状态:view-查看, edit-编辑, add-新增 |
| | | const pointDetailMode = ref<'view' | 'edit' | 'add'>('view') |
| | | |
| | | // 详情面板折叠状态 |
| | | const isCollapsed = ref(false) |
| | | |
| | | // 点位状态是否已被本地修改 |
| | | const isStatusModified = ref(false) |
| | |
| | | function clearSearch() { |
| | | searchKeyword.value = '' |
| | | hasSearched.value = false |
| | | pointList.value = [] |
| | | filterExpanded.value = false |
| | | showStatusDropdown.value = false |
| | | filterExpanded.value = true |
| | | loadPointList() |
| | | } |
| | | |
| | |
| | | }) |
| | | |
| | | const showLeftContent = computed(() => { |
| | | return hasSearched.value && searchKeyword.value.trim().length > 0 |
| | | return true |
| | | }) |
| | | |
| | | watch([searchKeyword, hasSearched], ([kw, searched]) => { |
| | | const hasKeyword = kw.trim().length > 0 |
| | | filterExpanded.value = hasKeyword && searched |
| | | if (!hasKeyword) { |
| | | showStatusDropdown.value = false |
| | | } |
| | | }) |
| | | watch(searchKeyword, (val) => { |
| | | const hasKeyword = val.trim().length > 0 |
| | | filterExpanded.value = hasKeyword |
| | | if (!hasKeyword) { |
| | | showStatusDropdown.value = false |
| | | } |
| | |
| | | name: point.name || '', |
| | | address: fullAddress, |
| | | fullAddress, |
| | | city: '', // 手动添加时可能无法获取城市,需要通过 reverseGeocode 或其他方式,这里先置空 |
| | | lon: Number(point.lon)?.toFixed ? Number(point.lon).toFixed(7) : `${point.lon}`, |
| | | lat: Number(point.lat)?.toFixed ? Number(point.lat).toFixed(7) : `${point.lat}`, |
| | | } |
| | |
| | | |
| | | // 点击点位 |
| | | function handlePointClick(point: any) { |
| | | isCollapsed.value = false |
| | | activePointId.value = point.id |
| | | currentPoint.value = JSON.parse(JSON.stringify(point)) // 深拷贝,避免直接修改列表数据 |
| | | pointDetailMode.value = 'view' |
| | |
| | | addressOptions.value = currentPoint.value.name ? [{ label: currentPoint.value.name, value: currentPoint.value.name }] : [] |
| | | createTempMarker(position) |
| | | // 重新渲染点位,此时 renderPoints 会跳过当前选中的点,避免重叠 |
| | | renderPoints(allPoints.value) |
| | | renderPoints(pointList.value) |
| | | } |
| | | |
| | | // 取消编辑 |
| | | function handleCancelEdit() { |
| | | if (pointDetailMode.value === 'add') { |
| | | handleCancelAddPoint() |
| | | // 新增模式下的取消,需要二次确认 |
| | | // 检查是否已录入内容 |
| | | const hasContent = currentPoint.value && ( |
| | | currentPoint.value.name |
| | | || currentPoint.value.remark |
| | | || (uploadedImages.value && uploadedImages.value.length > 0) |
| | | ) |
| | | |
| | | if (hasContent) { |
| | | confirmDialogConfig.value = { |
| | | title: '当前点位还未保存,确定离开吗?', |
| | | description: '删除点位,编辑的内容将丢失,确定离开吗?', |
| | | cancelText: '丢弃离开', |
| | | confirmText: '保存离开', |
| | | onConfirm: () => { |
| | | showConfirmDialog.value = false |
| | | }, |
| | | onCancel: () => { |
| | | handleCancelAddPoint() |
| | | }, |
| | | } |
| | | showConfirmDialog.value = true |
| | | } |
| | | else { |
| | | handleCancelAddPoint() |
| | | } |
| | | } |
| | | else { |
| | | pointDetailMode.value = 'view' |
| | |
| | | else { |
| | | // 必须在 nextTick 中渲染,确保 pointDetailMode 已经生效 |
| | | setTimeout(() => { |
| | | renderPoints(allPoints.value) |
| | | renderPoints(pointList.value) |
| | | }, 0) |
| | | } |
| | | } |
| | |
| | | } |
| | | |
| | | // 5. 获取电子围栏区域ID |
| | | const electronicAreaId = electronicFenceAreas.value[0]?.id?.toString() || '' |
| | | // 需求:取点位所在的运营区域的 electronicId |
| | | let electronicAreaId = '' |
| | | if (currentPoint.value.lon && currentPoint.value.lat && typeof AMap !== 'undefined') { |
| | | // 确保使用 AMap.GeometryUtil.isPointInRing 需要加载 GeometryUtil 插件 |
| | | // 或者手动实现点在多边形内判断(射线法),但 AMap 一般自带 |
| | | // 注意:JS API 2.0 默认包含 GeometryUtil 静态方法,但如果是 1.4 可能需要插件 |
| | | // 这里假设是 2.0+ |
| | | |
| | | const point = new AMap.LngLat(Number(currentPoint.value.lon), Number(currentPoint.value.lat)) |
| | | // 遍历所有运营区域(areaType === 1) |
| | | const targetArea = electronicFenceAreas.value.find((area) => { |
| | | if (area.areaType !== 1 || !area.fencePoints) |
| | | return false |
| | | try { |
| | | const path = area.fencePoints.split(',').map((p) => { |
| | | const [lng, lat] = p.split('_') |
| | | return new AMap.LngLat(Number(lng), Number(lat)) |
| | | }) |
| | | |
| | | // 兼容性处理:如果 GeometryUtil 不可用,使用 Polygon 实例的 contains 方法 |
| | | if (AMap.GeometryUtil && AMap.GeometryUtil.isPointInRing) { |
| | | return AMap.GeometryUtil.isPointInRing(point, path) |
| | | } else { |
| | | // 创建临时 polygon 用于判断 |
| | | const tempPolygon = new AMap.Polygon({ path }) |
| | | return tempPolygon.contains(point) |
| | | } |
| | | } |
| | | catch (e) { |
| | | console.error('判断点位区域失败:', e) |
| | | return false |
| | | } |
| | | }) |
| | | |
| | | if (targetArea) { |
| | | electronicAreaId = targetArea.electronicId?.toString() || targetArea.id?.toString() || '' |
| | | } |
| | | } |
| | | |
| | | // 兜底:如果没有匹配到任何区域,或者计算失败,尝试使用默认值(虽然业务上应该报错提示) |
| | | if (!electronicAreaId) { |
| | | electronicAreaId = electronicFenceAreas.value.find(area => area.areaType === 1)?.electronicId || electronicFenceAreas.value[0]?.id?.toString() || '' |
| | | // 可以考虑是否提示用户“点位不在任何运营区域内” |
| | | // window.$message?.warning('当前点位不在任何已知的运营区域内,将使用默认区域') |
| | | } |
| | | |
| | | const payload = { |
| | | pointName: currentPoint.value.name, |
| | |
| | | updateTime: point.updateTime, |
| | | source: 'api', |
| | | })) |
| | | pointList.value = [] |
| | | renderPoints(allPoints.value) |
| | | pointList.value = allPoints.value |
| | | renderPoints(pointList.value) |
| | | } |
| | | return |
| | | } |
| | |
| | | } |
| | | |
| | | pointList.value = [...apiPoints, ...amapPoints] |
| | | renderPoints(allPoints.value) |
| | | renderPoints(pointList.value) |
| | | } |
| | | catch (error) { |
| | | console.error('获取点位列表失败:', error) |
| | |
| | | const greenStatus = ['可运营', '启用中'] |
| | | |
| | | if (redStatus.includes(status)) { |
| | | return '/src/assets/point/mark-red.png' |
| | | return MARK_ICON_RED |
| | | } |
| | | else if (greenStatus.includes(status)) { |
| | | return '/src/assets/point/mark-green.png' |
| | | return MARK_ICON_GREEN |
| | | } |
| | | return '/src/assets/point/mark-red.png' |
| | | return MARK_ICON_RED |
| | | } |
| | | |
| | | function clampNumber(value: number, min: number, max: number) { |
| | |
| | | |
| | | if (extData.isNew) { |
| | | const iconSize = getScaledSize(32, 42) |
| | | const iconUrl = '/src/assets/point/mark-blue.png' |
| | | const iconUrl = MARK_ICON_BLUE |
| | | marker.setIcon(new AMap.Icon({ |
| | | size: iconSize, |
| | | image: iconUrl, |
| | |
| | | const isSelected = activePointId.value == point.id |
| | | && !(pointDetailMode.value === 'edit' || pointDetailMode.value === 'add') |
| | | |
| | | const iconUrl = isSelected ? '/src/assets/point/mark-blue.png' : getPointIconUrl(point.status) |
| | | const iconUrl = isSelected ? MARK_ICON_BLUE : getPointIconUrl(point.status) |
| | | const iconSize = isSelected ? getScaledSize(31, 42) : getScaledSize(32, 32) |
| | | marker.setIcon(new AMap.Icon({ |
| | | size: iconSize, |
| | |
| | | return |
| | | } |
| | | |
| | | const iconUrl = isSelected ? '/src/assets/point/mark-blue.png' : getPointIconUrl(point.status) |
| | | const iconUrl = isSelected ? MARK_ICON_BLUE : getPointIconUrl(point.status) |
| | | // 选中时尺寸为 40x40,未选中时为 32x32 |
| | | const iconSize = isSelected ? getScaledSize(31, 42) : getScaledSize(32, 32) |
| | | |
| | |
| | | return |
| | | if (point.source === 'amap') { |
| | | startAddFromAmap(point) |
| | | // 触发一次反查以获取城市信息用于生成ID |
| | | reverseGeocode([Number(point.lon), Number(point.lat)]) |
| | | } |
| | | else { |
| | | handlePointClick(point) |
| | |
| | | AMap.plugin('AMap.AutoComplete', () => { |
| | | const autoOptions = { |
| | | city: '全国', |
| | | pageSize: 50, // 设置返回结果数量为50 |
| | | } |
| | | const autoComplete = new AMap.AutoComplete(autoOptions) |
| | | autoComplete.search(keyword, (status: string, result: any) => { |
| | |
| | | AMap.plugin('AMap.AutoComplete', () => { |
| | | const autoOptions = { |
| | | city: '全国', |
| | | pageSize: 50, // 设置返回结果数量为50 |
| | | } |
| | | const autoComplete = new AMap.AutoComplete(autoOptions) |
| | | autoComplete.search(query, (status: string, result: any) => { |
| | |
| | | const address = regeocode.formattedAddress |
| | | const comp = regeocode.addressComponent |
| | | // 提取更精准的简短地名:优先 建筑 > 兴趣点 > 社区 > 街道 |
| | | const defaultName = comp.building || comp.neighborhood || comp.township || comp.streetNumber || address.slice(0, 10) |
| | | // const defaultName = comp.building || comp.neighborhood || comp.township || comp.streetNumber || address.slice(0, 10) |
| | | const defaultName = address |
| | | |
| | | // 更新地址下拉选项 |
| | | // label 决定显示内容,value 决定绑定值 |
| | |
| | | tempMarkerForm.value.name = defaultName |
| | | tempMarkerForm.value.address = defaultName |
| | | tempMarkerForm.value.fullAddress = address |
| | | tempMarkerForm.value.city = comp.city || comp.province // 优先使用城市名,直辖市可能在省份字段 |
| | | tempAddressInput.value = defaultName |
| | | }) |
| | | } |
| | |
| | | name: '', |
| | | address: '正在查询地址...', |
| | | fullAddress: '', |
| | | city: '', |
| | | lon: lnglat.getLng().toFixed(7), |
| | | lat: lnglat.getLat().toFixed(7), |
| | | } |
| | |
| | | |
| | | content.innerHTML = ` |
| | | <div class="temp-marker-wrapper" style="position: relative; width: 32px; height: 42px;"> |
| | | <img src="/src/assets/point/mark-blue.png" style="width: 32px; height: 42px; display: block;" /> |
| | | <img src="${MARK_ICON_BLUE}" style="width: 32px; height: 42px; display: block;" /> |
| | | <div class="marker-delete-btn" style=" |
| | | position: absolute; |
| | | top: -4px; |
| | |
| | | }) |
| | | |
| | | // 重新渲染点位列表,以移除未保存的新增标记 |
| | | renderPoints(allPoints.value) |
| | | renderPoints(pointList.value) |
| | | } |
| | | |
| | | // 城市拼音映射表 |
| | | const CITY_PINYIN_MAP: Record<string, string> = { |
| | | 北京: 'BJ', |
| | | 上海: 'SH', |
| | | 广州: 'GZ', |
| | | 深圳: 'SZ', |
| | | 杭州: 'HZ', |
| | | 成都: 'CD', |
| | | 武汉: 'WH', |
| | | 重庆: 'CQ', |
| | | 西安: 'XA', |
| | | 南京: 'NJ', |
| | | 苏州: 'SZ', |
| | | 天津: 'TJ', |
| | | 郑州: 'ZZ', |
| | | 长沙: 'CS', |
| | | 沈阳: 'SY', |
| | | 青岛: 'QD', |
| | | 宁波: 'NB', |
| | | 东莞: 'DG', |
| | | 无锡: 'WX', |
| | | 佛山: 'FS', |
| | | 合肥: 'HF', |
| | | 大连: 'DL', |
| | | 福州: 'FZ', |
| | | 厦门: 'XM', |
| | | 哈尔滨: 'HEB', |
| | | 济南: 'JN', |
| | | 温州: 'WZ', |
| | | 长春: 'CC', |
| | | 石家庄: 'SJZ', |
| | | 常州: 'CZ', |
| | | 泉州: 'QZ', |
| | | 南宁: 'NN', |
| | | 贵阳: 'GY', |
| | | 南昌: 'NC', |
| | | 南通: 'NT', |
| | | 金华: 'JH', |
| | | 徐州: 'XZ', |
| | | 太原: 'TY', |
| | | 嘉兴: 'JX', |
| | | 烟台: 'YT', |
| | | 惠州: 'HZ', |
| | | 保定: 'BD', |
| | | 台州: 'TZ', |
| | | 中山: 'ZS', |
| | | 绍兴: 'SX', |
| | | 乌鲁木齐: 'WLMQ', |
| | | 潍坊: 'WF', |
| | | 兰州: 'LZ', |
| | | } |
| | | |
| | | function getCityCode(cityName: string) { |
| | | if (!cityName) return 'XX' |
| | | // 移除“市”后缀 |
| | | const name = cityName.replace(/市$/, '') |
| | | // 查找映射表 |
| | | for (const key in CITY_PINYIN_MAP) { |
| | | if (name.includes(key)) { |
| | | return CITY_PINYIN_MAP[key] |
| | | } |
| | | } |
| | | // 如果没找到,默认返回 XX |
| | | return 'XX' |
| | | } |
| | | |
| | | function generatePointId(cityName: string) { |
| | | const cityCode = getCityCode(cityName) |
| | | const typeCode = 'PUD' |
| | | const prefix = `${cityCode}-${typeCode}-` |
| | | |
| | | // 查找当前最大的序列号 |
| | | let maxSeq = 0 |
| | | if (allPoints.value && allPoints.value.length > 0) { |
| | | allPoints.value.forEach(p => { |
| | | if (p.id && typeof p.id === 'string' && p.id.startsWith(prefix)) { |
| | | const seqStr = p.id.slice(prefix.length) |
| | | const seq = parseInt(seqStr, 10) |
| | | if (!isNaN(seq) && seq > maxSeq) { |
| | | maxSeq = seq |
| | | } |
| | | } |
| | | }) |
| | | } |
| | | |
| | | const newSeq = maxSeq + 1 |
| | | return `${prefix}${newSeq.toString().padStart(6, '0')}` |
| | | } |
| | | |
| | | function handleConfirmTempMarker() { |
| | |
| | | } |
| | | |
| | | if (!tempMarkerForm.value.id) { |
| | | tempMarkerForm.value.id = `P${Date.now()}` |
| | | // 根据城市生成规则ID |
| | | tempMarkerForm.value.id = generatePointId(tempMarkerForm.value.city) |
| | | } |
| | | |
| | | // 将地图弹窗的数据回显到右侧表单对象中 |
| | |
| | | } |
| | | |
| | | // 使用 mark-blue.png,因为它还处于打点/新增状态,未保存到数据库 |
| | | const iconUrl = '/src/assets/point/mark-blue.png' |
| | | const iconUrl = MARK_ICON_BLUE |
| | | const iconSize = getScaledSize(32, 42) |
| | | const icon = new AMap.Icon({ |
| | | size: iconSize, |
| | |
| | | loadPointList() // 撤销修改,重新加载数据 |
| | | } else { |
| | | setTimeout(() => { |
| | | renderPoints(allPoints.value) |
| | | renderPoints(pointList.value) |
| | | }, 0) |
| | | } |
| | | } else { |
| | |
| | | activePointId.value = null |
| | | currentPoint.value = null |
| | | pointDetailMode.value = 'view' |
| | | renderPoints(allPoints.value) |
| | | renderPoints(pointList.value) |
| | | } |
| | | } |
| | | |
| | | // 关闭点位详情 |
| | | // 关闭/折叠点位详情 |
| | | function handleCloseDetail() { |
| | | if (pointDetailMode.value !== 'view') { |
| | | // 如果是新增模式,保持原有逻辑(取消新增) |
| | | if (pointDetailMode.value === 'add') { |
| | | handleCancelEdit() |
| | | return |
| | | } |
| | | else { |
| | | activePointId.value = null |
| | | currentPoint.value = null |
| | | renderPoints(allPoints.value) |
| | | } |
| | | |
| | | // 如果是查看或编辑模式,切换折叠状态 |
| | | isCollapsed.value = !isCollapsed.value |
| | | } |
| | | function toPointList() { |
| | | router.push({ path: '/businessOperations/pointManagement' }) |
| | |
| | | <div class="flex a-center j-between"> |
| | | <div> |
| | | <div class="flex a-center j-between"> |
| | | <div class="flex a-center"> |
| | | <div class="point-id"> |
| | | <div class="flex a-center" style="align-items: flex-start;"> |
| | | <div class="point-id" style="word-break: break-all; white-space: normal;"> |
| | | {{ point.name }} |
| | | </div> |
| | | <div |
| | | v-if="point.source === 'api'" |
| | | class="point-status" |
| | | :class="getStatusClass(point.status)" |
| | | style="flex-shrink: 0;" |
| | | > |
| | | {{ point.status }} |
| | | </div> |
| | |
| | | <div |
| | | v-if="currentPoint && currentPoint.lon && currentPoint.lat" |
| | | class="point-detail" |
| | | :class="{ 'is-collapsed': isCollapsed }" |
| | | > |
| | | <!-- 关闭详情按钮 --> |
| | | <div |
| | |
| | | class="form-input" |
| | | type="text" |
| | | placeholder="请输入点位名称" |
| | | disabled |
| | | :disabled="pointDetailMode === 'view'" |
| | | /> |
| | | </div> |
| | | </div> |
| | |
| | | <div v-else class="point-detail-info"> |
| | | <!-- 点位名称 状态 --> |
| | | <div> |
| | | <div class="flex a-center"> |
| | | <div class="point-name"> |
| | | <div class="flex a-center" style="align-items: flex-start;"> |
| | | <div class="point-name" style="word-break: break-all; white-space: normal;"> |
| | | {{ currentPoint?.name }} |
| | | </div> |
| | | <div |
| | | class="point-status" |
| | | :class="getStatusClass(currentPoint?.status)" |
| | | style="flex-shrink: 0;" |
| | | > |
| | | {{ currentPoint?.status }} |
| | | </div> |
| | |
| | | { |
| | | title: '状态', |
| | | key: 'operationStatus', |
| | | width: 100, |
| | | width: 140, |
| | | resizable: true, |
| | | ellipsis: true, |
| | | render(row) { |
| | | const statusText = getPointStatusText(row.operationStatus, row.verificationStatus) |
| | | const statusClass = getStatusClass(statusText) |
| | |
| | | <NSpace> |
| | | <NButton |
| | | text |
| | | v-permission="70212" |
| | | type='primary' |
| | | onClick={() => handleEdit(row)} |
| | | > |
| | |
| | | </NButton> |
| | | <NButton |
| | | text |
| | | v-permission="70214" |
| | | type='primary' |
| | | onClick={() => { |
| | | dialog.create({ |
| | |
| | | > |
| | | { row.status === 1 ? '禁用' : '启用' } |
| | | </NButton> |
| | | <NPopconfirm onPositiveClick={() => delteteUser(row.id!)}> |
| | | <NPopconfirm v-permission="70213" onPositiveClick={() => delteteUser(row.id!)}> |
| | | {{ |
| | | default: () => '确认删除', |
| | | trigger: () => <NButton text type="error">删除</NButton>, |
| | |
| | | |
| | | <n-card class="flex-1"> |
| | | <template #header> |
| | | <n-flex justify="flex-end"> |
| | | <n-flex justify="flex-end" v-permission="70211"> |
| | | <NButton type="primary" secondary @click="handleAdd"> |
| | | 新增打卡地 |
| | | </NButton> |
| | |
| | | <n-date-picker v-model:value="selectedMonth" placeholder="选择月份" type="month" style="width: 260px" |
| | | @update:value="handleMonthChange" /> |
| | | <div class="flex gap-2"> |
| | | <n-button style="width: 84px;height: 32px;background-color: #E6F4FF;color: #0068FF;border: 1px solid #7ABDFF;" |
| | | <n-button v-permission="70252" style="width: 84px;height: 32px;background-color: #E6F4FF;color: #0068FF;border: 1px solid #7ABDFF;" |
| | | @click="handleCopy">复制</n-button> |
| | | <n-button style="width: 84px;height: 32px;background-color: #E6F4FF;color: #0068FF;border: 1px solid #7ABDFF;" |
| | | <n-button v-permission="70253" style="width: 84px;height: 32px;background-color: #E6F4FF;color: #0068FF;border: 1px solid #7ABDFF;" |
| | | @click="handleDownload">下载表</n-button> |
| | | <n-button type="primary" style="width: 84px; height: 32px" @click="handleEdit">编辑</n-button> |
| | | <n-button v-permission="70251" type="primary" style="width: 84px; height: 32px" @click="handleEdit">编辑</n-button> |
| | | </div> |
| | | </div> |
| | | |
| | |
| | | <NSpace> |
| | | <NButton |
| | | text |
| | | v-permission="70242" |
| | | type='primary' |
| | | onClick={() => handleEdit(row)} |
| | | > |
| | | 编辑 |
| | | </NButton> |
| | | <NPopconfirm onPositiveClick={() => handleDelete(row.id!)}> |
| | | <NPopconfirm v-permission="70243" onPositiveClick={() => handleDelete(row.id!)}> |
| | | {{ |
| | | default: () => '确认删除', |
| | | trigger: () => <NButton text type="error">删除</NButton>, |
| | |
| | | |
| | | <n-card class="flex-1"> |
| | | <template #header> |
| | | <n-flex justify="flex-end"> |
| | | <n-flex justify="flex-end" v-permission="70241"> |
| | | <NButton type="primary" secondary @click="handleAdd"> |
| | | 新增班次 |
| | | </NButton> |
| | |
| | | width: 134, |
| | | fixed: 'right', |
| | | render: (row) => ( |
| | | <NButton text type="primary" onClick={() => handleViewDetail(row)}> |
| | | <NButton v-permission="70222" text type="primary" onClick={() => handleViewDetail(row)}> |
| | | 打卡明细 |
| | | </NButton> |
| | | ) |
| | |
| | | |
| | | <n-card style="border-radius: 6px; margin-top: 12px" content-style="padding: 12px"> |
| | | <template #header> |
| | | <n-flex justify="flex-end"> |
| | | <n-flex justify="flex-end" v-permission="70221"> |
| | | <NButton type="primary" secondary @click="handleExport"> |
| | | 导出 |
| | | </NButton> |
| | |
| | | <script setup lang="ts"> |
| | | import { ref, h } from 'vue' |
| | | import { useBoolean } from '@/hooks' |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { NCheckbox } from 'naive-ui' |
| | | import { getNoticeList, updateNoticeConfig } from '@/service/api/attendance' |
| | | import { ref, h } from "vue"; |
| | | import { useBoolean } from "@/hooks"; |
| | | import type { DataTableColumns } from "naive-ui"; |
| | | import { NCheckbox } from "naive-ui"; |
| | | import { getNoticeList, updateNoticeConfig } from "@/service/api/attendance"; |
| | | |
| | | const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false) |
| | | const { |
| | | bool: loading, |
| | | setTrue: startLoading, |
| | | setFalse: endLoading, |
| | | } = useBoolean(false); |
| | | |
| | | const tableData = ref<any[]>([]) |
| | | const tableData = ref<any[]>([]); |
| | | |
| | | const columns: DataTableColumns<any> = [ |
| | | { |
| | | title: '序号', |
| | | key: 'serialNumber', |
| | | title: "序号", |
| | | key: "serialNumber", |
| | | width: 80, |
| | | }, |
| | | { |
| | | title: '通知类型', |
| | | key: 'notificationType', |
| | | title: "通知类型", |
| | | key: "notificationType", |
| | | render: (row) => { |
| | | switch (row.notificationType) { |
| | | //通知类型 1=新增事故,2=事故即将超时提示,3=事故超时提示,4=排班信息变动,5=事故状态变更,6=新增任务,7=任务过期提示,8=版本更新提示,9=变更协议政策内容,10=变更协议内容并需用户授权,11=打卡成功通知 |
| | | case 1: |
| | | return '新增事故' |
| | | return "新增事故"; |
| | | case 2: |
| | | return '事故即将超时提示' |
| | | return "事故即将超时提示"; |
| | | case 3: |
| | | return '事故超时提示' |
| | | return "事故超时提示"; |
| | | case 4: |
| | | return '排班信息变动' |
| | | return "排班信息变动"; |
| | | case 5: |
| | | return '事故状态变更' |
| | | return "事故状态变更"; |
| | | case 6: |
| | | return '新增任务' |
| | | return "新增任务"; |
| | | case 7: |
| | | return '任务过期提示' |
| | | return "任务过期提示"; |
| | | case 8: |
| | | return '版本更新提示' |
| | | return "版本更新提示"; |
| | | case 9: |
| | | return '变更协议政策内容' |
| | | return "变更协议政策内容"; |
| | | case 10: |
| | | return '变更协议内容并需用户授权' |
| | | return "变更协议内容并需用户授权"; |
| | | case 11: |
| | | return '打卡成功通知' |
| | | return "打卡成功通知"; |
| | | default: |
| | | return row.notificationType |
| | | return row.notificationType; |
| | | } |
| | | } |
| | | }, |
| | | }, |
| | | { |
| | | title: '通知方式', |
| | | key: 'method', |
| | | title: "通知方式", |
| | | key: "method", |
| | | render: (row) => { |
| | | return h('div', { class: 'flex items-center gap-4' }, [ |
| | | h(NCheckbox, { |
| | | checked: row.isPush, |
| | | onUpdateChecked: (value: boolean) => { |
| | | row.isPush = value |
| | | } |
| | | }, { default: () => 'PUSH推送' }), |
| | | h(NCheckbox, { |
| | | checked: row.isMessage, |
| | | onUpdateChecked: (value: boolean) => { |
| | | row.isMessage = value |
| | | } |
| | | }, { default: () => '短信通知' }) |
| | | ]) |
| | | } |
| | | return h("div", { class: "flex items-center gap-4" }, [ |
| | | h( |
| | | NCheckbox, |
| | | { |
| | | checked: row.isPush, |
| | | onUpdateChecked: (value: boolean) => { |
| | | row.isPush = value; |
| | | }, |
| | | }, |
| | | { default: () => "PUSH推送" } |
| | | ), |
| | | h( |
| | | NCheckbox, |
| | | { |
| | | checked: row.isMessage, |
| | | onUpdateChecked: (value: boolean) => { |
| | | row.isMessage = value; |
| | | }, |
| | | }, |
| | | { default: () => "短信通知" } |
| | | ), |
| | | ]); |
| | | }, |
| | | }, |
| | | { |
| | | title: '更新时间', |
| | | key: 'updateTime', |
| | | } |
| | | ] |
| | | title: "更新时间", |
| | | key: "updateTime", |
| | | }, |
| | | ]; |
| | | |
| | | const handleSave = () => { |
| | | console.log('保存', tableData.value) |
| | | startLoading() |
| | | updateNoticeConfig({ notifications: tableData.value }).then((res) => { |
| | | if (res.code === 200) { |
| | | window.$message.success('保存成功') |
| | | } |
| | | }).finally(() => { |
| | | getList() |
| | | endLoading() |
| | | }) |
| | | } |
| | | console.log("保存", tableData.value); |
| | | startLoading(); |
| | | updateNoticeConfig({ notifications: tableData.value }) |
| | | .then((res) => { |
| | | if (res.code === 200) { |
| | | window.$message.success("保存成功"); |
| | | } |
| | | }) |
| | | .finally(() => { |
| | | getList(); |
| | | endLoading(); |
| | | }); |
| | | }; |
| | | |
| | | async function getList() { |
| | | startLoading() |
| | | getNoticeList().then((res) => { |
| | | tableData.value = res.data || [] |
| | | tableData.value.forEach((item: any, index: number) => { |
| | | item.serialNumber = (pageParams.value.pageNum - 1) * pageParams.value.pageSize + index + 1 |
| | | startLoading(); |
| | | getNoticeList() |
| | | .then((res) => { |
| | | tableData.value = res.data || []; |
| | | tableData.value.forEach((item: any, index: number) => { |
| | | item.serialNumber = |
| | | (pageParams.value.pageNum - 1) * pageParams.value.pageSize + |
| | | index + |
| | | 1; |
| | | }); |
| | | }) |
| | | }).finally(() => { |
| | | endLoading() |
| | | }) |
| | | .finally(() => { |
| | | endLoading(); |
| | | }); |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getList() |
| | | }) |
| | | getList(); |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="h-full flex-col"> |
| | | <n-card style="border-radius: 6px; overflow: hidden;" |
| | | content-style="padding: 12px; height: 100%; display: flex; flex-direction: column;"> |
| | | <div style="flex: 1; overflow: auto;"> |
| | | <n-data-table :columns="columns" :data="tableData" :loading="loading" :bordered="false" :header-height="18" |
| | | :row-height="18" /> |
| | | <n-card |
| | | style="border-radius: 6px; overflow: hidden" |
| | | content-style="padding: 12px; height: 100%; display: flex; flex-direction: column;" |
| | | > |
| | | <div style="flex: 1; overflow: auto"> |
| | | <n-data-table |
| | | :columns="columns" |
| | | :data="tableData" |
| | | :loading="loading" |
| | | :bordered="false" |
| | | :header-height="18" |
| | | :row-height="18" |
| | | /> |
| | | </div> |
| | | </n-card> |
| | | |
| | | <div class="flex justify-end absolute bottom-0 left-0 right-0" |
| | | style="background-color: white; padding: 16px 24px; border-radius: 6px;"> |
| | | <n-button type="primary" style="width: 84px; height: 32px" @click="handleSave">保存</n-button> |
| | | <div |
| | | class="flex justify-end absolute bottom-0 left-0 right-0" |
| | | style="background-color: white; padding: 16px 24px; border-radius: 6px" |
| | | > |
| | | <n-button |
| | | v-permission="7041" |
| | | type="primary" |
| | | style="width: 84px; height: 32px" |
| | | @click="handleSave" |
| | | >保存</n-button |
| | | > |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | :deep(.n-data-table-th) { |
| | | background-color: #F4F6F8; |
| | | background-color: #f4f6f8; |
| | | } |
| | | </style> |
| | |
| | | password: string |
| | | roleIds: string[] | null |
| | | remarks: string |
| | | sex: number |
| | | } |
| | | |
| | | const props = defineProps<{ |
| | |
| | | avatar: '', |
| | | password: '', |
| | | roleIds: null, |
| | | sex: 1, |
| | | remarks: '' |
| | | }) |
| | | |
| | |
| | | { required: true, message: '请输入用户名称', trigger: 'blur' } |
| | | ], |
| | | uniqueNumber: [ |
| | | { required: true, message: '请输入编号', trigger: 'blur' } |
| | | { required: true, message: '请输入编号', trigger: 'blur' }, |
| | | { |
| | | pattern: /^[a-zA-Z0-9]{1,20}$/, |
| | | message: '编号支持字母数字,最多20个字符', |
| | | trigger: 'blur' |
| | | } |
| | | ], |
| | | password: [ |
| | | { required: true, message: '请输入初始密码', trigger: 'blur' } |
| | | { required: true, message: '请输入初始密码', trigger: 'blur' }, |
| | | { |
| | | pattern: /^.{8,20}$/, |
| | | message: '密码长度必须为8-20位字符', |
| | | trigger: 'blur' |
| | | }, |
| | | { |
| | | validator: (rule: any, value: string) => { |
| | | if (!value) return true; |
| | | |
| | | // 检查是否包含大小写字母、数字、特殊符号 |
| | | const hasLowercase = /[a-z]/.test(value); |
| | | const hasUppercase = /[A-Z]/.test(value); |
| | | const hasDigit = /\d/.test(value); |
| | | const hasSpecialChar = /[^a-zA-Z0-9]/.test(value); // 非字母数字字符即为特殊字符 |
| | | |
| | | // 计算包含的字符类型数量 |
| | | const charTypesCount = [hasLowercase, hasUppercase, hasDigit, hasSpecialChar].filter(Boolean).length; |
| | | |
| | | // 至少需要两种字符类型 |
| | | return charTypesCount >= 2; |
| | | }, |
| | | message: '密码必须包含大小写字母、数字、特殊符号中的至少两种', |
| | | trigger: 'blur' |
| | | } |
| | | ], |
| | | phoneNumber: [ |
| | | { required: true, message: '请输入联系电话', trigger: 'blur' }, |
| | |
| | | ], |
| | | roleIds: [ |
| | | { type: 'array', required: true, message: '请选择角色', trigger: 'change' } |
| | | ], |
| | | sex: [ |
| | | { type: 'number', required: true, message: '请选择性别', trigger: 'change' } |
| | | ] |
| | | } |
| | | |
| | |
| | | avatar: '', |
| | | password: '', |
| | | roleIds: null, |
| | | remarks: '' |
| | | remarks: '', |
| | | sex: 1 |
| | | } |
| | | fileList.value = [] |
| | | } |
| | |
| | | preset="card" |
| | | :title="mode === 'edit' ? '编辑账号' : '新增账号'" |
| | | class="w-600px" |
| | | :auto-focus="false" |
| | | :segmented="{ |
| | | content: true, |
| | | action: true, |
| | |
| | | > |
| | | <div class="grid grid-cols-2 gap-4"> |
| | | <n-form-item label="用户名称" path="nickName"> |
| | | <n-input v-model:value="formModel.nickName" placeholder="请输入" clearable /> |
| | | <n-input v-model:value="formModel.nickName" placeholder="请输入" clearable maxlength="20"/> |
| | | </n-form-item> |
| | | |
| | | <n-form-item label="编号" path="uniqueNumber"> |
| | | <n-input v-model:value="formModel.uniqueNumber" placeholder="请输入" clearable /> |
| | | <n-input v-model:value="formModel.uniqueNumber" placeholder="请输入" clearable maxlength="20"/> |
| | | </n-form-item> |
| | | </div> |
| | | |
| | |
| | | <n-select v-model:value="formModel.roleIds" :options="roleOptions" placeholder="请选择" clearable multiple/> |
| | | </n-form-item> |
| | | </div> |
| | | <div class="grid grid-cols-2 gap-4"> |
| | | <n-form-item label="性别" path="sex"> |
| | | <n-radio-group v-model:value="formModel.sex" size="small"> |
| | | <n-radio label="男" :value="1">男</n-radio> |
| | | <n-radio label="女" :value="2">女</n-radio> |
| | | </n-radio-group> |
| | | </n-form-item> |
| | | |
| | | </div> |
| | | <n-form-item label="备注" path="remarks"> |
| | | <n-input |
| | | v-model:value="formModel.remarks" |
| | | type="textarea" |
| | | placeholder="请输入" |
| | | :rows="3" |
| | | :maxlength="200" |
| | | show-count |
| | | clearable |
| | | /> |
| | | </n-form-item> |
| | |
| | | ], |
| | | newPassword: [ |
| | | { required: true, message: '请输入新密码', trigger: 'blur' }, |
| | | { min: 6, message: '密码长度至少6位', trigger: 'blur' }, |
| | | { |
| | | pattern: /^.{8,20}$/, |
| | | message: '密码长度必须为8-20位字符', |
| | | trigger: 'blur' |
| | | }, |
| | | { |
| | | validator: (rule: any, value: string) => { |
| | | if (!value) return true; |
| | | |
| | | // 检查是否包含大小写字母、数字、特殊符号 |
| | | const hasLowercase = /[a-z]/.test(value); |
| | | const hasUppercase = /[A-Z]/.test(value); |
| | | const hasDigit = /\d/.test(value); |
| | | const hasSpecialChar = /[^a-zA-Z0-9]/.test(value); // 非字母数字字符即为特殊字符 |
| | | |
| | | // 计算包含的字符类型数量 |
| | | const charTypesCount = [hasLowercase, hasUppercase, hasDigit, hasSpecialChar].filter(Boolean).length; |
| | | |
| | | // 至少需要两种字符类型 |
| | | return charTypesCount >= 2; |
| | | }, |
| | | message: '密码必须包含大小写字母、数字、特殊符号中的至少两种', |
| | | trigger: 'blur' |
| | | } |
| | | ], |
| | | confirmPassword: [ |
| | | { |
| | |
| | | |
| | | <n-card style="border-radius: 6px" content-style="padding: 12px"> |
| | | <div class="mb-4"> |
| | | <div class="text-sm font-medium mb-3">身份证:</div> |
| | | <div class="flex gap-4"> |
| | | <div class="text-sm font-medium">身份证:</div> |
| | | <div class="flex gap-4" v-if="personnelInfo.idCardFront || personnelInfo.idCardBack"> |
| | | <n-image |
| | | width="128" |
| | | height="80" |
| | |
| | | style="border-radius: 4px" |
| | | /> |
| | | </div> |
| | | <div v-else> |
| | | <span>-</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="mb-4"> |
| | | <div class="text-sm font-medium mb-3">驾驶证:</div> |
| | | <div class="text-sm font-medium">驾驶证:</div> |
| | | <n-image |
| | | width="128" |
| | | height="80" |
| | |
| | | object-fit="cover" |
| | | style="border-radius: 4px" |
| | | /> |
| | | <div v-else> |
| | | <span>-</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div> |
| | | <div class="text-sm font-medium mb-3">无犯罪记录证明:</div> |
| | | <div class="text-sm font-medium">无犯罪记录证明:</div> |
| | | <n-image |
| | | width="128" |
| | | height="80" |
| | |
| | | object-fit="cover" |
| | | style="border-radius: 4px" |
| | | /> |
| | | <div v-else> |
| | | <span>-</span> |
| | | </div> |
| | | </div> |
| | | </n-card> |
| | | </div> |
| | |
| | | import { useRouter } from 'vue-router' |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { NButton, NTag, NDropdown } from 'naive-ui' |
| | | import { usePermission } from '@/hooks' |
| | | import { getOperationsUserList, exportOperationsUserTemplate, exportOperationsUser, importOperationsUser, |
| | | addOperationsUser, editOperationsUser, updateOperationsUserStatus, deleteOperationsUser } from '@/service/api/attendance' |
| | | import { fetchRoleList } from '@/service/api/fleet' |
| | |
| | | import SearchPanel from '@/components/custom/SearchPanel.vue' |
| | | import PersonnelFormModal from './components/PersonnelFormModal.vue' |
| | | import ResetPasswordModal from './components/ResetPasswordModal.vue' |
| | | import dayjs from 'dayjs' |
| | | |
| | | const router = useRouter() |
| | | const dialog = useDialog() |
| | | const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false) |
| | | const { hasPermission } = usePermission() |
| | | |
| | | const searchModel = ref({ |
| | | nickName: '', |
| | |
| | | roleId: null, |
| | | status: null |
| | | } |
| | | getList() |
| | | } |
| | | |
| | | const handleExport = () => { |
| | |
| | | { |
| | | label: '编辑', |
| | | key: 'edit', |
| | | show: hasPermission(70115), |
| | | props: { |
| | | onClick: () => handleEdit(row) |
| | | } |
| | |
| | | { |
| | | label: row.status ? '禁用' : '启用', |
| | | key: 'toggle', |
| | | show: hasPermission(70116), |
| | | props: { |
| | | onClick: () => { |
| | | updateOperationsUserStatus({ |
| | |
| | | { |
| | | label: '重置密码', |
| | | key: 'password', |
| | | show: hasPermission(70117), |
| | | props: { |
| | | onClick: () => { |
| | | handleResetPassword(row) |
| | |
| | | { |
| | | label: '删除', |
| | | key: 'delete', |
| | | show: hasPermission(70118), |
| | | props: { |
| | | onClick: () => { |
| | | dialog.create({ |
| | | title: '确认删除吗?', |
| | | content: '', |
| | | title: '是否确认删除数据?', |
| | | content: `确认删除该人员?此操作不可恢复`, |
| | | positiveText: '删除', |
| | | negativeText: '取消', |
| | | draggable: true, |
| | |
| | | row.status ? <NTag type="primary" size="small" style={{ borderRadius: '4px' }} >启用</NTag> : <NTag type="error" size="small" style={{ borderRadius: '4px' }} >禁用</NTag> |
| | | ) |
| | | }, |
| | | { title: '创建时间', key: 'createTime', width: 180 , resizable: true }, |
| | | { title: '备注', key: 'remarks', width: 150, resizable: true }, |
| | | { title: '创建时间', key: 'createTime', width: 180 , resizable: true, render: (row) => dayjs(row.createTime).format('YYYY-MM-DD HH:mm') }, |
| | | { title: '备注', key: 'remarks', width: 150, resizable: true, ellipsis: { tooltip: true }, render: (row) => row.remarks || '-' }, |
| | | { |
| | | title: '操作', |
| | | key: 'actions', |
| | |
| | | fixed: 'right', |
| | | render: (row) => ( |
| | | <div class="flex gap-3"> |
| | | <a style={{ color: '#0068FF', cursor: 'pointer' }} onClick={() => router.push(`/operations/personnel/personnelManagement/detail/${row.id}`)}>详情</a> |
| | | <a v-permission="70111" style={{ color: '#0068FF', cursor: 'pointer' }} onClick={() => router.push(`/operations/personnel/personnelManagement/detail/${row.id}`)}>详情</a> |
| | | <NDropdown options={moreOptions(row)} trigger="click"> |
| | | <a style={{ color: '#0068FF', cursor: 'pointer' }}>更多</a> |
| | | </NDropdown> |
| | |
| | | <n-card class="flex-1 mt-12px"> |
| | | <template #header> |
| | | <n-flex justify="flex-end"> |
| | | <n-button @click="handleExport" style="width: 84px; height: 32px">导出</n-button> |
| | | <n-button @click="handleImport" style="width: 84px; height: 32px">导入</n-button> |
| | | <n-button @click="handleDownloadTemplate" style="width: 84px; height: 32px">下载模板</n-button> |
| | | <n-button style="width: 84px;height: 32px;background-color: #E6F4FF;color: #0068FF;border: 1px solid #7ABDFF;" |
| | | <n-button v-permission="70112" @click="handleExport" style="width: 84px; height: 32px">导出</n-button> |
| | | <n-button v-permission="70113" @click="handleImport" style="width: 84px; height: 32px">导入</n-button> |
| | | <n-button v-permission="70112" @click="handleDownloadTemplate" style="width: 84px; height: 32px">下载模板</n-button> |
| | | <n-button v-permission="70114" style="width: 84px;height: 32px;background-color: #E6F4FF;color: #0068FF;border: 1px solid #7ABDFF;" |
| | | @click="handleAdd">新增</n-button> |
| | | </n-flex> |
| | | </template> |
| | |
| | | formModel.value.id = id |
| | | const res = await getTaskOrderDetail(id) |
| | | if (res.code === 200) { |
| | | console.log(res.data) |
| | | formModel.value = { ...formModel.value, ...res.data } |
| | | if (formModel.value.accidentDetailVO?.vehiclePictures) { |
| | | formModel.value.accidentDetailVO.vehiclePictures = JSON.parse(formModel.value.accidentDetailVO.vehiclePictures) |
| | |
| | | v-if="formModel.detectionDetailVO?.vehiclePictures" /> |
| | | <div> |
| | | <div class="plateNumber mb-8px">{{ formModel.detectionDetailVO.plateNumber }}</div> |
| | | <div class="value">{{ formModel.detectionDetailVO.vehicleColor }} | 车身编号:{{ |
| | | formModel.detectionDetailVO.vehicleCode }} | VIN码:{{ |
| | | formModel.detectionDetailVO.vehicleVin }}</div> |
| | | <div class="value mb-8px">{{ formModel.detectionDetailVO.vehicleColor }} | VIN码:{{ |
| | | formModel.detectionDetailVO.vehicleVin }} |
| | | </div> |
| | | <div class="value">车辆位置:{{ formModel.detectionDetailVO.vehicleLocation }}</div> |
| | | </div> |
| | | </div> |
| | | <div class="head mb-12px">系统检测</div> |
| | |
| | | </div> |
| | | <div class="mb-16px">任务单号:{{ formModel.detectionDetailVO.id }}</div> |
| | | <div class="mb-16px">操作人:{{ formModel.detectionDetailVO.operationsUserName }}</div> |
| | | <div class="mb-16px">操作时间:{{ dayjs(formModel.detectionDetailVO.checkTime).format('YYYY-MM-DD HH:mm:ss') }}</div> |
| | | <div class="mb-16px">操作时间:{{ formModel.detectionDetailVO.checkTime ? dayjs(formModel.detectionDetailVO.checkTime).format('YYYY-MM-DD HH:mm:ss') : '' }}</div> |
| | | </div> |
| | | </div> |
| | | <!-- 充电详情 --> |
| | |
| | | </div> |
| | | <div class="mb-16px">任务单号:{{ formModel.chargingDetailVO.id }}</div> |
| | | <div class="mb-16px">开始充电前电量:{{ formModel.chargingDetailVO.startRemainingBattery }}%</div> |
| | | <div class="mb-16px">开始充电时间:{{ dayjs(formModel.chargingDetailVO.startTime).format('YYYY-MM-DD HH:mm:ss') }} |
| | | <div class="mb-16px">开始充电时间:{{ formModel.chargingDetailVO.startTime ? dayjs(formModel.chargingDetailVO.startTime).format('YYYY-MM-DD HH:mm:ss') : '' }} |
| | | </div> |
| | | <div class="mb-16px">开始充电操作人:{{ formModel.chargingDetailVO.startOperationsUserName }}</div> |
| | | <div class="mb-16px"> |
| | |
| | | </div> |
| | | </div> |
| | | <div class="mb-16px">充电后电量:{{ formModel.chargingDetailVO.endRemainingBattery }}%</div> |
| | | <div class="mb-16px">结束充电时间:{{ dayjs(formModel.chargingDetailVO.endTime).format('YYYY-MM-DD HH:mm:ss') }}</div> |
| | | <div class="mb-16px">结束充电时间:{{ formModel.chargingDetailVO.endTime ? dayjs(formModel.chargingDetailVO.endTime).format('YYYY-MM-DD HH:mm:ss') : '' }}</div> |
| | | <div class="mb-16px">结束充电操作人:{{ formModel.chargingDetailVO.endOperationsUserName }}</div> |
| | | <div class="mb-16px"> |
| | | <div class="mb-8px">结束充电图片:</div> |
| | |
| | | <img :src="formModel.preparationDetailVO.vehiclePictures" class="w-112px h-64px mr-16px" |
| | | v-if="formModel.preparationDetailVO?.vehiclePictures" /> |
| | | <div> |
| | | <div class="plateNumber mb-8px">{{ formModel.chargingDetailVO.plateNumber }}</div> |
| | | <div class="value mb-8px">{{ formModel.chargingDetailVO.vehicleColor }} | VIN码:{{ |
| | | formModel.chargingDetailVO.vehicleVin }} |
| | | <div class="plateNumber mb-8px">{{ formModel.preparationDetailVO.plateNumber }}</div> |
| | | <div class="value mb-8px">{{ formModel.preparationDetailVO.vehicleColor }} | VIN码:{{ |
| | | formModel.preparationDetailVO.vehicleVin }} |
| | | </div> |
| | | <div class="value">车辆位置:{{ formModel.chargingDetailVO.vehicleLocation }}</div> |
| | | <div class="value">车辆位置:{{ formModel.preparationDetailVO.vehicleLocation }}</div> |
| | | </div> |
| | | </div> |
| | | <div class="mb-16px"> |
| | |
| | | </div> |
| | | <div class="mb-16px">任务单号:{{ formModel.preparationDetailVO.id }}</div> |
| | | <div class="mb-16px">操作人:{{ formModel.preparationDetailVO.operationsUserName }}</div> |
| | | <div class="mb-16px">操作时间:{{ dayjs(formModel.preparationDetailVO.handleTime).format('YYYY-MM-DD HH:mm:ss') }} |
| | | <div class="mb-16px">操作时间:{{ formModel.preparationDetailVO.handleTime ? dayjs(formModel.preparationDetailVO.handleTime).format('YYYY-MM-DD HH:mm:ss') : '' }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | |
| | | <img :src="formModel.cleanDetailVO.vehiclePictures" class="w-112px h-64px mr-16px" |
| | | v-if="formModel.cleanDetailVO?.vehiclePictures" /> |
| | | <div> |
| | | <div class="plateNumber mb-8px">{{ formModel.chargingDetailVO.plateNumber }}</div> |
| | | <div class="value mb-8px">{{ formModel.chargingDetailVO.vehicleColor }} | VIN码:{{ |
| | | formModel.chargingDetailVO.vehicleVin }} |
| | | <div class="plateNumber mb-8px">{{ formModel.cleanDetailVO.plateNumber }}</div> |
| | | <div class="value mb-8px">{{ formModel.cleanDetailVO.vehicleColor }} | VIN码:{{ |
| | | formModel.cleanDetailVO.vehicleVin }} |
| | | </div> |
| | | <div class="value">车辆位置:{{ formModel.chargingDetailVO.vehicleLocation }}</div> |
| | | <div class="value">车辆位置:{{ formModel.cleanDetailVO.vehicleLocation }}</div> |
| | | </div> |
| | | </div> |
| | | <div class="mb-16px"> |
| | |
| | | </div> |
| | | <div class="mb-16px">任务单号:{{ formModel.cleanDetailVO.id }}</div> |
| | | <div class="mb-16px">操作人:{{ formModel.cleanDetailVO.operationsUserName }}</div> |
| | | <div class="mb-16px">操作时间:{{ dayjs(formModel.cleanDetailVO.handleTime).format('YYYY-MM-DD HH:mm:ss') }}</div> |
| | | <div class="mb-16px">操作时间:{{ formModel.cleanDetailVO.handleTime ? dayjs(formModel.cleanDetailVO.handleTime).format('YYYY-MM-DD HH:mm:ss') : '' }}</div> |
| | | </div> |
| | | </div> |
| | | <!-- 维修详情 --> |
| | |
| | | <img :src="formModel.repairDetailVO.vehiclePictures" class="w-112px h-64px mr-16px" |
| | | v-if="formModel.repairDetailVO?.vehiclePictures" /> |
| | | <div> |
| | | <div class="plateNumber mb-8px">{{ formModel.chargingDetailVO.plateNumber }}</div> |
| | | <div class="value mb-8px">{{ formModel.chargingDetailVO.vehicleColor }} | VIN码:{{ |
| | | formModel.chargingDetailVO.vehicleVin }} |
| | | <div class="plateNumber mb-8px">{{ formModel.repairDetailVO.plateNumber }}</div> |
| | | <div class="value mb-8px">{{ formModel.repairDetailVO.vehicleColor }} | VIN码:{{ |
| | | formModel.repairDetailVO.vehicleVin }} |
| | | </div> |
| | | <div class="value">车辆位置:{{ formModel.chargingDetailVO.vehicleLocation }}</div> |
| | | <div class="value">车辆位置:{{ formModel.repairDetailVO.vehicleLocation }}</div> |
| | | </div> |
| | | </div> |
| | | <div class="mb-16px">维修停运时间:{{ dayjs(formModel.repairDetailVO.startTime).format('YYYY-MM-DD HH:mm:ss') }}</div> |
| | | <div class="mb-16px">维修停运时间:{{ formModel.repairDetailVO.startTime ? dayjs(formModel.repairDetailVO.startTime).format('YYYY-MM-DD HH:mm:ss') : '' }}</div> |
| | | <div class="mb-16px">维修原因:{{ formModel.repairDetailVO.repairReason }}</div> |
| | | <div class="mb-16px"> |
| | | <div class="label mb-8px">维修前图片:</div> |
| | |
| | | :src="item.url" class="w-56px h-56px mr-16px" /> |
| | | </div> |
| | | </div> |
| | | <div class="mb-16px">送修时间:{{ dayjs(formModel.repairDetailVO.sendTime).format('YYYY-MM-DD HH:mm:ss') }}</div> |
| | | <div class="mb-16px">送修时间:{{ formModel.repairDetailVO.sendTime ? dayjs(formModel.repairDetailVO.sendTime).format('YYYY-MM-DD HH:mm:ss') : '' }}</div> |
| | | <div class="mb-16px">操作人:{{ formModel.repairDetailVO.sendUserName }}</div> |
| | | <div class="mb-16px">维修总费用:{{ formModel.repairDetailVO.repairMoney }}</div> |
| | | <div class="mb-16px"> |
| | |
| | | :src="item.url" class="w-56px h-56px mr-16px" /> |
| | | </div> |
| | | </div> |
| | | <div class="mb-16px">取回时间:{{ dayjs(formModel.repairDetailVO.returnTime).format('YYYY-MM-DD HH:mm:ss') }}</div> |
| | | <div class="mb-16px">取回时间:{{ formModel.repairDetailVO.returnTime ? dayjs(formModel.repairDetailVO.returnTime).format('YYYY-MM-DD HH:mm:ss') : '' }}</div> |
| | | <div class="mb-16px">操作人:{{ formModel.repairDetailVO.returnUserName }}</div> |
| | | <div class="mb-16px">任务单号:{{ formModel.repairDetailVO.id }}</div> |
| | | </div> |
| | |
| | | <img :src="formModel.routineDetailVO.vehiclePictures" class="w-112px h-64px mr-16px" |
| | | v-if="formModel.routineDetailVO?.vehiclePictures" /> |
| | | <div> |
| | | <div class="plateNumber mb-8px">{{ formModel.chargingDetailVO.plateNumber }}</div> |
| | | <div class="value mb-8px">{{ formModel.chargingDetailVO.vehicleColor }} | VIN码:{{ |
| | | formModel.chargingDetailVO.vehicleVin }} |
| | | <div class="plateNumber mb-8px">{{ formModel.routineDetailVO.plateNumber }}</div> |
| | | <div class="value mb-8px">{{ formModel.routineDetailVO.vehicleColor }} | VIN码:{{ |
| | | formModel.routineDetailVO.vehicleVin }} |
| | | </div> |
| | | <div class="value">车辆位置:{{ formModel.chargingDetailVO.vehicleLocation }}</div> |
| | | <div class="value">车辆位置:{{ formModel.routineDetailVO.vehicleLocation }}</div> |
| | | </div> |
| | | </div> |
| | | <div class="label mb-16px">任务完成情况:{{ formModel.routineDetailVO.taskDescription }}</div> |
| | |
| | | </div> |
| | | <div class="mb-16px">任务单号:{{ formModel.routineDetailVO.id }}</div> |
| | | <div class="mb-16px">操作人:{{ formModel.routineDetailVO.operationsUserName }}</div> |
| | | <div class="mb-16px">操作时间:{{ dayjs(formModel.routineDetailVO.checkTime).format('YYYY-MM-DD HH:mm:ss') }}</div> |
| | | <div class="mb-16px">操作时间:{{ formModel.routineDetailVO.checkTime ? dayjs(formModel.routineDetailVO.checkTime).format('YYYY-MM-DD HH:mm:ss') : '' }}</div> |
| | | </div> |
| | | </div> |
| | | <template #action> |
| | |
| | | <NSpace> |
| | | {(row.status === 0 || row.status === 1) && <NButton |
| | | text |
| | | v-permission="7053" |
| | | type='primary' |
| | | onClick={() => modalRef.value.openModal(row.id)} |
| | | > |
| | |
| | | </NButton>} |
| | | <NButton |
| | | text |
| | | v-permission="7054" |
| | | type='primary' |
| | | onClick={() => detailModalRef.value.openModal(row.id)} |
| | | > |
| | | 详情 |
| | | </NButton> |
| | | <NPopconfirm onPositiveClick={() => handleDeltete(row.id!)}> |
| | | <NPopconfirm v-permission="7055" onPositiveClick={() => handleDeltete(row.id!)}> |
| | | {{ |
| | | default: () => '确认删除', |
| | | trigger: () => <NButton text type="error">删除</NButton>, |
| | |
| | | <n-card style="border-radius: 6px; margin-top: 12px" content-style="padding: 12px"> |
| | | <template #header> |
| | | <n-flex justify="flex-end"> |
| | | <n-button @click="handleExport" style="width: 84px; height: 32px">导出</n-button> |
| | | <NButton type="primary" secondary @click="handleAdd"> |
| | | <n-button v-permission="7051" @click="handleExport" style="width: 84px; height: 32px">导出</n-button> |
| | | <NButton v-permission="7052" type="primary" secondary @click="handleAdd"> |
| | | 新增任务 |
| | | </NButton> |
| | | </n-flex> |
| | |
| | | <script setup lang="ts"> |
| | | import { ref, h, onMounted } from "vue"; |
| | | <script setup lang="tsx"> |
| | | import { ref, onMounted } from "vue"; |
| | | import type { DataTableColumns } from "naive-ui"; |
| | | import { NButton, NTag, NAvatar, NImage } from "naive-ui"; |
| | | import { useBoolean } from "@/hooks"; |
| | |
| | | align: "center", |
| | | render: (row) => { |
| | | const displayName = row.nickName || "-"; |
| | | return h("div", { class: "flex items-center justify-center gap-2" }, [ |
| | | h( |
| | | NAvatar, |
| | | { |
| | | round: true, |
| | | size: 32, |
| | | style: { backgroundColor: "#e0e0e0" }, |
| | | src: row.avatar || undefined, |
| | | }, |
| | | { default: () => displayName.charAt(0) } |
| | | ), |
| | | h("span", displayName), |
| | | ]); |
| | | return ( |
| | | <div class="flex items-center justify-center gap-2"> |
| | | <NAvatar |
| | | round |
| | | size={32} |
| | | style={{ backgroundColor: "#e0e0e0" }} |
| | | src={row.avatar || undefined} |
| | | > |
| | | {displayName.charAt(0)} |
| | | </NAvatar> |
| | | <span>{displayName}</span> |
| | | </div> |
| | | ); |
| | | }, |
| | | }, |
| | | { |
| | |
| | | const roleName = Array.isArray(row.roleNames) |
| | | ? row.roleNames.join("、") |
| | | : row.roleNames || "-"; |
| | | return h( |
| | | NTag, |
| | | { type: "info", size: "small" }, |
| | | { |
| | | default: () => roleName, |
| | | } |
| | | return ( |
| | | <NTag type="info" size="small"> |
| | | {roleName} |
| | | </NTag> |
| | | ); |
| | | }, |
| | | }, |
| | |
| | | render: (row) => { |
| | | const statusText = auditStatusLabelMap[row.auditStatus]; |
| | | const tagType = auditStatusTagTypeMap[row.auditStatus]; |
| | | return h( |
| | | NTag, |
| | | { type: tagType as any, size: "small" }, |
| | | { |
| | | default: () => statusText, |
| | | } |
| | | return ( |
| | | <NTag type={tagType as any} size="small"> |
| | | {statusText} |
| | | </NTag> |
| | | ); |
| | | }, |
| | | }, |
| | |
| | | if (row.idCardFront && row.idCardBack) { |
| | | const idCardFront = JSON.parse(row.idCardFront).url; |
| | | const idCardBack = JSON.parse(row.idCardBack).url; |
| | | return h("div", { class: "flex gap-1 justify-center" }, [ |
| | | h(NImage, { |
| | | width: 60, |
| | | height: 40, |
| | | src: idCardFront, |
| | | objectFit: "cover", |
| | | style: { borderRadius: "4px" }, |
| | | previewSrc: idCardFront, |
| | | }), |
| | | h(NImage, { |
| | | width: 60, |
| | | height: 40, |
| | | src: idCardBack, |
| | | objectFit: "cover", |
| | | style: { borderRadius: "4px" }, |
| | | previewSrc: idCardBack, |
| | | }), |
| | | ]); |
| | | return ( |
| | | <div class="flex gap-1 justify-center"> |
| | | <NImage |
| | | width={60} |
| | | height={40} |
| | | src={idCardFront} |
| | | objectFit="cover" |
| | | style={{ borderRadius: "4px" }} |
| | | previewSrc={idCardFront} |
| | | /> |
| | | <NImage |
| | | width={60} |
| | | height={40} |
| | | src={idCardBack} |
| | | objectFit="cover" |
| | | style={{ borderRadius: "4px" }} |
| | | previewSrc={idCardBack} |
| | | /> |
| | | </div> |
| | | ); |
| | | } |
| | | return h( |
| | | "div", |
| | | { |
| | | class: "flex gap-1 justify-center", |
| | | }, |
| | | [ |
| | | h( |
| | | "div", |
| | | { |
| | | style: { |
| | | width: "60px", |
| | | height: "40px", |
| | | backgroundColor: "#f0f0f0", |
| | | borderRadius: "4px", |
| | | display: "flex", |
| | | alignItems: "center", |
| | | justifyContent: "center", |
| | | }, |
| | | }, |
| | | "📷" |
| | | ), |
| | | h( |
| | | "div", |
| | | { |
| | | style: { |
| | | width: "60px", |
| | | height: "40px", |
| | | backgroundColor: "#f0f0f0", |
| | | borderRadius: "4px", |
| | | display: "flex", |
| | | alignItems: "center", |
| | | justifyContent: "center", |
| | | }, |
| | | }, |
| | | "📷" |
| | | ), |
| | | ] |
| | | return ( |
| | | <div class="flex gap-1 justify-center"> |
| | | <div |
| | | style={{ |
| | | width: "60px", |
| | | height: "40px", |
| | | backgroundColor: "#f0f0f0", |
| | | borderRadius: "4px", |
| | | display: "flex", |
| | | alignItems: "center", |
| | | justifyContent: "center", |
| | | }} |
| | | > |
| | | 📷 |
| | | </div> |
| | | <div |
| | | style={{ |
| | | width: "60px", |
| | | height: "40px", |
| | | backgroundColor: "#f0f0f0", |
| | | borderRadius: "4px", |
| | | display: "flex", |
| | | alignItems: "center", |
| | | justifyContent: "center", |
| | | }} |
| | | > |
| | | 📷 |
| | | </div> |
| | | </div> |
| | | ); |
| | | }, |
| | | }, |
| | |
| | | render: (row) => { |
| | | if (row.driverLicense) { |
| | | const driverLicense = JSON.parse(row.driverLicense).url; |
| | | return h(NImage, { |
| | | width: 60, |
| | | height: 40, |
| | | src: driverLicense, |
| | | objectFit: "cover", |
| | | style: { borderRadius: "4px" }, |
| | | previewSrc: driverLicense, |
| | | }); |
| | | return ( |
| | | <NImage |
| | | width={60} |
| | | height={40} |
| | | src={driverLicense} |
| | | objectFit="cover" |
| | | style={{ borderRadius: "4px" }} |
| | | previewSrc={driverLicense} |
| | | /> |
| | | ); |
| | | } |
| | | return h( |
| | | "div", |
| | | { |
| | | style: { |
| | | return ( |
| | | <div |
| | | style={{ |
| | | width: "60px", |
| | | height: "40px", |
| | | backgroundColor: "#f0f0f0", |
| | |
| | | alignItems: "center", |
| | | justifyContent: "center", |
| | | margin: "0 auto", |
| | | }, |
| | | }, |
| | | "📷" |
| | | }} |
| | | > |
| | | 📷 |
| | | </div> |
| | | ); |
| | | }, |
| | | }, |
| | |
| | | const certificateOfNoCriminalRecord = JSON.parse( |
| | | row.certificateOfNoCriminalRecord |
| | | ).url; |
| | | return h(NImage, { |
| | | width: 60, |
| | | height: 40, |
| | | src: certificateOfNoCriminalRecord, |
| | | objectFit: "cover", |
| | | style: { borderRadius: "4px" }, |
| | | previewSrc: certificateOfNoCriminalRecord, |
| | | }); |
| | | return ( |
| | | <NImage |
| | | width={60} |
| | | height={40} |
| | | src={certificateOfNoCriminalRecord} |
| | | objectFit="cover" |
| | | style={{ borderRadius: "4px" }} |
| | | previewSrc={certificateOfNoCriminalRecord} |
| | | /> |
| | | ); |
| | | } |
| | | return h( |
| | | "div", |
| | | { |
| | | style: { |
| | | return ( |
| | | <div |
| | | style={{ |
| | | width: "60px", |
| | | height: "40px", |
| | | backgroundColor: "#f0f0f0", |
| | |
| | | alignItems: "center", |
| | | justifyContent: "center", |
| | | margin: "0 auto", |
| | | }, |
| | | }, |
| | | "📷" |
| | | }} |
| | | > |
| | | 📷 |
| | | </div> |
| | | ); |
| | | }, |
| | | }, |
| | |
| | | align: "center", |
| | | fixed: "right", |
| | | render: (row) => { |
| | | return h("div", { class: "flex items-center justify-center gap-2" }, [ |
| | | row.auditStatus === 1 |
| | | ? h( |
| | | NButton, |
| | | { |
| | | text: true, |
| | | type: "primary", |
| | | onClick: () => handleApprove(row.id), |
| | | }, |
| | | { default: () => "审核" } |
| | | ) |
| | | : null, |
| | | h( |
| | | NButton, |
| | | { |
| | | text: true, |
| | | type: "error", |
| | | onClick: () => handleDelete(row.id), |
| | | }, |
| | | { default: () => "删除" } |
| | | ), |
| | | ]); |
| | | return ( |
| | | <div class="flex items-center justify-center gap-2"> |
| | | {row.auditStatus === 1 ? ( |
| | | <NButton |
| | | v-permission="80112" |
| | | text |
| | | type="primary" |
| | | onClick={() => handleApprove(row.id)} |
| | | > |
| | | 审核 |
| | | </NButton> |
| | | ) : null} |
| | | <NButton |
| | | v-permission="80113" |
| | | text |
| | | type="error" |
| | | onClick={() => handleDelete(row.id)} |
| | | > |
| | | 删除 |
| | | </NButton> |
| | | </div> |
| | | ); |
| | | }, |
| | | }, |
| | | ]; |
| | |
| | | > |
| | | <div class="mb-4 flex justify-end gap-2"> |
| | | <n-button |
| | | v-permission="80111" |
| | | style=" |
| | | width: 84px; |
| | | height: 32px; |
| | |
| | | <script setup lang="ts"> |
| | | <script setup lang="tsx"> |
| | | import { ref, h, onMounted } from "vue"; |
| | | import type { DataTableColumns } from "naive-ui"; |
| | | import { NButton, NTag, NTooltip } from "naive-ui"; |
| | |
| | | align: "center", |
| | | fixed: "right", |
| | | render: (row) => { |
| | | return h("div", { class: "flex items-center justify-center gap-2" }, [ |
| | | row.auditStatus === 1 |
| | | ? h( |
| | | NButton, |
| | | { |
| | | text: true, |
| | | type: "primary", |
| | | onClick: () => handleApprove(row.id), |
| | | }, |
| | | { default: () => "审核" } |
| | | ) |
| | | : null, |
| | | h( |
| | | NButton, |
| | | { |
| | | text: true, |
| | | type: "error", |
| | | onClick: () => handleDelete(row.id), |
| | | }, |
| | | { default: () => "删除" } |
| | | ), |
| | | ]); |
| | | return ( |
| | | <div class="flex items-center justify-center gap-2"> |
| | | {row.auditStatus === 1 ? ( |
| | | <NButton |
| | | v-permission="80122" |
| | | text |
| | | type="primary" |
| | | onClick={() => handleApprove(row.id)} |
| | | > |
| | | 审核 |
| | | </NButton> |
| | | ) : null} |
| | | <NButton |
| | | v-permission="80123" |
| | | text |
| | | type="error" |
| | | onClick={() => handleDelete(row.id)} |
| | | > |
| | | 删除 |
| | | </NButton> |
| | | </div> |
| | | ); |
| | | }, |
| | | }, |
| | | ]; |
| | |
| | | positiveText: "确定", |
| | | negativeText: "取消", |
| | | onPositiveClick: async () => { |
| | | const { error } = await deletePolicyApplicationById(id); |
| | | if (!error) { |
| | | const res = await deletePolicyApplicationById(id); |
| | | if (res.code === 200) { |
| | | window.$message?.success("删除成功"); |
| | | getPolicyApplicationList(); |
| | | } else { |
| | |
| | | |
| | | const handleConfirmReview = async (data: any) => { |
| | | const auditStatus = data?.result === "pass" ? 2 : 3; |
| | | const { error } = await batchAuditPolicyApplication({ |
| | | const res = await batchAuditPolicyApplication({ |
| | | auditStatus, |
| | | ids: selectedIds.value, |
| | | auditRemarks: data?.auditRemarks || "", |
| | | }); |
| | | if (!error) { |
| | | if (res.code === 200) { |
| | | window.$message?.success("审核成功"); |
| | | selectedIds.value = []; |
| | | getPolicyApplicationList(); |
| | |
| | | > |
| | | <div class="mb-4 flex justify-end gap-2"> |
| | | <n-button |
| | | v-permission="80121" |
| | | style=" |
| | | width: 84px; |
| | | height: 32px; |
| | |
| | | :data="tableData" |
| | | :loading="loading" |
| | | :scroll-x="1500" |
| | | :row-key="(row: PolicyApplicationItem) => row.id" |
| | | :row-key="(row: any) => row.id" |
| | | @update:checked-row-keys="handleCheck" |
| | | /> |
| | | <Pagination :count="count" @change="changePage" /> |
| | |
| | | <script setup lang="ts"> |
| | | <script setup lang="tsx"> |
| | | import { ref, h, computed, onMounted } from "vue"; |
| | | import type { DataTableColumns, FormInst, FormRules } from "naive-ui"; |
| | | import { NButton } from "naive-ui"; |
| | |
| | | updateFeedbackType, |
| | | deleteFeedbackType, |
| | | } from "@/service/api/serviceCenter"; |
| | | |
| | | interface FeedbackTypeItem { |
| | | id: number; |
| | | feedbackType: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | const { bool: loading, setTrue: startLoading, setFalse: endLoading } = |
| | | useBoolean(false); |
| | |
| | | key: "actions", |
| | | width: 160, |
| | | align: "center", |
| | | render: (row) => |
| | | h("div", { class: "flex items-center justify-center gap-2" }, [ |
| | | h( |
| | | NButton, |
| | | { |
| | | text: true, |
| | | type: "primary", |
| | | onClick: () => handleEdit(row), |
| | | }, |
| | | { default: () => "编辑" } |
| | | ), |
| | | h( |
| | | NButton, |
| | | { |
| | | text: true, |
| | | type: "error", |
| | | onClick: () => handleDelete(row), |
| | | }, |
| | | { default: () => "删除" } |
| | | ), |
| | | ]), |
| | | render: (row) => { |
| | | return ( |
| | | <div class="flex items-center justify-center gap-2"> |
| | | <NButton |
| | | v-permission="80232" |
| | | text |
| | | type="primary" |
| | | onClick={() => handleEdit(row)} |
| | | > |
| | | 编辑 |
| | | </NButton> |
| | | <NButton |
| | | v-permission="8023" |
| | | text |
| | | type="error" |
| | | onClick={() => handleDelete(row)} |
| | | > |
| | | 删除 |
| | | </NButton> |
| | | </div> |
| | | ); |
| | | }, |
| | | }, |
| | | ]; |
| | | |
| | |
| | | await formRef.value?.validate(); |
| | | const payload = { feedbackType: formModel.value.feedbackType }; |
| | | if (modalMode.value === "add") { |
| | | const { error } = await saveFeedbackType(payload); |
| | | if (!error) { |
| | | const res = await saveFeedbackType(payload); |
| | | if (res.code === 200) { |
| | | window.$message?.success("新增成功"); |
| | | modalVisible.value = false; |
| | | getFeedbackTypeList(); |
| | |
| | | } |
| | | return; |
| | | } |
| | | const { error } = await updateFeedbackType({ |
| | | const res = await updateFeedbackType({ |
| | | id: formModel.value.id as number, |
| | | ...payload, |
| | | }); |
| | | if (!error) { |
| | | if (res.code === 200) { |
| | | window.$message?.success("编辑成功"); |
| | | modalVisible.value = false; |
| | | getFeedbackTypeList(); |
| | |
| | | positiveText: "确定", |
| | | negativeText: "取消", |
| | | onPositiveClick: async () => { |
| | | const { error } = await deleteFeedbackType(row.id); |
| | | if (!error) { |
| | | const res = await deleteFeedbackType(row.id); |
| | | if (res.code === 200) { |
| | | window.$message?.success("删除成功"); |
| | | getFeedbackTypeList(); |
| | | } else { |
| | |
| | | async function getFeedbackTypeList() { |
| | | startLoading(); |
| | | try { |
| | | const res = await fetchFeedbackTypeList(); |
| | | const res = await fetchFeedbackTypeList({ |
| | | pageNum: 1, |
| | | pageSize: 9999, |
| | | }); |
| | | if (res.code === 200 && res.data) { |
| | | const list = Array.isArray(res.data) |
| | | ? res.data |
| | |
| | | > |
| | | <div class="mb-4 flex justify-end gap-2"> |
| | | <n-button |
| | | v-permission="80221" |
| | | style=" |
| | | width: 84px; |
| | | height: 32px; |
| | |
| | | <script setup lang="ts"> |
| | | <script setup lang="tsx"> |
| | | import { ref, h, onMounted } from "vue"; |
| | | import type { DataTableColumns } from "naive-ui"; |
| | | import { NButton } from "naive-ui"; |
| | |
| | | width: 260, |
| | | align: "center", |
| | | render: (row) => { |
| | | const buttons = [ |
| | | h( |
| | | NButton, |
| | | { text: true, type: "primary", onClick: () => handleView(row) }, |
| | | { default: () => "详情" } |
| | | ), |
| | | ]; |
| | | if ([1,2].includes(row.handleStatus)) { |
| | | buttons.push( |
| | | h( |
| | | NButton, |
| | | { text: true, type: "primary", onClick: () => handleStart(row) }, |
| | | { default: () => "处理" } |
| | | ) |
| | | ); |
| | | } |
| | | buttons.push( |
| | | h( |
| | | NButton, |
| | | { text: true, type: "primary", onClick: () => handleEdit(row) }, |
| | | { default: () => "编辑" } |
| | | ), |
| | | h( |
| | | NButton, |
| | | { text: true, type: "error", onClick: () => handleDelete(row) }, |
| | | { default: () => "删除" } |
| | | ) |
| | | ); |
| | | return h( |
| | | "div", |
| | | { class: "flex items-center justify-center gap-2" }, |
| | | buttons |
| | | return ( |
| | | <div class="flex items-center justify-center gap-2"> |
| | | <NButton |
| | | v-permission="80212" |
| | | text |
| | | type="primary" |
| | | onClick={() => handleView(row)} |
| | | > |
| | | 详情 |
| | | </NButton> |
| | | {[1, 2].includes(row.handleStatus || 0) ? ( |
| | | <NButton |
| | | v-permission="80213" |
| | | text |
| | | type="primary" |
| | | onClick={() => handleStart(row)} |
| | | > |
| | | 处理 |
| | | </NButton> |
| | | ) : null} |
| | | <NButton |
| | | v-permission="80215" |
| | | text |
| | | type="primary" |
| | | onClick={() => handleEdit(row)} |
| | | > |
| | | 编辑 |
| | | </NButton> |
| | | <NButton |
| | | v-permission="80216" |
| | | text |
| | | type="error" |
| | | onClick={() => handleDelete(row)} |
| | | > |
| | | 删除 |
| | | </NButton> |
| | | </div> |
| | | ); |
| | | }, |
| | | }, |
| | |
| | | } |
| | | |
| | | function handleStart(row: WorkOrderItem) { |
| | | handleRow.value = row; |
| | | handleModalVisible.value = true; |
| | | } |
| | | |
| | | function handleProcess(row: WorkOrderItem) { |
| | | handleRow.value = row; |
| | | handleModalVisible.value = true; |
| | | } |
| | |
| | | positiveText: "确定", |
| | | negativeText: "取消", |
| | | onPositiveClick: async () => { |
| | | const { error } = await deleteWorkOrder(row.id); |
| | | if (!error) { |
| | | const res = await deleteWorkOrder(row.id); |
| | | if (res.code === 200) { |
| | | window.$message?.success("删除成功"); |
| | | getWorkOrderList(); |
| | | } else { |
| | |
| | | > |
| | | <div class="mb-4 flex justify-end gap-2"> |
| | | <n-button |
| | | v-permission="80211" |
| | | style=" |
| | | width: 96px; |
| | | height: 32px; |
| | |
| | | // 虚拟权限数据 - 运维APP |
| | | const operationPermissions = ref(transformTreeData([ |
| | | { |
| | | id: '10', |
| | | menuName: '车辆管理', |
| | | id: 1, |
| | | menuName: '任务中心', |
| | | type: 'page', |
| | | children: [ |
| | | { |
| | | id: '1001', |
| | | menuName: '运营车辆', |
| | | children: [ |
| | | { id: '100101', menuName: '查看' }, |
| | | { id: '100102', menuName: '编辑' }, |
| | | ], |
| | | }, |
| | | { id: 3, menuName: '筛选', type: 'button' }, |
| | | { id: 4, menuName: '查询', type: 'button' }, |
| | | { id: 5, menuName: '到达事故现场', type: 'button' }, |
| | | { id: 6, menuName: '处理事故', type: 'button' }, |
| | | ], |
| | | }, |
| | | { |
| | | id: '11', |
| | | menuName: '任务管理', |
| | | id: 7, |
| | | menuName: '工作台', |
| | | type: 'page', |
| | | children: [ |
| | | { id: 9, menuName: '事故上报', type: 'button' }, |
| | | { id: 10, menuName: '车辆管理', type: 'button' }, |
| | | { id: 11, menuName: '站点点位', type: 'button' }, |
| | | { |
| | | id: '1101', |
| | | menuName: '任务列表', |
| | | id: 12, |
| | | menuName: '车辆详情', |
| | | type: 'page', |
| | | children: [ |
| | | { id: '110101', menuName: '查看' }, |
| | | { id: '110102', menuName: '接单' }, |
| | | { id: '110103', menuName: '完成' }, |
| | | { id: 13, menuName: '车门解锁', type: 'button' }, |
| | | { id: 14, menuName: '后备箱解锁', type: 'button' }, |
| | | { id: 15, menuName: '发车/收车', type: 'button' }, |
| | | { id: 16, menuName: '接单/测试', type: 'button' }, |
| | | { id: 17, menuName: '车辆整备', type: 'button' }, |
| | | { id: 18, menuName: '车辆充电', type: 'button' }, |
| | | { id: 19, menuName: '车辆检测', type: 'button' }, |
| | | { id: 20, menuName: '车辆维修', type: 'button' }, |
| | | { id: 21, menuName: '车辆清洁', type: 'button' }, |
| | | { id: 22, menuName: '运维记录', type: 'button' }, |
| | | { id: 23, menuName: '查看保单', type: 'button' }, |
| | | ], |
| | | }, |
| | | ], |
| | |
| | | const model = ref({ ...initialModel }) |
| | | function handleResetSearch() { |
| | | model.value = { ...initialModel } |
| | | getList() |
| | | } |
| | | const pageParams = ref({ |
| | | pageNum: 1, |
| | |
| | | 关闭订单 |
| | | </NButton> |
| | | )} |
| | | {row.status === 6 && ( |
| | | {row.status === 8 && ( |
| | | <NButton |
| | | text |
| | | type="primary" |
| | |
| | | > |
| | | 详情 |
| | | </NButton> |
| | | <NButton |
| | | {row.status === 0 && <NButton |
| | | text |
| | | v-permission="60117" |
| | | type="error" |
| | |
| | | }} |
| | | > |
| | | 删除 |
| | | </NButton> |
| | | </NButton>} |
| | | </NSpace> |
| | | ) |
| | | }, |
| | |
| | | { |
| | | label: t('transaction.city'), |
| | | path: 'cityCode', |
| | | render: () => <n-cascader v-model:value={model.value.cityCode} options={pcaOptions.value} value-field="code" label-field="name" check-strategy="all" clearable />, |
| | | render: () => <n-cascader v-model:value={model.value.cityCode} options={pcaOptions.value} value-field="code" label-field="name" check-strategy="all" clearable placement="left-start" />, |
| | | }, |
| | | { |
| | | label: t('transaction.status'), |
| | |
| | | const response = await exportOrderExcel(params) |
| | | if (response instanceof Blob) { |
| | | downloadFile(response, `订单列表_${new Date().getTime()}.xlsx`) |
| | | message.success('导出成功') |
| | | message.success(`导出成功,共${checkedRowKeysRef.value.length > 0 ? checkedRowKeysRef.value.length : count.value}条数据`) |
| | | } |
| | | } |
| | | |
| | |
| | | <n-select v-model:value="formModel.cityId" :options="cityList" /> |
| | | </n-form-item> |
| | | <n-form-item label="说明" path="remark"> |
| | | <n-input v-model:value="formModel.remark" type="textarea" /> |
| | | <n-input v-model:value="formModel.remark" type="textarea" :maxlength="200" show-count/> |
| | | </n-form-item> |
| | | <n-form-item label="计费规则" path="pricingType"> |
| | | <n-radio-group v-model:value="formModel.pricingType" name="pricingType"> |
| | |
| | | return false |
| | | } |
| | | |
| | | const validateTimeRanges = () => { |
| | | // 验证起步价时段 |
| | | if (model.value.startingFareJson && model.value.startingFareJson.length > 0) { |
| | | // 检查每个时段的开始时间不能大于等于结束时间 |
| | | for (const item of model.value.startingFareJson) { |
| | | if (item.num1 && item.num2) { |
| | | // 将时间字符串转换为分钟数进行比较 |
| | | const [startHour, startMinute] = item.num1.split(':').map(Number); |
| | | const [endHour, endMinute] = item.num2.split(':').map(Number); |
| | | |
| | | const startMinutes = startHour * 60 + startMinute; |
| | | const endMinutes = endHour * 60 + endMinute; |
| | | |
| | | if (startMinutes >= endMinutes) { |
| | | window.$message.error('起步价时段中,结束时间必须大于开始时间'); |
| | | return false; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 检查时段之间不能重叠 |
| | | for (let i = 0; i < model.value.startingFareJson.length; i++) { |
| | | for (let j = i + 1; j < model.value.startingFareJson.length; j++) { |
| | | const item1 = model.value.startingFareJson[i]; |
| | | const item2 = model.value.startingFareJson[j]; |
| | | |
| | | if (item1.num1 && item1.num2 && item2.num1 && item2.num2) { |
| | | const [startHour1, startMinute1] = item1.num1.split(':').map(Number); |
| | | const [endHour1, endMinute1] = item1.num2.split(':').map(Number); |
| | | const [startHour2, startMinute2] = item2.num1.split(':').map(Number); |
| | | const [endHour2, endMinute2] = item2.num2.split(':').map(Number); |
| | | |
| | | const start1 = startHour1 * 60 + startMinute1; |
| | | const end1 = endHour1 * 60 + endMinute1; |
| | | const start2 = startHour2 * 60 + startMinute2; |
| | | const end2 = endHour2 * 60 + endMinute2; |
| | | |
| | | // 检查两个时间段是否重叠 |
| | | if (start1 < end2 && start2 < end1) { |
| | | window.$message.error('起步价时段不能重叠'); |
| | | return false; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 验证夜间行驶费用时段 |
| | | if (model.value.basicPricingJson1.num14 && model.value.basicPricingJson1.num141) { |
| | | const [startHour, startMinute] = model.value.basicPricingJson1.num14.split(':').map(Number); |
| | | const [endHour, endMinute] = model.value.basicPricingJson1.num141.split(':').map(Number); |
| | | |
| | | const startMinutes = startHour * 60 + startMinute; |
| | | const endMinutes = endHour * 60 + endMinute; |
| | | |
| | | if (startMinutes >= endMinutes) { |
| | | window.$message.error('夜间行驶费用时段中,结束时间必须大于开始时间'); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | // 验证第一个高峰期时段 |
| | | if (model.value.basicPricingJson1.num21 && model.value.basicPricingJson1.num211) { |
| | | const [startHour, startMinute] = model.value.basicPricingJson1.num21.split(':').map(Number); |
| | | const [endHour, endMinute] = model.value.basicPricingJson1.num211.split(':').map(Number); |
| | | |
| | | const startMinutes = startHour * 60 + startMinute; |
| | | const endMinutes = endHour * 60 + endMinute; |
| | | |
| | | if (startMinutes >= endMinutes) { |
| | | window.$message.error('高峰期费用时段中,结束时间必须大于开始时间'); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | // 验证第二个高峰期时段 |
| | | if (model.value.basicPricingJson1.num22 && model.value.basicPricingJson1.num221) { |
| | | const [startHour, startMinute] = model.value.basicPricingJson1.num22.split(':').map(Number); |
| | | const [endHour, endMinute] = model.value.basicPricingJson1.num221.split(':').map(Number); |
| | | |
| | | const startMinutes = startHour * 60 + startMinute; |
| | | const endMinutes = endHour * 60 + endMinute; |
| | | |
| | | if (startMinutes >= endMinutes) { |
| | | window.$message.error('高峰期费用时段中,结束时间必须大于开始时间'); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | // 检查高峰期时段之间不能重叠 |
| | | if (model.value.basicPricingJson1.num21 && model.value.basicPricingJson1.num211 && |
| | | model.value.basicPricingJson1.num22 && model.value.basicPricingJson1.num221) { |
| | | const [startHour1, startMinute1] = model.value.basicPricingJson1.num21.split(':').map(Number); |
| | | const [endHour1, endMinute1] = model.value.basicPricingJson1.num211.split(':').map(Number); |
| | | const [startHour2, startMinute2] = model.value.basicPricingJson1.num22.split(':').map(Number); |
| | | const [endHour2, endMinute2] = model.value.basicPricingJson1.num221.split(':').map(Number); |
| | | |
| | | const start1 = startHour1 * 60 + startMinute1; |
| | | const end1 = endHour1 * 60 + endMinute1; |
| | | const start2 = startHour2 * 60 + startMinute2; |
| | | const end2 = endHour2 * 60 + endMinute2; |
| | | |
| | | // 检查两个高峰期时段是否重叠 |
| | | if (start1 < end2 && start2 < end1) { |
| | | window.$message.error('高峰期费用时段不能重叠'); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | return true; |
| | | }; |
| | | |
| | | const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false) |
| | | |
| | | const route = useRoute() |
| | |
| | | } |
| | | |
| | | const handleSubmit = async () => { |
| | | if (!validateTimeRanges()) { |
| | | return; |
| | | } |
| | | await formRef.value?.validate() |
| | | if (type.value === 'add') { |
| | | if (category.value === '2' && hasNullValue(model.value.basicPricingJson2)) { |
| | |
| | | } |
| | | }) |
| | | } |
| | | if (hasNullValue(model.value.dynamicPricingJson)) { |
| | | if (model.value.isDynamicPrice === 1 && hasNullValue(model.value.dynamicPricingJson)) { |
| | | window.$message.error('请配置动态定价') |
| | | return |
| | | } |
| | |
| | | id: id.value, |
| | | isDynamicPrice: model.value.isDynamicPrice, |
| | | } |
| | | return |
| | | setPricing(params).then((res) => { |
| | | if (res.code === 200) { |
| | | window.$message.success('设置成功') |
| | |
| | | <div class="pl-4"> |
| | | <n-form-item label="起步价设置" path="num1" required> |
| | | 起步价为 |
| | | <n-input-number v-model:value="model.basicPricingJson2.num1" min="0" max="999" :precision="0" |
| | | <n-input-number v-model:value="model.basicPricingJson2.num1" min="0" max="999" :precision="2" |
| | | :show-button="false" placeholder="最多3位数" class="mx-3" /> |
| | | 元,起步价包含 |
| | | <n-input-number v-model:value="model.basicPricingJson2.num2" min="0" max="999" :precision="0" |
| | |
| | | <div class="pl-4"> |
| | | <n-form-item label="里程费设置" path="num4" required> |
| | | 超过起步里程后每公里 |
| | | <n-input-number v-model:value="model.basicPricingJson2.num4" min="0" max="999" :precision="0" |
| | | <n-input-number v-model:value="model.basicPricingJson2.num4" min="0" max="999" :precision="2" |
| | | :show-button="false" placeholder="最多3位数" class="mx-3" /> |
| | | 元 |
| | | </n-form-item> |
| | |
| | | <div class="pl-4"> |
| | | <n-form-item label="时长费设置" path="num5" required> |
| | | 超过起步时长后每分钟 |
| | | <n-input-number v-model:value="model.basicPricingJson2.num5" min="0" max="999" :precision="0" |
| | | <n-input-number v-model:value="model.basicPricingJson2.num5" min="0" max="999" :precision="2" |
| | | :show-button="false" placeholder="最多3位数" class="mx-3" /> |
| | | 元 |
| | | </n-form-item> |
| | |
| | | <div class="pl-20"> |
| | | <n-form-item label="起步价设置" path="num3" required> |
| | | 起步价为 |
| | | <n-input-number v-model:value="item.num3" min="0" max="999" :precision="0" :show-button="false" |
| | | <n-input-number v-model:value="item.num3" min="0" max="999" :precision="2" :show-button="false" |
| | | placeholder="最多3位数" class="mx-3" /> |
| | | 元,起步价包含 |
| | | <n-input-number v-model:value="item.num4" min="0" max="999" :precision="0" :show-button="false" |
| | |
| | | <div class="pl-20"> |
| | | <n-form-item label="里程费设置" path="num6" required> |
| | | 超过起步里程后每公里 |
| | | <n-input-number v-model:value="item.num6" min="0" max="999" :precision="0" :show-button="false" |
| | | <n-input-number v-model:value="item.num6" min="0" max="999" :precision="2" :show-button="false" |
| | | placeholder="最多3位数" class="mx-3" /> |
| | | 元 |
| | | </n-form-item> |
| | |
| | | <div class="pl-20"> |
| | | <n-form-item label="时长费设置" path="num7" required> |
| | | 超过起步时长后每分钟 |
| | | <n-input-number v-model:value="item.num7" min="0" max="999" :precision="0" :show-button="false" |
| | | <n-input-number v-model:value="item.num7" min="0" max="999" :precision="2" :show-button="false" |
| | | placeholder="最多3位数" class="mx-3" /> |
| | | 元 |
| | | </n-form-item> |
| | |
| | | <n-input-number v-model:value="model.basicPricingJson1.num4" min="0" max="999" :precision="0" |
| | | :show-button="false" placeholder="最多3位数" class="mx-3" /> |
| | | 分钟后开始收取等待费,为 |
| | | <n-input-number v-model:value="model.basicPricingJson1.num5" min="0" max="999" :precision="0" |
| | | <n-input-number v-model:value="model.basicPricingJson1.num5" min="0" max="999" :precision="2" |
| | | :show-button="false" placeholder="最多3位数" class="mx-3" /> |
| | | 元/分,不足一分钟按一分钟计算 |
| | | </n-form-item> |
| | |
| | | <n-input-number v-model:value="model.basicPricingJson1.num7" min="0" max="999" :precision="0" |
| | | :show-button="false" placeholder="最多3位数" class="mr-3" /> |
| | | 公里,超出部分每公里加收 |
| | | <n-input-number v-model:value="model.basicPricingJson1.num8" min="0" max="999" :precision="0" |
| | | <n-input-number v-model:value="model.basicPricingJson1.num8" min="0" max="999" :precision="2" |
| | | :show-button="false" placeholder="最多3位数" class="mr-3" /> |
| | | 元 |
| | | </NSpace> |
| | |
| | | <n-input-number v-model:value="model.basicPricingJson1.num10" min="0" max="999" :precision="0" |
| | | :show-button="false" placeholder="最多3位数" class="mr-3" /> |
| | | 公里时,超出部分每公里加收 |
| | | <n-input-number v-model:value="model.basicPricingJson1.num11" min="0" max="999" :precision="0" |
| | | <n-input-number v-model:value="model.basicPricingJson1.num11" min="0" max="999" :precision="2" |
| | | :show-button="false" placeholder="最多3位数" class="mr-3" /> |
| | | 元 |
| | | </NSpace> |
| | |
| | | <n-input-number v-model:value="model.basicPricingJson1.num12" min="0" max="999" :precision="0" |
| | | :show-button="false" placeholder="最多3位数" class="mr-3" /> |
| | | 公里以后,超出部分每公里加收 |
| | | <n-input-number v-model:value="model.basicPricingJson1.num13" min="0" max="999" :precision="0" |
| | | <n-input-number v-model:value="model.basicPricingJson1.num13" min="0" max="999" :precision="2" |
| | | :show-button="false" placeholder="最多3位数" class="mr-3" /> |
| | | 元 |
| | | </NSpace> |
| | |
| | | <n-time-picker v-model:formatted-value="model.basicPricingJson1.num141" format="HH:mm" |
| | | value-format="HH:mm" clearable class="w-80px" /> |
| | | 点为夜间行车时间,起步价变为 |
| | | <n-input-number v-model:value="model.basicPricingJson1.num15" min="0" max="999" :precision="0" |
| | | <n-input-number v-model:value="model.basicPricingJson1.num15" min="0" max="999" :precision="2" |
| | | :show-button="false" placeholder="最多3位数" class="mr-3" /> |
| | | 元 |
| | | </NSpace> |
| | | <NSpace align="center"> |
| | | 里程费为每公里 |
| | | <n-input-number v-model:value="model.basicPricingJson1.num16" min="0" max="999" :precision="0" |
| | | <n-input-number v-model:value="model.basicPricingJson1.num16" min="0" max="999" :precision="2" |
| | | :show-button="false" placeholder="最多3位数" class="mr-3" /> |
| | | 元,时长费为每 |
| | | <n-input-number v-model:value="model.basicPricingJson1.num17" min="0" max="999" :precision="0" |
| | | <n-input-number v-model:value="model.basicPricingJson1.num17" min="0" max="999" :precision="2" |
| | | :show-button="false" placeholder="最多3位数" class="mr-3" /> |
| | | 元 |
| | | </NSpace> |
| | | <NSpace align="center"> |
| | | 远途费为每公里加收 |
| | | <n-input-number v-model:value="model.basicPricingJson1.num18" min="0" max="999" :precision="0" |
| | | <n-input-number v-model:value="model.basicPricingJson1.num18" min="0" max="999" :precision="2" |
| | | :show-button="false" placeholder="最多3位数" class="mr-3" /> |
| | | <n-input-number v-model:value="model.basicPricingJson1.num19" min="0" max="999" :precision="0" |
| | | <n-input-number v-model:value="model.basicPricingJson1.num19" min="0" max="999" :precision="2" |
| | | :show-button="false" placeholder="最多3位数" class="mr-3" /> |
| | | <n-input-number v-model:value="model.basicPricingJson1.num20" min="0" max="999" :precision="0" |
| | | <n-input-number v-model:value="model.basicPricingJson1.num20" min="0" max="999" :precision="2" |
| | | :show-button="false" placeholder="最多3位数" class="mr-3" /> |
| | | 元 |
| | | </NSpace> |
| | |
| | | <n-time-picker v-model:formatted-value="model.basicPricingJson1.num221" format="HH:mm" |
| | | value-format="HH:mm" clearable class="w-80px" /> |
| | | 为高峰期行车时间,起步价变为 |
| | | <n-input-number v-model:value="model.basicPricingJson1.num23" min="0" max="999" :precision="0" |
| | | <n-input-number v-model:value="model.basicPricingJson1.num23" min="0" max="999" :precision="2" |
| | | :show-button="false" placeholder="最多3位数" class="mr-3" /> |
| | | 元 |
| | | </NSpace> |
| | | <NSpace align="center"> |
| | | 里程费为每公里 |
| | | <n-input-number v-model:value="model.basicPricingJson1.num24" min="0" max="999" :precision="0" |
| | | <n-input-number v-model:value="model.basicPricingJson1.num24" min="0" max="999" :precision="2" |
| | | :show-button="false" placeholder="最多3位数" class="mr-3" /> |
| | | 元,时长费为每 |
| | | <n-input-number v-model:value="model.basicPricingJson1.num25" min="0" max="999" :precision="0" |
| | | <n-input-number v-model:value="model.basicPricingJson1.num25" min="0" max="999" :precision="2" |
| | | :show-button="false" placeholder="最多3位数" class="mr-3" /> |
| | | 元 |
| | | </NSpace> |
| | | <NSpace align="center"> |
| | | 远途费为每公里加收 |
| | | <n-input-number v-model:value="model.basicPricingJson1.num26" min="0" max="999" :precision="0" |
| | | <n-input-number v-model:value="model.basicPricingJson1.num26" min="0" max="999" :precision="2" |
| | | :show-button="false" placeholder="最多3位数" class="mr-3" /> |
| | | <n-input-number v-model:value="model.basicPricingJson1.num27" min="0" max="999" :precision="0" |
| | | <n-input-number v-model:value="model.basicPricingJson1.num27" min="0" max="999" :precision="2" |
| | | :show-button="false" placeholder="最多3位数" class="mr-3" /> |
| | | <n-input-number v-model:value="model.basicPricingJson1.num28" min="0" max="999" :precision="0" |
| | | <n-input-number v-model:value="model.basicPricingJson1.num28" min="0" max="999" :precision="2" |
| | | :show-button="false" placeholder="最多3位数" class="mr-3" /> |
| | | 元 |
| | | </NSpace> |
| | |
| | | <NSpace vertical :size="16"> |
| | | <NSpace align="center"> |
| | | 节假日起步价变为 |
| | | <n-input-number v-model:value="model.basicPricingJson1.num29" min="0" max="999" :precision="0" |
| | | <n-input-number v-model:value="model.basicPricingJson1.num29" min="0" max="999" :precision="2" |
| | | :show-button="false" placeholder="最多3位数" class="mr-3" /> |
| | | 元 |
| | | </NSpace> |
| | | <NSpace align="center"> |
| | | 里程费为每公里 |
| | | <n-input-number v-model:value="model.basicPricingJson1.num30" min="0" max="999" :precision="0" |
| | | <n-input-number v-model:value="model.basicPricingJson1.num30" min="0" max="999" :precision="2" |
| | | :show-button="false" placeholder="最多3位数" class="mr-3" /> |
| | | 元,时长费为每 |
| | | <n-input-number v-model:value="model.basicPricingJson1.num31" min="0" max="999" :precision="0" |
| | | <n-input-number v-model:value="model.basicPricingJson1.num31" min="0" max="999" :precision="2" |
| | | :show-button="false" placeholder="最多3位数" class="mr-3" /> |
| | | 元 |
| | | </NSpace> |
| | | <NSpace align="center"> |
| | | 远途费为每公里加收 |
| | | <n-input-number v-model:value="model.basicPricingJson1.num32" min="0" max="999" :precision="0" |
| | | <n-input-number v-model:value="model.basicPricingJson1.num32" min="0" max="999" :precision="2" |
| | | :show-button="false" placeholder="最多3位数" class="mr-3" /> |
| | | <n-input-number v-model:value="model.basicPricingJson1.num33" min="0" max="999" :precision="0" |
| | | <n-input-number v-model:value="model.basicPricingJson1.num33" min="0" max="999" :precision="2" |
| | | :show-button="false" placeholder="最多3位数" class="mr-3" /> |
| | | <n-input-number v-model:value="model.basicPricingJson1.num34" min="0" max="999" :precision="0" |
| | | <n-input-number v-model:value="model.basicPricingJson1.num34" min="0" max="999" :precision="2" |
| | | :show-button="false" placeholder="最多3位数" class="mr-3" /> |
| | | 元 |
| | | </NSpace> |
| | |
| | | :show-button="false" class="mx-3" /> |
| | | 单,价格上浮 |
| | | <n-input-number v-model:value="model.dynamicPricingJson.num5" min="0" :precision="0" |
| | | :show-button="false" class="mx-3" />% |
| | | :show-button="false" class="mx-3" max="100"/>% |
| | | </n-form-item> |
| | | </div> |
| | | <div class="pl-20"> |
| | |
| | | :show-button="false" class="mx-3" /> |
| | | km/h视为拥堵,拥堵价格上浮比例: |
| | | <n-input-number v-model:value="model.dynamicPricingJson.num8" min="0" :precision="0" |
| | | :show-button="false" class="mx-3" />% |
| | | :show-button="false" class="mx-3" max="100" />% |
| | | </n-form-item> |
| | | </div> |
| | | <div class="pl-20"> |
| | |
| | | <n-form-item label="夜间行驶费用设置" path="basicPricingJson1"> |
| | | <NSpace vertical> |
| | | <NSpace align="center"> |
| | | {{ model.basicPricingJson1.num14 }} 点为夜间行车时间,起步价变为 {{ model.basicPricingJson1.num15 }} 元 |
| | | {{ model.basicPricingJson1.num14 }}-{{ model.basicPricingJson1.num141 }} 点为夜间行车时间,起步价变为 {{ model.basicPricingJson1.num15 }} 元 |
| | | </NSpace> |
| | | <NSpace align="center"> |
| | | 里程费为每公里 {{ model.basicPricingJson1.num16 }} 元, 时长费为每分钟 {{ model.basicPricingJson1.num17 }} 元 |
| | |
| | | <n-form-item label="高峰期费用设置" path="basicPricingJson1"> |
| | | <NSpace vertical> |
| | | <NSpace align="center"> |
| | | {{ model.basicPricingJson1.num21 }}-{{ model.basicPricingJson1.num22 }} 为高峰期行车时间,起步价变为 {{ |
| | | {{ model.basicPricingJson1.num21 }}-{{ model.basicPricingJson1.num211 }} 和 {{ model.basicPricingJson1.num22 }}-{{ model.basicPricingJson1.num221 }} 为高峰期行车时间,起步价变为 {{ |
| | | model.basicPricingJson1.num23 }} 元 |
| | | </NSpace> |
| | | <NSpace align="center"> |
| | |
| | | |
| | | const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false) |
| | | const { t } = useI18n() |
| | | const dialog = useDialog() |
| | | |
| | | const initialModel = { |
| | | vehicleModel: '', |
| | |
| | | const model = ref({ ...initialModel }) |
| | | function handleResetSearch() { |
| | | model.value = { ...initialModel } |
| | | getList() |
| | | } |
| | | |
| | | const formRef = ref<FormInst | null>() |
| | |
| | | > |
| | | 编辑 |
| | | </NButton> |
| | | <NPopconfirm v-permission="6026" onPositiveClick={() => delteteUser(row.id!)}> |
| | | {{ |
| | | default: () => '确认删除', |
| | | trigger: () => <NButton text type="error">删除</NButton>, |
| | | <NButton |
| | | text |
| | | v-permission="6026" |
| | | type="error" |
| | | onClick={() => { |
| | | dialog.create({ |
| | | title: '是否确认删除数据?', |
| | | content: '确认删除吗该定价配置?删除后不可恢复', |
| | | positiveText: '确定', |
| | | negativeText: '取消', |
| | | draggable: true, |
| | | onPositiveClick: () => { |
| | | delteteUser(row.id!) |
| | | }, |
| | | }) |
| | | }} |
| | | </NPopconfirm> |
| | | > |
| | | 删除 |
| | | </NButton> |
| | | </NSpace> |
| | | ) |
| | | }, |
| | |
| | | twoPushTime: null, // 二轮推单时间 |
| | | threePushDistance: null, // 三轮推单距离 |
| | | threePushTime: null, // 三轮推单时间 |
| | | fourPushDistance: null, // 四轮推单距离 |
| | | fourPushTime: null, // 四轮推单时间 |
| | | fivePushDistance: null, // 五轮推单距离 |
| | | fivePushTime: null, // 五轮推单时间 |
| | | passwordFreeAmount: null, // 免密授权金额 |
| | | prepaidLimit: null, // 预付额度 订单大于金额需预付 |
| | | ruleType: 1, // 规则类型 |
| | |
| | | } |
| | | |
| | | async function handleSubmit() { |
| | | startLoading() |
| | | formRef.value?.validate().then(() => { |
| | | try { |
| | | await formRef.value?.validate() |
| | | if (!model.value.cancleCount) { |
| | | message.error('请输入每日取消订单数') |
| | | return |
| | | } |
| | | if (!model.value.onePushDistance || !model.value.onePushTime || !model.value.twoPushDistance || !model.value.twoPushTime || !model.value.threePushDistance || !model.value.threePushTime || !model.value.fourPushDistance || !model.value.fourPushTime || !model.value.fivePushDistance || !model.value.fivePushTime) { |
| | | message.error('请输入播单策略') |
| | | return |
| | | } |
| | | if (!model.value.ruleType) { |
| | | message.error('请选择举手规则') |
| | | return |
| | | } |
| | | if (!model.value.simultaneouslyOrderCount) { |
| | | message.error('请输入同时抢单数') |
| | | return |
| | | } |
| | | // 校验分单倾向配置是否有空数据 |
| | | if (data.value.some((item) => item.value === null)) { |
| | | message.error('请配置分单倾向') |
| | | endLoading() |
| | | return |
| | | } |
| | | // 校验权重配置是否有空数据 |
| | | if (data1.value.some((item) => item.value === null)) { |
| | | message.error('请配置订单价值权重') |
| | | endLoading() |
| | | return |
| | | } |
| | | // 重组分单倾向配置 |
| | |
| | | ...obj, |
| | | ...obj1 |
| | | } |
| | | startLoading() |
| | | editTradingStrategy(params).then((res) => { |
| | | if (res.code === 200) { |
| | | window.$message.success('保存成功') |
| | | getDetail() |
| | | } |
| | | }).finally(() => { |
| | | endLoading() |
| | | }) |
| | | }) |
| | | } catch (error) { |
| | | endLoading() |
| | | console.log('验证失败', error) |
| | | } |
| | | } |
| | | |
| | | const getDetail = ()=> { |
| | |
| | | label-style="color: rgba(23, 31, 38, 0.8);"> |
| | | <n-input-number v-model:value="model.simultaneouslyOrderCount" min="0" :show-button="false" |
| | | class="mr-2 w-218px" /> |
| | | 可同时抢单 |
| | | 辆车可同时抢单 |
| | | </n-form-item> |
| | | </NSpace> |
| | | <NSpace :size="[24, 16]"> |
| | |
| | | input.onchange = async (e: any) => { |
| | | const file = e.target.files?.[0] |
| | | if (file) { |
| | | const { error } = await importWhitelist(file) |
| | | if (!error) { |
| | | const { code } = await importWhitelist(file) |
| | | if (code === 200) { |
| | | message.success('导入成功') |
| | | getWhitelistList() |
| | | } |
| | |
| | | positiveText: '确定', |
| | | negativeText: '取消', |
| | | onPositiveClick: async () => { |
| | | const { error } = await freezeWhitelistByIds(selectedRowKeys.value.join(',')) |
| | | if (!error) { |
| | | const { code } = await freezeWhitelistByIds(selectedRowKeys.value.join(',')) |
| | | if (code === 200) { |
| | | message.success('批量冻结成功') |
| | | selectedRowKeys.value = [] |
| | | getWhitelistList() |
| | |
| | | positiveText: '确定', |
| | | negativeText: '取消', |
| | | onPositiveClick: async () => { |
| | | const { error } = await freezeWhitelistByIds(row.id.toString()) |
| | | if (!error) { |
| | | const { code } = await freezeWhitelistByIds(row.id.toString()) |
| | | if (code === 200) { |
| | | message.success('冻结成功') |
| | | getWhitelistList() |
| | | } |
| | |
| | | positiveText: '确定', |
| | | negativeText: '取消', |
| | | onPositiveClick: async () => { |
| | | const { error } = await unfreezeWhitelist(row.id) |
| | | if (!error) { |
| | | const { code } = await unfreezeWhitelist(row.id) |
| | | if (code === 200) { |
| | | message.success('解冻成功') |
| | | getWhitelistList() |
| | | } |
| | |
| | | positiveText: '确定', |
| | | negativeText: '取消', |
| | | onPositiveClick: async () => { |
| | | const { error } = await deleteWhitelistByIds(row.id.toString()) |
| | | if (!error) { |
| | | const { code } = await deleteWhitelistByIds(row.id.toString()) |
| | | if (code === 200) { |
| | | message.success('删除成功') |
| | | getWhitelistList() |
| | | } |