提交 ba1f1b2f 作者: 方治民

feat: 添加 Mapbox、LinkedList 示例

上级 97412e34
......@@ -88,6 +88,7 @@
"mockjs": "^1.1.0",
"nanoid": "^4.0.1",
"pinia": "^2.0.33",
"pinyin-pro": "^3.13.1",
"qs": "~6.9.7",
"stompjs": "^2.3.3",
"urijs": "^1.19.11",
......@@ -108,6 +109,7 @@
"@dcloudio/vite-plugin-uni": "3.0.0-alpha-3070720230316001",
"@iconify/json": "^2.2.36",
"@types/lodash-es": "^4.17.7",
"@types/mapbox-gl": "^2.7.10",
"@types/mockjs": "^1.0.7",
"@types/node": "^18.15.3",
"@types/prettier": "^2.7.2",
......@@ -132,6 +134,7 @@
"jest-environment-node": "27.5.1",
"less": "^4.1.3",
"lint-staged": "^13.2.0",
"mapbox-gl": "^2.13.0",
"npm-run-all": "^4.1.5",
"picocolors": "^1.0.0",
"pont-engine": "^1.5.7",
......
......@@ -58,6 +58,10 @@
background-image: url('data:image/svg+xml,%3Csvg xmlns="http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg" width="20" height="20" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"%3E%3Cpath fill="currentColor" d="M12 16c1.671 0 3-1.331 3-3s-1.329-3-3-3s-3 1.331-3 3s1.329 3 3 3z"%2F%3E%3Cpath fill="currentColor" d="M20.817 11.186a8.94 8.94 0 0 0-1.355-3.219a9.053 9.053 0 0 0-2.43-2.43a8.95 8.95 0 0 0-3.219-1.355a9.028 9.028 0 0 0-1.838-.18V2L8 5l3.975 3V6.002c.484-.002.968.044 1.435.14a6.961 6.961 0 0 1 2.502 1.053a7.005 7.005 0 0 1 1.892 1.892A6.967 6.967 0 0 1 19 13a7.032 7.032 0 0 1-.55 2.725a7.11 7.11 0 0 1-.644 1.188a7.2 7.2 0 0 1-.858 1.039a7.028 7.028 0 0 1-3.536 1.907a7.13 7.13 0 0 1-2.822 0a6.961 6.961 0 0 1-2.503-1.054a7.002 7.002 0 0 1-1.89-1.89A6.996 6.996 0 0 1 5 13H3a9.02 9.02 0 0 0 1.539 5.034a9.096 9.096 0 0 0 2.428 2.428A8.95 8.95 0 0 0 12 22a9.09 9.09 0 0 0 1.814-.183a9.014 9.014 0 0 0 3.218-1.355a8.886 8.886 0 0 0 1.331-1.099a9.228 9.228 0 0 0 1.1-1.332A8.952 8.952 0 0 0 21 13a9.09 9.09 0 0 0-.183-1.814z"%2F%3E%3C%2Fsvg%3E') !important;
}
.mapboxgl-ctrl .mapboxgl-ctrl-layer-button .mapboxgl-ctrl-icon {
background-image: url('data:image/svg+xml,%3Csvg xmlns="http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg" width="24" height="24" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"%3E%3Cpath fill="currentColor" d="m13.387 3.425l6.365 4.243a1 1 0 0 1 0 1.664l-6.365 4.244a2.5 2.5 0 0 1-2.774 0L4.248 9.332a1 1 0 0 1 0-1.664l6.365-4.243a2.5 2.5 0 0 1 2.774 0Zm6.639 8.767a2.002 2.002 0 0 1-.577.598l-6.05 4.084a2.5 2.5 0 0 1-2.798 0l-6.05-4.084a2 2 0 0 1-.779-2.29l6.841 4.56a2.5 2.5 0 0 0 2.613.098l.16-.098l6.841-4.56a1.996 1.996 0 0 1-.201 1.692Zm0 3.25a2.002 2.002 0 0 1-.577.598l-6.05 4.084a2.5 2.5 0 0 1-2.798 0l-6.05-4.084a2 2 0 0 1-.779-2.29l6.841 4.56a2.5 2.5 0 0 0 2.613.098l.16-.098l6.841-4.56a1.996 1.996 0 0 1-.201 1.692Z"%2F%3E%3C%2Fsvg%3E') !important;
}
/* stylelint-disable-next-line selector-no-vendor-prefix */
.mapboxgl-map:-webkit-full-screen {
.mapboxgl-ctrl-bottom-left {
......@@ -71,49 +75,61 @@
}
.mapboxgl-ctrl-layer-button {
// position: relative;
z-index: 1;
}
.mapboxgl-ctrl-layer-wrap {
z-index: 100;
display: flex;
justify-items: center;
align-items: center;
position: absolute;
top: 0;
left: -220px;
visibility: hidden;
padding: 10px;
border-radius: 3px;
box-shadow: 0 0 3px #333;
background-color: #fff;
}
.mapboxgl-ctrl-layer {
width: 60px;
height: 60px;
display: flex;
justify-content: center;
border: 2px solid #fff;
position: relative;
}
.mapboxgl-ctrl-layer-wrap {
display: none;
justify-items: center;
align-items: center;
position: absolute;
right: 40px;
padding: 10px;
border-radius: 3px;
box-shadow: 0 0 3px #333;
background-color: #fff;
.mapboxgl-ctrl-layer {
width: 60px;
height: 60px;
display: flex;
justify-content: center;
border: 2px solid #fff;
position: relative;
&.active {
border-color: #3385ff;
}
img {
width: 100%;
height: 100%;
}
span {
display: inline-flex;
justify-content: center;
background-color: rgb(0 0 0 / 60%);
color: white;
position: absolute;
bottom: 0;
width: 100%;
}
}
}
.mapboxgl-ctrl-layer.active {
border-color: #3385ff;
}
&.-active .mapboxgl-ctrl-layer-wrap {
display: flex;
}
.mapboxgl-ctrl-layer img {
width: 100%;
height: 100%;
}
.mapboxgl-ctrl-layer span {
display: inline-flex;
justify-content: center;
background-color: rgb(0 0 0 / 60%);
color: white;
position: absolute;
bottom: 0;
width: 100%;
}
.mapboxgl-ctrl-layer-button.-active .mapboxgl-ctrl-layer-wrap {
visibility: visible;
}
.mapboxgl-ctrl-bottom-left,
.mapboxgl-ctrl-bottom-right {
padding-bottom: 0;
/* stylelint-disable-next-line function-no-unknown */
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
......@@ -2,7 +2,6 @@
const props = defineProps({
icon: {
type: String,
required: true,
},
size: {
type: [Number, String],
......
......@@ -8,6 +8,20 @@
}
},
// === 示例 ===
{
"path": "pages/example/mapbox/index",
"style": {
"navigationBarTitleText": "Mapbox 地图示例"
}
},
{
"path": "pages/example/linkedlist/index",
"style": {
"navigationBarTitleText": "LinkedList 大列表示例"
}
},
// === PDF 文件预览 ===
{
"path": "pages/common/viewer/pdf",
......
import Mock from 'mockjs'
export function getData() {
const data = Array(1000)
.fill(0)
.map(() => {
const name = Mock.Random.cname()
return {
name,
phone: Mock.Random.integer(13000000000, 19999999999),
avatar: Mock.Random.image('50x50', Mock.Random.color(), '#757575', 'png', name.charAt(0)),
unit: Mock.Random.cword(3, 5),
}
})
return {
data,
total: data.length,
}
}
<script setup lang="ts">
import { pinyin } from 'pinyin-pro'
import { getData } from './data'
interface Params {
multiple?: boolean
}
const data = reactive({
params: {} as Params,
search: '',
result: {} as { data: Recordable[]; total: number },
show: false,
})
const list = computed(() => {
const result = data.result?.data
?.filter((item: any) => {
if (data.search) {
return (
item.name?.includes(data.search) ||
item.unit?.includes(data.search) ||
String(item.phone)?.includes(data.search)
)
}
return true
})
?.map((item: any) => {
const props = {}
if (item.name) {
item.letter = pinyin(item.name.charAt(), { pattern: 'first' })[0].toUpperCase()
} else {
item.letter = '★'
}
return {
...item,
...props,
// TODO: 存在性能问题,需要优化后再使用
src: item.avatar || '/static/images/me/user_default.jpg',
text: item.name,
subText: item.unit,
}
})
.reduce((prev: any, cur: any) => {
const letter = cur.letter
const index = prev.findIndex((item: any) => item.letter === letter)
if (index === -1) {
prev.push({
letter,
data: [cur],
})
} else {
prev[index].data.push(cur)
}
return prev
}, [])
.sort((a: any, b: any) => {
return a.letter.charCodeAt(0) - b.letter.charCodeAt(0)
})
return result || []
})
onLoad(({ params }) => {
// data.params = JSON.parse(decodeURIComponent(params))
console.log('params', params, data.params)
// 获取人员列表
Message.loading('加载中...')
data.result = getData()
Message.hideLoading()
})
function search(e: any) {
data.search = e.detail.value || ''
}
const choose = (item: any) => {
// if (data.params?.multiple) {
// item.checked = !item.checked
// } else {
// uni.$emit('chooseShootOrderReceiverUser', item)
// uni.navigateBack()
// }
console.log('item', item)
}
</script>
<template>
<view class="wrap">
<!-- -->
<fui-index-list
isSrc
:listData="list"
@init="data.show = true"
:isSelect="data.params?.multiple"
:subRight="false"
@click="choose"
>
<fui-search-bar @input="search" @clear="data.search = ''" />
<template #footer>
<fui-loadmore v-if="!data.show" />
<fui-divider :text="`${data.result.total || 0}联系人`" v-if="data.show" />
</template>
</fui-index-list>
</view>
</template>
<style lang="less" scoped>
//
</style>
<script>
import { nanoid } from 'nanoid'
// FIXED: 重要说明
// renderjs 组件暂不支持 setup 组件写法,对 ts 支持也不友好,这里的写法需要参考 vue2 的写法
// 总共分为两块 script
// 1. 逻辑层 Script
// 2. 视图层 Script
// 通信方式
// 1. 通过在逻辑层 data 中设置的属性向视图层传递数据
// 2. 通过在逻辑层 methods 定义方法,在视图层调用传递参数(注意:只能传递可进行 JSON 序列化的数据)
export default {
data() {
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',
geometry: {
type: 'Point',
coordinates: [data.center.lng, data.center.lat],
},
properties: {
name: 'Hello World ✨',
},
}
console.log('onLoad', this.point, data)
},
getCurrentPosition() {
// 获取用户定位
uni.getLocation({
type: 'wgs84',
success: (res) => {
// 更新定位
this.position = {
coords: res,
timestamp: Date.now(),
}
},
fail: () => {
Message.toast('获取位置失败,请打开定位权限')
},
})
},
},
}
</script>
<!-- renderjs 视图层模块 -->
<script module="mapbox" lang="renderjs" src="./mapbox.module.js"></script>
<template>
<!-- #ifdef APP-PLUS || H5 -->
<view
class="map wrap"
:id="id"
: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>
<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>
import { appendScript, appendStylesheet } from '/@/utils/dom'
import { accessToken, defaultStyle, loadMapControl } from '/@/components/Map/Mapbox'
// renderjs 官方文档
// https://uniapp.dcloud.io/tutorial/renderjs.html
// renderjs 的一些细节问题
// https://juejin.cn/post/7049185827582115870
export default {
mounted() {},
methods: {
loadLibs() {
const id = 'mapbox-gl'
const resource = 'static/mapbox-gl-js/v2.13.0/mapbox-gl'
return Promise.all([appendScript(id, `${resource}.js`), appendStylesheet(id, `${resource}.css`)])
},
initMap(options) {
// 移除地图
if (this.map && this.map.remove) {
this.map.remove()
}
// 加载地图
const mapboxgl = window.mapboxgl
const map = new mapboxgl.Map({
accessToken,
container: options.container,
style: Object.assign({
...defaultStyle,
...options?.style,
}),
})
// 绑定作用域
this.map = map
// 加载地图控件
map.on('load', () => {
// #ifdef APP-PLUS
if (options?.control?.geolocate) {
window.navigator.permissions = undefined
options.control.geolocate = {
geolocation: {
getCurrentPosition: (onPositionSuccess, _onPositionError, options) => {
this.onPositionSuccess = onPositionSuccess
this.$ownerInstance.callMethod('getCurrentPosition', options)
},
},
}
}
// #endif
// 加载地图图层
this.initMapLayers()
.then(() => {
// 调用逻辑层方法
this.loaded = true
this.$ownerInstance.callMethod('onMapLoad', {
center: map.getCenter(),
})
// 加载地图控件
loadMapControl(mapboxgl, map, options?.control)
})
.catch(console.log)
})
},
initMapLayers() {
this.addPointLayer()
return this.addPopupImage()
},
addPointLayer() {
this.map.addLayer({
id: 'point',
type: 'symbol',
source: {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [],
},
},
layout: {
'text-field': ['get', 'name'],
'icon-text-fit': 'both',
'icon-image': 'popup',
'icon-allow-overlap': true,
'text-allow-overlap': true,
},
})
},
addPopupImage() {
return new Promise((resolve, reject) => {
const popup = 'https://docs.mapbox.com/mapbox-gl-js/assets/popup.png'
this.map.loadImage(popup, (error, image) => {
if (error) {
reject(error)
} else {
this.map.addImage('popup', image, {
stretchX: [
[25, 55],
[85, 115],
],
stretchY: [[25, 100]],
content: [25, 25, 115, 100],
pixelRatio: 2,
})
resolve()
}
})
})
},
changeOptions(options) {
if (!options.container) {
return
}
if (typeof window.mapboxgl === 'object') {
this.initMap(options)
} else {
this.loadLibs().then(() => {
this.initMap(options)
})
}
},
changePosition(position) {
if (!this.loaded) {
return
}
this.onPositionSuccess?.(position)
},
changeDataPoint(point) {
if (!this.loaded) {
return
}
// 更新数据源
this.map.getSource('point').setData({
type: 'FeatureCollection',
features: [point],
})
},
},
}
<script setup lang="ts">
import dayjs from 'dayjs'
import { checkUpgrade } from '@/utils/upgrade'
import { useRuntime } from '@/hooks/app/useRuntime'
const { app } = useRuntime()
const year = ref(dayjs().year())
const title = ref('Hello World')
const version = computed(() => app.value.version)
onMounted(() => {
// test API
API.example.hello.request().then((body) => {
title.value = body
console.log('[API]', body, $app.name, $app.version)
Message.toast(body)
})
// @unocss-include
const data = reactive({
items: [
{
name: 'Mapbox 地图示例',
icon: 'emojione-globe-showing-europe-africa',
page: '/pages/example/mapbox/index',
},
{
name: 'LinkedList 大列表示例',
icon: 'emojione-letter-z',
page: '/pages/example/linkedlist/index',
},
],
})
const surprise = () => {
// #ifdef APP-PLUS
const orientation = plus.navigator.getOrientation()
if (orientation === 0) {
plus.screen.lockOrientation('landscape-primary')
} else if (orientation === 90) {
plus.screen.lockOrientation('portrait-primary')
}
Message.toast('🥳 surprise~')
// #endif
// #ifndef APP-PLUS
Message.toast(`在手机上运行点击有惊喜~ ╰(*°▽°*)╯`)
// #endif
}
const animate = ref()
const handUp = () => {
if (animate.value) {
return
}
Message.toast('👋🏻')
animate.value = 'animate-swing'
setTimeout(() => (animate.value = null), 1500)
function goto(url: string) {
uni.navigateTo({ url })
}
</script>
<template>
<view class="content flex-center flex-col">
<fui-avatar
src="/static/logo.png"
radius="14"
size="large"
background="transparent"
class="!mb-5"
@click="surprise"
/>
<view class="flex-center flex-col mb-5">
<text class="title">{{ title }}</text>
<text class="title">{{ $t('app.hello') }}</text>
<Icon icon="emojione:grinning-face" size="48" class="mt-3" />
<uni-icons type="hand-up" size="30" class="mt-3" :class="[animate]" @click="handUp" />
</view>
<fui-footer isFixed>
<template #text>
<view v-if="version" @click="checkUpgrade(true)">V{{ version }}</view>
<view class="mt-1">壹润科技 版权所有</view>
<view class="mt-1">Copyright © 2021-{{ year }} Yiring. All Ringhts Reserved</view>
</template>
</fui-footer>
<view class="fui-wrap">
<scroll-view class="fui-scroll__box" scroll-y :style="{ height: '100%' }">
<view class="fui-list__view">
<fui-list-cell
arrow
:padding="[0, '32rpx']"
:bottomBorder="false"
radius="16rpx"
marginTop="24"
v-for="(item, idx) in data.items"
:key="idx"
@click="goto(item.page)"
>
<view class="fui-list__item fui-align__center">
<Icon :class="[`icon-${item.icon}`]" size="48" />
<text>{{ item.name }}</text>
</view>
</fui-list-cell>
</view>
</scroll-view>
</view>
</template>
<style lang="less" scoped>
.content {
height: calc(100vh - 100rpx);
<style lang="less">
page {
height: 100%;
}
.fui-swiper__box,
.fui-swiper__item,
.fui-scroll__box {
width: 100%;
}
.fui-list__view {
width: 100%;
min-height: 101%;
padding: 8rpx 32rpx 32rpx;
box-sizing: border-box;
}
.fui-list__item {
width: 100%;
height: 112rpx;
}
.title {
.fui-list__item text {
padding-left: 24rpx;
padding-right: 12rpx;
flex-shrink: 0;
}
.fui-item__icon-box {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 36rpx;
color: #8f8f94;
margin: 50rpx 0 30rpx 0;
}
.fui-item__icon,
.fui-vip__icon {
width: 48rpx;
height: 48rpx;
display: block;
flex-shrink: 0;
}
</style>
This source diff could not be displayed because it is too large. You can view the blob instead.
export function appendStylesheet(id: string, href: string) {
return new Promise((resolve, reject) => {
const link = document.createElement('link')
link.id = id
link.rel = 'stylesheet'
link.href = href
link.onload = resolve
link.onerror = link.onabort = reject
document.head.appendChild(link)
})
}
export function appendScript(id: string, src: string) {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.id = id
script.src = src
script.onload = resolve
script.onerror = script.onabort = reject
document.head.appendChild(script)
})
}
export function removeChild(id: string) {
const child = document.getElementById(id)
if (child) {
child.parentNode.removeChild(child)
}
}
......@@ -18,11 +18,14 @@ const transformRules = {
'=': '_eqe_',
}
const ICONS = ['emojione:globe-showing-europe-africa', 'emojione:letter-z']
export default defineConfig({
presets: [
// https://github.com/MellowCo/unocss-preset-weapp
presetWeapp(),
],
safelist: ICONS.map((icon) => `icon-${icon.replace(/:/g, '-')}`),
shortcuts: [
{
'border-base': 'border border-[#eee]',
......@@ -44,6 +47,7 @@ export default defineConfig({
// https://github.com/MellowCo/unocss-preset-weapp/tree/main/src/transformer/transformerClass
transformerClass({
transformRules,
include: [/\.[jt]sx$/, /\.vue$/, /\.vue\?vue/],
}),
],
})
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论