init
This commit is contained in:
@@ -0,0 +1,779 @@
|
||||
<template>
|
||||
<div class="pendulum-analysis" role="dialog" aria-modal="true">
|
||||
<div class="safe-area">
|
||||
<header class="header">
|
||||
<h2 class="title">单摆分析</h2>
|
||||
<div class="header-actions">
|
||||
<button
|
||||
class="btn ghost"
|
||||
@click="onRetry"
|
||||
:disabled="loading"
|
||||
v-show="!showLengthInput"
|
||||
>
|
||||
{{ loading ? '分析中…' : '重新分析' }}
|
||||
</button>
|
||||
<button class="btn danger" @click="closeAnalysis" aria-label="关闭">×</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 状态条 -->
|
||||
<div class="status-bar" v-if="loading || error || lastUpdated">
|
||||
<div class="loading" v-if="loading">
|
||||
<div class="spinner" aria-hidden="true"></div>
|
||||
<span>正在分析数据,请稍候…</span>
|
||||
</div>
|
||||
<div class="error" v-else-if="error">
|
||||
<span>{{ error }}</span>
|
||||
<button class="btn small" @click="onRetry">重试</button>
|
||||
</div>
|
||||
<div class="meta" v-else-if="lastUpdated">
|
||||
<span>上次更新:{{ lastUpdated }}</span>
|
||||
<span v-if="sessionId">会话:{{ sessionId.slice(0, 8) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main class="content">
|
||||
<!-- 摆长输入界面 -->
|
||||
<section v-if="showLengthInput" class="length-input-section">
|
||||
<h3>请输入摆长参数</h3>
|
||||
<div class="input-form">
|
||||
<div class="input-group">
|
||||
<label for="pendulum-length">摆长 (米):</label>
|
||||
<input
|
||||
id="pendulum-length"
|
||||
v-model.number="pendulumLength"
|
||||
type="number"
|
||||
step="0.001"
|
||||
min="0.1"
|
||||
max="10"
|
||||
class="length-input"
|
||||
placeholder="请输入摆长,如 1.0"
|
||||
/>
|
||||
</div>
|
||||
<div class="input-actions">
|
||||
<button class="btn primary" @click="startAnalysis" :disabled="!isValidLength">
|
||||
开始分析
|
||||
</button>
|
||||
<button class="btn ghost" @click="closeAnalysis">
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-tips">
|
||||
<p>提示:</p>
|
||||
<ul>
|
||||
<li>摆长是从悬挂点到小球中心的距离</li>
|
||||
<li>请确保输入的摆长单位为米</li>
|
||||
<li>典型的实验室单摆摆长范围:0.5-2.0米</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 分析结果界面(原有内容) -->
|
||||
<template v-else>
|
||||
<!-- 左侧:图片栅格(横屏优先) -->
|
||||
<section class="results" aria-label="分析图像">
|
||||
<!-- 骨架屏 -->
|
||||
<div v-if="loading && !results.length" class="skeleton-grid">
|
||||
<div class="skeleton-card" v-for="i in 3" :key="'skel-' + i"></div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="results.length" class="image-grid">
|
||||
<figure v-for="(image, index) in results" :key="index" class="result-item" @click="openLightbox(index)">
|
||||
<img :src="image.url" :alt="image.title" class="result-image" loading="eager" decoding="async" />
|
||||
<figcaption class="result-title">{{ image.title }}</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else class="empty">
|
||||
上传数据后将显示分析图像
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 右侧:参数信息(粘性面板) -->
|
||||
<aside class="analysis-info" v-if="analysisInfo">
|
||||
<h3>分析结果</h3>
|
||||
<div class="info-grid">
|
||||
<div class="info-item" v-for="row in formattedInfo" :key="row.key">
|
||||
<div class="info-label">{{ row.label }}</div>
|
||||
<div class="info-value">{{ row.value }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tips">
|
||||
<p>提示:</p>
|
||||
<ul>
|
||||
<li>点击左侧图像可全屏查看。</li>
|
||||
<li>如网络抖动,可点“重新分析”。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 图片灯箱 -->
|
||||
<div v-if="lightbox.open" class="lightbox" @click.self="closeLightbox">
|
||||
<button class="btn danger close-lightbox" @click="closeLightbox" aria-label="关闭预览">×</button>
|
||||
<div class="lightbox-inner">
|
||||
<img :src="results[lightbox.index].url" :alt="results[lightbox.index].title" />
|
||||
<div class="lightbox-caption">{{ results[lightbox.index].title }}</div>
|
||||
</div>
|
||||
<button v-if="results.length > 1" class="nav prev" @click.stop="navLightbox(-1)" aria-label="上一张">‹</button>
|
||||
<button v-if="results.length > 1" class="nav next" @click.stop="navLightbox(1)" aria-label="下一张">›</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||
|
||||
type RawPoint = { frame: number; time: number; x: number; y: number };
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
data: RawPoint[],
|
||||
/** 可选:后端基础地址,如 'http://localhost:5000';默认同源 */
|
||||
apiBase?: string,
|
||||
/** 可选:组件打开时自动分析 */
|
||||
autoAnalyze?: boolean
|
||||
}>(), {
|
||||
apiBase: '',
|
||||
autoAnalyze: false
|
||||
});
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const loading = ref(false);
|
||||
const error = ref('');
|
||||
const results = ref<{ url: string; title: string }[]>([]);
|
||||
const analysisInfo = ref<Record<string, any> | null>(null);
|
||||
const sessionId = ref<string | null>(null);
|
||||
const lastUpdated = ref<string | null>(null);
|
||||
const pendulumLength = ref<number>(1.0); // 摆长,单位:米
|
||||
const showLengthInput = ref(true); // 显示摆长输入界面
|
||||
|
||||
let controller: AbortController | null = null;
|
||||
|
||||
const closeAnalysis = () => emit('close');
|
||||
const onRetry = () => analyzeData(true);
|
||||
|
||||
/** 验证摆长输入是否有效 */
|
||||
const isValidLength = computed(() => {
|
||||
return pendulumLength.value > 0.1 && pendulumLength.value <= 10;
|
||||
});
|
||||
|
||||
/** 开始分析 */
|
||||
const startAnalysis = () => {
|
||||
if (!isValidLength.value) return;
|
||||
showLengthInput.value = false;
|
||||
analyzeData(true);
|
||||
};
|
||||
|
||||
/** 将相对 URL 前缀化为 {apiBase}+path,保证跨域部署可用 */
|
||||
const withBase = (url: string) => {
|
||||
if (!url) return url;
|
||||
try {
|
||||
// 已是绝对 URL
|
||||
new URL(url);
|
||||
return url;
|
||||
} catch {
|
||||
const base = (props.apiBase || '').replace(/\/+$/, '');
|
||||
return base + url;
|
||||
}
|
||||
};
|
||||
|
||||
/** 新后端字段映射与格式化 */
|
||||
const labelMap: Record<string, string> = {
|
||||
pendulum_length_m: '摆长',
|
||||
gravity_acceleration_m_s2: '重力加速度',
|
||||
damping_coefficient_s_inv: '阻尼系数 γ',
|
||||
period_s: '周期',
|
||||
angle_max_rad: '最大摆角'
|
||||
};
|
||||
const unitMap: Record<string, string> = {
|
||||
pendulum_length_m: ' m',
|
||||
gravity_acceleration_m_s2: ' m/s²',
|
||||
damping_coefficient_s_inv: ' s⁻¹',
|
||||
period_s: ' s',
|
||||
angle_max_rad: ' rad'
|
||||
};
|
||||
const formatNumber = (v: any) => typeof v === 'number' ? (Math.abs(v) < 1e-3 ? v.toExponential(3) : v.toFixed(4)) : v;
|
||||
|
||||
const formattedInfo = computed(() => {
|
||||
if (!analysisInfo.value) return [];
|
||||
return Object.entries(analysisInfo.value).map(([key, val]) => ({
|
||||
key,
|
||||
label: labelMap[key] || key,
|
||||
value: `${formatNumber(val)}${unitMap[key] || ''}`
|
||||
}));
|
||||
});
|
||||
|
||||
/** 生成 CSV(列满足后端:time, x, y, pendulum_length) */
|
||||
const buildCsv = () => {
|
||||
const lines = ['time,x,y,pendulum_length']; // 添加摆长列
|
||||
for (const r of props.data) {
|
||||
lines.push(`${r.time},${r.x},${r.y},${pendulumLength.value}`);
|
||||
}
|
||||
return lines.join('\n');
|
||||
};
|
||||
|
||||
/** 图片标题 */
|
||||
const getImageTitle = (index: number) => {
|
||||
const titles = [
|
||||
'单摆拟合分析',
|
||||
'物理参数摘要',
|
||||
'周期与摆角关系',
|
||||
];
|
||||
return titles[index] || `图像 ${index + 1}`;
|
||||
};
|
||||
|
||||
/** 主流程:发送到 {apiBase}/api/analyze */
|
||||
const analyzeData = async (force = false) => {
|
||||
if (!props.data?.length) return;
|
||||
if (loading.value && !force) return;
|
||||
|
||||
// 取消上一次请求
|
||||
if (controller) controller.abort();
|
||||
controller = new AbortController();
|
||||
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
if (force) results.value = [];
|
||||
|
||||
try {
|
||||
const csv = buildCsv();
|
||||
const fd = new FormData();
|
||||
fd.append('csv_file', new Blob([csv], { type: 'text/csv' }), 'pendulum_data.csv');
|
||||
|
||||
const timeout = setTimeout(() => controller?.abort(), 60_000);
|
||||
const endpoint = `${(props.apiBase || '').replace(/\/+$/, '')}/api/analyze`;
|
||||
|
||||
const resp = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
body: fd,
|
||||
signal: controller.signal
|
||||
}).finally(() => clearTimeout(timeout));
|
||||
|
||||
// 解析错误
|
||||
if (!resp.ok) {
|
||||
let msg = `服务器错误: ${resp.status}`;
|
||||
try {
|
||||
const j = await resp.json();
|
||||
if (j?.error) msg = j.error;
|
||||
} catch { /* ignore */ }
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
const json = await resp.json();
|
||||
|
||||
if (json.success === false) {
|
||||
throw new Error(json.error || '分析失败');
|
||||
}
|
||||
|
||||
// 结果
|
||||
sessionId.value = json.session_id || null;
|
||||
|
||||
if (Array.isArray(json.images)) {
|
||||
results.value = json.images.map((p: string, i: number) => ({
|
||||
url: withBase(p),
|
||||
title: getImageTitle(i)
|
||||
}));
|
||||
} else {
|
||||
results.value = [];
|
||||
}
|
||||
|
||||
analysisInfo.value = json.analysis_data || null;
|
||||
lastUpdated.value = new Date().toLocaleString();
|
||||
} catch (e: any) {
|
||||
if (e?.name === 'AbortError') {
|
||||
error.value = '请求已取消';
|
||||
} else if (
|
||||
e?.message?.includes('Failed to fetch') ||
|
||||
e?.message?.includes('NetworkError') ||
|
||||
e?.message?.includes('Network request failed')
|
||||
) {
|
||||
error.value = '网络异常,请检查连接后重试';
|
||||
} else {
|
||||
error.value = e?.message || '分析过程中发生错误';
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (props.autoAnalyze && props.data?.length) {
|
||||
// 如果设置了自动分析,跳过摆长输入界面
|
||||
showLengthInput.value = false;
|
||||
analyzeData(true);
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
console.log('PendulumAnalysis component unmounted');
|
||||
|
||||
});
|
||||
|
||||
/** 灯箱逻辑 */
|
||||
const lightbox = ref({ open: false, index: 0 });
|
||||
const openLightbox = (i: number) => {
|
||||
lightbox.value.open = true;
|
||||
lightbox.value.index = i;
|
||||
nextTick(() => {
|
||||
// 禁止页面滚动(移动端体验更好)
|
||||
document.body.style.overflow = 'hidden';
|
||||
});
|
||||
};
|
||||
const closeLightbox = () => {
|
||||
lightbox.value.open = false;
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
const navLightbox = (step: number) => {
|
||||
if (!results.value.length) return;
|
||||
const n = results.value.length;
|
||||
lightbox.value.index = (lightbox.value.index + step + n) % n;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* ========== 基础布局(横屏优先) ========== */
|
||||
.pendulum-analysis {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
background: radial-gradient(1200px 800px at 70% -10%, rgba(0, 255, 170, 0.08), transparent 60%),
|
||||
#0b0f12;
|
||||
color: #e8f8f4;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.safe-area {
|
||||
padding: calc(12px + env(safe-area-inset-top)) 16px calc(16px + env(safe-area-inset-bottom));
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
letter-spacing: 0.5px;
|
||||
color: #00ffaa;
|
||||
text-shadow: 0 0 12px rgba(0, 255, 170, 0.25);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
appearance: none;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: #e8f8f4;
|
||||
border-radius: 10px;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
transition: transform .05s ease, background .2s ease, border-color .2s ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border-color: rgba(255, 255, 255, 0.28);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.btn.small {
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.btn.ghost {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.btn.danger {
|
||||
border-color: rgba(255, 90, 90, 0.45);
|
||||
color: #ffdede;
|
||||
}
|
||||
|
||||
.btn.danger:hover {
|
||||
background: rgba(255, 90, 90, 0.15);
|
||||
}
|
||||
|
||||
/* 状态条 */
|
||||
.status-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-height: 44px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
border-radius: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.error,
|
||||
.meta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid rgba(0, 255, 170, 0.35);
|
||||
border-top-color: #00ffaa;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.9s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #ff7a7a;
|
||||
}
|
||||
|
||||
/* 主内容区:横屏优先双列 */
|
||||
.content {
|
||||
display: grid;
|
||||
grid-template-columns: 1.5fr 1fr;
|
||||
/* 图像区域更大 */
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px),
|
||||
(orientation: portrait) {
|
||||
.content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* 摆长输入界面 */
|
||||
.length-input-section {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 60vh;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.length-input-section h3 {
|
||||
margin: 0 0 24px 0;
|
||||
color: #00ffaa;
|
||||
font-size: 20px;
|
||||
text-shadow: 0 0 12px rgba(0, 255, 170, 0.25);
|
||||
}
|
||||
|
||||
.input-form {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
color: #d6efe8;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.length-input {
|
||||
appearance: none;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #e8f8f4;
|
||||
border-radius: 10px;
|
||||
padding: 12px 16px;
|
||||
font-size: 16px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
transition: border-color 0.2s ease, background 0.2s ease;
|
||||
}
|
||||
|
||||
.length-input:focus {
|
||||
outline: none;
|
||||
border-color: #00ffaa;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.length-input::placeholder {
|
||||
color: rgba(232, 248, 244, 0.5);
|
||||
}
|
||||
|
||||
.input-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn.primary {
|
||||
background: linear-gradient(135deg, #00ffaa, #00cc88);
|
||||
border-color: #00ffaa;
|
||||
color: #0b0f12;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn.primary:hover {
|
||||
background: linear-gradient(135deg, #00cc88, #00aa77);
|
||||
}
|
||||
|
||||
.btn.primary:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: rgba(232, 248, 244, 0.5);
|
||||
}
|
||||
|
||||
.input-tips {
|
||||
margin-top: 16px;
|
||||
font-size: 12px;
|
||||
opacity: 0.85;
|
||||
text-align: left;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.input-tips ul {
|
||||
margin: 4px 0 0 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 左侧:图像区 */
|
||||
.results {
|
||||
min-height: 40vh;
|
||||
}
|
||||
|
||||
.image-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@media (orientation: landscape) {
|
||||
.image-grid {
|
||||
grid-auto-rows: minmax(200px, 36vh);
|
||||
}
|
||||
}
|
||||
|
||||
.result-item {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-rows: 1fr auto;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.35);
|
||||
cursor: zoom-in;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.result-item:hover {
|
||||
border-color: rgba(0, 255, 170, 0.25);
|
||||
}
|
||||
|
||||
.result-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
object-fit: contain;
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-size: 12px;
|
||||
padding: 6px 10px;
|
||||
color: #cfeee6;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
/* 骨架屏 */
|
||||
.skeleton-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.skeleton-card {
|
||||
height: min(36vh, 320px);
|
||||
border-radius: 14px;
|
||||
background:
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.06) 25%, rgba(255, 255, 255, 0.12) 37%, rgba(255, 255, 255, 0.06) 63%) 0 0 / 400% 100%,
|
||||
rgba(255, 255, 255, 0.04);
|
||||
animation: shimmer 1.6s infinite;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: 100% 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.empty {
|
||||
opacity: 0.7;
|
||||
text-align: center;
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
/* 右侧:参数面板(粘性) */
|
||||
.analysis-info {
|
||||
position: sticky;
|
||||
top: 8px;
|
||||
align-self: start;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0.35), rgba(0, 0, 0, 0.2));
|
||||
border-radius: 14px;
|
||||
padding: 14px 16px;
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.analysis-info h3 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #00ffaa;
|
||||
font-size: 16px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 10px;
|
||||
border-bottom: 1px dashed rgba(255, 255, 255, 0.08);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: #d6efe8;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #00ffaa;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
.tips {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
ul{
|
||||
margin: 2px 0 0 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 灯箱 */
|
||||
.lightbox {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2000;
|
||||
background: rgba(0, 0, 0, 0.86);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: env(safe-area-inset-top) 20px env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.lightbox-inner {
|
||||
width: 95vw;
|
||||
height: 86vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.lightbox-inner img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
background: #000;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.lightbox-caption {
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: #cfeee6;
|
||||
}
|
||||
|
||||
.close-lightbox {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
padding: 8px 12px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.nav:hover {
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
|
||||
.nav.prev {
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.nav.next {
|
||||
right: 10px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user