Commit b1fea99bc5155d6014b6d3550f9f4b42a45f51ba

Authored by 张 峰林
Committed by xp.Huang
1 parent b65d9399

feat:看板添加手机端逻辑

... ... @@ -53,6 +53,7 @@ export interface DataBoardRecord {
53 53 publicId: string;
54 54 organizationId?: string;
55 55 accessCredentials?: string;
  56 + platform?: string;
56 57 }
57 58
58 59 export interface DataBoardList {
... ... @@ -137,7 +138,12 @@ export interface MasterDeviceList {
137 138 }
138 139
139 140 export interface ComponentInfoDetail {
140   - data: { componentData: DataComponentRecord[]; componentLayout: Layout[] };
  141 + data: {
  142 + componentData: DataComponentRecord[];
  143 + componentLayout: Layout[];
  144 + phoneModel?: string | object;
  145 + platform: string;
  146 + };
141 147 }
142 148
143 149 export interface DeviceAttributeParams {
... ...
... ... @@ -21,6 +21,7 @@ enum Api {
21 21 SendLoginSmsCode = '/noauth/send_login_code/',
22 22 ResetCode = '/noauth/reset_code/',
23 23 ResetPassword = '/noauth/reset/',
  24 + APP_GET_TOKEN = '/third/login/id/',
24 25 }
25 26
26 27 /**
... ... @@ -100,3 +101,9 @@ export const getUserToken = (id: string) => {
100 101 url: `/third/login/id/${id}`,
101 102 });
102 103 };
  104 +
  105 +export const doAppLogin = (userId: string) => {
  106 + return defHttp.get<Record<'refreshToken' | 'token', string>>({
  107 + url: `${Api.APP_GET_TOKEN}${userId}`,
  108 + });
  109 +};
... ...
  1 +<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1702374950759" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1997" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M144.700101 684.994006l535.580045 0L680.280146 359.237781 144.700101 359.237781 144.700101 684.994006 144.700101 684.994006zM918.373823 440.680675l0-81.442894c0-44.791136-36.649711-81.437777-81.437777-81.437777l-692.235944 0c-44.791136 0-81.437777 36.646642-81.437777 81.437777L63.262324 684.994006c0 44.791136 36.646642 81.442894 81.437777 81.442894l692.235944 0c44.788066 0 81.437777-36.650735 81.437777-81.442894l0-81.437777c22.396079 0 40.7194-18.322297 40.7194-40.7194l0-81.436754C959.093223 459.003995 940.769902 440.680675 918.373823 440.680675L918.373823 440.680675zM877.655446 481.400075l0 81.436754L877.655446 684.994006c0 22.395056-18.323321 40.718377-40.7194 40.718377l-692.235944 0c-22.396079 0-40.7194-18.323321-40.7194-40.718377L103.980701 359.237781c0-22.396079 18.323321-40.7194 40.7194-40.7194l692.235944 0c22.396079 0 40.7194 18.323321 40.7194 40.7194L877.655446 481.400075 877.655446 481.400075zM877.655446 481.400075" fill="#272636" p-id="1998"></path></svg>
\ No newline at end of file
... ...
  1 +<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1702374946029" class="icon" viewBox="0 0 1294 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1850" xmlns:xlink="http://www.w3.org/1999/xlink" width="252.734375" height="200"><path d="M0 727.578947l188.631579 0 0 296.421053-188.631579 0 0-296.421053Z" p-id="1851"></path><path d="M269.473684 565.894737l188.631579 0 0 458.105263-188.631579 0 0-458.105263Z" p-id="1852"></path><path d="M565.894737 377.263158l161.684211 0 0 646.736842-161.684211 0 0-646.736842Z" p-id="1853"></path><path d="M835.368421 188.631579l188.631579 0 0 835.368421-188.631579 0 0-835.368421Z" p-id="1854"></path><path d="M1104.842105 0l188.631579 0 0 1024-188.631579 0 0-1024Z" p-id="1855"></path></svg>
\ No newline at end of file
... ...
  1 +<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1702374954963" class="icon" viewBox="0 0 1280 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2277" xmlns:xlink="http://www.w3.org/1999/xlink" width="250" height="200"><path d="M1269.82 309.76C915.48-17.98 364.38-17.86 10.18 309.76c-13.32 12.32-13.58 33.18-0.7 45.96l68.48 67.94c12.28 12.2 32.04 12.46 44.8 0.76 291.84-267.36 742.6-267.42 1034.5 0 12.76 11.7 32.52 11.42 44.8-0.76l68.48-67.94c12.86-12.78 12.6-33.64-0.72-45.96zM640 704c-70.7 0-128 57.3-128 128s57.3 128 128 128 128-57.3 128-128-57.3-128-128-128z m405.34-167.18c-230.52-203.86-580.42-203.64-810.68 0-13.8 12.2-14.24 33.38-1.14 46.3l68.88 67.98c12 11.84 31.32 12.64 44.1 1.6 167.9-145.14 419.48-144.82 586.98 0 12.78 11.04 32.1 10.26 44.1-1.6l68.88-67.98c13.12-12.92 12.66-34.12-1.12-46.3z" p-id="2278"></path></svg>
\ No newline at end of file
... ...
... ... @@ -16,6 +16,7 @@ export const PageEnum = {
16 16 DEVICE_LIST: '/device/list',
17 17
18 18 SHARE_PAGE: '/share/:viewType/:id/:publicId',
  19 + APP_PAGE: '/appPage/:boardId/:userId',
19 20
20 21 RULE_CHAIN_DETAIL: '/rule/chain/:id',
21 22
... ...
... ... @@ -2,7 +2,7 @@ import { RouteLocationNormalizedLoaded } from 'vue-router';
2 2
3 3 const menuMap = new Map();
4 4
5   -menuMap.set('/visual/board/detail/:boardId/:boardName/:organizationId?', '/visual/board');
  5 +menuMap.set('/visual/board/detail/:boardId/:boardName/:platform/:organizationId?', '/visual/board');
6 6 menuMap.set('/rule/chain/:id', '/rule/chain');
7 7
8 8 export const useMenuActiveFix = (route: RouteLocationNormalizedLoaded) => {
... ...
... ... @@ -11,7 +11,8 @@ import { getAuthCache } from '/@/utils/auth';
11 11 const LOGIN_PATH = PageEnum.BASE_LOGIN;
12 12 const ROOT_PATH = RootRoute.path;
13 13 const SHARE_PATH = PageEnum.SHARE_PAGE;
14   -const whitePathList: string[] = [LOGIN_PATH, SHARE_PATH];
  14 +const APP_PATH = PageEnum.APP_PAGE;
  15 +const whitePathList: string[] = [LOGIN_PATH, SHARE_PATH, APP_PATH];
15 16 // const userInfo1 = getAuthCache(USER_INFO_KEY);
16 17 // const userInfo = ref(userInfo1);
17 18
... ...
  1 +import { AppRouteRecordRaw } from '../types';
  2 +import { PageEnum } from '/@/enums/pageEnum';
  3 +
  4 +export const APP_PAGE_ROUTER: AppRouteRecordRaw = {
  5 + path: PageEnum.APP_PAGE,
  6 + name: 'appPage',
  7 + component: () => import('/@/views/sys/appPage/index.vue'),
  8 + meta: {
  9 + title: '公开',
  10 + hideBreadcrumb: true,
  11 + hideChildrenInMenu: true,
  12 + },
  13 +};
... ...
... ... @@ -5,6 +5,7 @@ import { PageEnum } from '/@/enums/pageEnum';
5 5 import { t } from '/@/hooks/web/useI18n';
6 6 import { LAYOUT } from '../constant';
7 7 import { PUBLIC_PAGE_ROUTER } from './public';
  8 +import { APP_PAGE_ROUTER } from './appPage';
8 9
9 10 const modules = import.meta.globEager('./modules/**/*.ts');
10 11 const routeModuleList: AppRouteModule[] = [];
... ... @@ -87,4 +88,5 @@ export const basicRoutes = [
87 88 REDIRECT_ROUTE,
88 89 PAGE_NOT_FOUND_ROUTE,
89 90 PUBLIC_PAGE_ROUTER,
  91 + APP_PAGE_ROUTER,
90 92 ];
... ...
... ... @@ -105,7 +105,7 @@ export const getFormSchemas = (hasAlarmNotify: Ref<boolean>): FormSchema[] => {
105 105 field: FormFieldsEnum.ALARM_PROFILED,
106 106 label: '',
107 107 component: 'AlarmProfileSelect',
108   - rules: [{ required: true, message: `请选择${FormFieldsEnum.ALARM_PROFILED}` }],
  108 + // rules: [{ required: true, message: `请选择${FormFieldsEnum.ALARM_PROFILED}` }],
109 109 ifShow: ({ model }) => model[FormFieldsEnum.OUT_TARGET] === ExecutionActionEnum.MSG_NOTIFY,
110 110 componentProps: () => {
111 111 return {
... ...
  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 +export const isShareMode = () => {
  2 + const sharePageReg = /^\/share\/[^/]+\/[^/]+\/[^/]+$/;
  3 + const { pathname } = location;
  4 + return sharePageReg.test(pathname);
  5 +};
... ...
  1 +<script lang="ts" setup>
  2 + import { onMounted } from 'vue';
  3 + import { Spin } from 'ant-design-vue';
  4 + import { ref } from 'vue';
  5 + import { useRoute } from 'vue-router';
  6 + import { useUserStore } from '/@/store/modules/user';
  7 + import BoardDetail from '/@/views/visual/board/detail/index.vue';
  8 + import { doAppLogin } from '/@/api/sys/user';
  9 +
  10 + const loading = ref(true);
  11 + const ROUTE = useRoute();
  12 + const contentData = ref<any>();
  13 + const canLoadComponent = ref(false);
  14 +
  15 + const modelVisable = ref(false);
  16 +
  17 + const userStore = useUserStore();
  18 +
  19 + const getShareToken = async () => {
  20 + const { params } = ROUTE;
  21 + const { userId }: any = params;
  22 + const { token, refreshToken } = await doAppLogin(userId);
  23 + userStore.storeToken(token, refreshToken);
  24 + };
  25 +
  26 + const getContentLoading = ref(false);
  27 + const getContentData = async () => {
  28 + try {
  29 + getContentLoading.value = true;
  30 + loading.value = false;
  31 + canLoadComponent.value = true;
  32 + modelVisable.value = false;
  33 + } catch (error) {
  34 + } finally {
  35 + getContentLoading.value = false;
  36 + }
  37 + };
  38 +
  39 + onMounted(async () => {
  40 + await getShareToken();
  41 + await getContentData();
  42 + });
  43 +</script>
  44 +
  45 +<template>
  46 + <Spin
  47 + :spinning="loading"
  48 + tip="正在加载中..."
  49 + size="large"
  50 + class="!flex justify-center items-center w-full h-full share-full-loading"
  51 + >
  52 + <BoardDetail v-if="canLoadComponent" :value="contentData" />
  53 + </Spin>
  54 +</template>
  55 +
  56 +<style lang="less">
  57 + .share-page-token-modal {
  58 + .ant-modal-close {
  59 + display: none;
  60 + }
  61 + }
  62 +</style>
... ...
1 1 <script lang="ts" setup>
2 2 import { BasicModal, useModalInner } from '/@/components/Modal';
3 3 import { BasicForm, useForm } from '/@/components/Form';
4   - import { formSchema } from '../config/panelDetail';
  4 + import { formSchema, PlatformType } from '../config/panelDetail';
5 5 import { addDataBoard, updateDataBoard } from '/@/api/dataBoard';
6 6 import { AddDataBoardParams } from '/@/api/dataBoard/model';
7 7 import { useMessage } from '/@/hooks/web/useMessage';
... ... @@ -34,6 +34,9 @@
34 34 if (!Reflect.get(value, 'accessCredentials')) {
35 35 Reflect.deleteProperty(value, 'accessCredentials');
36 36 }
  37 + if (value.platform === PlatformType.PC) {
  38 + Reflect.deleteProperty(value, 'phoneModel');
  39 + }
37 40 return value as any;
38 41 };
39 42
... ...
... ... @@ -4,7 +4,13 @@ export enum ViewType {
4 4 PRIVATE_VIEW = 'PRIVATE_VIEW',
5 5 PUBLIC_VIEW = 'PUBLIC_VIEW',
6 6 }
  7 +import { Platform } from '../../palette/components/PagerHeader/config';
  8 +
7 9 useComponentRegister('OrgTreeSelect', OrgTreeSelect);
  10 +export enum PlatformType {
  11 + PHONE = 'phone',
  12 + PC = 'pc',
  13 +}
8 14
9 15 export const formSchema: FormSchema[] = [
10 16 {
... ... @@ -24,6 +30,32 @@ export const formSchema: FormSchema[] = [
24 30 rules: [{ required: true, message: '组织为必填项' }],
25 31 },
26 32 {
  33 + field: 'platform',
  34 + label: '平台',
  35 + required: true,
  36 + component: 'RadioGroup',
  37 + defaultValue: PlatformType.PC,
  38 + componentProps({ formModel }) {
  39 + return {
  40 + defaultValue: PlatformType.PC,
  41 + options: [
  42 + { label: 'PC端', value: PlatformType.PC },
  43 + { label: '移动端', value: PlatformType.PHONE },
  44 + ],
  45 + onChange(e) {
  46 + formModel.phoneModel =
  47 + e?.target?.value === PlatformType.PHONE ? JSON.stringify(Platform[1]) : [];
  48 + },
  49 + };
  50 + },
  51 + },
  52 + {
  53 + field: 'phoneModel',
  54 + label: '手机端尺寸',
  55 + component: 'Input',
  56 + ifShow: false,
  57 + },
  58 + {
27 59 field: 'remark',
28 60 label: '备注',
29 61 component: 'InputTextArea',
... ...
... ... @@ -135,12 +135,13 @@
135 135
136 136 const handleViewBoard = (record: DataBoardRecord) => {
137 137 const hasDetailPermission = hasPermission(VisualBoardPermission.DETAIL);
  138 + const { platform = 'pc' } = record || {};
138 139 if (hasDetailPermission) {
139 140 const boardId = encode(record.id);
140 141 const boardName = encode(record.name);
141 142 const organizationId = encode(record!.organizationId!);
142 143
143   - router.push(`/visual/board/detail/${boardId}/${boardName}/${organizationId}`);
  144 + router.push(`/visual/board/detail/${boardId}/${boardName}/${platform}/${organizationId}`);
144 145 } else createMessage.warning('没有权限');
145 146 };
146 147 </script>
... ... @@ -170,7 +171,7 @@
170 171 </Dropdown>
171 172 </template>
172 173 <section @click="handleViewBoard(item)">
173   - <div class="flex data-card__info">
  174 + <div class="flex data-card__info relative">
174 175 <div>
175 176 <div>组件数量</div>
176 177 <Statistic class="text-2xl" :value="item.componentNum">
... ... @@ -179,6 +180,12 @@
179 180 </template>
180 181 </Statistic>
181 182 </div>
  183 + <div class="absolute" style="right: 1%">
  184 + <Icon
  185 + :icon="item.platform === 'pc' ? 'ri:computer-line' : 'clarity:mobile-phone-solid'"
  186 + style="font-size: 32px"
  187 + />
  188 + </div>
182 189 </div>
183 190 <div class="flex justify-between mt-4 text-sm" style="color: #999">
184 191 <div class="flex min-w-20 mr-3">
... ...
... ... @@ -23,6 +23,7 @@
23 23 import { WidgetDataType } from '../../hooks/useDataSource';
24 24 import { ExtraDataSource } from '../../types';
25 25 import { OrderByEnum } from '/@/views/device/localtion/cpns/TimePeriodForm/config';
  26 + import { useApp } from '../../hooks/useApp';
26 27
27 28 type DeviceOption = Record<'label' | 'value' | 'organizationId', string>;
28 29
... ... @@ -38,6 +39,8 @@
38 39
39 40 const historyData = ref<{ ts: number; value: string; name: string }[]>([]);
40 41
  42 + const { getIsAppPage } = useApp();
  43 +
41 44 const { deviceAttrs, getDeviceKeys, getSearchParams, setChartOptions, getDeviceAttribute } =
42 45 useHistoryData();
43 46
... ... @@ -294,7 +297,7 @@
294 297 :destroy-on-close="true"
295 298 :show-ok-btn="false"
296 299 cancel-text="关闭"
297   - width="70%"
  300 + :width="getIsAppPage ? '100%' : '75%'"
298 301 title="历史趋势"
299 302 >
300 303 <section
... ...
  1 +export const Platform = [
  2 + { key: 'iPhone 8', title: 'iPhone 8', width: 375, height: 667 },
  3 + { key: 'iPhone 8 Plus', title: 'iPhone 8 Plus', width: 415, height: 737 },
  4 + { key: 'iPhone X/XS', title: 'iPhone X/XS', width: 376, height: 813 },
  5 + { key: 'iPad 4', title: 'iPad 4', width: 709, height: 1025 },
  6 + { key: 'Galaxy S9', title: 'Galaxy S9', width: 361, height: 741 },
  7 + { key: 'Galaxy S10/S10+', title: 'Galaxy S10/S10+', width: 413, height: 870 },
  8 + { key: 'Pixel 2', title: 'Pixel 2', width: 413, height: 732 },
  9 + { key: 'custom', title: '自定义', width: '', height: '' },
  10 +];
... ...
1 1 <script lang="ts" setup>
2   - import { PageHeader } from 'ant-design-vue';
3   - import { computed, unref } from 'vue';
  2 + import { PageHeader, RadioButton, RadioGroup, InputNumber, Button } from 'ant-design-vue';
  3 + import { computed, ref, unref, onMounted } from 'vue';
4 4 import { useRoute, useRouter } from 'vue-router';
5 5 import { decode } from '../..';
6 6 import { RollbackOutlined } from '@ant-design/icons-vue';
  7 + import { Platform } from './config';
  8 + import { useApp } from '../../hooks/useApp';
  9 + import { getDataComponent } from '/@/api/dataBoard';
  10 + import { updateDataBoard } from '/@/api/dataBoard';
  11 + import { useMessage } from '/@/hooks/web/useMessage';
7 12
8 13 defineProps<{ widgetNumber: number }>();
  14 + const emits = defineEmits(['getPhoneSize']);
9 15
10 16 const ROUTE = useRoute();
11 17 const ROUTER = useRouter();
  18 +
  19 + const { isPhone } = useApp();
12 20 const getIsSharePage = computed(() => {
13 21 return ROUTE.matched.find((item) => item.path === '/share/:viewType/:id/:publicId');
14 22 });
... ... @@ -22,7 +30,85 @@
22 30 ROUTER.go(-1);
23 31 };
24 32
25   - // const handleOpenCreatePanel = () => {};
  33 + const getSize = () => {
  34 + return {
  35 + width: '',
  36 + height: '',
  37 + };
  38 + };
  39 +
  40 + const phoneRadio = ref<string>('iPhone 8');
  41 + const phoneSize = ref<{ width?: number | string; height?: number | string }>(getSize());
  42 + const ifShowWidth = ref<boolean>(false);
  43 +
  44 + const phoneModel = ref<{
  45 + key: string;
  46 + width: string | number;
  47 + height: number | string;
  48 + title: string;
  49 + }>();
  50 + const getDataList = async () => {
  51 + const values = await getDataComponent(decode((ROUTE.params as { boardId: string }).boardId));
  52 + phoneModel.value = values?.data.phoneModel && JSON.parse(values?.data.phoneModel as string);
  53 + phoneRadio.value = unref(phoneModel)?.key as any;
  54 + ifShowWidth.value = unref(phoneRadio) === 'custom' ? true : false;
  55 + phoneSize.value = {
  56 + width: unref(phoneModel)?.width,
  57 + height: unref(phoneModel)?.height,
  58 + };
  59 +
  60 + emits('getPhoneSize', unref(phoneSize));
  61 + };
  62 +
  63 + const updateComponent = () => {
  64 + const { boardId, boardName, organizationId, platform } = ROUTE.params;
  65 +
  66 + const values = {
  67 + id: boardId,
  68 + boardName,
  69 + organizationId,
  70 + platform,
  71 + phoneModel: JSON.stringify({
  72 + key: unref(phoneRadio),
  73 + ...phoneSize.value,
  74 + }),
  75 + };
  76 + updateDataBoard(values as any);
  77 + };
  78 +
  79 + const handleChange = (e) => {
  80 + const { target } = e || {};
  81 + phoneSize.value = getSize();
  82 + if (target.value === 'custom') {
  83 + ifShowWidth.value = true;
  84 + return;
  85 + } else {
  86 + ifShowWidth.value = false;
  87 + const values = Platform.find((item) => item.key == target.value);
  88 + phoneSize.value = {
  89 + width: values?.width || '',
  90 + height: values?.height || '',
  91 + };
  92 + }
  93 + updateComponent();
  94 + emits('getPhoneSize', unref(phoneSize));
  95 + };
  96 +
  97 + const { createMessage } = useMessage();
  98 +
  99 + const handleCustom = () => {
  100 + const { width, height } = unref(phoneSize);
  101 + if (!width || !height) {
  102 + createMessage.warning('请填写尺寸大小');
  103 + return;
  104 + }
  105 + emits('getPhoneSize', unref(phoneSize));
  106 + updateComponent();
  107 + };
  108 +
  109 + onMounted(() => {
  110 + getDataList();
  111 + });
26 112 </script>
27 113
28 114 <template>
... ... @@ -46,6 +132,18 @@
46 132 <span class="mr-3 text-sm text-gray-500">已创建组件:</span>
47 133 <span class="text-blue-500"> {{ widgetNumber }}个</span>
48 134 </div>
  135 + <div v-if="isPhone()" class="flex items-center my-1">
  136 + <RadioGroup v-model:value="phoneRadio" button-style="solid" @change="handleChange">
  137 + <RadioButton v-for="item in Platform" :value="item.key" :key="item.key">{{
  138 + item.title
  139 + }}</RadioButton>
  140 + </RadioGroup>
  141 + <div class="ml-1" v-if="ifShowWidth">
  142 + <InputNumber v-model:value="phoneSize.width" :min="300" placeholder="请输入宽度" />
  143 + <InputNumber v-model:value="phoneSize.height" :min="300" placeholder="请输入高度" />
  144 + <Button type="primary" @click="handleCustom" class="ml-1">确定</Button>
  145 + </div>
  146 + </div>
49 147 </PageHeader>
50 148 </template>
51 149
... ...
... ... @@ -7,6 +7,7 @@
7 7 import { BasicForm } from '/@/components/Form';
8 8 import { BasicModal } from '/@/components/Modal';
9 9 import { nextTick } from 'vue';
  10 + import { useApp } from '../../hooks/useApp';
10 11 const emit = defineEmits(['register', 'getAlarmForm', 'getHistoryForm']);
11 12 // const emit = defineEmits<{
12 13 // (event: 'getAlarmForm', data: WidgetDataType): void;
... ... @@ -15,6 +16,8 @@
15 16 // const fontId = ref('');
16 17 const [registerModal, { closeModal }] = useModalInner(async () => {});
17 18
  19 + const { getIsAppPage } = useApp();
  20 +
18 21 const [register, method] = useForm({
19 22 schemas: formSchema(),
20 23 baseColProps: useGridLayout(1) as unknown as ColEx,
... ... @@ -53,7 +56,7 @@
53 56 :destroy-on-close="true"
54 57 :show-ok-btn="true"
55 58 cancel-text="关闭"
56   - width="40%"
  59 + :width="getIsAppPage ? '80%' : '40%'"
57 60 title="历史趋势"
58 61 >
59 62 <section
... ...
  1 +import { computed } from 'vue';
  2 +import { useRoute } from 'vue-router';
  3 +
  4 +export const useApp = () => {
  5 + const ROUTE = useRoute();
  6 + const getIsAppPage = computed(() => {
  7 + return ROUTE.matched.find((item) => item.path === '/appPage/:boardId/:userId');
  8 + });
  9 +
  10 + const isPhone = () => {
  11 + const values = location?.pathname.split('/') || [];
  12 + return values[values?.length - 2] === 'phone' ? true : false;
  13 + };
  14 +
  15 + return { getIsAppPage, isPhone };
  16 +};
... ...
1 1 import { unref } from 'vue';
2 2 import { Layout } from 'vue3-grid-layout';
3   -import { DEFAULT_MAX_COL, DEFAULT_WIDGET_HEIGHT, DEFAULT_WIDGET_WIDTH } from '..';
  3 +import { DEFAULT_MAX_COL, DEFAULT_WIDGET_HEIGHT, DEFAULT_WIDGET_WIDTH, PHONE_SIZE } from '..';
  4 +import { useApp } from './useApp';
4 5
5 6 interface GapRecord {
6 7 maxGap: number;
... ... @@ -8,9 +9,14 @@ interface GapRecord {
8 9 endIndex: Nullable<number>;
9 10 }
10 11
  12 +const { isPhone } = useApp();
  13 +
11 14 export const useCalcNewWidgetPosition = (
12 15 layoutInfo: Layout[],
13   - randomLayout = { width: DEFAULT_WIDGET_WIDTH, height: DEFAULT_WIDGET_HEIGHT }
  16 + randomLayout = {
  17 + width: isPhone() ? PHONE_SIZE.DEFAULT_WIDGET_WIDTH : DEFAULT_WIDGET_WIDTH,
  18 + height: isPhone() ? PHONE_SIZE.DEFAULT_WIDGET_HEIGHT : DEFAULT_WIDGET_HEIGHT,
  19 + }
14 20 ) => {
15 21 let maxWidth = 0;
16 22 let maxHeight = 0;
... ...
... ... @@ -30,6 +30,10 @@ export const useDataSource = (propsRef: ComputedRef<Recordable>) => {
30 30 const getIsSharePage = computed(() => {
31 31 return ROUTE.matched.find((item) => item.path === '/share/:viewType/:id/:publicId');
32 32 });
  33 + //小程序打开的页面
  34 + const getIsAppPage = computed(() => {
  35 + return ROUTE.matched.find((item) => item.path === '/appPage/:boardId/:userId');
  36 + });
33 37
34 38 const getBoardId = computed(() => {
35 39 return decode((ROUTE.params as { boardId: string }).boardId);
... ... @@ -131,8 +135,8 @@ export const useDataSource = (propsRef: ComputedRef<Recordable>) => {
131 135
132 136 return {
133 137 loading,
134   - draggable: !unref(getIsSharePage),
135   - resizable: !unref(getIsSharePage),
  138 + draggable: !unref(getIsSharePage) && !unref(getIsAppPage),
  139 + resizable: !unref(getIsSharePage) && !unref(getIsAppPage),
136 140 dataSource,
137 141 rawDataSource,
138 142 getDataSource,
... ...
... ... @@ -11,6 +11,12 @@ export const DEFAULT_WIDGET_HEIGHT = 6;
11 11 export const DEFAULT_MIN_HEIGHT = 5;
12 12 export const DEFAULT_MIN_WIDTH = 3;
13 13 export const DEFAULT_ITEM_MARIGN = 20;
  14 +export const PHONE_SIZE = {
  15 + DEFAULT_WIDGET_WIDTH: 12,
  16 + DEFAULT_WIDGET_HEIGHT: 4,
  17 + DEFAULT_MIN_HEIGHT: 4,
  18 + DEFAULT_MIN_WIDTH: 11,
  19 +};
14 20
15 21 import { ViewTypeEnum } from '/@/views/sys/share/config/config';
16 22
... ...
... ... @@ -10,6 +10,7 @@
10 10 DEFAULT_MIN_WIDTH,
11 11 DEFAULT_ITEM_MARIGN,
12 12 VisualComponentPermission,
  13 + PHONE_SIZE,
13 14 } from './index';
14 15 import { useDragGridLayout } from './hooks/useDragGridLayout';
15 16 import { WidgetHeader, WidgetWrapper } from './components/WidgetWrapper';
... ... @@ -19,6 +20,7 @@
19 20 import { DataSourceBindPanel } from '../dataSourceBindPanel';
20 21 import { PageHeader } from './components/PagerHeader';
21 22 import { useShare } from './hooks/useShare';
  23 + import { useApp } from './hooks/useApp';
22 24 import { useRole } from '/@/hooks/business/useRole';
23 25 import { Authority } from '/@/components/Authority';
24 26 import { useModal } from '/@/components/Modal';
... ... @@ -33,6 +35,11 @@
33 35 import { useSocket } from '/@/views/visual/packages/hook/socket/useSocket';
34 36 import { createAlarmContext } from './hooks/useAlarmTime';
35 37 import { createHistoryContext } from './hooks/useHistoryForm';
  38 + import { MoreOutlined, LeftOutlined } from '@ant-design/icons-vue';
  39 + import WIFISVG from '/@/assets/svg/wifi.svg';
  40 + import SIGNALSVG from '/@/assets/svg/signal.svg';
  41 + import BATTERYSVG from '/@/assets/svg/battery.svg';
  42 + import { useRoute } from 'vue-router';
36 43
37 44 const props = defineProps<{
38 45 value?: Recordable;
... ... @@ -44,6 +51,8 @@
44 51
45 52 const containerRectRef = ref<DOMRect>({} as unknown as DOMRect);
46 53
  54 + const ROUTE = useRoute();
  55 +
47 56 const { loading, draggable, resizable, dataSource, rawDataSource, setLayoutInfo, getDataSource } =
48 57 useDataSource(getProps);
49 58
... ... @@ -63,6 +72,9 @@
63 72 }
64 73 });
65 74 const { getIsSharePage } = useShare();
  75 +
  76 + // getIsAppPage 是否是小程序进入的页面 isPhone 是否是创建的手机端
  77 + const { getIsAppPage, isPhone } = useApp();
66 78 const { isCustomerUser } = useRole();
67 79 const handleOpenCreatePanel = () => {
68 80 openModal(true, { mode: DataActionModeEnum.CREATE } as ModalParamsType);
... ... @@ -157,6 +169,28 @@
157 169 },
158 170 { immediate: true }
159 171 );
  172 + watch(
  173 + getIsAppPage,
  174 + (value) => {
  175 + if (value) {
  176 + const root = document.querySelector('#app');
  177 + (root as HTMLDivElement).style.backgroundColor =
  178 + unref(getDarkMode) === ThemeEnum.LIGHT ? '#F5F5F5' : '#1b1b1b';
  179 + }
  180 + },
  181 + { immediate: true }
  182 + );
  183 + const getDataBoardName = computed(() => {
  184 + return decodeURIComponent((ROUTE.params as { boardName: string }).boardName || '');
  185 + });
  186 +
  187 + const phoneSize = ref({
  188 + width: '' || 375,
  189 + height: '' || 667,
  190 + });
  191 + const getPhoneSize = (values) => {
  192 + phoneSize.value = { ...values };
  193 + };
160 194 </script>
161 195
162 196 <template>
... ... @@ -164,7 +198,11 @@
164 198 ref="containerRefEl"
165 199 class="palette w-full h-full flex-col bg-neutral-100 flex dark:bg-dark-700 dark:text-light-50"
166 200 >
167   - <PageHeader :widget-number="dataSource.length">
  201 + <PageHeader
  202 + v-if="!getIsAppPage"
  203 + :widget-number="dataSource.length"
  204 + @getPhoneSize="getPhoneSize"
  205 + >
168 206 <Authority :value="VisualComponentPermission.CREATE">
169 207 <Button
170 208 v-if="!getIsSharePage && !isCustomerUser"
... ... @@ -177,55 +215,104 @@
177 215 </PageHeader>
178 216
179 217 <Spin :spinning="loading">
180   - <GridLayout
181   - v-model:layout="dataSource"
182   - :col-num="DEFAULT_MAX_COL"
183   - :row-height="30"
184   - :margin="[DEFAULT_ITEM_MARIGN, DEFAULT_ITEM_MARIGN]"
185   - :is-draggable="draggable"
186   - :is-resizable="resizable"
187   - :vertical-compact="true"
188   - :use-css-transforms="true"
189   - style="width: 100%"
190   - >
191   - <GridItem
192   - v-for="item in dataSource"
193   - :key="item.i"
194   - :static="item.static"
195   - :x="item.x"
196   - :y="item.y"
197   - :w="item.w"
198   - :h="item.h"
199   - :i="item.i"
200   - :min-h="DEFAULT_MIN_HEIGHT"
201   - :min-w="DEFAULT_MIN_WIDTH"
202   - :style="{ display: 'flex', flexWrap: 'wrap' }"
203   - class="grid-item-layout"
204   - @resized="resized"
205   - @resize="resize"
206   - @moved="moved"
207   - @container-resized="containerResized"
208   - drag-ignore-from=".no-drag"
  218 + <div class="w-full h-full flex justify-center items-center mb-3">
  219 + <div
  220 + :style="
  221 + !getIsAppPage && isPhone()
  222 + ? {
  223 + width: phoneSize.width + 'px',
  224 + height: phoneSize.height + 'px',
  225 + border: '2px solid #e5e7eb',
  226 + }
  227 + : { width: '100%', height: '100%' }
  228 + "
  229 + style="border-radius: 1%"
209 230 >
210   - <WidgetWrapper>
211   - <template #header>
212   - <WidgetHeader
213   - :raw-data-source="rawDataSource"
214   - :source-info="item"
215   - @update="handleUpdateWidget"
216   - @open-trend="handleOpenTrend"
217   - @open-alarm="handleOpenAlarm"
218   - @ok="getDataSource"
219   - />
220   - </template>
221   - <WidgetDistribute :source-info="item" />
222   - </WidgetWrapper>
223   - </GridItem>
224   - </GridLayout>
225   - <Empty
226   - v-if="!dataSource.length"
227   - class="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2"
228   - />
  231 + <!-- 手机端写的模拟样式 -->
  232 + <div
  233 + v-if="!getIsAppPage && isPhone()"
  234 + style="height: 60px; background: white"
  235 + class="px-1 py-1 relative"
  236 + >
  237 + <div class="flex justify-between">
  238 + <div>thingskit</div>
  239 + <div class="flex items-center">
  240 + <img :src="WIFISVG" alt="" class="w-3 h-3" />
  241 + <img :src="SIGNALSVG" alt="" class="w-3 h-3 mx-1" />
  242 + <img :src="BATTERYSVG" alt="" class="w-4 h-4 rotate-45" />
  243 + <h1 class="mb-0">18:13</h1>
  244 + </div>
  245 + </div>
  246 + <div class="flex items-center justify-between">
  247 + <LeftOutlined class="transform cursor-pointer text-lg" />
  248 + <h1 class="font-bold">{{ getDataBoardName }}</h1>
  249 + <MoreOutlined class="transform rotate-90 cursor-pointer text-lg" />
  250 + </div>
  251 + </div>
  252 +
  253 + <div
  254 + id="appLayoutId"
  255 + :style="
  256 + !getIsAppPage && isPhone() ? { height: `calc(${phoneSize.height}px - 67px)` } : {}
  257 + "
  258 + class="overflow-y-scroll"
  259 + >
  260 + <GridLayout
  261 + v-model:layout="dataSource"
  262 + :col-num="DEFAULT_MAX_COL"
  263 + :row-height="30"
  264 + :margin="[DEFAULT_ITEM_MARIGN, DEFAULT_ITEM_MARIGN]"
  265 + :is-draggable="draggable"
  266 + :is-resizable="resizable"
  267 + :vertical-compact="true"
  268 + :use-css-transforms="true"
  269 + style="width: 100%"
  270 + :style="{
  271 + '--is-app': isPhone() ? 'auto' : 'none',
  272 + }"
  273 + >
  274 + <GridItem
  275 + v-for="item in dataSource"
  276 + :key="item.i"
  277 + :static="item.static"
  278 + :x="item.x"
  279 + :y="item.y"
  280 + :w="item.w"
  281 + :h="item.h"
  282 + :i="item.i"
  283 + :min-h="isPhone() ? PHONE_SIZE.DEFAULT_MIN_HEIGHT : DEFAULT_MIN_HEIGHT"
  284 + :min-w="isPhone() ? PHONE_SIZE.DEFAULT_MIN_WIDTH : DEFAULT_MIN_WIDTH"
  285 + :style="{ display: 'flex', flexWrap: 'wrap' }"
  286 + class="grid-item-layout"
  287 + @resized="resized"
  288 + @resize="resize"
  289 + @moved="moved"
  290 + @container-resized="containerResized"
  291 + drag-ignore-from=".no-drag"
  292 + >
  293 + <WidgetWrapper>
  294 + <template #header>
  295 + <WidgetHeader
  296 + :raw-data-source="rawDataSource"
  297 + :source-info="item"
  298 + @update="handleUpdateWidget"
  299 + @open-trend="handleOpenTrend"
  300 + @open-alarm="handleOpenAlarm"
  301 + @ok="getDataSource"
  302 + />
  303 + </template>
  304 + <WidgetDistribute :source-info="item" />
  305 + </WidgetWrapper>
  306 + </GridItem>
  307 + </GridLayout>
  308 + </div>
  309 + <Empty
  310 + v-if="!dataSource.length"
  311 + :class="isPhone() ? 'absolute' : 'fixed'"
  312 + class="top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2"
  313 + />
  314 + </div>
  315 + </div>
229 316 </Spin>
230 317
231 318 <DataSourceBindPanel @register="register" :layout="dataSource" @ok="getDataSource" />
... ... @@ -238,4 +325,9 @@
238 325 </section>
239 326 </template>
240 327
241   -<style lang="less" scoped></style>
  328 +<style lang="less">
  329 + .vue-grid-item {
  330 + pointer-events: painted;
  331 + touch-action: var(--is-app) !important;
  332 + }
  333 +</style>
... ...
... ... @@ -91,6 +91,7 @@ export interface ComponentLayoutType {
91 91 export interface ApiDataBoardDataType {
92 92 componentData: ComponentDataType[];
93 93 componentLayout: ComponentLayoutType[];
  94 + phoneModel?: string;
94 95 }
95 96
96 97 export interface ApiDataBoardInfoType {
... ...