|
|
@@ -0,0 +1,533 @@
|
|
|
+<script setup lang="ts">
|
|
|
+import { onMounted, onUnmounted, ref, reactive, watch } from 'vue'
|
|
|
+import * as THREE from 'three'
|
|
|
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
|
|
+import { Sky } from 'three/examples/jsm/objects/Sky.js'
|
|
|
+import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
|
|
+import WaterLevelLabel from './WaterLevelLabel.vue'
|
|
|
+import {
|
|
|
+ sceneLabels,
|
|
|
+ type WaterLevelLabelConfig,
|
|
|
+} from '../config/sceneConfig'
|
|
|
+
|
|
|
+const containerRef = ref<HTMLDivElement>()
|
|
|
+
|
|
|
+let scene: THREE.Scene
|
|
|
+let camera: THREE.PerspectiveCamera
|
|
|
+let renderer: THREE.WebGLRenderer
|
|
|
+let controls: OrbitControls
|
|
|
+let animationId: number
|
|
|
+let sky: Sky
|
|
|
+
|
|
|
+let ducaoGroup: THREE.Group | null = null
|
|
|
+let raycaster: THREE.Raycaster
|
|
|
+let mouse: THREE.Vector2
|
|
|
+
|
|
|
+const pickedPosition = ref<{ x: number; y: number; z: number } | null>({ x: 0, y: 0, z: 0 })
|
|
|
+const cameraInfo = ref({
|
|
|
+ position: { x: 0, y: 0, z: 0 },
|
|
|
+ target: { x: 0, y: 0, z: 0 },
|
|
|
+ distance: 0,
|
|
|
+ minDistance: 0,
|
|
|
+ maxDistance: 0,
|
|
|
+})
|
|
|
+
|
|
|
+const showCoordinatePanel = ref(false)
|
|
|
+const showCameraPanel = ref(false)
|
|
|
+
|
|
|
+let sceneInitialized = false
|
|
|
+
|
|
|
+const modelTransform = reactive({ positionX: 0, positionY: 0, positionZ: 0, rotationX: 0, rotationY: 0, rotationZ: 0, scaleX: 1, scaleY: 1, scaleZ: 1 })
|
|
|
+
|
|
|
+watch(modelTransform, () => {
|
|
|
+ if (!ducaoGroup) return
|
|
|
+ const t = modelTransform
|
|
|
+ ducaoGroup.position.set(t.positionX, t.positionY, t.positionZ)
|
|
|
+ ducaoGroup.rotation.set(THREE.MathUtils.degToRad(t.rotationX), THREE.MathUtils.degToRad(t.rotationY), THREE.MathUtils.degToRad(t.rotationZ))
|
|
|
+ ducaoGroup.scale.set(t.scaleX, t.scaleY, t.scaleZ)
|
|
|
+}, { deep: true })
|
|
|
+
|
|
|
+// ==================== Props(嵌入其他项目时使用)====================
|
|
|
+const props = withDefaults(defineProps<{
|
|
|
+ labelValues?: Record<string, number>
|
|
|
+}>(), {
|
|
|
+ labelValues: () => ({}),
|
|
|
+})
|
|
|
+
|
|
|
+// ==================== 水位标签(仅加载渡槽场景的标签)====================
|
|
|
+interface LabelData {
|
|
|
+ config: WaterLevelLabelConfig
|
|
|
+ componentRef: ReturnType<typeof ref<InstanceType<typeof WaterLevelLabel> | null>>
|
|
|
+}
|
|
|
+const labelDataList: LabelData[] = []
|
|
|
+const labelValues = reactive<Record<string, number>>({})
|
|
|
+
|
|
|
+function initLabelData() {
|
|
|
+ labelDataList.length = 0
|
|
|
+ for (const cfg of sceneLabels.ducao) {
|
|
|
+ const externalValue = props.labelValues[cfg.id]
|
|
|
+ labelValues[cfg.id] = externalValue !== undefined ? externalValue : cfg.initialValue
|
|
|
+ labelDataList.push({
|
|
|
+ config: cfg,
|
|
|
+ componentRef: ref<InstanceType<typeof WaterLevelLabel> | null>(null),
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+initLabelData()
|
|
|
+
|
|
|
+watch(() => props.labelValues, (levels) => {
|
|
|
+ for (const [id, value] of Object.entries(levels)) {
|
|
|
+ if (labelValues[id] !== undefined) {
|
|
|
+ labelValues[id] = value
|
|
|
+ }
|
|
|
+ }
|
|
|
+}, { deep: true })
|
|
|
+
|
|
|
+function setLabelValue(id: string, value: number) {
|
|
|
+ labelValues[id] = value
|
|
|
+}
|
|
|
+
|
|
|
+function setLabelValues(levels: Record<string, number>) {
|
|
|
+ for (const [id, value] of Object.entries(levels)) {
|
|
|
+ labelValues[id] = value
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 释放所有 Three.js 资源(可重复调用)
|
|
|
+function disposeScene() {
|
|
|
+ cancelAnimationFrame(animationId)
|
|
|
+ animationId = 0
|
|
|
+
|
|
|
+ labelDataList.forEach(d => d.componentRef.value?.dispose())
|
|
|
+
|
|
|
+ if (ducaoGroup) {
|
|
|
+ if (scene) scene.remove(ducaoGroup)
|
|
|
+ ducaoGroup.traverse((child) => {
|
|
|
+ const mesh = child as THREE.Mesh
|
|
|
+ if (mesh.isMesh) {
|
|
|
+ mesh.geometry?.dispose()
|
|
|
+ if (Array.isArray(mesh.material)) {
|
|
|
+ mesh.material.forEach(m => m.dispose())
|
|
|
+ } else {
|
|
|
+ mesh.material?.dispose()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ ducaoGroup = null
|
|
|
+ }
|
|
|
+
|
|
|
+ if (sky) {
|
|
|
+ if (scene) scene.remove(sky)
|
|
|
+ sky.material.dispose()
|
|
|
+ sky = null
|
|
|
+ }
|
|
|
+
|
|
|
+ if (scene) {
|
|
|
+ while (scene.children.length > 0) {
|
|
|
+ scene.remove(scene.children[0])
|
|
|
+ }
|
|
|
+ scene = null
|
|
|
+ }
|
|
|
+
|
|
|
+ if (controls) {
|
|
|
+ controls.dispose()
|
|
|
+ controls = null
|
|
|
+ }
|
|
|
+
|
|
|
+ if (renderer) {
|
|
|
+ const domEl = renderer.domElement
|
|
|
+ if (domEl && domEl.parentNode) {
|
|
|
+ domEl.parentNode.removeChild(domEl)
|
|
|
+ }
|
|
|
+ renderer.forceContextLoss()
|
|
|
+ renderer.dispose()
|
|
|
+ renderer = null
|
|
|
+ }
|
|
|
+
|
|
|
+ camera = null
|
|
|
+ sceneInitialized = false
|
|
|
+}
|
|
|
+
|
|
|
+function initScene() {
|
|
|
+ if (sceneInitialized) {
|
|
|
+ disposeScene()
|
|
|
+ }
|
|
|
+ const container = containerRef.value!
|
|
|
+
|
|
|
+ scene = new THREE.Scene()
|
|
|
+ scene.background = new THREE.Color(0x87ceeb)
|
|
|
+
|
|
|
+ camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.1, 100000)
|
|
|
+
|
|
|
+ renderer = new THREE.WebGLRenderer({ antialias: true })
|
|
|
+ renderer.setSize(container.clientWidth, container.clientHeight)
|
|
|
+ renderer.domElement.style.width = '100%'
|
|
|
+ renderer.domElement.style.height = '100%'
|
|
|
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
|
|
|
+ renderer.toneMapping = THREE.ACESFilmicToneMapping
|
|
|
+ renderer.toneMappingExposure = 1.0
|
|
|
+ container.appendChild(renderer.domElement)
|
|
|
+
|
|
|
+ controls = new OrbitControls(camera, renderer.domElement)
|
|
|
+ controls.enableDamping = true
|
|
|
+ controls.dampingFactor = 0.03
|
|
|
+ controls.screenSpacePanning = false
|
|
|
+ controls.mouseButtons = {
|
|
|
+ LEFT: THREE.MOUSE.PAN,
|
|
|
+ MIDDLE: THREE.MOUSE.DOLLY,
|
|
|
+ RIGHT: THREE.MOUSE.ROTATE,
|
|
|
+ }
|
|
|
+ controls.maxPolarAngle = Math.PI / 2.1
|
|
|
+ controls.minDistance = 5
|
|
|
+ controls.maxDistance = 50000
|
|
|
+
|
|
|
+ sky = new Sky()
|
|
|
+ sky.scale.setScalar(10000000)
|
|
|
+ scene.add(sky)
|
|
|
+
|
|
|
+ const skyUniforms = sky.material.uniforms
|
|
|
+ skyUniforms.turbidity.value = 6
|
|
|
+ skyUniforms.rayleigh.value = 0.1
|
|
|
+ skyUniforms.mieCoefficient.value = 0.005
|
|
|
+ skyUniforms.mieDirectionalG.value = 0.7
|
|
|
+ skyUniforms.sunPosition.value.set(20, 30, 10)
|
|
|
+
|
|
|
+ const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.8)
|
|
|
+ scene.add(hemisphereLight)
|
|
|
+
|
|
|
+ const dirLight = new THREE.DirectionalLight(0xffffff, 1.2)
|
|
|
+ dirLight.position.set(100, 100, 50)
|
|
|
+ scene.add(dirLight)
|
|
|
+
|
|
|
+ raycaster = new THREE.Raycaster()
|
|
|
+ mouse = new THREE.Vector2()
|
|
|
+
|
|
|
+ loadDucaoModel()
|
|
|
+ labelDataList.forEach(d => d.componentRef.value?.init(scene, camera))
|
|
|
+ sceneInitialized = true
|
|
|
+ animate()
|
|
|
+}
|
|
|
+
|
|
|
+async function loadDucaoModel() {
|
|
|
+ try {
|
|
|
+ const loader = new GLTFLoader()
|
|
|
+ const urls = ['NoLod_0.glb', 'NoLod_1.glb', 'NoLod_2.glb', 'NoLod_3.glb']
|
|
|
+ const group = new THREE.Group()
|
|
|
+
|
|
|
+ for (const url of urls) {
|
|
|
+ try {
|
|
|
+ const gltf = await loader.loadAsync(`/ducao/${url}`)
|
|
|
+ group.add(gltf.scene)
|
|
|
+ } catch (_) {}
|
|
|
+ }
|
|
|
+
|
|
|
+ const box = new THREE.Box3().setFromObject(group)
|
|
|
+ const center = box.getCenter(new THREE.Vector3())
|
|
|
+ group.position.copy(center).multiplyScalar(-1)
|
|
|
+
|
|
|
+ ducaoGroup = group
|
|
|
+ scene.add(group)
|
|
|
+
|
|
|
+ controls.target.set(360.829, 0, 1725.719)
|
|
|
+ camera.position.set(410.009, 23.458, 1740.156)
|
|
|
+ controls.update()
|
|
|
+ } catch (_) {}
|
|
|
+}
|
|
|
+
|
|
|
+function onMouseClick(event: MouseEvent) {
|
|
|
+ const target = event.target as HTMLElement
|
|
|
+ if (target.closest('.panel') || target.closest('.toolbar')) return
|
|
|
+ if (!containerRef.value) return
|
|
|
+ const rect = containerRef.value.getBoundingClientRect()
|
|
|
+ mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
|
|
|
+ mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1
|
|
|
+
|
|
|
+ raycaster.setFromCamera(mouse, camera)
|
|
|
+ const intersects = raycaster.intersectObject(scene, true)
|
|
|
+ if (intersects.length > 0) {
|
|
|
+ const point = intersects[0].point
|
|
|
+ pickedPosition.value = { x: Math.round(point.x * 100) / 100, y: Math.round(point.y * 100) / 100, z: Math.round(point.z * 100) / 100 }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function animate() {
|
|
|
+ animationId = requestAnimationFrame(animate)
|
|
|
+ controls.update()
|
|
|
+ labelDataList.forEach(d => d.componentRef.value?.tick())
|
|
|
+
|
|
|
+ cameraInfo.value.position = {
|
|
|
+ x: Number(camera.position.x.toFixed(3)),
|
|
|
+ y: Number(camera.position.y.toFixed(3)),
|
|
|
+ z: Number(camera.position.z.toFixed(3)),
|
|
|
+ }
|
|
|
+ cameraInfo.value.target = {
|
|
|
+ x: Number(controls.target.x.toFixed(3)),
|
|
|
+ y: Number(controls.target.y.toFixed(3)),
|
|
|
+ z: Number(controls.target.z.toFixed(3)),
|
|
|
+ }
|
|
|
+ cameraInfo.value.distance = Number(camera.position.distanceTo(controls.target).toFixed(3))
|
|
|
+ cameraInfo.value.minDistance = controls.minDistance
|
|
|
+ cameraInfo.value.maxDistance = controls.maxDistance
|
|
|
+
|
|
|
+ renderer.render(scene, camera)
|
|
|
+}
|
|
|
+
|
|
|
+function onResize() {
|
|
|
+ if (!containerRef.value) return
|
|
|
+ const width = containerRef.value.clientWidth
|
|
|
+ const height = containerRef.value.clientHeight
|
|
|
+ camera.aspect = width / height
|
|
|
+ camera.updateProjectionMatrix()
|
|
|
+ renderer.setSize(width, height)
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ initScene()
|
|
|
+ window.addEventListener('resize', onResize)
|
|
|
+ window.addEventListener('click', onMouseClick)
|
|
|
+})
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ window.removeEventListener('resize', onResize)
|
|
|
+ window.removeEventListener('click', onMouseClick)
|
|
|
+ disposeScene()
|
|
|
+})
|
|
|
+
|
|
|
+defineExpose({
|
|
|
+ labelDataList,
|
|
|
+ labelValues,
|
|
|
+ setLabelValue,
|
|
|
+ setLabelValues,
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <div ref="containerRef" class="scene-container" />
|
|
|
+ <WaterLevelLabel
|
|
|
+ v-for="data in labelDataList"
|
|
|
+ :key="data.config.id"
|
|
|
+ :ref="(el: any) => { if (el) data.componentRef.value = el }"
|
|
|
+ :label-id="data.config.id"
|
|
|
+ :type="data.config.type"
|
|
|
+ :position-x="data.config.positionX"
|
|
|
+ :position-y="data.config.positionY"
|
|
|
+ :position-z="data.config.positionZ"
|
|
|
+ :value="labelValues[data.config.id]"
|
|
|
+ />
|
|
|
+ <div class="toolbar">
|
|
|
+ <button class="toolbar-btn" :class="{ active: showCoordinatePanel }" @click="showCoordinatePanel = !showCoordinatePanel">坐标</button>
|
|
|
+ <button class="toolbar-btn" :class="{ active: showCameraPanel }" @click="showCameraPanel = !showCameraPanel">相机</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="showCoordinatePanel && pickedPosition" class="panel coordinate-panel">
|
|
|
+ <div class="panel-header">
|
|
|
+ <span class="panel-title">拾取坐标 (m)</span>
|
|
|
+ <button class="toggle-btn" @click="showCoordinatePanel = false">×</button>
|
|
|
+ </div>
|
|
|
+ <div class="coordinate-item">
|
|
|
+ <span class="coordinate-label">X:</span>
|
|
|
+ <span class="coordinate-value">{{ pickedPosition.x }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="coordinate-item">
|
|
|
+ <span class="coordinate-label">Y:</span>
|
|
|
+ <span class="coordinate-value">{{ pickedPosition.y }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="coordinate-item">
|
|
|
+ <span class="coordinate-label">Z:</span>
|
|
|
+ <span class="coordinate-value">{{ pickedPosition.z }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="showCameraPanel" class="panel camera-panel">
|
|
|
+ <div class="panel-header">
|
|
|
+ <span class="panel-title">相机信息</span>
|
|
|
+ <button class="toggle-btn" @click="showCameraPanel = false">×</button>
|
|
|
+ </div>
|
|
|
+ <div class="camera-section">
|
|
|
+ <div class="section-label">位置 (m)</div>
|
|
|
+ <div class="camera-item">
|
|
|
+ <span class="camera-label">X:</span>
|
|
|
+ <span class="camera-value">{{ cameraInfo.position.x }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="camera-item">
|
|
|
+ <span class="camera-label">Y:</span>
|
|
|
+ <span class="camera-value">{{ cameraInfo.position.y }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="camera-item">
|
|
|
+ <span class="camera-label">Z:</span>
|
|
|
+ <span class="camera-value">{{ cameraInfo.position.z }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="camera-section">
|
|
|
+ <div class="section-label">目标点 (m)</div>
|
|
|
+ <div class="camera-item">
|
|
|
+ <span class="camera-label">X:</span>
|
|
|
+ <span class="camera-value">{{ cameraInfo.target.x }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="camera-item">
|
|
|
+ <span class="camera-label">Y:</span>
|
|
|
+ <span class="camera-value">{{ cameraInfo.target.y }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="camera-item">
|
|
|
+ <span class="camera-label">Z:</span>
|
|
|
+ <span class="camera-value">{{ cameraInfo.target.z }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="camera-section">
|
|
|
+ <div class="section-label">缩放距离 (m)</div>
|
|
|
+ <div class="camera-item">
|
|
|
+ <span class="camera-label">当前:</span>
|
|
|
+ <span class="camera-value">{{ cameraInfo.distance }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="camera-item">
|
|
|
+ <span class="camera-label">最近:</span>
|
|
|
+ <span class="camera-value">{{ cameraInfo.minDistance }}m</span>
|
|
|
+ </div>
|
|
|
+ <div class="camera-item">
|
|
|
+ <span class="camera-label">最远:</span>
|
|
|
+ <span class="camera-value">{{ cameraInfo.maxDistance }}m</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.scene-container {
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ width: 100vw;
|
|
|
+ height: 100vh;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.toolbar {
|
|
|
+ position: fixed;
|
|
|
+ top: 20px;
|
|
|
+ right: 20px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+ z-index: 1001;
|
|
|
+}
|
|
|
+
|
|
|
+.toolbar-btn {
|
|
|
+ padding: 10px 16px;
|
|
|
+ border: none;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: rgba(0, 0, 0, 0.85);
|
|
|
+ color: #888;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: bold;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s;
|
|
|
+ min-width: 70px;
|
|
|
+}
|
|
|
+
|
|
|
+.toolbar-btn:hover {
|
|
|
+ background: rgba(30, 30, 30, 0.9);
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.toolbar-btn.active {
|
|
|
+ color: #fff;
|
|
|
+ box-shadow: 0 0 10px rgba(255, 255, 255, 0.2);
|
|
|
+}
|
|
|
+
|
|
|
+.toolbar-btn:nth-child(1).active { color: #4fc3f7; }
|
|
|
+.toolbar-btn:nth-child(2).active { color: #ff9800; }
|
|
|
+
|
|
|
+.panel {
|
|
|
+ position: fixed;
|
|
|
+ top: 20px;
|
|
|
+ left: 20px;
|
|
|
+ background: rgba(0, 0, 0, 0.85);
|
|
|
+ color: white;
|
|
|
+ padding: 15px;
|
|
|
+ border-radius: 8px;
|
|
|
+ font-family: 'Courier New', monospace;
|
|
|
+ z-index: 1000;
|
|
|
+ max-height: calc(100vh - 40px);
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.coordinate-panel { min-width: 200px; }
|
|
|
+.camera-panel { min-width: 200px; }
|
|
|
+
|
|
|
+.panel-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ padding-bottom: 8px;
|
|
|
+ border-bottom: 1px solid rgba(255, 255, 255, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+.panel-title {
|
|
|
+ font-weight: bold;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.coordinate-panel .panel-title { color: #4fc3f7; }
|
|
|
+.camera-panel .panel-title { color: #ff9800; }
|
|
|
+
|
|
|
+.toggle-btn {
|
|
|
+ width: 22px;
|
|
|
+ height: 22px;
|
|
|
+ border: none;
|
|
|
+ border-radius: 4px;
|
|
|
+ background: rgba(255, 255, 255, 0.15);
|
|
|
+ color: white;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 1;
|
|
|
+ cursor: pointer;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ transition: background 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+.toggle-btn:hover {
|
|
|
+ background: rgba(255, 255, 255, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+.coordinate-item {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin: 5px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.coordinate-label {
|
|
|
+ color: #81c784;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+
|
|
|
+.coordinate-value {
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.camera-section {
|
|
|
+ margin-bottom: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.section-label {
|
|
|
+ color: #81c784;
|
|
|
+ font-size: 11px;
|
|
|
+ margin-bottom: 5px;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+
|
|
|
+.camera-item {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin: 3px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.camera-label {
|
|
|
+ color: #4fc3f7;
|
|
|
+}
|
|
|
+
|
|
|
+.camera-value {
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+</style>
|