feat: initialize mini program map engine
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
.tmp-ts/
|
||||||
|
project.private.config.json
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
15
miniprogram/app.json
Normal file
15
miniprogram/app.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"pages": [
|
||||||
|
"pages/map/map",
|
||||||
|
"pages/index/index",
|
||||||
|
"pages/logs/logs"
|
||||||
|
],
|
||||||
|
"window": {
|
||||||
|
"navigationBarTextStyle": "black",
|
||||||
|
"navigationBarTitleText": "CMR Mini",
|
||||||
|
"navigationBarBackgroundColor": "#ffffff"
|
||||||
|
},
|
||||||
|
"style": "v2",
|
||||||
|
"componentFramework": "glass-easel",
|
||||||
|
"lazyCodeLoading": "requiredComponents"
|
||||||
|
}
|
||||||
18
miniprogram/app.ts
Normal file
18
miniprogram/app.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// app.ts
|
||||||
|
App<IAppOption>({
|
||||||
|
globalData: {},
|
||||||
|
onLaunch() {
|
||||||
|
// 展示本地存储能力
|
||||||
|
const logs = wx.getStorageSync('logs') || []
|
||||||
|
logs.unshift(Date.now())
|
||||||
|
wx.setStorageSync('logs', logs)
|
||||||
|
|
||||||
|
// 登录
|
||||||
|
wx.login({
|
||||||
|
success: res => {
|
||||||
|
console.log(res.code)
|
||||||
|
// 发送 res.code 到后台换取 openId, sessionKey, unionId
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
10
miniprogram/app.wxss
Normal file
10
miniprogram/app.wxss
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/**app.wxss**/
|
||||||
|
.container {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 200rpx 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
98
miniprogram/engine/camera/camera.ts
Normal file
98
miniprogram/engine/camera/camera.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
export interface CameraState {
|
||||||
|
centerWorldX: number
|
||||||
|
centerWorldY: number
|
||||||
|
viewportWidth: number
|
||||||
|
viewportHeight: number
|
||||||
|
visibleColumns: number
|
||||||
|
translateX?: number
|
||||||
|
translateY?: number
|
||||||
|
rotationRad?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScreenPoint {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorldPoint {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTileSizePx(camera: CameraState): number {
|
||||||
|
if (!camera.viewportWidth || !camera.visibleColumns) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return camera.viewportWidth / camera.visibleColumns
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rotateScreenPoint(point: ScreenPoint, centerX: number, centerY: number, rotationRad: number): ScreenPoint {
|
||||||
|
if (!rotationRad) {
|
||||||
|
return point
|
||||||
|
}
|
||||||
|
|
||||||
|
const deltaX = point.x - centerX
|
||||||
|
const deltaY = point.y - centerY
|
||||||
|
const cos = Math.cos(rotationRad)
|
||||||
|
const sin = Math.sin(rotationRad)
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: centerX + deltaX * cos - deltaY * sin,
|
||||||
|
y: centerY + deltaX * sin + deltaY * cos,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function worldToScreen(
|
||||||
|
camera: CameraState,
|
||||||
|
world: WorldPoint,
|
||||||
|
includeTranslate = false,
|
||||||
|
): ScreenPoint {
|
||||||
|
const tileSize = getTileSizePx(camera)
|
||||||
|
const translateX = includeTranslate ? (camera.translateX || 0) : 0
|
||||||
|
const translateY = includeTranslate ? (camera.translateY || 0) : 0
|
||||||
|
const centerX = camera.viewportWidth / 2
|
||||||
|
const centerY = camera.viewportHeight / 2
|
||||||
|
|
||||||
|
const rotated = rotateScreenPoint(
|
||||||
|
{
|
||||||
|
x: centerX + (world.x - camera.centerWorldX) * tileSize,
|
||||||
|
y: centerY + (world.y - camera.centerWorldY) * tileSize,
|
||||||
|
},
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
camera.rotationRad || 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: rotated.x + translateX,
|
||||||
|
y: rotated.y + translateY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function screenToWorld(
|
||||||
|
camera: CameraState,
|
||||||
|
screen: ScreenPoint,
|
||||||
|
includeTranslate = true,
|
||||||
|
): WorldPoint {
|
||||||
|
const tileSize = getTileSizePx(camera)
|
||||||
|
const translateX = includeTranslate ? (camera.translateX || 0) : 0
|
||||||
|
const translateY = includeTranslate ? (camera.translateY || 0) : 0
|
||||||
|
const centerX = camera.viewportWidth / 2
|
||||||
|
const centerY = camera.viewportHeight / 2
|
||||||
|
|
||||||
|
const unrotated = rotateScreenPoint(
|
||||||
|
{
|
||||||
|
x: screen.x - translateX,
|
||||||
|
y: screen.y - translateY,
|
||||||
|
},
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
-(camera.rotationRad || 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: camera.centerWorldX + (unrotated.x - centerX) / tileSize,
|
||||||
|
y: camera.centerWorldY + (unrotated.y - centerY) / tileSize,
|
||||||
|
}
|
||||||
|
}
|
||||||
51
miniprogram/engine/layer/gpsLayer.ts
Normal file
51
miniprogram/engine/layer/gpsLayer.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { type CameraState } from '../camera/camera'
|
||||||
|
import { lonLatToWorldTile } from '../../utils/projection'
|
||||||
|
import { worldToScreen } from '../camera/camera'
|
||||||
|
import { type MapLayer, type LayerRenderContext } from './mapLayer'
|
||||||
|
import { type MapScene } from '../renderer/mapRenderer'
|
||||||
|
import { type ScreenPoint } from './trackLayer'
|
||||||
|
|
||||||
|
export class GpsLayer implements MapLayer {
|
||||||
|
projectPoint(scene: MapScene, camera: CameraState): ScreenPoint {
|
||||||
|
const worldPoint = lonLatToWorldTile(scene.gpsPoint, scene.zoom)
|
||||||
|
const screenPoint = worldToScreen(camera, worldPoint, false)
|
||||||
|
return {
|
||||||
|
x: screenPoint.x + scene.translateX,
|
||||||
|
y: screenPoint.y + scene.translateY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPulseRadius(pulseFrame: number): number {
|
||||||
|
return 18 + 6 * (0.5 + 0.5 * Math.sin(pulseFrame / 6))
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(context: LayerRenderContext): void {
|
||||||
|
const { ctx, camera, scene, pulseFrame } = context
|
||||||
|
const gpsScreenPoint = this.projectPoint(scene, camera)
|
||||||
|
const pulse = this.getPulseRadius(pulseFrame)
|
||||||
|
|
||||||
|
ctx.save()
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.fillStyle = 'rgba(33, 158, 188, 0.22)'
|
||||||
|
ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, pulse, 0, Math.PI * 2)
|
||||||
|
ctx.fill()
|
||||||
|
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.fillStyle = '#21a1bc'
|
||||||
|
ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, 9, 0, Math.PI * 2)
|
||||||
|
ctx.fill()
|
||||||
|
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.strokeStyle = '#ffffff'
|
||||||
|
ctx.lineWidth = 3
|
||||||
|
ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, 13, 0, Math.PI * 2)
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
ctx.fillStyle = '#0b3d4a'
|
||||||
|
ctx.font = 'bold 16px sans-serif'
|
||||||
|
ctx.textAlign = 'left'
|
||||||
|
ctx.textBaseline = 'bottom'
|
||||||
|
ctx.fillText('GPS', gpsScreenPoint.x + 14, gpsScreenPoint.y - 12)
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
}
|
||||||
15
miniprogram/engine/layer/mapLayer.ts
Normal file
15
miniprogram/engine/layer/mapLayer.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { type CameraState } from '../camera/camera'
|
||||||
|
import { type MapScene } from '../renderer/mapRenderer'
|
||||||
|
import { type TileStore } from '../tile/tileStore'
|
||||||
|
|
||||||
|
export interface LayerRenderContext {
|
||||||
|
ctx: any
|
||||||
|
camera: CameraState
|
||||||
|
scene: MapScene
|
||||||
|
pulseFrame: number
|
||||||
|
tileStore: TileStore
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MapLayer {
|
||||||
|
draw(context: LayerRenderContext): void
|
||||||
|
}
|
||||||
124
miniprogram/engine/layer/tileLayer.ts
Normal file
124
miniprogram/engine/layer/tileLayer.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import { createTileGrid, type TileItem } from '../../utils/tile'
|
||||||
|
import { getTileSizePx, type CameraState } from '../camera/camera'
|
||||||
|
import { type MapScene } from '../renderer/mapRenderer'
|
||||||
|
import { type TileStore } from '../tile/tileStore'
|
||||||
|
import { type MapLayer, type LayerRenderContext } from './mapLayer'
|
||||||
|
|
||||||
|
function buildGridKey(scene: MapScene, tileSize: number): string {
|
||||||
|
return [
|
||||||
|
scene.tileSource,
|
||||||
|
scene.zoom,
|
||||||
|
scene.centerTileX,
|
||||||
|
scene.centerTileY,
|
||||||
|
scene.viewportWidth,
|
||||||
|
scene.viewportHeight,
|
||||||
|
tileSize,
|
||||||
|
scene.overdraw,
|
||||||
|
].join('|')
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TileLayer implements MapLayer {
|
||||||
|
lastVisibleTileCount: number
|
||||||
|
lastReadyTileCount: number
|
||||||
|
cachedGridKey: string
|
||||||
|
cachedTiles: TileItem[]
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.lastVisibleTileCount = 0
|
||||||
|
this.lastReadyTileCount = 0
|
||||||
|
this.cachedGridKey = ''
|
||||||
|
this.cachedTiles = []
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareTiles(scene: MapScene, camera: CameraState, tileStore: TileStore): TileItem[] {
|
||||||
|
const tileSize = getTileSizePx(camera)
|
||||||
|
if (!tileSize) {
|
||||||
|
this.lastVisibleTileCount = 0
|
||||||
|
this.lastReadyTileCount = 0
|
||||||
|
this.cachedGridKey = ''
|
||||||
|
this.cachedTiles = []
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const gridKey = buildGridKey(scene, tileSize)
|
||||||
|
if (gridKey !== this.cachedGridKey) {
|
||||||
|
this.cachedGridKey = gridKey
|
||||||
|
this.cachedTiles = createTileGrid({
|
||||||
|
urlTemplate: scene.tileSource,
|
||||||
|
zoom: scene.zoom,
|
||||||
|
centerTileX: scene.centerTileX,
|
||||||
|
centerTileY: scene.centerTileY,
|
||||||
|
viewportWidth: scene.viewportWidth,
|
||||||
|
viewportHeight: scene.viewportHeight,
|
||||||
|
tileSize,
|
||||||
|
overdraw: scene.overdraw,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
tileStore.queueVisibleTiles(this.cachedTiles, scene, gridKey)
|
||||||
|
this.lastVisibleTileCount = this.cachedTiles.length
|
||||||
|
this.lastReadyTileCount = 0
|
||||||
|
return this.cachedTiles
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(context: LayerRenderContext): void {
|
||||||
|
const { ctx, scene, camera, tileStore } = context
|
||||||
|
const tiles = this.prepareTiles(scene, camera, tileStore)
|
||||||
|
|
||||||
|
for (const tile of tiles) {
|
||||||
|
const entry = tileStore.getEntry(tile.url)
|
||||||
|
const drawLeft = tile.leftPx + scene.translateX
|
||||||
|
const drawTop = tile.topPx + scene.translateY
|
||||||
|
|
||||||
|
if (entry && entry.status === 'ready' && entry.image) {
|
||||||
|
ctx.drawImage(entry.image, drawLeft, drawTop, tile.sizePx, tile.sizePx)
|
||||||
|
this.lastReadyTileCount += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentFallback = tileStore.getParentFallbackSlice(tile, scene)
|
||||||
|
let drewFallback = false
|
||||||
|
if (parentFallback) {
|
||||||
|
ctx.drawImage(
|
||||||
|
parentFallback.entry.image,
|
||||||
|
parentFallback.sourceX,
|
||||||
|
parentFallback.sourceY,
|
||||||
|
parentFallback.sourceWidth,
|
||||||
|
parentFallback.sourceHeight,
|
||||||
|
drawLeft,
|
||||||
|
drawTop,
|
||||||
|
tile.sizePx,
|
||||||
|
tile.sizePx,
|
||||||
|
)
|
||||||
|
drewFallback = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const childFallback = tileStore.getChildFallback(tile, scene)
|
||||||
|
if (childFallback) {
|
||||||
|
const cellWidth = tile.sizePx / childFallback.division
|
||||||
|
const cellHeight = tile.sizePx / childFallback.division
|
||||||
|
for (const child of childFallback.children) {
|
||||||
|
const childImageWidth = child.entry.image.width || 256
|
||||||
|
const childImageHeight = child.entry.image.height || 256
|
||||||
|
ctx.drawImage(
|
||||||
|
child.entry.image,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
childImageWidth,
|
||||||
|
childImageHeight,
|
||||||
|
drawLeft + child.offsetX * cellWidth,
|
||||||
|
drawTop + child.offsetY * cellHeight,
|
||||||
|
cellWidth,
|
||||||
|
cellHeight,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
drewFallback = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!drewFallback) {
|
||||||
|
ctx.fillStyle = entry && entry.status === 'error' ? '#d9b2b2' : '#dbeed4'
|
||||||
|
ctx.fillRect(drawLeft, drawTop, tile.sizePx, tile.sizePx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
61
miniprogram/engine/layer/trackLayer.ts
Normal file
61
miniprogram/engine/layer/trackLayer.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { type CameraState } from '../camera/camera'
|
||||||
|
import { lonLatToWorldTile } from '../../utils/projection'
|
||||||
|
import { worldToScreen } from '../camera/camera'
|
||||||
|
import { type MapLayer, type LayerRenderContext } from './mapLayer'
|
||||||
|
import { type MapScene } from '../renderer/mapRenderer'
|
||||||
|
|
||||||
|
export interface ScreenPoint {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TrackLayer implements MapLayer {
|
||||||
|
projectPoints(scene: MapScene, camera: CameraState): ScreenPoint[] {
|
||||||
|
return scene.track.map((point) => {
|
||||||
|
const worldPoint = lonLatToWorldTile(point, scene.zoom)
|
||||||
|
const screenPoint = worldToScreen(camera, worldPoint, false)
|
||||||
|
return {
|
||||||
|
x: screenPoint.x + scene.translateX,
|
||||||
|
y: screenPoint.y + scene.translateY,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(context: LayerRenderContext): void {
|
||||||
|
const { ctx, camera, scene } = context
|
||||||
|
const points = this.projectPoints(scene, camera)
|
||||||
|
|
||||||
|
ctx.save()
|
||||||
|
ctx.lineCap = 'round'
|
||||||
|
ctx.lineJoin = 'round'
|
||||||
|
ctx.strokeStyle = 'rgba(23, 109, 93, 0.96)'
|
||||||
|
ctx.lineWidth = 6
|
||||||
|
ctx.beginPath()
|
||||||
|
|
||||||
|
points.forEach((screenPoint, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
ctx.moveTo(screenPoint.x, screenPoint.y)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.lineTo(screenPoint.x, screenPoint.y)
|
||||||
|
})
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
ctx.fillStyle = '#f7fbf2'
|
||||||
|
ctx.strokeStyle = '#176d5d'
|
||||||
|
ctx.lineWidth = 4
|
||||||
|
points.forEach((screenPoint, index) => {
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(screenPoint.x, screenPoint.y, 10, 0, Math.PI * 2)
|
||||||
|
ctx.fill()
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.fillStyle = '#176d5d'
|
||||||
|
ctx.font = 'bold 14px sans-serif'
|
||||||
|
ctx.textAlign = 'center'
|
||||||
|
ctx.textBaseline = 'middle'
|
||||||
|
ctx.fillText(String(index + 1), screenPoint.x, screenPoint.y)
|
||||||
|
ctx.fillStyle = '#f7fbf2'
|
||||||
|
})
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
}
|
||||||
1429
miniprogram/engine/map/mapEngine.ts
Normal file
1429
miniprogram/engine/map/mapEngine.ts
Normal file
File diff suppressed because it is too large
Load Diff
247
miniprogram/engine/renderer/canvasMapRenderer.ts
Normal file
247
miniprogram/engine/renderer/canvasMapRenderer.ts
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
import { getTileSizePx, type CameraState } from '../camera/camera'
|
||||||
|
import {
|
||||||
|
TileStore,
|
||||||
|
type TileStoreCallbacks,
|
||||||
|
type TileStoreStats,
|
||||||
|
} from '../tile/tileStore'
|
||||||
|
import { type LonLatPoint } from '../../utils/projection'
|
||||||
|
import { type MapLayer } from '../layer/mapLayer'
|
||||||
|
import { TileLayer } from '../layer/tileLayer'
|
||||||
|
import { TrackLayer } from '../layer/trackLayer'
|
||||||
|
import { GpsLayer } from '../layer/gpsLayer'
|
||||||
|
|
||||||
|
const RENDER_FRAME_MS = 16
|
||||||
|
|
||||||
|
export interface CanvasMapScene {
|
||||||
|
tileSource: string
|
||||||
|
zoom: number
|
||||||
|
centerTileX: number
|
||||||
|
centerTileY: number
|
||||||
|
viewportWidth: number
|
||||||
|
viewportHeight: number
|
||||||
|
visibleColumns: number
|
||||||
|
overdraw: number
|
||||||
|
translateX: number
|
||||||
|
translateY: number
|
||||||
|
rotationRad: number
|
||||||
|
previewScale: number
|
||||||
|
previewOriginX: number
|
||||||
|
previewOriginY: number
|
||||||
|
track: LonLatPoint[]
|
||||||
|
gpsPoint: LonLatPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CanvasMapRendererStats = TileStoreStats
|
||||||
|
|
||||||
|
function buildCamera(scene: CanvasMapScene): CameraState {
|
||||||
|
return {
|
||||||
|
centerWorldX: scene.centerTileX,
|
||||||
|
centerWorldY: scene.centerTileY,
|
||||||
|
viewportWidth: scene.viewportWidth,
|
||||||
|
viewportHeight: scene.viewportHeight,
|
||||||
|
visibleColumns: scene.visibleColumns,
|
||||||
|
rotationRad: scene.rotationRad,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CanvasMapRenderer {
|
||||||
|
canvas: any
|
||||||
|
ctx: any
|
||||||
|
dpr: number
|
||||||
|
scene: CanvasMapScene | null
|
||||||
|
tileStore: TileStore
|
||||||
|
tileLayer: TileLayer
|
||||||
|
layers: MapLayer[]
|
||||||
|
renderTimer: number
|
||||||
|
animationTimer: number
|
||||||
|
destroyed: boolean
|
||||||
|
animationPaused: boolean
|
||||||
|
pulseFrame: number
|
||||||
|
lastStats: CanvasMapRendererStats
|
||||||
|
onStats?: (stats: CanvasMapRendererStats) => void
|
||||||
|
onTileError?: (message: string) => void
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
onStats?: (stats: CanvasMapRendererStats) => void,
|
||||||
|
onTileError?: (message: string) => void,
|
||||||
|
) {
|
||||||
|
this.onStats = onStats
|
||||||
|
this.onTileError = onTileError
|
||||||
|
this.canvas = null
|
||||||
|
this.ctx = null
|
||||||
|
this.dpr = 1
|
||||||
|
this.scene = null
|
||||||
|
this.tileStore = new TileStore({
|
||||||
|
onTileReady: () => {
|
||||||
|
this.scheduleRender()
|
||||||
|
},
|
||||||
|
onTileError: (message) => {
|
||||||
|
if (this.onTileError) {
|
||||||
|
this.onTileError(message)
|
||||||
|
}
|
||||||
|
this.scheduleRender()
|
||||||
|
},
|
||||||
|
} satisfies TileStoreCallbacks)
|
||||||
|
this.tileLayer = new TileLayer()
|
||||||
|
this.layers = [
|
||||||
|
this.tileLayer,
|
||||||
|
new TrackLayer(),
|
||||||
|
new GpsLayer(),
|
||||||
|
]
|
||||||
|
this.renderTimer = 0
|
||||||
|
this.animationTimer = 0
|
||||||
|
this.destroyed = false
|
||||||
|
this.animationPaused = false
|
||||||
|
this.pulseFrame = 0
|
||||||
|
this.lastStats = {
|
||||||
|
visibleTileCount: 0,
|
||||||
|
readyTileCount: 0,
|
||||||
|
memoryTileCount: 0,
|
||||||
|
diskTileCount: 0,
|
||||||
|
memoryHitCount: 0,
|
||||||
|
diskHitCount: 0,
|
||||||
|
networkFetchCount: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attachCanvas(canvasNode: any, width: number, height: number, dpr: number): void {
|
||||||
|
this.canvas = canvasNode
|
||||||
|
this.ctx = canvasNode.getContext('2d')
|
||||||
|
this.dpr = dpr || 1
|
||||||
|
|
||||||
|
canvasNode.width = Math.max(1, Math.floor(width * this.dpr))
|
||||||
|
canvasNode.height = Math.max(1, Math.floor(height * this.dpr))
|
||||||
|
|
||||||
|
if (typeof this.ctx.setTransform === 'function') {
|
||||||
|
this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0)
|
||||||
|
} else {
|
||||||
|
this.ctx.scale(this.dpr, this.dpr)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tileStore.attachCanvas(canvasNode)
|
||||||
|
this.startAnimation()
|
||||||
|
this.scheduleRender()
|
||||||
|
}
|
||||||
|
|
||||||
|
updateScene(scene: CanvasMapScene): void {
|
||||||
|
this.scene = scene
|
||||||
|
this.scheduleRender()
|
||||||
|
}
|
||||||
|
|
||||||
|
setAnimationPaused(paused: boolean): void {
|
||||||
|
this.animationPaused = paused
|
||||||
|
if (!paused) {
|
||||||
|
this.scheduleRender()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
this.destroyed = true
|
||||||
|
if (this.renderTimer) {
|
||||||
|
clearTimeout(this.renderTimer)
|
||||||
|
this.renderTimer = 0
|
||||||
|
}
|
||||||
|
if (this.animationTimer) {
|
||||||
|
clearTimeout(this.animationTimer)
|
||||||
|
this.animationTimer = 0
|
||||||
|
}
|
||||||
|
this.tileStore.destroy()
|
||||||
|
this.canvas = null
|
||||||
|
this.ctx = null
|
||||||
|
this.scene = null
|
||||||
|
}
|
||||||
|
|
||||||
|
startAnimation(): void {
|
||||||
|
if (this.animationTimer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const tick = () => {
|
||||||
|
if (this.destroyed) {
|
||||||
|
this.animationTimer = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.animationPaused) {
|
||||||
|
this.pulseFrame = (this.pulseFrame + 1) % 360
|
||||||
|
this.scheduleRender()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.animationTimer = setTimeout(tick, 33) as unknown as number
|
||||||
|
}
|
||||||
|
|
||||||
|
tick()
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleRender(): void {
|
||||||
|
if (this.renderTimer || !this.ctx || !this.scene || this.destroyed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderTimer = setTimeout(() => {
|
||||||
|
this.renderTimer = 0
|
||||||
|
this.renderFrame()
|
||||||
|
}, RENDER_FRAME_MS) as unknown as number
|
||||||
|
}
|
||||||
|
|
||||||
|
emitStats(stats: CanvasMapRendererStats): void {
|
||||||
|
if (
|
||||||
|
stats.visibleTileCount === this.lastStats.visibleTileCount
|
||||||
|
&& stats.readyTileCount === this.lastStats.readyTileCount
|
||||||
|
&& stats.memoryTileCount === this.lastStats.memoryTileCount
|
||||||
|
&& stats.diskTileCount === this.lastStats.diskTileCount
|
||||||
|
&& stats.memoryHitCount === this.lastStats.memoryHitCount
|
||||||
|
&& stats.diskHitCount === this.lastStats.diskHitCount
|
||||||
|
&& stats.networkFetchCount === this.lastStats.networkFetchCount
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastStats = stats
|
||||||
|
if (this.onStats) {
|
||||||
|
this.onStats(stats)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFrame(): void {
|
||||||
|
if (!this.ctx || !this.scene) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const scene = this.scene
|
||||||
|
const ctx = this.ctx
|
||||||
|
const camera = buildCamera(scene)
|
||||||
|
const tileSize = getTileSizePx(camera)
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, scene.viewportWidth, scene.viewportHeight)
|
||||||
|
ctx.fillStyle = '#dbeed4'
|
||||||
|
ctx.fillRect(0, 0, scene.viewportWidth, scene.viewportHeight)
|
||||||
|
|
||||||
|
if (!tileSize) {
|
||||||
|
this.emitStats(this.tileStore.getStats(0, 0))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const previewScale = scene.previewScale || 1
|
||||||
|
const previewOriginX = scene.previewOriginX || scene.viewportWidth / 2
|
||||||
|
const previewOriginY = scene.previewOriginY || scene.viewportHeight / 2
|
||||||
|
|
||||||
|
ctx.save()
|
||||||
|
ctx.translate(previewOriginX, previewOriginY)
|
||||||
|
ctx.scale(previewScale, previewScale)
|
||||||
|
ctx.translate(-previewOriginX, -previewOriginY)
|
||||||
|
|
||||||
|
for (const layer of this.layers) {
|
||||||
|
layer.draw({
|
||||||
|
ctx,
|
||||||
|
camera,
|
||||||
|
scene,
|
||||||
|
pulseFrame: this.pulseFrame,
|
||||||
|
tileStore: this.tileStore,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.restore()
|
||||||
|
this.emitStats(this.tileStore.getStats(this.tileLayer.lastVisibleTileCount, this.tileLayer.lastReadyTileCount))
|
||||||
|
}
|
||||||
|
}
|
||||||
67
miniprogram/engine/renderer/canvasOverlayRenderer.ts
Normal file
67
miniprogram/engine/renderer/canvasOverlayRenderer.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { type MapLayer } from '../layer/mapLayer'
|
||||||
|
import { buildCamera, type MapScene } from './mapRenderer'
|
||||||
|
import { type TileStore } from '../tile/tileStore'
|
||||||
|
|
||||||
|
export class CanvasOverlayRenderer {
|
||||||
|
canvas: any
|
||||||
|
ctx: any
|
||||||
|
dpr: number
|
||||||
|
layers: MapLayer[]
|
||||||
|
|
||||||
|
constructor(layers: MapLayer[]) {
|
||||||
|
this.canvas = null
|
||||||
|
this.ctx = null
|
||||||
|
this.dpr = 1
|
||||||
|
this.layers = layers
|
||||||
|
}
|
||||||
|
|
||||||
|
attachCanvas(canvasNode: any, width: number, height: number, dpr: number): void {
|
||||||
|
this.canvas = canvasNode
|
||||||
|
this.ctx = canvasNode.getContext('2d')
|
||||||
|
this.dpr = dpr || 1
|
||||||
|
|
||||||
|
canvasNode.width = Math.max(1, Math.floor(width * this.dpr))
|
||||||
|
canvasNode.height = Math.max(1, Math.floor(height * this.dpr))
|
||||||
|
|
||||||
|
if (typeof this.ctx.setTransform === 'function') {
|
||||||
|
this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0)
|
||||||
|
} else {
|
||||||
|
this.ctx.scale(this.dpr, this.dpr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.canvas = null
|
||||||
|
this.ctx = null
|
||||||
|
}
|
||||||
|
|
||||||
|
render(scene: MapScene, tileStore: TileStore, pulseFrame: number): void {
|
||||||
|
if (!this.ctx) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const camera = buildCamera(scene)
|
||||||
|
const ctx = this.ctx
|
||||||
|
const previewScale = scene.previewScale || 1
|
||||||
|
const previewOriginX = scene.previewOriginX || scene.viewportWidth / 2
|
||||||
|
const previewOriginY = scene.previewOriginY || scene.viewportHeight / 2
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, scene.viewportWidth, scene.viewportHeight)
|
||||||
|
ctx.save()
|
||||||
|
ctx.translate(previewOriginX, previewOriginY)
|
||||||
|
ctx.scale(previewScale, previewScale)
|
||||||
|
ctx.translate(-previewOriginX, -previewOriginY)
|
||||||
|
|
||||||
|
for (const layer of this.layers) {
|
||||||
|
layer.draw({
|
||||||
|
ctx,
|
||||||
|
camera,
|
||||||
|
scene,
|
||||||
|
pulseFrame,
|
||||||
|
tileStore,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
}
|
||||||
44
miniprogram/engine/renderer/mapRenderer.ts
Normal file
44
miniprogram/engine/renderer/mapRenderer.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { type CameraState } from '../camera/camera'
|
||||||
|
import { type TileStoreStats } from '../tile/tileStore'
|
||||||
|
import { type LonLatPoint } from '../../utils/projection'
|
||||||
|
|
||||||
|
export interface MapScene {
|
||||||
|
tileSource: string
|
||||||
|
zoom: number
|
||||||
|
centerTileX: number
|
||||||
|
centerTileY: number
|
||||||
|
viewportWidth: number
|
||||||
|
viewportHeight: number
|
||||||
|
visibleColumns: number
|
||||||
|
overdraw: number
|
||||||
|
translateX: number
|
||||||
|
translateY: number
|
||||||
|
rotationRad: number
|
||||||
|
previewScale: number
|
||||||
|
previewOriginX: number
|
||||||
|
previewOriginY: number
|
||||||
|
track: LonLatPoint[]
|
||||||
|
gpsPoint: LonLatPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MapRendererStats = TileStoreStats
|
||||||
|
|
||||||
|
export interface MapRenderer {
|
||||||
|
attachCanvas(canvasNode: any, width: number, height: number, dpr: number): void
|
||||||
|
updateScene(scene: MapScene): void
|
||||||
|
setAnimationPaused(paused: boolean): void
|
||||||
|
destroy(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCamera(scene: MapScene): CameraState {
|
||||||
|
return {
|
||||||
|
centerWorldX: scene.centerTileX,
|
||||||
|
centerWorldY: scene.centerTileY,
|
||||||
|
viewportWidth: scene.viewportWidth,
|
||||||
|
viewportHeight: scene.viewportHeight,
|
||||||
|
visibleColumns: scene.visibleColumns,
|
||||||
|
translateX: scene.translateX,
|
||||||
|
translateY: scene.translateY,
|
||||||
|
rotationRad: scene.rotationRad,
|
||||||
|
}
|
||||||
|
}
|
||||||
213
miniprogram/engine/renderer/tilePersistentCache.ts
Normal file
213
miniprogram/engine/renderer/tilePersistentCache.ts
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
const STORAGE_KEY = 'cmr-tile-disk-cache-index-v1'
|
||||||
|
const ROOT_DIR = `${wx.env.USER_DATA_PATH}/cmr-tile-cache`
|
||||||
|
const MAX_PERSISTED_TILES = 600
|
||||||
|
const PERSIST_DELAY_MS = 300
|
||||||
|
|
||||||
|
export interface TilePersistentCacheRecord {
|
||||||
|
filePath: string
|
||||||
|
lastAccessedAt: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type TilePersistentCacheIndex = Record<string, TilePersistentCacheRecord>
|
||||||
|
|
||||||
|
let sharedTilePersistentCache: TilePersistentCache | null = null
|
||||||
|
|
||||||
|
function getFileExtension(url: string): string {
|
||||||
|
const matched = url.match(/\.(png|jpg|jpeg|webp)(?:$|\?)/i)
|
||||||
|
if (!matched) {
|
||||||
|
return '.tile'
|
||||||
|
}
|
||||||
|
|
||||||
|
return `.${matched[1].toLowerCase()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function hashUrl(url: string): string {
|
||||||
|
let hash = 2166136261
|
||||||
|
for (let index = 0; index < url.length; index += 1) {
|
||||||
|
hash ^= url.charCodeAt(index)
|
||||||
|
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (hash >>> 0).toString(36)
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneIndex(rawIndex: any): TilePersistentCacheIndex {
|
||||||
|
const nextIndex: TilePersistentCacheIndex = {}
|
||||||
|
if (!rawIndex || typeof rawIndex !== 'object') {
|
||||||
|
return nextIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(rawIndex).forEach((url) => {
|
||||||
|
const record = rawIndex[url]
|
||||||
|
if (!record || typeof record.filePath !== 'string') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nextIndex[url] = {
|
||||||
|
filePath: record.filePath,
|
||||||
|
lastAccessedAt: typeof record.lastAccessedAt === 'number' ? record.lastAccessedAt : 0,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return nextIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TilePersistentCache {
|
||||||
|
fs: WechatMiniprogram.FileSystemManager
|
||||||
|
rootDir: string
|
||||||
|
index: TilePersistentCacheIndex
|
||||||
|
persistTimer: number
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.fs = wx.getFileSystemManager()
|
||||||
|
this.rootDir = ROOT_DIR
|
||||||
|
this.index = {}
|
||||||
|
this.persistTimer = 0
|
||||||
|
|
||||||
|
this.ensureRootDir()
|
||||||
|
this.loadIndex()
|
||||||
|
this.pruneMissingFiles()
|
||||||
|
this.pruneIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureRootDir(): void {
|
||||||
|
try {
|
||||||
|
this.fs.accessSync(this.rootDir)
|
||||||
|
} catch {
|
||||||
|
this.fs.mkdirSync(this.rootDir, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadIndex(): void {
|
||||||
|
try {
|
||||||
|
this.index = cloneIndex(wx.getStorageSync(STORAGE_KEY))
|
||||||
|
} catch {
|
||||||
|
this.index = {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getCount(): number {
|
||||||
|
return Object.keys(this.index).length
|
||||||
|
}
|
||||||
|
|
||||||
|
schedulePersist(): void {
|
||||||
|
if (this.persistTimer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.persistTimer = setTimeout(() => {
|
||||||
|
this.persistTimer = 0
|
||||||
|
wx.setStorageSync(STORAGE_KEY, this.index)
|
||||||
|
}, PERSIST_DELAY_MS) as unknown as number
|
||||||
|
}
|
||||||
|
|
||||||
|
pruneMissingFiles(): void {
|
||||||
|
let changed = false
|
||||||
|
|
||||||
|
Object.keys(this.index).forEach((url) => {
|
||||||
|
const record = this.index[url]
|
||||||
|
try {
|
||||||
|
this.fs.accessSync(record.filePath)
|
||||||
|
} catch {
|
||||||
|
delete this.index[url]
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
this.schedulePersist()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getCachedPath(url: string): string | null {
|
||||||
|
const record = this.index[url]
|
||||||
|
if (!record) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.fs.accessSync(record.filePath)
|
||||||
|
record.lastAccessedAt = Date.now()
|
||||||
|
this.schedulePersist()
|
||||||
|
return record.filePath
|
||||||
|
} catch {
|
||||||
|
delete this.index[url]
|
||||||
|
this.schedulePersist()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getTargetPath(url: string): string {
|
||||||
|
const existingRecord = this.index[url]
|
||||||
|
if (existingRecord) {
|
||||||
|
existingRecord.lastAccessedAt = Date.now()
|
||||||
|
this.schedulePersist()
|
||||||
|
return existingRecord.filePath
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = `${this.rootDir}/${hashUrl(url)}${getFileExtension(url)}`
|
||||||
|
this.index[url] = {
|
||||||
|
filePath,
|
||||||
|
lastAccessedAt: Date.now(),
|
||||||
|
}
|
||||||
|
this.schedulePersist()
|
||||||
|
return filePath
|
||||||
|
}
|
||||||
|
|
||||||
|
markReady(url: string, filePath: string): void {
|
||||||
|
this.index[url] = {
|
||||||
|
filePath,
|
||||||
|
lastAccessedAt: Date.now(),
|
||||||
|
}
|
||||||
|
this.pruneIfNeeded()
|
||||||
|
this.schedulePersist()
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(url: string): void {
|
||||||
|
const record = this.index[url]
|
||||||
|
if (!record) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.fs.unlinkSync(record.filePath)
|
||||||
|
} catch {
|
||||||
|
// Ignore unlink errors for already-missing files.
|
||||||
|
}
|
||||||
|
|
||||||
|
delete this.index[url]
|
||||||
|
this.schedulePersist()
|
||||||
|
}
|
||||||
|
|
||||||
|
pruneIfNeeded(): void {
|
||||||
|
const urls = Object.keys(this.index)
|
||||||
|
if (urls.length <= MAX_PERSISTED_TILES) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const removableUrls = urls.sort((leftUrl, rightUrl) => {
|
||||||
|
return this.index[leftUrl].lastAccessedAt - this.index[rightUrl].lastAccessedAt
|
||||||
|
})
|
||||||
|
|
||||||
|
while (removableUrls.length > MAX_PERSISTED_TILES) {
|
||||||
|
const nextUrl = removableUrls.shift() as string
|
||||||
|
const record = this.index[nextUrl]
|
||||||
|
if (record) {
|
||||||
|
try {
|
||||||
|
this.fs.unlinkSync(record.filePath)
|
||||||
|
} catch {
|
||||||
|
// Ignore unlink errors for already-missing files.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete this.index[nextUrl]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTilePersistentCache(): TilePersistentCache {
|
||||||
|
if (!sharedTilePersistentCache) {
|
||||||
|
sharedTilePersistentCache = new TilePersistentCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
return sharedTilePersistentCache
|
||||||
|
}
|
||||||
161
miniprogram/engine/renderer/webglMapRenderer.ts
Normal file
161
miniprogram/engine/renderer/webglMapRenderer.ts
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import { TrackLayer } from '../layer/trackLayer'
|
||||||
|
import { GpsLayer } from '../layer/gpsLayer'
|
||||||
|
import { TileLayer } from '../layer/tileLayer'
|
||||||
|
import { TileStore, type TileStoreCallbacks } from '../tile/tileStore'
|
||||||
|
import { type MapRenderer, type MapRendererStats, type MapScene } from './mapRenderer'
|
||||||
|
import { WebGLTileRenderer } from './webglTileRenderer'
|
||||||
|
import { WebGLVectorRenderer } from './webglVectorRenderer'
|
||||||
|
|
||||||
|
const RENDER_FRAME_MS = 16
|
||||||
|
const ANIMATION_FRAME_MS = 33
|
||||||
|
|
||||||
|
export class WebGLMapRenderer implements MapRenderer {
|
||||||
|
tileStore: TileStore
|
||||||
|
tileLayer: TileLayer
|
||||||
|
trackLayer: TrackLayer
|
||||||
|
gpsLayer: GpsLayer
|
||||||
|
tileRenderer: WebGLTileRenderer
|
||||||
|
vectorRenderer: WebGLVectorRenderer
|
||||||
|
scene: MapScene | null
|
||||||
|
renderTimer: number
|
||||||
|
animationTimer: number
|
||||||
|
destroyed: boolean
|
||||||
|
animationPaused: boolean
|
||||||
|
pulseFrame: number
|
||||||
|
lastStats: MapRendererStats
|
||||||
|
onStats?: (stats: MapRendererStats) => void
|
||||||
|
onTileError?: (message: string) => void
|
||||||
|
|
||||||
|
constructor(onStats?: (stats: MapRendererStats) => void, onTileError?: (message: string) => void) {
|
||||||
|
this.onStats = onStats
|
||||||
|
this.onTileError = onTileError
|
||||||
|
this.tileStore = new TileStore({
|
||||||
|
onTileReady: () => {
|
||||||
|
this.scheduleRender()
|
||||||
|
},
|
||||||
|
onTileError: (message) => {
|
||||||
|
if (this.onTileError) {
|
||||||
|
this.onTileError(message)
|
||||||
|
}
|
||||||
|
this.scheduleRender()
|
||||||
|
},
|
||||||
|
} satisfies TileStoreCallbacks)
|
||||||
|
this.tileLayer = new TileLayer()
|
||||||
|
this.trackLayer = new TrackLayer()
|
||||||
|
this.gpsLayer = new GpsLayer()
|
||||||
|
this.tileRenderer = new WebGLTileRenderer(this.tileLayer, this.tileStore)
|
||||||
|
this.vectorRenderer = new WebGLVectorRenderer(this.trackLayer, this.gpsLayer)
|
||||||
|
this.scene = null
|
||||||
|
this.renderTimer = 0
|
||||||
|
this.animationTimer = 0
|
||||||
|
this.destroyed = false
|
||||||
|
this.animationPaused = false
|
||||||
|
this.pulseFrame = 0
|
||||||
|
this.lastStats = {
|
||||||
|
visibleTileCount: 0,
|
||||||
|
readyTileCount: 0,
|
||||||
|
memoryTileCount: 0,
|
||||||
|
diskTileCount: 0,
|
||||||
|
memoryHitCount: 0,
|
||||||
|
diskHitCount: 0,
|
||||||
|
networkFetchCount: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attachCanvas(canvasNode: any, width: number, height: number, dpr: number): void {
|
||||||
|
this.tileRenderer.attachCanvas(canvasNode, width, height, dpr)
|
||||||
|
this.vectorRenderer.attachContext(this.tileRenderer.gl, canvasNode)
|
||||||
|
this.startAnimation()
|
||||||
|
this.scheduleRender()
|
||||||
|
}
|
||||||
|
|
||||||
|
updateScene(scene: MapScene): void {
|
||||||
|
this.scene = scene
|
||||||
|
this.scheduleRender()
|
||||||
|
}
|
||||||
|
|
||||||
|
setAnimationPaused(paused: boolean): void {
|
||||||
|
this.animationPaused = paused
|
||||||
|
if (!paused) {
|
||||||
|
this.scheduleRender()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
this.destroyed = true
|
||||||
|
if (this.renderTimer) {
|
||||||
|
clearTimeout(this.renderTimer)
|
||||||
|
this.renderTimer = 0
|
||||||
|
}
|
||||||
|
if (this.animationTimer) {
|
||||||
|
clearTimeout(this.animationTimer)
|
||||||
|
this.animationTimer = 0
|
||||||
|
}
|
||||||
|
this.vectorRenderer.destroy()
|
||||||
|
this.tileRenderer.destroy()
|
||||||
|
this.tileStore.destroy()
|
||||||
|
this.scene = null
|
||||||
|
}
|
||||||
|
|
||||||
|
startAnimation(): void {
|
||||||
|
if (this.animationTimer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const tick = () => {
|
||||||
|
if (this.destroyed) {
|
||||||
|
this.animationTimer = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.animationPaused) {
|
||||||
|
this.pulseFrame = (this.pulseFrame + 1) % 360
|
||||||
|
this.scheduleRender()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.animationTimer = setTimeout(tick, ANIMATION_FRAME_MS) as unknown as number
|
||||||
|
}
|
||||||
|
|
||||||
|
tick()
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleRender(): void {
|
||||||
|
if (this.renderTimer || !this.scene || this.destroyed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderTimer = setTimeout(() => {
|
||||||
|
this.renderTimer = 0
|
||||||
|
this.renderFrame()
|
||||||
|
}, RENDER_FRAME_MS) as unknown as number
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFrame(): void {
|
||||||
|
if (!this.scene) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tileRenderer.render(this.scene)
|
||||||
|
this.vectorRenderer.render(this.scene, this.pulseFrame)
|
||||||
|
this.emitStats(this.tileStore.getStats(this.tileLayer.lastVisibleTileCount, this.tileLayer.lastReadyTileCount))
|
||||||
|
}
|
||||||
|
|
||||||
|
emitStats(stats: MapRendererStats): void {
|
||||||
|
if (
|
||||||
|
stats.visibleTileCount === this.lastStats.visibleTileCount
|
||||||
|
&& stats.readyTileCount === this.lastStats.readyTileCount
|
||||||
|
&& stats.memoryTileCount === this.lastStats.memoryTileCount
|
||||||
|
&& stats.diskTileCount === this.lastStats.diskTileCount
|
||||||
|
&& stats.memoryHitCount === this.lastStats.memoryHitCount
|
||||||
|
&& stats.diskHitCount === this.lastStats.diskHitCount
|
||||||
|
&& stats.networkFetchCount === this.lastStats.networkFetchCount
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastStats = stats
|
||||||
|
if (this.onStats) {
|
||||||
|
this.onStats(stats)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
329
miniprogram/engine/renderer/webglTileRenderer.ts
Normal file
329
miniprogram/engine/renderer/webglTileRenderer.ts
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
import { rotateScreenPoint, type ScreenPoint } from '../camera/camera'
|
||||||
|
import { type TileStore, type TileStoreEntry } from '../tile/tileStore'
|
||||||
|
import { TileLayer } from '../layer/tileLayer'
|
||||||
|
import { buildCamera, type MapScene } from './mapRenderer'
|
||||||
|
|
||||||
|
interface TextureRecord {
|
||||||
|
key: string
|
||||||
|
texture: any
|
||||||
|
}
|
||||||
|
|
||||||
|
function createShader(gl: any, type: number, source: string): any {
|
||||||
|
const shader = gl.createShader(type)
|
||||||
|
if (!shader) {
|
||||||
|
throw new Error('WebGL shader 创建失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
gl.shaderSource(shader, source)
|
||||||
|
gl.compileShader(shader)
|
||||||
|
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||||||
|
const message = gl.getShaderInfoLog(shader) || 'unknown shader error'
|
||||||
|
gl.deleteShader(shader)
|
||||||
|
throw new Error(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return shader
|
||||||
|
}
|
||||||
|
|
||||||
|
function createProgram(gl: any, vertexSource: string, fragmentSource: string): any {
|
||||||
|
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource)
|
||||||
|
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource)
|
||||||
|
const program = gl.createProgram()
|
||||||
|
if (!program) {
|
||||||
|
throw new Error('WebGL program 创建失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
gl.attachShader(program, vertexShader)
|
||||||
|
gl.attachShader(program, fragmentShader)
|
||||||
|
gl.linkProgram(program)
|
||||||
|
gl.deleteShader(vertexShader)
|
||||||
|
gl.deleteShader(fragmentShader)
|
||||||
|
|
||||||
|
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
||||||
|
const message = gl.getProgramInfoLog(program) || 'unknown program error'
|
||||||
|
gl.deleteProgram(program)
|
||||||
|
throw new Error(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return program
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WebGLTileRenderer {
|
||||||
|
canvas: any
|
||||||
|
gl: any
|
||||||
|
tileLayer: TileLayer
|
||||||
|
tileStore: TileStore
|
||||||
|
dpr: number
|
||||||
|
program: any
|
||||||
|
positionBuffer: any
|
||||||
|
texCoordBuffer: any
|
||||||
|
positionLocation: number
|
||||||
|
texCoordLocation: number
|
||||||
|
textureCache: Map<string, TextureRecord>
|
||||||
|
|
||||||
|
constructor(tileLayer: TileLayer, tileStore: TileStore) {
|
||||||
|
this.canvas = null
|
||||||
|
this.gl = null
|
||||||
|
this.tileLayer = tileLayer
|
||||||
|
this.tileStore = tileStore
|
||||||
|
this.dpr = 1
|
||||||
|
this.program = null
|
||||||
|
this.positionBuffer = null
|
||||||
|
this.texCoordBuffer = null
|
||||||
|
this.positionLocation = -1
|
||||||
|
this.texCoordLocation = -1
|
||||||
|
this.textureCache = new Map<string, TextureRecord>()
|
||||||
|
}
|
||||||
|
|
||||||
|
attachCanvas(canvasNode: any, width: number, height: number, dpr: number): void {
|
||||||
|
this.canvas = canvasNode
|
||||||
|
this.dpr = dpr || 1
|
||||||
|
canvasNode.width = Math.max(1, Math.floor(width * this.dpr))
|
||||||
|
canvasNode.height = Math.max(1, Math.floor(height * this.dpr))
|
||||||
|
|
||||||
|
const gl = canvasNode.getContext('webgl') || canvasNode.getContext('experimental-webgl')
|
||||||
|
if (!gl) {
|
||||||
|
throw new Error('当前环境不支持 WebGL')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.gl = gl
|
||||||
|
this.program = createProgram(
|
||||||
|
gl,
|
||||||
|
'attribute vec2 a_position; attribute vec2 a_texCoord; varying vec2 v_texCoord; void main() { gl_Position = vec4(a_position, 0.0, 1.0); v_texCoord = a_texCoord; }',
|
||||||
|
'precision mediump float; varying vec2 v_texCoord; uniform sampler2D u_texture; void main() { gl_FragColor = texture2D(u_texture, v_texCoord); }',
|
||||||
|
)
|
||||||
|
this.positionBuffer = gl.createBuffer()
|
||||||
|
this.texCoordBuffer = gl.createBuffer()
|
||||||
|
this.positionLocation = gl.getAttribLocation(this.program, 'a_position')
|
||||||
|
this.texCoordLocation = gl.getAttribLocation(this.program, 'a_texCoord')
|
||||||
|
|
||||||
|
gl.viewport(0, 0, canvasNode.width, canvasNode.height)
|
||||||
|
gl.disable(gl.DEPTH_TEST)
|
||||||
|
gl.enable(gl.BLEND)
|
||||||
|
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
|
||||||
|
|
||||||
|
this.tileStore.attachCanvas(canvasNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
if (this.gl) {
|
||||||
|
this.textureCache.forEach((record) => {
|
||||||
|
this.gl && this.gl.deleteTexture(record.texture)
|
||||||
|
})
|
||||||
|
if (this.program) {
|
||||||
|
this.gl.deleteProgram(this.program)
|
||||||
|
}
|
||||||
|
if (this.positionBuffer) {
|
||||||
|
this.gl.deleteBuffer(this.positionBuffer)
|
||||||
|
}
|
||||||
|
if (this.texCoordBuffer) {
|
||||||
|
this.gl.deleteBuffer(this.texCoordBuffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.textureCache.clear()
|
||||||
|
this.program = null
|
||||||
|
this.positionBuffer = null
|
||||||
|
this.texCoordBuffer = null
|
||||||
|
this.gl = null
|
||||||
|
this.canvas = null
|
||||||
|
}
|
||||||
|
|
||||||
|
render(scene: MapScene): void {
|
||||||
|
if (!this.gl || !this.program || !this.positionBuffer || !this.texCoordBuffer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const gl = this.gl
|
||||||
|
const camera = buildCamera(scene)
|
||||||
|
const tiles = this.tileLayer.prepareTiles(scene, camera, this.tileStore)
|
||||||
|
|
||||||
|
gl.viewport(0, 0, this.canvas.width, this.canvas.height)
|
||||||
|
gl.clearColor(0.8588, 0.9333, 0.8314, 1)
|
||||||
|
gl.clear(gl.COLOR_BUFFER_BIT)
|
||||||
|
gl.useProgram(this.program)
|
||||||
|
|
||||||
|
for (const tile of tiles) {
|
||||||
|
const readyEntry = this.tileStore.getEntry(tile.url)
|
||||||
|
|
||||||
|
if (readyEntry && readyEntry.status === 'ready' && readyEntry.image) {
|
||||||
|
this.drawEntry(readyEntry, tile.url, 0, 0, readyEntry.image.width || 256, readyEntry.image.height || 256, tile.leftPx, tile.topPx, tile.sizePx, tile.sizePx, scene)
|
||||||
|
this.tileLayer.lastReadyTileCount += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentFallback = this.tileStore.getParentFallbackSlice(tile, scene)
|
||||||
|
if (parentFallback) {
|
||||||
|
this.drawEntry(
|
||||||
|
parentFallback.entry,
|
||||||
|
tile.url + '|parent',
|
||||||
|
parentFallback.sourceX,
|
||||||
|
parentFallback.sourceY,
|
||||||
|
parentFallback.sourceWidth,
|
||||||
|
parentFallback.sourceHeight,
|
||||||
|
tile.leftPx,
|
||||||
|
tile.topPx,
|
||||||
|
tile.sizePx,
|
||||||
|
tile.sizePx,
|
||||||
|
scene,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const childFallback = this.tileStore.getChildFallback(tile, scene)
|
||||||
|
if (!childFallback) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const cellWidth = tile.sizePx / childFallback.division
|
||||||
|
const cellHeight = tile.sizePx / childFallback.division
|
||||||
|
for (const child of childFallback.children) {
|
||||||
|
this.drawEntry(
|
||||||
|
child.entry,
|
||||||
|
tile.url + '|child|' + child.offsetX + '|' + child.offsetY,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
child.entry.image.width || 256,
|
||||||
|
child.entry.image.height || 256,
|
||||||
|
tile.leftPx + child.offsetX * cellWidth,
|
||||||
|
tile.topPx + child.offsetY * cellHeight,
|
||||||
|
cellWidth,
|
||||||
|
cellHeight,
|
||||||
|
scene,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawEntry(
|
||||||
|
entry: TileStoreEntry,
|
||||||
|
cacheKey: string,
|
||||||
|
sourceX: number,
|
||||||
|
sourceY: number,
|
||||||
|
sourceWidth: number,
|
||||||
|
sourceHeight: number,
|
||||||
|
drawLeft: number,
|
||||||
|
drawTop: number,
|
||||||
|
drawWidth: number,
|
||||||
|
drawHeight: number,
|
||||||
|
scene: MapScene,
|
||||||
|
): void {
|
||||||
|
if (!this.gl || !entry.image) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const texture = this.getTexture(cacheKey, entry)
|
||||||
|
if (!texture) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const gl = this.gl
|
||||||
|
const imageWidth = entry.image.width || 256
|
||||||
|
const imageHeight = entry.image.height || 256
|
||||||
|
const texLeft = sourceX / imageWidth
|
||||||
|
const texTop = sourceY / imageHeight
|
||||||
|
const texRight = (sourceX + sourceWidth) / imageWidth
|
||||||
|
const texBottom = (sourceY + sourceHeight) / imageHeight
|
||||||
|
|
||||||
|
const topLeft = this.transformToClip(drawLeft, drawTop, scene)
|
||||||
|
const topRight = this.transformToClip(drawLeft + drawWidth, drawTop, scene)
|
||||||
|
const bottomLeft = this.transformToClip(drawLeft, drawTop + drawHeight, scene)
|
||||||
|
const bottomRight = this.transformToClip(drawLeft + drawWidth, drawTop + drawHeight, scene)
|
||||||
|
|
||||||
|
const positions = new Float32Array([
|
||||||
|
topLeft.x, topLeft.y,
|
||||||
|
topRight.x, topRight.y,
|
||||||
|
bottomLeft.x, bottomLeft.y,
|
||||||
|
bottomLeft.x, bottomLeft.y,
|
||||||
|
topRight.x, topRight.y,
|
||||||
|
bottomRight.x, bottomRight.y,
|
||||||
|
])
|
||||||
|
const texCoords = new Float32Array([
|
||||||
|
texLeft, texTop,
|
||||||
|
texRight, texTop,
|
||||||
|
texLeft, texBottom,
|
||||||
|
texLeft, texBottom,
|
||||||
|
texRight, texTop,
|
||||||
|
texRight, texBottom,
|
||||||
|
])
|
||||||
|
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, texture.texture)
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer)
|
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STREAM_DRAW)
|
||||||
|
gl.enableVertexAttribArray(this.positionLocation)
|
||||||
|
gl.vertexAttribPointer(this.positionLocation, 2, gl.FLOAT, false, 0, 0)
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer)
|
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, texCoords, gl.STREAM_DRAW)
|
||||||
|
gl.enableVertexAttribArray(this.texCoordLocation)
|
||||||
|
gl.vertexAttribPointer(this.texCoordLocation, 2, gl.FLOAT, false, 0, 0)
|
||||||
|
|
||||||
|
gl.drawArrays(gl.TRIANGLES, 0, 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
getTexture(cacheKey: string, entry: TileStoreEntry): TextureRecord | null {
|
||||||
|
if (!this.gl || !entry.image) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = cacheKey + '|' + entry.sourcePath
|
||||||
|
const existing = this.textureCache.get(key)
|
||||||
|
if (existing) {
|
||||||
|
return existing
|
||||||
|
}
|
||||||
|
|
||||||
|
const texture = this.gl.createTexture()
|
||||||
|
if (!texture) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
this.gl.bindTexture(this.gl.TEXTURE_2D, texture)
|
||||||
|
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE)
|
||||||
|
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE)
|
||||||
|
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR)
|
||||||
|
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR)
|
||||||
|
this.gl.pixelStorei(this.gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 1)
|
||||||
|
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, entry.image)
|
||||||
|
|
||||||
|
const record = { key, texture }
|
||||||
|
this.textureCache.set(key, record)
|
||||||
|
return record
|
||||||
|
}
|
||||||
|
|
||||||
|
transformToClip(x: number, y: number, scene: MapScene): ScreenPoint {
|
||||||
|
const rotated = rotateScreenPoint(
|
||||||
|
{ x, y },
|
||||||
|
scene.viewportWidth / 2,
|
||||||
|
scene.viewportHeight / 2,
|
||||||
|
scene.rotationRad || 0,
|
||||||
|
)
|
||||||
|
const translated = {
|
||||||
|
x: rotated.x + scene.translateX,
|
||||||
|
y: rotated.y + scene.translateY,
|
||||||
|
}
|
||||||
|
const previewed = this.applyPreview(translated.x, translated.y, scene)
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: this.toClipX(previewed.x, scene.viewportWidth),
|
||||||
|
y: this.toClipY(previewed.y, scene.viewportHeight),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyPreview(x: number, y: number, scene: MapScene): { x: number; y: number } {
|
||||||
|
const scale = scene.previewScale || 1
|
||||||
|
const originX = scene.previewOriginX || scene.viewportWidth / 2
|
||||||
|
const originY = scene.previewOriginY || scene.viewportHeight / 2
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: originX + (x - originX) * scale,
|
||||||
|
y: originY + (y - originY) * scale,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toClipX(x: number, width: number): number {
|
||||||
|
return x / width * 2 - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
toClipY(y: number, height: number): number {
|
||||||
|
return 1 - y / height * 2
|
||||||
|
}
|
||||||
|
}
|
||||||
234
miniprogram/engine/renderer/webglVectorRenderer.ts
Normal file
234
miniprogram/engine/renderer/webglVectorRenderer.ts
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
import { buildCamera, type MapScene } from './mapRenderer'
|
||||||
|
import { TrackLayer } from '../layer/trackLayer'
|
||||||
|
import { GpsLayer } from '../layer/gpsLayer'
|
||||||
|
|
||||||
|
function createShader(gl: any, type: number, source: string): any {
|
||||||
|
const shader = gl.createShader(type)
|
||||||
|
if (!shader) {
|
||||||
|
throw new Error('WebGL shader 创建失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
gl.shaderSource(shader, source)
|
||||||
|
gl.compileShader(shader)
|
||||||
|
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||||||
|
const message = gl.getShaderInfoLog(shader) || 'unknown shader error'
|
||||||
|
gl.deleteShader(shader)
|
||||||
|
throw new Error(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return shader
|
||||||
|
}
|
||||||
|
|
||||||
|
function createProgram(gl: any, vertexSource: string, fragmentSource: string): any {
|
||||||
|
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource)
|
||||||
|
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource)
|
||||||
|
const program = gl.createProgram()
|
||||||
|
if (!program) {
|
||||||
|
throw new Error('WebGL program 创建失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
gl.attachShader(program, vertexShader)
|
||||||
|
gl.attachShader(program, fragmentShader)
|
||||||
|
gl.linkProgram(program)
|
||||||
|
gl.deleteShader(vertexShader)
|
||||||
|
gl.deleteShader(fragmentShader)
|
||||||
|
|
||||||
|
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
||||||
|
const message = gl.getProgramInfoLog(program) || 'unknown program error'
|
||||||
|
gl.deleteProgram(program)
|
||||||
|
throw new Error(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return program
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WebGLVectorRenderer {
|
||||||
|
canvas: any
|
||||||
|
gl: any
|
||||||
|
dpr: number
|
||||||
|
trackLayer: TrackLayer
|
||||||
|
gpsLayer: GpsLayer
|
||||||
|
program: any
|
||||||
|
positionBuffer: any
|
||||||
|
colorBuffer: any
|
||||||
|
positionLocation: number
|
||||||
|
colorLocation: number
|
||||||
|
|
||||||
|
constructor(trackLayer: TrackLayer, gpsLayer: GpsLayer) {
|
||||||
|
this.canvas = null
|
||||||
|
this.gl = null
|
||||||
|
this.dpr = 1
|
||||||
|
this.trackLayer = trackLayer
|
||||||
|
this.gpsLayer = gpsLayer
|
||||||
|
this.program = null
|
||||||
|
this.positionBuffer = null
|
||||||
|
this.colorBuffer = null
|
||||||
|
this.positionLocation = -1
|
||||||
|
this.colorLocation = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
attachCanvas(canvasNode: any, width: number, height: number, dpr: number): void {
|
||||||
|
this.canvas = canvasNode
|
||||||
|
this.dpr = dpr || 1
|
||||||
|
canvasNode.width = Math.max(1, Math.floor(width * this.dpr))
|
||||||
|
canvasNode.height = Math.max(1, Math.floor(height * this.dpr))
|
||||||
|
this.attachContext(canvasNode.getContext('webgl') || canvasNode.getContext('experimental-webgl'), canvasNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
attachContext(gl: any, canvasNode: any): void {
|
||||||
|
if (!gl) {
|
||||||
|
throw new Error('当前环境不支持 WebGL Vector Layer')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.canvas = canvasNode
|
||||||
|
this.gl = gl
|
||||||
|
this.program = createProgram(
|
||||||
|
gl,
|
||||||
|
'attribute vec2 a_position; attribute vec4 a_color; varying vec4 v_color; void main() { gl_Position = vec4(a_position, 0.0, 1.0); v_color = a_color; }',
|
||||||
|
'precision mediump float; varying vec4 v_color; void main() { gl_FragColor = v_color; }',
|
||||||
|
)
|
||||||
|
this.positionBuffer = gl.createBuffer()
|
||||||
|
this.colorBuffer = gl.createBuffer()
|
||||||
|
this.positionLocation = gl.getAttribLocation(this.program, 'a_position')
|
||||||
|
this.colorLocation = gl.getAttribLocation(this.program, 'a_color')
|
||||||
|
|
||||||
|
gl.viewport(0, 0, canvasNode.width, canvasNode.height)
|
||||||
|
gl.enable(gl.BLEND)
|
||||||
|
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
if (this.gl) {
|
||||||
|
if (this.program) {
|
||||||
|
this.gl.deleteProgram(this.program)
|
||||||
|
}
|
||||||
|
if (this.positionBuffer) {
|
||||||
|
this.gl.deleteBuffer(this.positionBuffer)
|
||||||
|
}
|
||||||
|
if (this.colorBuffer) {
|
||||||
|
this.gl.deleteBuffer(this.colorBuffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.program = null
|
||||||
|
this.positionBuffer = null
|
||||||
|
this.colorBuffer = null
|
||||||
|
this.gl = null
|
||||||
|
this.canvas = null
|
||||||
|
}
|
||||||
|
|
||||||
|
render(scene: MapScene, pulseFrame: number): void {
|
||||||
|
if (!this.gl || !this.program || !this.positionBuffer || !this.colorBuffer || !this.canvas) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const gl = this.gl
|
||||||
|
const camera = buildCamera(scene)
|
||||||
|
const trackPoints = this.trackLayer.projectPoints(scene, camera)
|
||||||
|
const gpsPoint = this.gpsLayer.projectPoint(scene, camera)
|
||||||
|
const positions: number[] = []
|
||||||
|
const colors: number[] = []
|
||||||
|
|
||||||
|
for (let index = 1; index < trackPoints.length; index += 1) {
|
||||||
|
this.pushSegment(positions, colors, trackPoints[index - 1], trackPoints[index], 6, [0.09, 0.43, 0.36, 0.96], scene)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const point of trackPoints) {
|
||||||
|
this.pushCircle(positions, colors, point.x, point.y, 10, [0.09, 0.43, 0.36, 1], scene)
|
||||||
|
this.pushCircle(positions, colors, point.x, point.y, 6.5, [0.97, 0.98, 0.95, 1], scene)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pushCircle(positions, colors, gpsPoint.x, gpsPoint.y, this.gpsLayer.getPulseRadius(pulseFrame), [0.13, 0.62, 0.74, 0.22], scene)
|
||||||
|
this.pushCircle(positions, colors, gpsPoint.x, gpsPoint.y, 13, [1, 1, 1, 0.95], scene)
|
||||||
|
this.pushCircle(positions, colors, gpsPoint.x, gpsPoint.y, 9, [0.13, 0.63, 0.74, 1], scene)
|
||||||
|
|
||||||
|
gl.viewport(0, 0, this.canvas.width, this.canvas.height)
|
||||||
|
gl.useProgram(this.program)
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer)
|
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STREAM_DRAW)
|
||||||
|
gl.enableVertexAttribArray(this.positionLocation)
|
||||||
|
gl.vertexAttribPointer(this.positionLocation, 2, gl.FLOAT, false, 0, 0)
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer)
|
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STREAM_DRAW)
|
||||||
|
gl.enableVertexAttribArray(this.colorLocation)
|
||||||
|
gl.vertexAttribPointer(this.colorLocation, 4, gl.FLOAT, false, 0, 0)
|
||||||
|
|
||||||
|
gl.drawArrays(gl.TRIANGLES, 0, positions.length / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
pushSegment(
|
||||||
|
positions: number[],
|
||||||
|
colors: number[],
|
||||||
|
start: { x: number; y: number },
|
||||||
|
end: { x: number; y: number },
|
||||||
|
width: number,
|
||||||
|
color: [number, number, number, number],
|
||||||
|
scene: MapScene,
|
||||||
|
): void {
|
||||||
|
const deltaX = end.x - start.x
|
||||||
|
const deltaY = end.y - start.y
|
||||||
|
const length = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
|
||||||
|
if (!length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalX = -deltaY / length * (width / 2)
|
||||||
|
const normalY = deltaX / length * (width / 2)
|
||||||
|
const topLeft = this.toClip(start.x + normalX, start.y + normalY, scene)
|
||||||
|
const topRight = this.toClip(end.x + normalX, end.y + normalY, scene)
|
||||||
|
const bottomLeft = this.toClip(start.x - normalX, start.y - normalY, scene)
|
||||||
|
const bottomRight = this.toClip(end.x - normalX, end.y - normalY, scene)
|
||||||
|
|
||||||
|
this.pushTriangle(positions, colors, topLeft, topRight, bottomLeft, color)
|
||||||
|
this.pushTriangle(positions, colors, bottomLeft, topRight, bottomRight, color)
|
||||||
|
}
|
||||||
|
|
||||||
|
pushCircle(
|
||||||
|
positions: number[],
|
||||||
|
colors: number[],
|
||||||
|
centerX: number,
|
||||||
|
centerY: number,
|
||||||
|
radius: number,
|
||||||
|
color: [number, number, number, number],
|
||||||
|
scene: MapScene,
|
||||||
|
): void {
|
||||||
|
const segments = 20
|
||||||
|
const center = this.toClip(centerX, centerY, scene)
|
||||||
|
for (let index = 0; index < segments; index += 1) {
|
||||||
|
const startAngle = index / segments * Math.PI * 2
|
||||||
|
const endAngle = (index + 1) / segments * Math.PI * 2
|
||||||
|
const start = this.toClip(centerX + Math.cos(startAngle) * radius, centerY + Math.sin(startAngle) * radius, scene)
|
||||||
|
const end = this.toClip(centerX + Math.cos(endAngle) * radius, centerY + Math.sin(endAngle) * radius, scene)
|
||||||
|
this.pushTriangle(positions, colors, center, start, end, color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pushTriangle(
|
||||||
|
positions: number[],
|
||||||
|
colors: number[],
|
||||||
|
first: { x: number; y: number },
|
||||||
|
second: { x: number; y: number },
|
||||||
|
third: { x: number; y: number },
|
||||||
|
color: [number, number, number, number],
|
||||||
|
): void {
|
||||||
|
positions.push(first.x, first.y, second.x, second.y, third.x, third.y)
|
||||||
|
for (let index = 0; index < 3; index += 1) {
|
||||||
|
colors.push(color[0], color[1], color[2], color[3])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toClip(x: number, y: number, scene: MapScene): { x: number; y: number } {
|
||||||
|
const previewScale = scene.previewScale || 1
|
||||||
|
const originX = scene.previewOriginX || scene.viewportWidth / 2
|
||||||
|
const originY = scene.previewOriginY || scene.viewportHeight / 2
|
||||||
|
const scaledX = originX + (x - originX) * previewScale
|
||||||
|
const scaledY = originY + (y - originY) * previewScale
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: scaledX / scene.viewportWidth * 2 - 1,
|
||||||
|
y: 1 - scaledY / scene.viewportHeight * 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
206
miniprogram/engine/sensor/compassHeadingController.ts
Normal file
206
miniprogram/engine/sensor/compassHeadingController.ts
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
export interface CompassHeadingControllerCallbacks {
|
||||||
|
onHeading: (headingDeg: number) => void
|
||||||
|
onError: (message: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
type SensorSource = 'compass' | 'motion' | null
|
||||||
|
|
||||||
|
const ABSOLUTE_HEADING_CORRECTION = 0.24
|
||||||
|
|
||||||
|
function normalizeHeadingDeg(headingDeg: number): number {
|
||||||
|
const normalized = headingDeg % 360
|
||||||
|
return normalized < 0 ? normalized + 360 : normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeHeadingDeltaDeg(deltaDeg: number): number {
|
||||||
|
let normalized = deltaDeg
|
||||||
|
|
||||||
|
while (normalized > 180) {
|
||||||
|
normalized -= 360
|
||||||
|
}
|
||||||
|
|
||||||
|
while (normalized < -180) {
|
||||||
|
normalized += 360
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
function interpolateHeadingDeg(currentDeg: number, targetDeg: number, factor: number): number {
|
||||||
|
return normalizeHeadingDeg(currentDeg + normalizeHeadingDeltaDeg(targetDeg - currentDeg) * factor)
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CompassHeadingController {
|
||||||
|
callbacks: CompassHeadingControllerCallbacks
|
||||||
|
listening: boolean
|
||||||
|
source: SensorSource
|
||||||
|
compassCallback: ((result: WechatMiniprogram.OnCompassChangeCallbackResult) => void) | null
|
||||||
|
motionCallback: ((result: WechatMiniprogram.OnDeviceMotionChangeCallbackResult) => void) | null
|
||||||
|
absoluteHeadingDeg: number | null
|
||||||
|
pitchDeg: number | null
|
||||||
|
rollDeg: number | null
|
||||||
|
motionReady: boolean
|
||||||
|
compassReady: boolean
|
||||||
|
|
||||||
|
constructor(callbacks: CompassHeadingControllerCallbacks) {
|
||||||
|
this.callbacks = callbacks
|
||||||
|
this.listening = false
|
||||||
|
this.source = null
|
||||||
|
this.compassCallback = null
|
||||||
|
this.motionCallback = null
|
||||||
|
this.absoluteHeadingDeg = null
|
||||||
|
this.pitchDeg = null
|
||||||
|
this.rollDeg = null
|
||||||
|
this.motionReady = false
|
||||||
|
this.compassReady = false
|
||||||
|
}
|
||||||
|
|
||||||
|
start(): void {
|
||||||
|
if (this.listening) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.absoluteHeadingDeg = null
|
||||||
|
this.pitchDeg = null
|
||||||
|
this.rollDeg = null
|
||||||
|
this.motionReady = false
|
||||||
|
this.compassReady = false
|
||||||
|
this.source = null
|
||||||
|
|
||||||
|
if (typeof wx.startCompass === 'function' && typeof wx.onCompassChange === 'function') {
|
||||||
|
this.startCompassSource()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.callbacks.onError('当前环境不支持罗盘方向监听')
|
||||||
|
}
|
||||||
|
|
||||||
|
stop(): void {
|
||||||
|
this.detachCallbacks()
|
||||||
|
|
||||||
|
if (this.motionReady) {
|
||||||
|
wx.stopDeviceMotionListening({ complete: () => {} })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.compassReady) {
|
||||||
|
wx.stopCompass({ complete: () => {} })
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listening = false
|
||||||
|
this.source = null
|
||||||
|
this.absoluteHeadingDeg = null
|
||||||
|
this.pitchDeg = null
|
||||||
|
this.rollDeg = null
|
||||||
|
this.motionReady = false
|
||||||
|
this.compassReady = false
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
this.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
startMotionSource(previousMessage: string): void {
|
||||||
|
if (typeof wx.startDeviceMotionListening !== 'function' || typeof wx.onDeviceMotionChange !== 'function') {
|
||||||
|
this.callbacks.onError(previousMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const callback = (result: WechatMiniprogram.OnDeviceMotionChangeCallbackResult) => {
|
||||||
|
if (typeof result.alpha !== 'number' || Number.isNaN(result.alpha)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pitchDeg = typeof result.beta === 'number' && !Number.isNaN(result.beta)
|
||||||
|
? result.beta * 180 / Math.PI
|
||||||
|
: null
|
||||||
|
this.rollDeg = typeof result.gamma === 'number' && !Number.isNaN(result.gamma)
|
||||||
|
? result.gamma * 180 / Math.PI
|
||||||
|
: null
|
||||||
|
|
||||||
|
const alphaDeg = result.alpha * 180 / Math.PI
|
||||||
|
this.applyAbsoluteHeading(normalizeHeadingDeg(360 - alphaDeg), 'motion')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.motionCallback = callback
|
||||||
|
wx.onDeviceMotionChange(callback)
|
||||||
|
wx.startDeviceMotionListening({
|
||||||
|
interval: 'ui',
|
||||||
|
success: () => {
|
||||||
|
this.motionReady = true
|
||||||
|
this.listening = true
|
||||||
|
this.source = 'motion'
|
||||||
|
},
|
||||||
|
fail: (res) => {
|
||||||
|
this.detachMotionCallback()
|
||||||
|
const motionMessage = res && res.errMsg ? res.errMsg : 'startDeviceMotionListening failed'
|
||||||
|
this.callbacks.onError(`${previousMessage};${motionMessage}`)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
startCompassSource(): void {
|
||||||
|
const callback = (result: WechatMiniprogram.OnCompassChangeCallbackResult) => {
|
||||||
|
if (typeof result.direction !== 'number' || Number.isNaN(result.direction)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.applyAbsoluteHeading(normalizeHeadingDeg(result.direction), 'compass')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.compassCallback = callback
|
||||||
|
wx.onCompassChange(callback)
|
||||||
|
wx.startCompass({
|
||||||
|
success: () => {
|
||||||
|
this.compassReady = true
|
||||||
|
this.listening = true
|
||||||
|
this.source = 'compass'
|
||||||
|
},
|
||||||
|
fail: (res) => {
|
||||||
|
this.detachCompassCallback()
|
||||||
|
this.callbacks.onError(res && res.errMsg ? res.errMsg : 'startCompass failed')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
applyAbsoluteHeading(headingDeg: number, source: 'compass' | 'motion'): void {
|
||||||
|
if (this.absoluteHeadingDeg === null) {
|
||||||
|
this.absoluteHeadingDeg = headingDeg
|
||||||
|
} else {
|
||||||
|
this.absoluteHeadingDeg = interpolateHeadingDeg(this.absoluteHeadingDeg, headingDeg, ABSOLUTE_HEADING_CORRECTION)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.source = source
|
||||||
|
this.callbacks.onHeading(this.absoluteHeadingDeg)
|
||||||
|
}
|
||||||
|
|
||||||
|
detachCallbacks(): void {
|
||||||
|
this.detachMotionCallback()
|
||||||
|
this.detachCompassCallback()
|
||||||
|
}
|
||||||
|
|
||||||
|
detachMotionCallback(): void {
|
||||||
|
if (!this.motionCallback) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof wx.offDeviceMotionChange === 'function') {
|
||||||
|
wx.offDeviceMotionChange(this.motionCallback)
|
||||||
|
}
|
||||||
|
this.motionCallback = null
|
||||||
|
}
|
||||||
|
|
||||||
|
detachCompassCallback(): void {
|
||||||
|
if (!this.compassCallback) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof wx.offCompassChange === 'function') {
|
||||||
|
wx.offCompassChange(this.compassCallback)
|
||||||
|
}
|
||||||
|
this.compassCallback = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
567
miniprogram/engine/tile/tileStore.ts
Normal file
567
miniprogram/engine/tile/tileStore.ts
Normal file
@@ -0,0 +1,567 @@
|
|||||||
|
import { buildTileUrl, type TileItem } from '../../utils/tile'
|
||||||
|
import { getTilePersistentCache, type TilePersistentCache } from '../renderer/tilePersistentCache'
|
||||||
|
|
||||||
|
const MAX_PARENT_FALLBACK_DEPTH = 2
|
||||||
|
const MAX_CHILD_FALLBACK_DEPTH = 1
|
||||||
|
const MAX_CONCURRENT_DOWNLOADS = 6
|
||||||
|
const MAX_MEMORY_CACHE_SIZE = 240
|
||||||
|
const ERROR_RETRY_DELAY_MS = 4000
|
||||||
|
|
||||||
|
export type TileStatus = 'idle' | 'loading' | 'ready' | 'error'
|
||||||
|
|
||||||
|
export interface TileStoreEntry {
|
||||||
|
image: any
|
||||||
|
status: TileStatus
|
||||||
|
sourcePath: string
|
||||||
|
downloadTask: WechatMiniprogram.DownloadTask | null
|
||||||
|
priority: number
|
||||||
|
lastUsedAt: number
|
||||||
|
lastAttemptAt: number
|
||||||
|
lastVisibleKey: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TileStoreStats {
|
||||||
|
visibleTileCount: number
|
||||||
|
readyTileCount: number
|
||||||
|
memoryTileCount: number
|
||||||
|
diskTileCount: number
|
||||||
|
memoryHitCount: number
|
||||||
|
diskHitCount: number
|
||||||
|
networkFetchCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ParentFallbackTileSource {
|
||||||
|
entry: TileStoreEntry
|
||||||
|
zoom: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChildFallbackTileSource {
|
||||||
|
division: number
|
||||||
|
children: Array<{
|
||||||
|
entry: TileStoreEntry
|
||||||
|
offsetX: number
|
||||||
|
offsetY: number
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TileStoreScene {
|
||||||
|
tileSource: string
|
||||||
|
zoom: number
|
||||||
|
viewportWidth: number
|
||||||
|
viewportHeight: number
|
||||||
|
translateX: number
|
||||||
|
translateY: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TileStoreCallbacks {
|
||||||
|
onTileReady?: () => void
|
||||||
|
onTileError?: (message: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function positiveModulo(value: number, divisor: number): number {
|
||||||
|
return ((value % divisor) + divisor) % divisor
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTilePriority(tile: TileItem, scene: TileStoreScene): number {
|
||||||
|
const viewportCenterX = scene.viewportWidth / 2 + scene.translateX
|
||||||
|
const viewportCenterY = scene.viewportHeight / 2 + scene.translateY
|
||||||
|
const tileCenterX = tile.leftPx + tile.sizePx / 2
|
||||||
|
const tileCenterY = tile.topPx + tile.sizePx / 2
|
||||||
|
const deltaX = tileCenterX - viewportCenterX
|
||||||
|
const deltaY = tileCenterY - viewportCenterY
|
||||||
|
|
||||||
|
return deltaX * deltaX + deltaY * deltaY
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindImageLoad(
|
||||||
|
image: any,
|
||||||
|
src: string,
|
||||||
|
onReady: () => void,
|
||||||
|
onError: () => void,
|
||||||
|
): void {
|
||||||
|
image.onload = onReady
|
||||||
|
image.onerror = onError
|
||||||
|
image.src = src
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TileStore {
|
||||||
|
canvas: any
|
||||||
|
diskCache: TilePersistentCache
|
||||||
|
tileCache: Map<string, TileStoreEntry>
|
||||||
|
pendingUrls: string[]
|
||||||
|
pendingSet: Set<string>
|
||||||
|
pendingQueueDirty: boolean
|
||||||
|
activeDownloadCount: number
|
||||||
|
destroyed: boolean
|
||||||
|
memoryHitCount: number
|
||||||
|
diskHitCount: number
|
||||||
|
networkFetchCount: number
|
||||||
|
onTileReady?: () => void
|
||||||
|
onTileError?: (message: string) => void
|
||||||
|
|
||||||
|
constructor(callbacks?: TileStoreCallbacks) {
|
||||||
|
this.canvas = null
|
||||||
|
this.diskCache = getTilePersistentCache()
|
||||||
|
this.tileCache = new Map<string, TileStoreEntry>()
|
||||||
|
this.pendingUrls = []
|
||||||
|
this.pendingSet = new Set<string>()
|
||||||
|
this.pendingQueueDirty = false
|
||||||
|
this.activeDownloadCount = 0
|
||||||
|
this.destroyed = false
|
||||||
|
this.memoryHitCount = 0
|
||||||
|
this.diskHitCount = 0
|
||||||
|
this.networkFetchCount = 0
|
||||||
|
this.onTileReady = callbacks && callbacks.onTileReady ? callbacks.onTileReady : undefined
|
||||||
|
this.onTileError = callbacks && callbacks.onTileError ? callbacks.onTileError : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
attachCanvas(canvas: any): void {
|
||||||
|
this.canvas = canvas
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
this.destroyed = true
|
||||||
|
this.tileCache.forEach((entry) => {
|
||||||
|
if (entry.downloadTask) {
|
||||||
|
entry.downloadTask.abort()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.pendingUrls = []
|
||||||
|
this.pendingSet.clear()
|
||||||
|
this.pendingQueueDirty = false
|
||||||
|
this.activeDownloadCount = 0
|
||||||
|
this.tileCache.clear()
|
||||||
|
this.canvas = null
|
||||||
|
}
|
||||||
|
|
||||||
|
getReadyMemoryTileCount(): number {
|
||||||
|
let count = 0
|
||||||
|
this.tileCache.forEach((entry) => {
|
||||||
|
if (entry.status === 'ready' && entry.image) {
|
||||||
|
count += 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
getStats(visibleTileCount: number, readyTileCount: number): TileStoreStats {
|
||||||
|
return {
|
||||||
|
visibleTileCount,
|
||||||
|
readyTileCount,
|
||||||
|
memoryTileCount: this.getReadyMemoryTileCount(),
|
||||||
|
diskTileCount: this.diskCache.getCount(),
|
||||||
|
memoryHitCount: this.memoryHitCount,
|
||||||
|
diskHitCount: this.diskHitCount,
|
||||||
|
networkFetchCount: this.networkFetchCount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getEntry(url: string): TileStoreEntry | undefined {
|
||||||
|
return this.tileCache.get(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
touchTile(url: string, priority: number, usedAt: number): TileStoreEntry {
|
||||||
|
let entry = this.tileCache.get(url)
|
||||||
|
if (!entry) {
|
||||||
|
entry = {
|
||||||
|
image: null,
|
||||||
|
status: 'idle',
|
||||||
|
sourcePath: '',
|
||||||
|
downloadTask: null,
|
||||||
|
priority,
|
||||||
|
lastUsedAt: usedAt,
|
||||||
|
lastAttemptAt: 0,
|
||||||
|
lastVisibleKey: '',
|
||||||
|
}
|
||||||
|
this.tileCache.set(url, entry)
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.priority !== priority && this.pendingSet.has(url)) {
|
||||||
|
this.pendingQueueDirty = true
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.priority = priority
|
||||||
|
entry.lastUsedAt = usedAt
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
|
trimPendingQueue(protectedUrls: Set<string>): void {
|
||||||
|
const nextPendingUrls: string[] = []
|
||||||
|
for (const url of this.pendingUrls) {
|
||||||
|
if (!protectedUrls.has(url)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nextPendingUrls.push(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pendingUrls = nextPendingUrls
|
||||||
|
this.pendingSet = new Set<string>(nextPendingUrls)
|
||||||
|
this.pendingQueueDirty = true
|
||||||
|
}
|
||||||
|
|
||||||
|
queueTile(url: string): void {
|
||||||
|
if (this.pendingSet.has(url)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = this.tileCache.get(url)
|
||||||
|
if (!entry || entry.status === 'loading' || entry.status === 'ready') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pendingSet.add(url)
|
||||||
|
this.pendingUrls.push(url)
|
||||||
|
this.pendingQueueDirty = true
|
||||||
|
}
|
||||||
|
|
||||||
|
queueVisibleTiles(tiles: TileItem[], scene: TileStoreScene, visibleKey: string): void {
|
||||||
|
const usedAt = Date.now()
|
||||||
|
const protectedUrls = new Set<string>()
|
||||||
|
const parentPriorityMap = new Map<string, number>()
|
||||||
|
const countedMemoryHits = new Set<string>()
|
||||||
|
|
||||||
|
for (const tile of tiles) {
|
||||||
|
const priority = getTilePriority(tile, scene)
|
||||||
|
const entry = this.touchTile(tile.url, priority, usedAt)
|
||||||
|
protectedUrls.add(tile.url)
|
||||||
|
|
||||||
|
if (entry.status === 'ready' && entry.lastVisibleKey !== visibleKey && !countedMemoryHits.has(tile.url)) {
|
||||||
|
this.memoryHitCount += 1
|
||||||
|
entry.lastVisibleKey = visibleKey
|
||||||
|
countedMemoryHits.add(tile.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let depth = 1; depth <= MAX_PARENT_FALLBACK_DEPTH; depth += 1) {
|
||||||
|
const fallbackZoom = scene.zoom - depth
|
||||||
|
if (fallbackZoom < 0) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
const scale = Math.pow(2, depth)
|
||||||
|
const fallbackX = Math.floor(tile.x / scale)
|
||||||
|
const fallbackY = Math.floor(tile.y / scale)
|
||||||
|
const fallbackUrl = buildTileUrl(scene.tileSource, fallbackZoom, fallbackX, fallbackY)
|
||||||
|
const fallbackPriority = priority / (depth + 1)
|
||||||
|
const existingPriority = parentPriorityMap.get(fallbackUrl)
|
||||||
|
|
||||||
|
if (typeof existingPriority !== 'number' || fallbackPriority < existingPriority) {
|
||||||
|
parentPriorityMap.set(fallbackUrl, fallbackPriority)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parentPriorityMap.forEach((priority, url) => {
|
||||||
|
const entry = this.touchTile(url, priority, usedAt)
|
||||||
|
protectedUrls.add(url)
|
||||||
|
if (entry.status === 'ready' && entry.lastVisibleKey !== visibleKey && !countedMemoryHits.has(url)) {
|
||||||
|
this.memoryHitCount += 1
|
||||||
|
entry.lastVisibleKey = visibleKey
|
||||||
|
countedMemoryHits.add(url)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.trimPendingQueue(protectedUrls)
|
||||||
|
|
||||||
|
parentPriorityMap.forEach((_priority, url) => {
|
||||||
|
const entry = this.tileCache.get(url)
|
||||||
|
if (!entry) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.status === 'idle' || (entry.status === 'error' && usedAt - entry.lastAttemptAt > ERROR_RETRY_DELAY_MS)) {
|
||||||
|
if (entry.status === 'error') {
|
||||||
|
entry.status = 'idle'
|
||||||
|
}
|
||||||
|
this.queueTile(url)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const tile of tiles) {
|
||||||
|
const entry = this.tileCache.get(tile.url)
|
||||||
|
if (!entry) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.status === 'idle' || (entry.status === 'error' && usedAt - entry.lastAttemptAt > ERROR_RETRY_DELAY_MS)) {
|
||||||
|
if (entry.status === 'error') {
|
||||||
|
entry.status = 'idle'
|
||||||
|
}
|
||||||
|
this.queueTile(tile.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pruneMemoryCache(protectedUrls)
|
||||||
|
this.pumpTileQueue()
|
||||||
|
}
|
||||||
|
|
||||||
|
pumpTileQueue(): void {
|
||||||
|
if (this.destroyed || !this.canvas) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.pendingQueueDirty && this.pendingUrls.length > 1) {
|
||||||
|
this.pendingUrls.sort((leftUrl, rightUrl) => {
|
||||||
|
const leftEntry = this.tileCache.get(leftUrl)
|
||||||
|
const rightEntry = this.tileCache.get(rightUrl)
|
||||||
|
const leftPriority = leftEntry ? leftEntry.priority : Number.MAX_SAFE_INTEGER
|
||||||
|
const rightPriority = rightEntry ? rightEntry.priority : Number.MAX_SAFE_INTEGER
|
||||||
|
return leftPriority - rightPriority
|
||||||
|
})
|
||||||
|
this.pendingQueueDirty = false
|
||||||
|
}
|
||||||
|
|
||||||
|
while (this.activeDownloadCount < MAX_CONCURRENT_DOWNLOADS && this.pendingUrls.length) {
|
||||||
|
const url = this.pendingUrls.shift() as string
|
||||||
|
this.pendingSet.delete(url)
|
||||||
|
|
||||||
|
const entry = this.tileCache.get(url)
|
||||||
|
if (!entry || entry.status === 'loading' || entry.status === 'ready') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
this.startTileDownload(url, entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startTileDownload(url: string, entry: TileStoreEntry): void {
|
||||||
|
if (this.destroyed || !this.canvas) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.status = 'loading'
|
||||||
|
entry.lastAttemptAt = Date.now()
|
||||||
|
this.activeDownloadCount += 1
|
||||||
|
|
||||||
|
let finished = false
|
||||||
|
const finish = () => {
|
||||||
|
if (finished) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
finished = true
|
||||||
|
entry.downloadTask = null
|
||||||
|
this.activeDownloadCount = Math.max(0, this.activeDownloadCount - 1)
|
||||||
|
this.pumpTileQueue()
|
||||||
|
}
|
||||||
|
|
||||||
|
const markReady = () => {
|
||||||
|
entry.status = 'ready'
|
||||||
|
finish()
|
||||||
|
if (this.onTileReady) {
|
||||||
|
this.onTileReady()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const markError = (message: string) => {
|
||||||
|
entry.status = 'error'
|
||||||
|
finish()
|
||||||
|
if (this.onTileError) {
|
||||||
|
this.onTileError(`${message}: ${url}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadLocalImage = (localPath: string, fromPersistentCache: boolean) => {
|
||||||
|
if (this.destroyed || !this.canvas) {
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const localImage = this.canvas.createImage()
|
||||||
|
entry.image = localImage
|
||||||
|
entry.sourcePath = localPath
|
||||||
|
|
||||||
|
bindImageLoad(
|
||||||
|
localImage,
|
||||||
|
localPath,
|
||||||
|
() => {
|
||||||
|
this.diskCache.markReady(url, localPath)
|
||||||
|
markReady()
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
this.diskCache.remove(url)
|
||||||
|
if (fromPersistentCache) {
|
||||||
|
downloadToPersistentPath()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
markError('瓦片本地载入失败')
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tryRemoteImage = () => {
|
||||||
|
if (this.destroyed || !this.canvas) {
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteImage = this.canvas.createImage()
|
||||||
|
entry.image = remoteImage
|
||||||
|
entry.sourcePath = url
|
||||||
|
|
||||||
|
bindImageLoad(
|
||||||
|
remoteImage,
|
||||||
|
url,
|
||||||
|
markReady,
|
||||||
|
() => markError('瓦片远程载入失败'),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadToPersistentPath = () => {
|
||||||
|
this.networkFetchCount += 1
|
||||||
|
const filePath = this.diskCache.getTargetPath(url)
|
||||||
|
const task = wx.downloadFile({
|
||||||
|
url,
|
||||||
|
filePath,
|
||||||
|
success: (res) => {
|
||||||
|
if (this.destroyed) {
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedPath = res.filePath || filePath || res.tempFilePath
|
||||||
|
if (res.statusCode !== 200 || !resolvedPath) {
|
||||||
|
tryRemoteImage()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loadLocalImage(resolvedPath, false)
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
tryRemoteImage()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
entry.downloadTask = task
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedPath = this.diskCache.getCachedPath(url)
|
||||||
|
if (cachedPath) {
|
||||||
|
this.diskHitCount += 1
|
||||||
|
loadLocalImage(cachedPath, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadToPersistentPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
pruneMemoryCache(protectedUrls: Set<string>): void {
|
||||||
|
if (this.tileCache.size <= MAX_MEMORY_CACHE_SIZE) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const removableEntries: Array<{ url: string; lastUsedAt: number; priority: number }> = []
|
||||||
|
this.tileCache.forEach((entry, url) => {
|
||||||
|
if (protectedUrls.has(url) || this.pendingSet.has(url) || entry.status === 'loading') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
removableEntries.push({
|
||||||
|
url,
|
||||||
|
lastUsedAt: entry.lastUsedAt,
|
||||||
|
priority: entry.priority,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
removableEntries.sort((leftEntry, rightEntry) => {
|
||||||
|
if (leftEntry.lastUsedAt !== rightEntry.lastUsedAt) {
|
||||||
|
return leftEntry.lastUsedAt - rightEntry.lastUsedAt
|
||||||
|
}
|
||||||
|
return rightEntry.priority - leftEntry.priority
|
||||||
|
})
|
||||||
|
|
||||||
|
while (this.tileCache.size > MAX_MEMORY_CACHE_SIZE && removableEntries.length) {
|
||||||
|
const nextEntry = removableEntries.shift() as { url: string }
|
||||||
|
this.tileCache.delete(nextEntry.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
findParentFallback(tile: TileItem, scene: TileStoreScene): ParentFallbackTileSource | null {
|
||||||
|
for (let depth = 1; depth <= MAX_PARENT_FALLBACK_DEPTH; depth += 1) {
|
||||||
|
const fallbackZoom = scene.zoom - depth
|
||||||
|
if (fallbackZoom < 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const scale = Math.pow(2, depth)
|
||||||
|
const fallbackX = Math.floor(tile.x / scale)
|
||||||
|
const fallbackY = Math.floor(tile.y / scale)
|
||||||
|
const fallbackUrl = buildTileUrl(scene.tileSource, fallbackZoom, fallbackX, fallbackY)
|
||||||
|
const fallbackEntry = this.tileCache.get(fallbackUrl)
|
||||||
|
|
||||||
|
if (fallbackEntry && fallbackEntry.status === 'ready' && fallbackEntry.image) {
|
||||||
|
return {
|
||||||
|
entry: fallbackEntry,
|
||||||
|
zoom: fallbackZoom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
getParentFallbackSlice(tile: TileItem, scene: TileStoreScene): {
|
||||||
|
entry: TileStoreEntry
|
||||||
|
sourceX: number
|
||||||
|
sourceY: number
|
||||||
|
sourceWidth: number
|
||||||
|
sourceHeight: number
|
||||||
|
} | null {
|
||||||
|
const fallback = this.findParentFallback(tile, scene)
|
||||||
|
if (!fallback) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const zoomDelta = scene.zoom - fallback.zoom
|
||||||
|
const division = Math.pow(2, zoomDelta)
|
||||||
|
const imageWidth = fallback.entry.image.width || 256
|
||||||
|
const imageHeight = fallback.entry.image.height || 256
|
||||||
|
const sourceWidth = imageWidth / division
|
||||||
|
const sourceHeight = imageHeight / division
|
||||||
|
const offsetX = positiveModulo(tile.x, division)
|
||||||
|
const offsetY = positiveModulo(tile.y, division)
|
||||||
|
|
||||||
|
return {
|
||||||
|
entry: fallback.entry,
|
||||||
|
sourceX: offsetX * sourceWidth,
|
||||||
|
sourceY: offsetY * sourceHeight,
|
||||||
|
sourceWidth,
|
||||||
|
sourceHeight,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getChildFallback(tile: TileItem, scene: TileStoreScene): ChildFallbackTileSource | null {
|
||||||
|
for (let depth = 1; depth <= MAX_CHILD_FALLBACK_DEPTH; depth += 1) {
|
||||||
|
const childZoom = scene.zoom + depth
|
||||||
|
const division = Math.pow(2, depth)
|
||||||
|
const children: ChildFallbackTileSource['children'] = []
|
||||||
|
|
||||||
|
for (let offsetY = 0; offsetY < division; offsetY += 1) {
|
||||||
|
for (let offsetX = 0; offsetX < division; offsetX += 1) {
|
||||||
|
const childX = tile.x * division + offsetX
|
||||||
|
const childY = tile.y * division + offsetY
|
||||||
|
const childUrl = buildTileUrl(scene.tileSource, childZoom, childX, childY)
|
||||||
|
const childEntry = this.tileCache.get(childUrl)
|
||||||
|
if (!childEntry || childEntry.status !== 'ready' || !childEntry.image) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
children.push({
|
||||||
|
entry: childEntry,
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (children.length) {
|
||||||
|
return {
|
||||||
|
division,
|
||||||
|
children,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
4
miniprogram/pages/index/index.json
Normal file
4
miniprogram/pages/index/index.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"usingComponents": {
|
||||||
|
}
|
||||||
|
}
|
||||||
54
miniprogram/pages/index/index.ts
Normal file
54
miniprogram/pages/index/index.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// index.ts
|
||||||
|
// 获取应用实例
|
||||||
|
const app = getApp<IAppOption>()
|
||||||
|
const defaultAvatarUrl = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0'
|
||||||
|
|
||||||
|
Component({
|
||||||
|
data: {
|
||||||
|
motto: 'Hello World',
|
||||||
|
userInfo: {
|
||||||
|
avatarUrl: defaultAvatarUrl,
|
||||||
|
nickName: '',
|
||||||
|
},
|
||||||
|
hasUserInfo: false,
|
||||||
|
canIUseGetUserProfile: wx.canIUse('getUserProfile'),
|
||||||
|
canIUseNicknameComp: wx.canIUse('input.type.nickname'),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 事件处理函数
|
||||||
|
bindViewTap() {
|
||||||
|
wx.navigateTo({
|
||||||
|
url: '../logs/logs',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onChooseAvatar(e: any) {
|
||||||
|
const { avatarUrl } = e.detail
|
||||||
|
const { nickName } = this.data.userInfo
|
||||||
|
this.setData({
|
||||||
|
"userInfo.avatarUrl": avatarUrl,
|
||||||
|
hasUserInfo: nickName && avatarUrl && avatarUrl !== defaultAvatarUrl,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onInputChange(e: any) {
|
||||||
|
const nickName = e.detail.value
|
||||||
|
const { avatarUrl } = this.data.userInfo
|
||||||
|
this.setData({
|
||||||
|
"userInfo.nickName": nickName,
|
||||||
|
hasUserInfo: nickName && avatarUrl && avatarUrl !== defaultAvatarUrl,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getUserProfile() {
|
||||||
|
// 推荐使用wx.getUserProfile获取用户信息,开发者每次通过该接口获取用户个人信息均需用户确认,开发者妥善保管用户快速填写的头像昵称,避免重复弹窗
|
||||||
|
wx.getUserProfile({
|
||||||
|
desc: '展示用户信息', // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写
|
||||||
|
success: (res) => {
|
||||||
|
console.log(res)
|
||||||
|
this.setData({
|
||||||
|
userInfo: res.userInfo,
|
||||||
|
hasUserInfo: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
27
miniprogram/pages/index/index.wxml
Normal file
27
miniprogram/pages/index/index.wxml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<!--index.wxml-->
|
||||||
|
<scroll-view class="scrollarea" scroll-y type="list">
|
||||||
|
<view class="container">
|
||||||
|
<view class="userinfo">
|
||||||
|
<block wx:if="{{canIUseNicknameComp && !hasUserInfo}}">
|
||||||
|
<button class="avatar-wrapper" open-type="chooseAvatar" bind:chooseavatar="onChooseAvatar">
|
||||||
|
<image class="avatar" src="{{userInfo.avatarUrl}}"></image>
|
||||||
|
</button>
|
||||||
|
<view class="nickname-wrapper">
|
||||||
|
<text class="nickname-label">昵称</text>
|
||||||
|
<input type="nickname" class="nickname-input" placeholder="请输入昵称" bind:change="onInputChange" />
|
||||||
|
</view>
|
||||||
|
</block>
|
||||||
|
<block wx:elif="{{!hasUserInfo}}">
|
||||||
|
<button wx:if="{{canIUseGetUserProfile}}" bindtap="getUserProfile"> 获取头像昵称 </button>
|
||||||
|
<view wx:else> 请使用2.10.4及以上版本基础库 </view>
|
||||||
|
</block>
|
||||||
|
<block wx:else>
|
||||||
|
<image bindtap="bindViewTap" class="userinfo-avatar" src="{{userInfo.avatarUrl}}" mode="cover"></image>
|
||||||
|
<text class="userinfo-nickname">{{userInfo.nickName}}</text>
|
||||||
|
</block>
|
||||||
|
</view>
|
||||||
|
<view class="usermotto">
|
||||||
|
<text class="user-motto">{{motto}}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
62
miniprogram/pages/index/index.wxss
Normal file
62
miniprogram/pages/index/index.wxss
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
/**index.wxss**/
|
||||||
|
page {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.scrollarea {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userinfo {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
color: #aaa;
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userinfo-avatar {
|
||||||
|
overflow: hidden;
|
||||||
|
width: 128rpx;
|
||||||
|
height: 128rpx;
|
||||||
|
margin: 20rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usermotto {
|
||||||
|
margin-top: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-wrapper {
|
||||||
|
padding: 0;
|
||||||
|
width: 56px !important;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 40px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
display: block;
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nickname-wrapper {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-top: .5px solid rgba(0, 0, 0, 0.1);
|
||||||
|
border-bottom: .5px solid rgba(0, 0, 0, 0.1);
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nickname-label {
|
||||||
|
width: 105px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nickname-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
4
miniprogram/pages/logs/logs.json
Normal file
4
miniprogram/pages/logs/logs.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"usingComponents": {
|
||||||
|
}
|
||||||
|
}
|
||||||
21
miniprogram/pages/logs/logs.ts
Normal file
21
miniprogram/pages/logs/logs.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// logs.ts
|
||||||
|
// const util = require('../../utils/util.js')
|
||||||
|
import { formatTime } from '../../utils/util'
|
||||||
|
|
||||||
|
Component({
|
||||||
|
data: {
|
||||||
|
logs: [],
|
||||||
|
},
|
||||||
|
lifetimes: {
|
||||||
|
attached() {
|
||||||
|
this.setData({
|
||||||
|
logs: (wx.getStorageSync('logs') || []).map((log: string) => {
|
||||||
|
return {
|
||||||
|
date: formatTime(new Date(log)),
|
||||||
|
timeStamp: log
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
6
miniprogram/pages/logs/logs.wxml
Normal file
6
miniprogram/pages/logs/logs.wxml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<!--logs.wxml-->
|
||||||
|
<scroll-view class="scrollarea" scroll-y type="list">
|
||||||
|
<block wx:for="{{logs}}" wx:key="timeStamp" wx:for-item="log">
|
||||||
|
<view class="log-item">{{index + 1}}. {{log.date}}</view>
|
||||||
|
</block>
|
||||||
|
</scroll-view>
|
||||||
16
miniprogram/pages/logs/logs.wxss
Normal file
16
miniprogram/pages/logs/logs.wxss
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
page {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.scrollarea {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
.log-item {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.log-item:last-child {
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
}
|
||||||
4
miniprogram/pages/map/map.json
Normal file
4
miniprogram/pages/map/map.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"navigationBarTitleText": "地图",
|
||||||
|
"disableScroll": true
|
||||||
|
}
|
||||||
173
miniprogram/pages/map/map.ts
Normal file
173
miniprogram/pages/map/map.ts
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import { MapEngine, type MapEngineStageRect, type MapEngineViewState } from '../../engine/map/mapEngine'
|
||||||
|
|
||||||
|
const INTERNAL_BUILD_VERSION = 'map-build-58'
|
||||||
|
|
||||||
|
let mapEngine: MapEngine | null = null
|
||||||
|
|
||||||
|
function getFallbackStageRect(): MapEngineStageRect {
|
||||||
|
const systemInfo = wx.getSystemInfoSync()
|
||||||
|
const width = Math.max(320, systemInfo.windowWidth - 20)
|
||||||
|
const height = Math.max(280, Math.floor(systemInfo.windowHeight * 0.66))
|
||||||
|
|
||||||
|
return {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
left: 10,
|
||||||
|
top: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Page({
|
||||||
|
data: {} as MapEngineViewState,
|
||||||
|
|
||||||
|
onLoad() {
|
||||||
|
mapEngine = new MapEngine(INTERNAL_BUILD_VERSION, {
|
||||||
|
onData: (patch) => {
|
||||||
|
this.setData(patch)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
this.setData(mapEngine.getInitialData())
|
||||||
|
},
|
||||||
|
|
||||||
|
onReady() {
|
||||||
|
this.measureStageAndCanvas()
|
||||||
|
},
|
||||||
|
|
||||||
|
onUnload() {
|
||||||
|
if (mapEngine) {
|
||||||
|
mapEngine.destroy()
|
||||||
|
mapEngine = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
measureStageAndCanvas() {
|
||||||
|
const page = this
|
||||||
|
const applyStage = (rawRect?: Partial<WechatMiniprogram.BoundingClientRectCallbackResult>) => {
|
||||||
|
const fallbackRect = getFallbackStageRect()
|
||||||
|
const rect: MapEngineStageRect = {
|
||||||
|
width: rawRect && typeof rawRect.width === 'number' ? rawRect.width : fallbackRect.width,
|
||||||
|
height: rawRect && typeof rawRect.height === 'number' ? rawRect.height : fallbackRect.height,
|
||||||
|
left: rawRect && typeof rawRect.left === 'number' ? rawRect.left : fallbackRect.left,
|
||||||
|
top: rawRect && typeof rawRect.top === 'number' ? rawRect.top : fallbackRect.top,
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentEngine = mapEngine
|
||||||
|
if (!currentEngine) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentEngine.setStage(rect)
|
||||||
|
|
||||||
|
const canvasQuery = wx.createSelectorQuery().in(page)
|
||||||
|
canvasQuery.select('#mapCanvas').fields({ node: true, size: true })
|
||||||
|
canvasQuery.exec((canvasRes) => {
|
||||||
|
const canvasRef = canvasRes[0] as any
|
||||||
|
if (!canvasRef || !canvasRef.node) {
|
||||||
|
page.setData({
|
||||||
|
statusText: `WebGL 引擎初始化失败 (${INTERNAL_BUILD_VERSION})`,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const dpr = wx.getSystemInfoSync().pixelRatio || 1
|
||||||
|
try {
|
||||||
|
currentEngine.attachCanvas(canvasRef.node, rect.width, rect.height, dpr)
|
||||||
|
} catch (error) {
|
||||||
|
page.setData({
|
||||||
|
statusText: `WebGL 初始化失败 (${INTERNAL_BUILD_VERSION})`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = wx.createSelectorQuery().in(page)
|
||||||
|
query.select('.map-stage').boundingClientRect()
|
||||||
|
query.exec((res) => {
|
||||||
|
const rect = res[0] as WechatMiniprogram.BoundingClientRectCallbackResult | undefined
|
||||||
|
applyStage(rect)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
handleTouchStart(event: WechatMiniprogram.TouchEvent) {
|
||||||
|
if (mapEngine) {
|
||||||
|
mapEngine.handleTouchStart(event)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleTouchMove(event: WechatMiniprogram.TouchEvent) {
|
||||||
|
if (mapEngine) {
|
||||||
|
mapEngine.handleTouchMove(event)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleTouchEnd(event: WechatMiniprogram.TouchEvent) {
|
||||||
|
if (mapEngine) {
|
||||||
|
mapEngine.handleTouchEnd(event)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleTouchCancel() {
|
||||||
|
if (mapEngine) {
|
||||||
|
mapEngine.handleTouchCancel()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleRecenter() {
|
||||||
|
if (mapEngine) {
|
||||||
|
mapEngine.handleRecenter()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleRotateStep() {
|
||||||
|
if (mapEngine) {
|
||||||
|
mapEngine.handleRotateStep()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleRotationReset() {
|
||||||
|
if (mapEngine) {
|
||||||
|
mapEngine.handleRotationReset()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSetManualMode() {
|
||||||
|
if (mapEngine) {
|
||||||
|
mapEngine.handleSetManualMode()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSetNorthUpMode() {
|
||||||
|
if (mapEngine) {
|
||||||
|
mapEngine.handleSetNorthUpMode()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSetHeadingUpMode() {
|
||||||
|
if (mapEngine) {
|
||||||
|
mapEngine.handleSetHeadingUpMode()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleAutoRotateCalibrate() {
|
||||||
|
if (mapEngine) {
|
||||||
|
mapEngine.handleAutoRotateCalibrate()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
152
miniprogram/pages/map/map.wxml
Normal file
152
miniprogram/pages/map/map.wxml
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
<view class="page">
|
||||||
|
<view class="page__header">
|
||||||
|
<view>
|
||||||
|
<view class="page__eyebrow">CMR MINI PROGRAM</view>
|
||||||
|
<view class="page__title">{{mapName}}</view>
|
||||||
|
</view>
|
||||||
|
<view class="page__badge">{{mapReadyText}}</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="map-stage-wrap">
|
||||||
|
<view
|
||||||
|
class="map-stage"
|
||||||
|
catchtouchstart="handleTouchStart"
|
||||||
|
catchtouchmove="handleTouchMove"
|
||||||
|
catchtouchend="handleTouchEnd"
|
||||||
|
catchtouchcancel="handleTouchCancel"
|
||||||
|
>
|
||||||
|
<view class="map-content">
|
||||||
|
<canvas
|
||||||
|
id="mapCanvas"
|
||||||
|
type="webgl"
|
||||||
|
canvas-id="mapCanvas"
|
||||||
|
class="map-canvas map-canvas--base"
|
||||||
|
></canvas>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="map-stage__crosshair"></view>
|
||||||
|
|
||||||
|
<view class="map-stage__overlay">
|
||||||
|
<view class="overlay-card">
|
||||||
|
<view class="overlay-card__label">WEBGL MAP ENGINE</view>
|
||||||
|
<view class="overlay-card__title">North Up / Heading Up / Manual</view>
|
||||||
|
<view class="overlay-card__desc">
|
||||||
|
地图北已经固定为正上方。现在支持手动旋转、北朝上、朝向朝上三种模式,并提供指北针用于校验朝向。
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="compass-widget">
|
||||||
|
<view class="compass-widget__ring">
|
||||||
|
<view class="compass-widget__north">N</view>
|
||||||
|
<view class="compass-widget__needle" style="transform: translateX(-50%) rotate({{compassNeedleDeg}}deg);"></view>
|
||||||
|
<view class="compass-widget__center"></view>
|
||||||
|
</view>
|
||||||
|
<view class="compass-widget__label">{{sensorHeadingText}}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view class="info-panel" scroll-y enhanced show-scrollbar="true">
|
||||||
|
<view class="info-panel__row">
|
||||||
|
<text class="info-panel__label">Build</text>
|
||||||
|
<text class="info-panel__value">{{buildVersion}}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-panel__row">
|
||||||
|
<text class="info-panel__label">Renderer</text>
|
||||||
|
<text class="info-panel__value">{{renderMode}}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-panel__row info-panel__row--stack">
|
||||||
|
<text class="info-panel__label">Projection</text>
|
||||||
|
<text class="info-panel__value">{{projectionMode}}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-panel__row">
|
||||||
|
<text class="info-panel__label">Heading Mode</text>
|
||||||
|
<text class="info-panel__value">{{orientationModeText}}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-panel__row">
|
||||||
|
<text class="info-panel__label">Sensor Heading</text>
|
||||||
|
<text class="info-panel__value">{{sensorHeadingText}}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-panel__row">
|
||||||
|
<text class="info-panel__label">North Ref</text>
|
||||||
|
<text class="info-panel__value">{{northReferenceText}}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-panel__row">
|
||||||
|
<text class="info-panel__label">Auto Source</text>
|
||||||
|
<text class="info-panel__value">{{autoRotateSourceText}}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-panel__row">
|
||||||
|
<text class="info-panel__label">Calibration</text>
|
||||||
|
<text class="info-panel__value">{{autoRotateCalibrationText}}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-panel__row info-panel__row--stack">
|
||||||
|
<text class="info-panel__label">Tile URL</text>
|
||||||
|
<text class="info-panel__value">{{tileSource}}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-panel__row">
|
||||||
|
<text class="info-panel__label">Zoom</text>
|
||||||
|
<text class="info-panel__value">{{zoom}}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-panel__row">
|
||||||
|
<text class="info-panel__label">Rotation</text>
|
||||||
|
<text class="info-panel__value">{{rotationText}}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-panel__row">
|
||||||
|
<text class="info-panel__label">Center Tile</text>
|
||||||
|
<text class="info-panel__value">{{centerText}}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-panel__row">
|
||||||
|
<text class="info-panel__label">Tile Size</text>
|
||||||
|
<text class="info-panel__value">{{tileSizePx}}px</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-panel__row">
|
||||||
|
<text class="info-panel__label">Visible Tiles</text>
|
||||||
|
<text class="info-panel__value">{{visibleTileCount}}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-panel__row">
|
||||||
|
<text class="info-panel__label">Ready Tiles</text>
|
||||||
|
<text class="info-panel__value">{{readyTileCount}}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-panel__row">
|
||||||
|
<text class="info-panel__label">Memory Tiles</text>
|
||||||
|
<text class="info-panel__value">{{memoryTileCount}}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-panel__row">
|
||||||
|
<text class="info-panel__label">Disk Tiles</text>
|
||||||
|
<text class="info-panel__value">{{diskTileCount}}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-panel__row">
|
||||||
|
<text class="info-panel__label">Cache Hit</text>
|
||||||
|
<text class="info-panel__value">{{cacheHitRateText}}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-panel__row">
|
||||||
|
<text class="info-panel__label">Disk Hits</text>
|
||||||
|
<text class="info-panel__value">{{diskHitCount}}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-panel__row">
|
||||||
|
<text class="info-panel__label">Net Fetches</text>
|
||||||
|
<text class="info-panel__value">{{networkFetchCount}}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-panel__row info-panel__row--stack">
|
||||||
|
<text class="info-panel__label">Status</text>
|
||||||
|
<text class="info-panel__value">{{statusText}}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="control-row">
|
||||||
|
<view class="control-chip control-chip--primary" bindtap="handleRecenter">回到首屏</view>
|
||||||
|
<view class="control-chip control-chip--secondary" bindtap="handleRotationReset">旋转归零</view>
|
||||||
|
</view>
|
||||||
|
<view class="control-row control-row--triple">
|
||||||
|
<view class="control-chip {{orientationMode === 'manual' ? 'control-chip--active' : ''}}" bindtap="handleSetManualMode">手动</view>
|
||||||
|
<view class="control-chip {{orientationMode === 'north-up' ? 'control-chip--active' : ''}}" bindtap="handleSetNorthUpMode">北朝上</view>
|
||||||
|
<view class="control-chip {{orientationMode === 'heading-up' ? 'control-chip--active' : ''}}" bindtap="handleSetHeadingUpMode">朝向朝上</view>
|
||||||
|
</view>
|
||||||
|
<view class="control-row" wx:if="{{orientationMode === 'heading-up'}}">
|
||||||
|
<view class="control-chip" bindtap="handleAutoRotateCalibrate">按当前方向校准</view>
|
||||||
|
</view>
|
||||||
|
<view class="control-row" wx:if="{{orientationMode === 'manual'}}">
|
||||||
|
<view class="control-chip" bindtap="handleRotateStep">旋转 +15°</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
337
miniprogram/pages/map/map.wxss
Normal file
337
miniprogram/pages/map/map.wxss
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
.page {
|
||||||
|
height: 100vh;
|
||||||
|
padding: 20rpx 20rpx 24rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, #d8f3dc 0%, rgba(216, 243, 220, 0) 32%),
|
||||||
|
linear-gradient(180deg, #f7fbf2 0%, #eef6ea 100%);
|
||||||
|
color: #163020;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page__eyebrow {
|
||||||
|
font-size: 20rpx;
|
||||||
|
letter-spacing: 4rpx;
|
||||||
|
color: #5f7a65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page__title {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 44rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page__badge {
|
||||||
|
padding: 10rpx 18rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: #163020;
|
||||||
|
color: #f7fbf2;
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-stage-wrap {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 66vh;
|
||||||
|
min-height: 520rpx;
|
||||||
|
max-height: 72vh;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-stage {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 2rpx solid rgba(22, 48, 32, 0.08);
|
||||||
|
border-radius: 32rpx;
|
||||||
|
background: #dbeed4;
|
||||||
|
box-shadow: 0 18rpx 40rpx rgba(22, 48, 32, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-content {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-canvas {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-canvas--base {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-stage__crosshair {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
width: 44rpx;
|
||||||
|
height: 44rpx;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
border: 3rpx solid rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 0 0 4rpx rgba(22, 48, 32, 0.2);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-stage__crosshair::before,
|
||||||
|
.map-stage__crosshair::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-stage__crosshair::before {
|
||||||
|
left: 50%;
|
||||||
|
top: -18rpx;
|
||||||
|
width: 2rpx;
|
||||||
|
height: 76rpx;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-stage__crosshair::after {
|
||||||
|
left: -18rpx;
|
||||||
|
top: 50%;
|
||||||
|
width: 76rpx;
|
||||||
|
height: 2rpx;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-stage__overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 24rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-card {
|
||||||
|
width: 68%;
|
||||||
|
padding: 22rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: rgba(247, 251, 242, 0.92);
|
||||||
|
box-shadow: 0 12rpx 30rpx rgba(22, 48, 32, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-card__label {
|
||||||
|
font-size: 20rpx;
|
||||||
|
letter-spacing: 3rpx;
|
||||||
|
color: #5f7a65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-card__title {
|
||||||
|
margin-top: 10rpx;
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-card__desc {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #45624b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compass-widget {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compass-widget__ring {
|
||||||
|
position: relative;
|
||||||
|
width: 108rpx;
|
||||||
|
height: 108rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(247, 251, 242, 0.94);
|
||||||
|
border: 2rpx solid rgba(22, 48, 32, 0.12);
|
||||||
|
box-shadow: 0 10rpx 24rpx rgba(22, 48, 32, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.compass-widget__north {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 10rpx;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #d62828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compass-widget__needle {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 18rpx;
|
||||||
|
width: 4rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
transform-origin: 50% 36rpx;
|
||||||
|
background: linear-gradient(180deg, #d62828 0%, #163020 100%);
|
||||||
|
border-radius: 999rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compass-widget__center {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
width: 14rpx;
|
||||||
|
height: 14rpx;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #163020;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compass-widget__label {
|
||||||
|
min-width: 92rpx;
|
||||||
|
padding: 6rpx 10rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(247, 251, 242, 0.94);
|
||||||
|
font-size: 20rpx;
|
||||||
|
text-align: center;
|
||||||
|
color: #163020;
|
||||||
|
box-shadow: 0 8rpx 18rpx rgba(22, 48, 32, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 22rpx 20rpx 28rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.88);
|
||||||
|
box-shadow: 0 12rpx 32rpx rgba(22, 48, 32, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel__row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
padding: 10rpx 0;
|
||||||
|
border-bottom: 1rpx solid rgba(22, 48, 32, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel__row--stack {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel__row:last-of-type {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel__label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 22rpx;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
|
color: #5f7a65;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel__value {
|
||||||
|
font-size: 25rpx;
|
||||||
|
color: #163020;
|
||||||
|
text-align: right;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel__row--stack .info-panel__value {
|
||||||
|
display: block;
|
||||||
|
margin-top: 10rpx;
|
||||||
|
text-align: left;
|
||||||
|
color: #45624b;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 14rpx;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel__actions--triple .info-panel__action {
|
||||||
|
font-size: 23rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel__action {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: #d7e8da;
|
||||||
|
color: #163020;
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel__action--primary {
|
||||||
|
background: #2d6a4f;
|
||||||
|
color: #f7fbf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel__action--secondary {
|
||||||
|
background: #eef6ea;
|
||||||
|
color: #45624b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel__action--active {
|
||||||
|
background: #2d6a4f;
|
||||||
|
color: #f7fbf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 14rpx;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-row--triple .control-chip {
|
||||||
|
font-size: 23rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-chip {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 20rpx 16rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: #d7e8da;
|
||||||
|
color: #163020;
|
||||||
|
font-size: 26rpx;
|
||||||
|
text-align: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-chip--primary {
|
||||||
|
background: #2d6a4f;
|
||||||
|
color: #f7fbf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-chip--secondary {
|
||||||
|
background: #eef6ea;
|
||||||
|
color: #45624b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-chip--active {
|
||||||
|
background: #2d6a4f;
|
||||||
|
color: #f7fbf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
61
miniprogram/utils/projection.ts
Normal file
61
miniprogram/utils/projection.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
export interface LonLatPoint {
|
||||||
|
lon: number
|
||||||
|
lat: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WebMercatorPoint {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorldTilePoint {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_LATITUDE = 85.05112878
|
||||||
|
const EARTH_RADIUS = 6378137
|
||||||
|
|
||||||
|
function clampLatitude(lat: number): number {
|
||||||
|
return Math.max(-MAX_LATITUDE, Math.min(MAX_LATITUDE, lat))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lonLatToWebMercator(point: LonLatPoint): WebMercatorPoint {
|
||||||
|
const latitude = clampLatitude(point.lat)
|
||||||
|
const lonRad = point.lon * Math.PI / 180
|
||||||
|
const latRad = latitude * Math.PI / 180
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: EARTH_RADIUS * lonRad,
|
||||||
|
y: EARTH_RADIUS * Math.log(Math.tan(Math.PI / 4 + latRad / 2)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function webMercatorToLonLat(point: WebMercatorPoint): LonLatPoint {
|
||||||
|
return {
|
||||||
|
lon: point.x / EARTH_RADIUS * 180 / Math.PI,
|
||||||
|
lat: (2 * Math.atan(Math.exp(point.y / EARTH_RADIUS)) - Math.PI / 2) * 180 / Math.PI,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lonLatToWorldTile(point: LonLatPoint, zoom: number): WorldTilePoint {
|
||||||
|
const latitude = clampLatitude(point.lat)
|
||||||
|
const scale = Math.pow(2, zoom)
|
||||||
|
const latRad = latitude * Math.PI / 180
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: (point.lon + 180) / 360 * scale,
|
||||||
|
y: (1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2 * scale,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function worldTileToLonLat(point: WorldTilePoint, zoom: number): LonLatPoint {
|
||||||
|
const scale = Math.pow(2, zoom)
|
||||||
|
const lon = point.x / scale * 360 - 180
|
||||||
|
const latRad = Math.atan(Math.sinh(Math.PI * (1 - 2 * point.y / scale)))
|
||||||
|
|
||||||
|
return {
|
||||||
|
lon,
|
||||||
|
lat: latRad * 180 / Math.PI,
|
||||||
|
}
|
||||||
|
}
|
||||||
61
miniprogram/utils/tile.ts
Normal file
61
miniprogram/utils/tile.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
export interface TileItem {
|
||||||
|
key: string
|
||||||
|
url: string
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
leftPx: number
|
||||||
|
topPx: number
|
||||||
|
sizePx: number
|
||||||
|
isCenter: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const TILE_OVERLAP_PX = 2
|
||||||
|
|
||||||
|
export interface TileGridOptions {
|
||||||
|
urlTemplate: string
|
||||||
|
zoom: number
|
||||||
|
centerTileX: number
|
||||||
|
centerTileY: number
|
||||||
|
viewportWidth: number
|
||||||
|
viewportHeight: number
|
||||||
|
tileSize: number
|
||||||
|
overdraw: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildTileUrl(template: string, z: number, x: number, y: number): string {
|
||||||
|
return template
|
||||||
|
.replace('{z}', String(z))
|
||||||
|
.replace('{x}', String(x))
|
||||||
|
.replace('{y}', String(y))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTileGrid(options: TileGridOptions): TileItem[] {
|
||||||
|
const tiles: TileItem[] = []
|
||||||
|
const halfWidth = options.viewportWidth / 2
|
||||||
|
const halfHeight = options.viewportHeight / 2
|
||||||
|
const horizontalRange = Math.ceil(halfWidth / options.tileSize) + options.overdraw
|
||||||
|
const verticalRange = Math.ceil(halfHeight / options.tileSize) + options.overdraw
|
||||||
|
|
||||||
|
for (let dy = -verticalRange; dy <= verticalRange; dy += 1) {
|
||||||
|
for (let dx = -horizontalRange; dx <= horizontalRange; dx += 1) {
|
||||||
|
const x = options.centerTileX + dx
|
||||||
|
const y = options.centerTileY + dy
|
||||||
|
|
||||||
|
const rawLeft = halfWidth + (dx - 0.5) * options.tileSize
|
||||||
|
const rawTop = halfHeight + (dy - 0.5) * options.tileSize
|
||||||
|
|
||||||
|
tiles.push({
|
||||||
|
key: `${options.zoom}-${x}-${y}`,
|
||||||
|
url: buildTileUrl(options.urlTemplate, options.zoom, x, y),
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
leftPx: Math.floor(rawLeft - TILE_OVERLAP_PX / 2),
|
||||||
|
topPx: Math.floor(rawTop - TILE_OVERLAP_PX / 2),
|
||||||
|
sizePx: Math.ceil(options.tileSize) + TILE_OVERLAP_PX,
|
||||||
|
isCenter: dx === 0 && dy === 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tiles
|
||||||
|
}
|
||||||
19
miniprogram/utils/util.ts
Normal file
19
miniprogram/utils/util.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export const formatTime = (date: Date) => {
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = date.getMonth() + 1
|
||||||
|
const day = date.getDate()
|
||||||
|
const hour = date.getHours()
|
||||||
|
const minute = date.getMinutes()
|
||||||
|
const second = date.getSeconds()
|
||||||
|
|
||||||
|
return (
|
||||||
|
[year, month, day].map(formatNumber).join('/') +
|
||||||
|
' ' +
|
||||||
|
[hour, minute, second].map(formatNumber).join(':')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatNumber = (n: number) => {
|
||||||
|
const s = n.toString()
|
||||||
|
return s[1] ? s : '0' + s
|
||||||
|
}
|
||||||
37
package-lock.json
generated
Normal file
37
package-lock.json
generated
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "miniprogram-ts-quickstart",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "miniprogram-ts-quickstart",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"miniprogram-api-typings": "^2.8.3-1",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/miniprogram-api-typings": {
|
||||||
|
"version": "2.12.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/miniprogram-api-typings/-/miniprogram-api-typings-2.12.0.tgz",
|
||||||
|
"integrity": "sha512-ibvbqeslVFur0IAvTxLMvsbtvVcMo6gwvOnj0YZHV7aeDLu091VQRrETT2QuiG9P6aZWRcxeNGJChRKVPCp9VQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
package.json
Normal file
16
package.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "miniprogram-ts-quickstart",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"scripts": {
|
||||||
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
||||||
|
"typecheck:watch": "tsc --noEmit -p tsconfig.json --watch"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "",
|
||||||
|
"devDependencies": {
|
||||||
|
"miniprogram-api-typings": "^2.8.3-1",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
50
project.config.json
Normal file
50
project.config.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"description": "项目配置文件",
|
||||||
|
"miniprogramRoot": "miniprogram/",
|
||||||
|
"compileType": "miniprogram",
|
||||||
|
"setting": {
|
||||||
|
"useCompilerPlugins": [
|
||||||
|
"typescript"
|
||||||
|
],
|
||||||
|
"babelSetting": {
|
||||||
|
"ignore": [],
|
||||||
|
"disablePlugins": [],
|
||||||
|
"outputPath": ""
|
||||||
|
},
|
||||||
|
"coverView": false,
|
||||||
|
"postcss": false,
|
||||||
|
"minified": false,
|
||||||
|
"enhance": false,
|
||||||
|
"showShadowRootInWxmlPanel": false,
|
||||||
|
"packNpmRelationList": [],
|
||||||
|
"ignoreUploadUnusedFiles": true,
|
||||||
|
"compileHotReLoad": false,
|
||||||
|
"skylineRenderEnable": true,
|
||||||
|
"es6": false,
|
||||||
|
"compileWorklet": false,
|
||||||
|
"uglifyFileName": false,
|
||||||
|
"uploadWithSourceMap": true,
|
||||||
|
"packNpmManually": false,
|
||||||
|
"minifyWXSS": true,
|
||||||
|
"minifyWXML": true,
|
||||||
|
"localPlugins": false,
|
||||||
|
"condition": false,
|
||||||
|
"swc": false,
|
||||||
|
"disableSWC": true,
|
||||||
|
"disableUseStrict": false
|
||||||
|
},
|
||||||
|
"simulatorType": "wechat",
|
||||||
|
"simulatorPluginLibVersion": {},
|
||||||
|
"condition": {},
|
||||||
|
"srcMiniprogramRoot": "miniprogram/",
|
||||||
|
"editorSetting": {
|
||||||
|
"tabIndent": "insertSpaces",
|
||||||
|
"tabSize": 2
|
||||||
|
},
|
||||||
|
"libVersion": "trial",
|
||||||
|
"packOptions": {
|
||||||
|
"ignore": [],
|
||||||
|
"include": []
|
||||||
|
},
|
||||||
|
"appid": "wx9d42aa29805ded5d"
|
||||||
|
}
|
||||||
207
readme.md
Normal file
207
readme.md
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
# CMR Mini Program(彩图奔跑小程序)
|
||||||
|
|
||||||
|
## 📌 项目简介
|
||||||
|
|
||||||
|
CMR Mini Program 是一个基于微信小程序的定向运动平台,支持:
|
||||||
|
|
||||||
|
- 自定义地图(非第三方地图)
|
||||||
|
- 基于瓦片(XYZ)渲染
|
||||||
|
- TFW / GIS 数据定位
|
||||||
|
- 控制点(打卡点)与路线系统
|
||||||
|
- 适用于赛事、训练、互动地图等场景
|
||||||
|
|
||||||
|
本项目核心目标:
|
||||||
|
|
||||||
|
> 构建一套完全自研的「轻量级地图引擎 + 定向运动系统」
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧠 技术架构
|
||||||
|
|
||||||
|
```text
|
||||||
|
GeoTIFF + TFW
|
||||||
|
↓
|
||||||
|
QGIS 处理
|
||||||
|
↓
|
||||||
|
XYZ 瓦片(3857)
|
||||||
|
↓
|
||||||
|
小程序地图引擎
|
||||||
|
↓
|
||||||
|
控制点 / 路线 / 游戏逻辑
|
||||||
|
🗺️ 地图系统设计
|
||||||
|
坐标体系
|
||||||
|
类型 说明
|
||||||
|
EPSG:4326 经纬度(WGS84)
|
||||||
|
EPSG:3857 Web Mercator(瓦片使用)
|
||||||
|
瓦片规则
|
||||||
|
/{z}/{x}/{y}.png
|
||||||
|
|
||||||
|
标准 XYZ 瓦片
|
||||||
|
|
||||||
|
支持本地或服务器部署
|
||||||
|
|
||||||
|
支持自定义地图(非在线地图)
|
||||||
|
|
||||||
|
TFW 支持
|
||||||
|
|
||||||
|
支持读取:
|
||||||
|
|
||||||
|
meta.tfw
|
||||||
|
|
||||||
|
TFW 六参数:
|
||||||
|
|
||||||
|
A 像素宽度
|
||||||
|
D 旋转
|
||||||
|
B 旋转
|
||||||
|
E 像素高度(负)
|
||||||
|
C 左上角X
|
||||||
|
F 左上角Y
|
||||||
|
|
||||||
|
自动识别:
|
||||||
|
|
||||||
|
✅ 经纬度(EPSG:4326)
|
||||||
|
|
||||||
|
✅ 米制坐标(EPSG:3857)
|
||||||
|
|
||||||
|
并转换为地图中心点。
|
||||||
|
|
||||||
|
🏗️ 项目结构
|
||||||
|
cmr-miniprogram/
|
||||||
|
│
|
||||||
|
├── app.js # 全局逻辑
|
||||||
|
├── app.json # 全局配置
|
||||||
|
├── app.wxss # 全局样式
|
||||||
|
├── project.config.json # 项目配置
|
||||||
|
├── sitemap.json # 索引配置
|
||||||
|
│
|
||||||
|
├── pages/
|
||||||
|
│ ├── index/ # 首页
|
||||||
|
│ └── map/ # 地图页(核心)
|
||||||
|
│
|
||||||
|
├── components/ # 公共组件
|
||||||
|
│
|
||||||
|
├── utils/
|
||||||
|
│ ├── projection.js # 坐标转换
|
||||||
|
│ ├── tile.js # 瓦片计算
|
||||||
|
│ ├── tfw.js # TFW解析
|
||||||
|
│
|
||||||
|
├── engine/ # 地图引擎(核心)
|
||||||
|
│ ├── renderer/ # 渲染
|
||||||
|
│ ├── gesture/ # 手势处理
|
||||||
|
│ ├── camera/ # 视图控制
|
||||||
|
│ ├── overlay/ # 覆盖物(控制点等)
|
||||||
|
│
|
||||||
|
└── assets/
|
||||||
|
|
||||||
|
👉 小程序项目结构由 app.js / app.json / pages 等组成,是官方标准结构
|
||||||
|
|
||||||
|
⚙️ 开发环境
|
||||||
|
必备工具
|
||||||
|
|
||||||
|
微信开发者工具
|
||||||
|
|
||||||
|
VS Code(推荐)
|
||||||
|
|
||||||
|
Node.js(LTS)
|
||||||
|
|
||||||
|
开发模式
|
||||||
|
VS Code → 写代码
|
||||||
|
微信开发者工具 → 编译 / 运行
|
||||||
|
🚀 功能模块规划
|
||||||
|
1. 地图引擎(核心)
|
||||||
|
|
||||||
|
瓦片加载(XYZ)
|
||||||
|
|
||||||
|
经纬度 ↔ 屏幕坐标转换
|
||||||
|
|
||||||
|
拖拽 / 缩放
|
||||||
|
|
||||||
|
相机系统(center / zoom)
|
||||||
|
|
||||||
|
2. 数据层
|
||||||
|
|
||||||
|
TFW 解析
|
||||||
|
|
||||||
|
meta.json(可选)
|
||||||
|
|
||||||
|
控制点 JSON
|
||||||
|
|
||||||
|
3. 覆盖物系统
|
||||||
|
|
||||||
|
控制点(打卡点)
|
||||||
|
|
||||||
|
路线绘制
|
||||||
|
|
||||||
|
编号 / 标记
|
||||||
|
|
||||||
|
高亮 / 状态变化
|
||||||
|
|
||||||
|
4. 交互系统
|
||||||
|
|
||||||
|
点击点位
|
||||||
|
|
||||||
|
GPS 定位
|
||||||
|
|
||||||
|
判定是否到达控制点
|
||||||
|
|
||||||
|
5. 游戏逻辑
|
||||||
|
|
||||||
|
打卡顺序
|
||||||
|
|
||||||
|
计时
|
||||||
|
|
||||||
|
成绩记录
|
||||||
|
|
||||||
|
NPC / 问答扩展
|
||||||
|
|
||||||
|
📍 当前进度
|
||||||
|
|
||||||
|
✅ QGIS 瓦片生成
|
||||||
|
|
||||||
|
✅ XYZ 瓦片加载
|
||||||
|
|
||||||
|
✅ TFW 自动定位(支持 4326 / 3857)
|
||||||
|
|
||||||
|
✅ 浏览器调试页
|
||||||
|
|
||||||
|
🚧 小程序地图引擎开发中
|
||||||
|
|
||||||
|
🧩 后续计划
|
||||||
|
|
||||||
|
地图引擎小程序版(Canvas / View)
|
||||||
|
|
||||||
|
控制点系统
|
||||||
|
|
||||||
|
路线规划
|
||||||
|
|
||||||
|
离线地图支持
|
||||||
|
|
||||||
|
B端赛事管理系统
|
||||||
|
|
||||||
|
Web + 小程序统一架构
|
||||||
|
|
||||||
|
💡 核心设计理念
|
||||||
|
|
||||||
|
不依赖第三方地图(如高德/腾讯)
|
||||||
|
|
||||||
|
全部地图数据可控
|
||||||
|
|
||||||
|
支持离线 / 私有部署
|
||||||
|
|
||||||
|
面向定向运动深度定制
|
||||||
|
|
||||||
|
📌 适用场景
|
||||||
|
|
||||||
|
定向运动赛事
|
||||||
|
|
||||||
|
校园活动
|
||||||
|
|
||||||
|
城市探索游戏
|
||||||
|
|
||||||
|
户外训练
|
||||||
|
|
||||||
|
教育互动地图
|
||||||
|
|
||||||
|
🧠 一句话总结
|
||||||
|
|
||||||
|
这是一个“自研地图引擎 + 定向运动系统”的小程序项目,而不是普通地图应用。
|
||||||
33
tsconfig.json
Normal file
33
tsconfig.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"module": "CommonJS",
|
||||||
|
"target": "ES2020",
|
||||||
|
"allowJs": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"alwaysStrict": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"strict": true,
|
||||||
|
"strictPropertyInitialization": true,
|
||||||
|
"lib": [
|
||||||
|
"ES2020"
|
||||||
|
],
|
||||||
|
"typeRoots": [
|
||||||
|
"./typings"
|
||||||
|
],
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
8
typings/index.d.ts
vendored
Normal file
8
typings/index.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/// <reference path="./types/index.d.ts" />
|
||||||
|
|
||||||
|
interface IAppOption {
|
||||||
|
globalData: {
|
||||||
|
userInfo?: WechatMiniprogram.UserInfo,
|
||||||
|
}
|
||||||
|
userInfoReadyCallback?: WechatMiniprogram.GetUserInfoSuccessCallback,
|
||||||
|
}
|
||||||
1
typings/types/index.d.ts
vendored
Normal file
1
typings/types/index.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference path="./wx/index.d.ts" />
|
||||||
74
typings/types/wx/index.d.ts
vendored
Normal file
74
typings/types/wx/index.d.ts
vendored
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
/*! *****************************************************************************
|
||||||
|
Copyright (c) 2021 Tencent, Inc. All rights reserved.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
***************************************************************************** */
|
||||||
|
|
||||||
|
/// <reference path="./lib.wx.app.d.ts" />
|
||||||
|
/// <reference path="./lib.wx.page.d.ts" />
|
||||||
|
/// <reference path="./lib.wx.api.d.ts" />
|
||||||
|
/// <reference path="./lib.wx.cloud.d.ts" />
|
||||||
|
/// <reference path="./lib.wx.component.d.ts" />
|
||||||
|
/// <reference path="./lib.wx.behavior.d.ts" />
|
||||||
|
/// <reference path="./lib.wx.event.d.ts" />
|
||||||
|
|
||||||
|
declare namespace WechatMiniprogram {
|
||||||
|
type IAnyObject = Record<string, any>
|
||||||
|
type Optional<F> = F extends (arg: infer P) => infer R ? (arg?: P) => R : F
|
||||||
|
type OptionalInterface<T> = { [K in keyof T]: Optional<T[K]> }
|
||||||
|
interface AsyncMethodOptionLike {
|
||||||
|
success?: (...args: any[]) => void
|
||||||
|
}
|
||||||
|
type PromisifySuccessResult<
|
||||||
|
P,
|
||||||
|
T extends AsyncMethodOptionLike
|
||||||
|
> = P extends { success: any }
|
||||||
|
? void
|
||||||
|
: P extends { fail: any }
|
||||||
|
? void
|
||||||
|
: P extends { complete: any }
|
||||||
|
? void
|
||||||
|
: Promise<Parameters<Exclude<T['success'], undefined>>[0]>
|
||||||
|
}
|
||||||
|
|
||||||
|
declare const console: WechatMiniprogram.Console
|
||||||
|
declare const wx: WechatMiniprogram.Wx
|
||||||
|
/** 引入模块。返回模块通过 `module.exports` 或 `exports` 暴露的接口。 */
|
||||||
|
declare function require(
|
||||||
|
/** 需要引入模块文件相对于当前文件的相对路径,或 npm 模块名,或 npm 模块路径。不支持绝对路径 */
|
||||||
|
module: string
|
||||||
|
): any
|
||||||
|
/** 引入插件。返回插件通过 `main` 暴露的接口。 */
|
||||||
|
declare function requirePlugin(
|
||||||
|
/** 需要引入的插件的 alias */
|
||||||
|
module: string
|
||||||
|
): any
|
||||||
|
/** 插件引入当前使用者小程序。返回使用者小程序通过 [插件配置中 `export` 暴露的接口](https://developers.weixin.qq.com/miniprogram/dev/framework/plugin/using.html#%E5%AF%BC%E5%87%BA%E5%88%B0%E6%8F%92%E4%BB%B6)。
|
||||||
|
*
|
||||||
|
* 该接口只在插件中存在
|
||||||
|
*
|
||||||
|
* 最低基础库: `2.11.1` */
|
||||||
|
declare function requireMiniProgram(): any
|
||||||
|
/** 当前模块对象 */
|
||||||
|
declare let module: {
|
||||||
|
/** 模块向外暴露的对象,使用 `require` 引用该模块时可以获取 */
|
||||||
|
exports: any
|
||||||
|
}
|
||||||
|
/** `module.exports` 的引用 */
|
||||||
|
declare let exports: any
|
||||||
19671
typings/types/wx/lib.wx.api.d.ts
vendored
Normal file
19671
typings/types/wx/lib.wx.api.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
270
typings/types/wx/lib.wx.app.d.ts
vendored
Normal file
270
typings/types/wx/lib.wx.app.d.ts
vendored
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
/*! *****************************************************************************
|
||||||
|
Copyright (c) 2021 Tencent, Inc. All rights reserved.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
***************************************************************************** */
|
||||||
|
|
||||||
|
declare namespace WechatMiniprogram.App {
|
||||||
|
interface ReferrerInfo {
|
||||||
|
/** 来源小程序或公众号或App的 appId
|
||||||
|
*
|
||||||
|
* 以下场景支持返回 referrerInfo.appId:
|
||||||
|
* - 1020(公众号 profile 页相关小程序列表): appId
|
||||||
|
* - 1035(公众号自定义菜单):来源公众号 appId
|
||||||
|
* - 1036(App 分享消息卡片):来源应用 appId
|
||||||
|
* - 1037(小程序打开小程序):来源小程序 appId
|
||||||
|
* - 1038(从另一个小程序返回):来源小程序 appId
|
||||||
|
* - 1043(公众号模板消息):来源公众号 appId
|
||||||
|
*/
|
||||||
|
appId: string
|
||||||
|
/** 来源小程序传过来的数据,scene=1037或1038时支持 */
|
||||||
|
extraData?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
type SceneValues =
|
||||||
|
| 1001
|
||||||
|
| 1005
|
||||||
|
| 1006
|
||||||
|
| 1007
|
||||||
|
| 1008
|
||||||
|
| 1011
|
||||||
|
| 1012
|
||||||
|
| 1013
|
||||||
|
| 1014
|
||||||
|
| 1017
|
||||||
|
| 1019
|
||||||
|
| 1020
|
||||||
|
| 1023
|
||||||
|
| 1024
|
||||||
|
| 1025
|
||||||
|
| 1026
|
||||||
|
| 1027
|
||||||
|
| 1028
|
||||||
|
| 1029
|
||||||
|
| 1030
|
||||||
|
| 1031
|
||||||
|
| 1032
|
||||||
|
| 1034
|
||||||
|
| 1035
|
||||||
|
| 1036
|
||||||
|
| 1037
|
||||||
|
| 1038
|
||||||
|
| 1039
|
||||||
|
| 1042
|
||||||
|
| 1043
|
||||||
|
| 1044
|
||||||
|
| 1045
|
||||||
|
| 1046
|
||||||
|
| 1047
|
||||||
|
| 1048
|
||||||
|
| 1049
|
||||||
|
| 1052
|
||||||
|
| 1053
|
||||||
|
| 1056
|
||||||
|
| 1057
|
||||||
|
| 1058
|
||||||
|
| 1059
|
||||||
|
| 1064
|
||||||
|
| 1067
|
||||||
|
| 1069
|
||||||
|
| 1071
|
||||||
|
| 1072
|
||||||
|
| 1073
|
||||||
|
| 1074
|
||||||
|
| 1077
|
||||||
|
| 1078
|
||||||
|
| 1079
|
||||||
|
| 1081
|
||||||
|
| 1082
|
||||||
|
| 1084
|
||||||
|
| 1089
|
||||||
|
| 1090
|
||||||
|
| 1091
|
||||||
|
| 1092
|
||||||
|
| 1095
|
||||||
|
| 1096
|
||||||
|
| 1097
|
||||||
|
| 1099
|
||||||
|
| 1102
|
||||||
|
| 1124
|
||||||
|
| 1125
|
||||||
|
| 1126
|
||||||
|
| 1129
|
||||||
|
|
||||||
|
interface LaunchShowOption {
|
||||||
|
/** 打开小程序的路径 */
|
||||||
|
path: string
|
||||||
|
/** 打开小程序的query */
|
||||||
|
query: IAnyObject
|
||||||
|
/** 打开小程序的场景值
|
||||||
|
* - 1001:发现栏小程序主入口,「最近使用」列表(基础库2.2.4版本起包含「我的小程序」列表)
|
||||||
|
* - 1005:微信首页顶部搜索框的搜索结果页
|
||||||
|
* - 1006:发现栏小程序主入口搜索框的搜索结果页
|
||||||
|
* - 1007:单人聊天会话中的小程序消息卡片
|
||||||
|
* - 1008:群聊会话中的小程序消息卡片
|
||||||
|
* - 1011:扫描二维码
|
||||||
|
* - 1012:长按图片识别二维码
|
||||||
|
* - 1013:扫描手机相册中选取的二维码
|
||||||
|
* - 1014:小程序模板消息
|
||||||
|
* - 1017:前往小程序体验版的入口页
|
||||||
|
* - 1019:微信钱包(微信客户端7.0.0版本改为支付入口)
|
||||||
|
* - 1020:公众号 profile 页相关小程序列表
|
||||||
|
* - 1023:安卓系统桌面图标
|
||||||
|
* - 1024:小程序 profile 页
|
||||||
|
* - 1025:扫描一维码
|
||||||
|
* - 1026:发现栏小程序主入口,「附近的小程序」列表
|
||||||
|
* - 1027:微信首页顶部搜索框搜索结果页「使用过的小程序」列表
|
||||||
|
* - 1028:我的卡包
|
||||||
|
* - 1029:小程序中的卡券详情页
|
||||||
|
* - 1030:自动化测试下打开小程序
|
||||||
|
* - 1031:长按图片识别一维码
|
||||||
|
* - 1032:扫描手机相册中选取的一维码
|
||||||
|
* - 1034:微信支付完成页
|
||||||
|
* - 1035:公众号自定义菜单
|
||||||
|
* - 1036:App 分享消息卡片
|
||||||
|
* - 1037:小程序打开小程序
|
||||||
|
* - 1038:从另一个小程序返回
|
||||||
|
* - 1039:摇电视
|
||||||
|
* - 1042:添加好友搜索框的搜索结果页
|
||||||
|
* - 1043:公众号模板消息
|
||||||
|
* - 1044:带 shareTicket 的小程序消息卡片 [详情](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/share.html)
|
||||||
|
* - 1045:朋友圈广告
|
||||||
|
* - 1046:朋友圈广告详情页
|
||||||
|
* - 1047:扫描小程序码
|
||||||
|
* - 1048:长按图片识别小程序码
|
||||||
|
* - 1049:扫描手机相册中选取的小程序码
|
||||||
|
* - 1052:卡券的适用门店列表
|
||||||
|
* - 1053:搜一搜的结果页
|
||||||
|
* - 1056:聊天顶部音乐播放器右上角菜单
|
||||||
|
* - 1057:钱包中的银行卡详情页
|
||||||
|
* - 1058:公众号文章
|
||||||
|
* - 1059:体验版小程序绑定邀请页
|
||||||
|
* - 1064:微信首页连Wi-Fi状态栏
|
||||||
|
* - 1067:公众号文章广告
|
||||||
|
* - 1069:移动应用
|
||||||
|
* - 1071:钱包中的银行卡列表页
|
||||||
|
* - 1072:二维码收款页面
|
||||||
|
* - 1073:客服消息列表下发的小程序消息卡片
|
||||||
|
* - 1074:公众号会话下发的小程序消息卡片
|
||||||
|
* - 1077:摇周边
|
||||||
|
* - 1078:微信连Wi-Fi成功提示页
|
||||||
|
* - 1079:微信游戏中心
|
||||||
|
* - 1081:客服消息下发的文字链
|
||||||
|
* - 1082:公众号会话下发的文字链
|
||||||
|
* - 1084:朋友圈广告原生页
|
||||||
|
* - 1089:微信聊天主界面下拉,「最近使用」栏(基础库2.2.4版本起包含「我的小程序」栏)
|
||||||
|
* - 1090:长按小程序右上角菜单唤出最近使用历史
|
||||||
|
* - 1091:公众号文章商品卡片
|
||||||
|
* - 1092:城市服务入口
|
||||||
|
* - 1095:小程序广告组件
|
||||||
|
* - 1096:聊天记录
|
||||||
|
* - 1097:微信支付签约页
|
||||||
|
* - 1099:页面内嵌插件
|
||||||
|
* - 1102:公众号 profile 页服务预览
|
||||||
|
* - 1124:扫“一物一码”打开小程序
|
||||||
|
* - 1125:长按图片识别“一物一码”
|
||||||
|
* - 1126:扫描手机相册中选取的“一物一码”
|
||||||
|
* - 1129:微信爬虫访问 [详情](https://developers.weixin.qq.com/miniprogram/dev/reference/configuration/sitemap.html)
|
||||||
|
*/
|
||||||
|
scene: SceneValues
|
||||||
|
/** shareTicket,详见 [获取更多转发信息]((转发#获取更多转发信息)) */
|
||||||
|
shareTicket: string
|
||||||
|
/** 当场景为由从另一个小程序或公众号或App打开时,返回此字段 */
|
||||||
|
referrerInfo?: ReferrerInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PageNotFoundOption {
|
||||||
|
/** 不存在页面的路径 */
|
||||||
|
path: string
|
||||||
|
/** 打开不存在页面的 query */
|
||||||
|
query: IAnyObject
|
||||||
|
/** 是否本次启动的首个页面(例如从分享等入口进来,首个页面是开发者配置的分享页面) */
|
||||||
|
isEntryPage: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Option {
|
||||||
|
/** 生命周期回调—监听小程序初始化
|
||||||
|
*
|
||||||
|
* 小程序初始化完成时触发,全局只触发一次。
|
||||||
|
*/
|
||||||
|
onLaunch(options: LaunchShowOption): void
|
||||||
|
/** 生命周期回调—监听小程序显示
|
||||||
|
*
|
||||||
|
* 小程序启动,或从后台进入前台显示时
|
||||||
|
*/
|
||||||
|
onShow(options: LaunchShowOption): void
|
||||||
|
/** 生命周期回调—监听小程序隐藏
|
||||||
|
*
|
||||||
|
* 小程序从前台进入后台时
|
||||||
|
*/
|
||||||
|
onHide(): void
|
||||||
|
/** 错误监听函数
|
||||||
|
*
|
||||||
|
* 小程序发生脚本错误,或者 api
|
||||||
|
*/
|
||||||
|
onError(/** 错误信息,包含堆栈 */ error: string): void
|
||||||
|
/** 页面不存在监听函数
|
||||||
|
*
|
||||||
|
* 小程序要打开的页面不存在时触发,会带上页面信息回调该函数
|
||||||
|
*
|
||||||
|
* **注意:**
|
||||||
|
* 1. 如果开发者没有添加 `onPageNotFound` 监听,当跳转页面不存在时,将推入微信客户端原生的页面不存在提示页面。
|
||||||
|
* 2. 如果 `onPageNotFound` 回调中又重定向到另一个不存在的页面,将推入微信客户端原生的页面不存在提示页面,并且不再回调 `onPageNotFound`。
|
||||||
|
*
|
||||||
|
* 最低基础库: 1.9.90
|
||||||
|
*/
|
||||||
|
onPageNotFound(options: PageNotFoundOption): void
|
||||||
|
/**
|
||||||
|
* 小程序有未处理的 Promise 拒绝时触发。也可以使用 [wx.onUnhandledRejection](https://developers.weixin.qq.com/miniprogram/dev/api/base/app/app-event/wx.onUnhandledRejection.html) 绑定监听。注意事项请参考 [wx.onUnhandledRejection](https://developers.weixin.qq.com/miniprogram/dev/api/base/app/app-event/wx.onUnhandledRejection.html)。
|
||||||
|
* **参数**:与 [wx.onUnhandledRejection](https://developers.weixin.qq.com/miniprogram/dev/api/base/app/app-event/wx.onUnhandledRejection.html) 一致
|
||||||
|
*/
|
||||||
|
onUnhandledRejection: OnUnhandledRejectionCallback
|
||||||
|
/**
|
||||||
|
* 系统切换主题时触发。也可以使用 wx.onThemeChange 绑定监听。
|
||||||
|
*
|
||||||
|
* 最低基础库: 2.11.0
|
||||||
|
*/
|
||||||
|
onThemeChange: OnThemeChangeCallback
|
||||||
|
}
|
||||||
|
|
||||||
|
type Instance<T extends IAnyObject> = Option & T
|
||||||
|
type Options<T extends IAnyObject> = Partial<Option> &
|
||||||
|
T &
|
||||||
|
ThisType<Instance<T>>
|
||||||
|
type TrivialInstance = Instance<IAnyObject>
|
||||||
|
|
||||||
|
interface Constructor {
|
||||||
|
<T extends IAnyObject>(options: Options<T>): void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetAppOption {
|
||||||
|
/** 在 `App` 未定义时返回默认实现。当App被调用时,默认实现中定义的属性会被覆盖合并到App中。一般用于独立分包
|
||||||
|
*
|
||||||
|
* 最低基础库: 2.2.4
|
||||||
|
*/
|
||||||
|
allowDefault?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetApp {
|
||||||
|
<T = IAnyObject>(opts?: GetAppOption): Instance<T>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare let App: WechatMiniprogram.App.Constructor
|
||||||
|
declare let getApp: WechatMiniprogram.App.GetApp
|
||||||
68
typings/types/wx/lib.wx.behavior.d.ts
vendored
Normal file
68
typings/types/wx/lib.wx.behavior.d.ts
vendored
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
/*! *****************************************************************************
|
||||||
|
Copyright (c) 2021 Tencent, Inc. All rights reserved.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
***************************************************************************** */
|
||||||
|
|
||||||
|
declare namespace WechatMiniprogram.Behavior {
|
||||||
|
type BehaviorIdentifier = string
|
||||||
|
type Instance<
|
||||||
|
TData extends DataOption,
|
||||||
|
TProperty extends PropertyOption,
|
||||||
|
TMethod extends MethodOption,
|
||||||
|
TCustomInstanceProperty extends IAnyObject = Record<string, never>
|
||||||
|
> = Component.Instance<TData, TProperty, TMethod, TCustomInstanceProperty>
|
||||||
|
type TrivialInstance = Instance<IAnyObject, IAnyObject, IAnyObject>
|
||||||
|
type TrivialOption = Options<IAnyObject, IAnyObject, IAnyObject>
|
||||||
|
type Options<
|
||||||
|
TData extends DataOption,
|
||||||
|
TProperty extends PropertyOption,
|
||||||
|
TMethod extends MethodOption,
|
||||||
|
TCustomInstanceProperty extends IAnyObject = Record<string, never>
|
||||||
|
> = Partial<Data<TData>> &
|
||||||
|
Partial<Property<TProperty>> &
|
||||||
|
Partial<Method<TMethod>> &
|
||||||
|
Partial<OtherOption> &
|
||||||
|
Partial<Lifetimes> &
|
||||||
|
ThisType<Instance<TData, TProperty, TMethod, TCustomInstanceProperty>>
|
||||||
|
interface Constructor {
|
||||||
|
<
|
||||||
|
TData extends DataOption,
|
||||||
|
TProperty extends PropertyOption,
|
||||||
|
TMethod extends MethodOption,
|
||||||
|
TCustomInstanceProperty extends IAnyObject = Record<string, never>
|
||||||
|
>(
|
||||||
|
options: Options<TData, TProperty, TMethod, TCustomInstanceProperty>
|
||||||
|
): BehaviorIdentifier
|
||||||
|
}
|
||||||
|
|
||||||
|
type DataOption = Component.DataOption
|
||||||
|
type PropertyOption = Component.PropertyOption
|
||||||
|
type MethodOption = Component.MethodOption
|
||||||
|
type Data<D extends DataOption> = Component.Data<D>
|
||||||
|
type Property<P extends PropertyOption> = Component.Property<P>
|
||||||
|
type Method<M extends MethodOption> = Component.Method<M>
|
||||||
|
|
||||||
|
type DefinitionFilter = Component.DefinitionFilter
|
||||||
|
type Lifetimes = Component.Lifetimes
|
||||||
|
|
||||||
|
type OtherOption = Omit<Component.OtherOption, 'options'>
|
||||||
|
}
|
||||||
|
/** 注册一个 `behavior`,接受一个 `Object` 类型的参数。*/
|
||||||
|
declare let Behavior: WechatMiniprogram.Behavior.Constructor
|
||||||
924
typings/types/wx/lib.wx.cloud.d.ts
vendored
Normal file
924
typings/types/wx/lib.wx.cloud.d.ts
vendored
Normal file
@@ -0,0 +1,924 @@
|
|||||||
|
/*! *****************************************************************************
|
||||||
|
Copyright (c) 2021 Tencent, Inc. All rights reserved.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
***************************************************************************** */
|
||||||
|
|
||||||
|
interface IAPIError {
|
||||||
|
errMsg: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IAPIParam<T = any> {
|
||||||
|
config?: ICloudConfig
|
||||||
|
success?: (res: T) => void
|
||||||
|
fail?: (err: IAPIError) => void
|
||||||
|
complete?: (val: T | IAPIError) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IAPISuccessParam {
|
||||||
|
errMsg: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type IAPICompleteParam = IAPISuccessParam | IAPIError
|
||||||
|
|
||||||
|
type IAPIFunction<T, P extends IAPIParam<T>> = (param?: P) => Promise<T>
|
||||||
|
|
||||||
|
interface IInitCloudConfig {
|
||||||
|
env?:
|
||||||
|
| string
|
||||||
|
| {
|
||||||
|
database?: string
|
||||||
|
functions?: string
|
||||||
|
storage?: string
|
||||||
|
}
|
||||||
|
traceUser?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICloudConfig {
|
||||||
|
env?: string
|
||||||
|
traceUser?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IICloudAPI {
|
||||||
|
init: (config?: IInitCloudConfig) => void
|
||||||
|
[api: string]: AnyFunction | IAPIFunction<any, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICloudService {
|
||||||
|
name: string
|
||||||
|
|
||||||
|
getAPIs: () => { [name: string]: IAPIFunction<any, any> }
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICloudServices {
|
||||||
|
[serviceName: string]: ICloudService
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICloudMetaData {
|
||||||
|
session_id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
declare class InternalSymbol {}
|
||||||
|
|
||||||
|
interface AnyObject {
|
||||||
|
[x: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnyArray = any[]
|
||||||
|
|
||||||
|
type AnyFunction = (...args: any[]) => any
|
||||||
|
|
||||||
|
/**
|
||||||
|
* extend wx with cloud
|
||||||
|
*/
|
||||||
|
interface WxCloud {
|
||||||
|
init: (config?: ICloudConfig) => void
|
||||||
|
|
||||||
|
callFunction(param: OQ<ICloud.CallFunctionParam>): void
|
||||||
|
callFunction(
|
||||||
|
param: RQ<ICloud.CallFunctionParam>
|
||||||
|
): Promise<ICloud.CallFunctionResult>
|
||||||
|
|
||||||
|
uploadFile(param: OQ<ICloud.UploadFileParam>): WechatMiniprogram.UploadTask
|
||||||
|
uploadFile(
|
||||||
|
param: RQ<ICloud.UploadFileParam>
|
||||||
|
): Promise<ICloud.UploadFileResult>
|
||||||
|
|
||||||
|
downloadFile(
|
||||||
|
param: OQ<ICloud.DownloadFileParam>
|
||||||
|
): WechatMiniprogram.DownloadTask
|
||||||
|
downloadFile(
|
||||||
|
param: RQ<ICloud.DownloadFileParam>
|
||||||
|
): Promise<ICloud.DownloadFileResult>
|
||||||
|
|
||||||
|
getTempFileURL(param: OQ<ICloud.GetTempFileURLParam>): void
|
||||||
|
getTempFileURL(
|
||||||
|
param: RQ<ICloud.GetTempFileURLParam>
|
||||||
|
): Promise<ICloud.GetTempFileURLResult>
|
||||||
|
|
||||||
|
deleteFile(param: OQ<ICloud.DeleteFileParam>): void
|
||||||
|
deleteFile(
|
||||||
|
param: RQ<ICloud.DeleteFileParam>
|
||||||
|
): Promise<ICloud.DeleteFileResult>
|
||||||
|
|
||||||
|
database: (config?: ICloudConfig) => DB.Database
|
||||||
|
|
||||||
|
CloudID: ICloud.ICloudIDConstructor
|
||||||
|
CDN: ICloud.ICDNConstructor
|
||||||
|
}
|
||||||
|
|
||||||
|
declare namespace ICloud {
|
||||||
|
interface ICloudAPIParam<T = any> extends IAPIParam<T> {
|
||||||
|
config?: ICloudConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// === API: callFunction ===
|
||||||
|
type CallFunctionData = AnyObject
|
||||||
|
|
||||||
|
interface CallFunctionResult extends IAPISuccessParam {
|
||||||
|
result: AnyObject | string | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CallFunctionParam extends ICloudAPIParam<CallFunctionResult> {
|
||||||
|
name: string
|
||||||
|
data?: CallFunctionData
|
||||||
|
slow?: boolean
|
||||||
|
}
|
||||||
|
// === end ===
|
||||||
|
|
||||||
|
// === API: uploadFile ===
|
||||||
|
interface UploadFileResult extends IAPISuccessParam {
|
||||||
|
fileID: string
|
||||||
|
statusCode: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UploadFileParam extends ICloudAPIParam<UploadFileResult> {
|
||||||
|
cloudPath: string
|
||||||
|
filePath: string
|
||||||
|
header?: AnyObject
|
||||||
|
}
|
||||||
|
// === end ===
|
||||||
|
|
||||||
|
// === API: downloadFile ===
|
||||||
|
interface DownloadFileResult extends IAPISuccessParam {
|
||||||
|
tempFilePath: string
|
||||||
|
statusCode: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DownloadFileParam extends ICloudAPIParam<DownloadFileResult> {
|
||||||
|
fileID: string
|
||||||
|
cloudPath?: string
|
||||||
|
}
|
||||||
|
// === end ===
|
||||||
|
|
||||||
|
// === API: getTempFileURL ===
|
||||||
|
interface GetTempFileURLResult extends IAPISuccessParam {
|
||||||
|
fileList: GetTempFileURLResultItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetTempFileURLResultItem {
|
||||||
|
fileID: string
|
||||||
|
tempFileURL: string
|
||||||
|
maxAge: number
|
||||||
|
status: number
|
||||||
|
errMsg: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetTempFileURLParam extends ICloudAPIParam<GetTempFileURLResult> {
|
||||||
|
fileList: string[]
|
||||||
|
}
|
||||||
|
// === end ===
|
||||||
|
|
||||||
|
// === API: deleteFile ===
|
||||||
|
interface DeleteFileResult extends IAPISuccessParam {
|
||||||
|
fileList: DeleteFileResultItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteFileResultItem {
|
||||||
|
fileID: string
|
||||||
|
status: number
|
||||||
|
errMsg: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteFileParam extends ICloudAPIParam<DeleteFileResult> {
|
||||||
|
fileList: string[]
|
||||||
|
}
|
||||||
|
// === end ===
|
||||||
|
|
||||||
|
// === API: CloudID ===
|
||||||
|
abstract class CloudID {
|
||||||
|
constructor(cloudID: string)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICloudIDConstructor {
|
||||||
|
new (cloudId: string): CloudID
|
||||||
|
(cloudId: string): CloudID
|
||||||
|
}
|
||||||
|
// === end ===
|
||||||
|
|
||||||
|
// === API: CDN ===
|
||||||
|
abstract class CDN {
|
||||||
|
target: string | ArrayBuffer | ICDNFilePathSpec
|
||||||
|
constructor(target: string | ArrayBuffer | ICDNFilePathSpec)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICDNFilePathSpec {
|
||||||
|
type: 'filePath'
|
||||||
|
filePath: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICDNConstructor {
|
||||||
|
new (options: string | ArrayBuffer | ICDNFilePathSpec): CDN
|
||||||
|
(options: string | ArrayBuffer | ICDNFilePathSpec): CDN
|
||||||
|
}
|
||||||
|
// === end ===
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Database ===
|
||||||
|
declare namespace DB {
|
||||||
|
/**
|
||||||
|
* The class of all exposed cloud database instances
|
||||||
|
*/
|
||||||
|
class Database {
|
||||||
|
readonly config: ICloudConfig
|
||||||
|
readonly command: DatabaseCommand
|
||||||
|
readonly Geo: IGeo
|
||||||
|
readonly serverDate: () => ServerDate
|
||||||
|
readonly RegExp: IRegExpConstructor
|
||||||
|
|
||||||
|
private constructor()
|
||||||
|
|
||||||
|
collection(collectionName: string): CollectionReference
|
||||||
|
}
|
||||||
|
|
||||||
|
class CollectionReference extends Query {
|
||||||
|
readonly collectionName: string
|
||||||
|
|
||||||
|
private constructor(name: string, database: Database)
|
||||||
|
|
||||||
|
doc(docId: string | number): DocumentReference
|
||||||
|
|
||||||
|
add(options: OQ<IAddDocumentOptions>): void
|
||||||
|
add(options: RQ<IAddDocumentOptions>): Promise<IAddResult>
|
||||||
|
}
|
||||||
|
|
||||||
|
class DocumentReference {
|
||||||
|
private constructor(docId: string | number, database: Database)
|
||||||
|
|
||||||
|
field(object: Record<string, any>): this
|
||||||
|
|
||||||
|
get(options: OQ<IGetDocumentOptions>): void
|
||||||
|
get(options?: RQ<IGetDocumentOptions>): Promise<IQuerySingleResult>
|
||||||
|
|
||||||
|
set(options: OQ<ISetSingleDocumentOptions>): void
|
||||||
|
set(options?: RQ<ISetSingleDocumentOptions>): Promise<ISetResult>
|
||||||
|
|
||||||
|
update(options: OQ<IUpdateSingleDocumentOptions>): void
|
||||||
|
update(
|
||||||
|
options?: RQ<IUpdateSingleDocumentOptions>
|
||||||
|
): Promise<IUpdateResult>
|
||||||
|
|
||||||
|
remove(options: OQ<IRemoveSingleDocumentOptions>): void
|
||||||
|
remove(
|
||||||
|
options?: RQ<IRemoveSingleDocumentOptions>
|
||||||
|
): Promise<IRemoveResult>
|
||||||
|
|
||||||
|
watch(options: IWatchOptions): RealtimeListener
|
||||||
|
}
|
||||||
|
|
||||||
|
class RealtimeListener {
|
||||||
|
// "And Now His Watch Is Ended"
|
||||||
|
close: () => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
class Query {
|
||||||
|
where(condition: IQueryCondition): Query
|
||||||
|
|
||||||
|
orderBy(fieldPath: string, order: string): Query
|
||||||
|
|
||||||
|
limit(max: number): Query
|
||||||
|
|
||||||
|
skip(offset: number): Query
|
||||||
|
|
||||||
|
field(object: Record<string, any>): Query
|
||||||
|
|
||||||
|
get(options: OQ<IGetDocumentOptions>): void
|
||||||
|
get(options?: RQ<IGetDocumentOptions>): Promise<IQueryResult>
|
||||||
|
|
||||||
|
count(options: OQ<ICountDocumentOptions>): void
|
||||||
|
count(options?: RQ<ICountDocumentOptions>): Promise<ICountResult>
|
||||||
|
|
||||||
|
watch(options: IWatchOptions): RealtimeListener
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DatabaseCommand {
|
||||||
|
eq(val: any): DatabaseQueryCommand
|
||||||
|
neq(val: any): DatabaseQueryCommand
|
||||||
|
gt(val: any): DatabaseQueryCommand
|
||||||
|
gte(val: any): DatabaseQueryCommand
|
||||||
|
lt(val: any): DatabaseQueryCommand
|
||||||
|
lte(val: any): DatabaseQueryCommand
|
||||||
|
in(val: any[]): DatabaseQueryCommand
|
||||||
|
nin(val: any[]): DatabaseQueryCommand
|
||||||
|
|
||||||
|
geoNear(options: IGeoNearCommandOptions): DatabaseQueryCommand
|
||||||
|
geoWithin(options: IGeoWithinCommandOptions): DatabaseQueryCommand
|
||||||
|
geoIntersects(
|
||||||
|
options: IGeoIntersectsCommandOptions
|
||||||
|
): DatabaseQueryCommand
|
||||||
|
|
||||||
|
and(
|
||||||
|
...expressions: Array<DatabaseLogicCommand | IQueryCondition>
|
||||||
|
): DatabaseLogicCommand
|
||||||
|
or(
|
||||||
|
...expressions: Array<DatabaseLogicCommand | IQueryCondition>
|
||||||
|
): DatabaseLogicCommand
|
||||||
|
nor(
|
||||||
|
...expressions: Array<DatabaseLogicCommand | IQueryCondition>
|
||||||
|
): DatabaseLogicCommand
|
||||||
|
not(expression: DatabaseLogicCommand): DatabaseLogicCommand
|
||||||
|
|
||||||
|
exists(val: boolean): DatabaseQueryCommand
|
||||||
|
|
||||||
|
mod(divisor: number, remainder: number): DatabaseQueryCommand
|
||||||
|
|
||||||
|
all(val: any[]): DatabaseQueryCommand
|
||||||
|
elemMatch(val: any): DatabaseQueryCommand
|
||||||
|
size(val: number): DatabaseQueryCommand
|
||||||
|
|
||||||
|
set(val: any): DatabaseUpdateCommand
|
||||||
|
remove(): DatabaseUpdateCommand
|
||||||
|
inc(val: number): DatabaseUpdateCommand
|
||||||
|
mul(val: number): DatabaseUpdateCommand
|
||||||
|
min(val: number): DatabaseUpdateCommand
|
||||||
|
max(val: number): DatabaseUpdateCommand
|
||||||
|
rename(val: string): DatabaseUpdateCommand
|
||||||
|
bit(val: number): DatabaseUpdateCommand
|
||||||
|
|
||||||
|
push(...values: any[]): DatabaseUpdateCommand
|
||||||
|
pop(): DatabaseUpdateCommand
|
||||||
|
shift(): DatabaseUpdateCommand
|
||||||
|
unshift(...values: any[]): DatabaseUpdateCommand
|
||||||
|
addToSet(val: any): DatabaseUpdateCommand
|
||||||
|
pull(val: any): DatabaseUpdateCommand
|
||||||
|
pullAll(val: any): DatabaseUpdateCommand
|
||||||
|
|
||||||
|
project: {
|
||||||
|
slice(val: number | [number, number]): DatabaseProjectionCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
aggregate: {
|
||||||
|
__safe_props__?: Set<string>
|
||||||
|
|
||||||
|
abs(val: any): DatabaseAggregateCommand
|
||||||
|
add(val: any): DatabaseAggregateCommand
|
||||||
|
addToSet(val: any): DatabaseAggregateCommand
|
||||||
|
allElementsTrue(val: any): DatabaseAggregateCommand
|
||||||
|
and(val: any): DatabaseAggregateCommand
|
||||||
|
anyElementTrue(val: any): DatabaseAggregateCommand
|
||||||
|
arrayElemAt(val: any): DatabaseAggregateCommand
|
||||||
|
arrayToObject(val: any): DatabaseAggregateCommand
|
||||||
|
avg(val: any): DatabaseAggregateCommand
|
||||||
|
ceil(val: any): DatabaseAggregateCommand
|
||||||
|
cmp(val: any): DatabaseAggregateCommand
|
||||||
|
concat(val: any): DatabaseAggregateCommand
|
||||||
|
concatArrays(val: any): DatabaseAggregateCommand
|
||||||
|
cond(val: any): DatabaseAggregateCommand
|
||||||
|
convert(val: any): DatabaseAggregateCommand
|
||||||
|
dateFromParts(val: any): DatabaseAggregateCommand
|
||||||
|
dateToParts(val: any): DatabaseAggregateCommand
|
||||||
|
dateFromString(val: any): DatabaseAggregateCommand
|
||||||
|
dateToString(val: any): DatabaseAggregateCommand
|
||||||
|
dayOfMonth(val: any): DatabaseAggregateCommand
|
||||||
|
dayOfWeek(val: any): DatabaseAggregateCommand
|
||||||
|
dayOfYear(val: any): DatabaseAggregateCommand
|
||||||
|
divide(val: any): DatabaseAggregateCommand
|
||||||
|
eq(val: any): DatabaseAggregateCommand
|
||||||
|
exp(val: any): DatabaseAggregateCommand
|
||||||
|
filter(val: any): DatabaseAggregateCommand
|
||||||
|
first(val: any): DatabaseAggregateCommand
|
||||||
|
floor(val: any): DatabaseAggregateCommand
|
||||||
|
gt(val: any): DatabaseAggregateCommand
|
||||||
|
gte(val: any): DatabaseAggregateCommand
|
||||||
|
hour(val: any): DatabaseAggregateCommand
|
||||||
|
ifNull(val: any): DatabaseAggregateCommand
|
||||||
|
in(val: any): DatabaseAggregateCommand
|
||||||
|
indexOfArray(val: any): DatabaseAggregateCommand
|
||||||
|
indexOfBytes(val: any): DatabaseAggregateCommand
|
||||||
|
indexOfCP(val: any): DatabaseAggregateCommand
|
||||||
|
isArray(val: any): DatabaseAggregateCommand
|
||||||
|
isoDayOfWeek(val: any): DatabaseAggregateCommand
|
||||||
|
isoWeek(val: any): DatabaseAggregateCommand
|
||||||
|
isoWeekYear(val: any): DatabaseAggregateCommand
|
||||||
|
last(val: any): DatabaseAggregateCommand
|
||||||
|
let(val: any): DatabaseAggregateCommand
|
||||||
|
literal(val: any): DatabaseAggregateCommand
|
||||||
|
ln(val: any): DatabaseAggregateCommand
|
||||||
|
log(val: any): DatabaseAggregateCommand
|
||||||
|
log10(val: any): DatabaseAggregateCommand
|
||||||
|
lt(val: any): DatabaseAggregateCommand
|
||||||
|
lte(val: any): DatabaseAggregateCommand
|
||||||
|
ltrim(val: any): DatabaseAggregateCommand
|
||||||
|
map(val: any): DatabaseAggregateCommand
|
||||||
|
max(val: any): DatabaseAggregateCommand
|
||||||
|
mergeObjects(val: any): DatabaseAggregateCommand
|
||||||
|
meta(val: any): DatabaseAggregateCommand
|
||||||
|
min(val: any): DatabaseAggregateCommand
|
||||||
|
millisecond(val: any): DatabaseAggregateCommand
|
||||||
|
minute(val: any): DatabaseAggregateCommand
|
||||||
|
mod(val: any): DatabaseAggregateCommand
|
||||||
|
month(val: any): DatabaseAggregateCommand
|
||||||
|
multiply(val: any): DatabaseAggregateCommand
|
||||||
|
neq(val: any): DatabaseAggregateCommand
|
||||||
|
not(val: any): DatabaseAggregateCommand
|
||||||
|
objectToArray(val: any): DatabaseAggregateCommand
|
||||||
|
or(val: any): DatabaseAggregateCommand
|
||||||
|
pow(val: any): DatabaseAggregateCommand
|
||||||
|
push(val: any): DatabaseAggregateCommand
|
||||||
|
range(val: any): DatabaseAggregateCommand
|
||||||
|
reduce(val: any): DatabaseAggregateCommand
|
||||||
|
reverseArray(val: any): DatabaseAggregateCommand
|
||||||
|
rtrim(val: any): DatabaseAggregateCommand
|
||||||
|
second(val: any): DatabaseAggregateCommand
|
||||||
|
setDifference(val: any): DatabaseAggregateCommand
|
||||||
|
setEquals(val: any): DatabaseAggregateCommand
|
||||||
|
setIntersection(val: any): DatabaseAggregateCommand
|
||||||
|
setIsSubset(val: any): DatabaseAggregateCommand
|
||||||
|
setUnion(val: any): DatabaseAggregateCommand
|
||||||
|
size(val: any): DatabaseAggregateCommand
|
||||||
|
slice(val: any): DatabaseAggregateCommand
|
||||||
|
split(val: any): DatabaseAggregateCommand
|
||||||
|
sqrt(val: any): DatabaseAggregateCommand
|
||||||
|
stdDevPop(val: any): DatabaseAggregateCommand
|
||||||
|
stdDevSamp(val: any): DatabaseAggregateCommand
|
||||||
|
strcasecmp(val: any): DatabaseAggregateCommand
|
||||||
|
strLenBytes(val: any): DatabaseAggregateCommand
|
||||||
|
strLenCP(val: any): DatabaseAggregateCommand
|
||||||
|
substr(val: any): DatabaseAggregateCommand
|
||||||
|
substrBytes(val: any): DatabaseAggregateCommand
|
||||||
|
substrCP(val: any): DatabaseAggregateCommand
|
||||||
|
subtract(val: any): DatabaseAggregateCommand
|
||||||
|
sum(val: any): DatabaseAggregateCommand
|
||||||
|
switch(val: any): DatabaseAggregateCommand
|
||||||
|
toBool(val: any): DatabaseAggregateCommand
|
||||||
|
toDate(val: any): DatabaseAggregateCommand
|
||||||
|
toDecimal(val: any): DatabaseAggregateCommand
|
||||||
|
toDouble(val: any): DatabaseAggregateCommand
|
||||||
|
toInt(val: any): DatabaseAggregateCommand
|
||||||
|
toLong(val: any): DatabaseAggregateCommand
|
||||||
|
toObjectId(val: any): DatabaseAggregateCommand
|
||||||
|
toString(val: any): DatabaseAggregateCommand
|
||||||
|
toLower(val: any): DatabaseAggregateCommand
|
||||||
|
toUpper(val: any): DatabaseAggregateCommand
|
||||||
|
trim(val: any): DatabaseAggregateCommand
|
||||||
|
trunc(val: any): DatabaseAggregateCommand
|
||||||
|
type(val: any): DatabaseAggregateCommand
|
||||||
|
week(val: any): DatabaseAggregateCommand
|
||||||
|
year(val: any): DatabaseAggregateCommand
|
||||||
|
zip(val: any): DatabaseAggregateCommand
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DatabaseAggregateCommand {}
|
||||||
|
|
||||||
|
enum LOGIC_COMMANDS_LITERAL {
|
||||||
|
AND = 'and',
|
||||||
|
OR = 'or',
|
||||||
|
NOT = 'not',
|
||||||
|
NOR = 'nor'
|
||||||
|
}
|
||||||
|
|
||||||
|
class DatabaseLogicCommand {
|
||||||
|
and(...expressions: DatabaseLogicCommand[]): DatabaseLogicCommand
|
||||||
|
or(...expressions: DatabaseLogicCommand[]): DatabaseLogicCommand
|
||||||
|
nor(...expressions: DatabaseLogicCommand[]): DatabaseLogicCommand
|
||||||
|
not(expression: DatabaseLogicCommand): DatabaseLogicCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
enum QUERY_COMMANDS_LITERAL {
|
||||||
|
// comparison
|
||||||
|
EQ = 'eq',
|
||||||
|
NEQ = 'neq',
|
||||||
|
GT = 'gt',
|
||||||
|
GTE = 'gte',
|
||||||
|
LT = 'lt',
|
||||||
|
LTE = 'lte',
|
||||||
|
IN = 'in',
|
||||||
|
NIN = 'nin',
|
||||||
|
// geo
|
||||||
|
GEO_NEAR = 'geoNear',
|
||||||
|
GEO_WITHIN = 'geoWithin',
|
||||||
|
GEO_INTERSECTS = 'geoIntersects',
|
||||||
|
// element
|
||||||
|
EXISTS = 'exists',
|
||||||
|
// evaluation
|
||||||
|
MOD = 'mod',
|
||||||
|
// array
|
||||||
|
ALL = 'all',
|
||||||
|
ELEM_MATCH = 'elemMatch',
|
||||||
|
SIZE = 'size'
|
||||||
|
}
|
||||||
|
|
||||||
|
class DatabaseQueryCommand extends DatabaseLogicCommand {
|
||||||
|
eq(val: any): DatabaseLogicCommand
|
||||||
|
neq(val: any): DatabaseLogicCommand
|
||||||
|
gt(val: any): DatabaseLogicCommand
|
||||||
|
gte(val: any): DatabaseLogicCommand
|
||||||
|
lt(val: any): DatabaseLogicCommand
|
||||||
|
lte(val: any): DatabaseLogicCommand
|
||||||
|
in(val: any[]): DatabaseLogicCommand
|
||||||
|
nin(val: any[]): DatabaseLogicCommand
|
||||||
|
|
||||||
|
exists(val: boolean): DatabaseLogicCommand
|
||||||
|
|
||||||
|
mod(divisor: number, remainder: number): DatabaseLogicCommand
|
||||||
|
|
||||||
|
all(val: any[]): DatabaseLogicCommand
|
||||||
|
elemMatch(val: any): DatabaseLogicCommand
|
||||||
|
size(val: number): DatabaseLogicCommand
|
||||||
|
|
||||||
|
geoNear(options: IGeoNearCommandOptions): DatabaseLogicCommand
|
||||||
|
geoWithin(options: IGeoWithinCommandOptions): DatabaseLogicCommand
|
||||||
|
geoIntersects(
|
||||||
|
options: IGeoIntersectsCommandOptions
|
||||||
|
): DatabaseLogicCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PROJECTION_COMMANDS_LITERAL {
|
||||||
|
SLICE = 'slice'
|
||||||
|
}
|
||||||
|
|
||||||
|
class DatabaseProjectionCommand {}
|
||||||
|
|
||||||
|
enum UPDATE_COMMANDS_LITERAL {
|
||||||
|
// field
|
||||||
|
SET = 'set',
|
||||||
|
REMOVE = 'remove',
|
||||||
|
INC = 'inc',
|
||||||
|
MUL = 'mul',
|
||||||
|
MIN = 'min',
|
||||||
|
MAX = 'max',
|
||||||
|
RENAME = 'rename',
|
||||||
|
// bitwise
|
||||||
|
BIT = 'bit',
|
||||||
|
// array
|
||||||
|
PUSH = 'push',
|
||||||
|
POP = 'pop',
|
||||||
|
SHIFT = 'shift',
|
||||||
|
UNSHIFT = 'unshift',
|
||||||
|
ADD_TO_SET = 'addToSet',
|
||||||
|
PULL = 'pull',
|
||||||
|
PULL_ALL = 'pullAll'
|
||||||
|
}
|
||||||
|
|
||||||
|
class DatabaseUpdateCommand {}
|
||||||
|
|
||||||
|
class Batch {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A contract that all API provider must adhere to
|
||||||
|
*/
|
||||||
|
class APIBaseContract<
|
||||||
|
PromiseReturn,
|
||||||
|
CallbackReturn,
|
||||||
|
Param extends IAPIParam,
|
||||||
|
Context = any
|
||||||
|
> {
|
||||||
|
getContext(param: Param): Context
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In case of callback-style invocation, this function will be called
|
||||||
|
*/
|
||||||
|
getCallbackReturn(param: Param, context: Context): CallbackReturn
|
||||||
|
|
||||||
|
getFinalParam<T extends Param>(param: Param, context: Context): T
|
||||||
|
|
||||||
|
run<T extends Param>(param: T): Promise<PromiseReturn>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGeoPointConstructor {
|
||||||
|
new (longitude: number, latitide: number): GeoPoint
|
||||||
|
new (geojson: IGeoJSONPoint): GeoPoint
|
||||||
|
(longitude: number, latitide: number): GeoPoint
|
||||||
|
(geojson: IGeoJSONPoint): GeoPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGeoMultiPointConstructor {
|
||||||
|
new (points: GeoPoint[] | IGeoJSONMultiPoint): GeoMultiPoint
|
||||||
|
(points: GeoPoint[] | IGeoJSONMultiPoint): GeoMultiPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGeoLineStringConstructor {
|
||||||
|
new (points: GeoPoint[] | IGeoJSONLineString): GeoLineString
|
||||||
|
(points: GeoPoint[] | IGeoJSONLineString): GeoLineString
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGeoMultiLineStringConstructor {
|
||||||
|
new (
|
||||||
|
lineStrings: GeoLineString[] | IGeoJSONMultiLineString
|
||||||
|
): GeoMultiLineString
|
||||||
|
(
|
||||||
|
lineStrings: GeoLineString[] | IGeoJSONMultiLineString
|
||||||
|
): GeoMultiLineString
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGeoPolygonConstructor {
|
||||||
|
new (lineStrings: GeoLineString[] | IGeoJSONPolygon): GeoPolygon
|
||||||
|
(lineStrings: GeoLineString[] | IGeoJSONPolygon): GeoPolygon
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGeoMultiPolygonConstructor {
|
||||||
|
new (polygons: GeoPolygon[] | IGeoJSONMultiPolygon): GeoMultiPolygon
|
||||||
|
(polygons: GeoPolygon[] | IGeoJSONMultiPolygon): GeoMultiPolygon
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGeo {
|
||||||
|
Point: IGeoPointConstructor
|
||||||
|
MultiPoint: IGeoMultiPointConstructor
|
||||||
|
LineString: IGeoLineStringConstructor
|
||||||
|
MultiLineString: IGeoMultiLineStringConstructor
|
||||||
|
Polygon: IGeoPolygonConstructor
|
||||||
|
MultiPolygon: IGeoMultiPolygonConstructor
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGeoJSONPoint {
|
||||||
|
type: 'Point'
|
||||||
|
coordinates: [number, number]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGeoJSONMultiPoint {
|
||||||
|
type: 'MultiPoint'
|
||||||
|
coordinates: Array<[number, number]>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGeoJSONLineString {
|
||||||
|
type: 'LineString'
|
||||||
|
coordinates: Array<[number, number]>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGeoJSONMultiLineString {
|
||||||
|
type: 'MultiLineString'
|
||||||
|
coordinates: Array<Array<[number, number]>>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGeoJSONPolygon {
|
||||||
|
type: 'Polygon'
|
||||||
|
coordinates: Array<Array<[number, number]>>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGeoJSONMultiPolygon {
|
||||||
|
type: 'MultiPolygon'
|
||||||
|
coordinates: Array<Array<Array<[number, number]>>>
|
||||||
|
}
|
||||||
|
|
||||||
|
type IGeoJSONObject =
|
||||||
|
| IGeoJSONPoint
|
||||||
|
| IGeoJSONMultiPoint
|
||||||
|
| IGeoJSONLineString
|
||||||
|
| IGeoJSONMultiLineString
|
||||||
|
| IGeoJSONPolygon
|
||||||
|
| IGeoJSONMultiPolygon
|
||||||
|
|
||||||
|
abstract class GeoPoint {
|
||||||
|
longitude: number
|
||||||
|
latitude: number
|
||||||
|
|
||||||
|
constructor(longitude: number, latitude: number)
|
||||||
|
|
||||||
|
toJSON(): Record<string, any>
|
||||||
|
toString(): string
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class GeoMultiPoint {
|
||||||
|
points: GeoPoint[]
|
||||||
|
|
||||||
|
constructor(points: GeoPoint[])
|
||||||
|
|
||||||
|
toJSON(): IGeoJSONMultiPoint
|
||||||
|
toString(): string
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class GeoLineString {
|
||||||
|
points: GeoPoint[]
|
||||||
|
|
||||||
|
constructor(points: GeoPoint[])
|
||||||
|
|
||||||
|
toJSON(): IGeoJSONLineString
|
||||||
|
toString(): string
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class GeoMultiLineString {
|
||||||
|
lines: GeoLineString[]
|
||||||
|
|
||||||
|
constructor(lines: GeoLineString[])
|
||||||
|
|
||||||
|
toJSON(): IGeoJSONMultiLineString
|
||||||
|
toString(): string
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class GeoPolygon {
|
||||||
|
lines: GeoLineString[]
|
||||||
|
|
||||||
|
constructor(lines: GeoLineString[])
|
||||||
|
|
||||||
|
toJSON(): IGeoJSONPolygon
|
||||||
|
toString(): string
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class GeoMultiPolygon {
|
||||||
|
polygons: GeoPolygon[]
|
||||||
|
|
||||||
|
constructor(polygons: GeoPolygon[])
|
||||||
|
|
||||||
|
toJSON(): IGeoJSONMultiPolygon
|
||||||
|
toString(): string
|
||||||
|
}
|
||||||
|
|
||||||
|
type GeoInstance =
|
||||||
|
| GeoPoint
|
||||||
|
| GeoMultiPoint
|
||||||
|
| GeoLineString
|
||||||
|
| GeoMultiLineString
|
||||||
|
| GeoPolygon
|
||||||
|
| GeoMultiPolygon
|
||||||
|
|
||||||
|
interface IGeoNearCommandOptions {
|
||||||
|
geometry: GeoPoint
|
||||||
|
maxDistance?: number
|
||||||
|
minDistance?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGeoWithinCommandOptions {
|
||||||
|
geometry: GeoPolygon | GeoMultiPolygon
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGeoIntersectsCommandOptions {
|
||||||
|
geometry:
|
||||||
|
| GeoPoint
|
||||||
|
| GeoMultiPoint
|
||||||
|
| GeoLineString
|
||||||
|
| GeoMultiLineString
|
||||||
|
| GeoPolygon
|
||||||
|
| GeoMultiPolygon
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IServerDateOptions {
|
||||||
|
offset: number
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class ServerDate {
|
||||||
|
readonly options: IServerDateOptions
|
||||||
|
constructor(options?: IServerDateOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IRegExpOptions {
|
||||||
|
regexp: string
|
||||||
|
options?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IRegExpConstructor {
|
||||||
|
new (options: IRegExpOptions): RegExp
|
||||||
|
(options: IRegExpOptions): RegExp
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class RegExp {
|
||||||
|
readonly regexp: string
|
||||||
|
readonly options: string
|
||||||
|
constructor(options: IRegExpOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DocumentId = string | number
|
||||||
|
|
||||||
|
interface IDocumentData {
|
||||||
|
_id?: DocumentId
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
type IDBAPIParam = IAPIParam
|
||||||
|
|
||||||
|
interface IAddDocumentOptions extends IDBAPIParam {
|
||||||
|
data: IDocumentData
|
||||||
|
}
|
||||||
|
|
||||||
|
type IGetDocumentOptions = IDBAPIParam
|
||||||
|
|
||||||
|
type ICountDocumentOptions = IDBAPIParam
|
||||||
|
|
||||||
|
interface IUpdateDocumentOptions extends IDBAPIParam {
|
||||||
|
data: IUpdateCondition
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IUpdateSingleDocumentOptions extends IDBAPIParam {
|
||||||
|
data: IUpdateCondition
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ISetDocumentOptions extends IDBAPIParam {
|
||||||
|
data: IUpdateCondition
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ISetSingleDocumentOptions extends IDBAPIParam {
|
||||||
|
data: IUpdateCondition
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IRemoveDocumentOptions extends IDBAPIParam {
|
||||||
|
query: IQueryCondition
|
||||||
|
}
|
||||||
|
|
||||||
|
type IRemoveSingleDocumentOptions = IDBAPIParam
|
||||||
|
|
||||||
|
interface IWatchOptions {
|
||||||
|
// server realtime data init & change event
|
||||||
|
onChange: (snapshot: ISnapshot) => void
|
||||||
|
// error while connecting / listening
|
||||||
|
onError: (error: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ISnapshot {
|
||||||
|
id: number
|
||||||
|
docChanges: ISingleDBEvent[]
|
||||||
|
docs: Record<string, any>
|
||||||
|
type?: SnapshotType
|
||||||
|
}
|
||||||
|
|
||||||
|
type SnapshotType = 'init'
|
||||||
|
|
||||||
|
interface ISingleDBEvent {
|
||||||
|
id: number
|
||||||
|
dataType: DataType
|
||||||
|
queueType: QueueType
|
||||||
|
docId: string
|
||||||
|
doc: Record<string, any>
|
||||||
|
updatedFields?: Record<string, any>
|
||||||
|
removedFields?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type DataType = 'init' | 'update' | 'replace' | 'add' | 'remove' | 'limit'
|
||||||
|
|
||||||
|
type QueueType = 'init' | 'enqueue' | 'dequeue' | 'update'
|
||||||
|
|
||||||
|
interface IQueryCondition {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
type IStringQueryCondition = string
|
||||||
|
|
||||||
|
interface IQueryResult extends IAPISuccessParam {
|
||||||
|
data: IDocumentData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IQuerySingleResult extends IAPISuccessParam {
|
||||||
|
data: IDocumentData
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IUpdateCondition {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
type IStringUpdateCondition = string
|
||||||
|
|
||||||
|
interface IAddResult extends IAPISuccessParam {
|
||||||
|
_id: DocumentId
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IUpdateResult extends IAPISuccessParam {
|
||||||
|
stats: {
|
||||||
|
updated: number
|
||||||
|
// created: number,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ISetResult extends IAPISuccessParam {
|
||||||
|
_id: DocumentId
|
||||||
|
stats: {
|
||||||
|
updated: number
|
||||||
|
created: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IRemoveResult extends IAPISuccessParam {
|
||||||
|
stats: {
|
||||||
|
removed: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICountResult extends IAPISuccessParam {
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Optional<T> = { [K in keyof T]+?: T[K] }
|
||||||
|
|
||||||
|
type OQ<
|
||||||
|
T extends Optional<
|
||||||
|
Record<'complete' | 'success' | 'fail', (...args: any[]) => any>
|
||||||
|
>
|
||||||
|
> =
|
||||||
|
| (RQ<T> & Required<Pick<T, 'success'>>)
|
||||||
|
| (RQ<T> & Required<Pick<T, 'fail'>>)
|
||||||
|
| (RQ<T> & Required<Pick<T, 'complete'>>)
|
||||||
|
| (RQ<T> & Required<Pick<T, 'success' | 'fail'>>)
|
||||||
|
| (RQ<T> & Required<Pick<T, 'success' | 'complete'>>)
|
||||||
|
| (RQ<T> & Required<Pick<T, 'fail' | 'complete'>>)
|
||||||
|
| (RQ<T> & Required<Pick<T, 'fail' | 'complete' | 'success'>>)
|
||||||
|
|
||||||
|
type RQ<
|
||||||
|
T extends Optional<
|
||||||
|
Record<'complete' | 'success' | 'fail', (...args: any[]) => any>
|
||||||
|
>
|
||||||
|
> = Pick<T, Exclude<keyof T, 'complete' | 'success' | 'fail'>>
|
||||||
636
typings/types/wx/lib.wx.component.d.ts
vendored
Normal file
636
typings/types/wx/lib.wx.component.d.ts
vendored
Normal file
@@ -0,0 +1,636 @@
|
|||||||
|
/*! *****************************************************************************
|
||||||
|
Copyright (c) 2021 Tencent, Inc. All rights reserved.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
***************************************************************************** */
|
||||||
|
|
||||||
|
declare namespace WechatMiniprogram.Component {
|
||||||
|
type Instance<
|
||||||
|
TData extends DataOption,
|
||||||
|
TProperty extends PropertyOption,
|
||||||
|
TMethod extends Partial<MethodOption>,
|
||||||
|
TCustomInstanceProperty extends IAnyObject = {},
|
||||||
|
TIsPage extends boolean = false
|
||||||
|
> = InstanceProperties &
|
||||||
|
InstanceMethods<TData> &
|
||||||
|
TMethod &
|
||||||
|
(TIsPage extends true ? Page.ILifetime : {}) &
|
||||||
|
TCustomInstanceProperty & {
|
||||||
|
/** 组件数据,**包括内部数据和属性值** */
|
||||||
|
data: TData & PropertyOptionToData<TProperty>
|
||||||
|
/** 组件数据,**包括内部数据和属性值**(与 `data` 一致) */
|
||||||
|
properties: TData & PropertyOptionToData<TProperty>
|
||||||
|
}
|
||||||
|
type TrivialInstance = Instance<
|
||||||
|
IAnyObject,
|
||||||
|
IAnyObject,
|
||||||
|
IAnyObject,
|
||||||
|
IAnyObject
|
||||||
|
>
|
||||||
|
type TrivialOption = Options<IAnyObject, IAnyObject, IAnyObject, IAnyObject>
|
||||||
|
type Options<
|
||||||
|
TData extends DataOption,
|
||||||
|
TProperty extends PropertyOption,
|
||||||
|
TMethod extends MethodOption,
|
||||||
|
TCustomInstanceProperty extends IAnyObject = {},
|
||||||
|
TIsPage extends boolean = false
|
||||||
|
> = Partial<Data<TData>> &
|
||||||
|
Partial<Property<TProperty>> &
|
||||||
|
Partial<Method<TMethod, TIsPage>> &
|
||||||
|
Partial<OtherOption> &
|
||||||
|
Partial<Lifetimes> &
|
||||||
|
ThisType<
|
||||||
|
Instance<
|
||||||
|
TData,
|
||||||
|
TProperty,
|
||||||
|
TMethod,
|
||||||
|
TCustomInstanceProperty,
|
||||||
|
TIsPage
|
||||||
|
>
|
||||||
|
>
|
||||||
|
interface Constructor {
|
||||||
|
<
|
||||||
|
TData extends DataOption,
|
||||||
|
TProperty extends PropertyOption,
|
||||||
|
TMethod extends MethodOption,
|
||||||
|
TCustomInstanceProperty extends IAnyObject = {},
|
||||||
|
TIsPage extends boolean = false
|
||||||
|
>(
|
||||||
|
options: Options<
|
||||||
|
TData,
|
||||||
|
TProperty,
|
||||||
|
TMethod,
|
||||||
|
TCustomInstanceProperty,
|
||||||
|
TIsPage
|
||||||
|
>
|
||||||
|
): string
|
||||||
|
}
|
||||||
|
type DataOption = Record<string, any>
|
||||||
|
type PropertyOption = Record<string, AllProperty>
|
||||||
|
type MethodOption = Record<string, Function>
|
||||||
|
|
||||||
|
interface Data<D extends DataOption> {
|
||||||
|
/** 组件的内部数据,和 `properties` 一同用于组件的模板渲染 */
|
||||||
|
data?: D
|
||||||
|
}
|
||||||
|
interface Property<P extends PropertyOption> {
|
||||||
|
/** 组件的对外属性,是属性名到属性设置的映射表 */
|
||||||
|
properties: P
|
||||||
|
}
|
||||||
|
interface Method<M extends MethodOption, TIsPage extends boolean = false> {
|
||||||
|
/** 组件的方法,包括事件响应函数和任意的自定义方法,关于事件响应函数的使用,参见 [组件间通信与事件](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/events.html) */
|
||||||
|
methods: M & (TIsPage extends true ? Partial<Page.ILifetime> : {})
|
||||||
|
}
|
||||||
|
type PropertyType =
|
||||||
|
| StringConstructor
|
||||||
|
| NumberConstructor
|
||||||
|
| BooleanConstructor
|
||||||
|
| ArrayConstructor
|
||||||
|
| ObjectConstructor
|
||||||
|
| null
|
||||||
|
type ValueType<T extends PropertyType> = T extends null
|
||||||
|
? any
|
||||||
|
: T extends StringConstructor
|
||||||
|
? string
|
||||||
|
: T extends NumberConstructor
|
||||||
|
? number
|
||||||
|
: T extends BooleanConstructor
|
||||||
|
? boolean
|
||||||
|
: T extends ArrayConstructor
|
||||||
|
? any[]
|
||||||
|
: T extends ObjectConstructor
|
||||||
|
? IAnyObject
|
||||||
|
: never
|
||||||
|
type FullProperty<T extends PropertyType> = {
|
||||||
|
/** 属性类型 */
|
||||||
|
type: T
|
||||||
|
/** 属性初始值 */
|
||||||
|
value?: ValueType<T>
|
||||||
|
/** 属性值被更改时的响应函数 */
|
||||||
|
observer?:
|
||||||
|
| string
|
||||||
|
| ((
|
||||||
|
newVal: ValueType<T>,
|
||||||
|
oldVal: ValueType<T>,
|
||||||
|
changedPath: Array<string | number>
|
||||||
|
) => void)
|
||||||
|
/** 属性的类型(可以指定多个) */
|
||||||
|
optionalTypes?: ShortProperty[]
|
||||||
|
}
|
||||||
|
type AllFullProperty =
|
||||||
|
| FullProperty<StringConstructor>
|
||||||
|
| FullProperty<NumberConstructor>
|
||||||
|
| FullProperty<BooleanConstructor>
|
||||||
|
| FullProperty<ArrayConstructor>
|
||||||
|
| FullProperty<ObjectConstructor>
|
||||||
|
| FullProperty<null>
|
||||||
|
type ShortProperty =
|
||||||
|
| StringConstructor
|
||||||
|
| NumberConstructor
|
||||||
|
| BooleanConstructor
|
||||||
|
| ArrayConstructor
|
||||||
|
| ObjectConstructor
|
||||||
|
| null
|
||||||
|
type AllProperty = AllFullProperty | ShortProperty
|
||||||
|
type PropertyToData<T extends AllProperty> = T extends ShortProperty
|
||||||
|
? ValueType<T>
|
||||||
|
: FullPropertyToData<Exclude<T, ShortProperty>>
|
||||||
|
type FullPropertyToData<T extends AllFullProperty> = ValueType<T['type']>
|
||||||
|
type PropertyOptionToData<P extends PropertyOption> = {
|
||||||
|
[name in keyof P]: PropertyToData<P[name]>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InstanceProperties {
|
||||||
|
/** 组件的文件路径 */
|
||||||
|
is: string
|
||||||
|
/** 节点id */
|
||||||
|
id: string
|
||||||
|
/** 节点dataset */
|
||||||
|
dataset: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InstanceMethods<D extends DataOption> {
|
||||||
|
/** `setData` 函数用于将数据从逻辑层发送到视图层
|
||||||
|
*(异步),同时改变对应的 `this.data` 的值(同步)。
|
||||||
|
*
|
||||||
|
* **注意:**
|
||||||
|
*
|
||||||
|
* 1. **直接修改 this.data 而不调用 this.setData 是无法改变页面的状态的,还会造成数据不一致**。
|
||||||
|
* 1. 仅支持设置可 JSON 化的数据。
|
||||||
|
* 1. 单次设置的数据不能超过1024kB,请尽量避免一次设置过多的数据。
|
||||||
|
* 1. 请不要把 data 中任何一项的 value 设为 `undefined` ,否则这一项将不被设置并可能遗留一些潜在问题。
|
||||||
|
*/
|
||||||
|
setData(
|
||||||
|
/** 这次要改变的数据
|
||||||
|
*
|
||||||
|
* 以 `key: value` 的形式表示,将 `this.data` 中的 `key` 对应的值改变成 `value`。
|
||||||
|
*
|
||||||
|
* 其中 `key` 可以以数据路径的形式给出,支持改变数组中的某一项或对象的某个属性,如 `array[2].message`,`a.b.c.d`,并且不需要在 this.data 中预先定义。
|
||||||
|
*/
|
||||||
|
data: Partial<D> & IAnyObject,
|
||||||
|
/** setData引起的界面更新渲染完毕后的回调函数,最低基础库: `1.5.0` */
|
||||||
|
callback?: () => void
|
||||||
|
): void
|
||||||
|
|
||||||
|
/** 检查组件是否具有 `behavior` (检查时会递归检查被直接或间接引入的所有behavior) */
|
||||||
|
hasBehavior(behavior: Behavior.BehaviorIdentifier): void
|
||||||
|
/** 触发事件,参见组件事件 */
|
||||||
|
triggerEvent<DetailType = any>(
|
||||||
|
name: string,
|
||||||
|
detail?: DetailType,
|
||||||
|
options?: TriggerEventOption
|
||||||
|
): void
|
||||||
|
/** 创建一个 SelectorQuery 对象,选择器选取范围为这个组件实例内 */
|
||||||
|
createSelectorQuery(): SelectorQuery
|
||||||
|
/** 创建一个 IntersectionObserver 对象,选择器选取范围为这个组件实例内 */
|
||||||
|
createIntersectionObserver(
|
||||||
|
options: CreateIntersectionObserverOption
|
||||||
|
): IntersectionObserver
|
||||||
|
/** 使用选择器选择组件实例节点,返回匹配到的第一个组件实例对象(会被 `wx://component-export` 影响) */
|
||||||
|
selectComponent(selector: string): TrivialInstance
|
||||||
|
/** 使用选择器选择组件实例节点,返回匹配到的全部组件实例对象组成的数组 */
|
||||||
|
selectAllComponents(selector: string): TrivialInstance[]
|
||||||
|
/**
|
||||||
|
* 选取当前组件节点所在的组件实例(即组件的引用者),返回它的组件实例对象(会被 `wx://component-export` 影响)
|
||||||
|
*
|
||||||
|
* 最低基础库版本:[`2.8.2`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||||
|
**/
|
||||||
|
selectOwnerComponent(): TrivialInstance
|
||||||
|
/** 获取这个关系所对应的所有关联节点,参见 组件间关系 */
|
||||||
|
getRelationNodes(relationKey: string): TrivialInstance[]
|
||||||
|
/**
|
||||||
|
* 立刻执行 callback ,其中的多个 setData 之间不会触发界面绘制(只有某些特殊场景中需要,如用于在不同组件同时 setData 时进行界面绘制同步)
|
||||||
|
*
|
||||||
|
* 最低基础库版本:[`2.4.0`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||||
|
**/
|
||||||
|
groupSetData(callback?: () => void): void
|
||||||
|
/**
|
||||||
|
* 返回当前页面的 custom-tab-bar 的组件实例
|
||||||
|
*
|
||||||
|
* 最低基础库版本:[`2.6.2`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||||
|
**/
|
||||||
|
getTabBar(): TrivialInstance
|
||||||
|
/**
|
||||||
|
* 返回页面标识符(一个字符串),可以用来判断几个自定义组件实例是不是在同一个页面内
|
||||||
|
*
|
||||||
|
* 最低基础库版本:[`2.7.1`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||||
|
**/
|
||||||
|
getPageId(): string
|
||||||
|
/**
|
||||||
|
* 执行关键帧动画,详见[动画](https://developers.weixin.qq.com/miniprogram/dev/framework/view/animation.html)
|
||||||
|
*
|
||||||
|
* 最低基础库版本:[`2.9.0`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||||
|
**/
|
||||||
|
animate(
|
||||||
|
selector: string,
|
||||||
|
keyFrames: KeyFrame[],
|
||||||
|
duration: number,
|
||||||
|
callback?: () => void
|
||||||
|
): void
|
||||||
|
/**
|
||||||
|
* 执行关键帧动画,详见[动画](https://developers.weixin.qq.com/miniprogram/dev/framework/view/animation.html)
|
||||||
|
*
|
||||||
|
* 最低基础库版本:[`2.9.0`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||||
|
**/
|
||||||
|
animate(
|
||||||
|
selector: string,
|
||||||
|
keyFrames: ScrollTimelineKeyframe[],
|
||||||
|
duration: number,
|
||||||
|
scrollTimeline: ScrollTimelineOption
|
||||||
|
): void
|
||||||
|
/**
|
||||||
|
* 清除关键帧动画,详见[动画](https://developers.weixin.qq.com/miniprogram/dev/framework/view/animation.html)
|
||||||
|
*
|
||||||
|
* 最低基础库版本:[`2.9.0`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||||
|
**/
|
||||||
|
clearAnimation(selector: string, callback: () => void): void
|
||||||
|
/**
|
||||||
|
* 清除关键帧动画,详见[动画](https://developers.weixin.qq.com/miniprogram/dev/framework/view/animation.html)
|
||||||
|
*
|
||||||
|
* 最低基础库版本:[`2.9.0`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||||
|
**/
|
||||||
|
clearAnimation(
|
||||||
|
selector: string,
|
||||||
|
options?: ClearAnimationOptions,
|
||||||
|
callback?: () => void
|
||||||
|
): void
|
||||||
|
getOpenerEventChannel(): EventChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ComponentOptions {
|
||||||
|
/**
|
||||||
|
* [启用多slot支持](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/wxml-wxss.html#组件wxml的slot)
|
||||||
|
*/
|
||||||
|
multipleSlots?: boolean
|
||||||
|
/**
|
||||||
|
* [组件样式隔离](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/wxml-wxss.html#组件样式隔离)
|
||||||
|
*/
|
||||||
|
addGlobalClass?: boolean
|
||||||
|
/**
|
||||||
|
* [组件样式隔离](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/wxml-wxss.html#组件样式隔离)
|
||||||
|
*/
|
||||||
|
styleIsolation?:
|
||||||
|
| 'isolated'
|
||||||
|
| 'apply-shared'
|
||||||
|
| 'shared'
|
||||||
|
| 'page-isolated'
|
||||||
|
| 'page-apply-shared'
|
||||||
|
| 'page-shared'
|
||||||
|
/**
|
||||||
|
* [纯数据字段](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/pure-data.html) 是一些不用于界面渲染的 data 字段,可以用于提升页面更新性能。从小程序基础库版本 2.8.2 开始支持。
|
||||||
|
*/
|
||||||
|
pureDataPattern?: RegExp
|
||||||
|
/**
|
||||||
|
* [虚拟化组件节点](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/wxml-wxss.html#%E8%99%9A%E6%8B%9F%E5%8C%96%E7%BB%84%E4%BB%B6%E8%8A%82%E7%82%B9) 使自定义组件内部的第一层节点由自定义组件本身完全决定。从小程序基础库版本 [`2.11.2`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html) 开始支持 */
|
||||||
|
virtualHost?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TriggerEventOption {
|
||||||
|
/** 事件是否冒泡
|
||||||
|
*
|
||||||
|
* 默认值: `false`
|
||||||
|
*/
|
||||||
|
bubbles?: boolean
|
||||||
|
/** 事件是否可以穿越组件边界,为false时,事件将只能在引用组件的节点树上触发,不进入其他任何组件内部
|
||||||
|
*
|
||||||
|
* 默认值: `false`
|
||||||
|
*/
|
||||||
|
composed?: boolean
|
||||||
|
/** 事件是否拥有捕获阶段
|
||||||
|
*
|
||||||
|
* 默认值: `false`
|
||||||
|
*/
|
||||||
|
capturePhase?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RelationOption {
|
||||||
|
/** 目标组件的相对关系 */
|
||||||
|
type: 'parent' | 'child' | 'ancestor' | 'descendant'
|
||||||
|
/** 关系生命周期函数,当关系被建立在页面节点树中时触发,触发时机在组件attached生命周期之后 */
|
||||||
|
linked?(target: TrivialInstance): void
|
||||||
|
/** 关系生命周期函数,当关系在页面节点树中发生改变时触发,触发时机在组件moved生命周期之后 */
|
||||||
|
linkChanged?(target: TrivialInstance): void
|
||||||
|
/** 关系生命周期函数,当关系脱离页面节点树时触发,触发时机在组件detached生命周期之后 */
|
||||||
|
unlinked?(target: TrivialInstance): void
|
||||||
|
/** 如果这一项被设置,则它表示关联的目标节点所应具有的behavior,所有拥有这一behavior的组件节点都会被关联 */
|
||||||
|
target?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PageLifetimes {
|
||||||
|
/** 页面生命周期回调—监听页面显示
|
||||||
|
*
|
||||||
|
* 页面显示/切入前台时触发。
|
||||||
|
*/
|
||||||
|
show(): void
|
||||||
|
/** 页面生命周期回调—监听页面隐藏
|
||||||
|
*
|
||||||
|
* 页面隐藏/切入后台时触发。 如 `navigateTo` 或底部 `tab` 切换到其他页面,小程序切入后台等。
|
||||||
|
*/
|
||||||
|
hide(): void
|
||||||
|
/** 页面生命周期回调—监听页面尺寸变化
|
||||||
|
*
|
||||||
|
* 所在页面尺寸变化时执行
|
||||||
|
*/
|
||||||
|
resize(size: Page.IResizeOption): void
|
||||||
|
}
|
||||||
|
|
||||||
|
type DefinitionFilter = <T extends TrivialOption>(
|
||||||
|
/** 使用该 behavior 的 component/behavior 的定义对象 */
|
||||||
|
defFields: T,
|
||||||
|
/** 该 behavior 所使用的 behavior 的 definitionFilter 函数列表 */
|
||||||
|
definitionFilterArr?: DefinitionFilter[]
|
||||||
|
) => void
|
||||||
|
|
||||||
|
interface Lifetimes {
|
||||||
|
/** 组件生命周期声明对象,组件的生命周期:`created`、`attached`、`ready`、`moved`、`detached` 将收归到 `lifetimes` 字段内进行声明,原有声明方式仍旧有效,如同时存在两种声明方式,则 `lifetimes` 字段内声明方式优先级最高
|
||||||
|
*
|
||||||
|
* 最低基础库: `2.2.3` */
|
||||||
|
lifetimes: Partial<{
|
||||||
|
/**
|
||||||
|
* 在组件实例刚刚被创建时执行,注意此时不能调用 `setData`
|
||||||
|
*
|
||||||
|
* 最低基础库版本:[`1.6.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||||
|
*/
|
||||||
|
created(): void
|
||||||
|
/**
|
||||||
|
* 在组件实例进入页面节点树时执行
|
||||||
|
*
|
||||||
|
* 最低基础库版本:[`1.6.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||||
|
*/
|
||||||
|
attached(): void
|
||||||
|
/**
|
||||||
|
* 在组件在视图层布局完成后执行
|
||||||
|
*
|
||||||
|
* 最低基础库版本:[`1.6.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||||
|
*/
|
||||||
|
ready(): void
|
||||||
|
/**
|
||||||
|
* 在组件实例被移动到节点树另一个位置时执行
|
||||||
|
*
|
||||||
|
* 最低基础库版本:[`1.6.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||||
|
*/
|
||||||
|
moved(): void
|
||||||
|
/**
|
||||||
|
* 在组件实例被从页面节点树移除时执行
|
||||||
|
*
|
||||||
|
* 最低基础库版本:[`1.6.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||||
|
*/
|
||||||
|
detached(): void
|
||||||
|
/**
|
||||||
|
* 每当组件方法抛出错误时执行
|
||||||
|
*
|
||||||
|
* 最低基础库版本:[`2.4.1`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||||
|
*/
|
||||||
|
error(err: Error): void
|
||||||
|
}>
|
||||||
|
/**
|
||||||
|
* @deprecated 旧式的定义方式,基础库 `2.2.3` 起请在 lifetimes 中定义
|
||||||
|
*
|
||||||
|
* 在组件实例刚刚被创建时执行
|
||||||
|
*
|
||||||
|
* 最低基础库版本:[`1.6.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||||
|
*/
|
||||||
|
created(): void
|
||||||
|
/**
|
||||||
|
* @deprecated 旧式的定义方式,基础库 `2.2.3` 起请在 lifetimes 中定义
|
||||||
|
*
|
||||||
|
* 在组件实例进入页面节点树时执行
|
||||||
|
*
|
||||||
|
* 最低基础库版本:[`1.6.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||||
|
*/
|
||||||
|
attached(): void
|
||||||
|
/**
|
||||||
|
* @deprecated 旧式的定义方式,基础库 `2.2.3` 起请在 lifetimes 中定义
|
||||||
|
*
|
||||||
|
* 在组件在视图层布局完成后执行
|
||||||
|
*
|
||||||
|
* 最低基础库版本:[`1.6.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||||
|
*/
|
||||||
|
ready(): void
|
||||||
|
/**
|
||||||
|
* @deprecated 旧式的定义方式,基础库 `2.2.3` 起请在 lifetimes 中定义
|
||||||
|
*
|
||||||
|
* 在组件实例被移动到节点树另一个位置时执行
|
||||||
|
*
|
||||||
|
* 最低基础库版本:[`1.6.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||||
|
*/
|
||||||
|
moved(): void
|
||||||
|
/**
|
||||||
|
* @deprecated 旧式的定义方式,基础库 `2.2.3` 起请在 lifetimes 中定义
|
||||||
|
*
|
||||||
|
* 在组件实例被从页面节点树移除时执行
|
||||||
|
*
|
||||||
|
* 最低基础库版本:[`1.6.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||||
|
*/
|
||||||
|
detached(): void
|
||||||
|
/**
|
||||||
|
* @deprecated 旧式的定义方式,基础库 `2.2.3` 起请在 lifetimes 中定义
|
||||||
|
*
|
||||||
|
* 每当组件方法抛出错误时执行
|
||||||
|
*
|
||||||
|
* 最低基础库版本:[`2.4.1`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||||
|
*/
|
||||||
|
error(err: Error): void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OtherOption {
|
||||||
|
/** 类似于mixins和traits的组件间代码复用机制,参见 [behaviors](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/behaviors.html) */
|
||||||
|
behaviors: Behavior.BehaviorIdentifier[]
|
||||||
|
/**
|
||||||
|
* 组件数据字段监听器,用于监听 properties 和 data 的变化,参见 [数据监听器](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/observer.html)
|
||||||
|
*
|
||||||
|
* 最低基础库版本:[`2.6.1`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||||
|
*/
|
||||||
|
observers: Record<string, (...args: any[]) => any>
|
||||||
|
/** 组件间关系定义,参见 [组件间关系](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/lifetimes.html) */
|
||||||
|
relations: {
|
||||||
|
[componentName: string]: RelationOption
|
||||||
|
}
|
||||||
|
/** 组件接受的外部样式类,参见 [外部样式类](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/wxml-wxss.html) */
|
||||||
|
externalClasses?: string[]
|
||||||
|
/** 组件所在页面的生命周期声明对象,参见 [组件生命周期](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/lifetimes.html)
|
||||||
|
*
|
||||||
|
* 最低基础库版本: [`2.2.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html) */
|
||||||
|
pageLifetimes?: Partial<PageLifetimes>
|
||||||
|
/** 一些选项(文档中介绍相关特性时会涉及具体的选项设置,这里暂不列举) */
|
||||||
|
options: ComponentOptions
|
||||||
|
|
||||||
|
/** 定义段过滤器,用于自定义组件扩展,参见 [自定义组件扩展](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/extend.html)
|
||||||
|
*
|
||||||
|
* 最低基础库版本: [`2.2.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html) */
|
||||||
|
definitionFilter?: DefinitionFilter
|
||||||
|
/**
|
||||||
|
* 组件自定义导出,当使用 `behavior: wx://component-export` 时,这个定义段可以用于指定组件被 selectComponent 调用时的返回值,参见 [组件间通信与事件](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/events.html)
|
||||||
|
* 最低基础库版本: [`2.2.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html) */
|
||||||
|
export: () => IAnyObject
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KeyFrame {
|
||||||
|
/** 关键帧的偏移,范围[0-1] */
|
||||||
|
offset?: number
|
||||||
|
/** 动画缓动函数 */
|
||||||
|
ease?: string
|
||||||
|
/** 基点位置,即 CSS transform-origin */
|
||||||
|
transformOrigin?: string
|
||||||
|
/** 背景颜色,即 CSS background-color */
|
||||||
|
backgroundColor?: string
|
||||||
|
/** 底边位置,即 CSS bottom */
|
||||||
|
bottom?: number | string
|
||||||
|
/** 高度,即 CSS height */
|
||||||
|
height?: number | string
|
||||||
|
/** 左边位置,即 CSS left */
|
||||||
|
left?: number | string
|
||||||
|
/** 宽度,即 CSS width */
|
||||||
|
width?: number | string
|
||||||
|
/** 不透明度,即 CSS opacity */
|
||||||
|
opacity?: number | string
|
||||||
|
/** 右边位置,即 CSS right */
|
||||||
|
right?: number | string
|
||||||
|
/** 顶边位置,即 CSS top */
|
||||||
|
top?: number | string
|
||||||
|
/** 变换矩阵,即 CSS transform matrix */
|
||||||
|
matrix?: number[]
|
||||||
|
/** 三维变换矩阵,即 CSS transform matrix3d */
|
||||||
|
matrix3d?: number[]
|
||||||
|
/** 旋转,即 CSS transform rotate */
|
||||||
|
rotate?: number
|
||||||
|
/** 三维旋转,即 CSS transform rotate3d */
|
||||||
|
rotate3d?: number[]
|
||||||
|
/** X 方向旋转,即 CSS transform rotateX */
|
||||||
|
rotateX?: number
|
||||||
|
/** Y 方向旋转,即 CSS transform rotateY */
|
||||||
|
rotateY?: number
|
||||||
|
/** Z 方向旋转,即 CSS transform rotateZ */
|
||||||
|
rotateZ?: number
|
||||||
|
/** 缩放,即 CSS transform scale */
|
||||||
|
scale?: number[]
|
||||||
|
/** 三维缩放,即 CSS transform scale3d */
|
||||||
|
scale3d?: number[]
|
||||||
|
/** X 方向缩放,即 CSS transform scaleX */
|
||||||
|
scaleX?: number
|
||||||
|
/** Y 方向缩放,即 CSS transform scaleY */
|
||||||
|
scaleY?: number
|
||||||
|
/** Z 方向缩放,即 CSS transform scaleZ */
|
||||||
|
scaleZ?: number
|
||||||
|
/** 倾斜,即 CSS transform skew */
|
||||||
|
skew?: number[]
|
||||||
|
/** X 方向倾斜,即 CSS transform skewX */
|
||||||
|
skewX?: number
|
||||||
|
/** Y 方向倾斜,即 CSS transform skewY */
|
||||||
|
skewY?: number
|
||||||
|
/** 位移,即 CSS transform translate */
|
||||||
|
translate?: Array<number | string>
|
||||||
|
/** 三维位移,即 CSS transform translate3d */
|
||||||
|
translate3d?: Array<number | string>
|
||||||
|
/** X 方向位移,即 CSS transform translateX */
|
||||||
|
translateX?: number | string
|
||||||
|
/** Y 方向位移,即 CSS transform translateY */
|
||||||
|
translateY?: number | string
|
||||||
|
/** Z 方向位移,即 CSS transform translateZ */
|
||||||
|
translateZ?: number | string
|
||||||
|
}
|
||||||
|
interface ClearAnimationOptions {
|
||||||
|
/** 基点位置,即 CSS transform-origin */
|
||||||
|
transformOrigin?: boolean
|
||||||
|
/** 背景颜色,即 CSS background-color */
|
||||||
|
backgroundColor?: boolean
|
||||||
|
/** 底边位置,即 CSS bottom */
|
||||||
|
bottom?: boolean
|
||||||
|
/** 高度,即 CSS height */
|
||||||
|
height?: boolean
|
||||||
|
/** 左边位置,即 CSS left */
|
||||||
|
left?: boolean
|
||||||
|
/** 宽度,即 CSS width */
|
||||||
|
width?: boolean
|
||||||
|
/** 不透明度,即 CSS opacity */
|
||||||
|
opacity?: boolean
|
||||||
|
/** 右边位置,即 CSS right */
|
||||||
|
right?: boolean
|
||||||
|
/** 顶边位置,即 CSS top */
|
||||||
|
top?: boolean
|
||||||
|
/** 变换矩阵,即 CSS transform matrix */
|
||||||
|
matrix?: boolean
|
||||||
|
/** 三维变换矩阵,即 CSS transform matrix3d */
|
||||||
|
matrix3d?: boolean
|
||||||
|
/** 旋转,即 CSS transform rotate */
|
||||||
|
rotate?: boolean
|
||||||
|
/** 三维旋转,即 CSS transform rotate3d */
|
||||||
|
rotate3d?: boolean
|
||||||
|
/** X 方向旋转,即 CSS transform rotateX */
|
||||||
|
rotateX?: boolean
|
||||||
|
/** Y 方向旋转,即 CSS transform rotateY */
|
||||||
|
rotateY?: boolean
|
||||||
|
/** Z 方向旋转,即 CSS transform rotateZ */
|
||||||
|
rotateZ?: boolean
|
||||||
|
/** 缩放,即 CSS transform scale */
|
||||||
|
scale?: boolean
|
||||||
|
/** 三维缩放,即 CSS transform scale3d */
|
||||||
|
scale3d?: boolean
|
||||||
|
/** X 方向缩放,即 CSS transform scaleX */
|
||||||
|
scaleX?: boolean
|
||||||
|
/** Y 方向缩放,即 CSS transform scaleY */
|
||||||
|
scaleY?: boolean
|
||||||
|
/** Z 方向缩放,即 CSS transform scaleZ */
|
||||||
|
scaleZ?: boolean
|
||||||
|
/** 倾斜,即 CSS transform skew */
|
||||||
|
skew?: boolean
|
||||||
|
/** X 方向倾斜,即 CSS transform skewX */
|
||||||
|
skewX?: boolean
|
||||||
|
/** Y 方向倾斜,即 CSS transform skewY */
|
||||||
|
skewY?: boolean
|
||||||
|
/** 位移,即 CSS transform translate */
|
||||||
|
translate?: boolean
|
||||||
|
/** 三维位移,即 CSS transform translate3d */
|
||||||
|
translate3d?: boolean
|
||||||
|
/** X 方向位移,即 CSS transform translateX */
|
||||||
|
translateX?: boolean
|
||||||
|
/** Y 方向位移,即 CSS transform translateY */
|
||||||
|
translateY?: boolean
|
||||||
|
/** Z 方向位移,即 CSS transform translateZ */
|
||||||
|
translateZ?: boolean
|
||||||
|
}
|
||||||
|
interface ScrollTimelineKeyframe {
|
||||||
|
composite?: 'replace' | 'add' | 'accumulate' | 'auto'
|
||||||
|
easing?: string
|
||||||
|
offset?: number | null
|
||||||
|
[property: string]: string | number | null | undefined
|
||||||
|
}
|
||||||
|
interface ScrollTimelineOption {
|
||||||
|
/** 指定滚动元素的选择器(只支持 scroll-view),该元素滚动时会驱动动画的进度 */
|
||||||
|
scrollSource: string
|
||||||
|
/** 指定滚动的方向。有效值为 horizontal 或 vertical */
|
||||||
|
orientation?: string
|
||||||
|
/** 指定开始驱动动画进度的滚动偏移量,单位 px */
|
||||||
|
startScrollOffset: number
|
||||||
|
/** 指定停止驱动动画进度的滚动偏移量,单位 px */
|
||||||
|
endScrollOffset: number
|
||||||
|
/** 起始和结束的滚动范围映射的时间长度,该时间可用于与关键帧动画里的时间 (duration) 相匹配,单位 ms */
|
||||||
|
timeRange: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** Component构造器可用于定义组件,调用Component构造器时可以指定组件的属性、数据、方法等。
|
||||||
|
*
|
||||||
|
* * 使用 `this.data` 可以获取内部数据和属性值,但不要直接修改它们,应使用 `setData` 修改。
|
||||||
|
* * 生命周期函数无法在组件方法中通过 `this` 访问到。
|
||||||
|
* * 属性名应避免以 data 开头,即不要命名成 `dataXyz` 这样的形式,因为在 WXML 中, `data-xyz=""` 会被作为节点 dataset 来处理,而不是组件属性。
|
||||||
|
* * 在一个组件的定义和使用时,组件的属性名和 data 字段相互间都不能冲突(尽管它们位于不同的定义段中)。
|
||||||
|
* * 从基础库 `2.0.9` 开始,对象类型的属性和 data 字段中可以包含函数类型的子字段,即可以通过对象类型的属性字段来传递函数。低于这一版本的基础库不支持这一特性。
|
||||||
|
* * `bug` : 对于 type 为 Object 或 Array 的属性,如果通过该组件自身的 `this.setData` 来改变属性值的一个子字段,则依旧会触发属性 observer ,且 observer 接收到的 `newVal` 是变化的那个子字段的值, `oldVal` 为空, `changedPath` 包含子字段的字段名相关信息。
|
||||||
|
*/
|
||||||
|
declare let Component: WechatMiniprogram.Component.Constructor
|
||||||
1435
typings/types/wx/lib.wx.event.d.ts
vendored
Normal file
1435
typings/types/wx/lib.wx.event.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
259
typings/types/wx/lib.wx.page.d.ts
vendored
Normal file
259
typings/types/wx/lib.wx.page.d.ts
vendored
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
/*! *****************************************************************************
|
||||||
|
Copyright (c) 2021 Tencent, Inc. All rights reserved.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
***************************************************************************** */
|
||||||
|
|
||||||
|
declare namespace WechatMiniprogram.Page {
|
||||||
|
type Instance<
|
||||||
|
TData extends DataOption,
|
||||||
|
TCustom extends CustomOption
|
||||||
|
> = OptionalInterface<ILifetime> &
|
||||||
|
InstanceProperties &
|
||||||
|
InstanceMethods<TData> &
|
||||||
|
Data<TData> &
|
||||||
|
TCustom
|
||||||
|
type Options<
|
||||||
|
TData extends DataOption,
|
||||||
|
TCustom extends CustomOption
|
||||||
|
> = (TCustom & Partial<Data<TData>> & Partial<ILifetime>) &
|
||||||
|
ThisType<Instance<TData, TCustom>>
|
||||||
|
type TrivialInstance = Instance<IAnyObject, IAnyObject>
|
||||||
|
interface Constructor {
|
||||||
|
<TData extends DataOption, TCustom extends CustomOption>(
|
||||||
|
options: Options<TData, TCustom>
|
||||||
|
): void
|
||||||
|
}
|
||||||
|
interface ILifetime {
|
||||||
|
/** 生命周期回调—监听页面加载
|
||||||
|
*
|
||||||
|
* 页面加载时触发。一个页面只会调用一次,可以在 onLoad 的参数中获取打开当前页面路径中的参数。
|
||||||
|
*/
|
||||||
|
onLoad(
|
||||||
|
/** 打开当前页面路径中的参数 */
|
||||||
|
query: Record<string, string | undefined>
|
||||||
|
): void | Promise<void>
|
||||||
|
/** 生命周期回调—监听页面显示
|
||||||
|
*
|
||||||
|
* 页面显示/切入前台时触发。
|
||||||
|
*/
|
||||||
|
onShow(): void | Promise<void>
|
||||||
|
/** 生命周期回调—监听页面初次渲染完成
|
||||||
|
*
|
||||||
|
* 页面初次渲染完成时触发。一个页面只会调用一次,代表页面已经准备妥当,可以和视图层进行交互。
|
||||||
|
*
|
||||||
|
|
||||||
|
* 注意:对界面内容进行设置的 API 如`wx.setNavigationBarTitle`,请在`onReady`之后进行。
|
||||||
|
*/
|
||||||
|
onReady(): void | Promise<void>
|
||||||
|
/** 生命周期回调—监听页面隐藏
|
||||||
|
*
|
||||||
|
* 页面隐藏/切入后台时触发。 如 `navigateTo` 或底部 `tab` 切换到其他页面,小程序切入后台等。
|
||||||
|
*/
|
||||||
|
onHide(): void | Promise<void>
|
||||||
|
/** 生命周期回调—监听页面卸载
|
||||||
|
*
|
||||||
|
* 页面卸载时触发。如`redirectTo`或`navigateBack`到其他页面时。
|
||||||
|
*/
|
||||||
|
onUnload(): void | Promise<void>
|
||||||
|
/** 监听用户下拉动作
|
||||||
|
*
|
||||||
|
* 监听用户下拉刷新事件。
|
||||||
|
* - 需要在`app.json`的`window`选项中或页面配置中开启`enablePullDownRefresh`。
|
||||||
|
* - 可以通过`wx.startPullDownRefresh`触发下拉刷新,调用后触发下拉刷新动画,效果与用户手动下拉刷新一致。
|
||||||
|
* - 当处理完数据刷新后,`wx.stopPullDownRefresh`可以停止当前页面的下拉刷新。
|
||||||
|
*/
|
||||||
|
onPullDownRefresh(): void | Promise<void>
|
||||||
|
/** 页面上拉触底事件的处理函数
|
||||||
|
*
|
||||||
|
* 监听用户上拉触底事件。
|
||||||
|
* - 可以在`app.json`的`window`选项中或页面配置中设置触发距离`onReachBottomDistance`。
|
||||||
|
* - 在触发距离内滑动期间,本事件只会被触发一次。
|
||||||
|
*/
|
||||||
|
onReachBottom(): void | Promise<void>
|
||||||
|
/** 用户点击右上角转发
|
||||||
|
*
|
||||||
|
* 监听用户点击页面内转发按钮(`<button>` 组件 `open-type="share"`)或右上角菜单“转发”按钮的行为,并自定义转发内容。
|
||||||
|
*
|
||||||
|
* **注意:只有定义了此事件处理函数,右上角菜单才会显示“转发”按钮**
|
||||||
|
*
|
||||||
|
* 此事件需要 return 一个 Object,用于自定义转发内容
|
||||||
|
*/
|
||||||
|
onShareAppMessage(
|
||||||
|
/** 分享发起来源参数 */
|
||||||
|
options: IShareAppMessageOption
|
||||||
|
): ICustomShareContent | void
|
||||||
|
/**
|
||||||
|
* 监听右上角菜单“分享到朋友圈”按钮的行为,并自定义分享内容
|
||||||
|
*
|
||||||
|
* 本接口为 Beta 版本,暂只在 Android 平台支持,详见 [分享到朋友圈 (Beta)](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/share-timeline.html)
|
||||||
|
*
|
||||||
|
* 基础库 2.11.3 开始支持,低版本需做兼容处理。
|
||||||
|
*/
|
||||||
|
onShareTimeline(): ICustomTimelineContent | void
|
||||||
|
|
||||||
|
/** 页面滚动触发事件的处理函数
|
||||||
|
*
|
||||||
|
* 监听用户滑动页面事件。
|
||||||
|
*/
|
||||||
|
onPageScroll(
|
||||||
|
/** 页面滚动参数 */
|
||||||
|
options: IPageScrollOption
|
||||||
|
): void | Promise<void>
|
||||||
|
|
||||||
|
/** 当前是 tab 页时,点击 tab 时触发,最低基础库: `1.9.0` */
|
||||||
|
onTabItemTap(
|
||||||
|
/** tab 点击参数 */
|
||||||
|
options: ITabItemTapOption
|
||||||
|
): void | Promise<void>
|
||||||
|
|
||||||
|
/** 窗口尺寸改变时触发,最低基础库:`2.4.0` */
|
||||||
|
onResize(
|
||||||
|
/** 窗口尺寸参数 */
|
||||||
|
options: IResizeOption
|
||||||
|
): void | Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听用户点击右上角菜单“收藏”按钮的行为,并自定义收藏内容。
|
||||||
|
* 基础库 2.10.3,安卓 7.0.15 版本起支持,iOS 暂不支持
|
||||||
|
*/
|
||||||
|
onAddToFavorites(options: IAddToFavoritesOption): IAddToFavoritesContent
|
||||||
|
}
|
||||||
|
interface InstanceProperties {
|
||||||
|
/** 页面的文件路径 */
|
||||||
|
is: string
|
||||||
|
|
||||||
|
/** 到当前页面的路径 */
|
||||||
|
route: string
|
||||||
|
|
||||||
|
/** 打开当前页面路径中的参数 */
|
||||||
|
options: Record<string, string | undefined>
|
||||||
|
}
|
||||||
|
|
||||||
|
type DataOption = Record<string, any>
|
||||||
|
type CustomOption = Record<string, any>
|
||||||
|
|
||||||
|
type InstanceMethods<D extends DataOption> = Component.InstanceMethods<D>
|
||||||
|
|
||||||
|
interface Data<D extends DataOption> {
|
||||||
|
/** 页面的初始数据
|
||||||
|
*
|
||||||
|
* `data` 是页面第一次渲染使用的**初始数据**。
|
||||||
|
*
|
||||||
|
* 页面加载时,`data` 将会以`JSON`字符串的形式由逻辑层传至渲染层,因此`data`中的数据必须是可以转成`JSON`的类型:字符串,数字,布尔值,对象,数组。
|
||||||
|
*
|
||||||
|
* 渲染层可以通过 `WXML` 对数据进行绑定。
|
||||||
|
*/
|
||||||
|
data: D
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICustomShareContent {
|
||||||
|
/** 转发标题。默认值:当前小程序名称 */
|
||||||
|
title?: string
|
||||||
|
/** 转发路径,必须是以 / 开头的完整路径。默认值:当前页面 path */
|
||||||
|
path?: string
|
||||||
|
/** 自定义图片路径,可以是本地文件路径、代码包文件路径或者网络图片路径。支持PNG及JPG。显示图片长宽比是 5:4,最低基础库: `1.5.0`。默认值:使用默认截图 */
|
||||||
|
imageUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICustomTimelineContent {
|
||||||
|
/** 自定义标题,即朋友圈列表页上显示的标题。默认值:当前小程序名称 */
|
||||||
|
title?: string
|
||||||
|
/** 自定义页面路径中携带的参数,如 `path?a=1&b=2` 的 “?” 后面部分 默认值:当前页面路径携带的参数 */
|
||||||
|
query?: string
|
||||||
|
/** 自定义图片路径,可以是本地文件路径、代码包文件路径或者网络图片路径。支持 PNG 及 JPG。显示图片长宽比是 1:1。默认值:默认使用小程序 Logo*/
|
||||||
|
imageUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPageScrollOption {
|
||||||
|
/** 页面在垂直方向已滚动的距离(单位px) */
|
||||||
|
scrollTop: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IShareAppMessageOption {
|
||||||
|
/** 转发事件来源。
|
||||||
|
*
|
||||||
|
* 可选值:
|
||||||
|
* - `button`:页面内转发按钮;
|
||||||
|
* - `menu`:右上角转发菜单。
|
||||||
|
*
|
||||||
|
* 最低基础库: `1.2.4`
|
||||||
|
*/
|
||||||
|
from: 'button' | 'menu' | string
|
||||||
|
/** 如果 `from` 值是 `button`,则 `target` 是触发这次转发事件的 `button`,否则为 `undefined`
|
||||||
|
*
|
||||||
|
* 最低基础库: `1.2.4` */
|
||||||
|
target: any
|
||||||
|
/** 页面中包含`<web-view>`组件时,返回当前`<web-view>`的url
|
||||||
|
*
|
||||||
|
* 最低基础库: `1.6.4`
|
||||||
|
*/
|
||||||
|
webViewUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITabItemTapOption {
|
||||||
|
/** 被点击tabItem的序号,从0开始,最低基础库: `1.9.0` */
|
||||||
|
index: string
|
||||||
|
/** 被点击tabItem的页面路径,最低基础库: `1.9.0` */
|
||||||
|
pagePath: string
|
||||||
|
/** 被点击tabItem的按钮文字,最低基础库: `1.9.0` */
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IResizeOption {
|
||||||
|
size: {
|
||||||
|
/** 变化后的窗口宽度,单位 px */
|
||||||
|
windowWidth: number
|
||||||
|
/** 变化后的窗口高度,单位 px */
|
||||||
|
windowHeight: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IAddToFavoritesOption {
|
||||||
|
/** 页面中包含web-view组件时,返回当前web-view的url */
|
||||||
|
webviewUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IAddToFavoritesContent {
|
||||||
|
/** 自定义标题,默认值:页面标题或账号名称 */
|
||||||
|
title?: string
|
||||||
|
/** 自定义图片,显示图片长宽比为 1:1,默认值:页面截图 */
|
||||||
|
imageUrl?: string
|
||||||
|
/** 自定义query字段,默认值:当前页面的query */
|
||||||
|
query?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetCurrentPages {
|
||||||
|
(): Array<Instance<IAnyObject, IAnyObject>>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册小程序中的一个页面。接受一个 `Object` 类型参数,其指定页面的初始数据、生命周期回调、事件处理函数等。
|
||||||
|
*/
|
||||||
|
declare let Page: WechatMiniprogram.Page.Constructor
|
||||||
|
/**
|
||||||
|
* 获取当前页面栈。数组中第一个元素为首页,最后一个元素为当前页面。
|
||||||
|
|
||||||
|
* __注意:__
|
||||||
|
|
||||||
|
* - __不要尝试修改页面栈,会导致路由以及页面状态错误。__
|
||||||
|
* - 不要在 `App.onLaunch` 的时候调用 `getCurrentPages()`,此时 `page` 还没有生成。
|
||||||
|
*/
|
||||||
|
declare let getCurrentPages: WechatMiniprogram.Page.GetCurrentPages
|
||||||
Reference in New Issue
Block a user