基于 UniMax V5 的前端开发笔记

–@ acted by yuanbao at 20210301
在线查看:文档:UniMaxFPS 平台版本升级记录(v4-v5)….
链接:http://note.youdao.com/noteshare?id=d8882095064293a0b635686b1ffb878c&sub=FA014DEA5EAC4B059684181D7F99EDE8

VS Code 开发建议

插件安装,强制要求安装红框那几个。

image-20210309150208799

代码格式化【Prettier】

相关配置修改:

  • 修改配置文件 .editorconfig (使用 ctrl+p 搜索该文件)

    使用 tab 做缩进,缩进 4 格。

root = true

[*]
charset = utf-8
indent_style = tab
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
max_line_length = off
trim_trailing_whitespace = false
  • 打开 文件 -> 首选项 -> 设置 搜索 Detect Indentation 然后去掉勾选

代码格式化使用快捷键:shift + alt + f 【在 npm install、npm run build 后】

  • 设置格式化自动换行的长度

修改项目.prettierrc 文件,设置属性 “printWidth”: 140,

image-20210415114426578

最终编辑器底部显示如下即为成功:

image-20210423104738362

Demo 页面

Demo1:

1 个 vue 文件,里面写展示页面、新增页面、修改页面,实现单表的增删查改。

修改写的一个 template,新增是一个 dialog,这个没要求,但要求是 dialog 里面的字段数不要超过 6 个,否则就用 template 来展示。

为每一个页面、按钮都写了具体代码,比较冗余,不存在复用。

附文件:ybTestUser.vue

Demo2:

2 个 vue 文件,其中一个是展示界面,另一个是 input 界面。展示界面通过传参来控制打开的是新增还是编辑。

新增与编辑复用 input 界面,且数据、操作互不干扰。

代码相对简练,友好。

附文件:ybTestUser2.vue | ybTestUser2-input.vue

前端 VUE 的一些坑

1. vue 报错:Component template should contain exactly one root element.

** 解决:**template 标签下要包含一个有且一个 div 标签。如:

在这里插入图片描述

2. 表格数据列对应属性的二次属性

QQ图片20210312141903

这种形式不对,应该写成这样:使用 refEntity、refName。

tips:和 V4.5 的近似,注意大小写不要弄错。

待验证:应该也是支持三级属性的展示,前提先使用一个二级属性。

image-20210312142001389

坑:refName 不起作用,最终是由 refEntity 与 prop 共同作用

3. 表格高度占比不可设置,只能设置固定高度

QQ图片20210313113234

后刘庆告知了两个单位,vh 表示纵向屏幕占比,vw 为横向屏幕占比。

所以此处可以写 height: "30vh",

不确定是否在不同分辨率下的占比生效,待验证。

4. 公司 demo 服务超时后,不提示重新登录,而是直接弹框几个错误。

只能通过手动退出用户,再重新登录。

image-20210313114446630

5:删除操作问题的实现

写法 1:

let url = "apsShiftPatternController!del.m";
let params = qs.stringify({
    id: ids.toString(),
    entity: "com.epichust.entity.calender.ApsShiftPatternEntity",
});

这种写法,因为调用方法为 del,在平台 defaultController 有判断逻辑会走平台的 delete 方法,所以自己写的 del 方法会失效。所以此情形适用于:简单删除单表,不用自己写删除方法直接用平台的。要求:传参必须包含 id、entity 且不能错,方法为 del。

tips:冻结、激活都可用此形式,方法名分别为 active、freeze。

写法 2:

let url = "apsShiftPatternController!deleteObj.m";
let params = qs.stringify({
    ids: ids.toString() // 自行定义参变量
});

适用于:较复杂删除逻辑,自己写删除方法。参也可以自定义。

6. 编辑页面下拉框数据无法自动映射

image-20210313132454904

如上,有默认 key 值,但未被转换成 value 值。

** 解决:** 先加载表单数据,然后在获取 combox 数据时给设定值

getIsCheck() {
	this.$http.post("mbfWorkShiftClassController!isCheck.m").then((res) => {
		this.isCheckList = res;
		// 无值则默认选择第一个
        if (!this.ruleForm.isCheck) {
        	this.ruleForm.isCheck = this.optionData(res);
        }
	});
},

//遍历循环下拉框默认显示第一位 --工具
optionData(res) {
	let optionId = "";
	for (let i = 0; i < res.length; i++) {
		if (res[i].key != "") {
			optionId = res[i].key;
			break;
		}
	}
	return optionId;
},

** 此处有坑:** 这里的 combox 的键值对均为字符串类型,所以在编辑时带值过去时注意格式也应该为字符形式。

this.ruleForm.isCheck = mbfWorkShiftClass.isCheck + "";

7. 表格中字段转换为日期格式

import moment from "moment";

formatter: (row, col) => {
	if (!row.plannedBeginDate) return "";
	else return moment(new Date(row.plannedBeginDate)).format("YYYY-MM-DD");
},

// 完整时间格式
YYYY-MM-DD HH🇲🇲ss

8. 表格筛选条件下拉框

在 gridOptions 的 dataSource 的 filters 中:

{
	field: "isActive",
	compare: "eq,ne",
	data: "",
	type: "select",
	options: {
		keyName: "label",
		valueName: "value",
		// selectRequest: "bppComboxController!getXXCombox.m", // 后台请求方式
		selectOptions: [ // 前端对象映射方式,两者二选一
			{ label: "否", value: "0" },
			{ label: "是", value: "1" },
		],
	},
},

注意:这儿的 keyName 为要展示的值,valueName 为隐藏值,与之前平台理解的映射关系相反。

此处有坑!(YB&CD) 已向平台组反馈:

​ selectOptions 类的键值对必须为 label 和 value,且 keyName: “label”,
​ valueName: “value”,

9. 关于隐藏组件

表格中的某一个字段列隐藏:

​ grid/columns/ 字段列增加 visible: false,

表单中某一输入框隐藏:

​ el-form/el-form-item 上增加 v-show=“false”

另:form 表单上

  • required 写在 el-form-item 标签上;
  • disabled 写在 el-form-item 的 el-input 标签上;

10. 表格单元格文本替换和表单下拉框的共用数据源实现

1> 在 util 文件夹下定义了一个 fpsComboxData.js 文件

格式写:备注、export const 变量名=变量值集合;

内容为:

// 资源类型
export const resTypeList = [
	{ key: "S", value: "工厂" },
	{ key: "L", value: "产线" },
	{ key: "W", value: "工位" },
	{ key: "E", value: "设备" },
];

// 日历类型
export const calendarTypeList = [
	{ key: "N", value: "计划型日历" },
	{ key: "P", value: "预估停台时间产生的日历" },
	{ key: "S", value: "实际停台时间产生的日历" },
	{ key: "D", value: "调度当次停台事件产生的日历" }
];

// export { resTypeList };

2> 页面文件中,做引入:

import { resTypeList,calendarTypeList } from '@/util/fpsComboxData';

并在 export default 的 return 中赋值给变量

resTypeList: resTypeList,

如此后,点击变量可以显示具体值。

image-20210322101715767

3> 表单下拉框写法不变:

<el-form-item label="资源类型" prop="resType">
	<el-select
		:popper-append-to-body="false"
		clearable
		v-model="formDataEdite.resType"
		placeholder="请选择"
	>
		<el-option
			v-for="item in resTypeList"
			:key="item.key"
			:label="item.value"
			:value="item.key"
		></el-option>
	</el-select>
</el-form-item>

另:若需要下拉选择后,再执行逻辑

el-select 标签中加 @change=“方法名”,当前对象的值的获取可以直接用 this.formDataEdite.resType 这样的形式

4> 表格列文本替换写法调整为:

{
	prop: "resType",
	label: "资源类型",
	width: "120px",
	formatter: (row, col) => {
		const item = resTypeList.find(function(v){
			return (v.key === row.resType)
		})
		return item ? item.value : '';
	},
},

以上内容可参考工作日历 mbfCalendar.vue 中的 resTypeList 的使用。

11. 后端新坑,非主键字段的实体关联

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "RES_CODE", referencedColumnName = "RES_CODE", insertable = false, updatable = false)
@Where(clause="IS_DELETE = 0")
public ApsResEntity getResEntity()
{
	return resEntity;
}

public void setResEntity(ApsResEntity resEntity)
{
	this.resEntity = resEntity;
}

以上写法为过去后端实体类注解方式,然而报错:

Invocation of init method failed; nested exception is org.hibernate.MappingException: Unable to find column with logical name: RES_CODE in org.hibernate.mapping.Table(aps_res) and its related supertables and secondary tables

解决:修改referencedColumnName=“resCode”,用类的属性关联映射解决问题。之前都是用表的字段关联映射的,不清楚此处的原因,有可能是因为此处的目标类中的 @Column 注解是写在属性上的而不是方法上导致的。

另外,这些注解最好是放在 get 方法上而不是属性上,否则可能会有问题。

12. 表格,背景色设置

  • 单元格设置:单元格的值、字体颜色、字体大小、背景色等

    {
    	prop: "state",
    	label: vue.$t("base.TXT_WO_STATE"),
    	width: "100px",
    	formatter: (row, col) => {
    		if (row.state == 0) {
    			return `<span style="color:red">未发布</span>`;
    		} else if (row.state == 1) {
    			return `<span style="color:green">已发布</span>`;
    		}
    	},
    },
    
  • 行背景色设置:根据某列的值判断,渲染整行的颜色

    在 grid 下设置‘row-style’属性

    'row-style': function({row, rowIndex}){
    	let style = {}
    	if(row.configurability == 1){
    		style = { backgroundColor:'red' }
    	}
    	return style
    },
    

13 下拉框和查找带回框

下拉框

下拉框基本都使用的是 ElementUI 的 el-select,未使用平台封装的 eh-select。实现方式参考第 10 节。
参考:https://element.eleme.cn/2.0/#/zh-CN/component/select

几个有用的属性:
  • clearable 允许清空
  • filterable 允许搜索
  • allow-create 允许创造选择项(自己输入的内容成为选项)
若要实现既可选择又可输入:

​ 1> 加属性:

image-20210608151210210

​ 2> 注册方法:给对应的字段赋值

// 用于版本号既可选择也可输入
selectBlurVer(e) {
	this.formDataVer.verCode = e.target.value;
},
查找带回

1> 先写查找带回页面

路径放在 fps/commons/searchBackTemplate/ 下。

内部主要是使用了 eh-search-back 组件来完成表格的定义和带回。

image-20210324152545493

2> 使用页面,先做引入:

import quotaSearchBack from "@/views/fps/commons/searchBackTemplate/quotaSearchBack";

组件注册:

components: {quotaSearchBack},

使用:

<quota-search-back v-model="formData.quotaCode" @selectedRows="selectBackDataQuota" :params="quotaParams" labelField="code" searchField="code"></quota-search-back>
  • :params 可以用来传参到查找带回页面,让查找带回的表格带参查询;

    • 也可以设置为按钮触发
  • @selectedRows 用来写查找带回行数据后的后续逻辑,比如给几个框赋值;

    • selectBackDataQuota(rows) {
      	if (rows.length == 1) {
      		this.formDetail.quotaCode = rows[0].code;
      		this.formDetail.quotaName = rows[0].name;
      		this.formDetail.compareType = rows[0].compareType;
      		this.formDetail.purposeType = rows[0].purposeType;
      	}
      },
      
  • labelField 行数据的使用字段,如果只用给当前框设值就只用写 labelField 即可,不用写 @selectedRows 及相应方法

3> 页面完整代码:

quotaSearchBack.vue

apsResQuota-main.vue

4> 搜索框内回车搜索实现

form 表单增加两行,即可让 form 内的每一项回车即可执行相应方法。

@submit.native.prevent
@keyup.enter.native="enterSearch"

image-20210329150246324

然后定义方法:

// 回车搜索
enterSearch() {
	this.$refs.quotaSearchBack.searchGrid();
},

5> 主页面传参到带回页面作默认搜索

主页面:

image-20210330152120930

传参:

resSearchParams: { resClass: "S" },

带回页面:

有两种方式,方式一为放入表格的 filter 窗口中(要求在 form 中写输入框),方式二传参到表格后台查询逻辑的 params。

方式一:

image-20210330152355746

定义过滤字段,并赋初始值:

image-20210330152632551

效果图:

image-20210330155120560

方式二:

image-20210330155605235

代码:

// 定义传参方法给到表格
const getGridOption = function (params = {}){
    gridOptions.dataSource.params = params;
    return gridOptions;
};

赋值
gridOptions: getGridOption(this.params),

6> 主页面传参到带回页面作默认搜索 - 动态传参

前面 5> 的方式适用于静态方式,当需要动态传参就不再适用了。比如需要传参同一表单另一个字段的值到当前字段的查找带回页面作筛选条件。实现步骤:

页面一:直接传参

image-20210531172727463

页面二:设置该参数接收,并设置监听作响应

写筛选框方式不变。

然后,filterFormObj(其名称不固定)下写:(增加过滤字段)

resType: {
    field: "resType",
    compare: "eq",
    data: this.resType, // this.params.resType
},

props 下写:

// YB-另外传参
resType: {
    required: false,
    type: String,
    default() {
    	return "";
    },
},

watch 下写:

// 监听该参数的值变化做响应
resType: {
    handler: function (newval, oldVal) {
    	this.filterFormObj.resType.data = newval;
    },
},

14 弹框提醒

方式一:MessageType = ‘success’ | ‘warning’ | ‘info’ | ‘error’

this.$message({
    message: "请完善数据!",
    type: "warning",
});

方式二:

this.$message.success("操作成功!");

设置显示时间:使用 duration

self.$message({
    message: res.data,
    type: "warn",
    duration: 5000,
});

15 表单输入框的时间选择、日期选择

日期 + 时间选择:

<el-form-item label="计划开始时间" prop="startTime">
	<el-date-picker
		placeholder="选择日期时间"
		v-model="formData.startTime"
		type="datetime"
		value-format="yyyy-MM-dd HH🇲🇲ss"
	></el-date-picker>
</el-form-item>

日期选择:

<el-date-picker
	placeholder="选择日期"
	v-model="ruleForm.birthday"
	type="date"
	value-format="yyyy-MM-dd"
></el-date-picker>

时间选择:

<el-time-picker
    placeholder="选择时间"
    v-model="ruleForm.checkBeginDate"
    value-format="HH🇲🇲ss"
></el-time-picker>

日期范围,开始日期 - 结束日期:

<el-date-picker
	v-model="orderTig.beginDate"
	type="daterange"
	unlink-panels
	:range-separator="$t('base.i18n_to_120')"
	:start-placeholder="$t('base.i18n_startDate')"
	:end-placeholder="$t('base.i18n_theEndDate')"
	:picker-options="$static.DatePickerOption"
	value-format="yyyy-MM-dd"
></el-date-picker>

另外 1:若两个日期之间,前后大小关系约束:

​ :picker-options=“startDatePicker(formData.endTime)”

// 起止时间约束
startDatePicker(end) {
	return {
		disabledDate(time) {
			if (end) {
				// 如果结束时间不为空,则小于结束时间
				return new Date(end.substring(0, 10).replace(/-/g, "/")).getTime() < time.getTime();
			}
		},
	};
},
endDatePicker(start) {
	return {
		disabledDate(time) {
			if (start) {
				// 如果开始时间不为空,则结束时间大于开始时间,且大于当前时间
				return (
					new Date(start.substring(0, 10).replace(/-/g, "/")).getTime() > time.getTime() ||
					time.getTime() < Date.now() - 1000 * 3600 * 24
				);
			} else {
				return time.getTime() < Date.now() - 1000 * 3600 * 24; //开始时间不选时,结束时间最大值大于等于当天
			}
		},
	};
},

另外 2:

​ value-format=“yyyy-MM-dd HH🇲🇲ss” 用于组件的实际值
​ format=“yyyy-MM-dd HH:mm” 用于组件操作时可选择的内容范围

其他输入框样式:在 el-input 中

  • type=“number” 数字
  • type=“textarea” 长文本 :rows=“2” 设置高度 或 autosize 自适应高度
    • 那两个属性自己实践了无效,可能原因是被 el-form-item 限定。没有 el-form-item 则是正常的。

若要约束数字长度,或只要某些数字,可在 rule 中写正则方式。

e.g.

image-20210409104900900

16 事件监听 @todo

@change=“xxMethod” 用于监听某个输入框、选择框发生值的变更后执行的逻辑,如用来自动搜索。

17 Tab 页的新增和去除

移除 tab

  • 方式一:this.$refs.calendarTabs.removeTab("calendar-add");
  • 方式二:this.$emit("close", "planOrder-edit");
    • $emit 然后调用父组件内的方法,有时候不太灵,可能没指定 tabs,建议方式一。

增加 tab

  • this.tabs.push({ name: "planOrder-edit", title: "修改订单" });
  • this.tabs.push({ name: "planOrder-edit", title: "修改订单", closable: false}); 不可关闭

18 excel 导入踩坑

调用方式,使用 EhInputButton 组件,传参两个方法,一个是用来获取标识码,另一个则是上传文件的执行逻辑。

<eh-input-button
    :upload="'apsPlanOrderImportController!goUploadData.m'"
    :action="'apsPlanOrderImportController!importPlanOrder.m'"
></eh-input-button>

此处有坑:

image-20210407113122456

组件中定义上传 action 的 url 写法是这样,而 prefixURl 的值写死为产品的 "uma-unimax-web/"

,需要修改为 "UniMaxFPS/" 。

19 弹出确认对话框方式

demo 如下:

let msg = "是否齐套当前条件下的所有工单?";
this.$confirm(msg, "提示", {
	confirmButtonText: this.$t("base.i18n_ok"),
	cancelButtonText: this.$t("base.i18n_cancel"),
	type: "warning",
})
	.then(() => {
		let data = qs.stringify({
			calendarGid: row.id,
		});
		self.$http.post("mbfCalendarController!deleteCalendar.m", data).then((res) => {
			
		});
	})
	.catch(() => {
		this.$message({ type: "info", message: "已取消当前操作!" });
	});

另:then() 里面的方法如果有抛错也会走 catch

20 莫名报错

刷新表格:this.$refs.mainGrid2.bindGrid(); 然而报错:

vue.esm.js:571 [Vue warn]: Error in event handler for “click”: “TypeError: Cannot read property ‘bindGrid’ of undefined”

猜测原因:

如果这个第二个 tab 没有最初就放在 tabs 里面,而是通过手动 push 的方式打开 tab 页,就可能导致对第二个 tab 页内 grid 元素获取不到。

可以尝试下 this.$nextTick 方法

21 给表格自己写查询逻辑【简化方式写 hql】

前端:

const gridOptions = {
	dataSource: {
		bean: "apsWoMrlKittingController",
		method: "selectWO",
		entity: "com.epichust.mestar.udc.entity.NoEntityBean",

后端:

public void selectWO()
{
    Map<String, String> params = this.pageData.getParams();
    SelectData selectData = this.getSelectService().getProcessData(pageData);
    // 自己写查询逻辑
    selectData.setSelectRecordsQueryString(apsWoMrlKittingService.getRecordsQueryString(params));
    selectData.setSelectCountQueryString(apsWoMrlKittingService.getCountQueryString(params));
    selectData.setQueryLang(SelectData.QUERY_LANGUAGE_HQL);
    IReturnData returnData = this.getSelectService().process(selectData);
    this.returnData = returnData;
}

image-20210410122810090

setSelectRecordsQueryString 里面,可以写 hql 也可以写 sql,setQueryLang 可以指定类型。setSelectRecordsQueryString 写成 count(1) 即可。

注意写成 new map() 并且每个属性写 as,这样前端表格字段能够匹配上。

tips: 钟传富 new map() 后写 distinct 作去重会报错,无法使用该关键字。

后张青提出的问题:自己写的表格查询逻辑时,V5 查询模板不生效。

image-20210425180023328

image-20210425180137991

附 Dao 层 demo 代码:DaoDemo.java

22 树结构渲染展示更多内容

效果图:

image-20210413191014611

前端:

// 树结构渲染-YB增加悬浮展示更多内容
renderContent(h, { node, data, store }) {

	return (
		<div title={data.onKeyPress}>
			{data.type === "parent" ? (
				node.expanded ? (
					<i class="el-icon-folder-opened"></i>
				) : (
					<i class="el-icon-folder"></i>
				)
			) : (
				<i class="el-icon-document"></i>
			)}
			{data.text}
		</div>
	);
},

后端:

image-20210413190809025

23 JS 控制打开新的一级 Tab 页

image-20210414142542892

实践可用 demo 代码:

this.$router.push({
    name: "apsWorkOrder", // 菜单表中的rel字段值,注意不是href的路径
    query: {id: '11',}, // 传参用,会显式的拼接在浏览器的url?后
    params: {id: '22',}, // 传参用,目标页面可通过this.$route.params取值
});

24 对表单数据清空

  • 方式一:this.$refs.formData.resetFields();

    理解偏差:

  1. 该方法是对输入后的数据重置恢复为输入前的数据,是重置,并非清空。
  2. 该语句必须在 form 已经完成渲染后才能执行,否则会找不到对象。故下面使用 this.$nextTick
  • 方式二:遍历清空属性值

定义方法,传参数据绑定的 formData

// 清除表单的值
clearForm(formData) {
    for (const key in formData) {
    	formData[key] = "";
    }
},

使用:

image-20210414144834341

以上:用来解决新增、编辑时表单数据互窜的问题。

25 布局控制

左右布局占比,在 eh-layout 中设置 left-width、right-width,值可为多少百分比或多少 px.

top-width、bottom-width 测试后无效(YB)。

<eh-layout left-width="30%">
    <div slot="left">
    </div>
    <div slot="center">
    </div>
</eh-layout>

26 CD

1> 去掉查询按钮

#scheduleAddOrEdit {
	.tool-bar {
		.el-button {
			&:last-of-type {
				visibility: hidden;
			}
		}
	}
}

2> 对话框调整宽高

img

27 引入一个外部 js 文件,并提供方法

** 应用背景:**eric- 使用 sse 与后端建立长连接,接收后端动态推送的消息然后前端通过 atmosphere.js 来打印到页面上。

实现过程:

​ 1. 拷贝 js 文件至 src/util/ 下

image-20210721164954066

  1. 用另一个 js 封装一下:import js,并创建方法、变量,然后 export 这个方法

    ** 注:** 此步并非必须,可直接在 vue 文件中直接使用原 js 文件。这么做的目的:一是封装一层后给多个 vue 页面直接用;二是熟悉下 import/export 操作。

import atmosphere from "./atmosphere-min.js";

// YB-开始建立sse连接,传参用户名和回调方法
function startListen(userName, callback) {
	
}

export {
	startListen
}
  1. vue 文件中引入并使用这个方法
import { startListen } from "@/util/atmosphere-application.js";

let userName = this.$store.getters.user.loginName; // 获取当前用户
startListen(userName,  (res) => {
    console.log("日志回调:" + res);
    let resObject = JSON.parse(res);
    let text = resObject.text;
    this.scheduleLogText += ">>" + text + "<br/>";
});