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> | ... | ... |