包含基础组件、动态渲染组件(dialog、message)、布局组件、受控表单组件、动态表单组件五类
1. 定制化主题
(1)主题的方案设计需要以下两种规范:
- 颜色的设计规范
- CSS的开发规范
(2)开发一个多状态的按钮组件
这里维度分成按钮类型、按钮种类和按钮禁用状三种维度状态。按钮类型有Default、Primary、Success、Warn和Danger这五类;按钮种类有Contented和Outlined两类;按钮禁用状态有Enabled和Diabled两类。 三种维度的叠加管理分解成三个步骤:
- 第一步,基础按钮组件样式的开发
- 第二步,实现按钮不同维度组合的样式
- 第三步,组件的使用状态叠加
第一步:
.@{prefix-name}-button {
position: relative;
display: inline-block;
font-weight: 400;
white-space: nowrap;
text-align: center;
cursor: pointer;
user-select: none;
touch-action: manipulation;
height: 32px;
padding: 4px 15px;
font-size: 14px;
border-radius: 2px;
box-sizing: border-box;
border-width: 1px;
}
第二步:
具体 CSS Variable 语义化,如下面所示:
:root {
// 按钮 default-contained: 默认状态
--@{prefix-name}-btn-default-contained-color: @gray-1;
--@{prefix-name}-btn-default-contained-border-color: @gray-6;
--@{prefix-name}-btn-default-contained-bg-color: @gray-6;
// 按钮 primary-contained: 默认状态
--@{prefix-name}-btn-primary-contained-color: @blue-1;
--@{prefix-name}-btn-primary-contained-border-color: @blue-6;
--@{prefix-name}-btn-primary-contained-bg-color: @blue-6;
// 按钮 success-contained: 默认状态
--@{prefix-name}-btn-success-contained-color: @green-1;
--@{prefix-name}-btn-success-contained-border-color: @green-6;
--@{prefix-name}-btn-success-contained-bg-color: @green-6;
// 按钮 warning-contained: 默认状态
--@{prefix-name}-btn-warning-contained-color: @gold-1;
--@{prefix-name}-btn-warning-contained-border-color: @gold-6;
--@{prefix-name}-btn-warning-contained-bg-color: @gold-6;
// 按钮 danger-contained: 默认状态
--@{prefix-name}-btn-danger-contained-color: @red-1;
--@{prefix-name}-btn-danger-contained-border-color: @red-6;
--@{prefix-name}-btn-danger-contained-bg-color: @red-6;
// 按钮 default-outlined: 默认状态
--@{prefix-name}-btn-default-outlined-color: @gray-6;
--@{prefix-name}-btn-default-outlined-border-color: @gray-6;
--@{prefix-name}-btn-default-outlined-bg-color: @gray-1;
// 按钮 primary-outlined: 默认状态
--@{prefix-name}-btn-primary-outlined-color: @blue-6;
--@{prefix-name}-btn-primary-outlined-border-color: @blue-6;
--@{prefix-name}-btn-primary-outlined-bg-color: @blue-1;
// 按钮 success-outlined: 默认状态
--@{prefix-name}-btn-success-outlined-color: @green-6;
--@{prefix-name}-btn-success-outlined-border-color: @green-6;
--@{prefix-name}-btn-success-outlined-bg-color: @green-1;
// 按钮 warning-outlined: 默认状态
--@{prefix-name}-btn-warning-outlined-color: @gold-6;
--@{prefix-name}-btn-warning-outlined-border-color: @gold-6;
--@{prefix-name}-btn-warning-outlined-bg-color: @gold-1;
// 按钮 danger-outlined: 默认状态
--@{prefix-name}-btn-danger-outlined-color: @red-6;
--@{prefix-name}-btn-danger-outlined-border-color: @red-6;
--@{prefix-name}-btn-danger-outlined-bg-color: @red-1;
}
具体 className 组合实现如下述所示:
@import '../theme/variable.less';
.@{prefix-name}-button {
// ....
// contented
&.@{prefix-name}-button-default-contained {
background: ~'var(--@{prefix-name}-btn-default-contained-bg-color)';
color: ~'var(--@{prefix-name}-btn-default-contained-color)';
border: 1px solid ~'var(--@{prefix-name}-btn-default-contained-border-color)';
}
&.@{prefix-name}-button-primary-contained {
background: ~'var(--@{prefix-name}-btn-primary-contained-bg-color)';
color: ~'var(--@{prefix-name}-btn-primary-contained-color)';
border: 1px solid ~'var(--@{prefix-name}-btn-primary-contained-border-color)';
}
&.@{prefix-name}-button-success-contained {
background: ~'var(--@{prefix-name}-btn-success-contained-bg-color)';
color: ~'var(--@{prefix-name}-btn-success-contained-color)';
border: 1px solid ~'var(--@{prefix-name}-btn-success-contained-border-color)';
}
&.@{prefix-name}-button-warning-contained {
background: ~'var(--@{prefix-name}-btn-warning-contained-bg-color)';
color: ~'var(--@{prefix-name}-btn-warning-contained-color)';
border: 1px solid ~'var(--@{prefix-name}-btn-warning-contained-border-color)';
}
&.@{prefix-name}-button-danger-contained {
background: ~'var(--@{prefix-name}-btn-danger-contained-bg-color)';
color: ~'var(--@{prefix-name}-btn-danger-contained-color)';
border: 1px solid ~'var(--@{prefix-name}-btn-danger-contained-border-color)';
}
// outlined
&.@{prefix-name}-button-default-outlined {
background: ~'var(--@{prefix-name}-btn-default-outlined-bg-color)';
color: ~'var(--@{prefix-name}-btn-default-outlined-color)';
border: 1px solid ~'var(--@{prefix-name}-btn-default-outlined-border-color)';
}
&.@{prefix-name}-button-primary-outlined {
background: ~'var(--@{prefix-name}-btn-primary-outlined-bg-color)';
color: ~'var(--@{prefix-name}-btn-primary-outlined-color)';
border: 1px solid ~'var(--@{prefix-name}-btn-primary-outlined-border-color)';
}
&.@{prefix-name}-button-success-outlined {
background: ~'var(--@{prefix-name}-btn-success-outlined-bg-color)';
color: ~'var(--@{prefix-name}-btn-success-outlined-color)';
border: 1px solid ~'var(--@{prefix-name}-btn-success-outlined-border-color)';
}
&.@{prefix-name}-button-warning-outlined {
background: ~'var(--@{prefix-name}-btn-warning-outlined-bg-color)';
color: ~'var(--@{prefix-name}-btn-warning-outlined-color)';
border: 1px solid ~'var(--@{prefix-name}-btn-warning-outlined-border-color)';
}
&.@{prefix-name}-button-danger-outlined {
background: ~'var(--@{prefix-name}-btn-danger-outlined-bg-color)';
color: ~'var(--@{prefix-name}-btn-danger-outlined-color)';
border: 1px solid ~'var(--@{prefix-name}-btn-danger-outlined-border-color)';
}
}
Vue 代码实现如下:
<template>
<button
:class="{
[baseClassName]: true,
[btnClassName]: true
}"
>
<slot v-if="$slots.default"></slot>
</button>
</template>
<script setup lang="ts">
import { prefixName } from '../theme/index';
import type { ButtonType, ButtonVariant } from './types';
const props = withDefaults(
defineProps<{
type?: ButtonType;
variant?: ButtonVariant;
}>(),
{
type: 'default',
variant: 'contained',
disabled: false
}
);
const baseClassName = `${prefixName}-button`;
const btnClassName = `${baseClassName}-${props.type}-${props.variant}`;
</script>
第三步:
实现按钮其他状态叠加,例如鼠标悬浮时候(Hover)等状态,也是添加对应的 CSS Variable,然后在 Less 里使用对应变量。
对多状态的按钮组件进行主题控制:
主题核心就是颜色梯度的控制,我们在处理按钮组件的不同颜色的时候,只是选择某个颜色的某个梯度号,这里具体拆解成两步:
- 对按钮不同状态维度组合选择对应色板的颜色梯度;
- 将选好的颜色用新 className 来覆盖原来的 CSS Variable。
2. 动态渲染组件
(1)动态渲染组件的两个技术特点就是:
- 以直接函数式地使用来执行渲染,使用者不需要写代码来挂载组件;
- 组件内部实现了动态挂载和卸载节点的操作。
(2)动态渲染组件一般可以实现组件库里的哪些功能组件?
- 消息提醒组件(Message)
- 对话框组件 (Dialog)
(3)实现动态渲染组件需要做什么技术准备?
在设计动态渲染组件的 API 时,使用步骤尽量少,使用代码尽量精简。而动态渲染组件整个生命周期,最核心的就是“动态挂载”和“动态卸载”两个步骤:
import { Module } from 'xxxx'
// 创建动态组件 mod1
const mod1 = Module.create({ /* 组件参数 */ });
// 挂载渲染 mod1
mod1.open();
// 卸载动态组件 mod1
mod1.close();
// 创建动态组件 mod2
const mod2 = Module.create({ /* 组件参数 */ });
// 挂载渲染 mod2
mod2.open();
// 卸载动态组件 mod2
mod2.close();
最简单的 Vue.js 3.x 代码实现:
import { defineComponent, createApp, h } from 'vue';
// 用 JSX 语法实现一个Vue.js 3.x的组件
const ModuleComponent = defineComponent({
setup(props, context) {
return () => {
return (
<div>这是一个动态渲染的组件</div>
);
};
}
});
// 实现动态渲染组件的过程
export const createModule = () => {
// 创建动态节点DOM
const dom = document.createElement('div');
// 把 DOM 追加到页面 body标签里
const body = document.querySelector('body') as HTMLBodyElement;
body.appendChild(dom)
const app = createApp({
render() {
return h(DialogComponent, {});
}
});
// 返回当前组件的操作实例
// 其中封装了挂载和卸载组件的方法
return {
open(): () => {
// 把组件 ModuleComponent 作为一个独立应用挂载在 DOM 节点上
app.mount(dom);
},
close: () => {
// 卸载组件
app.unmount();
// 销毁动态节点
dom.remove();
}
}
}
(4)如何实现一个动态 Dialog 组件?
实现过程,具体步骤如下:
- 第一步,实现 Dialog 的实体组件,用 JSX 语法或模板语法都可以。这里,Dialog 组件要用 Emit 方式注册好回调事件;
- 第二步,封装 createDialog 函数来创建一个 Dialog 的实例。这个过程要注意配置好 Dialog 的回调函数等操作;
- 第三步,封装 close 函数来控制卸载关闭这个组件。
这个是 JSX 实现的 Dialog 实体组件:
// ./dialog.tsx
import { defineComponent } from 'vue';
import { prefixName } from '../theme/index';
export const DialogComponent = defineComponent({
props: {
text: String
},
emits: ['onOk'],
setup(props, context) {
const { emit } = context;
const onOk = () => {
emit('onOk');
};
return () => {
return (
<div class={`${prefixName}-dialog-mask`}>
<div class={`${prefixName}-dialog`}>
<div class={`${prefixName}-dialog-text`}>{props.text}</div>
<div class={`${prefixName}-dialog-footer`}>
<button class={`${prefixName}-dialog-btn`} onClick={onOk}>
确定
</button>
</div>
</div>
</div>
);
};
}
});
以下是封装了函数方法调用的动态渲染组件的方式:
import { createApp, h } from 'vue';
import { DialogComponent } from './dialog';
function createDialog(params: { text: string; onOk: () => void }) {
const dom = document.createElement('div');
const body = document.querySelector('body') as HTMLBodyElement;
body.appendChild(dom);
const app = createApp({
render() {
return h(DialogComponent, {
text: params.text,
onOnOk: params.onOk
});
}
});
app.mount(dom);
return {
close: () => {
app.unmount();
dom.remove();
}
};
}
const Dialog: { createDialog: typeof createDialog } = {
createDialog
};
export default Dialog;
3. 动态Form组件
(1) 代表表单结构的Schema
const formSchema = [
{
field: '用户名',
type: 'input',
prop: 'username',
value: '',
attr: {
placeholder: '请输入用户名'
}
},
{
field: '密码',
type: 'input',
prop: 'password',
value: '',
attr: {
placeholder: '请输入密码'
}
},
{
field: '角色',
type: 'checkbox',
prop: 'roles',
value: [],
children: [
{
value: 1,
field: '管理员'
},
{
value: 2,
field: '普通用户'
},
{
value: 3,
field: '测试用户'
}
]
},
{
field: '性别',
type: 'radio',
prop: 'gender',
value: 0,
children: [
{
value: 1,
field: '男'
},
{
value: 2,
field: '女'
}
]
},
{
field: '头像',
type: 'input',
prop: 'photo',
value: '',
attr: {
placeholder: '请输入头像链接'
}
},
]
(2) 表单组件
import { cloneDeep } from 'lodash-es'
export default {
props: {
schema: {
type: Array as PropType<FormItem[]>,
default: () => []
}
},
emits: ['change'],
setup(props, { emit }) {
const rand = () > {
return Math.random().toString(36).subString(2)
}
const model = ref<any>({
...cloneDeep(
props.schema.reduce((prev, cur) => {
prev[cur.prop] = cur.value
if(['radio', 'checkbox'].includes(cur.type)) {
cur.children?.forEach(o => {
// id也可数据库取值
o.id = rand()
})
}
return prev
}, {} as Record<string, any>)
)
})
watch(() => model.value, () => {
emit('change', model.value)
}, {
deep: true
})
return {
model
}
}
}
(3) 表单组件使用
<base-form :schema="formSchema"></base-form>