2026-08-04 21:01:39 +08:00
< script setup lang = "ts" >
import { computed , onMounted , ref , watch } from 'vue'
import { ElMessage , ElMessageBox } from 'element-plus'
import { Check , CircleAlert , History , RotateCcw , ShieldAlert , X } from '@lucide/vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import DataTable , { type DataTableColumn } from '@/shared/components/DataTable.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import {
approveMeasurement ,
getMeasurementReviewDetail ,
listMeasurementReviewHistory ,
rejectMeasurement ,
releaseRejectedMeasurement ,
} from './api'
import type { MeasurementReviewDetail , MeasurementReviewItem , MeasurementReviewRecord } from './types'
const props = defineProps < {
customerId ?: string
/** @deprecated 仅用于兼容尚未完成 customerId 映射的上层集成。 */
tenantId ?: string
inboundId : string
} > ()
const emit = defineEmits < {
changed : []
} > ()
const access = useAccessPreviewStore ()
const detail = ref < MeasurementReviewDetail | null >( null )
const history = ref < MeasurementReviewRecord [] >([])
const loading = ref ( false )
const historyLoading = ref ( false )
const actionLoading = ref ( '' )
const loadError = ref < ApiErrorDetails | null >( null )
const historyError = ref < ApiErrorDetails | null >( null )
const reasonDialog = ref < 'reject' | 'release' | null > ( null )
const reason = ref ( '' )
const requestSerial = ref ( 0 )
const reasonDialogOpen = computed ({
get : () => reasonDialog . value !== null ,
set : ( open : boolean ) => { if ( ! open ) reasonDialog . value = null },
})
const canReview = computed (() => access . can ( 'firstleg:inbound:measurement-review' ))
const canRelease = computed (() => access . can ( 'firstleg:inbound:exception-release' ))
const customerId = computed (() => props . customerId || props . tenantId || '' )
const isPending = computed (() => detail . value ? . measureReviewStatus === 'PENDING' && detail . value ? . processStatus === 'MEASURE_PENDING' )
const isRejected = computed (() => detail . value ? . measureReviewStatus === 'REJECTED' && detail . value ? . processStatus === 'MEASURE_REJECTED' )
const confirmedWeights = computed (() => detail . value ? . weightResults . filter (( item ) => item . confirmedAt && item . finalWeightKg !== null ). length ?? 0 )
const reviewStatusLabel = computed (() => {
if ( isPending . value ) return '待审核'
if ( isRejected . value ) return '已驳回'
if ( detail . value ? . measureReviewStatus === 'APPROVED' ) return '已通过'
return detail . value ? . measureReviewStatus || '未提交'
})
const measurementColumns : DataTableColumn [] = [
2026-08-07 16:17:45 +08:00
{ key : 'boxCode' , label : '扫描箱号' , minWidth : 190 , slotName : 'boxCode' },
{ key : 'packageCount' , label : '确认箱数' , width : 90 , slotName : 'packageCount' },
{ key : 'dimensions' , label : '实测长 × 宽 × 高(cm) ' , minWidth : 190 , slotName : 'dimensions' },
{ key : 'unitWeightKg' , label : '实测重量(kg) ' , minWidth : 115 , slotName : 'unitWeightKg' },
{ key : 'machineNo' , label : '机器编号' , minWidth : 130 , slotName : 'machineNo' },
{ key : 'measuredAt' , label : '测量时间' , minWidth : 150 , slotName : 'measuredAt' },
{ key : 'intelligentImageFileId' , label : '当前图片' , minWidth : 110 , slotName : 'image' },
2026-08-04 21:01:39 +08:00
{ key : 'unitVolumeCbm' , label : '每箱体积(m³)' , minWidth : 125 , slotName : 'unitVolumeCbm' },
{ key : 'totalWeightKg' , label : '总毛重(kg) ' , minWidth : 110 , slotName : 'totalWeightKg' },
{ key : 'totalVolumeCbm' , label : '总体积(m³)' , minWidth : 110 , slotName : 'totalVolumeCbm' },
{ key : 'volumeWeightKg' , label : '材积重(kg) ' , minWidth : 110 , slotName : 'volumeWeightKg' },
{ key : 'note' , label : '超限提示 / 备注' , minWidth : 200 , slotName : 'note' },
]
function formatNumber ( value : number | null | undefined , unit = '' ) {
if ( value === null || value === undefined || Number . isNaN ( value )) return '-'
return ` ${ value . toLocaleString ( 'zh-CN' , { maximumFractionDigits : 3 } )} ${ unit } `
}
function formatDate ( value : string | null | undefined ) {
return value ? value . replace ( 'T' , ' ' ) : '-'
}
function resultLabel ( value : string | null | undefined ) {
if ( value === 'APPROVED' ) return '审核通过'
if ( value === 'REJECTED' ) return '审核驳回'
return value || '未知结果'
}
function isConflict ( error : unknown ) {
const details = getApiErrorDetails ( error )
return details . code === 'A0409' || details . code === '409' || /并发|版本|已被更新|状态已变化/ . test ( details . message )
}
async function loadReview () {
const serial = ++ requestSerial . value
loadError . value = null
loading . value = true
try {
const result = await getMeasurementReviewDetail ( customerId . value , props . inboundId )
if ( serial === requestSerial . value ) detail . value = result
} catch ( error ) {
if ( serial === requestSerial . value ) {
detail . value = null
loadError . value = getApiErrorDetails ( error , '测量审核数据加载失败' )
}
} finally {
if ( serial === requestSerial . value ) loading . value = false
}
}
async function loadHistory () {
historyError . value = null
historyLoading . value = true
try {
const result = await listMeasurementReviewHistory ( customerId . value , props . inboundId )
history . value = result . records
} catch ( error ) {
history . value = []
historyError . value = getApiErrorDetails ( error , '审核历史加载失败' )
} finally {
historyLoading . value = false
}
}
async function reload () {
await Promise . all ([ loadReview (), loadHistory ()])
}
async function confirmApprove () {
if ( ! detail . value || ! isPending . value || ! canReview . value ) return
try {
await ElMessageBox . confirm (
'审核通过后,系统会自动试算并发布费用,同时完成托盘库位占用和入库。客户无需再次确认费用。是否继续?' ,
'确认测量审核通过' ,
{ confirmButtonText : '通过并完成入库' , cancelButtonText : '返回' , type : 'warning' },
)
} catch {
return
}
actionLoading . value = 'approve'
try {
await approveMeasurement ( customerId . value , props . inboundId , {
measurementId : detail . value . measurementId ,
expectedVersion : detail . value . version ,
})
ElMessage . success ( '审核已通过,费用发布和入库正在完成' )
await reload ()
emit ( 'changed' )
} catch ( error ) {
ElMessage . error ( isConflict ( error ) ? '数据已被更新,请刷新后重新审核' : getApiErrorDetails ( error , '审核通过失败' ). message )
if ( isConflict ( error )) await reload ()
} finally {
actionLoading . value = ''
}
}
function openReasonDialog ( kind : 'reject' | 'release' ) {
reason . value = ''
reasonDialog . value = kind
}
async function submitReason () {
if ( ! detail . value || ! reasonDialog . value ) return
const value = reason . value . trim ()
if ( ! value ) {
ElMessage . warning ( reasonDialog . value === 'reject' ? '请填写驳回原因' : '请填写异常放行原因' )
return
}
const kind = reasonDialog . value
actionLoading . value = kind
try {
if ( kind === 'reject' ) {
await rejectMeasurement ( customerId . value , props . inboundId , {
measurementId : detail . value . measurementId ,
expectedVersion : detail . value . version ,
reason : value ,
})
ElMessage . success ( '测量已驳回,已退回测量修正' )
} else {
await releaseRejectedMeasurement ( customerId . value , props . inboundId , {
measurementId : detail . value . measurementId ,
expectedVersion : detail . value . version ,
reason : value ,
})
ElMessage . success ( '异常放行已提交,后续计费入库将继续处理' )
}
reasonDialog . value = null
await reload ()
emit ( 'changed' )
} catch ( error ) {
ElMessage . error ( isConflict ( error ) ? '数据已被更新,请刷新后重试' : getApiErrorDetails ( error , kind === 'reject' ? '测量驳回失败' : '异常放行失败' ). message )
if ( isConflict ( error )) await reload ()
} finally {
actionLoading . value = ''
}
}
watch (() => [ customerId . value , props . inboundId ], () => {
if ( customerId . value && props . inboundId ) void reload ()
}, { immediate : true })
onMounted (() => {
if ( ! customerId . value || ! props . inboundId ) loadError . value = { message : '缺少所属企业或入库单标识,无法加载审核数据' }
})
</ script >
< template >
< section class = "review-panel" aria -labelledby =" measurement -review-title " >
< header class = "review-header" >
< div >
< h2 id = "measurement-review-title" > 测量审核 </ h2 >
< p > 核对原单 、 设备实测和称重 OCR 后 , 再决定是否进入自动计费入库 。 </ p >
</ div >
< div class = "review-header-status" >
< StatusBadge v-if = "detail" :status="reviewStatusLabel" />
< el-button text : loading = "loading || historyLoading" aria -label =" 刷新审核数据 " @click ="reload" >< RotateCcw :size = "16" /> 刷新 </ el-button >
</ div >
</ header >
< el-alert v-if = "loadError" type="error" :closable="false" show-icon role="alert" >
< template # title >< div class = "review-alert-title" >< span >{{ loadError . message }}</ span >< ApiErrorMeta :details = "loadError" /></ div ></ template >
< el-button link type = "danger" @click ="reload" > 重新加载 </ el -button >
</ el-alert >
< el-skeleton v-if = "loading && !detail" :rows="8" animated />
< template v-else-if = "detail" >
< div class = "comparison-grid" >
< article class = "comparison-card" >
< span class = "comparison-label" > 原单申报 </ span >
< strong >{{ detail . originalDocumentNo || detail . inboundCode || '-' }}</ strong >
< dl >
< div >< dt > 申报毛重 </ dt >< dd >{{ formatNumber ( detail . originalGrossWeightKg , ' kg' ) }}</ dd ></ div >
< div >< dt > 申报体积 </ dt >< dd >{{ formatNumber ( detail . originalVolumeCbm , ' CBM' ) }}</ dd ></ div >
</ dl >
</ article >
< article class = "comparison-card" >
< span class = "comparison-label" > 设备实测 </ span >
< strong >{{ formatNumber ( detail . actualPackageCount , ' 箱' ) }}</ strong >
< dl >
< div >< dt > 实测体积 </ dt >< dd >{{ formatNumber ( detail . actualVolumeCbm , ' CBM' ) }}</ dd ></ div >
< div >< dt > 材积重 </ dt >< dd >{{ formatNumber ( detail . volumeWeightKg , ' kg' ) }}</ dd ></ div >
</ dl >
</ article >
< article class = "comparison-card" >
< span class = "comparison-label" > 称重 OCR </ span >
< strong >{{ formatNumber ( detail . actualGrossWeightKg , ' kg' ) }}</ strong >
< dl >
< div >< dt > 已确认图片 </ dt >< dd >{{ confirmedWeights }} / {{ detail . weightResults . length }}</ dd ></ div >
< div >< dt > 测量来源 </ dt >< dd >{{ detail . sourceType || '-' }}</ dd ></ div >
</ dl >
</ article >
</ div >
< div v-if = "detail.reviewReason" class="review-reason" >
< CircleAlert :size = "17" />< span >< strong > 最近审核说明 : </ strong >{{ detail . reviewReason }}</ span >
</ div >
< div class = "review-section" >
< div class = "section-heading" >< div >< h3 > 三组数据对比 </ h3 >< p >{{ detail . companyName || '-' }} · {{ detail . channelName || detail . channelCode || '-' }}</ p ></ div >< span class = "section-count" >{{ detail . items . length }} 条箱规 </ span ></ div >
< DataTable :data = "detail.items" :columns = "measurementColumns" empty -text =" 暂无设备测量明细 " border stripe >
2026-08-07 16:17:45 +08:00
< template # boxCode = "{ row }" >{{ ( row as MeasurementReviewItem ). boxCode || ( row as MeasurementReviewItem ). itemNo || '-' }}</ template >
2026-08-04 21:01:39 +08:00
< template # packageCount = "{ row }" >{{ ( row as MeasurementReviewItem ). packageCount ?? '-' }}</ template >
< template # dimensions = "{ row }" >{{ [( row as MeasurementReviewItem ). lengthCm , ( row as MeasurementReviewItem ). widthCm , ( row as MeasurementReviewItem ). heightCm ]. map (( value ) => formatNumber ( value )). join ( ' × ' ) }}</ template >
< template # outerDimensions = "{ row }" >{{ [( row as MeasurementReviewItem ). outerLengthCm , ( row as MeasurementReviewItem ). outerWidthCm , ( row as MeasurementReviewItem ). outerHeightCm ]. map (( value ) => formatNumber ( value )). join ( ' × ' ) }}</ template >
< template # girthCm = "{ row }" >{{ formatNumber (( row as MeasurementReviewItem ). girthCm ) }}</ template >
< template # unitWeightKg = "{ row }" >{{ formatNumber (( row as MeasurementReviewItem ). unitWeightKg ) }}</ template >
2026-08-07 16:17:45 +08:00
< template # machineNo = "{ row }" >{{ ( row as MeasurementReviewItem ). machineNo || '-' }}</ template >
< template # measuredAt = "{ row }" >{{ formatDate (( row as MeasurementReviewItem ). measuredAt ) }}</ template >
< template # image = "{ row }" >{{ ( row as MeasurementReviewItem ). intelligentImageFileId ? '已关联' : '暂无' }}</ template >
2026-08-04 21:01:39 +08:00
< template # unitVolumeCbm = "{ row }" >{{ formatNumber (( row as MeasurementReviewItem ). unitVolumeCbm ) }}</ template >
< template # totalWeightKg = "{ row }" >{{ formatNumber (( row as MeasurementReviewItem ). totalWeightKg ) }}</ template >
< template # totalVolumeCbm = "{ row }" >{{ formatNumber (( row as MeasurementReviewItem ). totalVolumeCbm ) }}</ template >
< template # volumeWeightKg = "{ row }" >{{ formatNumber (( row as MeasurementReviewItem ). volumeWeightKg ) }}</ template >
< template # note = "{ row }" >< div class = "measurement-note" >< strong v-if = "(row as MeasurementReviewItem).overLimitWarning" >{{ ( row as MeasurementReviewItem ) .overLimitWarning }}</ strong >< span >{{ ( row as MeasurementReviewItem ). remark || (( row as MeasurementReviewItem ). overLimitWarning ? '' : '-' ) }}</ span ></ div ></ template >
< template # empty >< el-empty description = "暂无设备测量明细" :image-size = "48" /></ template >
</ DataTable >
</ div >
< div class = "review-section" >
< div class = "section-heading" >< div >< h3 > 称重图片识别结果 </ h3 >< p > 原始 OCR 值不可覆盖 , 只有已确认且有效的结果会计入实际重量 。 </ p ></ div >< span class = "section-count" >{{ detail . weightResults . length }} 张 </ span ></ div >
< div class = "weight-list" >
< div v-for = "item in detail.weightResults" :key="item.id" class="weight-row" >
< div >< strong >{{ item . fileId || '未关联文件' }}</ strong >< span >{{ item . ocrProvider || 'OCR' }} · {{ item . recognitionStatus || '待确认' }}</ span >< p v-if = "item.ocrRawText">OCR 原文:{{ item.ocrRawText }}</p><p v-if="item.adjustmentReason" > 调整原因 : {{ item.adjustmentReason }}</ p ></ div >
< div >< span > 原始 </ span >< strong >{{ formatNumber ( item . ocrWeightValue , ` ${ item . ocrWeightUnit || '' } ` ) }}</ strong ></ div >
< div >< span > 调整后 </ span >< strong >{{ formatNumber ( item . adjustedWeightValue , ` ${ item . adjustedWeightUnit || '' } ` ) }}</ strong ></ div >
< div >< span > 最终 </ span >< strong >{{ formatNumber ( item . finalWeightKg , ' kg' ) }}</ strong ></ div >
< div >< span > 置信度 </ span >< strong >{{ item . confidence === null ? '-' : ` ${ Math . round ( item . confidence * 100 ) } %` }}</ strong ></ div >
</ div >
< el-empty v-if = "!detail.weightResults.length" description="暂无称重图片识别结果" :image-size="48" />
</ div >
</ div >
< div class = "review-actions" : class = "{ 'review-actions--disabled': !isPending && !isRejected }" >
< div >< strong > 审核结论 </ strong >< span v-if = "isPending">通过后将自动完成计费、费用发布和入库。</span><span v-else-if="isRejected" > 当前测量已驳回 , 可修正后重新提交 , 或按权限申请异常放行 。 </ span >< span v-else > 当前记录不在可操作的测量审核状态 。 </ span ></ div >
< div class = "action-buttons" >
< el-button v-if = "isPending && canReview" type="danger" plain :loading="actionLoading === 'reject'" @click="openReasonDialog('reject')"><X :size="16" /> 驳回 </ el-button >
< el-button v-if = "isPending && canReview" type="primary" :loading="actionLoading === 'approve'" @click="confirmApprove"><Check :size="16" /> 通过并完成入库 </ el-button >
< el-button v-if = "isRejected && canRelease" type="warning" plain :loading="actionLoading === 'release'" @click="openReasonDialog('release')"><ShieldAlert :size="16" /> 异常放行 </ el-button >
</ div >
</ div >
< div class = "review-section history-section" >
< div class = "section-heading" >< div >< h3 >< History :size = "17" /> 审核历史 </ h3 >< p > 每次审核均保留操作人 、 原因和放行开关快照 。 </ p ></ div ></ div >
< el-alert v-if = "historyError" type="warning" :closable="false" show-icon><template #title><div class="review-alert-title"><span>{{ historyError.message }}</span><ApiErrorMeta :details="historyError" /></div></template><el-button link type="warning" @click="loadHistory" > 重试 </ el -button ></ el-alert >
< el-skeleton v-else-if = "historyLoading" :rows="3" animated />
< el-empty v-else-if = "!history.length" description="暂无审核历史" :image-size="48" />
< ol v-else class = "history-list" >
< li v-for = "item in history" :key="item.id" >
< div class = "history-marker" : class = "{ 'history-marker--success': item.reviewResult === 'APPROVED', 'history-marker--danger': item.reviewResult === 'REJECTED' }" />
< div class = "history-content" >< div >< strong >{{ resultLabel ( item . reviewResult ) }}{{ item . exceptionRelease ? ' · 异常放行' : '' }}</ strong >< time >{{ formatDate ( item . reviewedAt ) }}</ time ></ div >< p >{{ item . releaseReason || item . reviewReason || '未填写说明' }}</ p >< span >{{ item . reviewerName || '系统' }} · {{ item . allowRejectedConfig ? '放行开关已开启' : '放行开关未开启' }}</ span ></ div >
</ li >
</ ol >
</ div >
</ template >
2026-08-10 15:36:53 +08:00
< el-dialog v-model = "reasonDialogOpen" :title="reasonDialog === 'reject' ? '填写驳回原因' : '填写异常放行原因'" width="min(520px, calc(100vw - 28px))" destroy -on-close >
2026-08-04 21:01:39 +08:00
< el-alert v-if = "reasonDialog === 'release'" title="异常放行不会抹去驳回记录;请说明为什么允许该测量继续进入自动计费入库。" type="warning" :closable="false" show -icon />
< el-input v-model = "reason" type="textarea" :rows="5" maxlength="1000" show-word-limit :placeholder="reasonDialog === 'reject' ? '请说明需要修正的测量问题' : '请填写异常放行的业务原因'" />
< template # footer >< el-button :disabled = "Boolean(actionLoading)" @click ="reasonDialog = null" > 取消 </ el -button >< el-button : type = "reasonDialog === 'reject' ? 'danger' : 'warning'" :loading = "Boolean(actionLoading)" @click ="submitReason" > 确认 {{ reasonDialog = == ' reject ' ? ' 驳回 ' : ' 放行 ' }} </ el -button ></ template >
</ el-dialog >
</ section >
</ template >
< style scoped >
. review - panel { display : grid ; gap : 20 px ; color : var ( -- cc - text ); }
. review - header { display : flex ; justify - content : space - between ; align - items : flex - start ; gap : 18 px ; padding - bottom : 4 px ; }
. review - header h2 { margin : 0 0 5 px ; font - size : 22 px ; line - height : 1.25 ; }
. review - header p , . section - heading p { margin : 0 ; color : var ( -- cc - muted ); font - size : 13 px ; line - height : 1.55 ; }
. review - header - status { display : flex ; align - items : center ; gap : 10 px ; white - space : nowrap ; }
. review - alert - title { display : inline - flex ; align - items : center ; gap : 8 px ; flex - wrap : wrap ; }
. comparison - grid { display : grid ; grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr )); gap : 12 px ; }
. comparison - card { min - width : 0 ; padding : 16 px ; border : 1 px solid var ( -- cc - border ); border - radius : 8 px ; background : var ( -- cc - surface ); }
. comparison - card strong { display : block ; margin : 8 px 0 12 px ; font - size : 21 px ; line - height : 1.2 ; overflow - wrap : anywhere ; }
. comparison - label { color : var ( -- cc - muted ); font - size : 12 px ; font - weight : 700 ; }
dl { display : grid ; gap : 7 px ; margin : 0 ; }. comparison - card dl div { display : flex ; justify - content : space - between ; gap : 12 px ; font - size : 12 px ; }. comparison - card dt { color : var ( -- cc - muted ); }. comparison - card dd { margin : 0 ; font - weight : 600 ; text - align : right ; }
. review - reason { display : flex ; gap : 8 px ; align - items : flex - start ; padding : 12 px 14 px ; border : 1 px solid # f3d19e ; border - radius : 8 px ; background : # fdf6ec ; color : # 8 a5a00 ; font - size : 13 px ; line - height : 1.55 ; }
. review - section { display : grid ; gap : 13 px ; padding - top : 2 px ; }. section - heading { display : flex ; align - items : flex - end ; justify - content : space - between ; gap : 12 px ; }. section - heading h3 { display : flex ; align - items : center ; gap : 7 px ; margin : 0 0 4 px ; font - size : 16 px ; }. section - count { color : var ( -- cc - muted ); font - size : 12 px ; white - space : nowrap ; }
. measurement - note { overflow - wrap : anywhere ; }. measurement - note strong , . measurement - note span { display : block ; }. measurement - note strong { color : var ( -- el - color - warning - dark - 2 ); }. measurement - note strong + span { margin - top : 4 px ; color : var ( -- cc - muted ); }
. weight - list { display : grid ; gap : 8 px ; }. weight - row { display : grid ; grid - template - columns : minmax ( 190 px , 1.8 fr ) repeat ( 4 , minmax ( 80 px , 1 fr )); gap : 12 px ; align - items : center ; padding : 12 px 14 px ; border : 1 px solid var ( -- cc - border ); border - radius : 8 px ; background : var ( -- cc - surface ); }. weight - row > div { min - width : 0 ; }. weight - row strong , . weight - row span , . weight - row p { display : block ; overflow - wrap : anywhere ; }. weight - row span { margin - top : 3 px ; color : var ( -- cc - muted ); font - size : 11 px ; }. weight - row p { margin : 5 px 0 0 ; color : var ( -- cc - muted ); font - size : 11 px ; line - height : 1.45 ; }. weight - row > div : not ( : first - child ) { text - align : right ; }
. review - actions { display : flex ; align - items : center ; justify - content : space - between ; gap : 16 px ; padding : 15 px 16 px ; border : 1 px solid # b3d8ff ; border - radius : 10 px ; background : # ecf5ff ; }. review - actions > div : first - child { display : grid ; gap : 4 px ; min - width : 0 ; }. review - actions span { color : # 606266 ; font - size : 12 px ; line - height : 1.5 ; }. review - actions -- disabled { border - color : var ( -- cc - border ); background : var ( -- cc - surface - muted ); }. action - buttons { display : flex ; flex - wrap : wrap ; justify - content : flex - end ; gap : 8 px ; }
. history - list { display : grid ; gap : 0 ; margin : 0 ; padding : 0 ; list - style : none ; }. history - list li { display : grid ; grid - template - columns : 16 px minmax ( 0 , 1 fr ); gap : 10 px ; position : relative ; padding - bottom : 16 px ; }. history - list li : not ( : last - child ) :: after { content : '' ; position : absolute ; top : 16 px ; bottom : 0 ; left : 7 px ; width : 1 px ; background : var ( -- cc - border ); }. history - marker { z - index : 1 ; width : 14 px ; height : 14 px ; margin - top : 2 px ; border : 3 px solid # 909399 ; border - radius : 50 % ; background : var ( -- cc - surface ); }. history - marker -- success { border - color : # 67 c23a ; }. history - marker -- danger { border - color : # f56c6c ; }. history - content { min - width : 0 ; }. history - content > div { display : flex ; justify - content : space - between ; gap : 12 px ; }. history - content time , . history - content > span { color : var ( -- cc - muted ); font - size : 11 px ; }. history - content p { margin : 5 px 0 4 px ; color : var ( -- cc - text ); font - size : 13 px ; overflow - wrap : anywhere ; }
. el - button { min - height : 36 px ; }. el - button : deep ( svg ) { flex : 0 0 auto ; }. review - panel : deep (. el - dialog__body ) { display : grid ; gap : 14 px ; }
@ media ( max - width : 720 px ) { . review - header , . section - heading , . review - actions { align - items : flex - start ; flex - direction : column ; }. review - header - status , . action - buttons { width : 100 % ; justify - content : space - between ; }. comparison - grid { grid - template - columns : 1 fr ; }. weight - row { grid - template - columns : 1 fr 1 fr ; }. weight - row > div : not ( : first - child ) { text - align : left ; }. review - actions . action - buttons . el - button { flex : 1 1 140 px ; }. review - actions . action - buttons { justify - content : stretch ; }. review - actions . action - buttons : deep (. el - button ) { width : 100 % ; } }
@ media ( max - width : 380 px ) { . review - header h2 { font - size : 19 px ; }. review - header p { font - size : 12 px ; }. comparison - card { padding : 13 px ; }. weight - row { grid - template - columns : 1 fr ; }. review - actions { padding : 13 px ; } }
</ style >