fix: 物流管理页面修复 reactive import 缺失导致白屏
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8b2c154284
commit
276a3c0017
@ -1,76 +1,131 @@
|
||||
<!--
|
||||
OrderTrackingPage.vue - 物流轨迹查询页面
|
||||
职责:通过订单号或订单ID查询快递100物流轨迹,以时间线形式展示。
|
||||
支持订单号自动转换为订单ID查询,显示关联订单信息。
|
||||
OrderTrackingPage.vue - 物流管理页面
|
||||
职责:展示所有订单的物流状态总览,支持按状态/快递公司/订单号筛选。
|
||||
点击"查看轨迹"弹窗显示该订单的物流时间线。
|
||||
-->
|
||||
<template>
|
||||
<section class="page tracking-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>物流轨迹查询</h2>
|
||||
<p class="subline">输入订单号或订单ID查询物流轨迹。</p>
|
||||
<h2>物流管理</h2>
|
||||
<p class="subline">查看所有订单的物流状态,点击查看详情。</p>
|
||||
</div>
|
||||
<RouterLink class="back-link" to="/orders">返回订单中心</RouterLink>
|
||||
</div>
|
||||
|
||||
<!-- 查询表单 -->
|
||||
<!-- 筛选工具栏 -->
|
||||
<section class="panel">
|
||||
<div class="panel-header"><h3>查询轨迹</h3></div>
|
||||
<div class="query-row">
|
||||
<div class="panel-header"><h3>物流列表</h3><span>共 {{ filteredList.length }} 条</span></div>
|
||||
<section class="toolbar toolbar-4">
|
||||
<label>
|
||||
<span>订单号 / 订单ID</span>
|
||||
<input v-model.trim="queryInput" type="text" placeholder="输入订单号(如 SO20260601001)或订单ID" @keyup.enter="queryTrace" />
|
||||
<span>订单号 / 客户名</span>
|
||||
<input v-model.trim="filters.keyword" type="text" placeholder="搜索订单号或客户名" />
|
||||
</label>
|
||||
<button class="primary-btn" :disabled="!queryInput || loading" @click="queryTrace">{{ loading ? '查询中...' : '查询' }}</button>
|
||||
</div>
|
||||
<label>
|
||||
<span>物流状态</span>
|
||||
<select v-model="filters.trace_status">
|
||||
<option value="">全部状态</option>
|
||||
<option value="pending">待发货</option>
|
||||
<option value="in_transit">运输中</option>
|
||||
<option value="delivered">已签收</option>
|
||||
<option value="exception">异常</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>快递公司</span>
|
||||
<select v-model="filters.express_company">
|
||||
<option value="">全部快递</option>
|
||||
<option v-for="company in expressCompanies" :key="company" :value="company">{{ company }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="toolbar-actions">
|
||||
<button class="primary-btn" @click="handleSearch">查询</button>
|
||||
<button class="ghost-btn" @click="handleReset">重置</button>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<!-- 关联订单信息 -->
|
||||
<section v-if="orderInfo" class="panel">
|
||||
<div class="panel-header"><h3>订单信息</h3></div>
|
||||
<div class="info-grid">
|
||||
<article><p>订单号</p><strong>{{ orderInfo.order_no }}</strong></article>
|
||||
<article><p>客户</p><strong>{{ orderInfo.customer_name }}</strong></article>
|
||||
<article><p>状态</p><strong>{{ orderInfo.statusText }}</strong></article>
|
||||
<article><p>金额</p><strong>¥{{ orderInfo.sale_price_total }}</strong></article>
|
||||
<article v-if="orderInfo.express_company"><p>快递公司</p><strong>{{ orderInfo.express_company }}</strong></article>
|
||||
<article v-if="orderInfo.tracking_number"><p>快递单号</p><strong>{{ orderInfo.tracking_number }}</strong></article>
|
||||
</div>
|
||||
</section>
|
||||
<!-- 物流列表 -->
|
||||
<p v-if="loading" class="loading">正在加载物流数据...</p>
|
||||
<div v-else-if="!filteredList.length" class="empty-state">暂无物流数据</div>
|
||||
<div v-else class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>订单号</th>
|
||||
<th>客户</th>
|
||||
<th>快递公司</th>
|
||||
<th>快递单号</th>
|
||||
<th>物流状态</th>
|
||||
<th>最后更新</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in filteredList" :key="row.order_id">
|
||||
<td>{{ row.order_no }}</td>
|
||||
<td>{{ row.customer_name }}</td>
|
||||
<td>{{ row.express_company || '-' }}</td>
|
||||
<td>{{ row.tracking_number || '-' }}</td>
|
||||
<td>
|
||||
<span :class="['status-badge', row.trace_status_class]">{{ row.trace_status_text }}</span>
|
||||
</td>
|
||||
<td>{{ row.last_update || '-' }}</td>
|
||||
<td>
|
||||
<button v-if="row.has_trace" class="link-btn" @click="openTraceModal(row)">查看轨迹</button>
|
||||
<span v-else class="no-trace">暂无轨迹</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 轨迹时间线 -->
|
||||
<section v-if="traceList.length" class="panel">
|
||||
<div class="panel-header">
|
||||
<h3>物流轨迹</h3>
|
||||
<span>共 {{ traceList.length }} 条记录</span>
|
||||
</div>
|
||||
<div class="trace-timeline">
|
||||
<div v-for="(item, index) in traceList" :key="index" class="trace-item">
|
||||
<div class="trace-dot" :class="{ active: index === 0 }"></div>
|
||||
<div class="trace-connector" v-if="index < traceList.length - 1"></div>
|
||||
<div class="trace-card">
|
||||
<div class="trace-time">{{ item.node_time }}</div>
|
||||
<div class="trace-desc">{{ item.node_desc }}</div>
|
||||
<span v-if="item.source_platform" class="trace-source">{{ item.source_platform }}</span>
|
||||
<span v-if="item.remark" class="trace-location">{{ item.remark }}</span>
|
||||
<!-- 轨迹弹窗 -->
|
||||
<div v-if="showTraceModal" class="modal-mask" @click.self="closeTraceModal">
|
||||
<section class="modal-panel trace-modal">
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
<h4>物流轨迹</h4>
|
||||
<span>{{ traceModalData.order_no }} · {{ traceModalData.customer_name }}</span>
|
||||
</div>
|
||||
<button class="modal-close" @click="closeTraceModal">×</button>
|
||||
</div>
|
||||
|
||||
<!-- 订单物流摘要 -->
|
||||
<div class="trace-summary">
|
||||
<span>快递公司:{{ traceModalData.express_company || '-' }}</span>
|
||||
<span>快递单号:{{ traceModalData.tracking_number || '-' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 加载中 -->
|
||||
<p v-if="traceLoading" class="loading">正在查询轨迹...</p>
|
||||
|
||||
<!-- 轨迹时间线 -->
|
||||
<div v-else-if="traceList.length" class="trace-timeline">
|
||||
<div v-for="(item, index) in traceList" :key="index" class="trace-item">
|
||||
<div class="trace-dot" :class="{ active: index === 0 }"></div>
|
||||
<div class="trace-connector" v-if="index < traceList.length - 1"></div>
|
||||
<div class="trace-card">
|
||||
<div class="trace-time">{{ item.node_time }}</div>
|
||||
<div class="trace-desc">{{ item.node_desc }}</div>
|
||||
<span v-if="item.source_platform" class="trace-source">{{ item.source_platform }}</span>
|
||||
<span v-if="item.remark" class="trace-location">{{ item.remark }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<section v-if="!traceList.length && !loading && queried" class="panel">
|
||||
<div class="empty-state">
|
||||
<p>该订单暂无物流轨迹记录</p>
|
||||
<p class="subline">可能原因:订单未创建物流任务、未填写快递单号、或快递100尚未获取到轨迹</p>
|
||||
</div>
|
||||
</section>
|
||||
<!-- 无轨迹 -->
|
||||
<div v-else class="empty-state">
|
||||
<p>暂无物流轨迹</p>
|
||||
<p class="subline">可能原因:未填写快递单号,或快递100尚未获取到轨迹</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute, RouterLink } from 'vue-router';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useToast } from '../composables/useToast';
|
||||
import { fetchOrderList } from '../mockApi';
|
||||
|
||||
@ -78,128 +133,206 @@ const route = useRoute();
|
||||
const toast = useToast();
|
||||
const API_BASE = import.meta.env.VITE_API_BASE_URL || 'http://127.0.0.1:8000';
|
||||
|
||||
const queryInput = ref('');
|
||||
const loading = ref(true);
|
||||
const orderList = ref([]);
|
||||
const filters = reactive({ keyword: '', trace_status: '', express_company: '' });
|
||||
|
||||
// Trace modal
|
||||
const showTraceModal = ref(false);
|
||||
const traceLoading = ref(false);
|
||||
const traceList = ref([]);
|
||||
const loading = ref(false);
|
||||
const queried = ref(false);
|
||||
const orderInfo = ref(null);
|
||||
const traceModalData = ref({});
|
||||
|
||||
const expressCompanies = computed(() => {
|
||||
const set = new Set(orderList.value.map(r => r.express_company).filter(Boolean));
|
||||
return [...set].sort();
|
||||
});
|
||||
|
||||
const filteredList = computed(() => {
|
||||
return orderList.value.filter(row => {
|
||||
if (filters.keyword) {
|
||||
const kw = filters.keyword.toLowerCase();
|
||||
if (!row.order_no.toLowerCase().includes(kw) && !row.customer_name.toLowerCase().includes(kw)) return false;
|
||||
}
|
||||
if (filters.express_company && row.express_company !== filters.express_company) return false;
|
||||
if (filters.trace_status) {
|
||||
if (filters.trace_status === 'pending' && row.trace_status !== 'pending') return false;
|
||||
if (filters.trace_status === 'in_transit' && row.trace_status !== 'in_transit') return false;
|
||||
if (filters.trace_status === 'delivered' && row.trace_status !== 'delivered') return false;
|
||||
if (filters.trace_status === 'exception' && row.trace_status !== 'exception') return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem('admin_token') || '';
|
||||
}
|
||||
|
||||
async function queryTrace() {
|
||||
if (!queryInput.value.trim()) return;
|
||||
async function loadAllOrders() {
|
||||
loading.value = true;
|
||||
traceList.value = [];
|
||||
orderInfo.value = null;
|
||||
queried.value = false;
|
||||
|
||||
try {
|
||||
let orderId = parseInt(queryInput.value, 10);
|
||||
// Load orders that are in logistics stages
|
||||
const statuses = ['pending_factory', 'pending_driver', 'accepted', 'picked_up', 'delivered', 'production', 'shipped'];
|
||||
const allOrders = [];
|
||||
|
||||
// If not a number, try searching by order_no
|
||||
if (!orderId || orderId <= 0) {
|
||||
const orderData = await fetchOrderList({ order_no: queryInput.value.trim(), page_size: 10 });
|
||||
if (orderData.rows && orderData.rows.length > 0) {
|
||||
orderId = orderData.rows[0].orderId;
|
||||
orderInfo.value = {
|
||||
order_no: orderData.rows[0].orderNo,
|
||||
customer_name: orderData.rows[0].customerName,
|
||||
statusText: orderData.rows[0].statusText,
|
||||
sale_price_total: orderData.rows[0].salePriceTotal,
|
||||
};
|
||||
} else {
|
||||
toast.error('未找到匹配的订单');
|
||||
queried.value = true;
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch order detail for info
|
||||
if (!orderInfo.value) {
|
||||
for (const status of statuses) {
|
||||
try {
|
||||
const resp = await fetch(`${API_BASE}/api/orders/${orderId}`, {
|
||||
headers: { 'Content-Type': 'application/json', ...(getToken() ? { Authorization: `Bearer ${getToken()}` } : {}) },
|
||||
});
|
||||
const result = await resp.json();
|
||||
if (resp.ok && result.code === 0 && result.data) {
|
||||
const d = result.data;
|
||||
const task = d.logistics_info?.task;
|
||||
orderInfo.value = {
|
||||
order_no: d.order_no,
|
||||
customer_name: d.customer_name,
|
||||
statusText: d.order_status,
|
||||
sale_price_total: Number(d.sale_price_total || 0).toFixed(2),
|
||||
express_company: task?.express_company || null,
|
||||
tracking_number: task?.tracking_number || null,
|
||||
};
|
||||
}
|
||||
const data = await fetchOrderList({ order_status: status, page_size: 100 });
|
||||
allOrders.push(...data.rows);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// Fetch trace
|
||||
const resp = await fetch(`${API_BASE}/api/logistics/${orderId}/trace`, {
|
||||
headers: { 'Content-Type': 'application/json', ...(getToken() ? { Authorization: `Bearer ${getToken()}` } : {}) },
|
||||
});
|
||||
const result = await resp.json();
|
||||
if (resp.ok && result.code === 0) {
|
||||
traceList.value = (result.data || {}).trace_list || [];
|
||||
queried.value = true;
|
||||
if (traceList.value.length === 0) {
|
||||
toast.info('暂无物流轨迹');
|
||||
} else {
|
||||
toast.success(`查询成功,共 ${traceList.value.length} 条轨迹`);
|
||||
}
|
||||
} else {
|
||||
toast.error(result.message || '查询失败');
|
||||
queried.value = true;
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error(err.message || '网络请求失败');
|
||||
queried.value = true;
|
||||
// Fetch logistics info for each order
|
||||
const enriched = await Promise.all(allOrders.map(async (order) => {
|
||||
const row = {
|
||||
...order,
|
||||
express_company: null,
|
||||
tracking_number: null,
|
||||
trace_status: 'pending',
|
||||
trace_status_text: '待发货',
|
||||
trace_status_class: 'status-pending',
|
||||
last_update: '-',
|
||||
has_trace: false,
|
||||
};
|
||||
|
||||
try {
|
||||
const resp = await fetch(`${API_BASE}/api/logistics/tasks?order_id=${order.orderId}`, {
|
||||
headers: { 'Content-Type': 'application/json', ...(getToken() ? { Authorization: `Bearer ${getToken()}` } : {}) },
|
||||
});
|
||||
const result = await resp.json();
|
||||
if (resp.ok && result.code === 0 && result.data?.list?.length) {
|
||||
const task = result.data.list[0];
|
||||
row.express_company = task.express_company || null;
|
||||
row.tracking_number = task.tracking_number || null;
|
||||
row.trace_status = mapTaskStatus(task.task_status);
|
||||
row.trace_status_text = mapTaskStatusText(task.task_status);
|
||||
row.trace_status_class = mapTaskStatusClass(task.task_status);
|
||||
row.last_update = task.updated_at || task.created_at || '-';
|
||||
row.has_trace = Boolean(task.tracking_number);
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
return row;
|
||||
}));
|
||||
|
||||
orderList.value = enriched;
|
||||
} catch (e) {
|
||||
toast.error(e.message || '加载物流数据失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const orderIdParam = route.query.order_id;
|
||||
if (orderIdParam) {
|
||||
queryInput.value = orderIdParam;
|
||||
queryTrace();
|
||||
}
|
||||
});
|
||||
function mapTaskStatus(status) {
|
||||
const map = { pending: 'pending', assigned: 'in_transit', in_transit: 'in_transit', delivered: 'delivered', completed: 'delivered' };
|
||||
return map[status] || 'pending';
|
||||
}
|
||||
|
||||
function mapTaskStatusText(status) {
|
||||
const map = { pending: '待分配', assigned: '已分配', in_transit: '运输中', delivered: '已送达', completed: '已完成' };
|
||||
return map[status] || status || '待发货';
|
||||
}
|
||||
|
||||
function mapTaskStatusClass(status) {
|
||||
const map = { pending: 'status-pending', assigned: 'status-transit', in_transit: 'status-transit', delivered: 'status-delivered', completed: 'status-delivered' };
|
||||
return map[status] || 'status-pending';
|
||||
}
|
||||
|
||||
async function openTraceModal(row) {
|
||||
traceModalData.value = row;
|
||||
traceList.value = [];
|
||||
showTraceModal.value = true;
|
||||
|
||||
if (!row.has_trace) return;
|
||||
|
||||
traceLoading.value = true;
|
||||
try {
|
||||
const resp = await fetch(`${API_BASE}/api/logistics/${row.orderId}/trace`, {
|
||||
headers: { 'Content-Type': 'application/json', ...(getToken() ? { Authorization: `Bearer ${getToken()}` } : {}) },
|
||||
});
|
||||
const result = await resp.json();
|
||||
if (resp.ok && result.code === 0) {
|
||||
traceList.value = (result.data || {}).trace_list || [];
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
finally { traceLoading.value = false; }
|
||||
}
|
||||
|
||||
function closeTraceModal() {
|
||||
showTraceModal.value = false;
|
||||
traceList.value = [];
|
||||
}
|
||||
|
||||
function handleSearch() { /* filters are reactive, auto-applied */ }
|
||||
function handleReset() {
|
||||
filters.keyword = '';
|
||||
filters.trace_status = '';
|
||||
filters.express_company = '';
|
||||
}
|
||||
|
||||
onMounted(loadAllOrders);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page{padding:0;display:grid;gap:16px}
|
||||
.page-header{display:flex;justify-content:space-between;gap:12px;align-items:center}
|
||||
.subline{margin:0;color:#64748b}
|
||||
.back-link{padding:10px 14px;border:1px solid #cbd5e1;border-radius:12px;text-decoration:none;background:#fff;color:#334155}
|
||||
.panel{border:1px solid #dbe3ef;border-radius:18px;padding:16px;background:#fff}
|
||||
.panel-header{display:flex;justify-content:space-between;gap:12px;align-items:center;margin-bottom:12px}
|
||||
.query-row{display:flex;gap:12px;align-items:flex-end}
|
||||
.query-row label{display:flex;flex-direction:column;gap:4px;flex:1;max-width:400px}
|
||||
.query-row label span{font-size:13px;color:#64748b}
|
||||
.query-row input,.ghost-btn,.primary-btn{border-radius:12px;border:1px solid #cbd5e1;padding:10px 14px;background:#fff}
|
||||
.primary-btn{background:#2563eb;color:#fff;border-color:#2563eb}
|
||||
.primary-btn:disabled{opacity:0.5;cursor:not-allowed}
|
||||
.info-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px}
|
||||
.info-grid article{padding:12px;border-radius:14px;background:#f8fbff;border:1px solid #dbe3ef}
|
||||
.info-grid p{display:block;color:#64748b;margin:0 0 8px}
|
||||
.info-grid strong{font-size:16px}
|
||||
.trace-timeline{position:relative;padding-left:24px}
|
||||
.trace-item{position:relative;padding-bottom:20px}
|
||||
.trace-dot{position:absolute;left:-24px;top:4px;width:12px;height:12px;border-radius:50%;background:#cbd5e1;border:2px solid #fff;z-index:1}
|
||||
.trace-dot.active{background:#2563eb}
|
||||
.trace-connector{position:absolute;left:-19px;top:16px;width:2px;height:calc(100% - 12px);background:#e5e7eb}
|
||||
.trace-card{padding:12px 16px;border:1px solid #dbe3ef;border-radius:14px;background:#f8fbff}
|
||||
.trace-time{font-size:13px;color:#64748b;margin-bottom:4px}
|
||||
.trace-desc{font-size:15px;color:#1e293b;line-height:1.5}
|
||||
.trace-source{display:inline-block;margin-top:6px;padding:2px 8px;border-radius:999px;background:#eff6ff;color:#1d4ed8;font-size:11px}
|
||||
.trace-location{display:inline-block;margin-top:6px;margin-left:8px;font-size:12px;color:#64748b}
|
||||
.empty-state{text-align:center;padding:32px 16px;color:#64748b}
|
||||
.empty-state p{margin:0 0 8px}
|
||||
.page { padding: 0; display: grid; gap: 16px; }
|
||||
.page-header { display: flex; justify-content: space-between; gap: 12px; align-items: center; }
|
||||
.page-header h2 { margin: 0 0 4px; font-size: 24px; }
|
||||
.subline { margin: 0; color: #64748b; font-size: 14px; }
|
||||
|
||||
.panel { border: 1px solid #dbe3ef; border-radius: 18px; padding: 20px; background: #fff; }
|
||||
.panel-header { display: flex; justify-content: space-between; gap: 12px; align-items: center; margin-bottom: 16px; }
|
||||
.panel-header h3 { margin: 0; font-size: 16px; }
|
||||
.panel-header span { color: #64748b; font-size: 13px; }
|
||||
|
||||
.toolbar { display: grid; gap: 12px; align-items: end; }
|
||||
.toolbar-4 { grid-template-columns: repeat(3, minmax(0, 1fr)) auto; }
|
||||
.toolbar label { display: grid; gap: 6px; }
|
||||
.toolbar span { color: #475569; font-size: 13px; font-weight: 600; }
|
||||
.toolbar input, .toolbar select { border: 1px solid #d1d5db; border-radius: 10px; padding: 10px 12px; background: #fff; font-size: 14px; }
|
||||
.toolbar-actions { display: flex; gap: 10px; align-items: end; }
|
||||
|
||||
.table-wrap { overflow-x: auto; }
|
||||
table { width: 100%; border-collapse: collapse; min-width: 800px; }
|
||||
th, td { text-align: left; padding: 12px 14px; border-bottom: 1px solid #e5e7eb; }
|
||||
th { background: #f8fafc; font-size: 13px; font-weight: 600; color: #334155; white-space: nowrap; }
|
||||
td { font-size: 14px; }
|
||||
|
||||
.status-badge { display: inline-flex; padding: 4px 12px; border-radius: 999px; font-size: 12px; font-weight: 600; white-space: nowrap; }
|
||||
.status-pending { background: #fef3c7; color: #92400e; }
|
||||
.status-transit { background: #dbeafe; color: #1d4ed8; }
|
||||
.status-delivered { background: #dcfce7; color: #166534; }
|
||||
.status-exception { background: #fee2e2; color: #b91c1c; }
|
||||
|
||||
.link-btn { background: none; border: none; color: #2563eb; cursor: pointer; font-size: 13px; padding: 4px 8px; }
|
||||
.no-trace { color: #9ca3af; font-size: 13px; }
|
||||
|
||||
.loading { margin: 0; color: #6b7280; text-align: center; padding: 20px; }
|
||||
.empty-state { padding: 32px; border: 1px dashed #cbd5e1; border-radius: 16px; color: #64748b; text-align: center; }
|
||||
|
||||
/* Modal */
|
||||
.modal-mask { position: fixed; inset: 0; z-index: 50; display: flex; align-items: center; justify-content: center; padding: 24px; background: rgba(15,23,42,0.48); backdrop-filter: blur(2px); }
|
||||
.trace-modal { width: min(640px, 100%); max-height: 85vh; overflow: auto; border-radius: 20px; border: 1px solid #dbe3ef; background: #fff; padding: 24px; box-shadow: 0 24px 80px rgba(15,23,42,0.24); }
|
||||
.modal-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 16px; }
|
||||
.modal-header h4 { margin: 0 0 6px; font-size: 18px; }
|
||||
.modal-header span { color: #64748b; font-size: 13px; }
|
||||
.modal-close { width: 36px; height: 36px; border: 1px solid #d1d5db; background: #f8fafc; font-size: 22px; border-radius: 12px; cursor: pointer; color: #334155; }
|
||||
|
||||
.trace-summary { display: flex; gap: 20px; margin-bottom: 16px; padding: 12px 16px; background: #f8fafc; border-radius: 12px; font-size: 13px; color: #475569; }
|
||||
|
||||
.trace-timeline { position: relative; padding-left: 24px; }
|
||||
.trace-item { position: relative; padding-bottom: 20px; }
|
||||
.trace-dot { position: absolute; left: -24px; top: 4px; width: 12px; height: 12px; border-radius: 50%; background: #cbd5e1; border: 2px solid #fff; z-index: 1; }
|
||||
.trace-dot.active { background: #2563eb; }
|
||||
.trace-connector { position: absolute; left: -19px; top: 16px; width: 2px; height: calc(100% - 12px); background: #e5e7eb; }
|
||||
.trace-card { padding: 12px 16px; border: 1px solid #dbe3ef; border-radius: 14px; background: #f8fbff; }
|
||||
.trace-time { font-size: 13px; color: #64748b; margin-bottom: 4px; }
|
||||
.trace-desc { font-size: 15px; color: #1e293b; line-height: 1.5; }
|
||||
.trace-source { display: inline-block; margin-top: 6px; padding: 2px 8px; border-radius: 999px; background: #eff6ff; color: #1d4ed8; font-size: 11px; }
|
||||
.trace-location { display: inline-block; margin-top: 6px; margin-left: 8px; font-size: 12px; color: #64748b; }
|
||||
|
||||
.ghost-btn, .primary-btn { border-radius: 10px; border: 1px solid #d1d5db; padding: 8px 14px; background: #fff; font-size: 13px; cursor: pointer; color: #334155; }
|
||||
.primary-btn { background: #2563eb; color: #fff; border-color: #2563eb; }
|
||||
|
||||
@media (max-width: 780px) { .toolbar { grid-template-columns: 1fr; } .page-header { flex-direction: column; align-items: flex-start; } }
|
||||
</style>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user