这是我第一次对vue3+ts进行实践,写的比较菜。组件的使用方式是模仿element plus的。内容有点长,挑需要的看就好...
1. Input 输入框
2. CheckBox 复选框
3. RadioGroup 单选框
4. Form和FormItem 表单(主要用于排版)
5. Table和TableColumn 表格
6. Upload 上传
7. Message 提示
8. Icon 图标
源码和示例都在这个仓库中gitee.com/chenxiangzh… 可以直接安装运行查看部分demos:
1. Input
使用
<template>
<s-input
@update="updateForm"
name="ip"
:value="state.ip"
:rules="[
{ type: 'required', message: 'ip不能为空' },
{ type: 'ip', message: 'ip格式不对' },
]"
/>
</template>
<script lang="ts" setup>
import { reactive } from "vue"
import { SInput } from "@/components"
const state = reactive({
ip: ""
})
const updateForm = (param) => {
state[param.name] = param.value
}
</script>
效果
封装
types
// 属性值
type InputProp={
type: string, // 输入框类型
name: string,
value: string|number|boolean,
placeholder?: string,
rules?: RuleProp[], // 校验规则
inputStyle: Object // 输入框样式
}
// 校验类型:每个校验类型会对应一个校验函数。
type RuleKeys = "required" | "ip" | "port" | "range";
interface IRuleStrategies {
[index in RuleKeys]: (...params: any[]) => boolean;
[index: string]: (...params: any[]) => boolean;
}
// 校验对象
interface RuleProp {
type: RuleKeys;
message: string;
}
SInput.vue
<template>
<div class="input-container">
<input
:type="type"
class="input-control"
@blur="testError"
:value="value"
@input="input"
:class="{ 'is-invalid': inputRef.error }"
:placeholder="placeholder"
:style="{...inputStyle }"
/>
<div v-if="inputRef.error" class="error-text">
{{ inputRef.message }}
</div>
</div>
</template>
<script lang="ts" setup>
import { PropType, reactive } from "vue"
import { RuleProp } from "@types"
import { ruleStrategies } from "@/utils"
const emitFn = defineEmits(["update"])
const props = defineProps({
type: {
type: String,
default: "text"
},
name: String,
value: [String, Number, Boolean],
placeholder: String,
rules: Array as PropType<RuleProp[]>,
inputStyle: Object
})
// 响应式错误信息
const inputRef = reactive({
error: false,
message: ""
})
// 检查错误
const testError = () => {
if (props.rules) {
const allPassed = props.rules.every((rule: RuleProp) => {
let passed = true
inputRef.message = rule.message
passed = ruleStrategies[rule.type](props.value) // 根据校验类型判断是否通过
return passed
})
inputRef.error = !allPassed
}
}
// 传回数据
const input = (e: Event) => {
const targetValue = (e.target as HTMLInputElement).value
emitFn("update", { name: props.name, value: targetValue }) // 传回一个对象
}
</script>
<style lang="less">
.input-container {
.input-control{
padding: 4px 10px;
border-radius: @smallRadius;
border: 1px solid @borderColor;
align-items: center;
font-size: 1em;
&:focus{
border: 1px solid @activeColor;
outline: 2px solid @mainColor;
}
}
.error-text{
padding:6px 0 0 1px;
color:@danger;
text-align:left;
}
}
</style>
校验规则(通用)
import {IRuleStrategies} from '@types'
// 判断是否为空
const isRequired=(val:string):boolean=>{
return val!== ""
}
// 判断是否为整数
const isInteger=(value:any)=>{
return Number.isInteger(Number(value))
}
// 判断是否为ip
const isIp=(ip:string):boolean=>{
var rep = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/;
return rep.test(ip)
}
// 判断端口是否合法
const isPort=(port:number):boolean=>{
if(!isInteger(port)||65535<port||port<1){
return false;
}
return true;
}
// 判断整数范围
const isInRange:number,max:number=20,min:number=-10):boolean=>{
if(!isInteger(value)||value>max||min<-10){
return false
}
return true
}
// 导出校验策略
export const ruleStrategies:IRuleStrategies={
"required":isRequired,
"ip":isIp,
"port":isPort,
"range":isInRange
}
2. CheckBox
使用
<s-checkbox
:checked="state.remember"
name="remember"
@update="updateForm"
label="记住密码"
/>
<script lang="ts" setup>
import { SCheckbox } from "@/components"
// ...
const updateForm = (param:BaseForm) => {
state[param.name] = param.value
}
</script>
封装
<template>
<div class="s-checkbox">
<input type="checkbox" id="checkbox" :checked="props.checked" @input="update">
<label for="checkbox">{{ props.label }}</label>
</div>
</template>
<script lang="ts" setup>
const emit = defineEmits(["update"])
// 支持传入 checked name label
const props = defineProps({
checked: {
type: Boolean,
default: false
},
name: String,
label: {
type: String,
default: ""
}
})
const update = (e:Event) => {
const targetChecked = (e.target as HTMLInputElement).checked
emit("update", { name: props.name, value: targetChecked }) // 传出名称与布尔值
}
</script>
3. RadioGroup
支持横向分布与竖直分布,通过layout改变
使用
<template>
<s-radio-group
name="status"
:options="statusOptions"
layout="column"
:value="state.status"
@update="updateForm"
/>
</template>
<script lang="ts" setup>
import { SRadioGroup } from "@/components"
// ...
const statusOptions = [
{ label: "启用", value: 1 },
{ label: "禁用", value: 0 }
]
const updateForm = (params: BaseForm) => {
state[params.name]: Number(params.value)
}
</script>
封装
<template>
<div :class="['s-radio-group',{column:props.layout==='column'},{row:props.layout==='row'}]">
<div class="s-radio" v-for="item in props.options" :key="item.key" @click="onChange">
<input
type="radio"
:id="item.key"
:value="item.value"
:checked="props.value === item.value"
/>
<label :for="item.key" :data-value="item.value">{{ item.label }}</label>
</div>
</div>
</template>
<script lang="ts" setup>
// 单选框
type Radio = {
label: string;
value: string | number;
key?: string;
};
type RadioOptions = Array<Radio>;
import { PropType } from "vue"
const props = defineProps({
options: Array as PropType<RadioOptions>, // 选项
value: [String, Number], // 值
name: String, // 名称
layout: { // 布局方向
type: String,
default: "row"
}
})
const emit = defineEmits(["update"])
// 改变值
const onChange = (e: Event) => {
const target = (e.target as HTMLInputElement)
if (target.tagName === "LABEL") {
emit("update", { name: props.name, value: target.dataset.value })
} else if (target.tagName === "INPUT") {
emit("update", { name: props.name, value: target.value })
}
}
</script>
<style lang="less" scoped>
.column{
display: flex;
flex-direction: column;
margin-bottom: 10px;
}
.row{
display: grid;
grid-template-columns: 1fr 1fr;
}
.s-radio-group{
.s-radio {
input,label{
cursor: pointer;
}
line-height: @listHeight;
height: @listHeight;
margin-right: 10px;
white-space: nowrap;
}
}
</style>
4. Form和FormItem
这个组件主要用于表单的布局
使用
<s-form width="400px" :labelCol="2">
<s-form-item label="IP地址">
<s-input
:rules="[
{ type: 'required', message: 'ip不能为空' },
{ type: 'ip', message: 'ip格式不对' },
]"
:value="form.ip"
@update="updateForm"
name="ip"
/>
</s-form-item>
</s-form>
效果见Input的效果
封装
SForm.vue
<script lang="ts">
import { h } from "vue"
export default {
name: "SForm",
props: {
width: String, // 表单宽度
labelCol: Number // label所占宽度的比例
},
setup (props, context) {
if (!context.slots || !context.slots.default) return null
// 将form的属性传给formitem
const slots = context.slots.default().map(slot => ({
...slot,
props: {
...props, // 父组件form的属性
...slot.props // 子组件formitem的属性,如果有,会覆盖父组件form的属性(以增强子组件样式的优先级
}
}))
return () => h("div", {
className: "s-form"
}, slots)
}
}
</script>
<style lang="less">
.s-form{
.s-form-item{
margin-top: 10px
}
}
</style>
SFormItem.vue
<template>
<div class="s-form-item" :style="{width}">
<div class="label" :style="{width:labelWidth}">{{label?`${label}:`:' '}}</div>
<slot></slot>
</div>
</template>
<script lang="ts">
export default {
name: "s-form-item",
props: {
label: { // label名称
type: String,
default: ""
},
width: { // 占表格宽度的百分比
type: String,
default: "100%"
},
labelCol: { // label所占宽度
type: Number,
default: 1
}
},
setup (props: {labelCol:number, width:string}) {
const persents = ["10%", "20%", "30%", "40%", "50%", "60%", "80%", "90%", "100%"]
const labelWidth = persents[props.labelCol]
return {
labelWidth,
...props
}
}
}
</script>
<style lang="less" scoped>
.s-form-item{
display: flex;
align-items: baseline;
.label{
text-align: right;
}
}
</style>
5. Table和TableColumn
- 支持横向分布和纵向分布
- 可自定义内容 (这是我花时间最久的组件)
使用
<s-table :dataSource="dataSource" layout="horizon" :header-style="{width:'180px'}">
<s-table-column prop="name" label="/"/>
<s-table-column prop="status" label="采集通道状态">
<template #default="defaultProps">
<span v-if="defaultProps.value">已启用</span>
<span v-else class="danger-text">异常</span>
</template>
</s-table-column>
<s-table-column prop="mockInput" label="模拟音频输入">
<template #default="defaultProps">
{{MOCK_INPUT[defaultProps.value]}}
</template>
</s-table-column>
<s-table-column prop="audioHertz" label="音频采样率">
<template #default="defaultProps">
{{HERTZ[defaultProps.value]}}
</template>
</s-table-column>
<s-table-column prop="encodeRate" label="音频编码码率">
<template #default="defaultProps">
{{RATE[defaultProps.value]}}
</template>
</s-table-column>
<s-table-column prop="encodeFormat" label="音频编码格式"/>
<s-table-column prop="vol" label="音量"/>
<s-table-column prop="outputIp" label="输出地址"/>
<s-table-column prop="outputPort" label="输出端口"/>
</s-table>
<script lang="ts" setup>
const dataSource=[
{
"id": 1,
"name": "音频采集通道1",
"status": 0,
"mockInput": 0,
"audioHertz": 0,
"encodeRate": 1,
"encodeFormat": "MP2",
"outputIp": "192.168.42.10",
"outputPort": 8000,
"vol":0
},
{
"id": 2,
"name": "音频采集通道2",
"status": 1,
"mockInput": 1,
"audioHertz": 1,
"encodeRate": 2,
"encodeFormat": "MP3",
"outputIp": "192.168.42.108",
"outputPort": 89,
"vol":0
},
{
"id": 3,
"name": "音频采集通道3",
"status": 1,
"mockInput": 0,
"audioHertz": 1,
"encodeRate": 2,
"encodeFormat": "MP3",
"outputIp": "192.168.8.10",
"outputPort": 8080,
"vol":0
},
{
"id": 4,
"name": "音频采集通道4",
"status": 1,
"mockInput": 1,
"audioHertz": 0,
"encodeRate": 0,
"encodeFormat": "MP2",
"outputIp": "192.166.42.10",
"outputPort": 100,
"vol":0
}
]
</script>
效果
横向效果
竖向效果
封装
types
// Table 方向
type TableLayout = "horizon" | "column";
// Table 支持的头部样式
interface HeaderStyle {
width?: string;
height?: string;
textAlign?: TableAlign;
}
// Table 文本方向
type TableAlign = "left" | "center" | "right";
// 真实运用到dom的头部样式
interface RealHeaderStyle extends HeaderStyle {
minWidth?: string;
minHeight?: string;
lineHeight?: string;
flex?: number;
display?: string;
alignItems?: string;
justifyContent?: string;
[index: string]: string | number | undefined;
}
// 每一条数据的格式(根据需要自定义)
type TableCell = {
id: number | string;
[index: string]: any;
};
STable.vue
<script lang="ts">
import { h, PropType, reactive, watchEffect } from "vue"
import { TableCell, HeaderStyle, TableAlign } from "@types"
export default {
name: "STable",
props: {
// 数据源
dataSource: {
type: Array as PropType<TableCell[]>,
default: () => []
},
// 头部样式
headerStyle: Object as PropType<HeaderStyle>,
// 表格排列方向
layout: {
type: String,
default: "horizon"
},
// 文本布局
align: {
type: String as PropType<TableAlign>,
default: "left"
}
},
setup (props, context) {
if (!context.slots || !context.slots.default) return null
let slots = reactive<any[]>([])
watchEffect(() => {
if (!context.slots || !context.slots.default) return null
slots = context.slots.default().map((slot) => ({
...slot,
props: {
...props,
...slot.props
}
}))
})
// 根据不同的layout渲染不同的样式
if (props.layout === "column") {
return () =>
h(
"div",
{
className: "s-table table-layout-column"
},
slots
)
} else {
return () =>
h(
"div",
{
className: "s-table-wrap"
},
[h("div", { className: "s-table table-layout-horizon" }, slots)]
)
}
}
}
</script>
<style lang="less">
// 横向时样式,需要包裹一下
.s-table-wrap {
overflow: auto;
.s-table {
border-radius: @bigRadius;
&.table-layout-horizon {
display: grid;
.table-columns {
display: flex;
.table-cell {
flex: 1;
}
// 从第二行开始
&:nth-child(n + 2) {
.table-title,
.table-cell {
border-right: 1px dotted @borderColor;
border-bottom: 1px dotted @borderColor;
}
.table-title {
background-color: @default;
}
}
&:first-child {
.table-title {
color: @white;
}
.table-cell {
background-color: @default;
border-bottom: 1px dotted @borderColor;
&:nth-child(n + 3) {
border-left: 1px dotted @borderColor;
}
}
}
}
}
}
}
// 竖向时样式
.s-table {
overflow-x: auto;
border-radius: @bigRadius;
&.table-layout-column {
display: flex;
.table-columns {
display: flex;
flex-direction: column;
flex-basis: 100px;
&:nth-child(n + 3) {
.table-cell {
border-top: 1px dotted @borderColor;
}
}
.table-title {
white-space: nowrap;
border-bottom: 1px dotted @borderColor;
background-color: @default;
position: relative;
}
.table-cell {
flex: 1;
}
}
}
}
</style>
STableColumn.vue
<template>
<ul class="table-columns" :key="prop">
<li :class="['table-title',textAlign]" :key="prop" :style="{...style}">{{label}}</li>
<li :class="['table-cell',textAlign]" v-for="data in dataSource" :key="data.id">
<slot name="default" :data="data" :value="data[prop]">
{{data[prop]}} <!-- 后备数据 -->
</slot>
</li>
</ul>
</template>
<script lang='ts'>
import { PropType, ref } from "vue"
import { TableCell, HeaderStyle, RealHeaderStyle, TableLayout, TableAlign } from "@types"
export default {
props: {
// Table传过来的数据源
dataSource: {
type: Array as PropType<TableCell[]>,
default: () => []
},
// 列对应的字段
prop: {
type: String,
default: ""
},
// 列名
label: {
type: String,
require: true
},
// Table传过来的头部样式
headerStyle: Object as PropType<HeaderStyle>,
layout: {
type: String as PropType<TableLayout>,
default: "horizon"
},
// 列宽(优先级高
width: {
type: String,
default: ""
},
// 列的文字布局(优先级高
align: {
type: String as PropType<TableAlign>,
default: "left"
}
},
setup (props, context) {
const style = ref<RealHeaderStyle>({})
const textAlign = ref(props.align)
const colWidth = ref(props.width)
// 头部的默认样式与自定义样式
const HS = props.headerStyle || undefined
// 【头左体右】表格样式
if (props.layout === "horizon") {
style.value.minWidth = "180px" // 默认样式
if (HS) {
style.value = {
...style.value,
...HS, // 如果table传了样式,覆盖默认样式
}
}
} else {
// 【头上体下】表格样式(正常方向表格)
if (HS) {
style.value = { ...HS }
if (HS.width) {
style.value.minWidth = HS.width // 避免头部的宽度小于内容宽度
}
if (HS.height) {
style.value.minHeight = HS.height
style.value.lineHeight = HS.height
}
}
style.value.minWidth = colWidth.value // 如果列传了宽度,覆盖表格传的样式
}
return {
style,
textAlign
}
}
}
</script>
<style lang="less">
.table-cell,.table-title{
padding: @itemSpace;
}
.table-columns{
.center{
text-align: center;
}
.left{
text-align: left;
}
.right{
text-align: right;
}
}
</style>
6. Upload
使用
<s-upload @onSuccess="onSucessUpload" :showFiles="true" action=" ">
<s-button :disabled="version.isUpdate">浏览</s-button>
</s-upload>
效果
封装
types
// 文件上传限制
type Limit={
size?:number, // 文件大小 单位M
maxFiles?:number, // 文件数量
[index:string]:string|number|undefined
}
// 文件状态
enum FILE_STATUS{
EMPTY=0,
SUCCESS=1,
ERROR=2,
UPLOADING=3
}
// 组件状态
type State={
fileData:any[]|object,// 当前文件
fileStatus:FILE_STATUS, // 文件上传状态
fileList:FileList|[], // 文件列表
fileIndex:number // 文件列表的处理索引
}
// Upload属性
type UploadProp={
action?: string, // 上传链接
initFile?: Array<any> | Object,// 初始文件
accept?: string | Array<string>,// 允许上传的格式
limit?: Limit, // 上传限制
multiple?:boolean,// 是否允许多选,
beforeUpload: (files:FileList)=>boolean,// 上传前处理函数
showFiles: boolean, // 是否显示文件信息
help: string// 辅助信息
}
<template>
<div class="upload-container">
<!-- 上传触件 -->
<div class="trigger-container" @click="onUpload">
<input
class="hidden"
ref="fileUploader"
type="file"
:multiple="multiple"
:accept="acceptType"
@change="fileChange"
/>
<slot></slot>
</div>
<!-- 提示信息 -->
<div v-if="help" class="file-help">
{{help}}
</div>
<!-- 文件信息 -->
<ul class="files-container" v-if="showFiles">
<li v-for="file in fileList" :key="file.name" class="sspace-vertical">
<s-icon icon="icon-file" type="symbol"/>
<span class="sspace-horizon">{{file.name}}</span>
</li>
</ul>
</div>
</template>
<script lang='ts'>
import $axios from "@/request"
import { PropType, computed, ref, watch, reactive, toRefs } from "vue"
import { Message, SIcon } from "@/components"
import { Limit, FILE_STATUS, State} from "@types"
export default {
name: "s-upload",
components: { SIcon },
props: {
// 上传连接
action: String,
// 初始文件
initFile: {
type: [Array, Object],
default: null
},
// 允许上传的格式
accept: {
type: [String, Array],
default: "image/*"
},
// 上传限制
limit: Object as PropType<Limit>,
// 是否允许多选
multiple: {
type: Boolean,
default: false
},
// 上传前处理函数
beforeUpload: Function as PropType<(files:FileList)=>boolean>,
// 是否显示文件信息
showFiles: {
type: Boolean,
default: false
},
// 辅助信息
help: String
},
emits: ["onSuccess", "onError"],
setup (props, context) {
const fileUploader = ref<null | HTMLInputElement>(null)
const acceptType = computed(() => {
if (typeof props.accept !== "string") {
if (Array.isArray(props.accept)) {
return props.accept.join()
} else {
console.error("accept接收字符串或数组,请输入正确的格式")
}
}
return props.accept
})
const state = reactive<State>({
fileData: props.initFile,
fileStatus: FILE_STATUS.ERROR,
fileList: [],
fileIndex: 0
})
// 监听是否有初始文件
watch(() => props.initFile, (val) => {
if (val) {
state.fileStatus = FILE_STATUS.SUCCESS
state.fileData = val
}
})
const onUpload = (e:Event) => {
if (fileUploader.value) {
fileUploader.value.click()
}
}
// 自定义验证 处理beforeUploadu
const customCheck = async (files:FileList) => {
return new Promise((resolve, reject) => {
if (props.beforeUpload) {
const result = props.beforeUpload(files)
if (typeof result !== "boolean") {
reject(new Error("beforeUploadu应该返回一个布尔值"))
}
resolve(result)
} else {
resolve(true)
}
})
}
// 文件大小验证
const sizeCheck = (files:FileList) => {
return new Promise((resolve, reject) => {
const { size } = props.limit
if (size) {
let index = 0
while (index < files.length) {
const file = files[index]
const fileSize = file.size / 1024
if (fileSize > size) {
const msg = `${file.name}文件大小超出${size}K,请重新调整!`
Message.error(msg)
reject(new Error(msg))
}
index++
}
resolve(true)
}
resolve(true)
})
}
// 文件数量验证
const lengthCheck = (files:FileList) => {
return new Promise((resolve, reject) => {
const { maxFiles } = props.limit
if (maxFiles) {
console.log(files.length, maxFiles)
if (files.length > maxFiles) {
const msg = `文件数量不得超过${maxFiles}个`
Message.error(msg)
reject(new Error(msg))
}
resolve(true)
}
resolve(true)
})
}
// 处理上传文件
const fileChange = async (e:Event) => {
const target = e.target as HTMLInputElement
const files = target.files
if (files && file.length) {
// 上传前验证
await customCheck(files)
if (props.limit) {
await sizeCheck(files)
await lengthCheck(files)
}
// 本地 不上传到服务器时,直接传回
if (!props.action) {
context.emit("onSuccess", files)
state.fileList = files
state.fileStatus = FILE_STATUS.SUCCESS
} else {
state.fileStatus = FILE_STATUS.UPLOADING
state.fileList = files
state.fileIndex = 0
uploadFile(state.fileList[state.fileIndex])
}
}
}
// 上传文件
const uploadFile = async (file:File) => {
try {
const fd = new FormData()
fd.append("file", file)
const data = await $axios.upload(props.action, fd)
if (data) {
await isFinish()
} else {
throw new Error(`${file.name}在上传过程中发生错误,上传中止`)
}
} catch (err) {
state.fileStatus = FILE_STATUS.ERROR
state.fileList = []
context.emit("onError",`${file.name}在上传过程中发生错误,上传中止`)
} finally {
state.fileIndex = 0
}
state.fileStatus = FILE_STATUS.SUCCESS
context.emit("onSuccess", state.fileList)
state.fileList = []
}
// 遍历所有文件
const isFinish = () => {
return new Promise((resolve, reject) => {
// 如果有多个文件
if (props.multiple && state.fileList.length > 1) {
// 判断当前文件索引和文件列表长度
if (state.fileIndex < state.fileList.length - 1) {
state.fileIndex++
uploadFile(state.fileList[state.fileIndex])
} else {
resolve(FILE_STATUS.SUCCESS)
}
} else {
resolve(FILE_STATUS.SUCCESS)
}
})
}
return {
acceptType,
fileChange,
onUpload,
fileUploader,
...toRefs(state),
...toRefs(props)
}
}
}
</script>
<style scoped lang="less">
.upload-container{
display: flex;
flex-direction: column;
.file-help{
color:@help;
margin: 10px 0;
font-size: 0.85em;
}
.files-container{
cursor: default;
}
}
</style>
7. Message
使用
import { Message } from '@/components'
Message.success("Login successful!")
效果
封装
首先定义一些常量
// 停留时间
const MESSAGE_TIMEOUT: number = 3000;
// message样式
const MESSAGE_STYLE: IMessageStyle = {
warn: {
icon: "icon-warn-fill",
color: "#E6A23C",
backgroundColor: "#fff7e6",
borderColor: "#ffe7ba",
},
error: {
icon: "icon-error-fill",
color: "#F56C6C",
backgroundColor: "#fff1f0",
borderColor: "#ffccc7",
},
success: {
icon: "icon-success-fill",
color: "#67C23A",
backgroundColor: "#f6ffed",
borderColor: "#d9f7be",
},
info: {
icon: "icon-info-fill",
color: "#40a9ff",
backgroundColor: "#e6f7ff",
borderColor: "#bae7ff",
},
};
SMessage.vue
<template>
<transition name="fade">
<div class="s-message" :style="MESSAGE_STYLE[props.type]" v-if="isShow">
<s-icon :icon="MESSAGE_STYLE[props.type].icon" />
<span class="text">{{ props.text }}</span>
</div>
</transition >
</template>
<script lang="ts" setup>
import { MESSAGE_TIMEOUT, MESSAGE_STYLE } from "@/utils"
import { SIcon } from "@/components"
import { ref, onMounted } from "vue"
const props = defineProps({
text: {
type: String,
default: ""
},
type: {
type: String,
default: "warn" // warn 警告 error 错误 success 成功
},
timeout: {
type: Number,
default: MESSAGE_TIMEOUT
}
})
const isShow = ref<boolean>(false)
onMounted(() => {
isShow.value = true
setTimeout(() => {
isShow.value = false
}, props.timeout)
})
</script>
<style scoped lang="less">
.fade-enter-active{
animation: fade .5s;
}
.fade-leave-active {
animation: fade .5s reverse;
}
// /* 定义帧动画 */
@keyframes fade {
0% {
opacity: 0;
transform: translateY(-50px);
}
100% {
opacity: 1;
}
}
.s-message {
min-width: 300px;
max-width: 350px;
padding: 12px @itemSpace;
position: fixed;
z-index: 9999;
left: 50%;
margin-left: -150px;
top: 25px;
border-radius: 4px;
.text {
vertical-align: middle;
}
}
</style>
SMessage\index.ts
import { MESSAGE_TIMEOUT } from "@/utils"
import { createVNode, render } from 'vue'
import SMessage from './SMessage.vue'
const div = document.createElement('div')
// 添加到body上
document.body.appendChild(div)
// 定时器标识
let timer: any = null
// 渲染虚拟dom
const renderMessage=(vnode:any)=>{
render(vnode, div)
clearTimeout(timer)
timer = setTimeout(() => {
render(null, div)
}, MESSAGE_TIMEOUT)
}
export default {
error:(msg:string)=>{
const vnode = createVNode(SMessage, { type:'error', text:msg })
renderMessage(vnode)
},
warn:(msg:string)=>{
const vnode = createVNode(SMessage, { type:'warn', text:msg })
renderMessage(vnode)
},
success:(msg:string)=>{
const vnode = createVNode(SMessage, { type:'success', text:msg })
renderMessage(vnode)
},
info:(msg:string)=>{
const vnode = createVNode(SMessage, { type:'info', text:msg })
renderMessage(vnode)
},
}
8. Icon
使用
<s-icon icon="icon-file" type="symbol"/>
封装
这里使用的icon均自于www.iconfont.cn/,icon组件支持的写…
- Font class
- Symbol
选择好icon后,将代码粘贴进去
同时在入口文件导入
SIcon.vue
<template>
<i :class="[props.icon, 'iconfont']" v-if="type==='font-class'"></i>
<svg class="icon" aria-hidden="true" v-else>
<use :xlink:href="`#${props.icon}`"></use>
</svg>
</template>
<script lang="ts" setup>
const props = defineProps({
icon: {
type: String,
required: true
},
type: {
type: String,
default: "font-class"
}
})
</script>
<style lang="less" scoped>
i {
margin-right: 4px;
vertical-align: middle;
}
</style>