2025-08-11 15:24:06 +08:00
|
|
|
|
import * as Cesium from 'cesium'
|
|
|
|
|
|
import CesiumNavigation from 'cesium-navigation-es6'
|
|
|
|
|
|
import 'cesium/Build/Cesium/Widgets/widgets.css'
|
|
|
|
|
|
|
2025-09-10 16:17:41 +08:00
|
|
|
|
const getCatesian3FromPX = (viewer, px) => {
|
|
|
|
|
|
const picks = viewer.scene.drillPick(px)
|
|
|
|
|
|
viewer.scene.render()
|
|
|
|
|
|
let cartesian
|
|
|
|
|
|
let isOn3dtiles = false
|
|
|
|
|
|
for (var i = 0; i < picks.length; i++) {
|
|
|
|
|
|
if (picks[i] && picks[i].primitive && picks[i].primitive instanceof Cesium.Cesium3DTileset) {
|
|
|
|
|
|
//模型上拾取
|
|
|
|
|
|
isOn3dtiles = true
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isOn3dtiles) {
|
|
|
|
|
|
cartesian = viewer.scene.pickPosition(px)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
var ray = viewer.camera.getPickRay(px)
|
|
|
|
|
|
if (!ray) return null
|
|
|
|
|
|
cartesian = viewer.scene.globe.pick(ray, viewer.scene)
|
|
|
|
|
|
}
|
|
|
|
|
|
return cartesian
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-11 15:24:06 +08:00
|
|
|
|
export default class MyCesium {
|
2025-08-26 17:32:02 +08:00
|
|
|
|
static ImageryProviderUrl = '/map/mapWX/{z}/{x}/{y}.jpg'
|
2025-08-11 15:24:06 +08:00
|
|
|
|
static RoadProviderUrl = ''
|
2025-09-17 20:34:51 +08:00
|
|
|
|
static TerrainProviderUrl = '/map/mapTerrain/'
|
2025-08-11 15:24:06 +08:00
|
|
|
|
static center = [116.39742, 39.90906, 2000000]
|
|
|
|
|
|
static ENTITY_TYPES = {
|
|
|
|
|
|
WARZONE: 1,
|
|
|
|
|
|
SAFEZONE: 2,
|
|
|
|
|
|
MODEL: 3,
|
|
|
|
|
|
IMAGE: 4,
|
|
|
|
|
|
ROUTE: 5,
|
|
|
|
|
|
LINE: 6, // 两个模型之间的连线
|
|
|
|
|
|
POINT_PLANNING: 7, // 规划点
|
|
|
|
|
|
GRAPHICS: 8, // 折线
|
|
|
|
|
|
AIR_ROUTE: 9, // 航路
|
|
|
|
|
|
ROAMING_PLANE: 10,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
viewer = null
|
|
|
|
|
|
// 未完成的在地图上移动类的操作
|
|
|
|
|
|
operations = []
|
2025-09-10 16:23:40 +08:00
|
|
|
|
// 已添加的军标
|
|
|
|
|
|
plots = []
|
2025-09-18 20:31:52 +08:00
|
|
|
|
// 已添加的路线
|
|
|
|
|
|
routes = []
|
|
|
|
|
|
|
2025-08-16 10:13:46 +08:00
|
|
|
|
constructor(dom, options = {}) {
|
2025-08-11 15:24:06 +08:00
|
|
|
|
const imageryProvider = new Cesium.UrlTemplateImageryProvider({
|
|
|
|
|
|
url: window._CONFIG.ImageryProviderUrl || MyCesium.ImageryProviderUrl,
|
|
|
|
|
|
tilingScheme: new Cesium.WebMercatorTilingScheme(),
|
|
|
|
|
|
maximumLevel: 15,
|
|
|
|
|
|
})
|
|
|
|
|
|
const roadProvider = new Cesium.UrlTemplateImageryProvider({
|
|
|
|
|
|
url: window._CONFIG.RoadProviderUrl || MyCesium.RoadProviderUrl,
|
|
|
|
|
|
tilingScheme: new Cesium.WebMercatorTilingScheme(),
|
|
|
|
|
|
maximumLevel: 15,
|
|
|
|
|
|
format: 'image/png',
|
|
|
|
|
|
})
|
|
|
|
|
|
const terrainProvider = new Cesium.CesiumTerrainProvider({
|
|
|
|
|
|
url: window._CONFIG.TerrainProviderUrl || MyCesium.TerrainProviderUrl,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const viewer = new Cesium.Viewer(dom, {
|
|
|
|
|
|
shouldAnimate: true, // 粒子效果
|
|
|
|
|
|
geocoder: false, // 是否显示位置查找工具(true表示是,false表示否)
|
|
|
|
|
|
homeButton: false, // 是否显示首页位置工具
|
|
|
|
|
|
sceneModePicker: false, // 是否显示视角模式切换工具
|
|
|
|
|
|
baseLayerPicker: false, // 是杏显示默认图层选择工具
|
|
|
|
|
|
navigationHelpButton: false, // 是否显示导航帮助工具
|
|
|
|
|
|
animation: false, // 是杏显示动画工具
|
|
|
|
|
|
timeline: false, // 是否显示时间轴工具
|
|
|
|
|
|
fullscreenButton: false, // 是否显示全屏按钮工具
|
|
|
|
|
|
imageryProvider,
|
|
|
|
|
|
terrainProvider,
|
|
|
|
|
|
infoBox: false, // 是否显示信息框
|
|
|
|
|
|
selectionIndicator: false, // 是否显示选中实体时的绿框
|
|
|
|
|
|
})
|
|
|
|
|
|
this.viewer = viewer
|
|
|
|
|
|
|
2025-08-16 10:13:46 +08:00
|
|
|
|
const { center = MyCesium.center } = options
|
|
|
|
|
|
|
2025-08-11 15:24:06 +08:00
|
|
|
|
// viewer.scene.globe.depthTestAgainstTerrain = true
|
|
|
|
|
|
|
|
|
|
|
|
viewer.imageryLayers.addImageryProvider(roadProvider)
|
|
|
|
|
|
viewer.camera.setView({
|
2025-08-16 10:13:46 +08:00
|
|
|
|
destination: Cesium.Cartesian3.fromDegrees(...center),
|
2025-08-11 15:24:06 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 不显示Cesium的Logo
|
|
|
|
|
|
viewer._cesiumWidget._creditContainer.style.display = 'none'
|
|
|
|
|
|
viewer.scene.sun.show = true //太阳和月亮
|
|
|
|
|
|
viewer.scene.moon.show = true
|
|
|
|
|
|
viewer.scene.skyAtmosphere.show = true //大气层,显示天空颜色
|
|
|
|
|
|
// 设置外天空盒
|
|
|
|
|
|
viewer.scene.skyBox = new Cesium.SkyBox({
|
|
|
|
|
|
sources: {
|
|
|
|
|
|
positiveX: '/images/Standard-Cube-Map/px1.png',
|
|
|
|
|
|
negativeX: '/images/Standard-Cube-Map/nx1.png',
|
|
|
|
|
|
positiveY: '/images/Standard-Cube-Map/pz.png',
|
|
|
|
|
|
negativeY: '/images/Standard-Cube-Map/nz1.png',
|
|
|
|
|
|
positiveZ: '/images/Standard-Cube-Map/py.png',
|
|
|
|
|
|
negativeZ: '/images/Standard-Cube-Map/ny1.png',
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
viewer.scene.skyBox.show = true //天空盒
|
|
|
|
|
|
|
|
|
|
|
|
// 创建指北针小部件并将其添加到地图
|
|
|
|
|
|
new CesiumNavigation(viewer, {
|
|
|
|
|
|
// 用于在使用重置导航重置地图视图时设置默认视图控制。接受的值是Cesium.Cartographic 和Cesium.Rectangle.
|
2025-08-16 10:13:46 +08:00
|
|
|
|
defaultResetView: Cesium.Cartographic.fromDegrees(...center),
|
2025-08-11 15:24:06 +08:00
|
|
|
|
// 用于启用或禁用罗盘。true是启用罗盘,false是禁用罗盘。默认值为true。如果将选项设置为false,则罗盘将不会添加到地图中。
|
|
|
|
|
|
enableCompass: true,
|
|
|
|
|
|
// 用于启用或禁用缩放控件。true是启用,false是禁用。默认值为true。如果将选项设置为false,则缩放控件 将不会添加到地图中。
|
|
|
|
|
|
enableZoomControls: true,
|
|
|
|
|
|
// 用于启用或禁用距离图例。true是启用,false是禁用。默认值为true。如果将选项设置为false,距离图例将不会添加到地图中。
|
|
|
|
|
|
enableDistanceLegend: true,
|
|
|
|
|
|
// 用于启用或禁用指南针外环。true是启用,false是禁用。默认值为true。如果将选项设置为false,则该环将可见但无效。
|
|
|
|
|
|
enableCompassOuterRing: true,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 取消未完成的在地图上移动类的操作(划线之类的)
|
|
|
|
|
|
cancelPreviousOperation() {
|
|
|
|
|
|
this.operations.forEach((operation) => operation.clear())
|
|
|
|
|
|
this.operations = []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-09-10 16:23:40 +08:00
|
|
|
|
* 根据四个角落坐标确定视口位置
|
|
|
|
|
|
* @param leftUp
|
|
|
|
|
|
* @param rightUp
|
|
|
|
|
|
* @param rightDown
|
|
|
|
|
|
* @param leftDown
|
2025-08-11 15:24:06 +08:00
|
|
|
|
*/
|
2025-09-10 16:23:40 +08:00
|
|
|
|
setClientByAllCorner(leftUp, rightUp, rightDown, leftDown) {
|
2025-08-16 10:13:46 +08:00
|
|
|
|
const [leftUpLon, leftUpLat] = leftUp
|
|
|
|
|
|
const [rightUpLon, rightUpLat] = rightUp
|
|
|
|
|
|
const [rightDownLon, rightDownLat] = rightDown
|
|
|
|
|
|
const [leftDownLon, leftDownLat] = leftDown
|
|
|
|
|
|
// 计算矩形区域的边界
|
|
|
|
|
|
const west = Math.min(leftDownLon, leftUpLon, rightDownLon, rightUpLon)
|
|
|
|
|
|
const east = Math.max(leftDownLon, leftUpLon, rightDownLon, rightUpLon)
|
|
|
|
|
|
const south = Math.min(leftDownLat, leftUpLat, rightDownLat, rightUpLat)
|
|
|
|
|
|
const north = Math.max(leftDownLat, leftUpLat, rightDownLat, rightUpLat)
|
|
|
|
|
|
// 创建矩形范围
|
|
|
|
|
|
const rectangle = Cesium.Rectangle.fromDegrees(west, south, east, north)
|
|
|
|
|
|
this.viewer.camera.setView({
|
|
|
|
|
|
destination: rectangle,
|
|
|
|
|
|
orientation: {
|
|
|
|
|
|
heading: 0.0,
|
|
|
|
|
|
pitch: -Cesium.Math.PI_OVER_TWO,
|
|
|
|
|
|
roll: 0.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2025-09-10 16:17:41 +08:00
|
|
|
|
|
2025-09-11 17:37:42 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 根据中心坐标确定视口位置
|
|
|
|
|
|
* @param position.longitude
|
|
|
|
|
|
* @param position.latitude
|
|
|
|
|
|
*/
|
|
|
|
|
|
setClientByCenter(position) {
|
|
|
|
|
|
this.viewer.camera.flyTo({
|
|
|
|
|
|
destination: Cesium.Cartesian3.fromDegrees(position.longitude, position.latitude, 500000), // 目标点坐标
|
|
|
|
|
|
orientation: {
|
|
|
|
|
|
heading: 0, // 航向角(弧度),0表示正北
|
|
|
|
|
|
pitch: Cesium.Math.toRadians(-90), // 俯仰角(弧度),-90度表示垂直向下看
|
|
|
|
|
|
roll: 0.0, // 翻滚角(弧度),0表示水平
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 移除军标
|
|
|
|
|
|
* @param id
|
|
|
|
|
|
*/
|
|
|
|
|
|
removePlotById(id) {
|
|
|
|
|
|
const targetIndex = this.plots.findIndex((item) => item.id === id)
|
|
|
|
|
|
if (targetIndex === -1) return
|
|
|
|
|
|
this.viewer.entities.remove(this.plots[targetIndex])
|
|
|
|
|
|
this.plots.splice(targetIndex, 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-10 16:17:41 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 添加军标(base64图片)
|
|
|
|
|
|
* @param base64
|
2025-09-11 17:37:42 +08:00
|
|
|
|
* @param screenPosition.x
|
|
|
|
|
|
* @param screenPosition.y
|
2025-09-10 16:17:41 +08:00
|
|
|
|
*/
|
2025-09-11 17:37:42 +08:00
|
|
|
|
addPlotByOffset(base64, screenPosition, customId) {
|
2025-09-10 16:17:41 +08:00
|
|
|
|
this.cancelPreviousOperation()
|
|
|
|
|
|
|
2025-09-10 16:23:40 +08:00
|
|
|
|
const position = getCatesian3FromPX(this.viewer, screenPosition)
|
|
|
|
|
|
if (!position) return false
|
2025-09-10 16:17:41 +08:00
|
|
|
|
|
2025-09-11 17:37:42 +08:00
|
|
|
|
const id = customId || Cesium.createGuid()
|
2025-09-10 16:17:41 +08:00
|
|
|
|
const isEnemy = false
|
|
|
|
|
|
const color = 'red'
|
|
|
|
|
|
const radius = 150000
|
2025-09-10 16:23:40 +08:00
|
|
|
|
const plot = {
|
2025-09-10 16:17:41 +08:00
|
|
|
|
id,
|
|
|
|
|
|
position,
|
|
|
|
|
|
billboard: {
|
|
|
|
|
|
image: base64,
|
|
|
|
|
|
width: 50,
|
|
|
|
|
|
height: 50,
|
2025-09-11 17:37:42 +08:00
|
|
|
|
scaleByDistance: new Cesium.NearFarScalar(1000000.0, 1.0, 10000000.0, 0.2), // 重点:设置随距离缩放
|
2025-09-10 16:17:41 +08:00
|
|
|
|
},
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
type: MyCesium.ENTITY_TYPES.IMAGE,
|
|
|
|
|
|
color,
|
|
|
|
|
|
isEnemy,
|
|
|
|
|
|
radius,
|
|
|
|
|
|
collisions: new Set(),
|
|
|
|
|
|
},
|
2025-09-10 16:23:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
this.viewer.entities.add(plot)
|
|
|
|
|
|
this.plots.push(plot)
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 将屏幕坐标转换为世界坐标(在椭球体表面)
|
|
|
|
|
|
const cartesian = this.viewer.camera.pickEllipsoid(screenPosition, this.viewer.scene.globe.ellipsoid)
|
|
|
|
|
|
if (Cesium.defined(cartesian)) {
|
|
|
|
|
|
// 2. 将世界坐标 (Cartesian3) 转换为地理坐标 Cartographic (弧度)
|
|
|
|
|
|
const cartographic = Cesium.Cartographic.fromCartesian(cartesian)
|
|
|
|
|
|
// 3. 将弧度转换为度数
|
|
|
|
|
|
const longitude = Cesium.Math.toDegrees(cartographic.longitude)
|
|
|
|
|
|
const latitude = Cesium.Math.toDegrees(cartographic.latitude)
|
|
|
|
|
|
return { plotId: id, longitude, latitude }
|
|
|
|
|
|
}
|
2025-09-10 16:17:41 +08:00
|
|
|
|
}
|
2025-09-11 17:37:42 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 添加军标(base64图片)
|
|
|
|
|
|
* @param base64
|
|
|
|
|
|
* @param location.lon
|
|
|
|
|
|
* @param location.lat
|
|
|
|
|
|
*/
|
|
|
|
|
|
addPlotByLonLat(base64, location, customId) {
|
|
|
|
|
|
this.cancelPreviousOperation()
|
|
|
|
|
|
|
|
|
|
|
|
const position = Cesium.Cartesian3.fromDegrees(location.lon, location.lat, 0)
|
|
|
|
|
|
if (!position) return false
|
|
|
|
|
|
|
|
|
|
|
|
const id = customId || Cesium.createGuid()
|
|
|
|
|
|
const isEnemy = false
|
|
|
|
|
|
const color = 'red'
|
|
|
|
|
|
const radius = 150000
|
|
|
|
|
|
const plot = {
|
|
|
|
|
|
id,
|
|
|
|
|
|
position,
|
|
|
|
|
|
billboard: {
|
|
|
|
|
|
image: base64,
|
|
|
|
|
|
width: 50,
|
|
|
|
|
|
height: 50,
|
|
|
|
|
|
scaleByDistance: new Cesium.NearFarScalar(1000000.0, 1.0, 10000000.0, 0.2), // 重点:设置随距离缩放
|
|
|
|
|
|
},
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
type: MyCesium.ENTITY_TYPES.IMAGE,
|
|
|
|
|
|
color,
|
|
|
|
|
|
isEnemy,
|
|
|
|
|
|
radius,
|
|
|
|
|
|
collisions: new Set(),
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
this.viewer.entities.add(plot)
|
|
|
|
|
|
this.plots.push(plot)
|
|
|
|
|
|
}
|
2025-09-18 20:31:52 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 连续坐标绘制路线
|
|
|
|
|
|
* @param coordinates [[longitude, latitude], ...]
|
|
|
|
|
|
*/
|
|
|
|
|
|
drawRouteByCoordinates(coordinates) {
|
|
|
|
|
|
const route = {
|
|
|
|
|
|
id: Cesium.createGuid(),
|
|
|
|
|
|
polyline: {
|
|
|
|
|
|
positions: Cesium.Cartesian3.fromDegreesArray(coordinates.flat()),
|
|
|
|
|
|
width: 2,
|
|
|
|
|
|
material: Cesium.Color.fromCssColorString('red'),
|
|
|
|
|
|
show: true,
|
|
|
|
|
|
clampToGround: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
properties: {},
|
|
|
|
|
|
}
|
|
|
|
|
|
this.viewer.entities.add(route)
|
|
|
|
|
|
this.routes.push(route)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 连续坐标指定军标移动
|
|
|
|
|
|
* @param plotId 军标id
|
|
|
|
|
|
* @param coordinates [[longitude, latitude], ...]
|
|
|
|
|
|
*/
|
|
|
|
|
|
initPlotMoving(plot, coordinates) {
|
|
|
|
|
|
// viewer.clock 控制了场景中时间的变化,从而驱动动画。
|
|
|
|
|
|
const startTime = Cesium.JulianDate.now()
|
|
|
|
|
|
const stopTime = Cesium.JulianDate.addSeconds(startTime, 1, new Cesium.JulianDate()) // 动画持续1秒
|
|
|
|
|
|
this.viewer.clock.startTime = startTime.clone()
|
|
|
|
|
|
this.viewer.clock.stopTime = stopTime.clone()
|
|
|
|
|
|
this.viewer.clock.currentTime = startTime.clone()
|
|
|
|
|
|
this.viewer.clock.clockRange = Cesium.ClockRange.CLAMPED // 动画播放一次后停止:cite[2]
|
|
|
|
|
|
this.viewer.clock.multiplier = 1 // 时间流逝速度,默认为1
|
|
|
|
|
|
const positionProperty = new Cesium.SampledPositionProperty()
|
|
|
|
|
|
// 假设你获取到的路线坐标点数组是 positions(Cartesian3数组)
|
|
|
|
|
|
// 并且这些点应该在1秒内完成移动
|
|
|
|
|
|
const positions = Cesium.Cartesian3.fromDegreesArray(coordinates.flat()) // 你的坐标路线
|
|
|
|
|
|
const sampleTimeStep = 1 / (positions.length - 1) // 计算每个样本点之间的时间间隔(总时间为1秒)
|
|
|
|
|
|
for (let i = 0; i < positions.length; i++) {
|
|
|
|
|
|
const time = Cesium.JulianDate.addSeconds(startTime, i * sampleTimeStep, new Cesium.JulianDate())
|
|
|
|
|
|
positionProperty.addSample(time, positions[i])
|
|
|
|
|
|
}
|
|
|
|
|
|
// 将实体的位置关联到 SampledPositionProperty
|
|
|
|
|
|
plot.position = positionProperty
|
|
|
|
|
|
// 可选:让图标朝向运动方向
|
|
|
|
|
|
// plot.orientation = new Cesium.VelocityOrientationProperty(positionProperty) // cite[8]
|
|
|
|
|
|
|
|
|
|
|
|
plot._isMoving = true
|
|
|
|
|
|
}
|
|
|
|
|
|
movePlotByCoordinates(plotId, coordinates) {
|
|
|
|
|
|
// 实体
|
|
|
|
|
|
const targetPlot = this.viewer.entities.getById(plotId)
|
|
|
|
|
|
if (targetPlot._isMoving === true) {
|
|
|
|
|
|
// 1. 获取新的路线坐标点(假设是同步获取的,或者是在回调中)
|
|
|
|
|
|
const positions = Cesium.Cartesian3.fromDegreesArray(coordinates.flat()) // 你的坐标路线
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 清除旧的位置样本
|
|
|
|
|
|
targetPlot.position._property._values = []
|
|
|
|
|
|
targetPlot.position._property._times = []
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 计算新的开始时间和结束时间
|
|
|
|
|
|
const newStartTime = Cesium.JulianDate.now()
|
|
|
|
|
|
const newStopTime = Cesium.JulianDate.addSeconds(newStartTime, 1, new Cesium.JulianDate())
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 添加新的位置样本
|
|
|
|
|
|
const newSampleTimeStep = 1 / (positions.length - 1)
|
|
|
|
|
|
for (let i = 0; i < positions.length; i++) {
|
|
|
|
|
|
const time = Cesium.JulianDate.addSeconds(newStartTime, i * newSampleTimeStep, new Cesium.JulianDate())
|
|
|
|
|
|
targetPlot.position.addSample(time, positions[i])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 5. 重置时钟以立即开始新的动画
|
|
|
|
|
|
this.viewer.clock.startTime = newStartTime.clone()
|
|
|
|
|
|
this.viewer.clock.stopTime = newStopTime.clone()
|
|
|
|
|
|
this.viewer.clock.currentTime = newStartTime.clone()
|
|
|
|
|
|
this.viewer.clock.shouldAnimate = true // 确保动画开始播放:cite[2]
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.initPlotMoving(targetPlot, coordinates)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-11 15:24:06 +08:00
|
|
|
|
}
|