image frame

Doreen's Blog

一切都|
始终弄不明白,合适和喜欢哪个更重要。

JEPaaS

相关文档

· JEPaaS 帮助文档
· JEPaaS API文档
· Ext 中文文档

前端目录结构

je-paas-frontend (PC端平台源码目录结构)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
├── config                    // 调试打包运行脚本
│ ├── config.js // 配置文件入口
│ ├── gulp // 打包程序配置文件
│ │ ├── gulp-build.js // 打包文件--构建文件
│ │ ├── gulp-config.json // 打包程序配置文件,每次发布新包,请同步更新version属性
│ │ ├── gulp-copy.js // 打包文件--复制文件
│ │ └── gulp-rev.js // 打包文件--加MD5版本
│ ├── pro
│ │ └── jepaas.json // 环境配置
│ └── resourse
│ └── holiday // 登录页面背景图,主题色配置
├── dev.js // 代理服务程序--开发环境
├── doc
│ └── Framework.MD // 帮组文档
├── gulpfile.js // 打包程序
├── package.json // node配置文件,通过 npm install 安装项目所需要的依赖
├── product.cdn.js // cdn代理服务脚本
├── product.js // 代理服务
├── src // 业务代码
│ ├── config
│ │ ├── i18n // 国际化配置,中英文
│ │ ├── incloud // 初始化项目时候引入的资源(css && js)
│ │ ├── static // 平台用到的图片进行封装,JE.static
│ │ └── urls // 平台所有接口路径封装 体验:JE.getUrlMaps('method.doUpdateList')
│ ├── extjs
│ │ ├── app // 平台页面的主入口
│ │ ├── app.js // 系统主应用程序 一切应用的入口程序
│ │ ├── app4single // 单功能挂接(http://doc.jepaas.com/docs/je-doc-repository-know/je-doc-repository-know-1cgeaq631gblt)
│ │ ├── core // 核心包(核心组件 && 核心面板 等)
│ │ │ ├── action // 操作方法
│ │ │ ├── controller // 控制器
│ │ │ ├── store // 数据存储
│ │ │ ├── ux // 核心组件
│ │ │ ├── view // 核心面板
│ │ ├── sys // 业务代码
│ │ │ ├── busflow // 文档管理(已经废弃)
│ │ │ ├── calendar // 日历
│ │ │ ├── chart // 图表
│ │ │ ├── companyinfo // 公司信息编辑
│ │ │ ├── configuration // 系统设置面板(已经废弃)
│ │ │ ├── dataimpl // 未知
│ │ │ ├── dd // 数据字典
│ │ │ ├── documentation // (已经废弃)
│ │ │ ├── email // 邮件(已经废弃)
│ │ │ ├── exam // 考试(已经废弃)
│ │ │ ├── file // 文档管理(已经废弃)
│ │ │ ├── flowchart // 流程图
│ │ │ ├── formdesigner // 表单设计器
│ │ │ ├── funccfg // 功能配置
│ │ │ ├── icon // 图标
│ │ │ ├── im // 即时通讯(已经废弃)
│ │ │ ├── menu // 菜单管理
│ │ │ ├── phone // JEAPP配置
│ │ │ ├── plantform // (已经废弃)
│ │ │ ├── portal // 门户引擎
│ │ │ ├── processinfo // (已经废弃)
│ │ │ ├── qrcode // 二维码
│ │ │ ├── rbac // 权限
│ │ │ ├── roster // 花名册
│ │ │ ├── sass // sass
│ │ │ ├── shop // 商城 已废弃
│ │ │ ├── singleton // js&&css代码编辑器
│ │ │ ├── survey // 问卷调查(已经废弃)
│ │ │ ├── svg // 工业图
│ │ │ ├── table // 资源表
│ │ │ ├── transaction // 事务交办(已经废弃)
│ │ │ ├── upgrade // 系统设置表单(已经废弃)
│ │ │ ├── workflow // 工作流编辑器
│ │ │ ├── workflowfunc // 工作流
│ │ ├── tool // 加载系统类和和核心化组件
│ │ ├── util // 封装的类
│ │ └── vue // vue工具类
│ ├── main // html模板页
│ └── static // 静态资源
├── static // 静态资源目录
├── CHANGELOG.md // 帮助文档
├── README.MD // 帮助文档
├── README2.md // 帮助文档
├── commitlint.config.js // 代码提交规则

je-paas-frontend-plugin (PC插件源码目录结构)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
├── config                    // 打包程序配置目录
│ ├── config
│ │ ├── components.js // 配置打包平台的业务组件
│ │ └── jepaas.js // 配置文件
│ ├── config.js // 基础配置文件
│ ├── package // 打包配置文件
│ │ ├── gulp // gulp打包
│ │ ├── tpl // webpack打包模板页
│ │ └── webpack // webpack打包vue
│ ├── resourse // 项目资源文件
│ │ ├── holiday // 修改登录页面的配置信息
│ │ └── project
│ └── server
│ └── dev.js // express服务文件
├── dist // 打包之后的资源
├── docs // home和im插件说明文档
│ ├── assets
│ │ └── images
│ └── plugin
│ ├── home
│ └── im
├── gulpfile.js // 项目打包配置
├── index.html // html
├── jsconfig.json // 指定根目录和JavaScript服务提供的功能选项
├── package.json // 项目脚本和所需依赖
└── src // 业务功能
├── tpl // 模板
│ ├── formPreview // 表单规划器模板
│ ├── login // 登录模板
│ └── priceSheet // 报价单模板
└── vue // vue插件
├── components // 组件
├── install.js // 安装插件所需的方法
├── modules // 业务插件
├── plugins
└── static
├── commitlint.config.js // 代码提交规则
├── CHANGELOG.md // 帮助文档
├── README.MD // 帮助文档
├── README1.md // 帮助文档

二次开发

全局插件

创建(插件项目)

  1. 创建index.vue文件 (src/vue/components/name/index.vue) ; 写入组件页面内容

name好像不能使用驼峰命名

  1. 创建index.js文件 (src/vue/components/name/index.js) ; 安装组件

  2. 插件写入components.js配置文件中进行编译 (config/config/components.js)

1
2
3
4
5
6
module.exports = {
...
entry: [...,'name'], // 打包的入口
...
};

  1. 代码运行的时候 就会自动编译该插件 并在 (dist/static/vue/components/name)中输出编译完成的文件

使用(源码项目)

  1. 在JE的vue工具类中添加自定义扩展方法 (src/extjs/vue/util.js)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
Ext.define('JE.vue.util', {

// ...
/**
* vue弹框,统一调用
* element: 元素属性,{id: 组件id,name: 组件名称}
* 建议命名规则:
组件name={子系统名}-{头部/左侧菜单名}-{功能名}-{操作名};
组件id=id-{组件name}
* options: 业务属性
*/
showVueDialog: function(element, options) {
JE.loadScript(
['/static/vue/components/' + element.name + '/index.js', '/static/vue/components/' + element.name + '/index.css'],
function() {
//插件模板
var tpl =
'<' +
element.name +
' @confirm="handleConfrim" @callback="callback" :params="params"></' +
element.name +
'>';

// 如果存在 弹窗 先清除掉
if (document.getElementById(element.id)) {
JE.removeElement(document.getElementById(element.id));
}
//装载容器
var div = document.createElement('div');
div.setAttribute('id', element.id);
div.innerHTML = tpl;
var el = Ext.getBody().appendChild(div);
var params = options;
//显示
params.visible = true;
//载入页面
new Vue({
el: el.dom,
data: function() {
return {
params: params
};
},
methods: {
callback: function(num) {
return options.callback && options.callback(num);
},
handleConfrim() {
// 发送请求,刷新
console.log('确认');
}
}
});
},
true
);
}
// ...
})

  1. 在系统中进行配置,使用JE.vue.xxx()
1
2
3
4
5
6
7
8
9
10

// 在按钮点击事件中进行配置

function(btn,model,){
JE.vue.showVueDialog(
{ name:'transfer',id:'jevue-transfer' },
{ info:model.data }
)
}

Vue插件开发

一、Vue插件编写

  1. 创建index.vue文件 (src/vue/modules/name/index.vue) ; 写入插件页面内容

  2. 创建index.js文件 (src/vue/modules/name/index.js) ; 安装组件

1
2
3
4
5
6
7
8

import { install } from '../../install.js';
import index from './index.vue';
// 加载echarts依赖
JE.loadChartScript(true);
// 安装组件
install('Name', index);

  1. 插件写入jepaas.js配置文件中进行编译 (config/config/jepaas.js)
1
2
3
4
5
6
7
module.exports = {
...
entry:[
...
'name'
]
}
  1. 代码运行的时候 就会自动编译该插件 并在 (dist/pro/name/View.js)中输出编译完成的文件
1
2
3
4
5
6
7
8
// PRO.name.View 为插件的名字 使用的时候直接通过这个名字使用
Ext.define("PRO.name.View", {
extend: 'Ext.panel.Panel',
alias:'widget.pro.name',
border: 0,
layout: 'fit'
...
})

二、平台渲染Vue插件

  1. 通过插件的形式挂载(只有插件内容)
  • 新建菜单功能的时候,类型选择插件
  • 信息 为 JE_PLUGIN_NAME,PRO.name.View
  • 点击授权给开发人员

  1. 通过表单方法渲染的时候挂载(有别的内容)
  • 不管之前的菜单的类型为什么
  • 在表单渲染后afterrender 在面板中插入该插件的内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function(self){
// ------直接覆盖以前的内容,放在顶部
var panel = Ext.create('PRO.name.View',{
height:300,
dock:'top',
vueInfo:{
params:{
formValue:self.getValues()
},
callback:function(formValue){
self.getFields('SCHOOL_XX').setValue(formValue.aaa);
}
}
});
self.addDocked(panel);


// ------在以前的内容后添加
var grid = self;
//创建组件
var demoGrid = Ext.create('PRO.name.view',{
region:'south',
height:300,
mainGrid:grid
});
//加载到列表的父容器下方,父容器布局是border
grid.demoGrid = grid.ownerCt.add(demoGrid);
}


Vue与平台通信

  • 数据传递
  1. 平台通过 vueInfo.params 传递数据给vue
1
2
3
4
5
6
vueInfo:{
params:{
value1:'',
value2:''
}
}
  1. Vue通过props接收传递过来的数据
1
2
3
4
5
6
7
8
9
10
export defualt{
// ...
props:{
params:{
type:Object
}
// ...
}
// ...
}
  • Vue实时展示

给获取的字段添加change事件

1
2
3
4
5
6
7
function(filed,value,eOpts){
var domeView = field.up('jeeditview').down('[xtype=pro.name]');
if(domeView && domeView.vm){
domeView.vm.params.formValue.SCHOOL_XXLS = value;
}
}

  • 方法传递
  1. 平台通过 vueInfo.callback 改变数据库的值
1
2
3
4
5
vueInfo:{
callback:function(formValue){
self.getFields('SCHOOL_XX').setValue(formValue.aaa);
}
}
  1. Vue通过props接入传入的方法
1
2
3
4
5
props: {
callback: {
type: Function,
},
},
  1. 挂载事件执行方法
1
2
3
4
5
6
7
8
9
10
<button @click="showContent">点击</button>

// ...
methods:{
showContent(){
this.callback({aaa:this.formValue.SCHOOL_XXLS})
}
}


Ext.js二次开发

下了gitee里面的开源项目 按文档流程配置了一边,是可以运行的 效果如下:

  1. 创建自定义功能视图

src/pc/ext/demo/view/GridView.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// PRO.demo.view.GridView 这个面板的名称,唯一性指向(按项目路径命名 应该)
// 并且需要通过在工具类中挂载 才能使用
// 用ext创建一个Panel一个容器 存放数据列表
// me.store.loadData 和 loadData 应该不是一个方法,me.store.loadData估计是Ext的内置方法,用来展示动态数据
// 模板中的html变量需要用 {html:'xxx'} 这种结构来展示

/**
* 自定义功能的视图
*/
Ext.define("PRO.demo.view.GridView", {
extend: 'Ext.panel.Panel',
alias: 'widget.demo.gridview',
layout: 'fit',
initComponent: function () {
var me = this;
me.store = Ext.create('Ext.data.Store', { fields: ['html'], data: [] })
//数据视图
me.items = [
{
xtype: 'dataview',
itemId: 'view',
store: me.store,
autoScroll: true,
itemSelector: 'div.demo-list-item-wrap',
emptyText: '<div style="padding:20px;text-align:center;color:#444;font-size:20px;">没有数据</div>',
tpl: [
'<tpl for=".">',
'<div class="demo-list-item-wrap" style="padding:10px;">',
'{html}',
'</div>',
'</tpl>'
]
}];
//功能按钮
/* me.tbar = {
cls: 'je-button-bar',
items: [{
text: '获取选中的数据',
handler: function (btn) {
//主功能列表
var grid = me.mainGrid;
//获取选中的数据
var sels = grid.getSelectionModel().getSelection();
if (JE.isEmpty(sels)) {
JE.alert('请选择数据!');
} else {
JE.alert('选中了' + sels.length + '条数据!');
console.log('选中的主表数据', sels)
}
}
}]
}; */
me.callParent(arguments);
},
/**
* 加载数据
* @param {jegridview} grid 主功能列表
*/
loadData: function (grid) {
var me = this, data = [];

//循环列表数据
grid.store.each(function (rec) {
var html = [];
//读取列表的数据,进行展示
Ext.each(Object.keys(rec.data), function (key, i) {
// html.push(key + ':' + rec.get(key));
html.push(rec.get(key))
// if (i > 3) return;
})
data.push({ html: html.join(',') + '<hr><br/>' });
});
me.store.loadData(data);
console.log('展示所有列表数据')
},

/**
* 单击主列表 获取数据
* @param {*} view
* @param {*} record
*/
handleClick(view, record) {
var itemData = Object.values(record.raw).join(',')
var me = this
me.store.loadData([{ html: itemData }]);
console.log('展示点击的那一条数据', itemData)
}
});

  1. 自定义插件挂接到MainController中

src/pc/ext/demo/contoller/MainController.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// PRO.demo.controller.MainController 工具类的名称 很重要需要在表格功能内配置

/**
* 自定义功能主控制器
*/
Ext.define("PRO.demo.controller.MainController", {
extend: 'Ext.app.Controller',
init: function () {

var self = this;
var ctr = {}
self.control({ctr});
},
views: [
//加载引用的面板
'PRO.demo.view.GridView', //列表
]
});
  1. 在页面进行配置,把MainController挂载到页面上

  1. 配置相应的事件
  • 渲染后创建组件,并添加在父组件的下方
1
2
3
4
5
6
7
8
9
10
11
// ext相关语法 我也不清楚..
// PRO.demo.view.GridView 自定义面板创建完成,并配置相应内容
// 添加在页面上,并且self对象添加了一个demoGrid属性,里面是自定义面板的相关内容

function(self){
var grid = self;
//创建组件
var demoGrid = Ext.create('PRO.demo.view.GridView',{region:'south',height:400,mainGrid:grid});
//加载到列表的父容器下方,父容器布局是border
grid.demoGrid = grid.ownerCt.add(demoGrid);
}

  • 添加load监听事件、获取主数据
1
2
3
4
5
6
7
8
// 获取到自定义面板的对象grid.demoGrid后
// 执行他的自定义方法loadData获取全部的主数据

function(store,records){
var grid = store.gridObj
//执行demo插件的方法
grid.demoGrid.loadData(grid);
}

  • 然后照葫芦画瓢、添加单击事件、获取点击的数据
1
2
3
4
5
6
7
8
9
10
11
12
// 前两段是系统默认的,应该是获取到列表对象
// 通过grid.demoGrid获取到自定义面板的内容
// 并触发点击事件的方法
// me.store.loadData([{ html: xxx }]); 重新传递html变量的数据

function(view,record){
var grid = view.panel;
grid = view.lockingPartner?grid.ownerCt:grid;

grid.demoGrid.handleClick(view,record);
}

问题集

Object.keys() & for…in 顺序

Object.keys在内部会根据属性名key的类型进行不同的排序逻辑。分三种情况:

  • 如果属性名的类型是Number,那么Object.keys返回值是按照key从小到大排序 ‘1’也算是number
  • 如果属性名的类型是String,那么Object.keys返回值是按照属性被创建的时间升序排序。
  • 如果属性名的类型是Symbol,那么逻辑同String相同

解构赋值

可以取代 delete xxx

1
2
3
4
5
6
7

const { name,...rest } = { name:'z', age:10, sex:'female' }
// name = 'z' , rest = { age:10, sex:'female' }

const [ item,...rest ] = [ { first:'a' }, 23, 55 ]
// item = { first:'a' } , rest = [ 23, 55 ]

json简写

keyvalue名字一样的时候可以省略value

1
2
3
4
5
6
7

let name = 'zz'
let age = 10

let json = { name, age, say(){ console.log('') } }
// => let json = { name:name, age:age, say:function(){ console.log('') } }

JavaScript浮点数运算精度问题

描述

一开始看到百度的部分解决方案是通过扩大N倍换为整数,但是亲测有问题。直接乘10的N次方部分数据还是会出现精度问题。

解决

  1. 我是通过先转换成字符串、然后去掉小数点转为整数
  2. 两个数之间小数点位数进行比较、少的在乘10的n-m次方,运算后在除以10的Math.max(n,m)

目前没发现bug、不确定是正确的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* 浮点数运算
* type: 1 加法,2 减法
*/
function floatAddOrMinus(type: number, arg1: string, arg2: string) {
let len1 = 0
let len2 = 0
let m = 0
let item1 = 0
let item2 = 0;
try {
len1 = arg1.split(".")[1].length;
} catch (e) {
len1 = 0;
}
try {
len2 = arg2.split(".")[1].length;
} catch (e) {
len2 = 0;
}

if (len1 > len2) {
m = len1;
item1 = 0;
item2 = len1 - len2;
} else {
m = len2;
item1 = len2 - len1;
item2 = 0;
}

if (type === 1) {
return (
(Number(arg1.replace(".", "")) * 10 ** item1 +
Number(arg2.replace(".", "")) * 10 ** item2) /
10 ** m
);
} else {
return (
(Number(arg1.replace(".", "")) * 10 ** item1 -
Number(arg2.replace(".", "")) * 10 ** item2) /
10 ** m
);
}
}

vue3中ref和其他属性绑定值重名导致绑定失效

群友遇到的问题、我一开始看着也没觉得哪里不对。感觉以后能用的上,先存着 。

描述

使用element-plus 中的 el-form 的时候,model绑定了formData,但是在页面上对表单进行操作的时候,数据绑定无效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<el-form 
ref="formData"
:model="formData"
:rules="rule">
<el-row justify="center">
<el-col :span="12">
<el-form-item label="入库类型" prop="inOutType">
<el-select v-model="formData.inOutType" clearable>
<el-option label="外购" value="外购"></el-option>
<el-option label="其他" value="其他"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ...
setup(){
let formData: IEmptyObject = reactive({
warningUseTime: 0,
typeName: '',
categoryCode: '',
categoryName: '',
maintenanceCode: '',
maintenanceName: '',
model: '',
attribute: '',
unit: '',
warningProductionQuantity: 0
});

return { formData }
}
// ...

错误原因

ref绑定的内容和model绑定的内容重复了,vue3中ref绑定的内容也是通过在setup中定义变量进行获取。具体原因还没仔细看,估计是ref的优先级更高?

proxy多个路由代理

重写失效、请求路径包含某个 代理名 代理失效。

记下 还没解决

哈希路由引起样式问题

在一个页面引入了样式,并且添加了 scoped ,但是在进行页面跳转的时候,这些引入的样式会影响另一页面。刷新之后就好了

初步猜测是因为路由用的哈希模式的原因

hash 值变化不会导致浏览器向服务器发出请求

所以在第一个页面加载的样式,在跳转之后没有刷新,样式就一直会存在,因此影响到了页面的样式

解决方法

  • emit父子组件通讯,点击事件触发后、强制刷新
  • 监听hash值的变化、变化之后、强制刷新
1
2
3
4
5
6
7
8
9
10
11
12
setup: () => {
const route = useRoute();
watch(
() => route.path,
(to, from) => {
if (from.includes("papercss") && from !== to) {
location.reload();
}
}
);
}

el-date-picker限制时间选择范围

两个时间选择器、开始时间与结束时间的时间选择范围为24h,并且精确到 (format="yyyy-MM-dd HH:mm")

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- 基本使用 -->
<el-form-item prop="beginDateStr" label="开始时间">
<el-date-picker
v-model="formData.beginDateStr"
type="datetime"
placeholder="选择日期时间"
format="yyyy-MM-dd HH:mm"
>
</el-date-picker>
</el-form-item>
<el-form-item prop="endDateStr" label="结束时间">
<el-date-picker
v-model="formData.endDateStr"
type="datetime"
format="yyyy-MM-dd HH:mm"
placeholder="选择日期时间"
>
</el-date-picker>
</el-form-item>

Picker Options 配置

开始时间限制

首先对开始时间进行限制 限制在当前日期之前;并且当开始日期改变的时候,结束日期清空

disabledDate 日期禁用范围

1
2
3
4
5
6
7
8
<el-date-picker
v-model="formData.beginDateStr"
type="datetime"
placeholder="选择日期时间"
format="yyyy-MM-dd HH:mm"
:picker-options="pickerOptionsStart"
@change="handleChangeStart"
></el-date-picker>
1
2
3
4
5
6
7
8
9
private pickerOptionsStart = {
disabledDate: (time: Date) => {
return time.getTime() > Date.now();
},
}

private handleChangeStart() {
this.formData.endDateStr = '';
}

结束时间限制

因为是24h限制、所以 开始时间+24h 和 开始时间-24h 的时间都得禁用;精确到分钟,小时和分钟也得进行限制

selectableRange 时间选择范围
这个属性没有在el-date-picker里面,在el-time-picker里面,个人觉得文档写的不是很人性…
因为跨天的话、时间范围是不一样的 比如开始时间是2021-05-31 02:00
那么结束时间的范围是2021-05-31 02:00 - 2021-06-01 02:00
如果选的是31号 则 selectableRange = '02:00-23:59'
如果选的是1号 则 selectableRange = '00:00-02:00'
所以还得对日期选择进行监听,本来是想通过点击/change事件进行监听的
但是无效,然后通过监听input框内数据的变化 来进行监听

1
2
3
4
5
6
7
8
9
10
11
<el-date-picker
v-model="formData.endDateStr"
type="datetime"
format="yyyy-MM-dd HH:mm"
placeholder="选择日期时间"
@input="handleChangeInputDate($event)"
:picker-options="{
disabledDate: (time) => hanldeDisabledDate(time),
selectableRange: dafaulttime,
}"
></el-date-picker>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private dafaulttime: string = '00:00:00-23:59:59';

private handleChangeInputDate(e: Date) {
// 区别 同一天和下一天的时间限制
if (this.formData.beginDateStr) {
const begin = new EasyDate(this.formData.beginDateStr).format('dd');
const end = new EasyDate(e).format('dd');
if (begin === end) {
this.dafaulttime =
new EasyDate(this.formData.beginDateStr).format('HH:mm:ss') +
'-23:59:59';
} else {
this.dafaulttime =
'00:00:00-' +
new EasyDate(this.formData.beginDateStr).format('HH:mm:ss');
}
}
}

private hanldeDisabledDate(time: Date) {
// 24 * 3600 * 1000 == 8.64e7
if (this.formData.beginDateStr) {
const pickerMinDate = new Date(this.formData.beginDateStr).getTime();
const maxTime = pickerMinDate + 8.64e7;
const minTime = pickerMinDate - 8.64e7;
return time.getTime() >= maxTime || time.getTime() <= minTime;
} else {
return time.getTime() > Date.now();
}
}

针对daterangedatetimerange类型进行时间限制

不仅仅需要配置disabledDate,还需要配置onPick获取当前点击的日期
onPick Function({ maxDate, minDate }) 选中日期执行的回调,只选择一个的时候返回的minDate

实例:只能选择31天的范围

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<span>起止时间:</span>
<el-date-picker
style="width: 240px; margin-right: 10px"
v-model="dateRange"
type="datetimerange"
range-separator="至"
value-format="yyyy-MM-dd HH:mm:ss"
start-placeholder="开始日期"
end-placeholder="结束日期"
@change="dateChange"
:picker-options="{
disabledDate: (time) => hanldeDisabledDate(time),
onPick: (time) => hanldeOnPick(time),
}"
:clearable="false"
></el-date-picker>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

private hanldeOnPick(e: IEmptyObject) {
if (!e.maxDate) {
const timeRange = 8.64e7 * 31;
this.pickDateRange.maxTime = e.minDate.getTime() + timeRange;
this.pickDateRange.minTime = e.minDate.getTime() - timeRange;
} else {
this.pickDateRange.minTime = null;
this.pickDateRange.maxTime = null;
}
}

private hanldeDisabledDate(e: Date) {
const pickerDate = e.getTime();
if (this.pickDateRange.minTime && this.pickDateRange.maxTime) {
return (
e.getTime() <= this.pickDateRange.minTime ||
e.getTime() >= Date.now() ||
e.getTime() >= this.pickDateRange.maxTime
);
} else {
return e.getTime() >= Date.now();
}
}

主要问题

结束时间的 picker-options 属性,如果直接通过一个对象传递的话 ,会失效..,不清楚原因。我猜测是双向数据绑定相关的东西的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
// √ 生效
:picker-options="{
disabledDate: (time) => hanldeDisabledDate(time),
selectableRange: dafaulttime,
}"

// × 不生效
:picker-options="pickerOption"

private pickerOption = {
disabledDate: (time) => hanldeDisabledDate(time),
selectableRange: dafaulttime,
}

multipart/form-data 文件流请求和参数相关

一般的接口请求都是以json的形式进行传输、这次的表单会有文件上传的情况 所以用formdata的形式进行传输。

表单数据存在body内 且content-type需要设置为application/json,文件数据存在file内

  1. formData请求,参数必须为FromData类型的
1
2
3
4
5
6
let params = new FormData()

params.append('file',value,name)

// params.get('file') 获取file值
// params.set('file',newValue) 设置file值
  1. 对于不是文本/二进制类型的数据,还需要使用Blob对象进行转换

如上图所示,body内传递的是Object对象,而且,bodycontent-type需要设置为application/json类型

1
2
3
4
5
let obj = {...}
let blob = new Blob([JSON.stringify(obj)],{type:'application/json'})

params.append('body',blob)

  1. axios主要配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// http.ts
const instance = axios.create({
timeout: 600000, // by huangrong
headers: { 'Content-Type': 'application/json' },
baseURL: '/',
// transformResponse: (data: any) => {
// //
// }
});

instance.upload = (url, data, config = {}, params) => {
const formData = new FormData();
data.forEach(item => {
formData.append('file', item.value, item.name || item.filename);
});
if (params && Object.keys(params).length > 0) {
for (const m in params) {
if (params.hasOwnProperty(m)) {
const key = m;
const value = params[m];
formData.append(key, value);
}
}
}
return instance.request({
method: 'post',
...config,
url,
data: formData,
});
};


// 请求拦截器
instance.interceptors.request.use(config => {
const { params = {} } = config;
config.params = { ...params, time: new Date().getTime() };
// add token if need
config.headers = {
...(config.headers || {}),
'X-Auth-Token': CookieStorage.token,
'X-Stargate-AppId': 25,
};
return config;
}, err => {
// show err
}
);

// common-api.ts
export function uploadFile(path: string, file: IUploadParam[], config?: AxiosRequestConfig, params?: IEmptyObject) {
return http.upload(path, file, config, params).then(res => {
return Promise.resolve(res.data);
});
}

// index.vue
uploadFile(
this.path,
(this.$refs.imageUpload as any).fileList,
{},
{
body: new Blob(
[
JSON.stringify({
...this.formData,
factory: this.factory,
workshop: this.workshop,
}),
],
{ type: 'application/json' }
),
}
)

axios实现文件导出功能,无法获取response的异常信息

如图所示: 上方是调文件导出接口,返回的异常信息。下方是普通请求获取的异常信息。上方的data内没有errorMsg

错误原因

可以看到上方红圈内的dataBlob类型的(文件存储类型),所以不能直接获取,需要FileReader对象进行处理,转换获取blob、file文件的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 响应拦截器
instance.interceptors.response.use(response => {
return Promise.resolve(response);
}, error => {
// show error_msg and reject
// console.log('响应失败信息', error.response);
if (error && error.response && error.response.status) {
let errCode = 0;
let errMsg = '';
if (!!error.response.data) {
// 文件流错误处理
if (error.response.config.responseType === 'blob') {
const reader = new FileReader();
reader.readAsText(error.response.data);
reader.onload = () => {
try {
const { errorMsg } = JSON.parse(reader.result as string);
errMsg = errorMsg;
handleError(error.response.status, errMsg);
} catch (error) {
console.log('解析错误');
}
};
} else {
// 普通错误处理
const { status, errorMsg } = error.response.data;
errCode = status;
errMsg = errorMsg;
handleError(error.response.status, errMsg);
}
}
}
return Promise.reject(error);
});

uni-app

文件目录结构

生命周期

应用生命周期 - App.vue

函数名 说明
onLaunch uni-app 初始化完成时触发(全局只触发一次)
onShow uni-app 启动,或从后台进入前台显示
onHide uni-app 从前台进入后台
onError uni-app 报错时触发
onUniNViewMessage nvue 页面发送的数据进行监听,可参考 nvue 向 vue 通讯
onUnhandledRejection 对未处理的 Promise 拒绝事件监听函数(2.8.1+)
onPageNotFound 页面不存在监听函数
onThemeChange 监听系统主题变化

页面生命周期

函数名 说明 平台差异说明 最低版本
onInit 监听页面初始化,其参数同 onLoad 参数,为上个页面传递的数据,参数类型为 Object(用于页面传参),触发时机早于 onLoad 百度小程序 3.1.0+
onLoad 监听页面加载,其参数为上个页面传递的数据,参数类型为 Object(用于页面传参),参考示例
onShow 监听页面显示。页面每次出现在屏幕上都触发,包括从下级页面点返回露出当前页面
onReady 监听页面初次渲染完成。注意如果渲染速度快,会在页面进入动画完成前触发
onHide 监听页面隐藏
onUnload 监听页面卸载
onResize 监听窗口尺寸变化 App、微信小程序
onPullDownRefresh 监听用户下拉动作,一般用于下拉刷新
onReachBottom 页面滚动到底部的事件(不是scroll-view滚到底),常用于下拉下一页数据。具体见下方注意事项
onTabItemTap 点击 tab 时触发,参数为Object,具体见下方注意事项 微信小程序、QQ小程序、支付宝小程序、百度小程序、H5、App
onShareAppMessage 用户点击右上角分享 微信小程序、QQ小程序、支付宝小程序、字节小程序、飞书小程序、快手小程序
onPageScroll 监听页面滚动,参数为Object nvue暂不支持
onNavigationBarButtonTap 监听原生标题栏按钮点击事件,参数为Object App、H5
onBackPress 监听页面返回,返回 event = {from:backbutton、 navigateBack} ,backbutton 表示来源是左上角返回按钮或 android 返回键;navigateBack表示来源是 uni.navigateBack ;详细说明及使用:onBackPress 详解。支付宝小程序只有真机能触发,只能监听非navigateBack引起的返回,不可阻止默认行为。 app、H5、支付宝小程序
onNavigationBarSearchInputChanged 监听原生标题栏搜索输入框输入内容变化事件 App、H5 1.6.0
onNavigationBarSearchInputConfirmed 监听原生标题栏搜索输入框搜索事件,用户点击软键盘上的“搜索”按钮时触发。 App、H5 1.6.0
onNavigationBarSearchInputClicked 监听原生标题栏搜索输入框点击事件(pages.json 中的 searchInput 配置 disabled 为 true 时才会触发) App、H5 1.6.0
onShareTimeline 监听用户点击右上角转发到朋友圈 微信小程序 2.8.1+
onAddToFavorites 监听用户点击右上角收藏 微信小程序 2.8.1+

路由

跳转方式:navigator组件跳转(声明式)、调用API跳转(编程式)

1
<navigator url="/pages/404/index">404</navigator>
1
2
3
4
5
6
7
8
9
10
11
uni.navigateTo({ url: "/pages/index/index" });

/**
* open-type属性
* 值:
* navigate - 打卡新页面
* redirectTo - 页面重定向
* navigateBack - 页面返回
* switchTab - tab切换
* reLaunch - 重加载
**/

tab切换 - 调用APIuni.switchTab、使用组件<navigator open-type="switchTab"/>

路由传参与接收

传参

1
uni.navigateTo(url:'page?name=doreen&age=18')

接收

1
2
3
onLoad:function(option){
console.log(option.name,option.age)
}

页面配置

pages.json内配置页面路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"pages": [ 
//pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "uni-app"
}
},
{
"path": "pages/404/index",
"style": {
"navigationBarTitleText": "页面不存在",
"enablePullDownRefresh": false
}
}
],

pages配置项

属性 类型 子属性 子类型 默认值 描述
path string
style Object
navigationBarBackgroundColor HexColor #000000 导航栏背景颜色
navigationBarTextStyle String white 导航栏标题颜色及状态栏前景颜色,仅支持 black/white
navigationBarTitleText String 导航栏标题文字内容
navigationBarBackgroundColor HexColor #000000 导航栏背景颜色
……

tabBar配置项

属性 类型 必填 默认值 描述
color HexColor tab上文字默认颜色
selectedColor HexColor tab上文字选中时的默认颜色
backgroundColor HexColor tab的背景色
list Array tab的列表,2-5个

list对象属性如下:

属性 类型 必填 说明
pagePath String 页面路径,必须在pages中先定义
text String tab上按钮文字
iconPath String 图片路径
selectedIconPath String 选中时的图片路径
visible Boolean 该项是否展示,默认展示

配置了tabBar之后list内的页面不能使用uni.navigateTo进行跳转,需要改成uni.switchTab
对于在list内的页面,使用<navigator url=''>进行跳转时要加上 open-type="switchTab" 属性

subPackages分包配置

主包: 即放置默认启动页面/TabBar 页面,以及一些所有分包都需用到公共资源/JS 脚本 (公共资源)
分包: 则是根据pages.json的配置进行划分。(按需加载的资源)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"pages": [{
"path": "pages/index/index",
"style": { ... }
}],
// .....
"subPackages": [{
"root": "pagesA", // 分包文件夹名称(与pages文件夹同级)
"pages": [{
"path": "list/list", // 路径
"style": { ... }
}]
}, {
"root": "pagesB",
"pages": [{
"path": "detail/detail",
"style": { ... }
}]
}],

}

常用API

组件通信

父子组件通信

和Vue一样

  1. 父组件通过绑定属性将数据传递给子组件,子组件通过props进行接收
  2. 子组件通过$emit进行广播,父组件通过@xx进行接收
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<!--  child.vue -->
<template>
<view>
<text>子组件</text>
{{msg}}
<button type="default" @click="getChild">子传父</button>
</view>
</template>
<script>
export default {
props:['msg'], // 接收父组件传递的数据,只读
methods: {
getChild(){
this.$emit('getChild','aaaa') // 传递给父组件的数据
}
}
}
</script>

<!-- parent.vue -->
<template>
<view>
<!-- 传递&获取数据 -->
<child :msg='title' @getChild='say'></child>
</view>
</template>

<script>
import Child from '@/componets/child.vue' // 引入子组件
export default {
data() {
return {
title: "Hello",
};
},
components:{
Child // 声明
},
methods: {
say(info){
this.title = info // 获取子组件的数据
}
},
};
</script>


全局通信

uniapp提供了 uni.$emituni.$on 两个api进行全局的通信 (和vue兄弟组件通信的方式差不多,这里不限于兄弟组件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<!-- pages1 -->
<template>
<view>
<text>{{name}}</text>
</view>
</template>
<script>
export default {
data() {
return {
name:'me'
};
},
onLoad() {
// 发布广播
uni.$on('getMeFun',(str)=>{
this.name = str
console.log('me 页面的全局事件被触发')
})
},
};
</script>

<!-- pages2 -->
<!-- 点击事件触发后打印文字、并将pages1的数据修改为 a-me -->
<template>
<view>
<button type="default" @click="hanldeClick">点击</button>
</view>
</template>

<script>
export default {
methods: {
hanldeClick(){
uni.$emit('getMeFun','a-me')
}
}
}
</script>

Vuex

axios封装&proxyTable代理配置

只在开发环境有效、打包之后代理配置就不生效了

vite.config.ts 内进行代理配置

多代理配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
server: {
open: true,
proxy: {
'/api': {
target: 'http://rap2api.taobao.org/app/mock/data',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
},
'/music-api': {
target: 'https://api.uomg.com/api/rand.music?format=json&sort=%E7%83%AD%E6%AD%8C%E6%A6%9C',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/music-api/, '')
},
'/music-lyric': {
// target:'https://api.imjad.cn/cloudmusic/?type=lyric&id=1479526505'
target: 'https://api.imjad.cn/cloudmusic/',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/music-lyric/, '')
},
'/one-api': {
target: 'https://api.tianapi.com/txapi/one/index?key=7764228bedff0b2310879d47173c4603&rand=1',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/one-api/, '')
},
'/diary-api': {
target: 'http://api.tianapi.com/txapi/tiangou/index?key=7764228bedff0b2310879d47173c4603',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/diary-api/, '')
},

},
}

新建一个http.ts文件对axios封装

最基础的配置只需要实例化axios对象,配置baseUrl就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import { CookieStorage } from '@/utils/cookies';
import axios, { AxiosRequestConfig } from 'axios';
import qs from 'qs';

const instance = axios.create({
timeout: 600000,
headers: { 'Content-Type': 'application/json' },
baseURL: '/',
});

// 保存下载文件
function saveFile(binaryData: string, fileName: string, mime?: string) {
const fileDownload = require('js-file-download');
fileDownload(binaryData, fileName, mime);
}

instance.download = {
get(url, config) {
const { filename = 'temp', mime = '' } = config || {};
return instance.get(url, { ...config, responseType: 'blob' }).then(res => {
saveFile(res.data, filename, mime);
return Promise.resolve(res);
});
},
post: (url, data, config) => {
const { filename = 'temp', mime = '' } = config || {};
return instance.post(url, data, { ...config, responseType: 'blob' }).then(res => {
saveFile(res.data, filename, mime);
return Promise.resolve(res);
});
},
};

// ne-upload
instance.upload = (url, data, config = {}, params) => {
const formData = new FormData();
data.forEach(item => {
formData.append('file', item.value, item.name || item.filename);
});
if (params && Object.keys(params).length > 0) {
for (const m in params) {
if (params.hasOwnProperty(m)) {
const key = m;
const value = params[m];
formData.append(key, value);
}
}
}
return instance.request({
method: 'post',
...config,
url,
data: formData,
});
};

instance.formdata = (url, data, config = {}) => {
const formData = qs.stringify(data); // 会将汉字encode
return instance.request({
...config,
url,
data: formData,
method: config.method || 'post',
headers: { ...config.headers, 'Content-Type': 'application/x-www-form-urlencoded' },
});
};

// 请求拦截器
instance.interceptors.request.use(config => {
const { params = {} } = config;
config.params = { ...params, time: new Date().getTime() };
// add token if need
config.headers = {
...(config.headers || {}),
'X-Auth-Token': CookieStorage.token,
'X-Stargate-AppId': 25,
};
return config;
}, err => {
// show err
}
);

// 响应拦截器
instance.interceptors.response.use(response => {
return Promise.resolve(response);
}, error => {
// show error_msg and reject
console.log('响应失败信息', error.response);
if (error && error.response && error.response.status) {
let errCode = 0;
let errMsg = '';
if (!!error.response.data) {
const { status, errorMsg } = error.response.data;
errCode = status;
errMsg = errorMsg;
}
switch (error.response.status) {
case 400:
window._vm.$messageSelf.error(errMsg);
break;
case 401:
// to login or authorize
window._vm.$messageSelf.error(errMsg);
CookieStorage.clear();
// window.location.href = '';
break;
case 403:
// to login or authorize
window._vm.$messageSelf.error(errMsg);
CookieStorage.clear();
window.location.href = '/';
break;
case 404:
// request failed, no res on server
window._vm.$messageSelf.error('系统错误,请稍候再试');
break;
case 500:
// server error
window._vm.$messageSelf.error(errMsg);
break;
}
}
return Promise.reject(error);
}
);

export default instance;

新建一个common-api.ts对公共请求进行封装

引入http.ts文件,并根据业务类型进程处理

1
2
3
4
5
6
7
8
9
10
11
12
13
import http from '@/utils/http';

export function commonRequest(path: string, params: any = {}, method: string = 'get') {
if (method === 'post') {
return http.post(path, params).then(res => {
return Promise.resolve(res.data);
});
} else {
return http.get(path, { params }).then(res => {
return Promise.resolve(res.data);
});
}
}

具体使用

按照commonRequest方法要求进行传参,并使用。根据不同的接口名,匹配不同的接口地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>
<div>
<button @click="handleRequest('/music-api')">/music-api</button>
<button @click="handleRequest('/music-lyric')">/music-lyric</button>
<button @click="handleRequest('/one-api')">/one-api</button>
<button @click="handleRequest('/diary-api')">/diary-api</button>
<h3>{{ showData }}</h3>
</div>
</template>
<script lang='ts'>
import { defineComponent } from "@vue/runtime-core";
import { ref } from "vue";
import { commonRequest } from "../api/http";

export default defineComponent({
setup: () => {
let showData = ref("e");

function handleRequest(api: string) {
commonRequest(api, {}).then((res) => (showData.value = res));
}
return { showData, handleRequest };
},
});
</script>

【旅行日记】稻城亚丁

八月中下旬的时候,趁着中风险地区解封、疫情好转 赶紧把四天的福利假给调了
本来还想组织下语言下午发的,不小心直接按了回车发给项目经理😂 (不是第一次了
周一的早上九点多 顺利请了后面四天的假 哈哈哈
一整天都无心工作 脑子里想的都是 要去稻城亚丁还是去九寨沟
为了充分利用完六天长假 最后报了五天的稻城亚丁!
出发前一天晚上 蓝色暴雨预警 不愿在笑man
早上也是下了小雨,预约了早上五点的网约车,师傅三点多就到等候点了,我到的时候他在车上已经睡着了
路上聊了几句,知道我是一个人去报团旅游一直嘱咐我出门要跟家里人报信、出去玩最好找认识的人一起
最好在团里找个人一起… 当时就感觉好暖啊!!
因为路上暴雨滑坡耽误了一些时间 到了新都桥那边的酒店吃完饭的已经九十点钟了
因为有必须要买的东西,不得不十点了还去找商店,周边基本都是酒店,走了十多分钟才找到一家
很幸运店家刚准备关门,结账的时候老板问我酒店在附近吗
我说没有 走了十几二十分钟才找到这一家😭
他也嘱咐我说 以后晚上出门要结伴、这么晚一个人不安全…


后面几天的行程基本和另外两个团友 (一个比我大四天的小姐姐和一个上海过来的小哥) 一起度过的、没觉得孤单、挺开心的
还有个60多岁的老奶奶也是一个人报团的 十一公里四千多米的山路也走完了全程 还是蛮佩服的
顺便夸自己一嘴 我们是第一批到的牛奶海 没有🏇, 纯徒步(感觉自己身体素质还可以
当然 有好的身体素质还要做出好的选择,很多人看到路标五色海比较近就先去五色海了
但是去五色海那个路真的陡 也很耗时,先去牛奶海 返程再去五色海是最佳选择😁

山顶的风景确实不错 但是对于去过西藏的人来说 还是差点意思
感觉去了西藏之后 看哪的风景都不会觉得太惊艳了 有机会还想去西藏!

HarmonyOS初尝试

抱着凑热闹的心态4月30号赶上了harmony开发者一轮公测报名的末班车,经过漫长的等待终于5月12号晚上收到了短信,十几分钟后系统就给推送了。当时收到推送还是很激动的,马上就更新了。总的来说挺好、没发现什么bug太严重的bug。

官方文档

UI框架主要分为javajs,js应用所用到的东西和原生js、html、css没有太大差别,有的语法和vue有些相似,上手应该是比较容易的。主要是这个开发工具用的不太熟悉。
主要问题是 previewer 的时候可以进行热更新但是不能进行网络请求。我不知道是我操作的问题还是本来就不允许。看了官方文档是说的部分接口不能请求。

主要目录结构

主要是在 entry/main/js 下进行编写

各个文件夹的作用:

  • app.js文件用于全局JavaScript逻辑和应用生命周期管理。
  • pages目录用于存放所有组件页面。
  • common目录用于存放公共资源文件,比如:媒体资源,自定义组件和JS文件。
  • resources目录用于存放资源配置文件,比如:全局样式、多分辨率加载等配置文件。
  • i18n目录用于配置不同语言场景资源内容,比如:应用文本词条,图片路径等资源。
  • share目录用于配置多个实例共享的资源内容,比如:share中的图片和JSON文件可被default1和default2实例共享。

pages

一个文件夹对应一个页面。并且要在config.json文件下对路由进行配置

1
2
3
4
5
6
7
8
9
10
// pages数组内排在首位的 代表展示的首页
"js": [
{
"pages": [
"pages/demo/demo", // 对应demo.hml
"pages/index/index"
],
"name": "default",
}
]

i18n

内部存放的是.json文件,在页面中使用的时候直接通过 $t('...') 来获取

1
2
3
4
5
6
7
// zh-CN.json
{
"strings": {
"app_bar": {
"title": "标题"
},
}
1
2
3
4
5
// index.hml
<text>
{{ $t('strings.app_bar.title') }}
<!-- 标题 -->
</text>

组件相关

例如在 common 中创建 component 文件夹,存放公共组件。以tabbar组件为例

组件使用

通过element标签进行获取、使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- tabbar.hml -->
<toolbar>....</toolbar>


<!-- home.hml -->
<!-- 在页面的顶部通过element引入组件所在的地址 并且通过name属性取名 -->
<element name='tab-bar-com' src='../../common/component/tabbar/tabbar.hml'>
</element>

...

<div>
...
<!-- 把组件放在页面需要的位置 -->
<tab-bar-com></tab-bar-com>
</div>

父子组件传值

与vue类似,子组件通过props接收父组件的数据,父组件通过@获取子组件传递的数据

子组件tabbar

1
2
3
4
5
<!-- tabbar.hml -->
<toolbar style="position : fixed; bottom : 0px;" data='hello'>
<toolbar-item icon='{{ iconurl }}' value='我的' on:click="handleClick"></toolbar-item>
</toolbar>

1
2
3
4
5
6
7
8
9
10
11
12
// tabbar.js
export default {
props: ['iconurl'], // 接收父组件传来的数据

handleClick(e){
// emit来广播给父组件 传递给父组件数据
this.$emit('getItem', {
title: e.target.attr.value
})
}

}

父组件

1
2
3
4
5
6
7
8
<!-- home.hml -->

<div>
<!-- 通过绑定属性的方式把数据传递给子组件 -->
<!-- 通过 @..='fn' 的方式获取子组件传递的数据 -->
<tab-bar-com iconurl='https://....' @get-item='getTabbarItem'></tab-bar-com>
</div>

1
2
3
4
5
6
7
8
9
// home.js
export.default{
// 获取子组件传来的数据
getTabbarItem(item) {
// 还有一层 detail
console.log(item.detail.title) // 我的
},
}

Vue3-音乐播放器

在线展示

API文档

接口说明

地址 https://api.uomg.com/api/rand.music
返回格式 json / mp3
请求方式 get / post

请求参数

名称 类型 必填 描述
sort string 选填 默认为热歌榜 [热歌榜,新歌榜,飙升榜,抖音榜,电音榜]
mid int 选填 网易云歌单ID
format string 选填 选择输出的格式

返回参数

名称 类型 描述
code string 状态码
name string 歌曲名称
url string 歌曲地址
picurl string 歌曲封面地址

返回示例

1
2
3
4
5
6
7
8
9
{
"code": 1,
"data": {
"name": "江南",
"url": "http://music.163.com/song/media/outer/url?id=26305527",
"picurl": "http://p4.music.126.net/CcthX_ZCexIdriZADoNn3g==/109951165628166191.jpg",
"artistsname": "林俊杰"
}
}
  • 歌词接口

接口说明

地址 https://api.imjad.cn/cloudmusic
返回格式 json
请求方式 get / post

请求参数

名称 类型 必填 描述
type string 必填 值为lyric时,返回歌词
id int 必填 网易云歌区ID

返回参数

名称 类型 描述
code string 状态码
lrc object 歌词数据
version int 版本
lyric string 歌词

返回示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"sgc": false,
"sfy": false,
"qfy": false,
"lrc": {
"version": 8,
"lyric": "[00:00.000] 作曲 : 祝何\n[00:00.693] 作词 : 祝何\n[00:02.80]编曲 Arranger:祝何\n[00:06.84]楚河流沙几聚散 日月沧桑尽变换\n[00:35.08]乱世多少红颜换一声长叹\n[00:40.38]谁曾巨鹿踏破了秦关 千里兵戈血染\n[00:46.45]终究也不过是风轻云淡\n[00:51.51]长枪策马平天下 此番诀别却为难\n[00:57.72]一声虞兮虞兮泪眼已潸然\n[01:03.07]与君共饮这杯中冷暖 西风彻夜回忆吹不断\n[01:09.38]醉里挑灯看剑 妾舞阑珊\n[01:14.91]垓下一曲离乱 楚歌声四方\n[01:20.31]含悲 辞君 饮剑 血落凝寒霜\n[01:25.87]难舍一段过往 缘尽又何妨\n[01:31.77]与你魂归之处便是苍茫\n[02:00.25]长枪策马平天下 此番诀别却为难\n[02:06.17]一声虞兮虞兮泪眼已潸然\n[02:11.70]与君共饮这杯中冷暖 西风彻夜回忆吹不断\n[02:18.00]醉里挑灯看剑 妾舞阑珊\n[02:23.25]垓下一曲离乱 楚歌声四方\n[02:28.74]含悲 辞君 饮剑 血落凝寒霜\n[02:34.73]难舍一段过往 缘尽又何妨\n[02:40.11]与你魂归之处便是苍茫\n[02:46.39]汉兵刀剑纷乱 折断了月光\n[02:51.55]江畔 只身 孤舟 余生不思量\n[02:57.42]难舍一段过往 缘尽又何妨\n[03:03.01]与你来生共寄山高水长\n[03:13.11]混音师 Mixing Engineer:唐瑜\n[03:13.78]和声 Backing vocals:田跃君\n[03:14.24]制作人 Produced by :蒋雪儿 Snow.J\n[03:14.67]监制 Executive producer: 蒋雪儿 Snow.J\n[03:15.18]OP/SP :乐无限 ETERNAL MUSIC\n[03:15.90]【未经授权不得翻唱或使用】\n"
},
"klyric": {
"version": 0,
"lyric": null
},
"tlyric": {
"version": 0,
"lyric": null
},
"code": 200
}

基本思路

音乐播放 —— audio

html内添加audio标签进行相关配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<audio
ref="audio"
autoplay
@pause="handlePause"
@play="handlePlay"
@timeupdate="handleMusicTimeChange"
:src="music.value.url"
></audio>

<!--

绑定ref方便用js对Audio对象进行操作

用到的属性:
autoplay 是否自动播放
src 音频地址

用到的方法:
pause 暂停当前播放的音频时触发
play 开始播放音频时触发
timeupdate 播放位置发生改变时触发
-->

歌词获取和展示
  1. 首先将获取随机歌曲和歌词数据的方法封装成一个方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

import { ref, reactive } from 'vue'
import { getData } from "../api/api";
import { toSeconds } from '../utils/time'
export async function useMusic() {

// 随机歌曲相关信息获取
let datas: any = await getData(2, "", {});
let music = reactive({ value: datas.data.data });
let music_id = ref(music.value.url.split("?id=")[1]);

// 歌词通过歌曲id获取
let lyric: any = await getData(3, "", {
type: "lyric",
id: music_id.value,
});
// 对歌词数据进行处理 [{text:'歌词',time:2.333 (歌词开始秒数)},...]
let arr = lyric.data.lrc.lyric.split('\n')
let obj: any = reactive({ data: [] })
arr.forEach((element: string) => {
let key = toSeconds(element.split(']')[0].slice(1))
let value = element.split(']')[1]
obj.data.push({ text: value, time: key })
});

// 设置一个index作为歌词数据的索引
let index = ref(0)

return { index, obj, music }
}

  1. 对audio标签绑定ref,对audio对象进行操作。
  • timeupdate事件进行监听
    • 通过audio.value.currentTime获取到当前播放的秒数
    • 从前向后与歌词数据 (obj.data[index.value + 1].time) 的time进行对比
    • 如果当前秒数小于当前索引的歌词数据的秒数、就展示该条歌词数据;否则index++与下一条数据进行比较
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
export default defineComponent({
setup: async () => {
// 在异步组合api中,生命周期函数必须写在 数据/方法前面
onMounted(() => {});

let { index,obj,music } = await useMusic();

// 绑定audio对象
let audio = ref();

// 每条歌词数据
let msg = obj.data && obj.data[0].text ? ref(obj.data[0].text) : ref(" ");

// 进度条改变触发事件
function handleMusicTimeChange() {
if (audio.value.currentTime >= obj.data[index.value + 1].time) {
index.value++;
}
msg.value = obj.data[index.value].text;

}
return {
msg,
index,
obj,
audio,
music,
handleMusicTimeChange,
};
},
});

未解决的问题
1
2
3
4
5
6
7
8
9
10
/**
* 目前的播放器进度条不能进行控制 控制之后 不知道怎么获取对应索引的歌词
* (
* 进度条长度和音频总时长是按比例的
* 所以进度条播放位置发生改变时 timeupdate对于不同长度的歌曲触发秒数间隔都是不一样的
* currentTime也不一定能和time对应上 所以不知道怎么获取到歌词
*
* 不知道怎么实现QAQ , 所以就没展示audio的控件(controls)
* )
*/

Vue3+Vite+ElementPlus+VueRouter相关配置

demo在线展示 |
仓库地址

element-plus源码有点问题 打包的时候会报错,需要手动修改…

vite中文文档

前端构建工具,能够显著提升前端开发体验

初始化项目

1
npm init @vitejs/app

element-plus UI

element-ui没有兼容vue3,但是新推出了element-plus来兼容vue3

安装element-plus

1
npm i element-plus -S

修改配置文件引入库

修改main.ts

1
2
3
4
5
6
7
import { createApp } from 'vue'
import App from './App.vue'
import ElementPlus from 'element-plus' // +
import 'element-plus/lib/theme-chalk/index.css' // +

createApp(App).use(ElementPlus).mount('#app') // .use(ElementPlus)

ElMessageBox消息弹框

一个我自己推出来的功能.. 文档没找着
原因是我要在setup里面使用消息弹框组件 但按文档的配置需要用到this

1
2
3
4
5
6
7
8
9
10
11
// 但是文档上是写的 this.$alert('...')
// 众所周知 vue3 的setup里面没有 this
// 根据 消息提示 组件的配置方法: ElMessage('...')
// 我大胆猜想了下消息弹框的使用 然后 成功了..

setup:()=>{
// ...
ElMessageBox.alert('...')
}


vue-router

安装插件

1
npm i vue-router@next -S

编写路由配置文件

新建router.ts文件
没有在vite.config.ts配置的话 不能使用@代替src

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { createRouter, createWebHistory } from 'vue-router'

const Router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
component: () => import('../components/HelloWorld.vue')
},
{
path: '/home',
name: 'home',
component: () => import('../components/Home.vue')
},
]
})

export default Router

修改配置文件引入库

main.ts文件内添加内容

1
2
3
import router from './router/router'

createApp(App).use(router).mount('#app') // .use(router)

修改App.vue

将文件修改成

1
2
3
4
<div id="app">
<!-- 展示路由内容 -->
<router-view></router-view>
</div>

axios

安装插件

1
npm i axios -S

新建 http.ts 文件对axios进行配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import axios from 'axios'

axios.defaults.baseURL = '/api'
//'http://rap2api.taobao.org/app/mock/data' 不代理
axios.defaults.timeout = 10000

export function get(url: string, params: any) {
return new Promise((resolve, reject) => {
axios.get(url, {
params: params
}).then((res) => {
resolve(res)
}).catch((err) => {
reject(err)
})
})
}

新建 api.ts 文件对接口请求进行封装

1
2
import { get } from './http'
export const getData = (url: string, params: any) => get(url, params)

在 vite.config.ts 中配置proxy代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ...
server: {
host: '10.9.37.4',
port: 8080,
open: true,
proxy: {
'/api': {
target: 'http://rap2api.taobao.org/app/mock/data', // 目标接口域名
changeOrigin: true,
secure: false, // 是否是https
ws: true,
rewrite: (path) => path.replace(/^\/api/, '') // 重新接口地址
}
}

}

使用

  • 引入方法
    1
    import { getData } from '../api/api'
  • 使用

在组合api中使用异步获取的话 父组件使用的时候要在外层添加 <suspense>进行包裹 不然数据不会展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// child component
setup: async () => {
let datas: any = await getData('/1531382', { scope: 'response' })

let color = reactive(JSON.parse(JSON.stringify(datas.data.data.color)))
return { color }
}

// App.vue
<template>
<suspense>
<router-view></router-view>
</suspense>
</template>

vite.config.ts配置

详情

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

const path = require('path')

// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
alias: {
'@': path.join(__dirname, 'src') // 配置用@来代表src目录
},
// 服务器相关配置
server: {
port: 8080,
host: '0.0.0.0',
open: true, // 是否自动开启浏览器
},
})

Flex相关

父容器的属性

flex-direction (主轴/项目排列方向)

row(默认,水平,起点在左端) | row-reverse | column | column-reverse

flex-wrap (是否换行)

nowrap(默认不换行) | wrap(换行,第一行在上方) | wrap-reverse(换行,第一行在下方)

flex-flow (flex-direction属性和flex-wrap属性的简写形式,默认值为row nowrap)

<flex-deirection> || <flex-wrap>

justify-content (主轴的对齐方式)

flex-start(默认 左对齐) | flex-end | center | space-between (两端对齐,项目之间的间隔都相等。) | space-around (每个项目两侧的间隔相等)

align-items (定义项目在交叉轴上如何对齐)

flex-start (交叉轴的起点对齐) | flex-end(交叉轴的终点对齐) | center(交叉轴的中点对齐) | baseline(项目的第一行文字的基线对齐) | stretch(默认值 如果项目未设置高度或设为auto,将占满整个容器的高度)

子项目的属性

order (属性定义项目的排列序列。数值越小排列越靠前,默认为0)

flex-grow (项目的放大比例,默认为0)

如果所有项目的flex-grow属性都为1,则它们将等分剩余空间(如果有的话)。
如果一个项目的flex-grow属性为2,其他项目都为1,则前者占据的剩余空间将比其他项多一倍。

flex-shrink (项目的缩小比列,默认为1)

如果所有项目的flex-shrink属性都为1,当空间不足时,都将等比例缩小。如果一个项目的flex-shrink属性为0,其他项目都为1,则空间不足时,前者不缩小

  • 计算方式:
    (项目n宽度*n的缩小比例/(项目1宽度*1的缩小比例)+...+(项目n宽度*n的缩小比例))*超出的宽度

    100+400且flex-shrink都为1的子项目 父级为400 ,子项目超出100
    100的子项目应该缩小的宽度:100*1/(100*1+400*1)=0.2 0.2*100 = 20
    400的子项目应该缩小的宽度:400*1/(100*1+400*1)=0.8 0.8*100 = 80

flex-basis (项目的固定空间 与width类似)

length | auto(默认) | 百分比(主轴长度)

  • flex-growflex-shrink一起使用的时候的计算方式
父容器主轴的长/宽 设置为`900`

所有元素都设置为` flex:1 `
其中`元素1`设置` flex-basis: 30% ` 
`元素2`设置`flex-grow: 2`

1. 先看`flex-basis`,获取到应有的长度
1
2
3
// 只有元素1设置了宽度
width1 = 900*0.3 = 270

2. 因为设置了`flex-grow`,所有元素平分剩余的宽度
1
2
3
4
5
6
7
8
9
// 剩余 630
// 总份数: 1+2+1=4

// 元素1:占1份
width1 = 630*(1/4) + 270 = 427.5
// 元素2:占2份
width2 = 630*(2/4) = 315
// 元素3:占1份
width3 = 630*(1/4) = 157.5

flex (属性是flex-grow, flex-shrink 和 flex-basis的简写,默认值为0 1 auto。后两个属性可选 )

none | [ <flex-grow> <flex-shrink>?|| <flex-basis> ]

align-self (属性允许单个项目有与其他项目不一样的对齐方式 默认为auto)

auto | flex-start | flex-end | center | baseline | stretch

可视化

原型链

原型链有点懵 画了图好像理解了点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// person的构造函数
function Person(name){
this.name = name
}
// person原型对象方法
Person.prototype.eat = function(){
console.log('person eat')
}

// ...
function Student(){}
Student.prototype.sayhi = function(){
console.log('student sayhi')
}


Student.prototype = new Person('doreen')
var stu = new Student()
stu.eat() // person eat
stu.sayhi() // 报错 stu.sayhi is not a function

// stu.__proto__ => new Person()
// stu.__proto__.__proto__ === Person.prototype

总结

  • 构造函数可以实例化对象
  • 构造函数中有一个属性叫prototype,是构造函数的原型对象
  • 原型对象中有一个叫constructor构造器,指向自己所在的构造函数
  • 实例对象的原型对象 (__proto__) 指向的是构造函数的原型对象
  • 构造函数中的原型对象 (prototype) 中的方法是可以被实例对象直接访问
  • Copyrights © 2019-2023 John Doe
  • Visitors:1912 | Views:3630

请我喝杯咖啡吧~

支付宝
微信