|
@@ -25,7 +25,7 @@
|
|
|
<el-row gutter="20">
|
|
|
<el-col :span="2"></el-col>
|
|
|
<el-col v-for="(item, index) in cloudbtnbox" :key="index" :span="4">
|
|
|
- <el-button style="width: 100%" @click="openSubDialog(item.btnname)">
|
|
|
+ <el-button style="width: 100%" @click="handleButtonClick(item.btnname)">
|
|
|
<el-image
|
|
|
:src="getImgPath(item.url)"
|
|
|
alt="img"
|
|
@@ -37,37 +37,184 @@
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
</div>
|
|
|
- <!-- 动态加载子弹窗 -->
|
|
|
- <component
|
|
|
- v-for="(dialog, index) in activeSubDialogs"
|
|
|
- :key="index"
|
|
|
- :is="dialog.component"
|
|
|
- v-model="dialog.visible"
|
|
|
- v-bind="dialog.props"
|
|
|
- @confirm="handleSubDialogConfirm(dialog.type, $event)"
|
|
|
- @cancel="closeSubDialog(dialog.type)"
|
|
|
+
|
|
|
+ <!-- 文件选择对话框 -->
|
|
|
+ <FileSelectDialog
|
|
|
+ v-model="fileSelectDialogVisible"
|
|
|
+ :renderer="renderer"
|
|
|
+ :three-scene-ref="threeSceneRef"
|
|
|
+ :scene="scene"
|
|
|
+ :plt-data="pltData"
|
|
|
+ @confirm="handleFileSelectConfirm"
|
|
|
+ @cancel="fileSelectDialogVisible = false"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- 域对话框 -->
|
|
|
+ <DomainDialog
|
|
|
+ v-model="domainDialogVisible"
|
|
|
+ :renderer="renderer"
|
|
|
+ :three-scene-ref="threeSceneRef"
|
|
|
+ :scene="scene"
|
|
|
+ :plt-data="pltData"
|
|
|
+ :active-zone="pltStore.currentZone"
|
|
|
+ :variable-options="pltData?.metadata?.variables || []"
|
|
|
+ :initial-data="{ ranges: calculateVariableRanges() }"
|
|
|
+ @confirm="handleDomainConfirm"
|
|
|
+ @cancel="domainDialogVisible = false"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- 云图对话框 -->
|
|
|
+ <CloudMapDialog
|
|
|
+ v-model="cloudMapDialogVisible"
|
|
|
+ :renderer="renderer"
|
|
|
+ :three-scene-ref="threeSceneRef"
|
|
|
+ :scene="scene"
|
|
|
+ :plt-data="pltData"
|
|
|
+ :active-zone="pltStore.currentZone"
|
|
|
+ :variable-options="pltData?.metadata?.variables || []"
|
|
|
+ :initial-data="{ ranges: calculateVariableRanges() }"
|
|
|
+ @confirm="handleCloudMapConfirm"
|
|
|
+ @cancel="cloudMapDialogVisible = false"
|
|
|
/>
|
|
|
+
|
|
|
+ <!-- 色卡对话框 -->
|
|
|
+ <ColorCardDialog
|
|
|
+ v-model="colorCardDialogVisible"
|
|
|
+ :renderer="renderer"
|
|
|
+ :three-scene-ref="threeSceneRef"
|
|
|
+ :scene="scene"
|
|
|
+ :plt-data="pltData"
|
|
|
+ @confirm="handleColorCardConfirm"
|
|
|
+ @cancel="colorCardDialogVisible = false"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- 等值线对话框 -->
|
|
|
+ <ContourDialog
|
|
|
+ v-model="contourDialogVisible"
|
|
|
+ :renderer="renderer"
|
|
|
+ :three-scene-ref="threeSceneRef"
|
|
|
+ :scene="scene"
|
|
|
+ :plt-data="pltData"
|
|
|
+ :active-zone="pltStore.currentZone"
|
|
|
+ :variable-options="pltData?.metadata?.variables || []"
|
|
|
+ :initial-data="{ ranges: calculateVariableRanges() }"
|
|
|
+ @confirm="handleContourConfirm"
|
|
|
+ @cancel="contourDialogVisible = false"
|
|
|
+ />
|
|
|
+
|
|
|
<div
|
|
|
style="overflow: auto"
|
|
|
v-loading="isLoading"
|
|
|
element-loading-text="拼命加载中..."
|
|
|
>
|
|
|
- <cloudChart height="400px" :data="pltData"/>
|
|
|
+ <div style="height: 400px; position: relative">
|
|
|
+ <ThreeScene
|
|
|
+ ref="threeSceneRef"
|
|
|
+ :api-data="pltData"
|
|
|
+ :scene-config="sceneConfig"
|
|
|
+ :camera-config="cameraConfig"
|
|
|
+ :controls-config="controlsConfig"
|
|
|
+ :show-helpers="true"
|
|
|
+ :helpers-config="helpersConfig"
|
|
|
+ :light-config="lightConfig"
|
|
|
+ style="width: 100%; height: 100%"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <!-- 色卡 -->
|
|
|
+ <div
|
|
|
+ class="color-bar-container"
|
|
|
+ v-if="colorBarConfig && colorBarConfig.visible"
|
|
|
+ >
|
|
|
+ <div class="color-bar" :style="colorBarStyle"></div>
|
|
|
+ <div class="color-bar-labels">
|
|
|
+ <span v-for="(label, index) in colorBarLabels" :key="index">{{
|
|
|
+ label
|
|
|
+ }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="color-bar-title" v-if="colorBarConfig.showTitle">
|
|
|
+ {{ colorBarConfig.title || "Color Bar" }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</el-dialog>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { defineProps, defineEmits } from "vue"
|
|
|
+import { ref, markRaw, nextTick } from "vue"
|
|
|
import FileSelectDialog from "./dialog/FileSelectDialog.vue"
|
|
|
import DomainDialog from "./dialog/DomainDialog.vue"
|
|
|
import CloudMapDialog from "./dialog/CloudMapDialog.vue"
|
|
|
import ColorCardDialog from "./dialog/ColorCardDialog.vue"
|
|
|
import ContourDialog from "./dialog/ContourDialog.vue"
|
|
|
-import cloudChart from "@/views/threejsView/index.vue" // 云图
|
|
|
-import h5wasm from 'h5wasm'
|
|
|
-import { usePltDataStore } from '@/store/modules/pltData'
|
|
|
+import cloudChart from "@/views/threejsView/index.vue"
|
|
|
+import h5wasm from "h5wasm"
|
|
|
+import { usePltDataStore } from "@/store/modules/pltData"
|
|
|
+import { PltDataRenderer } from "@/views/threejsView/utils/renderers/PltDataRenderer"
|
|
|
+import ThreeScene from "@/components/ThreeScene/ThreeScene.vue"
|
|
|
+import * as THREE from "three"
|
|
|
+
|
|
|
+const scene = new THREE.Scene()
|
|
|
+const camera = new THREE.PerspectiveCamera(
|
|
|
+ 75,
|
|
|
+ window.innerWidth / window.innerHeight,
|
|
|
+ 0.1,
|
|
|
+ 1000
|
|
|
+)
|
|
|
+
|
|
|
+const threeSceneRef = ref(null)
|
|
|
+
|
|
|
+// 对话框可见性状态
|
|
|
+const fileSelectDialogVisible = ref(false)
|
|
|
+const domainDialogVisible = ref(false)
|
|
|
+const cloudMapDialogVisible = ref(false)
|
|
|
+const colorCardDialogVisible = ref(false)
|
|
|
+const contourDialogVisible = ref(false)
|
|
|
+
|
|
|
+const sceneConfig = {
|
|
|
+ backgroundColor: 0xffffff,
|
|
|
+ backgroundColor: 0xc7c7c7c7,
|
|
|
+ fog: true,
|
|
|
+ fogOptions: {
|
|
|
+ color: 0xc7c7c7c7,
|
|
|
+ near: 5,
|
|
|
+ far: 1000
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const cameraConfig = {
|
|
|
+ type: "perspective",
|
|
|
+ position: { x: 0, y: 2, z: 10 },
|
|
|
+ lookAt: { x: 0, y: 0, z: 0 },
|
|
|
+ fov: 60
|
|
|
+}
|
|
|
+
|
|
|
+const controlsConfig = {
|
|
|
+ enableDamping: true,
|
|
|
+ dampingFactor: 0.05,
|
|
|
+ maxPolarAngle: Math.PI * 0.9,
|
|
|
+ minDistance: 2,
|
|
|
+ maxDistance: 20
|
|
|
+}
|
|
|
+
|
|
|
+const helpersConfig = {
|
|
|
+ axesHelperSize: 5,
|
|
|
+ gridHelperSize: 20,
|
|
|
+ statsHelper: true
|
|
|
+}
|
|
|
+
|
|
|
+const lightConfig = {
|
|
|
+ ambient: {
|
|
|
+ color: 0x404040,
|
|
|
+ intensity: 0.8
|
|
|
+ },
|
|
|
+ directional: {
|
|
|
+ color: 0xffffff,
|
|
|
+ intensity: 1.5,
|
|
|
+ position: { x: 3, y: 4, z: 5 }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
const props = defineProps({
|
|
|
modelValue: {
|
|
|
type: Boolean,
|
|
@@ -83,9 +230,7 @@ const props = defineProps({
|
|
|
|
|
|
const emit = defineEmits(["update:modelValue", "close"])
|
|
|
|
|
|
-// 添加色卡配置状态
|
|
|
const colorCardConfig = ref(null)
|
|
|
-
|
|
|
let cloudbtnbox = ref([
|
|
|
{ url: "meshFile.png", btnname: "文件选择" },
|
|
|
{ url: "yu.png", btnname: "域" },
|
|
@@ -93,62 +238,84 @@ let cloudbtnbox = ref([
|
|
|
{ url: "seka.png", btnname: "色卡" },
|
|
|
{ url: "dengzx.png", btnname: "等值线" }
|
|
|
])
|
|
|
-const subDialogComponents = {
|
|
|
- 文件选择: FileSelectDialog,
|
|
|
- 域: DomainDialog,
|
|
|
- 云图: CloudMapDialog,
|
|
|
- 色卡: ColorCardDialog,
|
|
|
- 等值线: ContourDialog
|
|
|
-}
|
|
|
+
|
|
|
let isLoading = ref(false)
|
|
|
-// 当前活动的子弹窗
|
|
|
-const activeSubDialogs = ref([])
|
|
|
let pltData = ref()
|
|
|
-// 打开对应的子弹窗
|
|
|
-const openSubDialog = (btnName) => {
|
|
|
- // 先关闭同类型的对话框(如果存在)
|
|
|
- closeSubDialog(btnName)
|
|
|
-
|
|
|
- // 打开新对话框
|
|
|
- activeSubDialogs.value.push({
|
|
|
- type: btnName,
|
|
|
- component: subDialogComponents[btnName],
|
|
|
- visible: true,
|
|
|
- props: {}
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-// 关闭子弹窗
|
|
|
-const closeSubDialog = (dialogType) => {
|
|
|
- activeSubDialogs.value = activeSubDialogs.value.filter(
|
|
|
- (d) => d.type !== dialogType
|
|
|
- )
|
|
|
-}
|
|
|
+const renderer = ref()
|
|
|
+const pltStore = usePltDataStore()
|
|
|
|
|
|
-// 处理子弹窗确认
|
|
|
-const handleSubDialogConfirm = (dialogType, data) => {
|
|
|
- switch (dialogType) {
|
|
|
+// 处理按钮点击
|
|
|
+const handleButtonClick = (btnName) => {
|
|
|
+ switch (btnName) {
|
|
|
case "文件选择":
|
|
|
- // 获取结果文件
|
|
|
- getResultFile(data.fid)
|
|
|
+ fileSelectDialogVisible.value = true
|
|
|
break
|
|
|
case "域":
|
|
|
- console.log("dddddddddd域", data)
|
|
|
+ domainDialogVisible.value = true
|
|
|
break
|
|
|
case "云图":
|
|
|
- console.log("dddddddddd云图", data)
|
|
|
+ cloudMapDialogVisible.value = true
|
|
|
break
|
|
|
case "色卡":
|
|
|
- colorCardConfig.value = data
|
|
|
- applyColorCardConfig() // 应用色卡配置
|
|
|
+ colorCardDialogVisible.value = true
|
|
|
break
|
|
|
case "等值线":
|
|
|
- console.log("dddddddddd等值线", data)
|
|
|
+ contourDialogVisible.value = true
|
|
|
break
|
|
|
default:
|
|
|
break
|
|
|
}
|
|
|
- closeSubDialog(dialogType)
|
|
|
+}
|
|
|
+
|
|
|
+// 计算所有变量的范围
|
|
|
+const calculateVariableRanges = () => {
|
|
|
+ const ranges = {}
|
|
|
+ pltData.value?.zones?.forEach(zone => {
|
|
|
+ Object.entries(zone.variables || {}).forEach(([name, data]) => {
|
|
|
+ if (!ranges[name]) {
|
|
|
+ ranges[name] = { min: Infinity, max: -Infinity }
|
|
|
+ }
|
|
|
+ ranges[name].min = Math.min(ranges[name].min, ...data)
|
|
|
+ ranges[name].max = Math.max(ranges[name].max, ...data)
|
|
|
+ })
|
|
|
+ })
|
|
|
+ return ranges
|
|
|
+}
|
|
|
+
|
|
|
+// 处理各对话框的确认事件
|
|
|
+const handleFileSelectConfirm = (data) => {
|
|
|
+ getResultFile(data.fid)
|
|
|
+ fileSelectDialogVisible.value = false
|
|
|
+}
|
|
|
+
|
|
|
+const handleDomainConfirm = (data) => {
|
|
|
+ console.log("域设置确认", data)
|
|
|
+ domainDialogVisible.value = false
|
|
|
+}
|
|
|
+
|
|
|
+const handleCloudMapConfirm = (data) => {
|
|
|
+ console.log("云图设置确认", data)
|
|
|
+ cloudMapDialogVisible.value = false
|
|
|
+}
|
|
|
+
|
|
|
+const handleColorCardConfirm = (data) => {
|
|
|
+ colorCardConfig.value = data
|
|
|
+ applyColorCardConfig()
|
|
|
+ colorCardDialogVisible.value = false
|
|
|
+}
|
|
|
+
|
|
|
+const handleContourConfirm = (params) => {
|
|
|
+ threeSceneRef.value?.enableContour(
|
|
|
+ params.zoneName,
|
|
|
+ params.variableName,
|
|
|
+ params.options
|
|
|
+ )
|
|
|
+
|
|
|
+ threeSceneRef.value?.updateColorMapping(params.variableName, {
|
|
|
+ min: params.options.minValue,
|
|
|
+ max: params.options.maxValue
|
|
|
+ })
|
|
|
+ contourDialogVisible.value = false
|
|
|
}
|
|
|
|
|
|
const getImgPath = (url) => {
|
|
@@ -166,12 +333,11 @@ const getResultFile = (fid) => {
|
|
|
getPltData(fid)
|
|
|
}
|
|
|
|
|
|
+// 获取PLT数据
|
|
|
const getPltData = async (fpid) => {
|
|
|
let url = import.meta.env.VITE_BASE_URL + getUrl()
|
|
|
try {
|
|
|
- // 1. 先初始化h5wasm
|
|
|
- const { ready, File, FS } = await h5wasm.ready;
|
|
|
- // 必须等待WASM加载完成
|
|
|
+ const { ready, File, FS } = await h5wasm.ready
|
|
|
const response = await fetch(url, {
|
|
|
method: "POST",
|
|
|
headers: {
|
|
@@ -185,21 +351,21 @@ const getPltData = async (fpid) => {
|
|
|
fid: fpid
|
|
|
})
|
|
|
})
|
|
|
- // 获取二进制数据
|
|
|
const arrayBuffer = await response.arrayBuffer()
|
|
|
- // 将文件写入虚拟文件系统
|
|
|
const filename = `data_${Date.now()}.h5`
|
|
|
FS.writeFile(filename, new Uint8Array(arrayBuffer))
|
|
|
- // 打开HDF5文件
|
|
|
- const file = new h5wasm.File(filename, 'r') // 'r' 表示只读模式
|
|
|
- // 3. 调试:打印完整结构
|
|
|
- console.log('HDF5结构:', {
|
|
|
- keys: Array.from(file.keys()),
|
|
|
- attrs: Object.entries(file.attrs).map(([k, v]) => ({ [k]: v.value }))
|
|
|
- })
|
|
|
- // 解析为Three.js可用格式
|
|
|
- const parsedData = await parseHDF5ToThreeJS(file);
|
|
|
+ const file = new h5wasm.File(filename, "r")
|
|
|
+ const parsedData = await parseHDF5ToThreeJS(file)
|
|
|
pltData.value = parsedData
|
|
|
+ console.log("解析后的数据:", parsedData)
|
|
|
+
|
|
|
+ pltStore.initialize(parsedData.zones)
|
|
|
+ await nextTick()
|
|
|
+ ;["far", "symm"].forEach((name) => {
|
|
|
+ if (pltStore.domainNames.includes(name)) {
|
|
|
+ threeSceneRef.value?.setZoneVisibility(name, false)
|
|
|
+ }
|
|
|
+ })
|
|
|
isLoading.value = false
|
|
|
} catch (error) {
|
|
|
isLoading.value = false
|
|
@@ -207,30 +373,36 @@ const getPltData = async (fpid) => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+watch(
|
|
|
+ () => pltData.value,
|
|
|
+ (newData) => {
|
|
|
+ if (newData) {
|
|
|
+ threeSceneRef.value?.updateData(newData)
|
|
|
+ }
|
|
|
+ }
|
|
|
+)
|
|
|
+
|
|
|
// HDF5转Three.js格式的解析器
|
|
|
const parseHDF5ToThreeJS = (h5file) => {
|
|
|
- const pltStore = usePltDataStore()
|
|
|
const result = {
|
|
|
- "data": { "datasetType": "plt" },
|
|
|
+ data: { datasetType: "plt" },
|
|
|
metadata: {
|
|
|
- title: h5file.attrs.title?.value || '',
|
|
|
+ title: h5file.attrs.title?.value || "",
|
|
|
variables: JSON.parse(h5file.attrs.variables?.value || "[]"),
|
|
|
- version: h5file.attrs.version?.value || ''
|
|
|
+ version: h5file.attrs.version?.value || ""
|
|
|
},
|
|
|
zones: []
|
|
|
}
|
|
|
const zones = []
|
|
|
|
|
|
- // 增强型数据提取方法
|
|
|
const extractDataset = (group, name) => {
|
|
|
try {
|
|
|
- // 尝试多种访问方式
|
|
|
- const dataset = group.get?.(name) || // 方法1: get()
|
|
|
- group[name]?.value || // 方法2: 直接访问
|
|
|
- Object.entries(group).find(([k]) => k === name)?.[1]?.value // 方法3: 遍历查找
|
|
|
-
|
|
|
- // 特殊处理数组类型数据
|
|
|
- if (dataset && typeof dataset === 'object' && 'value' in dataset) {
|
|
|
+ const dataset =
|
|
|
+ group.get?.(name) ||
|
|
|
+ group[name]?.value ||
|
|
|
+ Object.entries(group).find(([k]) => k === name)?.[1]?.value
|
|
|
+
|
|
|
+ if (dataset && typeof dataset === "object" && "value" in dataset) {
|
|
|
return dataset.value
|
|
|
}
|
|
|
return dataset
|
|
@@ -240,24 +412,21 @@ const parseHDF5ToThreeJS = (h5file) => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 处理每个区域
|
|
|
for (const zoneKey of h5file.keys()) {
|
|
|
try {
|
|
|
- // 正确获取组对象
|
|
|
const zone = h5file.get(zoneKey)
|
|
|
- if (!zone || typeof zone !== 'object') continue
|
|
|
+ if (!zone || typeof zone !== "object") continue
|
|
|
|
|
|
console.log(`处理 ${zoneKey}:`, zone)
|
|
|
|
|
|
const zoneData = {
|
|
|
- name: zoneKey.replace(/^zone_\d+_/, ''), // 去除可能的zone_前缀
|
|
|
- vertices: extractDataset(zone, 'vertices'),
|
|
|
- indices: extractDataset(zone, 'indices'),
|
|
|
+ name: zoneKey.replace(/^zone_\d+_/, ""),
|
|
|
+ vertices: extractDataset(zone, "vertices"),
|
|
|
+ indices: extractDataset(zone, "indices"),
|
|
|
variables: {}
|
|
|
}
|
|
|
|
|
|
- // 提取变量数据
|
|
|
- result.metadata.variables.forEach(varName => {
|
|
|
+ result.metadata.variables.forEach((varName) => {
|
|
|
const data = extractDataset(zone, `var_${varName}`)
|
|
|
if (data) zoneData.variables[varName] = data
|
|
|
})
|
|
@@ -268,9 +437,8 @@ const parseHDF5ToThreeJS = (h5file) => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- pltStore.setZones(zones)
|
|
|
+ pltStore.initialize(zones)
|
|
|
result.zones = zones
|
|
|
- console.log('最终解析结果:', result)
|
|
|
return result
|
|
|
}
|
|
|
|
|
@@ -284,25 +452,60 @@ function getUrl(channelNo = "service") {
|
|
|
return url
|
|
|
}
|
|
|
|
|
|
-// 添加应用色卡配置的方法
|
|
|
+// 应用色卡配置
|
|
|
const applyColorCardConfig = () => {
|
|
|
if (!pltData.value || !colorCardConfig.value) return
|
|
|
-
|
|
|
- // 确保pltData有colorCard配置对象
|
|
|
- if (!pltData.value.config) {
|
|
|
- pltData.value.config = {}
|
|
|
- }
|
|
|
-
|
|
|
+
|
|
|
+ pltData.value.config = pltData.value.config || {}
|
|
|
pltData.value.config.colorCard = {
|
|
|
...colorCardConfig.value,
|
|
|
visible: colorCardConfig.value.check1,
|
|
|
showTitle: colorCardConfig.value.check2
|
|
|
}
|
|
|
-
|
|
|
- // 强制更新云图组件
|
|
|
- pltData.value = {...pltData.value}
|
|
|
+
|
|
|
+ if (threeSceneRef.value) {
|
|
|
+ threeSceneRef.value.updateColorMapping(null, {
|
|
|
+ colorCard: pltData.value.config.colorCard
|
|
|
+ })
|
|
|
+ }
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
+/* 原有样式保持不变 */
|
|
|
+.extreme-label {
|
|
|
+ transform: translate(-50%, 0);
|
|
|
+ white-space: nowrap;
|
|
|
+ pointer-events: none;
|
|
|
+}
|
|
|
+.color-bar-container {
|
|
|
+ position: absolute;
|
|
|
+ right: 20px;
|
|
|
+ bottom: 20px;
|
|
|
+ z-index: 1000;
|
|
|
+ background: rgba(255, 255, 255, 0.8);
|
|
|
+ padding: 10px;
|
|
|
+ border-radius: 4px;
|
|
|
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
|
|
+}
|
|
|
+
|
|
|
+.color-bar {
|
|
|
+ height: 20px;
|
|
|
+ width: 100%;
|
|
|
+ margin-bottom: 5px;
|
|
|
+ border-radius: 3px;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.color-bar-labels {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.color-bar-title {
|
|
|
+ text-align: center;
|
|
|
+ font-weight: bold;
|
|
|
+ margin-top: 5px;
|
|
|
+}
|
|
|
</style>
|