提交 aea522a2 作者: 方治民

feat: 新增 Echarts 组件,完善 Mapbox 组件及小部件

上级 19ce46c4
export * from './src/hook'
export { default as Echarts } from './src/index.vue'
import { merge } from 'lodash-es'
import { loadEchartsLibs } from '/@/components/Echarts/src/tools'
import * as EchartsHelper from './inject'
// 全局注入工具函数
window.EchartsHelper = EchartsHelper
export default {
data() {
return {
option: {},
}
},
methods: {
loadLibs: loadEchartsLibs,
init() {
// 如果已经初始化过了,就直接更新配置项
if (this.chart && this.chart.setOption) {
this.chart.setOption(this.option)
return
}
// 初始化组件
const chart = window.echarts.init(document.getElementById(this.option.id))
// 设置图表配置项
chart.setOption(this.option)
this.chart = chart
},
changeOption(option) {
if (!option?.id) {
return
}
this.option = merge(this.option, option)
if (typeof window.echarts === 'object' && typeof window.echarts.init === 'function') {
this.init()
} else {
this.loadLibs().then(() => {
this.init()
})
}
},
},
}
import type { EChartsOption } from 'echarts'
import { tryOnMounted } from '@vueuse/core'
import { isProdMode } from '@/utils/env'
// 组件名称
export const name = 'Echarts'
export interface EchartsInstance {
setOption: (option: Partial<EChartsOption>) => Promise<void>
}
/**
* 注册 Echarts 实例
* @param option Echarts 配置项
*/
export function useEcharts<T extends EchartsInstance, P extends EChartsOption>(
option?: P,
): [(instance: T) => void, EchartsInstance] {
const instanceRef = ref<T>()
function register(instance: T) {
if (isProdMode()) {
if (instance === unref(instanceRef)) {
return
}
onUnmounted(() => {
instanceRef.value = null
})
}
instanceRef.value = instance
option && instance?.setOption(option)
}
function getInstance() {
const instance = unref(instanceRef)
if (!instance) {
console.warn('Echarts instance is undefined!')
}
return instance as T
}
return [
register,
{
setOption: (option: Partial<P>): Promise<void> => {
return new Promise((resolve) => {
tryOnMounted(() => {
getInstance()?.setOption(option)
resolve()
})
})
},
},
]
}
<!-- renderjs 逻辑层 -->
<script>
import { nanoid } from 'nanoid'
export default {
emits: ['register'],
data() {
return {
id: nanoid(),
option: {},
}
},
mounted() {
this.$emit('register', this)
},
methods: {
setOption(option) {
this.option = {
id: this.id,
...option,
}
},
},
}
</script>
<!-- renderjs 视图层模块 -->
<script module="echarts" lang="renderjs" src="./echarts.module.js"></script>
<template>
<!-- #ifdef APP-PLUS || H5 -->
<view class="wrap echarts" :id="id" :option="option" :change:option="echarts.changeOption" />
<!-- #endif -->
<!-- #ifndef APP-PLUS || H5 -->
<view class="wrap empty">非 APP、H5 环境不支持</view>
<!-- #endif -->
</template>
<style lang="less" scoped>
//
</style>
export function symbolWeatherReplace(data: Recordable) {
return data
}
import { appendScript } from '/@/utils/dom'
/**
* 加载 Echarts 依赖库
*/
export function loadEchartsLibs() {
const id = 'echarts'
const version = '5.4.3'
const resource = `static/js/${id}/${version}/${id}`
return Promise.all([appendScript(id, `${resource}.min.js`)])
}
......@@ -5,7 +5,7 @@ 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'
import { useUserStoreWithOut } from '@/store/modules/user'
// 组件名称
export const name = 'Mapbox'
......@@ -45,6 +45,7 @@ export function useMapbox<T extends MapboxInstance, P extends MapboxConfig>(
return [
register,
{
setRegion: (region: string) => getInstance()?.setRegion(region),
setConfig: (config: Partial<P>) => getInstance()?.setConfig(config),
isReady: computed(() => instanceRef?.value?.isReady) as unknown as ComputedRef<boolean>,
......@@ -105,7 +106,7 @@ export function addDefaultSplotLayer(
map: MapboxInstance,
id: string,
layer?: Partial<mapboxgl.Layer>,
beforeId = 'fill-placeholder',
beforeId: string = 'fill-placeholder',
) {
map.addLayer(
merge(
......@@ -139,8 +140,8 @@ export function addDefaultSymbolLayer(
map: MapboxInstance,
id: string,
layer?: Partial<mapboxgl.Layer>,
beforeId = 'symbol-placeholder',
popop = true,
beforeId: string = 'symbol-placeholder',
popop: boolean = true,
) {
map.addLayer(
merge(
......@@ -190,9 +191,10 @@ export function flyTo(
* @returns source url
*/
export function buildGeoJSONSourceDataUrl(config: GeoJSONSourceDataUrlParams | string): string {
const userStore = useUserStoreWithOut()
const baseURL = `${API_URL}${API_URL_PREFIX}`
const defaultParams = {
Authorization: getToken(),
Authorization: userStore.getToken,
}
const url = typeof config === 'string' ? config : config.url
......
<script>
import { nanoid } from 'nanoid'
import { useUserStoreWithOut } from '@/store/modules/user'
import { getRegionConfig } from '@/components/Map/Mapbox/regions'
// FIXED: 重要说明
// renderjs 组件暂不支持 setup 组件写法,对 ts 支持也不友好,这里的写法需要参考 vue2 的写法
......@@ -18,6 +20,7 @@
data() {
return {
id: nanoid(),
onMapEvent: noop,
onLoadedEvent: noop,
onSourceRequestHandleEvent: noop,
onSourceRequestHandleErrorEvent: noop,
......@@ -38,7 +41,7 @@
setPaintPropertyOptions: undefined,
setLayoutPropertyOptions: undefined,
flyToOptions: undefined,
positionOptions: undefined,
regionOptions: undefined,
// change options 锁,结合 event,配合数组实现先进先出队列控制
changeLock: false,
......@@ -57,27 +60,6 @@
this.$emit('register', this)
},
methods: {
getCurrentPosition() {
// 获取用户定位
uni.getLocation({
type: 'wgs84',
success: (res) => {
// 更新定位
// 加入到 change 事件队列中
this.changeOptionsQueue.push({
fn: 'position',
coords: res,
timestamp: Date.now(),
})
// 尝试触发 change 事件
this.tryTriggerChange()
},
fail: () => {
Message.toast('获取位置失败,请打开定位权限')
},
})
},
onMapLoad(data) {
this.loaded = true
Message.hideLoading()
......@@ -91,8 +73,8 @@
// 触发 onSourceRequestHandle 事件
this.onSourceRequestHandleEvent?.(data)
},
onSourceRequestErrorHandle(data) {
console.debug('📢 Request Error Handle', data)
async onSourceRequestErrorHandle(data) {
console.debug(' Request Error Handle', data)
// 触发 onSourceRequestErrorHandle 事件
this.onSourceRequestErrorHandleEvent?.(data)
......@@ -102,7 +84,8 @@
const { status, message } = JSON.parse(data)
if (status === 401) {
// 跳转到登录页
uni.reLaunch({ url: '/pages/login/login' })
const userStore = useUserStoreWithOut()
await userStore.logout()
Message.toast('登录信息过期,请重新登录!')
} else {
Message.toast(message)
......@@ -113,6 +96,12 @@
}
}
},
// 监听地图事件(on,custom...)
onEventHandle(event) {
console.debug('📢 Map Event', event)
// 触发事件
this.onMapEvent?.(event)
},
// 尝试触发事件, 通过 changeLock 控制
tryTriggerChange() {
if (!this.changeLock && this.changeOptionsQueue.length) {
......@@ -123,19 +112,50 @@
},
// 监听地图配置从逻辑层到视图层的响应式变化事件
onMapOptionsChangeEvent(event) {
console.debug('🔊 Map Event', event)
console.debug('🔊 Map Options Change Event', event)
this.changeLock = false
this.tryTriggerChange()
},
/**
* 解析区域编码获得区域配置
* @param {string} code 区域编码
*/
analysisRegion(code) {
const region = code || '43'
const regionConfig = getRegionConfig(region)
return {
region,
regionConfig,
}
},
// 设置地图区域编码,用于覆盖遮罩层
setRegion(value) {
const { region, regionConfig } = this.analysisRegion(value)
// 加入到 change 事件队列中
this.changeOptionsQueue.push({
fn: 'region',
region,
regionConfig,
})
// 尝试触发 change 事件
this.tryTriggerChange()
},
// 设置地图配置
setConfig(config) {
const { region, regionConfig } = this.analysisRegion(config?.region)
this.options = {
container: this.id,
style: config?.style,
options: config?.options,
attribution: config?.attribution,
extra: config?.extra,
region,
regionConfig,
}
this.onMapEvent = config?.onMapEvent || noop
this.onLoadedEvent = config?.onLoaded || noop
this.onSourceRequestHandleEvent = config?.onSourceRequestHandle || noop
this.onSourceRequestErrorHandleEvent = config?.onSourceRequestErrorHandle || noop
......@@ -319,6 +339,24 @@
// 尝试触发 change 事件
this.tryTriggerChange()
},
// 获取当前位置
getCurrentPosition() {
// 获取用户定位
uni.getLocation({
type: 'wgs84',
success: (res) => {
// 更新定位
this.position = {
coords: res,
_: Date.now(),
}
},
fail: () => {
Message.toast('获取位置失败,请打开定位权限')
},
})
},
},
}
</script>
......@@ -355,8 +393,8 @@
:change:removeOptions="mapbox.changeRemoveOptions"
:flyToOptions="flyToOptions"
:change:flyToOptions="mapbox.changeFlyToOptions"
:positionOptions="positionOptions"
:change:positionOptions="mapbox.changePositionOptions"
:regionOptions="regionOptions"
:change:regionOptions="mapbox.changeRegionOptions"
/>
<!-- #endif -->
<!-- #ifndef APP-PLUS || H5 -->
......
......@@ -30,7 +30,7 @@
// JSON 数据
option: {
type: Object as PropType<Option>,
default: () => ({}),
default: () => ({}) as Option,
},
})
......@@ -80,6 +80,20 @@
toggleShow,
toggleExpand,
})
function getBorderColor(color: string) {
switch (color) {
case 'white':
case '#fff':
case '#ffffff':
case '#FFFFFF':
case 'rgb(255, 255, 255)':
case 'rgba(255, 255, 255, 1)':
return '#d7d7d7'
default:
return color
}
}
</script>
<template>
......@@ -107,7 +121,11 @@
<view
v-if="item.color"
class="color"
:style="{ backgroundColor: item.color, ...data.option.blockStyle }"
:style="{
backgroundColor: item.color,
border: `1rpx solid ${getBorderColor(item.color)}`,
...data.option.blockStyle,
}"
/>
<!-- 标签 -->
......@@ -209,9 +227,9 @@
}
.color {
width: 38rpx;
width: 42rpx;
height: 24rpx;
margin-right: 8rpx;
margin-right: 10rpx !important;
}
.label {
......
......@@ -13,7 +13,7 @@ export interface OptionItem {
export interface Option {
// 选项
items: OptionItem[] | OptionItem[][]
items?: OptionItem[] | OptionItem[][]
// label 样式
labelStyle?: Recordable
// 色块样式
......
......@@ -23,10 +23,12 @@ export function useTimeBarWidget<T extends TimeBarInstance, P extends TimeBarPro
{
...fns,
getTime: () => get()?.getTime(),
setTime: (time: Dayjs[]) => get()?.setTime(time),
setTime: (time: Dayjs[], mode: boolean) => get()?.setTime(time, mode),
getCheckedOption: () => get()?.getCheckedOption(),
setCheckedOption: (index: number) => get()?.setCheckedOption(index),
getTimeBarValue: () => get()?.getTimeBarValue(),
setHourRange: (hourRange: Array<string | number> | string) => get()?.setHourRange(hourRange),
setMinuteRange: (minuteRange: Array<string | number> | string) => get()?.setMinuteRange(minuteRange),
},
]
}
......
......@@ -32,6 +32,8 @@ export interface TimeBarTime {
format?: string
value?: Dayjs[]
onChange?: (e: TimeBarChangeEvent) => void
hourRange?: Array<number | string> | string
minuteRange?: Array<number | string> | string
}
export interface TimeBarButton {
......@@ -87,7 +89,7 @@ export interface TimeBarInstance extends BasicWidgetInstance<TimeBarProps> {
* 设置时间
* @param time 时间
*/
setTime: (time: Dayjs[]) => void
setTime: (time: Dayjs[], mode?: boolean) => void
/**
* 获取选中的选项
* @returns 选中的选项
......@@ -103,4 +105,17 @@ export interface TimeBarInstance extends BasicWidgetInstance<TimeBarProps> {
* @returns 时间栏的值
*/
getTimeBarValue: () => TimeBarChangeEvent
/**
* 设置小时可选范围
* @param hourRange
* @returns
*/
setHourRange: (hourRange: Array<number | string> | string) => void
/**
* 设置分钟可选范围
* @param minuteRange
* @returns
*/
setMinuteRange: (minuteRange: Array<number | string> | string) => void
}
......@@ -88,6 +88,7 @@
// 打开 Select 组件
model.selectPopup.title = button.name
model.selectPopup.options = button.options ?? []
model.selectPopup.checked = button.options?.filter((item) => item.checked).map((item) => item.value) ?? []
model.selectPopup.multiple = button.multiple ?? false
model.selectPopup.show = true
} else if (button.type === 'filter') {
......@@ -108,13 +109,26 @@
multiple: false,
title: '',
options: [],
onConfirm: (e: Recordable) => {
checked: [],
onConfirm: (item: Recordable, close?: boolean) => {
if (model.selectPopup.multiple) {
item.checked = !item.checked
} else {
model.selectPopup.options.forEach((option) => (option.checked = false))
item.checked = true
}
model.activeButton?.handle({
type: 'change',
name: model.activeButton.name,
value: toRaw(e.options),
value: toRaw(
model.selectPopup.options[model.selectPopup.multiple ? 'filter' : 'find'](
(item) => item.checked,
),
),
})
model.selectPopup.onClose()
close && model.selectPopup.onClose()
},
onClose: () => (model.selectPopup.show = false),
},
......@@ -191,22 +205,79 @@
<!-- 交互组件 -->
<!-- 1. Select Popup -->
<fui-select
<!-- <fui-select
maskClosable
:type="model.selectPopup.multiple ? 'select' : 'list'"
:show="model.selectPopup.show"
:title="`选择${model.selectPopup.title}`"
:options="model.selectPopup.options"
:multiple="model.selectPopup.multiple"
@click="model.selectPopup.onConfirm"
@confirm="model.selectPopup.onConfirm"
@close="model.selectPopup.onClose"
/>
/> -->
<fui-bottom-popup
:show="model.selectPopup.show"
:z-index="900"
@close="model.selectPopup.onClose"
:safeArea="false"
>
<view class="popup-wrap">
<view class="fui-title">{{ `选择${model.selectPopup.title}` }}</view>
<view class="fui-icon__close" @tap="model.selectPopup.onClose">
<fui-icon name="close" :size="48" />
</view>
<scroll-view scroll-y class="fui-scroll__view">
<view class="fui-custom__wrap">
<!-- 多选 -->
<template v-if="model.selectPopup.multiple">
<fui-checkbox-group>
<fui-label
v-for="(item, index) in model.selectPopup.options"
:key="index"
@tap="model.selectPopup.onConfirm(item, false)"
>
<fui-list-cell>
<view class="fui-align__center">
<fui-checkbox v-model:checked="item.checked" :value="item.value" />
<text class="fui-text pl-2">{{ item.text }}</text>
</view>
</fui-list-cell>
</fui-label>
</fui-checkbox-group>
</template>
<!-- 单选 -->
<template v-else>
<!-- -->
<fui-radio-group>
<fui-label
v-for="(item, index) in model.selectPopup.options"
:key="index"
@tap="model.selectPopup.onConfirm(item, true)"
>
<fui-list-cell>
<view class="fui-align__center">
<fui-radio :checked="item.checked" :value="item.value" />
<text class="fui-text pl-2">{{ item.text }}</text>
</view>
</fui-list-cell>
</fui-label>
</fui-radio-group>
</template>
</view>
</scroll-view>
</view>
<!-- 底部安全区 -->
<fui-safe-area />
</fui-bottom-popup>
<!-- 2. 过滤 Popup -->
<fui-bottom-popup
:show="model.filterPopup.show"
:z-index="900"
@close="model.filterPopup.onClose"
:safeArea="true"
:safeArea="false"
>
<view class="popup-wrap">
<view class="fui-title">{{ model.filterPopup.title }}</view>
......@@ -268,6 +339,9 @@
</view>
</scroll-view>
</view>
<!-- 底部安全区 -->
<fui-safe-area />
</fui-bottom-popup>
</view>
</template>
......@@ -358,7 +432,7 @@
}
.popup-wrap {
height: 500rpx;
height: 520rpx;
padding-top: 30rpx;
position: relative;
}
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -170,7 +170,32 @@
.mapboxgl-popup-content .popup-row {
display: flex;
align-items: flex-start;
}
.mapboxgl-popup-content .popup-row.timing {
justify-content: center;
padding-top: 6px;
}
.mapboxgl-popup-content .popup-row.timing button {
background: #1890ff;
border: none;
padding: 4px 8px;
color: #fff;
border-radius: 5px;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
}
.mapboxgl-popup-content .popup-row.timing button .icon {
width: 14px;
height: 14px;
margin-right: 5px;
display: inline-block;
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00IDloNHYxMUg0em0xMiA0aDR2N2gtNHptLTYtOWg0djE2aC00eiIvPjwvc3ZnPg==');
}
.mapboxgl-popup-content .popup-chart-icon {
......@@ -182,6 +207,7 @@
.mapboxgl-popup-content .popup-title {
font-weight: bold;
min-width: 3em;
}
.mapboxgl-popup-content .popup-title sub {
......
export function appendStylesheet(id: string, href: string) {
return new Promise((resolve, reject) => {
const element = document.getElementById(id) as HTMLLinkElement
if (element && element.href === href) {
resolve(0)
return
}
const link = document.createElement('link')
link.id = id
link.rel = 'stylesheet'
......@@ -12,9 +18,16 @@ export function appendStylesheet(id: string, href: string) {
export function appendScript(id: string, src: string) {
return new Promise((resolve, reject) => {
const element = document.getElementById(id) as HTMLScriptElement
if (element && element.src === src) {
resolve(0)
return
}
const script = document.createElement('script')
script.id = id
script.src = src
script.onload = resolve
script.onerror = script.onabort = reject
document.head.appendChild(script)
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论