提交 2a075912 作者: 方治民

feat: 迁移 Widget 组件目录、初步实现 TimeBarWidget

上级 1be73da7
......@@ -155,8 +155,8 @@ export const defaultStyle: mapboxgl.Style = {
{
id: 'background',
type: 'background',
layout: { visibility: 'none' },
paint: { 'background-color': 'hsla(0, 0%, 0%, 0)' },
layout: { visibility: 'visible' },
paint: { 'background-color': '#FFFFFF' },
},
// 天空图层
{
......@@ -230,8 +230,7 @@ export const defaultStyle: mapboxgl.Style = {
visibility: 'visible',
},
paint: {
'fill-color': '#fff',
'fill-opacity': 0.3,
'fill-color': '#F4F5F7',
},
minzoom: 5,
},
......@@ -779,3 +778,36 @@ export function getImageSource(map: mapboxgl.Map, sourceName: string): mapboxgl.
export const EmptyImage =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAABlBMVEXMzMz/+v+u+uEzAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAACklEQVQImWNgAAAAAgAB9HFkpgAAAABJRU5ErkJggg=='
/**
* Mapbox 组件配置参数类型定义
*/
export interface MapboxConfig {
/**
* Mapbox 构造函数参数
* @link https://docs.mapbox.com/mapbox-gl-js/api/map/
*/
options?: Partial<mapboxgl.MapboxOptions>
/**
* Mapbox 地图样式
* @link https://docs.mapbox.com/mapbox-gl-js/style-spec/
*/
style?: Partial<mapboxgl.Style>
/**
* 地图资源/数据源来源或归属描述信息
*/
attribution?: {
/**
* 文本
* @example 湖南省气象台
* @default ''
*/
text: string
/**
* 位置, 默认为 'bottom-right'
* @example 'bottom-left', 'bottom-right'
* @default 'bottom-right'
*/
align?: 'bottom-left' | 'bottom-right'
}
}
<!-- 顶部 Bar 组件 -->
<script setup lang="ts">
//
</script>
<template>
<view class="wrap top-bar">
<!-- -->
</view>
</template>
<style lang="scss" scoped>
//
</style>
......@@ -12,6 +12,11 @@
type: Boolean,
default: false,
},
// 是否有展开按钮
showExpandButton: {
type: Boolean,
default: true,
},
// 高度
height: {
type: Number,
......@@ -32,6 +37,7 @@
const data = reactive({
show: props.show,
expand: props.expand,
showExpandButton: props.showExpandButton,
height: props.height,
maxHeight: props.maxHeight,
})
......@@ -41,7 +47,9 @@
}
function toggleExpand(expand?: boolean) {
data.expand = expand ?? !data.expand
if (data.showExpandButton) {
data.expand = expand ?? !data.expand
}
}
function setProps(value: Partial<typeof props>) {
......@@ -59,7 +67,7 @@
<template>
<view class="wrap bottom-bar" :class="{ expand: data.expand }" v-show="data.show">
<view class="action" @tap="toggleExpand()">
<view class="action" @tap="toggleExpand()" v-show="data.showExpandButton">
<Icon icon="solar-double-alt-arrow-up-line-duotone" size="60" class="icon" />
</view>
......@@ -89,8 +97,6 @@
}
.bottom-bar {
@include animate();
position: fixed;
bottom: 0;
left: 0;
......
......@@ -3,6 +3,8 @@ export interface BottomBarProps {
show?: boolean
// 是否展开
expand?: boolean
// 是否有展开按钮
showExpandButton?: boolean
// 高度 rpx
height?: number
// 最大高度 rpx
......
export type * from './src/types'
export * from './src/hook'
export { default as TimeBarWidget } from './src/TimeBar.vue'
<!-- 顶部时间栏 Bar 组件 -->
<script setup lang="ts">
import dayjs from 'dayjs'
import type { TimeBarButton, TimeBarLabel, TimeBarTime } from './types'
import { formatTime } from './hook'
import { deepMerge } from '@/utils'
const props = defineProps({
// 是否显示
show: {
type: Boolean,
default: true,
},
// 是否只读
readonly: {
type: Boolean,
default: false,
},
// 对齐方式
align: {
type: String,
default: 'left',
},
// 标签
label: {
type: Object as PropType<TimeBarLabel>,
default: () => ({}) as TimeBarLabel,
},
// 时间
time: {
type: Object as PropType<TimeBarTime>,
default: () => ({}) as TimeBarTime,
},
// 扩展按钮
buttons: {
type: Array as PropType<TimeBarButton[]>,
default: () => [] as TimeBarButton[],
},
})
const emits = defineEmits(['register'])
const data = reactive({
show: props.show,
readonly: props.readonly,
align: props.align,
label: props.label,
time: props.time,
buttons: props.buttons,
// 组件自身的数据
showDropdownMenu: false,
})
function toggleShow(show?: boolean) {
data.show = show ?? !data.show
}
function setProps(value: Partial<typeof props>) {
deepMerge(data, value)
if (!data?.time?.value) {
const time = dayjs()
data.time.value = [time.subtract(1, 'days'), time]
}
}
const labelText = computed(() => {
let text: string
if (data.label.options?.length) {
text = checkedLabelOption.value?.text
} else {
text = data.label.text
}
return text || ''
})
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(' - ')
}
return text || ''
})
const timePickerType = computed(() => {
let type: number
if (data.time) {
type = checkedLabelOption.value?.timeType || data.time.timeType
}
return type || 3
})
const dropdownMenu = ref(null)
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
if (item.checked) {
data.label?.onChange?.({
option: { text: item.text, value: item.value },
value: toRaw(data.time.value),
})
}
})
}
function openDropdownMenu() {
data.showDropdownMenu = true
dropdownMenu.value?.show()
}
function closeDropdownMenu() {
data.showDropdownMenu = false
}
const showTimePicker = ref(false)
function changeTime(e: Recordable) {
if (e.startDate) {
data.time.value = [dayjs(e.startDate.result), dayjs(e.endDate.result)]
} 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
}
emits('register', {
setProps,
toggleShow,
})
</script>
<template>
<view class="wrap time-bar" :class="[data.align]" v-show="data.show">
<view class="label">
<Icon
icon="ic-outline-access-time"
size="36"
color="#333"
class="icon"
v-if="data.label.breforeIcon !== false"
/>
<template v-if="data.label.options?.length">
<fui-dropdown-menu
:size="28"
selectedColor="#465CFF"
:options="data.label.options"
@click="changeLabelOption"
@close="closeDropdownMenu"
ref="dropdownMenu"
>
<view class="fui-filter__item" @tap="openDropdownMenu">
<text>{{ checkedLabelOption.text }}</text>
<view class="fui-filter__icon" :class="{ 'fui-icon__ani': data.showDropdownMenu }">
<fui-icon name="turningdown" :size="32" />
</view>
</view>
</fui-dropdown-menu>
</template>
<template v-else>
<view class="text after">{{ labelText }}</view>
</template>
</view>
<view class="">
<view class="time-wrap" @tap="showTimePicker = true">
<!-- -->
<view class="time">{{ timeText }}</view>
</view>
<view class="buttons" v-if="data.buttons?.length">
<!-- -->
</view>
</view>
<!-- 时间选择组件 -->
<fui-date-picker
start="开始时间"
end="结束时间"
:range="data.time.type === 'range'"
:show="showTimePicker"
:type="timePickerType"
:value="formatTime(data.time?.value?.[0])"
:valueEnd="formatTime(data.time?.value?.[1])"
@change="changeTime"
@cancel="showTimePicker = false"
/>
</view>
</template>
<style lang="scss" scoped>
@mixin animate() {
transition: all 0.35s;
}
.wrap {
@include animate();
position: absolute;
top: 0;
left: 0;
// 层级需要比其他 widget 高,否则 picker 会被遮挡
z-index: 100;
background-color: white;
}
.time-bar {
padding: 20rpx;
width: 100%;
display: flex;
align-items: center;
font-size: 28rpx;
// box-shadow: 0 0 15rpx rgba(0, 0, 0, 0.1);
&.center {
justify-content: center;
}
> .label {
display: flex;
align-items: center;
font-size: 28rpx;
color: #333;
.icon {
margin-right: 10rpx;
}
.text {
margin-right: 5rpx;
&.after::after {
content: ':';
}
}
.fui-filter__item {
display: flex;
align-items: center;
justify-content: center;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
background-color: #fff;
margin-right: 10rpx;
}
.fui-filter__icon {
transition: all 0.15s linear;
margin-left: 5rpx;
}
.fui-icon__ani {
transform: rotate(180deg);
}
:deep(.fui-dropdown__menu-list) {
width: 230rpx;
}
}
.time-wrap {
flex: auto;
.time {
color: #1890ff;
}
}
}
</style>
import type { Dayjs } from 'dayjs'
import type { TimeBarInstance, TimeBarProps } from './types'
import { getBooleanOrDefault } from '../../utils'
import { isProdMode } from '/@/utils/env'
export function formatTime(time: Dayjs, format = 'YYYY-MM-DD HH:mm:ss') {
return time?.format(format) ?? ''
}
export function getFormatByType(type: number) {
switch (type) {
case 1:
return 'YYYY'
case 2:
return 'YYYY-MM'
case 3:
return 'YYYY-MM-DD'
case 4:
return 'YYYY-MM-DD HH:00'
case 5:
return 'YYYY-MM-DD HH:mm'
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'
type FirstUIDatePickerType = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8
export interface TimeBarChangeEvent {
value: Dayjs[]
option?: {
text?: string
value?: string
}
}
export interface TimeBarLabel {
text?: string
breforeIcon?: boolean
afterIcon?: boolean
options?: {
text?: string
value?: string
checked?: boolean
format?: string
/**
* 对应 first-ui datePicker 组件 type 参数
*/
timeType?: FirstUIDatePickerType
}[]
onClick?: () => void
onChange?: (e: TimeBarChangeEvent) => void
}
export interface TimeBarTime {
type: 'single' | 'range'
/**
* 对应 first-ui datePicker 组件 type 参数
*/
timeType: FirstUIDatePickerType
format?: string
value?: Dayjs[]
onChange?: (e: TimeBarChangeEvent) => void
}
export interface TimeBarButton {
icon?: string
label: string
onClick?: (e: { index: number; label: string }, time: string | { start?: string; end?: string }) => void
}
export interface TimeBarProps {
// 是否显示
show?: boolean
// 是否只读
readonly?: boolean
// 对齐方式
align?: 'left' | 'center' | 'right'
// 标题
label?: TimeBarLabel
// 时间
time: TimeBarTime
// 扩展按钮
buttons?: TimeBarButton[]
}
export interface TimeBarInstance {
setProps: (props: TimeBarProps) => void
toggleShow: (show?: boolean) => void
}
......@@ -3,20 +3,67 @@
<script setup lang="ts">
import { defaultLegendConfig } from './config'
import { useShare } from '@/hooks/page/useShare'
import { LegendWidget, useLegendWidget } from '@/components/Map/Mapbox/widgets/Legend'
import { BottomBarWidget, useBottomBarWidget } from '@/components/Map/Mapbox/widgets/BottomBar'
import { SwitchControlWidget, useSwitchControlWidget } from '@/components/Map/Mapbox/widgets/SwitchControl'
import type { MapboxConfig } from '@/components/Map/Mapbox'
import { LegendWidget, useLegendWidget } from '@/components/Map/Widgets/Legend'
import { TimeBarWidget, formatTime, useTimeBarWidget } from '@/components/Map/Widgets/TimeBarWidget'
import { BottomBarWidget, useBottomBarWidget } from '@/components/Map/Widgets/BottomBar'
import { SwitchControlWidget, useSwitchControlWidget } from '@/components/Map/Widgets/SwitchControl'
useShare()
const config = {
// 测试地图数据来源标注展示
const config: MapboxConfig = {
// 说明: 地图数据来源标注展示
attribution: {
text: '湖南省气象台',
// align: 'bottom-left',
},
style: {
// 说明: 根据每个页面的 widget 布局情况,可能需要适当调整地图的中心位置,让界面显示效果更好
center: [111.6, 26.170844],
},
}
// 顶部时间轴小部件
const [registerTimeBarWidget] = useTimeBarWidget({
show: true,
label: {
options: [
{
text: '小时级',
value: 'hour',
format: 'M月D日H时',
timeType: 4,
checked: true,
},
{
text: '分钟级',
value: 'minute',
format: 'M月D日H时m分',
timeType: 5,
},
],
onChange: ({ option, value }) => {
console.log(
'[TimeBarWidget] ChangeOption',
option,
value.map((item) => formatTime(item)),
)
},
},
time: {
type: 'range',
timeType: 4,
format: 'M月D日H时',
onChange: ({ option, value }) => {
console.log(
'[TimeBarWidget] ChangeTime',
option,
value.map((item) => formatTime(item)),
)
},
},
})
// 前后切换小部件
const [registerSwitchControlWidget] = useSwitchControlWidget({
show: true,
......@@ -36,6 +83,7 @@
const [registerBottomBarWidget, { height }] = useBottomBarWidget({
show: true,
expand: true,
showExpandButton: true,
height: 150,
maxHeight: 240,
})
......@@ -49,6 +97,7 @@
<!-- 地图上方所有小部件 -->
<view class="widgets">
<!-- -->
<TimeBarWidget @register="registerTimeBarWidget" />
<!-- 前后切换小部件 -->
<SwitchControlWidget @register="registerSwitchControlWidget" />
......@@ -87,10 +136,10 @@
.page {
// #ifdef H5
:deep(.mapboxgl-ctrl-bottom-right) {
bottom: calc(80rpx + v-bind(height));
bottom: calc(85rpx + v-bind(height));
}
:deep(.mapboxgl-ctrl-bottom-left) {
bottom: calc(80rpx + v-bind(height));
bottom: calc(85rpx + v-bind(height));
}
// #endif
// #ifdef APP-PLUS
......
......@@ -7,13 +7,13 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
BottomBar: typeof import('./../src/components/Map/Mapbox/widgets/BottomBar/src/BottomBar.vue')['default']
BottomBar: typeof import('./../src/components/Map/Widgets/BottomBar/src/BottomBar.vue')['default']
CacheImage: typeof import('./../src/components/CacheImage/index.vue')['default']
CustomPicker: typeof import('./../src/components/CustomPicker/index.vue')['default']
Empty: typeof import('./../src/components/Empty/index.vue')['default']
FDragItem: typeof import('./../src/components/FirstUI/fui-drag/f-drag-item.vue')['default']
FIndexListItem: typeof import('./../src/components/FirstUI/fui-index-list/f-index-list-item.vue')['default']
FloatFillterWidget: typeof import('./../src/components/Map/Mapbox/widgets/FloatFillterWidget.vue')['default']
FloatFillterWidget: typeof import('./../src/components/Map/Widgets/FloatFillterWidget.vue')['default']
FuiActionsheet: typeof import('./../src/components/FirstUI/fui-actionsheet/fui-actionsheet.vue')['default']
FuiAlert: typeof import('./../src/components/FirstUI/fui-alert/fui-alert.vue')['default']
FuiAnimation: typeof import('./../src/components/FirstUI/fui-animation/fui-animation.vue')['default']
......@@ -138,15 +138,17 @@ declare module 'vue' {
Grid: typeof import('./../src/components/Layout/Grid.vue')['default']
Header: typeof import('./../src/components/Layout/Header.vue')['default']
Icon: typeof import('./../src/components/Icon/index.vue')['default']
LeftBarWidget: typeof import('./../src/components/Map/Mapbox/widgets/LeftBarWidget.vue')['default']
Legend: typeof import('./../src/components/Map/Mapbox/widgets/Legend/src/Legend.vue')['default']
LeftBarWidget: typeof import('./../src/components/Map/Widgets/LeftBarWidget.vue')['default']
Legend: typeof import('./../src/components/Map/Widgets/Legend/src/Legend.vue')['default']
Mapbox: typeof import('./../src/components/Map/Mapbox/index.vue')['default']
MenuHeader: typeof import('./../src/components/Layout/MenuHeader.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SwitchControl: typeof import('./../src/components/Map/Mapbox/widgets/SwitchControl/src/SwitchControl.vue')['default']
SwitchControl: typeof import('./../src/components/Map/Widgets/SwitchControl/src/SwitchControl.vue')['default']
ThumbnailPreview: typeof import('./../src/components/ThumbnailPreview/index.vue')['default']
TopBarWidget: typeof import('./../src/components/Map/Mapbox/widgets/TopBarWidget.vue')['default']
TimeBar: typeof import('./../src/components/Map/Widgets/TimeBarWidget/src/TimeBar.vue')['default']
TopBar: typeof import('./../src/components/Map/Mapbox/widgets/TopBarWidget/src/TopBar.vue')['default']
TopBarWidget: typeof import('./../src/components/Map/Mapbox/widgets/TopBarWidget/TopBarWidget.vue')['default']
View: typeof import('./../src/components/Layout/View.vue')['default']
}
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论