1.组件库
组件库组成:
- 基础组件
- 输入表单组件
- 数据组件
- 通知组件
2.基础组件
布局/颜色/字体
container组件
<script setup >无法设置name,多定义一个<script>,只设置option- defineProps定义porps类型
- withDefaults 设置参数默认值
- useSlots显示默认插槽
Container.vue
<template>
<section
class="el-container"
:class="{ 'is-vertical': isVertical }"
>
<slot />
</section>
</template>
<script lang="ts">
export default{
name:'ElContainer'
}
</script>
<script setup lang="ts">
import {useSlots,computed,VNode,Component} from 'vue'
interface Props {
direction?:string
}
const props = defineProps<Props>()
const slots = useSlots()
const isVertical = computed(() => {
if (slots && slots.default) {
return slots.default().some((vn:VNode) => {
const tag = (vn.type as Component).name
return tag === 'ElHeader' || tag === 'ElFooter'
})
} else {
if (props.direction === 'vertical') {
return true
} else {
return false
}
}
})
</script>
<style lang="scss">
@import '../styles/mixin';
@include b(container) {
display: flex;
flex-direction: row;
flex: 1;
flex-basis: auto;
box-sizing: border-box;
min-width: 0;
@include when(vertical) {
flex-direction: column;
}
}
</style>
Header.vue
<template>
<header
class="el-header"
:style="{ height }"
>
<slot />
</header>
</template>
<script lang="ts">
export default{
name:'ElHeader'
}
</script>
<script setup lang="ts">
import {withDefaults} from 'vue'
interface Props {
height?:string
}
withDefaults(defineProps<Props>(),{
height:"60px"
})
</script>
<style lang="scss">
@import '../styles/mixin';
@include b(header) {
padding: $--header-padding;
box-sizing: border-box;
flex-shrink: 0;
}
</style>
index.ts
import {App} from 'vue'
import ElContainer from './Container.vue'
import ElHeader from './Header.vue'
export default {
install(app:App){
app.component(ElContainer.name,ElContainer)
app.component(ElHeader.name,ElHeader)
}
}
src\components\styles\mixin.scss
$namespace: 'el';
$state-prefix: 'is-';
$--header-padding: 10px;
@mixin b($block) {
$B: $namespace + '-' + $block !global;
.#{$B} {
@content;
}
}
// 添加ben后缀啥的
@mixin when($state) {
@at-root {
&.#{$state-prefix + $state} {
@content;
}
}
}
3.单元测试
mini-jest
function add(x,y){
return Number(x) + Number(y)
}
function expect(result){
return {
toBe(arg){
if(arg !== result) {
throw new Error(`输入与预期不符,预期:${arg},实际:${result}`)
}
}
}
}
function test(title,fn){
try {
fn()
console.log("测试通过")
} catch (error) {
console.log(error)
console.log("测试不通过")
}
}
test('单元测试1', ()=> {
expect(add(1,'2')).toBe(3)
})
jest
安装
npm install -D jest@26 vue-jest@next @vue/test-utils@next
npm install -D babel-jest@26 @babel/core @babel/preset-env
npm install -D ts-jest@26 @babel/preset-typescript @types/jest
.babel.config.js
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript',
],
}
jest.config.js
module.exports = {
transform: {
// .vue文件用 vue-jest 处理
'^.+\\.vue$': 'vue-jest',
// .js或者.jsx用 babel-jest处理
'^.+\\.jsx?$': 'babel-jest',
//.ts文件用ts-jest处理
'^.+\\.ts$': 'ts-jest'
},
testMatch: ['**/?(*.)+(spec).[jt]s?(x)'],
collectCoverage: true,
coverageReporters: ["json", "html"],
}
package.json
"scripts": {
"test": "jest"
}
第三方jest插件
安装
npm i @vue/test-utils -D
Button.vue
<template>
<button
class="el-button"
:class="[
buttonSize ? `el-button--${buttonSize}` : '',
type ? `el-button--${type}` : ''
]"
>
<slot />
</button>
</template>
<script lang="ts">
export default{
name:'ElButton'
}
</script>
<script setup lang="ts">
import {computed, withDefaults} from 'vue'
import { getCurrentInstance } from 'vue'
function useGlobalOptions() {
const instance = getCurrentInstance()
if (!instance) {
console.warn('useGlobalOptions must be call in setup function')
return
}
return instance.appContext.config.globalProperties.$ELEMENT || {}
}
interface Props {
size?:""|'small'|'medium'|'large',
type?:""|'primary'|'success'|'danger'
}
const props = withDefaults(defineProps<Props>(),{
size:"",
type:""
})
const globalConfig = useGlobalOptions()
const buttonSize = computed(()=>{
return props.size||globalConfig.size
})
</script>
Button.spec.js
import Button from './Button.vue';
import { mount } from '@vue/test-utils';
describe('按钮测试', () => {
it('测试能够显示文本', () => {
const content = 'test';
const wrapper = mount(Button, {
slots: {
default: content
}
});
expect(wrapper.text()).toBe(content);
});
it('通过size属性控制大小', () => {
const size = 'medium';
const wrapper = mount(Button, {
props: {
size
}
});
// size内部通过class控制
console.log('classess',wrapper.classes());
expect(wrapper.classes()).toContain(`el-button--${size}`);
});
});
4.表单
form设计思路
- jform
- 整体的from数据管理
- 所有的规则rules,并触发所有el-form-item的校验事件
- 提交方法
- el-form-item
- 每个输入框单独的校验规则
- el-input
- 实际录入的组件
常用使用格式
<el-form :model="ruleForm" :rules="rules" ref="form">
<el-form-item label="用户名" prop="username">
<el-input v-model="ruleForm.username"></el-input>
<!-- <el-input :model-value="" @update:model-value=""></el-input> -->
</el-form-item>
<el-form-item label="密码" prop="passwd">
<el-input type="textarea" v-model="ruleForm.passwd"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm()">登录</el-button>
</el-form-item>
</el-form>
相关技术点
- async-validator 实现校验
- defineExpose 父组件直接访问子组件内容
demo
JForm.vue
<template>
<form>
<slot></slot>
</form>
</template>
<script lang="ts" setup>
import { provide, reactive, toRefs, getCurrentInstance } from "vue";
import {formKey} from './type'
interface Props {
model: Object,
rules: Object,
}
const props = defineProps<Props>()
const { proxy } = getCurrentInstance();
const itemList = [];
function validate(cb){
const tasks = itemList.map((item) => {
return item.validate()
});
Promise.all(tasks)
.then(() => cb(true))
.catch(() => {
console.log("catch-false");
cb(false);
});
};
proxy.$mitt.on("JForm.addField", (field) => {
itemList.push(field);
});
provide(formKey, props);
//<script setup>如果想被父组件直接调用私有方法,需要 expose导出
defineExpose({
validate,
})
</script>
JFormItem.vue
<template>
<div class="j-form-item">
<label for="">
{{ label }}
</label>
<slot></slot>
<p class="errors">
{{ error }}
</p>
</div>
</template>
<script setup lang="ts">
import Schema from "async-validator";
import {formKey} from './type'
import {
reactive,
onMounted,
ref,
toRefs,
provide,
inject,
getCurrentInstance,
} from "vue";
interface Props {
label:string,
prop:string,
}
const props = defineProps<Props>()
const { proxy } = getCurrentInstance();
let error = ref();
const jForm = inject(formKey);//这里注入 form的总对象
const validate = () => {
if (!props.prop) return;
const rules = jForm.rules[props.prop];
const value = jForm.model[props.prop];
const validator = new Schema({ [props.prop]: rules });
// 返回promise,全局可以统一处理
return validator.validate({ [props.prop]: value }, (errors) => {
// errors存在则校验失败
console.log("err", errors);
if (errors) {
error.value = errors[0].message;
} else {
// 校验通过
error.value = "";
}
});
};
const jFormItem = reactive({
...toRefs(props),
validate,
});
provide("jFormItem", props);
onMounted(() => {
proxy.$mitt.on("JFormItem.validate", validate);//留给子组件调用
if (props.prop) {
proxy.$mitt.emit("JForm.addField", jFormItem);//每注册一个组件 ,都回传到总form里面统一管理
}
});
</script>
<style scoped>
.errors {
color: red;
font-size: 12px;
}
</style>
JInput.vue
<template>
<input type="text" :value="modelValue" @input="handleChange" />
</template>
<script lang="ts">
export default {
name: "j-input"
}
</script>
<script setup lang="ts">
import { getCurrentInstance } from "vue";
interface Props {
modelValue: String
}
const props = defineProps<Props>()
const emits = defineEmits('update:modelValue')
const { proxy } = getCurrentInstance();
const handleChange = (e) => {
emits("update:modelValue", e.target.value);
proxy.$mitt.emit("JFormItem.validate");
};
const handleBlur = () => {
console.log("blur");
};
</script>
FormTest.vue
<template>
<div>
<h2>表单form组件</h2>
<j-form ref="form" :model="formData" :rules="rules">
<j-form-item label="名称" prop="name">
<j-input v-model="formData.name"></j-input>
</j-form-item>
<j-form-item label="邮箱" prop="email">
<j-input v-model="formData.email"></j-input>
</j-form-item>
<j-form-item>
<button @click="onSubmit">提交</button>
</j-form-item>
</j-form>
<hr />
</div>
</template>
<script>
import { reactive, getCurrentInstance } from "vue";
import JInput from "./jform/JInput.vue";
import JForm from "./jform/JForm.vue";
import JFormItem from "./jform/JFormItem.vue";
export default {
components: {
JForm,
JFormItem,
JInput,
},
setup() {
const formData = reactive({ name: "" ,email: ""});
const rules = {
name: [{ required: "true", message: "请输入名称" }],
email: [{ required: "true", message: "请输入邮箱" }],
};
const { proxy } = getCurrentInstance();
const onSubmit = (e) => {
e.preventDefault();
proxy.$refs.form.validate((valid) => {
console.log("valid", valid);
});
};
return { formData, rules, onSubmit };
},
};
</script>
type.ts 用于provide与inject类型推到
import {InjectionKey,ref} from 'vue'
export type FormData = {
model:Object,
rules:Object
}
export const formKey:InjectionKey<FormData> = Symbol('')
plugins/emitter.ts
import mitt from "mitt";
export default {
install(Vue) {
const _emitter = mitt();
// 全局发布(在Vue全局方法中自定义$pub发布方法)
// 这里做了$pub方法能够携带多个参数的处理,方便我们再业务中触发事件时带多个参数
Vue.config.globalProperties.$mitt = _emitter
},
};
//使用
app.use(emitter)
app.use(emitter)
5.弹框
一般用法
<el-dialog
title="提示"
:visible.sync="dialogVisible"
width="30%"
v-model:visible="dialogVisible"
>
<span>这是一段信息</span>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="dialogVisible = false">确 定</el-button>
</span>
</template>
</el-dialog>
动态创建组件
<template>
<el-button plain @click="open1"> 成功 </el-button>
<el-button plain @click="open2"> 警告 </el-button>
</template>
<script setup>
import { Notification } from 'element3'
function open1() {
Notification.success({
title: '成功',
message: '这是一条成功的提示消息',
type: 'success'
})
}
function open2() {
Notification.warning({
title: '警告',
message: '这是一条警告的提示消息',
type: 'warning'
})
}
</script>
开始搭建
<teleport
:disabled="!appendToBody"
to="body"
>
<div class="el-dialog">
<div class="el-dialog__content">
<slot />
</div>
</div>
</teleport>
添加用例
it('函数会创建组件', () => {
const instanceProxy = Notification('foo')
expect(instanceProxy.close).toBeTruthy()
})
it('默认配置 ', () => {
const instanceProxy = Notification('foo')
expect(instanceProxy.$props.position).toBe('top-right')
expect(instanceProxy.$props.message).toBe('foo')
expect(instanceProxy.$props.duration).toBe(4500)
expect(instanceProxy.$props.verticalOffset).toBe(16)
})
test('字符串信息', () => {
const instanceProxy = Notification.info('foo')
expect(instanceProxy.$props.type).toBe('info')
expect(instanceProxy.$props.message).toBe('foo')
})
test('成功信息', () => {
const instanceProxy = Notification.success('foo')
expect(instanceProxy.$props.type).toBe('success')
expect(instanceProxy.$props.message).toBe('foo')
})
原理
- install 方法 用于全局注册 $message('xxx') 和 组件 message.vue的注册
- 调用直接使用 $message('xxx')
demo
src\components\message\index.ts 用于全局注册
import { Plugin, App } from 'vue';
import Message from './message.vue';
import message from './message.js';
// 挂载组件方法
const install = (app: App): App => {
//
app.config.globalProperties.$message = message;
app.component(Message.name, Message);
return app;
};
export default install as Plugin;
src\components\message\message.js
import { createApp, h ,render} from "vue";
import Message from "./message.vue";
const MOUNT_COMPONENT_REF = 'el_component'
const COMPONENT_CONTAINER_SYMBOL = Symbol('el_component_container')
/**
* 创建组件实例对象
* 返回的实例和调用 getCurrentComponent() 返回的一致
* @param {*} Component
*/
export function createComponent(Component, props, children) {
const vnode = h(Component, { ...props, ref: MOUNT_COMPONENT_REF }, children)
const container = document.createElement('div')
vnode[COMPONENT_CONTAINER_SYMBOL] = container
render(vnode, container)
return vnode.component
}
/**
* 销毁组件实例对象
* @param {*} ComponnetInstance 通过createComponent方法得到的组件实例对象
*/
export function unmountComponent(ComponnetInstance) {
render(undefined, ComponnetInstance.vnode[COMPONENT_CONTAINER_SYMBOL])
}
function createNotificationByOpts(opts) {
return createComponent(Message, opts)
}
const message = (props) => {
const instance = createNotificationByOpts(props)
document.body.append(instance.vnode.el)
return instance.proxy
};
export default message
src\components\message\message.vue
<template>
<div class="message" v-if="isShow">
<p>
{{ message }}
</p>
<button @click="close">关闭弹窗</button>
</div>
</template>
<script lang="ts" setup>
import { ref, defineProps, onMounted, withDefaults } from "vue";
interface Props {
message: string,
duration: number
}
const props = withDefaults(defineProps<Props>(), {
message: '',
duration: 3,
})
let isShow = ref(false);
const show = () => {
console.log("duration", props.duration);
isShow.value = true;
setTimeout(() => {
isShow.value = false;
}, props.duration * 1000 || 10000000);
};
const close = () => {
isShow.value = false;
};
onMounted(() => {
show();
})
</script>
main.ts
import message from './components/message';
createApp(App).use(message).mount('#app');
MessageTest.vue
<template>
<div>
<button @click="handleOpen">测试显示</button>
</div>
</template>
<script lang="ts" setup>
import { getCurrentInstance } from "vue";
const { proxy } = getCurrentInstance();
const handleOpen = () => {
const component = proxy.$message({
message: "xxxxx",
});
setTimeout(() => {
component.close();
}, 2000);
};
</script>
创建一个空组件
<template>
<div class="el-nofication">
<slot />
</div>
</template>
<script>
</script>
<style lang="scss">
@import '../styles/mixin';
</style>
测试用例驱动开发
import Notification from "./Notification.vue"
import { mount } from "@vue/test-utils"
describe("Notification", () => {
it('渲染标题title', () => {
const title = 'this is a title'
const wrapper = mount(Notification, {
props: {
title
}
})
expect(wrapper.get('.el-notification__title').text()).toContain(title)
})
it('信息message渲染', () => {
const message = 'this is a message'
const wrapper = mount(Notification, {
props: {
message
}
})
expect(wrapper.get('.el-notification__content').text()).toContain(message)
})
it('位置渲染', () => {
const position = 'bottom-right'
const wrapper = mount(Notification, {
props: {
position
}
})
expect(wrapper.find('.el-notification').classes()).toContain('right')
expect(wrapper.vm.verticalProperty).toBe('bottom')
expect(wrapper.find('.el-notification').element.style.bottom).toBe('0px')
})
it('位置偏移', () => {
const verticalOffset = 50
const wrapper = mount(Notification, {
props: {
verticalOffset
}
})
expect(wrapper.vm.verticalProperty).toBe('top')
expect(wrapper.find('.el-notification').element.style.top).toBe(
`${verticalOffset}px`
)
})
})
完善后的代码
<template>
<div class="el-notification" :style="positionStyle" @click="onClickHandler">
<div class="el-notification__title">
{{ title }}
</div>
<div class="el-notification__message">
{{ message }}
</div>
<button
v-if="showClose"
class="el-notification__close-button"
@click="onCloseHandler"
></button>
</div>
</template>
<script setup>
const instance = getCurrentInstance()
const visible = ref(true)
const verticalOffsetVal = ref(props.verticalOffset)
const typeClass = computed(() => {
return props.type ? `el-icon-${props.type}` : ''
})
const horizontalClass = computed(() => {
return props.position.endsWith('right') ? 'right' : 'left'
})
const verticalProperty = computed(() => {
return props.position.startsWith('top') ? 'top' : 'bottom'
})
const positionStyle = computed(() => {
return {
[verticalProperty.value]: `${verticalOffsetVal.value}px`
}
})
</script>
<style lang="scss">
.el-notification {
position: fixed;
right: 10px;
top: 50px;
width: 330px;
padding: 14px 26px 14px 13px;
border-radius: 8px;
border: 1px solid #ebeef5;
background-color: #fff;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
overflow: hidden;
}
</style>
6.通知
demo2-jest
Notification.spec.ts
import Notification from './Notification.vue';
import { mount } from '@vue/test-utils';
describe("Notification", () => {
it('测试标题title', () => {
const title = 'this is a title'
const wrapper = mount(Notification, {
props: {
title
}
})
expect(wrapper.get('.el-notification__title').text()).toContain(title)
})
it('测试内容', () => {
const message = 'this is a message'
const wrapper = mount(Notification, {
props: {
message
}
})
expect(wrapper.get('.el-notification__content').text()).toContain(message)
})
it('测试渲染位置', () => {
const position = 'bottom-right'
const wrapper = mount(Notification, {
props: {
position
}
})
expect(wrapper.find('.el-notification').classes()).toContain('right')
expect(wrapper.vm.verticalProperty).toBe('bottom')
const el = <HTMLElement>wrapper.find('.el-notification').element
expect(el.style.bottom).toBe('0px')
})
it('测试位置偏移', () => {
const verticalOffset = 50
const wrapper = mount(Notification, {
props: {
verticalOffset
}
})
expect(wrapper.vm.verticalProperty).toBe('top')
const el = <HTMLElement>wrapper.find('.el-notification').element
expect(el.style.top).toBe( `${verticalOffset}px` )
})
it('测试按钮参数', () => {
const showClose = false
const wrapper = mount(Notification, {
props: {
showClose
}
})
expect(wrapper.find('.el-notification__close-button').exists()).toBe(showClose)
// expect(wrapper.find('.el-icon-close').exists()).toBe(true)
})
it('测试点击关闭按钮', async () => {
const showClose = true
const wrapper = mount(Notification, {
props: {
showClose
}
})
const closeBtn = wrapper.get('.el-notification__close-button')
await closeBtn.trigger('click')
expect(wrapper.get('.el-notification').isVisible()).toBe(false)
})
it('测试一段时间之后自动关闭', async () => {
jest.useFakeTimers()
const wrapper = mount(Notification, {
props: {
duration: 8000
}
})
const flushPromises = () => new Promise(resolve => setImmediate(resolve));
jest.runOnlyPendingTimers()
await flushPromises()
expect(wrapper.get('.el-notification').isVisible()).toBe(false)
})
})
Notification.vue
<template>
<div v-show="visible" :class="['el-notification',verticalProperty,horizontalClass]" :style="positionStyle"
@click="onClickHandler">
<div class="el-notification__title">
{{ title }}
</div>
<div class="el-notification__content">
{{ message }}
</div>
<button v-if="showClose" class="el-notification__close-button" @click="onCloseHandler"></button>
</div>
</template>
<script setup lang="ts">
import { getCurrentInstance, ref, computed, withDefaults } from 'vue'
const instance = getCurrentInstance()
const visible = ref(true)
interface Props {
title: string,
message: string,
verticalOffset: number,
type: string,
position: string,
showClose: boolean,
duration:number
}
const props = withDefaults(defineProps<Props>(), {
title: '',
message: '',
verticalOffset: 0,
type: '',
position: 'top-right',
showClose: true,
duration:3
})
const verticalOffsetVal = ref(props.verticalOffset)
const typeClass = computed(() => {
return props.type ? `el-icon-${props.type}` : ''
})
const horizontalClass = computed(() => {
return props.position.endsWith('right') ? 'right' : 'left'
})
const verticalProperty = computed(() => {
return props.position.startsWith('top') ? 'top' : 'bottom'
})
const positionStyle = computed(() => {
return {
[verticalProperty.value]: `${verticalOffsetVal.value}px`
}
})
function onCloseHandler() {
visible.value = false
}
let timer
function delayClose() {
if (props.duration > 0) {
timer = setTimeout(() => {
onCloseHandler()
}, props.duration)
}
}
delayClose()
</script>
<style lang="scss">
.el-notification {
position: fixed;
right: 10px;
top: 50px;
width: 330px;
padding: 14px 26px 14px 13px;
border-radius: 8px;
border: 1px solid #ebeef5;
background-color: #fff;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
overflow: hidden;
}
</style>
添加关闭的逻辑
it('set the showClose ', () => {
const showClose = true
const wrapper = mount(Notification, {
props: {
showClose
}
})
expect(wrapper.find('.el-notification__closeBtn').exists()).toBe(true)
expect(wrapper.find('.el-icon-close').exists()).toBe(true)
})
it('点击关闭按钮', async () => {
const showClose = true
const wrapper = mount(Notification, {
props: {
showClose
}
})
const closeBtn = wrapper.get('.el-notification__closeBtn')
await closeBtn.trigger('click')
expect(wrapper.get('.el-notification').isVisible()).toBe(false)
})
it('持续时间之后自动管理', async () => {
jest.useFakeTimers()
const wrapper = mount(Notification, {
props: {
duration: 1000
}
})
jest.runTimersToTime(1000)
await flushPromises()
expect(wrapper.get('.el-notification').isVisible()).toBe(false)
})
7.树
- expaned 显示是否展开
- checked 用来决定复选框选中列表
<el-tree
:data="data"
show-checkbox
v-model:expanded="expandedList"
v-model:checked="checkedList"
:defaultNodeKey="defaultNodeKey"
>
</el-tree>
<script>
export default {
data() {
return {
expandedList: [4, 5],
checkedList: [5],
data: [
{
id: 1,
label: '一级 1',
children: [
{
id: 4,
label: '二级 1-1',
children: [
{
id: 9,
label: '三级 1-1-1'
},
{
id: 10,
label: '三级 1-1-2'
}
]
}
]
},
{
id: 2,
label: '一级 2',
children: [
{
id: 5,
label: '二级 2-1'
},
{
id: 6,
label: '二级 2-2'
}
]
}
],
defaultNodeKey: {
childNodes: 'children',
label: 'label'
}
}
}
}
</script>
初始化树节点
//输入
4
/ \
2 7
/ \ / \
1 3 6 9
//输出
4
/ \
7 2
/ \ / \
9 6 3 1
//节点的构造函数
/**
* Definition for a binary tree node.
*/
function TreeNode(val, left, right) {
this.val = (val===undefined ? 0 : val)
this.left = (left===undefined ? null : left)
this.right = (right===undefined ? null : right)
}
递归替换树
var invertTree = function(root) {
// 递归 终止条件
if(root==null) {
return root
}
// 递归的逻辑
[root.left, root.right] = [invertTree(root.right), invertTree(root.left)]
return root
}
tree.vue
<template>
<div class="el-tree">
<el-tree-node v-for="child in tree.root.childNodes" :node="child" :key="child.id"></el-tree-node>
</div>
</template>
<script>
import ElTreeNode from './TreeNode.vue'
const instance = getCurrentInstance()
const tree = new Tree(props.data, props.defaultNodeKey, {
asyncLoadFn: props.asyncLoadFn,
isAsync: props.async
})
const state = reactive({
tree
})
provide('elTree', instance)
useTab()
useExpand(props, state)
function useExpand(props, state) {
const instance = getCurrentInstance()
const { emit } = instance
if (props.defaultExpandAll) {
state.tree.expandAll()
}
watchEffect(() => {
emit('update:expanded', state.tree.expanded)
})
watchEffect(() => {
state.tree.setExpandedByIdList(props.expanded, true)
})
onMounted(() => {
state.tree.root.expand(true)
})
}
</script>
TreeNode.vue
<div
v-show="node.isVisable"
class="el-tree-node"
:class="{
'is-expanded': node.isExpanded,
'is-current': elTree.proxy.dragState.current === node,
'is-checked': node.isChecked,
}"
role="TreeNode"
ref="TreeNode"
:id="'TreeNode' + node.id"
@click.stop="onClickNode"
>
<div class="el-tree-node__content">
<span
:class="[
{ expanded: node.isExpanded, 'is-leaf': node.isLeaf },
'el-tree-node__expand-icon',
elTree.props.iconClass
]"
@click.stop="
node.isLeaf ||
(elTree.props.accordion ? node.collapse() : node.expand())
">
</span>
<el-checkbox
v-if="elTree.props.showCheckbox"
:modelValue="node.isChecked"
@update:modelValue="onChangeCheckbox"
@click="elTree.emit('check', node, node.isChecked, $event)"
>
</el-checkbox
<!-- 这里针对动态节点输出处理-->
<el-node-content
class="el-tree-node__label"
:node="node"
></el-node-content>
</div>
<div
class="el-tree-node__children"
v-show="node.isExpanded"
v-if="!elTree.props.renderAfterExpand || node.isRendered"
role="group"
:aria-expanded="node.isExpanded"
>
<el-tree-node
v-for="child in node.childNodes"
:key="child.id"
:node="child"
>
</el-tree-node>
</div>
</div>
//操作事件
<script>
import ElCheckbox from '../checkbox'
import ElNodeContent from './NodeContent'
import { TreeNode } from './entity/TreeNode'
import { inject } from 'vue'
const elTree = inject('elTree')
const onClickNode = (e) => {
!elTree.props.expandOnClickNode ||
props.node.isLeaf ||
(elTree.props.accordion ? props.node.collapse() : props.node.expand())
!elTree.props.checkOnClickNode ||
props.node.setChecked(undefined, elTree.props.checkStrictly)
elTree.emit('node-click', props.node, e)
elTree.emit('current-change', props.node, e)
props.node.isExpanded
? elTree.emit('node-expand', props.node, e)
: elTree.emit('node-collapse', props.node, e)
}
const onChangeCheckbox = (e) => {
props.node.setChecked(undefined, elTree.props.checkStrictly)
elTree.emit('check-change', props.node, e)
}
return {
elTree,
onClickNode,
onChangeCheckbox
}
</script>
动态渲染自定义内容
NodeContent.vue
<script>
import { TreeNode } from './entity/TreeNode'
import { inject, h } from 'vue'
export default {
name: 'ElNodeContent',
props: {
node: {
required: true,
type: TreeNode
}
},
render(ctx) {
const elTree = inject('elTree')
if (typeof elTree.slots.default === 'function') {
return elTree.slots.default({ node: ctx.node, data: ctx.node.data.raw })
} else if (typeof elTree.props.renderContent === 'function') {
return elTree.props.renderContent({
node: ctx.node,
data: ctx.node.data.raw
})
}
return h('span', ctx.node.label)
}
}
</script>
test2.vue 传入自定义插槽内容
<div class="custom-tree-container">
<div class="block">
<p>使用 render-content</p>
<el-tree
:data="data1"
show-checkbox
default-expand-all
:expand-on-click-node="false"
:render-content="renderContent"
>
</el-tree>
</div>
</div>
<script>
function renderContent({ node, data }) {
return (
<span class="custom-tree-node">
<span>{data.label}</span>
<span>
<el-button
size="mini"
type="text"
onClick={() => this.append(node, data)}
>
Append
</el-button>
<el-button
size="mini"
type="text"
onClick={() => this.remove(node, data)}
>
Delete
</el-button>
</span>
</span>
)
}
</script>
8.表格
html表格格式
<table>
<thead>
<tr>
<th>111</th>
<th>222</th>
</tr>
</thead>
<tbody>
<tr>
<td>333</td>
<td>444</td>
</tr>
<tr>
<td>555</td>
<td>666</td>
</tr>
</tbody>
</table>
columns + data 版本
<el-table-column />标签 + data版本
<el-table :data="tableData" border style="width: 100%">
<el-table-column fixed prop="date" label="日期" width="150">
</el-table-column>
<el-table-column prop="name" label="姓名" width="120"> </el-table-column>
<el-table-column prop="province" label="省份" width="120"> </el-table-column>
<el-table-column prop="city" label="市区" width="120"> </el-table-column>
<el-table-column prop="address" label="地址" width="300"> </el-table-column>
<el-table-column prop="zip" label="邮编" width="120"> </el-table-column>
<el-table-column fixed="right" label="操作" width="100">
<template v-slot="scope">
<el-button @click="handleClick(scope.row)" type="text" size="small"
>查看</el-button
>
<el-button type="text" size="small">编辑</el-button>
</template>
</el-table-column>
</el-table>
<script>
export default {
methods: {
handleClick(row) {
console.log(row)
}
},
data() {
return {
tableData: [
{
date: '2016-05-02',
name: '王小虎',
province: '上海',
city: '普陀区',
address: '上海市普陀区金沙江路 1518 弄',
zip: 200333
},
{
date: '2016-05-04',
name: '王小虎',
province: '上海',
city: '普陀区',
address: '上海市普陀区金沙江路 1517 弄',
zip: 200333
},
{
date: '2016-05-01',
name: '王小虎',
province: '上海',
city: '普陀区',
address: '上海市普陀区金沙江路 1519 弄',
zip: 200333
},
{
date: '2016-05-03',
name: '王小虎',
province: '上海',
city: '普陀区',
address: '上海市普陀区金沙江路 1516 弄',
zip: 200333
}
]
}
}
}
</script>
结构
el-table
- el-table-column
- el-table-body
- el-table-header
特殊处理
- hidden-columns 负责隐藏列的显示,并且通过 table-store 进行表格内部的状态管理。
- 每当 table 中的 table-store 被修改后,table-header、table-body 都需要重新渲染。
el-table
<template>
<div class="el-table">
<div class="hidden-columns" ref="hiddenColumns">
<slot></slot>
</div>
<div class="el-table__header-wrapper"
ref="headerWrapper">
<table-header ref="tableHeader"
:store="store">
</table-header>
</div>
<div class="el-table__body-wrapper"
ref="bodyWrapper">
<table-body :context="context"
:store="store">
</table-body>
</div>
</div>
</template>
setup(props) {
let table = getCurrentInstance()
const store = createStore(table, {
rowKey: props.rowKey,
defaultExpandAll: props.defaultExpandAll,
selectOnIndeterminate: props.selectOnIndeterminate,
// TreeTable 的相关配置
indent: props.indent,
lazy: props.lazy,
lazyColumnIdentifier: props.treeProps.hasChildren || 'hasChildren',
childrenColumnName: props.treeProps.children || 'children',
data: props.data
})
table.store = store
const layout = new TableLayout({
store: table.store,
table,
fit: props.fit,
showHeader: props.showHeader
})
table.layout = layout
const tableId = 'el-table_' + tableIdSeed++
table.tableId = tableId
table.state = {
isGroup,
resizeState,
doLayout,
debouncedUpdateLayout
}
return {
layout,
store,
handleHeaderFooterMousewheel,
handleMouseLeave,
tableId,
tableSize,
clearSort,
doLayout,
sort,
setDragVisible,
context: table
}
}
table-header
setup(props, { emit }) {
const instance = getCurrentInstance()
const parent = instance.parent
const storeData = parent.store.states
const filterPanels = ref({})
const {
tableLayout,
onColumnsChange,
onScrollableChange
} = useLayoutObserver(parent)
const hasGutter = computed(() => {
return !props.fixed && tableLayout.gutterWidth
})
onMounted(() => {
nextTick(() => {
const { prop, order } = props.defaultSort
const init = true
parent.store.commit('sort', { prop, order, init })
})
})
const {
handleHeaderClick,
handleHeaderContextMenu,
handleMouseDown,
handleMouseMove,
handleMouseOut,
handleSortClick,
handleFilterClick
} = useEvent(props, emit)
const {
getHeaderRowStyle,
getHeaderRowClass,
getHeaderCellStyle,
getHeaderCellClass
} = useStyle(props)
const { isGroup, toggleAllSelection, columnRows } = useUtils(props)
instance.state = {
onColumnsChange,
onScrollableChange
}
// eslint-disable-next-line
instance.filterPanels = filterPanels
return {
columns: storeData.columns,
filterPanels,
hasGutter,
onColumnsChange,
onScrollableChange,
columnRows,
getHeaderRowClass,
getHeaderRowStyle,
getHeaderCellClass,
getHeaderCellStyle,
handleHeaderClick,
handleHeaderContextMenu,
handleMouseDown,
handleMouseMove,
handleMouseOut,
handleSortClick,
handleFilterClick,
isGroup,
toggleAllSelection
}
}
render() {
return h(
'table',
{
border: '0',
cellpadding: '0',
cellspacing: '0',
class: 'el-table__header'
},
[
hColgroup(this.columns, this.hasGutter),
h(
'thead',
{
class: { 'is-group': this.isGroup, 'has-gutter': this.hasGutter }
},
this.columnRows.map((subColumns, rowIndex) =>{
..
}
}]
);
table-body渲染
render() {
return h(
'table',
{
class: 'el-table__body',
cellspacing: '0',
cellpadding: '0',
border: '0'
},
[
hColgroup(this.store.states.columns.value),
h('tbody', {}, [
data.reduce((acc, row) => {
return acc.concat(this.wrappedRowRender(row, acc.length))
}, []),
h(
ElTooltip,
{
modelValue: this.tooltipVisible,
content: this.tooltipContent,
manual: true,
effect: this.$parent.tooltipEffect,
placement: 'top'
},
{
default: () => this.tooltipTrigger
}
)
])
]
)
}
9.文档
yarn add -D vuepress@next
docs/README.md 配置首页
---
home: true
heroImage: /theme.png
title: 网站快速成型工具
tagline: 一套为开发者、设计师和产品经理准备的基于 Vue 3 的桌面端组件库
heroText: 网站快速成型工具
actions:
- text: 快速上手
link: /install
type: primary
- text: 项目简介
link: /button
type: secondary
features:
- title: 简洁至上
details: 以 Markdown 为中心的项目结构,以最少的配置帮助你专注于写作。
- title: Vue 驱动
details: 享受 Vue 的开发体验,可以在 Markdown 中使用 Vue 组件,又可以使用 Vue 来开发自定义主题。
- title: 高性能
details: VuePress 会为每个页面预渲染生成静态的 HTML,同时,每个页面被加载的时候,将作为 SPA 运行。
footer: powdered by vuepress and me
---
# 额外的信息
项目配置信息
module.exports = {
themeConfig:{
title:"title",
description:"vuepress搭建的文档",
logo:"/logo.svg",
navbar:[
{
link:"/",
text:"首页"
},{
link:"/install",
text:"安装"
},
]
}
}
docs/install.md
## 安装
### npm 安装
推荐使用 npm 的方式安装,它能更好地和 [webpack](https://webpack.js.org/) 打包工具配合使用。``shell
npm i element3 -S
``
### CDN
目前可以通过 xxxxxx
``html
<!-- 引入样式 -->
<link
rel="stylesheet"
href="https://xxxxx/index.css"
/>
<!-- 引入组件库 -->
<script src="xxxx"></script>
``
演示UI渲染的代码与下面具体代码共用一份
通过自定义 Markdown-loader 处理一份md生成ui和源代码
例子
:::demo
- ```html
<el-button type="text">文字按钮</el-button>
<el-button type="text" disabled>文字按钮</el-button>
- ```
:::
实现逻辑
- md.render解析代码,如果遇到:::demo + ::: ,进入特殊解析阶段
- 通过genInlineComponentText模拟vue动态解析组件成html
- 把源代码通过
<script>标签包裹输出
代码
md-loader/src/index.js
const { stripScript, stripTemplate, genInlineComponentText } = require('./util')
const md = require('./config')
module.exports = function (source) {
const content = md.render(source)
const startTag = '<!--element-demo:'
const startTagLen = startTag.length
const endTag = ':element-demo-->'
const endTagLen = endTag.length
let componenetsString = ''
let id = 0 // demo 的 id
const output = [] // 输出的内容
let start = 0 // 字符串开始位置
let commentStart = content.indexOf(startTag)
let commentEnd = content.indexOf(endTag, commentStart + startTagLen)
while (commentStart !== -1 && commentEnd !== -1) {
output.push(content.slice(start, commentStart))
const commentContent = content.slice(commentStart + startTagLen, commentEnd)
const html = stripTemplate(commentContent)
const script = stripScript(commentContent)
const demoComponentContent = genInlineComponentText(html, script)
const demoComponentName = `element-demo${id}`
output.push(`<template #source><${demoComponentName} /></template>`)
componenetsString += `${JSON.stringify(
demoComponentName
)}: ${demoComponentContent},`
// 重新计算下一次的位置
id++
start = commentEnd + endTagLen
commentStart = content.indexOf(startTag, start)
commentEnd = content.indexOf(endTag, commentStart + startTagLen)
}
// 仅允许在 demo 不存在时,才可以在 Markdown 中写 script 标签
// todo: 优化这段逻辑
let pageScript = ''
if (componenetsString) {
pageScript = `<script>
import hljs from 'highlight.js'
import * as Vue from "vue"
export default {
name: 'component-doc',
components: {
${componenetsString}
}
}
</script>`
} else if (content.indexOf('<script>') === 0) {
// 硬编码,有待改善
start = content.indexOf('</script>') + '</script>'.length
pageScript = content.slice(0, start)
}
output.push(content.slice(start))
return `
<template>
<section class="content element-doc">
${output.join('')}
</section>
</template>
${pageScript}
`
}
md-loader/src/util.js
// const { compileTemplate } = require('@vue/component-compiler-utils');
// const compiler = require('vue-template-compiler');
const compiler = require('@vue/compiler-dom') // 模板
function stripScript(content) {
const result = content.match(/<(script)>([\s\S]+)<\/\1>/)
return result && result[2] ? result[2].trim() : ''
}
function stripStyle(content) {
const result = content.match(/<(style)\s*>([\s\S]+)<\/\1>/)
return result && result[2] ? result[2].trim() : ''
}
// 编写例子时不一定有 template。所以采取的方案是剔除其他的内容
function stripTemplate(content) {
content = content.trim()
if (!content) {
return content
}
content = content.replace(/<(script|style)[\s\S]+<\/\1>/g, '').trim()
// 过滤<template>
const ret = content.match(/<(template)\s*>([\s\S]+)<\/\1>/)
return ret ? ret[2].trim() : content
}
function genInlineComponentText(template, script) {
const compiled = compiler.compile(template, { prefixIdentifiers: true })
const code = compiled.code.replace(/return\s+?function\s+?render/, () => {
return 'function render '
})
let demoComponentContent = `
${code}
`
// todo: 这里采用了硬编码有待改进
script = script.trim()
if (script) {
script = script
.replace(/export\s+default/, 'const democomponentExport =')
.replace(/import ([,{}\w\s]+) from (['"\w]+)/g, function (s0, s1, s2) {
if (s2 === `'vue'`) {
return `
const ${s1} = Vue
`
} else if (s2 === `'element3'`) {
return `
const ${s1} = Element3
`
}
})
} else {
script = 'const democomponentExport = {}'
}
demoComponentContent = `(function() {
${demoComponentContent}
${script}
return {
mounted(){
this.$nextTick(()=>{
const blocks = document.querySelectorAll('pre code:not(.hljs)')
Array.prototype.forEach.call(blocks, hljs.highlightBlock)
})
},
render,
...democomponentExport
}
})()`
return demoComponentContent
}
module.exports = {
stripScript,
stripStyle,
stripTemplate,
genInlineComponentText
}
md-loader/src/containers.js
下面把模拟vue渲染的html 渲染到demo-block结构里面
const mdContainer = require('markdown-it-container')
module.exports = (md) => {
md.use(mdContainer, 'demo', {
validate(params) {
return params.trim().match(/^demo\s*(.*)$/)
},
render(tokens, idx) {
const m = tokens[idx].info.trim().match(/^demo\s*(.*)$/)
if (tokens[idx].nesting === 1) {
const description = m && m.length > 1 ? m[1] : ''
const content =
tokens[idx + 1].type === 'fence' ? tokens[idx + 1].content : ''
return `<demo-block>
${description ? `<div>${md.render(description)}</div>` : ''}
<!--element-demo: ${content}:element-demo-->
`
}
return '</demo-block>'
}
})
md.use(mdContainer, 'tip')
md.use(mdContainer, 'warning')
}
自定义demo-block.vue ,支持查看/隐藏源码
<!-- DemoBlock.vue -->
<template>
<div class="demo-block">
<div class="source">
<slot name="source"></slot>
</div>
<div class="meta" ref="meta">
<div class="description" v-if="$slots.default">
<slot></slot>
</div>
<div class="highlight">
<slot name="highlight"></slot>
</div>
</div>
<div
class="demo-block-control"
ref="control"
@click="isExpanded = !isExpanded"
>
<span>{{ controlText }}</span>
</div>
</div>
</template>
<script>
import { ref, computed, watchEffect, onMounted } from 'vue'
export default {
setup() {
const meta = ref(null)
const isExpanded = ref(false)
const controlText = computed(() =>
isExpanded.value ? '隐藏代码' : '显示代码'
)
const codeAreaHeight = computed(() =>
[...meta.value.children].reduce((t, i) => i.offsetHeight + t, 56)
)
onMounted(() => {
watchEffect(() => {
meta.value.style.height = isExpanded.value
? `${codeAreaHeight.value}px`
: '0'
})
})
return {
meta,
isExpanded,
controlText
}
}
}
</script>
md-loader/src/fence.js 匹配策略
// 覆盖默认的 fence 渲染策略
module.exports = (md) => {
const defaultRender = md.renderer.rules.fence
md.renderer.rules.fence = (tokens, idx, options, env, self) => {
const token = tokens[idx]
// 判断该 fence 是否在 :::demo 内
const prevToken = tokens[idx - 1]
const isInDemoContainer =
prevToken &&
prevToken.nesting === 1 &&
prevToken.info.trim().match(/^demo\s*(.*)$/)
if (token.info === 'html' && isInDemoContainer) {
return `<template #highlight><pre v-pre><code class="html">${md.utils.escapeHtml(
token.content
)}</code></pre></template>`
}
return defaultRender(tokens, idx, options, env, self)
}
}