Java道经第3卷 - 第7阶 - Vue(二)
传送门:JB3-7-Vue(一)
传送门:JB3-7-Vue(二)
文章目录
S03. 封装ElementPlus组件
E01. 基本环境搭建
1. 封装通用工具JS文件
/util/index.js:
/**
* 判断非空值
*
* @param value 被判断的值
* @return boolean 返回 true 表示不为 null 也不为 undefined
*/
export function isNotNull(value) {
return value !== null && value !== undefined;
}
/**
* 判断空值
*
* @param value 被判断的值
* @return boolean 返回 true 表示为 null 或 undefined
*/
export function isNull(value) {
return !isNotNull(value);
}
/**
* 判断是否存在空值
*
* @param values 被判断的值,不定长列表
* @return boolean 返回 true 表示包含 null 或 undefined
*/
export function hasNull(...values) {
for (let value in values) {
if (isNull(value)) {
return true;
}
}
return false;
}
/**
* 判断空字符串
*
* @param value 被判断的值
* @return boolean 返回 true 表示不为 null 或 undefined 或空字符串
*/
export function isNotEmpty(value) {
return value !== null && value !== undefined && value !== '';
}
/**
* 判断非空字符串
*
* @param value 被判断的值
* @return boolean 返回 true 表示为 null 或 undefined 或空字符串
*/
export function isEmpty(value) {
return !isNotEmpty(value);
}
/**
* 判断是否存在空字符串
*
* @param values 被判断的值,不定长列表
* @return boolean 返回 true 表示包含 null 或 undefined 或空字符串
*/
export function hasEmpty(...values) {
for (let value in values) {
if (isEmpty(value)) {
return true;
}
}
return false;
}
// 用于将六大元素处理为两位数格式(年除外)
function toDouble(e) {
return e < 10 ? '0' + e : e;
}
/**
* 日期字符串处理:1999-01-02T12:12:12 -> 1999年01月02日 12:12
*
* @param dateStr 日期字符串
* @return string 返回格式化后的日期字符串
*/
export function datetimeFormat(dateStr) {
if (isNull(dateStr)) return '';
// 将日期字符串转为日期格式
let date = new Date(dateStr);
// 获取日期中的元素: 年,月,日,时,分
let yy = toDouble(date.getFullYear());
let mm = toDouble(date.getMonth() + 1);
let dd = toDouble(date.getDate());
let hh = toDouble(date.getHours());
let mi = toDouble(date.getMinutes());
// 返回美化后的日期字符串
return `${yy}年${mm}月${dd}日 ${hh}:${mi}`;
}
/**
* 日期字符串处理:1999-01-02T12:12:12 -> 1999年01月02日
*
* @param dateStr 日期字符串
* @return string 返回格式化后的日期字符串
*/
export function dateFormat(dateStr) {
if (isNull(dateStr)) return '';
// 将日期字符串转为日期格式
let date = new Date(dateStr);
// 获取日期中的元素: 年,月,日,时,分
let yy = toDouble(date.getFullYear());
let mm = toDouble(date.getMonth() + 1);
let dd = toDouble(date.getDate());
// 返回美化后的日期字符串
return `${yy}年${mm}月${dd}日`;
}
2. 封装通用请求JS文件
心法:该文件依赖 ElementPlus 框架,VueRouter 路由功能和 /util/index.js 文件。
/request/index.js:
import {ElMessage, ElMessageBox} from "element-plus";
import {hasNull, isNotNull, isNull} from "../util/index.js";
import router from "../router/index.js";
/** 常用响应码 */
export const STATUS = {
SUCCESS: 1000,
TOKEN_EXPIRED: 8000
}
/**
* 获取服务器响应中的data数据
*
* @param res axios响应结果对象
* @return result 请求成功时返回data数据或true,响应失败时返回null
* */
export function getResponseData(res) {
let result = null;
// 若响应结果为空,提示并返回null
if (isNull(res)) {
ElMessage.warning('服务器无响应!');
return result;
}
// 若存在2层data,则直接拆除第1层data
res = undefined !== res.data && undefined !== res.data.data ? res.data : res;
// 请求成功,返回 data 数据
if (res.code === STATUS.SUCCESS) {
// DQL 操作成功时返回查询到的数据,DML 操作成功时返回 true
result = isNotNull(res.data) ? res.data : true;
}
// 请求失败 - Token过期:提示并跳转到登录页面
else if (res.code === STATUS.TOKEN_EXPIRED) {
ElMessage.warning('Token过期,请重新登录!');
setTimeout(() => router.push('/'), 1000);
}
// 请求失败 - 服务器异常
else {
ElMessage.warning(res['message']);
console.error(res['coderMessage']); // TODO 生产环境下删除
}
return result;
}
/**
* 新增一条数据(表单,异步)
*
* <p> form: 表单对象,必须是reactive变量,必传。
* <p> api: API请求函数名(不带小括号),必传。
* <p> params: API请求函数参数,JSON格式,作为请求参数,必传。
* <p> args: API请求函数参数,JSON格式,不作为请求参数,而是有一些其它作用,可选。
* <p> callback: 回调函数名(不带小括号),即当操作成功后调用的函数,可选。
*/
export async function myInsert(config) {
// 必传参数
const form = config['form'];
const api = config['api'];
const params = config['params'];
// 空值保护
if (hasNull(form, api, params)) return;
// 可选参数
const args = config['args'];
const callback = config['callback'];
// 验证表单:只有全部校验规则都通过,valid 参数才为 true
form.value.validate(async (valid) => {
if (valid) {
// 发送请求
let res = isNotNull(args) ? await api(params, args) : await api(params);
if (isNotNull(getResponseData(res))) {
// 存在回调函数时,异步调用回调函数
if (callback) await callback();
}
}
});
}
/**
* 分页查询数据(异步)
*
* <p> api: API请求函数名(不带小括号),必传。
* <p> params: API请求函数参数,JSON格式,作为请求参数,必传。
* <p> args: API请求函数参数,JSON格式,不作为请求参数,而是有一些其它作用,可选。
* <p> records: 分页结果中的数据列表,必传。
* <p> pageInfo: 分页结果中的分页信息,包括 pageNum,pageSize 和 total 三项,必传。
*/
export async function myPage(config) {
// 必传参数
const api = config['api'];
const records = config['records'];
const pageInfo = config['pageInfo'];
const params = config['params'];
// 空值保护
if (hasNull(api, params, records, pageInfo)) return;
// 可选参数
const args = config['args'];
// 发送请求
let res = isNotNull(args) ? await api(params, args) : await api(params);
let data = getResponseData(res);
if (isNotNull(data)) {
records.value = data['list'] || data['records'];
pageInfo['pageNum'] = data['pageNum'] || data['pageNumber'] || data['page'];
pageInfo['pageSize'] = data['pageSize'] || data['size'];
pageInfo['total'] = data['total'] || data['totalRow'];
}
}
/**
* 修改一条数据(表单,异步)
*
* <p> form: 表单对象,必须是reactive变量,必传。
* <p> api: API请求函数名(不带小括号),必传。
* <p> params: API请求函数参数,JSON格式,作为请求参数,必传。
* <p> args: API请求函数参数,JSON格式,不作为请求参数,而是有一些其它作用,可选。
* <p> callback: 回调函数名(不带小括号),即当操作成功后调用的函数,可选。
* <p> successTip: 是否需要成功提示,默认true,可选。
* <p> successTipContent: 操作成功后的提示文字,可选。
*/
export async function myUpdate(config) {
await myInsert(config);
}
/**
* 删除一条数据(异步)
*
* <p> id: 数据主键,必传。
* <p> api: API请求函数名(不带小括号),必传。
* <p> args: API请求函数参数,JSON格式,不作为请求参数,而是有一些其它作用,可选。
* <p> callback: 回调函数名(不带小括号),即当操作成功后调用的函数,可选。
*/
export async function myDelete(config) {
// 必传参数
const id = config['id'];
const api = config['api'];
// 空值保护
if (hasNull(id, api)) return;
// 可选参数
const args = config['args'];
const callback = config['callback'];
// 危险操作保护
await ElMessageBox.confirm('即将删除1条数据,确认吗?').then(async () => {
// 发送请求
let res = isNotNull(args) ? await api(id, args) : await api(id);
if (isNotNull(getResponseData(res))) {
// 存在回调函数时,异步调用回调函数
if (callback) callback();
}
});
}
/**
* 批量删除数据(异步)
*
* <p> ids: 主键数组,必传。
* <p> api: API请求函数名(不带小括号),必传。
* <p> args: API请求函数参数,JSON格式,不作为请求参数,而是有一些其它作用,可选。
* <p> callback: 回调函数名(不带小括号),即当操作成功后调用的函数,可选。
*/
export async function myDeleteBatch(config) {
// 必传参数
const ids = config['ids'];
const api = config['api'];
// 空值保护
if (hasNull(ids, api)) return;
// 主键数组保护
if (ids.length <= 0) {
ElMessage.warning('至少选择1项!');
return;
}
// 可选参数
const args = config['args'];
const callback = config['callback'];
// 危险操作保护
await ElMessageBox.confirm(`即将删除 ${ids.length} 条数据,确认吗?`).then(async () => {
// 发送请求
let res = isNotNull(args) ? await api(ids.join(','), args) : await api(ids.join(','));
if (isNotNull(getResponseData(res))) {
// 存在回调函数时,异步调用回调函数
if (callback) callback();
}
})
}
E02. 封装基础组件
1. 封装组件 - 路径导航
心法:该组件依赖 VueRouter 路由功能。

- 封装路径导航通用组件:
components/MyNav.vue:
<script setup>
import router from "../router";
let parent = defineProps({
/* 导航项
*
* icon: 图标
* label: 文案
* url: 导航项点击跳转地址
*/
items: {type: Array, required: true},
});
</script>
<template>
<el-breadcrumb class="my-nav breadcrumb-nav">
<el-button class="back-btn"
size="small"
link
icon="Back"
@click="router.back(-1);">
返回
</el-button>
<el-divider class="divider" direction="vertical"/>
<el-breadcrumb-item class="my-nav-item"
v-for="(item ,i) in items"
:key="item"
:to="item['url']">
<!--type: 最后1个按钮为default,其余为info-->
<el-button class="item-btn"
size="small"
link
:icon="item['icon']"
:type="i === items.length - 1 ? 'info' : 'default'">
{{ item['label'] }}
</el-button>
</el-breadcrumb-item>
</el-breadcrumb>
</template>
<style scoped lang="scss">
.my-nav {
* {
float: left; // 左浮动
}
.el-button {
font-size: 1rem; // 字号
padding-top: 0; // 上内边距
}
}
</style>
- 测试路径导航通用组件:
views/my/MyNavTest.vue:
<script setup>
import MyNav from "../../components/MyNav.vue";
import {useDark} from "@vueuse/core";
let items = [
{icon: 'House', label: '首页', url: '/House'},
{icon: 'Files', label: '内容管理', url: '/Files'},
{icon: 'Picture', label: '横幅管理'},
];
useDark();
</script>
<template>
<MyNav :items="items"/>
</template>
2. 封装组件 - 图标文字
心法:该组件不依赖任何功能或文件。

- 封装图标文字通用组件:
components/MyIcon.vue:
<script setup>
let parent = defineProps({
// 图标名称,必填
icon: {type: String, required: true},
// 标题文案
label: {type: String, required: false},
// 图标和标题的共同尺寸,默认14px
size: {type: Number, required: false, default: 14},
});
</script>
<template>
<el-icon class="icon" :size="size">
<component :is="icon"/>
</el-icon>
<span class="text" :style="{'font-size' : size + 'px'}">
{{ label }}
</span>
</template>
- 测试图标文字通用组件:
views/my/MyIconTest.vue:
<script setup>
import MyIcon from "../../components/MyIcon.vue";
import {useDark} from "@vueuse/core";
useDark();
</script>
<template>
<myIcon label="苹果" icon="Apple" size="50"/>
</template>
3. 封装组件 - 数据页头
心法:该组件依赖 /util/index.js 文件。

- 封装页头栅格通用组件:
components/MyHead.vue:
<script setup>
import {ref} from "vue";
import {isNotNull} from "../util";
let parent = defineProps({
/* 页头项
*
* span: 列占位,默认 1,全类型通用
* offset: 列偏移,默认 0,全类型通用
* type: 表头项类型,支持 ipt, opt, rdo, btn 4种,默认 btn:
* type = ipt: 单行文本框
* placeholder: 单行文本框背景字,默认 "请输入.."
* iptValue: 单行文本框默认值
* callback:点击时的回调函数,回传当前控件的 iptValue 值
* type = opt: 下拉菜单
* placeholder: 单行文本框背景字,默认 "请选择.."
* optValue: 下拉菜单默认值
* filterable: 下拉菜单是否开启过滤,默认 true
* options: 下拉菜单或单选按钮项,格式 [{ label: '', value: '' }]
* callback:选择时的回调函数,回传当前控件的 optValue 值
* type = rdo: 单选按钮
* options: 下拉菜单或单选按钮项,格式 [{ label: '', value: '' }]
* rdoValue: 单选按钮默认值,(type = rdo专属)
* callback:选择时的回调函数,回传当前控件的 rdoValue 值
* type = btn: 普通按钮
* btnTitle: 鼠标经过按钮时的文字提示,默认 "普通按钮"
* btnType: 按钮类型,默认 "Info"
* btnIcon: 按钮图标,默认 "Apple"
*/
items: {type: Array, required: true}
});
// 存放父组件传递的全部文本框的值 + 全部单选按钮的值 + 全部下拉菜单的值
let iptValues = ref({}), rdoValues = ref({}), optValues = ref({});
// 分类存储 iptValues, rdoValues 和 optValues 的值
for (let item of parent['items']) {
// 使用当前 item 的字符串形式作为 key 值,以保证唯一性
let key = JSON.stringify(item);
// 将全部单行文本的值存入 iptValues 对象
if (item['iptValue'] != null) iptValues.value[key] = item['iptValue'];
// 将全部单选按钮的值存入 rdoValues 对象
if (item['rdoValue'] != null) rdoValues.value[key] = item['rdoValue'];
// 将全部下拉菜单的值存入 optValues 对象
if (item['optValue'] != null) optValues.value[key] = item['optValue'];
}
</script>
<template>
<el-row class="my-head">
<el-col v-for="item in items"
:key="item"
:span="isNotNull(item['span']) ? item['span'] : 1"
:offset="isNotNull(item['offset']) ? item['offset'] : 0">
<!--ipt文本框-->
<el-input class="ipt"
v-if="item['type'] === 'ipt'"
size="small"
clearable
v-model="iptValues[JSON.stringify(item)]"
:placeholder="isNotNull(item['placeholder']) ? item['placeholder'] : '请输入..'"
@keyup.enter="item['callback'](iptValues[JSON.stringify(item)])"
@clear="item['callback'](iptValues[JSON.stringify(item)])">
<template #append>
<el-button class="ipt-btn"
size="small"
title="根据输入的条件进行模糊搜索"
:icon="'Search'"
@click="item['callback'](iptValues[JSON.stringify(item)])"/>
</template>
</el-input>
<!--opt下拉菜单-->
<el-select class="opt"
v-else-if="item['type'] === 'opt'" size="small"
clearable
v-model="optValues[JSON.stringify(item)]"
:filterable="isNotNull(item['filterable']) ? item['filterable'] : true"
:placeholder="isNotNull(item['placeholder']) ? item['placeholder'] : '请选择..'"
@change="item['callback'](optValues[JSON.stringify(item)])">
<el-option class="opt-item"
v-for="option in item['options']"
:key="option"
:label="option['label']"
:value="option['value']"/>
</el-select>
<!--rdo单选按钮-->
<el-radio-group class="rdo"
v-else-if="item['type'] === 'rdo'"
size="small"
v-model="rdoValues[JSON.stringify(item)]"
@change="item['callback'](rdoValues[JSON.stringify(item)])">
<el-radio-button class="rdo-item"
v-for="option in item['options']"
:key="option"
:label="option['value']">
<span>{{ option['label'] }}</span>
</el-radio-button>
</el-radio-group>
<!--普通按钮-->
<el-button class="btn"
v-else
plain
size="small"
:title="isNotNull(item['btnTitle']) ? item['btnTitle'] : '普通按钮'"
:icon="isNotNull(item['btnIcon']) ? item['btnIcon'] : 'Apple'"
:type="isNotNull(item['btnType']) ? item['btnType'] : 'info'"
@click="item['callback']"/>
</el-col>
</el-row>
</template>
<style scoped lang="scss">
:deep(.el-input__inner) {
padding-left: 5px; // 左内边距
box-sizing: border-box; // 忽略内边距影响
}
:deep(.el-select__selection) {
padding-left: 5px; // 左内边距
box-sizing: border-box; // 忽略内边距影响
}
.my-head {
display: flex; // flex布局
align-items: center; // 垂直居中
margin: 30px auto 10px; // 外边距
.ipt, .opt {
width: 95%; // 宽度
}
.rdo, .btn {
float: right; // 右浮动
}
}
</style>
- 测试页头栅格通用组件:
views/my/MyHeadTest.vue:
<script setup>
import MyHead from "../../components/MyHead.vue";
import {useDark} from "@vueuse/core/index";
let items = [
{
type: 'ipt', iptValue: 0, span: 5, placeholder: '请输入用户昵称搜索',
callback: res => console.log(res),
},
{
type: 'opt', optValue: 0, span: 5, offset: 2, placeholder: '请选择你的性别',
callback: res => console.log(res),
options: [{label: '女', value: 0}, {label: '男', value: 1}]
},
{
type: 'rdo', rdoValue: 0, span: 4,
callback: res => console.log(res),
options: [{label: '我是女孩', value: 0}, {label: '我是男孩', value: 1}]
},
{
type: 'btn', btnTitle: '普通按钮', btnIcon: 'Plus', btnType: 'primary',
callback: () => console.log('普通按钮')
}
]
useDark();
</script>
<template>
<myHead :items="items"/>
</template>
4. 组件封装 - 数据表格
心法:该组件依赖 ElementPlus 框架,/util/index.js 文件,/request/index.js 文件和 VueRouter 路由功能。

- 封装数据表格通用组件:
components/MyTable.vue:
<script setup>
import {ref} from "vue";
import {ElMessageBox} from "element-plus";
import {myDelete, myDeleteBatch} from "../request";
import {isNotNull, datetimeFormat} from "../util";
import router from "../router";
let parent = defineProps({
// 模块名称,必填,用于删除和批量删除业务
module: {type: String, required: true},
// 表格行数据,必填
records: {type: Array, required: true},
/*
* 表格列数据,必填:
*
* label: 列名数组,全类型通用
* prop: 属性名,全类型通用
* width: 列宽,全类型通用
* sortable: 是否支持排序,默认 true,全类型通用
* tooltip: 是否支持溢出内容的鼠标经过提示,默认 false,全类型通用
* type: 表格列类型,支持 tag, star, card, img, icon, text 6种,默认 text:
* type = tag: 标签
* prefix: 前缀文字
* suffix: 后缀文字
* format: 前置格式化函数
* tagType: 标签颜色,默认 "warning"
* tagTypeFn: 标签颜色函数
* type = card: 卡片
* prefix: 前缀文字
* suffix: 后缀文字
* format: 前置格式化函数
* type = img: 图片
* minio: MINIO地址前缀函数
* imgWidth: 图片宽度,默认 100px
* imgHeight: 图片高度,默认 100px
* type = icon: 图标
* iconType: 图标颜色,默认 "primary"
* iconSize: 图标尺寸,默认 "small"
* type = text: 文本
* prefix: 前缀文字
* suffix: 后缀文字
* format: 前置格式化函数
*/
columns: {type: Array, required: true},
/* 分页信息
*
* pageNum: 当前第几页,默认 1
* pageSize: 每页多少条,默认 5
* total: 一共多少条
* callback: 分页回调函数
*/
pageInfo: {type: Object, required: true},
/* 表格尾按钮:
*
* label: 按钮文案,默认 "普通按钮"
* type: 按钮颜色,默认 "success"
* icon: 按钮图标,默认 "InfoFilled"
* callback: 当点击按钮时触发的回调函数
*/
buttons: {type: Array, required: false},
// 当点击添加按钮时跳入的路由组件名称
insertPage: {type: String, required: false},
// 跳入添加页面之前,附加的参数,JSON格式,默认存储到 sessionStorage 中
insertPageParam: {type: Object, required: false},
// 当点击修改按钮时跳入的路由组件名称
updatePage: {type: String, required: false},
// 跳入修改页面之前,附加的参数,JSON格式,默认存储到 sessionStorage 中
updatePageParam: {type: Object, required: false},
// 单条删除API方法
deleteApi: {type: Function, required: false},
// 打印报表API方法
excelApi: {type: Function, required: false},
// 批量删除API方法
deleteBatchApi: {type: Function, required: false},
// 单条删除和批量删除结束后的回调方法
deleteCallback: {type: Function, required: false},
// 每页多少条可选数量,默认 [1, 5, 10, 18]
pageSizes: {type: Array, required: false, default: [1, 5, 10, 18]},
});
/* ==================== 处理表格多级标题 ==================== */
// 表格列数据对象
let columnsObj = {};
/*
* 原格式:
*
* [
* {label: ['a', 'b'], prop: 'id01'},
* {label: ['a', 'c'], prop: 'id02'},
* {label: ['b', 'c'], prop: 'id03'},
* ]
*
* 新格式:
*
* {
* 'a': [{label: 'b', prop: 'id01'}, {label: 'c', prop: 'id02'}],
* 'b': [{label: 'c', prop: 'id03'}],
* }
*
*/
for (let column of parent['columns']) {
// '主键' 或 ['主键'] -> ['基本信息', '主键']
if (typeof column['label'] === 'string' || column['label'].length === 1) {
column['label'] = ['基本信息', column['label']]
}
// 获取一级标题
let parentName = column['label'][0];
column['label'] = column['label'][1];
// 若 columnsObj 中已经存在该一级标题对应的数组,则取出该数组,并加入该一级标题
if (columnsObj.hasOwnProperty(parentName)) {
columnsObj[parentName].push(column);
}
// 若 columnsObj 中不经存在该一级标题对应的数组,则创建新数组,再加入该一级标题
else {
columnsObj[parentName] = [column];
}
}
/* ==================== 处理表格数据显示 ==================== */
/** 处理表格数据显示: row['user.username'] -> row['user']['username'] */
function toMustache(row, prop) {
let propArray = prop.split('.');
for (let i = 0, j = propArray.length; i < j; i++) {
// i=0: row = row['user']
// i=1: row = row['user']['username']
if (row) row = row[propArray[i]];
}
return row;
}
/* ==================== 查看card内容详情 ==================== */
/**
* 查看card内容详情
*
* @param row 当前行数据
* @param prop 属性
*/
function cardDetail(row, prop) {
ElMessageBox.alert(row[prop], '记录详情', {
confirmButtonText: '了解',
callback: () => {
},
});
}
/* ==================== 查看详情 ==================== */
// 菜单详情抽屉 + 菜单详情列表数据
let detailDrawer = ref(), detailItems = ref([]);
/** 查看记录详情 */
function detail(record) {
// 每次清空,防止追加
detailItems.value = [];
// 填充表格数据到详情列表中
for (let i in parent['columns']) {
let item = parent['columns'][i];
item['value'] = toMustache(record, item['prop']);
detailItems.value.push(item);
}
// 填充 创建时间 和 修改时间 信息
detailItems.value.push({label: '创建时间', value: record['created'], format: datetimeFormat});
detailItems.value.push({label: '修改时间', value: record['updated'], format: datetimeFormat});
// 打开抽屉
detailDrawer.value = true;
}
/* ==================== 单条删除 ==================== */
/**
* 调用 myDelete() 工具删除该行数据
*
* @param row 当前行数据
*/
function remove(row) {
myDelete({
id: row['id'],
api: parent['deleteApi'],
args: {module: parent['module']},
callback: parent['deleteCallback']
});
}
/* ==================== 批量删除 ==================== */
// 批量删除主键数组
let ids = [];
// 当表格首列的多选框被选中或取消选中时触发
function selectionChange(rows) {
// 记录当前被选中的行
ids = rows.map(e => e['id']);
}
/** 调用 myDeleteBatch() 工具批量删除数据*/
function removeBatch() {
myDeleteBatch({
ids: ids,
api: parent['deleteBatchApi'],
args: {module: parent['module']},
callback: parent['deleteCallback']
});
}
/* ==================== 单条添加 ==================== */
/** 路由到指定的添加页面 */
function insert() {
if (isNotNull(parent['insertPage'])) {
if (isNotNull(parent['insertPageParam'])) {
sessionStorage.setItem('insertPageParam', JSON.stringify(parent['insertPageParam']));
}
router.push(parent['insertPage']);
} else {
ElMessageBox.alert('功能还未开发,敬请期待!', '提示', {type: 'warning'});
}
}
/* ==================== 单条修改 ==================== */
/**
* 存储当前行数据,并路由到指定的修改页面。
*
* @param row 当前行数据
*/
function update(row) {
if (isNotNull(parent['updatePage'])) {
if (isNotNull(parent['updatePageParam'])) {
sessionStorage.setItem('updatePageParam', JSON.stringify(parent['updatePageParam']));
}
sessionStorage.setItem('row', JSON.stringify(row));
router.push(parent['updatePage']);
} else {
ElMessageBox.alert('功能还未开发,敬请期待!', '提示', {type: 'warning'});
}
}
/* ==================== 打印报表 ==================== */
/** 调用对应API,打印报表 */
function excel() {
if (isNotNull(parent['excelApi'])) {
parent['excelApi']();
} else {
ElMessageBox.alert('功能还未开发,敬请期待!', '提示', {type: 'warning'});
}
}
/* ==================== 处理表格属性 ==================== */
/** 是否使用 tooltip 属性 */
function useTooltip(item) {
// 配置优先
if (isNotNull(item['tooltip'])) return item['tooltip'];
// 评分,图片,图标和卡片都不使用 tooltip 属性,其余情况使用 tooltip 属性
return item['type'] !== 'star' &&
item['type'] !== 'img' &&
item['type'] !== 'icon' &&
item['type'] !== 'card';
}
/** 是否使用 sortable 属性 */
function useSortable(item) {
// 配置优先
if (isNotNull(item['sortable'])) return item['sortable'];
// 评分,图片,图标和卡片都不使用 sortable 属性,其余情况使用 sortable 属性
return item['type'] !== 'star' &&
item['type'] !== 'img' &&
item['type'] !== 'icon' &&
item['card'] !== 'icon';
}
/** 是否使用 width 属性 */
function useWidth(item) {
// 配置优先
if (isNotNull(item['width'])) return item['width'];
// 卡片默认宽度 300px
if (item['type'] === 'card') return 300;
// 标签默认宽度 110px
if (item['type'] === 'tag') return 110;
// 图片默认宽度 80px
if (item['type'] === 'img') return 80;
// 其他默认宽度 200px
return 200;
}
</script>
<template>
<el-table class="my-table"
tooltip-effect="light"
size="small"
stripe
highlight-current-row
:data="records"
@selection-change="selectionChange">
<el-table-column class="head-col"
label="序号"
align="center"
fixed="left">
<el-table-column type="selection" width="40"/>
<el-table-column type="index" width="40"/>
</el-table-column>
<el-table-column class="body-col"
align="center"
v-for="(childrenArray, parentLabel) in columnsObj"
:key="childrenArray"
:label="parentLabel">
<el-table-column class="body-col-inner"
v-for="item in childrenArray"
:key="item"
:label="item['label']"
:property="item['prop']"
:sortable="useSortable(item)"
:show-overflow-tooltip="useTooltip(item)"
:width="useWidth(item)">
<template #default="scope">
<el-tag v-if="item['type'] === 'tag' && isNotNull(toMustache(scope.row, item['prop']))"
:type="isNotNull(item['tagTypeFn']) ? item['tagTypeFn'](scope.row[item['prop']]) :
isNotNull(item['tagType']) ? item['tagType'] : 'warning'">
<span>{{ item['prefix'] }}</span>
<span v-if="isNotNull(item['format'])">{{ item['format'](toMustache(scope.row, item['prop'])) }}</span>
<span v-else>{{ toMustache(scope.row, item['prop']) }}</span>
<span>{{ item['suffix'] }}</span>
</el-tag>
<el-rate v-else-if="item['type'] === 'star' && isNotNull(scope.row[item['prop']])"
disabled show-score
v-model="scope.row[item['prop']]"
text-color="#ff9900"
score-template="{value}"/>
<el-card v-else-if="item['type'] === 'card' && isNotNull(toMustache(scope.row, item['prop']))"
@click="cardDetail(scope.row, item['prop'])">
<span>{{ item['prefix'] }}</span>
<span v-if="isNotNull(item['format'])">{{ item['format'](toMustache(scope.row, item['prop'])) }}</span>
<span v-else>{{ toMustache(scope.row, item['prop']) }}</span>
<span>{{ item['suffix'] }}</span>
</el-card>
<el-image v-else-if="item['type'] === 'img' && isNotNull(toMustache(scope.row, item['prop']))"
fit="fill"
preview-teleported
:src="item['minio'](toMustache(scope.row, item['prop']))"
:preview-src-list="[item['minio'](toMustache(scope.row, item['prop']))]"
:style="{width: item['imgWidth'], height: item['imgHeight']}"/>
<el-button v-else-if="item['type'] === 'icon' && isNotNull(toMustache(scope.row, item['prop']))"
plain
:type="isNotNull(item['iconType']) ? item['iconType'] : 'primary'"
:size="isNotNull(item['iconSize']) ? item['iconSize'] : 'small'"
:icon="toMustache(scope.row, item['prop'])"/>
<div class="text" v-else>
<span>{{ item['prefix'] }}</span>
<span v-if="isNotNull(item['format'])">{{ item['format'](toMustache(scope.row, item['prop'])) }}</span>
<span v-else>{{ toMustache(scope.row, item['prop']) }}</span>
<span>{{ item['suffix'] }}</span>
</div>
</template>
</el-table-column>
</el-table-column>
<el-table-column class="time-col"
label="时间信息"
align="center">
<el-table-column class="created-col"
label="记录首次创建时间"
property="created"
sortable
:width="150"
:formatter="row => datetimeFormat(row['created'])"/>
<el-table-column class="updated-col"
label="记录最后修改时间"
property="updated"
sortable
:width="150"
:formatter="row => datetimeFormat(row['updated'])"/>
</el-table-column>
<el-table-column class="foot-col"
align="center"
fixed="right"
:width="170">
<template #header>
<el-button class="insert-btn"
plain
size="small"
icon="CirclePlusFilled"
type="primary"
title="单增一条记录"
@click="insert"/>
<el-button class="delete-batch-btn"
plain
size="small"
icon="DeleteFilled"
type="danger"
title="批量删除记录"
@click="removeBatch"/>
<el-button class="excel-btn"
plain
size="small"
icon="Grid"
type="warning"
title="打印数据报表"
@click="excel"/>
</template>
<template #default="scope">
<el-row :gutter="0">
<el-col class="info-btn-col" :span="12">
<el-button class="info-btn"
link
size="small"
type="info"
icon="InfoFilled"
@click="detail(scope.row)">
查看详情
</el-button>
</el-col>
<el-col class="delete-btn-col" :span="12">
<el-button class="delete-btn"
link
size="small"
type="danger"
icon="Delete"
@click="remove(scope.row)">
删除记录
</el-button>
</el-col>
<el-col class="update-btn-col" :span="12">
<el-button class="update-btn"
link
size="small"
type="warning"
icon="Edit"
@click="update(scope.row)">
修改记录
</el-button>
</el-col>
<el-col class="custom-btn-col"
:span="12"
v-for="button in buttons"
:key="button">
<el-button class="custom-btn"
link
size="small"
:type="isNotNull(button['type']) ? button['type'] : 'success'"
:icon="isNotNull(button['icon']) ? button['icon'] : 'InfoFilled'"
@click="button['callback'](scope.row)">
{{ isNotNull(button['label']) ? button['label'] : '普通按钮' }}
</el-button>
</el-col>
</el-row>
</template>
</el-table-column>
<!--用于调控表格宽度,不设置宽度,则宽度自撑开到100%-->
<el-table-column/>
</el-table>
<el-drawer class="detail-drawer"
v-model="detailDrawer"
title="记录详情"
size="30%">
<el-descriptions class="my-list"
border
label-align="center"
:column="1">
<el-descriptions-item class="list-item"
v-for="item in detailItems"
:key="item"
:label="item['label']"
:span="isNotNull(item['colspan']) ? item['colspan'] : 1">
<template #default>
<el-tag v-if="item['type'] === 'tag' && isNotNull(item['value'])"
:type="isNotNull(item['tagTypeFn']) ? item['tagTypeFn'](item['value']) :
isNotNull(item['tagType']) ? item['tagType'] : 'warning'">
<span>{{ item['prefix'] }}</span>
<span v-if="isNotNull(item['format'])">{{ item['format'](item['value']) }}</span>
<span v-else>{{ item['value'] }}</span>
<span>{{ item['suffix'] }}</span>
</el-tag>
<el-rate v-else-if="item['type'] === 'star' && isNotNull(item['value'])"
disabled
show-score
v-model="item['value']"
text-color="#ff9900"
score-template="{value}"/>
<el-card v-else-if="item['type'] === 'card' && isNotNull(item['value'])"
:style="{height: item['height'] + 'px'}">
<span>{{ item['prefix'] }}</span>
<span v-if="isNotNull(item['format'])">{{ item['format'](item['value']) }}</span>
<span v-else>{{ item['value'] }}</span>
<span>{{ item['suffix'] }}</span>
</el-card>
<el-image v-else-if="item['type'] === 'img' && isNotNull(item['value'])"
fit="fill"
preview-teleported
:src="item['minio'](item['value'])"
:preview-src-list="[item['minio'](item['value'])]"
:style="{width: item['imgWidth'] + 'px', height: item['imgHeight'] + 'px'}"/>
<el-button v-else-if="item['type'] === 'icon' && isNotNull(item['value'])"
plain
:type="isNotNull(item['iconType']) ? item['iconType'] : 'primary'"
:size="isNotNull(item['iconSize']) ? item['iconSize'] : 'small'"
:icon="item['value']">
</el-button>
<div class="text" v-else>
<span>{{ item['prefix'] }}</span>
<span v-if="isNotNull(item['format'])">{{ item['format'](item['value']) }}</span>
<span v-else>{{ item['value'] }}</span>
<span>{{ item['suffix'] }}</span>
</div>
</template>
</el-descriptions-item>
</el-descriptions>
</el-drawer>
<el-pagination class="my-pager"
layout="total, sizes, prev, pager, next, jumper"
:background="true"
:page-sizes="pageSizes"
:current-page="isNotNull(pageInfo['pageNum']) ? pageInfo['pageNum'] : 1"
:page-size="isNotNull(pageInfo['pageSize']) ? pageInfo['pageSize'] : 5"
:total="pageInfo['total']"
@size-change="pageSize => pageInfo['callback'](pageInfo['pageNum'], pageSize)"
@current-change="pageNum => pageInfo['callback'](pageNum, pageInfo['pageSize'])"/>
</template>
<style scoped lang="scss">
.my-table {
height: 480px; // 高度
.el-image {
width: 100%; // 宽度
height: 50px; // 高度
margin-bottom: -10px; // 下边距
}
.el-card {
height: 62px; // 高度(大概2行文字)
--el-card-padding: 10px !important; // 内边距
white-space: normal !important; // 自动换行
overflow-y: scroll !important; // 垂直溢出滚动
}
}
.foot-col-inner {
text-align: left; // 居左
}
:deep(.el-descriptions__label.el-descriptions__cell.is-bordered-label) {
min-width: 107px !important; // 最小列宽107
max-width: 137px !important; // 最大列宽137
}
.my-list {
.el-image {
width: 100px; // 宽度
height: 100px; // 高度
}
.el-card {
max-height: 420px; // 最大高度(不超过15行)
--el-card-padding: 10px !important; // 内边距
white-space: normal !important; // 自动换行
overflow-y: scroll !important; // 垂直溢出滚动
}
}
.my-pager {
margin: 20px; // 外边距
justify-content: center; // 自居中
}
</style>
- 测试数据表格通用组件:
views/my/MyTableTest.vue:
<script setup>
import MyTable from "../../components/MyTable.vue";
import {useDark} from "@vueuse/core/index";
import {reactive} from "vue";
let records = [
{
f01: '刘能', f02: 0, f03: 3.5, f04: '亚洲舞王', f05: '刘能.jpg', f06: 'Apple',
created: new Date(), updated: new Date(999999)
},
{
f01: '赵四', f02: 1, f03: 3, f04: '玉田老丈人', f05: '赵四.jpg', f06: 'House',
created: new Date(), updated: new Date(999999)
},
];
let columns = [
{label: '文本', prop: 'f01', width: 90, sortable: true, tooltip: true},
{label: '评分', prop: 'f03', type: 'star', width: 160, sortable: true, tooltip: false},
{
label: '卡片', prop: 'f04', type: 'card', prefix: '->', suffix: '<-', width: 160,
format: v => '~' + v + '~'},
{
label: '图片', prop: 'f05', type: 'img', imgWidth: 120, imgHeight: 120,
minio: () => 'http://192.168.40.77:9001/mylesson/avatar/5641df5204-1736390233362.jpg',
},
{label: '图标', prop: 'f06', type: 'icon', width: 70, iconType: 'danger', iconSize: 'large'},
{
label: ['标签', '静态标签'], prop: 'f02', type: 'tag', prefix: '->', suffix: '<-', tagType: 'success', width: 90,
format: v => v === 0 ? '零' : '非零',
},
{
label: ['标签', '动态标签'], prop: 'f02', type: 'tag', prefix: '->', suffix: '<-', width: 90
tagTypeFn: v => v === 0 ? 'success' : 'primary',
format: v => v === 0 ? '零' : '非零',
}
];
let buttons = [
{type: 'upload', label: '上传图片', callback: () => console.log('上传图片')},
]
let pageInfo = reactive({
pageNum: 1,
pageSize: 5,
total: 50,
callback: (page, size) => {
pageInfo['pageNum'] = page;
pageInfo['pageSize'] = size;
}
});
useDark();
</script>
<template>
<MyTable module="user"
:records="records"
:columns="columns"
:buttons="buttons"
:insert-page="'user/UserInsert'"
:insert-page-param="{a: 1}"
:update-page="'user/UserUpdate'"
:update-page-param="{b: 1}"
:delete-api="()=>console.log('使用axios发送删除请求')"
:delete-batch-api="()=>console.log('使用axios发送批量删除请求')"
:delete-callback="()=>console.log('删除成功后的回调')"
:excel-api="()=>console.log('使用axios发送导出Excel请求')"
:pageInfo="pageInfo"
:page-sizes="[1,5,10,12]"/>
</template>
5. 封装组件 - 数据表单
心法:该组件依赖 /util/index.js 文件和 /request/index.js 文件。

- 封装表单组件:
components/MyForm.vue:
<script setup>
import {shallowRef} from "vue";
import {myInsert, myUpdate} from "../request";
import {isNotNull} from "../util";
let parent = defineProps({
// 表单类型:支持 insert 和 update 两种
type: {type: String, required: false, default: 'insert'},
/* 表单项
*
* label: 文案,默认表单项属性名,全类型通用
* prop: 属性名,全类型通用
* required: 是否必填,默认 false,全类型通用
* span: 每个表单项的宽度占 24 分之多少,默认 24,表示独占一行,全类型通用
* hidden: 是否隐藏,默认 false,需要占位时改为 true,全类型通用
* placeholder: 背景字,默认 "请输入..",icon 和 cascader 类型除外
* disabled: 是否禁用,默认 false,icon 和 cascader 类型除外
* readonly: 是否只读,默认 false,icon 和 cascader 类型除外
* type: 表单项类型,支持 number, password, textarea, select, datetime, date, icon, cascader, text 9种,默认 text:
* type = number: 数字框
* min: 最小值,默认 0
* max: 最大值
* precision: 精度,默认 0
* type = textarea: 文本域
* rows: 行数,默认 8
* cols: 列数
* type = select: 下拉框
* options: 下拉菜单项,格式 [{ label: '', value: '' }]
* multiple: 是否支持多选,默认 false
* type = icon: ICON单选框
* iconSize: 按钮大小,默认 14
* type = cascader: 级联框
* options: 级联菜单项,格式 [{ label: '', value: '', children: [] }]
* cascaderChange: 当级联菜单发生改变时触发的函数(不带小括号)
*/
items: {type: Array, required: true},
// 表单规则
rules: {type: Object, required: true},
// API函数:表单提交时调用的API函数(不带小括号)
api: {type: Function, required: true},
// API参数:API请求函数参数,作为请求参数
params: {type: Object, required: true},
// API参数:API请求函数参数,不作为请求参数,而是有一些其它作用
args: {type: Object, required: false, default: null},
// 回调函数:表单提交后调用的函数(不带小括号)
callback: {type: Function, required: false},
// 表单宽度
width: {type: String, required: false, default: '100%'},
});
// 表单对象
let form = shallowRef();
// 添加数据: 发送请求 + 重置表单
async function insert() {
await myInsert({
form: form,
api: parent['api'],
params: parent['params'],
args: parent['args'],
callback: parent['callback'],
});
form.value.resetFields;
}
// 修改数据: 发送请求 + 重置表单
async function update() {
await myUpdate({
form: form,
api: parent['api'],
params: parent['params'],
args: parent['args'],
callback: parent['callback'],
});
form.value.resetFields;
}
const ICONS = [
'Plus', 'Minus', 'CirclePlus', 'Search', 'Female', 'Male', 'Aim', 'House', 'FullScreen', 'Loading',
'Link', 'Service', 'Pointer', 'Star', 'Notification', 'Connection', 'ChatDotRound', 'Setting',
'Clock', 'Position', 'Discount', 'Odometer', 'ChatSquare', 'ChatRound', 'ChatLineRound', 'ChatLineSquare',
'ChatDotSquare', 'View', 'Hide', 'Unlock', 'Lock', 'RefreshRight', 'RefreshLeft', 'Refresh', 'Bell',
'MuteNotification', 'User', 'Check', 'CircleCheck', 'Warning', 'CircleClose', 'Close', 'PieChart', 'More',
'Compass', 'Filter', 'Switch', 'Select', 'SemiSelect', 'CloseBold', 'EditPen', 'Edit', 'Message',
'MessageBox', 'TurnOff', 'Finished', 'Delete', 'Crop', 'SwitchButton', 'Operation', 'Open', 'Remove',
'ZoomOut', 'ZoomIn', 'InfoFilled', 'CircleCheckFilled', 'SuccessFilled', 'WarningFilled', 'CircleCloseFilled',
'QuestionFilled', 'WarnTriangleFilled', 'UserFilled', 'MoreFilled', 'Tools', 'HomeFilled', 'Menu',
'UploadFilled', 'Avatar', 'HelpFilled', 'Share', 'StarFilled', 'Comment', 'Histogram', 'Grid', 'Promotion',
'DeleteFilled', 'RemoveFilled', 'CirclePlusFilled', 'ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown',
'ArrowLeftBold', 'ArrowUpBold', 'ArrowRightBold', 'ArrowDownBold', 'DArrowRight', 'DArrowLeft', 'Download',
'Upload', 'Top', 'Bottom', 'Back', 'Right', 'TopRight', 'TopLeft', 'BottomRight', 'BottomLeft', 'Sort',
'SortUp', 'SortDown', 'Rank', 'CaretLeft', 'CaretTop', 'CaretRight', 'CaretBottom', 'DCaret', 'Expand',
'Fold', 'DocumentAdd', 'Document', 'Notebook', 'Tickets', 'Memo', 'Collection', 'Postcard', 'ScaleToOriginal',
'SetUp', 'DocumentDelete', 'DocumentChecked', 'DataBoard', 'DataAnalysis', 'CopyDocument', 'FolderChecked',
'Files', 'Folder', 'FolderDelete', 'FolderRemove', 'FolderOpened', 'DocumentCopy', 'DocumentRemove',
'FolderAdd', 'FirstAidKit', 'Reading', 'DataLine', 'Management', 'Checked', 'Ticket', 'Failed', 'TrendCharts',
'List', 'Microphone', 'Mute', 'Mic', 'VideoPause', 'VideoCamera', 'VideoPlay', 'Headset', 'Monitor', 'Film',
'Camera', 'Picture', 'PictureRounded', 'Iphone', 'Cellphone', 'VideoCameraFilled', 'PictureFilled',
'Platform', 'CameraFilled', 'BellFilled', 'Location', 'LocationInformation', 'DeleteLocation', 'Coordinate',
'Bicycle', 'OfficeBuilding', 'School', 'Guide', 'AddLocation', 'MapLocation', 'Place', 'LocationFilled',
'Van', 'Watermelon', 'Pear', 'NoSmoking', 'Smoking', 'Mug', 'GobletSquareFull', 'GobletFull', 'KnifeFork',
'Sugar', 'Bowl', 'MilkTea', 'Lollipop', 'Coffee', 'Chicken', 'Dish', 'IceTea', 'ColdDrink', 'CoffeeCup',
'DishDot', 'IceDrink', 'IceCream', 'Dessert', 'IceCreamSquare', 'ForkSpoon', 'IceCreamRound', 'Food',
'HotWater', 'Grape', 'Fries', 'Apple', 'Burger', 'Goblet', 'GobletSquare', 'Orange', 'Cherry', 'Printer',
'Calendar', 'CreditCard', 'Box', 'Money', 'Refrigerator', 'Cpu', 'Football', 'Brush', 'Suitcase',
'SuitcaseLine', 'Umbrella', 'AlarmClock', 'Medal', 'GoldMedal', 'Present', 'Mouse', 'Watch', 'QuartzWatch',
'Magnet', 'Help', 'Soccer', 'ToiletPaper', 'ReadingLamp', 'Paperclip', 'MagicStick', 'Basketball',
'Baseball', 'Coin', 'Goods', 'Sell', 'SoldOut', 'Key', 'ShoppingCart', 'ShoppingCartFull', 'ShoppingTrolley',
'Phone', 'Scissor', 'Handbag', 'ShoppingBag', 'Trophy', 'TrophyBase', 'Stopwatch', 'Timer', 'CollectionTag',
'TakeawayBox', 'PriceTag', 'Wallet', 'Opportunity', 'PhoneFilled', 'WalletFilled', 'GoodsFilled', 'Flag',
'BrushFilled', 'Briefcase', 'Stamp', 'Sunrise', 'Sunny', 'Ship', 'MostlyCloudy', 'PartlyCloudy', 'Sunset',
'Drizzling', 'Pouring', 'Cloudy', 'Moon', 'MoonNight', 'Lightning', 'ChromeFilled', 'Eleme', 'ElemeFilled',
'ElementPlus', 'Shop', 'SwitchFilled', 'WindPower'
];
</script>
<template>
<el-form class="my-form"
ref="form"
status-icon
label-width="auto"
size="small"
:model="params"
:rules="rules"
:style="{'width': width}">
<el-row :gutter="20">
<el-col class="layout-col"
v-for="item in items"
:key="item"
:span="isNotNull(item['span']) ? item['span'] : 24">
<el-form-item class="form-item"
:label="isNotNull(item['label']) ? item['label'] : item['prop']"
:prop="item['prop']"
:required="item['required']"
v-show="!item['hidden']">
<!--数字框-->
<el-input-number v-if="item['type'] === 'number'"
clearable
v-model="params[item['prop']]"
:min="isNotNull(item['min']) ? item['min'] : 0"
:max="item['max']"
:precision="isNotNull(item['precision']) ? item['precision'] : 0"
:placeholder="isNotNull(item['placeholder']) ? item['placeholder'] : '请输入..'"
:disabled="item['disabled']"
:readonly="item['readonly']"/>
<!--密码框-->
<el-input v-else-if="item['type'] === 'password'"
clearable
show-password
v-model="params[item['prop']]"
:placeholder="isNotNull(item['placeholder']) ? item['placeholder'] : '请输入..'"
:disabled="item['disabled']"
:readonly="item['readonly']"/>
<!--文本域-->
<el-input v-else-if="item['type'] === 'textarea'"
type="textarea"
clearable
resize="none"
v-model="params[item['prop']]"
:rows="isNotNull(item['rows']) ? item['rows'] : 8"
:cols="item['cols']"
:placeholder="isNotNull(item['placeholder']) ? item['placeholder'] : '请输入..'"
:disabled="item['disabled']"
:readonly="item['readonly']"/>
<!--下拉框-->
<el-select v-else-if="item['type'] === 'select'"
clearable
filterable
:multiple="item['multiple']"
v-model="params[item['prop']]"
:placeholder="isNotNull(item['placeholder']) ? item['placeholder'] : '请输入..'"
:disabled="item['disabled']"
:readonly="item['readonly']">
<el-option class="select-item"
v-for="option in item['options']"
:key="option"
:label="option['label']"
:value="option['value']"/>
</el-select>
<!--时间框-->
<el-date-picker v-else-if="item['type'] === 'datetime'"
type="datetime"
clearable
v-model="params[item['prop']]"
format="YYYY-MM-DD HH:mm"
value-format="YYYY-MM-DDTHH:mm:ss"
:placeholder="isNotNull(item['placeholder']) ? item['placeholder'] : '请输入..'"
:disabled="item['disabled']"
:readonly="item['readonly']"/>
<!--日期框-->
<el-date-picker v-else-if="item['type'] === 'date'"
type="date"
clearable
v-model="params[item['prop']]"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
:placeholder="isNotNull(item['placeholder']) ? item['placeholder'] : '请输入..'"
:disabled="item['disabled']"
:readonly="item['readonly']"/>
<!--ICON单选框-->
<el-radio-group v-else-if="item['type'] === 'icon'"
class="icon-list"
size="small"
v-model="params[item['prop']]">
<el-radio-button class="icon-item"
v-for="icon in ICONS"
:key="icon"
:label="icon">
<template #default>
<el-icon :size="isNotNull(item['iconSize']) ? item['iconSize'] : 14">
<component :is="icon"/>
</el-icon>
</template>
</el-radio-button>
</el-radio-group>
<!--级联框-->
<el-cascader v-else-if="item['type'] === 'cascader'"
v-model="params[item['prop']]"
:options="item['options']"
:props="{expandTrigger: 'hover'}"
@change="params[item['cascaderChange']]"
style="width: 100%;"/>
<!--文本框-->
<el-input v-else
clearable
v-model="params[item['prop']]"
:placeholder="isNotNull(item['placeholder']) ? item['placeholder'] : '请输入..'"
:disabled="item['disabled']"
:readonly="item['readonly']"/>
</el-form-item>
</el-col>
</el-row>
<el-form-item class="btn-item">
<el-button class="insert-btn"
v-if="type === 'insert'"
type="primary"
@click="insert">
确认添加
</el-button>
<el-button class="update-btn"
v-else-if="type === 'update'"
type="primary"
@click="update">
确认修改
</el-button>
</el-form-item>
</el-form>
</template>
<style scoped lang="scss">
.my-form {
.el-input-number, .el-button {
width: 100%; // 宽度
}
}
:deep(.el-date-editor) {
--el-date-editor-width: 100%; // 宽度
}
.icon-list {
margin-left: -2px; // 左外边距
max-height: 160px; // 最大高度
overflow-y: scroll; // Y滚动
.el-radio-button {
margin-bottom: 5px; // 下外边距
}
}
/* 取消首尾按钮的圆角 */
:deep(.el-radio-button:first-child .el-radio-button__inner),
:deep(.el-radio-button:last-child .el-radio-button__inner) {
border-radius: 0; // 圆角
}
/* 修改单选按钮样式 */
:deep(.el-radio-button--small .el-radio-button__inner) {
border-left: 1px solid #7e7e7e; // 左边框
margin: 0 2px; // 左右外边距
}
</style>
- 测试表单组件:
views/my/MyFormTest.vue:
<script setup>
import MyForm from '../../components/MyForm.vue';
import {useDark} from "@vueuse/core/index";
import {ref, shallowReactive} from "vue";
let insertFormItems = ref([
{
label: '数字', prop: 'f01', type: 'number', min: 0, max: 100, precision: 2, span: 12, hidden: false,
placeholder: 'f01', required: true, disabled: false, readonly: false
},
{
label: '文本', prop: 'f02', type: 'text', span: 12, hidden: false,
placeholder: 'f02', required: true, disabled: false, readonly: false
},
{
label: '文本域', prop: 'f03', type: 'textarea', rows: 8, cols: 10, span: 24, hidden: false,
placeholder: 'f03', required: true, disabled: false, readonly: false
},
{
label: '下拉菜单', prop: 'f04', type: 'select', span: 12, hidden: false,
placeholder: 'f04', required: true, disabled: false, readonly: false,
options: [
{label: '男', value: '男'},
{label: '女', value: '女'}
]
},
{
label: '密码框', prop: 'f05', type: 'password', span: 12, hidden: false,
placeholder: 'f05', required: true, disabled: false, readonly: false
}
]);
let insertParams = shallowReactive({f01: 0, f02: '', f03: '', f04: '', f05: ''});
let updateFormItems = ref([
{
label: '日期时间', prop: 'f06', type: 'datetime', span: 12, hidden: false,
placeholder: 'f06', required: true, disabled: false, readonly: false
},
{
label: '日期', prop: 'f07', type: 'date', span: 12, hidden: false,
placeholder: 'f07', required: true, disabled: false, readonly: false
},
{
label: '图标', prop: 'f08', type: 'icon', iconSize: 10, span: 24, hidden: false,
placeholder: 'f08', required: true, disabled: false, readonly: false
},
{
label: '级联', prop: 'f09', type: 'cascader', span: 24, hidden: false,
options: [
{
label: '性别', value: 'gender', children: [
{label: '男', value: 'male'},
{label: '女', value: 'female'}
]
},
{
label: '年龄', value: 'age', children: [
{label: '老年人', value: 'old'},
{label: '年轻人', value: 'young'}
]
}
],
placeholder: 'f09', required: true, disabled: false, readonly: false
}
]);
let updateParams = shallowReactive({f06: '', f07: '', f08: '', f09: []});
useDark();
</script>
<template>
<el-row style="padding: 50px;" :gutter="50">
<el-col :span="12">
<MyForm type="insert"
:items="insertFormItems"
:params="insertParams"
:rules="{'f01': ''}"
:api="() => 'axios.post(insert_xxx)'"
:callback="() => '添加完成回调'"/>
</el-col>
<el-col :span="12">
<MyForm type="update"
:items="updateFormItems"
:params="updateParams"
:rules="{'f06': ''}"
:api="() => 'axios.post(update_xxx)'"
:callback="() => '修改完成回调'"/>
</el-col>
</el-row>
</template>
6. 封装组件 - 上传文件
心法:该组件依赖 ElementPlus 框架,/util/index.js 文件和 /request/index.js 文件。

- 开发上传文件通用组件:
components/MyUpload.vue:
<script setup>
import {shallowRef} from "vue";
import {ElMessage} from "element-plus";
import {getResponseData} from "../request";
import {hasNull, isNotNull} from "../util";
// 上传控件对象
let uploader = shallowRef();
// 当前token值
let currentToken = sessionStorage.getItem('token');
let parent = defineProps({
// 控件名称:对应后台API接口中的文件参数名,如 avatarFile 等
name: {type: String, required: true},
// 上传地址:对应后台API接口地址
url: {type: String, required: true},
// 回调函数:上传成功后的回调函数
callback: {type: Function, required: false},
// 单次文件上传的最大数量限制,默认 1
limit: {type: Number, required: false, default: 1},
// 是否自动上传,默认 false
autoUpload: {type: Boolean, required: false, default: false},
// 上传提交按钮主题颜色,默认 "warning"
btnType: {type: String, required: false, default: 'warning'},
// 支持上传的MIME格式数组 默认 ['image/jpeg', 'image/png']
allowTypes: {type: Array, required: false, default: ['image/jpeg', 'image/png']},
// 支持上传的文件最大限制,单位MB,默认10M
maxSize: {type: Number, required: false, default: 10},
});
/**
* 文件上传前的校验过程
*
* @param file 文件对象,必传
* @return true 校验成功,false 校验失败
*/
function beforeUpload(file) {
let allowTypes = parent['allowTypes'];
let maxSize = parent['maxSize'];
// 空值保护
if(hasNull(file, allowTypes)) return false;
// 校验文件MIME类型
if (!allowTypes.includes(file.type)) {
ElMessage.warning(`仅支持 ${allowTypes.toString()} 格式!`);
return false;
}
// 校验文件大小
if (file.size / 1024 / 1024 > maxSize) {
ElMessage.error(`文件大小不能超过 ${maxSize} MB`)
return false;
}
return true;
}
/**
* 上传文件成功后执行的函数
*
* @param res 响应对象
*/
async function onSuccess(res) {
let data = getResponseData(res);
if (isNotNull(data)) {
// 清空上传控件的文件列表
uploader.value.clearFiles();
// 存在回调函数时,异步调用回调函数,并将响应数据作为参数传入
if (parent['callback']) await parent['callback'](data);
}
}
</script>
<template>
<el-upload class="my-uploader"
ref="uploader"
list-type="picture"
:name="name"
:action="url"
:headers="{'token': currentToken}"
:drag="true"
:limit="limit"
:show-file-list="false"
:auto-upload="autoUpload"
:before-upload="beforeUpload"
:on-success="onSuccess">
<el-link class="el-icon--upload"
icon="UploadFilled"
:underline="false"/>
<div class="el-upload__text">拖拽文件 或 <em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip">
仅支持 {{ allowTypes.toString().replaceAll('image/', '').replaceAll(',', ', ') }}
格式文件,且单个文件不超过{{ maxSize }}MB
</div>
</template>
</el-upload>
<!--上传按钮-->
<el-button class="upload-btn"
v-if="!autoUpload"
:type="btnType"
@click="uploader.submit()">
确认上传
</el-button>
</template>
<style scoped lang="scss">
.upload-btn {
margin-top: 10px; // 上外边距
width: 100%; // 宽度
}
</style>
- 测试上传文件通用组件:
views/my/MyUploadTestr.vue:
<script setup>
import MyUpload from "../../components/MyUpload.vue";
import {useDark} from "@vueuse/core";
useDark();
</script>
<template>
<MyUpload name="avatarFile"
url="http://localhost:5266/api/v1/user/avatar/upload/1"
:callback="() => console.log('上传成功')"/>
</template>
Java道经第3卷 - 第7阶 - Vue(二)
传送门:JB3-7-Vue(一)
传送门:JB3-7-Vue(二)
&spm=1001.2101.3001.5002&articleId=148792472&d=1&t=3&u=674824a7434e4dcf9da9d7e8bd3d68d8)
419

被折叠的 条评论
为什么被折叠?



