提交 147866b0 作者: 方治民

feat: 实现 ToolBoxWidget 小部件的核心功能,优化其他小部件的样式

上级 b229c6c4
......@@ -67,12 +67,12 @@
<template>
<view class="wrap bottom-bar" :class="{ expand: data.expand }" v-show="data.show">
<view class="action" @tap="toggleExpand()" v-show="data.showExpandButton">
<view class="action flex-center" @tap="toggleExpand()" v-show="data.showExpandButton">
<Icon icon="solar-double-alt-arrow-up-line-duotone" size="60" class="icon" />
</view>
<!-- 自定义内容 -->
<view class="content">
<view class="content flex-center">
<slot></slot>
</view>
......@@ -114,9 +114,6 @@
.action {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
.icon {
@include animate();
......@@ -129,9 +126,6 @@
}
.content {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: calc(100% - 60rpx - 20rpx);
overflow: hidden;
......
......@@ -70,15 +70,15 @@
</script>
<template>
<view class="wrap legend" @click="data.expand = !data.expand">
<view class="wrap legend flex-center" @click="data.expand = !data.expand">
<!-- 展开/收起 -->
<view class="expand-action" v-show="!data.expand">
<view class="expand-action flex-center" v-show="!data.expand">
<view class="text">图例</view>
<Icon icon="ic-round-keyboard-arrow-up" color="#1890ff" size="42" />
</view>
<!-- 内容 -->
<view class="expand-content" v-show="data.expand">
<view class="expand-content flex-center" v-show="data.expand">
<!-- 标题 -->
<view v-if="data.title" class="title">{{ data.title }}</view>
......@@ -86,7 +86,7 @@
<DefineTemplate v-slot="{ item, sub }">
<view class="item">
<!-- 图标 -->
<view v-if="item.icon" class="icon">
<view v-if="item.icon" class="icon flex-center">
<CacheImage :src="item.icon" width="34" height="34" />
</view>
......@@ -146,19 +146,13 @@
}
.legend {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 12rpx 20rpx;
background-color: #fff;
border-radius: 10rpx;
box-shadow: 0 0 15rpx rgba(0, 0, 0, 0.3);
.expand-action {
display: flex;
justify-content: center;
align-items: center;
font-size: 26rpx;
font-weight: bold;
color: #1890ff;
......@@ -169,9 +163,6 @@
}
.expand-content {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
font-size: 24rpx;
color: #555;
......@@ -195,9 +186,6 @@
justify-content: flex-start;
.icon {
display: flex;
justify-content: center;
align-items: center;
margin-right: 8rpx;
padding: 4rpx;
}
......
......@@ -7,12 +7,19 @@
type: Boolean,
default: true,
},
// 底部距离 rpx
bottom: {
type: String,
default: '1rpx',
},
})
const emits = defineEmits(['register', 'prev', 'next'])
const height = computed(() => `calc(50% - 60rpx - ${Number(data.bottom.replace('rpx', '')) / 2}rpx)`)
const data = reactive({
show: props.show,
bottom: props.bottom,
// 往前
prev: () => {},
// 往后
......@@ -47,10 +54,10 @@
<template>
<view class="wrap switch-control">
<view class="prev" @tap="onPrev">
<view class="flex-center prev" @tap="onPrev">
<Icon class="icon" icon="ic-baseline-arrow-back-ios" size="50" color="white" />
</view>
<view class="next" @tap="onNext">
<view class="flex-center next" @tap="onNext">
<Icon class="icon" icon="ic-baseline-arrow-forward-ios" size="50" color="white" />
</view>
</view>
......@@ -61,15 +68,14 @@
.switch-control {
> * {
@include animate();
width: 60rpx;
height: 120rpx;
background-color: rgba(0, 0, 0, 0.3);
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: calc(50% - 100rpx);
top: v-bind(height);
z-index: 99;
}
......
import type { BasicWidgetInstance, BasicWidgetProps } from '../../utils'
export interface SwitchControlProps extends BasicWidgetProps {
// 底部距离 rpx
bottom?: string
// 上一个
prev?: () => void
// 下一个
......
......@@ -124,6 +124,7 @@
// ================== TimePicker 选择相关开始 ===================
const showTimePicker = ref(false)
const zIndex = computed(() => (showTimePicker.value ? 200 : 110))
function changeTime(e: Recordable) {
if (e.startDate) {
data.time.value = [dayjs(e.startDate.result), dayjs(e.endDate.result)]
......@@ -140,7 +141,6 @@
watch(
() => data.time.value,
() => data.time.onChange?.(getTimeBarValue()),
{ deep: true },
)
// ================== TimePicker 选择相关结束 ===================
......@@ -174,7 +174,7 @@
@close="showDropdownMenu = false"
ref="dropdownMenu"
>
<view class="fui-filter__item" @tap="openDropdownMenu">
<view class="fui-filter__item flex-center" @tap="openDropdownMenu">
<text>{{ checkedLabelOption.text }}</text>
<view class="fui-filter__icon" :class="{ 'fui-icon__ani': showDropdownMenu }">
<fui-icon name="turningdown" :size="32" />
......@@ -187,7 +187,7 @@
</template>
</view>
<view class="flex flex-auto justify-between items-center">
<view class="flex flex-auto justify-between items-center h-55rpx">
<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" />
......@@ -198,8 +198,8 @@
<fui-button
bold
:size="24"
width="140rpx"
height="60rpx"
width="130rpx"
height="55rpx"
radius="8rpx"
color="#1890FF"
background="#E7F3FF"
......@@ -244,7 +244,7 @@
top: 0;
left: 0;
// 层级需要比其他 widget 高,否则 picker 会被遮挡
z-index: 100;
z-index: v-bind(zIndex);
background-color: white;
}
......@@ -268,11 +268,11 @@
color: #333;
.icon {
margin-right: 10rpx;
margin-right: 6rpx;
}
.text {
margin-right: 5rpx;
margin-right: 2rpx;
&.after::after {
content: ':';
......@@ -280,14 +280,11 @@
}
.fui-filter__item {
display: flex;
align-items: center;
justify-content: center;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
background-color: #fff;
margin-right: 10rpx;
margin-right: 6rpx;
}
.fui-filter__icon {
......@@ -312,9 +309,16 @@
&.left {
justify-content: flex-start;
flex: none;
.time {
max-width: 350rpx;
}
}
.time {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #1890ff;
&.readonly {
......
export type * from './src/types'
export * from './src/hook'
export { default as ToolBoxWidget } from './src/ToolBox.vue'
<!-- 左侧工具盒子控制组件 -->
<script setup lang="ts">
import { createReusableTemplate } from '@vueuse/core'
import type { ToolBoxButton, ToolBoxButtonGroup } from './types'
const props = defineProps({
// 显示
show: {
type: Boolean,
default: true,
},
// 展开
expand: {
type: Boolean,
default: false,
},
// 是否显示是否展开按钮
showExpandButton: {
type: Boolean,
default: false,
},
// 底部距离 rpx
bottom: {
type: String,
default: '1rpx',
},
// 底部内边距 rpx (用于避免遮挡)
bottomPadding: {
type: String,
default: '1rpx',
},
// 工具集
tools: {
type: Array as PropType<ToolBoxButtonGroup[]>,
default: () => [],
},
})
const emits = defineEmits(['register'])
// 定义复用渲染组件
const [DefineTemplate, ReuseTemplate] = createReusableTemplate<{ items: ToolBoxButton[] }>()
const height = computed(() => `calc(30rpx + ${data.bottomPadding} + ${data.bottom})`)
const data = reactive({
show: props.show,
expand: props.expand,
showExpandButton: props.showExpandButton,
bottom: props.bottom,
bottomPadding: props.bottomPadding,
tools: props.tools,
})
function toggleShow(show?: boolean) {
data.show = show ?? !data.show
}
function toggleExpand(expand?: boolean) {
data.expand = expand ?? !data.expand
}
function setProps(value: Partial<typeof props>) {
Object.assign(data, value)
}
function onButtonTap(button: ToolBoxButton) {
console.log('onButtonTap', button)
if (button.disabled) {
return
}
if (button.type === 'button') {
button.handle?.({ type: 'click', name: button.name })
} else if (button.type === 'select') {
// 打开 Select 组件
model.selectPopup.title = button.name
model.selectPopup.options = button.options ?? []
model.selectPopup.multiple = button.multiple ?? false
model.selectPopup.show = true
} else if (button.type === 'filter') {
// 打开 Filter 组件
model.filterPopup.title = button.name
model.filterPopup.show = true
}
// 设置当前激活的按钮
model.activeButton = button
}
const zIndex = computed(() => (model.selectPopup.show || model.filterPopup.show ? 200 : 100))
const model = reactive({
activeButton: null as ToolBoxButton | null,
selectPopup: {
show: false,
multiple: false,
title: '',
options: [],
onConfirm: (e: Recordable) => {
console.log('onConfirm', e)
model.activeButton?.handle({
type: 'change',
name: model.activeButton.name,
value: toRaw(e.options),
})
model.selectPopup.onClose()
},
onClose: () => (model.selectPopup.show = false),
},
filterPopup: {
show: false,
title: '',
min: '',
max: '',
onConfirm: () => {
console.log('onConfirm', model.filterPopup.min, model.filterPopup.max)
model.activeButton?.handle({
type: 'change',
name: model.activeButton.name,
value: [model.filterPopup.min, model.filterPopup.max],
})
model.filterPopup.onClose()
},
onRest: () => {
console.log('onRest')
model.filterPopup.min = ''
model.filterPopup.max = ''
},
onClose: () => (model.filterPopup.show = false),
},
})
emits('register', {
setProps,
toggleShow,
toggleExpand,
})
</script>
<template>
<view class="wrap tool-box">
<DefineTemplate v-slot="{ items }">
<view class="button-group flex-center" v-show="data.expand">
<template v-for="(item, index) in items" :key="`tool_box_button_${index}_${item.name}`">
<view class="button flex-center" :class="{ disabled: item.disabled }" @tap="onButtonTap(item)">
<CacheImage class="icon" :src="item.icon" width="40" height="40" />
<view class="text">{{ item.name }}</view>
<!-- 过滤的结果展示 -->
<view v-if="item.type === 'filter'" class="result">
<view class="text">{{ item.result }}</view>
</view>
</view>
<fui-divider v-if="index !== items.length - 1" :height="4" />
</template>
</view>
</DefineTemplate>
<view class="top">
<template v-for="tool in data.tools.filter((item) => item.align === 'top')" :key="`group_${tool.key}`">
<ReuseTemplate :items="tool.buttons" />
</template>
</view>
<view class="bottom">
<template v-for="tool in data.tools.filter((item) => item.align === 'bottom')" :key="`group_${tool.key}`">
<ReuseTemplate :items="tool.buttons" />
</template>
<!-- 展开/收起操作按钮 -->
<view
v-if="data.showExpandButton"
class="expand-action flex-center"
:class="{ expand: data.expand }"
@tap="data.expand = !data.expand"
>
<view class="text">{{ data.expand ? '收起' : '展开' }}</view>
<Icon icon="ic-round-keyboard-arrow-up" size="42" color="#1890ff" class="icon" />
</view>
</view>
<!-- 交互组件 -->
<!-- 1. Select Popup -->
<fui-select
maskClosable
:show="model.selectPopup.show"
:title="`选择${model.selectPopup.title}`"
:options="model.selectPopup.options"
:multiple="model.selectPopup.multiple"
@confirm="model.selectPopup.onConfirm"
@close="model.selectPopup.onClose"
/>
<!-- 2. 过滤 Popup -->
<fui-bottom-popup
:show="model.filterPopup.show"
:z-index="900"
@close="model.filterPopup.onClose"
:safeArea="true"
>
<view class="popup-wrap">
<view class="fui-title">{{ model.filterPopup.title }}</view>
<view class="fui-icon__close" @tap="model.filterPopup.onClose">
<fui-icon name="close" :size="48" />
</view>
<scroll-view scroll-y class="fui-scroll__view">
<view class="fui-custom__wrap">
<view class="branch">
<view class="title">阈值范围(仅支持数值)</view>
<view class="timeBox">
<!-- -->
<fui-input
type="number"
borderTop
isFillet
inputBorder
maxlength="10"
backgroundColor="#F5F5F5"
placeholder="最小值"
v-model="model.filterPopup.min"
/>
<view class="mark split" />
<fui-input
type="number"
borderTop
isFillet
inputBorder
maxlength="10"
backgroundColor="#F5F5F5"
placeholder="最大值"
v-model="model.filterPopup.max"
/>
</view>
</view>
<view class="signMan">
<fui-button
width="330rpx"
height="86rpx"
radius="86rpx"
text="重置"
background="#FFFFFF"
border-color="#AAAAAA"
color="#AAAAAA"
:margin="['70rpx', '0rpx', '0rpx', '30rpx']"
@click="model.filterPopup.onRest"
/>
<fui-button
width="330rpx"
height="90rpx"
radius="90rpx"
text="确定"
background="#1890FF"
color="#FFFFFF"
:margin="['70rpx', '30rpx', '0rpx', '0rpx']"
@click="model.filterPopup.onConfirm"
/>
</view>
</view>
</scroll-view>
</view>
</fui-bottom-popup>
</view>
</template>
<style lang="scss" scoped>
@import '../../common.scss';
.wrap {
@include animate();
}
.tool-box {
.expand-action {
font-size: 26rpx;
font-weight: bold;
color: #1890ff;
padding: 12rpx 20rpx;
background-color: #fff;
border-radius: 10rpx;
box-shadow: 0 0 15rpx rgba(0, 0, 0, 0.3);
margin-top: 30rpx;
.text {
margin-right: 6rpx;
}
.icon {
@include animate();
transform: rotate(0);
}
&.expand {
.icon {
transform: rotate(180deg) !important;
}
}
}
.top {
position: absolute;
top: 125rpx;
right: 30rpx;
z-index: v-bind(zIndex);
}
.bottom {
@include animate();
position: absolute;
right: 30rpx;
bottom: v-bind(height);
z-index: v-bind(zIndex);
display: flex;
flex-direction: column;
align-items: flex-end;
}
.button-group {
flex-direction: column;
box-shadow: 0 0 15rpx rgba(0, 0, 0, 0.3);
background-color: white;
border-radius: 10rpx;
padding: 5rpx 0;
width: 95rpx;
+ .button-group {
margin-top: 30rpx;
}
.button {
flex-direction: column;
font-size: 24rpx;
color: #666;
padding: 10rpx;
min-width: 80rpx;
&.disabled {
filter: grayscale(1);
}
.text {
margin-top: 5rpx;
}
}
}
}
.popup-wrap {
height: 500rpx;
padding-top: 30rpx;
position: relative;
}
.fui-custom__wrap {
width: 100%;
display: flex;
align-items: center;
flex-direction: column;
font-size: 28rpx;
.branch {
margin-top: 30rpx;
width: 690rpx;
display: flex;
flex-direction: column;
.title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.timeBox {
width: 100%;
margin-top: 30rpx;
display: flex;
align-items: center;
justify-content: space-between;
.mark {
width: 20rpx;
height: 4rpx;
background: #ddd;
margin: 0 20rpx;
}
.timeSelect {
width: 260rpx;
height: 80rpx;
padding: 0 30rpx;
background: #f4f5f9;
border-radius: 45rpx;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
.text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.selected {
background: #f3f9ff;
color: #1890ff;
border: 2rpx solid #1890ff;
}
}
.typeBox {
width: 100%;
margin-top: 10rpx;
display: flex;
align-items: center;
flex-wrap: wrap;
.typeSelect {
width: 156rpx;
height: 80rpx;
margin-top: 20rpx;
padding: 0 30rpx;
background: #f4f5f9;
border-radius: 45rpx;
display: flex;
align-items: center;
justify-content: center;
border: 2rpx solid #f4f5f9;
overflow: hidden;
.text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
+ .typeSelect {
margin-left: 20rpx;
}
}
.typeSelected {
background: #f3f9ff;
color: #1890ff;
border: 2rpx solid #1890ff;
}
}
}
.signMan {
width: 100%;
background: #fff;
margin-bottom: 70rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.handleBtn {
border: 1rpx solid #aaa;
border-radius: 86rpx;
}
}
.fui-title {
font-size: 32rpx;
font-weight: bold;
text-align: center;
padding-bottom: 24rpx;
}
.fui-icon__close {
position: absolute;
top: 20rpx;
right: 30rpx;
}
.fui-scroll__view {
width: 100%;
height: 790rpx;
}
</style>
import { getBasicWidgetInstanceFunctions, getBooleanOrDefault, registerFactory } from '../../utils'
import type { ToolBoxInstance, ToolBoxProps } from './types'
// 组件名称
export const name = 'ToolBoxWidget'
/**
* 左侧工具盒子组件响应式 Hook
* @param props 组件参数
* @returns 组件响应式数据
*/
export function useToolBoxWidget<T extends ToolBoxInstance, P extends ToolBoxProps>(
props: P,
): [(instance: T) => void, ToolBoxInstance] {
const instanceRef = ref<T>()
const register = registerFactory(instanceRef, props)
const { get, setProps, toggleShow } = getBasicWidgetInstanceFunctions(instanceRef, name)
return [
register,
{
setProps,
toggleShow,
toggleExpand: (expand?: boolean) => get()?.toggleExpand(getBooleanOrDefault(expand)),
},
]
}
import type { BasicWidgetInstance, BasicWidgetProps } from '../../utils'
export interface ToolBoxButtonHandleEvent {
// 事件类型
type: 'click' | 'change'
// 事件名称
name: string
// 事件值
value?: string | string[] | { text: string; value: string } | { text: string; value: string }[]
}
export interface ToolBoxButton {
// 按钮类型
type: 'select' | 'button' | 'filter'
// 按钮名称
name: string
// 按钮图标
icon?: string
// 按钮是否禁用
disabled?: boolean
// select 类型按钮选项
options?: {
id?: string
// 选项名称
text: string
// 选项值
value: string
// 是否选中
checked?: boolean
// 是否禁用
disabled?: boolean
}[]
// select 类型下是否支持多选
multiple?: boolean
// 按钮事件
handle?: (e: ToolBoxButtonHandleEvent) => void
// 按钮值
value?: string | string[]
// filter 类型的结果
result?: string | string[]
}
export interface ToolBoxButtonGroup {
// 标识
key: string
// 对齐方向
align?: 'top' | 'bottom'
// 按钮列表
buttons: ToolBoxButton[]
}
export interface ToolBoxProps extends BasicWidgetProps {
// 是否展开
expand?: boolean
// 是否显示是否展开按钮
showExpandButton?: boolean
// 底部距离 rpx
bottom?: string
// 底部内边距 rpx
bottomPadding?: string
// 工具集
tools?: ToolBoxButtonGroup[]
}
export interface ToolBoxInstance extends BasicWidgetInstance<ToolBoxProps> {
toggleExpand: (expand?: boolean) => void
}
......@@ -6,6 +6,7 @@
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 { ToolBoxWidget, useToolBoxWidget } from '@/components/Map/Widgets/ToolBoxWidget'
import { BottomBarWidget, useBottomBarWidget } from '@/components/Map/Widgets/BottomBar'
import { SwitchControlWidget, useSwitchControlWidget } from '@/components/Map/Widgets/SwitchControl'
......@@ -87,8 +88,116 @@
],
})
// 左侧工具栏小部件
const [registerToolBoxWidget, { setProps: setToolBoxProps }] = useToolBoxWidget({
show: true,
expand: true,
showExpandButton: true,
// 说明: 如果底部使用 bottom-left attribution 则需要加上 65rpx,默认显示在右侧,不会冲突
bottomPadding: '65rpx',
tools: [
{
key: 'filter',
align: 'top',
buttons: [
{
name: '过滤',
icon: '/static/icons/toolbox/filter.png',
type: 'filter',
handle: () => {},
},
{
name: '图层',
icon: '/static/icons/toolbox/menu.png',
type: 'select',
multiple: true,
options: [
{
text: '色斑',
value: 'contour',
checked: true,
},
{
text: '数值',
value: 'text',
},
],
handle: () => {},
},
{
name: '区域',
icon: '/static/icons/toolbox/location.png',
type: 'select',
multiple: false,
options: [
{
text: '湖南省',
value: '430000',
checked: true,
},
{
text: '长沙市',
value: '430100',
},
{
text: '湘潭市',
value: '430300',
},
],
handle: () => {},
},
{
name: '站点',
icon: '/static/icons/toolbox/station.png',
type: 'select',
multiple: false,
handle: () => {},
},
],
},
{
key: 'action',
align: 'bottom',
buttons: [
{
name: '要素',
icon: '/static/icons/toolbox/element.png',
type: 'select',
multiple: true,
handle: () => {},
},
{
name: '排名',
icon: '/static/icons/toolbox/rank.png',
type: 'button',
handle: () => {},
},
{
name: '累加',
icon: '/static/icons/toolbox/cumulative.png',
type: 'button',
disabled: true,
handle: () => {},
},
],
},
{
key: 'desc',
align: 'bottom',
buttons: [
{
name: '综述',
icon: '/static/icons/toolbox/describe.png',
type: 'button',
handle: () => {},
},
],
},
],
})
// 前后切换小部件
const [registerSwitchControlWidget] = useSwitchControlWidget({
const [registerSwitchControlWidget, { setProps: setSwitchControlProps }] = useSwitchControlWidget({
show: true,
prev: () => {
const { option, value } = getTimeBarValue()
......@@ -124,6 +233,16 @@
function testPackUp() {
toggleLegendWidgetExpand()
}
watch(
() => height.value,
(value) => {
console.log('[useBottomBarWidget] height', value)
setToolBoxProps({ bottom: value })
setSwitchControlProps({ bottom: value })
},
)
</script>
<template>
......@@ -133,9 +252,12 @@
<!-- 地图上方所有小部件 -->
<view class="widgets">
<!-- -->
<!-- 时间栏小部件 -->
<TimeBarWidget @register="registerTimeBarWidget" />
<!-- 左侧工具栏小部件 -->
<ToolBoxWidget @register="registerToolBoxWidget" />
<!-- 前后切换小部件 -->
<SwitchControlWidget @register="registerSwitchControlWidget" />
......
......@@ -147,6 +147,7 @@ declare module 'vue' {
SwitchControl: typeof import('./../src/components/Map/Widgets/SwitchControl/src/SwitchControl.vue')['default']
ThumbnailPreview: typeof import('./../src/components/ThumbnailPreview/index.vue')['default']
TimeBar: typeof import('./../src/components/Map/Widgets/TimeBarWidget/src/TimeBar.vue')['default']
ToolBox: typeof import('./../src/components/Map/Widgets/ToolBoxWidget/src/ToolBox.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 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论