Vue生态 UI 框架
重要通知
提供基于Vue生态体系可以参考借鉴的UI框架最佳实践指引与核心组件功能的自定义实现。
element-plus
Element是一套为开发者、设计师和产品经理准备的基于Vue的桌面端组件库,别称为网站快速成型工具。
- 官网:https://element-plus.org/zh-CN/
- GitHub:https://github.com/element-plus/element-plus
- TS接口声明: https://github.com/element-plus/element-plus/blob/dev/typings/global.d.ts
安装配置
按需引入-手动导入
Element Plus 提供了基于 ES Module 开箱即用的 Tree Shaking 功能。
- 安装配置
> npm i unplugin-element-plus -D
- vite.config.ts
import { defineConfig } from 'vite';
import ElementPlus from 'unplugin-element-plus/vite';
plugins: [ElementPlus()]
- 业务组件
<template>
<el-button>I am ElButton</el-button>
</template>
<script>
import { ElButton } from 'element-plus';
export default {
components: { ElButton },
}
</script>
- Vue3语法示例
<template>
<el-button>Default</el-button>
</template>
<script lang="ts" setup>
import { Button } from '@element-plus/icons-vue';
</script>
ICON图标系统
<template>
<section>
<el-icon><Document /></el-icon>
</section>
</template>
<script lang="ts" setup>
import { Document, Icon } from '@element-plus/icons-vue';
</script>
- 模块导入列表
import {
ElButton
} from 'element-plus';
el-popover
<el-popover ref="elPopoverRef"></el-popover>
<script lang="ts" setup>
// 手动关闭
elPopoverRef.value?.hide()
</script>
el-cascader 级联选择器
nextTick(() => {
const $el = document.querySelectorAll('.el-cascader-panel .el-cascader-node[aria-owns]')
Array.from($el).map((item) => item.removeAttribute('aria-owns'))
})
<el-table>
表格
多选功能
单选功能
<script setup lang="ts">
const initList: Record<string, any>[] = [
// 多选
{ type: "selection" },
// 单选,具体参考List/fitness_admin_fe/src/widgets/recommend/components/select-action/config.ts
// List/fitness_admin_fe/src/components/table/table-list.vue
{ type: "selection", mark: "radio" }
]
</script>
<script setup lang="ts">
// 表格配置
const initTableColumns = computed<Record<string, any>[]>(() => {
(props.tableColumns as Record<string, any>[]).forEach(
(item: Record<string, any>) => {
item.align ??= "center";
item.ellipsisTooltip ??= false;
// 多选
if (item.type === "selection") {
item.width ??= 50;
}
// 单选
if (item.type === "selection" && item.mark === "radio") {
item.className = "radio";
}
}
)
});
</script>
<style lang="scss">
/** 单选 */
.el-table {
> .el-table__inner-wrapper {
.el-table__header-wrapper {
> .el-table__header {
.el-table__cell.radio {
.el-checkbox {
&::before {
content: "单选";
color: var(--el-table-header-text-color);
}
.el-checkbox__input {
display: none;
}
}
}
}
}
.el-table__body-wrapper {
.el-table__body {
.el-table__row {
.el-table__cell.radio {
.el-checkbox {
.el-checkbox__inner {
border-radius: 100%;
}
}
}
}
}
}
}
}
</style>
问题反馈
<el-table>
需要添加key标志才能促使<el-table-column>
因为遍历的数据而更新,即<el-table :key='变化值'>
Feedback 反馈组件
ElMessageBox
const action = await ElMessageBox.confirm(
"你确定要退出当前登录吗?",
"操作提示",
{
confirmButtonText: "确定"
}
)
.then(() => true)
.catch(() => false);
if (!action) return;
<el-image-viewer>
<template>
<el-image-viewer
v-if="imageViewer.visual"
hide-on-click-modal
:url-list="imageViewer.list"
/>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
import { ElImage, ElImageViewer } from 'element-plus'
const imageViewer = reactive({
visual: false,
list: []
});
</script>
<el-dialog>
<!--
<component-dialog v-model='visible' @update:submit="submitBackhaul" @update:action="actionBackhaul" />
const componentDialog = defineAsyncComponent(() => import('@/components/concat-account.vue'))
-->
<template>
<el-dialog
v-model="initValue"
:before-close="close"
:close-on-click-modal="false"
title="报告上传"
width="700px"
class="com-erp-dialog define-"
>
<template #footer>
<span class="dialog-footer">
<el-button @click="close()">取消</el-button>
<el-button :loading="submitLoading" type="primary" :disabled="disabledSubmit" @click="confirm()">
确定
</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { ElMessage, ElDialog } from 'element-plus';
const props = defineProps({
modelValue: {
type: Boolean,
default() {
return false;
}
}
});
const emit = defineEmits(['update:modelValue', 'action:submit', 'action:action']);
// 限制重复提交
const submitLoading = ref(false);
// 限制上传尚未完成
let complete = ref(true);
const initValue = computed({
get: () => props.modelValue,
set: (val) => {
emit('update:modelValue', val);
}
});
const disabledSubmit = computed(() => !complete.value);
// 提交表单
async function confirm() {
ElMessage.closeAll();
if (submitLoading.value) {
return ElMessage.warning('正在提交中,请不要重复提交');
}
}
// 关闭模态框
function close() {
emit('update:modelValue', false);
}
</script>
<style lang="scss">
@import '@/assets/style/dialog.scss';
</style>
<style lang="scss" scoped></style>
<el-scrollbar>
<el-scrollbar></el-scrollbar>
<style lang="scss" scoped>
/* 不设置固定高度,当内容超过一定高度后,自动使用滚动 */
:deep(.el-scrollbar__wrap) {
max-height: 200px;
}
</style>
<el-pagination>
<template>
<!--分页模块-->
<component-table-pagination
v-if="handleColumns.length && !hidden?.pagination"
v-model:current-page="paging.currentPage"
v-model:page-size="paging.limit"
:total="paging.totalCnt"
@update:callback="uploadPaginationCallback"
/>
</template>
<script lang="ts" setup>
// 第三方资源类库
import { ref, reactive, computed } from 'vue';
import componentTablePagination from './table-pagination.vue';
/**
* @当前业务
* 具体逻辑实现代码
*/
</script>
<!--当前页面作用域-->
<style lang="scss" scoped>
.page-module {
position: relative;
}
</style>
<template>
<div class="component-el-pagination" :class="{ null: !total }">
<template v-if="total">
<el-pagination
v-model:current-page="initCurrentPage"
v-model:page-size="initPageSize"
popper-class="component-el-pagination-table-list"
background
layout="total, sizes, prev, pager, next, jumper"
:page-sizes="TABLE_PAGINATION_PAGE_SIZES"
:total="total"
@size-change="sizeChange"
@current-change="updateCondition"
/>
</template>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { ElPagination } from 'element-plus';
const props = defineProps({
total: {
type: Number,
default() {
return 0;
}
},
currentPage: {
type: Number,
default() {
return 1;
}
},
pageSize: {
type: Number,
default() {
return 20;
}
}
});
const emit = defineEmits(['update:current-page', 'update:page-size', 'update:callback']);
const TABLE_PAGINATION_PAGE_SIZES = [20, 50, 100, 200];
const initCurrentPage = computed({
get: () => props.currentPage,
set: (value: number) => {
emit('update:current-page', value);
}
});
const initPageSize = computed({
get: () => props.pageSize,
set: (value: number) => {
emit('update:page-size', value);
}
});
const sizeChange = () => {
emit('update:current-page', 1);
updateCondition();
};
const updateCondition = () => {
emit('update:callback');
};
</script>
<style lang="scss" scoped>
.component-el-pagination {
background-color: #ffffff;
overflow: hidden;
position: relative;
padding-top: 12px;
padding-bottom: 12px;
padding-right: 12px;
&.null {
padding-bottom: 3px;
}
:deep(.el-pagination) {
float: right;
.btn-next,
.btn-prev,
.el-pager li {
background-color: #fff;
margin: 0 4px;
&.is-active {
background-color: #409eff;
}
&:not(.is-active) {
border: 1px solid #b3c0e7;
}
font-weight: 400;
}
.el-pagination__total {
margin-right: 5px;
}
.el-pagination__sizes {
margin-right: -1px;
}
.el-select {
.el-input__inner {
border-color: #b3c0e7;
}
}
.el-pagination__jump {
margin-left: 20px;
.el-input--mini {
width: 50px;
}
.el-pagination__editor {
margin-left: 8px;
margin-right: 8px;
}
.el-input__wrapper {
.el-input__inner {
border: 0;
}
}
}
}
}
</style>
<el-select-v2>
该组件v-model="selectOptions.value"中selectOptions.value === ""空字符串时,palceholder不显示,只有为null或undefined时才显示。
<el-form>
表单
<el-form-item>
抽离- 原有冗余结构
<template>
<el-form ref="ruleFormRef" :model="ruleForm" label-width="120px">
<el-form-item label="课程名称:" prop="title" :rules="rules.title">
<el-input
v-model="ruleForm.title"
class="name"
clearable
style="width: 280px"
placeholder="请输入内容"
/>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { ElForm, ElFormItem, ElInput } from "element-plus";
import type { FormInstance, FormRules } from "element-plus";
const ruleFormRef = ref<FormInstance>();
const ruleForm = reactive({
title: ""
});
const rules = reactive<FormRules>({
title: [
{ required: true, message: "请输入课程名称", trigger: ["blur", "change"] }
]
});
// 验证器
async function formValidater() {
return new Promise((resolve) => {
ruleFormRef.value?.validate((valid: boolean) => {
resolve(valid);
});
});
}
// 提交表单
async function submitAction() {
const validater = await formValidater();
if (!validater) return;
ElMessage.closeAll();
if (submitLoading.value) {
ElMessage.warning('正在提交中,请稍后再操作!');
return;
}
submitLoading.value = true;
// 提交参数
const submitOptions: Record<string, any> = {};
}
</script>
- 分离后友好结构
<!--父级组件-->
<template>
<el-form ref="ruleFormRef" :model="ruleForm" label-width="120px">
<child
v-model="ruleForm.title"
/>
</el-form>
</template>
<script setup lang="ts">
import child from "./components/child.vue";
</script>
<!--子级组件-->
<!--
import child from "./components/child.vue";
-->
<template>
<el-form-item label="课程名称:" prop="title" :rules="rules.title">
<el-input
v-model="initValue"
class="name"
clearable
style="width: 280px"
placeholder="请输入内容"
/>
</el-form-item>
</template>
<script setup lang="ts">
// 第三方资源类库或插件
import { reactive, computed } from "vue";
import { ElFormItem, ElInput } from "element-plus";
import type { FormInstance, FormRules } from "element-plus";
// 全局资源库
// 局部资源库
/**
* @当前业务
* 具体逻辑实现代码
*/
const props = defineProps({
modelValue: {
type: String,
default() {
return "";
}
}
});
const emit = defineEmits(["update:modelValue"]);
const ruleFormRef = ref<FormInstance>();
const rules = reactive<FormRules>({
title: [
{ required: true, message: "请输入课程名称", trigger: ["blur", "change"] }
]
});
const initValue = computed({
get: () => props.modelValue,
set: (val) => {
emit("update:modelValue", val);
}
});
const initRules = computed<any[]>(() => props.rules);
</script>
表单提交验证
<template>
<el-form ref="ruleFormRef" :model="ruleForm" :rules="rules" label-width="120px" status-icon>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="close">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="submitForm(ruleFormRef, 1)">发布</el-button>
</span>
</template>
</template>
<script lang="ts" setup>
// 第三方资源类库
import { ElForm, ElFormItem, ElTooltip, ElImage, ElLink, ElInput } from 'element-plus';
import type { FormInstance, FormRules } from 'element-plus';
// 全局资源库
/**
* @当前业务
* 具体逻辑实现代码
*/
const submitLoading = ref(false);
const ruleFormRef = ref<FormInstance>();
const ruleForm = reactive({});
const rules = reactive<FormRules>({});
const submitFormFunc = async (
formEl: FormInstance | undefined,
submitType: number
) => {
if (!formEl) return;
// 对整个表单的内容进行验证。 接收一个回调函数,或返回 Promise。
const validate = await formEl.validate((valid: boolean, invalidFields) => {
// 无效选项
if (invalidFields && Object.entries(invalidFields).length) {
const message =
Object.entries(invalidFields)[0]?.[1]?.[0]?.message ||
"提示消息内容!";
ElMessageBox.alert(message, "消息提示", {
confirmButtonText: "确定"
});
}
return valid;
});
if (!validate) return;
// 清理某个字段的表单验证信息。
ruleFormRef.value?.clearValidate('group_id');
// 验证具体的某个字段。
ruleFormRef.value?.validateField();
// 重置该表单项,将其值重置为初始值,并移除校验结果
ruleFormRef.value?.resetFields();
// 滚动到指定的字段
ruleFormRef.value?.scrollToField();
submitLoading.value = true;
// 请求参数
const params: Record<string, any> = {
//
};
}
</script>
表单动态required赋值
// 解决Element-plus表单动态required赋值问题
watchEffect(() => {
// 动态更新订单号是否为必填
if ((rules.order_sn as any)?.[0]) {
(rules.order_sn as any)[0].required = Number(props.need_order) === 1;
}
window.setTimeout(() => {
ruleFormRef.value?.clearValidate('activity_id');
ruleFormRef.value?.clearValidate('phone');
ruleFormRef.value?.clearValidate('order_sn');
ruleFormRef.value?.clearValidate('kf_name');
ruleFormRef.value?.clearValidate('deadline');
ruleFormRef.value?.clearValidate('skip_condition');
ruleFormRef.value?.clearValidate('force_failed');
});
});
列表添加删除动态校验
<template>
<el-form ref="ruleFormRef" :model="ruleForm" label-width="120px" status-icon>
<el-form-item label="大类名称:" prop="name" :rules="rules.name">
<el-input v-model="ruleForm.name" class="name" placeholder="请输入大类名称
" clearable />
</el-form-item>
<el-form-item label="大类难度:" class="define-asterisk aux-asterisk-style">
<ul class="difficulty-content">
<li v-for="(dx, index) in ruleForm.difficultys" :key="dx.uuid" class="com-flex">
<!--这里需要特别注意: :prop的值必须是标签<el-form :model="ruleForm">上ruleForm中的变量链,尤其是组件嵌套时需要注意-->
<!--rules则毫无关系,可以自由写-->
<el-form-item :prop="'difficultys.' + index + '.value'" :rules="rules.difficultys">
<el-input v-model="dx.value" placeholder="请输入大类难度" clearable />
</el-form-item>
<div v-if="index > 0" class="com-flex h center delete">
<el-icon size="18" @click="deleteItem(dx, index)">
<Delete />
</el-icon>
</div>
<div v-if="index === ruleForm.difficultys.length - 1" @click="addItem(dx, index)"
class="com-flex h center add">
<el-icon size="18">
<CirclePlusFilled />
</el-icon>
添加
</div>
</li>
</ul>
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
// 第三方资源类库或插件
import { ref, computed, reactive, watchEffect } from "vue";
import {
ElDialog,
ElForm,
ElFormItem,
ElInput,
ElButton,
ElScrollbar,
ElMessage,
ElMessageBox,
ElIcon
} from "element-plus";
import { CirclePlusFilled, Delete } from "@element-plus/icons-vue";
import type { FormInstance, FormRules } from "element-plus";
const DIFFICULTY_TEMP = () => {
return {
uuid: uuid(),
value: ""
}
}
const ruleFormRef = ref();
const ruleForm = reactive({
difficultys: [
DIFFICULTY_TEMP()
],
course: {
control: true,
list: [
{ weight: "" }
]
}
});
const rules = reactive<FormRules>({
difficultys: [{ required: true, message: "请输入大类难度", trigger: ["blur", "change"] }],
group_id: [{ required: true, validator: validatorRules, message: '请选择销售组别', trigger: ['blur', 'change'] }]
});
/**
* 验证器
* @param rule 规则对象
* @param value 选项值
* @param callback 回调函数
*/
function weightValidator(rule: any, value: any, callback: any) {
const indexString = rule.field
.replaceAll("course.list.", "")
.replaceAll(".weight", "");
const index = Number(indexString);
const item = course.value.list[index] || {};
const { message } = (rules?.rest_time as any)?.[0];
}
// 自定义验证规则
function validatorRules(
rule: Record<string, any>,
value: any, // 即v-model值
callback: any
) {
const { field, message } = rule
const filedsRule = ruleForm.filter((item) => item.name === field)
if (!filedsRule.length) return;
const filedRule = filedsRule[0];
const filedValue = ['target_name'].includes(filedRule.name) ? filedRule.subject : filedRule.value
if ([undefined, null, ''].includes(filedValue)) {
callback(new Error(message));
return;
}
callback()
}
// 清除某个字段验证
ruleFormRef.value?.clearValidate('group_id')
// 对列表中某一个选项校验或清除,根据:prop配置来赋值
ruleFormRef.value?.clearValidate("course.list.0.weight");
ruleFormRef.value?.validateField("course.list.0.weight");
:prop=`'course.list.' + ${index} + '.weight'`
:rules="rules.median"
</script>
<style lang="scss" scoped>
.aux-asterisk-style {
> :deep(.el-form-item__label) {
&::before {
right: 85px;
}
}
}
:deep(div.define-asterisk) {
>.el-form-item__label {
position: relative;
&::before {
content: "*";
position: absolute;
top: 50%;
margin-top: -4px;
padding-top: 2px;
width: 8px;
height: 8px;
line-height: 8px;
text-align: center;
color: rgba(255, 77, 79, 1);
}
}
}
</style>
水印
<template>
<el-form ref="ruleForm" :model="ruleForm" :rules="rules">
<el-checkbox v-model="watermark.font">文字水印</el-checkbox><span v-if="!watermarkType" class="type-alt">文字水印与用户水印至少选择一个</span>
<el-form-item class="watermark-item" prop="fontText">
<el-input
class="item-input"
:maxlength="15"
show-word-limit
v-model="formDate.fontText"
placeholder="请输入文字水印"
></el-input>
</el-form-item>
<div class="com-flex">
<el-form-item label="处理方式:" prop="resource">
<el-radio-group v-model="ruleForm.resource">
<el-radio label="1">通过</el-radio>
<el-radio label="0">驳回</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item prop="content" style="margin-left:25px;">
<el-input v-if="ruleForm.resource == '0'" v-model="ruleForm.content" placeholder="请输入驳回说明" style="width:400px;"></el-input>
</el-form-item>
</div>
<el-row type="flex" justify="end">
<el-button @click="dialogFlag = false">取消</el-button>
<el-button type="primary" :loading="watermark.submit" @click="submitForm">确定</el-button>
</el-row>
</el-form>
</template>
<script>
import Vue from 'vue'
import Component from 'vue-class-component'
// @Component({ components: {} })
@Component
export default class Super extends Vue {
name = '';
watermark = {
submit: false,
id: null,
edit: false,
viewer: false,
font: true,
user: false,
radio: '1',
module: null,
number: null,
}
ruleForm = {
fontText: '',
resource: '',
content: '',
};
rules = {
fontText: {
validator: (rule, value, callback) => {
if (this.watermark.font && (!value || value === '')) {
return callback('文字水印不能为空');
}
callback();
},
trigger: 'blur',
},
resource: [
{ required: true, message: '请选择处理方式', trigger: 'change' } // trigger: ['blur', 'change']
],
content: {
validator: (rule, value, callback) => {
if (Number(this.ruleForm.resource) == 0 && (!value || value == '')) {
return callback('请输入驳回说明');
}
callback();
},
trigger: 'blur',
}
}
get watermarkType() {
return this.watermark.font || this.watermark.user;
}
submitFormFunc() {
this.$refs.formBox.validate(valid => {
if (valid) {
let url = this.$URL.createCategory;
if (this.formDate.id) {
url = this.$URL.getCategoryUpdate;
}
this.formDate.type = this.$route.query.type;
let options = {};
// 水印
if (this.typeId == 205) {
if (!this.watermarkType) {
return;
}
if (this.watermark.submit) {
return;
}
url = 'api/watermark/edit';
options = {
title: this.formDate.fontText,
is_text: this.watermark.font ? 1 : -1,
is_user: this.watermark.user ? 1 : -1,
style: Number(this.watermark.radio),
module: this.watermark.module,
number: this.watermark.number,
}
options['id'] = this.watermark.id;
} else {
options = this.formDate;
}
this.watermark.submit = true;
this.$API.post(url, options, result => {
this.watermark.submit = false;
if (result.code == 0) {
this.$message.success('操作成功');
this.dialogFlag = false;
this.getCategoryType();
} else {
this.$message.error(result.msg);
}
}, () => {
this.watermark.submit = false;
});
}
});
}
mounted() {
this.$nextTick(() => {});
}
created() {
//
}
}
</script>
<style lang='less' scoped>
@media only screen and (max-width: 1680px) {
}
</style>
其他移动端UI框架
Vant
Mint UI
- Mint UI:http://mint-ui.github.io/
- https://github.com/ElemeFE/mint-ui
其他框架
- MUI: https://mui.com/
- Vuetify: https://vuetifyjs.com/
前端UI框架
- Ant Design Mobile
- NutUI
- Varlet
- Arco Design Mobile