跳到主要内容

综合示例:浏览器完成完整链路

本示例把 Project Key、模型上传、状态轮询、3D 模型加载和预览图展示串成一个完整的浏览器页面。代码基于线上 Demo 的实际流程整理,适合用于验证接口联通性;正式业务仍建议由服务端保管 Project Key。

线上 Demo:打开 Project Key 上传与 Viewer Demo

安全提示

Project Key 只在当前页面内存中使用。示例不会把 Key 写入 URL、LocalStorage、Cookie 或日志。请只使用测试工程 Key,不要在公开页面输入生产主 Key。

完整 HTML 示例​

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>天工图学 API 综合示例</title>
<style>
.layout { display: grid; grid-template-columns: 360px 1fr; gap: 16px; }
#viewer { height: 560px; border: 1px solid #ccd3df; }
#status { white-space: pre-wrap; background: #f5f7fa; padding: 12px; }
@media (max-width: 800px) { .layout { grid-template-columns: 1fr; } }
</style>
</head>
<body>
<h1>Project Key 上传与 3D 浏览</h1>
<p>线上 Demo:<a href="https://tiangongtuxue.com/manual/demo-app/">打开 Demo</a></p>

<div class="layout">
<section>
<label>Project Key
<input id="project-key" type="password" autocomplete="off" />
</label>
<label>模型名称
<input id="model-name" value="bracket.step" />
</label>
<label>模型文件
<input id="model-file" type="file" accept=".prt,.prt.x,.step,.stp,.igs,.iges,.x_t,.sldprt,.catpart,.jt,.par,.psm,.sat,.sab" />
</label>
<small>支持 .prt、.prt.x、.step、.stp、.igs、.iges、.x_t、.sldprt、.catpart、.jt、.par、.psm、.sat、.sab</small>
<button id="start">上传并解析</button>
<button id="clear">清除</button>
<pre id="status">等待操作</pre>
<img id="preview" alt="模型预览图" style="max-width:100%;display:none" />
</section>
<section id="viewer"></section>
</div>

<script>
(() => {
const API_BASE = '/api';
let accessToken = '';
let modelId = '';
let viewer = null;
let viewerLibraryPromise = null;

const $ = (id) => document.getElementById(id);
const showStatus = (message) => { $('status').textContent = message; };

// 解析成功后再加载固定版本 Viewer,避免页面启动阶段执行大型 UMD 包。
function loadViewerLibrary() {
if (viewerLibraryPromise) return viewerLibraryPromise;
viewerLibraryPromise = new Promise((resolve, reject) => {
if (typeof window.TuoVisualViewer?.TuoVisualViewer === 'function') {
resolve(window.TuoVisualViewer.TuoVisualViewer);
return;
}
const script = document.createElement('script');
script.src = 'https://resource.api.tiangongtuxue.com/tuo-visual-viewer/1.0.0/tuo-visual-viewer.umd.js';
script.async = false;
script.onload = () => typeof window.TuoVisualViewer?.TuoVisualViewer === 'function'
? resolve(window.TuoVisualViewer.TuoVisualViewer)
: reject(new Error('Viewer 组件加载成功但未找到构造函数'));
script.onerror = () => reject(new Error('Viewer 组件加载失败,请检查网络或 CDN 访问'));
(document.head || document.body || document.documentElement).appendChild(script);
});
return viewerLibraryPromise;
}

// 统一请求函数:解析 JSON 错误,并避免在页面日志中输出 Token。
async function request(path, init = {}) {
const response = await fetch(API_BASE + path, init);
const text = await response.text();
let body = {};
try { body = JSON.parse(text); } catch (_) {}
if (!response.ok) {
throw new Error(body.error?.message || `${response.status} ${text}`);
}
return body;
}

// 第一步:Project Key 只用于换取短期 Access Token。
async function getAccessToken(projectKey) {
const body = await request('/app/projects/access-token', {
method: 'POST',
headers: { Authorization: `Bearer ${projectKey}` },
});
return body.token || body.data?.token;
}

// 第二步:创建模型并获取一次性上传凭证。
async function createModel(fileName) {
const body = await request('/v1/models/upload-token', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: fileName, description: 'browser demo', webhook: null }),
});
return body.data;
}

// 第三步:使用返回的上传地址和凭证直传文件。
async function uploadFile(grant, file) {
const form = new FormData();
form.append('token', grant.uploadToken);
form.append('file', file, file.name);
const response = await fetch(grant.uploadUrl, { method: 'POST', body: form });
if (!response.ok) throw new Error(`文件上传失败:${response.status}`);
}

// 第四步:轮询结果接口,直到成功、失败或过期。
async function waitForResult(id) {
for (let attempt = 1; attempt <= 120; attempt += 1) {
const body = await request(`/v1/models/${id}/result`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
const result = body.data || body;
showStatus(`第 ${attempt} 次查询\n状态:${result.status}\n阶段:${result.phase}\n${result.message || ''}`);

if (result.status === 'succeeded') return result;
if (result.status === 'failed' || result.status === 'expired') {
throw new Error(result.message || `模型状态:${result.status}`);
}
// 正式业务建议使用退避间隔或 Webhook;这里使用 5 秒便于演示。
await new Promise((resolve) => setTimeout(resolve, 5000));
}
throw new Error('解析超时,请稍后重新查询模型状态');
}

// 第五步:下载预览图并加载 3D 结果。
async function showResult(result) {
const preview = $('preview');
if (result.imageUrl) {
// 预览图接口同样需要鉴权,先取 Blob 再交给 img 展示。
const previewResponse = await fetch(result.imageUrl, {
headers: { Authorization: `Bearer ${accessToken}` },
});
if (!previewResponse.ok) throw new Error(`预览图获取失败:${previewResponse.status}`);
preview.src = URL.createObjectURL(await previewResponse.blob());
preview.style.display = 'block';
}

if (result.url) {
const response = await fetch(result.url, {
headers: { Authorization: `Bearer ${accessToken}` },
});
if (!response.ok) throw new Error(`三维结果获取失败:${response.status}`);
const blob = await response.blob();
const Viewer = await loadViewerLibrary();
viewer?.destroy();
viewer = new Viewer($('viewer'));
await viewer.load(blob, { format: 'tgvdata' });
}
showStatus('解析完成:预览图和 3D 模型已经加载。');
}

$('start').onclick = async () => {
try {
const projectKey = $('project-key').value.trim();
const file = $('model-file').files[0];
if (!projectKey || !file) throw new Error('请输入 Project Key 并选择模型文件');

showStatus('正在换取短期 Access Token…');
accessToken = await getAccessToken(projectKey);
if (!accessToken) throw new Error('响应中没有 Access Token');

showStatus('正在创建模型并获取上传授权…');
const grant = await createModel($('model-name').value.trim() || file.name);
modelId = grant.modelId;

showStatus(`正在上传文件…\nmodelId:${modelId}`);
await uploadFile(grant, file);

showStatus('文件已上传,正在查询模型状态…');
const result = await waitForResult(modelId);
await showResult(result);
} catch (error) {
showStatus(`错误:${error.message}`);
}
};

$('clear').onclick = () => {
// 清除内存中的 Key、Token、模型 ID 和 Viewer。
accessToken = '';
modelId = '';
$('project-key').value = '';
$('model-file').value = '';
$('preview').removeAttribute('src');
$('preview').style.display = 'none';
viewer?.destroy();
viewer = null;
showStatus('已清除');
};
})();
</script>
</body>
</html>

流程对应关系​

代码步骤调用接口或动作产生的数据
getAccessTokenPOST /api/app/projects/access-tokenaccessToken
createModelPOST /api/v1/models/upload-tokenmodelId、uploadToken、uploadUrl
uploadFile使用上传授权直传云存储文件上传完成
waitForResultGET /api/v1/models/:modelId/result状态、阶段、结果地址
showResult获取 imageUrl 和 url预览图、TGVData Viewer

实际线上版本可直接访问:Project Key 上传与 Viewer Demo。