Files
17168ERP/web/admin/region/bed/index.aspx
T
2026-11-10 18:03:52 +08:00

1168 lines
56 KiB
Plaintext

<%@ Page Title="" Language="C#" MasterPageFile="~/admin/Templates/TBS5ADM001/MasterPage.master" AutoEventWireup="true" CodeFile="index.aspx.cs" Inherits="admin_region_bed_index" %>
<asp:Content ID="Content1" ContentPlaceHolderID="page_header" Runat="Server">
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>
<script src="/js/httpVueLoader.js"></script>
<style>
/* Breadcrumb 樣式 */
.drilldown-breadcrumb {
position: absolute;
top: 15px;
left: 15px;
z-index: 10;
background: rgba(255, 255, 255, 0.95);
padding: 10px 20px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
font-size: 1.1rem;
font-weight: bold;
}
.drilldown-breadcrumb a { text-decoration: none; color: #1976d2; cursor: pointer; }
.drilldown-breadcrumb a:hover { text-decoration: underline; }
.drilldown-breadcrumb .separator { margin: 0 10px; color: #757575; }
.drilldown-breadcrumb .active { color: #424242; }
/* 提示訊息 */
.drilldown-hint {
position: absolute;
bottom: 15px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.7);
color: #fff;
padding: 8px 16px;
border-radius: 20px;
font-size: 0.9rem;
z-index: 10;
pointer-events: none;
letter-spacing: 1px;
}
/* 篩選面板動畫 */
.filter-panel {
transition: all 0.3s ease-in-out;
overflow: hidden;
}
</style>
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="page_nav" Runat="Server">
<div class="card mb-3 w-100 shadow-sm border-0">
<!-- 查詢條件收合標題列 -->
<div class="card-header bg-white d-flex justify-content-between align-items-center"
style="cursor:pointer; padding: 10px 15px; border-bottom: 1px solid #eee;"
@click="isFilterExpanded = !isFilterExpanded">
<h6 class="mb-0 text-primary fw-bold">
<span class="mdi mdi-filter-variant"></span> 查詢與統計條件
</h6>
<span :class="isFilterExpanded ? 'mdi mdi-chevron-up' : 'mdi mdi-chevron-down'" style="font-size: 24px; color: #1976d2;"></span>
</div>
<!-- 面板內容 -->
<div class="card-body bg-light p-3 filter-panel" v-show="isFilterExpanded" style="height: 200px;">
<div class="d-flex h-25 w-100" style="align-items:center;align-content:center;justify-content: center;" >
<v-btn elevation="2" @click.prevent="changeDate('-')"><span class="mdi mdi-menu-left mdi-36px"></span></v-btn> {{ todayStr }}
<v-btn elevation="2" @click.prevent="changeDate('+')"><span class="mdi mdi-menu-right mdi-36px"></span></v-btn>
</div>
<div class="d-flex h-75 w-100 mt-2">
<!-- 左側統計區塊 40% -->
<div class="d-flex gap-2 pe-3" style="flex: 0 0 40%; height: 100%; border-right: 2px solid #ccc;">
<!-- 總房間 -->
<div class="p-2 border rounded text-center flex-fill d-flex flex-column justify-content-center" style="background-color: #f0f8ff;">
<div><strong>總房間</strong></div>
<div>{{ summary?.totalRooms || 0 }}</div>
<div class="text-primary">男: {{ summary?.totalMaleRooms || 0 }}</div>
<div class="text-danger">女: {{ (summary?.totalRooms || 0) - (summary?.totalMaleRooms || 0) }}</div>
</div>
<!-- 總床位 -->
<div class="p-2 border rounded text-center flex-fill d-flex flex-column justify-content-center" style="background-color: #fff0f5;">
<div><strong>總床位</strong></div>
<div>{{ summary?.totalBeds || 0 }}</div>
<div class="text-primary">男: {{ summary?.totalMaleBeds || 0 }}</div>
<div class="text-danger">女: {{ (summary?.totalBeds || 0) - (summary?.totalMaleBeds || 0) }}</div>
</div>
<!-- 可用床位 -->
<div class="p-2 border rounded text-center flex-fill d-flex flex-column justify-content-center" style="background-color: #f5fff0;">
<!-- 標題 -->
<h6 class="mb-2">查詢結果</h6>
<!-- 內容 -->
<div><strong>可用床位</strong></div>
<div class="text-primary">男: {{ filteredStats.maleBeds }}|女: {{ filteredStats.femaleBeds }}</div>
</div>
</div>
<!-- 右側篩選區塊 60% -->
<div class="d-flex gap-3 ms-10" style="flex: 0 0 60%; height: 100%; align-items: flex-start;">
<!-- 分組 1: 區域 & 房間下拉框 -->
<div class="p-2 rounded shadow-sm" style="background-color: #fff; min-width: 160px;">
<h6 class="mb-2">區域與房間</h6>
<div class="d-flex flex-column gap-2">
<select class="form-select" v-model="filter.selectedArea">
<option :value="null">全部區域</option>
<option v-for="region in filter.areas" :value="region.uuid" :key="region.uuid">{{region.name}}</option>
</select>
<select class="form-select" v-model="filter.selectedRoom">
<option :value="null">房間</option>
<option v-for="room in filter.rooms" :value="room.uuid" :key="room.uuid">{{room.name}}</option>
</select>
</div>
</div>
<!-- 分組 2: 日期篩選 -->
<div class="p-2 rounded shadow-sm" style="background-color: #fff; min-width: 160px;">
<h6 class="mb-2">日期篩選</h6>
<div class="d-flex flex-column gap-2">
<input type="date" class="form-control" v-model="filter.startDate" :min="today" @change="onStartDateChange"/>
</div>
</div>
<!-- 分組 3: 占用床位 & 性別復選框 -->
<div class="p-2 rounded shadow-sm" style="background-color: #fff; min-width: 200px;">
<h6 class="mb-2">床位與性別</h6>
<div class="d-flex gap-3 p-2 mb-2 border rounded shadow-sm" style="background-color: #f8f9fa;">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="occupied" v-model="filter.occupied" @change="toggleOccupied('occupied')">
<label class="form-check-label" for="occupied">已佔用床位</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="unoccupied" v-model="filter.unoccupied" @change="toggleOccupied('unoccupied')">
<label class="form-check-label" for="unoccupied">可用床位</label>
</div>
</div>
<div class="d-flex gap-3 p-2 border rounded shadow-sm" style="background-color: #f8f9fa;">
<div class="form-check">
<input type="checkbox" id="male" :checked="filter.Gender === true" @change="toggleGender(true)">
<label class="form-check-label" for="male">男</label>
</div>
<div class="form-check">
<input type="checkbox" id="female" :checked="filter.Gender === false" @change="toggleGender(false)">
<label class="form-check-label" for="female">女</label>
</div>
</div>
</div>
<div class="p-2 rounded shadow-sm" style="background-color: #fff; min-width: 160px;" v-if="uniqueLimits.length > 0">
<h6 class="mb-2">特殊需求</h6>
<div class="d-flex flex-wrap gap-2 p-2 border rounded shadow-sm" style="background-color: #f8f9fa;">
<div class="form-check" v-for="limit in uniqueLimits" :key="limit.paramNo">
<input class="form-check-input" type="checkbox" :id="'limit_' + limit.paramNo" :value="limit.paramNo" v-model="filter.selectedLimits">
<label class="form-check-label" :for="'limit_' + limit.paramNo">{{ limit.paramName }}</label>
</div>
</div>
</div>
<!-- 分組 4: 查詢與清空按鈕 -->
<div class="p-2 rounded shadow-sm" style="background-color: #fff; min-width: 120px;">
<h6 class="mb-2">操作</h6>
<div class="d-flex flex-column gap-2">
<button class="btn btn-success" type="button" @click="search">查詢</button>
<button class="btn btn-primary" type="button" @click="clearFilter">清空條件</button>
</div>
</div>
</div>
</div>
</div>
</div>
</asp:Content>
<asp:Content ID="Content3" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server">
<!-- 3D 視圖容器 -->
<div class="mb-4 shadow-sm rounded" style="background-color: #fff; padding: 10px;">
<h4 class="mb-2 text-primary fw-bold" style="padding-left: 10px;">3D 寮房空間總覽</h4>
<div id="three-container" ref="threeContainer" style="width: 100%; height: 650px; position: relative; border: 1px solid #ddd; border-radius: 8px; overflow: hidden;">
<!-- 視角切換開關 (Drilldown vs Flat) -->
<div style="position: absolute; top: 15px; left: 50%; transform: translateX(-50%); z-index: 10; display:flex; gap:0; box-shadow: 0 4px 6px rgba(0,0,0,0.1); border-radius: 4px; overflow: hidden;">
<button type="button" class="btn btn-sm fw-bold" :class="viewMode === 'DRILLDOWN' ? 'btn-primary' : 'btn-light border'" @click="setViewMode('DRILLDOWN')" style="padding: 8px 16px;">階層導覽</button>
<button type="button" class="btn btn-sm fw-bold" :class="viewMode === 'FLAT' ? 'btn-primary' : 'btn-light border'" @click="setViewMode('FLAT')" style="padding: 8px 16px;">展開全部</button>
</div>
<!-- Drilldown Breadcrumb UI -->
<div class="drilldown-breadcrumb" v-show="viewMode === 'DRILLDOWN'">
<a @click.prevent="goToTop"><i class="mdi mdi-domain"></i> 全部樓層</a>
<span v-if="viewState.level === 'REGION' || viewState.level === 'ROOM'">
<span class="separator">/</span>
<a @click.prevent="drillToRegion(viewState.region)">{{ viewState.region.name }}</a>
</span>
<span v-if="viewState.level === 'ROOM'">
<span class="separator">/</span>
<span class="active">{{ viewState.room.name }}</span>
</span>
</div>
<!-- 畫面操作提示 -->
<div class="drilldown-hint">
<span v-if="viewMode === 'FLAT'"><i class="mdi mdi-cursor-default-click"></i> 點擊【立體床位】查看/新增排程掛單</span>
<span v-else-if="viewState.level === 'ALL'"><i class="mdi mdi-cursor-default-click"></i> 點擊【樓層區塊】進入樓層視角</span>
<span v-else-if="viewState.level === 'REGION'"><i class="mdi mdi-cursor-default-click"></i> 點擊【房間地板】進入獨立房間,或直接點擊【立體床位】</span>
<span v-else-if="viewState.level === 'ROOM'"><i class="mdi mdi-cursor-default-click"></i> 點擊【立體床位】查看/新增排程掛單</span>
</div>
<!-- 圖例 -->
<div style="position: absolute; top: 15px; right: 15px; background: rgba(255,255,255,0.9); padding: 12px; border-radius: 8px; z-index: 10; pointer-events: none; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">
<div v-if="viewMode === 'FLAT' || viewState.level === 'ROOM' || viewState.level === 'REGION'" style="display: flex; align-items: center; margin-bottom: 5px;">
<div style="width: 15px; height: 15px; background-color: #4caf50; margin-right: 8px;"></div> 空床
</div>
<div v-if="viewMode === 'FLAT' || viewState.level === 'ROOM' || viewState.level === 'REGION'" style="display: flex; align-items: center; margin-bottom: 5px;">
<div style="width: 15px; height: 15px; background-color: #f44336; margin-right: 8px;"></div> 佔用
</div>
<div v-if="viewMode === 'FLAT' || viewState.level !== 'ALL'" style="display: flex; align-items: center; margin-bottom: 5px;">
<div style="width: 15px; height: 15px; background-color: #bbdefb; margin-right: 8px; border: 1px solid #ccc;"></div> 男眾房
</div>
<div v-if="viewMode === 'FLAT' || viewState.level !== 'ALL'" style="display: flex; align-items: center;">
<div style="width: 15px; height: 15px; background-color: #f8bbd0; margin-right: 8px; border: 1px solid #ccc;"></div> 女眾房
</div>
<div v-if="viewMode === 'DRILLDOWN' && viewState.level === 'ALL'" style="display: flex; align-items: center;">
<div style="width: 15px; height: 15px; background-color: #3f51b5; margin-right: 8px; border: 1px solid #ccc;"></div> 樓層/區域
</div>
<div v-if="viewMode === 'FLAT' || viewState.level === 'ROOM' || viewState.level === 'REGION'" style="display: flex; align-items: center; margin-bottom: 5px;">
<div style="width: 15px; height: 15px; background-color: #ff9800; margin-right: 8px;"></div> 空床(特殊需求)
</div>
</div>
</div>
</div>
<div>
<v-dialog v-model="bedSchedule.dialogVisible" max-width="900px">
<v-card style="min-height:50vh; max-height:80vh; display:flex; flex-direction:column;">
<v-card-title class="d-flex flex-column align-items-start">
<div class="d-flex w-100 align-items-center">
<span class="text-h6">床位排程明細 - {{ bedSchedule.selectedBed?.name }}</span>
<v-spacer></v-spacer>
<v-btn icon @click="closeBedSchedule"><v-icon>mdi-close</v-icon></v-btn>
</div>
<!-- 新增:顯示床位需求條件的標籤 -->
<div v-if="bedSchedule.selectedBed?.limits && bedSchedule.selectedBed.limits.length > 0" class="mt-2 w-100">
<span class="text-subtitle-2 mr-2">床位需求:</span>
<v-chip
v-for="limit in bedSchedule.selectedBed.limits"
:key="limit.paramNo"
color="orange darken-1"
text-color="white"
small
class="mr-2 mb-1"
>
<v-icon left small>mdi-alert-circle-outline</v-icon>
{{ limit.paramName }}
</v-chip>
</div>
</v-card-title>
<v-card-text style="flex:1 1 auto; min-height:0; overflow-y:auto;">
<div style="min-height:0;">
<v-data-table
:headers="bedSchedule.scheduleHeaders"
:items="bedSchedule.selectedBed?.schedules || []"
:items-per-page="9999"
class="elevation-1"
dense
hide-default-footer
>
<template #item.scheduleDate="{item}">
{{ item.scheduledate | timeString('YYYY-MM-DD') }}
</template>
<template #item.actions="{item}">
<%-- <a :href="'/admin/guadan/create.aspx?orderId='+item.guaDanOrderNo" class="btn btn-primary">
查看掛單
</a>--%>
<v-btn @click.prevent="viewGuadan(item.uuid)"></v-btn>
</template>
</v-data-table>
</div>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text color="primary" @click="closeBedSchedule">關閉</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog
v-model="viewdialog"
min-width="85%"
max-width="95%"
min-height="900"
max-height="95%"
hide-overlay
transition="dialog-bottom-transition">
<v-card>
<v-toolbar dark color="primary">
<v-btn icon dark @click="viewdialog = false">
<v-icon>mdi-close</v-icon>
</v-btn>
<v-toolbar-title>掛單作業</v-toolbar-title>
</v-toolbar>
<v-card-text>
<view-component v-if="viewdialog" @close="viewdialog = false" :form-Data="formData" />
</v-card-text>
</v-card>
</v-dialog>
</div>
</asp:Content>
<asp:Content ID="Content4" ContentPlaceHolderID="offCanvasRight" Runat="Server">
</asp:Content>
<asp:Content ID="Content5" ContentPlaceHolderID="footer_script" Runat="Server">
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
Vue.filter('timeString', function (value, myFormat) {
return value == null || value == "" ? "" : moment(value).format(myFormat || 'YYYY-MM-DD, HH:mm:ss');
});
// 縮放設定常數 (調整字體與佈局間距)
// - TEXT_SCALE_DIVISOR 控制文字看板大小 (越小字越大,原本是 15,前一版是 7.5,現在改為 11)
// - BED_WIDTH, BED_DEPTH 影響單一床的物理尺寸
// - SPACING 影響床與床、房間與房間之間的距離倍率
const CFG = {
TEXT_SCALE: 13, // [微調] 配合整體放大,文字立牌稍微調大一點點
BED_W: 6, // [放大] 床位寬度 (原 4)
BED_D: 12, // [放大] 床位深度 (原 8)
BED_H: 2.5, // [放大] 床位高度 (原 1.5)
COL_SPACING: 16, // [放大] 配合床變寬,調整 X 軸間距 (原 12)
ROW_SPACING: 20, // [放大] 配合床變長,調整 Z 軸間距 (原 14)
ROOM_MARGIN: 10, // [放大] 房間邊緣走道寬度 (原 8)
ROOM_GAP: 25, // [縮小] 房間與房間的間距,讓房間靠攏 (原 60)
REGION_GAP: 60, // [縮小] 樓層與樓層的間距 (原 120)
WALL_H: 12 // [放大] 牆壁高度 (原 8)
};
new Vue({
el: '#app',
vuetify: new Vuetify(vuetify_options),
data: {
viewdialog: false,
formData: null,
regions: [],
summary: {},
todayStr: new Date().toISOString().split('T')[0],
today: new Date().toISOString().split('T')[0],
thisDate: new Date(),
// 查詢面板收合狀態
isFilterExpanded: true,
filter: {
startDate: null,
endDate: null,
Gender: null,
selectedArea: null,
selectedRoom: null,
occupied: false,
unoccupied: false,
areas: [],
rooms: [],
selectedLimits: []
},
// 視角模式切換 (DRILLDOWN 階層 / FLAT 展開全部)
viewMode: 'DRILLDOWN',
// Drilldown 狀態機
viewState: {
level: 'ALL', // 'ALL', 'REGION', 'ROOM'
region: null,
room: null
},
bedSchedule: {
dialogVisible: false,
selectedBed: null,
scheduleHeaders: [
{ text: '使用日期', value: 'scheduledate' },
{ text: '掛單單號', value: 'guaDanOrderNo' },
{ text: '標題', value: 'title' },
{ text: '掛單人', value: 'usename' },
{ text: '查看掛單', value: 'actions' },
],
},
// Three.js 狀態管理
three: {
scene: null,
camera: null,
renderer: null,
controls: null,
raycaster: null,
mouse: null,
animationId: null,
meshes: []
}
},
components: {
'view-component': httpVueLoader('../../guadan/view.vue'),
},
methods: {
viewGuadan(no) {
this.formData = no;
this.viewdialog = true;
},
uniqueLimits() {
const limitsMap = new Map();
if (this.regions) {
this.regions.forEach(region => {
(region.room || []).forEach(room => {
(room.beds || []).forEach(bed => {
(bed.limits || []).forEach(limit => {
if (!limitsMap.has(limit.paramNo)) {
limitsMap.set(limit.paramNo, limit.paramName);
}
});
});
});
});
}
return Array.from(limitsMap, ([paramNo, paramName]) => ({ paramNo, paramName }));
},
// [修改] 原本的 getBedColor 改為 getBedStyle,回傳完整材質設定
getBedStyle(bed) {
if (!bed.canuse) {
return { color: 0xf44336, emissive: 0x000000, intensity: 0 }; // 佔用: 紅色,不發光
}
if (bed.limits && bed.limits.length > 0) {
// 空床(有限制): 橘色,加上自發光效果 (Emissive) 吸引目光
return { color: 0xff9800, emissive: 0xff5722, intensity: 0.7 };
}
return { color: 0x4caf50, emissive: 0x000000, intensity: 0 }; // 空床(無限制): 綠色,不發光
},
getBedLabelText(bed) {
let text = `${bed.name}\n[${bed.canuse ? '空床' : '佔用'}]`;
// 如果有 limits 資料,就換行並將 paramName 串接起來
if (bed.limits && bed.limits.length > 0) {
const limitNames = bed.limits.map(l => l.paramName).join('、');
text += `\n(${limitNames})`;
}
return text;
},
changeDate(plusOrMinus) {
if (plusOrMinus == "+") this.thisDate.setDate(this.thisDate.getDate() + 1);
else if (plusOrMinus == "-") this.thisDate.setDate(this.thisDate.getDate() - 1);
this.filter.startDate = this.thisDate.toISOString().split('T')[0];
this.filter.endDate = this.thisDate.toISOString().split('T')[0];
this.todayStr = this.thisDate.toISOString().split('T')[0];
},
// [新增] 集中管理床位顏色邏輯
getBedColor(bed) {
if (!bed.canuse) return 0xf44336; // 佔用: 紅色
if (bed.limits && bed.limits.length > 0) return 0xff9800; // 空床(有需求): 橘色
return 0x4caf50; // 空床(無需求): 綠色
},
showBedSchedule(bed) {
this.bedSchedule.selectedBed = bed;
this.bedSchedule.dialogVisible = true;
},
closeBedSchedule() {
this.bedSchedule.selectedBed = null;
this.bedSchedule.dialogVisible = false;
},
search() { this.GetRegionList(); },
onStartDateChange() {
if (this.filter.endDate && this.filter.endDate < this.filter.startDate) this.filter.endDate = this.filter.startDate;
if (!this.filter.endDate) this.filter.endDate = this.filter.startDate;
},
resetFilter() {
this.filter.startDate = null; this.filter.endDate = null;
this.filter.selectedArea = null; this.filter.selectedRoom = null;
this.filter.occupied = false; this.filter.unoccupied = false;
this.filter.Gender = null;
this.filter.selectedLimits = []; // [新增] 清空選擇
},
clearFilter() {
this.resetFilter();
this.GetRegionList();
},
GetRegionList() {
const payload = {
startDate: this.filter.startDate, endDate: this.filter.endDate,
areaId: this.filter.selectedArea, roomId: this.filter.selectedRoom,
occupied: this.filter.occupied, unoccupied: this.filter.unoccupied,
gender: this.filter.Gender,
};
axios.post(HTTP_HOST + 'api/region/list', payload)
.then((res) => {
this.regions = res.data.regions;
this.summary = res.data.summary;
try { this.regions.sort((a, b) => Number(a.isStop) - Number(b.isStop)); } catch (e) {}
this.restoreViewState();
this.render3DRooms();
})
.catch((err) => console.error('API 錯誤', err));
},
toggleOccupied(type) {
if (type === "occupied" && this.filter.occupied) this.filter.unoccupied = false;
if (type === "unoccupied" && this.filter.unoccupied) this.filter.occupied = false;
},
toggleGender(value) {
this.filter.Gender = this.filter.Gender === value ? null : value;
},
getRegionWithRoom() {
axios.get(HTTP_HOST + 'api/region/regionwithroom').then((res) => { this.filter.areas = res.data; });
},
async loadRooms() {
try {
let url = "/api/room/roomwithbed";
const params = {};
if (this.filter.selectedArea) params.RegionUuid = this.filter.selectedArea;
const res = await axios.get(url, { params });
this.filter.rooms = res.data;
this.filter.selectedRoom = null;
} catch (error) {}
},
shouldShowBed(bed) {
if (this.filter.occupied && bed.canuse === true) return false;
if (this.filter.unoccupied && bed.canuse === false) return false;
if (this.filter.Gender !== null && bed.gender !== this.filter.Gender) return false;
// 判斷需求條件:如果使用者有勾選,床位必須包含「至少一個」勾選的條件
if (this.filter.selectedLimits && this.filter.selectedLimits.length > 0) {
if (!bed.limits || bed.limits.length === 0) return false;
const hasMatch = bed.limits.some(l => this.filter.selectedLimits.includes(l.paramNo));
if (!hasMatch) return false;
}
return true;
},
setViewMode(mode) {
this.viewMode = mode;
this.resetCamera();
this.render3DRooms();
},
goToTop() {
this.viewState.level = 'ALL';
this.viewState.region = null;
this.viewState.room = null;
this.resetCamera();
this.render3DRooms();
},
drillToRegion(regionInfo) {
this.viewState.level = 'REGION';
this.viewState.region = regionInfo;
this.viewState.room = null;
this.resetCamera();
this.render3DRooms();
},
drillToRoom(roomInfo) {
this.viewState.level = 'ROOM';
this.viewState.room = roomInfo;
this.resetCamera();
this.render3DRooms();
},
restoreViewState() {
if (this.viewState.level === 'REGION') {
const foundReg = this.regions.find(r => r.uuid === this.viewState.region.uuid);
if (foundReg) this.viewState.region = foundReg;
else this.goToTop();
} else if (this.viewState.level === 'ROOM') {
const foundReg = this.regions.find(r => r.uuid === this.viewState.region.uuid);
if (foundReg) {
this.viewState.region = foundReg;
const foundRoom = foundReg.room.find(rm => rm.uuid === this.viewState.room.uuid);
if (foundRoom) this.viewState.room = foundRoom;
else this.drillToRegion(foundReg);
} else {
this.goToTop();
}
}
},
resetCamera() {
if (!this.three.camera || !this.three.controls) return;
if (this.viewMode === 'FLAT') {
this.three.camera.position.set(0, 250, 300); // 因總體尺寸放大,鏡頭需更遠
this.three.controls.target.set(0, 0, 50);
} else {
if (this.viewState.level === 'ALL') {
this.three.camera.position.set(0, 100, 130);
} else if (this.viewState.level === 'REGION') {
this.three.camera.position.set(0, 120, 150);
} else if (this.viewState.level === 'ROOM') {
this.three.camera.position.set(0, 45, 60);
}
this.three.controls.target.set(0, 0, 0);
}
},
initThree() {
const container = this.$refs.threeContainer;
if (!container) return;
this.three.raycaster = new THREE.Raycaster();
this.three.mouse = new THREE.Vector2();
this.three.scene = new THREE.Scene();
this.three.scene.background = new THREE.Color(0xf5f7fa);
this.three.camera = new THREE.PerspectiveCamera(50, container.clientWidth / container.clientHeight, 0.1, 4000);
this.three.renderer = new THREE.WebGLRenderer({ antialias: true });
this.three.renderer.setSize(container.clientWidth, container.clientHeight);
this.three.renderer.setPixelRatio(window.devicePixelRatio);
container.appendChild(this.three.renderer.domElement);
this.three.controls = new OrbitControls(this.three.camera, this.three.renderer.domElement);
this.three.controls.enableDamping = true;
this.three.controls.dampingFactor = 0.05;
this.three.controls.maxPolarAngle = Math.PI / 2 - 0.05;
const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
this.three.scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.6);
dirLight.position.set(20, 50, 30);
this.three.scene.add(dirLight);
this.onCanvasClick = this.onCanvasClick.bind(this);
this.onWindowResize = this.onWindowResize.bind(this);
this.animate = this.animate.bind(this);
container.addEventListener('click', this.onCanvasClick, false);
window.addEventListener('resize', this.onWindowResize, false);
this.resetCamera();
this.animate();
},
makeTextSprite(message, fontsize = 32, fontColor = "rgba(0, 0, 0, 1.0)", bgColor = null) {
const lines = message.split('\n');
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
context.font = `Bold ${fontsize}px "Microsoft JhengHei", Arial, sans-serif`;
let maxWidth = 0;
lines.forEach(line => {
const metrics = context.measureText(line);
if (metrics.width > maxWidth) maxWidth = metrics.width;
});
canvas.width = maxWidth + 40;
canvas.height = (fontsize * 1.5) * lines.length + 10;
context.font = `Bold ${fontsize}px "Microsoft JhengHei", Arial, sans-serif`;
if (bgColor) {
context.fillStyle = bgColor;
context.roundRect ? context.roundRect(0, 0, canvas.width, canvas.height, 10) : context.fillRect(0,0, canvas.width, canvas.height);
context.fill();
}
context.fillStyle = fontColor;
context.textAlign = "center";
context.textBaseline = "middle";
lines.forEach((line, index) => {
const yPos = (fontsize * 1.5) * index + (fontsize * 1.5) / 2 + 5;
context.fillText(line, canvas.width / 2, yPos);
});
const texture = new THREE.CanvasTexture(canvas);
texture.generateMipmaps = false;
texture.minFilter = THREE.LinearFilter;
const spriteMaterial = new THREE.SpriteMaterial({ map: texture, transparent: true });
const sprite = new THREE.Sprite(spriteMaterial);
// 套用全域縮放常數
sprite.scale.set(canvas.width / CFG.TEXT_SCALE, canvas.height / CFG.TEXT_SCALE, 1);
return sprite;
},
clearMeshes() {
this.three.meshes.forEach(mesh => {
this.three.scene.remove(mesh);
if (mesh.geometry) mesh.geometry.dispose();
if (mesh.material) {
if (Array.isArray(mesh.material)) mesh.material.forEach(m => m.dispose());
else mesh.material.dispose();
}
if (mesh.material && mesh.material.map) mesh.material.map.dispose();
});
this.three.meshes = [];
},
render3DRooms() {
if (!this.three.scene) return;
this.clearMeshes();
if (this.viewMode === 'FLAT') {
this.renderAllBedsFlat();
} else {
if (this.viewState.level === 'ALL') {
this.renderAllRegions();
} else if (this.viewState.level === 'REGION') {
this.renderRoomsInRegion();
} else if (this.viewState.level === 'ROOM') {
this.renderBedsInRoom();
}
}
},
// 【模式: 全展開 (FLAT)】
renderAllBedsFlat() {
let regionOffsetZ = -20;
this.regions.forEach((region) => {
if (region.isStop) return;
const validRooms = (region.room || []).filter(r => r.beds && r.beds.length > 0);
if (validRooms.length === 0) return;
let totalWidth = 0;
validRooms.forEach(room => {
const cols = 2;
const roomWidth = (cols * CFG.COL_SPACING) + CFG.ROOM_MARGIN;
totalWidth += roomWidth + CFG.ROOM_GAP;
});
totalWidth -= CFG.ROOM_GAP;
const rLabel = this.makeTextSprite(region.name, 56, "#ffffff", "rgba(63, 81, 181, 0.9)");
rLabel.position.set(0, 35, regionOffsetZ - 20); // 調高
this.three.scene.add(rLabel);
this.three.meshes.push(rLabel);
let roomOffsetX = -totalWidth / 2;
let maxRoomDepth = 0;
validRooms.forEach((room) => {
const bedCount = room.beds.length;
const cols = 2;
const rows = Math.ceil(bedCount / cols);
const roomWidth = (cols * CFG.COL_SPACING) + CFG.ROOM_MARGIN;
const roomDepth = (rows * CFG.ROW_SPACING) + CFG.ROOM_MARGIN;
if (roomDepth > maxRoomDepth) maxRoomDepth = roomDepth;
const centerX = roomOffsetX + roomWidth / 2;
const centerZ = regionOffsetZ + roomDepth / 2;
const floorColor = room.gender ? 0xe3f2fd : 0xfce4ec;
const floorGeo = new THREE.PlaneGeometry(roomWidth, roomDepth);
const floorMat = new THREE.MeshLambertMaterial({ color: floorColor, side: THREE.DoubleSide });
const floorMesh = new THREE.Mesh(floorGeo, floorMat);
floorMesh.rotation.x = -Math.PI / 2;
floorMesh.position.set(centerX, 0, centerZ);
const edges = new THREE.EdgesGeometry(floorGeo);
const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0x999999 }));
floorMesh.add(line);
this.three.scene.add(floorMesh);
this.three.meshes.push(floorMesh);
const wallMat = new THREE.MeshLambertMaterial({ color: 0xb0bec5, transparent: true, opacity: 0.3, side: THREE.DoubleSide });
const backWall = new THREE.Mesh(new THREE.PlaneGeometry(roomWidth, CFG.WALL_H), wallMat);
backWall.position.set(centerX, CFG.WALL_H/2, centerZ - roomDepth/2);
this.three.scene.add(backWall);
this.three.meshes.push(backWall);
const leftWall = new THREE.Mesh(new THREE.PlaneGeometry(roomDepth, CFG.WALL_H), wallMat);
leftWall.rotation.y = Math.PI / 2;
leftWall.position.set(centerX - roomWidth/2, CFG.WALL_H/2, centerZ);
this.three.scene.add(leftWall);
this.three.meshes.push(leftWall);
const rightWall = new THREE.Mesh(new THREE.PlaneGeometry(roomDepth, CFG.WALL_H), wallMat);
rightWall.rotation.y = Math.PI / 2;
rightWall.position.set(centerX + roomWidth/2, CFG.WALL_H/2, centerZ);
this.three.scene.add(rightWall);
this.three.meshes.push(rightWall);
let avail = 0;
room.beds.forEach(b => { if(b.canuse) avail++; });
const labelStr = `${room.name} ${room.gender ? '(男)' : '(女)'}\n(可用: ${avail}/${bedCount})`;
const label = this.makeTextSprite(labelStr, 32, "#333333", "rgba(255,255,255,0.85)");
label.position.set(centerX, CFG.WALL_H + 5, centerZ - roomDepth/2);
this.three.scene.add(label);
this.three.meshes.push(label);
const startX = centerX - roomWidth/2 + CFG.ROOM_MARGIN;
const startZ = centerZ - roomDepth/2 + CFG.ROOM_MARGIN;
room.beds.forEach((bed, index) => {
if (!this.shouldShowBed(bed)) return;
const bedGeometry = new THREE.BoxGeometry(CFG.BED_W, CFG.BED_H, CFG.BED_D);
//const bedColor = bed.canuse ? 0x4caf50 : 0xf44336;
//const bedColor = this.getBedColor(bed);
const style = this.getBedStyle(bed);
const bedMaterial = new THREE.MeshLambertMaterial({
color: style.color,
emissive: style.emissive,
emissiveIntensity: style.intensity
});
//const bedMaterial = new THREE.MeshLambertMaterial({ color: bedColor });
const bedMesh = new THREE.Mesh(bedGeometry, bedMaterial);
const r = Math.floor(index / cols);
const c = index % cols;
const xPos = startX + (c * CFG.COL_SPACING);
const zPos = startZ + (r * CFG.ROW_SPACING);
bedMesh.position.set(xPos, CFG.BED_H/2, zPos);
bedMesh.userData = { type: 'bed', bedInfo: bed };
this.three.scene.add(bedMesh);
this.three.meshes.push(bedMesh);
//const bedLabelStr = `${bed.name}\n[${bed.canuse ? '空床' : '佔用'}]`;
const bedLabelStr = this.getBedLabelText(bed);
const bedLabel = this.makeTextSprite(bedLabelStr, 20, "#ffffff", "rgba(0,0,0,0.7)");
bedLabel.position.set(xPos, CFG.BED_H + 4, zPos);
this.three.scene.add(bedLabel);
this.three.meshes.push(bedLabel);
});
roomOffsetX += roomWidth + CFG.ROOM_GAP;
});
regionOffsetZ += maxRoomDepth + CFG.REGION_GAP;
});
},
// 【Level 1: 渲染所有大樓/樓層區塊 (Drilldown 模式)】
renderAllRegions() {
// 區塊基礎大小
const bW = 40, bH = 15, bD = 40;
let offsetX = - (Math.min(this.regions.length, 3) * (bW+20)) / 2 + (bW/2);
let offsetZ = -10;
this.regions.forEach((region, i) => {
if (region.isStop) return;
let total = 0, avail = 0;
(region.room || []).forEach(r => (r.beds || []).forEach(b => {
total++;
if (b.canuse) avail++;
}));
const boxGeo = new THREE.BoxGeometry(bW, bH, bD);
const boxMat = new THREE.MeshLambertMaterial({ color: 0x3f51b5, transparent: true, opacity: 0.85 });
const boxMesh = new THREE.Mesh(boxGeo, boxMat);
boxMesh.position.set(offsetX, bH/2, offsetZ);
const edges = new THREE.EdgesGeometry(boxGeo);
const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0xffffff, linewidth: 2 }));
boxMesh.add(line);
boxMesh.userData = { type: 'region', regionInfo: region };
this.three.scene.add(boxMesh);
this.three.meshes.push(boxMesh);
const labelStr = `${region.name}\n(床位: ${avail} 可用 / ${total} 總計)`;
const label = this.makeTextSprite(labelStr, 32, "#ffffff", "rgba(0,0,0,0.6)");
label.position.set(offsetX, bH + 10, offsetZ);
this.three.scene.add(label);
this.three.meshes.push(label);
offsetX += bW + 20;
if ((i + 1) % 3 === 0) {
offsetX = - (Math.min(this.regions.length, 3) * (bW+20)) / 2 + (bW/2);
offsetZ += bD + 20;
}
});
},
// 【Level 2: 渲染選擇樓層內的所有房間與床位 (Drilldown 模式)】
renderRoomsInRegion() {
const rooms = this.viewState.region.room || [];
const maxCols = 3;
let offsetZ = -10;
for (let i = 0; i < rooms.length; i += maxCols) {
const rowRooms = rooms.slice(i, i + maxCols);
// 假設所有房間寬度差不多,先算個大概置中點
let offsetX = - ((rowRooms.length - 1) * 60) / 2;
let maxRoomDepth = 0;
rowRooms.forEach((room) => {
const bedCount = room.beds ? room.beds.length : 0;
const cols = 2;
const rows = bedCount > 0 ? Math.ceil(bedCount / cols) : 1;
const roomWidth = (cols * CFG.COL_SPACING) + CFG.ROOM_MARGIN;
const roomDepth = (rows * CFG.ROW_SPACING) + CFG.ROOM_MARGIN;
if (roomDepth > maxRoomDepth) maxRoomDepth = roomDepth;
const floorColor = room.gender ? 0xe3f2fd : 0xfce4ec;
const floorGeo = new THREE.PlaneGeometry(roomWidth, roomDepth);
const floorMat = new THREE.MeshLambertMaterial({ color: floorColor, side: THREE.DoubleSide });
const floorMesh = new THREE.Mesh(floorGeo, floorMat);
floorMesh.rotation.x = -Math.PI / 2;
floorMesh.position.set(offsetX, 0, offsetZ);
const edges = new THREE.EdgesGeometry(floorGeo);
const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial( { color: 0x999999 } ) );
floorMesh.add(line);
floorMesh.userData = { type: 'room', roomInfo: room };
this.three.scene.add(floorMesh);
this.three.meshes.push(floorMesh);
const wallMat = new THREE.MeshLambertMaterial({ color: 0xb0bec5, transparent: true, opacity: 0.4, side: THREE.DoubleSide });
const backWall = new THREE.Mesh(new THREE.PlaneGeometry(roomWidth, CFG.WALL_H), wallMat);
backWall.position.set(offsetX, CFG.WALL_H/2, offsetZ - roomDepth/2);
this.three.scene.add(backWall);
this.three.meshes.push(backWall);
const leftWall = new THREE.Mesh(new THREE.PlaneGeometry(roomDepth, CFG.WALL_H), wallMat);
leftWall.rotation.y = Math.PI / 2;
leftWall.position.set(offsetX - roomWidth/2, CFG.WALL_H/2, offsetZ);
this.three.scene.add(leftWall);
this.three.meshes.push(leftWall);
const rightWall = new THREE.Mesh(new THREE.PlaneGeometry(roomDepth, CFG.WALL_H), wallMat);
rightWall.rotation.y = Math.PI / 2;
rightWall.position.set(offsetX + roomWidth/2, CFG.WALL_H/2, offsetZ);
this.three.scene.add(rightWall);
this.three.meshes.push(rightWall);
let total = 0, avail = 0;
(room.beds || []).forEach(b => {
total++;
if (b.canuse) avail++;
});
const labelStr = `${room.name} ${room.gender ? '(男)' : '(女)'}\n(可用: ${avail}/${total})`;
const label = this.makeTextSprite(labelStr, 32, "#333333", "rgba(255,255,255,0.85)");
label.position.set(offsetX, CFG.WALL_H + 5, offsetZ - roomDepth/2);
this.three.scene.add(label);
this.three.meshes.push(label);
if (room.beds && room.beds.length > 0) {
const startX = offsetX - roomWidth/2 + CFG.ROOM_MARGIN;
const startZ = offsetZ - roomDepth/2 + CFG.ROOM_MARGIN;
room.beds.forEach((bed, index) => {
if (!this.shouldShowBed(bed)) return;
const bedGeometry = new THREE.BoxGeometry(CFG.BED_W, CFG.BED_H, CFG.BED_D);
//const bedColor = bed.canuse ? 0x4caf50 : 0xf44336;
//const bedColor = this.getBedColor(bed);
//const bedMaterial = new THREE.MeshLambertMaterial({ color: bedColor });
const style = this.getBedStyle(bed);
const bedMaterial = new THREE.MeshLambertMaterial({
color: style.color,
emissive: style.emissive,
emissiveIntensity: style.intensity
});
const bedMesh = new THREE.Mesh(bedGeometry, bedMaterial);
const r = Math.floor(index / cols);
const c = index % cols;
const xPos = startX + (c * CFG.COL_SPACING);
const zPos = startZ + (r * CFG.ROW_SPACING);
bedMesh.position.set(xPos, CFG.BED_H/2, zPos);
bedMesh.userData = { type: 'bed', bedInfo: bed };
this.three.scene.add(bedMesh);
this.three.meshes.push(bedMesh);
//const bedLabelStr = `${bed.name}\n[${bed.canuse ? '空床' : '佔用'}]`;
const bedLabelStr = this.getBedLabelText(bed);
const bedLabel = this.makeTextSprite(bedLabelStr, 20, "#ffffff", "rgba(0,0,0,0.7)");
bedLabel.position.set(xPos, CFG.BED_H + 4, zPos);
this.three.scene.add(bedLabel);
this.three.meshes.push(bedLabel);
});
}
offsetX += roomWidth + CFG.ROOM_GAP;
});
offsetZ += maxRoomDepth + CFG.REGION_GAP;
}
},
// 【Level 3: 渲染單一聚焦房間內的所有立體床鋪 (Drilldown 模式)】
renderBedsInRoom() {
const room = this.viewState.room;
if (!room || !room.beds) return;
const bedCount = room.beds.length;
if (bedCount === 0) return;
const cols = 2;
const rows = Math.ceil(bedCount / cols);
const roomWidth = (cols * CFG.COL_SPACING) + CFG.ROOM_MARGIN;
const roomDepth = (rows * CFG.ROW_SPACING) + CFG.ROOM_MARGIN;
const floorColor = room.gender ? 0xe3f2fd : 0xfce4ec;
const floorGeo = new THREE.PlaneGeometry(roomWidth, roomDepth);
const floorMat = new THREE.MeshLambertMaterial({ color: floorColor, side: THREE.DoubleSide });
const floorMesh = new THREE.Mesh(floorGeo, floorMat);
floorMesh.rotation.x = -Math.PI / 2;
floorMesh.position.set(0, 0, 0);
const edges = new THREE.EdgesGeometry(floorGeo);
const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial( { color: 0x999999 } ) );
floorMesh.add(line);
this.three.scene.add(floorMesh);
this.three.meshes.push(floorMesh);
const wallMat = new THREE.MeshLambertMaterial({ color: 0xb0bec5, transparent: true, opacity: 0.4, side: THREE.DoubleSide });
const backWall = new THREE.Mesh(new THREE.PlaneGeometry(roomWidth, CFG.WALL_H), wallMat);
backWall.position.set(0, CFG.WALL_H/2, -roomDepth/2);
this.three.scene.add(backWall);
this.three.meshes.push(backWall);
const leftWall = new THREE.Mesh(new THREE.PlaneGeometry(roomDepth, CFG.WALL_H), wallMat);
leftWall.rotation.y = Math.PI / 2;
leftWall.position.set(-roomWidth/2, CFG.WALL_H/2, 0);
this.three.scene.add(leftWall);
this.three.meshes.push(leftWall);
const rightWall = new THREE.Mesh(new THREE.PlaneGeometry(roomDepth, CFG.WALL_H), wallMat);
rightWall.rotation.y = Math.PI / 2;
rightWall.position.set(roomWidth/2, CFG.WALL_H/2, 0);
this.three.scene.add(rightWall);
this.three.meshes.push(rightWall);
const startX = -roomWidth/2 + CFG.ROOM_MARGIN;
const startZ = -roomDepth/2 + CFG.ROOM_MARGIN;
room.beds.forEach((bed, index) => {
if (!this.shouldShowBed(bed)) return;
const bedGeometry = new THREE.BoxGeometry(CFG.BED_W, CFG.BED_H, CFG.BED_D);
//const bedColor = bed.canuse ? 0x4caf50 : 0xf44336;
//const bedColor = this.getBedColor(bed);
//const bedMaterial = new THREE.MeshLambertMaterial({ color: bedColor });
const style = this.getBedStyle(bed);
const bedMaterial = new THREE.MeshLambertMaterial({
color: style.color,
emissive: style.emissive,
emissiveIntensity: style.intensity
});
const bedMesh = new THREE.Mesh(bedGeometry, bedMaterial);
const row = Math.floor(index / cols);
const col = index % cols;
const xPos = startX + (col * CFG.COL_SPACING);
const zPos = startZ + (row * CFG.ROW_SPACING);
bedMesh.position.set(xPos, CFG.BED_H/2, zPos);
bedMesh.userData = { type: 'bed', bedInfo: bed };
this.three.scene.add(bedMesh);
this.three.meshes.push(bedMesh);
//const labelStr = `${bed.name}\n[${bed.canuse ? '空床' : '佔用'}]`;
const labelStr = this.getBedLabelText(bed);
const bedLabel = this.makeTextSprite(labelStr, 24, "#ffffff", "rgba(0,0,0,0.7)");
bedLabel.position.set(xPos, CFG.BED_H + 4, zPos);
this.three.scene.add(bedLabel);
this.three.meshes.push(bedLabel);
});
},
animate() {
this.three.animationId = requestAnimationFrame(this.animate);
if(this.three.controls) this.three.controls.update();
if(this.three.renderer && this.three.scene && this.three.camera) {
this.three.renderer.render(this.three.scene, this.three.camera);
}
},
onCanvasClick(event) {
const container = this.$refs.threeContainer;
if (!container || !this.three.raycaster || !this.three.mouse) return;
const rect = container.getBoundingClientRect();
this.three.mouse.x = ((event.clientX - rect.left) / container.clientWidth) * 2 - 1;
this.three.mouse.y = -((event.clientY - rect.top) / container.clientHeight) * 2 + 1;
this.three.raycaster.setFromCamera(this.three.mouse, this.three.camera);
const meshesOnly = this.three.scene.children.filter(child => child instanceof THREE.Mesh);
const intersects = this.three.raycaster.intersectObjects(meshesOnly, true);
if (intersects.length > 0) {
const clickedObject = intersects[0].object;
const userData = clickedObject.userData;
if (userData) {
if (userData.type === 'region' && this.viewMode === 'DRILLDOWN') {
this.drillToRegion(userData.regionInfo);
} else if (userData.type === 'room' && this.viewMode === 'DRILLDOWN') {
this.drillToRoom(userData.roomInfo);
} else if (userData.type === 'bed') {
this.showBedSchedule(userData.bedInfo);
}
}
}
},
onWindowResize() {
const container = this.$refs.threeContainer;
if (!container || !this.three.camera || !this.three.renderer) return;
this.three.camera.aspect = container.clientWidth / container.clientHeight;
this.three.camera.updateProjectionMatrix();
this.three.renderer.setSize(container.clientWidth, container.clientHeight);
}
},
watch: {
'filter.selectedArea'() { this.loadRooms(); },
'filter.startDate'() { this.GetRegionList(); },
'filter.occupied'() { this.render3DRooms(); },
'filter.unoccupied'() { this.render3DRooms(); },
'filter.Gender'() { this.render3DRooms(); },
'isFilterExpanded'() {
setTimeout(() => { this.onWindowResize(); }, 350);
},
'filter.selectedLimits'() { this.render3DRooms(); },
},
mounted() {
if (!this.regions) this.regions = [];
this.filter.startDate = this.today;
this.getRegionWithRoom();
this.loadRooms();
this.$nextTick(() => {
this.initThree();
this.GetRegionList();
});
},
beforeDestroy() {
if (this.three.animationId) cancelAnimationFrame(this.three.animationId);
window.removeEventListener('resize', this.onWindowResize);
const container = this.$refs.threeContainer;
if (container) container.removeEventListener('click', this.onCanvasClick);
},
computed: {
filteredStats() {
if (!this.regions || !Array.isArray(this.regions) || this.regions.length === 0) {
return { maleBeds: 0, femaleBeds: 0 };
}
let maleBeds = 0;
let femaleBeds = 0;
this.regions.forEach(region => {
(region.room || []).forEach(room => {
(room.beds || []).forEach(bed => {
if (bed.canuse) {
if (bed.gender) maleBeds++;
else femaleBeds++;
}
});
});
});
return { maleBeds, femaleBeds };
}
}
});
</script>
</asp:Content>