'分系列表详情'
This commit is contained in:
@@ -0,0 +1,849 @@
|
||||
<template>
|
||||
<div class="archive-detail-page" v-loading="loading">
|
||||
<el-card v-if="!loading && !canRequest" class="state-card" shadow="never">
|
||||
<el-empty description="缺少 user_id 或 id 参数,无法加载" />
|
||||
</el-card>
|
||||
|
||||
<template v-else-if="!loading && !firstRecord">
|
||||
<el-card class="state-card" shadow="never">
|
||||
<el-empty description="暂无检测详情数据" />
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<header class="page-head">
|
||||
<h1 class="page-title">分析对比详情</h1>
|
||||
</header>
|
||||
|
||||
<el-card class="detail-card" shadow="hover">
|
||||
<div class="detail-body">
|
||||
<div class="viewers-col">
|
||||
<div class="compare-row" :class="{ 'compare-row--single': !secondRecord }">
|
||||
<section v-if="firstRecord" class="compare-pane">
|
||||
<header class="pane-head">
|
||||
<span class="pane-tag pane-tag--a">{{ paneTitleFirst }}</span>
|
||||
<span class="pane-time">
|
||||
<span class="pane-time__label">检测时间</span>
|
||||
{{ firstRecord.create_time || '—' }}
|
||||
</span>
|
||||
</header>
|
||||
<div class="viewer-card">
|
||||
<div v-if="hasTriple1" class="triptych">
|
||||
<div class="triptych-panel triptych-side" :class="{ 'is-empty': !c1Left }"
|
||||
@click="moveRight1" title="与中间图互换">
|
||||
<img v-if="c1Left" :src="c1Left" class="triptych-img triptych-img--side"
|
||||
alt="" />
|
||||
<span v-else class="panel-empty">左</span>
|
||||
</div>
|
||||
<div class="triptych-panel triptych-center" :class="{ 'is-empty': !c1Front }">
|
||||
<img v-if="c1Front" :src="c1Front" class="triptych-img triptych-img--center"
|
||||
alt="" />
|
||||
<span v-else class="panel-empty">中</span>
|
||||
</div>
|
||||
<div class="triptych-panel triptych-side" :class="{ 'is-empty': !c1Right }"
|
||||
@click="moveLeft1" title="与中间图互换">
|
||||
<img v-if="c1Right" :src="c1Right" class="triptych-img triptych-img--side"
|
||||
alt="" />
|
||||
<span v-else class="panel-empty">右</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="singleUrl1" class="single-wrap">
|
||||
<img :src="singleUrl1" class="single-img" alt="" />
|
||||
</div>
|
||||
<div v-else class="pane-empty">暂无分析图</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="secondRecord" class="compare-pane compare-pane--b">
|
||||
<header class="pane-head">
|
||||
<span class="pane-tag pane-tag--b">{{ paneTitleSecond }}</span>
|
||||
<span class="pane-time">
|
||||
<span class="pane-time__label">检测时间</span>
|
||||
{{ secondRecord.create_time || '—' }}
|
||||
</span>
|
||||
</header>
|
||||
<div class="viewer-card">
|
||||
<div v-if="hasTriple2" class="triptych">
|
||||
<div class="triptych-panel triptych-side" :class="{ 'is-empty': !c2Left }"
|
||||
@click="moveRight2" title="与中间图互换">
|
||||
<img v-if="c2Left" :src="c2Left" class="triptych-img triptych-img--side"
|
||||
alt="" />
|
||||
<span v-else class="panel-empty">左</span>
|
||||
</div>
|
||||
<div class="triptych-panel triptych-center" :class="{ 'is-empty': !c2Front }">
|
||||
<img v-if="c2Front" :src="c2Front" class="triptych-img triptych-img--center"
|
||||
alt="" />
|
||||
<span v-else class="panel-empty">中</span>
|
||||
</div>
|
||||
<div class="triptych-panel triptych-side" :class="{ 'is-empty': !c2Right }"
|
||||
@click="moveLeft2" title="与中间图互换">
|
||||
<img v-if="c2Right" :src="c2Right" class="triptych-img triptych-img--side"
|
||||
alt="" />
|
||||
<span v-else class="panel-empty">右</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="singleUrl2" class="single-wrap">
|
||||
<img :src="singleUrl2" class="single-img" alt="" />
|
||||
</div>
|
||||
<div v-else class="pane-empty">暂无分析图</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div v-if="thumbKeys.length" class="thumb-section">
|
||||
<div class="thumb-section-head">
|
||||
<span class="thumb-section-title">模式图</span>
|
||||
<div class="thumb-section-actions">
|
||||
<span class="thumb-section-active" :title="activeLabel">{{ activeLabel }}</span>
|
||||
<el-button type="primary" link @click="thumbExpanded = !thumbExpanded">
|
||||
{{ thumbExpanded ? '收起' : '展开' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="thumbExpanded" class="thumb-strip-wrap">
|
||||
<div class="thumb-strip">
|
||||
<button v-for="key in thumbKeys" :key="key" type="button" class="thumb-btn"
|
||||
:class="{ active: key === activeImageKey }" @click="switchMode(key)">
|
||||
<span class="thumb-btn__frame">
|
||||
<img v-if="thumbUrlFor(key)" :src="thumbUrlFor(key)" alt="" />
|
||||
<span v-else class="thumb-btn__placeholder">—</span>
|
||||
</span>
|
||||
<span class="thumb-btn__label">{{ labelForKey(key) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="metrics-aside">
|
||||
<div v-if="metricRecord" class="metric-card">
|
||||
<div class="metric-card-head">
|
||||
<h3>综合评估</h3>
|
||||
<p v-if="metricTimeHint" class="metric-sub">{{ metricTimeHint }}</p>
|
||||
</div>
|
||||
<div class="metric-card-body">
|
||||
<div v-for="m in metricRows(metricRecord)" :key="m.key" class="metric-row">
|
||||
<div class="metric-head">
|
||||
<span class="metric-name">{{ m.label }}</span>
|
||||
<span class="metric-num">{{ m.value }}</span>
|
||||
</div>
|
||||
<el-progress :percentage="m.pct" :color="progressColors" :stroke-width="8"
|
||||
:show-text="false" />
|
||||
</div>
|
||||
<div v-if="!metricRows(metricRecord).length" class="metric-empty">暂无评估数据</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { getUserDetectionDetail } from '@/api/user';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import {
|
||||
buildImageEntries,
|
||||
orderedImageKeys,
|
||||
type ImageEntry,
|
||||
type StateAngles,
|
||||
} from '@/utils/userArchiveImages';
|
||||
|
||||
const CONTENT_LABELS: Record<string, string> = {
|
||||
brightness: '亮度',
|
||||
pigment: '色素',
|
||||
red_zone: '红区',
|
||||
skin_color: '肤色',
|
||||
yanmin: '炎敏',
|
||||
yumin: '肤质综合',
|
||||
};
|
||||
|
||||
const route = useRoute();
|
||||
const loading = ref(false);
|
||||
const thumbExpanded = ref(true);
|
||||
const firstRecord = ref<any>(null);
|
||||
const secondRecord = ref<any>(null);
|
||||
|
||||
const entriesFirst = ref<ImageEntry[]>([]);
|
||||
const entriesSecond = ref<ImageEntry[]>([]);
|
||||
const activeImageKey = ref('');
|
||||
|
||||
const c1Left = ref('');
|
||||
const c1Front = ref('');
|
||||
const c1Right = ref('');
|
||||
const c2Left = ref('');
|
||||
const c2Front = ref('');
|
||||
const c2Right = ref('');
|
||||
|
||||
const progressColors = [
|
||||
{ color: '#f56c6c', percentage: 40 },
|
||||
{ color: '#e6a23c', percentage: 70 },
|
||||
{ color: '#67c23a', percentage: 100 },
|
||||
];
|
||||
|
||||
const requestParams = computed(() => {
|
||||
const uid = route.query.user_id;
|
||||
const id = route.query.id;
|
||||
const u = uid == null || uid === '' ? NaN : Number(Array.isArray(uid) ? uid[0] : uid);
|
||||
const i = id == null || id === '' ? NaN : Number(Array.isArray(id) ? id[0] : id);
|
||||
const body: Record<string, number> = {};
|
||||
if (Number.isFinite(u)) body.user_id = u;
|
||||
if (Number.isFinite(i)) body.id = i;
|
||||
return body;
|
||||
});
|
||||
|
||||
const canRequest = computed(() => Object.keys(requestParams.value).length > 0);
|
||||
|
||||
const paneTitleFirst = computed(
|
||||
() => firstRecord.value?.nickname || firstRecord.value?.name || firstRecord.value?.username || '检测一'
|
||||
);
|
||||
const paneTitleSecond = computed(
|
||||
() => secondRecord.value?.nickname || secondRecord.value?.name || secondRecord.value?.username || '检测二'
|
||||
);
|
||||
|
||||
const pageSubtitle = computed(() => {
|
||||
const uid = firstRecord.value?.user_id;
|
||||
if (uid == null || uid === '') return '';
|
||||
return `用户 ID:${uid}`;
|
||||
});
|
||||
|
||||
const thumbKeys = computed(() => orderedImageKeys(entriesFirst.value, entriesSecond.value));
|
||||
|
||||
const activeLabel = computed(() => {
|
||||
const k = activeImageKey.value;
|
||||
return labelForKey(k);
|
||||
});
|
||||
|
||||
function labelForKey(key: string): string {
|
||||
return (
|
||||
entriesFirst.value.find((e) => e.key === key)?.label ||
|
||||
entriesSecond.value.find((e) => e.key === key)?.label ||
|
||||
key
|
||||
);
|
||||
}
|
||||
|
||||
function thumbUrlFor(key: string): string {
|
||||
return (
|
||||
entriesFirst.value.find((e) => e.key === key)?.thumbUrl ||
|
||||
entriesSecond.value.find((e) => e.key === key)?.thumbUrl ||
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
const hasTriple1 = computed(() => !!(c1Left.value || c1Front.value || c1Right.value));
|
||||
const hasTriple2 = computed(() => !!(c2Left.value || c2Front.value || c2Right.value));
|
||||
const singleUrl1 = computed(() => c1Front.value || c1Left.value || c1Right.value || '');
|
||||
const singleUrl2 = computed(() => c2Front.value || c2Left.value || c2Right.value || '');
|
||||
|
||||
/** 同一人对比:评估指标只展示一份,优先用检测一有 contents,否则用检测二 */
|
||||
const metricRecord = computed(() => {
|
||||
const a = firstRecord.value;
|
||||
const b = secondRecord.value;
|
||||
const hasContents = (r: any) => r?.contents && typeof r.contents === 'object' && Object.keys(r.contents).length;
|
||||
if (a && hasContents(a)) return a;
|
||||
if (b && hasContents(b)) return b;
|
||||
return a ?? b ?? null;
|
||||
});
|
||||
|
||||
const metricTimeHint = computed(() => {
|
||||
const a = firstRecord.value?.create_time;
|
||||
const b = secondRecord.value?.create_time;
|
||||
if (secondRecord.value && a && b && a !== b) {
|
||||
return `检测一 ${a} · 检测二 ${b}`;
|
||||
}
|
||||
return metricRecord.value?.create_time || '';
|
||||
});
|
||||
|
||||
function metricRows(record: any) {
|
||||
const c = record?.contents;
|
||||
if (!c || typeof c !== 'object') return [];
|
||||
return Object.entries(c).map(([key, val]) => {
|
||||
const n = Number(val);
|
||||
const pct = Number.isFinite(n) ? Math.min(100, Math.max(0, n)) : 0;
|
||||
return {
|
||||
key,
|
||||
label: CONTENT_LABELS[key] || key,
|
||||
value: String(val ?? '--'),
|
||||
pct,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function anglesFor(entries: ImageEntry[], key: string): StateAngles {
|
||||
const e = entries.find((x) => x.key === key);
|
||||
return e?.angles ?? { left: '', front: '', right: '' };
|
||||
}
|
||||
|
||||
function applyActiveKey() {
|
||||
const key = activeImageKey.value;
|
||||
const a1 = anglesFor(entriesFirst.value, key);
|
||||
const a2 = anglesFor(entriesSecond.value, key);
|
||||
c1Left.value = a1.left;
|
||||
c1Front.value = a1.front;
|
||||
c1Right.value = a1.right;
|
||||
c2Left.value = a2.left;
|
||||
c2Front.value = a2.front;
|
||||
c2Right.value = a2.right;
|
||||
}
|
||||
|
||||
function switchMode(key: string) {
|
||||
activeImageKey.value = key;
|
||||
applyActiveKey();
|
||||
}
|
||||
|
||||
function moveRight1() {
|
||||
if (!c1Left.value) return;
|
||||
const t = c1Front.value;
|
||||
c1Front.value = c1Left.value;
|
||||
c1Left.value = t;
|
||||
}
|
||||
|
||||
function moveLeft1() {
|
||||
if (!c1Right.value) return;
|
||||
const t = c1Front.value;
|
||||
c1Front.value = c1Right.value;
|
||||
c1Right.value = t;
|
||||
}
|
||||
|
||||
function moveRight2() {
|
||||
if (!c2Left.value) return;
|
||||
const t = c2Front.value;
|
||||
c2Front.value = c2Left.value;
|
||||
c2Left.value = t;
|
||||
}
|
||||
|
||||
function moveLeft2() {
|
||||
if (!c2Right.value) return;
|
||||
const t = c2Front.value;
|
||||
c2Front.value = c2Right.value;
|
||||
c2Right.value = t;
|
||||
}
|
||||
|
||||
function normalizePayload(raw: any): { first: any; second: any } {
|
||||
const payload = raw?.data?.data ?? raw?.data ?? raw;
|
||||
if (!payload || typeof payload !== 'object') return { first: null, second: null };
|
||||
if (payload.first != null && payload.second != null) {
|
||||
return { first: payload.first, second: payload.second };
|
||||
}
|
||||
return { first: payload, second: null };
|
||||
}
|
||||
|
||||
async function loadDetail() {
|
||||
if (!canRequest.value) {
|
||||
firstRecord.value = null;
|
||||
secondRecord.value = null;
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getUserDetectionDetail(requestParams.value);
|
||||
const { first, second } = normalizePayload(res);
|
||||
firstRecord.value = first;
|
||||
secondRecord.value = second;
|
||||
|
||||
entriesFirst.value = buildImageEntries(first);
|
||||
entriesSecond.value = second ? buildImageEntries(second) : [];
|
||||
|
||||
const keys = orderedImageKeys(entriesFirst.value, entriesSecond.value);
|
||||
activeImageKey.value = keys[0] ?? '';
|
||||
applyActiveKey();
|
||||
} catch (e: any) {
|
||||
firstRecord.value = null;
|
||||
secondRecord.value = null;
|
||||
entriesFirst.value = [];
|
||||
entriesSecond.value = [];
|
||||
ElMessage.error(e?.message || '获取检测详情失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [route.query.user_id, route.query.id] as const,
|
||||
() => loadDetail(),
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$accent: #52a857;
|
||||
$accent-blue: #409eff;
|
||||
|
||||
.archive-detail-page {
|
||||
min-height: calc(100vh - 120px);
|
||||
padding: 20px 24px 28px;
|
||||
background: var(--el-fill-color-extra-light);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.state-card {
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.page-head {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin: 8px 0 0;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.detail-card .el-card__body) {
|
||||
padding: 20px 22px 24px;
|
||||
}
|
||||
|
||||
.detail-body {
|
||||
display: flex;
|
||||
gap: 22px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.viewers-col {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.compare-row {
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
align-items: stretch;
|
||||
|
||||
&--single .compare-pane {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.compare-pane {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
overflow: hidden;
|
||||
|
||||
&--b .pane-head {
|
||||
background: linear-gradient(180deg, rgba(64, 158, 255, 0.06) 0%, var(--el-bg-color) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.pane-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
padding: 12px 14px;
|
||||
background: linear-gradient(180deg, rgba(82, 168, 87, 0.08) 0%, var(--el-bg-color) 100%);
|
||||
border-bottom: 1px solid var(--el-border-color-extra-light);
|
||||
}
|
||||
|
||||
.pane-tag {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
padding: 5px 12px;
|
||||
border-radius: 20px;
|
||||
letter-spacing: 0.02em;
|
||||
|
||||
&--a {
|
||||
color: darken($accent, 8%);
|
||||
background: rgba(82, 168, 87, 0.14);
|
||||
border: 1px solid rgba(82, 168, 87, 0.25);
|
||||
}
|
||||
|
||||
&--b {
|
||||
color: $accent-blue;
|
||||
background: rgba(64, 158, 255, 0.1);
|
||||
border: 1px solid rgba(64, 158, 255, 0.22);
|
||||
}
|
||||
}
|
||||
|
||||
.pane-time {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-regular);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pane-time__label {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.viewer-card {
|
||||
flex: 1;
|
||||
min-height: 300px;
|
||||
max-height: 52vh;
|
||||
margin: 0 12px 4px;
|
||||
padding: 14px;
|
||||
background: var(--el-fill-color-dark);
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
}
|
||||
|
||||
html.dark .viewer-card {
|
||||
background: #0d0d10;
|
||||
border-color: var(--el-border-color);
|
||||
}
|
||||
|
||||
.viewer-hint {
|
||||
margin: 0 12px 12px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.triptych {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
height: 100%;
|
||||
min-height: 272px;
|
||||
max-height: 47vh;
|
||||
}
|
||||
|
||||
.triptych-panel {
|
||||
position: relative;
|
||||
background: #0a0a0c;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.is-empty {
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
}
|
||||
|
||||
.triptych-center {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border-color: rgba(82, 168, 87, 0.45);
|
||||
box-shadow: 0 0 0 1px rgba(82, 168, 87, 0.12);
|
||||
}
|
||||
|
||||
.triptych-side {
|
||||
flex: 0 0 20%;
|
||||
min-width: 76px;
|
||||
max-width: 148px;
|
||||
|
||||
&:not(.is-empty) {
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(82, 168, 87, 0.65);
|
||||
box-shadow: 0 0 0 1px rgba(82, 168, 87, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.triptych-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.triptych-img--side {
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
.triptych-img--center {
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
max-height: 45vh;
|
||||
}
|
||||
|
||||
.panel-empty,
|
||||
.pane-empty {
|
||||
font-size: 12px;
|
||||
color: #6b6b75;
|
||||
}
|
||||
|
||||
.pane-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 200px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
|
||||
.single-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
min-height: 248px;
|
||||
}
|
||||
|
||||
.single-img {
|
||||
max-width: 100%;
|
||||
max-height: 45vh;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.thumb-section {
|
||||
padding: 14px 16px 16px;
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.thumb-section-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.thumb-section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.thumb-section-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.thumb-section-active {
|
||||
font-size: 12px;
|
||||
color: $accent;
|
||||
font-weight: 500;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.thumb-strip-wrap {
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.thumb-strip {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 10px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 6px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--el-border-color) transparent;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--el-border-color);
|
||||
border-radius: 999px;
|
||||
}
|
||||
}
|
||||
|
||||
.thumb-btn {
|
||||
flex: 0 0 auto;
|
||||
width: 76px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
transition: color 0.15s ease;
|
||||
|
||||
&__frame {
|
||||
display: block;
|
||||
padding: 3px;
|
||||
border-radius: 10px;
|
||||
border: 2px solid transparent;
|
||||
background: var(--el-fill-color-light);
|
||||
transition:
|
||||
border-color 0.15s ease,
|
||||
box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
object-fit: cover;
|
||||
border-radius: 7px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__placeholder {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 7px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
background: var(--el-fill-color);
|
||||
}
|
||||
|
||||
&__label {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
padding: 0 2px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--el-text-color-primary);
|
||||
|
||||
.thumb-btn__frame {
|
||||
border-color: var(--el-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
// color: darken($accent, 5%);
|
||||
|
||||
.thumb-btn__frame {
|
||||
border-color: $accent;
|
||||
box-shadow: 0 2px 12px rgba(82, 168, 87, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.metrics-aside {
|
||||
width: 292px;
|
||||
flex-shrink: 0;
|
||||
position: sticky;
|
||||
max-height: calc(100vh - 100px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: var(--el-bg-color);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.metric-card-head {
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--el-border-color-extra-light);
|
||||
background: var(--el-fill-color-extra-light);
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.metric-sub {
|
||||
margin: 8px 0 0;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.metric-card-body {
|
||||
padding: 12px 14px 16px;
|
||||
}
|
||||
|
||||
.metric-row {
|
||||
padding: 10px 12px 12px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 10px;
|
||||
background: var(--el-fill-color-light);
|
||||
border: 1px solid var(--el-border-color-extra-light);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.metric-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.metric-name {
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.metric-num {
|
||||
color: $accent;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.metric-empty {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
text-align: center;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
:deep(.el-progress-bar__outer) {
|
||||
border-radius: 999px;
|
||||
background-color: var(--el-fill-color-dark);
|
||||
}
|
||||
|
||||
:deep(.el-progress-bar__inner) {
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.detail-body {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.metrics-aside {
|
||||
width: 100%;
|
||||
position: static;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.compare-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -45,7 +45,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue';
|
||||
import { getUserDetectionList, getUserarchiveDetail, userDetectionOperation } from '@/api/user';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { getUserDetectionList, userDetectionOperation } from '@/api/user';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { Search, Refresh } from '@element-plus/icons-vue';
|
||||
|
||||
@@ -94,10 +95,16 @@ const resetQuery = () => {
|
||||
queryParams.page = 1;
|
||||
loadList();
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
// 档案详情
|
||||
const archiveDetail = async (row: any) => {
|
||||
await getUserarchiveDetail({ id: row.id });
|
||||
const archiveDetail = (row: any) => {
|
||||
const { href } = router.resolve({
|
||||
path: '/user/archiveDetail',
|
||||
query: {
|
||||
...(row.user_id != null ? { user_id: row.user_id } : {}),
|
||||
},
|
||||
});
|
||||
window.open(href, '_blank');
|
||||
};
|
||||
|
||||
// 快速操作(OK → state=5,问题 → state=3)
|
||||
|
||||
Reference in New Issue
Block a user