Quellcode durchsuchen

feat: 添加视图页面

liaojiaxing vor 10 Monaten
Ursprung
Commit
30bff83e7d

+ 1 - 1
README.md

@@ -21,7 +21,7 @@ components/Charts/
 │   └── BasicBar // 基础柱状图
 |         ├── index.ts // 暴露组件相关信息
 |         └── src
-|             ├── index.vue // 图表组件
+|             ├── BasicBar.vue // 图表组件
 |             ├── Config.vue // 配置组件
 |             └── props.ts // 组件属性及组件初始数据
 ...其他图表组件

+ 5 - 5
src/components/Charts/Line/BasicLine/src/props.ts

@@ -76,12 +76,12 @@ export const defaultPropsValue: EChartsOption = {
         xData: ['轴标签A', '轴标签B', '轴标签C', '轴标签D'],
         series: [
           {
-            type: 'bar',
+            type: 'line',
             name: '系列1',
             data: [89.3, 92.1, 94.4, 85.4]
           },
           {
-            type: 'bar',
+            type: 'line',
             name: '系列2',
             data: [95.8, 89.4, 91.2, 76.9]
           },
@@ -100,9 +100,9 @@ export const defaultPropsValue: EChartsOption = {
           const xData = data.map((item) => item.name); 
           // 系列数据
           const series = [
-            { type: 'bar', name: '苹果', data: data.map(item => item.apple) },
-            { type: 'bar', name: 'VIVO', data: data.map(item => item.vivo) },
-            { type: 'bar', name: '小米', data: data.map(item => item.mi) },
+            { type: 'line', name: '苹果', data: data.map(item => item.apple) },
+            { type: 'line', name: 'VIVO', data: data.map(item => item.vivo) },
+            { type: 'line', name: '小米', data: data.map(item => item.mi) },
           ];
 
           // 返回图表数据

+ 3 - 3
src/components/index.ts

@@ -1,9 +1,9 @@
 // 暴露大屏组件资源
-const allComponents = {
+const componentAll = {
   Title: () => import("@/components/Text/Title"),
   BasicLine: () => import("@/components/Charts/Line/BasicLine"),
   BasicBar: () => import("@/components/Charts/Bar/BasicBar"),
 };
 
-export default allComponents;
-export type ComponentType = keyof typeof allComponents;
+export default componentAll;
+export type ComponentType = keyof typeof componentAll;

+ 4 - 0
src/router/index.ts

@@ -9,6 +9,10 @@ const routes: Array<RouteRecordRaw> = [
     path: '/designer',
     component: () => import('@/views/designer/index.vue'),
   },
+  {
+    path: '/view',
+    component: () => import('@/views/view/index.vue'),
+  },
   {
     path: '/:pathMatch(.*)*',
     component: () => import('@/views/system/404.vue')

+ 6 - 3
src/store/modules/project.ts

@@ -18,7 +18,7 @@ type ProjectState = {
       props: Record<string, any>;
     };
   } | null;
-  mode: "edit" | "preview";
+  mode: "edit" | "player";
   selectedElementKeys: number[];
 };
 const defaultPage: Page = {
@@ -44,7 +44,7 @@ export const useProjectStore = defineStore({
       sizeType: "",
       width: 0,
       height: 0,
-      fillType: ScreenFillEnum.Auto,
+      fillType: ScreenFillEnum.AUTO,
       pages: [{ ...defaultPage }],
     },
     // 当前编辑页面索引
@@ -177,7 +177,7 @@ export const useProjectStore = defineStore({
     clearAddCompData() {
       this.addCompData = null;
     },
-    setMode(mode: "edit" | "preview") {
+    setMode(mode: "edit" | "player") {
       this.mode = mode;
     },
     // 设置选中的元素
@@ -191,6 +191,9 @@ export const useProjectStore = defineStore({
     // 设置当前页面背景
     setCurrentPageBackground(background: any) {
       this.projectInfo.pages[this.activePageIndex].background = background;
+    },
+    setFillType(fillType: ScreenFillEnum) {
+      this.projectInfo.fillType = fillType;
     }
   },
 });

+ 0 - 4
src/views/designer/component/ComponentWrapper.vue

@@ -70,10 +70,6 @@ const warpperStyle = computed(() => {
     height = 260,
     x,
     y,
-    paddingLeft = 0,
-    paddingRight = 0,
-    paddingTop = 0,
-    paddingBottm = 0,
   } = componentData.container.props || {};
   const style = transformStyle(componentData.container?.style || {});
   

+ 31 - 12
src/views/designer/component/Workspace.vue

@@ -7,17 +7,26 @@
     <Flex class="workspace-bottom" justify="space-between" align="center">
       <div class="bottom-left">
         <span style="margin-right: 12px"
-          >画布尺寸:{{ projectStore.projectInfo.width }}*{{ projectStore.projectInfo.height }}px</span
+          >画布尺寸:{{ projectStore.projectInfo.width }} *
+          {{ projectStore.projectInfo.height }}px</span
         >
-        <span>画布自适应:<Select size="small" style="width: 120px" /></span>
+        <span
+          >画布自适应:<Select
+            size="small"
+            style="width: 120px"
+            :value="projectStore.projectInfo.fillType"
+            :options="fillOptions"
+            @change="projectStore.setFillType"
+        /></span>
       </div>
       <div class="bottom-right">
-        <Button 
+        <Button
           size="small"
           type="text"
           :disabled="stageStore.scale <= 0.1"
           @click="handleSizeChange(Number(stageStore.scale) - 0.1)"
-        ><MinusCircleOutlined /></Button>
+          ><MinusCircleOutlined
+        /></Button>
         <AutoComplete
           size="small"
           style="width: 120px"
@@ -30,7 +39,8 @@
           type="text"
           :disabled="stageStore.scale >= 4"
           @click="handleSizeChange(Number(stageStore.scale) + 0.1)"
-        ><PlusCircleOutlined /></Button>
+          ><PlusCircleOutlined
+        /></Button>
       </div>
     </Flex>
   </Flex>
@@ -43,6 +53,7 @@ import Scaleplate from "./Scaleplate.vue";
 import Stage from "./Stage.vue";
 import { useProjectStore } from "@/store/modules/project";
 import { useStageStore } from "@/store/modules/stage";
+import { ScreenFillEnum } from "@/enum/screenFillEnum";
 
 const projectStore = useProjectStore();
 const stageStore = useStageStore();
@@ -58,16 +69,24 @@ const sizeOptions = [
   { value: 2, label: "200%" },
   { value: 3, label: "300%" },
   { value: 4, label: "400%" },
-  { value: 0, label: "适应大小" }
+  { value: 0, label: "适应大小" },
+];
+
+const fillOptions = [
+  { value: ScreenFillEnum.AUTO, label: "自动" },
+  { value: ScreenFillEnum.FILL_HEIGHT, label: "宽度填充" },
+  { value: ScreenFillEnum.FILL_WIDTH, label: "高度填充" },
+  { value: ScreenFillEnum.FILL_BOTH, label: "双向铺满" },
+  { value: ScreenFillEnum.NONE, label: "无" },
 ];
 
 const handleSizeChange = (val: any) => {
-  if(Number.isFinite(val)) {
-    stageStore.setScale(val as number);
-  } 
-  if(typeof val === "string") {
-    const n = +((val + '').replace("%", ""));
-    if(Number.isNaN(n) && n >= 10 && n <= 400) {
+  if (Number.isFinite(val)) {
+    stageStore.setScale((val as number) < 0.1 ? 0.1 : val);
+  }
+  if (typeof val === "string") {
+    const n = +(val + "").replace("%", "");
+    if (Number.isNaN(n) && n >= 10 && n <= 400) {
       stageStore.setScale((n / 100).toFixed(2) as unknown as number);
     } else {
       stageStore.setScale(0.1);

+ 16 - 2
src/views/designer/index.vue

@@ -8,8 +8,8 @@
         <MenuBar />
       </div>
       <div class="header-right">
-        <Button size="small" style="margin-right: 8px;"><DesktopOutlined/>预览</Button>
-        <Button size="small" type="primary"><SaveOutlined/>保存</Button>
+        <Button size="small" style="margin-right: 8px;" @click="handlePreview"><DesktopOutlined/>预览</Button>
+        <Button size="small" type="primary" @click="handleSave" :loading="loading"><SaveOutlined/>保存</Button>
       </div>
     </LayoutHeader>
 
@@ -35,6 +35,7 @@
 </template>
 
 <script lang="ts" setup>
+import { ref } from "vue";
 import {
   Layout,
   LayoutHeader,
@@ -51,7 +52,20 @@ import Configurator from './component/Configurator.vue';
 import MenuBar from './component/MenuBar.vue';
 
 const projectStore = useProjectStore();
+const loading = ref(false);
 
+const handlePreview = () => {
+  console.log('预览');
+  localStorage.setItem('currentProject', JSON.stringify(projectStore.projectInfo));
+  window.open('#/view?id=1');
+}
+const handleSave = () => {
+  loading.value = true;
+  // TODO 保存项目
+  setTimeout(() => {
+    loading.value = false;
+  }, 1000);
+}
 </script>
 
 <style lang="less" scoped>

+ 29 - 0
src/views/view/component/RenderComponent.vue

@@ -0,0 +1,29 @@
+<template>
+  <div :style="containerStyle">
+    <component :is="component" v-bind="element.props" :width="element.container.props.width" :height="element.container.props.height"/>
+  </div>
+</template>
+
+<script setup lang="ts">
+import type { CustomElement } from '#/project';
+import { defineProps, ref, onMounted, defineAsyncComponent } from 'vue';
+import componentAll from '@/components';
+
+const props = defineProps<{
+  element: CustomElement;
+}>();
+const component = defineAsyncComponent(componentAll[props.element.componentType]);
+const containerStyle = ref({
+  width: `${props.element.container.props.width}px`,
+  height: `${props.element.container.props.height}px`,
+  position: 'absolute',
+  left: `${props.element.container.props.x}px`,
+  top: `${props.element.container.props.y}px`,
+  transform: `rotate(${props.element.container.props?.rotate}deg)`,
+  zIndex: props.element.zIndex,
+});
+</script>
+
+<style scoped>
+
+</style>

+ 98 - 0
src/views/view/index.vue

@@ -0,0 +1,98 @@
+<template>
+  <div class="player-page" :style="bodyStyle">
+    <div class="page-wrapper" :style="pageWapperStyle">
+      <RenderComponent v-for="element in currentPage?.elements" :key="element.key" :element="element"/>
+      <Result
+        v-if="!currentPage"
+        status="warning"
+        title="很抱歉!当前项目未知错误."
+        subTitle="请联系管理员"
+      >
+        <template #extra>
+          <Button key="console" type="primary" @click="$router.replace('/')">返回首页</Button>
+        </template>
+      </Result>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import type { ProjectInfo } from '#/project';
+import { ref, onMounted, onBeforeUnmount, StyleValue } from "vue";
+import { Result, Button } from "ant-design-vue";
+import RenderComponent from "./component/RenderComponent.vue";
+import { ScreenFillEnum } from "@/enum/screenFillEnum";
+
+const projectInfo = JSON.parse(localStorage.getItem("currentProject") || "{}") as ProjectInfo;
+document.title = `${projectInfo?.name || '沙鲁大屏项目'}--沙鲁低码平台`;
+const currentPage = ref(projectInfo.pages?.[0]);
+const pageWapperStyle = ref<StyleValue>();
+const bodyStyle = ref<StyleValue>();
+
+// 页面样式
+const getWapperStyle = () => {
+  if (!currentPage.value) return;
+
+  const { background } = currentPage.value;
+  const pageBackground =
+    background.type === "color"
+      ? { background: background.color }
+      : {
+          background: `url(${background.image}) no-repeat center center`,
+          backgroundSize: background.fillType,
+        };
+  const { width = 1280, height = 720 } = projectInfo;
+  const { clientWidth, clientHeight } = document.documentElement;
+
+  let scale: string | number = 1;
+  switch (projectInfo.fillType) {
+    case ScreenFillEnum.FILL_HEIGHT:
+      scale = clientHeight / height;
+      break;
+    case ScreenFillEnum.FILL_WIDTH:
+      scale = clientWidth / width;
+      break;
+    case ScreenFillEnum.FILL_BOTH:
+      const scaleX = clientWidth / width;
+      const scaleY = clientHeight / height;
+      scale = `${scaleX},${scaleY}`;
+      break;
+    default:
+      scale = Math.min(clientWidth / width, clientHeight / height);
+  }
+
+  bodyStyle.value = {
+    background: '#000',
+  } as unknown as StyleValue;
+  pageWapperStyle.value = {
+    position: 'absolute',
+    left: '50%',
+    top: '50%',
+    width: `${width}px`,
+    height: `${height}px`,
+    overflow: "hidden",
+    border: "1px solid #f0f0f0",
+    boxShadow: "0 0 10px rgba(0, 0, 0, 0.1)",
+    transform: `scale(${scale}) translate(-50%, -50%)`,
+    transformOrigin: "0 0",
+    ...pageBackground,
+  } as unknown as StyleValue;
+};
+
+onMounted(() => {
+  getWapperStyle();
+  window.addEventListener("resize", getWapperStyle);
+});
+onBeforeUnmount(() => {
+  window.removeEventListener("resize", getWapperStyle);
+});
+</script>
+
+<style lang="less" scoped>
+.player-page {
+  height: 100%;
+  width: 100%;
+  position: fixed;
+  overflow: hidden;
+}
+</style>