提交 e5033ec2 作者: 方治民

feat: 整合湖南天气 APP 地图模块的成果,更新地图示例页

上级 8cb96c9e
import type mapboxgl from 'mapbox-gl'
import qs from 'qs'
import { isEmpty, isNull, isObject, isString, isUndefined, merge, omitBy } from 'lodash-es'
import { DEFAULT_MAP_CENTER, DEFAULT_MAP_ZOOM, HandlerUtil } from './index'
import type { GeoJSONSourceDataUrlParams, HandlerUtilType, MapboxConfig, MapboxInstance } from './index'
import { API_URL, API_URL_PREFIX } from '/@/utils/net'
import { isProdMode } from '@/utils/env'
import { getToken } from '@/utils/auth'
// 组件名称
export const name = 'Mapbox'
/**
* 注册 Mapbox 实例
* @param config Mapbox 配置项
*/
export function useMapbox<T extends MapboxInstance, P extends MapboxConfig>(
config: P,
): [(instance: T) => void, MapboxInstance] {
const instanceRef = ref<T>()
function register(instance: T) {
if (isProdMode()) {
if (instance === unref(instanceRef)) {
return
}
onUnmounted(() => {
instanceRef.value = null
})
}
instanceRef.value = instance
config && instance?.setConfig(config)
}
function getInstance() {
const instance = unref(instanceRef)
if (!instance) {
console.warn('Mapbox instance is undefined!')
}
return instance as T
}
return [
register,
{
setConfig: (config: Partial<P>) => getInstance()?.setConfig(config),
isReady: computed(() => instanceRef?.value?.isReady) as unknown as ComputedRef<boolean>,
on: (type: string, layerId: string, listener: (...args: any[]) => void) =>
getInstance()?.on(type, layerId, listener),
addSource: (id: string, source: mapboxgl.AnySourceData) => getInstance()?.addSource(id, source),
removeSource: (id: string) => getInstance()?.removeSource(id),
setGeoJSONSourceForRequest: (
id: string,
config: string | GeoJSONSourceDataUrlParams,
handler?: (
data: Recordable,
util: HandlerUtilType,
) => GeoJSON.Feature<GeoJSON.Geometry> | GeoJSON.FeatureCollection<GeoJSON.Geometry>,
filters?: string[],
) =>
getInstance()?.setGeoJSONSourceForRequest(id, buildGeoJSONSourceDataUrl(config), handler, [
...(filters || []),
]),
setGeoJSONSourceData: (id: string, data: mapboxgl.GeoJSONSource['setData'], filter?: string) =>
getInstance()?.setGeoJSONSourceData(id, data, filter),
setVectorTileSourceTiles: (id: string, tiles: string[]) =>
getInstance()?.setVectorTileSourceTiles(id, tiles),
addLayer: (layer: mapboxgl.Layer, beforeId?: string, popup?: boolean) =>
getInstance()?.addLayer(layer, beforeId, popup),
removeLayer: (id: string) => getInstance()?.removeLayer(id),
setPaintProperty: (layerId: string, name: string, value: any) =>
getInstance()?.setPaintProperty(layerId, name, value),
setLayoutProperty: (layerId: string, name: string, value: any) =>
getInstance()?.setLayoutProperty(layerId, name, value),
setFilter: (layerId: string, filter: any[]) => getInstance()?.setFilter(layerId, filter),
flyTo: (options: mapboxgl.FlyToOptions) => getInstance()?.flyTo(options),
},
]
}
/**
* 添加默认的 GeoJSON Source - 空白数据源
* @param map 地图实例
* @param id source id
* @param features features
*/
export function addDefaultGeoJSONSource(map: MapboxInstance, id: string, features?: GeoJSON.Feature[]) {
map.addSource(id, {
type: 'geojson',
data: HandlerUtil.createFeatureCollection(features),
})
}
/**
* 添加默认的色斑 Fill Layer
* @param map 地图实例
* @param id layer id
* @param layer layer config
* @param beforeId before layer id
*/
export function addDefaultSplotLayer(
map: MapboxInstance,
id: string,
layer?: Partial<mapboxgl.Layer>,
beforeId = 'fill-placeholder',
) {
map.addLayer(
merge(
{
id,
type: 'fill',
source: id,
paint: {
'fill-color': {
type: 'identity',
property: 'color',
},
'fill-opacity': 1,
},
},
layer,
),
beforeId,
)
}
/**
* 添加默认的点位 Symbol Layer - 默认支持 Symbol 的 Click + Popup 事件
* @param map 地图实例
* @param id layer id
* @param layer layer config
* @param beforeId before layer id
* @param popop 是否支持 Popup
*/
export function addDefaultSymbolLayer(
map: MapboxInstance,
id: string,
layer?: Partial<mapboxgl.Layer>,
beforeId = 'symbol-placeholder',
popop = true,
) {
map.addLayer(
merge(
{
id,
type: 'symbol',
source: id,
layout: {
'text-field': '{value}',
// 'text-font': ['PingFang SC', 'Helvetica', 'Helvetica Neue', 'Microsoft YaHei', 'sans-serif'],
'text-font': ['Open Sans Regular', 'Arial Unicode MS Regular'],
'text-size': 14,
'text-offset': [0, 1.2],
'icon-image': 'point.normal',
'icon-anchor': 'top',
'icon-size': 0.4,
},
paint: {
'text-color': '#3766fd',
'text-halo-color': '#fff',
'text-halo-width': 1,
},
},
layer,
),
beforeId,
popop,
)
}
export function flyTo(
map: MapboxInstance,
center: [number, number] = DEFAULT_MAP_CENTER,
zoom: number = DEFAULT_MAP_ZOOM,
) {
map.flyTo({
center,
zoom,
speed: 0.2,
essential: true,
})
}
/**
* 构建 GeoJSON Source Data Url - 默认添加 token + 过滤空参数
* @param config 接口配置
* @returns source url
*/
export function buildGeoJSONSourceDataUrl(config: GeoJSONSourceDataUrlParams | string): string {
const baseURL = `${API_URL}${API_URL_PREFIX}`
const defaultParams = {
Authorization: getToken(),
}
const url = typeof config === 'string' ? config : config.url
const params = typeof config === 'string' ? defaultParams : { ...defaultParams, ...config.params }
const query = qs.stringify(
omitBy(
params,
(value) =>
isNull(value) ||
isUndefined(value) ||
(isString(value) && isEmpty(value)) ||
(isObject(value) && isEmpty(value)),
),
{ addQueryPrefix: true, encode: true, encodeValuesOnly: true },
)
return `${baseURL}${url}${query}`
}
export type * from './src/types'
export * from './src/hook'
export { default as AffixFilterWidget } from './src/AffixFilter.vue'
/**
* 吸附过滤器组件
*
* 说明: 用于吸附在地图上的过滤器组件,可以用于筛选地图上的数据或改变查询条件等,支持的组件有: Select
*
* 可以应用到的一些模块:
* 1. 山洪地质灾害
* 2. 森林火险
* 3. 水情监测
* 4. 卫星云图
* 5. 形势场
* 6. 雷达拼图
* ...
*/
<!-- 吸附过滤器组件 -->
<script setup lang="ts">
import type { Option, OptionItem } from './types'
import { deepMerge } from '@/utils'
const props = defineProps({
// 是否显示
show: {
type: Boolean,
default: true,
},
// 布局
layout: {
type: String,
default: 'horizontal',
},
// 风格
style: {
type: String,
default: 'button',
},
// 距离顶部的距离
top: {
type: Number,
default: 125,
},
// 距离左侧的距离
left: {
type: Number,
default: 30,
},
// 选项
options: {
type: Array as PropType<Option[]>,
default: () => [],
},
})
const emits = defineEmits(['register'])
const positionTop = computed(() => `${data.top}rpx`)
const positionLeft = computed(() => `${data.left}rpx`)
const data = reactive({
show: props.show,
layout: props.layout,
style: props.style,
top: props.top,
left: props.left,
options: props.options,
})
function toggleShow(show?: boolean) {
data.show = show ?? !data.show
}
function setProps(value: Partial<typeof props>) {
deepMerge(data, value)
}
function getProp(key: keyof typeof props) {
return data[key]
}
function setOptionItems(key: string, items: OptionItem[]) {
data.options = data.options.map((option) => {
if (option.key === key) {
option.items = items
}
return option
})
}
function setOptionItemChecked(key: string, value: string) {
data.options = data.options.map((option) => {
if (option.key === key) {
option.items = option.items.map((item) => {
item.checked = item.value === value
return item
})
}
return option
})
}
const zIndex = computed(() => (model.show ? 200 : 110))
const model = reactive({
show: false,
activeOption: null as Option | null,
})
function optionClick(option: Option) {
console.log(option)
model.activeOption = option
model.show = true
}
function getItemLabel(option: Option) {
return option.items.find((item) => item.checked)?.text ?? option.placeholder
}
function optionChange(e: { text: string; value: string }) {
const currentOptionChecked = model.activeOption?.items.find((item) => item.checked)
data.options = data.options.map((option) => {
if (option.key === model.activeOption?.key) {
option.items = option.items.map((item) => {
item.checked = item.value === e.value
return item
})
}
return option
})
// 判断是否真的发生了变化
if (
currentOptionChecked?.value !==
data.options.find((option) => option.key === model.activeOption?.key)?.items.find((item) => item.checked)
?.value
) {
model.activeOption?.onChange?.(
{
key: model.activeOption.key,
value: toRaw(model.activeOption.items.find((item) => item.checked)),
},
data.options.map((option) => {
return {
key: option.key,
value: toRaw(option.items.find((item) => item.checked)),
}
}),
)
}
closePicker()
}
function closePicker() {
model.activeOption = null
model.show = false
}
emits('register', {
setProps,
getProp,
toggleShow,
setOptionItems,
setOptionItemChecked,
})
</script>
<template>
<view class="wrap affix-filter" :class="[data.layout, data.style]">
<!-- -->
<view class="filter-item flex-center" v-for="option in data.options" :key="option.key">
<view class="fui-filter__item flex-center" @tap="optionClick(option)">
<text>{{ getItemLabel(option) }}</text>
<view class="ml-10rpx icon" :class="{ open: model.activeOption?.key === option.key }">
<fui-icon name="turningdown" :size="32" color="#999" />
</view>
</view>
</view>
<!-- 复用选择器 -->
<fui-picker
linkage
:options="model.activeOption?.items"
:show="model.show"
@change="optionChange"
@cancel="closePicker"
/>
</view>
</template>
<style lang="scss" scoped>
@import '../../common.scss';
.affix-filter {
display: flex;
box-sizing: border-box;
position: absolute;
top: v-bind(positionTop);
left: v-bind(positionLeft);
z-index: v-bind(zIndex);
&.vertical {
flex-direction: column;
.filter-item + .filter-item {
margin-top: 30rpx;
}
}
&.horizontal {
.filter-item + .filter-item {
margin-left: 30rpx;
}
}
&.bar {
width: 100%;
background-color: white;
left: 0;
padding: 10rpx 30rpx;
.filter-item {
flex: 1;
box-shadow: none !important;
}
}
.filter-item {
background-color: white;
padding: 15rpx 30rpx;
font-size: 28rpx;
color: #333;
border-radius: 10rpx;
box-shadow: 0 0 15rpx rgba(0, 0, 0, 0.1);
.icon {
@include animate();
transform: rotate(0deg);
&.open {
transform: rotate(180deg);
}
}
}
}
</style>
import { registerWidgetFactory, unpackWidgetInstance } from '../../utils'
import type { AffixFilterInstance, AffixFilterProps, OptionItem } from './types'
// 组件名称
export const name = 'AffixFilterWidget'
/**
* 吸附过滤器组件
* @param props 组件参数
* @returns 组件响应式数据
*/
export function useAffixFilterWidget<T extends AffixFilterInstance, P extends AffixFilterProps>(
props: P,
): [(instance: T) => void, AffixFilterInstance] {
const instanceRef = ref<T>()
const register = registerWidgetFactory(instanceRef, props)
const { get, ...fns } = unpackWidgetInstance(instanceRef, name)
return [
register,
{
...fns,
setOptionItem: (key: string, item: OptionItem) => get()?.setOptionItem(key, item),
},
]
}
import type { BasicWidgetInstance, BasicWidgetProps } from '../../utils'
export interface CheckedOption {
key: string
value: OptionItem
}
export interface OptionItem {
text: string
value: string
checked?: boolean
disabled?: boolean
[key: string]: any
}
export interface Option {
// 标识
key: string
// 提示信息
placeholder?: string
// 选项
items: OptionItem[]
// 选项改变时的回调
onChange?: (checkedOption: CheckedOption, checkedOptions: CheckedOption[]) => void
}
export interface AffixFilterProps extends BasicWidgetProps {
/**
* 布局方式,水平或垂直
* @default horizontal
*/
layout?: 'horizontal' | 'vertical'
/**
* 风格,按钮或者条形
* @default button
*/
style?: 'button' | 'bar'
/**
* 距离顶部的距离 rpx
* @default 130rpx
*/
top?: number
/**
* 距离左侧的距离 rpx
* @default 30rpx
*/
left?: number
/**
* 过滤选项集合,仅支持 Select
*/
options: Option[]
}
export interface AffixFilterInstance extends BasicWidgetInstance<AffixFilterProps> {
/**
* 设置选项
* @param key 选项标识
* @param items 选项可选 options
*/
setOptionItems: (key: string, items: OptionItem[]) => void
/**
* 设置选项选中
* @param key 选项标识
* @param value 选项值
*/
setOptionItemChecked: (key: string, value: string) => void
}
export type * from './src/types'
export * from './src/hook'
export { default as BottomBarWidget } from './src/BottomBar.vue'
/**
* 底部工具栏组件
*
* 说明: 此组件仅提供一个可交互的容器,用于放置其他组件,如: Tabs、Button 等一些自定义场景
*
* 可以应用到的一些模块:(基于已有蓝湖设计稿评估,仅供参考)
* 1. 降水监测
* 2. 气温监测
* 3. 相对湿度
* 4. 雷达拼图
* 5. 强对流监测
* 6. 冰雪天气
* 7. 台风监测
* 8. 卫星云图
* 9. 环境监测
* 10. 山洪地质灾害、面雨量相关
* 11. 相关预报模块
* ...
*
*/
<!-- 底部 Bar 组件 -->
<script setup lang="ts">
import { deepMerge } from '@/utils'
const props = defineProps({
// 是否显示
show: {
type: Boolean,
default: true,
},
// 是否展开
expand: {
type: Boolean,
default: false,
},
// 展开按钮标题
expandTitle: {
type: String,
},
// 是否有展开按钮
showExpandButton: {
type: Boolean,
default: true,
},
// 是否有展开按钮下边框
showExpandBorder: {
type: Boolean,
default: false,
},
// 高度
height: {
type: Number,
default: 0,
},
// 最高高度
maxHeight: {
type: Number,
default: 0,
},
// 是否 Y 轴可滚动
scrollY: {
type: Boolean,
default: false,
},
})
const emits = defineEmits(['register'])
const iHeight = computed(() => `${data.height}rpx`)
const iMaxHeight = computed(() => `${data.maxHeight}rpx`)
const data = reactive({
show: props.show,
expand: props.expand,
expandTitle: props.expandTitle,
showExpandButton: props.showExpandButton,
showExpandBorder: props.showExpandBorder,
height: props.height,
maxHeight: props.maxHeight,
scrollY: props.scrollY,
})
function toggleShow(show?: boolean) {
data.show = show ?? !data.show
}
function toggleExpand(expand?: boolean) {
if (data.showExpandButton) {
data.expand = expand ?? !data.expand
}
}
function setProps(value: Partial<typeof props>) {
deepMerge(data, value)
}
function getProp(key: keyof typeof props) {
return data[key]
}
emits('register', {
setProps,
getProp,
toggleShow,
toggleExpand,
// TODO: 此处高度还需考虑底部安全区
height: computed(() => (data.show ? (data.expand ? data.maxHeight : data.height) : 0)),
})
</script>
<template>
<view class="wrap bottom-bar" :class="{ expand: data.expand }" v-show="data.show">
<view
class="action flex-center"
:class="{ border: data.showExpandBorder }"
v-show="data.showExpandButton"
@tap="toggleExpand()"
>
<text class="text" v-if="data.expandTitle">{{ data.expandTitle }}</text>
<Icon icon="solar-double-alt-arrow-up-line-duotone" size="42" color="#999" class="icon" />
</view>
<!-- 自定义内容 -->
<scroll-view class="content flex-center" :scroll-y="data.scrollY">
<slot></slot>
</scroll-view>
<!-- 底部安全区 -->
<fui-safe-area />
</view>
</template>
<style lang="scss" scoped>
@import '../../common.scss';
.wrap {
@include animate();
position: absolute;
left: 30rpx;
bottom: 30rpx;
z-index: 99;
background-color: white;
}
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
// padding: 30rpx 12rpx;
padding: 10px 12px;
// width: calc(100% - 60rpx);
width: 100%;
height: v-bind(iHeight);
max-height: v-bind(iMaxHeight);
box-shadow: 0 0 15rpx rgba(0, 0, 0, 0.1);
box-sizing: border-box;
&.expand {
height: v-bind(iMaxHeight);
.icon {
transform: rotateX(180deg) !important;
}
}
.action {
width: 100%;
position: relative;
// top: -15rpx;
font-size: 24rpx;
color: #999;
padding-bottom: 10rpx;
&.border {
border-bottom: 2rpx solid #f6f6f6;
}
.text {
margin-right: 0.5em;
}
.icon {
@include animate();
transform: rotateX(0);
transform-style: preserve-3d;
}
}
.content {
box-sizing: border-box;
width: 100%;
// height: calc(100% - 60rpx - 20rpx);
height: 100%;
overflow: hidden;
position: relative;
}
}
</style>
import { getBooleanOrDefault, registerWidgetFactory, unpackWidgetInstance } from '../../utils'
import type { BottomBarInstance, BottomBarProps } from './types'
// 组件名称
export const name = 'BottomBarWidget'
/**
* 底部交互/展示小部件响应式 Hook
* @param props 组件参数
* @returns 组件响应式数据
*/
export function useBottomBarWidget<T extends BottomBarInstance, P extends BottomBarProps>(
props: P,
): [(instance: T) => void, BottomBarInstance] {
const instanceRef = ref<T>()
const register = registerWidgetFactory(instanceRef, props)
const { get, ...fns } = unpackWidgetInstance(instanceRef, name)
return [
register,
{
...fns,
toggleExpand: (expand?: boolean) => get()?.toggleExpand(getBooleanOrDefault(expand)),
height: computed(() => instanceRef?.value?.height) as unknown as ComputedRef<number>,
},
]
}
import type { BasicWidgetInstance, BasicWidgetProps } from '../../utils'
export interface BottomBarProps extends BasicWidgetProps {
/**
* 是否展开
* @default false
*/
expand?: boolean
/**
* 展开标题
*/
expandTitle?: string
/**
* 是否有展开按钮
* @default true
*/
showExpandButton?: boolean
/**
* 是否有展开边框
* @default false
*/
showExpandBorder?: boolean
/**
* 高度 rpx
* @default 0 rpx
*/
height: number
/**
* 最大高度 rpx
* @default 0 rpx
*/
maxHeight?: number
/**
* 是否支持 Y 滚动
* @default false
*/
scrollY?: boolean
}
export interface BottomBarInstance extends BasicWidgetInstance<BottomBarProps> {
/**
* 响应式组件当前高度,单位 rpx
*/
height: ComputedRef<number>
/**
* 切换展开状态
* @param expand 是否展开
*/
toggleExpand: (expand?: boolean) => void
}
export type * from './src/types'
export * from './src/hook'
export { default as LegendWidget } from './src/Legend.vue'
/**
* 图例组件
*
* 说明: 配合地图上的数据展示的图例,支持动态配置
*
* 可以应用到的一些模块:(基于已有蓝湖设计稿评估,仅供参考)
* 所有需要用到图例展示的模块
* 各种场景下的配置项,可以参考: \src\pages\business\monitor\rain\config.ts defaultLegendConfig
*
*/
<!-- 图例组件 -->
<script setup lang="ts">
import { isArray } from 'lodash-es'
import { createReusableTemplate } from '@vueuse/core'
import type { Option } from './types'
import { deepMerge } from '@/utils'
import { cssAdditionCalc } from '@/components/Map/Widgets/utils'
const props = defineProps({
// 显示
show: {
type: Boolean,
default: true,
},
// 展开
expand: {
type: Boolean,
default: false,
},
// 标题
title: {
type: String,
},
// 底部距离 rpx
bottom: {
type: Number,
default: 0,
},
// JSON 数据
option: {
type: Object as PropType<Option>,
default: () => ({}),
},
})
const emits = defineEmits(['register'])
// 定义复用渲染组件
const [DefineTemplate, ReuseTemplate] = createReusableTemplate<{ item: Recordable; sub?: boolean }>()
const positionBottom = computed(() => cssAdditionCalc(30, data.bottom))
const data = reactive({
show: props.show,
expand: props.expand,
title: props.title,
option: props.option,
bottom: props.bottom,
})
function setTitle(title: string) {
data.title = title
}
function setOption(option: Option) {
data.option = option
}
function toggleShow(show?: boolean) {
data.show = show ?? !data.show
}
function toggleExpand(expand?: boolean) {
data.expand = expand ?? !data.expand
}
function setProps(value: Partial<typeof props>) {
deepMerge(data, value)
}
function getProp(key: keyof typeof props) {
return data[key]
}
emits('register', {
setProps,
getProp,
setTitle,
setOption,
toggleShow,
toggleExpand,
})
</script>
<template>
<view class="wrap legend flex-center" @click="data.expand = !data.expand" v-show="data.show">
<!-- 展开/收起 -->
<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 flex-center" v-show="data.expand">
<!-- 标题 -->
<view v-if="data.title" class="title">{{ data.title }}</view>
<!-- 定义复用渲染组件 -->
<DefineTemplate v-slot="{ item, sub }">
<view class="item">
<!-- 图标 -->
<view v-if="item.icon" class="icon flex-center">
<CacheImage :src="item.icon" width="34" height="34" background="transparent" />
</view>
<!-- 色块 -->
<view
v-if="item.color"
class="color"
:style="{ backgroundColor: item.color, ...data.option.blockStyle }"
/>
<!-- 标签 -->
<view
v-if="String(item.label)"
class="label"
:class="{ 'color-label': item.color }"
:style="!sub && { ...data.option.labelStyle }"
>
{{ item.label }}
</view>
<!-- 子项集合 -->
<view v-if="item.items" class="sub-items">
<view v-for="(subItem, index) in item.items" :key="index" class="item">
<ReuseTemplate :item="subItem" :sub="true" />
</view>
</view>
</view>
</DefineTemplate>
<!-- JSON -->
<view class="option">
<template v-if="isArray(data.option.items?.[0])">
<view v-for="(item, index) in data.option.items" :key="index" class="item-wrap">
<view v-for="(subItem, subIndex) in item" :key="subIndex" class="items">
<ReuseTemplate :item="subItem" />
</view>
</view>
</template>
<template v-else>
<view class="item-wrap">
<view v-for="(item, index) in data.option.items" :key="index" class="items">
<ReuseTemplate :item="item" />
</view>
</view>
</template>
</view>
</view>
</view>
</template>
<style lang="scss" scoped>
@import '../../common.scss';
.wrap {
@include animate();
position: absolute;
left: 30rpx;
bottom: v-bind(positionBottom);
z-index: 99;
}
.legend {
flex-direction: column;
padding: 12rpx 20rpx;
background-color: #fff;
border-radius: 10rpx;
box-shadow: 0 0 15rpx rgba(0, 0, 0, 0.2);
.expand-action {
font-size: 26rpx;
font-weight: bold;
color: #1890ff;
.text {
margin-right: 6rpx;
}
}
.expand-content {
flex-direction: column;
font-size: 24rpx;
color: #555;
.title {
margin-bottom: 15rpx;
font-weight: bold;
}
.option {
display: flex;
.item-wrap {
+ .item-wrap {
margin-left: 15rpx;
}
.item {
display: flex;
align-items: center;
justify-content: flex-start;
.icon {
margin-right: 8rpx;
padding: 4rpx;
}
.color {
width: 38rpx;
height: 24rpx;
margin-right: 8rpx;
}
.label {
color: #777;
letter-spacing: 1px;
white-space: pre-wrap;
text-align: center;
&.color-label {
height: 24rpx;
}
}
}
.sub-items {
display: flex;
.item {
flex-direction: column;
justify-content: center;
margin: 2rpx 5rpx;
font-size: 20rpx;
}
}
}
}
}
}
</style>
import { getBooleanOrDefault, registerWidgetFactory, unpackWidgetInstance } from '../../utils'
import type { LegendInstance, LegendProps, Option } from './types'
// 组件名称
export const name = 'LegendWidget'
/**
* 图例组件响应式 Hook
* @param props 组件参数
* @returns 组件响应式数据
*/
export function useLegendWidget<T extends LegendInstance, P extends LegendProps>(
props: P,
): [(instance: T) => void, LegendInstance] {
const instanceRef = ref<T>()
const register = registerWidgetFactory(instanceRef, props)
const { get, ...fns } = unpackWidgetInstance(instanceRef, name)
return [
register,
{
...fns,
setTitle: (title: string) => get()?.setTitle(title),
setOption: (option: Option) => get()?.setOption(option),
toggleExpand: (expand?: boolean) => get()?.toggleExpand(getBooleanOrDefault(expand)),
},
]
}
import type { BasicWidgetInstance, BasicWidgetProps } from '../../utils'
export interface OptionItem {
// 颜色
color?: string
// 图标
icon?: string
// 标签
label?: string
// 多子项
items?: OptionItem[]
}
export interface Option {
// 选项
items: OptionItem[] | OptionItem[][]
// label 样式
labelStyle?: Recordable
// 色块样式
blockStyle?: Recordable
}
export interface LegendProps extends BasicWidgetProps {
/**
* 是否展开
* @default false
*/
expand?: boolean
/**
* 标题
*/
title: string
/**
* 选项
*/
option?: Option
/**
* 底部距离 rpx
* @default 0
*/
bottom?: number
}
export interface LegendInstance extends BasicWidgetInstance<LegendProps> {
/**
* 设置标题
* @param title 标题
*/
setTitle: (title: string) => void
/**
* 设置选项
* @param option 选项
*/
setOption: (option: Option) => void
/**
* 切换展开状态
* @param expand 是否展开
*/
toggleExpand: (expand?: boolean) => void
}
export type * from './src/types'
export * from './src/hook'
export { default as SwitchWidget } from './src/Switch.vue'
/**
* 前后切换组件
*
* 说明: 用于地图页面两侧的前后切换组件,通常与 TimeBarWidget 配合使用
*
* 可以应用到的一些模块:(基于已有蓝湖设计稿评估,仅供参考)
* 所有需要用到前后切换时间的场景,实况、预报等
*
*/
<!-- 前后切换组件 -->
<script setup lang="ts">
const props = defineProps({
// 是否显示
show: {
type: Boolean,
default: true,
},
// 底部距离 rpx
bottom: {
type: Number,
default: 0,
},
})
const emits = defineEmits(['register', 'prev', 'next'])
const height = computed(() => `calc(50% - 60rpx${data.bottom ? ` - ${data.bottom / 2}rpx` : ''})`)
const data = reactive({
show: props.show,
bottom: props.bottom,
prev: () => {},
next: () => {},
})
function toggleShow(show?: boolean) {
data.show = show ?? !data.show
}
function setProps(value: Partial<typeof props>) {
Object.assign(data, value)
}
function getProp(key: keyof typeof props) {
return data[key]
}
function onPrev() {
data.prev?.()
emits('prev')
}
function onNext() {
data.next?.()
emits('next')
}
emits('register', {
setProps,
getProp,
toggleShow,
prev: onPrev,
next: onNext,
})
</script>
<template>
<view class="wrap switch-control" v-show="data.show">
<view class="flex-center prev" @tap="onPrev">
<Icon class="icon" icon="ic-baseline-arrow-back-ios" size="50" color="white" />
</view>
<view class="flex-center next" @tap="onNext">
<Icon class="icon" icon="ic-baseline-arrow-forward-ios" size="50" color="white" />
</view>
</view>
</template>
<style lang="scss" scoped>
@import '../../common.scss';
.switch-control {
> * {
@include animate();
width: 60rpx;
height: 120rpx;
background-color: rgba(0, 0, 0, 0.3);
position: absolute;
top: v-bind(height);
z-index: 99;
}
.prev {
left: 0;
border-radius: 0 15rpx 15rpx 0;
.icon {
position: relative;
left: 12rpx;
}
}
.next {
right: 0;
border-radius: 15rpx 0 0 15rpx;
}
}
</style>
import { registerWidgetFactory, unpackWidgetInstance } from '../../utils'
import type { SwitchControlInstance, SwitchControlProps } from './types'
// 组件名称
export const name = 'SwitchControlWidget'
/**
* 前后切换组件响应式 Hook
* @param props 组件参数
* @returns 组件响应式数据
*/
export function useSwitchWidget<T extends SwitchControlInstance, P extends SwitchControlProps>(
props: P,
): [(instance: T) => void, SwitchControlInstance] {
const instanceRef = ref<T>()
const register = registerWidgetFactory(instanceRef, props)
const { get, ...fns } = unpackWidgetInstance(instanceRef, name)
return [
register,
{
...fns,
prev: () => get()?.prev(),
next: () => get()?.next(),
},
]
}
import type { BasicWidgetInstance, BasicWidgetProps } from '../../utils'
export interface SwitchControlProps extends BasicWidgetProps {
// 底部距离 rpx
bottom?: number
// 上一个
prev?: () => void
// 下一个
next?: () => void
}
export interface SwitchControlInstance extends BasicWidgetInstance<SwitchControlProps> {
/**
* 手动调用上一个
*/
prev: () => void
/**
* 手动调用下一个
*/
next: () => void
}
export type * from './src/types'
export * from './src/hook'
export { default as TimeBarWidget } from './src/TimeBar.vue'
/**
* 时间栏组件
*
* 说明: 用于页面顶部的时间选择和过滤
*
* 可以应用到的一些模块:(基于已有蓝湖设计稿评估,仅供参考)
* 所有需要用到时间轴的场景,实况、预报以及部分非地图的内页,均可以实现适配
*
*/
import type { Dayjs } from 'dayjs'
import { registerWidgetFactory, unpackWidgetInstance } from '../../utils'
import type { TimeBarInstance, TimeBarProps } from './types'
// 组件名称
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 = registerWidgetFactory(instanceRef, props)
const { get, ...fns } = unpackWidgetInstance(instanceRef, name)
return [
register,
{
...fns,
getTime: () => get()?.getTime(),
setTime: (time: Dayjs[]) => get()?.setTime(time),
getCheckedOption: () => get()?.getCheckedOption(),
setCheckedOption: (index: number) => get()?.setCheckedOption(index),
getTimeBarValue: () => get()?.getTimeBarValue(),
},
]
}
/**
* 格式化时间
* @param time 时间
* @param format 格式化
* @returns 格式化后的时间,如果时间为空则返回空字符串
*/
export function formatTime(time: Dayjs, format = 'YYYY-MM-DD HH:mm:ss') {
return time?.format(format) ?? ''
}
export function isAfter(time: Dayjs, compare: Dayjs) {
return time && compare && time.isAfter(compare)
}
export function isBefore(time: Dayjs, compare: Dayjs) {
return time && compare && time.isBefore(compare)
}
export function isAfterAndEqual(time: Dayjs, compare: Dayjs) {
return time && compare && (time.isAfter(compare) || time.isSame(compare))
}
export function isBeforeAndEqual(time: Dayjs, compare: Dayjs) {
return time && compare && (time.isBefore(compare) || time.isSame(compare))
}
/**
* 根据 type 属性返回对应的格式化字符串
* @param type fui-date-picker 组件的 type 属性
* @returns 根据 type 属性返回对应的格式化字符串
*/
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:00'
case 5:
return 'YYYY-MM-DD HH:mm:00'
case 6:
return 'YYYY-MM-DD HH:mm:ss'
default:
return 'YYYY-MM-DD HH:mm:ss'
}
}
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?: TimeBarLabel['options'][0]
}
export interface TimeBarLabel {
text?: string
breforeIcon?: boolean
afterIcon?: boolean
options?: {
text?: string
value?: string
checked?: boolean
format?: string
timeType?: FirstUIDatePickerType
}[]
onClick?: () => void
onChange?: (e: TimeBarChangeEvent) => void
}
export interface TimeBarTime {
type?: 'single' | 'range'
timeType?: FirstUIDatePickerType
format?: string
value?: Dayjs[]
onChange?: (e: TimeBarChangeEvent) => void
}
export interface TimeBarButton {
/**
* 图标,仅支持 fui-icon name
*/
icon?: string
label: string
onClick?: (e: { index: number; label: string }, time: TimeBarChangeEvent) => void
}
export interface TimeBarProps extends BasicWidgetProps {
/**
* 是否只读
* @default false
*/
readonly?: boolean
/**
* 对齐方式
* @default left
*/
align?: 'left' | 'center' | 'right'
/**
* 标题
* @default { text: '时间' }
*/
label?: TimeBarLabel
/**
* 时间
*/
time?: TimeBarTime
/**
* 最小时间
*/
minTime?: Dayjs
/**
* 最大时间
*/
maxTime?: Dayjs
/**
* 扩展按钮
*/
buttons?: TimeBarButton[]
}
export interface TimeBarInstance extends BasicWidgetInstance<TimeBarProps> {
/**
* 获取时间
* @returns 时间
*/
getTime: () => Dayjs[]
/**
* 设置时间
* @param time 时间
*/
setTime: (time: Dayjs[]) => void
/**
* 获取选中的选项
* @returns 选中的选项
*/
getCheckedOption: () => TimeBarLabel['options'][0]
/**
* 设置选中的选项
* @param index 选项索引
*/
setCheckedOption: (index: number) => void
/**
* 获取时间栏的值
* @returns 时间栏的值
*/
getTimeBarValue: () => TimeBarChangeEvent
}
export type * from './src/types'
export * from './src/hook'
export { default as ToolBoxWidget } from './src/ToolBox.vue'
/**
* 工具箱组件
*
* 说明: 用于展示地图上的右侧的工具箱,支持的组件有: Select、Filter、Button 之类的实现
*
* 可以应用到的一些模块:(基于已有蓝湖设计稿评估,仅供参考)
* 所有需要用到工具箱的场景,实况、预报,以及部分需要使用 AffixFilter 的场景,也可以考虑并入到工具箱中
*
*/
import { getBooleanOrDefault, registerWidgetFactory, unpackWidgetInstance } 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 = registerWidgetFactory(instanceRef, props)
const { get, ...fns } = unpackWidgetInstance(instanceRef, name)
return [
register,
{
...fns,
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 {
/**
* 是否展开
* @default true
*/
expand?: boolean
/**
* 是否显示是否展开按钮
* @default true
*/
showExpandButton?: boolean
/**
* 顶部距离 rpx
* @default 0
*/
top?: number
/**
* 底部距离 rpx
* @default 0
*/
bottom?: number
/**
* 底部内边距 rpx
* @default 0
*/
bottomPadding?: number
/**
* 工具集
*/
tools?: ToolBoxButtonGroup[]
}
export interface ToolBoxInstance extends BasicWidgetInstance<ToolBoxProps> {
/**
* 展开或收起工具箱
* @param expand 是否展开
*/
toggleExpand: (expand?: boolean) => void
}
@mixin animate() {
transition: all 0.35s;
}
import { isBoolean } from 'lodash-es'
import { isProdMode } from '@/utils/env'
export interface BasicWidgetProps {
/**
* 是否显示
* @default true
*/
show?: boolean
}
export interface BasicWidgetInstance<T extends BasicWidgetProps = BasicWidgetProps> {
/**
* 设置小部件属性
* @param props 小部件属性
*/
setProps: (props: Partial<T>) => void
/**
* 获取小部件属性
* @param key 属性名称
* @returns 属性值
*/
getProp: <K extends keyof T>(key: K) => T[K]
/**
* 切换显示状态
* @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 registerWidgetFactory<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 getWidgetInstance<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 } = unpackWidgetInstance<BasicWidgetInstance, BasicWidgetProps>(instanceRef, 'basic widget')
* ```
*/
export function unpackWidgetInstance<T extends BasicWidgetInstance, P extends BasicWidgetProps>(
instanceRef: Ref<Nullable<T>>,
name?: string,
) {
const get = () => getWidgetInstance(instanceRef, name)
return {
get,
setProps: (props: Partial<P>) => get()?.setProps(props),
// @ts-expect-error
getProp: <K extends keyof P>(key: K) => get()?.getProp(key),
toggleShow: (show?: boolean) => get()?.toggleShow(getBooleanOrDefault(show)),
} as T & { get: () => T }
}
/**
* css calc 加法计算函数
* @param values rpx 数值
* @returns css calc 函数或 rpx 数值
*/
export function cssAdditionCalc(...values: number[]): string {
const result = values
.filter((item) => item)
.map((item) => `${item}rpx`)
.join(' + ')
if (result.includes('+')) {
return `calc(${result})`
} else {
return result
}
}
import type { Dayjs } from 'dayjs'
export interface BasicMapPage {
/**
* 页面 ID,用于区分不同页面的数据
*/
id?: string
/**
* 页面是否完成初始化(默认情况下,地图加载完成,请求到第一轮带有最新时间标识的结果时,标记为初始化完成)
*/
init: boolean
/**
* 页面正在请求的数据源数量,由于 setGeoJSONSourceForRequest 的异步特性,需要记录当前正在请求的数据源数量,当所有数据源请求完成时,隐藏 Loading
*/
requests?: number
/**
* 当前请求到的数据最新时间
*/
latest?: Dayjs
}
import type { BasicMapPage } from '@/components/Map/types'
export interface Page extends BasicMapPage {
/**
* 接口查询参数
*/
query?: {
/**
* 注入的变量,请求时不会传递,仅用于注入。
* 在 setGeoJSONSourceForRequest 的 handler 中可以实现替换字符串模板的功能。
*/
inject?: Recordable
}
/**
* 过滤条件
*/
filter?: Recordable
}
<script> <script setup lang="ts">
import { nanoid } from 'nanoid' import type { Page } from './config'
import { DEFAULT_MAP_CENTER } from '@/components/Map/Mapbox'
import { addDefaultGeoJSONSource, addDefaultSymbolLayer, useMapbox } from '@/components/Map/Mapbox/hook'
// FIXED: 重要说明 // 页面参数
// renderjs 组件暂不支持 setup 组件写法,对 ts 支持也不友好,这里的写法需要参考 vue2 的写法 const page = reactive<Page>({
id: 'example-mapbox',
init: false,
requests: 0,
latest: null,
query: {},
})
// 总共分为两块 script // 地图组件
// 1. 逻辑层 Script const center: [number, number] = [DEFAULT_MAP_CENTER[0], DEFAULT_MAP_CENTER[1] + 0.5]
// 2. 视图层 Script const [registerMap, map] = useMapbox({
// 通信方式 style: { center },
// 1. 通过在逻辑层 data 中设置的属性向视图层传递数据 onLoaded: (data) => {
// 2. 通过在逻辑层 methods 定义方法,在视图层调用传递参数(注意:只能传递可进行 JSON 序列化的数据) console.log('✨✨✨ Map Loaded', data)
export default { // 添加数值数据源
data() { addDefaultGeoJSONSource(map, `${page.id}-text`, [
return { {
id: nanoid(),
options: {},
point: {},
position: {},
}
},
mounted() {
this.options = {
container: this.id,
control: {
navigation: {
zoom: true,
compass: true,
},
info: true,
reset: true,
geolocate: true,
layer: true,
},
}
},
methods: {
onMapLoad(data) {
this.point = {
type: 'Feature', type: 'Feature',
geometry: { geometry: {
type: 'Point', type: 'Point',
coordinates: [data.center.lng, data.center.lat], coordinates: [data.center.lng, data.center.lat],
}, },
properties: { properties: {
name: 'Hello World ✨', title: 'Hello',
}, description: 'World',
}
console.log('onLoad', this.point, data)
},
getCurrentPosition() {
// 获取用户定位
uni.getLocation({
type: 'wgs84',
success: (res) => {
// 更新定位
this.position = {
coords: res,
timestamp: Date.now(),
}
}, },
fail: () => { },
Message.toast('获取位置失败,请打开定位权限') ])
}, // 添加数值图层
}) addDefaultSymbolLayer(map, `${page.id}-text`, {
}, layout: {
'text-field': '{title} {description}',
},
})
},
onSourceRequestHandle: () => {
page.requests--
if (page.requests === 0) {
Message.hideLoading()
}
},
onSourceRequestErrorHandle: () => {
Message.hideLoading()
}, },
} })
</script> </script>
<!-- renderjs 视图层模块 -->
<script module="mapbox" lang="renderjs" src="./mapbox.module.js"></script>
<template> <template>
<!-- #ifdef APP-PLUS || H5 --> <view class="page h-100vh bg-white">
<view <!-- 地图组件 -->
class="map wrap" <Mapbox @register="registerMap" />
:id="id" </view>
:options="options"
:change:options="mapbox.changeOptions"
:position="position"
:change:position="mapbox.changePosition"
:data-point="point"
:change:data-point="mapbox.changeDataPoint"
/>
<!-- #endif -->
<!-- #ifndef APP-PLUS || H5 -->
<view class="empty wrap">非 APP、H5 环境不支持</view>
<!-- #endif -->
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
page { //
height: 100%;
}
.wrap {
display: flex;
width: 100%;
height: 100%;
/* #ifdef APP */
height: 100vh;
/* #endif */
}
.empty {
justify-content: center;
align-items: center;
}
</style> </style>
.mapboxgl-popup-tip {
position: relative;
}
.mapboxgl-popup-anchor-right .mapboxgl-popup-tip {
left: -1px;
}
.mapboxgl-popup-anchor-left .mapboxgl-popup-tip {
right: -1px;
}
.mapboxgl-popup-anchor-top .mapboxgl-popup-tip {
bottom: -1px;
}
.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip {
top: -1px;
}
.mapboxgl-popup-content { .mapboxgl-popup-content {
box-shadow: 0 1px 5px #6e6e6e !important; box-shadow: 0 1px 10px #999 !important;
z-index: 0;
} }
.mapboxgl-popup-content .mapboxgl-custom-popup { .mapboxgl-popup-content .mapboxgl-custom-popup {
...@@ -106,4 +127,74 @@ ...@@ -106,4 +127,74 @@
/* stylelint-disable-next-line function-no-unknown */ /* stylelint-disable-next-line function-no-unknown */
padding-bottom: constant(safe-area-inset-bottom); padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom);
transition: all 0.35s;
}
.mapboxgl-ctrl-attrib-inner {
display: inline-block !important;
vertical-align: middle;
line-height: 18px;
font-size: 12px;
font-family: -apple-system-font, BlinkMacSystemFont, 'Droid Sans', 'Noto Sans', 'PingFang SC', 'Heiti SC',
'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', SimSun, sans-serif;
color: rgba(0, 0, 0, 0.75);
text-decoration: none;
}
.mapboxgl-ctrl-attrib.mapboxgl-compact {
padding-right: 8px;
display: flex;
justify-content: center;
align-items: center;
}
.mapboxgl-ctrl-attrib-button {
position: initial;
}
/* Custom style */
.mapboxgl-popup-close-button {
display: none;
}
.mapboxgl-popup-content {
box-shadow: 0 1px 25px rgba(0, 0, 0, 0.2);
}
.mapboxgl-popup-content .custom-popup {
font-family: '微软雅黑';
position: relative;
}
.mapboxgl-popup-content .popup-row {
display: flex;
align-items: center;
}
.mapboxgl-popup-content .popup-chart-icon {
width: 14px;
height: 14px;
display: inline-block;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABrUlEQVQ4T2NkwAESspK2C0oIaTIw/P//4t7zh8sXLHPAppQRlwH5jcW3TLzNVUHypzYcvTS5ZaI+7Q0ozAwsVZZmyWRmZmJ6+4NdSNLVjZeNg414F9QWBW9oTBX2Bzn15LXvDLu+ajEoaSmTb8C2D2oMavrqDGfWHbqt/uLJtL///v9advPV/OPHj3+HhQdKIKK7AGbA7VV7Gfx/v2X49o+BYcOLn/N7V25OIsmAGyv2MuT/e8UgxMfBUHnj28ruFRsjSDYg798rBmGaGNDe3hLNysyS9/ffX7H3r+6LtKUy8cBiARYGIC/AXFDylO2LpLLam48fP7159PjRNMauzva7jvY2SidOnWG4f/cag4b8H4Z///8xvHjzk+HlLz4GIV4ehjf3njIYcbMwgNLEpR+sDEqamgw2VhYM69ZvusaYmpr6NcDPi4uJCWeqxpra//z5y7Bx87avjIWFhQ9UVVXlf/z4gStbYBXn5ORkuHjx4guQAddNTEw0QKq+fv3KwMrKCqY5ODgY7ty5g9NQkPyjR4/ugbxQJCAgkPj3719eUpzw+/fvn1++fFkAALdq+Ka65YhZAAAAAElFTkSuQmCC');
}
.mapboxgl-popup-content .popup-title {
font-weight: bold;
}
.mapboxgl-popup-content .popup-title sub {
font-size: 10px;
font-weight: 100;
}
.mapboxgl-ctrl-attrib .mapboxgl-ctrl-attrib-inner {
color: #777;
display: inline !important;
}
.mapboxgl-ctrl-attrib {
padding: 2px 4px 2px 24px !important;
border-radius: 12px 3px 3px 12px !important;
} }
This source diff could not be displayed because it is too large. You can view the blob instead.
# 删除 Mapbox-GL-JS 的 Token 令牌校验,使用完整的离线版本
[参考链接](https://blog.csdn.net/Sakura1998gis/article/details/131707234)
如果升级了 js sdk,通过编辑 mapbox-gl.js 文件,搜索并修改以下代码:
```diff
- `_skuToken,this._requestManager._customAccessToken,(t=>{if(t&&(t.message===e.AUTH_ERR_MSG||401===t.status)`
+ `_skuToken,this._requestManager._customAccessToken,(t=>{if(t&&(false)`
```
This source diff could not be displayed because it is too large. You can view the blob instead.
/* eslint-disable */ /* eslint-disable */
/* prettier-ignore */ /* prettier-ignore */
// @ts-nocheck // @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import // Generated by unplugin-auto-import
export {} export {}
declare global { declare global {
...@@ -91,5 +90,5 @@ declare global { ...@@ -91,5 +90,5 @@ declare global {
// for type re-export // for type re-export
declare global { declare global {
// @ts-ignore // @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue' export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode } from 'vue'
} }
...@@ -7,6 +7,8 @@ export {} ...@@ -7,6 +7,8 @@ export {}
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
AffixFilter: typeof import('./../src/components/Map/Widgets/AffixFilter/src/AffixFilter.vue')['default']
BottomBar: typeof import('./../src/components/Map/Widgets/BottomBar/src/BottomBar.vue')['default']
CacheImage: typeof import('./../src/components/CacheImage/index.vue')['default'] CacheImage: typeof import('./../src/components/CacheImage/index.vue')['default']
Empty: typeof import('./../src/components/Empty/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'] FDragItem: typeof import('./../src/components/FirstUI/fui-drag/f-drag-item.vue')['default']
...@@ -133,8 +135,13 @@ declare module 'vue' { ...@@ -133,8 +135,13 @@ declare module 'vue' {
FuiWaterfall: typeof import('./../src/components/FirstUI/fui-waterfall/fui-waterfall.vue')['default'] FuiWaterfall: typeof import('./../src/components/FirstUI/fui-waterfall/fui-waterfall.vue')['default']
FuiWaterfallItem: typeof import('./../src/components/FirstUI/fui-waterfall-item/fui-waterfall-item.vue')['default'] FuiWaterfallItem: typeof import('./../src/components/FirstUI/fui-waterfall-item/fui-waterfall-item.vue')['default']
Icon: typeof import('./../src/components/Icon/index.vue')['default'] Icon: typeof import('./../src/components/Icon/index.vue')['default']
Legend: typeof import('./../src/components/Map/Widgets/Legend/src/Legend.vue')['default']
Mapbox: typeof import('./../src/components/Map/Mapbox/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
Switch: typeof import('./../src/components/Map/Widgets/Switch/src/Switch.vue')['default']
ThumbnailPreview: typeof import('./../src/components/ThumbnailPreview/index.vue')['default'] ThumbnailPreview: typeof import('./../src/components/ThumbnailPreview/index.vue')['default']
TimeBar: typeof import('./../src/components/Map/Widgets/TimeBar/src/TimeBar.vue')['default']
ToolBox: typeof import('./../src/components/Map/Widgets/ToolBox/src/ToolBox.vue')['default']
} }
} }
...@@ -2,6 +2,8 @@ import type { AxiosRequestConfig } from 'axios' ...@@ -2,6 +2,8 @@ import type { AxiosRequestConfig } from 'axios'
declare global { declare global {
type Recordable<T = any> = Record<string, T> type Recordable<T = any> = Record<string, T>
type Nullable<T> = T | null
type NonNullable<T> = T extends null | undefined ? never : T
interface ImportMetaEnv extends ViteEnv { interface ImportMetaEnv extends ViteEnv {
__: unknown __: unknown
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论