Showing
27 changed files
with
777 additions
and
341 deletions
| ... | ... | @@ -10,6 +10,7 @@ import { FileUploadResponse } from '../../oem/model'; | 
| 10 | 10 | enum API { | 
| 11 | 11 | basicUrl = '/configuration/center', | 
| 12 | 12 | UPLOAD = '/oss/upload', | 
| 13 | + SHARE = '/configuration/center/share', | |
| 13 | 14 | } | 
| 14 | 15 | |
| 15 | 16 | export const getPage = (params: queryPageParams) => { | 
| ... | ... | @@ -49,3 +50,20 @@ export const saveOrUpdateConfigurationCenter = ( | 
| 49 | 50 | export const uploadThumbnail = (file: FormData) => { | 
| 50 | 51 | return defHttp.post<FileUploadResponse>({ url: API.UPLOAD, params: file }); | 
| 51 | 52 | }; | 
| 53 | + | |
| 54 | +export const shareConfiguration = (record: { | |
| 55 | + id: string; | |
| 56 | + isShare: boolean; | |
| 57 | + accessCredentials: string; | |
| 58 | +}) => { | |
| 59 | + const { accessCredentials, isShare, id } = record; | |
| 60 | + return defHttp.post( | |
| 61 | + { | |
| 62 | + url: `${API.SHARE}/${id}`, | |
| 63 | + params: { isShare, ...(accessCredentials ? { accessCredentials } : {}) }, | |
| 64 | + }, | |
| 65 | + { | |
| 66 | + joinParamsToUrl: true, | |
| 67 | + } | |
| 68 | + ); | |
| 69 | +}; | ... | ... | 
| ... | ... | @@ -22,6 +22,7 @@ enum DataBoardUrl { | 
| 22 | 22 | DELETE_DATA_BOARD = '/data_board', | 
| 23 | 23 | UPDATE_DATA_BOARD = '/data_board/update', | 
| 24 | 24 | UPDATE_DATA_BOARD_LAYOUT = '/data_board', | 
| 25 | + SHARE_DATA_BOARD = '/data_board/share', | |
| 25 | 26 | } | 
| 26 | 27 | |
| 27 | 28 | enum DataComponentUrl { | 
| ... | ... | @@ -232,3 +233,16 @@ export const getDeviceRelation = (params: { deviceId: string; isSlave: boolean } | 
| 232 | 233 | params, | 
| 233 | 234 | }); | 
| 234 | 235 | }; | 
| 236 | + | |
| 237 | +export const shareBoard = (record: { | |
| 238 | + id: string; | |
| 239 | + accessCredentials?: string; | |
| 240 | + isShare?: boolean; | |
| 241 | +}) => { | |
| 242 | + const { id, isShare, accessCredentials } = record; | |
| 243 | + return defHttp.post({ | |
| 244 | + url: `${DataBoardUrl.SHARE_DATA_BOARD}/${id}?isShare=${isShare}${ | |
| 245 | + accessCredentials ? `&accessCredentials=${accessCredentials}` : '' | |
| 246 | + }`, | |
| 247 | + }); | |
| 248 | +}; | ... | ... | 
| ... | ... | @@ -36,6 +36,10 @@ enum DeviceManagerApi { | 
| 36 | 36 | GATEWAY_DEVICE = '/device/gateway/list', | 
| 37 | 37 | |
| 38 | 38 | DEVICE_STATE_LOG_URL = '/device/state/log', | 
| 39 | + | |
| 40 | + DEVICE_PUBLIC = '/customer/public/device', | |
| 41 | + | |
| 42 | + DEVICE_PRIVATE = '/customer/device', | |
| 39 | 43 | } | 
| 40 | 44 | |
| 41 | 45 | export const devicePage = (params: DeviceQueryParam) => { | 
| ... | ... | @@ -308,3 +312,21 @@ export const deviceStateLogPost = (data) => { | 
| 308 | 312 | // } | 
| 309 | 313 | ); | 
| 310 | 314 | }; | 
| 315 | + | |
| 316 | +export const publicDevice = (tbDeviceId: string) => { | |
| 317 | + return defHttp.post( | |
| 318 | + { | |
| 319 | + url: `${DeviceManagerApi.DEVICE_PUBLIC}/${tbDeviceId}`, | |
| 320 | + }, | |
| 321 | + { joinPrefix: false } | |
| 322 | + ); | |
| 323 | +}; | |
| 324 | + | |
| 325 | +export const privateDevice = (tbDeviceId: string) => { | |
| 326 | + return defHttp.delete( | |
| 327 | + { | |
| 328 | + url: `${DeviceManagerApi.DEVICE_PRIVATE}/${tbDeviceId}`, | |
| 329 | + }, | |
| 330 | + { joinPrefix: false } | |
| 331 | + ); | |
| 332 | +}; | ... | ... | 
src/api/sys/model/shareModel.ts
0 → 100644
src/api/sys/share.ts
0 → 100644
| 1 | +import { defHttp } from '/@/utils/http/axios'; | |
| 2 | +import { ViewTypeEnum } from '/@/views/sys/share/config/config'; | |
| 3 | + | |
| 4 | +enum Api { | |
| 5 | + CHECK = '/share/check', | |
| 6 | + PUBLIC_LOGIN = '/auth/login/public', | |
| 7 | + SHARE_CONTENT = '/share', | |
| 8 | +} | |
| 9 | + | |
| 10 | +export const checkShareAccessToken = (type: ViewTypeEnum, id: string) => { | |
| 11 | + return defHttp.get<Record<'data', boolean>>({ | |
| 12 | + url: `${Api.CHECK}/${type}/${id}`, | |
| 13 | + }); | |
| 14 | +}; | |
| 15 | + | |
| 16 | +export const sharePageLogin = (publicId: string) => { | |
| 17 | + return defHttp.post<Record<'token' | 'refreshToken', string>>( | |
| 18 | + { | |
| 19 | + url: Api.PUBLIC_LOGIN, | |
| 20 | + data: { publicId }, | |
| 21 | + }, | |
| 22 | + { | |
| 23 | + joinPrefix: false, | |
| 24 | + } | |
| 25 | + ); | |
| 26 | +}; | |
| 27 | + | |
| 28 | +export const getShareContent = (record: Record<'accessCredentials' | 'id', string>) => { | |
| 29 | + const { id, accessCredentials } = record; | |
| 30 | + return defHttp.get({ | |
| 31 | + url: `${Api.SHARE_CONTENT}/${ViewTypeEnum.DATA_BOARD}/share_data/${id}`, | |
| 32 | + params: { accessCredentials }, | |
| 33 | + }); | |
| 34 | +}; | ... | ... | 
| ... | ... | @@ -10,7 +10,8 @@ import { getAuthCache } from '/@/utils/auth'; | 
| 10 | 10 | |
| 11 | 11 | const LOGIN_PATH = PageEnum.BASE_LOGIN; | 
| 12 | 12 | const ROOT_PATH = RootRoute.path; | 
| 13 | -const whitePathList: string[] = [LOGIN_PATH]; | |
| 13 | +const SHARE_PATH = PageEnum.SHARE_PAGE; | |
| 14 | +const whitePathList: string[] = [LOGIN_PATH, SHARE_PATH]; | |
| 14 | 15 | // const userInfo1 = getAuthCache(USER_INFO_KEY); | 
| 15 | 16 | // const userInfo = ref(userInfo1); | 
| 16 | 17 | |
| ... | ... | @@ -32,7 +33,7 @@ export function createPermissionGuard(router: Router) { | 
| 32 | 33 | const token = userStore.getJwtToken; | 
| 33 | 34 | // Whitelist can be directly entered | 
| 34 | 35 | // 路由守卫拦截, 如果是已经登陆情况, 就不要回到登陆页面了; | 
| 35 | - if (whitePathList.includes(to.path as PageEnum)) { | |
| 36 | + if (to.matched.find((item) => whitePathList.includes(item.path))) { | |
| 36 | 37 | if (to.path === LOGIN_PATH && token) { | 
| 37 | 38 | const isSessionTimeout = userStore.getSessionTimeout; | 
| 38 | 39 | try { | ... | ... | 
| ... | ... | @@ -4,6 +4,7 @@ import { mainOutRoutes } from './mainOut'; | 
| 4 | 4 | import { PageEnum } from '/@/enums/pageEnum'; | 
| 5 | 5 | import { t } from '/@/hooks/web/useI18n'; | 
| 6 | 6 | import { LAYOUT } from '../constant'; | 
| 7 | +import { PUBLIC_PAGE_ROUTER } from './public'; | |
| 7 | 8 | |
| 8 | 9 | const modules = import.meta.globEager('./modules/**/*.ts'); | 
| 9 | 10 | const routeModuleList: AppRouteModule[] = []; | 
| ... | ... | @@ -86,4 +87,5 @@ export const basicRoutes = [ | 
| 86 | 87 | REDIRECT_ROUTE, | 
| 87 | 88 | PAGE_NOT_FOUND_ROUTE, | 
| 88 | 89 | DATA_BOARD_SHARE, | 
| 90 | + PUBLIC_PAGE_ROUTER, | |
| 89 | 91 | ]; | ... | ... | 
src/router/routes/public.ts
0 → 100644
| 1 | +import { AppRouteRecordRaw } from '../types'; | |
| 2 | +import { PageEnum } from '/@/enums/pageEnum'; | |
| 3 | + | |
| 4 | +export const PUBLIC_PAGE_ROUTER: AppRouteRecordRaw = { | |
| 5 | + path: PageEnum.SHARE_PAGE, | |
| 6 | + name: 'publicPage', | |
| 7 | + component: () => import('/@/views/sys/share/index.vue'), | |
| 8 | + meta: { | |
| 9 | + title: '公开', | |
| 10 | + hideBreadcrumb: true, | |
| 11 | + hideChildrenInMenu: true, | |
| 12 | + }, | |
| 13 | +}; | ... | ... | 
src/views/common/ShareModal/config/index.ts
0 → 100644
| 1 | +import { FormSchema } from '/@/components/Form'; | |
| 2 | + | |
| 3 | +export enum ViewTypeEnum { | |
| 4 | + PRIVATE_VIEW = 'PRIVATE_VIEW', | |
| 5 | + PUBLIC_VIEW = 'PUBLIC_VIEW', | |
| 6 | +} | |
| 7 | + | |
| 8 | +export enum ViewTypeNameEnum { | |
| 9 | + PRIVATE_VIEW = '私有', | |
| 10 | + PUBLIC_VIEW = '公共', | |
| 11 | +} | |
| 12 | + | |
| 13 | +export enum FieldsEnum { | |
| 14 | + IS_SHARE = 'isShare', | |
| 15 | + ACCESS_CREDENTIALS = 'accessCredentials', | |
| 16 | +} | |
| 17 | + | |
| 18 | +export const schemas: FormSchema[] = [ | |
| 19 | + { | |
| 20 | + field: FieldsEnum.IS_SHARE, | |
| 21 | + label: '公开性', | |
| 22 | + component: 'Switch', | |
| 23 | + defaultValue: false, | |
| 24 | + }, | |
| 25 | + { | |
| 26 | + field: FieldsEnum.ACCESS_CREDENTIALS, | |
| 27 | + label: '访问凭证', | |
| 28 | + component: 'Input', | |
| 29 | + ifShow: ({ model }) => model[FieldsEnum.IS_SHARE], | |
| 30 | + componentProps: { | |
| 31 | + maxLength: 64, | |
| 32 | + }, | |
| 33 | + }, | |
| 34 | +]; | ... | ... | 
src/views/common/ShareModal/index.ts
0 → 100644
| 1 | +export { default as ShareModal } from './index.vue'; | ... | ... | 
src/views/common/ShareModal/index.vue
0 → 100644
| 1 | +<script lang="ts" setup> | |
| 2 | + import { BasicForm, useForm } from '/@/components/Form'; | |
| 3 | + import { BasicModal, useModalInner } from '/@/components/Modal'; | |
| 4 | + import { FieldsEnum, schemas, ViewTypeEnum } from './config'; | |
| 5 | + import { DataBoardRecord } from '/@/api/dataBoard/model'; | |
| 6 | + import { Alert } from 'ant-design-vue'; | |
| 7 | + import { ref, unref } from 'vue'; | |
| 8 | + import { nextTick } from 'vue'; | |
| 9 | + | |
| 10 | + const props = defineProps<{ | |
| 11 | + shareApi?: (...args: any[]) => Promise<any>; | |
| 12 | + }>(); | |
| 13 | + | |
| 14 | + const loading = ref(false); | |
| 15 | + const record = ref<DataBoardRecord>({} as unknown as DataBoardRecord); | |
| 16 | + | |
| 17 | + const [register, { closeModal }] = useModalInner(async (data: DataBoardRecord) => { | |
| 18 | + record.value = data; | |
| 19 | + await nextTick(); | |
| 20 | + setFieldsValue({ | |
| 21 | + [FieldsEnum.IS_SHARE]: data.viewType === ViewTypeEnum.PUBLIC_VIEW, | |
| 22 | + [FieldsEnum.ACCESS_CREDENTIALS]: data.accessCredentials, | |
| 23 | + }); | |
| 24 | + }); | |
| 25 | + | |
| 26 | + const [registerForm, { getFieldsValue, setFieldsValue }] = useForm({ | |
| 27 | + schemas, | |
| 28 | + showActionButtonGroup: false, | |
| 29 | + layout: 'inline', | |
| 30 | + labelWidth: 120, | |
| 31 | + }); | |
| 32 | + | |
| 33 | + const emit = defineEmits(['register', 'success']); | |
| 34 | + | |
| 35 | + const handleSubmit = async () => { | |
| 36 | + try { | |
| 37 | + loading.value = true; | |
| 38 | + const value = getFieldsValue(); | |
| 39 | + const { id } = unref(record); | |
| 40 | + await props?.shareApi?.({ id, ...value }); | |
| 41 | + closeModal(); | |
| 42 | + emit('success'); | |
| 43 | + } finally { | |
| 44 | + loading.value = false; | |
| 45 | + } | |
| 46 | + }; | |
| 47 | +</script> | |
| 48 | + | |
| 49 | +<template> | |
| 50 | + <BasicModal title="分享" @register="register" @ok="handleSubmit" :okButtonProps="{ loading }"> | |
| 51 | + <Alert | |
| 52 | + class="!mb-4" | |
| 53 | + type="info" | |
| 54 | + message="私有视图只有项目成员可以浏览,公开视图拥有一个公开的 URL,任何人无需登录即可浏览." | |
| 55 | + /> | |
| 56 | + <BasicForm @register="registerForm" /> | |
| 57 | + </BasicModal> | |
| 58 | +</template> | ... | ... | 
| ... | ... | @@ -15,6 +15,8 @@ export enum ConfigurationPermission { | 
| 15 | 15 | DELETE = 'api:yt:configuration:center:delete', | 
| 16 | 16 | DESIGN = 'api:yt:configuration:center:get_configuration_info:design', | 
| 17 | 17 | PREVIEW = 'api:yt:configuration:center:get_configuration_info:preview', | 
| 18 | + SHARE = 'api:yt:configuration:center:share', | |
| 19 | + UN_SHARE = 'api:yt:configuration:center:monopoly', | |
| 18 | 20 | } | 
| 19 | 21 | |
| 20 | 22 | export const PC_DEFAULT_CONTENT = | ... | ... | 
| 1 | -<script setup lang="ts"> | |
| 2 | - import { List, Card, Button, PaginationProps, Tooltip } from 'ant-design-vue'; | |
| 3 | - import { ReloadOutlined } from '@ant-design/icons-vue'; | |
| 4 | - import { computed, onMounted, reactive, ref, unref } from 'vue'; | |
| 5 | - import { OrganizationIdTree, useResetOrganizationTree } from '../../common/organizationIdTree'; | |
| 6 | - import { | |
| 7 | - deleteConfigurationCenter, | |
| 8 | - getPage, | |
| 9 | - } from '/@/api/configuration/center/configurationCenter'; | |
| 10 | - import { ConfigurationCenterItemsModal } from '/@/api/configuration/center/model/configurationCenterModal'; | |
| 11 | - import { PageWrapper } from '/@/components/Page'; | |
| 12 | - import { BasicForm, useForm } from '/@/components/Form'; | |
| 13 | - import { ConfigurationPermission, searchFormSchema } from './center.data'; | |
| 14 | - import { useMessage } from '/@/hooks/web/useMessage'; | |
| 15 | - import { Authority } from '/@/components/Authority'; | |
| 16 | - import { isDevMode } from '/@/utils/env'; | |
| 17 | - import ConfigurationCenterDrawer from './ConfigurationCenterDrawer.vue'; | |
| 18 | - import { useDrawer } from '/@/components/Drawer'; | |
| 19 | - import { getBoundingClientRect } from '/@/utils/domUtils'; | |
| 20 | - import configurationSrc from '/@/assets/icons/configuration.svg'; | |
| 21 | - import { cloneDeep } from 'lodash'; | |
| 22 | - import { usePermission } from '/@/hooks/web/usePermission'; | |
| 23 | - import { useGlobSetting } from '/@/hooks/setting'; | |
| 24 | - import { AuthIcon, CardLayoutButton } from '/@/components/Widget'; | |
| 25 | - import AuthDropDown from '/@/components/Widget/AuthDropDown.vue'; | |
| 26 | - | |
| 27 | - const listColumn = ref(5); | |
| 28 | - | |
| 29 | - const { createMessage } = useMessage(); | |
| 30 | - | |
| 31 | - const organizationId = ref<Nullable<number>>(null); | |
| 32 | - | |
| 33 | - const pagination = reactive<PaginationProps>({ | |
| 34 | - size: 'small', | |
| 35 | - showTotal: (total: number) => `共 ${total} 条数据`, | |
| 36 | - current: 1, | |
| 37 | - pageSize: unref(listColumn) * 2, | |
| 38 | - onChange: (page: number) => { | |
| 39 | - pagination.current = page; | |
| 40 | - getListData(); | |
| 41 | - }, | |
| 42 | - }); | |
| 43 | - | |
| 44 | - const loading = ref(false); | |
| 45 | - | |
| 46 | - const dataSource = ref<ConfigurationCenterItemsModal[]>([]); | |
| 47 | - | |
| 48 | - const [registerForm, { getFieldsValue }] = useForm({ | |
| 49 | - schemas: searchFormSchema, | |
| 50 | - showAdvancedButton: true, | |
| 51 | - labelWidth: 100, | |
| 52 | - compact: true, | |
| 53 | - resetFunc: () => { | |
| 54 | - resetFn(); | |
| 55 | - organizationId.value = null; | |
| 56 | - return getListData(); | |
| 57 | - }, | |
| 58 | - submitFunc: async () => { | |
| 59 | - const value = getFieldsValue(); | |
| 60 | - getListData(value); | |
| 61 | - }, | |
| 62 | - }); | |
| 63 | - | |
| 64 | - async function getListData(value: Recordable = {}) { | |
| 65 | - try { | |
| 66 | - loading.value = true; | |
| 67 | - const pageSize = unref(listColumn) * 2; | |
| 68 | - const { items, total } = await getPage({ | |
| 69 | - organizationId: unref(organizationId), | |
| 70 | - ...value, | |
| 71 | - page: pagination.current!, | |
| 72 | - pageSize, | |
| 73 | - }); | |
| 74 | - | |
| 75 | - dataSource.value = items; | |
| 76 | - Object.assign(pagination, { total, pageSize }); | |
| 77 | - } catch (error) { | |
| 78 | - } finally { | |
| 79 | - loading.value = false; | |
| 80 | - } | |
| 81 | - } | |
| 82 | - | |
| 83 | - onMounted(() => { | |
| 84 | - getListData(); | |
| 85 | - }); | |
| 86 | - | |
| 87 | - const searchInfo = reactive<Recordable>({}); | |
| 88 | - const { organizationIdTreeRef, resetFn } = useResetOrganizationTree(searchInfo); | |
| 89 | - const handleSelect = (orgId: number) => { | |
| 90 | - organizationId.value = orgId; | |
| 91 | - getListData(); | |
| 92 | - }; | |
| 93 | - | |
| 94 | - const [registerDrawer, { openDrawer }] = useDrawer(); | |
| 95 | - | |
| 96 | - const { hasPermission } = usePermission(); | |
| 97 | - | |
| 98 | - const getPreviewFlag = computed(() => { | |
| 99 | - return hasPermission(ConfigurationPermission.PREVIEW); | |
| 100 | - }); | |
| 101 | - | |
| 102 | - const getDesignFlag = computed(() => { | |
| 103 | - return hasPermission(ConfigurationPermission.DESIGN); | |
| 104 | - }); | |
| 105 | - | |
| 106 | - const handleCreateOrUpdate = (record?: ConfigurationCenterItemsModal) => { | |
| 107 | - if (record) { | |
| 108 | - openDrawer(true, { | |
| 109 | - isUpdate: true, | |
| 110 | - record: cloneDeep(record), | |
| 111 | - }); | |
| 112 | - } else { | |
| 113 | - openDrawer(true, { | |
| 114 | - isUpdate: false, | |
| 115 | - }); | |
| 116 | - } | |
| 117 | - }; | |
| 118 | - | |
| 119 | - const { configurationPrefix } = useGlobSetting(); | |
| 120 | - const isDev = isDevMode(); | |
| 121 | - | |
| 122 | - const handlePreview = (record: ConfigurationCenterItemsModal) => { | |
| 123 | - if (!unref(getPreviewFlag)) return; | |
| 124 | - window.open( | |
| 125 | - `${configurationPrefix}/${isDev ? '?dev=1&' : '?'}configurationId=${record!.id}&lightbox=1` | |
| 126 | - ); | |
| 127 | - }; | |
| 128 | - | |
| 129 | - const handleDesign = (record: ConfigurationCenterItemsModal) => { | |
| 130 | - if (!unref(getDesignFlag)) return; | |
| 131 | - window.open(`${configurationPrefix}/${isDev ? '?dev=1&' : '?'}configurationId=${record!.id}`); | |
| 132 | - }; | |
| 133 | - | |
| 134 | - const handleDelete = async (record: ConfigurationCenterItemsModal) => { | |
| 135 | - try { | |
| 136 | - await deleteConfigurationCenter([record.id]); | |
| 137 | - createMessage.success('删除成功'); | |
| 138 | - await getListData(); | |
| 139 | - } catch (error) {} | |
| 140 | - }; | |
| 141 | - | |
| 142 | - const handleCardLayoutChange = () => { | |
| 143 | - pagination.current = 1; | |
| 144 | - getListData(); | |
| 145 | - }; | |
| 146 | - | |
| 147 | - const listEl = ref<Nullable<ComponentElRef>>(null); | |
| 148 | - | |
| 149 | - onMounted(() => { | |
| 150 | - const clientHeight = document.documentElement.clientHeight; | |
| 151 | - const rect = getBoundingClientRect(unref(listEl)!.$el!) as DOMRect; | |
| 152 | - // margin-top 24 height 24 | |
| 153 | - const paginationHeight = 24 + 24 + 8; | |
| 154 | - // list pading top 8 maring-top 8 extra slot 56 | |
| 155 | - const listContainerMarginBottom = 8 + 8 + 56; | |
| 156 | - const listContainerHeight = | |
| 157 | - clientHeight - rect.top - paginationHeight - listContainerMarginBottom; | |
| 158 | - const listContainerEl = (unref(listEl)!.$el as HTMLElement).querySelector( | |
| 159 | - '.ant-spin-container' | |
| 160 | - ) as HTMLElement; | |
| 161 | - listContainerEl && | |
| 162 | - (listContainerEl.style.height = listContainerHeight + 'px') && | |
| 163 | - (listContainerEl.style.overflowY = 'auto') && | |
| 164 | - (listContainerEl.style.overflowX = 'hidden'); | |
| 165 | - }); | |
| 166 | -</script> | |
| 167 | - | |
| 168 | -<template> | |
| 169 | - <PageWrapper dense contentFullHeight contentClass="flex"> | |
| 170 | - <OrganizationIdTree @select="handleSelect" ref="organizationIdTreeRef" /> | |
| 171 | - <section class="flex-auto p-4 w-3/4 xl:w-4/5 w-full configuration-list"> | |
| 172 | - <div class="flex-auto w-full bg-light-50 dark:bg-dark-900 p-4"> | |
| 173 | - <BasicForm @register="registerForm" /> | |
| 174 | - </div> | |
| 175 | - <List | |
| 176 | - ref="listEl" | |
| 177 | - :loading="loading" | |
| 178 | - class="flex-auto bg-light-50 dark:bg-dark-900 !p-2 !mt-4" | |
| 179 | - position="bottom" | |
| 180 | - :pagination="pagination" | |
| 181 | - :data-source="dataSource" | |
| 182 | - :grid="{ gutter: 4, column: listColumn }" | |
| 183 | - > | |
| 184 | - <template #header> | |
| 185 | - <div class="flex gap-3 justify-end"> | |
| 186 | - <Authority :value="ConfigurationPermission.CREATE"> | |
| 187 | - <Button type="primary" @click="handleCreateOrUpdate()">新增组态</Button> | |
| 188 | - </Authority> | |
| 189 | - <CardLayoutButton v-model:value="listColumn" @change="handleCardLayoutChange" /> | |
| 190 | - <Tooltip title="刷新"> | |
| 191 | - <Button type="primary" @click="getListData"> | |
| 192 | - <ReloadOutlined /> | |
| 193 | - </Button> | |
| 194 | - </Tooltip> | |
| 195 | - </div> | |
| 196 | - </template> | |
| 197 | - <template #renderItem="{ item }"> | |
| 198 | - <List.Item> | |
| 199 | - <Card hoverable> | |
| 200 | - <template #cover> | |
| 201 | - <div class="h-full w-full !flex justify-center items-center text-center p-1"> | |
| 202 | - <img | |
| 203 | - class="w-full h-36" | |
| 204 | - alt="example" | |
| 205 | - :src="item.thumbnail || configurationSrc" | |
| 206 | - @click="handlePreview(item)" | |
| 207 | - /> | |
| 208 | - </div> | |
| 209 | - </template> | |
| 210 | - <template class="ant-card-actions" #actions> | |
| 211 | - <Tooltip title="预览"> | |
| 212 | - <AuthIcon | |
| 213 | - :auth="ConfigurationPermission.PREVIEW" | |
| 214 | - class="!text-lg" | |
| 215 | - icon="ant-design:eye-outlined" | |
| 216 | - @click="handlePreview(item)" | |
| 217 | - /> | |
| 218 | - </Tooltip> | |
| 219 | - <Tooltip title="设计"> | |
| 220 | - <AuthIcon | |
| 221 | - :auth="ConfigurationPermission.DESIGN" | |
| 222 | - class="!text-lg" | |
| 223 | - icon="ant-design:edit-outlined" | |
| 224 | - @click="handleDesign(item)" | |
| 225 | - /> | |
| 226 | - </Tooltip> | |
| 227 | - <AuthDropDown | |
| 228 | - :dropMenuList="[ | |
| 229 | - { | |
| 230 | - text: '编辑', | |
| 231 | - auth: ConfigurationPermission.UPDATE, | |
| 232 | - icon: 'clarity:note-edit-line', | |
| 233 | - event: '', | |
| 234 | - onClick: handleCreateOrUpdate.bind(null, item), | |
| 235 | - }, | |
| 236 | - { | |
| 237 | - text: '删除', | |
| 238 | - auth: ConfigurationPermission.DELETE, | |
| 239 | - icon: 'ant-design:delete-outlined', | |
| 240 | - event: '', | |
| 241 | - popconfirm: { | |
| 242 | - title: '是否确认删除操作?', | |
| 243 | - onConfirm: handleDelete.bind(null, item), | |
| 244 | - }, | |
| 245 | - }, | |
| 246 | - ]" | |
| 247 | - :trigger="['hover']" | |
| 248 | - /> | |
| 249 | - </template> | |
| 250 | - <Card.Meta> | |
| 251 | - <template #title> | |
| 252 | - <span class="truncate">{{ item.name }}</span> | |
| 253 | - </template> | |
| 254 | - <template #description> | |
| 255 | - <div class="truncate h-11"> | |
| 256 | - <div class="truncate">{{ item.organizationDTO.name }}</div> | |
| 257 | - <div class="truncate">{{ item.remark || '' }} </div> | |
| 258 | - </div> | |
| 259 | - </template> | |
| 260 | - </Card.Meta> | |
| 261 | - </Card> | |
| 262 | - </List.Item> | |
| 263 | - </template> | |
| 264 | - </List> | |
| 265 | - </section> | |
| 266 | - <ConfigurationCenterDrawer @register="registerDrawer" @success="getListData" /> | |
| 267 | - </PageWrapper> | |
| 268 | -</template> | |
| 269 | - | |
| 270 | -<style lang="less" scoped> | |
| 271 | - .configuration-list:deep(.ant-list-header) { | |
| 272 | - border-bottom: none !important; | |
| 273 | - } | |
| 274 | - | |
| 275 | - .configuration-list:deep(.ant-list-pagination) { | |
| 276 | - height: 24px; | |
| 277 | - } | |
| 278 | - | |
| 279 | - .configuration-list:deep(.ant-card-body) { | |
| 280 | - padding: 16px !important; | |
| 281 | - } | |
| 282 | - | |
| 283 | - .configuration-list:deep(.ant-list-empty-text) { | |
| 284 | - @apply w-full h-full flex justify-center items-center; | |
| 285 | - } | |
| 286 | -</style> | |
| 1 | +<script setup lang="ts"> | |
| 2 | + import { List, Card, Button, PaginationProps, Tooltip } from 'ant-design-vue'; | |
| 3 | + import { ReloadOutlined } from '@ant-design/icons-vue'; | |
| 4 | + import { computed, onMounted, reactive, ref, unref } from 'vue'; | |
| 5 | + import { OrganizationIdTree, useResetOrganizationTree } from '../../common/organizationIdTree'; | |
| 6 | + import { | |
| 7 | + deleteConfigurationCenter, | |
| 8 | + getPage, | |
| 9 | + shareConfiguration, | |
| 10 | + } from '/@/api/configuration/center/configurationCenter'; | |
| 11 | + import { ConfigurationCenterItemsModal } from '/@/api/configuration/center/model/configurationCenterModal'; | |
| 12 | + import { PageWrapper } from '/@/components/Page'; | |
| 13 | + import { BasicForm, useForm } from '/@/components/Form'; | |
| 14 | + import { ConfigurationPermission, searchFormSchema } from './center.data'; | |
| 15 | + import { useMessage } from '/@/hooks/web/useMessage'; | |
| 16 | + import { Authority } from '/@/components/Authority'; | |
| 17 | + import { isDevMode } from '/@/utils/env'; | |
| 18 | + import ConfigurationCenterDrawer from './ConfigurationCenterDrawer.vue'; | |
| 19 | + import { useDrawer } from '/@/components/Drawer'; | |
| 20 | + import { getBoundingClientRect } from '/@/utils/domUtils'; | |
| 21 | + import configurationSrc from '/@/assets/icons/configuration.svg'; | |
| 22 | + import { cloneDeep } from 'lodash'; | |
| 23 | + import { usePermission } from '/@/hooks/web/usePermission'; | |
| 24 | + import { useGlobSetting } from '/@/hooks/setting'; | |
| 25 | + import { AuthIcon, CardLayoutButton } from '/@/components/Widget'; | |
| 26 | + import AuthDropDown from '/@/components/Widget/AuthDropDown.vue'; | |
| 27 | + import { ShareModal } from '/@/views/common/ShareModal'; | |
| 28 | + import { ViewTypeNameEnum } from '../../common/ShareModal/config'; | |
| 29 | + import { useModal } from '/@/components/Modal'; | |
| 30 | + import { useCopyToClipboard } from '/@/hooks/web/useCopyToClipboard'; | |
| 31 | + | |
| 32 | + const listColumn = ref(5); | |
| 33 | + | |
| 34 | + const { createMessage } = useMessage(); | |
| 35 | + | |
| 36 | + const organizationId = ref<Nullable<number>>(null); | |
| 37 | + | |
| 38 | + const pagination = reactive<PaginationProps>({ | |
| 39 | + size: 'small', | |
| 40 | + showTotal: (total: number) => `共 ${total} 条数据`, | |
| 41 | + current: 1, | |
| 42 | + pageSize: unref(listColumn) * 2, | |
| 43 | + onChange: (page: number) => { | |
| 44 | + pagination.current = page; | |
| 45 | + getListData(); | |
| 46 | + }, | |
| 47 | + }); | |
| 48 | + | |
| 49 | + const loading = ref(false); | |
| 50 | + | |
| 51 | + const dataSource = ref<ConfigurationCenterItemsModal[]>([]); | |
| 52 | + | |
| 53 | + const [registerForm, { getFieldsValue }] = useForm({ | |
| 54 | + schemas: searchFormSchema, | |
| 55 | + showAdvancedButton: true, | |
| 56 | + labelWidth: 100, | |
| 57 | + compact: true, | |
| 58 | + resetFunc: () => { | |
| 59 | + resetFn(); | |
| 60 | + organizationId.value = null; | |
| 61 | + return getListData(); | |
| 62 | + }, | |
| 63 | + submitFunc: async () => { | |
| 64 | + const value = getFieldsValue(); | |
| 65 | + getListData(value); | |
| 66 | + }, | |
| 67 | + }); | |
| 68 | + | |
| 69 | + async function getListData(value: Recordable = {}) { | |
| 70 | + try { | |
| 71 | + loading.value = true; | |
| 72 | + const pageSize = unref(listColumn) * 2; | |
| 73 | + const { items, total } = await getPage({ | |
| 74 | + organizationId: unref(organizationId), | |
| 75 | + ...value, | |
| 76 | + page: pagination.current!, | |
| 77 | + pageSize, | |
| 78 | + }); | |
| 79 | + | |
| 80 | + dataSource.value = items; | |
| 81 | + Object.assign(pagination, { total, pageSize }); | |
| 82 | + } catch (error) { | |
| 83 | + } finally { | |
| 84 | + loading.value = false; | |
| 85 | + } | |
| 86 | + } | |
| 87 | + | |
| 88 | + onMounted(() => { | |
| 89 | + getListData(); | |
| 90 | + }); | |
| 91 | + | |
| 92 | + const searchInfo = reactive<Recordable>({}); | |
| 93 | + const { organizationIdTreeRef, resetFn } = useResetOrganizationTree(searchInfo); | |
| 94 | + const handleSelect = (orgId: number) => { | |
| 95 | + organizationId.value = orgId; | |
| 96 | + getListData(); | |
| 97 | + }; | |
| 98 | + | |
| 99 | + const [registerDrawer, { openDrawer }] = useDrawer(); | |
| 100 | + | |
| 101 | + const { hasPermission } = usePermission(); | |
| 102 | + | |
| 103 | + const getPreviewFlag = computed(() => { | |
| 104 | + return hasPermission(ConfigurationPermission.PREVIEW); | |
| 105 | + }); | |
| 106 | + | |
| 107 | + const getDesignFlag = computed(() => { | |
| 108 | + return hasPermission(ConfigurationPermission.DESIGN); | |
| 109 | + }); | |
| 110 | + | |
| 111 | + const getShareFlag = computed(() => { | |
| 112 | + return hasPermission(ConfigurationPermission.SHARE); | |
| 113 | + }); | |
| 114 | + | |
| 115 | + const handleCreateOrUpdate = (record?: ConfigurationCenterItemsModal) => { | |
| 116 | + if (record) { | |
| 117 | + openDrawer(true, { | |
| 118 | + isUpdate: true, | |
| 119 | + record: cloneDeep(record), | |
| 120 | + }); | |
| 121 | + } else { | |
| 122 | + openDrawer(true, { | |
| 123 | + isUpdate: false, | |
| 124 | + }); | |
| 125 | + } | |
| 126 | + }; | |
| 127 | + | |
| 128 | + const { configurationPrefix } = useGlobSetting(); | |
| 129 | + const isDev = isDevMode(); | |
| 130 | + | |
| 131 | + const handlePreview = (record: ConfigurationCenterItemsModal) => { | |
| 132 | + if (!unref(getPreviewFlag)) return; | |
| 133 | + window.open( | |
| 134 | + `${configurationPrefix}/${isDev ? '?dev=1&' : '?'}configurationId=${record!.id}&lightbox=1` | |
| 135 | + ); | |
| 136 | + }; | |
| 137 | + | |
| 138 | + const handleDesign = (record: ConfigurationCenterItemsModal) => { | |
| 139 | + if (!unref(getDesignFlag)) return; | |
| 140 | + window.open(`${configurationPrefix}/${isDev ? '?dev=1&' : '?'}configurationId=${record!.id}`); | |
| 141 | + }; | |
| 142 | + | |
| 143 | + const handleDelete = async (record: ConfigurationCenterItemsModal) => { | |
| 144 | + try { | |
| 145 | + await deleteConfigurationCenter([record.id]); | |
| 146 | + createMessage.success('删除成功'); | |
| 147 | + await getListData(); | |
| 148 | + } catch (error) {} | |
| 149 | + }; | |
| 150 | + | |
| 151 | + const handleCardLayoutChange = () => { | |
| 152 | + pagination.current = 1; | |
| 153 | + getListData(); | |
| 154 | + }; | |
| 155 | + | |
| 156 | + const { clipboardRef, isSuccessRef } = useCopyToClipboard(); | |
| 157 | + const handleCreateShareUrl = (record: ConfigurationCenterItemsModal) => { | |
| 158 | + if (!unref(getShareFlag)) return; | |
| 159 | + const { origin } = location; | |
| 160 | + const searchParams = new URLSearchParams(); | |
| 161 | + isDev && searchParams.set('dev', '1'); | |
| 162 | + searchParams.set('share', 'SCADA'); | |
| 163 | + searchParams.set('configurationId', record.id); | |
| 164 | + searchParams.set('publicId', record.publicId || ''); | |
| 165 | + searchParams.set('lightbox', '1'); | |
| 166 | + const url = `${origin}${configurationPrefix}/?${searchParams.toString()}`; | |
| 167 | + clipboardRef.value = url; | |
| 168 | + if (unref(isSuccessRef)) { | |
| 169 | + createMessage.success('复制成功~'); | |
| 170 | + } | |
| 171 | + }; | |
| 172 | + | |
| 173 | + const [registerShareModal, { openModal }] = useModal(); | |
| 174 | + | |
| 175 | + const handleOpenShareModal = (record: ConfigurationCenterItemsModal) => { | |
| 176 | + openModal(true, record); | |
| 177 | + }; | |
| 178 | + | |
| 179 | + const listEl = ref<Nullable<ComponentElRef>>(null); | |
| 180 | + | |
| 181 | + onMounted(() => { | |
| 182 | + const clientHeight = document.documentElement.clientHeight; | |
| 183 | + const rect = getBoundingClientRect(unref(listEl)!.$el!) as DOMRect; | |
| 184 | + // margin-top 24 height 24 | |
| 185 | + const paginationHeight = 24 + 24 + 8; | |
| 186 | + // list pading top 8 maring-top 8 extra slot 56 | |
| 187 | + const listContainerMarginBottom = 8 + 8 + 56; | |
| 188 | + const listContainerHeight = | |
| 189 | + clientHeight - rect.top - paginationHeight - listContainerMarginBottom; | |
| 190 | + const listContainerEl = (unref(listEl)!.$el as HTMLElement).querySelector( | |
| 191 | + '.ant-spin-container' | |
| 192 | + ) as HTMLElement; | |
| 193 | + listContainerEl && | |
| 194 | + (listContainerEl.style.height = listContainerHeight + 'px') && | |
| 195 | + (listContainerEl.style.overflowY = 'auto') && | |
| 196 | + (listContainerEl.style.overflowX = 'hidden'); | |
| 197 | + }); | |
| 198 | +</script> | |
| 199 | + | |
| 200 | +<template> | |
| 201 | + <PageWrapper dense contentFullHeight contentClass="flex"> | |
| 202 | + <OrganizationIdTree @select="handleSelect" ref="organizationIdTreeRef" /> | |
| 203 | + <section class="flex-auto p-4 w-3/4 xl:w-4/5 w-full configuration-list"> | |
| 204 | + <div class="flex-auto w-full bg-light-50 dark:bg-dark-900 p-4"> | |
| 205 | + <BasicForm @register="registerForm" /> | |
| 206 | + </div> | |
| 207 | + <List | |
| 208 | + ref="listEl" | |
| 209 | + :loading="loading" | |
| 210 | + class="flex-auto bg-light-50 dark:bg-dark-900 !p-2 !mt-4" | |
| 211 | + position="bottom" | |
| 212 | + :pagination="pagination" | |
| 213 | + :data-source="dataSource" | |
| 214 | + :grid="{ gutter: 4, column: listColumn }" | |
| 215 | + > | |
| 216 | + <template #header> | |
| 217 | + <div class="flex gap-3 justify-end"> | |
| 218 | + <Authority :value="ConfigurationPermission.CREATE"> | |
| 219 | + <Button type="primary" @click="handleCreateOrUpdate()">新增组态</Button> | |
| 220 | + </Authority> | |
| 221 | + <CardLayoutButton v-model:value="listColumn" @change="handleCardLayoutChange" /> | |
| 222 | + <Tooltip title="刷新"> | |
| 223 | + <Button type="primary" @click="getListData"> | |
| 224 | + <ReloadOutlined /> | |
| 225 | + </Button> | |
| 226 | + </Tooltip> | |
| 227 | + </div> | |
| 228 | + </template> | |
| 229 | + <template #renderItem="{ item }"> | |
| 230 | + <List.Item> | |
| 231 | + <Card hoverable class="card-container"> | |
| 232 | + <template #cover> | |
| 233 | + <div | |
| 234 | + class="img-container h-full w-full !flex justify-center items-center text-center p-1 relative" | |
| 235 | + > | |
| 236 | + <img | |
| 237 | + class="w-full h-36" | |
| 238 | + alt="example" | |
| 239 | + :src="item.thumbnail || configurationSrc" | |
| 240 | + @click="handlePreview(item)" | |
| 241 | + /> | |
| 242 | + <span | |
| 243 | + class="absolute top-0 left-0 text-light-50 transform -rotate-45 translate-y-1" | |
| 244 | + > | |
| 245 | + {{ ViewTypeNameEnum[item.viewType] }} | |
| 246 | + </span> | |
| 247 | + </div> | |
| 248 | + </template> | |
| 249 | + <template class="ant-card-actions" #actions> | |
| 250 | + <Tooltip title="预览"> | |
| 251 | + <AuthIcon | |
| 252 | + :auth="ConfigurationPermission.PREVIEW" | |
| 253 | + class="!text-lg" | |
| 254 | + icon="ant-design:eye-outlined" | |
| 255 | + @click="handlePreview(item)" | |
| 256 | + /> | |
| 257 | + </Tooltip> | |
| 258 | + <Tooltip title="设计"> | |
| 259 | + <AuthIcon | |
| 260 | + :auth="ConfigurationPermission.DESIGN" | |
| 261 | + class="!text-lg" | |
| 262 | + icon="ant-design:edit-outlined" | |
| 263 | + @click="handleDesign(item)" | |
| 264 | + /> | |
| 265 | + </Tooltip> | |
| 266 | + <Tooltip title="分享"> | |
| 267 | + <AuthIcon | |
| 268 | + :auth="ConfigurationPermission.SHARE" | |
| 269 | + class="!text-lg" | |
| 270 | + icon="ant-design:share-alt-outlined" | |
| 271 | + @click="handleCreateShareUrl(item)" | |
| 272 | + /> | |
| 273 | + </Tooltip> | |
| 274 | + <AuthDropDown | |
| 275 | + :dropMenuList="[ | |
| 276 | + { | |
| 277 | + text: '分享', | |
| 278 | + auth: ConfigurationPermission.SHARE, | |
| 279 | + icon: 'ant-design:share-alt-outlined', | |
| 280 | + event: '', | |
| 281 | + onClick: handleOpenShareModal.bind(null, item), | |
| 282 | + }, | |
| 283 | + { | |
| 284 | + text: '编辑', | |
| 285 | + auth: ConfigurationPermission.UPDATE, | |
| 286 | + icon: 'clarity:note-edit-line', | |
| 287 | + event: '', | |
| 288 | + onClick: handleCreateOrUpdate.bind(null, item), | |
| 289 | + }, | |
| 290 | + { | |
| 291 | + text: '删除', | |
| 292 | + auth: ConfigurationPermission.DELETE, | |
| 293 | + icon: 'ant-design:delete-outlined', | |
| 294 | + event: '', | |
| 295 | + popconfirm: { | |
| 296 | + title: '是否确认删除操作?', | |
| 297 | + onConfirm: handleDelete.bind(null, item), | |
| 298 | + }, | |
| 299 | + }, | |
| 300 | + ]" | |
| 301 | + :trigger="['hover']" | |
| 302 | + /> | |
| 303 | + </template> | |
| 304 | + <Card.Meta> | |
| 305 | + <template #title> | |
| 306 | + <span class="truncate">{{ item.name }}</span> | |
| 307 | + </template> | |
| 308 | + <template #description> | |
| 309 | + <div class="truncate h-11"> | |
| 310 | + <div class="truncate">{{ item.organizationDTO.name }}</div> | |
| 311 | + <div class="truncate">{{ item.remark || '' }} </div> | |
| 312 | + </div> | |
| 313 | + </template> | |
| 314 | + </Card.Meta> | |
| 315 | + </Card> | |
| 316 | + </List.Item> | |
| 317 | + </template> | |
| 318 | + </List> | |
| 319 | + </section> | |
| 320 | + <ConfigurationCenterDrawer @register="registerDrawer" @success="getListData" /> | |
| 321 | + <ShareModal | |
| 322 | + @register="registerShareModal" | |
| 323 | + :shareApi="shareConfiguration" | |
| 324 | + @success="getListData" | |
| 325 | + /> | |
| 326 | + </PageWrapper> | |
| 327 | +</template> | |
| 328 | + | |
| 329 | +<style lang="less" scoped> | |
| 330 | + .configuration-list:deep(.ant-list-header) { | |
| 331 | + border-bottom: none !important; | |
| 332 | + } | |
| 333 | + | |
| 334 | + .configuration-list:deep(.ant-list-pagination) { | |
| 335 | + height: 24px; | |
| 336 | + } | |
| 337 | + | |
| 338 | + .configuration-list:deep(.ant-card-body) { | |
| 339 | + padding: 16px !important; | |
| 340 | + } | |
| 341 | + | |
| 342 | + .configuration-list:deep(.ant-list-empty-text) { | |
| 343 | + @apply w-full h-full flex justify-center items-center; | |
| 344 | + } | |
| 345 | + | |
| 346 | + .card-container { | |
| 347 | + // background-color: red; | |
| 348 | + .img-container { | |
| 349 | + border-top-left-radius: 80px; | |
| 350 | + background-color: #fff; | |
| 351 | + } | |
| 352 | + } | |
| 353 | + | |
| 354 | + .card-container:deep(.ant-card-cover) { | |
| 355 | + background-color: red; | |
| 356 | + } | |
| 357 | +</style> | ... | ... | 
| 1 | 1 | import { formatToDate } from '/@/utils/dateUtil'; | 
| 2 | 2 | import { BasicColumn } from '/@/components/Table'; | 
| 3 | 3 | import { FormSchema } from '/@/components/Table'; | 
| 4 | -import { DeviceTypeEnum, DeviceState } from '/@/api/device/model/deviceModel'; | |
| 4 | +import { DeviceTypeEnum, DeviceState, DeviceRecord } from '/@/api/device/model/deviceModel'; | |
| 5 | 5 | import { deviceProfile } from '/@/api/device/deviceManager'; | 
| 6 | +import { h } from 'vue'; | |
| 7 | +import { Tag } from 'ant-design-vue'; | |
| 6 | 8 | |
| 7 | 9 | // 表格列数据 | 
| 8 | 10 | export const columns: BasicColumn[] = [ | 
| ... | ... | @@ -50,9 +52,13 @@ export const columns: BasicColumn[] = [ | 
| 50 | 52 | width: 100, | 
| 51 | 53 | }, | 
| 52 | 54 | { | 
| 53 | - title: '标签', | |
| 54 | - dataIndex: 'label', | |
| 55 | + title: '公开', | |
| 56 | + dataIndex: 'public', | |
| 55 | 57 | width: 100, | 
| 58 | + customRender({ record }: { record: DeviceRecord }) { | |
| 59 | + const flag = record?.customerAdditionalInfo?.isPublic; | |
| 60 | + return h(Tag, { color: flag ? 'blue' : 'orange' }, () => (flag ? '公开' : '私有')); | |
| 61 | + }, | |
| 56 | 62 | }, | 
| 57 | 63 | { | 
| 58 | 64 | title: '最后连接时间', | ... | ... | 
| ... | ... | @@ -146,6 +146,13 @@ | 
| 146 | 146 | onClick: handleDispatchCustomer.bind(null, record), | 
| 147 | 147 | }, | 
| 148 | 148 | { | 
| 149 | + label: record?.customerAdditionalInfo?.isPublic ? '私有' : '公开', | |
| 150 | + icon: record?.customerAdditionalInfo?.isPublic | |
| 151 | + ? 'ant-design:lock-outlined' | |
| 152 | + : 'ant-design:unlock-outlined', | |
| 153 | + onClick: handlePublicDevice.bind(null, record), | |
| 154 | + }, | |
| 155 | + { | |
| 149 | 156 | label: '上下线记录', | 
| 150 | 157 | icon: 'ant-design:rise-outlined', | 
| 151 | 158 | onClick: handleUpAndDownRecord.bind(null, record), | 
| ... | ... | @@ -198,6 +205,8 @@ | 
| 198 | 205 | checkDeviceOccupied, | 
| 199 | 206 | cancelDispatchCustomer, | 
| 200 | 207 | getGATEWAY, | 
| 208 | + privateDevice, | |
| 209 | + publicDevice, | |
| 201 | 210 | } from '/@/api/device/deviceManager'; | 
| 202 | 211 | import { PageEnum } from '/@/enums/pageEnum'; | 
| 203 | 212 | import { useGo } from '/@/hooks/web/usePage'; | 
| ... | ... | @@ -218,6 +227,7 @@ | 
| 218 | 227 | import { Authority } from '/@/components/Authority'; | 
| 219 | 228 | import { useRoute, useRouter } from 'vue-router'; | 
| 220 | 229 | import { useBatchOperation } from '/@/utils/useBatchOperation'; | 
| 230 | + import { useSyncConfirm } from '/@/hooks/component/useSyncConfirm'; | |
| 221 | 231 | |
| 222 | 232 | export default defineComponent({ | 
| 223 | 233 | name: 'DeviceManagement', | 
| ... | ... | @@ -440,6 +450,28 @@ | 
| 440 | 450 | reload(); | 
| 441 | 451 | }; | 
| 442 | 452 | |
| 453 | + const { createSyncConfirm } = useSyncConfirm(); | |
| 454 | + const handlePublicDevice = async (record: DeviceRecord) => { | |
| 455 | + try { | |
| 456 | + const publicFlag = record?.customerAdditionalInfo?.isPublic; | |
| 457 | + const type = publicFlag ? '私有' : '公开'; | |
| 458 | + const flag = await createSyncConfirm({ | |
| 459 | + iconType: 'warning', | |
| 460 | + title: `您确定要将设备 '${record.name}' 设为${type}吗?`, | |
| 461 | + content: `确认后,设备及其所有数据将被设为${type}并${ | |
| 462 | + publicFlag ? '不' : '' | |
| 463 | + }可被其他人访问。`, | |
| 464 | + }); | |
| 465 | + if (!flag) return; | |
| 466 | + if (publicFlag) { | |
| 467 | + await privateDevice(record.tbDeviceId); | |
| 468 | + } else { | |
| 469 | + await publicDevice(record.tbDeviceId); | |
| 470 | + } | |
| 471 | + reload(); | |
| 472 | + } catch (error) {} | |
| 473 | + }; | |
| 474 | + | |
| 443 | 475 | onMounted(() => { | 
| 444 | 476 | const queryParams = ROUTE.query as Record<'deviceProfileId', undefined | string>; | 
| 445 | 477 | const { setFieldsValue } = getForm(); | 
| ... | ... | @@ -479,6 +511,7 @@ | 
| 479 | 511 | registerImportModal, | 
| 480 | 512 | handleBatchImport, | 
| 481 | 513 | handleImportFinally, | 
| 514 | + handlePublicDevice, | |
| 482 | 515 | }; | 
| 483 | 516 | }, | 
| 484 | 517 | }); | ... | ... | 
src/views/sys/share/config/config.ts
0 → 100644
| 1 | +import { useRouter } from 'vue-router'; | |
| 2 | + | |
| 3 | +export enum ViewTypeEnum { | |
| 4 | + DATA_BOARD = 'DATA_BOARD', | |
| 5 | + LARGE_SCREEN = 'LARGE_SCREEN', | |
| 6 | + SCADA = 'SCADA', | |
| 7 | +} | |
| 8 | + | |
| 9 | +export const goShareUrl = (options: { type: ViewTypeEnum; id: string }, openNew?: false) => { | |
| 10 | + const { type, id } = options; | |
| 11 | + const ROUTER = useRouter(); | |
| 12 | + const { origin } = location; | |
| 13 | + const path = `/share/${type}/${id}`; | |
| 14 | + openNew ? ROUTER.push(path) : open(`${origin}${path}`); | |
| 15 | +}; | ... | ... | 
src/views/sys/share/index.vue
0 → 100644
| 1 | +<script lang="ts" setup> | |
| 2 | + import { onMounted } from 'vue'; | |
| 3 | + import { BasicModal, useModal } from '/@/components/Modal'; | |
| 4 | + import { Spin } from 'ant-design-vue'; | |
| 5 | + import { ref } from 'vue'; | |
| 6 | + import { useRoute } from 'vue-router'; | |
| 7 | + import { checkShareAccessToken, sharePageLogin, getShareContent } from '/@/api/sys/share'; | |
| 8 | + import { ShareRouteParams } from '/@/api/sys/model/shareModel'; | |
| 9 | + import { useUserStore } from '/@/store/modules/user'; | |
| 10 | + import { BasicForm, useForm } from '/@/components/Form'; | |
| 11 | + import { FieldsEnum } from '/@/views/common/ShareModal/config'; | |
| 12 | + import BoardDetail from '/@/views/visual/board/detail/index.vue'; | |
| 13 | + | |
| 14 | + const loading = ref(true); | |
| 15 | + const ROUTE = useRoute(); | |
| 16 | + const contentData = ref<any>(); | |
| 17 | + const canLoadComponent = ref(false); | |
| 18 | + | |
| 19 | + const [register, { openModal }] = useModal(); | |
| 20 | + | |
| 21 | + const [registerForm, { getFieldsValue }] = useForm({ | |
| 22 | + schemas: [ | |
| 23 | + { | |
| 24 | + field: FieldsEnum.ACCESS_CREDENTIALS, | |
| 25 | + component: 'Input', | |
| 26 | + label: '访问令牌', | |
| 27 | + }, | |
| 28 | + ], | |
| 29 | + showActionButtonGroup: false, | |
| 30 | + layout: 'inline', | |
| 31 | + labelWidth: 100, | |
| 32 | + }); | |
| 33 | + | |
| 34 | + const userStore = useUserStore(); | |
| 35 | + | |
| 36 | + const getShareToken = async () => { | |
| 37 | + const { params } = ROUTE; | |
| 38 | + const { publicId } = params as Partial<ShareRouteParams>; | |
| 39 | + const { token, refreshToken } = await sharePageLogin(publicId!); | |
| 40 | + userStore.storeToken(token, refreshToken); | |
| 41 | + }; | |
| 42 | + | |
| 43 | + const getCheckNeedAccessToken = async () => { | |
| 44 | + const { params } = ROUTE; | |
| 45 | + loading.value = true; | |
| 46 | + const { viewType, id } = params as Partial<ShareRouteParams>; | |
| 47 | + const { data } = await checkShareAccessToken(viewType!, id!); | |
| 48 | + data ? openModal(data) : await getContentData(); | |
| 49 | + }; | |
| 50 | + | |
| 51 | + const getContentLoading = ref(false); | |
| 52 | + const getContentData = async () => { | |
| 53 | + try { | |
| 54 | + getContentLoading.value = true; | |
| 55 | + const { params } = ROUTE; | |
| 56 | + const { id } = params as unknown as ShareRouteParams; | |
| 57 | + const value = getFieldsValue() as Record<'accessCredentials', string>; | |
| 58 | + const result = await getShareContent({ id, ...value }); | |
| 59 | + contentData.value = result; | |
| 60 | + loading.value = false; | |
| 61 | + canLoadComponent.value = true; | |
| 62 | + openModal(false); | |
| 63 | + } catch (error) { | |
| 64 | + } finally { | |
| 65 | + getContentLoading.value = false; | |
| 66 | + } | |
| 67 | + }; | |
| 68 | + | |
| 69 | + const handleSubmit = async () => { | |
| 70 | + await getContentData(); | |
| 71 | + }; | |
| 72 | + | |
| 73 | + onMounted(async () => { | |
| 74 | + await getShareToken(); | |
| 75 | + await getCheckNeedAccessToken(); | |
| 76 | + }); | |
| 77 | +</script> | |
| 78 | + | |
| 79 | +<template> | |
| 80 | + <BasicModal | |
| 81 | + @register="register" | |
| 82 | + title="公开" | |
| 83 | + @ok="handleSubmit" | |
| 84 | + :showCancelBtn="false" | |
| 85 | + :okButtonProps="{ loading: getContentLoading }" | |
| 86 | + > | |
| 87 | + <BasicForm @register="registerForm" /> | |
| 88 | + </BasicModal> | |
| 89 | + <Spin | |
| 90 | + :spinning="loading" | |
| 91 | + tip="正在加载中..." | |
| 92 | + size="large" | |
| 93 | + class="!flex justify-center items-center w-full h-full share-full-loading" | |
| 94 | + > | |
| 95 | + <BoardDetail v-if="canLoadComponent" :value="contentData" /> | |
| 96 | + </Spin> | |
| 97 | +</template> | |
| 98 | + | |
| 99 | +<style lang="less" scoped></style> | ... | ... | 
| ... | ... | @@ -30,10 +30,18 @@ | 
| 30 | 30 | |
| 31 | 31 | const { createMessage } = useMessage(); | 
| 32 | 32 | |
| 33 | + const handleSubmitValue = (value: Recordable) => { | |
| 34 | + if (!Reflect.get(value, 'accessCredentials')) { | |
| 35 | + Reflect.deleteProperty(value, 'accessCredentials'); | |
| 36 | + } | |
| 37 | + return value as any; | |
| 38 | + }; | |
| 39 | + | |
| 33 | 40 | const handleCreatePanel = async () => { | 
| 34 | 41 | await method.validate(); | 
| 35 | 42 | try { | 
| 36 | - const value = method.getFieldsValue() as AddDataBoardParams; | |
| 43 | + let value = method.getFieldsValue() as AddDataBoardParams; | |
| 44 | + value = handleSubmitValue(value); | |
| 37 | 45 | loading.value = true; | 
| 38 | 46 | changeLoading(true); | 
| 39 | 47 | await addDataBoard(value); | 
| ... | ... | @@ -52,7 +60,8 @@ | 
| 52 | 60 | await method.validate(); | 
| 53 | 61 | try { | 
| 54 | 62 | loading.value = true; | 
| 55 | - const value = method.getFieldsValue() as UpdateDataBoardParams; | |
| 63 | + let value = method.getFieldsValue() as UpdateDataBoardParams; | |
| 64 | + value = handleSubmitValue(value); | |
| 56 | 65 | value.id = unref(recordId) as string; | 
| 57 | 66 | changeLoading(true); | 
| 58 | 67 | await updateDataBoard(value); | ... | ... | 
| 1 | +import { ViewTypeEnum } from '/@/views/sys/share/config/config'; | |
| 2 | + | |
| 1 | 3 | export enum MoreActionEvent { | 
| 2 | 4 | EDIT = 'edit', | 
| 3 | 5 | COPY = 'copy', | 
| 4 | 6 | DELETE = 'delete', | 
| 7 | + SHARE = 'share', | |
| 5 | 8 | } | 
| 6 | 9 | |
| 7 | 10 | export enum VisualBoardPermission { | 
| ... | ... | @@ -24,11 +27,8 @@ export const DEFAULT_WIDGET_HEIGHT = 6; | 
| 24 | 27 | export const DEFAULT_MIN_HEIGHT = 5; | 
| 25 | 28 | export const DEFAULT_MIN_WIDTH = 3; | 
| 26 | 29 | |
| 27 | -export const DATA_BOARD_SHARE_URL = ( | |
| 28 | - boardId = ':boardId', | |
| 29 | - tenantId = ':tenantId', | |
| 30 | - name = ':boardName?' | |
| 31 | -) => `/data/board/share/${boardId}/${tenantId}/${name}`; | |
| 30 | +export const DATA_BOARD_SHARE_URL = (id: string, publicId: string) => | |
| 31 | + `/share/${ViewTypeEnum.DATA_BOARD}/${id}/${publicId}`; | |
| 32 | 32 | |
| 33 | 33 | export const isBataBoardSharePage = (url: string) => { | 
| 34 | 34 | const reg = /^\/data\/board\/share/g; | ... | ... | 
| ... | ... | @@ -35,22 +35,6 @@ export const formSchema: FormSchema[] = [ | 
| 35 | 35 | }, | 
| 36 | 36 | }, | 
| 37 | 37 | { | 
| 38 | - field: 'viewType', | |
| 39 | - label: '公开性', | |
| 40 | - component: 'RadioGroup', | |
| 41 | - defaultValue: ViewType.PRIVATE_VIEW, | |
| 42 | - helpMessage: [ | |
| 43 | - '私有视图只有项目成员可以浏览。公开视图拥有一个公开的 URL,任何人无需登录即可浏览。 ', | |
| 44 | - ], | |
| 45 | - componentProps: { | |
| 46 | - placeholder: '请选择公开性', | |
| 47 | - options: [ | |
| 48 | - { label: '私有看板', value: ViewType.PRIVATE_VIEW }, | |
| 49 | - { label: '公开看板', value: ViewType.PUBLIC_VIEW, disabled: true }, | |
| 50 | - ], | |
| 51 | - }, | |
| 52 | - }, | |
| 53 | - { | |
| 54 | 38 | field: 'remark', | 
| 55 | 39 | label: '备注', | 
| 56 | 40 | component: 'InputTextArea', | ... | ... | 
| ... | ... | @@ -14,7 +14,6 @@ | 
| 14 | 14 | DEFAULT_MIN_WIDTH, | 
| 15 | 15 | DEFAULT_WIDGET_HEIGHT, | 
| 16 | 16 | DEFAULT_WIDGET_WIDTH, | 
| 17 | - isBataBoardSharePage, | |
| 18 | 17 | MoreActionEvent, | 
| 19 | 18 | VisualComponentPermission, | 
| 20 | 19 | } from '../config/config'; | 
| ... | ... | @@ -22,7 +21,6 @@ | 
| 22 | 21 | addDataComponent, | 
| 23 | 22 | deleteDataComponent, | 
| 24 | 23 | getDataComponent, | 
| 25 | - getShareBoardComponentInfo, | |
| 26 | 24 | updateDataBoardLayout, | 
| 27 | 25 | } from '/@/api/dataBoard'; | 
| 28 | 26 | import { useRoute, useRouter } from 'vue-router'; | 
| ... | ... | @@ -48,6 +46,10 @@ | 
| 48 | 46 | import { useScript } from '/@/hooks/web/useScript'; | 
| 49 | 47 | import { BAI_DU_MAP_GL_LIB, BAI_DU_MAP_TRACK_ANIMATION } from '/@/utils/fnUtils'; | 
| 50 | 48 | |
| 49 | + const props = defineProps<{ | |
| 50 | + value: Recordable; | |
| 51 | + }>(); | |
| 52 | + | |
| 51 | 53 | const ROUTE = useRoute(); | 
| 52 | 54 | |
| 53 | 55 | const ROUTER = useRouter(); | 
| ... | ... | @@ -67,13 +69,12 @@ | 
| 67 | 69 | return decode((ROUTE.params as { boardName: string }).boardName || ''); | 
| 68 | 70 | }); | 
| 69 | 71 | |
| 70 | - const getSharePageParams = computed(() => { | |
| 71 | - const { boardId, tenantId } = ROUTE.params as { boardId: string; tenantId: string }; | |
| 72 | - return { boardId: decode(boardId), tenantId: decode(tenantId) }; | |
| 72 | + const getSharePageData = computed(() => { | |
| 73 | + return props.value; | |
| 73 | 74 | }); | 
| 74 | 75 | |
| 75 | 76 | const getIsSharePage = computed(() => { | 
| 76 | - return isBataBoardSharePage(ROUTE.path); | |
| 77 | + return ROUTE.matched.find((item) => item.path === '/share/:viewType/:id/:publicId'); | |
| 77 | 78 | }); | 
| 78 | 79 | |
| 79 | 80 | const widgetEl = new Map<string, Fn>(); | 
| ... | ... | @@ -220,19 +221,9 @@ | 
| 220 | 221 | return {} as ComponentInfoDetail; | 
| 221 | 222 | }; | 
| 222 | 223 | |
| 223 | - const getSharePageComponentData = async () => { | |
| 224 | - try { | |
| 225 | - const params = unref(getSharePageParams); | |
| 226 | - return await getShareBoardComponentInfo(params); | |
| 227 | - } catch (error) {} | |
| 228 | - return {} as ComponentInfoDetail; | |
| 229 | - }; | |
| 230 | - | |
| 231 | 224 | const getDataBoradDetail = async () => { | 
| 232 | 225 | try { | 
| 233 | - return unref(getIsSharePage) | |
| 234 | - ? await getSharePageComponentData() | |
| 235 | - : await getBasePageComponentData(); | |
| 226 | + return unref(getIsSharePage) ? unref(getSharePageData) : await getBasePageComponentData(); | |
| 236 | 227 | } catch (error) {} | 
| 237 | 228 | return {} as ComponentInfoDetail; | 
| 238 | 229 | }; | 
| ... | ... | @@ -388,7 +379,7 @@ | 
| 388 | 379 | |
| 389 | 380 | <template> | 
| 390 | 381 | <section class="flex flex-col overflow-hidden h-full w-full board-detail"> | 
| 391 | - <PageHeader> | |
| 382 | + <PageHeader v-if="!getIsSharePage"> | |
| 392 | 383 | <template #title> | 
| 393 | 384 | <div class="flex items-center"> | 
| 394 | 385 | <img | ... | ... | 
| ... | ... | @@ -10,7 +10,7 @@ | 
| 10 | 10 | import { DATA_BOARD_SHARE_URL, MoreActionEvent, VisualBoardPermission } from './config/config'; | 
| 11 | 11 | import { useModal } from '/@/components/Modal'; | 
| 12 | 12 | import PanelDetailModal from './components/PanelDetailModal.vue'; | 
| 13 | - import { getDataBoardList, deleteDataBoard } from '/@/api/dataBoard'; | |
| 13 | + import { getDataBoardList, deleteDataBoard, shareBoard } from '/@/api/dataBoard'; | |
| 14 | 14 | import { DataBoardRecord } from '/@/api/dataBoard/model'; | 
| 15 | 15 | import { ViewType } from './config/panelDetail'; | 
| 16 | 16 | import { useRouter } from 'vue-router'; | 
| ... | ... | @@ -21,6 +21,7 @@ | 
| 21 | 21 | import { encode } from './config/config'; | 
| 22 | 22 | import { useForm, BasicForm } from '/@/components/Form'; | 
| 23 | 23 | import { formSchema } from './config/searchForm'; | 
| 24 | + import { ShareModal } from '/@/views/common/ShareModal'; | |
| 24 | 25 | |
| 25 | 26 | const ListItem = List.Item; | 
| 26 | 27 | const router = useRouter(); | 
| ... | ... | @@ -77,14 +78,15 @@ | 
| 77 | 78 | getDatasource(); | 
| 78 | 79 | } | 
| 79 | 80 | |
| 80 | - const createShareUrl = (boardId: string, tenantId: string, name: string) => { | |
| 81 | + const createShareUrl = (record: DataBoardRecord) => { | |
| 81 | 82 | const { origin } = location; | 
| 82 | - return `${origin}${DATA_BOARD_SHARE_URL(encode(boardId), encode(tenantId), encode(name))}`; | |
| 83 | + const { id, publicId } = record; | |
| 84 | + return `${origin}${DATA_BOARD_SHARE_URL(id, publicId)}`; | |
| 83 | 85 | }; | 
| 84 | 86 | |
| 85 | 87 | const { clipboardRef } = useCopyToClipboard(); | 
| 86 | 88 | const handleCopyShareUrl = (record: DataBoardRecord) => { | 
| 87 | - clipboardRef.value = createShareUrl(record.id, record.tenantId, record.name); | |
| 89 | + clipboardRef.value = createShareUrl(record); | |
| 88 | 90 | unref(clipboardRef) ? createMessage.success('复制成功') : createMessage.error('未找到分享链接'); | 
| 89 | 91 | }; | 
| 90 | 92 | |
| ... | ... | @@ -92,7 +94,13 @@ | 
| 92 | 94 | const dropMenuList = computed<DropMenu[]>(() => { | 
| 93 | 95 | const hasUpdatePermission = hasPermission(VisualBoardPermission.UPDATE); | 
| 94 | 96 | const hasDeletePermission = hasPermission(VisualBoardPermission.DELETE); | 
| 95 | - const basicMenu: DropMenu[] = []; | |
| 97 | + const basicMenu: DropMenu[] = [ | |
| 98 | + { | |
| 99 | + text: '分享', | |
| 100 | + event: MoreActionEvent.SHARE, | |
| 101 | + icon: 'ant-design:share-alt-outlined', | |
| 102 | + }, | |
| 103 | + ]; | |
| 96 | 104 | if (hasUpdatePermission) | 
| 97 | 105 | basicMenu.push({ | 
| 98 | 106 | text: '编辑', | 
| ... | ... | @@ -124,6 +132,10 @@ | 
| 124 | 132 | } | 
| 125 | 133 | }; | 
| 126 | 134 | |
| 135 | + const handleOpenShareModal = (record: DataBoardRecord) => { | |
| 136 | + openShareModal(true, record); | |
| 137 | + }; | |
| 138 | + | |
| 127 | 139 | const handleMenuEvent = (event: DropMenu, record: DataBoardRecord) => { | 
| 128 | 140 | if (event.event === MoreActionEvent.EDIT) handleEdit(record); | 
| 129 | 141 | if (event.event === MoreActionEvent.DELETE) { | 
| ... | ... | @@ -133,6 +145,7 @@ | 
| 133 | 145 | onOk: () => handleRemove(record), | 
| 134 | 146 | }); | 
| 135 | 147 | } | 
| 148 | + if (event.event === MoreActionEvent.SHARE) handleOpenShareModal(record); | |
| 136 | 149 | }; | 
| 137 | 150 | |
| 138 | 151 | const handleEdit = (record: DataBoardRecord) => { | 
| ... | ... | @@ -156,6 +169,8 @@ | 
| 156 | 169 | |
| 157 | 170 | const [registerModal, { openModal }] = useModal(); | 
| 158 | 171 | |
| 172 | + const [registerShareModal, { openModal: openShareModal }] = useModal(); | |
| 173 | + | |
| 159 | 174 | const handleViewBoard = (record: DataBoardRecord) => { | 
| 160 | 175 | const hasDetailPermission = hasPermission(VisualBoardPermission.DETAIL); | 
| 161 | 176 | if (hasDetailPermission) { | 
| ... | ... | @@ -238,10 +253,7 @@ | 
| 238 | 253 | <span> | 
| 239 | 254 | {{ item.viewType === ViewType.PRIVATE_VIEW ? '私有看板' : '公共看板' }} | 
| 240 | 255 | </span> | 
| 241 | - <span | |
| 242 | - style="display: none" | |
| 243 | - v-if="item.viewType === ViewType.PUBLIC_VIEW && false" | |
| 244 | - > | |
| 256 | + <span v-if="item.viewType === ViewType.PUBLIC_VIEW"> | |
| 245 | 257 | <Tooltip title="点击复制分享链接"> | 
| 246 | 258 | <ShareAltOutlined class="ml-2" @click.stop="handleCopyShareUrl(item)" /> | 
| 247 | 259 | </Tooltip> | 
| ... | ... | @@ -255,6 +267,7 @@ | 
| 255 | 267 | </template> | 
| 256 | 268 | </List> | 
| 257 | 269 | </Spin> | 
| 270 | + <ShareModal @register="registerShareModal" :shareApi="shareBoard" @success="getDatasource" /> | |
| 258 | 271 | <PanelDetailModal @register="registerModal" @change="getDatasource" /> | 
| 259 | 272 | </PageWrapper> | 
| 260 | 273 | </template> | ... | ... |