Files
PlantAI-dev/apps/web/af3_sandbox/index.html
T
2026-08-03 18:42:47 +08:00

160 lines
5.3 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AF3 Mol* 三维查看沙箱</title>
<link rel="icon" href="data:,">
<link rel="stylesheet" href="/static/vendor/molstar/molstar.css">
<link rel="stylesheet" href="/static/af3-sandbox.css">
</head>
<body>
<div id="app">
<main class="sandbox">
<header class="toolbar">
<div class="title">AF3 Mol* 三维结构查看器</div>
<label class="control">
<span>结构</span>
<select v-model="selected" :disabled="loading || structures.length === 0" @change="loadSelected">
<option v-for="item in structures" :key="item.name" :value="item.name">
{{ item.name }}
</option>
</select>
</label>
<label class="control">
<span>表示</span>
<select v-model="representation" :disabled="loading || !selected" @change="loadSelected">
<option value="cartoon">Cartoon</option>
<option value="ball_and_stick">Ball and stick</option>
<option value="surface">Surface</option>
</select>
</label>
<button type="button" :disabled="loading || !selected" @click="loadSelected">重新加载</button>
<button type="button" :disabled="!viewerReady" @click="resetCamera">重置视角</button>
<a
class="download"
:class="{ disabled: !selected }"
:href="downloadUrl"
:aria-disabled="!selected"
:download="selected || null"
@click="guardDownload"
>下载 CIF</a>
<span class="status" :class="{ error: error }">{{ error || status }}</span>
</header>
<section class="viewer-shell">
<div id="molstar-viewer"></div>
<div v-if="fatalError" class="empty-state error">{{ fatalError }}</div>
</section>
</main>
</div>
<script src="/shared-static/cdn/vue.global.prod.js"></script>
<script src="/static/vendor/molstar/molstar.js"></script>
<script src="/static/af3-viewer.js"></script>
<script>
const { createApp, ref, computed, onMounted, onUnmounted } = Vue;
createApp({
setup() {
const structures = ref([]);
const selected = ref('');
const representation = ref('cartoon');
const status = ref('初始化中');
const error = ref('');
const fatalError = ref('');
const loading = ref(false);
const viewerReady = ref(false);
let viewer = null;
const downloadUrl = computed(() =>
selected.value ? `/api/structures/${encodeURIComponent(selected.value)}` : '#',
);
function hasWebGL() {
const canvas = document.createElement('canvas');
return Boolean(
window.WebGL2RenderingContext && canvas.getContext('webgl2')
|| window.WebGLRenderingContext && canvas.getContext('webgl'),
);
}
async function loadSelected() {
if (!viewer || !selected.value) return;
loading.value = true;
error.value = '';
status.value = '加载中';
try {
const url = `/api/structures/${encodeURIComponent(selected.value)}`;
await viewer.load(url, representation.value);
status.value = '加载完成';
} catch (loadError) {
console.error(loadError);
error.value = `加载失败:${loadError.message || loadError}`;
} finally {
loading.value = false;
}
}
function resetCamera() {
viewer?.resetCamera();
}
function guardDownload(event) {
if (!selected.value) event.preventDefault();
}
onMounted(async () => {
try {
if (!hasWebGL()) {
throw new Error('当前浏览器未启用 WebGL,请开启硬件加速后重试');
}
viewer = await window.Af3MolstarViewer.create('molstar-viewer');
viewerReady.value = true;
const response = await fetch('/api/structures');
if (!response.ok) throw new Error(`结构列表请求失败(${response.status})`);
const body = await response.json();
structures.value = body.structures || [];
if (!structures.value.length) {
throw new Error('数据目录中没有可用的 CIF/mmCIF 文件');
}
const preferred = structures.value.find(
(item) => item.name === 'gcn4_dimer_demo_model.cif',
);
selected.value = (preferred || structures.value[0]).name;
await loadSelected();
} catch (mountError) {
console.error(mountError);
fatalError.value = mountError.message || String(mountError);
error.value = fatalError.value;
}
});
onUnmounted(() => {
viewer?.dispose();
viewer = null;
});
return {
structures,
selected,
representation,
status,
error,
fatalError,
loading,
viewerReady,
downloadUrl,
loadSelected,
resetCamera,
guardDownload,
};
},
}).mount('#app');
</script>
</body>
</html>