Files
sh-app/components/nbd-record-card.vue
T
2026-07-07 18:02:29 +08:00

361 lines
8.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="record-card">
<!-- 加载中 -->
<view v-if="list.length === 0" class="record-card__empty">
<text class="record-card__empty-text">暂无{{ title }}记录</text>
</view>
<!-- 有记录 -->
<template v-else>
<view class="record-card__section" :style="{ ...props.customStyle }">
<view class="record-card__header">
<view class="record-card__bar"></view>
<text class="record-card__title">{{ title }}记录</text>
<text v-if="list.length > 1" class="record-card__more" @click="showPopup = true">查看全部({{ list.length }})</text>
</view>
<!-- 最新一条记录 -->
<view class="record-card__card" v-if="latest"
:class="cardStatus(latest) === 'pass' ? 'record-card__card--pass' : 'record-card__card--fail'">
<view v-for="field in fields" :key="field.key">
<view v-if="isVisible(field, latest)" :class="field.type === 'photo' ? 'record-card__row--stack' : 'record-card__row'">
<text :class="field.type === 'photo' ? 'record-card__label--stack' : 'record-card__label'">{{ field.label }}</text>
<!-- text默认 -->
<text v-if="!field.type || field.type === 'text'" class="record-card__value">
{{ latest[field.key] || '--' }}
</text>
<!-- tag -->
<view v-else-if="field.type === 'tag'" class="record-card__tag"
:style="tagBgStyle(field, latest)">
<text class="record-card__tag-icon" :style="tagIconStyle(field, latest)">
{{ tagPass(field, latest) ? '\u2713' : '\u2717' }}
</text>
<text class="record-card__tag-text" :style="tagTextStyle(field, latest)">
{{ tagDisplayText(field, latest) }}
</text>
</view>
<!-- photo -->
<view v-else-if="field.type === 'photo'" class="record-card__value--stack">
<nbd-photo-view :attachUrl="latest[field.key]" :cols="field.cols || 4" />
</view>
</view>
</view>
</view>
</view>
<!-- 弹窗全部记录 -->
<u-popup v-model="showPopup" mode="bottom" :safe-area-inset-bottom="true">
<view class="record-card__popup">
<view class="record-card__popup-hd">
<text class="record-card__popup-title">全部{{ title }}记录</text>
<text class="record-card__popup-close" @click="showPopup = false"></text>
</view>
<scroll-view class="record-card__popup-bd" scroll-y>
<view v-for="(item, index) in list" :key="index"
class="record-card__card record-card__card--popup"
:class="cardStatus(item) === 'pass' ? 'record-card__card--pass' : 'record-card__card--fail'">
<view v-for="field in fields" :key="field.key">
<view v-if="isVisible(field, item)" :class="field.type === 'photo' ? 'record-card__row--stack' : 'record-card__row'">
<text :class="field.type === 'photo' ? 'record-card__label--stack' : 'record-card__label'">{{ field.label }}</text>
<!-- text -->
<text v-if="!field.type || field.type === 'text'" class="record-card__value">
{{ item[field.key] || '--' }}
</text>
<!-- tag -->
<view v-else-if="field.type === 'tag'" class="record-card__tag"
:style="tagBgStyle(field, item)">
<text class="record-card__tag-icon" :style="tagIconStyle(field, item)">
{{ tagPass(field, item) ? '\u2713' : '\u2717' }}
</text>
<text class="record-card__tag-text" :style="tagTextStyle(field, item)">
{{ tagDisplayText(field, item) }}
</text>
</view>
<!-- photo -->
<view v-else-if="field.type === 'photo'" class="record-card__value--stack">
<nbd-photo-view :attachUrl="item[field.key]" :cols="field.cols || 4" />
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</u-popup>
</template>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
fields: { type: Array, default: () => [] },
list: { type: Array, default: () => [] },
title: { type: String, default: '' },
customStyle: { type: Object, default: () => ({}) },
statusKey: { type: String, default: '' },
statusMatch: { default: '合格' },
})
const showPopup = ref(false)
const latest = computed(() => props.list[0] || null)
// ---- 字段可见性 ----
const isVisible = (field, item) => {
const cond = field.showWhen
if (!cond) return true
const val = item[cond.key]
if (cond.notEqual !== undefined) return val !== cond.notEqual
if (cond.equal !== undefined) return val === cond.equal
return true
}
// ---- tag 辅助 ----
const tagPass = (field, item) => item[field.key] === field.tagMatch
const tagIconStyle = (field, item) => ({
color: tagPass(field, item)
? (field.passColor || '#22c55e')
: (field.failColor || '#ef4444'),
})
const tagTextStyle = (field, item) => ({
color: tagPass(field, item)
? (field.passColor || '#16a34a')
: (field.failColor || '#dc2626'),
})
const tagBgStyle = (field, item) => ({
background: tagPass(field, item)
? (field.passBg || '#ebf9f0')
: (field.failBg || '#fef2f2'),
})
const tagDisplayText = (field, item) =>
tagPass(field, item)
? (field.tagPassText ?? item[field.key] ?? '--')
: (field.tagFailText ?? item[field.key] ?? '--')
// ---- 卡片状态 ----
const cardStatus = (item) => {
if (!props.statusKey) return ''
return item[props.statusKey] === props.statusMatch ? 'pass' : 'fail'
}
</script>
<style lang="scss" scoped>
$primary: #2979ff;
$card-bg: #fff;
$card-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.06);
$text: #1a1a2e;
$text-secondary: #8e99a4;
.record-card {
&__empty {
padding: 40rpx 24rpx;
text-align: center;
}
&__empty-text {
font-size: 26rpx;
color: $text-secondary;
}
&__header {
display: flex;
align-items: center;
margin-bottom: 12rpx;
padding: 0 6rpx;
}
&__bar {
width: 6rpx;
height: 32rpx;
border-radius: 3rpx;
background: $primary;
margin-right: 14rpx;
}
&__title {
font-size: 28rpx;
font-weight: 700;
color: $text;
flex: 1;
}
&__more {
font-size: 24rpx;
color: $primary;
padding: 6rpx 16rpx;
background: rgba($primary, 0.08);
border-radius: 20rpx;
}
&__card {
position: relative;
box-sizing: border-box;
width: 100%;
background: $card-bg;
border-radius: 12rpx;
padding: 20rpx;
box-shadow: $card-shadow;
overflow: hidden;
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
right: 0;
height: 6rpx;
transition: background 0.3s;
}
&--pass::before {
background: #22c55e;
}
&--fail::before {
background: #ef4444;
}
}
&__row {
display: flex;
align-items: center;
padding: 8rpx 0;
border-bottom: 1rpx solid #f0f0f0;
&--stack {
flex-direction: column;
align-items: flex-start;
.record-card__value {
margin-top: 8rpx;
width: 100%;
}
}
}
&__label {
font-size: 26rpx;
color: $text-secondary;
flex-shrink: 0;
margin-right: 24rpx;
width: 160rpx;
&--stack {
font-size: 26rpx;
color: $text-secondary;
flex-shrink: 0;
margin-right: 24rpx;
width: 140rpx;
margin-top: 8rpx;
}
}
&__value {
color: $text;
flex: 1;
text-align: left;
font-family: "SF Mono", "Menlo", "Consolas", monospace;
font-size: 28rpx;
font-weight: 500;
color: #0f1724;
letter-spacing: 0.3rpx;
&--stack {
padding-left: 20rpx;
padding-bottom: 8rpx;
padding-top: 8rpx;
font-family: "SF Mono", "Menlo", "Consolas", monospace;
font-size: 28rpx;
font-weight: 500;
color: #0f1724;
letter-spacing: 0.3rpx;
}
}
&__tag {
display: inline-flex;
align-items: center;
gap: 6rpx;
padding: 6rpx 16rpx;
border-radius: 8rpx;
min-width: 100rpx;
justify-content: center;
// background 由 tagBgStyle() 内联控制
&-icon {
font-size: 22rpx;
font-weight: 700;
// color 由 tagIconStyle() 内联控制
}
&-text {
font-size: 24rpx;
font-weight: 600;
// color 由 tagTextStyle() 内联控制
}
}
// ===== 弹窗 =====
&__popup {
background: #fff;
display: flex;
flex-direction: column;
height: 80vh;
border-radius: 24rpx 24rpx 0 0;
}
&__popup-hd {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 28rpx 12rpx;
border-bottom: 1rpx solid #f0f0f0;
}
&__popup-title {
font-size: 30rpx;
font-weight: 700;
color: $text;
}
&__popup-close {
font-size: 32rpx;
color: $text-secondary;
padding: 8rpx;
}
&__popup-bd {
flex: 1;
box-sizing: border-box;
overflow-y: auto;
background: #f5f5f5;
padding: 12rpx 20rpx 20rpx;
}
&__card--popup {
box-sizing: border-box;
width: 100%;
margin-bottom: 12rpx;
box-shadow: $card-shadow;
}
&__card--popup:last-child {
margin-bottom: 0;
}
}
</style>