| | |
| | | </n-button> |
| | | </n-input-group> |
| | | <n-modal |
| | | v-model:show="showModal" preset="card" :title="$t('components.iconSelector.selectorTitle')" size="small" class="w-800px" :bordered="false" |
| | | v-model:show="showModal" |
| | | :mask-closable="false" preset="card" :title="$t('components.iconSelector.selectorTitle')" size="small" class="w-800px" :bordered="false" |
| | | > |
| | | <template #header-extra> |
| | | <n-button type="warning" size="small" ghost @click="clearIcon"> |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <n-modal :show="show" :auto-focus="false" @update:show="handleUpdateShow"> |
| | | <n-modal :mask-closable="false" :show="show" :auto-focus="false" @update:show="handleUpdateShow"> |
| | | <n-card |
| | | class="operation-log-modal-card" |
| | | :bordered="false" |
| | |
| | | |
| | | function resetSetting() { |
| | | window.$dialog.warning({ |
| | | maskClosable: false, |
| | | title: t('app.resetSettingTitle'), |
| | | content: t('app.resetSettingContent'), |
| | | positiveText: t('common.confirm'), |
| | |
| | | </CommonWrapper> |
| | | <n-modal |
| | | v-model:show="showModal" |
| | | :mask-closable="false" |
| | | class="w-560px fixed top-60px inset-x-0 max-w-full" |
| | | size="small" |
| | | preset="card" |
| | |
| | | <script setup lang="ts"> |
| | | import { useAuthStore } from '@/store' |
| | | import { renderIcon } from '@/utils/icon' |
| | | import IconBookOpen from '~icons/icon-park-outline/book-open' |
| | | import IconGithub from '~icons/icon-park-outline/github' |
| | | import IconLogout from '~icons/icon-park-outline/logout' |
| | | import IconUser from '~icons/icon-park-outline/user' |
| | | |
| | | const { t } = useI18n() |
| | | |
| | |
| | | function handleSelect(key: string | number) { |
| | | if (key === 'loginOut') { |
| | | window.$dialog?.info({ |
| | | maskClosable: false, |
| | | title: t('app.loginOutTitle'), |
| | | content: t('app.loginOutContent'), |
| | | positiveText: t('common.confirm'), |
| | |
| | | import { router } from './router' |
| | | import App from './App.vue' |
| | | import { NModal } from 'naive-ui' |
| | | // 同时也修改 NDialog 的全局默认行为,以防止 dialog.create 创建的弹窗点击蒙版关闭 |
| | | |
| | | // 全局配置 NModal 的 maskClosable 默认值为 false,防止点击蒙版关闭弹窗 |
| | | if (NModal && NModal.props && NModal.props.maskClosable) { |
| | | NModal.props.maskClosable.default = false |
| | | } |
| | | // 注意: NDialogProvider 是一个组件,并不是直接包含 dialog options 的地方,但 dialog 会受到 NModal 的影响。 |
| | | |
| | | function getCurrentRouteInfo() { |
| | | const currentRoute = router.currentRoute.value |
| | |
| | | activeMenu: '/assetManagement/vehicleManagement/operatingVehicle', |
| | | }, |
| | | { |
| | | name: 'operatingVehicleGeofenceDetail', |
| | | path: '/assetManagement/vehicleManagement/operatingVehicle/geofence/detail', |
| | | title: '运营区域详情', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:auto-focus', |
| | | menuType: 'page', |
| | | componentPath: '/businessOperations/geofenceManagement/detail.vue', |
| | | id: 2026062401, |
| | | keepAlive: false, |
| | | pid: 501, |
| | | hide: true, // 不在菜单中显示 |
| | | activeMenu: '/assetManagement/vehicleManagement/operatingVehicle', |
| | | }, |
| | | { |
| | | name: 'vehicleMaterial', |
| | | path: '/assetManagement/vehicleManagement/vehicleMaterial', |
| | | title: '车辆素材管理', |
| | |
| | | <template> |
| | | <n-modal |
| | | v-model:show="visible" |
| | | :mask-closable="false" |
| | | preset="card" |
| | | :title="modalTitle" |
| | | :style="{ width: '600px' }" |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <n-modal v-model:show="visible"> |
| | | <n-modal v-model:show="visible" :mask-closable="false"> |
| | | <n-card |
| | | style="width: 1000px; height: 80vh; max-height: 800px; border-radius: 8px; display: flex; flex-direction: column;" |
| | | :bordered="false" |
| | |
| | | import { fetchVehiclePageList } from '@/service/api/fleet' |
| | | |
| | | interface VehicleItem { |
| | | id: number |
| | | id: number | string |
| | | vehicleCode: string |
| | | plateNumber: string |
| | | vehicleModel: string |
| | |
| | | }) |
| | | |
| | | // 选中的行 |
| | | const checkedRowKeys = ref<number[]>([]) |
| | | const checkedRowKeys = ref<string[]>([]) |
| | | |
| | | const listData = ref<VehicleItem[]>([]) |
| | | |
| | | // 缓存所有加载过的数据,以便在 handleConfirm 时能找到不在当前页但在 checkedRowKeys 中的项 |
| | | const allLoadedData = ref<Map<number, VehicleItem>>(new Map()) |
| | | const allLoadedData = ref<Map<string, VehicleItem>>(new Map()) |
| | | |
| | | const count = ref(0) |
| | | const currentPage = ref(1) |
| | |
| | | }, |
| | | ]) |
| | | |
| | | function shouldIgnoreRowClick(target: EventTarget | null) { |
| | | const element = target as HTMLElement | null |
| | | return Boolean( |
| | | element?.closest( |
| | | '.n-checkbox, .n-checkbox-box, .n-checkbox-input, button, a, input, textarea, .n-base-selection', |
| | | ), |
| | | ) |
| | | } |
| | | |
| | | function handleUpdateChecked(keys: Array<string | number>) { |
| | | checkedRowKeys.value = keys.map(key => String(key)) |
| | | } |
| | | |
| | | function toggleRowSelection(row: VehicleItem) { |
| | | if (props.selectionDisabled?.(row)) |
| | | return |
| | | |
| | | const rowKey = String(row.id) |
| | | if (!rowKey) |
| | | return |
| | | |
| | | if (checkedRowKeys.value.includes(rowKey)) { |
| | | checkedRowKeys.value = checkedRowKeys.value.filter(key => key !== rowKey) |
| | | return |
| | | } |
| | | |
| | | checkedRowKeys.value = [...checkedRowKeys.value, rowKey] |
| | | } |
| | | |
| | | function getRowProps(row: VehicleItem) { |
| | | return { |
| | | style: props.selectionDisabled?.(row) ? '' : 'cursor: pointer;', |
| | | onClick: (event: MouseEvent) => { |
| | | if (shouldIgnoreRowClick(event.target)) |
| | | return |
| | | toggleRowSelection(row) |
| | | }, |
| | | } |
| | | } |
| | | |
| | | // 获取车辆列表 |
| | | async function getVehicleList() { |
| | | startLoading() |
| | |
| | | |
| | | // 存入缓存 |
| | | listData.value.forEach((item) => { |
| | | allLoadedData.value.set(item.id, item) |
| | | allLoadedData.value.set(String(item.id), item) |
| | | }) |
| | | } |
| | | else { |
| | |
| | | resetSearchModel() |
| | | currentPage.value = 1 |
| | | pageSize.value = 10 |
| | | checkedRowKeys.value = props.existingVehicles.map(v => v.id) |
| | | checkedRowKeys.value = props.existingVehicles.map(v => String(v.id)) |
| | | // 清空并初始化 allLoadedData |
| | | allLoadedData.value.clear() |
| | | props.existingVehicles.forEach((v) => { |
| | | allLoadedData.value.set(v.id, v) |
| | | allLoadedData.value.set(String(v.id), v) |
| | | }) |
| | | |
| | | getVehicleList() |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <n-modal v-model:show="visible"> |
| | | <n-modal v-model:show="visible" :mask-closable="false"> |
| | | <n-card |
| | | style=" |
| | | width: 1000px; |
| | |
| | | :data="listData" |
| | | :loading="loading" |
| | | :bordered="false" |
| | | :row-key="(row: VehicleItem) => row.id" |
| | | :row-key="(row: VehicleItem) => String(row.id)" |
| | | :row-props="getRowProps" |
| | | :pagination="false" |
| | | @update:checked-row-keys="handleUpdateChecked" |
| | | > |
| | | <template #empty> |
| | | <div class="flex-center"> |
| | |
| | | <script setup lang="tsx"> |
| | | import noDataImg from '@/assets/element-img/no-data.png' |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { NButton, NIcon, NSpace, NTag } from 'naive-ui' |
| | | import { NButton, NTag } from 'naive-ui' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { addUserToFleet, addVehicleToFleet, fetchFleetDetail, fetchFleetUserList, fetchFleetVehicleList, fetchRoleList, removeUserFromFleet, removeVehicleFromFleet } from '@/service/api/fleet' |
| | | import { useBoolean } from '@/hooks' |
| | |
| | | // 移除车辆 |
| | | async function handleRemoveVehicle(vehicleId: number) { |
| | | window.$dialog.warning({ |
| | | maskClosable: false, |
| | | title: '是否确认移除车辆?', |
| | | content: '移除后车辆自动任务将不再下发给车队!', |
| | | positiveText: '确定', |
| | |
| | | // 移除人员 |
| | | async function handleRemoveStaff(userId: number) { |
| | | window.$dialog.warning({ |
| | | maskClosable: false, |
| | | title: '是否确认移除人员?', |
| | | content: '', |
| | | positiveText: '确定', |
| | |
| | | align: 'left', |
| | | key: 'locationName', |
| | | width: 180, |
| | | ellipsis: true, |
| | | ellipsis: { |
| | | tooltip: true, |
| | | }, |
| | | resizable: true, |
| | | }, |
| | | { |
| | |
| | | |
| | | async function handleDeleteFleet(id: number) { |
| | | window.$dialog.warning({ |
| | | maskClosable: false, |
| | | title: '是否确认删除', |
| | | content: '删除后数据不可恢复,请谨慎操作', |
| | | positiveText: '确定', |
| | |
| | | import { NButton, NCard, NImage, NModal, NSpin } from 'naive-ui' |
| | | import { useBoolean } from '@/hooks' |
| | | import { getVehicleMaterialList } from '@/service/api/vehicle' |
| | | import { downloadFile } from '@/utils' |
| | | import iconPreview from '@/assets/element-img/icon-perview.png' |
| | | import iconDownload from '@/assets/element-img/icon-download.png' |
| | | import selectedIcon from '@/assets/element-img/selected.png' |
| | |
| | | <script setup lang="tsx"> |
| | | import { onMounted, ref, watch } from 'vue' |
| | | import type { FormInst, FormRules, UploadFileInfo } from 'naive-ui' |
| | | import { NTooltip, tooltipDark } from 'naive-ui' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import dayjs from 'dayjs' |
| | | import { addVehicle, editVehicle, getVehicleById } from '@/service/api/vehicle' |
| | |
| | | import wordIcon from '@/assets/element-img/word.png' |
| | | import pdfIcon from '@/assets/element-img/pdf.png' |
| | | import uploadIcon from '@/assets/element-img/upload.png' |
| | | import noticeIcon from '@/assets/element-img/notice.png' |
| | | import noDataIcon from '@/assets/element-img/no-data.png' |
| | | |
| | | const router = useRouter() |
| | |
| | | try { |
| | | const res = await getVehicleById(vehicleId.value) |
| | | if (res.code !== 200 || !res.data) { |
| | | // window.$message.error(res.msg || '获取车辆详情失败') |
| | | // window.$message.error(res.msg || '获取车辆详情失败') |
| | | return |
| | | } |
| | | const data = res.data |
| | |
| | | } |
| | | catch (error) { |
| | | console.error('获取车辆详情失败:', error) |
| | | // window.$message.error('获取车辆详情失败') |
| | | // window.$message.error('获取车辆详情失败') |
| | | } |
| | | } |
| | | |
| | |
| | | catch (error) { |
| | | console.error('获取运营区域失败:', error) |
| | | } |
| | | } |
| | | |
| | | function renderOperationAreaOptionLabel(option: any) { |
| | | const content = option?.label || '' |
| | | if (!content) |
| | | return '-' |
| | | |
| | | return ( |
| | | <NTooltip placement="top-center"> |
| | | {{ |
| | | trigger: () => ( |
| | | <div |
| | | title={content} |
| | | style={{ |
| | | display: 'block', |
| | | width: '100%', |
| | | maxWidth: '140px', |
| | | overflow: 'hidden', |
| | | textOverflow: 'ellipsis', |
| | | whiteSpace: 'nowrap', |
| | | }} |
| | | > |
| | | {content} |
| | | </div> |
| | | ), |
| | | default: () => content, |
| | | }} |
| | | </NTooltip> |
| | | ) |
| | | } |
| | | |
| | | function renderOperationAreaTag(tag: any) { |
| | | const content = tag.option?.label || '' |
| | | if (!content) |
| | | return null |
| | | |
| | | return ( |
| | | <NTooltip themeOverrides={tooltipDark}> |
| | | {{ |
| | | trigger: () => ( |
| | | <div |
| | | title={content} |
| | | style={{ |
| | | display: 'block', |
| | | width: '100%', |
| | | maxWidth: '140px', |
| | | overflow: 'hidden', |
| | | textOverflow: 'ellipsis', |
| | | whiteSpace: 'nowrap', |
| | | }} |
| | | > |
| | | {content} |
| | | </div> |
| | | ), |
| | | default: () => content, |
| | | }} |
| | | </NTooltip> |
| | | ) |
| | | } |
| | | |
| | | onMounted(() => { |
| | |
| | | // 处理返回 |
| | | function handleBack() { |
| | | window.$dialog.warning({ |
| | | maskClosable: false, |
| | | title: '提示', |
| | | content: '是否放弃当前修改?', |
| | | positiveText: '确定', |
| | |
| | | placeholder="请选择" |
| | | filterable |
| | | clearable |
| | | :render-label="renderOperationAreaOptionLabel" |
| | | :render-tag="renderOperationAreaTag" |
| | | @update:show="isOperationAreaSelectOpen = $event" |
| | | > |
| | | <template #arrow> |
| | |
| | | :actions="[]" |
| | | style="width: 100%; margin-right: 4px;" |
| | | /> |
| | | <n-tooltip placement="top-start" trigger="hover"> |
| | | <NTooltip placement="top-center" trigger="hover"> |
| | | <template #trigger> |
| | | <icon-park-outline-help color="#0256ff" /> |
| | | </template> |
| | | 以机动车行驶证为准 |
| | | </n-tooltip> |
| | | </NTooltip> |
| | | </n-form-item> |
| | | </n-gi> |
| | | |
| | |
| | | |
| | | if (hasData) { |
| | | window.$dialog.warning({ |
| | | maskClosable: false, |
| | | title: '确认取消', |
| | | content: '表单内有已输入的数据,取消后将不会保存,是否确认取消?', |
| | | positiveText: '确定', |
| | |
| | | <n-button style="padding: 7px 29px; border-radius: 4px;" @click="handleCancel"> |
| | | 取消 |
| | | </n-button> |
| | | <n-button style="width: unset; |
| | | padding: 7px 16px; |
| | | <n-button |
| | | style="width: unset; |
| | | padding: 7px 16px; |
| | | border-radius: 4px; |
| | | border: 1px solid #7ABDFF; |
| | | background-color: #E6F4FF; |
| | | color: #0068FF; |
| | | cursor: pointer; |
| | | " @click="handleSave(true)"> |
| | | " @click="handleSave(true)" |
| | | > |
| | | 保存并运营 |
| | | </n-button> |
| | | <n-button type="primary" style="padding: 7px 29px;border-radius: 4px;" @click="handleSave(false)"> |
| | |
| | | align: 'left', |
| | | width: 120, |
| | | key: 'vehicleVin', |
| | | ellipsis: { tooltip: true }, |
| | | resizable: true, |
| | | }, |
| | | { |
| | |
| | | } |
| | | const idStr = selectedVehicles.value.map(item => item.id).join(',') |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '确认开始运营', |
| | | content: () => `确认对选中的 ${selectedVehicles.value.length} 辆车辆执行开始运营吗?`, |
| | | negativeText: '取消', |
| | |
| | | window.$message.success('下载成功') |
| | | } |
| | | else { |
| | | // window.$message.error('下载失败:接口未返回正确的文件模板') |
| | | // window.$message.error('下载失败:接口未返回正确的文件模板') |
| | | } |
| | | } |
| | | catch (error) { |
| | |
| | | return |
| | | } |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '确认开始运营', |
| | | content: () => { |
| | | return ( |
| | |
| | | |
| | | function handleDelete(row: any) { |
| | | window.$dialog.warning({ |
| | | maskClosable: false, |
| | | title: '是否确认删除', |
| | | content: () => { |
| | | return ( |
| | |
| | | <script setup lang="ts"> |
| | | import { NButton, NImage, NModal, NSpace, NTag } from 'naive-ui' |
| | | import { NImage, NModal } from 'naive-ui' |
| | | import { computed } from 'vue' |
| | | import successIcon from '@/assets/point/success.png' |
| | | import failIcon from '@/assets/point/fail.png' |
| | | import dayjs from 'dayjs' |
| | | |
| | | interface Props { |
| | |
| | | <div class="detail-label" style="color:rgba(23, 31, 38, 0.6)"> |
| | | 操作时间: |
| | | </div> |
| | | <span class="detail-value">{{ detailData.detectionTime ? dayjs(detailData.detectionTime).format("YYYY/MM/DD HH:mm:ss") : '-' }}</span> |
| | | <span class="detail-value">{{ detailData.detectionTime ? dayjs(detailData.detectionTime).format("YYYY/MM/DD HH:mm") : '-' }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | |
| | | <div class="value"> |
| | | {{ |
| | | detailData.inspectionExpireDate |
| | | ? dayjs(detailData.inspectionExpireDate).format('YYYY/MM/DD HH:mm:ss') |
| | | ? dayjs(detailData.inspectionExpireDate).format('YYYY/MM/DD HH:mm') |
| | | : "-" |
| | | }} |
| | | </div> |
| | |
| | | <div class="value"> |
| | | {{ |
| | | detailData.insuranceExpireDate |
| | | ? dayjs(detailData.insuranceExpireDate).format('YYYY/MM/DD HH:mm:ss') |
| | | ? dayjs(detailData.insuranceExpireDate).format('YYYY/MM/DD HH:mm') |
| | | : "-" |
| | | }} |
| | | </div> |
| | |
| | | <div class="value"> |
| | | {{ |
| | | detailData.licenseExpireDate |
| | | ? dayjs(detailData.licenseExpireDate).format('YYYY/MM/DD HH:mm:ss') |
| | | ? dayjs(detailData.licenseExpireDate).format('YYYY/MM/DD HH:mm') |
| | | : "-" |
| | | }} |
| | | </div> |
| | |
| | | </div> |
| | | <div style="flex: 1"> |
| | | <div class="label">操作时间</div> |
| | | <div class="value">{{ detailData.addTime || "-" }}</div> |
| | | <div class="value">{{ detailData.addTime ? dayjs(detailData.addTime).format("YYYY/MM/DD HH:mm") : "-" }}</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | |
| | | <script setup lang="ts"> |
| | | import { NButton, NImage, NModal } from 'naive-ui' |
| | | import { NImage, NModal } from 'naive-ui' |
| | | import { computed } from 'vue' |
| | | import dayjs from 'dayjs' |
| | | |
| | |
| | | <div class="value"> |
| | | {{ |
| | | detailData.sendTime |
| | | ? dayjs(detailData.sendTime).format("YYYY/MM/DD HH:mm:ss") |
| | | ? dayjs(detailData.sendTime).format("YYYY/MM/DD HH:mm") |
| | | : "-" |
| | | }} |
| | | </div> |
| | |
| | | {{ |
| | | detailData.repairStartTime |
| | | ? dayjs(detailData.repairStartTime).format( |
| | | "YYYY/MM/DD HH:mm:ss", |
| | | "YYYY/MM/DD HH:mm", |
| | | ) |
| | | : "-" |
| | | }} |
| | |
| | | <div class="value"> |
| | | {{ |
| | | detailData.receiveTime |
| | | ? dayjs(detailData.receiveTime).format("YYYY/MM/DD HH:mm:ss") |
| | | ? dayjs(detailData.receiveTime).format("YYYY/MM/DD HH:mm") |
| | | : "-" |
| | | }} |
| | | </div> |
| | |
| | | } |
| | | .value { |
| | | color: rgba(23, 31, 38, 1); |
| | | font-size: 13px; |
| | | font-size: 13px; |
| | | line-height: 18px; |
| | | } |
| | | .pic { |
| | |
| | | <div class="value"> |
| | | {{ |
| | | detailData.operationTime |
| | | ? dayjs(detailData.operationTime).format("YYYY/MM/DD HH:mm:ss") |
| | | ? dayjs(detailData.operationTime).format("YYYY/MM/DD HH:mm") |
| | | : "-" |
| | | }} |
| | | </div> |
| | |
| | | import { NAvatar, NButton, NImage, NModal, NSpace, NSpin, NTag } from 'naive-ui' |
| | | import { getTaskDetail } from '@/service/api/vehicleConvoy' |
| | | import { collectImageUrls } from '@/utils/common' |
| | | import IconCar from '~icons/icon-park-outline/car' |
| | | import IconCheckOne from '~icons/icon-park-outline/check-one' |
| | | import IconCaution from '~icons/icon-park-outline/caution' |
| | | import IconParkOutlineImageFiles from '~icons/icon-park-outline/image-files' |
| | | |
| | | const props = defineProps<{ |
| | | show: boolean |
| | |
| | | <script setup lang="tsx"> |
| | | import noDataImg from '@/assets/element-img/no-data.png' |
| | | import { NButton, NCard, NCascader, NDataTable, NDatePicker, NForm, NFormItem, NImage, NInput, NSelect, NSpace, NTabPane, NTabs, NTag, tooltipDark } from 'naive-ui' |
| | | import { NButton, NCard, NCascader, NDataTable, NDatePicker, NForm, NFormItem, NImage, NInput, NSelect, NSpace, NTabPane, NTabs, NTag, NTooltip } from 'naive-ui' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { useBoolean } from '@/hooks' |
| | | import InspectionDetailModal from './components/InspectionDetailModal.vue' |
| | |
| | | // 事故任务类型选项 |
| | | const taskErrorIdOptions = ref<any[]>([]) |
| | | |
| | | function renderTaskErrorOptionLabel(option: any) { |
| | | const content = option?.errorName || option?.label || '' |
| | | if (!content) |
| | | return '-' |
| | | |
| | | return ( |
| | | <NTooltip placement="top-center"> |
| | | {{ |
| | | trigger: () => ( |
| | | <div |
| | | style={{ |
| | | display: 'block', |
| | | width: '100%', |
| | | maxWidth: '140px', |
| | | overflow: 'hidden', |
| | | textOverflow: 'ellipsis', |
| | | whiteSpace: 'nowrap', |
| | | }} |
| | | > |
| | | {content} |
| | | </div> |
| | | ), |
| | | default: () => content, |
| | | }} |
| | | </NTooltip> |
| | | ) |
| | | } |
| | | |
| | | function renderTaskErrorTag(tag: any) { |
| | | const content = taskErrorIdDisplayLabel.value |
| | | if (!content) |
| | | return null |
| | | |
| | | return ( |
| | | <NTooltip placement="top-center"> |
| | | {{ |
| | | trigger: () => ( |
| | | <div |
| | | title={content} |
| | | style={{ |
| | | display: 'block', |
| | | width: '100%', |
| | | maxWidth: '140px', |
| | | overflow: 'hidden', |
| | | textOverflow: 'ellipsis', |
| | | whiteSpace: 'nowrap', |
| | | }} |
| | | > |
| | | {content} |
| | | </div> |
| | | ), |
| | | default: () => content, |
| | | }} |
| | | </NTooltip> |
| | | ) |
| | | } |
| | | |
| | | function findTaskErrorOptionPath(options: any[], targetValue: string | number | null | undefined, parents: any[] = []): any[] { |
| | | if (targetValue === null || targetValue === undefined || targetValue === '') |
| | | return [] |
| | | |
| | | for (const option of options || []) { |
| | | const currentPath = [...parents, option] |
| | | if (String(option?.id ?? '') === String(targetValue)) |
| | | return currentPath |
| | | |
| | | const childPath = findTaskErrorOptionPath(option?.child || [], targetValue, currentPath) |
| | | if (childPath.length) |
| | | return childPath |
| | | } |
| | | |
| | | return [] |
| | | } |
| | | |
| | | const taskErrorIdDisplayLabel = computed(() => { |
| | | const matchedPath = findTaskErrorOptionPath(taskErrorIdOptions.value, taskSearchModel.value.taskErrorId) |
| | | if (!matchedPath.length) |
| | | return '' |
| | | |
| | | return matchedPath |
| | | .map(item => item?.errorName) |
| | | .filter(Boolean) |
| | | .join(' / ') |
| | | }) |
| | | |
| | | // 获取事故类型列表 |
| | | function getAccidentTypeList() { |
| | | getTaskAccidentTypeList().then((res) => { |
| | |
| | | // 3: { type: 'info', text: '已收车' }, |
| | | // 4: { type: 'error', text: '车辆异常' }, |
| | | |
| | | 1: { type: 'success', text: '运营中' }, |
| | | 1: { type: 'success', text: '运营中' }, |
| | | 2: { type: 'success', text: '出车中' }, |
| | | 3: { type: 'success', text: '接单中' }, |
| | | 4: { type: 'success', text: '停止接单' }, |
| | |
| | | // 已收车 |
| | | function _handleRecall() { |
| | | window.$dialog.warning({ |
| | | maskClosable: false, |
| | | title: '确认收车', |
| | | content: `确认将车辆 ${vehicleDetail.value.plateNumber} 收车吗?`, |
| | | positiveText: '确定', |
| | |
| | | |
| | | // 查看运营区域 |
| | | function handleViewArea() { |
| | | if (!vehicleDetail.value.electronicId) |
| | | return |
| | | |
| | | router.push({ |
| | | path: '/businessOperations/geofenceManagement/detail', |
| | | name: 'operatingVehicleGeofenceDetail', |
| | | query: { |
| | | type: 'detail', |
| | | id: vehicleDetail.value.electronicId, |
| | |
| | | // 删除任务记录 |
| | | function handleDeleteTask(row: any) { |
| | | window.$dialog.warning({ |
| | | maskClosable: false, |
| | | title: '确认删除', |
| | | content: `确认删除该任务记录吗?`, |
| | | positiveText: '确定', |
| | |
| | | onPositiveClick: () => { |
| | | const id = String(row.id ?? row.taskId ?? '') |
| | | if (!id) { |
| | | // window.$message.error('缺少ID,无法删除') |
| | | // window.$message.error('缺少ID,无法删除') |
| | | return |
| | | } |
| | | deleteTaskById(id).then(({ error }) => { |
| | |
| | | |
| | | function handleDeleteCharging(row: any) { |
| | | window.$dialog.warning({ |
| | | maskClosable: false, |
| | | title: '确认删除', |
| | | content: `确认删除该充电记录吗?`, |
| | | positiveText: '确定', |
| | |
| | | getChargingList() |
| | | } |
| | | else { |
| | | // window.$message.error('删除失败') |
| | | // window.$message.error('删除失败') |
| | | } |
| | | }) |
| | | }, |
| | |
| | | |
| | | function handleDeleteClean(row: any) { |
| | | window.$dialog.warning({ |
| | | maskClosable: false, |
| | | title: '确认删除', |
| | | content: `确认删除该车辆清洁记录吗?`, |
| | | positiveText: '确定', |
| | |
| | | if (res.code === 200) { |
| | | window.$message.success('删除成功') |
| | | getCleanList() |
| | | return |
| | | } |
| | | // window.$message.error('删除失败') |
| | | // window.$message.error('删除失败') |
| | | }) |
| | | }, |
| | | }) |
| | |
| | | accidentDetailModalRef.value?.openModal(res.data, id) |
| | | else |
| | | routineDetailModalRef.value?.openModal(res.data, id) |
| | | return |
| | | } |
| | | // window.$message.error(res.msg || '获取任务详情失败') |
| | | // window.$message.error(res.msg || '获取任务详情失败') |
| | | } |
| | | else { |
| | | window.$message.info(`查看${type}记录详情: ${row.id}`) |
| | |
| | | v-model:value="taskSearchModel.taskErrorId" |
| | | placeholder="请选择任务类型" |
| | | style="width: 180px" |
| | | class="task-error-cascader" |
| | | :options="taskErrorIdOptions" |
| | | value-field="id" |
| | | label-field="errorName" |
| | | children-field="child" |
| | | check-strategy="child" |
| | | clearable |
| | | :render-label="renderTaskErrorOptionLabel" |
| | | :render-tag="renderTaskErrorTag" |
| | | @update:show="isTaskErrorIdOpen = $event" |
| | | > |
| | | <template #arrow> |
| | |
| | | :deep(.n-tabs-pane-wrapper) { |
| | | flex: 1; |
| | | } |
| | | |
| | | :deep(.task-error-cascader .n-base-selection-tags) { |
| | | flex-wrap: nowrap; |
| | | overflow: hidden; |
| | | } |
| | | :deep(.task-error-cascader .n-base-selection-overlay__wrapper) { |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | } |
| | | </style> |
| | |
| | | align: 'left', |
| | | width: 160, |
| | | key: 'vehicleVin', |
| | | ellipsis: { tooltip: true }, |
| | | resizable: true, |
| | | }, |
| | | { |
| | |
| | | } |
| | | |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '批量转为在库车辆', |
| | | content: () => { |
| | | return ( |
| | |
| | | |
| | | function handleConvertToInventory(row: any) { |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '转为在库车辆', |
| | | content: () => { |
| | | return ( |
| | |
| | | } |
| | | |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '删除确认', |
| | | content, |
| | | positiveText: '确认', |
| | |
| | | console.error('删除图片校验失败', error) |
| | | // fallback directly to delete if check fails |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '删除确认', |
| | | content: '确认删除该图片?此操作不可恢复。', |
| | | positiveText: '确认', |
| | |
| | | <script setup lang="ts"> |
| | | import { ref } from 'vue' |
| | | import type { FormInst, FormRules } from 'naive-ui' |
| | | import { NButton, NForm, NFormItem, NInput, NInputNumber, NModal, NSpace, useMessage } from 'naive-ui' |
| | | import { NButton, NForm, NFormItem, NInput, NModal, NSpace, useMessage } from 'naive-ui' |
| | | import { addVehicleModel, editVehicleModel, getVehicleModelDetail } from '@/service/api/vehicleConvoy' |
| | | |
| | | const emit = defineEmits<{ |
| | |
| | | |
| | | function handleDelete(row: any) { |
| | | dialog.create({ |
| | | maskClosable: false, |
| | | title: '是否确认删除', |
| | | content: `确认删除该车型?删除后不可恢复。`, |
| | | positiveText: '确定', |
| | |
| | | <script setup lang="tsx"> |
| | | import noDataImg from '@/assets/element-img/no-data.png' |
| | | import { computed, onMounted, ref, watch } from 'vue' |
| | | import { computed, onMounted, onUnmounted, ref, nextTick, watch } from 'vue' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { NButton, NInput, NRadio, NRadioGroup, NSelect, useDialog, useMessage } from 'naive-ui' |
| | | import type { DataTableColumns } from 'naive-ui' |
| | |
| | | |
| | | const isEdit = computed(() => route.query.mode === 'edit') |
| | | const planId = computed(() => String(route.query.id || '')) |
| | | |
| | | const scrollContainerRef = ref<HTMLElement | null>(null) |
| | | const showFooterShadow = ref(false) |
| | | let resizeObserver: ResizeObserver | null = null |
| | | |
| | | function checkScroll() { |
| | | if (!scrollContainerRef.value) return |
| | | const { scrollTop, scrollHeight, clientHeight } = scrollContainerRef.value |
| | | showFooterShadow.value = scrollHeight > clientHeight && Math.ceil(scrollTop + clientHeight) < scrollHeight - 2 |
| | | } |
| | | |
| | | let draggedIndex: number | null = null |
| | | const initialSnapshot = ref('') |
| | |
| | | |
| | | onMounted(() => { |
| | | if (isEdit.value) { |
| | | getDetail() |
| | | getDetail().finally(() => { |
| | | nextTick(() => { |
| | | checkScroll() |
| | | }) |
| | | }) |
| | | } |
| | | else { |
| | | setInitialSnapshot() |
| | | nextTick(() => { |
| | | checkScroll() |
| | | }) |
| | | } |
| | | |
| | | if (scrollContainerRef.value) { |
| | | resizeObserver = new ResizeObserver(() => { |
| | | checkScroll() |
| | | }) |
| | | if (scrollContainerRef.value.firstElementChild) { |
| | | resizeObserver.observe(scrollContainerRef.value.firstElementChild) |
| | | } |
| | | resizeObserver.observe(scrollContainerRef.value) |
| | | } |
| | | }) |
| | | |
| | | onUnmounted(() => { |
| | | if (resizeObserver) { |
| | | resizeObserver.disconnect() |
| | | resizeObserver = null |
| | | } |
| | | }) |
| | | |
| | | watch( |
| | | () => [chargePoints.value.length, vehicles.value.length], |
| | | () => { |
| | | nextTick(() => { |
| | | checkScroll() |
| | | }) |
| | | } |
| | | ) |
| | | |
| | | watch( |
| | | () => planId.value, |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="h-full flex flex-col gap-4"> |
| | | <n-card style="border-radius: 6px" content-style="padding: 16px 24px;"> |
| | | <div class="h-full flex flex-col page-container"> |
| | | <div |
| | | ref="scrollContainerRef" |
| | | class="flex-1 p-12px flex flex-col gap-4 overflow-auto page-scroll-content" |
| | | @scroll="checkScroll" |
| | | > |
| | | <n-card style="border-radius: 6px" content-style="padding: 16px 24px;"> |
| | | <div class="text-lg font-bold"> |
| | | {{ isEdit ? "编辑巡游计划" : "新增巡游计划" }} |
| | | </div> |
| | |
| | | </n-card> |
| | | |
| | | <n-card |
| | | style="border-radius: 6px; margin-bottom: 80px" |
| | | style="border-radius: 6px;" |
| | | content-style="padding: 24px;" |
| | | > |
| | | <div class="mb-4 flex items-center"> |
| | |
| | | </div> |
| | | </div> |
| | | </n-card> |
| | | </div> |
| | | |
| | | <div |
| | | class="absolute bottom-0 left-0 right-0 flex justify-end gap-3 bg-white px-6 py-4" |
| | | style="box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1); z-index: 100" |
| | | class="absolute bottom-0 left-0 right-0 flex justify-end gap-3 bg-white px-6 py-4 transition-shadow duration-300" |
| | | :style="{ boxShadow: showFooterShadow ? '0 -2px 8px rgba(0, 0, 0, 0.1)' : 'none', zIndex: 100 }" |
| | | > |
| | | <NButton style="width: 84px; height: 32px" @click="handleCancel"> |
| | | 返回 |
| | |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped></style> |
| | | <style scoped lang="scss"> |
| | | .page-container { |
| | | position: relative; |
| | | height: calc(100vh - 76px - 28px); |
| | | padding-bottom: 64px; |
| | | } |
| | | |
| | | .page-scroll-content { |
| | | scrollbar-width: none; |
| | | -ms-overflow-style: none; |
| | | } |
| | | |
| | | .page-scroll-content::-webkit-scrollbar { |
| | | display: none; |
| | | } |
| | | </style> |
| | |
| | | <script setup lang="tsx"> |
| | | import noDataImg from '@/assets/element-img/no-data.png' |
| | | import { NButton, NDatePicker, NInput, NRadio, NRadioGroup, NSelect, useDialog, useMessage } from 'naive-ui' |
| | | import { NButton, NInput, NRadio, NRadioGroup, useDialog, useMessage } from 'naive-ui' |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import dayjs from 'dayjs' |
| | | import { computed, onMounted, ref, watch } from 'vue' |
| | |
| | | |
| | | function handleDelete(row: CruisePlanRow) { |
| | | dialog.create({ |
| | | maskClosable: false, |
| | | title: '确认删除该巡游计划吗?', |
| | | content: () => { |
| | | return ( |
| | |
| | | <script setup lang="tsx"> |
| | | import noDataImg from '@/assets/element-img/no-data.png' |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { computed, onMounted, ref } from 'vue' |
| | | import { computed, onMounted, onUnmounted, ref, nextTick, watch } from 'vue' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { NButton, NTag, useMessage } from 'naive-ui' |
| | | import SearchPanel from '@/components/custom/SearchPanel.vue' |
| | |
| | | const showPointModal = ref(false) |
| | | const showVehicleModal = ref(false) |
| | | const saving = ref(false) |
| | | |
| | | const scrollContainerRef = ref<HTMLElement | null>(null) |
| | | const showFooterShadow = ref(true) |
| | | let resizeObserver: ResizeObserver | null = null |
| | | |
| | | function checkScroll() { |
| | | if (!scrollContainerRef.value) return |
| | | const { scrollTop, scrollHeight, clientHeight } = scrollContainerRef.value |
| | | // 如果滚动高度大于容器高度,并且没有滚动到底部(允许 2px 误差) |
| | | showFooterShadow.value = scrollHeight > clientHeight && Math.ceil(scrollTop + clientHeight) < scrollHeight - 2 |
| | | } |
| | | |
| | | const selectedPoint = ref<PointItem | null>(null) |
| | | const selectedVehicles = ref<VehicleItem[]>([]) |
| | |
| | | } |
| | | |
| | | onMounted(() => { |
| | | fetchDetail() |
| | | fetchDetail().finally(() => { |
| | | nextTick(() => { |
| | | checkScroll() |
| | | }) |
| | | }) |
| | | |
| | | if (scrollContainerRef.value) { |
| | | resizeObserver = new ResizeObserver(() => { |
| | | checkScroll() |
| | | }) |
| | | // 监听内容高度变化 |
| | | if (scrollContainerRef.value.firstElementChild) { |
| | | resizeObserver.observe(scrollContainerRef.value.firstElementChild) |
| | | } |
| | | // 监听容器本身变化 |
| | | resizeObserver.observe(scrollContainerRef.value) |
| | | } |
| | | }) |
| | | |
| | | onUnmounted(() => { |
| | | if (resizeObserver) { |
| | | resizeObserver.disconnect() |
| | | resizeObserver = null |
| | | } |
| | | }) |
| | | |
| | | watch( |
| | | () => [selectedPoint.value, selectedVehicles.value.length, pagedVehicleList.value.length], |
| | | () => { |
| | | nextTick(() => { |
| | | checkScroll() |
| | | }) |
| | | } |
| | | ) |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="special-point-add-page roboTaxi"> |
| | | <div class="p-12px h-full flex flex-col gap-12px"> |
| | | <div |
| | | ref="scrollContainerRef" |
| | | class="page-scroll-content p-12px h-full flex flex-col gap-12px overflow-auto" |
| | | @scroll="checkScroll" |
| | | > |
| | | <n-card class="content-card" :bordered="false"> |
| | | <div class="flex justify-between items-center mb-12px"> |
| | | <div class="section-title-wrap"> |
| | |
| | | </n-card> |
| | | </div> |
| | | |
| | | <div class="page-footer"> |
| | | <div |
| | | class="absolute bottom-0 left-0 right-0 flex justify-end gap-3 bg-white px-6 py-4 transition-shadow duration-300" |
| | | :style="{ boxShadow: showFooterShadow ? '0 -2px 8px rgba(0, 0, 0, 0.1)' : 'none', zIndex: 100 }" |
| | | > |
| | | <NButton class="w-84px" @click="handleCancel"> |
| | | 取消 |
| | | </NButton> |
| | |
| | | |
| | | <style scoped lang="scss"> |
| | | .special-point-add-page { |
| | | height: calc(100vh - 76px); |
| | | position: relative; |
| | | height: calc(100vh - 76px - 28px); |
| | | padding-bottom: 64px; |
| | | } |
| | | |
| | | .page-scroll-content { |
| | | scrollbar-width: none; |
| | | -ms-overflow-style: none; |
| | | } |
| | | |
| | | .page-scroll-content::-webkit-scrollbar { |
| | | display: none; |
| | | } |
| | | |
| | | .page-title-card, |
| | |
| | | import { useBoolean } from '@/hooks' |
| | | |
| | | interface Point { |
| | | id: number |
| | | id: number | string |
| | | name?: string |
| | | pointAddress?: string |
| | | address?: string |
| | |
| | | const model = ref({ |
| | | pointName: '', |
| | | }) |
| | | const selectedPoints = ref<number[]>([]) |
| | | const allSelectedMap = ref<Map<number, Point>>(new Map()) |
| | | const selectedPoints = ref<string[]>([]) |
| | | const allSelectedMap = ref<Map<string, Point>>(new Map()) |
| | | const isSingleSelect = computed(() => props.maxSelectCount === 1) |
| | | |
| | | const listData = ref<any[]>([]) |
| | |
| | | emit('update:show', false) |
| | | } |
| | | |
| | | function handleUpdateChecked(keys: number[]) { |
| | | function handleUpdateChecked(keys: Array<string | number>) { |
| | | const normalizedKeys = keys.map(key => String(key)) |
| | | selectedPoints.value = isSingleSelect.value && keys.length > 1 |
| | | ? [keys[keys.length - 1]] |
| | | : keys |
| | | ? [normalizedKeys[normalizedKeys.length - 1]] |
| | | : normalizedKeys |
| | | |
| | | // 遍历当前页数据,如果有被选中的,存入 map |
| | | listData.value.forEach((item) => { |
| | | if (selectedPoints.value.includes(item.id)) { |
| | | const pointId = String(item.id) |
| | | if (selectedPoints.value.includes(pointId)) { |
| | | // 如果 map 中已有该 id,尝试合并属性以保留额外信息(如 isCharge 等) |
| | | // 优先使用新数据覆盖,但保留新数据中没有的旧字段 |
| | | const old = allSelectedMap.value.get(item.id) |
| | | const old = allSelectedMap.value.get(pointId) |
| | | if (old) { |
| | | allSelectedMap.value.set(item.id, { ...old, ...item }) |
| | | allSelectedMap.value.set(pointId, { ...old, ...item }) |
| | | } |
| | | else { |
| | | allSelectedMap.value.set(item.id, item) |
| | | allSelectedMap.value.set(pointId, item) |
| | | } |
| | | } |
| | | }) |
| | |
| | | } |
| | | |
| | | function handleRowClick(row: Point) { |
| | | const rowKey = String(row.id) |
| | | if (isSingleSelect.value) { |
| | | handleUpdateChecked([row.id]) |
| | | handleUpdateChecked([rowKey]) |
| | | return |
| | | } |
| | | |
| | | const nextKeys = selectedPoints.value.includes(row.id) |
| | | ? selectedPoints.value.filter(id => id !== row.id) |
| | | : [...selectedPoints.value, row.id] |
| | | const nextKeys = selectedPoints.value.includes(rowKey) |
| | | ? selectedPoints.value.filter(id => id !== rowKey) |
| | | : [...selectedPoints.value, rowKey] |
| | | handleUpdateChecked(nextKeys) |
| | | } |
| | | |
| | | function shouldIgnoreRowClick(target: EventTarget | null) { |
| | | const element = target as HTMLElement | null |
| | | return Boolean( |
| | | element?.closest( |
| | | '.n-checkbox, .n-checkbox-box, .n-checkbox-input, button, a, input, textarea, .n-base-selection', |
| | | ), |
| | | ) |
| | | } |
| | | |
| | | function rowProps(row: Point) { |
| | | return { |
| | | style: 'cursor: pointer;', |
| | | onClick: () => handleRowClick(row), |
| | | onClick: (event: MouseEvent) => { |
| | | if (shouldIgnoreRowClick(event.target)) |
| | | return |
| | | handleRowClick(row) |
| | | }, |
| | | } |
| | | } |
| | | |
| | |
| | | model.value.pointName = '' |
| | | // 初始化 |
| | | selectedPoints.value = props.existingPoints |
| | | ? props.existingPoints.map(p => p.id) |
| | | ? props.existingPoints.map(p => String(p.id)) |
| | | : [] |
| | | allSelectedMap.value.clear() |
| | | if (props.existingPoints) { |
| | | props.existingPoints.forEach((p) => { |
| | | allSelectedMap.value.set(p.id, p) |
| | | allSelectedMap.value.set(String(p.id), p) |
| | | }) |
| | | } |
| | | |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <n-modal :show="show" @update:show="emit('update:show', $event)"> |
| | | <n-modal :mask-closable="false" :show="show" @update:show="emit('update:show', $event)"> |
| | | <n-card |
| | | style=" |
| | | width: 1000px; |
| | |
| | | flex-height |
| | | style="height: 100%" |
| | | :loading="loading" |
| | | :row-key="(row: Point) => row.id" |
| | | :row-key="(row: Point) => String(row.id)" |
| | | :row-props="rowProps" |
| | | @update:checked-row-keys="handleUpdateChecked" |
| | | > |
| | |
| | | |
| | | function handleRemovePoint(type: 'charge' | 'park', pointId: number | string) { |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '删除确认', |
| | | content: '确认移除该特殊点位吗?', |
| | | positiveText: '确认', |
| | |
| | | else if (modalType.value === 'edit') { |
| | | if (formModel.value.areaStatus !== fenceDetail.value.electronicFenceAreas.find((item: any) => item.areaType === 1)?.areaStatus) { |
| | | dialog.create({ |
| | | maskClosable: false, |
| | | title: `确认${formModel.value.areaStatus === 1 ? '启用' : '停用'}该围栏吗?`, |
| | | content: '', |
| | | positiveText: '确定', |
| | |
| | | disabled={row.status !== 3} |
| | | onClick={() => { |
| | | dialog.create({ |
| | | maskClosable: false, |
| | | title: '是否确认复制?', |
| | | content: `复制为【${row.electronicName}_副本】`, |
| | | positiveText: '确定', |
| | |
| | | } |
| | | } |
| | | catch { |
| | | // message.error('导出失败') |
| | | // message.error('导出失败') |
| | | } |
| | | } |
| | | |
| | |
| | | onClick={() => { |
| | | const name = row.provinceCode === row.cityCode ? row.provinceName || '' : `${row.provinceName || ''}${row.cityName || ''}${row.districtName || ''}` |
| | | dialog.create({ |
| | | maskClosable: false, |
| | | title: '是否确认删除数据?', |
| | | content: `确认删除【${name || ''}】城市的开通记录吗?`, |
| | | positiveText: '确认', |
| | |
| | | function handleChangeStatus(row: any) { |
| | | const actionText = row.status === 1 ? '启用' : '禁用' |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: `是否确认${actionText}城市?`, |
| | | content: row.status === 0 ? '禁用后,该城市用户不可下单!' : `确认${actionText}该城市吗?`, |
| | | positiveText: '确定', |
| | |
| | | return |
| | | } |
| | | window.$dialog.warning({ |
| | | maskClosable: false, |
| | | title: '提示', |
| | | content: () => h('span', null, [ |
| | | '当前场站包含', |
| | |
| | | ) { |
| | | return new Promise<boolean>((resolve) => { |
| | | window.$dialog?.warning({ |
| | | maskClosable: false, |
| | | title, |
| | | content, |
| | | positiveText: options?.positiveText || '确认', |
| | |
| | | function openPointSaveLeaveDialog() { |
| | | return new Promise<'save' | 'discard' | 'cancel'>((resolve) => { |
| | | window.$dialog?.warning({ |
| | | maskClosable: false, |
| | | title: '当前点位还未保存,确定离开吗?', |
| | | content: '删除点位,编辑的内容将丢失,确定离开吗?', |
| | | positiveText: '保存离开', |
| | |
| | | isPointAddressDropdownVisible.value = show |
| | | } |
| | | |
| | | function handleMapCanvasClick(event: any) { |
| | | async function handleMapCanvasClick(event: any) { |
| | | const lnglat = event?.lnglat |
| | | if (!lnglat) |
| | | return |
| | |
| | | updatePickedPoint([lnglat.getLng(), lnglat.getLat()], true) |
| | | return |
| | | } |
| | | if ( |
| | | activeTab.value === 'point' |
| | | && !isPointFormMode.value |
| | | && !pointPanelClosed.value |
| | | && !!selectedId.value |
| | | && !!pointDetailData.value |
| | | ) { |
| | | await handleClosePointPanel({ clearSelection: true }) |
| | | return |
| | | } |
| | | if (activeTab.value !== 'fence' || !isFenceFormMode.value) |
| | | return |
| | | const pendingArea = fenceRestrictedAreas.value.find( |
| | |
| | | |
| | | function handleDelete(row: any) { |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '是否确认删除数据?', |
| | | content: '确认删除该点位现场信息吗?删除后不可恢复。', |
| | | positiveText: '确定', |
| | |
| | | |
| | | <template> |
| | | <n-card class="w-full h-full"> |
| | | <div class="text-base font-bold">首页开发中...</div> |
| | | <div class="text-base font-bold"> |
| | | 首页开发中... |
| | | </div> |
| | | </n-card> |
| | | </template> |
| | | |
| | |
| | | |
| | | async function handleSwitchChange(row: any) { |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: `是否确认${row.status === 1 ? '禁用' : '启用'}打卡地?`, |
| | | content: '', |
| | | positiveText: '确认', |
| | |
| | | style={{ color: isEnabled ? '#0068FF' : '#0068FF' }} |
| | | onClick={() => { |
| | | dialog.create({ |
| | | maskClosable: false, |
| | | title: `是否确认${isEnabled ? '禁用' : '启用'}打卡地?`, |
| | | content: '', |
| | | positiveText: '确定', |
| | |
| | | positiveText: '确认', |
| | | negativeText: '取消', |
| | | maskClosable: false, |
| | | autoFocus: false, |
| | | autoFocus: false, |
| | | onPositiveClick: () => { |
| | | handleDeleteSchedule(teamIndex, dateIndex) |
| | | }, |
| | |
| | | positiveText: '确认', |
| | | negativeText: '取消', |
| | | maskClosable: false, |
| | | autoFocus: false, |
| | | autoFocus: false, |
| | | onPositiveClick: () => { |
| | | handleDeleteSchedule(teamIndex, dateIndex) |
| | | }, |
| | |
| | | </n-card> |
| | | |
| | | <!-- 排班弹窗 --> |
| | | <n-modal v-model:show="showScheduleModal" preset="card" title="排班选择" class="w-400px"> |
| | | <n-modal v-model:show="showScheduleModal" :mask-closable="false" preset="card" title="排班选择" class="w-400px"> |
| | | <div class="flex flex-col gap-3"> |
| | | <div |
| | | v-for="shift in shiftTags" :key="`${shift.name}-${shift.time}`" |
| | |
| | | import { addShift, deleteShift, editShift, getShiftList } from '@/service/api/attendance' |
| | | import { NButton, NSpace } from 'naive-ui' |
| | | import ShiftFormModal from './components/ShiftFormModal.vue' |
| | | import SearchPanel from '@/components/custom/SearchPanel.vue' |
| | | import OperationLogModal from '@/components/custom/OperationLogModal.vue' |
| | | import dayjs from 'dayjs' |
| | | |
| | |
| | | |
| | | async function handleSwitchChange(row: any) { |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: `是否确认${row.status === 1 ? '禁用' : '启用'}打卡地?`, |
| | | content: '', |
| | | positiveText: '确认', |
| | |
| | | } |
| | | catch (error) { |
| | | console.error('获取巡游打卡规则详情失败:', error) |
| | | // message.error('获取巡游打卡规则详情失败') |
| | | // message.error('获取巡游打卡规则详情失败') |
| | | } |
| | | } |
| | | |
| | |
| | | :bordered="false" |
| | | size="huge" |
| | | class="custom-modal" |
| | | :mask-closable="true" |
| | | :mask-closable="false" |
| | | @update:show="(val) => emit('update:show', val)" |
| | | @close="handleClose" |
| | | > |
| | |
| | | } |
| | | catch (error) { |
| | | console.error('新增打卡点失败:', error) |
| | | // window.$message.error('新增打卡点失败') |
| | | // window.$message.error('新增打卡点失败') |
| | | } |
| | | } |
| | | |
| | |
| | | { default: () => '短信通知' }, |
| | | ), |
| | | ) |
| | | } else { |
| | | } |
| | | else { |
| | | row.isMessage = false |
| | | } |
| | | |
| | |
| | | function handleSave() { |
| | | console.log('保存', tableData.value) |
| | | startLoading() |
| | | const notifications = tableData.value.map((item) => ({ |
| | | const notifications = tableData.value.map(item => ({ |
| | | ...item, |
| | | isMessage: canShowMessageNotification(item.notificationType) ? item.isMessage : false, |
| | | })) |
| | |
| | | <script setup lang="tsx"> |
| | | import noDataImg from '@/assets/element-img/no-data.png' |
| | | import { computed, h, onActivated, ref } from 'vue' |
| | | import { computed, onActivated, ref } from 'vue' |
| | | import { useBoolean, usePermission } from '@/hooks' |
| | | import { useRouter } from 'vue-router' |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { NButton, NDropdown, NSwitch, NTag, NTooltip } from 'naive-ui' |
| | | import { NButton, NSwitch, NTag, NTooltip } from 'naive-ui' |
| | | import { addOperationsUser, deleteOperationsUser, editOperationsUser, exportOperationsUser, exportOperationsUserTemplate, getOperationsUserList, importOperationsUser, updateOperationsUserStatus } from '@/service/api/attendance' |
| | | import { fetchRoleList } from '@/service/api/fleet' |
| | | import { downloadFile, onlyAllowNumber, onlyAllowNumberAndLetter } from '@/utils' |
| | |
| | | const actionType = newStatus ? '启用' : '禁用' |
| | | |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: newStatus ? '确认启用' : '确认禁用', |
| | | content: `是否确认${actionType}该人员?`, |
| | | positiveText: '确认', |
| | |
| | | const nextStatus = row.status === 1 ? 0 : 1 |
| | | const actionText = nextStatus === 1 ? '停用' : '启用' |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: `是否确认${actionText}该安全员?`, |
| | | content: ``, |
| | | positiveText: '确定', |
| | |
| | | <script setup lang="ts"> |
| | | import { useBoolean } from '@/hooks' |
| | | import { NTag } from 'naive-ui' |
| | | import successIcon from '@/assets/point/success.png' |
| | | import failIcon from '@/assets/point/fail.png' |
| | | import dayjs from 'dayjs' |
| | | |
| | | const emit = defineEmits<{ |
| | |
| | | |
| | | function handleDelete(id: number) { |
| | | window.$dialog?.warning({ |
| | | maskClosable: false, |
| | | title: t('serviceReview.common.deleteTitle'), |
| | | content: t('serviceReview.common.deletePersonnelContent'), |
| | | positiveText: t('common.confirm'), |
| | |
| | | |
| | | function handleDelete(id: number) { |
| | | window.$dialog?.warning({ |
| | | maskClosable: false, |
| | | title: t('serviceReview.common.deleteTitle'), |
| | | content: t('serviceReview.common.deletePolicyContent'), |
| | | positiveText: t('common.confirm'), |
| | |
| | | |
| | | function handleDelete(row: FeedbackTypeItem) { |
| | | window.$dialog?.warning({ |
| | | maskClosable: false, |
| | | title: t('ticket.feedbackType.confirmDelete'), |
| | | content: t('ticket.feedbackType.confirmDeleteContent'), |
| | | positiveText: t('common.confirm'), |
| | |
| | | </n-card> |
| | | <n-modal |
| | | v-model:show="previewVisible" |
| | | :mask-closable="false" |
| | | preset="card" |
| | | :title="t('workOrder.detailPage.preview')" |
| | | :style="{ width: '720px' }" |
| | |
| | | } |
| | | if (payload.handleType === 2) { |
| | | window.$dialog?.warning({ |
| | | maskClosable: false, |
| | | title: t('workOrder.handleModal.confirmClose'), |
| | | content: '', |
| | | positiveText: t('common.confirm'), |
| | |
| | | |
| | | function handleDelete(row: WorkOrderItem) { |
| | | window.$dialog?.warning({ |
| | | maskClosable: false, |
| | | title: t('workOrder.confirmDelete'), |
| | | content: t('workOrder.confirmDeleteContent'), |
| | | positiveText: t('common.confirm'), |
| | |
| | | |
| | | async function handleDelete(id: number) { |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: t('permissionGroup.messages.deleteConfirmTitle'), |
| | | content: t('permissionGroup.messages.deleteConfirmContent'), |
| | | positiveText: t('common.confirm'), |
| | |
| | | positiveText: t('roleManagement.modal.closeConfirm.confirm'), |
| | | negativeText: t('roleManagement.modal.closeConfirm.continue'), |
| | | maskClosable: false, |
| | | autoFocus: false, |
| | | autoFocus: false, |
| | | onPositiveClick: () => { |
| | | closeModal() |
| | | }, |
| | |
| | | <NFormItem :label="t('roleManagement.modal.form.roleName')" path="roleName" required> |
| | | <NInput |
| | | v-model:value="formModel.roleName" :placeholder="t('roleManagement.modal.placeholder.roleName')" :disabled="mode === 'view'" :maxlength="20" |
| | | |
| | | /> |
| | | </NFormItem> |
| | | </NGridItem> |
| | |
| | | {{ t("roleManagement.detail.roleName") }}:<span class="detail-item-value">{{ roleDetailData.roleName }}</span> |
| | | </div> |
| | | <div class="detail-item detail-item-fixed"> |
| | | {{ t("roleManagement.detail.relatedPermissionGroup") }}:<span class="detail-item-value" >{{ roleDetailData.groupName }}</span> |
| | | {{ t("roleManagement.detail.relatedPermissionGroup") }}:<span class="detail-item-value">{{ roleDetailData.groupName }}</span> |
| | | </div> |
| | | <div class="detail-item"> |
| | | {{ t("roleManagement.detail.remarks") }}:<span class="detail-item-value" >{{ roleDetailData.remarks }}</span> |
| | | {{ t("roleManagement.detail.remarks") }}:<span class="detail-item-value">{{ roleDetailData.remarks }}</span> |
| | | </div> |
| | | </NSpace> |
| | | <!-- <div style="margin-top: 16px; display: flex; justify-content: flex-end;"> |
| | |
| | | :src="noDataImg" |
| | | class="empty-image" |
| | | @error="(e: any) => { e.target.src = 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png' }" |
| | | /> |
| | | > |
| | | </div> |
| | | </template> |
| | | </NTree> |
| | |
| | | <img |
| | | :src="noDataImg" |
| | | class="empty-image" |
| | | /> |
| | | > |
| | | </div> |
| | | </template> |
| | | </NDataTable> |
| | |
| | | |
| | | function handleResetPermissions() { |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '重置确认', |
| | | content: '确定将所有权限恢复为初始状态吗?', |
| | | positiveText: '确定', |
| | |
| | | |
| | | function handleDelete(id: number) { |
| | | window.$dialog.warning({ |
| | | maskClosable: false, |
| | | title: t('roleManagement.messages.deleteConfirmTitle'), |
| | | content: t('roleManagement.messages.deleteConfirmContent'), |
| | | positiveText: t('common.confirm'), |
| | |
| | | style="width: 240px;" |
| | | clearable |
| | | :time-picker-props="{ |
| | | onConfirm: (val) => { |
| | | formModel.startTime = val |
| | | startTimeRef?.handlePanelClose() |
| | | }, |
| | | }" |
| | | :actions="[]" |
| | | onConfirm: (val) => { |
| | | formModel.startTime = val |
| | | startTimeRef?.handlePanelClose() |
| | | }, |
| | | }" |
| | | :actions="[]" |
| | | :update-value-on-close="true" :close-on-select="true" |
| | | /> |
| | | </n-form-item> |
| | |
| | | :actions="[]" :update-value-on-close="true" :close-on-select="true" |
| | | clearable |
| | | :time-picker-props="{ |
| | | onConfirm: (val) => { |
| | | formModel.endTime = val |
| | | endTimeRef?.handlePanelClose() |
| | | }, |
| | | }" |
| | | onConfirm: (val) => { |
| | | formModel.endTime = val |
| | | endTimeRef?.handlePanelClose() |
| | | }, |
| | | }" |
| | | /> |
| | | </n-form-item> |
| | | </n-space> |
| | |
| | | import noDataImg from '@/assets/element-img/no-data.png' |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { computed, h, onMounted, reactive, ref, watch } from 'vue' |
| | | import { NButton, NTag, NSwitch, NTooltip, useMessage } from 'naive-ui' |
| | | import { NButton, NSwitch, NTag, NTooltip, useMessage } from 'naive-ui' |
| | | import dayjs from 'dayjs' |
| | | import dragIcon from '@/assets/point/union.png' |
| | | import tooltipIcon from '@/assets/element-img/tooltip.png' |
| | |
| | | tagContent: payload.labelContent, |
| | | }) |
| | | if (!isSuccessCode(res?.code)) { |
| | | // message.error(res?.msg || '保存失败') |
| | | // message.error(res?.msg || '保存失败') |
| | | return |
| | | } |
| | | |
| | |
| | | status: nextStatus as 0 | 1, |
| | | }) |
| | | if (!isSuccessCode(res?.code)) { |
| | | // message.error(res?.msg || '状态更新失败') |
| | | // message.error(res?.msg || '状态更新失败') |
| | | return |
| | | } |
| | | await loadTabList(activeTab.value) |
| | |
| | | sortOrder: item.sortOrder, |
| | | }))) |
| | | if (!isSuccessCode(res?.code)) { |
| | | // message.error(res?.msg || '排序更新失败') |
| | | // message.error(res?.msg || '排序更新失败') |
| | | return |
| | | } |
| | | await loadTabList(activeTab.value) |
| | |
| | | @update:value="markDirty" |
| | | > |
| | | <n-space> |
| | | <n-radio :value="1"> 是 </n-radio> |
| | | <n-radio :value="0"> 否 </n-radio> |
| | | <n-radio :value="1"> |
| | | 是 |
| | | </n-radio> |
| | | <n-radio :value="0"> |
| | | 否 |
| | | </n-radio> |
| | | </n-space> |
| | | </n-radio-group> |
| | | </n-form-item> |
| | |
| | | }`" |
| | | style="width: 16px; height: 16px" |
| | | alt="arrow" |
| | | /> |
| | | > |
| | | </template> |
| | | <template #empty> |
| | | <div class="flex-center"> |
| | | <img :src="noDataImg" style="width: 50%" /> |
| | | <img :src="noDataImg" style="width: 50%"> |
| | | </div> |
| | | </template> |
| | | </n-select> |
| | |
| | | </n-form-item> |
| | | </n-gi> |
| | | <template #action> |
| | | <NButton @click="handleResetSearch"> 重置 </NButton> |
| | | <NButton @click="handleResetSearch"> |
| | | 重置 |
| | | </NButton> |
| | | <NButton type="primary" @click="handleSearch"> |
| | | 查询 |
| | | </NButton> |
| | |
| | | <n-card class="robotaxi-table-card" :bordered="false"> |
| | | <template #header> |
| | | <n-flex justify="space-between"> |
| | | <NButton @click="handleOpenOperationLog"> 操作日志 </NButton> |
| | | <NButton @click="handleOpenOperationLog"> |
| | | 操作日志 |
| | | </NButton> |
| | | <div class="flex items-center justify-end"> |
| | | <NButton type="primary" secondary @click="handleAddTag"> |
| | | 新增 |
| | |
| | | :src="noDataImg" |
| | | style="width: 123px; height: 125px" |
| | | @error="(e: any) => { e.target.src = 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png' }" |
| | | /> |
| | | > |
| | | </div> |
| | | </template> |
| | | </n-data-table> |
| | |
| | | |
| | | <div class="footer"> |
| | | <n-space justify="end"> |
| | | <NButton class="w-84px" @click="handleCancelChanges"> 取消 </NButton> |
| | | <NButton class="w-84px" @click="handleCancelChanges"> |
| | | 取消 |
| | | </NButton> |
| | | <NButton |
| | | v-permission="2026061535" |
| | | type="primary" |
| | |
| | | <n-card class="flex-1 robotaxi-table-card" :bordered="false"> |
| | | <template #header> |
| | | <div class="flex items-center justify-end"> |
| | | <NButton type="primary" v-permission="2026061530" secondary @click="handleManualPush"> |
| | | <NButton v-permission="2026061530" type="primary" secondary @click="handleManualPush"> |
| | | 手动推送 |
| | | </NButton> |
| | | </div> |
| | |
| | | else if (modalType.value === 'edit') { |
| | | if (formModel.value.areaStatus !== fenceDetail.value.electronicFenceAreas.find((item: any) => item.areaType === 1)?.areaStatus) { |
| | | dialog.create({ |
| | | maskClosable: false, |
| | | title: `确认${formModel.value.areaStatus === 1 ? '启用' : '停用'}该围栏吗?`, |
| | | content: '', |
| | | positiveText: '确定', |
| | |
| | | disabled={row.status !== 3} |
| | | onClick={() => { |
| | | dialog.create({ |
| | | maskClosable: false, |
| | | title: '是否确认复制?', |
| | | content: `复制为【${row.electronicName}_副本】`, |
| | | positiveText: '确定', |
| | |
| | | isPointAddressDropdownVisible.value = show |
| | | } |
| | | |
| | | function handleMapCanvasClick(event: any) { |
| | | async function handleMapCanvasClick(event: any) { |
| | | const lnglat = event?.lnglat |
| | | if (!lnglat) |
| | | return |
| | |
| | | updatePickedPoint([lnglat.getLng(), lnglat.getLat()], true) |
| | | return |
| | | } |
| | | if ( |
| | | activeTab.value === 'point' |
| | | && !isPointFormMode.value |
| | | && !pointPanelClosed.value |
| | | && !!selectedId.value |
| | | && !!pointDetailData.value |
| | | ) { |
| | | await handleClosePointPanel({ clearSelection: true }) |
| | | return |
| | | } |
| | | if (activeTab.value !== 'fence' || !isFenceFormMode.value) |
| | | return |
| | | const pendingArea = fenceRestrictedAreas.value.find( |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <n-modal :show="show" @update:show="emit('update:show', $event)"> |
| | | <n-modal :mask-closable="false" :show="show" @update:show="emit('update:show', $event)"> |
| | | <n-card |
| | | style="width: 1000px; height: 80vh; border-radius: 8px; display: flex; flex-direction: column;" |
| | | :bordered="false" |
| | |
| | | <script setup lang="tsx"> |
| | | import noDataImg from '@/assets/element-img/no-data.png' |
| | | import { computed, h, nextTick, onMounted, onUnmounted, ref, watch } from 'vue' |
| | | import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { useTabStore } from '@/store' |
| | | import SelectPointModal from '@/views/testDemo/specialPointManagement/components/SelectPointModal.vue' |
| | |
| | | async function initCreatePlanId() { |
| | | const res = await addTestPlan() |
| | | if (res?.code !== 200) { |
| | | // message.error(res?.message || '初始化测试计划失败') |
| | | // message.error(res?.message || '初始化测试计划失败') |
| | | return |
| | | } |
| | | const nextPlanId = resolvePlanId(res.data) |
| | | if (!nextPlanId) { |
| | | // message.error('初始化测试计划失败') |
| | | // message.error('初始化测试计划失败') |
| | | return |
| | | } |
| | | planId.value = nextPlanId |
| | |
| | | message.error(res?.message || '订单生成失败') |
| | | } |
| | | catch { |
| | | // message.error('订单生成失败,请稍后重试') |
| | | // message.error('订单生成失败,请稍后重试') |
| | | } |
| | | } |
| | | |
| | |
| | | const { bool: orderLoading, setTrue: startOrderLoading, setFalse: endOrderLoading } = useBoolean(false) |
| | | |
| | | const statusOptions = [ |
| | | { label: '待接单', value: 1, type: 'primary',color:'#E6F4FF',textColor:'#0068FF' }, |
| | | { label: '待出发', value: 2, type: 'primary' ,color:'#E6F4FF',textColor:'#0068FF' }, |
| | | { label: '接驾中', value: 3, type: 'warning' ,color:'#E6F4FF',textColor:'#0068FF' }, |
| | | { label: '待客上车', value: 4, type: 'primary',color:'#E6F4FF',textColor:'#0068FF' }, |
| | | { label: '乘客已上车', value: 5, type: 'success',color:'#F6F0FF',textColor:'#764DFF' }, |
| | | { label: '服务中', value: 6, type: 'success',color:'#F6F0FF',textColor:'#764DFF' }, |
| | | { label: '待客下车', value: 7, type: 'warning' ,color:'#F6F0FF',textColor:'#764DFF'}, |
| | | { label: '待付款', value: 8, type: 'warning' ,color:'#FFF8E6',textColor:'#FF9000'}, |
| | | { label: '待评价', value: 9, type: 'default',color:'#FFF8E6',textColor:'#FF9000' }, |
| | | { label: '已完成', value: 10, type: 'success',color:'#DCF5DD',textColor:'#0BB52D' }, |
| | | { label: '已取消', value: 0, type: 'default',color:'#171F260D',textColor:'#171F26CC' }, |
| | | { label: '待接单', value: 1, type: 'primary', color: '#E6F4FF', textColor: '#0068FF' }, |
| | | { label: '待出发', value: 2, type: 'primary', color: '#E6F4FF', textColor: '#0068FF' }, |
| | | { label: '接驾中', value: 3, type: 'warning', color: '#E6F4FF', textColor: '#0068FF' }, |
| | | { label: '待客上车', value: 4, type: 'primary', color: '#E6F4FF', textColor: '#0068FF' }, |
| | | { label: '乘客已上车', value: 5, type: 'success', color: '#F6F0FF', textColor: '#764DFF' }, |
| | | { label: '服务中', value: 6, type: 'success', color: '#F6F0FF', textColor: '#764DFF' }, |
| | | { label: '待客下车', value: 7, type: 'warning', color: '#F6F0FF', textColor: '#764DFF' }, |
| | | { label: '待付款', value: 8, type: 'warning', color: '#FFF8E6', textColor: '#FF9000' }, |
| | | { label: '待评价', value: 9, type: 'default', color: '#FFF8E6', textColor: '#FF9000' }, |
| | | { label: '已完成', value: 10, type: 'success', color: '#DCF5DD', textColor: '#0BB52D' }, |
| | | { label: '已取消', value: 0, type: 'default', color: '#171F260D', textColor: '#171F26CC' }, |
| | | ] |
| | | const orderStatusOptions = statusOptions.map(item => ({ label: item.label, value: item.value })) |
| | | |
| | |
| | | } |
| | | |
| | | dialog.create({ |
| | | maskClosable: false, |
| | | title: '确认启动批量测试?', |
| | | content: `将为 ${vehicles.value.length} 辆车按 ${testPoints.value.length} 个点位组合生成链式巡游订单,每车跑 ${lapCount.value} 圈`, |
| | | positiveText: '确定', |
| | |
| | | startBatchPolling() |
| | | } |
| | | else { |
| | | // message.error(res?.message || '启动失败') |
| | | // message.error(res?.message || '启动失败') |
| | | } |
| | | }, |
| | | }) |
| | |
| | | startBatchPolling() |
| | | } |
| | | else { |
| | | // message.error(res?.message || '重试失败') |
| | | // message.error(res?.message || '重试失败') |
| | | } |
| | | } |
| | | |
| | |
| | | await refreshBatchData() |
| | | } |
| | | else { |
| | | // message.error(res?.message || '取消失败') |
| | | // message.error(res?.message || '取消失败') |
| | | } |
| | | }, |
| | | }) |
| | |
| | | return |
| | | } |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '故障恢复确认', |
| | | content: `⚠️ 故障恢复将跳过订单 ${row.currentOrderId},并立即派发该车辆的下一段订单。\n\n• 此操作适用于车辆故障已修复、旧订单确认无法继续的场景\n• 旧订单将被标记为「已取消」,不会通知车端\n• 操作前请确认车端已不再执行旧订单,否则可能与新订单冲突\n• ERROR 状态的车辆执行后将重新进入 RUNNING 并派发下一段订单\n\n是否继续?`, |
| | | positiveText: '确定', |
| | |
| | | await refreshBatchData() |
| | | } |
| | | else { |
| | | // message.error(res?.message || res?.msg || '故障恢复失败') |
| | | // message.error(res?.message || res?.msg || '故障恢复失败') |
| | | } |
| | | } |
| | | finally { |
| | |
| | | <span class="font-bold" style="font-size: 16px">批次进度</span> |
| | | </div> |
| | | <div class="flex items-center gap-6 mb-4"> |
| | | <n-tag :type="getBatchStatusType(batchProgress.status)" size="medium"> |
| | | <NTag :type="getBatchStatusType(batchProgress.status)" size="medium"> |
| | | {{ getBatchStatusLabel(batchProgress.status) }} |
| | | </n-tag> |
| | | </NTag> |
| | | <span v-if="batchProgress.startTime" class="text-sm text-gray-500"> |
| | | 开始:{{ dayjs(batchProgress.startTime).format('YYYY/MM/DD HH:mm:ss') }} |
| | | </span> |
| | |
| | | <script setup lang="tsx"> |
| | | import noDataImg from '@/assets/element-img/no-data.png' |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { NButton, NSpace, NTooltip } from 'naive-ui' |
| | | import { NButton, NSpace } from 'naive-ui' |
| | | import { computed, onActivated, onMounted, ref } from 'vue' |
| | | import { useRouter } from 'vue-router' |
| | | import OperationLogModal from '@/components/custom/OperationLogModal.vue' |
| | |
| | | catch (error: any) { |
| | | listData.value = [] |
| | | count.value = 0 |
| | | // message.error(error?.message || '获取车辆列表失败') |
| | | // message.error(error?.message || '获取车辆列表失败') |
| | | } |
| | | finally { |
| | | loading.value = false |
| | |
| | | show.value = false |
| | | } |
| | | catch (error: any) { |
| | | // message.error(error?.message || (isAssignAction.value ? '分派车辆失败' : '改派车辆失败')) |
| | | // message.error(error?.message || (isAssignAction.value ? '分派车辆失败' : '改派车辆失败')) |
| | | } |
| | | finally { |
| | | submitting.value = false |
| | |
| | | catch (error: any) { |
| | | listData.value = [] |
| | | count.value = 0 |
| | | // message.error(error?.message || '获取测试车辆失败') |
| | | // message.error(error?.message || '获取测试车辆失败') |
| | | } |
| | | finally { |
| | | loading.value = false |
| | |
| | | emit('success') |
| | | } |
| | | catch (error: any) { |
| | | // message.error(error?.message || successText) |
| | | // message.error(error?.message || successText) |
| | | } |
| | | finally { |
| | | actionLoading.value = false |
| | |
| | | emit('success') |
| | | } |
| | | catch (error: any) { |
| | | // message.error(error?.message || successText) |
| | | // message.error(error?.message || successText) |
| | | } |
| | | finally { |
| | | actionLoading.value = false |
| | |
| | | return |
| | | } |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '确认批量移除所选车辆吗?', |
| | | content: '移除后将从测试计划中删除', |
| | | positiveText: '确定', |
| | |
| | | return |
| | | } |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '确认批量暂停所选车辆吗?', |
| | | content: '所选车辆可能正在执行订单,暂停服务后将无法接收新订单', |
| | | positiveText: '确定', |
| | |
| | | |
| | | function handleRemove(row: VehicleRow) { |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '确认移除该车辆吗?', |
| | | content: '移除后将从测试计划中删除', |
| | | positiveText: '确定', |
| | |
| | | |
| | | if (paused === 1) { |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '确认暂停该车辆吗?', |
| | | content: '该车辆正在执行订单,暂停服务后将无法接收新订单', |
| | | positiveText: '确定', |
| | |
| | | <script setup lang="tsx"> |
| | | import { computed } from 'vue' |
| | | import { NModal, NCard } from 'naive-ui' |
| | | import { NModal } from 'naive-ui' |
| | | import type1Img from '@/assets/element-img/type1.png' |
| | | import type2Img from '@/assets/element-img/type2.png' |
| | | |
| | |
| | | |
| | | const show = computed({ |
| | | get: () => props.visible, |
| | | set: (val) => emit('update:visible', val) |
| | | set: val => emit('update:visible', val), |
| | | }) |
| | | |
| | | function handleCancel() { |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <n-modal |
| | | <NModal |
| | | v-model:show="show" |
| | | :mask-closable="false" |
| | | preset="card" |
| | | title="订单生成方式说明" |
| | | class="w-[800px] order-gen-modal" |
| | |
| | | <!-- 全排列模式 --> |
| | | <div> |
| | | <div class="flex items-center gap-8px mb-12px"> |
| | | <div class="w-4px h-16px bg-[#0068FF] rounded-sm"></div> |
| | | <div class="w-4px h-16px bg-[#0068FF] rounded-sm" /> |
| | | <span class="text-16px font-bold text-[#333]">全排列模式</span> |
| | | </div> |
| | | <div class="bg-[#F7F8FA] rounded-4px p-12px"> |
| | | <div class="mb-12px text-13px" style="color:rgba(23, 31, 38, 0.6)">生成方式说明</div> |
| | | <div class="mb-12px text-13px" style="color:rgba(23, 31, 38, 0.6)"> |
| | | 生成方式说明 |
| | | </div> |
| | | <div class="flex items-start items justify-between gap-32px"> |
| | | <div class="flex-1 text-[#171F26] text-14px leading-24px"> |
| | | 输入A\B\C\D\E\F等多个点位(所有的遍历一次,正逆向,屏蔽过边界的道路和高德生成道路失败的订单,订单生成失败需要有标记),生成所有点位间的两两订单序列 |
| | |
| | | <!-- 串烧模式 --> |
| | | <div> |
| | | <div class="flex items-center gap-8px mb-12px"> |
| | | <div class="w-4px h-16px bg-[#0068FF] rounded-sm"></div> |
| | | <div class="w-4px h-16px bg-[#0068FF] rounded-sm" /> |
| | | <span class="text-16px font-bold text-[#333]">串烧模式</span> |
| | | </div> |
| | | <div class="bg-[#F7F8FA] rounded-4px p-12px"> |
| | | <div class="mb-12px text-13px" style="color:rgba(23, 31, 38, 0.6)">生成方式说明</div> |
| | | <div class="mb-12px text-13px" style="color:rgba(23, 31, 38, 0.6)"> |
| | | 生成方式说明 |
| | | </div> |
| | | <div class="flex items-start items justify-between gap-32px"> |
| | | <div class="flex-1 text-[#171F26] text-14px leading-24px"> |
| | | 生成方式说明:输入A、B、C、D、E等多个点位,生成A→B→C→D→E→...A的闭环订单序列,顺向和逆向都需要生成; |
| | |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </n-modal> |
| | | </NModal> |
| | | </template> |
| | | |
| | | <style scoped> |
| | |
| | | return ['取消订单', '改派', '删除'] |
| | | case 6: |
| | | return ['关闭订单', '详情', '删除'] |
| | | case 10: |
| | | case 10: |
| | | return ['详情', '删除'] |
| | | default: |
| | | return ['删除'] |
| | |
| | | |
| | | function confirmDeleteOrders(orderIds: Array<number | string>) { |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '确认删除订单吗?', |
| | | content: '删除后无法恢复', |
| | | positiveText: '确定', |
| | |
| | | } |
| | | if (action === '取消订单') { |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '确认取消订单吗?', |
| | | positiveText: '确定', |
| | | negativeText: '取消', |
| | |
| | | } |
| | | if (action === '关闭订单') { |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '确认关闭订单吗?', |
| | | positiveText: '确定', |
| | | negativeText: '取消', |
| | |
| | | |
| | | const isResume = planStatusCode.value === 3 |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: isResume ? '是否确认继续下发' : '是否确认中断下发', |
| | | content: isResume ? '继续后,系统将继续下发剩余订单' : '中断后,系统将暂停下发剩余订单', |
| | | positiveText: '确定', |
| | |
| | | <script setup lang="tsx"> |
| | | import noDataImg from '@/assets/element-img/no-data.png' |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { NButton, NSpace, NTag, NTooltip, useDialog, useMessage } from 'naive-ui' |
| | | import { NButton, NSpace, NTag, useDialog, useMessage } from 'naive-ui' |
| | | import { computed, onMounted, ref } from 'vue' |
| | | import { useRouter } from 'vue-router' |
| | | import { useBoolean } from '@/hooks' |
| | |
| | | |
| | | function handleInterrupt(row: TestPlanRow) { |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '是否确认中断下发', |
| | | content: '中断后,系统将暂停下发剩余订单', |
| | | positiveText: '确定', |
| | |
| | | |
| | | function handleClose(row: TestPlanRow) { |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '确认关闭测试吗?', |
| | | content: '关闭后测试计划将终止', |
| | | positiveText: '确定', |
| | |
| | | |
| | | function handleContinue(row: TestPlanRow) { |
| | | dialog.info({ |
| | | maskClosable: false, |
| | | title: '确认继续测试吗?', |
| | | content: '测试计划将继续执行', |
| | | positiveText: '确定', |
| | |
| | | import { computed, nextTick, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue' |
| | | import { useScriptTag } from '@vueuse/core' |
| | | import { useRoute } from 'vue-router' |
| | | import { createPoint, deletePoint, enableDisablePoint, fetchElectronicFenceAreaList, fetchPointList, updatePoint } from '@/service/api/copyText' |
| | | import { createPoint, deletePoint, fetchElectronicFenceAreaList, fetchPointList, updatePoint } from '@/service/api/copyText' |
| | | import PointStatusSteps from '@/components/PointStatusSteps/custom.vue' |
| | | import ConfirmDialog from '@/components/ConfirmDialog/index.vue' |
| | | import { debounce } from 'radash' |
| | |
| | | const targetStatus = isEnabled ? 3 : 2 |
| | | |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '提示', |
| | | content: `确认${actionText}该点位吗?`, |
| | | positiveText: '确定', |
| | |
| | | } |
| | | } |
| | | catch { |
| | | // message.error('导出失败') |
| | | // message.error('导出失败') |
| | | } |
| | | } |
| | | |
| | |
| | | // 删除 |
| | | function handleDelete(row: any) { |
| | | dialog.create({ |
| | | maskClosable: false, |
| | | title: '删除确认', |
| | | content: '确认删除该定向服务?删除后不可恢复。', |
| | | positiveText: '确定', |
| | |
| | | const statusValue = targetStatus ? 1 : 2 |
| | | const actionText = targetStatus ? '启用' : '禁用' |
| | | dialog.create({ |
| | | maskClosable: false, |
| | | title: `${actionText}确认`, |
| | | content: `确定要${actionText}该定向服务吗?`, |
| | | positiveText: '确定', |
| | |
| | | type: 'number', |
| | | required: true, |
| | | message: '请输入优惠金额', |
| | | trigger: ['blur', 'input'] |
| | | trigger: ['blur', 'input'], |
| | | }, |
| | | { |
| | | validator: (rule: any, value: number | null) => { |
| | | if (value === null || value === undefined) return true |
| | | if (value === null || value === undefined) |
| | | return true |
| | | const payAmount = Number(formModel.value.payAmount) |
| | | if (!isNaN(payAmount) && value > maxDiscountAmount.value) { |
| | | return new Error('优惠后金额需大于1元') |
| | | } |
| | | return true |
| | | }, |
| | | trigger: ['input', 'blur'] |
| | | } |
| | | ] |
| | | trigger: ['input', 'blur'], |
| | | }, |
| | | ], |
| | | } |
| | | |
| | | async function submitModal() { |
| | |
| | | import { cancelAndCloseOrder, deleteOrder, exportOrderExcel, getFleetList, getOrderList, refundOrder } from '@/service/api/transaction' |
| | | import { operationPageList } from '@/service/api/vehicleConvoy' |
| | | import { getRegionCascaderNoArea } from '@/service/api/operations' |
| | | import { NButton, NSpace, useMessage, useDialog,NTag } from 'naive-ui' |
| | | import { NButton, NSpace, NTag, useDialog, useMessage } from 'naive-ui' |
| | | import SearchPanel from '@/components/custom/SearchPanel.vue' |
| | | import OperationLogModal from '@/components/custom/OperationLogModal.vue' |
| | | import dayjs from 'dayjs' |
| | |
| | | // }, |
| | | // ] |
| | | const statusOptions = [ |
| | | { label: '待接单', value: 1, type: 'primary',color:'#E6F4FF',textColor:'#0068FF' }, |
| | | { label: '待出发', value: 2, type: 'primary' ,color:'#E6F4FF',textColor:'#0068FF' }, |
| | | { label: '接驾中', value: 3, type: 'warning' ,color:'#E6F4FF',textColor:'#0068FF' }, |
| | | { label: '待客上车', value: 4, type: 'primary',color:'#E6F4FF',textColor:'#0068FF' }, |
| | | { label: '乘客已上车', value: 5, type: 'success',color:'#F6F0FF',textColor:'#764DFF' }, |
| | | { label: '服务中', value: 6, type: 'success',color:'#F6F0FF',textColor:'#764DFF' }, |
| | | { label: '待客下车', value: 7, type: 'warning' ,color:'#F6F0FF',textColor:'#764DFF'}, |
| | | { label: '待付款', value: 8, type: 'warning' ,color:'#FFF8E6',textColor:'#FF9000'}, |
| | | { label: '待评价', value: 9, type: 'default',color:'#FFF8E6',textColor:'#FF9000' }, |
| | | { label: '已完成', value: 10, type: 'success',color:'#DCF5DD',textColor:'#0BB52D' }, |
| | | { label: '已取消', value: 0, type: 'default',color:'#171F260D',textColor:'#171F26CC' }, |
| | | { label: '待接单', value: 1, type: 'primary', color: '#E6F4FF', textColor: '#0068FF' }, |
| | | { label: '待出发', value: 2, type: 'primary', color: '#E6F4FF', textColor: '#0068FF' }, |
| | | { label: '接驾中', value: 3, type: 'warning', color: '#E6F4FF', textColor: '#0068FF' }, |
| | | { label: '待客上车', value: 4, type: 'primary', color: '#E6F4FF', textColor: '#0068FF' }, |
| | | { label: '乘客已上车', value: 5, type: 'success', color: '#F6F0FF', textColor: '#764DFF' }, |
| | | { label: '服务中', value: 6, type: 'success', color: '#F6F0FF', textColor: '#764DFF' }, |
| | | { label: '待客下车', value: 7, type: 'warning', color: '#F6F0FF', textColor: '#764DFF' }, |
| | | { label: '待付款', value: 8, type: 'warning', color: '#FFF8E6', textColor: '#FF9000' }, |
| | | { label: '待评价', value: 9, type: 'default', color: '#FFF8E6', textColor: '#FF9000' }, |
| | | { label: '已完成', value: 10, type: 'success', color: '#DCF5DD', textColor: '#0BB52D' }, |
| | | { label: '已取消', value: 0, type: 'default', color: '#171F260D', textColor: '#171F26CC' }, |
| | | ] |
| | | |
| | | const columns = computed<DataTableColumns<any>>(() => [ |
| | |
| | | return |
| | | } |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '提示', |
| | | content: '是否放弃当前编辑的免密支付策略?', |
| | | positiveText: '放弃', |
| | |
| | | { |
| | | label: '城市', |
| | | path: 'cityName', |
| | | render: () => <n-input v-model:value={model.value.cityName} allow-input={(value: string) => !value || /^[a-zA-Z\u4e00-\u9fa5]+$/.test(value)} clearable placeholder="请输入城市" />, |
| | | render: () => <n-input v-model:value={model.value.cityName} allow-input={(value: string) => !value || /^[a-z\u4E00-\u9FA5]+$/i.test(value)} clearable placeholder="请输入城市" />, |
| | | }, |
| | | { |
| | | label: '生效时间段', |
| | |
| | | // 删除 |
| | | function handleDelete(row: any) { |
| | | dialog.create({ |
| | | maskClosable: false, |
| | | title: '删除确认', |
| | | content: '确定要删除该免密支付策略吗?删除后不可恢复。', |
| | | positiveText: '确定', |
| | |
| | | if (result) { |
| | | if (modalType.value === 'add') { |
| | | dialog.success({ |
| | | maskClosable: false, |
| | | title: '创建成功,是否去设置价格?', |
| | | content: '', |
| | | positiveText: '设置价格', |
| | |
| | | else { |
| | | if (data.pricingType !== formModel.value.pricingType) { |
| | | dialog.success({ |
| | | maskClosable: false, |
| | | title: '编辑成功,是否去设置价格?', |
| | | content: '', |
| | | positiveText: '设置价格', |
| | |
| | | getCityList, |
| | | getTradingStrategyDetail, |
| | | } from '@/service/api/transaction' |
| | | import { handleInputNumberKeyDown } from '@/utils/common' |
| | | import dayjs from 'dayjs' |
| | | |
| | | const { |
| | |
| | | |
| | | function handleDeleteSpecialTimeRule(rule: SpecialTimeStrategyItem) { |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '删除确认', |
| | | content: isSpecialTimeRuleEffective(rule) |
| | | ? '该时段规则正在生效,删除后订单将按基础策略执行,是否确认删除?' |
| | |
| | | |
| | | function createCouponColumns(strategyIndex: number, type: 'normal' | 'afterUse'): DataTableColumns<any> { |
| | | return [ |
| | | { title: '添加时间', key: 'createTime', width: 220 }, |
| | | { title: '添加时间', key: 'createTime', width: 220, render: row => dayjs(row.createTime).format('YYYY/MM/DD HH:mm') }, |
| | | { title: '优惠券名称', key: 'couponName', ellipsis: true }, |
| | | { |
| | | title: '优惠券类型', |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <div class=" w-full h-full relative content flex flex-col"> |
| | | <div class=" w-full h-full relative content flex flex-col" > |
| | | <div class="p-12px main flex flex-col flex-1 overflow-hidden"> |
| | | <NTabs v-model:value="activeTab" type="line" class="custom-tabs" animated> |
| | | <NTabPane :name="1" tab="活动介绍内容" /> |
| | |
| | | box-shadow: 0px 0px 5px 0px rgba(23, 31, 38, 0.05); |
| | | padding: 16px 24px; |
| | | } |
| | | .n-button--secondary { |
| | | box-sizing: border-box; |
| | | color: #0068FF; |
| | | border: 1px solid #7ABDFF; |
| | | background-color: #E6F4FF; |
| | | } |
| | | |
| | | :deep(.n-tabs-tab-pad) { |
| | | width: 24px !important; |
| | |
| | | title: '优惠券名称', |
| | | key: 'couponName', |
| | | render: (row) => { |
| | | return <span style={{ color: '#1890FF', cursor: 'pointer' }}>{row.couponName}</span> |
| | | return <span>{row.couponName}</span> |
| | | }, |
| | | }, |
| | | { |
| | |
| | | pageSize.value = size |
| | | currentPage.value = isPageSizeChanged ? 1 : page |
| | | getList() |
| | | } |
| | | |
| | | function toggleRowSelection(row: any) { |
| | | const rowKey = String(row?.id ?? '') |
| | | if (!rowKey) |
| | | return |
| | | |
| | | if (checkedRowKeys.value.includes(rowKey)) { |
| | | checkedRowKeys.value = checkedRowKeys.value.filter(key => key !== rowKey) |
| | | return |
| | | } |
| | | |
| | | checkedRowKeys.value = [...checkedRowKeys.value, rowKey] |
| | | } |
| | | |
| | | function shouldIgnoreRowClick(target: EventTarget | null) { |
| | | const element = target as HTMLElement | null |
| | | return Boolean( |
| | | element?.closest( |
| | | '.n-checkbox, .n-checkbox-box, .n-checkbox-input, button, a, input, textarea, .n-base-selection', |
| | | ), |
| | | ) |
| | | } |
| | | |
| | | function getRowProps(row: any) { |
| | | return { |
| | | style: 'cursor: pointer;', |
| | | onClick: (event: MouseEvent) => { |
| | | if (shouldIgnoreRowClick(event.target)) |
| | | return |
| | | toggleRowSelection(row) |
| | | }, |
| | | } |
| | | } |
| | | |
| | | function handleSave() { |
| | |
| | | <!-- 表格区域 --> |
| | | <n-data-table |
| | | v-model:checked-row-keys="checkedRowKeys" style="height: 100%;" flex-height :columns="columns" :data="list" :loading="loading" |
| | | :bordered="false" :row-key="(row) => String(row.id)" :pagination="false" |
| | | :bordered="false" :row-key="(row) => String(row.id)" :row-props="getRowProps" :pagination="false" |
| | | > |
| | | <template #empty> |
| | | <div class="flex-center"> |
| | |
| | | return |
| | | } |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '确认删除', |
| | | content: '确定要删除选中的记录吗?', |
| | | positiveText: '确定', |
| | |
| | | render(row) { |
| | | const isNormal = isNormalStatus(row.status) |
| | | return ( |
| | | <NTag type={isNormal ? 'info' : 'warning'} size="small" bordered={false} style={{ color: isNormal ? '#1890FF' : '#FAAD14', backgroundColor: isNormal ? '#E6F7FF' : '#FFFBE6' }}> |
| | | <NTag color={{ color: isNormal ? '#E6F4FF' : '#FFF8E6', textColor: isNormal ? '#0068FF' : '#FF9000' }} size="small" bordered={false}> |
| | | {isNormal ? '正常' : '冻结'} |
| | | </NTag> |
| | | ) |
| | |
| | | currentPage.value = page |
| | | } |
| | | getList() |
| | | } |
| | | |
| | | function toggleRowSelection(row: any) { |
| | | if (!isNormalStatus(row?.status)) |
| | | return |
| | | |
| | | const rowKey = String(row?.id ?? '') |
| | | if (!rowKey) |
| | | return |
| | | |
| | | if (checkedRowKeys.value.includes(rowKey)) { |
| | | checkedRowKeys.value = checkedRowKeys.value.filter(key => key !== rowKey) |
| | | return |
| | | } |
| | | |
| | | checkedRowKeys.value = [...checkedRowKeys.value, rowKey] |
| | | } |
| | | |
| | | function shouldIgnoreRowClick(target: EventTarget | null) { |
| | | const element = target as HTMLElement | null |
| | | return Boolean( |
| | | element?.closest( |
| | | '.n-checkbox, .n-checkbox-box, .n-checkbox-input, button, a, input, textarea, .n-base-selection', |
| | | ), |
| | | ) |
| | | } |
| | | |
| | | function getRowProps(row: any) { |
| | | return { |
| | | style: isNormalStatus(row?.status) ? 'cursor: pointer;' : '', |
| | | onClick: (event: MouseEvent) => { |
| | | if (shouldIgnoreRowClick(event.target)) |
| | | return |
| | | toggleRowSelection(row) |
| | | }, |
| | | } |
| | | } |
| | | |
| | | function handleSave() { |
| | |
| | | <!-- 表格区域 --> |
| | | <n-data-table |
| | | v-model:checked-row-keys="checkedRowKeys" style="height: 100%;" flex-height :columns="columns" :data="list" :loading="loading" |
| | | :bordered="false" :row-key="(row) => String(row.id)" :pagination="false" |
| | | :bordered="false" :row-key="(row) => String(row.id)" :row-props="getRowProps" :pagination="false" |
| | | > |
| | | <template #empty> |
| | | <div class="flex-center"> |
| | |
| | | |
| | | function handlePublish(row: any) { |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '确认发布', |
| | | content: '确认发布该活动吗?', |
| | | positiveText: '确定', |
| | |
| | | |
| | | function handlePause(row: any) { |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '确认暂停', |
| | | content: '确认暂停该活动吗?', |
| | | positiveText: '确定', |
| | |
| | | |
| | | function handleResume(row: any) { |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '确认恢复', |
| | | content: '确认恢复该活动吗?', |
| | | positiveText: '确定', |
| | |
| | | |
| | | function handleStop(row: any) { |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '确认终止', |
| | | content: '确认终止该活动吗?终止后不可恢复。', |
| | | positiveText: '确定', |
| | |
| | | |
| | | function handleDelete(row: any) { |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '是否确认删除数据?', |
| | | content: '确认删除该活动?删除后不可恢复。', |
| | | positiveText: '确定', |
| | |
| | | const isUserTypeSelectOpen = ref(false) |
| | | const isStatusSelectOpen = ref(false) |
| | | |
| | | const checkedRowKeys = ref<number[]>([]) |
| | | const checkedRowKeys = ref<string[]>([]) |
| | | const list = ref<any[]>([]) |
| | | |
| | | const currentPage = ref(1) |
| | |
| | | render(row) { |
| | | const isNormal = isNormalStatus(row.status) |
| | | return ( |
| | | <NTag type={isNormal ? 'info' : 'warning'} size="small" bordered={false} style={{ color: isNormal ? '#1890FF' : '#FAAD14', backgroundColor: isNormal ? '#E6F7FF' : '#FFFBE6' }}> |
| | | <NTag color={{ color: isNormal ? '#E6F4FF' : '#FFF8E6', textColor: isNormal ? '#0068FF' : '#FF9000' }} size="small" bordered={false}> |
| | | {isNormal ? '正常' : '冻结'} |
| | | </NTag> |
| | | ) |
| | |
| | | getList() |
| | | } |
| | | |
| | | function toggleRowSelection(row: any) { |
| | | if (!isNormalStatus(row?.status)) |
| | | return |
| | | |
| | | const rowKey = String(row?.id ?? '') |
| | | if (!rowKey) |
| | | return |
| | | |
| | | if (checkedRowKeys.value.includes(rowKey)) { |
| | | checkedRowKeys.value = checkedRowKeys.value.filter(key => key !== rowKey) |
| | | return |
| | | } |
| | | |
| | | checkedRowKeys.value = [...checkedRowKeys.value, rowKey] |
| | | } |
| | | |
| | | function shouldIgnoreRowClick(target: EventTarget | null) { |
| | | const element = target as HTMLElement | null |
| | | return Boolean( |
| | | element?.closest( |
| | | '.n-checkbox, .n-checkbox-box, .n-checkbox-input, button, a, input, textarea, .n-base-selection', |
| | | ), |
| | | ) |
| | | } |
| | | |
| | | function getRowProps(row: any) { |
| | | return { |
| | | style: isNormalStatus(row?.status) ? 'cursor: pointer;' : '', |
| | | onClick: (event: MouseEvent) => { |
| | | if (shouldIgnoreRowClick(event.target)) |
| | | return |
| | | toggleRowSelection(row) |
| | | }, |
| | | } |
| | | } |
| | | |
| | | function handleSendAll() { |
| | | // if (!isNormalStatus(searchModel.value.status)) { |
| | | // message.warning('一键发放所有用户仅支持“冻结状态”筛选为“正常”') |
| | |
| | | } |
| | | |
| | | function handleSendSelected() { |
| | | const selectedRows = list.value.filter(item => checkedRowKeys.value.includes(item.id)) |
| | | const selectedRows = list.value.filter(item => checkedRowKeys.value.includes(String(item?.id ?? ''))) |
| | | const hasFrozen = selectedRows.some(row => !isNormalStatus(row.status)) |
| | | if (hasFrozen) { |
| | | message.warning('选中用户包含冻结状态,请仅选择“正常”用户') |
| | |
| | | <!-- 表格区域 --> |
| | | <n-data-table |
| | | v-model:checked-row-keys="checkedRowKeys" style="height: 100%;" flex-height :columns="columns" :data="list" :loading="loading" |
| | | :bordered="false" :row-key="(row) => row.id" :pagination="false" |
| | | :bordered="false" :row-key="(row) => String(row.id)" :row-props="getRowProps" :pagination="false" |
| | | > |
| | | <template #empty> |
| | | <div class="flex-center"> |
| | |
| | | |
| | | <style scoped lang="scss"> |
| | | .content { |
| | | height: calc(100vh - 76px); |
| | | height: calc(100vh - 76px - 28px); |
| | | width: 100%; |
| | | overflow-y: auto; |
| | | scrollbar-width: none; |
| | |
| | | } |
| | | |
| | | .box.full-height { |
| | | height: calc(100vh - 76px - 64px); |
| | | height: calc(100vh - 76px - 64px - 28px); |
| | | padding: 24px; |
| | | margin-bottom: 0; |
| | | } |
| | |
| | | return |
| | | |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '确认发放', |
| | | // content: `确定向当前筛选条件下所有用户发放优惠券么?共${payload.userCount}个用户。`, |
| | | content: `将发给状态为“正常”的用户,状态为“冻结”的用户将不发放,是否确认发放?`, |
| | |
| | | return |
| | | |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '确认发放', |
| | | content: `确定要给选中的 ${rows.length} 位用户发放该优惠券吗?`, |
| | | positiveText: '确定', |
| | |
| | | 删除 |
| | | </span> |
| | | </div> |
| | | </template> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | </style> |
| | |
| | | // } |
| | | } |
| | | catch { |
| | | // message.error('图片读取失败,请重试') |
| | | // message.error('图片读取失败,请重试') |
| | | formModel.value.bannerPictures = '' |
| | | fileList.value = [] |
| | | options.onError?.() |
| | |
| | | import noDataImg from '@/assets/element-img/no-data.png' |
| | | import { computed, h, onMounted, ref } from 'vue' |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { NImage, NSwitch, NTag, useDialog, useMessage } from 'naive-ui' |
| | | import { NImage, NTag, useDialog, useMessage } from 'naive-ui' |
| | | import SearchPanel from '@/components/custom/SearchPanel.vue' |
| | | import OperationLogModal from '@/components/custom/OperationLogModal.vue' |
| | | import BannerFormModal from './components/BannerFormModal.vue' |
| | |
| | | |
| | | function handleDelete(row: Banner) { |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '确认删除', |
| | | content: `确认删除广告 "${row.bannerTitle}" 吗?`, |
| | | positiveText: '确定', |
| | |
| | | return |
| | | |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '确认删除', |
| | | content: `确认删除选中的 ${selectedRowKeys.value.length} 条广告吗?`, |
| | | positiveText: '确定', |
| | |
| | | fetchData() |
| | | } |
| | | else { |
| | | // message.error('部分删除失败,请重试') |
| | | // message.error('部分删除失败,请重试') |
| | | fetchData() |
| | | } |
| | | } |
| | | catch (e) { |
| | | console.error(e) |
| | | // message.error('删除失败,请重试') |
| | | // message.error('删除失败,请重试') |
| | | } |
| | | }, |
| | | }) |
| | |
| | | } |
| | | const res = await getLoginAgreementDetail(row.id) |
| | | if (res?.code !== 200 || !res.data) { |
| | | // message.error(res?.msg || '协议详情获取失败') |
| | | // message.error(res?.msg || '协议详情获取失败') |
| | | return null |
| | | } |
| | | return res.data |
| | |
| | | |
| | | const res = await getLoginAgreementLatestVersion(nameId) |
| | | if (res?.code !== 200 || !res.data) { |
| | | // message.error(res?.msg || '最新版本号获取失败') |
| | | // message.error(res?.msg || '最新版本号获取失败') |
| | | return '' |
| | | } |
| | | return String(res.data) |
| | |
| | | type: agreementType.value, |
| | | }) |
| | | if (res?.code !== 200 || !res.data) { |
| | | // message.error(res?.msg || '协议版本列表获取失败') |
| | | // message.error(res?.msg || '协议版本列表获取失败') |
| | | return |
| | | } |
| | | const records = res.data.records || [] |
| | |
| | | try { |
| | | const res = await copyLoginAgreement(row.id) |
| | | if (res?.code !== 200) { |
| | | // message.error(res?.msg || '复制失败') |
| | | // message.error(res?.msg || '复制失败') |
| | | return |
| | | } |
| | | message.success('复制成功') |
| | |
| | | ? await enableLoginAgreement(row.id) |
| | | : await disableLoginAgreement(row.id) |
| | | if (res?.code !== 200) { |
| | | // message.error(res?.msg || `${loadingType === 'enable' ? '启用' : '停用'}失败`) |
| | | // message.error(res?.msg || `${loadingType === 'enable' ? '启用' : '停用'}失败`) |
| | | return |
| | | } |
| | | message.success(loadingType === 'enable' ? '启用成功' : '停用成功') |
| | |
| | | |
| | | function confirmEnableVersion(row: AgreementVersionRow) { |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '提示', |
| | | content: '启用新版本后,旧版本将失效,新用户将看到新版本协议,是否确认?', |
| | | positiveText: '确认', |
| | |
| | | |
| | | function confirmDisableVersion(row: AgreementVersionRow) { |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '提示', |
| | | content: '停用后乘客端将无协议展示,影响用户登录流程,是否确认停用?', |
| | | positiveText: '确认', |
| | |
| | | } |
| | | else { |
| | | tableData.value = [] |
| | | // message.error(res?.msg || '协议列表获取失败') |
| | | // message.error(res?.msg || '协议列表获取失败') |
| | | } |
| | | } |
| | | finally { |
| | |
| | | async function handleEdit(row: AgreementItem) { |
| | | const res = await getLoginAgreementDetail(row.nameId) |
| | | if (res?.code !== 200 || !res.data) { |
| | | // message.error(res?.msg || '协议详情获取失败') |
| | | // message.error(res?.msg || '协议详情获取失败') |
| | | return |
| | | } |
| | | router.push({ |
| | |
| | | await loadList() |
| | | } |
| | | else { |
| | | // message.error(res?.msg || '删除失败') |
| | | // message.error(res?.msg || '删除失败') |
| | | } |
| | | } |
| | | finally { |
| | |
| | | ) |
| | | const failed = responses.find(item => item?.code !== 200) |
| | | if (failed) { |
| | | // message.error(failed?.msg || '排序保存失败') |
| | | // message.error(failed?.msg || '排序保存失败') |
| | | await loadList() |
| | | return false |
| | | } |
| | |
| | | v-model:value="item.question" |
| | | type="textarea" |
| | | placeholder="请输入" |
| | | auto-height |
| | | :autosize="{ |
| | | minRows: 1, |
| | | maxRows: 5, |
| | | }" |
| | | :bordered="false" |
| | | :resizable="false" |
| | | /> |
| | |
| | | v-model:value="item.answer" |
| | | type="textarea" |
| | | placeholder="请输入" |
| | | :autosize="{ |
| | | minRows: 1, |
| | | maxRows: 5, |
| | | }" |
| | | :bordered="false" |
| | | :resizable="false" |
| | | auto-height |
| | | /> |
| | | </td> |
| | | <td style="text-align: center;"> |
| | |
| | | const formRef = ref<FormInst | null>(null) |
| | | |
| | | const formModel = ref<FormModel>({ |
| | | content: '' |
| | | content: '', |
| | | }) |
| | | |
| | | const rules: FormRules = { |
| | |
| | | |
| | | <template> |
| | | <div> |
| | | <n-form |
| | | ref="formRef" |
| | | <n-form |
| | | ref="formRef" |
| | | :model="formModel" |
| | | :rules="rules" |
| | | label-placement="left" |
| | | :rules="rules" |
| | | label-placement="left" |
| | | label-width="80" |
| | | require-mark-placement="left" |
| | | > |
| | |
| | | positiveText: '确定', |
| | | negativeText: '取消', |
| | | maskClosable: false, |
| | | autoFocus: false, |
| | | autoFocus: false, |
| | | onPositiveClick: () => { |
| | | doClose() |
| | | }, |
| | |
| | | <NInput v-model:value="formModel.nickName" placeholder="请输入" /> |
| | | </NFormItem> |
| | | </NGridItem> |
| | | |
| | | |
| | | <NGridItem> |
| | | <NFormItem label="手机号" path="account"> |
| | | <NInput v-model:value="formModel.account" placeholder="请输入" /> |
| | |
| | | } |
| | | |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '确认删除', |
| | | content: '确认删除选中的记录吗?', |
| | | positiveText: '确认', |
| | |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { NButton, NTag, useDialog, useMessage } from 'naive-ui' |
| | | import { useBoolean } from '@/hooks' |
| | | import { downloadFile } from '@/utils' |
| | | import { downloadFile, onlyAllowNumber, onlyAllowNumberAndLetter } from '@/utils' |
| | | import SearchPanel from '@/components/custom/SearchPanel.vue' |
| | | import OperationLogModal from '@/components/custom/OperationLogModal.vue' |
| | | import WhitelistEditModal from './components/WhitelistEditModal.vue' |
| | |
| | | import Pagination from '@/components/common/Pagination.vue' |
| | | import dayjs from 'dayjs' |
| | | import { useRouter } from 'vue-router' |
| | | import { onlyAllowNumber, onlyAllowNumberAndLetter } from '@/utils' |
| | | |
| | | // 路由实例,用于从列表跳转到白名单详情页。 |
| | | const router = useRouter() |
| | |
| | | // 确认后调用解冻接口,并在成功后刷新列表状态。 |
| | | function handleUnfreeze(row: any) { |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '确认解冻', |
| | | content: `确定要解冻用户 ${row.nickName || row.account} 吗?`, |
| | | positiveText: '确定', |
| | |
| | | } |
| | | |
| | | dialog.warning({ |
| | | maskClosable: false, |
| | | title: '确认删除', |
| | | content: `确定要删除用户 ${row.nickName || row.account} 吗?删除后数据不可恢复。`, |
| | | positiveText: '确定', |
| | |
| | | <script setup lang="tsx"> |
| | | import noDataImg from '@/assets/element-img/no-data.png' |
| | | import type { DataTableColumns, FormInst } from 'naive-ui' |
| | | import { NButton, NCard, NDataTable, NDatePicker, NForm, NFormItem, NGi, NInput, NSelect, NTooltip, useMessage } from 'naive-ui' |
| | | import { NButton, NCard, NDataTable, NForm, NFormItem, NGi, NTooltip } from 'naive-ui' |
| | | import dayjs from 'dayjs' |
| | | import { useBoolean } from '@/hooks' |
| | | import { downloadFile } from '@/utils' |