😊你好,我是小航,一个正在变秃、变强的文艺倾年。
😊好久没有更新有关前端实战教程了,本文主要讲解【全栈实战】基于 Vue3 + Wot Design Uni 动手封装组件!
😊这个教程你将会学到技术正确的选型、Vue3基础语法实践、父子组件传参、移动端组件封装、组件源码阅读技巧、小程序适配技巧等,一起卷起来吧!
目录
- 一、前言
- Wot Design Uni
- 技术选型
- UI库选型
- 开源热度对比
- 多端支持情况
- 组件数量
- ts 支持情况
- 发展趋势
- Tyme
- 语言支持情况
- 活跃程度
- 发展趋势
- 使用教程
- 二、封装
- 思路
- 父组件
- 子组件
- 初始化
- 样式搭建
- 工具封装
- 常量定义
- 逻辑设计
- 解决微信小程序 Tab 动画异常
- 效果展示
- 三、完整代码
- 代码
- 下一步建议
一、前言
由于本人近期需要一个支持输入农历、公历的移动端组件
,仅需要输入年、月、日、时,输入界面要求可以同时选择四列,且自由切换农历、公历。网上没找到现成的组件,只好自己封装一个了。下面先介绍一下自己的技术选型,本人开发采用的语言是Vue3 + Typescript
。
Wot Design Uni
技术选型
Uniapp的上手难度很低,而且Uniapp的语法风格与Vue保持高度一致,可以直接复用已有的Vue项目中的代码和经验。因此我这里使用的开发框架是Uniapp。
UI库选型
对于UI库的选型,我们常常从开源热度、多端支持情况、组件数量、ts支持情况、发展趋势来选择。
开源热度对比
截止到 2024-05-30
发表文章时的数据:
UI 框架 | uv-ui | uview-plus | wot-ui | TuniaoUI |
---|---|---|---|---|
github stars | 568 | 362 | 492 | 192 |
gitee stars | 555 | 126 | 35 | - |
github forks | 1.1k | 158 | 188 | 20 |
gitee forks | 75 | 4 | 30 | - |
实到这里就一决高下了,github star 数
: uv-ui(568)
> wot-ui(492)
> uview-plus(362)
> TuniaoUI(192)
,其中 uv-ui
和 wot-ui
拔得头筹。
多端支持情况
UI 框架 | uv-ui | uview-plus | wot-ui | TuniaoUI |
---|---|---|---|---|
h5 | ✅ | ✅ | ✅ | ✅ |
app(ios) | ✅ | ✅ | ✅ | ✅ |
app(android) | ✅ | ✅ | ✅ | ✅ |
微信小程序 | ✅ | ✅ | ✅ | ✅ |
支付宝小程序 | ✅ | ✅ | ✅ | ✅ |
QQ 小程序 | ✅ | ✅ | ❌ | ❌ |
百度小程序 | ✅ | ✅ | ❌ | ❌ |
头条小程序 | ✅ | ✅ | ❌ | ❌ |
组件数量
UI 框架 | uv-ui | uview-plus | wot-ui | TuniaoUI |
---|---|---|---|---|
总数 | 67 | 67 | 71 | 55 |
基础组件 | 8 | 11 | 8 | 5 |
表单组件 | 16 | 17 | 20 | 14 |
数据组件 | 13 | 4 | 18 | 4 |
反馈组件 | 8 | 10 | 16 | 8 |
布局组件 | 7 | 9 | - | 8 |
导航组件 | 8 | 8 | 9 | 5 |
其他组件 | 7 | 8 | - | 5 |
内容组件 | - | - | - | 6 |
组件数:wot(71)
> uv-ui(67)
= uview-plus(67)
> TuniaoUI(55)
ts 支持情况
查看 4 个组件库的源码,可以了解到:
uv-ui
和uView-plus
都是js
写的,并非ts
,可以通过ttou/uv-typings
提供类型支持。wot
和TuniaoUI
都是ts
写的,编码体验会好很多。
小知识:代码里如何辨别一个库是否有 ts 支持,写代码的时候按 ctrl + i
(Mac 里 cmd + i
),如果有提示就是有,啥都没有就是没有。
举个例子,编写 <xx-button type="" ...
,在 type=""
双引号里面按 ctrl + i
,看提示就知道了。
wot
有提示
uv-ui
无提示
发展趋势
wot-ui和
uv-ui皇城
PK
目前 wot-ui
还是比不过 uv-ui
的,但是 wot-ui
有反超的势头。
这里我选择了wot-ui。wot-ui 全称 wot-design-uni,是 wot-design 的 uniapp 版本,文档地址:https://wot-design-uni.netlify.app/
Tyme
Tyme是一个非常强大的日历工具库,可以看作 Lunar 的升级版,拥有更优的设计和扩展性,支持公历和农历、星座、干支、生肖、节气、法定假日等。这有助与我们进行农历和公历之间的转化。
开源地址:https://github.com/6tail/tyme4ts
语言支持情况
看着就很棒,支持了市面了主流的语言。
活跃程度
近两周刚更新源代码,说明出了问题能解决。我们在采用别人封装好的库时候,一定要多看看仓库活跃程度,如果这个仓库都好久没有更新维护了,不建议使用。
发展趋势
没毛病,就它了!
使用教程
// install
npm init -y
npm i typescript -D
npm i ts-node -D
npm i tyme4ts// test.ts
import {SolarDay} from 'tyme4ts';const solar: SolarDay = SolarDay.fromYmd(1986, 5, 29);// 1986年5月29日
console.log(solar.toString());// 农历丙寅年四月廿一
console.log(solar.getLunarDay().toString());// run
ts-node test.ts
二、封装
思路
示例数据:
// 公历2024年12月22日13时
birthdayValue: [2024, 12, 22, 13, 1]
用到的组件:
Input、Picker、Popup
父组件
<birthdayPickerv-model="state.userInfo.birthdayValue"prop="birthdayValue":rules="[{required: false,message: '请输入生辰',},]"></birthdayPicker>// 定义变量
const state = reactive({// 用户信息数据userInfo: {birthdayValue: null,},
})
子组件
初始化
Tips:便于调试,推荐onload加入debugger,可以直接看到小程序源代码是否刷新,小程序经常不刷新代码,经常误认为自己的编码问题,也是比较恼火。
onLoad(() => {debugger
})
定义组件名称
defineOptions({name: 'birthdayPicker',
})
定义接收父组件传值的接口
type bithdayPickerProps = {modelValue: number[] // 生日值disabled?: boolean // 是否禁用prop: string // 表单域 model 字段名,在使用表单校验功能的情况下,该属性是必填的rules: FormItemRule[] // 表单校验规则
}
const props = defineProps<bithdayPickerProps>()
const emit = defineEmits(['update:modelValue'])
定义用到的状态变量
const state = reactive({displayColumns: [],popupShow: false,birthdayArr: ['农历', '公历'],birthday: [],birthdayIndex: 0,pickerViewLoaded: false,
})// 计算Picker展示值
const displayBirthday = computed(() => {const birthday = state.birthday// 避免空数组调用join函数if (isNullBirthdayArray(birthday, 4)) return null// 公历2024年12月22日13时return (state.birthdayArr[state.birthdayIndex] +birthday[0] +'年' +findValueInCoulums(birthday[1], state.displayColumns[1]) +findValueInCoulums(birthday[2], state.displayColumns[2]) +birthday[3] +'时')
})
这里有个常见的面试考点:
如何在 ref() 与 reactive() 之间做正确选择?
(1)ref() 和 reactive() 用于跟踪其参数的更改。当使用它们初始化变量时,是向 Vue 提供信息:“嘿,每次这些变量发生更改时,请重新构建或重新运行依赖于它们的所有内容”。
(2)ref() 函数可以接受原始类型(最常见的是布尔值、字符串和数字)以及对象作为参数,而 reactive() 函数只能接受对象作为参数。
(3)ref() 有一个 .value 属性,你必须使用 .value 属性获取内容,但是使用 reactive() 的话可以直接访问。
(4)使用 ref() 函数可以替换整个对象实例,但是在使用 reactive() 函数时就不行。
(5)如果你喜欢组件内部状态仅有一个变量,那么 reactive() 就是适合你的正确工具。本人更喜欢使用reactive,看着比较舒服。
样式搭建
编写组件样式:
<template><wd-popupv-model="state.popupShow"position="bottom"custom-style="border-radius:32rpx;height: 300px;":close-on-click-modal="false"@close="closeBirthdayPicker"closable@after-enter="handleOpened"><wd-tabs v-model="state.birthdayIndex" animated swipeable ref="tabsRef" @change="onChangeTab"><wd-tab v-for="(item, index) in state.birthdayArr" :key="index" :title="item"><wd-picker-view:columns="state.displayColumns"v-model="state.birthday":column-change="onChangeColumn"ref="birthdayPicker"/></wd-tab></wd-tabs></wd-popup><div @click="openBirthdayPicker"><wd-input:prop="props.prop":rules="props.rules"type="text"label="生辰"v-model="displayBirthday"placeholder="请选择"readonlyrequiredplaceholder-style="color:#bfbfbf"><template #suffix><!--箭头--><wd-icon name="arrow-right" color="rgba(0, 0, 0, 0.25)" size="18px" /></template></wd-input></div>
</template>
基础的打开、关闭
// 打开生日选择器
const openBirthdayPicker = () => {if (props.disabled) returnstate.popupShow = trueconst value = props.modelValueif (isNullBirthdayArray(value, 5)) {state.birthday = [1940, 1, 1, 1]state.birthdayIndex = 0return}state.birthday = [value[0], value[1], value[2], value[3]]state.birthdayIndex = value[4]
}// 关闭生日选择器
const closeBirthdayPicker = () => {emit('update:modelValue', [...state.birthday, state.birthdayIndex])state.popupShow = false
}
工具封装
由于我这里会经常对bithday数组进行判空,判断依据有:是否有空值、长度是否满足,是否是数组等,这里我们封装成一个函数
const isNullBirthdayArray = (birthday: number[], validLength: number = birthday.length) => {return (!birthday ||(isArray(birthday) &&(!birthday.length ||birthday.length !== validLength ||birthday.every((item) => item === null))))
}
Picker值展示时需要根据birthday的值进行与当前columns映射
const findValueInCoulums = (value: number, columns: any[]) => {let res = ''columns.forEach((item) => {if (item.value === value) res = item.label})return res
}
常量定义
// 定义农历月份和日期的汉字表示
const chineseMonths = ['一月','二月','三月','四月','五月','六月','七月','八月','九月','十月','十一月','十二月',
]
const chineseDays = ['初一','初二','初三','初四','初五','初六','初七','初八','初九','初十','十一','十二','十三','十四','十五','十六','十七','十八','十九','二十','廿一','廿二','廿三','廿四','廿五','廿六','廿七','廿八','廿九','三十',
]
根据常量来更新展示列信息:
const yearColumns = Array.from({ length: new Date().getFullYear() - 1940 + 1 }, (_, i) => {return { label: `${1940 + i}`, value: 1940 + i }
})
const hourColumns = Array.from({ length: 24 }, (_, i) => ({label: `${i + 1}时`,value: i + 1,
}))
const initDays = (len: number, type: 'lunar' | 'gregorian') =>Array.from({ length: len }, (_, i) => {return {label: type === 'lunar' ? `${chineseDays[i]}` : `${i + 1}日`,value: i + 1,}})
const updateColumns = (birthday: number[]) => {let monthColumns = []let dayColumns = []const [_year, _month, _day, _hour, birthdayIndex] = birthday// 1.农历列if (birthdayIndex === 0) {const lunarYear: LunarYear = LunarYear.fromYear(_year)monthColumns = lunarYear.getMonths().map((month) => ({label: month.isLeap()? `闰${chineseMonths[month.getMonth() - 1]}`: chineseMonths[month.getMonth() - 1],value: month.getMonthWithLeap(),}))const lunarMonth: LunarMonth = LunarMonth.fromYm(_year, _month)const dayCount: number = lunarMonth.getDayCount()dayColumns = initDays(dayCount, 'lunar')}// 2.公历列if (birthdayIndex === 1) {monthColumns = Array.from({ length: 12 }, (_, i) => {return { label: `${chineseMonths[i]}`, value: i + 1 }})const solarMonth: SolarMonth = SolarMonth.fromYm(_year, _month)const dayCount: number = solarMonth.getDayCount()dayColumns = initDays(dayCount, 'gregorian')}state.displayColumns = [yearColumns, monthColumns, dayColumns, hourColumns]
}
逻辑设计
初始化列
// 1.初始化
onMounted(() => {handleShowValueUpdate(props.modelValue)
})// 2.监听父组件的值变化
watch(() => props.modelValue,(newValue) => {handleShowValueUpdate(newValue)},
)// 3.值变更时更新显示内容
const handleShowValueUpdate = (value: number[]) => {// 0.校验if (isNullBirthdayArray(value, 5)) {// 更新为初始值,状态值保持不变updateColumns([1940, 1, 1, 1, 0])return}// 1.获取选中项state.birthday = [value[0], value[1], value[2], value[3]]state.birthdayIndex = value[4]// 2.更新列updateColumns(value)
}// 4.当农历、公历切换时:
const onChangeTab = (index) => {state.pickerViewLoaded = trueconst [_year, _month, _day, _hour] = state.birthday// 1.农历转公历if (index.name === 1) {console.log('农历转公历前', index.name, state.birthday)const lunarHour = LunarHour.fromYmdHms(_year, _month, _day, _hour, 0, 0)const solarTime: SolarTime = lunarHour.getSolarTime()state.birthday = [solarTime.getYear(),solarTime.getMonth(),solarTime.getDay(),solarTime.getHour(),]console.log('农历转公历后', index.name, state.birthday)}// 2.公历转农历if (index.name === 0) {console.log('公历转农历前', index.name, state.birthday)const solarTime = SolarTime.fromYmdHms(_year, _month, _day, _hour, 0, 0)const lunarHour = solarTime.getLunarHour()state.birthday = [lunarHour.getYear(),lunarHour.getMonth(),lunarHour.getDay(),lunarHour.getHour(),]console.log('公历转农历后', index.name, state.birthday)}// 3.更新Columns列,注意此时state.birthdayIndex还未更新updateColumns([...state.birthday, index.name])
}// 5.更新列
const updateColumns = (birthday: number[]) => {let monthColumns = []let dayColumns = []const [_year, _month, _day, _hour, birthdayIndex] = birthday// 1.农历列if (birthdayIndex === 0) {const lunarYear: LunarYear = LunarYear.fromYear(_year)monthColumns = lunarYear.getMonths().map((month) => ({label: month.isLeap()? `闰${chineseMonths[month.getMonth() - 1]}`: chineseMonths[month.getMonth() - 1],value: month.getMonthWithLeap(),}))const lunarMonth: LunarMonth = LunarMonth.fromYm(_year, _month)const dayCount: number = lunarMonth.getDayCount()dayColumns = initDays(dayCount, 'lunar')}// 2.公历列if (birthdayIndex === 1) {monthColumns = Array.from({ length: 12 }, (_, i) => {return { label: `${chineseMonths[i]}`, value: i + 1 }})const solarMonth: SolarMonth = SolarMonth.fromYm(_year, _month)const dayCount: number = solarMonth.getDayCount()dayColumns = initDays(dayCount, 'gregorian')}state.displayColumns = [yearColumns, monthColumns, dayColumns, hourColumns]
}
当用户滑动列后进行更新picker
const onChangeColumn = (pickerView, value, columnIndex, resolve) => {// debugger// 1.更新birthday值const item = value[columnIndex]state.birthday[columnIndex] = Number(item.value)// 2.滑动列后重置后面的列for (let i = columnIndex + 1; i < 4; i++) {state.birthday[i] = 1}// 3.更新Columns列updateColumns([...state.birthday, state.birthdayIndex])// 4.更新pickerViewif (columnIndex === 0) {pickerView.setColumnData(1, state.displayColumns[1])pickerView.setColumnData(2, state.displayColumns[2])pickerView.setColumnData(3, state.displayColumns[3])} else if (columnIndex === 1) {pickerView.setColumnData(2, state.displayColumns[2])pickerView.setColumnData(3, state.displayColumns[3])} else if (columnIndex === 2) {pickerView.setColumnData(3, state.displayColumns[3])}// 5.更新pickerView组件resolve()
}
这里我进行了后列重置,避免出现后面的日期不存在于当月,造成列Index错误。
解决微信小程序 Tab 动画异常
// 解决微信小程序Tabs表现异常
const tabsRef = ref<TabsInstance>()
function handleOpened() {tabsRef.value?.updateLineStyle(false)
}
效果展示
选择农历后:
农历切换到公里后:
测试闰月:
测试校验规则
三、完整代码
代码
<template><wd-popupv-model="state.popupShow"position="bottom"custom-style="border-radius:32rpx;height: 300px;":close-on-click-modal="false"@close="closeBirthdayPicker"closable@after-enter="handleOpened"><wd-tabs v-model="state.birthdayIndex" animated swipeable ref="tabsRef" @change="onChangeTab"><wd-tab v-for="(item, index) in state.birthdayArr" :key="index" :title="item"><wd-picker-view:columns="state.displayColumns"v-model="state.birthday":column-change="onChangeColumn"ref="birthdayPicker"/></wd-tab></wd-tabs></wd-popup><div @click="openBirthdayPicker"><wd-input:prop="props.prop":rules="props.rules"type="text"label="生辰"v-model="displayBirthday"placeholder="请选择"readonlyrequiredplaceholder-style="color:#bfbfbf"><template #suffix><!--箭头--><wd-icon name="arrow-right" color="rgba(0, 0, 0, 0.25)" size="18px" /></template></wd-input></div>
</template><script lang="ts" setup>
import { LunarYear, LunarMonth, LunarHour, SolarTime, SolarMonth } from 'tyme4ts'
import { isArray } from 'wot-design-uni/components/common/util'
import { reactive, computed, watch, ref, defineProps, defineEmits, defineOptions } from 'vue'
import { FormItemRule } from 'wot-design-uni/components/wd-form/types'defineOptions({name: 'birthdayPicker',
})onLoad(() => {debugger
})// 解决微信小程序Tabs表现异常
const tabsRef = ref<TabsInstance>()
function handleOpened() {tabsRef.value?.updateLineStyle(false)
}// 定义农历月份和日期的汉字表示
const chineseMonths = ['一月','二月','三月','四月','五月','六月','七月','八月','九月','十月','十一月','十二月',
]
const chineseDays = ['初一','初二','初三','初四','初五','初六','初七','初八','初九','初十','十一','十二','十三','十四','十五','十六','十七','十八','十九','二十','廿一','廿二','廿三','廿四','廿五','廿六','廿七','廿八','廿九','三十',
]// 1.状态管理
const state = reactive({displayColumns: [],popupShow: false,birthdayArr: ['农历', '公历'],birthday: [],birthdayIndex: 0,pickerViewLoaded: false,
})// 2.定义组件接收值
type bithdayPickerProps = {modelValue: number[] // 生日值disabled?: boolean // 是否禁用prop: string // 表单域 model 字段名,在使用表单校验功能的情况下,该属性是必填的rules: FormItemRule[] // 表单校验规则
}
const props = defineProps<bithdayPickerProps>()
const emit = defineEmits(['update:modelValue'])// 3.计算Picker展示值
const findValueInCoulums = (value: number, columns: any[]) => {let res = ''columns.forEach((item) => {if (item.value === value) res = item.label})return res
}
const displayBirthday = computed(() => {const birthday = state.birthday// 避免空数组调用join函数if (isNullBirthdayArray(birthday, 4)) return null// 公历2024年12月22日13时return (state.birthdayArr[state.birthdayIndex] +birthday[0] +'年' +findValueInCoulums(birthday[1], state.displayColumns[1]) +findValueInCoulums(birthday[2], state.displayColumns[2]) +birthday[3] +'时')
})
const isNullBirthdayArray = (birthday: number[], validLength: number = birthday.length) => {return (!birthday ||(isArray(birthday) &&(!birthday.length ||birthday.length !== validLength ||birthday.every((item) => item === null))))
}
// 4.初始化
onMounted(() => {handleShowValueUpdate(props.modelValue)
})// 5.监听父组件的值变化
watch(() => props.modelValue,(newValue) => {handleShowValueUpdate(newValue)},
)// 6.值变更时更新显示内容
const handleShowValueUpdate = (value: number[]) => {// 0.校验if (isNullBirthdayArray(value, 5)) {// 更新为初始值,状态值保持不变updateColumns([1940, 1, 1, 1, 0])return}// 1.获取选中项state.birthday = [value[0], value[1], value[2], value[3]]state.birthdayIndex = value[4]// 2.更新列updateColumns(value)
}// 7.更新Columns:年份列、月份列、天数列、小时列
const yearColumns = Array.from({ length: new Date().getFullYear() - 1940 + 1 }, (_, i) => {return { label: `${1940 + i}`, value: 1940 + i }
})
const hourColumns = Array.from({ length: 24 }, (_, i) => ({label: `${i + 1}时`,value: i + 1,
}))
const initDays = (len: number, type: 'lunar' | 'gregorian') =>Array.from({ length: len }, (_, i) => {return {label: type === 'lunar' ? `${chineseDays[i]}` : `${i + 1}日`,value: i + 1,}})const updateColumns = (birthday: number[]) => {let monthColumns = []let dayColumns = []const [_year, _month, _day, _hour, birthdayIndex] = birthday// 1.农历列if (birthdayIndex === 0) {const lunarYear: LunarYear = LunarYear.fromYear(_year)monthColumns = lunarYear.getMonths().map((month) => ({label: month.isLeap()? `闰${chineseMonths[month.getMonth() - 1]}`: chineseMonths[month.getMonth() - 1],value: month.getMonthWithLeap(),}))const lunarMonth: LunarMonth = LunarMonth.fromYm(_year, _month)const dayCount: number = lunarMonth.getDayCount()dayColumns = initDays(dayCount, 'lunar')}// 2.公历列if (birthdayIndex === 1) {monthColumns = Array.from({ length: 12 }, (_, i) => {return { label: `${chineseMonths[i]}`, value: i + 1 }})const solarMonth: SolarMonth = SolarMonth.fromYm(_year, _month)const dayCount: number = solarMonth.getDayCount()dayColumns = initDays(dayCount, 'gregorian')}state.displayColumns = [yearColumns, monthColumns, dayColumns, hourColumns]
}// 8.当用户滑动列后触发
const onChangeColumn = (pickerView, value, columnIndex, resolve) => {// debugger// 1.更新birthday值const item = value[columnIndex]state.birthday[columnIndex] = Number(item.value)// 2.滑动列后重置后面的列for (let i = columnIndex + 1; i < 4; i++) {state.birthday[i] = 1}// 3.更新Columns列updateColumns([...state.birthday, state.birthdayIndex])// 4.更新pickerViewif (columnIndex === 0) {pickerView.setColumnData(1, state.displayColumns[1])pickerView.setColumnData(2, state.displayColumns[2])pickerView.setColumnData(3, state.displayColumns[3])} else if (columnIndex === 1) {pickerView.setColumnData(2, state.displayColumns[2])pickerView.setColumnData(3, state.displayColumns[3])} else if (columnIndex === 2) {pickerView.setColumnData(3, state.displayColumns[3])}// 5.更新pickerView组件resolve()
}// 9.当农历、公历切换时:
const onChangeTab = (index) => {state.pickerViewLoaded = trueconst [_year, _month, _day, _hour] = state.birthday// 1.农历转公历if (index.name === 1) {console.log('农历转公历前', index.name, state.birthday)const lunarHour = LunarHour.fromYmdHms(_year, _month, _day, _hour, 0, 0)const solarTime: SolarTime = lunarHour.getSolarTime()state.birthday = [solarTime.getYear(),solarTime.getMonth(),solarTime.getDay(),solarTime.getHour(),]console.log('农历转公历后', index.name, state.birthday)}// 2.公历转农历if (index.name === 0) {console.log('公历转农历前', index.name, state.birthday)const solarTime = SolarTime.fromYmdHms(_year, _month, _day, _hour, 0, 0)const lunarHour = solarTime.getLunarHour()state.birthday = [lunarHour.getYear(),lunarHour.getMonth(),lunarHour.getDay(),lunarHour.getHour(),]console.log('公历转农历后', index.name, state.birthday)}// 3.更新Columns列,注意此时state.birthdayIndex还未更新updateColumns([...state.birthday, index.name])
}// 打开生日选择器
const openBirthdayPicker = () => {if (props.disabled) returnstate.popupShow = trueconst value = props.modelValueif (isNullBirthdayArray(value, 5)) {state.birthday = [1940, 1, 1, 1]state.birthdayIndex = 0return}state.birthday = [value[0], value[1], value[2], value[3]]state.birthdayIndex = value[4]
}// 关闭生日选择器
const closeBirthdayPicker = () => {emit('update:modelValue', [...state.birthday, state.birthdayIndex])state.popupShow = false
}
</script><style lang="scss" scoped></style>
下一步建议
近期本人时间有限,暂不做改进,大佬们可以拿去做完善。
- 缺少 取消和确认 的操作。
- 组件不够通用,需要暴漏常用的函数和变量。
📌 [ 笔者 ] 文艺倾年
📃 [ 更新 ] 2024.12.22
❌ [ 勘误 ] /* 暂无 */
📜 [ 声明 ] 由于作者水平有限,本文有错误和不准确之处在所难免,本人也很想知道这些错误,恳望读者批评指正!