提交 b8da064c 作者: 方治民

refactor: 采用工厂模式重构 Widget Hook 使用方法

上级 c933db27
import { getBooleanOrDefault } from '../../utils'
import { getBasicWidgetInstanceFunctions, getBooleanOrDefault, registerFactory } from '../../utils'
import type { BottomBarInstance, BottomBarProps } from './types'
import { isProdMode } from '/@/utils/env'
// 组件名称
export const name = 'BottomBarWidget'
/**
* 底部交互/展示小部件响应式 Hook
* @param props 组件参数
* @returns 组件响应式数据
*/
export function useBottomBarWidget<T extends BottomBarInstance>(props: BottomBarProps): [(instance: T) => void, T] {
const instanceRef = ref<Nullable<T>>(null)
const height = computed(() => instanceRef?.value?.height)
function register(instance: T) {
if (isProdMode()) {
if (instance === unref(instanceRef)) {
return
}
onUnmounted(() => {
instanceRef.value = null
})
}
instanceRef.value = instance
props && instance.setProps(props)
}
function getInstance(): T {
const instance = unref(instanceRef)
if (!instance) {
console.warn('BottomBar 小部件还未加载完成')
}
return instance as T
}
export function useBottomBarWidget<T extends BottomBarInstance, P extends BottomBarProps>(
props: P,
): [(instance: T) => void, BottomBarInstance] {
const instanceRef = ref<T>()
const register = registerFactory(instanceRef, props)
const { get, setProps, toggleShow } = getBasicWidgetInstanceFunctions(instanceRef, name)
return [
register,
{
height,
setProps: (props: BottomBarProps) => {
getInstance()?.setProps(props)
},
toggleShow: (show?: boolean) => {
getInstance()?.toggleShow(getBooleanOrDefault(show))
},
toggleExpand: (expand?: boolean) => {
getInstance()?.toggleExpand(getBooleanOrDefault(expand))
setProps,
toggleShow,
toggleExpand: (expand?: boolean) => get()?.toggleExpand(getBooleanOrDefault(expand)),
height: computed(() => instanceRef?.value?.height) as unknown as ComputedRef<string>,
},
} as T,
]
}
export interface BottomBarProps {
// 是否显示
show?: boolean
import type { BasicWidgetInstance, BasicWidgetProps } from '../../utils'
export interface BottomBarProps extends BasicWidgetProps {
// 是否展开
expand?: boolean
// 是否有展开按钮
showExpandButton?: boolean
// 高度 rpx
height?: number
height: number
// 最大高度 rpx
maxHeight?: number
}
export interface BottomBarInstance {
setProps: (props: BottomBarProps) => void
toggleShow: (show?: boolean) => void
export interface BottomBarInstance extends BasicWidgetInstance<BottomBarProps> {
height: ComputedRef<string>
toggleExpand: (expand?: boolean) => void
height?: ComputedRef<string>
}
import { getBooleanOrDefault } from '../../utils'
import { getBasicWidgetInstanceFunctions, getBooleanOrDefault, registerFactory } from '../../utils'
import type { LegendInstance, LegendProps, Option } from './types'
import { isProdMode } from '/@/utils/env'
// 组件名称
export const name = 'LegendWidget'
/**
* 图例组件响应式 Hook
* @param props 组件参数
* @returns 组件响应式数据
*/
export function useLegendWidget<T extends LegendInstance>(props: LegendProps): [(instance: T) => void, T] {
const instanceRef = ref<Nullable<T>>(null)
function register(instance: T) {
if (isProdMode()) {
if (instance === unref(instanceRef)) {
return
}
onUnmounted(() => {
instanceRef.value = null
})
}
instanceRef.value = instance
props && instance?.setProps(props)
}
function getInstance(): T {
const instance = unref(instanceRef)
if (!instance) {
console.warn('图例小部件还未加载完成')
}
return instance as T
}
export function useLegendWidget<T extends LegendInstance, P extends LegendProps>(
props: P,
): [(instance: T) => void, LegendInstance] {
const instanceRef = ref<T>()
const register = registerFactory(instanceRef, props)
const { get, setProps, toggleShow } = getBasicWidgetInstanceFunctions(instanceRef, name)
return [
register,
{
setProps: (props: LegendProps) => {
getInstance()?.setProps(props)
},
setTitle: (title: string) => {
getInstance()?.setTitle(title)
},
setOption: (option: Option) => {
getInstance()?.setOption(option)
},
toggleShow: (show?: boolean) => {
getInstance()?.toggleShow(getBooleanOrDefault(show))
},
toggleExpand: (expand?: boolean) => {
getInstance()?.toggleExpand(getBooleanOrDefault(expand))
setProps,
toggleShow,
setTitle: (title: string) => get()?.setTitle(title),
setOption: (option: Option) => get()?.setOption(option),
toggleExpand: (expand?: boolean) => get()?.toggleExpand(getBooleanOrDefault(expand)),
},
} as T,
]
}
import type { BasicWidgetInstance, BasicWidgetProps } from '../../utils'
export interface OptionItem {
// 颜色
color?: string
......@@ -18,9 +20,7 @@ export interface Option {
blockStyle?: Recordable
}
export interface LegendProps {
// 是否显示
show?: boolean
export interface LegendProps extends BasicWidgetProps {
// 是否展开
expand?: boolean
// 标题
......@@ -29,10 +29,8 @@ export interface LegendProps {
option: Option
}
export interface LegendInstance {
setProps: (props: LegendProps) => void
export interface LegendInstance extends BasicWidgetInstance<LegendProps> {
setTitle: (title: string) => void
setOption: (option: Option) => void
toggleShow: (show?: boolean) => void
toggleExpand: (expand?: boolean) => void
}
import { getBooleanOrDefault } from '../../utils'
import { getBasicWidgetInstanceFunctions, registerFactory } from '../../utils'
import type { SwitchControlInstance, SwitchControlProps } from './types'
import { isProdMode } from '/@/utils/env'
// 组件名称
export const name = 'SwitchControlWidget'
/**
* 前后切换组件响应式 Hook
* @param props 组件参数
* @returns 组件响应式数据
*/
export function useSwitchControlWidget<T extends SwitchControlInstance>(
props: SwitchControlProps,
): [(instance: T) => void, T] {
const instanceRef = ref<Nullable<T>>(null)
function register(instance: T) {
if (isProdMode()) {
if (instance === unref(instanceRef)) {
return
}
onUnmounted(() => {
instanceRef.value = null
})
}
instanceRef.value = instance
props && instance?.setProps(props)
}
function getInstance(): T {
const instance = unref(instanceRef)
if (!instance) {
console.warn('前后切换小部件还未加载完成')
}
return instance as T
}
export function useSwitchControlWidget<T extends SwitchControlInstance, P extends SwitchControlProps>(
props: P,
): [(instance: T) => void, SwitchControlInstance] {
const instanceRef = ref<T>()
const register = registerFactory(instanceRef, props)
const { get, setProps, toggleShow } = getBasicWidgetInstanceFunctions(instanceRef, name)
return [
register,
{
setProps: (props: SwitchControlProps) => {
getInstance()?.setProps(props)
},
toggleShow: (show?: boolean) => {
getInstance()?.toggleShow(getBooleanOrDefault(show))
},
prev: () => {
getInstance()?.prev()
},
next: () => {
getInstance()?.next()
setProps,
toggleShow,
prev: () => get()?.prev(),
next: () => get()?.next(),
},
} as T,
]
}
export interface SwitchControlProps {
// 是否显示
show?: boolean
import type { BasicWidgetInstance, BasicWidgetProps } from '../../utils'
export interface SwitchControlProps extends BasicWidgetProps {
// 上一个
prev?: () => void
// 下一个
next?: () => void
}
export interface SwitchControlInstance {
export interface SwitchControlInstance extends BasicWidgetInstance<SwitchControlProps> {
// 暴露方法,允许手动调用上一个
prev: () => void
// 暴露方法,允许手动调用下一个
next: () => void
setProps: (props: SwitchControlProps) => void
toggleShow: (show?: boolean) => void
}
<!-- 顶部时间栏 Bar 组件 -->
<script setup lang="ts">
import type { Dayjs } from 'dayjs'
import dayjs from 'dayjs'
import type { TimeBarButton, TimeBarLabel, TimeBarTime } from './types'
import { formatTime } from './hook'
......@@ -25,7 +26,10 @@
// 标签
label: {
type: Object as PropType<TimeBarLabel>,
default: () => ({}) as TimeBarLabel,
default: () =>
({
text: '时间',
}) as TimeBarLabel,
},
// 时间
time: {
......@@ -47,9 +51,6 @@
label: props.label,
time: props.time,
buttons: props.buttons,
// 组件自身的数据
showDropdownMenu: false,
})
function toggleShow(show?: boolean) {
......@@ -61,62 +62,67 @@
if (!data?.time?.value) {
const time = dayjs()
data.time.value = [time.subtract(1, 'days'), time]
data.time.value = data.time.type === 'range' ? [time.subtract(1, 'days'), time] : [time]
}
}
const labelText = computed(() => {
let text: string
if (data.label.options?.length) {
text = checkedLabelOption.value?.text
} else {
text = data.label.text
function getTime(): Dayjs[] {
return toRaw(data.time.value)
}
return text || ''
})
function setTime(value: Dayjs[]) {
data.time.value = value
}
const timeText = computed(() => {
let text: string
if (data.time) {
const format = checkedLabelOption.value?.format || data.time.format
text = data.time.value.map((item) => dayjs(item).format(format)).join(' - ')
function getCheckedOption() {
return toRaw(checkedLabelOption.value)
}
return text || ''
})
function setCheckedOption(index: number) {
onLabelOptionChange((_, i) => i === index)
}
const timePickerType = computed(() => {
let type: number
if (data.time) {
type = checkedLabelOption.value?.timeType || data.time.timeType
function getTimeBarValue() {
return {
option: getCheckedOption(),
value: getTime(),
}
}
return type || 3
const labelText = computed(() => checkedLabelOption.value?.text || data.label.text || '')
const timePickerType = computed(() => checkedLabelOption.value?.timeType || data.time?.timeType || 3)
const timeText = computed(() => {
const format = checkedLabelOption.value?.format || data.time?.format
return data.time.value.map((item) => dayjs(item).format(format)).join(' - ') || ''
})
// ================== Option 下拉选择相关开始 ===================
const dropdownMenu = ref(null)
const showDropdownMenu = ref(false)
const checkedLabelOption = computed(() => data.label?.options?.find((option) => option.checked) || {})
function changeLabelOption(option: TimeBarLabel['options'][0]) {
data.showDropdownMenu = false
data.label.options.forEach((item) => {
item.checked = item.value === option.value
showDropdownMenu.value = false
onLabelOptionChange((item) => item.value === option.value)
}
function onLabelOptionChange(cb: (e: TimeBarLabel['options'][0], i: number) => boolean) {
data.label.options.forEach((item, index) => {
item.checked = cb(item, index)
if (item.checked) {
data.label?.onChange?.({
option: { text: item.text, value: item.value },
value: toRaw(data.time.value),
})
data.label?.onChange?.(getTimeBarValue())
}
})
}
function openDropdownMenu() {
data.showDropdownMenu = true
dropdownMenu.value?.show()
if (data.readonly) {
return
}
function closeDropdownMenu() {
data.showDropdownMenu = false
showDropdownMenu.value = true
dropdownMenu.value?.show()
}
// ================== Option 下拉选择相关结束 ===================
// ================== TimePicker 选择相关开始 ===================
const showTimePicker = ref(false)
function changeTime(e: Recordable) {
if (e.startDate) {
......@@ -124,16 +130,28 @@
} else {
data.time.value = [dayjs(e.result)]
}
data.time.onChange?.({
option: { text: checkedLabelOption.value.text, value: checkedLabelOption.value.value },
value: toRaw(data.time.value),
})
showTimePicker.value = false
}
function openTimePicker() {
if (!data.readonly) {
showTimePicker.value = true
}
}
watch(
() => data.time.value,
() => data.time.onChange?.(getTimeBarValue()),
{ deep: true },
)
// ================== TimePicker 选择相关结束 ===================
emits('register', {
setProps,
toggleShow,
getTime,
setTime,
getCheckedOption,
setCheckedOption,
getTimeBarValue,
})
</script>
......@@ -153,12 +171,12 @@
selectedColor="#465CFF"
:options="data.label.options"
@click="changeLabelOption"
@close="closeDropdownMenu"
@close="showDropdownMenu = false"
ref="dropdownMenu"
>
<view class="fui-filter__item" @tap="openDropdownMenu">
<text>{{ checkedLabelOption.text }}</text>
<view class="fui-filter__icon" :class="{ 'fui-icon__ani': data.showDropdownMenu }">
<view class="fui-filter__icon" :class="{ 'fui-icon__ani': showDropdownMenu }">
<fui-icon name="turningdown" :size="32" />
</view>
</view>
......@@ -169,14 +187,35 @@
</template>
</view>
<view class="">
<view class="time-wrap" @tap="showTimePicker = true">
<!-- -->
<view class="time">{{ timeText }}</view>
<view class="flex flex-auto justify-between items-center">
<view class="time-wrap" :class="{ left: data.buttons?.length }" @tap="openTimePicker">
<view class="time" :class="{ readonly: data.readonly }">{{ timeText }}</view>
<Icon icon="ic-baseline-keyboard-arrow-right" size="40" color="#666" />
</view>
<view class="buttons" v-if="data.buttons?.length">
<!-- -->
<fui-button
bold
:size="24"
width="140rpx"
height="60rpx"
radius="8rpx"
color="#1890FF"
background="#E7F3FF"
v-for="(button, index) in data.buttons"
:key="`time_bar_button_${index}`"
@tap="button.onClick?.({ index, label: button.label }, getTimeBarValue())"
>
<fui-icon
v-if="button.icon"
:name="button.icon"
:size="28"
style="color: #1890ff !important; margin-right: 6rpx"
/>
<text>{{ button.label }}</text>
<Icon icon="ic-baseline-keyboard-arrow-right" size="32" color="#666" />
</fui-button>
</view>
</view>
......@@ -213,6 +252,7 @@
.time-bar {
padding: 20rpx;
box-sizing: border-box;
width: 100%;
display: flex;
align-items: center;
......@@ -267,10 +307,21 @@
}
.time-wrap {
display: flex;
justify-content: space-between;
flex: auto;
&.left {
justify-content: flex-start;
flex: none;
}
.time {
color: #1890ff;
&.readonly {
color: #666;
}
}
}
}
......
import type { Dayjs } from 'dayjs'
import { getBasicWidgetInstanceFunctions, registerFactory } from '../../utils'
import type { TimeBarInstance, TimeBarProps } from './types'
import { getBooleanOrDefault } from '../../utils'
import { isProdMode } from '/@/utils/env'
// 组件名称
export const name = 'TimeBarWidget'
/**
* 顶部时间栏组件响应式 Hook
* @param props 组件参数
* @returns 组件响应式数据
*/
export function useTimeBarWidget<T extends TimeBarInstance, P extends TimeBarProps>(
props: P,
): [(instance: T) => void, TimeBarInstance] {
const instanceRef = ref<T>()
const register = registerFactory(instanceRef, props)
const { get, setProps, toggleShow } = getBasicWidgetInstanceFunctions(instanceRef, name)
return [
register,
{
setProps,
toggleShow,
getTime: () => get()?.getTime(),
setTime: (time: Dayjs[]) => get()?.setTime(time),
getCheckedOption: () => get()?.getCheckedOption(),
setCheckedOption: (index: number) => get()?.setCheckedOption(index),
},
]
}
/**
* 格式化时间
* @param time 时间
* @param format 格式化
* @returns 格式化后的时间,如果时间为空则返回空字符串
*/
export function formatTime(time: Dayjs, format = 'YYYY-MM-DD HH:mm:ss') {
return time?.format(format) ?? ''
}
/**
* 根据 type 属性返回对应的格式化字符串
* @param type fui-date-picker 组件的 type 属性
* @returns 根据 type 属性返回对应的格式化字符串
*/
export function getFormatByType(type: number) {
switch (type) {
case 1:
......@@ -16,56 +55,12 @@ export function getFormatByType(type: number) {
case 3:
return 'YYYY-MM-DD'
case 4:
return 'YYYY-MM-DD HH:00'
return 'YYYY-MM-DD HH:00:00'
case 5:
return 'YYYY-MM-DD HH:mm'
return 'YYYY-MM-DD HH:mm:00'
case 6:
return 'YYYY-MM-DD HH:mm:ss'
default:
return 'YYYY-MM-DD HH:mm:ss'
}
}
/**
* 顶部时间栏组件响应式 Hook
* @param props 组件参数
* @returns 组件响应式数据
*/
export function useTimeBarWidget<T extends TimeBarInstance>(props: TimeBarProps): [(instance: T) => void, T] {
const instanceRef = ref<Nullable<T>>(null)
function register(instance: T) {
if (isProdMode()) {
if (instance === unref(instanceRef)) {
return
}
onUnmounted(() => {
instanceRef.value = null
})
}
instanceRef.value = instance
props && instance?.setProps(props)
}
function getInstance(): T {
const instance = unref(instanceRef)
if (!instance) {
console.warn('时间栏小部件还未加载完成')
}
return instance as T
}
return [
register,
{
setProps: (props: TimeBarProps) => {
getInstance()?.setProps(props)
},
toggleShow: (show?: boolean) => {
getInstance()?.toggleShow(getBooleanOrDefault(show))
},
} as T,
]
}
import type { Dayjs } from 'dayjs'
import type { BasicWidgetInstance, BasicWidgetProps } from '../../utils'
/**
* 对应 first-ui datePicker 组件 type 参数
*/
type FirstUIDatePickerType = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8
export interface TimeBarChangeEvent {
value: Dayjs[]
option?: {
text?: string
value?: string
}
option?: TimeBarLabel['options'][0]
}
export interface TimeBarLabel {
......@@ -19,9 +20,6 @@ export interface TimeBarLabel {
value?: string
checked?: boolean
format?: string
/**
* 对应 first-ui datePicker 组件 type 参数
*/
timeType?: FirstUIDatePickerType
}[]
onClick?: () => void
......@@ -30,9 +28,6 @@ export interface TimeBarLabel {
export interface TimeBarTime {
type: 'single' | 'range'
/**
* 对应 first-ui datePicker 组件 type 参数
*/
timeType: FirstUIDatePickerType
format?: string
value?: Dayjs[]
......@@ -40,14 +35,15 @@ export interface TimeBarTime {
}
export interface TimeBarButton {
/**
* 图标,仅支持 fui-icon name
*/
icon?: string
label: string
onClick?: (e: { index: number; label: string }, time: string | { start?: string; end?: string }) => void
onClick?: (e: { index: number; label: string }, time: TimeBarChangeEvent) => void
}
export interface TimeBarProps {
// 是否显示
show?: boolean
export interface TimeBarProps extends BasicWidgetProps {
// 是否只读
readonly?: boolean
// 对齐方式
......@@ -60,7 +56,9 @@ export interface TimeBarProps {
buttons?: TimeBarButton[]
}
export interface TimeBarInstance {
setProps: (props: TimeBarProps) => void
toggleShow: (show?: boolean) => void
export interface TimeBarInstance extends BasicWidgetInstance<TimeBarProps> {
getTime: () => Dayjs[]
setTime: (time: Dayjs[]) => void
getCheckedOption: () => TimeBarChangeEvent
setCheckedOption: (index: number) => void
}
import { isBoolean } from 'lodash-es'
import { isProdMode } from '@/utils/env'
export interface BasicWidgetProps {
// 是否显示
show?: boolean
}
export interface BasicWidgetInstance<T extends BasicWidgetProps = BasicWidgetProps> {
/**
* 设置小部件属性
* @param props 小部件属性
*/
setProps: (props: Partial<T>) => void
/**
* 切换显示状态
* @param show 是否显示,强制指定显示状态
*/
toggleShow: (show?: boolean) => void
}
/**
* 获取 Boolean 值或默认值
* @param value Boolean 值
* @param def 默认值
* @returns 值或默认值
*/
export function getBooleanOrDefault(value: any, def = null): boolean {
return isBoolean(value) ? value : def
}
/**
* 注册组件工厂
* @param instanceRef 组件实例 Ref
* @param props 组件属性
* @param callback 回调函数
*/
export function registerFactory<T extends BasicWidgetInstance<P>, P>(
instanceRef: Ref<Nullable<T>>,
props: P,
callback?: (instance: T) => void,
): (instance: T) => void {
function register(instance: T) {
if (isProdMode()) {
if (instance === unref(instanceRef)) {
return
}
onUnmounted(() => {
instanceRef.value = null
})
}
instanceRef.value = instance
props && instance?.setProps(props)
callback && callback(instance)
}
return register
}
/**
* 获取组件实例
* @param instanceRef 组件实例 Ref
* @param name 组件名称
* @returns 组件实例
*/
export function getInstance<T extends BasicWidgetInstance>(instanceRef: Ref<Nullable<T>>, name?: string): T {
const instance = unref(instanceRef)
if (!instance) {
console.warn(`The ${name || 'widget'} instance has not been obtained`)
}
return instance as T
}
/**
* 获取基础组件实例函数
* @param instanceRef 组件实例 Ref
* @param name 组件名称
* @returns 组件实例函数
* @example
* ```ts
* const { get, setProps, toggleShow } = getBasicWidgetInstanceFunctions<BasicWidgetInstance, BasicWidgetProps>(instanceRef, 'basic widget')
* ```
*/
export function getBasicWidgetInstanceFunctions<T extends BasicWidgetInstance, P extends BasicWidgetProps>(
instanceRef: Ref<Nullable<T>>,
name?: string,
) {
const get = () => getInstance(instanceRef, name)
return {
get,
setProps: (props: Partial<P>) => get()?.setProps(props),
toggleShow: (show?: boolean) => get()?.toggleShow(getBooleanOrDefault(show)),
} as T & { get: () => T }
}
......@@ -11,7 +11,14 @@
useShare()
const config: MapboxConfig = {
// 页面参数
// const param = reactive({
// query: {},
// filter: {},
// })
// 地图配置
const config: MapboxConfig = reactive({
// 说明: 地图数据来源标注展示
attribution: {
text: '湖南省气象台',
......@@ -21,11 +28,13 @@
// 说明: 根据每个页面的 widget 布局情况,可能需要适当调整地图的中心位置,让界面显示效果更好
center: [111.6, 26.170844],
},
}
})
// 顶部时间轴小部件
const [registerTimeBarWidget] = useTimeBarWidget({
const [registerTimeBarWidget, { getTime, setTime, getCheckedOption }] = useTimeBarWidget({
show: true,
align: 'left',
readonly: false,
label: {
options: [
{
......@@ -62,12 +71,37 @@
)
},
},
buttons: [
{
icon: 'order',
label: '列表',
onClick: (e, { option, value }) => {
console.log(
'[TimeBarWidget] Button Click',
e,
option,
value.map((item) => formatTime(item)),
)
},
},
],
})
// 前后切换小部件
const [registerSwitchControlWidget] = useSwitchControlWidget({
show: true,
prev: () => Message.toast('prev'),
prev: () => {
const option = getCheckedOption()
const time = getTime()
console.log(
'[SwitchControlWidget] prev',
option,
time.map((item) => formatTime(item)),
)
setTime(time.map((item) => item.subtract(1, 'hours')))
Message.toast('prev')
},
next: () => Message.toast('next'),
})
......@@ -87,6 +121,10 @@
height: 150,
maxHeight: 240,
})
function testPackUp() {
toggleLegendWidgetExpand()
}
</script>
<template>
......@@ -108,7 +146,7 @@
<!-- 底部 Bar 小部件 -->
<BottomBarWidget @register="registerBottomBarWidget">
<!-- 内容 Slot -->
<view class="c-coolGray" @tap="toggleLegendWidgetExpand">底部交互控件/展示内容</view>
<view class="c-coolGray" @tap="testPackUp">底部交互控件/展示内容</view>
</BottomBarWidget>
</view>
</view>
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论