feat(material): 去Tab分段展示 + 总图段位标签带 + 跟踪表段列 / 移除看板与产能分析
- 移除 Tab 切换,5 段(入口/酸洗/清洗/烘干/出口)顺序堆叠 - 总图 SVG 顶部加段位色带(5 段不同色),同时跟踪表新增「段」列 - 修复横向溢出(minmax(0,1fr) + auto-fit 槽卡片 + overflow-x:hidden) - 删除菜单与路由中的「生产看板」「产能分析」,首页重定向到 /plan Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
188
README.md
188
README.md
@@ -1,188 +0,0 @@
|
||||
# 推拉酸洗线 L2 过程控制系统
|
||||
|
||||
基于 FastAPI + Vue 2 的推拉酸洗线二级过程控制系统,实现物料跟踪、实绩管理、计划管理、停机管理、设备管理、工艺预测等功能。
|
||||
|
||||
---
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 层 | 技术 |
|
||||
|----|------|
|
||||
| 后端 | Python 3.11+,FastAPI,SQLAlchemy 2(async),asyncpg |
|
||||
| 数据库 | PostgreSQL 16 |
|
||||
| 缓存 | Redis 7 |
|
||||
| 前端 | Vue 2.7,Element UI,Axios |
|
||||
| L1通信 | UDP(asyncio DatagramProtocol),监听 9000 端口 |
|
||||
|
||||
---
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
pickling-mes/
|
||||
├── backend/
|
||||
│ ├── app/
|
||||
│ │ ├── api/ # 路由(各模块接口)
|
||||
│ │ ├── models/ # SQLAlchemy 数据模型
|
||||
│ │ ├── services/ # 业务逻辑(UDP解析、预测模型)
|
||||
│ │ ├── config.py # 配置(环境变量)
|
||||
│ │ ├── database.py # 数据库连接
|
||||
│ │ └── main.py # 入口
|
||||
│ ├── tests/
|
||||
│ │ └── test_udp_sender.py # UDP报文模拟测试工具
|
||||
│ └── requirements.txt
|
||||
├── frontend/
|
||||
│ ├── src/
|
||||
│ │ ├── views/ # 页面组件
|
||||
│ │ ├── api/ # 接口调用
|
||||
│ │ ├── store/ # Vuex(认证)
|
||||
│ │ └── router/ # 路由
|
||||
│ └── package.json
|
||||
├── docs/ # 接口文档
|
||||
└── docker-compose.yml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 启动方式
|
||||
|
||||
### 方式一:Docker Compose(推荐)
|
||||
|
||||
**前置条件**:已安装 Docker Desktop
|
||||
|
||||
```bash
|
||||
cd pickling-mes
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
启动后访问:
|
||||
- 前端:http://localhost:8080
|
||||
- 后端 API 文档:http://localhost:8000/docs
|
||||
- 默认账号:`admin` / `admin123`
|
||||
|
||||
停止:
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 方式二:本地开发启动
|
||||
|
||||
**前置条件**:Python 3.11+,Node.js 18+,PostgreSQL 16,Redis 7
|
||||
|
||||
#### 1. 准备数据库
|
||||
|
||||
```bash
|
||||
# 启动 PostgreSQL 和 Redis(也可用 Docker 只起这两个服务)
|
||||
docker-compose up -d postgres redis
|
||||
```
|
||||
|
||||
#### 2. 启动后端
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 配置环境变量(可选,默认值已可用于本地开发)
|
||||
cp .env.example .env # 如有需要修改数据库连接
|
||||
|
||||
# 启动
|
||||
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
后端启动时会自动:
|
||||
- 建表(init_db)
|
||||
- 创建默认 admin 账号(admin / admin123)
|
||||
- 启动 UDP 监听服务(0.0.0.0:9000)
|
||||
|
||||
#### 3. 启动前端
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 开发模式启动
|
||||
npm run serve
|
||||
```
|
||||
|
||||
访问 http://localhost:8080
|
||||
|
||||
---
|
||||
|
||||
## 环境变量说明
|
||||
|
||||
在 `backend/.env` 中配置(不存在则使用下列默认值):
|
||||
|
||||
| 变量 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `DATABASE_URL` | `postgresql+asyncpg://postgres:password@localhost:5432/pickling_mes` | 数据库连接(async) |
|
||||
| `DATABASE_SYNC_URL` | `postgresql://postgres:password@localhost:5432/pickling_mes` | 数据库连接(同步,Alembic用)|
|
||||
| `REDIS_URL` | `redis://localhost:6379/0` | Redis 连接 |
|
||||
| `SECRET_KEY` | `dev-secret-key` | JWT 签名密钥,**生产环境必须修改** |
|
||||
| `L1_HOST` | `0.0.0.0` | UDP 监听地址 |
|
||||
| `L1_PORT` | `9000` | UDP 监听端口(L1 PLC 向此端口发送报文)|
|
||||
| `ACCESS_TOKEN_EXPIRE_MINUTES` | `480` | Token 有效期(分钟)|
|
||||
|
||||
---
|
||||
|
||||
## L1 通信(UDP)
|
||||
|
||||
系统启动后自动监听 UDP `0.0.0.0:9000`,接收 L1 PLC 推送的报文。
|
||||
|
||||
**已实现的报文类型**(Body 格式待 PLC 方协议文档确认后适配):
|
||||
|
||||
| 报文 ID | 含义 | 触发时机 |
|
||||
|---------|------|---------|
|
||||
| PC01 | 卷材入口 | 带钢上线时 |
|
||||
| PC02 | 卷材出口 | 带钢下线时 |
|
||||
| PC03 | 过程数据 | 周期推送(2s)|
|
||||
| PC04 | 质量缺陷 | 缺陷检出时 |
|
||||
| PC05 | 设备状态 | 状态变化时 |
|
||||
| PC20 | 心跳 | 每 10s |
|
||||
|
||||
**模拟测试**(无 PLC 时验证 UDP 通信):
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
python tests/test_udp_sender.py
|
||||
# 默认向 127.0.0.1:9000 发送 PC20/PC01/PC03/PC02 测试帧
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 预测模型
|
||||
|
||||
后端内置 4 个基于物理公式的工艺预测模型,无需训练数据即可运行:
|
||||
|
||||
| 模型 | 接口 | 说明 |
|
||||
|------|------|------|
|
||||
| 酸洗速度 | `POST /prediction/acid-speed` | 基于 Arrhenius 动力学,输出最大允许速度 |
|
||||
| 张力设定 | `POST /prediction/tension` | 基于截面积×屈服强度,输出各区张力 |
|
||||
| 质量预测 | `POST /prediction/quality` | 输出质量等级(A1~C)及改进建议 |
|
||||
| 消耗预测 | `POST /prediction/consumption` | 输出单卷酸、蒸汽、电、水消耗量 |
|
||||
|
||||
接口参数详见:http://localhost:8000/docs
|
||||
|
||||
---
|
||||
|
||||
## 主要功能模块
|
||||
|
||||
| 路径 | 模块 |
|
||||
|------|------|
|
||||
| `/dashboard` | 生产看板(实时指标、趋势图)|
|
||||
| `/material` | 物料跟踪(卷材全流程跟踪)|
|
||||
| `/production` | 实绩管理 |
|
||||
| `/plan` | 计划管理 |
|
||||
| `/downtime` | 停机管理 |
|
||||
| `/equipment` | 设备管理 |
|
||||
| `/message` | 报文监控(UDP收发日志)|
|
||||
| `/process-model` | 工艺段模型(酸洗速度预测)|
|
||||
| `/tension-model` | 张力设定 |
|
||||
| `/pdi` | PDI 管理(L3 下发→L2 确认)|
|
||||
| `/quality` | 质量管理 |
|
||||
| `/capacity` | 产能分析 |
|
||||
BIN
docs/.DS_Store
vendored
Normal file
BIN
docs/.DS_Store
vendored
Normal file
Binary file not shown.
Binary file not shown.
@@ -14,14 +14,8 @@ const routes = [
|
||||
path: '/',
|
||||
component: () => import('@/views/Layout.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
redirect: '/dashboard',
|
||||
redirect: '/plan',
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('@/views/Dashboard.vue'),
|
||||
meta: { title: '生产看板', icon: 'el-icon-monitor', requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'material',
|
||||
name: 'Material',
|
||||
@@ -70,12 +64,6 @@ const routes = [
|
||||
component: () => import('@/views/Quality.vue'),
|
||||
meta: { title: '质量管理', icon: 'el-icon-medal', requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'capacity',
|
||||
name: 'Capacity',
|
||||
component: () => import('@/views/Capacity.vue'),
|
||||
meta: { title: '产能分析', icon: 'el-icon-s-data', requiresAuth: true }
|
||||
},
|
||||
]
|
||||
},
|
||||
{ path: '*', redirect: '/' }
|
||||
|
||||
@@ -70,7 +70,6 @@ const IC = {
|
||||
}
|
||||
|
||||
const MENU = [
|
||||
{ path: '/dashboard', title: '生产看板', icon: IC.dashboard },
|
||||
{ path: '/plan', title: '计划管理', icon: IC.plan },
|
||||
{ path: '/material', title: '物料跟踪', icon: IC.material },
|
||||
{ path: '/production', title: '实绩管理', icon: IC.production },
|
||||
@@ -79,7 +78,6 @@ const MENU = [
|
||||
{ path: '/downtime', title: '停机管理', icon: IC.downtime },
|
||||
{ path: '/inspection', title: '设备巡检', icon: IC.inspection },
|
||||
{ path: '/quality', title: '质量管理', icon: IC.quality },
|
||||
{ path: '/capacity', title: '产能分析', icon: IC.capacity },
|
||||
]
|
||||
|
||||
export default {
|
||||
|
||||
@@ -36,8 +36,16 @@
|
||||
<div class="line-wrap card">
|
||||
<div class="card-header">推拉酸洗线 - 物料跟踪总图</div>
|
||||
<div class="line-body">
|
||||
<svg viewBox="0 0 1900 280" preserveAspectRatio="xMidYMid meet" class="line-svg">
|
||||
<rect x="0" y="0" width="1900" height="280" fill="#0a1218" />
|
||||
<svg viewBox="0 -32 1900 312" preserveAspectRatio="xMidYMid meet" class="line-svg">
|
||||
<rect x="0" y="-32" width="1900" height="312" fill="#0a1218" />
|
||||
|
||||
<!-- 段位标签带 -->
|
||||
<g v-for="s in sections" :key="'sec-'+s.name">
|
||||
<rect :x="s.bandX" y="-28" :width="s.bandW" height="22" :fill="s.color" opacity="0.18" rx="3"/>
|
||||
<rect :x="s.bandX" y="-28" :width="s.bandW" height="22" fill="none" :stroke="s.color" stroke-width="1" opacity="0.7" rx="3"/>
|
||||
<text :x="s.labelX" y="-13" text-anchor="middle" font-size="12" font-weight="bold" :fill="s.color"
|
||||
font-family="Arial,sans-serif">{{ s.name }}</text>
|
||||
</g>
|
||||
|
||||
<!-- 顶部标签 -->
|
||||
<g v-for="eq in equipments" :key="'lab-'+eq.k" font-family="Arial,sans-serif">
|
||||
@@ -232,19 +240,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 下半: 分段 Tab -->
|
||||
<div class="card sec-card">
|
||||
<div class="tab-bar">
|
||||
<div v-for="t in tabs" :key="t.k" :class="['tab', { active: tab === t.k }]" @click="tab = t.k">
|
||||
{{ t.label }}
|
||||
<span class="tab-cnt">{{ t.count }}</span>
|
||||
</div>
|
||||
<div class="tab-spacer"></div>
|
||||
<span class="hd-cnt">{{ rtItems.length }} 项实时数据</span>
|
||||
<!-- 入口段 -->
|
||||
<section class="sec">
|
||||
<div class="sec-title-bar">入口段
|
||||
<span class="ch-badge">在线 {{ onlinePlans.length }} / 生产中 {{ producingPlan ? 1 : 0 }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 入口段 -->
|
||||
<div v-if="tab === 'entry'" class="pane">
|
||||
<div class="pane">
|
||||
<div class="pane-grid entry-grid">
|
||||
<!-- 左:在线计划 + 移动按钮 -->
|
||||
<div class="sub-card">
|
||||
@@ -291,8 +292,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 酸洗段 -->
|
||||
<div v-if="tab === 'acid'" class="pane">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 酸洗段 -->
|
||||
<section class="sec">
|
||||
<div class="sec-title-bar">酸洗段(5 槽)</div>
|
||||
<div class="pane">
|
||||
<div class="tank-grid">
|
||||
<div v-for="(a, i) in acid" :key="'a'+i" class="sub-card">
|
||||
<div class="sub-header acid">{{ i+1 }}# 酸洗槽</div>
|
||||
@@ -323,8 +329,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 漂洗段 -->
|
||||
<div v-if="tab === 'rinse'" class="pane">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 漂洗段 -->
|
||||
<section class="sec">
|
||||
<div class="sec-title-bar">漂洗段(5 级)+ 烘干</div>
|
||||
<div class="pane">
|
||||
<div class="tank-grid">
|
||||
<div v-for="(r, i) in rinse" :key="'r'+i" class="sub-card">
|
||||
<div class="sub-header rinse">{{ i+1 }}# 漂洗</div>
|
||||
@@ -358,8 +369,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 出口段 -->
|
||||
<div v-if="tab === 'exit'" class="pane">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 出口段 -->
|
||||
<section class="sec">
|
||||
<div class="sec-title-bar">出口段</div>
|
||||
<div class="pane">
|
||||
<div class="pane-grid two">
|
||||
<div class="sub-card">
|
||||
<div class="sub-header">三辊张力装置</div>
|
||||
@@ -393,12 +409,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 跟踪表 -->
|
||||
<div v-if="tab === 'track'" class="pane">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 跟踪表 -->
|
||||
<section class="sec">
|
||||
<div class="sec-title-bar">物料跟踪表 <span class="hd-cnt">{{ equipments.length }} 台设备</span></div>
|
||||
<div class="pane">
|
||||
<table class="data-table compact tracking-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:32px;">#</th>
|
||||
<th style="width:72px;">段</th>
|
||||
<th>设备</th>
|
||||
<th style="width:64px;">状态</th>
|
||||
<th>当前钢卷</th>
|
||||
@@ -411,6 +433,11 @@
|
||||
<tr v-for="(eq, i) in equipments" :key="eq.k"
|
||||
:class="{ 'row-active': eq.k === currentEquipment.k, 'row-passed': i < currentEquipment.idx, 'row-pending': i > currentEquipment.idx }">
|
||||
<td class="td-num">{{ i + 1 }}</td>
|
||||
<td>
|
||||
<span class="sec-tag" :style="{ color: sectionColor(eq.section), borderColor: sectionColor(eq.section) }">
|
||||
{{ eq.section }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ eq.label }}</td>
|
||||
<td>
|
||||
<span v-if="eq.k === currentEquipment.k" class="badge badge-yellow">加工中</span>
|
||||
@@ -425,7 +452,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -435,27 +462,35 @@ function rnd(base, amp) { return base + (Math.random() - 0.5) * amp }
|
||||
function fix(v, n = 1) { return Number(v).toFixed(n) }
|
||||
|
||||
const EQUIPMENTS = [
|
||||
{ k:'uncoiler', label:'开卷机', type:'coiler', code:'DC-1' },
|
||||
{ k:'straightener', label:'九辊矫直机', type:'rolls9', code:'STR-9' },
|
||||
{ k:'crop_shear', label:'切头剪', type:'shear', code:'CRP' },
|
||||
{ k:'acid1', label:'酸洗槽1', type:'acid', idx:0 },
|
||||
{ k:'acid2', label:'酸洗槽2', type:'acid', idx:1 },
|
||||
{ k:'acid3', label:'酸洗槽3', type:'acid', idx:2 },
|
||||
{ k:'acid4', label:'酸洗槽4', type:'acid', idx:3 },
|
||||
{ k:'acid5', label:'酸洗槽5', type:'acid', idx:4 },
|
||||
{ k:'rinse', label:'漂洗段', type:'rinse' },
|
||||
{ k:'dryer', label:'热风烘干段', type:'dryer' },
|
||||
{ k:'br1', label:'1号夹送辊', type:'pinch', code:'BR-1' },
|
||||
{ k:'loop', label:'活套坑', type:'loop' },
|
||||
{ k:'br2', label:'2号夹送辊', type:'pinch', code:'BR-2' },
|
||||
{ k:'br3', label:'3号夹送辊', type:'pinch', code:'BR-3' },
|
||||
{ k:'tension', label:'三辊张力装置', type:'tension3', code:'TEN-3' },
|
||||
{ k:'leveler', label:'平整机', type:'leveler', code:'SPM' },
|
||||
{ k:'tail_shear', label:'切尾剪', type:'shear', code:'TLS' },
|
||||
{ k:'oiler', label:'静电涂油机', type:'oiler', code:'EOL' },
|
||||
{ k:'recoiler', label:'卷取机', type:'recoiler', code:'REC-1' },
|
||||
{ k:'uncoiler', label:'开卷机', type:'coiler', code:'DC-1', section:'入口段' },
|
||||
{ k:'straightener', label:'九辊矫直机', type:'rolls9', code:'STR-9', section:'入口段' },
|
||||
{ k:'crop_shear', label:'切头剪', type:'shear', code:'CRP', section:'入口段' },
|
||||
{ k:'acid1', label:'酸洗槽1', type:'acid', idx:0, section:'酸洗段' },
|
||||
{ k:'acid2', label:'酸洗槽2', type:'acid', idx:1, section:'酸洗段' },
|
||||
{ k:'acid3', label:'酸洗槽3', type:'acid', idx:2, section:'酸洗段' },
|
||||
{ k:'acid4', label:'酸洗槽4', type:'acid', idx:3, section:'酸洗段' },
|
||||
{ k:'acid5', label:'酸洗槽5', type:'acid', idx:4, section:'酸洗段' },
|
||||
{ k:'rinse', label:'漂洗段', type:'rinse', section:'清洗段' },
|
||||
{ k:'dryer', label:'热风烘干段', type:'dryer', section:'烘干段' },
|
||||
{ k:'br1', label:'1号夹送辊', type:'pinch', code:'BR-1', section:'出口段' },
|
||||
{ k:'loop', label:'活套坑', type:'loop', section:'出口段' },
|
||||
{ k:'br2', label:'2号夹送辊', type:'pinch', code:'BR-2', section:'出口段' },
|
||||
{ k:'br3', label:'3号夹送辊', type:'pinch', code:'BR-3', section:'出口段' },
|
||||
{ k:'tension', label:'三辊张力装置', type:'tension3', code:'TEN-3', section:'出口段' },
|
||||
{ k:'leveler', label:'平整机', type:'leveler', code:'SPM', section:'出口段' },
|
||||
{ k:'tail_shear', label:'切尾剪', type:'shear', code:'TLS', section:'出口段' },
|
||||
{ k:'oiler', label:'静电涂油机', type:'oiler', code:'EOL', section:'出口段' },
|
||||
{ k:'recoiler', label:'卷取机', type:'recoiler', code:'REC-1', section:'出口段' },
|
||||
]
|
||||
|
||||
const SECTION_COLORS = {
|
||||
'入口段': '#5a8fc8',
|
||||
'酸洗段': '#ffaa44',
|
||||
'清洗段': '#3aa0c8',
|
||||
'烘干段': '#e87a3a',
|
||||
'出口段': '#88c070',
|
||||
}
|
||||
|
||||
// 默认辊缝值 (mm)
|
||||
const DEFAULT_GAP = {
|
||||
straightener: 4.20,
|
||||
@@ -514,19 +549,9 @@ export default {
|
||||
_plansTimer: null,
|
||||
plans: [],
|
||||
moving: false,
|
||||
tab: 'entry',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
tabs() {
|
||||
return [
|
||||
{ k: 'entry', label: '入口段', count: this.onlinePlans.length },
|
||||
{ k: 'acid', label: '酸洗段', count: 5 },
|
||||
{ k: 'rinse', label: '漂洗段', count: 5 },
|
||||
{ k: 'exit', label: '出口段', count: 3 },
|
||||
{ k: 'track', label: '物料跟踪表', count: this.equipments.length },
|
||||
]
|
||||
},
|
||||
entryItems() {
|
||||
const f = (v, n=1) => Number(v).toFixed(n)
|
||||
return [
|
||||
@@ -554,6 +579,29 @@ export default {
|
||||
const step = (xEnd - xStart) / (n - 1)
|
||||
return EQUIPMENTS.map((e, i) => ({ ...e, x: xStart + step * i }))
|
||||
},
|
||||
sections() {
|
||||
const eqs = this.equipments
|
||||
const groups = []
|
||||
let cur = null
|
||||
eqs.forEach((e, i) => {
|
||||
if (!cur || cur.name !== e.section) {
|
||||
if (cur) groups.push(cur)
|
||||
cur = { name: e.section, color: SECTION_COLORS[e.section] || '#9aa8b6',
|
||||
startIdx: i, endIdx: i, x0: e.x, x1: e.x }
|
||||
} else {
|
||||
cur.endIdx = i
|
||||
cur.x1 = e.x
|
||||
}
|
||||
})
|
||||
if (cur) groups.push(cur)
|
||||
const half = (eqs[1].x - eqs[0].x) / 2
|
||||
return groups.map(g => ({
|
||||
...g,
|
||||
bandX: g.x0 - half + 4,
|
||||
bandW: (g.x1 - g.x0) + half * 2 - 8,
|
||||
labelX: (g.x0 + g.x1) / 2,
|
||||
}))
|
||||
},
|
||||
weldX() {
|
||||
const p = Math.max(0, Math.min(1, this.weld.position))
|
||||
return 50 + (1850 - 50) * p
|
||||
@@ -635,6 +683,7 @@ export default {
|
||||
methods: {
|
||||
fmt(v, n = 2) { return v != null && v !== '' ? Number(v).toFixed(n) : '—' },
|
||||
fix(v, n = 1) { return Number(v).toFixed(n) },
|
||||
sectionColor(s) { return SECTION_COLORS[s] || '#9aa8b6' },
|
||||
fmtTime(t) { return t ? t.slice(0, 16).replace('T', ' ') : '—' },
|
||||
async loadPlans() {
|
||||
try {
|
||||
@@ -777,7 +826,12 @@ export default {
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/styles/variables';
|
||||
|
||||
.mat-page { display: flex; flex-direction: column; gap: 10px; }
|
||||
.mat-page { display: flex; flex-direction: column; gap: 10px; min-width: 0; overflow-x: hidden; }
|
||||
.sec-tag {
|
||||
display: inline-block; font-size: 10.5px; padding: 1px 6px;
|
||||
border: 1px solid; border-radius: 3px; background: rgba(0,0,0,.25);
|
||||
font-weight: 600; letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
display: flex; align-items: center; gap: 18px; flex-wrap: wrap;
|
||||
@@ -791,34 +845,28 @@ export default {
|
||||
|
||||
.line-wrap { padding: 0; }
|
||||
.line-body { padding: 6px 10px 10px; background: #0a1218; }
|
||||
.line-svg { width: 100%; height: 280px; display: block; }
|
||||
.line-svg { width: 100%; height: 312px; display: block; }
|
||||
|
||||
.sec-card { padding: 0; }
|
||||
.tab-bar {
|
||||
display: flex; align-items: center; gap: 2px;
|
||||
border-bottom: 1px solid $border; padding: 0 10px;
|
||||
background: #161d24;
|
||||
.sec {
|
||||
background: $bg-card; border: 1px solid $border; border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.tab {
|
||||
padding: 9px 16px; font-size: 12.5px; cursor: pointer;
|
||||
color: $text-muted; border-bottom: 2px solid transparent;
|
||||
user-select: none;
|
||||
.sec-title-bar {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 7px 12px; font-size: 13px; color: #c8d4e0; font-weight: 600;
|
||||
background: #161d24; border-bottom: 1px solid $border;
|
||||
}
|
||||
.tab:hover { color: #c8d4e0; }
|
||||
.tab.active { color: $sms-highlight; border-bottom-color: $sms-highlight; font-weight: 600; }
|
||||
.tab-cnt {
|
||||
display: inline-block; margin-left: 5px;
|
||||
background: rgba(255,255,255,.06); border-radius: 9px;
|
||||
padding: 0 6px; font-size: 10px; color: #9aa8b6;
|
||||
}
|
||||
.tab.active .tab-cnt { background: rgba(0,200,255,.15); color: #00c8ff; }
|
||||
.tab-spacer { flex: 1; }
|
||||
|
||||
.pane { padding: 10px 12px; }
|
||||
.pane-grid { display: grid; grid-template-columns: 1fr 1.4fr; gap: 10px; }
|
||||
.pane-grid.two { grid-template-columns: 1fr 1fr; }
|
||||
.entry-grid { grid-template-columns: 1fr 1.3fr; }
|
||||
.tank-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 8px; }
|
||||
.pane-grid {
|
||||
display: grid; gap: 10px;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1.3fr);
|
||||
}
|
||||
.pane-grid.two { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
|
||||
.entry-grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1.3fr); }
|
||||
.tank-grid {
|
||||
display: grid; gap: 8px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
}
|
||||
|
||||
.sub-card { background: #0f161c; border: 1px solid $border; border-radius: 4px; display: flex; flex-direction: column; }
|
||||
.sub-header {
|
||||
|
||||
Reference in New Issue
Block a user