vue-widget-quasar-clivite-template技术交流

vue-widget-quasar-clivite-template核心是基于VUE的Widget机制开发框架,使用quasar作为UI库,通过quasar-cli和Vite结合方式进行前端工程编译打包。

​ 与现在的vue-widget-tempalte相比较,目录组织机构相同(views改为pages),Widget插件机制相同,只是在UI库方面由AntDesign转为Quasar,打包编译配置和插件基本相同,都利用了vite。

主要优势

为什么选择Quasar

  1. 官方理由:

  1. 个人理由:

XFramelib前端基础库

基于 VUE3+Hprose+Typescript 的前端框架,与ElementUI、AntDesign VUE等界面库无关,一直是来源于项目和服务于项目。

xframelib.png

vue-widget-quasar开发模板

初始配置

npm install 后需要对@quasar/app-vite的vite版本进行单独升级(默认版本太旧)

quasar-vite升级.pngquasar-vite升级.png

目录结构

目录结构.png

--public 不打包全局资源

​ --css 公共全局样式

​ --img 图片资源
​ --js js库

​ --SysConfig.js 网站系统配置,主要

--src

​ --actions

​ --api 根据后台WebAPI服务自动生成的前端代码

​ --assets 图片和css等资源(参与打包)

​ --boot 系统初始化、挂接全局资源的(不用修改)

​ --components 自带的通用基础组件

​ --composables 类似VueUse库包装

​ --css css变量;主题CSS

​ --directives 积累的指令

​ --enums 枚举变量

​ --enents 定义各类事件,按模块划分组织

​ --examples 自带组件使用示例

​ --layouts 各种布局页面,每个布局也都优先使用基于方式

​ --models公共模型对象

​ --netAPI 网络服务AIP调用,IP查询、QQ定位、天气状况

​ --pages 按布局layout名称定义不同的文件夹,组织视图页面

​ --permission 角色权限过滤的相关方法

​ --router 按布局layouts名划分组织定义路由;路由守卫

​ --service 通过后台hprose服务生成的业务服务TS代码

​ --settings 设置项,modal对话框设置,widgetMenu设置,widget设置,函数设置,project设置

​ --stores 按业务模块组织pinia 公共数据层

​ --utils 通用工具方法

​ --widgets 按Layout名定义不同的文件夹,组织业务组件Widgets

​ --workers 编写异步函数方法,都会编译成一个webworke

SysConifg.js解析

SysConfig.js是在XFramelib初始化时全局挂接到 Global.Config对象上的,对应模型为ISystemConfig,一二级配置项都具有自定义扩展能力。

export interface ISystemConfig {
  /**
   * 用户界面配置
   */
  UI: IUIObject;
  /**
   * 服务URL
   */
  ServiceURL: IServiceURL;
  /**
   * 地图相关Keys
   */
  MapKeys?: IMapKeys;
  /**
   * API服务路径
   */
  APIPath?: object; //
  //其他配置信息
  [props: string]: any;
}

UI为默认系统界面相关配置,对应模型为IUIObject,选项具有可扩展能力。

export interface IUIObject {
  /**
   * 网站标题
   */
  SiteTitle: string;
  /**
   * 系统的所属组名
   */
  Group?: string;
  /**
   * 版权
   */
  CopyRight: string; //'Copyright ©XXX  2021-2025',
  /**
   * 官方链接
   */
  WebSite?: string; //网站链接
  /**
   * 超时锁屏时间(单位:秒s)
   * 自动锁屏时间,为0不锁屏。
   */
  LockTime?: number; //
  /**
   * 是否是能访问互联网,还是内网部署应用
   * */
  IsInternet?: boolean;

  /**
   * 网站灰色模式,用于可能悼念的日期开启
   * 默认为false
   */
  GrayMode?: boolean;

  /**
   * 其他扩展的属性
   */
  [props: string]: any;
}

ServiceURL 为默认后台服务地址配置,可自定义扩展字段。

export interface IServiceURL {
  /**
   * 用户登录验证服务(统一用户登录:后台)
   */
  LoginAuthURL?: string; //用户验证
  /**
   * 用户登录界面(统一用户登录界面:前台)
   */
  UILoginURL?: string;//用户登录界面
  /**
   * 文件管理服务地址(统一文件管理:后台)
   */
  FileServiceURL?: string; //文件管理
  /**
   * 文件管理(统一文件管理:前台)
   */
  UIFileURL?: string; //文件管理,在线
  /**
   * 在线日志服务(统一日志记录)
   */
  LogServiceURL?: string;
  /**
   * 图标在线服务地址
   */
  IconServiceURL?: string;
  /**
   * Axios普通WebAPI的BaseURL
   * 全局默认的http请求地址(一般与主hprose相同或不同);文件上传地址
   */
  DefaultWebAPI?: string;
  /**
   * 默认HproseAPI的服务地址
   */
  DefaultHproseAPI?: string;
  /**
   * 其他扩展的URL属性
   */
  [props: string]: any;
}

MapKeys 为全局设置地图服务的KEYS,可以单个KEY或数组,对应模型为IMapKeys

export interface IMapKeys {
  /**
   * 天地图Key ,单个或数组
   */
  TDTKey?: string|string[];
  /**
   * MapboxKey ,单个或数组
   */
  MapboxKey?: string|string[];
  /**
   * Cesium Key ,单个或数组
   */
  CesiumKey?: string|string[];
  /**
   * Google地图Key ,单个或数组
   */
  GoogleKey?: string|string[];
  /**
   * 其他扩展的Key属性
   */
  [props: string]: any;
}

APIPath 为配置API服务的相对路径,与ServiceURL里的选项配合使用,字段名称和值自定义。

系统注册/更新

每个新项目系统要修改package.json的name和version,name和version共同作为唯一确认系统的ID。

packagename.png

开发模式下,每次系统运行,会在Public目录下生成MenuRoutes.json或 /help/register请求来生成系统元数据JSON。

基于widget Quasar-cli开发模板

helpregister.png

{
  "id": "fbe867c7bd3c208c41791a2e0e38c7fe",
  "name": "基于widget Quasar-cli开发模板",
  "product": "vue-widget-quasar-clivite-template",
  "version": "0.0.1",
  "routes": [
    {
      "path": "/default",
      "name": "DefaultLayout",
      "type": 0,
      "children": [
        {
          "path": "home",
          "name": "home2",
          "title": "首页2",
          "type": 0,
          "index": 1
        },
        {
          "path": "dashboard",
          "name": "dashboard2",
          "title": "仪表盘",
          "type": 0,
          "index": 2,
          "children": [
            {
              "path": "/default/dashboard/welcome",
              "name": "dashboard2-welcome",
              "title": "欢迎",
              "type": 0
            }
          ]
        },
        {
          "path": "http://www.baidu.com",
          "name": "http://www.baidu.com",
          "title": "百度",
          "type": 0,
          "index": 3
        }
      ]
    },
    {
      "path": "/main",
      "name": "Main",
      "title": "主页面布局",
      "type": 1,
      "children": [
        {
          "path": "/main/test",
          "name": "main-test",
          "type": 0,
          "index": 1
        }
      ]
    },
    {
      "path": "/back",
      "name": "BackLayout",
      "title": "",
      "type": 1,
      "children": [
        {
          "path": "dashboard",
          "name": "dashboard",
          "title": "仪表盘",
          "type": 0,
          "index": 1,
          "children": [
            {
              "path": "welcome",
              "name": "dashboard-welcome",
              "title": "欢迎",
              "type": 0
            }
          ]
        },
        {
          "path": "home",
          "name": "home",
          "title": "首页",
          "type": 0,
          "index": 2
        },
        {
          "path": "http://www.baidu.com",
          "name": "http://www.baidu.com",
          "title": "百度",
          "type": 0,
          "index": 3
        }
      ]
    },
    {
      "path": "/bigscreen",
      "name": "bigscreen",
      "title": "",
      "type": 0,
      "children": [
        {
          "path": "/test",
          "name": "test",
          "title": "routes.home",
          "type": 0
        },
        {
          "path": "/test2",
          "name": "test2",
          "title": "routes.home",
          "type": 0
        }
      ]
    }
  ],
  "widgetMenu": [
    {
      "name": "地图工具",
      "index": 1,
      "path": "linkMenuWidget",
      "type": 0,
      "children": [
        {
          "name": "测量工具",
          "path": "flyMenuWidget",
          "type": 0,
          "children": [
            {
              "name": "test1",
              "path": "flyRoamingWidget1",
              "type": 0
            },
            {
              "name": "test2",
              "path": "SimulationWidget2",
              "type": 0
            }
          ]
        },
        {
          "name": "量算工具",
          "path": "measureToolWidget",
          "type": 0
        },
        {
          "name": "标绘工具",
          "path": "drawToolWidget",
          "type": 0
        },
        {
          "name": "态势标绘",
          "path": "plotMapWidget",
          "type": 0
        },
        {
          "name": "截图/视频",
          "path": "photoVideoWidget",
          "type": 0
        }
      ]
    },
    {
      "name": "图层树",
      "index": 2,
      "path": "layerManagerWidget",
      "type": 0
    },
    {
      "name": "专题地图",
      "index": 3,
      "path": "layerManagerWidget",
      "type": 0
    },
    {
      "name": "场景视角",
      "index": 4,
      "path": "layerManagerWidget",
      "type": 0
    },
    {
      "name": "地图特效",
      "index": 5,
      "path": "layerManagerWidget",
      "type": 0
    }
  ],
  "widgets": [
    {
      "id": "HeaderTitleWidget",
      "label": "头部栏",
      "container": 0,
      "preload": true
    },
    {
      "id": "LogoTitleWidget",
      "label": "图标标题",
      "container": 0,
      "preload": true,
      "afterid": "HeaderTitleWidget"
    },
    {
      "id": "FooterCopyrightWidget",
      "label": "底部栏",
      "container": 1,
      "preload": false
    },
    {
      "id": "ModalContainerWidget",
      "label": "弹框容器",
      "container": 0,
      "preload": true
    },
    {
      "id": "SideMenuWidget",
      "label": "左侧菜单",
      "container": 5,
      "preload": true
    },
    {
      "id": "headerTitleWidget",
      "label": "头部栏",
      "container": 0,
      "preload": true
    },
    {
      "id": "bottomMenuWidget",
      "container": 1,
      "preload": true
    },
    {
      "id": "statusWidget",
      "container": 1,
      "preload": true
    },
    {
      "id": "cesiumWidget",
      "container": 2,
      "preload": true
    },
    {
      "id": "menuBarWidget",
      "container": 2,
      "preload": true
    },
    {
      "id": "linkMenuWidget",
      "container": 2,
      "preload": false
    },
    {
      "id": "templateMenuWidget",
      "container": 2,
      "preload": false
    },
    {
      "id": "flyMenuWidget",
      "container": 2,
      "preload": false
    },
    {
      "id": "HeaderTitleWidget",
      "label": "头部栏",
      "container": 0,
      "preload": true
    },
    {
      "id": "FooterCopyrightWidget",
      "label": "底部栏",
      "container": 1,
      "preload": true
    },
    {
      "id": "ModalContainerWidget",
      "label": "弹框容器",
      "container": 0,
      "preload": true
    },
    {
      "id": "LogoTitleWidget",
      "label": "头部栏",
      "container": 0,
      "preload": true
    },
    {
      "id": "TopMenuWidget",
      "label": "头部菜单栏",
      "container": 0,
      "preload": true
    },
    {
      "id": "ModalContainerWidget",
      "label": "弹框容器",
      "container": 0,
      "preload": true
    }
  ]
}

登录新版用户系统 https://gis-auth.digsur.com/ 进行系统注册(只有管理员级别用户才有权限)

sysregister.png

用户登录与权限控制

用户登录

统一用户登录

外部token验证与跳转

解决外部传入tk时,优先验证token,实现正确跳转。

即:A系统登录后,带着有效token,直接进入B系统里的使用相关功能

2024-10-21后开发模板版本,支持跳转的两种模式:

http://localhost:9001/#**/login?redirect=/default/pdfviewer&tk=xxxx**

http://localhost:9001/#/login?redirect=/default/pdfviewer&tk=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjAwMDY1YTMzLTc5NTEtNDI1MC1iMWIyLWFiN2I4NzFhNGFhMyIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiOiJhZG1pbiIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6IjAiLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3VzZXJkYXRhIjoiMCIsIm5iZiI6MTcyOTQ5NzM5MywiZXhwIjoxNzI5NDk3OTkzLCJpc3MiOiJGbHlMb2xvIiwiYXVkIjoiVGVzdEF1ZGllbmNlIn0.fqCiIjL8W518g3J5LyGKVDa668zrVZcp0Jpme6UCO_M

已有开展业务的项目,只需要更新替换src/router/router-guards.ts里的createRouterGuards方法就行:

export function createRouterGuards(router: Router) {
  let toPath = '';
  const breadcrumbsState = breadcrumbsStore();
  router.beforeEach((to, from, next) => {
    NProgress.start(); // start progress bar
    const toName = to.name as string;
    toPath = to.path;
    const userState = userStore();
    //获取
    const tokenInfo = getLocalToken();
    if (tokenInfo) {
      const systemRight = getCurrentSystemRight();
      //WM:解决刷新路径时,无法启动定时刷新任务的问题
      if (!Global.User && userState.id) {
        checkDoRefreshToken();
        //刷新用户的角色权限
        if (toName === 'NotFound') {
          //判断是否加入过权限控制的路由
          if (systemRight && systemRight.routes && systemRight.routes.length > 0) {
            const rightRoutes = getRightRoutes();
            if (rightRoutes) {
              //确保获取到路由权限后,赋值全局变量
              Global.User = userState.id;
              let first: RouteRecordRaw;
              rightRoutes.forEach((item) => {
                if (!first) {
                  first = item;
                }
                router.addRoute(item);
              });
              if (first) checkAddDefaultRoute(router, first);
              //WM:必须的,解决默认名为NotFound
              to.name = undefined;
              next(to);
            }
            return;
          }
        }
      }

      if (toName === 'login') {
        next({ path: defaultRoutePath });
        NProgress.done();
      } else {
        const hasRoute = router.hasRoute(toName);

        // 在免登录名单,直接进入
        if (isSystemRoute(toName) || hasRoute) {
          if (!systemRight && toName === 'NotFound') {
            //退出登录,当前无法获取到系统权限时
            logout();
            clearRight();
            //跳转到——》登录界面
            next({
              path: loginRoutePath,
              query: { redirect: to.fullPath },
              replace: true
            });
          } else {
            // Global.Logger().info(isSystemRoute(toName)+':'+toName);
            next();
            breadcrumbsState.setBreadcurmbs(to.matched, to.query);
          }
          NProgress.done();
        }
      }
    } else {
      const tokenValue = to.query?.tk?.toString();
      if (tokenValue) {
        //登录验证token
        doTokenCheck(tokenValue).then((checkResult) => {
          if (checkResult) {
            //WM:请求获取授权,以加载权限模块
            const sysID = getSystemID();
            //是否是超级管理员
            const isSuperLevel = userState.DefaultMaxRoleLevel === 0;
            //获取系统角色权限
            getSystemRoleRight(sysID, isSuperLevel)
              .then(() => {
                const rightRoutes = getRightRoutes();
                if (rightRoutes) {
                  let first: RouteRecordRaw|undefined=undefined;
                  rightRoutes.forEach((item) => {
                    if (!first) first = item;
                    router.addRoute(item);
                  });
                  if (first) checkAddDefaultRoute(router, first);
                  //WM:20241021 支持token跳转验证
                  if(to.query.redirect)
                    {
                      const redirectURL=to.query.redirect as string;
                      if(redirectURL.startsWith("http"))
                        {
                          window.open(redirectURL, '_self');
                        }
                        else{
                          console.log('99999999',redirectURL);
                          next({ path: redirectURL });
                        }
                    }
                  else
                  {
                    console.log('888888888',to.path);
                    next({ path: to.path });
                  }

                  return;
                }
              })
              .catch(() => {
                Global.Message.warn('获取用户功能权限失败!');
                next();
              });
          } else {
            Global.Logger().debug('验证外部的tk参数失败!');
            const rebackURL = Global.Config.ServiceURL.UILoginURL;
            if (rebackURL) { //统一登录界面
              const tofullPath = document.URL;
              window.open(rebackURL + '?redirect=' + tofullPath, '_self');
            } else {
              //内部登录
              let  redirectURL2=to.fullPath;
              if(to.query.redirect)
              {
                  redirectURL2=to.query.redirect as string;
              }
              //#region 登录界面
              next({
                path: loginRoutePath,
                query: { redirect: redirectURL2 },
                replace: true
              });
              //#endregion
            }
            NProgress.done();
          }
        })
      } 
      else{
        // not login
      if (isSystemRoute(toName)) {
        // 在免登录名单,直接进入
        next();
        breadcrumbsState.setBreadcurmbs(to.matched, to.query);
      } else {
        //#region 统一登录验证界面
          //跳转到主网站登录页面
          const rebackURL = Global.Config.ServiceURL.UILoginURL;
          if (rebackURL) {
            const tofullPath = document.URL;
            window.open(rebackURL + '?redirect=' + tofullPath, '_self');
          } else {
            //#region 登录界面
            next({
              path: loginRoutePath,
              query: { redirect: to.fullPath },
              replace: true
            });
            //#endregion
          }

          NProgress.done();
        //#endregion
      }

      }
    }
  });

  router.afterEach((to, from, failure) => {
    const asyncRouteStoreState = asyncRouteStore();
    //设置网页Title
    document.title = getPageTitle(to.meta.title);
    if (isNavigationFailure(failure)) {
      Global.Logger().debug('failed navigation', failure);
    }
    // 在这里设置需要缓存的组件名称
    const keepAliveComponents = asyncRouteStoreState.keepAliveComponents;
    const currentComName = to.matched.find((item) => item.name == to.name)?.components?.default.name;
    if (currentComName && !keepAliveComponents?.includes(currentComName) && to.meta?.keepAlive) {
      // 需要缓存的组件
      keepAliveComponents.push(currentComName);
    } else if (!to.meta?.keepAlive || to.name == 'Redirect') {
      // 不需要缓存的组件
      const index = asyncRouteStoreState.keepAliveComponents.findIndex((name) => name == currentComName);
      if (index != -1) {
        keepAliveComponents?.splice(index, 1);
      }
    }
    // store.commit('asyncRoute/setKeepAliveComponents', keepAliveComponents);
    NProgress.done(); // finish progress bar
  });

  router.onError((error) => {
    Global.Message.err('加载视图错误:' + error.message);
    Global.Logger().debug(error, '路由错误');
    router.push('/error/404');
  });
}

在线API代码生成

API文件夹、Service文件夹,前端服务对接代码都是通过后台服务元数据在线生成的。

seviceTool.png

  1. https://gis-hprosetest.digsur.com/ 在线生成Hprose服务代码,放在/src/service文件夹下

    https://hprosetest.gis.digsur.com/#/home

img

  1. https://gis-apitest.digsur.com/#/home 在线生成WebAPI服务代码,放在/src/api文件夹下

    https://apitest.gis.digsur.com/#/home

WebAPI

store使用

用户登录

1.接入统一用户权限管理系统

配置用户登录验证URL:SysConfig.js里ServiceURL项的 LoginAuthURL 属性

配置用户登录验证URL

2.用户登录密码加密方式

前端使用“XXTEA非对称加密”算法进行加密的。

3. 外部后台使用用户系统,验证token

https://auth.gis.digsur.com/swagger/index.html 的 /api/Token/check方法,如果验证正确token,返回用户信息;如果错误,则报500异常

验证token

Table表格规范化开发

参考模版代码为:src/pages/back/StandardTable/index.vue

下图为表格规范化结构:

表格规范化结构

涉及的主要组件为:

import ActionMenu from "@/components/Menu/ActionMenu.vue";
import BaseContent from "src/components/Quasar/BaseContent.vue";
import PaginationLine from "@/components/Quasar/TableParts/PaginationLine.vue";
import TopFunBar from "@/components/Quasar/TableParts/TopFunBar.vue";

BaseContent :为通用视图容器

TopFunBar:为顶部表格操作按钮栏

      <template #top="table">
        <TopFunBar :target="table" :batch="selected.length===0" :title="'服务列表'" @topBarClick="topBarClick" >
        </TopFunBar>
      </template>

PaginationLine:为分页切换栏

      <template v-slot:bottom>
        <div class="full-width row  justify-end">
          <PaginationLine :rows-number="rowSum" :rows-per-page="pagination.rowsPerPage" :max="pagesNumber">
          </PaginationLine>
        </div>
      </template>
    </q-table>

ActionMenu :表格右侧操作按钮

      <template #body-cell-opt="props">
        <q-td :props="props" :auto-width="true">
          <ActionMenu :currentRow="props.row" @doActionClick="doActionClick"
            :default-menus="getRightDefaultMenus(props.row)" :append-menus="getRightMoreMenus(props.row)" />
        </q-td>
      </template>

TabColumns使用:

import { columns, visibleColumns } from './models/TabColumns';
    <q-table class="fit sticky-header-table" selection="multiple" v-model:selected="selected" :dense="$q.screen.lt.md"
      separator="cell" flat bordered :rows="rows" :filter="filter" :columns="columns" row-key="name"
      :visible-columns="visibleColumns" v-model:pagination="pagination">

ActionMenus 使用

import { getDefaultMenus, getMoreMenus } from "./models/ActionMenus";

//#region 更多操作
//获取表格的菜单栏
function getRightDefaultMenus(rowItem) {
  const menusArray = [...getDefaultMenus()];
  return menusArray;
}
function getRightMoreMenus(rowItem) {
  return getMoreMenus();
}
const doActionClick = (action, row) => {
  Global.Logger().trace('表格菜单点击', action, row);
  switch (action) {
    case "preview"://预览
      break;
    case "edit"://编辑
      edit(row);
      break;
    case "delete"://删除
      DBService.deleteItem(row.id).then(p => {
        query();
        Global.Message.info('删除成功')
      });
      break;
    // case "preview":
    //   preview(row);
    //   break;
  }
};
//#endregion

index.vue完整表格页开发示例代码:

<template>
  <base-content class="q-pa-sm" :style="sizeStyle">
    <q-table class="fit sticky-header-table" selection="multiple" v-model:selected="selected" :dense="$q.screen.lt.md"
      separator="cell" flat bordered :rows="rows" :filter="filter" :columns="columns" row-key="name"
      :visible-columns="visibleColumns" v-model:pagination="pagination">
      <template #top="table">
        <TopFunBar :target="table" :batch="selected.length===0" :title="'服务列表'" @topBarClick="topBarClick" >
          <template #rightAppend>
            <q-btn-dropdown outlined label="自选列" icon="view_list" no-wrap v-show="$q.screen.gt.sm">
              <q-list>
                <q-item tag="label" v-for="item in columns" :key="item.name">
                  <q-item-section avatar>
                    <q-checkbox v-model="visibleColumns" :val="item.name" />
                  </q-item-section>
                  <q-item-section>
                    <q-item-label>{{ item.label }}</q-item-label>
                  </q-item-section>
                </q-item>
              </q-list>
            </q-btn-dropdown>
          </template>
        </TopFunBar>
      </template>
      <template #body-cell-picture="props">
        <q-td :props="props">
          <img v-if="props.row.picture" :src="props.row.picture" width="80" height="80" />
          <span v-else>暂无</span>
        </q-td>
      </template>
      <template #body-cell-profile="props">
        <q-td :props="props">
          <q-btn v-if="props.row.pid" label="下载档案" color="primary" @click="doDowload(props.row.pid)" />
          <span v-else>暂无</span>
        </q-td>
      </template>

      <template #body-cell-opt="props">
        <q-td :props="props" :auto-width="true">
          <ActionMenu :currentRow="props.row" @doActionClick="doActionClick"
            :default-menus="getRightDefaultMenus(props.row)" :append-menus="getRightMoreMenus(props.row)" />
        </q-td>
      </template>
      <template v-slot:bottom>
        <div class="full-width row  justify-end">
          <!-- <q-pagination v-model="currentPage" :max="pagesNumber" /> -->
          <PaginationLine :rows-number="rowSum" :rows-per-page="pagination.rowsPerPage" :max="pagesNumber">
          </PaginationLine>
        </div>
      </template>
    </q-table>
  </base-content>
</template>

<script lang="ts" setup>
import ActionMenu from "@/components/Menu/ActionMenu.vue";
import BaseContent from "src/components/Quasar/BaseContent.vue";
import { appStore } from 'src/stores';
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
import DBService from "@/database/CommonDB/DBService";
import { OffEventHandler, OnEventHandler } from '@/events';
import TableEvent from '@/events/modules/TableEvent';
import { doLoadModal } from '@/utils/WidgetsTool';
import { Global, SaveAs } from "xframelib";
import { getDefaultMenus, getMoreMenus } from "./models/ActionMenus";
import { columns, visibleColumns } from './models/TabColumns';
import PaginationLine from "@/components/Quasar/TableParts/PaginationLine.vue";
import TopFunBar from "@/components/Quasar/TableParts/TopFunBar.vue";

defineOptions({ name: "StandardTable" })
const appState = appStore();
const sizeStyle = computed(() => {
  return appState.showTabMenu ? "padding-bottom: 30px;" : "";
})
const pagination = ref({
  sortBy: 'id',
  descending: false,
  page: 1,
  rowsPerPage: 10
})
const currentPage = ref(1);
//总记录数
const rowSum = ref(0);
const pagesNumber = computed(() => {
  return Math.ceil(rowSum.value / pagination.value.rowsPerPage)
})
const filter = ref<string>("")
const selected = ref([]);
const keywordRef=ref('');

//页码发生变化
watch(() => currentPage.value, val => {
  query();
})

//#region 通用对话框的相关代码
function showModal(actionMethod: string, rowData?: any) {
  let modalID: string = '';
  let extraData: any;
  switch (actionMethod) {
    case 'add':
      modalID = 'addEditForm';
      extraData = {
        title: `新建`,
        footer: 'true'
      };
      break;
    case 'edit':
      modalID = 'addEditForm';
      extraData = {
        title: `编辑`,
        footer: 'true'
      };
      break;
  }
  if (modalID) {
    const modalData = {
      modalID,
      extraData,
      rowData,
      width: 700
    };
    doLoadModal(modalData);
  }
}
//#endregion
//#region 更多操作
//获取表格的菜单栏
function getRightDefaultMenus(rowItem) {
  const menusArray = [...getDefaultMenus()];
  return menusArray;
}
function getRightMoreMenus(rowItem) {
  return getMoreMenus();
}
const doActionClick = (action, row) => {
  Global.Logger().trace('表格菜单点击', action, row);
  switch (action) {
    case "preview"://预览
      break;
    case "edit"://编辑
      edit(row);
      break;
    case "delete"://删除
      DBService.deleteItem(row.id).then(p => {
        query();
        Global.Message.info('删除成功')
      });
      break;
    // case "preview":
    //   preview(row);
    //   break;
  }
};
//#endregion

/**
 * 表格上方菜单操作
 */
const topBarClick = (val: string, searchValue: string) => {
  switch (val) {
    case 'searchWord':
      keywordRef.value=searchValue;
      query();
      break;
    case 'creatNew':
      add();
      break;
    case 'batchDelete':
      batchDelete();
      break;
    case 'refresh':
      query();
      break;
  }
};

const rows = ref<any>([])
async function query(data:{page?:number,pageSize?:number} = undefined) {
  if (data) {
    if (data.page)
    {
      currentPage.value = data.page;
    }

    if (data.pageSize) {
      pagination.value.rowsPerPage = data.pageSize;
    }
  }
    //获取总记录数
    rowSum.value=await DBService.getCount();
    if(currentPage.value>pagesNumber.value)
       currentPage.value = pagesNumber.value;
  //请求用户数据
  DBService.getPageList(keywordRef.value, currentPage.value, pagination.value.rowsPerPage).then(data => {
    rows.value = data;
  });
}
//批量删除
 function batchDelete()
{
  if(selected.value.length>0)
  {
     DBService.batchDelete(selected.value).then(()=>query());
  }
}
//添加新记录
function add() {
  showModal('add', { name: '', age: 10 });
}
function edit(item) {
  showModal('edit', item);
}
//下载
async function doDowload(pid: number) {
  const pp = await DBService.getProfile(pid);
  if (pp) {

    SaveAs(pp.file, pp.name);
  }
}
onMounted(() => {
  OnEventHandler(TableEvent.RefeshTable, query);
  //初始化
  query();
})
onUnmounted(() => {
  OffEventHandler(TableEvent.RefeshTable, query);
})
</script>
<style lang="scss" scoped></style>

系统Logger替换Console.log

SysConfig.js里UI的ProductLog 来控制:发布后系统是否输出系统日志

发布后建议改为:false

Log配置

####Logging使用方法:

Global.Logger().info 代替 Console.log

5 actual logging methods, ordered and available as:

log.setLevel(level, [persist])

This disables all logging below the given level, so that after a log.setLevel("warn") call log.warn("something") or log.error("something") will output messages, but log.info("something") will not.

####本质原理:

xframelib使用封装了loglevel库,https://www.npmjs.com/package/loglevel

  //日志记录
  Logger: (name?: string) => {
    //标记是否第一次判断
    if (!firstLogger) {
      //开发环节和产品发布后
      if (import.meta.env?.DEV || SysConfig.UI.ProductLog) {
        Log.enableAll();
      } else {
        //Log.disableAll();//trace  0
        Log.setDefaultLevel("warn"); //3
      }
      firstLogger = true;
    }
    const logname = name || "default";
    return Log.getLogger(logname);
  }

settings配置

1. projectSetting.ts 系统配置

实现接口ProjectConfig:

export interface ProjectConfig {
  //左侧抽屉-菜单状态
  leftDrawerOpen:boolean;
   //刷新页面
   reloadFlag:boolean;
   //显示TabMenu
   showTabMenu:boolean;
   TabMenuHeight:number;
   //显示breadcrumbs
  showBreadCrumbs:boolean;
  //#region 新增的,根据业务需要进行增加
  //整体默认主题样式
  themeStyle: string;
  //暗黑样式
  darkTheme: string;
  //是否收缩,默认收缩(左侧)
  leftCollapsed: boolean;
  //是否浮动起来(左侧菜单)
  overlayMenu: boolean;
  // 针对后台类布局的设置
  //右侧-主内容高度
  layoutContentHeight: number;
  //右侧-主内容宽度
  layoutContentWidth: number;
  //#endregion

  //#region  未启用配置字段(备用)
  // 是否显示SettingButton
  showSettingButton: boolean;
  // pageLayout whether to enable keep-alive
  openKeepAlive: boolean;
  // Use error-handler-plugin
  useErrorHandle: boolean;
  //#endregion

  // Whether to display the logo
  showLogo: boolean;
  // Whether to show the global footer
  showFooter: boolean;
  //底部栏的高度
  footerHeight: number;
  // menuType: MenuTypeEnum;
  headerSetting: HeaderSetting;
  // menuSetting
  menuSetting: MenuSetting;
}

leftDrawerOpen 控制左侧抽屉_菜单状态(展开/收缩)

//显示TabMenu

showTabMenu:true,

TabMenuHeight:30,

TabMenu

//显示面包屑

showBreadCrumbs:true,

面包屑

2. widgetSettings配置

2.1 配置规则
2.2 配置布局白名单

src/permission/index.ts

配置或修改对应的免登录可看的layout名称,否则无法获取到对应的widgetConfig列表。例如:

const layoutIDwhiteList = ['portalLayout', 'bigScreenLayout', 'productLayout'];

/**
 * 白名单数组
 */
const resultWhiteList: Array<IWidgetConfig> = [];
/**
 * 获取不需要登录时,白名单
 * Layouts对应的WidgetConfig数组
 * @returns
 */
function getWhiteListWidgetConfig() {
  const layoutIDwhiteList = ['portalLayout', 'bigScreenLayout', 'productLayout'];
  if (resultWhiteList.length === 0)
    widgetConfigSetting.forEach((it) => {
      const layoutid = it.layoutID;
      if (layoutid) {
        const idx = layoutIDwhiteList.indexOf(layoutid);
        if (idx >= 0) resultWhiteList.push(it);
      }
    });
  return resultWhiteList;
}

3.modalSetting配置

modalSetting是用来配置动态对话框内容组件的配置,一般按布局或视图分文件配置

modalSetting

弹框使用方法
  1. 为Layout布局,配置弹框容器ModalContainerWidget.vue,并默认加载

    弹框容器路径:src/widgets/layouts/ModalContainerWidget.vue

      {
        layoutID: 'backLayout', //归属组
        id: 'ModalContainerWidget',
        label: '弹框容器',
        container: LayoutContainerEnum.top,
        component: () => import('src/widgets/layouts/ModalContainerWidget.vue'),
        preload: true
      },
    
    

    弹框容器配置

  2. 编写对话框内容组件

    以对话框src/pages/back/standardTable/modals/addEditForm.vue为例

    1)定义对话框名称标识,要与modalSetting里配置相同

    const name = 'addEditForm';
    defineOptions({ name: 'addEditForm' });
    

    2)固定内容,用于确认或取消的处理方法

    import { ref, onMounted, onUnmounted, watch } from 'vue';
    import { OffEventHandler, OnEventHandler, EmitMsg } from '@/events';
    import { IExtraProperty } from '@/models/IModalModels';
    
    //#region 固定模板
    //确定或取消的处理方法
    function OkCancelHandler(isOk: boolean) {
      if (isOk) {
        onSubmit();
      } else {
        formRef.value = {}
      }
    }
    onMounted(() => {
      //启动时监听
      OnEventHandler(name, OkCancelHandler);
      // init(props.data);
    });
    onUnmounted(() => {
      OffEventHandler(name, OkCancelHandler);
    });
    //#endregion 
    

    3)定义props,固定两个props属性名为dataextra,类型可自定义或默认为object。用于接收弹框打开时,从外面传入的相关参数。

    import { object, oneOfType } from 'vue-types';
    import { IExtraProperty } from '@/models/IModalModels';
    
    const props = defineProps({
      data: oneOfType([Object]).def({}),
      extra: object<IExtraProperty>().isRequired,
    });
    

    props.data是外部传入的主要数据,主要用于填充表格,使用示例如下:

    const formRef = ref({});
    watch(
      () => props.data,
      val => {
        //initData();
        if (val)
          formRef.value = { ...val }
        else
          formRef.value = {}
      }, { immediate: true, deep: true }
    );
    

在template模版里绑定相关业务值 formRef.name

  <q-form class="modalContent column">
    <q-scroll-area class="col">
      <div class="row q-col-gutter-x-md dialog_form q-pa-md">
        <div class="col-6">
          <h5>
            <q-icon name="star" color="red" />姓名:
          </h5>
          <q-input outlined dense v-model="formRef.name" type="text" />
        </div>
        <div class="col-6">
          <h5>年龄:</h5>
          <q-input outlined dense v-model="formRef.age" type="text" />
        </div>
        <div class="col-12">
          <h5>上传简历</h5>
          <q-btn no-wrap v-show="$q.screen.gt.sm" label="上传" icon="mdi-cloud-upload-outline" color="primary"
            @click="doUpload">
            <q-uploader ref="profileUploader" :max-files="1" class="hidden" accept=".doc, .docx, .pdf" field-name="file"
              @added="fileUpload" />
          </q-btn>
        </div>
      </div>
    </q-scroll-area>
  </q-form>
  1. 调用对话框

调用showModal方法,传入actionMethod方法名和数据,例如:

//添加新记录
function add() {
  showModal('add', { name: '', age: 10 });
}
//编辑
function edit(item) {
  showModal('edit', item);
}

showModal方法模版:

//#region 通用对话框的相关代码
function showModal(actionMethod: string, rowData?: any) {
  let modalID: string = '';
  let extraData: any;
  switch (actionMethod) {
    case 'add':
      modalID = 'addEditForm';
      extraData = {
        title: `新建`,
        footer: 'true'
      };
      break;
    case 'edit':
      modalID = 'addEditForm';
      extraData = {
        title: `编辑`,
        footer: 'true'
      };
      break;
  }
  if (modalID) {
    const modalData = {
      modalID,
      extraData,
      rowData,
      width: 700
    };
    doLoadModal(modalData);
  }
}
//#endregion

环境变量设置与使用

VUE自带环境变量与 .env里用户自定义环境变量

process.env.*

console.log('环境变量',process.env);
结果如下:
环境变量

1. VUE自带环境变量

process.env.App_URL 网站根地址

process.env.DEV 开发模式

process.env.PROD 开发模式

2. .env用户自定义环境变量

用户自定义环境变量,必须是:“VITE_ ”开头,且全部大写,保存在 .env文件里。

自定义环境变量:process.env.VITE_PROJ_NAME

用户自定义环境变量

3.环境变量使用(全局绑定)

全局绑定方式:

  //保存网站根地址
  app.config.globalProperties.$AppURL=process.env.APP_URL;

使用/调用方式

console.log(this.$AppURL);

锁屏功能

前端开发模板默认支持锁屏功能,锁屏后将清除本地用户token,需要重新登录,默认锁屏时间为1小时

主题设置

Quasar默认支持的黑/白模式主题;自定义扩展其他主题

1.Quasar默认的黑/白主题模式

主题存放路径:src/css/theme.css

1)黑白主题模式全局变量定义:

.body--light为浅色模式的通用变量定义;.body--dark为深色模式的通用变量定义。

黑白主题模式变量定义

2)单个组件的(局部)黑白主题模式样式定义:

在 style里,将样式定义,分别包在 .body--light和.body--dark里

<style lang="scss" scoped>
.body--light {
  .header {
    color: black;
    background-color: white;
  }
}

.body--dark {
  .header {
    color: white;
    background-color: $dark;
  }
}
</style>

单组件黑白样式

2.自定义扩展其他主题

存放路径在:public/theme/*.css定义不同主题的相关变量

扩展其他主题

需要修改SysConfig.js进行开启和切换主题样式

切换其他主题

3. 定义SCSS样式变量

路径:src/css/quasar.variables.scss,定义css变量,以$开头

Quasar SCSS (& Sass) Variables

scss样式变量

使用方式:$ITEM_COLOR

<style lang="scss" scoped>
.body--light {
  .base-menu-item {
    color: $ITEM_COLOR !important;

    .baseRootItemActive {
      color: $ACTIVE_COLOR !important;
    }

    .baseItemActive {
      color: $ACTIVE_COLOR !important;
      background: $ACTIVE_BACKGROUND;
      transition: all 0.618s;
      font-weight: bold;

      &:after {
        content: '';
        position: absolute;
        width: 3px;
        height: 100%;
        background: $ACTIVE_COLOR !important;
        top: 0;
        right: 0;
      }
    }
  }
}

.body--dark {
  .base-menu-item {
    color: $ITEM_COLOR_DARK !important;

    .baseRootItemActive {
      color: $ACTIVE_COLOR_DARK !important;
    }

    .baseItemActive {
      color: $ACTIVE_COLOR_DARK !important;
      background: $ACTIVE_BACKGROUND_DARK;
      transition: all 0.618s;
      font-weight: bold;

      &:after {
        content: '';
        position: absolute;
        width: 3px;
        height: 100%;
        background: $ACTIVE_COLOR_DARK !important;
        top: 0;
        right: 0;
      }
    }
  }
}
</style>

其他问题

1. quasar spa打包后index.html 双引号丢失问题

原因:quasar 使用了html-minifier库  npmjs.com/package/html-minifier
解决方法:
**Ignoring chunks of markup**
If you have chunks of markup you would like preserved, you can wrap them <!-- htmlmin:ignore -->.
![htmlignore.png](./data/htmlignore.png)

2. 打包后编译,运行预览

quasar serve dist/spa

3. 使用cesium注意事项:

4. cesium升级的注意事项

package.json里的cesium版本升级,同时需要拷贝 nodemodules/cesium/Build/Cesium文件下全部内容,替换项目public/Cesium文件下内容。

cesium文件夹拷贝

5. 项目打包报“Top-Level-Await”错误

修改quasar.config.ts文件,增加viteConf.esbuild项内容为:

 supported: {
  'top-level-await': true
 },

​ 解决方法截图:

Top-Level-Await错误解决方法

6. quasar默认的material图标查询

查询地址:https://mui.com/material-ui/material-icons/

Iconfify图标库地址:https://icon.gis.digsur.com/ 的material-symbols图标库,图标名要以下划线连接

7. 定义路径别名

参考:https://quasar.dev/quasar-cli-vite/handling-vite#folder-aliases

{
  "extends": "@quasar/app-vite/tsconfig-preset",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "src/*": ["src/*"],
      "app/*": ["*"],
      "components/*": ["src/components/*"],
      "layouts/*": ["src/layouts/*"],
      "pages/*": ["src/pages/*"],
      "assets/*": ["src/assets/*"],
      "boot/*": ["src/boot/*"],
      "stores/*": ["src/stores/*"],

      "utils/*": ["src/utils/*"]
    }
  }
}

https://www.npmjs.com/package/vite-tsconfig-paths

统一在tsconfig.json里paths项定义路径别名,通过在quasar.config.ts 增加vite插件**vite-tsconfig-paths**来启用。

定义路径别名的插件配置

8. 解决import路径警告问题

问题如下:

import找不到模块警告

解决方法:在tsconfig.json 添加上"include": ["src/**/*"]

找不到模块解决方法

参考:https://juejin.cn/post/6924264635218542605

https://segmentfault.com/q/1010000040178399

9.解决二级目录部署网站时相关资源请求路径不对问题

解决方法:

1)quasar.config.ts配置 启用 publicPath : './'

2)图片等资源路线去掉“/” 例如:

SampleData/README.md

function doTextDownlaod()
{
    get('SampleData/README.md').then(p=>{
        if(p.data)
        SaveAs(p.data,'test.md');
    })
}

img/logo.png

        <div id="logo-part">
          <img src="img/logo.png" alt="Logo" id="waitLogo" />
          <img src="img/spinner.svg" alt="" id="waitSpinner" />
        </div>

icon: 'apiExampleimg/basicLayerManager.png'

图片正确路径

WebWorker提升性能:vite-plugin-webworker-service插件

说明:使用vite-plugin-webworker-service来代替workerpool库。因为 workerpool库有局限性,无法使用import引入外部类库或方法。

vite-plugin-webworker-service - npm (npmjs.com)

Vite plugin webworker service is a lightweight, powerful, and easy-to-use tool designed to make working with WebWorkers in your projects as seamless as possible.

The plugin generates a webworker file based on the used files ending in .service, as well as a bridge between the main thread and the webworker thread at build time, thereby allowing you to enjoy the reliability of typescript typechecks and various code editor tools, which cannot be achieved by directly using webworkers with postMessage and onmessage

Quasar.config配置:
vite-plugin-webworker-service插件

使用在 src/workers目录下编写 **.service.ts文件,自动编译成module模块化的worker.js

使用示例代码

Animate.css使用方法

Animate.css is a library of ready-to-use, cross-browser animations for use in your web projects. Great for emphasis, home pages, sliders, and attention-guiding hints.
https://animate.style/

1. 在CSS中使用

直接使用动画的原始名称,例如 “fadeIn”

<script setup lang="ts">
import { array, number, string } from 'vue-types';
import IFlashCardSection from './IFlashCardSection';
const props = defineProps({
  animateIn: string().def('fadeIn'),
  dataList: array<IFlashCardSection>().def([]),//数据列表
  nwidth: number().def(340),//面板宽度
  nheight: number().def(238),//面板高度
})
</script>

在CSS里使用方法如下:animation:fadeIn;

.flash_card {
  position: relative;
  width: v-bind(nwidth);
  height: v-bind(nheight);
  text-align: center;
  display: inline-block;
  &:hover {
    .sectionHoverDiv {
      -webkit-animation: v-bind(animateIn) 2s;
      animation: v-bind(animateIn) 2s;
      visibility: visible;
    }
  }
}

2. 在VUE组件中使用——TransitionSlide.vue

参考:https://vuejs.org/guide/built-ins/transition.html#custom-transition-classes
https://www.quasar-cn.cn/options/animations

<!-- 多个元素/组件的示例 -->
<transition-group
  appear
  enter-active-class="animated fadeIn"
  leave-active-class="animated fadeOut"
>

  <p key="text">
     Lorem Ipsum
  </p>
  <q-btn
    key="email-button"
    color="secondary"
    icon="mail"
    label="Email"
  />
</transition-group>

在上面的多元素示例中注意:

注意使用 代替了 .
DOM/组件都有 key 属性标记,例如key="text" 或者 key="email-button"
上面的两个示例中都使用了appear属性,这使得动画在组件渲染后将立即执行一次。此属性是可选的。

<template>
  <Transition
    mode="out-in"
    name="animate__slide"
    :duration="duration"
    :appear="appear"
    :enter-active-class="`animated ${enter}`"
    :leave-active-class="`animated ${leave}`"
  >
    <slot></slot>
  </Transition>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
  name: 'TransitionSlide',

  props: {
    enter: {
      type: String,
      default: 'fadeIn'
    },
    leave: {
      type: String,
      default: 'fadeOut'
    },
    duration: {
      type: Number,
      default: 1000
    },
    appear: {
      type: Boolean,
      default: true
    }
  }
});
</script>

3.使用v-wow中使用方法

<template>
  <div :style="{ overflow: 'auto', height: containerHeight + 'px' }">
    <VideoPanel v-wow="{ 'animation-name': 'slideInDown' }"></VideoPanel>
    <div v-wow="{ 'animation-name': 'slideInUp' }" style="background-color: #f00; height: 400px; width: 100%"></div>
    <div v-wow="{ 'animation-name': 'slideInLeft' }" style="background-color: #0f0; height: 400px; width: 100%"></div>
    <div v-wow="{ 'animation-name': 'slideInUp' }" style="background-color: #00f; height: 400px; width: 100%"></div>
    <CarouselPanel v-wow="{ 'animation-name': 'slideInLeft', 'animation-duration': '2s' }"></CarouselPanel>
    <VideoPanel v-wow="{ 'animation-name': 'slideInRight', 'animation-duration': '2s' }"></VideoPanel>
  </div>
</template>

<script setup lang="ts">
import { computed } from 'vue';

import CarouselPanel from '../ScrollPage/CarouselPanel.vue';
import VideoPanel from '../ScrollPage/VideoPanel.vue';
const containerHeight = computed(() => {
  return document.body.clientHeight; //LayoutTool.getContentHeight();
});
</script>

4. 在Quasar中使用

Built-in animation classes
https://quasar.dev/options/animations#introduction

https://www.quasar-cn.cn/options/transitions/
Quasar 组件的过渡效果
一些 Quasar 组件可以通过transition-show/transition-hide 或 transition-prev/transition-next 或 transition 等属性来控制过渡效果:

1)transition-show/transition-hide

QBtnDropdown
QInnerLoading
QTooltip
QMenu
QDialog
QSelect (通过 QMenu 和 QDialog)
QPopupProxy (通过 QMenu 和 QDialog)

<q-menu
  transition-show="jump-down"
  transition-hide="jump-up"
/>

2)transition-prev/transition-next

QCarousel
QTabPanels
QStepper
transition

3)QIntersection

      <q-intersection once transition="flip-right">
      <FlashCardPanel :data-list="dataDemoList"></FlashCardPanel>
      </q-intersection>

配置Electron编译模式流程(桌面端)

  1. 执行以下指令:pnpm quasar mode add electronpnpm quasar dev -m electron

新增了 src-electron目录 和自动安装必要的依赖库
electron1.png

  1. 不一定需要执行,如果运行第三步报错可以尝试运行这一步。 找到并切换到./node_modules/electron目录下命令窗口,执行命令:* npm install** ,需等待2分左右
    单独安装electron
  2. 回到项目目录下,执行命令:pnpm dev -m electron 即可以本地窗体模式运行
    electron3.png
  3. 运行效果
    electron4.png

基于Capacitor 移动混合APP开发

什么是Capacitor

Capacitor: A cross-platform native runtime for web apps

Capacitor is an open source native runtime for building Web Native apps. Create cross-platform iOS, Android, and Progressive Web Apps with JavaScript, HTML, and CSS.

https://capacitorjs.com/

Capacitor原生跨平台插件

Plugins in Capacitor enable JavaScript to interface directly with Native APIs.

Capacitor中的插件使JavaScript能够直接调用Native API接口

Web apps can access the full power of Native APIs with plugins. Plugins wrap common native operations that might use very different APIs across platforms while exposing a consistent, cross-platform API to JavaScript.

Web应用程序可以通过插件访问Native API的全部功能。插件包装了常见的本机操作,这些操作可能跨平台使用非常不同的API,同时向JavaScript公开了一致的跨平台API。

Additionally, the plugin capability in Capacitor makes it possible for teams with a mix of traditional native developers and web developers to work together on different parts of the app.

此外,Capacitor中的插件功能使传统移动开发人员和Web开发人员的团队能够在应用程序的不同部分协同工作。

Capacitor automatically generates JavaScript hooks on the client, so most plugins only need to use Swift/Obj-C for iOS and/or Java/Kotlin for Android. Of course, adding custom JavaScript for a plugin is also possible.

Capacitor会自动在客户端生成JavaScript挂钩,因此大多数插件只需要使用iOS版的Swift/Oj-C和/或Android版的Java/Kotlin。当然,为插件添加自定义JavaScript也是可能的。

https://capacitorjs.com/docs/plugins

Capacitor插件源列表

官方版Capacitor跨平台插件

官网 https://capacitorjs.com/docs/apis

NPM https://www.npmjs.com/org/capacitor

社区版-Capacitor Community插件

Github https://github.com/capacitor-community

NPM https://www.npmjs.com/org/capacitor-community

社区版-Capawesome插件

官网:https://capawesome.io/plugins/

Github https://github.com/capawesome-team

NPM https://www.npmjs.com/org/capawesome

示例工程:https://github.com/robingenz/capacitor-plugin-demo

Cordova兼容插件-awesome-cordova-plugins

Capacitor has support for most Cordova plugins, so developers can use the hundreds of existing Cordova plugins in their Capacitor apps. While certain Cordova plugins are not compatible with Capacitor, most are, so it's worth trying one if there's no existing Capacitor-specific plugin available.

https://capacitorjs.com/docs/plugins/community

官网: https://danielsogl.gitbook.io/awesome-cordova-plugins

Awesome Cordova Plugins is a curated set of wrappers for Cordova plugins that make adding any native functionality you need to your Ionic mobile app easy.

Github https://github.com/danielsogl/awesome-cordova-plugins

NPM :https://www.npmjs.com/org/awesome-cordova-plugins

@ionic-native NPM :https://www.npmjs.com/org/ionic-native

Ionic Native was renamed to Awesome Cordova Plugins原因

Today we are announcing some changes to the open source ionic-native project, namely that community member Daniel Sogl will be taking it over and it will be renamed awesome-cordova-plugins.

https://ionic.io/blog/a-new-chapter-for-ionic-native

Quasar Capacitor开发模式

Capacitor5 需要Gradle8和java17,下载最新版的Android Studio即可。

Capacitor versions

Requirements

Step 1: 安装开发环境

下载安装 最新版的Android Studio,下载 Android Studio,

通过SDK Managers可补充安装更多的 Android SDK

SDK Manager

Sdk Manager安装SDK

配置Windows上的 Android环境变量 ANDROID_SDK_ROOT (注意:环境变量 ANDROID_HOME 已经废弃)

cmd命令行界面运行下面的命令

setx ANDROID_SDK_ROOT "%USERPROFILE%\AppData\Local\Android\Sdk"
setx path "%path%;%ANDROID_SDK_ROOT%\tools;%ANDROID_SDK_ROOT%\platform-tools"

Step2:安装打包环境

1)java环境配置,打包时用到keytool命令

下载JDK Java Downloads | Oracle

安装JDK 17,配置环境变量:JAVA_HOME 为 C:\Program Files\Java\jdk-17

为path变量增加 C:\Program Files\Java\jdk-17\bin (否则,打包是keytool命令行找不到)

cmd命令行界面运行:

setx JAVA_HOME "C:\Program Files\Java\jdk-17"
setx path "%path%;C:\Program Files\Java\jdk-17\bin"

2)打包发布apk包时,需要用到的处理命令:zipalign 、 apksigner

注意:配置android打包发布时的环境变量path,否则zipalign命令找不到。34.0.0为我的Android SDK版本号

例如:C:\Users\zorro\AppData\Local\Android\Sdk\build-tools\34.0.0

cmd命令行界面运行:

setx path "%path%;%ANDROID_SDK_ROOT%\build-tools\34.0.0"

Step 3:生成 Capacitor工程目录

生成src-capacitor

quasar mode add capacitor

/src-capacitor/package.json内容类似

dependencies: {
  "@capacitor/app": "^5.0.0",
  "@capacitor/cli": "^5.0.0",
  "@capacitor/core": "^5.0.0",
  "@capacitor/splash-screen": "^5.0.0"
}

The @capacitor/app and @capacitor/splash-screen are optional, but it helps Quasar with some UI functionality if they are installed.

在src-capacitor下运行

npm install

Step 4: 安装Capacitor插件

在quasar主工程下安装capacitor插件,例如命令如下:

pnpm add @capacitor-community/keep-awake

同时,在src-capacitor也得重新运行安装上面插件

安装插件

Step 5: 运行调试

$ quasar dev -m capacitor -T [android|ios] 将启动IDE打开(Android Studio 或 Xcode) 开发环境,来连接移动设备运行调试。

注意:首次运行时,需要下载gradle相关依赖jar包(需要翻墙)

android debug

运行调试:打开浏览器,同步调试和输出log。

Chrome 浏览器 URL 输入chrome://inspect/#devices .

Edge浏览器 URL输入 edge://inspect/#devices

web  debug

运行调试

Android 相关配置说明

gradle配置修改

修改distributionUrl下载地址,改为gradle国内镜像地址 https://mirrors.cloud.tencent.com/gradle/

目前统一使用8.0.2

distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.0.2-all.zip

gradle URL

完全替换下面内容,优先使用国内阿里的Maven源

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    
    repositories {
      maven {
        url 'https://maven.aliyun.com/repository/public/'
      }
        google()
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:8.0.0'
        classpath 'com.google.gms:google-services:4.3.15'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

apply from: "variables.gradle"

allprojects {
    repositories {
      maven {
        url 'https://maven.aliyun.com/repository/public/'
      }
      maven {
        url 'https://maven.aliyun.com/repository/central'
      }
        google()
        mavenCentral()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

AndroidManifest.xml修改

路径:\src-capacitor\android\app\src\main\AndroidManifest.xml

android功能权限修改
    <!-- Permissions -->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE"/>

    <!-- 地理位置权限 -->
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
    <uses-feature android:name="android.hardware.location.gps" />
    <!-- 图片权限 -->
    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <!-- 录音权限 -->
    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
    <uses-permission android:name="android.permission.READ_CONTACTS"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <!-- 视频需要开启的权限 -->
    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <!-- 通知权限 -->
    <uses-permission android:name="android.permission.VIBRATE" />
    <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
    <!-- 摄像头 -->
       <uses-permission android:name="android.permission.CAMERA" />
竖屏横屏控制

强制竖屏android:screenOrientation="portrait"

竖屏

参考资料:Capacitor打包APP应用 )

键盘浮在页面上面

参考链接:android如何让布局保持位于键盘上方(一直在键盘上面)_如何让悬浮床始终在键盘的上方-CSDN博客

解决方法:

在manifest文件中的activity标签中修改android:windowSoftInputMode属性
属性参数:

各值的含义:
【A】stateUnspecified:软键盘的状态并没有指定,系统将选择一个合适的状态或依赖于主题的设置
【B】stateUnchanged:当这个activity出现时,软键盘将一直保持在上一个activity里的状态,无论是隐藏还是显示
【C】stateHidden:用户选择activity时,软键盘总是被隐藏
【D】stateAlwaysHidden:当该Activity主窗口获取焦点时,软键盘也总是被隐藏的
【E】stateVisible:软键盘通常是可见的
【F】stateAlwaysVisible:用户选择activity时,软键盘总是显示的状态
【G】adjustUnspecified:默认设置,通常由系统自行决定是隐藏还是显示
【H】adjustResize:该Activity总是调整屏幕的大小以便留出软键盘的空间
【I 】adjustPan:当前窗口的内容将自动移动以便当前焦点从不被键盘覆盖和用户能总是看到输入内容的部分

增加或修改下面内容:

android:windowSoftInputMode="adjustPan"

修改设置

键盘浮在页面的上面,效果图:

键盘浮在页面的上面

Android APP名称设置

设置app安装名称和app的桌面显示名称,路径为

src_capacitor/android/app/src/main/res/values/strings.xml

App名称

app_name为安装时显示名称

title_activity_main为app桌面图标显示名称

Capacitor Android打包发布

打包发布版

 quasar build -m capacitor -T android

apk build发布

Android APP打包发布处理

注意:

apk文件实质上是zip压缩格式,再Android 11以上,apk需要进行4字节对齐。
安卓SDK提供了 对齐工具zipalign。

zipalign 是一种 zip 归档文件对齐工具。它可确保归档中的所有未压缩文件相对于文件开头都是对齐的。这样一来,您便可直接通过 mmap(2) 访问这些文件,而无需在 RAM 中复制相关数据并减少了应用的内存用量。

[Android 11 重打包对齐错误_failure -124: failed parse during installpackagel-CSDN博客

下面参考来源:quasar官网publish to store

  1. 使用JDK >bin目录下的keytool工具,生成密钥store,需要输入和记住密码

    keytool -genkey -v -keystore my-release-key.keystore -alias alias_name -keyalg RSA -keysize 2048 -validity 20000
    
  2. 使用zipalign 对齐apk安装包

    $ zipalign -v 4 <path-to-same-apk-file> HelloWorld.apk
    
  3. 使用android sdk 下的build-tools中找到,apksigner 进行签名

    apksigner sign --ks my-release-key.keystore --ks-key-alias alias_name <path-to-unsigned-apk-file>
    

打包后安装示例:

Capacitor版本升级后无法打包问题

capacitor升级到6.0.0后,导致app无法打包,报错:“Execution failed for task ':app:checkReleaseAarMetadata'.”

解决方法:

1、src-capacitor/android/variables.gradle中变量targetSdkVersion和compileSdkVersion值由33改为34

sdkversion

2、修改gradle-wrapper.properties里的gradle版本,路径为src-capacitor/android/gradle/wrapper/gradle-wrapper.properties

distributionUrl=https://mirrors.cloud.tencent.com/gradle**/gradle-8.2.1-all.zip**

gradle

3、build.gradle的buildscrptip路径为src-capacitor/android/build.gradle的dependencies

classpath 'com.android.tools.build:gradle:8.2.1'

buildgradle.png

Node低版本16.x下@quasar/app-vite无法编译和Capacitor无法打包问题

原因:@quasar/app-vite 和 @capacitor/cli 都要求node版本大于18.0.0

解决方法:

Capacitor 将file://本地地址转换http地址

https://ionicframework.com/docs/core-concepts/webview#file-protocol

File Protocol

Capacitor and Cordova apps are hosted on a local HTTP server and are served with the http:// protocol. Some plugins, however, attempt to access device files via the file:// protocol. To avoid difficulties between http:// and file://, paths to device files must be rewritten to use the local HTTP server. For example, file:///path/to/device/file must be rewritten as http://<host>:<port>/<prefix>/path/to/device/file before being rendered in the app.

For Capacitor apps, convert file URIs like so:

import { Capacitor } from '@capacitor/core';

Capacitor.convertFileSrc(filePath);

For Cordova apps, the Ionic Web View plugin provides a utility function for converting File URIs: window.Ionic.WebView.convertFileSrc(). There is also a corresponding Ionic Native plugin: @awesome-cordova-plugins/ionic-webview.

工程里示例代码:

function add3DTileModel() {
  const layer = new TilesetLayer('layer')
  viewer.addLayer(layer)
  const filePath='file:///storage/emulated/0/Android/data/com.digsur.flight.navi/files/end3d/tileset.json';
  let tileset = new Tileset(
    // 'end3d/tileset.json'
    Capacitor.convertFileSrc(filePath)
  )
  tileset.setHeight(1420)
  layer.addOverlay(tileset)
  viewer.flyToTarget(tileset);
}

Iconify图标在线服务+离线图标

23年了,icon 方案该升级了 - 掘金 (juejin.cn)

iconify 的方案充分利用 svg 能力,利用 iconify.json 存储图标矢量信息。再通过下游的不同消费方式,开发者可以制作任意自己喜欢的图标消费方式。利用开放的形态,成功的将生产端和消费端以一种非依赖的关系分开,使用者可以自由组合。在经过一些年的发展,又拥有海量的存量图标和丰富的生态。

在项目中,利用上述方案,我们在不改变设计师习惯的同时,保留了开发者熟悉工具,还创新的引入了更好的图标方案。

iconify 方案中,我们可以避免上述提到的"字体"所带来的一切弊端,同时具备了以下几项优势:

​ 作者:YeeWang
​ 链接:https://juejin.cn/post/7189164727485300793

**Iconify图标在线服务使用 **

  1. 为VSCode安装Iconify IntelliSense插件

  1. 前端工程中安装开发依赖 npm i -D @Iconify/vue

IMG256

  1. vue页面/组件开发中使用Iconify图标(默认为在线模式)

注意:服务地址最后不能带“/”

IMG256

Iconify图标在线服务列表

提供符合Iconify标准服务接口的 在线SVG图标服务

  1. 国外服务地址
  1. 国内服务地址

icononline.png

离线Icon图标使用说明

使用情景:不部署icon图标服务或内网环境下

离线操作步骤:
如果项目部署是需要使用离线图标方式,在项目开发结束后,打包前进行如下设置和操作。

  1. 下载IconOffline.exe命令行工具,放在前端工程src同目录下,指定图标在线服务地址URL,运行扫描前端工程源码,生成图标离线注册文件。

%ZM$WRK6SM{7(M9`WGHU5UK

  1. 成功后会在src下的components中见到生成的IconOffline.ts文件(有新增代码,重新生成离线图标文件)

  1. 将SysyConfig.js中的 IconServiceURL置空,重新启动工程或网站,即可切换到使用离线图标状态下。

VSCode 用户代码片段

CodeSnippets在线生成工具

https://snippet-generator.app/

代码片段生成实例


积累的代码片段代码

代码片段集合

开源前端工程

模板参考的前端工程

https://github.com/york11122/quasar-admin-vue3-typescript

https://york11122.github.io/quasar-admin-vue3-typescript/#/login

基于vue/quasar 的中后台前端解决方案

https://gitee.com/incimo/vue-quasar-manage

https://incimo.gitee.io/vue-quasar-manage

Quasar-Sika-Design 开箱即用的中台前端 / 设计解决方案

https://github.com/sika-code-cloud/quasar-sika-design

http://quasar.sikacode.com/

quasar admin

聚商汇WMS-开源仓库管理系统

https://github.com/GreaterWMS/GreaterWMS

This Inventory management system is the currently Ford Asia Pacific after-sales logistics warehousing supply chain process . After I leave Ford , I start this project . You can share your vacant warehouse space, use it for those in need, and generate income

https://www.56yhz.com/md/windows/zh-CN

https://space.bilibili.com/407321291/channel/series

CC-ADMIN 基于Quasar的后台管理系统

https://blog.51cto.com/u_15896157/5895903

https://github.com/zhy6599/cc-admin-web

https://www.cc-admin.top/#/login

cc-admin.png

基于 Quasar Framework 开发的 Electron 笔记应用

vue全家桶+Electron+Quasar框架快速构建跨平台应用

https://blog.csdn.net/leida_wt/article/details/113495857

quasar-awesome

https://github.com/quasarframework/quasar-awesome

https://next-quasar-admin.netlify.app/ mail

开源成熟NPM库收集与使用

1. 前端http拦截

拦截所有ajax请求并允许修改请求数据和响应数据!实际项目中可以用于请求添加统一签名、协议自动解析、接口调用统计、改为离线化资源等。

前端http拦截js库 xhook > ajax-hook >xspy

xhook

Easily intercept and modify XHR request and response

开源地址:https://github.com/jpillora/xhook

NPM地址:https://www.npmjs.com/package/xhook

xhook.before(handler(request[, callback])[, index])

Modifying any property of the request object will modify the underlying XHR before it is sent.

To make the handler is asynchronous, just include the optional callback function, which accepts an optional response object.

To provide a fake response, return or callback() a response object.

xhook.after(handler(request, response[, callback]) [, index])

Modifying any property of the response object will modify the underlying XHR before it is received.

To make the handler is asynchronous, just include the optional callback function.

xhook.enable()

Enables XHook (swaps out the native XMLHttpRequest class). XHook is enabled be default.

xhook.disable()

Disables XHook (swaps the native XMLHttpRequest class back in)

响应结果对象结构:response Object

示例代码:返回假结果 https://github.com/jpillora/xhook/blob/main/example/fake-response.html

<h5>example3.txt (which does not actually exist - verify in devtools)</h5>
<pre id="res"></pre>

<h5>example3.txt (which does not actually exist - verify in devtools) - fetch</h5>
<pre id="fetch_res"></pre>

<script src="../dist/xhook.js"></script>
<script type="text/javascript">

xhook.before(function(request, callback) {
  //asynchronously...
  setTimeout(function() {
    //callback with a fake response
    callback({
      status: 200,
      text: 'this is the third text file example (example3.txt)',
      headers: {
        Foo: 'Bar'
      }
    });
  }, 500);
});

//vanilla call
var xhr = new XMLHttpRequest();
xhr.open('GET', 'example1.txt');
xhr.addEventListener('readystatechange', function(e) {
  document.getElementById('res').innerHTML = xhr.responseText;
});
xhr.send();

fetch('example1.txt')
  .then(function(response) {
    return response.text();
  }).then(function(text) {
    document.getElementById('fetch_res').innerHTML = text;
  });
</script>

ajax-hook

开源地址:https://github.com/wendux/ajax-hook

原理:https://www.jianshu.com/p/7337ac624b8e

Ajax-hook实现的整体思路是实现一个XMLHttpRequest的代理对象,然后覆盖全局的XMLHttpRequest,

原理图

https://www.npmjs.com/package/ajax-hook

关键API

proxy(proxyObject, [window\])

拦截全局XMLHttpRequest

注意:proxy 是通过ES5的getter和setter特性实现的,并没有使用ES6 的Proxy对象,所以可以兼容ES5浏览器。

参数:

返回值: ProxyReturnObject

ProxyReturnObject 是一个对象,包含了 unProxyoriginXhr

示例代码:

let result: any
function loadProxy() {
 result = proxy({
    //请求发起前进入
    onRequest: (config, handler) => {
      const { url } = config;

      const start = url.startsWith('https://3ds');
      if (start) {
        console.log('拦截的URL', url);
        // const proxyURL=url.replace('//resource.dvgis.cn/data/3dtiles/dayanta/','//3ds/end3d/')
        get3DTilesFile(url,11).then(result => {
         const {data,isText}=result;
          handler.resolve({
            config,
            status: 200,
            headers: {
              'content-type':isText? 'application/json':'application/octet-stream',
              'content-length': result?.length ?? 0,
              'access-control-allow-origin': '*',
              'access-control-allow-methods': '*',
              'access-control-allow-headers': 'Origin,X-Requested-Width,Content-Type,Accept'
            },
            response: data,
          })   
        });
        return;
      }
      else
        handler.next(config);
    },
    //请求发生错误时进入,比如超时;注意,不包括http状态码错误,如404仍然会认为请求成功
    onError: (err, handler) => {
      console.log(err.type, err, '66666')
      handler.next(err)
    },
    //请求成功后进入
    onResponse: (response, handler) => {
      const url=response.config.url;
      // const start = url.indexOf('//resource.dvgis.cn')>0;
      // if(start)
      // {
      //   console.log('555拦截返回结',url,response)
      //   handler.next(response);
      // }
      // else
        handler.next(response)
    }
  })

取消拦截代码

onUnmounted(() => {
   if (result)
     result.unProxy();
})

xspy

Hook ajax request and/or response. Modify header, body, status, credentials, etc in request/response

开源地址:https://github.com/jpillora/xhook

https://www.npmjs.com/package/xspy

示例:

xspy.onRequest((req) => {
  if(!req.headers["Authorization"]){
    req.headers["Authorization"] = "bearer sakxxd0ejdalkjdalkjfajd";
  }
});
xspy.onRequest(async (request, sendResponse) => {
  var result = await someAsyncOperation();
  var response = {...};
  sendResponse(response);
});
xspy.onResponse((req, res) => {
  console.log(res.url, res.status, res.headers);
});

2. WebWorker相关类库

vite-plugin-webworker-service

https://www.npmjs.com/package/vite-plugin-webworker-service

Vite plugin webworker service is a lightweight, powerful, and easy-to-use tool designed to make working with WebWorkers in your projects as seamless as possible.

The plugin generates a webworker file based on the used files ending in .service, as well as a bridge between the main thread and the webworker thread at build time, thereby allowing you to enjoy the reliability of typescript typechecks and various code editor tools, which cannot be achieved by directly using webworkers with postMessage and onmessage

  1. Install via npm:

    npm install -D vite-plugin-webworker-service
    
  2. Integrate in your project:

    //vite.config.ts
    import WebWorkerPlugin from 'vite-plugin-webworker-service';
    
    export default defineConfig({
      plugins: [WebWorkerPlugin()]
    })
    
  3. vite or vite build

  4. Start using it! Check out Usage for more detailed examples.

使用方法:

Basic usage:

.ts/.js

import { add } from "some.service.ts"
import { x2state } from "any.service.ts"

const res = await add(1,2)
console.log(res) // 3
await x2state() // state.value === 6

[some].service.ts

import { state } from 'state.ts'

// this function calculate in webworker
export async function add(a: number, b: number) {
    const res = a + b
    state.value = res
    return res
}

[any].service.ts

import { state } from 'state.ts'

// this function calculate in webworker
export async function x2state() {
    state.value = state.value * 2
}

3. 前端基础功能库

iconv-lite 文本编码库

iconv-lite: Pure JS character encoding conversion

https://www.npmjs.com/package/iconv-lite

var iconv = require('iconv-lite');
// Convert from an encoded buffer to a js string.
str = iconv.decode(Buffer.from([0x68, 0x65, 0x6c, 0x6c, 0x6f]), 'win1251');

// Convert from a js string to an encoded buffer.
buf = iconv.encode("Sample input string", 'win1251');

// Check if encoding is supported
iconv.encodingExists("us-ascii")
import iconv from 'iconv-lite';
// 示例用法
let str = '要转换的字符串';
let buffer = iconv.encode(str, 'gbk');
let decodedStr = iconv.decode(buffer, 'gbk');
console.log(decodedStr);

jszip 压缩解压类库

https://www.npmjs.com/package/jszip

A library for creating, reading and editing .zip files with JavaScript, with a lovely and simple API.

https://stuk.github.io/jszip/

4. Vite/Rollup插件库

vite-plugin-commonjs

https://www.npmjs.com/package/vite-plugin-commonjs

将原本只支持require引用的类库,支持为import引入

例如:

var iconv = require('iconv-lite'); 改为 import iconv from 'iconv-lite';

// Top-level scope
const foo = require('foo').default
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
import foo from 'foo'

const foo = require('foo')
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
import * as foo from 'foo'

const foo = require('foo').bar
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
import * as __CJS_import__0__ from 'foo'; const { bar: foo } = __CJS_import__0__

// Non top-level scope
const foo = [{ bar: require('foo').bar }]import * as __CJS_import__0__ from 'foo'; const foo = [{ bar: __CJS_import__0__.bar }]

配置插件使用方式:

import commonjs from 'vite-plugin-commonjs'
export default {
  plugins: [
    commonjs(/* options */),
  ]
}

quasar工程里配置如下:

使用方式

否者,在使用import动态引入模块时,会报require引入错误,如下:

require报错

前端升级支持扫码登录方法

使用“帝测授权宝”APP -android版,实现前端系统的扫码登录。扫码APP是默认连接外网用户权限管理系统,也就是系统SysConfig.js的LoginAuthURL为:https://gis-auth.digsur.com

使用Antdesign版模版的升级

升级支持扫码登录需要修改内容:

扫码登录升级内容

使用Quasar版模版的升级

*xframelib使用知识点

0. Layout使用方法

1)LayoutContainer使用,获取对应的LayoutManager

示例代码:

<template>
    <LayoutContainer
      id="flydemoLayoutContainer"
      :widgetConfig="configRef"
      :layoutID="layoutIDRef"
      @containerLoaded="loadedHandler"
    >
    </LayoutContainer>
  </template>
  
  <script lang="ts" setup>
  import { getRightWidgetConfig } from 'src/permission';
import { defineOptions,onMounted, onUnmounted, ref } from 'vue';
import { Global,LayoutContainer, LayoutManager } from 'xframelib';
  
  defineOptions({
    name: 'flydemoLayout'
  });
  
  const configRef = ref(getRightWidgetConfig());
  const layoutIDRef = ref('flydemoLayout');
  //要加载的Widget列表 
  const initWidgetIDs=["MapDemoWidget"];
  let layoutManager: LayoutManager;
  //获取服务此Layout的layoutManager
  function loadedHandler(evt: any) {
    layoutManager=evt.layoutManager;
    if(layoutManager)
    {
        initWidgetIDs.forEach(id=>{
            //加载组件
            layoutManager.loadWidget(id);
        });
    }
  }
 onMounted(()=>{
     //关闭加载动画
  Global.Loading('end');
 })
  onUnmounted(()=>{
    if(layoutManager)
    {   
        //卸载组件
        initWidgetIDs.forEach(id=>{
            layoutManager.unloadWidget(id);
        });
    }

  });
  </script>
  
  <style scoped lang="scss">
  :deep(.centerdiv) {
    pointer-events: none !important;
    >*{
      pointer-events:auto;
    }
  }
  </style>
  

2)视图页面中获取对应的LayoutManger

监听SysEvents.LayoutContainerLoaded事件,传入参数为 {layoutID,layoutManager}。

注意:首次加载时,视图可能先于LayoutManager构建出来,就需要使用监听方式

示例代码关键代码

import { OnEventHandler,OffEventHandler } from 'src/events';
import { onMounted, onUnmounted } from 'vue';
import { Global,LayoutManager,SysEvents } from 'xframelib';

let layoutManager: LayoutManager|undefined;
OnEventHandler(SysEvents.LayoutContainerLoaded,init);
function init(evt)
{
    if(evt.layoutID==='flydemoLayout')
    {
      load(evt.layoutManager);
      OffEventHandler(SysEvents.LayoutContainerLoaded,init);
   } 
}

完整视图实例代码:GameControl.vue

<template>
</template>
<script setup lang="ts">
import { OnEventHandler,OffEventHandler } from 'src/events';
import { onMounted, onUnmounted } from 'vue';
import { Global,LayoutManager,SysEvents } from 'xframelib';

  //要加载的Widget列表
const initWidgetIDs=["GameControlWidget",'KeyboardControlWidget','PanelDataWidget2'];
let layoutManager: LayoutManager|undefined;
OnEventHandler(SysEvents.LayoutContainerLoaded,init);
function init(evt)
{
    if(evt.layoutID==='flydemoLayout')
    {
      load(evt.layoutManager);
      OffEventHandler(SysEvents.LayoutContainerLoaded,init);
   } 
}
function load(layoutmanager:LayoutManager)
{
  layoutManager= layoutmanager;
        if(layoutManager)
        {
            initWidgetIDs.forEach(id=>{
                layoutManager?.loadWidget(id);
            })
        }
}
onMounted(()=>{
    layoutManager= Global.LayoutMap.get("flydemoLayout");
    if(layoutManager)
    load(layoutManager);
});
onUnmounted(()=>{
    OffEventHandler(SysEvents.LayoutContainerLoaded,init);
    if(layoutManager)
    {
      layoutManager?.unloadWidgets(initWidgetIDs);
    }
})
</script>
<style scoped></style>

3)LayoutContainer的插槽应用

LayoutContainer的具名插槽有:main、back、front、left、right、bottom,默认插槽为 default

LayoutContainer容器代码(部分)如下:

<template>
  <div class="layoutContainer" :style="containerStyle">
    <div ref="topContainer" class="topContainer">
      <slot name="top"></slot>
      <component v-for="[key, item] in topContainerComponents" :ref="(el) => setItemRef(el, key)" :key="key" :is="item">
      </component>
    </div>
    <div>
      <!-- 主要容器-底部 -->
      <div ref="centerMainContainer" class="centerdiv mainContainer" v-if="isEnableRouterView">
        <slot name="main">
          <router-transition></router-transition>
        </slot>
      </div>
      <!-- 上一层-主容器 -->
      <div ref="centerBackContainer" class="centerdiv backContainer">
        <slot name="back"></slot>
        <component v-for="[key, item] in centerbackComponents" :ref="(el) => setItemRef(el, key)" :key="key" :is="item">
        </component>
      </div>
      <!-- 最上浮动-主容器 -->
      <div ref="centerFrontContainer" class="centerdiv centerFrontContainer">
        <slot name="front"></slot>
        <component v-for="[key, item] in centerfrontComponents" :ref="(el) => setItemRef(el, key)" :key="key"
          :is="item"></component>
      </div>

      <div ref="leftContainer" class="leftContainer">
        <slot name="left"></slot>
        <component v-for="[key, item] in leftContainerComponents" :ref="(el) => setItemRef(el, key)" :key="key"
          :is="item"></component>
      </div>

      <div ref="rightContainer" class="rightContainer">
        <slot name="right"></slot>
        <component v-for="[key, item] in rightContainerComponents" :ref="(el) => setItemRef(el, key)" :key="key"
          :is="item"></component>
      </div>
    </div>
    <div ref="bottomContainer" class="bottomContainer">
      <slot name="bottom"></slot>
      <component v-for="[key, item] in bottomContainerComponents" :ref="(el) => setItemRef(el, key)" :key="key"
        :is="item"></component>
    </div>
    <!-- 增加默认插槽 -->
    <slot name="default"></slot>
  </div>
</template>
<template>
    <LayoutContainer
      id="flydemoLayoutContainer"
      :widgetConfig="configRef"
      :layoutID="layoutIDRef"
      @containerLoaded="loadedHandler"
    >
    <template #back>  
       <div style="background-color: #f00;height:100%">这是布局对象的SLot插入内容背景          </div>
    </template>
    </LayoutContainer>
  </template>
  
  <script lang="ts" setup>
  </script>
  
  <style scoped lang="scss">
  :deep(.centerdiv) {
    pointer-events: none !important;
    >*{
      pointer-events:auto;
    }
  }
  </style>
  

1. V0.7.7之前通用知识点

1)Widget组件如何才能支持“隐藏”和“打开”——可见性控制

widget需要对外暴露 isShow属性 和 changeVisible方法

<template>
   <div v-show="isShow">
       <span>能支持“隐藏”和“打开”——可见性控制的Widget示例模版</span>
   </div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
onMounted(() => {

})
/**
* 对外暴露接口
*/
const isShow = ref(true);
function changeVisible(isVisible: boolean = false) {
   isShow.value = isVisible;
}
defineExpose({ changeVisible, isShow });
</script>
<style lang="scss" scoped></style>

2)监听后续Widget组件是否加载了

import { SysEvents} from 'xframelib';

import {OffEventHandler,OnEventHandler} from 'src/events/index';

OnEventHandler(SysEvents.WidgetLoadedEvent,onWidgetLoaded);

      function onWidgetLoaded(evt2)
      {
        if(evt2.layoutID===_LayoutID&&evt2.widgetID===_widgetID)
          {
            //TODO:做事情
            OffEventHandler(SysEvents.WidgetLoadedEvent,onWidgetLoaded);
          }
      }
      OnEventHandler(SysEvents.WidgetLoadedEvent,onWidgetLoaded);

3)使用Hprose的序列化与反序列化

Hprose序列化协议规范:https://github.com/hprose/hprose/tree/master/3.0

https://github.com/hprose/hprose/blob/master/3.0/Hprose 3.0 序列化协议规范.mediawiki

建议:使用Hprose的序列化取代JSON序列化

原因:

使用方法:

import { deserialize, serialize } from 'xframelib';

const testObj={
  "name": "wangming",
  "age": 40,
  "other": [
    "111",
    2222,
    "test"
  ]
};
//序列化
const result=serialize(testObj);
console.log('序列化结果:',result);
//反序列化对象
const obj=deserialize(result);
console.log('反序列化对象:',obj);

结果如下:

hprose序列化与反序列化

4)H5Tool中复制文本

H5Tool支持的复制文本方法

2. V0.7.8新增的

1) Global.getLayoutManager通过WidgetID反向获取LayoutManager

Global.WidgetConfigList存储所有的WidgetSetting,在开发模版的src/permission/index.ts的getRightWidgetConfig()方法里进行的初始化

WidgetConfigList初始化

通过WidgetID反向获取LayoutManager

        const tmpLayoutManager = Global.getLayoutManager(widgetID.value);
        tmpLayoutManager?.changeWidgetVisible(widgetID.value, true);

2)XWindow组件如何使用

vue仿win窗口实现悬浮窗拖动,调整大小,最大化,复原。

  //#region **********用于控制功能是否启用
 Enables:{
   TurfAsync:true,
   CesiumOfflineCache:true,//Cesium缓存
 }
 //#endregion

在App.vue的onBeforeMount()函数里添加控制代码
Enables控制

	   //参考:https://github.com/zorrowm/turf-async
	    //启用turfAsync
	    if(Global.Config.Enables.TurfAsync)
	    {
	      loadScript('./js/turf-async/index.js');
	    }
	     //启用Cesium缓存
	    if(Global.Config.Enables.CesiumOfflineCache)
	    {
	      loadScript('./js/CesiumOfflineCache.min.js');
	    }

在CesiumViewerWidget.vue里,setup中启用cesium缓存功能

    //参考:https://github.com/zorrowm/CesiumOfflineCache
    if (Global.Config.Enables.CesiumOfflineCache) {
      (window as any).CesiumOfflineCache?.ruleList?.add('*');
    }

3. V0.7.9

1)每个LayoutManager对象自动加载到Global.LayoutMap里

旧LayoutContainer代码
旧LayoutContainer初始化代码

新代码示例——节选,也可以去掉containerLoaded事件监听

<template>
  <div class="dc-container">
    <LayoutContainer :widgetConfig="configRef" :layoutID="layoutIDRef" />
  </div>
</template>

<script lang="ts">
import { getRightWidgetConfig } from 'src/permission';
import { appStore } from 'src/stores';
import { defineComponent, onMounted, ref } from 'vue';
import { Global, H5Tool, LayoutContainer } from 'xframelib';

export default defineComponent({
  name: 'bigScreenLayout',
  components: {
    LayoutContainer
  },
  setup(props, { attrs, slots, emit }) {
    const widgetCofig = getRightWidgetConfig();
    const configRef = ref(widgetCofig);
    const layoutIDRef = ref('bigScreenLayout');
    //获取服务此Layout的layoutManager
    // function loadedHandler(evt: any) {
    //   if (evt.layoutID === layoutIDRef.value) {
    //     //Global.Logger().debug(evt, 'loadedHandler');
    //     Global.LayoutMap.set(evt.layoutID, evt.layoutManager);
    //   }
    // }

2)监听SysEvents.LayoutContainerLoaded来判断LayoutManager已经构建

​ OnEventHandler(SysEvents.LayoutContainerLoaded, initHandler); 来监听判断LayoutContainer已经加载好了。(主要用于LayoutContainer放在Widget里做容器时)

示例代码如下:

<template>
  <TransitionSlide enter="fadeInUp" leave="fadeOutUp">
    <APIExamplePanel v-if="apiExamplesList" :data="apiExamplesList" @contentItemClicked="doItemClickHandler" />
  </TransitionSlide>
</template>

<script setup lang="ts">
import APIExamplePanel from 'components/Quasar/APIExample/index.vue';
import TransitionSlide from 'components/TransitionSlide.vue';
import { onMounted,ref } from 'vue';
import {  Global, LayoutManager, SysEvents } from 'xframelib';
import { useRoute, useRouter } from 'vue-router';
import { OffEventHandler, OnEventHandler } from 'src/events';
import SystemsEvent from 'src/events/modules/SystemsEvent';

const route=useRoute();
const router = useRouter();
const apiExamplesList = ref<any[]>();
//异步加载数据
import('src/settings/apiExampleSetting/cesium/index').then((p) => {
  apiExamplesList.value = p.default;
});

let currentItem:any;
let layoutManagerExamples: LayoutManager;
//新的容器widget
const layoutWidgetID = 'frontLayoutWidget';
function initHandler(data)
{
  if(Global.LayoutMap.has(layoutWidgetID))
  {
    layoutManagerExamples = Global.LayoutMap.get(layoutWidgetID);
    OffEventHandler(SysEvents.LayoutContainerLoaded, initHandler);
    cesiumLoadedHandler();
  }
}

function cesiumLoadedHandler()
{
  if(layoutManagerExamples&&currentItem?.path)
  {
    console.log('开始加载:',currentItem.path)
      layoutManagerExamples.loadWidget(currentItem.path);
  }
  else
  {
    console.log('失败加载:',currentItem.path)
  }
}

function doItemClickHandler(it) {
  currentItem=it;
  //主页面的LayoutManager
  const mainlayoutManager: LayoutManager = Global.LayoutMap.get('productLayout');

  if(route.query?.wid!=it.path)
    router.push({query:{wid:it.path}});

  if (!mainlayoutManager.isWidgetLoaded(layoutWidgetID)) {
  
    OnEventHandler(SysEvents.LayoutContainerLoaded, initHandler);
    mainlayoutManager.loadWidget(layoutWidgetID).then(() => {
       //首次直接加载
      console.log('首次直接加载',layoutWidgetID);
      //首次直接加载
    });
  } else {
    layoutManagerExamples = Global.LayoutMap.get(layoutWidgetID);
    if (layoutManagerExamples) {
      let isExist = false;
      const excludeWidgetIDs = ['cesiumWidget', it.path];
      layoutManagerExamples.unloadAllWidgets(excludeWidgetIDs);
      isExist = layoutManagerExamples.isWidgetLoaded(it.path);
      if (!isExist) {
        //没有加载,则加载
        layoutManagerExamples.loadWidget(it.path);
      } else {
        console.log('让widget可见');
        layoutManagerExamples.changeWidgetVisible(it.path, true);
      }
    }

    mainlayoutManager.getWidgetComponent(layoutWidgetID).changeVisible(true);
  }
}

onMounted(()=>{
  const wid=route.query?.wid;
  if(wid)
  {
    setTimeout(() => {
      doItemClickHandler({path:wid});
    }, 500);

  }
})
</script>

3)强化Widget链式排队加载,通过afterid实现顺序加载

通过SysEvents.WidgetLoadedEvent实现,等待父组件加载后,再加载当前子组件,实现链式依赖的顺序加载

核心代码片段如下:

 {
          const self=this;
          const afterID=widgetTarget.afterid;
          const laterWidget=widgetTarget;
          function onWidgetLoaded(evt2)
          {
            if(evt2.layoutID===self._LayoutID&&evt2.widgetID===afterID)
              {
                self._loadWidget(laterWidget);
                // console.log('0000晚加载widget',laterWidget);
                Global.EventBus.off(SysEvents.WidgetLoadedEvent,onWidgetLoaded);
              }
          }
          Global.EventBus.on(SysEvents.WidgetLoadedEvent,onWidgetLoaded);
          return this.loadWidget(widgetTarget.afterid);
        }

4)为H5Tool增加DOM操作方法等

H5Tool新增相关方法如下

4. V0.8.0

1)统一修改各层样式

统一修改各层样式,默认空白区域鼠标可以穿越,各元素鼠标可点击

默认各层容器样式改为,例如:

.leftContainer {
  position: absolute;
  top: 0px;
  left: 0px;
  z-index: var(--layout-left-zindex);
  height: 100%;
  pointer-events: none;
  >* {
    pointer-events: all !important;
  }
}

5. V0.8.1

1)增加RouterTransitionAnimate组件

增加具有默认动画的RouterTransitionAnimate组件

RouterTransitionAnimate支持视图切换时,自带动画效果,也可以修改设置动画效果

该组件的代码为:

<template>
  <SuspenseWithError>
    <router-view v-slot="{ Component, route }">
      <transition-group appear
        :enter-active-class="enterActive"
        :leave-active-class="leaveActive"
      >
        <keep-alive >
          <component v-if="route.meta.keepAlive"  :is="Component"  :key="route.name" />
        </keep-alive>
        <component  v-if="!route.meta.keepAlive" :is="Component" :key="route.name" />
      </transition-group>
    </router-view>
  </SuspenseWithError>
</template>

<script lang="ts" setup>
import SuspenseWithError from './SuspenseWithError.vue';
defineOptions({ name: 'RouterTransition' });
interface Props {
    enterActive?: string;
    leaveActive?:string;
}
//参考:https://quasar.dev/options/animations#usage
const props = withDefaults(defineProps<Props>(), {
  enterActive:"animated fadeIn",
  leaveActive:"animated fadeOut"
});
</script>

2)widget配置支持外部配置样式(重要)

widget配置对象(IWidgetConfig)启用layout属性(IWidgetLayout布局样式),增加cssClass外部配置样式类

IWidgetConfig配置接口对象,变化的两个属性:

widgetConfig配置对象外部样式

以前端开发模板里的 src/widgets/portal/PortalHeaderTitleWidget.vue为例

PortalHeaderTitleWidget.vue

  1. 给Widget配置外部布局和CSS样式

    /**
     * 组件配置项
     */
    const defaultWidgetCofig: Array<IWidgetConfig> = [
      {
        layoutID: 'portalLayout', //归属组
        id: 'HeaderTitleWidget',
        label: '头部栏',
        container: LayoutContainerEnum.top,
        component: () => import('src/widgets/portal/PortalHeaderTitleWidget.vue'),
        preload: true,
        layout:{
          top:30,
          left:10,
          width:"100%",
          height:"30px",
          background:"#f00"
        },
        cssClass:"a1 a2"
      },
    

    配置外部样式和布局

  2. 在Layout或对应视图里定义类名为a1、a2的样式

  3. 加载后效果——外部样式应用

    外部样式应用结果

6. V0.8.2

修改VueWindow组件(VWindow)拖动定位错误;弃用DownloadByUrl方法;移除窗体同步库WSynchro.js;更新依赖库版本;

重点: 修改VueWindow组件(VWindow)拖动定位错误

7. V0.8.3

v0.8.3 增加HproseRPC过程的请求字符串和返回字符串的外部解码类HproseRPCCodec;

function doDecodeRPC()
{
    const responseHeader={};
    if(rpcvalue.value==='request')//请求
    {
        rpcResult.value= HproseRPCCodec.Instance.decodeRequest(rpcInfo.value,responseHeader);
    }
    else //返回结果字符串
    {
        rpcResult.value= HproseRPCCodec.Instance.decodeResponse(rpcInfo.value,responseHeader);
    }
    console.log('解码请求头:',responseHeader);
}

http://localhost:9000/#/back/hprose-encode
RPC反序列化

8. V0.8.4

v0.8.4 为widget权限对象,增加layoutid属性;修改LockHelper锁屏功能BUG;去掉DownloadByUrl(可改用HttpDownload方法);为H5Tool增加readFileBytes方法(读取前端文件,为bytes二进制数组/字符串);更新依赖库版本;

依赖库版本升级

    <q-uploader ref="profileUploader2" :max-files="1" class="hidden"  accept=".doc, .docx, .pdf"
    field-name="file" @added="UploadFileToProfile" />

UploadFileToProfile上传文件代码

//单附件文件上传
async function UploadFileToProfile(files) {
  const file = files[0];
  const fileName=file.name;
  //***文件二进制内容******
  const data=await H5Tool.readFileBytes(file);
  const result = await ProfileService.SaveProfileInfoAsync(
    //@ts-ignore
  {
      id:'',
      file:data,
      name:fileName,
      studentid:currentSelectId,
  }
  );
  profileUploader2.value?.reset();
  if(result){
    Global.Message.info('档案上传成功!')
    query();
    return;
  }
  Global.Message.info('档案上传失败!')
}

ProfileService.SaveProfileInfoAsync方法内容

    /**
    *新建或更新档案 
    *@param profile  档案对象 
    *@returns System.Threading.Tasks.Task`1[System.Boolean]
    */
    async SaveProfileInfoAsync( profile?:Profile)
    {
      const proxyClient=await this.hproseProxyClient.getHproseProxy();
      if(!proxyClient)
      {
        console.warn('ProxyClient为空,SaveProfileInfoAsync方法无法调用');
      }
      
      return await proxyClient?.SaveProfileInfoAsync(profile);
    }

9.V0.8.5

动态加载Widget时绑定更多IWidgetConfig属性(如:id,layoutID等);加载widget报错,输出更多异常信息;

widget组件运行时,获取自身的options属性

import { getCurrentInstance } from 'vue';     
const instance = getCurrentInstance();
console.log('5555555555',instance?.proxy?.$options)

就是组件里可以拿到当前运行时的widget id和 layoutID,可用于内部卸载Widget自身。

widget外部绑定属性

测试卸载Widget自身:成功

      const instance = getCurrentInstance();
      const wid=instance?.proxy?.$options.id;
      const layoutid=instance?.proxy?.$options.layoutID;
      setTimeout(() => {
        console.log('开始卸载自身');
        if(wid)
        Global.LayoutMap.get(layoutid)?.unloadWidget(wid);

      }, 2000);

10. V0.8.6

修改get方法支持:业务服务API请求、请求网站public下资源、http完整URL请求;

之前的get方法是请求相对于SysConfig.js的ServiceURL下DefaultWebAPI后台服务的相对地址APIURL,但被广泛用于请求前端的相对资源的请求,这在之前是错误的用法。

*从v0.8.6版,get方法支持:业务服务API请求、请求网站public下资源、http完整URL请求;

/**

 * 业务服务Get请求、请求网站下资源、http完整请求
 * @param url public下资源相对路径
 * @param _params 参数
 * @param isForceAPI 是否强制为API请求,默认false
 * @returns 返回Promise对象
   */
 declare function get(url: string, _params?: any, isForceAPI?: boolean): Promise<any>;

get方法应用:
请求完整URL

  get('https://file.gis.digsur.com/swagger/v1/swagger.json').then(p=>{
      console.log('请求结果:',p);
        if(p.data)
        JsonDownload(p.data,'test')
    }).catch(()=>{
        Global.Message.warn('下载错误');
    })

请求网站相对资源

   get('SampleData/README.md').then(p=>{
         if(p.data)
         SaveAs(p.data,'test.md');
     })

请求后端API服务

   get('/api/Grade/GetGradeList',undefined,true)

11. V0.8.7

修改0.8.6版的getDownload内部错误(与HttpDownload方法共存使用);

使用示例:

 getDownload('./SampleData/README.md');
 
 getDownload('https://file.gis.digsur.com/swagger/v1/swagger.json');
 
  getDownload('https://file.gis.digsur.com/swagger/v1/swagger.json','2.json');

12. V0.8.9

为Function扩展promise方法(事件异步方法将按Promise异步执行);修改isElement判断错误;增加ZipTool用于文件在线压缩和解压;H5Tool增加blockEvent停止冒泡bindDropFileHanlder拖拽文件、readFilePromise异步读取文件、优化readFileBytes为bytes二进制数组;IsTool增加isStringLikeJson、isStringLikeKml方法;H5Tool增加了onPasteHandler和offPasteHandler使用document绑定粘贴事件(不是任意div元素都可以绑定paste事件,只有设置了 contenteditable="true" 的元素,才会触发该事件);FileDownload增加SaveToSelectedFile方法,将文件保存到选定路径;

  1. 为Function扩展promise方法

    实现事件方法以Promise异步方式执行

    readFile.promise

         if (isFunction(readFile.promise))
            readFile.promise(event.dataTransfer.files[0], '1111111111').then(p => {
              console.log('8888888888', p);
            });
    

    事件回调方法,例如:

    function readFile(file: File, msg: string, cb: Function) {
      var reader = new FileReader();
      console.log('5555555555')
      reader.onload = function (e) {
        try {
          console.log('结果:',reader.result);
          cb(null, reader.result)
        } catch (error) {
          cb(error)
          console.error('加载 Shapefile 失败:', error);
        }
      };
      reader.readAsText(file)//readAsArrayBuffer(file);
    }
    

    H5Tool的promisify使用,例如:

    const data=await H5Tool.promisify(readFile,event.dataTransfer.files[0],'11111111')
    
  2. bindDropFileHanlder拖拽文件

    /**

    * 给DIV对象绑定拖拽文件事件

    * @param ele id或classname或 HtmlElement对象

    * @param onDropFile 接收文件拖拽列表_回调函数

    */

    static bindDropFileHanlder(ele:string|Element,onDropFile:(fileList:FileList)=>{})

    给id为map的div增加拖拽文件加载事件

        //拖拽文件加载
        H5Tool.bindDropFileHanlder("map",dragFileHandler);
    

    拖拽文件处理方法:

    async function dragFileHandler(fileList: FileList) {
      if (!fileList || fileList.length === 0)
        return;
      {
        const len = fileList.length;
        console.log('文件个数为:', len);
        const files = Array.from(fileList) 
      }
    }
    
  3. readFilePromise异步读取文件

    /**

    • 以Promise方式读前端文件

    • @param file 文件或BLob

    • @param type 类型'FileBytes'|'ArrayBuffer'|'Text'|'BinaryString'|'DataURL',默认为ArrayBuffer

    • @param encoding 文本编码,默认为UTF-8

    • @returns

    • */

    static readFilePromise(file:File|Blob, type:'FileBytes'|'ArrayBuffer'|'Text'|'BinaryString'|'DataURL'= 'ArrayBuffer',encoding="UTF-8"):Promise<string|ArrayBuffer>

    示例代码:H5Tool.readFilePromise(file,'Text')

    async function fileUpload(files) {
      const file = files[0];
      const name = file.name;
    
      const data=await H5Tool.readFilePromise(file,'Text') as string;
      const json=JSON.parse(data);
      plotHelper.addFeatures(json);
    }
    
    /**
     * 读取前端文件,为bytes二进制数组(base64编码的)
     * @param file 
     * @returns 
     */
    static readFileBytes(file:File|Blob):Promise<any>
    {
      return H5Tool.readFilePromise(file,"FileBytes");
    }
    
  4. H5Tool增加了onPasteHandler和offPasteHandler使用document绑定粘贴事件

    不是任意div元素都可以绑定paste事件,只有设置了 contenteditable="true" 的元素,才会触发该事件

        H5Tool.onPasteHandler(pasteFileHandler);
    
    async function pasteFileHandler(copyContent: string | File[]) {
      if (isString(copyContent)) {
        Global.Message.info(copyContent as string);
      } else if (Array.isArray(copyContent)) {
        const files = copyContent as File[];
        if (files.length === 0) return;
        const result = await import2DFiles(files);
        if (result && result.length > 0) {
        }
      }
    }
    
  5. FileDownload增加SaveToSelectedFile方法

可能报“window.showSaveFilePicke个不存在”问题
handle = await window.showSaveFilePicker(options);

不存在方法错误

13. V0.9.0

  1. iconv字符编码使用

    import { iconv } from 'xframelib';
    
    //字符转编码
         const utf8Str='ÁÖ»¶4ÔÂÔ±¨.docx';
        const tmp =iconv.encode(utf8Str,"ISO-8859-1")
        const gb2312 = iconv.decode( tmp,'gbk')
        console.log('5555555',gb2312);
    

    沙发是

  2. ZipTool用于文件在线压缩和解压

直接读取压缩包文件,解压:readZipFromFile

readZipFromFile

   const zipfile=fileList[0];
   const result=await ZipTool.readZipFromFile(zipfile);
   fileArray.value=result.map(it=>it.filename);
   result.forEach(it=>{
    SaveAs(it.content,it.filename);
   })

saveZipFile、saveZipFromFiles、saveZipFileSync方法

 const files=Array.from(fileList);
    ZipTool.saveZipFromFiles('0000.zip',files);
    const list=[];
    for(let i=0;i<files.length;i++)
    {
        const file=files[i];
        const filename=file.name;
      const buffer= await H5Tool.readFilePromise(file,'ArrayBuffer');
      const content=new Uint8Array(buffer);
      list.push({
        filename,content
      })
    }

    ZipTool.saveZipFile('22222.zip',list);
    const tt=ZipTool.saveZipFileSync('33333.zip',list);
    console.log('压缩0000',tt);
    }

*重要业务组件使用

0. QLayoutMainContainer组件

解决LayoutContainer与QLayout等组件的兼容使用问题

用于LayoutContainer的mainContainer容器的router-view页面切换;

以支持视图里使用 QPage、QFoot等组件(这些Quasar组件要求必须放在QLayout里)

1. PDFViewer组件

2. ImageViewer组件

<template>
  <div >
    <q-layout  view="hHh lpR fFf" container style="height: calc(100vh - 50px)" class="shadow-2 ">
      <q-page-container class="pageContainer">

        <div class="pageBottom">
          <div class="q-col-gutter-xs row items-start ">
            <div class="col-4">
              <q-img src="/img/daxing.jpg"  @click="showImage(1)">
                <div class="absolute-bottom text-center" >
                  机场图片
                </div>
              </q-img>
            </div>

            <div class="col-4" >
              <q-img src="/img/daxing2.jpg" style="height:70px;" @click="showImage(2)">
                <div class="absolute-bottom text-center">
                  进近图
                </div>
              </q-img>
            </div>

            <div class="col-4">
              <q-img src="/img/daxing3.jpg" @click="showImage(3)">
                <div class="absolute-bottom text-center">
                  三维模型
                </div>
              </q-img>
            </div>
          </div>
        </div>
      </q-page-container>
    </q-layout>
    <ImageViewer  :url="imgURL"/>
  </div>
</template>

<script setup lang="ts">
import ImageViewer from 'src/components/ImageViewer.vue';
import { ref } from 'vue';

const  imgURL=ref(null);
function showImage(item:number)
{
  setTimeout(()=>{
    imgURL.value=undefined;
  },20);
  switch(item)
  {
    case 1:
    imgURL.value='img/daxing.jpg'
      break;
      case 2:
    imgURL.value='img/daxing2.jpg'
      break;
      case 3:
    imgURL.value='img/daxing3.jpg'
      break;
  }
}
</script>

<style  lang="scss" scoped>
.pageContainer
{
  position: relative;
  background-color: #0f0;
}
.pageBottom
{
  position:absolute;
  bottom: 0px;
  height:70px;
  width:100%;
  background-color: #f00;
}
</style>

ImageViewer示例

3.PopoverPanel组件

用于地图选中要素时弹框显示属性表或其他内容

4.XWindow组件

防Windows窗体的容器组件,支持缩小、放大/还原、关闭、拖拽移动、拖拽改变窗体大小,支持任务栏窗体图标控制。

<template>
    <XWindow v-show="isShow" top="10px" left="10px" nWidth="300px" nHeight="400px" title="XWindowWidget模版"
        icon="img/basicimage/arcgis_img.png" :hasMax="true"pid="widgetID"  @loaded="loadedHandle" @close="doClosePanel">
        <span>这是XWindowWidget模版,窗体的内容示例</span>
    </XWindow>
</template>

<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { Global, XWindow, XWindowManager,WindowsMap,MinWindowMap } from 'xframelib';
const widgetID = ref('xwindowWidgetTemplate');
let windowID = '';
//获取到窗体的id
function loadedHandle(panelData) {
    windowID = panelData.id;
}
function doClosePanel(panelData) {
    widgetID.value = panelData.pid;
    if (panelData.pid) {
        Global.LayoutManager?.unloadWidget(widgetID.value);
    }
}
onMounted(() => {
    //最小化后,定时打开widget
    setTimeout(() => {
        const tmpLayoutManager = Global.getLayoutManager(widgetID.value);
        tmpLayoutManager?.changeWidgetVisible(widgetID.value, true);
    }, 8000);
})
/**
 * 对外暴露接口
 */
const isShow = ref(true);
function changeVisible(isVisible: boolean = false) {
    isShow.value = isVisible;
    if (windowID && isVisible)
        XWindowManager.openWindowPanel(windowID);
}
defineExpose({ changeVisible, isShow });
</script>
<style lang="scss" scoped></style>

对应的WidgetSetting配置widgetdemos如下:

import { LayoutContainerEnum } from 'xframelib';
/**
 * 组件配置项
 */
const defaultWidgetCofig: Array<IWidgetConfig> = [ 
  {
    layoutID: 'bigScreenLayout', //归属组
    id: 'xwindowWidgetTemplate',
    label:'XWindowWidget示例',
    container: LayoutContainerEnum.centerFront,
    component: () => import('src/widgets/layouts/XWindowWidgetTemplate.vue'),
    preload: true
  },

];
export default defaultWidgetCofig;

5.Markdown组件

实现Markdown文档的在线编辑与在线预览

const props=defineProps(
    {
        file:{
            type:String,
            default:""
        },
        content:
        {
            type:String,
            default:"",
        },
        //是否显示主题列表
        showList:
        {
          type:Boolean,
          default:false,
        }
    }
  );

MarkdownViewer的大小默认为100%填充,可通过margin-bottom来控制下边距。

<template>
    <base-content scrollable>
        <MarkdownViewer file="README.md" :style="bottomStyle"/>
    </base-content>
</template>

<script setup lang="ts">
import BaseContent from 'src/components/Quasar/BaseContent.vue';
import MarkdownViewer from '@/components/Markdown/MarkdownViewer.vue';
import { appStore } from '@/stores';
import { computed } from 'vue';
const appState=appStore();
const bottomStyle=computed(()=>{
    if(appState.showFooter)
    return `margin-bottom:${appState.footerHeight}px;`;
    else
    return '';
}
);
</script>

传入参数说明:

file :md文件网络地址,加载md文档内容;

content:传入md文档的文本内容;

MarkdownEditor的大小默认为100%填充,可通过bottom来控制下边距。

<template>
    <base-content scrollable>
        <MarkdownEditor file="README.md" :style="bottomStyle"/>
    </base-content>
</template>

<script setup lang="ts">
import BaseContent from 'src/components/Quasar/BaseContent.vue';
import MarkdownEditor from '@/components/Markdown/MarkdownEditor.vue';
import { appStore } from '@/stores';
import { computed } from 'vue';
const appState=appStore();
const bottomStyle=computed(()=>{
    if(appState.showFooter)
    return `bottom:${appState.footerHeight}px;`;
    else
    return '';
}
);
</script>

6.SideMenuBar组件

实现左边或右边浮动路由菜单,放在q-drawer组件里使用。

  <q-layout v-if="$q.screen.lt.sm" view="hHH lpR fFf">
    <q-drawer v-model="rightDrawerOpen" side="right" bordered>
      <!-- drawer content -->
      <q-btn dense flat round v-tooltip="'菜单列表'" @click="toggleRightDrawer" >
        <Icon icon = "codicon:chrome-close" />
      </q-btn>
      <SideMenuBar :top-menu-children="menuList"></SideMenuBar>
    </q-drawer>
  </q-layout>

支持传入EnumPageMenu列表或Router列表

const menuList = [
  {
    label: '首页',
    icon: 'ant-design:home-outlined',
    kind: EnumPageMenu.Route,
    path: '/product/index'
  },
  {
    label: '帝测时空云平台',
    kind: EnumPageMenu.Route,
    path: '/product/productPage'
  },
  {
    label: '解决方案',
    children: [
      {
        label: '智慧农业',
        kind: EnumPageMenu.URL,
        path: 'https://www.baidu.com'
      },
      {
        label: '智慧水利',
        kind: EnumPageMenu.Route,
        path: '/river'
      },
      {
        label: '智慧物流',
        kind: EnumPageMenu.URL,
        path: 'https://www.126.com'
      }
    ]
  },
  {
    label: 'API示例',
    kind: EnumPageMenu.Route,
    path: '/product/apiexamples'
  },
];

Router列表

const topMenuChildren = computed(() => {
  const rightRoutes = productRoute.children;
  if (!rightRoutes) {
    Global.Message.warn('无法获取路由列表!');
    return [];
  }
  return rightRoutes;
});

7.ContextMenu组件

ContextMenu组件为通用的右键菜单。

8. WidgetMenuBar组件(MenuBarWidget)

解决了:同一组件多次调用(动态import),互相样式和状态互不影响。

WidgetMenBar是与src/settings/widgetMenuSetting配合使用的,一组横向或纵向的Widget构建的菜单栏,在大屏中使用较多。

之前问题:

多个MenuBarWidget

MenuBarWidget菜单栏widget,可以广泛复用(跨LayoutContainer)

新MenuBarWidget复用

只需要根据id,在menuBarStyle.scss里定义对应不同菜单栏的布局和样式

menuBarStyle定义修改样式

运行效果:(样式互不影响)

示例

前端开发知识点积累(大杂烩)

1. Flex布局知识点

1)Flex:1的作用

来源:https://mp.weixin.qq.com/s/uX8AesG2pLdEOXWCG_uAUg

在现代网页布局中,Flexbox(弹性盒子布局)是一种强大的工具。它通过 "flex" 属性,帮助开发者轻松控制元素的伸缩性。

Flex 属性的组成

Flex 属性是一个复合属性,包含以下三个子属性:

  1. 「flex-grow」:决定元素在容器中剩余空间的分配比例。默认值为 0,表示元素不会扩展。当设置为正数时,元素会按照设定比例扩展。
  2. 「flex-shrink」:决定元素在空间不足时的收缩比例。默认值为 1,表示元素会按比例收缩。当设置为 0 时,元素不会收缩。
  3. 「flex-basis」:定义元素在分配多余空间之前所占据的主轴空间。默认值为 auto,表示元素占据其本来大小。

语法格式为:

flex: <flex-grow> <flex-shrink> <flex-basis>;

将一个元素的 flex 属性设置为 1,相当于将其分配了一个相对于其他元素相同的可伸缩空间。换句话说,flex: 1 会使该元素尽可能地占据父容器中的剩余空间,同时保持其他元素的相对位置和大小。

具体来说:

flex: 1; /* 等同于 flex: 1 1 0%; */

实际应用示例

这种设置对于实现灵活、响应式的布局非常有用。例如,我们可以将导航栏中的项目设置为 flex: 1,使其自动平分导航栏的宽度。

「HTML 结构:」

<nav class="navbar">
  <a href="#">标签 1</a>
  <a href="#">标签 2</a>
  <a href="#">标签 3</a>
</nav>

「CSS 样式:」

.navbar {
  display: flex;
}

.navbar a {
  flex: 1;
  border: 1px solid #ccc;
  padding: 10px;
  text-align: center;
}

在这个示例中,通过设置链接的 flex 属性为 1,实现了它们的平均分配。无论导航栏的宽度如何变化,链接都会自动调整大小,以适应父容器的空间。

更多
比较 "flex: 1" 与其他值:

通过掌握 flex 属性及其子属性,你可以创建出更加灵活和响应式的网页布局,提升用户体验。

2)垂直分散居中

display: flex;

flex-direction:column;(row为水平方向,column为垂直方向);

    display: flex;
    flex-direction: column;
    justify-content: space-between;
    align-items: center;

垂直居中

2. Vue组件传参方式

来源:Vue 3组件通信13种方法,https://mp.weixin.qq.com/s/ZlImQB2FIsJ0R8s4SVl84g

1)父传子 Props方式

这是最基本也是最常用的通信方式。父组件通过属性向子组件传递数据。

「父组件:」

<template>
  <child :name="name"></child>
</template>

<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

const name = ref('小明')
</script>

「子组件:」

<template>
  <div>{{ props.name }}</div>
</template>

<script setup>
import { defineProps } from 'vue'

const props = defineProps({
  name: {
    type: String,
    default: '',
  },
})
</script>

2)子传父 Emit内部事件

子组件可以通过触发事件的方式向父组件传递数据。

「子组件:」

<template>
  <button @click="handleClick">点击我</button>
</template>

<script setup>
import { ref, defineEmits } from 'vue'

const message = ref('来自子组件的问候')
const emits = defineEmits(['greet'])

const handleClick = () => {
  emits('greet', message.value)
}
</script>

「父组件:」

<template>
  <child @greet="handleGreet"></child>
</template>

<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

const handleGreet = (message) => {
  console.log(message) // 输出: "来自子组件的问候"
}
</script>

3)父传子 Attributes ($attrs)

$attrs 包含了父组件传递给子组件的所有属性,除了那些已经被 props 或 emits 声明的。

「父组件:」

<template>
  <child name="小明" age="18" hobby="篮球"></child>
</template>

「子组件:」

<script setup>
import { useAttrs } from 'vue'

const attrs = useAttrs()
console.log(attrs) // { age: "18", hobby: "篮球" }
</script>

4)*父子组件, 双向绑定 (v-model)

v-model 提供了一种简洁的方式来实现父子组件之间的双向数据绑定。

「父组件:」

<template>
  <child v-model:name="name"></child>
</template>

<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

const name = ref('小明')
</script>

「子组件:」

<template>
  <input :value="name" @input="updateName" />
</template>

<script setup>
import { defineProps, defineEmits } from 'vue'

const props = defineProps(['name'])
const emit = defineEmits(['update:name'])

const updateName = (e) => {
  emit('update:name', e.target.value)
}
</script>

5)祖先组件与子孙组件,依赖注入 (Provide/Inject)

provide 和 inject 允许祖先组件向所有子孙组件传递数据,而不需要通过每一层组件手动传递。

「祖先组件:」

<script setup>
import { provide, ref } from 'vue'

const themeColor = ref('blue')
provide('theme', themeColor)
</script>

「子孙组件:」

<script setup>
import { inject } from 'vue'

const theme = inject('theme')
console.log(theme.value) // 'blue'
</script>

6)浏览器存储

localStorage 和 sessionStorage 可以用于在不同页面或组件之间共享数据。

// 存储数据
localStorage.setItem('user', JSON.stringify({ name: '小明', age: 18 }))

// 读取数据
const user = JSON.parse(localStorage.getItem('user'))

7)Window 对象

虽然不推荐,但在某些场景下,可以使用 window 对象在全局范围内共享数据。

// 设置全局数据
window.globalData = { message: '全局消息' }

// 在任何地方使用
console.log(window.globalData.message)

8)全局属性

Vue 3 提供了 app.config.globalProperties 来替代 Vue 2 中的 Vue.prototype,用于添加全局可用的属性。

// main.js
const app = createApp(App)
app.config.globalProperties.$http = axios

// 在组件中使用
import { getCurrentInstance } from 'vue'

const { proxy } = getCurrentInstance()
proxy.$http.get('/api/data')

9)其他(路由传参、Pinia 状态管理、Vuex 状态管理、公共事件通信 Mitt)

3、Typescript枚举类型与字符串转换

https://typescript.p6p.net/typescript-tutorial/enum.html

测试代码:

  enum EnumBasicLayer
      {
          Single='single',
          TDT_VEC='tdt_vec',
          TDT_IMG='tdt_img',
      }
    onMounted(() => {
      console.log('0000',EnumBasicLayer.Single,typeof EnumBasicLayer.Single)
      console.log('1111','single'===EnumBasicLayer.Single);
      const tt='tdt_vec'
      switch(tt)
      {
        case EnumBasicLayer.TDT_VEC:
          console.log('2222','switch string OK');
          break;
      }
      switch(EnumBasicLayer.TDT_VEC)
      {
         case 'tdt_vec':
         console.log('3333','switch Enum OK');
         break;
      }

测试结果:

0000 single string
1111 true
2222 switch string OK
3333 switch Enum OK

4、Font字体转换压缩——WOFF/WOFF2

WOFF & WOFF2

Web开放字体格式(Web Open Font Format,简称WOFF)是一种网页所采用的字体格式标准。此字体格式发展于2009年,[3]万维网联盟的Web字体工作小组标准化,现在已经是推荐标准。[4]此字体格式不但能够有效利用压缩来减少文件大小,并且不包含加密也不受DRM(数字著作权管理)限制。(来源:维基百科

这是专门给网页使用的字体格式,体积非常小,实测压缩思源宋体字体文件,可以把体积压缩到 OTF 字体 70% 的大小。

WOFF 和 WOFF2 的区别在于:

WOFF本质上是包含了基于SFNT的字体(如TrueTypeOpenType或其他开放字体格式),且这些字体均经过WOFF的编码工具压缩,以便嵌入网页中。[3]WOFF 1.0使用zlib压缩,[3]文件大小一般比TTF小40%。[11]而WOFF 2.0使用Brotli压缩,文件大小比上一版小30%。(来源:维基百科

因此,一般推荐直接使用 WOFF2。https://zhuanlan.zhihu.com/p/577387539)

在线转换工具网站:

https://fontconverter.com/zh/

https://convertio.co/zh/font-converter/

https://transfonter.org/

5、TypeScript接口反射及应用

参考:https://deepinout.com/typescript/typescript-questions/190_typescript_typescript_reflection_for_interfaces.html

在 TypeScript 中,我们可以使用 keyof 关键字来获取接口的所有属性名。例如,我们可以使用如下代码获取 Person 接口的属性名数组:

type PersonKeys = keyof Person;

// Output: "name" | "age" | "greet"
console.log(PersonKeys);

TypeScript

Copy

上面的代码中,我们定义了一个类型别名 PersonKeys,它是 Person 接口的属性名的联合类型。然后我们使用 console.log 打印出了 PersonKeys 的值,得到了 "name" | "age" | "greet"

实际应用

static async updateItem(id: number, itemPart: any = {}) {
    let student = await db.students.get(id);
    if (student) {
      //WM错误:无法使用
      // type fieldKeys = keyof IStudent;
      // const keys = Object.keys(itemPart) as fieldKeys[];
      // keys.forEach((field: string) => {
      //   student[field] = itemPart[field];
      // });
      const fields=this.getTableNames(db.students);
      if(fields&&itemPart)
      fields.forEach((field: string) => {
        const tmp=itemPart[field];
        if(tmp!=undefined)
        student[field] =tmp; 
      });

      student.lastModified = new Date();
      await db.students.put(student);
    }
  }

注意:keyof在vue中无法使用!!!!

IndexDB里通过数据库表字段,来控制传入的对象

private static tabFields:Map<string,string[]>=new Map();
  static  getTableNames(dbtable:any)
  {

    const key=dbtable.name;
    if(!this.tabFields.has(key))
      {
        const fields:string[]=[];
        this.tabFields.set(key,fields)
        const tt=dbtable.schema.indexes;
        const primKey= db.students.schema.primKey.name;
        tt.forEach(it=>{
          if(it.name!=primKey)
           fields.push(it.name);
        });
      }
      return this.tabFields.get(key);
  }

6、其他

MapShaper打包与精简的修改要点

1、MapShaper重新打包

MapShaper直接引入使用会报错误,例如:

requireError

因为:iconv-lite、mproj库都是通过require方式引入的

解决方法:

import取代require