feat: 五项目业务实现并对接完成
- MES(B): 工单/BOM/备料/工序字典/PLC下发/拧紧/扫码报工/半成品/AGV/追溯逻辑,WMS与海康RCS客户端,看板Redis缓存API(概览/设备/进度/报警/趋势)+SSE - WMS(C): JWT滑动续签、Excel导入、盘点、内部API、种子数据、独立Postgres配置 - WMS客户端(E): Go网关8891反向代理+内嵌Vue3十页 - 工位终端(D): SQLite本地缓存+模拟拧紧源+内嵌Vue3页面 - Dashboard(A): 看板数据改接MES内部缓存API,SSE实时刷新+vite代理 - 清理各项目球形磨遗留代码,新增部署手册.md
This commit is contained in:
@@ -0,0 +1,698 @@
|
||||
<template>
|
||||
<div class="page" tabindex="-1">
|
||||
<!-- 顶部:标题 / 工位选择 / 登录人 / 同步状态角标 -->
|
||||
<header class="top-bar">
|
||||
<div class="brand">北京动力 · 工位终端</div>
|
||||
|
||||
<div class="dock-select">
|
||||
<span class="dock-lab">工位</span>
|
||||
<el-select v-model="dock" size="large" class="dock-el">
|
||||
<el-option v-for="d in docks" :key="d" :label="d" :value="d" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="spacer"></div>
|
||||
|
||||
<div class="sync-badge" @click="manualFlush">
|
||||
<el-tag v-if="pendingTotal > 0" type="warning" size="large" effect="dark" round>
|
||||
待同步 {{ pendingTotal }} 条
|
||||
</el-tag>
|
||||
<el-tag v-else type="success" size="large" effect="dark" round>同步正常</el-tag>
|
||||
</div>
|
||||
|
||||
<div class="user-box">
|
||||
<span class="user-name">{{ user.realName || user.username || '-' }}</span>
|
||||
<el-button size="large" text type="primary" @click="logout">退出</el-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- NG 告警横幅 -->
|
||||
<transition name="fade">
|
||||
<div v-if="hasNg" class="alert-banner">⚠ 存在NG,请复检!</div>
|
||||
</transition>
|
||||
|
||||
<!-- 扫码区(常驻聚焦) -->
|
||||
<section class="scan-bar">
|
||||
<span class="sn-label">当前工件 SN:</span>
|
||||
<span class="sn-value">{{ currentSn || '——' }}</span>
|
||||
<el-input
|
||||
ref="scanRef"
|
||||
v-model.trim="scanInput"
|
||||
class="scan-input"
|
||||
size="large"
|
||||
placeholder="请扫描工件SN条码"
|
||||
clearable
|
||||
@keyup.enter="onScan"
|
||||
/>
|
||||
<el-button size="large" @click="focusScan">聚焦扫码</el-button>
|
||||
</section>
|
||||
|
||||
<!-- 主内容:任务区 + 工艺PDF + 拧紧结果 -->
|
||||
<main class="content">
|
||||
<!-- 任务区 -->
|
||||
<aside class="card task-card">
|
||||
<h3 class="card-title">当前工单</h3>
|
||||
<template v-if="task">
|
||||
<div class="order-no">{{ task.orderNo || '-' }}</div>
|
||||
<div class="kv"><span>产品型号</span><b>{{ task.productModel || '-' }}</b></div>
|
||||
<div class="kv"><span>进度</span><b>{{ progressDone }} / {{ progressTotal }} 件</b></div>
|
||||
<ul class="steps">
|
||||
<li
|
||||
v-for="(s, i) in taskSteps"
|
||||
:key="i"
|
||||
:class="{ done: isStepDone(s) }"
|
||||
>
|
||||
<span class="step-flag">{{ isStepDone(s) ? '✔' : i + 1 }}</span>
|
||||
<span class="step-name">{{ s.name || s.processCode || ('工序' + (i + 1)) }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<el-empty v-else description="当前工位暂无任务" :image-size="90" />
|
||||
</aside>
|
||||
|
||||
<section class="right-col">
|
||||
<!-- 工艺 PDF 区 -->
|
||||
<div v-if="pdfFile" class="card pdf-card">
|
||||
<div class="pdf-head">
|
||||
<h3 class="card-title">工艺图纸 · {{ pdfFile }}</h3>
|
||||
<el-button type="primary" size="large" @click="openPdf">放大查看</el-button>
|
||||
</div>
|
||||
<iframe v-if="pdfUrl" :src="pdfUrl" class="pdf-frame" title="工艺图纸"></iframe>
|
||||
</div>
|
||||
|
||||
<!-- 拧紧结果区 -->
|
||||
<div class="card table-card" :class="{ 'table-card-fill': !pdfFile }">
|
||||
<h3 class="card-title">拧紧结果(实时)</h3>
|
||||
<el-table
|
||||
:data="tightRows"
|
||||
height="100%"
|
||||
size="large"
|
||||
:row-class-name="rowClass"
|
||||
empty-text="暂无拧紧数据"
|
||||
>
|
||||
<el-table-column label="时间" width="200">
|
||||
<template #default="{ row }">{{ fmtTs(row.createdAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="screwNo" label="螺丝" width="120" align="center" />
|
||||
<el-table-column label="扭矩 (Nm)" width="160" align="center">
|
||||
<template #default="{ row }">{{ row.torque.toFixed(1) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="角度 (°)" width="140" align="center">
|
||||
<template #default="{ row }">{{ Math.round(row.angle) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结果" align="center">
|
||||
<template #default="{ row }">
|
||||
<span :class="row.result === 'OK' ? 'res-ok' : 'res-ng'">{{ row.result }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- 底部三大按钮 -->
|
||||
<footer class="actions">
|
||||
<el-button class="big-btn primary" size="large" @click="onProcessDone">工序完成上报</el-button>
|
||||
<el-button class="big-btn warning" size="large" @click="onTempStore">暂存退库</el-button>
|
||||
<el-button class="big-btn success" size="large" @click="manualFlush">重试同步</el-button>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
getSyncStats,
|
||||
getTaskCurrent,
|
||||
getTighteningList,
|
||||
pdfOpenUrl,
|
||||
reportProcessDone,
|
||||
reportTempStore,
|
||||
syncFlush
|
||||
} from '../api'
|
||||
import { clearAuth, getUser } from '../utils/request'
|
||||
|
||||
const DOCK_KEY = 'ws_dock'
|
||||
|
||||
const router = useRouter()
|
||||
const user = getUser()
|
||||
|
||||
// ---------- 工位 ----------
|
||||
const dockCount = 21 // DOCK01 ~ DOCK21
|
||||
const docks = Array.from({ length: dockCount }, (_, i) => 'DOCK' + String(i + 1).padStart(2, '0'))
|
||||
const dock = ref(localStorage.getItem(DOCK_KEY) || 'DOCK01')
|
||||
watch(dock, (v) => {
|
||||
localStorage.setItem(DOCK_KEY, v)
|
||||
refreshAll()
|
||||
})
|
||||
|
||||
// ---------- 同步状态角标 ----------
|
||||
const stats = ref({ pendingTorque: 0, pendingQueue: 0 })
|
||||
const pendingTotal = computed(() => stats.value.pendingTorque + stats.value.pendingQueue)
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
stats.value = await getSyncStats()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// ---------- 当前任务 ----------
|
||||
const task = ref(null)
|
||||
const taskSteps = computed(() => {
|
||||
const t = task.value
|
||||
if (!t) return []
|
||||
return Array.isArray(t.steps) ? t.steps : Array.isArray(t.stepList) ? t.stepList : []
|
||||
})
|
||||
const progressDone = computed(() => {
|
||||
const p = task.value?.progress
|
||||
if (p && typeof p === 'object') return Number(p.done ?? 0)
|
||||
return Number(task.value?.doneCount ?? 0)
|
||||
})
|
||||
const progressTotal = computed(() => {
|
||||
const p = task.value?.progress
|
||||
if (p && typeof p === 'object') return Number(p.total ?? 0)
|
||||
return Number(task.value?.totalCount ?? 0)
|
||||
})
|
||||
|
||||
function isStepDone(s) {
|
||||
return s && (s.done === true || s.status === 'done' || s.finished === true)
|
||||
}
|
||||
|
||||
function firstPendingStepNo() {
|
||||
for (let i = 0; i < taskSteps.value.length; i++) {
|
||||
if (!isStepDone(taskSteps.value[i])) {
|
||||
const s = taskSteps.value[i]
|
||||
return String(s.no ?? s.code ?? i + 1)
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
async function loadTask() {
|
||||
try {
|
||||
task.value = await getTaskCurrent(dock.value)
|
||||
} catch {
|
||||
task.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 工艺 PDF ----------
|
||||
const pdfFile = computed(() => {
|
||||
const f = task.value?.processFile || task.value?.processPdf
|
||||
return typeof f === 'string' && f.trim() !== '' ? f.trim() : ''
|
||||
})
|
||||
const pdfUrl = computed(() => (pdfFile.value ? pdfOpenUrl(pdfFile.value) : ''))
|
||||
|
||||
function openPdf() {
|
||||
if (pdfUrl.value) window.open(pdfUrl.value, '_blank')
|
||||
}
|
||||
|
||||
// ---------- 扫码 ----------
|
||||
const scanRef = ref()
|
||||
const scanInput = ref('')
|
||||
const currentSn = ref('')
|
||||
|
||||
function focusScan() {
|
||||
scanRef.value?.focus?.()
|
||||
}
|
||||
|
||||
function onScan() {
|
||||
const sn = scanInput.value.trim()
|
||||
if (!sn) return
|
||||
currentSn.value = sn
|
||||
scanInput.value = ''
|
||||
loadTight()
|
||||
}
|
||||
|
||||
watch(currentSn, () => loadTight())
|
||||
|
||||
// ---------- 拧紧结果 ----------
|
||||
const tightRows = ref([])
|
||||
const hasNg = computed(() => tightRows.value.some((r) => r.result === 'NG'))
|
||||
|
||||
async function loadTight() {
|
||||
try {
|
||||
const data = await getTighteningList({ sn: currentSn.value, limit: 50 })
|
||||
tightRows.value = Array.isArray(data?.list) ? data.list : []
|
||||
} catch {
|
||||
/* 轮询失败静默,避免刷屏 */
|
||||
}
|
||||
}
|
||||
|
||||
function rowClass({ row }) {
|
||||
return row.result === 'NG' ? 'ng-row' : ''
|
||||
}
|
||||
|
||||
function fmtTs(ts) {
|
||||
if (!ts) return '-'
|
||||
const d = new Date(Number(ts))
|
||||
const p = (n) => String(n).padStart(2, '0')
|
||||
return `${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
|
||||
}
|
||||
|
||||
// ---------- 底部按钮 ----------
|
||||
async function onProcessDone() {
|
||||
focusScan()
|
||||
try {
|
||||
let value
|
||||
try {
|
||||
const res = await ElMessageBox.prompt(
|
||||
`确认对工位 ${dock.value} 执行「工序完成上报」?请输入当前工序号`,
|
||||
'工序完成上报',
|
||||
{
|
||||
inputValue: firstPendingStepNo(),
|
||||
inputPlaceholder: '如:OP10',
|
||||
confirmButtonText: '确 认',
|
||||
cancelButtonText: '取 消',
|
||||
inputValidator: (v) => (v && String(v).trim() !== '' ? true : '请输入工序号'),
|
||||
distinguishCancelAndClose: true
|
||||
}
|
||||
)
|
||||
value = String(res.value || '').trim()
|
||||
} catch (e) {
|
||||
return // 用户取消/关闭
|
||||
}
|
||||
|
||||
const data = await reportProcessDone({
|
||||
dockCode: dock.value,
|
||||
orderNo: task.value?.orderNo || '',
|
||||
sn: currentSn.value,
|
||||
processCode: value,
|
||||
operator: user.realName || user.username || ''
|
||||
})
|
||||
if (data?.queued) {
|
||||
ElMessage.warning('MES 暂不可达,已离线缓存,稍后自动重传')
|
||||
} else {
|
||||
ElMessage.success('工序完成上报成功')
|
||||
}
|
||||
loadTask()
|
||||
loadStats()
|
||||
} catch (e) {
|
||||
/* 统一错误提示已在 request 中弹出 */
|
||||
}
|
||||
}
|
||||
|
||||
async function onTempStore() {
|
||||
focusScan()
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认将当前工件「${currentSn.value || '未扫码'}」暂存退库?`,
|
||||
'暂存退库',
|
||||
{ confirmButtonText: '确认暂存', cancelButtonText: '取消', type: 'warning' }
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const data = await reportTempStore({
|
||||
dockCode: dock.value,
|
||||
orderNo: task.value?.orderNo || '',
|
||||
sn: currentSn.value,
|
||||
operator: user.realName || user.username || ''
|
||||
})
|
||||
if (data?.queued) {
|
||||
ElMessage.warning('MES 暂不可达,已离线缓存,稍后自动重传')
|
||||
} else {
|
||||
ElMessage.success('暂存退库已上报')
|
||||
}
|
||||
loadTask()
|
||||
loadStats()
|
||||
} catch (e) {
|
||||
/* 统一错误提示 */
|
||||
}
|
||||
}
|
||||
|
||||
async function manualFlush() {
|
||||
try {
|
||||
const data = await syncFlush()
|
||||
ElMessage.success(`本轮同步成功 ${data?.count ?? 0} 条`)
|
||||
loadStats()
|
||||
} catch (e) {
|
||||
/* 统一错误提示 */
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 轮询与生命周期 ----------
|
||||
let timerTask = null
|
||||
let timerTight = null
|
||||
let timerStats = null
|
||||
|
||||
function refreshAll() {
|
||||
loadTask()
|
||||
loadTight()
|
||||
loadStats()
|
||||
}
|
||||
|
||||
function logout() {
|
||||
clearAuth()
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
refreshAll()
|
||||
focusScan()
|
||||
window.addEventListener('focus', focusScan)
|
||||
|
||||
timerTask = setInterval(loadTask, 5000) // 任务 5 秒
|
||||
timerTight = setInterval(loadTight, 2000) // 拧紧结果 2 秒
|
||||
timerStats = setInterval(loadStats, 10000) // 同步统计 10 秒
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(timerTask)
|
||||
clearInterval(timerTight)
|
||||
clearInterval(timerStats)
|
||||
window.removeEventListener('focus', focusScan)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 触屏终端:大字号、高对比、无滚动闪烁 */
|
||||
.page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #eef2f7;
|
||||
overflow: hidden;
|
||||
font-family: 'Microsoft YaHei', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
/* ---- 顶部 ---- */
|
||||
.top-bar {
|
||||
height: 76px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
padding: 0 28px;
|
||||
background: #16324f;
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.brand {
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 2px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dock-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dock-lab {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.dock-el {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.dock-el :deep(.el-input__inner) {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sync-badge {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sync-badge :deep(.el-tag) {
|
||||
font-size: 18px;
|
||||
height: 40px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.user-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.user-box :deep(.el-button) {
|
||||
font-size: 18px;
|
||||
color: #9fd0ff;
|
||||
}
|
||||
|
||||
/* ---- NG 告警横幅 ---- */
|
||||
.alert-banner {
|
||||
flex-shrink: 0;
|
||||
background: #d80f16;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-size: 34px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 8px;
|
||||
padding: 14px 0;
|
||||
animation: blink 1.2s infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.55; }
|
||||
}
|
||||
|
||||
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s; }
|
||||
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
||||
|
||||
/* ---- 扫码区 ---- */
|
||||
.scan-bar {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
padding: 12px 28px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #dde4ee;
|
||||
}
|
||||
|
||||
.sn-label {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: #16324f;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sn-value {
|
||||
min-width: 220px;
|
||||
font-size: 26px;
|
||||
font-weight: bold;
|
||||
color: #d80f16;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.scan-input {
|
||||
flex: 1;
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
.scan-bar :deep(.el-input__inner) {
|
||||
height: 52px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
/* ---- 主内容 ---- */
|
||||
.content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
padding: 20px 28px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 2px 10px rgba(22, 50, 79, 0.08);
|
||||
padding: 20px 24px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin: 0 0 16px;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #16324f;
|
||||
}
|
||||
|
||||
.task-card {
|
||||
width: 360px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.order-no {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #16324f;
|
||||
word-break: break-all;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.kv {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 22px;
|
||||
margin-bottom: 10px;
|
||||
color: #5a6a80;
|
||||
}
|
||||
|
||||
.kv b {
|
||||
color: #16324f;
|
||||
}
|
||||
|
||||
.steps {
|
||||
list-style: none;
|
||||
margin: 14px 0 0;
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.steps li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 20px;
|
||||
padding: 9px 4px;
|
||||
border-bottom: 1px dashed #e6ecf4;
|
||||
color: #16324f;
|
||||
}
|
||||
|
||||
.steps li.done {
|
||||
color: #2aa861;
|
||||
}
|
||||
|
||||
.steps li.done .step-name {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.step-flag {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #eaf1f8;
|
||||
font-weight: bold;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.steps li.done .step-flag {
|
||||
background: #2aa861;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.right-col {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.pdf-card {
|
||||
height: 38%;
|
||||
min-height: 240px;
|
||||
}
|
||||
|
||||
.pdf-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.pdf-head .card-title {
|
||||
margin-bottom: 8px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.pdf-frame {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
border: 1px solid #dde4ee;
|
||||
border-radius: 8px;
|
||||
background: #fafbfd;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
flex: 1;
|
||||
min-height: 220px;
|
||||
}
|
||||
|
||||
.table-card :deep(.el-table) {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.table-card :deep(.el-table th.el-table__cell) {
|
||||
font-size: 20px;
|
||||
color: #16324f;
|
||||
}
|
||||
|
||||
.res-ok {
|
||||
color: #2aa861;
|
||||
font-size: 26px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.res-ng {
|
||||
color: #d80f16;
|
||||
font-size: 26px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
:deep(.ng-row) {
|
||||
--el-table-tr-bg-color: #ffecec;
|
||||
background: #ffecec !important;
|
||||
}
|
||||
|
||||
/* ---- 底部三大按钮 ---- */
|
||||
.actions {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 48px;
|
||||
padding: 14px 0 18px;
|
||||
background: #fff;
|
||||
box-shadow: 0 -2px 10px rgba(22, 50, 79, 0.06);
|
||||
}
|
||||
|
||||
.big-btn {
|
||||
width: 320px;
|
||||
height: 72px;
|
||||
font-size: 27px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 6px;
|
||||
border-radius: 12px;
|
||||
color: #fff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.big-btn.primary { background: #1668dc; }
|
||||
.big-btn.primary:hover { background: #0e57c3; }
|
||||
|
||||
.big-btn.warning { background: #e6870f; }
|
||||
.big-btn.warning:hover { background: #cf7508; }
|
||||
|
||||
.big-btn.success { background: #2aa861; }
|
||||
.big-btn.success:hover { background: #239154; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user