Files

160 lines
5.3 KiB
HTML
Raw Permalink Normal View History

2026-08-03 18:42:47 +08:00
<!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>