|
|
<template>
|
|
|
<div class="app-container">
|
|
|
<div class="headTitle">{{ boardTitle }}</div>
|
|
|
<div class="subTitle">
|
|
|
<span v-if="boardCode">boardCode: {{ boardCode }}</span>
|
|
|
<span v-else>缺少 boardCode</span>
|
|
|
<span class="split">|</span>
|
|
|
<span>刷新: {{ refreshIntervalSec }}s</span>
|
|
|
<span class="split">|</span>
|
|
|
<span>最近更新: {{ lastUpdateText }}</span>
|
|
|
</div>
|
|
|
|
|
|
<div v-if="errorMsg" class="error">{{ errorMsg }}</div>
|
|
|
|
|
|
<div class="content" v-else>
|
|
|
<div class="stats">
|
|
|
<div class="stat stat-pending">
|
|
|
<div class="stat-label">待处理</div>
|
|
|
<div class="stat-value">{{ stats.pending || 0 }}</div>
|
|
|
</div>
|
|
|
<div class="stat stat-processing">
|
|
|
<div class="stat-label">处理中</div>
|
|
|
<div class="stat-value">{{ stats.processing || 0 }}</div>
|
|
|
</div>
|
|
|
<div class="stat stat-resolved">
|
|
|
<div class="stat-label">已解决</div>
|
|
|
<div class="stat-value">{{ stats.resolved || 0 }}</div>
|
|
|
</div>
|
|
|
<div class="stat stat-cancelled">
|
|
|
<div class="stat-label">已取消</div>
|
|
|
<div class="stat-value">{{ stats.cancelled || 0 }}</div>
|
|
|
</div>
|
|
|
<div class="stat stat-total">
|
|
|
<div class="stat-label">总数</div>
|
|
|
<div class="stat-value">{{ stats.total || 0 }}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="panels">
|
|
|
<div class="panel">
|
|
|
<div class="panel-title">待处理 / 处理中 ({{ stats.active || 0 }})</div>
|
|
|
<div class="table">
|
|
|
<div class="grid-header" :style="gridStyle(activeColumns)">
|
|
|
<div
|
|
|
v-for="col in activeColumns"
|
|
|
:key="col.field"
|
|
|
class="cell header"
|
|
|
:style="cellStyle(col)"
|
|
|
>
|
|
|
{{ col.label }}
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="grid-body">
|
|
|
<div
|
|
|
v-for="(row, rowIndex) in activeEvents"
|
|
|
:key="row.eventId || rowIndex"
|
|
|
class="grid-row"
|
|
|
:style="gridStyle(activeColumns)"
|
|
|
>
|
|
|
<div
|
|
|
v-for="col in activeColumns"
|
|
|
:key="col.field"
|
|
|
class="cell"
|
|
|
:style="cellStyle(col)"
|
|
|
>
|
|
|
<span
|
|
|
v-if="col.field === 'eventStatus'"
|
|
|
:class="['status', statusClass(row.eventStatus)]"
|
|
|
>
|
|
|
{{ statusText(row.eventStatus) }}
|
|
|
</span>
|
|
|
<span v-else>
|
|
|
{{ formatValue(row, col.field, rowIndex) }}
|
|
|
</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div v-if="!activeEvents || activeEvents.length === 0" class="empty">暂无数据</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="panel">
|
|
|
<div class="panel-title">已解决 / 已取消 ({{ closedEvents.length }})</div>
|
|
|
<div class="table">
|
|
|
<div class="grid-header" :style="gridStyle(closedColumns)">
|
|
|
<div
|
|
|
v-for="col in closedColumns"
|
|
|
:key="col.field"
|
|
|
class="cell header"
|
|
|
:style="cellStyle(col)"
|
|
|
>
|
|
|
{{ col.label }}
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="grid-body">
|
|
|
<div
|
|
|
v-for="(row, rowIndex) in closedEvents"
|
|
|
:key="row.eventId || rowIndex"
|
|
|
class="grid-row"
|
|
|
:style="gridStyle(closedColumns)"
|
|
|
>
|
|
|
<div
|
|
|
v-for="col in closedColumns"
|
|
|
:key="col.field"
|
|
|
class="cell"
|
|
|
:style="cellStyle(col)"
|
|
|
>
|
|
|
<span
|
|
|
v-if="col.field === 'eventStatus'"
|
|
|
:class="['status', statusClass(row.eventStatus)]"
|
|
|
>
|
|
|
{{ statusText(row.eventStatus) }}
|
|
|
</span>
|
|
|
<span v-else>
|
|
|
{{ formatValue(row, col.field, rowIndex) }}
|
|
|
</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div v-if="!closedEvents || closedEvents.length === 0" class="empty">暂无数据</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div v-if="loading" class="loading">加载中...</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
import { viewAndonBoard } from '@/api/production/andonBoard'
|
|
|
import { parseTime } from '@/utils/ruoyi'
|
|
|
|
|
|
export default {
|
|
|
name: 'AndonBoard',
|
|
|
data() {
|
|
|
return {
|
|
|
boardCode: '',
|
|
|
loading: false,
|
|
|
fetching: false,
|
|
|
errorMsg: '',
|
|
|
config: {},
|
|
|
stats: {},
|
|
|
activeEvents: [],
|
|
|
closedEvents: [],
|
|
|
activeColumns: [],
|
|
|
closedColumns: [],
|
|
|
refreshIntervalSec: 10,
|
|
|
lastServerTime: null,
|
|
|
refreshTimer: null,
|
|
|
refreshTimerSec: null,
|
|
|
}
|
|
|
},
|
|
|
computed: {
|
|
|
boardTitle() {
|
|
|
const name = this.config && this.config.boardName ? this.config.boardName : '安灯看板'
|
|
|
return name
|
|
|
},
|
|
|
lastUpdateText() {
|
|
|
return this.lastServerTime ? parseTime(this.lastServerTime, '{y}-{m}-{d} {h}:{i}:{s}') : '-'
|
|
|
}
|
|
|
},
|
|
|
created() {
|
|
|
this.boardCode = this.getBoardCode()
|
|
|
this.reload()
|
|
|
},
|
|
|
watch: {
|
|
|
'$route.query.boardCode': function () {
|
|
|
const nextCode = this.getBoardCode()
|
|
|
if (nextCode !== this.boardCode) {
|
|
|
this.boardCode = nextCode
|
|
|
this.reload()
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
beforeDestroy() {
|
|
|
this.clearRefreshTimer()
|
|
|
},
|
|
|
methods: {
|
|
|
getBoardCode() {
|
|
|
return (this.$route.query && this.$route.query.boardCode) || (this.$route.params && this.$route.params.boardCode) || ''
|
|
|
},
|
|
|
reload() {
|
|
|
this.clearRefreshTimer()
|
|
|
if (!this.boardCode) {
|
|
|
this.errorMsg = '请在URL中传入参数 boardCode,例如:/andonBoard?boardCode=AD-001'
|
|
|
return
|
|
|
}
|
|
|
this.errorMsg = ''
|
|
|
this.lastServerTime = null
|
|
|
this.fetchBoardData()
|
|
|
},
|
|
|
fetchBoardData() {
|
|
|
if (this.fetching) {
|
|
|
return Promise.resolve()
|
|
|
}
|
|
|
const showLoading = !this.lastServerTime
|
|
|
this.fetching = true
|
|
|
if (showLoading) {
|
|
|
this.loading = true
|
|
|
}
|
|
|
return viewAndonBoard(this.boardCode)
|
|
|
.then((res) => {
|
|
|
const data = (res && res.data) || {}
|
|
|
this.errorMsg = ''
|
|
|
this.config = data.config || {}
|
|
|
this.stats = data.stats || {}
|
|
|
this.activeEvents = data.activeEvents || []
|
|
|
this.closedEvents = data.closedEvents || []
|
|
|
this.lastServerTime = data.serverTime || null
|
|
|
|
|
|
const interval = Number(this.config && this.config.refreshIntervalSec)
|
|
|
this.refreshIntervalSec = interval && interval > 0 ? interval : 10
|
|
|
|
|
|
const cols = this.parseDisplayFields(this.config && this.config.displayFields)
|
|
|
this.activeColumns = cols.active
|
|
|
this.closedColumns = cols.closed
|
|
|
|
|
|
this.resetRefreshTimer()
|
|
|
})
|
|
|
.catch((e) => {
|
|
|
this.errorMsg = (e && e.message) || '加载失败'
|
|
|
})
|
|
|
.finally(() => {
|
|
|
this.fetching = false
|
|
|
if (showLoading) {
|
|
|
this.loading = false
|
|
|
}
|
|
|
this.resetRefreshTimer()
|
|
|
})
|
|
|
},
|
|
|
clearRefreshTimer() {
|
|
|
if (this.refreshTimer) {
|
|
|
clearInterval(this.refreshTimer)
|
|
|
this.refreshTimer = null
|
|
|
this.refreshTimerSec = null
|
|
|
}
|
|
|
},
|
|
|
resetRefreshTimer() {
|
|
|
const sec = this.refreshIntervalSec
|
|
|
if (!sec || sec <= 0) {
|
|
|
return
|
|
|
}
|
|
|
if (this.refreshTimer && this.refreshTimerSec === sec) {
|
|
|
return
|
|
|
}
|
|
|
this.clearRefreshTimer()
|
|
|
this.refreshTimerSec = sec
|
|
|
this.refreshTimer = setInterval(() => {
|
|
|
this.fetchBoardData()
|
|
|
}, sec * 1000)
|
|
|
},
|
|
|
parseDisplayFields(raw) {
|
|
|
const fallbackActive = ['callCode', 'callTypeCode', 'stationCode', 'deviceCode', 'eventStatus', 'priority', 'createTime', 'description']
|
|
|
const fallbackClosed = ['callCode', 'callTypeCode', 'stationCode', 'deviceCode', 'eventStatus', 'responseEndTime', 'resolution', 'cancelReason']
|
|
|
|
|
|
if (!raw) {
|
|
|
return {
|
|
|
active: this.normalizeColumns(fallbackActive),
|
|
|
closed: this.normalizeColumns(fallbackClosed),
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (typeof raw === 'string') {
|
|
|
const trimmed = raw.trim()
|
|
|
if (trimmed && trimmed[0] !== '[' && trimmed[0] !== '{') {
|
|
|
const fields = trimmed.split(/[,,;;\s]+/).filter(Boolean)
|
|
|
if (fields.length) {
|
|
|
const cols = this.normalizeColumns(fields)
|
|
|
return { active: cols, closed: cols }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
let parsed
|
|
|
try {
|
|
|
parsed = typeof raw === 'string' ? JSON.parse(raw) : raw
|
|
|
} catch (e) {
|
|
|
return {
|
|
|
active: this.normalizeColumns(fallbackActive),
|
|
|
closed: this.normalizeColumns(fallbackClosed),
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (Array.isArray(parsed)) {
|
|
|
const cols = this.normalizeColumns(parsed)
|
|
|
return { active: cols, closed: cols }
|
|
|
}
|
|
|
|
|
|
if (parsed && typeof parsed === 'object') {
|
|
|
const activeRaw = parsed.activeFields || parsed.active || parsed.fields || parsed
|
|
|
const closedRaw = parsed.closedFields || parsed.closed || parsed.fields || parsed
|
|
|
|
|
|
const activeCols = Array.isArray(activeRaw) || typeof activeRaw === 'object' ? this.normalizeColumns(activeRaw) : this.normalizeColumns(fallbackActive)
|
|
|
const closedCols = Array.isArray(closedRaw) || typeof closedRaw === 'object' ? this.normalizeColumns(closedRaw) : this.normalizeColumns(fallbackClosed)
|
|
|
|
|
|
return {
|
|
|
active: activeCols && activeCols.length ? activeCols : this.normalizeColumns(fallbackActive),
|
|
|
closed: closedCols && closedCols.length ? closedCols : this.normalizeColumns(fallbackClosed),
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
active: this.normalizeColumns(fallbackActive),
|
|
|
closed: this.normalizeColumns(fallbackClosed),
|
|
|
}
|
|
|
},
|
|
|
normalizeColumns(input) {
|
|
|
if (!input) return []
|
|
|
|
|
|
if (Array.isArray(input)) {
|
|
|
return input
|
|
|
.map((item) => {
|
|
|
if (!item) return null
|
|
|
if (typeof item === 'string') {
|
|
|
return { field: item, label: this.fieldLabel(item) }
|
|
|
}
|
|
|
if (typeof item === 'object') {
|
|
|
const field = item.field || item.prop || item.key
|
|
|
if (!field) return null
|
|
|
return {
|
|
|
field,
|
|
|
label: item.label || item.title || this.fieldLabel(field),
|
|
|
width: item.width,
|
|
|
align: item.align,
|
|
|
}
|
|
|
}
|
|
|
return null
|
|
|
})
|
|
|
.filter(Boolean)
|
|
|
}
|
|
|
|
|
|
if (input && typeof input === 'object') {
|
|
|
return Object.keys(input).map((field) => {
|
|
|
const val = input[field]
|
|
|
if (typeof val === 'string') {
|
|
|
return { field, label: val }
|
|
|
}
|
|
|
if (typeof val === 'object' && val) {
|
|
|
return {
|
|
|
field,
|
|
|
label: val.label || val.title || this.fieldLabel(field),
|
|
|
width: val.width,
|
|
|
align: val.align,
|
|
|
}
|
|
|
}
|
|
|
return { field, label: this.fieldLabel(field) }
|
|
|
})
|
|
|
}
|
|
|
|
|
|
return []
|
|
|
},
|
|
|
fieldLabel(field) {
|
|
|
const map = {
|
|
|
index: '序号',
|
|
|
eventId: '事件ID',
|
|
|
callCode: '呼叫单号',
|
|
|
callTypeCode: '呼叫类型',
|
|
|
sourceType: '触发源类型',
|
|
|
sourceRefId: '触发源',
|
|
|
productLineCode: '产线',
|
|
|
stationCode: '工位',
|
|
|
teamCode: '班组',
|
|
|
orderCode: '工单号',
|
|
|
materialCode: '物料编码',
|
|
|
deviceCode: '设备编码',
|
|
|
priority: '优先级',
|
|
|
eventStatus: '状态',
|
|
|
description: '描述',
|
|
|
ackBy: '确认人',
|
|
|
ackTime: '确认时间',
|
|
|
responseStartTime: '开始处理时间',
|
|
|
responseEndTime: '完成时间',
|
|
|
resolution: '解决措施',
|
|
|
cancelReason: '取消原因',
|
|
|
escalateLevel: '升级级别',
|
|
|
escalateTime: '升级时间',
|
|
|
ackDeadline: '确认截止',
|
|
|
resolveDeadline: '解决截止',
|
|
|
createTime: '创建时间',
|
|
|
updateTime: '更新时间',
|
|
|
}
|
|
|
return map[field] || field
|
|
|
},
|
|
|
formatValue(row, field, rowIndex) {
|
|
|
if (field === 'index') {
|
|
|
return rowIndex + 1
|
|
|
}
|
|
|
const v = row ? row[field] : null
|
|
|
if (v === null || v === undefined) return ''
|
|
|
|
|
|
if (field === 'eventStatus') {
|
|
|
return this.statusText(v)
|
|
|
}
|
|
|
|
|
|
if (field === 'sourceType') {
|
|
|
return this.sourceTypeText(v)
|
|
|
}
|
|
|
|
|
|
if (/(Time|Deadline)$/i.test(field)) {
|
|
|
const text = parseTime(v, '{y}-{m}-{d} {h}:{i}:{s}')
|
|
|
return text || ''
|
|
|
}
|
|
|
|
|
|
return String(v)
|
|
|
},
|
|
|
statusText(status) {
|
|
|
if (status === '0') return '待处理'
|
|
|
if (status === '1') return '处理中'
|
|
|
if (status === '2') return '已解决'
|
|
|
if (status === '3') return '已取消'
|
|
|
return String(status || '')
|
|
|
},
|
|
|
statusClass(status) {
|
|
|
if (status === '0') return 'pending'
|
|
|
if (status === '1') return 'processing'
|
|
|
if (status === '2') return 'resolved'
|
|
|
if (status === '3') return 'cancelled'
|
|
|
return ''
|
|
|
},
|
|
|
sourceTypeText(v) {
|
|
|
if (v === '0') return '工位'
|
|
|
if (v === '1') return '设备'
|
|
|
if (v === '2') return '报警'
|
|
|
if (v === '3') return '手动'
|
|
|
return String(v || '')
|
|
|
},
|
|
|
gridStyle(cols) {
|
|
|
const list = Array.isArray(cols) ? cols : []
|
|
|
const template = list
|
|
|
.map((c) => {
|
|
|
if (c && c.width) {
|
|
|
if (typeof c.width === 'number') return c.width + 'px'
|
|
|
return String(c.width)
|
|
|
}
|
|
|
return '1fr'
|
|
|
})
|
|
|
.join(' ')
|
|
|
|
|
|
return {
|
|
|
gridTemplateColumns: template || '1fr',
|
|
|
}
|
|
|
},
|
|
|
cellStyle(col) {
|
|
|
const style = {}
|
|
|
if (col && col.align) {
|
|
|
style.textAlign = col.align
|
|
|
}
|
|
|
return style
|
|
|
},
|
|
|
}
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="less">
|
|
|
.app-container {
|
|
|
background: #0f2740;
|
|
|
width: 100%;
|
|
|
height: 100%;
|
|
|
position: absolute;
|
|
|
top: 0;
|
|
|
left: 0;
|
|
|
padding: 4.2vw 2vw 1.2vw 2vw;
|
|
|
box-sizing: border-box;
|
|
|
color: #d6eaed;
|
|
|
overflow: hidden;
|
|
|
}
|
|
|
|
|
|
.headTitle {
|
|
|
position: absolute;
|
|
|
top: 5%;
|
|
|
left: 50%;
|
|
|
transform: translate(-50%, -100%);
|
|
|
font-size: 1.6vw;
|
|
|
letter-spacing: 0.3vw;
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
.subTitle {
|
|
|
position: absolute;
|
|
|
top: 8.1%;
|
|
|
left: 50%;
|
|
|
transform: translate(-50%, -100%);
|
|
|
font-size: 0.85vw;
|
|
|
color: #a9d5e8;
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
.split {
|
|
|
display: inline-block;
|
|
|
margin: 0 0.6vw;
|
|
|
color: rgba(255, 255, 255, 0.35);
|
|
|
}
|
|
|
|
|
|
.content {
|
|
|
height: 100%;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
}
|
|
|
|
|
|
.stats {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(5, 1fr);
|
|
|
gap: 1vw;
|
|
|
margin-bottom: 1.2vw;
|
|
|
}
|
|
|
|
|
|
.stat {
|
|
|
height: 5.6vw;
|
|
|
border-radius: 0.6vw;
|
|
|
padding: 0.8vw 1vw;
|
|
|
box-sizing: border-box;
|
|
|
background: rgba(0, 0, 0, 0.35);
|
|
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
justify-content: space-between;
|
|
|
}
|
|
|
|
|
|
.stat-label {
|
|
|
font-size: 0.95vw;
|
|
|
opacity: 0.9;
|
|
|
}
|
|
|
|
|
|
.stat-value {
|
|
|
font-size: 2.1vw;
|
|
|
font-weight: 700;
|
|
|
line-height: 1;
|
|
|
}
|
|
|
|
|
|
.stat-pending .stat-value {
|
|
|
color: #ff5f5f;
|
|
|
}
|
|
|
|
|
|
.stat-processing .stat-value {
|
|
|
color: #ffb020;
|
|
|
}
|
|
|
|
|
|
.stat-resolved .stat-value {
|
|
|
color: #16ceb9;
|
|
|
}
|
|
|
|
|
|
.stat-cancelled .stat-value {
|
|
|
color: #9aa3ad;
|
|
|
}
|
|
|
|
|
|
.stat-total .stat-value {
|
|
|
color: #ffffff;
|
|
|
}
|
|
|
|
|
|
.panels {
|
|
|
flex: 1;
|
|
|
display: grid;
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
gap: 1.2vw;
|
|
|
min-height: 0;
|
|
|
}
|
|
|
|
|
|
.panel {
|
|
|
background: rgba(0, 0, 0, 0.25);
|
|
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
|
border-radius: 0.6vw;
|
|
|
padding: 0.8vw;
|
|
|
box-sizing: border-box;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
min-height: 0;
|
|
|
}
|
|
|
|
|
|
.panel-title {
|
|
|
font-size: 1.05vw;
|
|
|
font-weight: 700;
|
|
|
margin-bottom: 0.6vw;
|
|
|
letter-spacing: 0.1vw;
|
|
|
}
|
|
|
|
|
|
.table {
|
|
|
flex: 1;
|
|
|
min-height: 0;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
}
|
|
|
|
|
|
.grid-header {
|
|
|
display: grid;
|
|
|
gap: 0;
|
|
|
background: rgba(9, 65, 112, 0.75);
|
|
|
border-radius: 0.4vw;
|
|
|
overflow: hidden;
|
|
|
}
|
|
|
|
|
|
.grid-body {
|
|
|
flex: 1;
|
|
|
min-height: 0;
|
|
|
overflow: auto;
|
|
|
margin-top: 0.4vw;
|
|
|
}
|
|
|
|
|
|
.grid-body::-webkit-scrollbar {
|
|
|
width: 0;
|
|
|
height: 0;
|
|
|
}
|
|
|
|
|
|
.grid-row {
|
|
|
display: grid;
|
|
|
background: rgba(3, 45, 87, 0.55);
|
|
|
}
|
|
|
|
|
|
.grid-row:nth-child(2n) {
|
|
|
background: rgba(5, 52, 96, 0.55);
|
|
|
}
|
|
|
|
|
|
.cell {
|
|
|
padding: 0.55vw 0.6vw;
|
|
|
font-size: 0.85vw;
|
|
|
box-sizing: border-box;
|
|
|
white-space: nowrap;
|
|
|
overflow: hidden;
|
|
|
text-overflow: ellipsis;
|
|
|
border-right: 1px solid rgba(255, 255, 255, 0.06);
|
|
|
}
|
|
|
|
|
|
.cell:last-child {
|
|
|
border-right: none;
|
|
|
}
|
|
|
|
|
|
.cell.header {
|
|
|
font-size: 0.85vw;
|
|
|
font-weight: 700;
|
|
|
color: #ffffff;
|
|
|
}
|
|
|
|
|
|
.status {
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.status.pending {
|
|
|
color: #ff5f5f;
|
|
|
}
|
|
|
|
|
|
.status.processing {
|
|
|
color: #ffb020;
|
|
|
}
|
|
|
|
|
|
.status.resolved {
|
|
|
color: #16ceb9;
|
|
|
}
|
|
|
|
|
|
.status.cancelled {
|
|
|
color: #9aa3ad;
|
|
|
}
|
|
|
|
|
|
.empty {
|
|
|
padding: 1vw;
|
|
|
text-align: center;
|
|
|
color: rgba(255, 255, 255, 0.55);
|
|
|
font-size: 0.9vw;
|
|
|
}
|
|
|
|
|
|
.error {
|
|
|
margin-top: 6vw;
|
|
|
padding: 1vw;
|
|
|
background: rgba(0, 0, 0, 0.45);
|
|
|
border: 1px solid rgba(255, 95, 95, 0.6);
|
|
|
border-radius: 0.6vw;
|
|
|
font-size: 1vw;
|
|
|
color: #ffbdbd;
|
|
|
}
|
|
|
|
|
|
.loading {
|
|
|
position: absolute;
|
|
|
top: 50%;
|
|
|
left: 50%;
|
|
|
transform: translate(-50%, -50%);
|
|
|
font-size: 1vw;
|
|
|
color: rgba(255, 255, 255, 0.8);
|
|
|
background: rgba(0, 0, 0, 0.35);
|
|
|
padding: 0.6vw 1.2vw;
|
|
|
border-radius: 0.4vw;
|
|
|
}
|
|
|
</style>
|