Merge branch 'deploy-1.0' into deploy-2.0
| | |
| | | next() |
| | | return |
| | | } |
| | | // 如果路由设置了requiresAuth为true,且用户未登录,重定向到首页 |
| | | // 如果路由设置了requiresAuth为true,且用户未登录,重定向到首页(列表页) |
| | | else if (to.meta.requiresAuth !== false && !isLogin) { |
| | | // 避免无限重定向到首页 |
| | | if (to.path === homePath) { |
| | | next() |
| | | return |
| | | } |
| | | next({ path: homePath, replace: true }) |
| | | return |
| | | } |
| | |
| | | return |
| | | } |
| | | catch { |
| | | // 如果路由初始化失败(比如 401 错误),重定向到登录页 |
| | | const redirect = to.fullPath !== '/' ? to.fullPath : undefined |
| | | next({ path: '/login', query: redirect ? { redirect } : undefined }) |
| | | // 如果路由初始化失败(比如 401 错误),重定向到首页 |
| | | next({ path: homePath, replace: true }) |
| | | return |
| | | } |
| | | } |
| | |
| | | <script setup lang="tsx"> |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { NAvatar, NButton, NSelect, NSpace } from 'naive-ui' |
| | | import { NButton, NImage, NSelect, NSpace } from 'naive-ui' |
| | | import { useBoolean } from '@/hooks' |
| | | import { fetchOperationsUserList, fetchRoleList } from '@/service/api/fleet' |
| | | import defaultAvatarImg from '@/assets/public/avater.png' |
| | | |
| | | interface StaffItem { |
| | | id: number |
| | |
| | | const currentPage = ref(1) |
| | | const pageSize = ref(10) |
| | | |
| | | function getAvatarUrl(avatar?: string) { |
| | | if (!avatar) |
| | | return '' |
| | | |
| | | try { |
| | | const parsedAvatar = JSON.parse(avatar) |
| | | return parsedAvatar?.url || '' |
| | | } |
| | | catch { |
| | | return avatar |
| | | } |
| | | } |
| | | |
| | | // 表格列定义 |
| | | const columns = computed<DataTableColumns<StaffItem>>(() => [ |
| | | { |
| | |
| | | key: 'nickName', |
| | | width: 200, |
| | | render: (row) => { |
| | | const avatarUrl = getAvatarUrl(row.avatar) |
| | | return ( |
| | | <NSpace align="center" justify="flex-start"> |
| | | <NAvatar round size={32} src={row.avatar} fallback-src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png" style={{ backgroundColor: '#d0d0d0' }}></NAvatar> |
| | | {avatarUrl |
| | | ? ( |
| | | <NImage |
| | | src={avatarUrl} |
| | | width={32} |
| | | height={32} |
| | | objectFit="cover" |
| | | imgProps={{ style: { borderRadius: '50%' } }} |
| | | previewDisabled={false} |
| | | fallbackSrc="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png" |
| | | /> |
| | | ) |
| | | : ( |
| | | <div |
| | | class="flex justify-center items-center" |
| | | style={{ width: '32px', height: '32px', borderRadius: '50%', backgroundColor: '#F6F7FA', flexShrink: 0 }} |
| | | > |
| | | <NImage |
| | | src={defaultAvatarImg} |
| | | width={17} |
| | | height={21} |
| | | previewDisabled |
| | | /> |
| | | </div> |
| | | )} |
| | | <span>{row.nickName}</span> |
| | | </NSpace> |
| | | ) |
| | |
| | | :checked-value="1" |
| | | :unchecked-value="2" |
| | | :disabled="isAreaStatusSwitchDisabled" |
| | | class="text-13px" |
| | | class="text-13px flex-shrink-0" |
| | | > |
| | | <template #checked> 启用 </template> |
| | | <template #unchecked> 停用 </template> |
| | |
| | | position: absolute; |
| | | inset: 0; |
| | | } |
| | | .flex-shrink-0 { |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | :deep(.n-date-picker) { |
| | | height: 100%; |
| | |
| | | ref, |
| | | shallowRef, |
| | | watch, |
| | | } from 'vue' |
| | | import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router' |
| | | h, |
| | | } from "vue"; |
| | | import { onBeforeRouteLeave, useRoute, useRouter } from "vue-router"; |
| | | import { |
| | | checkPointNameDuplicate, |
| | | createPoint, |
| | |
| | | } |
| | | }) |
| | | |
| | | /** |
| | | * 根据围栏修改的影响范围,生成提交时的二次确认提示文案 |
| | | * @param disabledPointCount - 因移出围栏而将被停用的点位数量 |
| | | * @returns 拼接好的确认提示文案(返回一个渲染函数以支持高亮)。如果没有任何移出影响,则返回空字符串 |
| | | */ |
| | | function buildFenceSubmitConfirmMessage(disabledPointCount = 0) { |
| | | const pointCount = fenceImpactDeltaSummary.value.remove.pointCount |
| | | const stationCount = fenceImpactDeltaSummary.value.remove.stationCount |
| | | const segments: string[] = [] |
| | | if (pointCount && stationCount) |
| | | segments.push(`将移出${pointCount}个点位、${stationCount}个场站`) |
| | | else if (pointCount) |
| | | segments.push(`将移出${pointCount}个点位`) |
| | | else if (stationCount) |
| | | segments.push(`将移出${stationCount}个场站`) |
| | | if (disabledPointCount > 0) |
| | | segments.push(`其中${disabledPointCount}个点位会被停用`) |
| | | if (!segments.length) |
| | | return '' |
| | | return `确认提交吗?本次修改${segments.join(',')}。` |
| | | // 1. 从状态中获取本次修改将要移出的【点位数量】和【场站数量】 |
| | | const pointCount = fenceImpactDeltaSummary.value.remove.pointCount; |
| | | const stationCount = fenceImpactDeltaSummary.value.remove.stationCount; |
| | | |
| | | // 2. 如果没有任何点位和场站被移出,直接返回空字符串 |
| | | if (!pointCount && !stationCount) return ""; |
| | | |
| | | // 3. 返回一个渲染函数,使用 h 函数生成虚拟 DOM 以支持文案的高亮显示 |
| | | return () => { |
| | | const parts = []; |
| | | parts.push('因围栏调整,有 '); |
| | | |
| | | // 拼接点位和场站高亮片段 |
| | | if (pointCount > 0) { |
| | | parts.push(h('span', { style: 'color: #0068FF;' }, `${pointCount}个点位`)); |
| | | } |
| | | if (pointCount > 0 && stationCount > 0) { |
| | | parts.push('、'); |
| | | } |
| | | if (stationCount > 0) { |
| | | parts.push(h('span', { style: 'color: #0068FF;' }, `${stationCount}个场站`)); |
| | | } |
| | | |
| | | parts.push(' 将变更为“已停用”,确认发布本次修改?'); |
| | | return h('span', null, parts); |
| | | }; |
| | | } |
| | | |
| | | const fenceOutOfRangePointList = computed(() => { |
| | |
| | | const failureType = source.fenceFailureType ?? source.failureType |
| | | const failureTime = source.fenceFailureTime ?? source.failureTime |
| | | if ( |
| | | effectType === undefined |
| | | && effectTime === undefined |
| | | && failureType === undefined |
| | | && failureTime === undefined |
| | | effectType == null && |
| | | effectTime == null && |
| | | failureType == null && |
| | | failureTime == null |
| | | ) { |
| | | return null |
| | | } |
| | |
| | | } |
| | | |
| | | const stationOperationLifecycle = computed(() => { |
| | | if (!stationOperationArea.value) |
| | | return null |
| | | const detailLifecycle = normalizeStationLifecycleSource(stationDetailData.value) |
| | | if (detailLifecycle) |
| | | return detailLifecycle |
| | | return stationOperationLifecycleFallback.value |
| | | }) |
| | | const detailLifecycle = normalizeStationLifecycleSource(stationDetailData.value); |
| | | if (detailLifecycle) return detailLifecycle; |
| | | |
| | | if (!stationOperationArea.value) return null; |
| | | return stationOperationLifecycleFallback.value; |
| | | }); |
| | | |
| | | // 围栏空间关系判断统一收口在这里,便于保存前复用原围栏页的点位校验逻辑。 |
| | | function isPointInFencePath(point: LngLatTuple, path: LngLatTuple[]) { |
| | |
| | | |
| | | function openConfirmDialog( |
| | | title: string, |
| | | content = '', |
| | | content: string | (() => any) = "", |
| | | options?: { |
| | | positiveText?: string |
| | | negativeText?: string |
| | |
| | | |
| | | const userList = ref<any[]>([]); |
| | | |
| | | const userOptions = computed(() => |
| | | userList.value.map(item => ({ |
| | | label: item.nickName || "-", |
| | | value: item.id, |
| | | searchText: `${item.nickName || ""}`.toLowerCase(), |
| | | })), |
| | | ); |
| | | |
| | | function handleUserFilter(pattern: string, option: any) { |
| | | const keyword = pattern.trim().toLowerCase(); |
| | | if (!keyword) |
| | | return true; |
| | | |
| | | return option.searchText?.includes(keyword) ?? false; |
| | | } |
| | | |
| | | const getUserList = async () => { |
| | | const res = await getOperationsUserListNoPage(); |
| | | if (res.code === 200) { |
| | |
| | | <n-form-item label="选择处理人" path="handlerUserId"> |
| | | <n-select |
| | | v-model:value="formModel.handlerUserId" |
| | | :options="userList" |
| | | value-field="id" |
| | | label-field="nickName" |
| | | :options="userOptions" |
| | | filterable |
| | | clearable |
| | | placeholder="请输入姓名搜索" |
| | | :filter="handleUserFilter" |
| | | @update:show="val => isUserSelectOpen = val" |
| | | > |
| | | <template #arrow> |
| | |
| | | <script setup lang="tsx"> |
| | | import { computed, onMounted, ref, watch } from 'vue' |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { NButton, NSpace, NTag, NTooltip } from 'naive-ui' |
| | | import { NButton, NImage, NSpace, NTag, NTooltip } from 'naive-ui' |
| | | import { useBoolean } from '@/hooks' |
| | | import OperationLogModal from '@/components/custom/OperationLogModal.vue' |
| | | import { |
| | |
| | | 3: 'error', |
| | | } |
| | | |
| | | function getAvatarUrl(avatar?: string) { |
| | | if (!avatar) |
| | | return '' |
| | | |
| | | try { |
| | | const parsedAvatar = JSON.parse(avatar) |
| | | return parsedAvatar?.url || '' |
| | | } |
| | | catch { |
| | | return avatar |
| | | } |
| | | } |
| | | |
| | | const columns: DataTableColumns<any> = [ |
| | | { |
| | | type: 'selection', |
| | |
| | | width: 150, |
| | | resizable: true, |
| | | render: (row) => { |
| | | const avatarUrl = getAvatarUrl(row.avatar) |
| | | return ( |
| | | <div class="flex items-center gap-2"> |
| | | {row.avatar && <img src={JSON.parse(row.avatar).url} alt="头像" class="w-24px h-24px rounded-full" onError={(e: any) => { e.target.src = 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png' }} />} |
| | | {!row.avatar && ( |
| | | {avatarUrl |
| | | ? ( |
| | | <NImage |
| | | src={avatarUrl} |
| | | width={24} |
| | | height={24} |
| | | objectFit="cover" |
| | | imgProps={{ style: { borderRadius: '50%' } }} |
| | | previewDisabled={false} |
| | | fallbackSrc="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png" |
| | | /> |
| | | ) |
| | | : ( |
| | | <div class="w-24px h-24px rounded-full flex justify-center" style="background-color: #F6F7FA;"> |
| | | <img src={defaultAvatarImg} class="w-17px h-21px" /> |
| | | <NImage |
| | | src={defaultAvatarImg} |
| | | width={17} |
| | | height={21} |
| | | previewDisabled |
| | | /> |
| | | </div> |
| | | )} |
| | | <NTooltip trigger="hover" placement="top" max-width="500px"> |