瀏覽代碼

feat: 添加在线保存和读取功能

liaojiaxing 10 月之前
父節點
當前提交
6e858cd138

+ 1 - 0
.env

@@ -0,0 +1 @@
+VITE_APP_BASE_URL=http://edesign.shalu.com

+ 1 - 0
.env.production

@@ -0,0 +1 @@
+VITE_APP_BASE_URL=''

+ 6 - 1
src/App.vue

@@ -3,10 +3,13 @@ import { ConfigProvider } from 'ant-design-vue'
 import zhCN from 'ant-design-vue/es/locale/zh_CN'
 import dayjs from 'dayjs'
 import 'dayjs/locale/zh-cn'
+import { Spin } from 'ant-design-vue'
+import { useAppStore } from './store/modules/app'
 
 import { ElConfigProvider } from 'element-plus'
 import zhcn from 'element-plus/es/locale/lang/zh-cn'
 
+const appStore = useAppStore()
 dayjs.locale('zh-cn')
 
 </script>
@@ -14,7 +17,9 @@ dayjs.locale('zh-cn')
 <template>
   <ConfigProvider :locale="zhCN">
     <ElConfigProvider :locale="zhcn">
-      <router-view />
+      <Spin :spinning="appStore.loading" tip="加载中...">
+        <router-view />
+      </Spin>
     </ElConfigProvider>
   </ConfigProvider>
 </template>

+ 6 - 7
src/api/index.ts

@@ -1,9 +1,8 @@
-import { http } from '@/utils/http';
+import { http } from "@/utils/http";
+import type { PageParams, PageResponse } from "./model";
 
-export const getImgList = () => {
-  return http.get('/get/resource/image-list');
-}
+export const editPageDesignApi = (data: PageParams) =>
+  http.post("/api/form/EditPageDesign", data);
 
-export const getBarList = (data: any) => {
-  return http.post('/get/example/bar/list', data);
-}
+export const getPageDesignApi = (data: { id: string}) => 
+  http.post<PageResponse>(`/api/form/GetPageDesign`, data);

+ 22 - 0
src/api/model.ts

@@ -0,0 +1,22 @@
+/* 保存页面数据 */
+export type PageParams = {
+  appPageId: string;
+  json: string;
+  type: number;
+  html: string;
+  js: string;
+}
+
+/* 获取页面数据 */
+export type PageResponse = {
+  appPageId: string;
+  creationTime: string;
+  fileName: string;
+  id: string;
+  isDeleted: boolean;
+  mobileHtml: string;
+  mobileJson: string;
+  pageName: string;
+  pcHtml: string;
+  pcJson: string;
+}

+ 0 - 51
src/config/chartDefaultConfig.ts

@@ -1,51 +0,0 @@
-import { EChartsOption } from "echarts"
-import { colorPreset } from "./colorDefaultConfig"
-
-// 图表默认配置项
-export const chartDefaultConfig: EChartsOption = {
-  // 调色盘-预设颜色
-  color: colorPreset[0].color,
-  // 标题
-  title: {
-    left: "center",
-    top: 8,
-    textStyle: {
-      color: "#fff",
-      fontSize: 16,
-    },
-  },
-  // 图例
-  legend: {
-    textStyle: {
-      color: "#fff",
-    },
-    top: 32,
-  },
-  // 布局
-  grid: {
-    bottom: 34,
-    right: 20,
-    top: 60,
-  },
-  // 提示框
-  tooltip: {},
-  // x轴
-  xAxis: {
-    type: "category",
-    axisLabel: {
-      color: "#9fadbf",
-    },
-  },
-  // y轴
-  yAxis: {
-    axisLabel: {
-      color: "#9fadbf",
-    },
-    splitLine: {
-      lineStyle: {
-        type: "dashed",
-        color: "#36485f",
-      },
-    },
-  },
-}

+ 0 - 23
src/config/colorDefaultConfig.ts

@@ -1,23 +0,0 @@
-// 颜色预设集合
-export const colorPreset = [
-  {
-    name: '清新',
-    color: ['#00a8e1', '#99cc00', '#e30039', '#fcd300', '#800080']
-  },
-  {
-    name: '复古',
-    color: ['#FFA69E', '#FAE3D9', '#B8F2E6', '#56E39F', '#3A837D']
-  },
-  {
-    name: '商务',
-    color: ['#194f97', '#555555', '#bd6b08', '#00686b', '#c82d31']
-  },
-  {
-    name: '经典',
-    color: ['#002c53', '#ffa510', '#0c84c6', '#ffffff', '#f74d4d']
-  },
-  {
-    name: '怀旧',
-    color: ['#3b6291', '#943c39', '#779043', '#624c7c', '#388498']
-  }
-]

+ 15 - 0
src/store/modules/app.ts

@@ -0,0 +1,15 @@
+import { defineStore } from "pinia";
+
+export const useAppStore = defineStore({
+  id: 'app',
+  state() {
+    return {
+      loading: false
+    }
+  },
+  actions: {
+    setPageLoading(loading: boolean) {
+      this.loading = loading
+    }
+  }
+})

+ 34 - 10
src/store/modules/project.ts

@@ -4,6 +4,8 @@ import { asyncComponentAll } from "shalu-dashboard-ui";
 import { ScreenFillEnum } from "@/enum/screenFillEnum";
 import { update, defaultsDeep } from "lodash";
 import { getNormalizedContainer } from "@/utils/common";
+import { editPageDesignApi } from "@/api/index";
+import { message } from "ant-design-vue";
 
 type ProjectState = {
   projectInfo: ProjectInfo;
@@ -38,6 +40,7 @@ export const useProjectStore = defineStore({
   state: (): ProjectState => ({
     // 项目信息
     projectInfo: {
+      pageId: "",
       name: "",
       description: "",
       sizeType: "",
@@ -69,7 +72,7 @@ export const useProjectStore = defineStore({
       return state.projectInfo.pages[state.activePageIndex].elements.filter(
         (item) => state.selectedElementKeys.includes(item.key)
       );
-    }
+    },
   },
   actions: {
     setProjectInfo(info: any) {
@@ -78,7 +81,7 @@ export const useProjectStore = defineStore({
     },
     getCurrentProjectInfo(): ProjectInfo | undefined {
       let info = JSON.parse(localStorage.getItem(CURRENT_PROJECT) || "null");
-      if(!info) {
+      if (!info) {
         info = {
           name: "默认项目",
           description: "这是一个默认项目",
@@ -87,11 +90,14 @@ export const useProjectStore = defineStore({
           height: 720,
           fillType: ScreenFillEnum.AUTO,
           pages: [{ ...defaultPage }],
-        }
+        };
       }
       this.setProjectInfo(info as unknown as ProjectInfo);
       return info;
     },
+    updateProjectInfo(info: any) {
+      Object.assign(this.projectInfo, info);
+    },
     addReferLine(line: ReferLine) {
       this.projectInfo.pages[this.activePageIndex].referLines.push(line);
     },
@@ -119,8 +125,10 @@ export const useProjectStore = defineStore({
       const { defaultPropsValue } =
         (await asyncComponentAll[element.componentType]?.()) || {};
 
-      const { width = 400, height = 260 } = defaultPropsValue?.container?.props  || {};
-      const { props: containerProps = {}, style = {} } = defaultPropsValue?.container || {};
+      const { width = 400, height = 260 } =
+        defaultPropsValue?.container?.props || {};
+      const { props: containerProps = {}, style = {} } =
+        defaultPropsValue?.container || {};
 
       const index =
         elements.filter((item) => item.componentType === element.componentType)
@@ -151,9 +159,11 @@ export const useProjectStore = defineStore({
     },
     // 更新组件
     updateElement(key: number, path: string, payload: any) {
-      const element = this.projectInfo.pages[
-        this.activePageIndex
-      ].elements.find((item) => item.key === key);
+      const pageIndex = this.activePageIndex;
+
+      const element = this.projectInfo.pages[pageIndex].elements.find((item) => item.key === key);
+      const elementIndex = this.projectInfo.pages[pageIndex].elements.findIndex((item) => item.key === key);
+
       // 如果是锁定状态不能修改宽高 位置
       if (
         element &&
@@ -168,7 +178,7 @@ export const useProjectStore = defineStore({
         return;
 
       if (element) {
-        update(element, path, () => payload);
+        update(this.projectInfo.pages[pageIndex].elements[elementIndex], path, () => payload);
       }
     },
     // 删除组件
@@ -205,6 +215,20 @@ export const useProjectStore = defineStore({
     },
     setFillType(fillType: ScreenFillEnum) {
       this.projectInfo.fillType = fillType;
-    }
+    },
+
+    // 保存当前项目到服务器
+    async handleSaveProject() {
+      const params = {
+        appPageId: this.projectInfo.pageId,
+        json: JSON.stringify(this.projectInfo),
+        html: "",
+        js: "",
+        type: 0,
+      };
+
+      editPageDesignApi(params);
+      message.success("保存成功");
+    },
   },
 });

+ 0 - 11
src/utils/common.ts

@@ -1,8 +1,6 @@
 import type { DataSource } from '#/echart';
 import { defaultsDeep } from "lodash";
 import { containerDefaultConfig } from "@/config/containerDefaultConfig";
-import { chartDefaultConfig } from "@/config/chartDefaultConfig";
-import { EChartsOption } from "echarts";
 import { PropType } from "vue";
 import { DataSourceType } from "@/enum/index";
 
@@ -15,15 +13,6 @@ export function getNormalizedContainer(config: Record<string, any>) {
   return defaultsDeep(config, containerDefaultConfig);
 }
 
-/**
- * 获取图表组件属性
- *  @param {Record<string, any>} config - 传入属性
- *  @returns {Record<string, any>} - 返回与默认配置合并后的属性
- */
-export function getNormalizedChart(config: EChartsOption) {
-  return defaultsDeep(config, chartDefaultConfig);
-}
-
 // 图表组件数据来源prop
 export const dataSource = {
   type: Object as PropType<DataSource>,

+ 48 - 16
src/utils/http/axios.ts

@@ -1,4 +1,4 @@
-import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
+import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
 
 interface ApiService {
   get<T>(url: string, config?: AxiosRequestConfig): Promise<T>;
@@ -7,34 +7,66 @@ interface ApiService {
   delete<T>(url: string, config?: AxiosRequestConfig): Promise<T>;
 }
 
+type RequestOptions = {
+  transformRequest?: AxiosRequestConfig['transformRequest'];
+  transformResponse?: <T>(response: T, config: AxiosRequestConfig) => T;
+}
+
 class HttpService implements ApiService {
-  private http: AxiosInstance;
+  private axiosInstance: AxiosInstance;
+  private options?: RequestOptions;
 
-  constructor(baseURL: string) {
-    this.http = axios.create({
+  constructor(baseURL: string, options?: RequestOptions) {
+    this.axiosInstance = axios.create({
       baseURL,
       timeout: 5000, // Set your desired timeout value
     });
+    this.options = options;
+  }
+
+  get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
+    return this.request({ url, method: 'GET', ...config })
   }
 
-  async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
-    const response: AxiosResponse<T> = await this.http.get(url, config);
-    return response.data;
+  post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
+    return this.request({ url, method: 'POST', data, ...config })
   }
 
-  async post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
-    const response: AxiosResponse<T> = await this.http.post(url, data, config);
-    return response.data;
+  put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
+    return this.request({ url, method: 'PUT', data, ...config })
   }
 
-  async put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
-    const response: AxiosResponse<T> = await this.http.put(url, data, config);
-    return response.data;
+  delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
+    return this.request({ url, method: 'DELETE', ...config })
   }
 
-  async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
-    const response: AxiosResponse<T> = await this.http.delete(url, config);
-    return response.data;
+  request<T = any>(config: AxiosRequestConfig): Promise<T> {
+    config.headers = {
+      ...config.headers,
+      Authorization: localStorage.getItem('token') || '',
+    }
+    
+   const { transformResponse } = this.options || {};
+    
+    return new Promise((resolve, reject) => {
+      this.axiosInstance
+      .request(config)
+      .then((response: AxiosResponse<T>) => {
+        if (transformResponse) {
+          try {
+            const res = transformResponse(response.data, config);
+            resolve(res);
+          }
+          catch (e) {
+            reject(e);
+          }
+        }
+        resolve(response.data);
+      })
+      .catch((e: Error | AxiosError) => {
+        reject(e);
+      })
+    })
   }
 }
 

+ 24 - 1
src/utils/http/index.ts

@@ -1,3 +1,26 @@
+import { message } from 'ant-design-vue';
 import HttpService from './axios';
+import { AxiosRequestConfig } from 'axios';
 
-export const http = new HttpService('/api');
+const baseURL = import.meta.env.VITE_APP_BASE_URL as string;
+
+type ResponseResult<T> = {
+  code: number;
+  isAuthorized: boolean;
+  isSuccess: boolean;
+  result: T;
+  error?: string;
+}
+const transformResponse = <T>(response: ResponseResult<T>, config: AxiosRequestConfig) => {
+  if (config.responseType === 'blob') {
+    return response;
+  }
+  if(response.code === 1) {
+    return response.result;
+  } else {
+    message.warning(response.error as string);
+    throw new Error(response.error)
+  }
+};
+
+export const http = new HttpService(baseURL, {transformResponse});

+ 1 - 1
src/views/designer/component/ComponentWrapper.vue

@@ -220,7 +220,7 @@ const handleDragPoint = (type: string, e: PointerEvent) => {
   );
 };
 // 拖拽点开始
-const handleDragStart = (_, e: PointerEvent) => {
+const handleDragStart = (_: any, e: PointerEvent) => {
   startPoint.x = e.x;
   startPoint.y = e.y;
   isPointDragFlag = true;

+ 34 - 3
src/views/designer/component/PageConfig.vue

@@ -3,21 +3,52 @@
 </template>
 
 <script setup lang="ts">
+import { computed } from 'vue';
 import { CusForm } from 'shalu-dashboard-ui';
 import { useProjectStore } from '@/store/modules/project';
+import { isEqual, omit } from 'lodash';
 
 const projectStore = useProjectStore();
-const formItems = [
+const formItems = computed(() => [
+  {
+    label: '项目名称',
+    prop: 'name',
+    type: 'input',
+    defaultValue: projectStore.projectInfo.name
+  },
+  {
+    label: '宽度',
+    prop: 'width',
+    type: 'inputNumber',
+    fieldProps: {
+      addonAfter: 'px',
+      min: 100
+    },
+    defaultValue: projectStore.projectInfo.width
+  },
+  {
+    label: '高度',
+    prop: 'height',
+    type: 'inputNumber',
+    fieldProps: {
+      addonAfter: 'px',
+      min: 100
+    },
+    defaultValue: projectStore.projectInfo.height
+  },
   {
     label: '页面背景',
     prop: 'background',
     type: 'backgroundSelect',
     defaultValue: projectStore.currentPage.background
   }
-];
+]);
 
 const handleChange = (value: Record<string, any>) => {
-  projectStore.setCurrentPageBackground(value.background);
+  if(!isEqual(value.background, projectStore.currentPage.background)) {
+    projectStore.setCurrentPageBackground(value.background);
+  };
+  projectStore.updateProjectInfo(omit(value, ['background']));
 };
 
 </script>

+ 8 - 15
src/views/designer/component/Stage.vue

@@ -181,25 +181,18 @@ const handleCanvasClick = (e: MouseEvent) => {
 
 /* 适应大小设置 */
 watch(
-  () => stageStore.scale,
-  (val) => {
-    if(!val) {
-      initScale();
-      initStagePosition();
-    }
+  () => [
+    stageStore.scale,
+    projectStore.projectInfo.width,
+    projectStore.projectInfo.height
+  ],
+  () => {
+    initScale();
+    initStagePosition();  
   }
 );
 
 onMounted(() => {
-  /* 获取项目信息 */
-  const obj = projectStore.getCurrentProjectInfo();
-  if (!obj) {
-    message.error("项目不存在");
-    setTimeout(() => {
-      router.push("/");
-    }, 500);
-  }
-
   initStagePosition();
   initScale();
   window.addEventListener("resize", initScale);

+ 27 - 14
src/views/designer/component/useComponentConfig.ts

@@ -50,20 +50,33 @@ export const useComponentConfig = () => {
             defaultValue: containerStyle?.borderStyle || "none",
           },
           {
-            label: "边框颜色",
-            prop: "style.borderColor",
-            type: "colorSelect",
-            defaultValue: containerStyle?.borderColor ?? "#EEEEEEFF",
-          },
-          {
-            label: "边框宽度",
-            prop: "style.borderWidth",
-            type: "inputNumber",
-            fieldProps: {
-              min: 0,
-              addonAfter: "px",
-            },
-            defaultValue: containerStyle?.borderWidth ?? 1,
+            label: '',
+            prop: "",
+            type: 'dependency',
+            name: ['style.borderStyle'],
+            children: (model: any) => {
+              return model['style.borderStyle'] !== 'none' ? [
+                {
+                  label: "边框颜色",
+                  prop: "style.borderColor",
+                  type: "colorSelect",
+                  defaultValue: containerStyle?.borderColor ?? "#EEEEEEFF",
+                  fieldProps: {
+                    gradient: false
+                  }
+                },
+                {
+                  label: "边框宽度",
+                  prop: "style.borderWidth",
+                  type: "inputNumber",
+                  fieldProps: {
+                    min: 0,
+                    addonAfter: "px",
+                  },
+                  defaultValue: containerStyle?.borderWidth ?? 1,
+                },
+              ] : [];
+            }
           },
           {
             label: "圆角",

+ 96 - 31
src/views/designer/index.vue

@@ -1,22 +1,33 @@
 <template>
-  <Layout style="height: 100vh;">
-    <LayoutHeader style="background: #fff;">
+  <Layout style="height: 100vh">
+    <LayoutHeader style="background: #fff">
       <div class="header-left">
-        <h1>{{ projectStore.projectInfo.name || '大屏标题'}}</h1>
+        <h1>{{ projectStore.projectInfo.name || "大屏标题" }}</h1>
       </div>
       <div class="header-middle">
         <MenuBar />
       </div>
       <div class="header-right">
-        <Button size="small" style="margin-right: 8px;" @click="handlePreview"><DesktopOutlined/>预览</Button>
-        <Button size="small" type="primary" @click="handleSave" :loading="loading"><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>
 
     <LayoutContent>
-      <div class="layer-wrapper" :style="{width: stageStore.showLayer ? '200px' : '0px'}">
+      <div
+        class="layer-wrapper"
+        :style="{ width: stageStore.showLayer ? '200px' : '0px' }"
+      >
         <!-- 图层管理 -->
-        <LayerManagement v-show="stageStore.showLayer"/>
+        <LayerManagement v-show="stageStore.showLayer" />
       </div>
       <div class="component-wrapper">
         <!-- 组件库 -->
@@ -35,39 +46,93 @@
 </template>
 
 <script lang="ts" setup>
-import { ref } from "vue";
+import { ref, watch } from "vue";
 import {
   Layout,
   LayoutHeader,
   LayoutContent,
-  Button
-} from 'ant-design-vue';
-import { DesktopOutlined, SaveOutlined } from '@ant-design/icons-vue';
-import { useProjectStore } from '@/store/modules/project';
-import { useStageStore } from '@/store/modules/stage';
+  Button,
+  message,
+} from "ant-design-vue";
+import { DesktopOutlined, SaveOutlined } from "@ant-design/icons-vue";
+import { useProjectStore } from "@/store/modules/project";
+import { useStageStore } from "@/store/modules/stage";
+import { useRoute } from "vue-router";
+import { useRequest } from "vue-hooks-plus";
+import { useAppStore } from "@/store/modules/app";
+import { getPageDesignApi } from "@/api";
 
-import LayerManagement from './component/LayerManagement.vue';
-import ComponentLibary from './component/ComponentLibary.vue';
-import Workspace from './component/Workspace.vue';
-import Configurator from './component/Configurator.vue';
-import MenuBar from './component/MenuBar.vue';
+import LayerManagement from "./component/LayerManagement.vue";
+import ComponentLibary from "./component/ComponentLibary.vue";
+import Workspace from "./component/Workspace.vue";
+import Configurator from "./component/Configurator.vue";
+import MenuBar from "./component/MenuBar.vue";
+
+const route = useRoute();
 
 const stageStore = useStageStore();
 const projectStore = useProjectStore();
+const appStore = useAppStore();
 const loading = ref(false);
 
-const handlePreview = () => {
-  console.log('预览');
-  localStorage.setItem('currentProject', JSON.stringify(projectStore.projectInfo));
-  window.open('#/view?id=1');
+const { run, loading: loadingPage } = useRequest(getPageDesignApi, {
+  manual: true,
+  onSuccess: (res) => {
+    const { appPageId, pcJson } = res;
+    if(pcJson) {
+      projectStore.setProjectInfo(JSON.parse(pcJson));
+      return;
+    }
+    if(appPageId) {
+      projectStore.updateProjectInfo({ pageId: appPageId });
+    }
+  },
+  onError: (e) => {
+    console.error(e);
+    message.error("获取页面信息失败");
+  },
+});
+
+/* 获取项目信息 */
+projectStore.getCurrentProjectInfo();
+// 传入pageId和token,获取页面信息
+if(route.query?.pageId && route.query?.token) {
+  const { token, pageId } = route.query;
+  localStorage.setItem("token", token as string);
+  run({id: pageId as string});
 }
-const handleSave = () => {
-  loading.value = true;
-  // TODO 保存项目
-  setTimeout(() => {
+
+watch(
+  () => loadingPage.value,
+  () => {
+    appStore.setPageLoading(loadingPage.value);
+  }
+)
+
+const handlePreview = () => {
+  localStorage.setItem(
+    "currentProject",
+    JSON.stringify(projectStore.projectInfo)
+  );
+  window.open("#/view?id=1");
+};
+
+const handleSave = async () => {
+  try {
+    loading.value = true;
+    localStorage.setItem(
+      "currentProject",
+      JSON.stringify(projectStore.projectInfo)
+    );
+
+    await projectStore.handleSaveProject();
+  } catch (e) {
     loading.value = false;
-  }, 1000);
-}
+    message.error("保存失败");
+  } finally {
+    loading.value = false;
+  }
+};
 </script>
 
 <style lang="less" scoped>
@@ -80,9 +145,9 @@ const handleSave = () => {
   border-bottom: solid 1px #eee;
   z-index: 2;
   .ant-btn {
-    font-size: 12px
+    font-size: 12px;
   }
-  .header-left h1{
+  .header-left h1 {
     width: 300px;
     overflow: hidden;
     white-space: nowrap;
@@ -120,4 +185,4 @@ const handleSave = () => {
   background: #fff;
   border-left: solid 1px #eee;
 }
-</style>
+</style>

+ 2 - 0
types/project.d.ts

@@ -64,6 +64,8 @@ declare interface Page {
 
 // 项目基本信息
 declare export interface ProjectInfo {
+  // 页面id
+  pageId: string;
   // 项目名称
   name: string;
   // 项目描述