vue-widget-quasar-clivite-template核心是基于VUE的Widget机制开发框架,使用quasar作为UI库,通过quasar-cli和Vite结合方式进行前端工程编译打包。
与现在的vue-widget-tempalte相比较,目录组织机构相同(views改为pages),Widget插件机制相同,只是在UI库方面由AntDesign转为Quasar,打包编译配置和插件基本相同,都利用了vite。
AntDesign仅仅是个好用的Web组件UI库,但Quasar不仅有组件且兼容支持Web、移动应用程序、桌面应用程序和浏览器扩展(一体化解决),而且有指令、组合函数、插件等。
Quasar组件粒度小,大部分组件可以互相嵌套灵活组合,形成万变变化(类似积木)
Quasar自带常用/好用的间距、阴影、断点以及可见性和定位等样式,特别是断点广泛适用,大大降低多屏适配的工作量
Quasar指令超级实用好用
基于 VUE3+Hprose+Typescript 的前端框架,与ElementUI、AntDesign VUE等界面库无关,一直是来源于项目和服务于项目。
网站系统配置Sysconfig.js对应的Global.Config
公共事件总线Global.EventBus
整体系统日志Global.Logger()
Layout布局容器和Widget机制实现
Axios的Http请求封装实现Global.Axios、AxiosHelper{get, post, requestPost, requestGet, getData, requestPostBody}
Hprose Proxy方式调用后台Hprose服务实现
与用户系统后台服务适配的Token验证/刷新和角色权限过滤
大文件下载BigFileDownload
普通文件下载FileDownload封装{ Download, SaveAs, JsonDownload, HttpDownload, DownloadByUrl }
大文件上传FileUpload
H5Tool常用小功能
SignalrClient
JQuery工具 (hasClass 、addClass、removeClass、toggleClass、setCssProperty)
StorageHelper 本地缓存对象
IsTool类型判断工具(数字、字符串、对象、数组、函数等)
ValidateTool验证工具(身份证、手机号、IP地址、邮箱、密码强度判断、URL、车牌号等)
与后端配合使用的加密解密算法XXTEA
npm install 后需要对@quasar/app-vite的vite版本进行单独升级(默认版本太旧)
quasar-vite升级.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
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。
开发模式下,每次系统运行,会在Public目录下生成MenuRoutes.json或 /help/register请求来生成系统元数据JSON。
{
"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/ 进行系统注册(只有管理员级别用户才有权限)
用户登录
统一用户登录
解决外部传入tk时,优先验证token,实现正确跳转。
即:A系统登录后,带着有效token,直接进入B系统里的使用相关功能
2024-10-21后开发模板版本,支持跳转的两种模式:
业务路径+token
http://localhost:9001/#**/back/pdfviewer?tk=xxxx**
http://localhost:9001/#/back/pdfviewer?tk=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjAwMDY1YTMzLTc5NTEtNDI1MC1iMWIyLWFiN2I4NzFhNGFhMyIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiOiJhZG1pbiIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6IjAiLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3VzZXJkYXRhIjoiMCIsIm5iZiI6MTcyOTQ5NjQwOCwiZXhwIjoxNzI5NDk3MDA4LCJpc3MiOiJGbHlMb2xvIiwiYXVkIjoiVGVzdEF1ZGllbmNlIn0.AXlIOAGRhZmve4aVuT78pwonJi58ujdaUifV1MEYroU
login+业务路径+token
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文件夹、Service文件夹,前端服务对接代码都是通过后台服务元数据在线生成的。
https://gis-hprosetest.digsur.com/ 在线生成Hprose服务代码,放在/src/service文件夹下
https://gis-apitest.digsur.com/#/home 在线生成WebAPI服务代码,放在/src/api文件夹下
配置用户登录验证URL:SysConfig.js里ServiceURL项的 LoginAuthURL 属性
前端使用“XXTEA非对称加密”算法进行加密的。
https://auth.gis.digsur.com/swagger/index.html 的 /api/Token/check方法,如果验证正确token,返回用户信息;如果错误,则报500异常
参考模版代码为:src/pages/back/StandardTable/index.vue
下图为表格规范化结构:
index.vue为表格所在主页面视图
models/TabColummns.ts 为表格相关属性字段定义
models/ActionMenus.ts 为表格行操作功能和扩展功能定义
modals为表格相关弹框对话框内容,存放不同对话框文件夹
涉及的主要组件为:
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>
SysConfig.js里UI的ProductLog 来控制:发布后系统是否输出系统日志
发布后建议改为:false
####Logging使用方法:
Global.Logger().info 代替 Console.log
5 actual logging methods, ordered and available as:
Global.Logger().trace(msg)
Global.Logger().debug(msg)
Global.Logger().info(msg)
Global.Logger().warn(msg)
Global.Logger().error(msg)
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);
}
实现接口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,
//显示面包屑
showBreadCrumbs:true,
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;
}
modalSetting是用来配置动态对话框内容组件的配置,一般按布局或视图分文件配置
为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
},
编写对话框内容组件
以对话框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属性名为data和extra,类型可自定义或默认为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>
调用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);
结果如下:
process.env.App_URL 网站根地址
process.env.DEV 开发模式
process.env.PROD 开发模式
用户自定义环境变量,必须是:“VITE_ ”开头,且全部大写,保存在 .env文件里。
自定义环境变量:process.env.VITE_PROJ_NAME
全局绑定方式:
//保存网站根地址
app.config.globalProperties.$AppURL=process.env.APP_URL;
使用/调用方式
console.log(this.$AppURL);
前端开发模板默认支持锁屏功能,锁屏后将清除本地用户token,需要重新登录,默认锁屏时间为1小时
通过SysConfig.UI.LockTime来设置锁屏时间,以秒为单位,不小于10秒。
锁屏监听onLockListener,本质是document监听 mousedown、mousemove事件
/**
* 全局监听Lock
*/
export function onLockListener() {
//开始监听
// console.log('开始监听 mousedown');
timekeeping();
document.addEventListener('mousedown', timekeeping);
document.addEventListener('mousemove', timekeeping);
}
取消锁屏监听 unlockListener
/**
* 取消全局监听Lock
*/
export function unLockListener() {
// console.log('注销监听 mousedown');
document.removeEventListener('mousedown', timekeeping);
document.removeEventListener('mousemove', timekeeping);
}
锁屏状态,以reactive对象,通过watch监视LockState.isLock状态,确定进入锁屏状态
watch(()=>LockState.isLock,value=>{
Global.Logger().debug('监听锁屏状态变化',value);
})
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>
存放路径在:public/theme/*.css定义不同主题的相关变量
需要修改SysConfig.js进行开启和切换主题样式
路径:src/css/quasar.variables.scss,定义css变量,以$开头
Quasar SCSS (& Sass) Variables
使用方式:$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>
原因: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 -->. 
quasar serve dist/spa
package.json里的cesium版本升级,同时需要拷贝 nodemodules/cesium/Build/Cesium文件下全部内容,替换项目public/Cesium文件下内容。
修改quasar.config.ts文件,增加viteConf.esbuild项内容为:
supported: { 'top-level-await': true },
解决方法截图:
查询地址:https://mui.com/material-ui/material-icons/
Iconfify图标库地址:https://icon.gis.digsur.com/ 的material-symbols图标库,图标名要以下划线连接。
参考: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**来启用。
问题如下:
解决方法:在tsconfig.json 添加上"include": ["src/**/*"]
参考:https://juejin.cn/post/6924264635218542605
https://segmentfault.com/q/1010000040178399
解决方法:
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'
说明:使用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配置:
使用在 src/workers目录下编写 **.service.ts文件,自动编译成module模块化的worker.js
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/
直接使用动画的原始名称,例如 “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;
}
}
}
参考: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>
<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>
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>
新增了 src-electron目录 和自动安装必要的依赖库
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.
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
官网 https://capacitorjs.com/docs/apis
NPM https://www.npmjs.com/org/capacitor
Github https://github.com/capacitor-community
NPM https://www.npmjs.com/org/capacitor-community
官网:https://capawesome.io/plugins/
Github https://github.com/capawesome-team
NPM https://www.npmjs.com/org/capawesome
示例工程:https://github.com/robingenz/capacitor-plugin-demo
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
Capacitor5 需要Gradle8和java17,下载最新版的Android Studio即可。
Requirements
- Xcode 14.1+ (for iOS)
- Android Studio Flamingo 2022.2.1 or newer (for Android)
下载安装 最新版的Android Studio,下载 Android Studio,
通过SDK Managers可补充安装更多的 Android 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"
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"
生成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
在quasar主工程下安装capacitor插件,例如命令如下:
pnpm add @capacitor-community/keep-awake
同时,在src-capacitor也得重新运行安装上面插件
$ quasar dev -m capacitor -T [android|ios]
将启动IDE打开(Android Studio 或 Xcode) 开发环境,来连接移动设备运行调试。
注意:首次运行时,需要下载gradle相关依赖jar包(需要翻墙)
运行调试:打开浏览器,同步调试和输出log。
Chrome 浏览器 URL 输入chrome://inspect/#devices
.
Edge浏览器 URL输入 edge://inspect/#devices
修改distributionUrl下载地址,改为gradle国内镜像地址 https://mirrors.cloud.tencent.com/gradle/
目前统一使用8.0.2
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.0.2-all.zip
完全替换下面内容,优先使用国内阿里的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
}
gradle.properties文件修改
增加内容:
android.overridePathCheck=true
android.enableJetifier=true
路径:\src-capacitor\android\app\src\main\AndroidManifest.xml
<!-- 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"
activity 竖屏节点属性:
android:screenOrientation="landscape"
参考资料: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"
键盘浮在页面的上面,效果图:
设置app安装名称和app的桌面显示名称,路径为
src_capacitor/android/app/src/main/res/values/strings.xml
app_name为安装时显示名称
title_activity_main为app桌面图标显示名称
quasar build -m capacitor -T android
注意:
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
使用JDK >bin目录下的keytool工具,生成密钥store,需要输入和记住密码
keytool -genkey -v -keystore my-release-key.keystore -alias alias_name -keyalg RSA -keysize 2048 -validity 20000
使用zipalign 对齐apk安装包
$ zipalign -v 4 <path-to-same-apk-file> HelloWorld.apk
使用android sdk 下的build-tools中找到,apksigner 进行签名
apksigner sign --ks my-release-key.keystore --ks-key-alias alias_name <path-to-unsigned-apk-file>
打包后安装示例:
capacitor升级到6.0.0后,导致app无法打包,报错:“Execution failed for task ':app:checkReleaseAarMetadata'.”
解决方法:
1、src-capacitor/android/variables.gradle中变量targetSdkVersion和compileSdkVersion值由33改为34
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**
3、build.gradle的buildscrptip路径为src-capacitor/android/build.gradle的dependencies
classpath 'com.android.tools.build:gradle:8.2.1'
原因:@quasar/app-vite 和 @capacitor/cli 都要求node版本大于18.0.0
解决方法:
@quasar/app-vite的修改
@capacitor/cli的修改
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);
}
23年了,icon 方案该升级了 - 掘金 (juejin.cn)
,iconify 的方案充分利用 svg 能力,利用 iconify.json
存储图标矢量信息。再通过下游的不同消费方式,开发者可以制作任意自己喜欢的图标消费方式。利用开放的形态,成功的将生产端和消费端以一种非依赖的关系分开,使用者可以自由组合。在经过一些年的发展,又拥有海量的存量图标和丰富的生态。
在项目中,利用上述方案,我们在不改变设计师习惯的同时,保留了开发者熟悉工具,还创新的引入了更好的图标方案。
iconify 方案中,我们可以避免上述提到的"字体"所带来的一切弊端,同时具备了以下几项优势:
作者:YeeWang
链接:https://juejin.cn/post/7189164727485300793
注意:服务地址最后不能带“/”
提供符合Iconify标准服务接口的 在线SVG图标服务
默认官方https://icon-sets.iconify.design/
后台服务:https://api.iconify.design/
https://icon-sets.iconify.design/
后台服务:https://icones.js.org/collections/
使用情景:不部署icon图标服务或内网环境下
离线操作步骤:
如果项目部署是需要使用离线图标方式,在项目开发结束后,打包前进行如下设置和操作。
https://snippet-generator.app/
代码片段存储位置
C:\Users\用户名\AppData\Roaming\Code\User\snippets
代码片段集合 下载
https://github.com/york11122/quasar-admin-vue3-typescript
https://york11122.github.io/quasar-admin-vue3-typescript/#/login
https://gitee.com/incimo/vue-quasar-manage
https://incimo.gitee.io/vue-quasar-manage
https://github.com/sika-code-cloud/quasar-sika-design
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
https://blog.51cto.com/u_15896157/5895903
https://github.com/zhy6599/cc-admin-web
https://www.cc-admin.top/#/login
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
拦截所有ajax请求并允许修改请求数据和响应数据!实际项目中可以用于请求添加统一签名、协议自动解析、接口调用统计、改为离线化资源等。
前端http拦截js库 xhook > ajax-hook >xspy
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
status
(Number) Required when for fake response
s (status
)statusText
(String) (statusText
)text
(String) (responseText
)headers
(Object) (Contains Name-Value pairs retrieved with getAllResponseHeaders()
)xml
(XML) (responseXML
)data
(Varies) (response
)示例代码:返回假结果 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>
开源地址:https://github.com/wendux/ajax-hook
原理:https://www.jianshu.com/p/7337ac624b8e
Ajax-hook实现的整体思路是实现一个XMLHttpRequest的代理对象,然后覆盖全局的XMLHttpRequest,
https://www.npmjs.com/package/ajax-hook
关键API
拦截全局XMLHttpRequest
注意:proxy 是通过ES5的getter和setter特性实现的,并没有使用ES6 的Proxy对象,所以可以兼容ES5浏览器。
参数:
proxyObject
是一个对象,包含三个可选的钩子onRequest
、onResponse
、onError
,我们可以直接在这三个钩子中对请求进行预处理。window
:可选参数,默认情况会使用当前窗口的window
对象,如果要拦截iframe中的请求,可以将iframe.contentWindow
传入,注意,只能拦截同源的iframe页面(不能跨域)。返回值: ProxyReturnObject
ProxyReturnObject 是一个对象,包含了 unProxy
和 originXhr
unProxy([window])
:取消拦截;取消后 XMLHttpRequest
将不会再被代理,浏览器原生XMLHttpRequest
会恢复到全局变量空间originXhr
: 浏览器原生的 XMLHttpRequest
示例代码:
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();
})
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);
});
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
Install via npm:
npm install -D vite-plugin-webworker-service
Integrate in your project:
//vite.config.ts
import WebWorkerPlugin from 'vite-plugin-webworker-service';
export default defineConfig({
plugins: [WebWorkerPlugin()]
})
vite or vite build
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
}
使用示例
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);
https://www.npmjs.com/package/jszip
A library for creating, reading and editing .zip files with JavaScript, with a lovely and simple API.
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引入错误,如下:
使用“帝测授权宝”APP -android版,实现前端系统的扫码登录。扫码APP是默认连接外网用户权限管理系统,也就是系统SysConfig.js的LoginAuthURL为:https://gis-auth.digsur.com
升级支持扫码登录需要修改内容:
增加@microsoft/signalr依赖库
npm install @microsoft/signalr
SysConfig.js中APIPath,增加 SignalR:'chathub'
修改或更新public/css/common.css
补充下面样式:
.righttop-login
{
position: absolute;
right: 0;
top:0;
width:50px;
height:50px;
}
.loginQRCode
{
margin:0 auto;
width:250px;
height:200px;
display: flex;
justify-content: center;
align-items: center;
font-size: 16px;
}
.login-top-append
{
font-size: 14px;
color:gray;
text-align: center;
margin-top: -10px;
margin-bottom: 10px;
}
下载替换views/shared/login/index.vue文件
补充与替换登录相关图片
修改或更新public/css/common.css
补充下面样式:
.righttop-login
{
position: absolute;
right: 0;
top:0;
width:50px;
height:50px;
}
.loginQRCode
{
margin:0 auto;
width:250px;
height:200px;
display: flex;
justify-content: center;
align-items: center;
font-size: 16px;
}
.login-top-append
{
font-size: 14px;
color:gray;
text-align: center;
margin-top: -10px;
margin-bottom: 10px;
}
下载替换pages/shared/login/index.vue文件
补充与替换登录相关图片
关闭加载动画,在onMounted中调用 Global.Loading('end');
@containerLoaded 是处理和获取对应LayoutManager对象;内部会发送SysEvents.LayoutContainerLoaded,通知该LayoutContainer容器被加载
示例代码:
<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>
监听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>
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>
具名插槽back使用
示例代码:
<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>
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>
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);
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);
结果如下:
H5Tool支持的复制文本方法
Global.WidgetConfigList存储所有的WidgetSetting,在开发模版的src/permission/index.ts的getRightWidgetConfig()方法里进行的初始化
通过WidgetID反向获取LayoutManager
const tmpLayoutManager = Global.getLayoutManager(widgetID.value);
tmpLayoutManager?.changeWidgetVisible(widgetID.value, true);
vue仿win窗口实现悬浮窗拖动,调整大小,最大化,复原。
同类开源工程和示例
https://github.com/mxywds/vue-float-window-pro
对外暴露的属性字段
{
top: {
type: (StringConstructor | NumberConstructor)[];
default: number;
};
left: {
type: (StringConstructor | NumberConstructor)[];
default: number;
};
nWidth: {
type: (StringConstructor | NumberConstructor)[];
default: string;
};
nHeight: {
type: (StringConstructor | NumberConstructor)[];
default: string;
};
icon: {
type: StringConstructor;
default: string;
};
title: {
type: StringConstructor;
default: string;
};
titleHeight: {
type: (StringConstructor | NumberConstructor)[];
default: string;
};
hasMin: {
type: BooleanConstructor;
default: boolean;
};
hasMax: {
type: BooleanConstructor;
default: boolean;
};
hasClose: {
type: BooleanConstructor;
default: boolean;
};
isDark: {
type: BooleanConstructor;
default: boolean;
};
pid: {
type: StringConstructor;
default: string;
};
tag: {
type: (StringConstructor | NumberConstructor | ArrayConstructor | ObjectConstructor)[];
default: string;
};
}
对外暴露的事件
("close" | "open" | "loaded" | "minimize"}
使用示例
import { XWindow, XWindowManager,WindowsMap,MinWindowMap } from 'xframelib';
<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>
//#region **********用于控制功能是否启用
Enables:{
TurfAsync:true,
CesiumOfflineCache:true,//Cesium缓存
}
//#endregion
在App.vue的onBeforeMount()函数里添加控制代码
//参考: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('*');
}
旧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);
// }
// }
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&¤tItem?.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>
通过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);
}
H5Tool新增相关方法如下
统一修改各层样式,默认空白区域鼠标可以穿越,各元素鼠标可点击
默认各层容器样式改为,例如:
.leftContainer {
position: absolute;
top: 0px;
left: 0px;
z-index: var(--layout-left-zindex);
height: 100%;
pointer-events: none;
>* {
pointer-events: all !important;
}
}
增加具有默认动画的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>
widget配置对象(IWidgetConfig)启用layout属性(IWidgetLayout布局样式),增加cssClass外部配置样式类
IWidgetConfig配置接口对象,变化的两个属性:
以前端开发模板里的 src/widgets/portal/PortalHeaderTitleWidget.vue为例
给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"
},
在Layout或对应视图里定义类名为a1、a2的样式
加载后效果——外部样式应用
修改VueWindow组件(VWindow)拖动定位错误;弃用DownloadByUrl方法;移除窗体同步库WSynchro.js;更新依赖库版本;
重点: 修改VueWindow组件(VWindow)拖动定位错误
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
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);
}
动态加载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自身:成功
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);
修改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)
修改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');
为Function扩展promise方法(事件异步方法将按Promise异步执行);修改isElement判断错误;增加ZipTool用于文件在线压缩和解压;H5Tool增加blockEvent停止冒泡bindDropFileHanlder拖拽文件、readFilePromise异步读取文件、优化readFileBytes为bytes二进制数组;IsTool增加isStringLikeJson、isStringLikeKml方法;H5Tool增加了onPasteHandler和offPasteHandler使用document绑定粘贴事件(不是任意div元素都可以绑定paste事件,只有设置了 contenteditable="true" 的元素,才会触发该事件);FileDownload增加SaveToSelectedFile方法,将文件保存到选定路径;
为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')
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)
}
}
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");
}
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) {
}
}
}
FileDownload增加SaveToSelectedFile方法
可能报“window.showSaveFilePicke个不存在”问题
handle = await window.showSaveFilePicker(options);
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);
沙发是
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);
}
解决LayoutContainer与QLayout等组件的兼容使用问题
用于LayoutContainer的mainContainer容器的router-view页面切换;
以支持视图里使用 QPage、QFoot等组件(这些Quasar组件要求必须放在QLayout里)
使用范围
web /mobile app/ electron
安装依赖
无
组件路径和方法
src/components/Quasar/QLayoutMainContainer/index.vue
使用示例
在Layouts里使用,例如:backLayout.vue的
<template #main>
<QLayoutMainContainer/>
</template>
import QLayoutMainContainer from 'src/components/Quasar/QLayoutMainContainer/index.vue';
<template>
<LayoutContainer
:widgetConfig="configRef"
:layoutID="layoutIDRef"
@containerLoaded="loadedHandler"
>
<template #main>
<QLayoutMainContainer/>
</template>
</LayoutContainer>
</template>
<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import { getRightWidgetConfig } from 'src/permission';
import { appStore,tabMenuStore } from 'src/stores';
import { onMounted, ref, watch } from 'vue';
import { Global, H5Tool, LayoutContainer, LayoutManager } from 'xframelib';
import QLayoutMainContainer from 'src/components/Quasar/QLayoutMainContainer/index.vue';
……
</script>
使用范围
web /mobile app/ electron
安装依赖
npm install @tato30/vue-pdf
组件路径和方法
src/components/PDFViewer/index.vue
src/components/PDFViewer/PDFTool.ts
示例
<template>
<div style="width:100%;height:500px;">
<PDFViewer></PDFViewer>
</div>
</template>
<script setup lang="ts">
import PDFViewer from 'src/components/PDFViewer/index.vue';
//任意位置改变PDF路径时引用
import { setPDF } from 'src/components/PDFViewer/PDFTool';
import {onMounted } from 'vue';
onMounted(()=>{
setPDF('/test.pdf');//任意位置进行调用setPDF
})
</script>
<style scoped lang="scss"></style>
示例效果截图
ERROR: Top-level await错误解决:
如下错误
[vite] error while updating dependencies:
Error: Build failed with 1 error:
node_modules/.pnpm/pdfjs-dist@4.2.67/node_modules/pdfjs-dist/build/pdf.mjs:19764:53: ERROR: Top-level await is not available in the configured target environment ("chrome87", "edge88", "es2020", "firefox78", "safari14" + 2 overrides)
at failureErrorWithLog (D:\WorkSpace\FlightNaviWebGIS\Front\node_modules\.pnpm\esbuild@0.21.5\node_modules\esbuild\lib\main.js:1472:15)
at D:\WorkSpace\FlightNaviWebGIS\Front\node_modules\.pnpm\esbuild@0.21.5\node_modules\esbuild\lib\main.js:945:25
at D:\WorkSpace\FlightNaviWebGIS\Front\node_modules\.pnpm\esbuild@0.21.5\node_modules\esbuild\lib\main.js:1353:9
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
解决参考:https://github.com/mozilla/pdf.js/issues/17245
vue-widget-quasar-clivite-template开发框架里的quasar.config.ts已经修改好了
this should be the final answer
{ build: { target: "es2022" }, esbuild: { target: "es2022" }, optimizeDeps:{ esbuildOptions: { target: "es2022", } } }
this fixed it for me, thank you!
使用范围
web /mobile app/ electron
安装依赖
npm install viewerjs
组件路径和方法
src/components/ImageViewer.vue
示例
<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>
用于地图选中要素时弹框显示属性表或其他内容
使用范围
web /mobile app/ electron
安装依赖
npm install floating-vue
使用的是VDropdown
组件路径和方法
src/components/PopoverPanel.vue
示例
<template>
<PopoverPanel :isShown='isOpen' :leftX="leftX" :topY="topY">
<template #content>
<q-table
v-model:pagination="pagination" title="属性表" :rows="rows" :columns="columns" row-key="name"
:dense="true" />
</template>
</PopoverPanel>
</template>
<script setup lang="ts">
import * as Cesium from 'cesium';
import PopoverPanel from 'src/components/PopoverPanel.vue';
import { getLineObject, getLinePrimitives, loadLineLayer } from 'src/workers/line.service.ts';
import { getPointObject, getPointPrimitives, loadPointLayer } from 'src/workers/point.service.ts';
import { nextTick, onMounted, ref } from 'vue';
import { Global } from 'xframelib';
let viewer: Cesium.Viewer;
const isOpen = ref(false);
const leftX = ref('0px');
const topY = ref('0px');
const columns = [
{
name: 'name',
// required: true,
label: '属性名',
align: 'left',
field: 'name',
},
{ name: 'value', align: 'center', label: '值', field: 'value' }
]
const rows = ref([]);
const pagination = ref({
rowsPerPage: 0
});
//获得当前视图
function getViewer(): Cesium.Viewer | undefined {
if (Global.CesiumViewer) {
if (!viewer) viewer = <Cesium.Viewer>Global.CesiumViewer;
}
return viewer;
}
let lastPick: any = undefined;
let line4490: any, point4490: any;
onMounted(() => {
getViewer();
const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
handler.setInputAction(async function (movement) {
const position = movement.position;
var pick = viewer.scene.pick(movement.position);
console.log(pick, movement.position, '选中对象')
if (pick) {
isOpen.value = false;
topY.value = position.y + 'px';
leftX.value = position.x + 'px';
if (lastPick) {
if (lastPick.primitive)
lastPick.primitive.appearance = new Cesium.PerInstanceColorAppearance();
}
pick.primitive.appearance = new Cesium.MaterialAppearance({
material: Cesium.Material.fromType('Color'),
faceForward: true
});
pick.primitive.appearance.material.uniforms.color = new Cesium.Color(1.0, 0.0, 0.0, 1);
lastPick = pick;
setTimeout(() => {
isOpen.value = true;
},20);
if (pick.id.length > 10) {
const current = await getLineObject(line4490, pick.id);
rows.value.length = 0;
if (current) {
const props = current.properties;
rows.value = [
{
name: '管线编号',
value: props['管线编'],
},
{
name: '类型',
value: props['图层'],
},
{
name: '材质',
value: props['材质'],
},
{
name: '长度',
value: props['长度'],
},
{
name: '位置',
value: props['所在道'],
},
{
name: '起点埋',
value: props['起点埋'],
},
{
name: '终点埋',
value: props['终点埋'],
},
{
name: '管径',
value: props['管径'],
}
]
}
}
else {
const currentPT = await getPointObject(point4490, pick.id);
rows.value.length = 0;
if (currentPT) {
const props = currentPT.properties;
rows.value = [
{
name: '编号',
value: props['ID'],
},
{
name: '类型',
value: props['图层'],
},
{
name: '特征',
value: props['特征'],
},
{
name: '地面高程',
value: props['地面高'],
},
{
name: '井底高程',
value: props['井底高'],
},
{
name: '埋设方法',
value: props['埋设方'],
}
]
}
}
}
else {
isOpen.value = false;
if (lastPick) {
if (lastPick.primitive)
lastPick.primitive.appearance = new Cesium.PerInstanceColorAppearance();
lastPick = undefined;
}
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
loadLineLayer('data/line_4490.json').then(async p => {
line4490 = p;
const linePrimitives = await getLinePrimitives(line4490);
if (linePrimitives)
linePrimitives.forEach(item => {
viewer.scene.primitives.add(item);
});
})
loadPointLayer('data/point_4490.json').then(async p => {
point4490 = p;
const pointPrimitives = await getPointPrimitives(point4490);
if (pointPrimitives)
viewer.scene.primitives.add(pointPrimitives);
})
viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(116.269988, 36.3500, 2000.0)
});
});
</script>
<style>
.q-table__bottom {
display: none;
}
</style>
防Windows窗体的容器组件,支持缩小、放大/还原、关闭、拖拽移动、拖拽改变窗体大小,支持任务栏窗体图标控制。
使用范围
web /mobile app/ electron
安装依赖
npm install xframelib@0.7.8
组件路径和方法
import {XWindow,XWindowManager,WindowMap,MinWindowMap} from 'xframelib'
使用XWindow的Widget示例代码
<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;
示例效果图
实现Markdown文档的在线编辑与在线预览
使用范围
web /mobile app/ electron
安装依赖
npm install md-editor-v3
组件路径和方法
MD编辑组件:src/components/Markdown/MarkdownEditor.vue
MD预览组件:src/components/Markdown/MarkdownViewer.vue
MarkdownViewer使用
传入参数说明:
file :md文件网络地址,加载md文档内容;
content:传入md文档的文本内容;
showList: 默认false,控制是否显示目录列表
const props=defineProps(
{
file:{
type:String,
default:""
},
content:
{
type:String,
default:"",
},
//是否显示主题列表
showList:
{
type:Boolean,
default:false,
}
}
);
MarkdownViewer的大小默认为100%填充,可通过margin-bottom来控制下边距。
MarkdownViewer使用示例
根据footer底部栏是否显示,控制MarkdownViewer的下面边距。
src/pages/adefault/markdown/MarkdownViewerTest.vue
<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来控制下边距。
MarkdownEditor使用示例
根据footer底部栏是否显示,通过bottom 来控制MarkdownEditor的下面边距。
src/pages/adefault/markdown/MarkdownEditorTest.vue
<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>
实现左边或右边浮动路由菜单,放在q-drawer组件里使用。
使用范围
web /mobile app/ electron
组件路径和方法
SideMenuBar组件:src/components/Menu/SideMenuBar/index.vue
SideMenuBar使用效果
SideMenuBar使用
<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;
});
ContextMenu组件为通用的右键菜单。
使用范围
web /mobile app/ electron
组件路径和方法
ContextMenu组件:src/components/Menu/ContextMenu.vue
使用说明
组件对外属性:
target 为要绑定的HTML元素,绑定方式:CSS ID、ref、true(true时是默认绑定为父对象)
menuList 为菜单对象列表,为IContextMenuItem数组(只支持2级菜单)
对外事件:
itemClicked 传递为菜单对象(IContextMenuItem类型)
IContextMenuItem接口类型,src/models/IContextMenuItem.ts,添加分割线则传入空对象{}
/**
* 定义右键菜单
*/
export interface IContextMenuItem
{
id?:string;//标识ID
label?:string;//菜单或分组名
icon?:string;//菜单图标
tag?: any; //传递的数据(备用)
children?: Array<IContextMenuItem>;
}
测试示例代码
绑定父元素:const parentRef = ref(true);
<div class="test3">父面板的右键菜单
<ContextMenu :target="parentRef" :menuList="menuList2" @itemClicked="doItemClicked"></ContextMenu>
<q-btn color="primary" label="开关右键菜单" @click="toggleMenu"></q-btn>
</div>
绑定HTML元素ID
<div id="map"> 模拟地图面板,点击右键菜单 </div>
<ContextMenu :target="'map'" :menuList="menuList" @itemClicked="doItemClicked"></ContextMenu>
绑定ref对应的HTML元素:const testRef = ref();
<div ref="testRef" class="test2"> 普通面板的右键菜单</div>
<ContextMenu :target="testRef" :menuList="menuList2" @itemClicked="doItemClicked"></ContextMenu>
</div>
完整示例代码:src/pages/adefault/ContextMenuTest.vue
<template>
<div class="row justify-center">
<div id="map"> 模拟地图面板,点击右键菜单 </div>
<div ref="testRef" class="test2"> 普通面板的右键菜单</div>
<div class="test3">父面板的右键菜单
<ContextMenu :target="parentRef" :menuList="menuList2" @itemClicked="doItemClicked"></ContextMenu>
<q-btn color="primary" label="开关右键菜单" @click="toggleMenu"></q-btn>
</div>
<ContextMenu :target="'map'" :menuList="menuList" @itemClicked="doItemClicked"></ContextMenu>
<ContextMenu :target="testRef" :menuList="menuList2" @itemClicked="doItemClicked"></ContextMenu>
</div>
</template>
<script setup lang="ts">
import { IContextMenuItem } from '@/models';
import ContextMenu from 'src/components/Menu/ContextMenu.vue';
import { ref } from 'vue';
import { Global } from 'xframelib';
const testRef = ref();
//地图默认右键菜单
const menuList:Array<IContextMenuItem> = [{
id: 'show-position',
label: '查看此处坐标',
icon: 'ic:baseline-info'
},
{},
{
id: 'zoom-in',
label: '放大',
icon: 'ic:baseline-zoom-in'
},
{
id: 'zoom-out',
label: '缩小',
icon: 'ic:baseline-zoom-out'
},
{
id: 'locate-center',
label: '移动到此处',
icon: 'ion:md-locate'
}
]
const menuList2:Array<IContextMenuItem> = [{
id: 'file',
label: '文件',
icon: 'icons8:audio-file',
children: [
{
id: 'create',
label: '新建文件',
icon: 'material-symbols:create-new-folder-outline'
},
{},
{
id: 'open',
label: '打开文件',
icon: 'system-uicons:create'
},
{
id: 'save',
label: '保存文件',
icon: 'ic:baseline-save'
}
]
},
{},
{
id: 'test1',
label: '测试1',
icon: 'ic:baseline-data-saver-on"'
},
{
id: 'test2',
label: '其他',
icon: 'ic:baseline-10k'
},
{},
{
id: 'test3',
label: '测试3',
icon: 'ion:md-locate'
}
]
const parentRef = ref(true);
function toggleMenu() {
parentRef.value = !parentRef.value;
}
function doItemClicked(item)
{
Global.Message.info(item.label)
}
</script>
<style scoped>
#map {
width: 400px;
height: 400px;
background-color: #0f0;
}
.test2 {
width: 400px;
height: 400px;
background-color: #f00;
}
.test3 {
width: 400px;
height: 400px;
background-color: #eee;
}
</style>
使用效果
解决了:同一组件多次调用(动态import),互相样式和状态互不影响。
WidgetMenBar是与src/settings/widgetMenuSetting配合使用的,一组横向或纵向的Widget构建的菜单栏,在大屏中使用较多。
之前问题:
MenuBarWidget菜单栏widget,可以广泛复用(跨LayoutContainer)
只需要根据id,在menuBarStyle.scss里定义对应不同菜单栏的布局和样式
运行效果:(样式互不影响)
来源:https://mp.weixin.qq.com/s/uX8AesG2pLdEOXWCG_uAUg
在现代网页布局中,Flexbox(弹性盒子布局)是一种强大的工具。它通过 "flex" 属性,帮助开发者轻松控制元素的伸缩性。
Flex 属性的组成
Flex 属性是一个复合属性,包含以下三个子属性:
语法格式为:
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-basis
限制,不会根据剩余空间自动调整。通过掌握 flex 属性及其子属性,你可以创建出更加灵活和响应式的网页布局,提升用户体验。
display: flex;
flex-direction:column;(row为水平方向,column为垂直方向);
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
来源:Vue 3组件通信13种方法,https://mp.weixin.qq.com/s/ZlImQB2FIsJ0R8s4SVl84g
这是最基本也是最常用的通信方式。父组件通过属性向子组件传递数据。
「父组件:」
<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>
子组件可以通过触发事件的方式向父组件传递数据。
「子组件:」
<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>
$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>
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>
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>
localStorage 和 sessionStorage 可以用于在不同页面或组件之间共享数据。
// 存储数据
localStorage.setItem('user', JSON.stringify({ name: '小明', age: 18 }))
// 读取数据
const user = JSON.parse(localStorage.getItem('user'))
虽然不推荐,但在某些场景下,可以使用 window 对象在全局范围内共享数据。
// 设置全局数据
window.globalData = { message: '全局消息' }
// 在任何地方使用
console.log(window.globalData.message)
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')
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
WOFF & WOFF2
Web开放字体格式(Web Open Font Format,简称WOFF)是一种网页所采用的字体格式标准。此字体格式发展于2009年,[3]由万维网联盟的Web字体工作小组标准化,现在已经是推荐标准。[4]此字体格式不但能够有效利用压缩来减少文件大小,并且不包含加密也不受DRM(数字著作权管理)限制。(来源:维基百科)
这是专门给网页使用的字体格式,体积非常小,实测压缩思源宋体字体文件,可以把体积压缩到 OTF 字体 70% 的大小。
WOFF 和 WOFF2 的区别在于:
WOFF本质上是包含了基于SFNT的字体(如TrueType、OpenType或其他开放字体格式),且这些字体均经过WOFF的编码工具压缩,以便嵌入网页中。[3]WOFF 1.0使用zlib压缩,[3]文件大小一般比TTF小40%。[11]而WOFF 2.0使用Brotli压缩,文件大小比上一版小30%。(来源:维基百科)
因此,一般推荐直接使用 WOFF2。(https://zhuanlan.zhihu.com/p/577387539)
在线转换工具网站:
https://convertio.co/zh/font-converter/
在 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);
}
MapShaper直接引入使用会报错误,例如:
因为:iconv-lite、mproj库都是通过require方式引入的
解决方法:
新增插件**@rollup/plugin-commonjs**
import commonjs from '@rollup/plugin-commonjs';
修改配置项format为 "cjs"
批量修改为import 引入模块