Commit 8e3c843458ef058a2c186e53a47bc7bc75816e65

Authored by ww
1 parent d405e83c

feat: 新增看板管理公共看板

... ... @@ -12,6 +12,7 @@
12 12 "MQTT",
13 13 "notif",
14 14 "PROTOBUF",
  15 + "SCADA",
15 16 "SNMP",
16 17 "unref",
17 18 "vben",
... ...
... ... @@ -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 +};
... ...
... ... @@ -48,6 +48,8 @@ export interface DataBoardRecord {
48 48 layout: Layout[];
49 49 defaultConfig: string;
50 50 tenantStatus: string;
  51 + publicId: string;
  52 + accessCredentials?: string;
51 53 }
52 54
53 55 export interface DataBoardList {
... ...
  1 +import { ViewTypeEnum } from '/@/views/sys/share/config/config';
  2 +
  3 +export interface ShareRouteParams {
  4 + id: string;
  5 + viewType: ViewTypeEnum;
  6 + publicId: string;
  7 +}
... ...
  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 +};
... ...
... ... @@ -14,4 +14,6 @@ export const PageEnum = {
14 14 //设备配置
15 15 DEVICE_PROFILE: '/product/profiles',
16 16 DEVICE_LIST: '/device/list',
  17 +
  18 + SHARE_PAGE: '/share/:viewType/:id/:publicId',
17 19 };
... ...
... ... @@ -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 ];
... ...
  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 +};
... ...
  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 +};
... ...
  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 '../../visual/board/config/share';
  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 +<script lang="ts" setup>
  2 + import { BasicForm, useForm } from '/@/components/Form';
  3 + import { BasicModal, useModalInner } from '/@/components/Modal';
  4 + import { FieldsEnum, schemas } from '../config/share';
  5 + import { DataBoardRecord } from '/@/api/dataBoard/model';
  6 + import { Alert } from 'ant-design-vue';
  7 + import { ref, unref } from 'vue';
  8 + import { shareBoard } from '/@/api/dataBoard';
  9 + import { ViewType } from '../config/panelDetail';
  10 + import { nextTick } from 'vue';
  11 +
  12 + const loading = ref(false);
  13 + const record = ref<DataBoardRecord>({} as unknown as DataBoardRecord);
  14 +
  15 + const [register, { closeModal }] = useModalInner(async (data: DataBoardRecord) => {
  16 + record.value = data;
  17 + await nextTick();
  18 + setFieldsValue({
  19 + [FieldsEnum.IS_SHARE]: data.viewType === ViewType.PUBLIC_VIEW,
  20 + [FieldsEnum.ACCESS_CREDENTIALS]: data.accessCredentials,
  21 + });
  22 + });
  23 +
  24 + const [registerForm, { getFieldsValue, setFieldsValue }] = useForm({
  25 + schemas,
  26 + showActionButtonGroup: false,
  27 + layout: 'inline',
  28 + labelWidth: 120,
  29 + });
  30 +
  31 + const emit = defineEmits(['register', 'success']);
  32 +
  33 + const handleSubmit = async () => {
  34 + try {
  35 + loading.value = true;
  36 + const value = getFieldsValue();
  37 + const { id } = unref(record);
  38 + await shareBoard({ id, ...value });
  39 + closeModal();
  40 + emit('success');
  41 + } finally {
  42 + loading.value = false;
  43 + }
  44 + };
  45 +</script>
  46 +
  47 +<template>
  48 + <BasicModal title="分享" @register="register" @ok="handleSubmit" :loading="loading">
  49 + <Alert
  50 + class="!mb-4"
  51 + type="info"
  52 + message="私有视图只有项目成员可以浏览,公开视图拥有一个公开的 URL,任何人无需登录即可浏览."
  53 + />
  54 + <BasicForm @register="registerForm" />
  55 + </BasicModal>
  56 +</template>
... ...
  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',
... ...
  1 +import { FormSchema } from '/@/components/Form';
  2 +
  3 +export enum FieldsEnum {
  4 + IS_SHARE = 'isShare',
  5 + ACCESS_CREDENTIALS = 'accessCredentials',
  6 +}
  7 +
  8 +export const schemas: FormSchema[] = [
  9 + {
  10 + field: FieldsEnum.IS_SHARE,
  11 + label: '公开性',
  12 + component: 'Switch',
  13 + defaultValue: false,
  14 + },
  15 + {
  16 + field: FieldsEnum.ACCESS_CREDENTIALS,
  17 + label: '访问凭证',
  18 + component: 'Input',
  19 + ifShow: ({ model }) => model[FieldsEnum.IS_SHARE],
  20 + componentProps: {
  21 + maxLength: 64,
  22 + },
  23 + },
  24 +];
... ...
... ... @@ -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
... ...
... ... @@ -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 './components/ShareModal.vue';
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" @success="getDatasource" />
258 271 <PanelDetailModal @register="registerModal" @change="getDatasource" />
259 272 </PageWrapper>
260 273 </template>
... ...