Files
pickling-mes/frontend/src/views/Downtime.vue
wangyu 193da0018f feat: 移除PDI和订单号字段,新增设备巡检模块
- 从物料跟踪页面移除订单号列和表单字段
- 从导航菜单移除PDI管理,添加设备巡检
- 新增InspectionLocation和InspectionRecord后端模型和API
- 新增设备巡检前端页面(左侧点位列表,右侧设备和历史记录)
2026-05-27 16:38:40 +08:00

209 lines
10 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>
<div>
<div class="card">
<div class="card-body" style="padding:10px 14px;">
<div class="flex-row" style="flex-wrap:wrap;gap:12px;">
<div class="flex-row">
<span class="kv-label">停机类别</span>
<select v-model="query.category_code" class="kv-input" style="width:130px;">
<option value="">全部</option>
<option v-for="c in categories" :key="c.code" :value="c.code">{{ c.name }}</option>
</select>
</div>
<div class="flex-row">
<span class="kv-label">类型</span>
<select v-model="query.is_planned" class="kv-input" style="width:100px;">
<option value="">全部</option>
<option :value="1">计划停机</option>
<option :value="0">非计划</option>
</select>
</div>
<div class="flex-row">
<span class="kv-label">日期</span>
<input v-model="query.start_date" type="date" class="kv-input" style="width:130px;" />
<span class="kv-label">~</span>
<input v-model="query.end_date" type="date" class="kv-input" style="width:130px;" />
</div>
<div class="flex-row">
<button class="btn btn-primary" @click="fetchData">查询</button>
<button class="btn btn-outline" @click="openDialog()"> 新增停机</button>
</div>
<div style="margin-left:auto;" class="flex-row">
<span class="kv-label">今日停机 <span class="kv-value" style="color:var(--accent-yellow)">{{ totalDuration }} min</span></span>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
停机记录
<span class="ch-badge"> {{ total }} </span>
</div>
<div class="table-scroll" v-loading="loading">
<table class="data-table">
<thead>
<tr>
<th>停机类别</th><th>班次</th><th>开始时间</th><th>结束时间</th>
<th>时长(min)</th><th>设备</th><th>故障描述</th>
<th>类型</th><th>报告人</th><th>处理人</th><th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="row in tableData" :key="row.id">
<td>{{ row.category_name || '—' }}</td>
<td>{{ row.shift || '—' }}</td>
<td class="td-muted">{{ fmtTime(row.start_time) }}</td>
<td class="td-muted">{{ fmtTime(row.end_time) }}</td>
<td :class="(row.duration||0) > 30 ? 'td-warn' : 'td-num'">{{ row.duration ? row.duration.toFixed(0) : '进行中' }}</td>
<td class="td-muted">{{ row.equipment_code || '—' }}</td>
<td style="max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{{ row.fault_desc || '—' }}</td>
<td>
<span :class="['badge', row.is_planned ? 'badge-blue' : 'badge-yellow']">
{{ row.is_planned ? '计划' : '非计划' }}
</span>
</td>
<td class="td-muted">{{ row.reporter || '—' }}</td>
<td class="td-muted">{{ row.handler || '—' }}</td>
<td><span class="action-link" @click="openDialog(row)">{{ row.end_time ? '编辑' : '结束停机' }}</span></td>
</tr>
<tr v-if="!tableData.length && !loading">
<td colspan="11" class="td-muted" style="text-align:center;padding:24px;">暂无停机记录</td>
</tr>
</tbody>
</table>
</div>
</div>
<div v-if="dialogVisible" class="modal-mask" @click.self="dialogVisible=false">
<div class="modal-box" style="width:680px;">
<div class="modal-header">
{{ editRow ? (editRow.end_time ? '编辑停机' : '结束停机') : '新增停机记录' }}
<span class="modal-close" @click="dialogVisible=false"></span>
</div>
<div class="modal-body">
<div class="grid-2" style="gap:12px;">
<div class="form-field">
<div class="kv-label">停机类别</div>
<select v-model="form.category_code" class="kv-input" @change="onCatChange">
<option value="">请选择</option>
<option v-for="c in categories" :key="c.code" :value="c.code">{{ c.name }}</option>
</select>
</div>
<div class="form-field">
<div class="kv-label">设备编号</div>
<input v-model="form.equipment_code" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">开始时间 *</div>
<input v-model="form.start_time" type="datetime-local" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">结束时间</div>
<input v-model="form.end_time" type="datetime-local" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">班次</div>
<select v-model="form.shift" class="kv-input">
<option value="">不限</option>
<option v-for="s in ['甲','乙','丙','丁']" :key="s" :value="s">{{ s }}</option>
</select>
</div>
<div class="form-field">
<div class="kv-label">类型</div>
<div class="flex-row" style="margin-top:4px;">
<label style="display:flex;align-items:center;gap:6px;cursor:pointer;color:var(--text-secondary);">
<input type="radio" :value="0" v-model="form.is_planned" /> 非计划停机
</label>
<label style="display:flex;align-items:center;gap:6px;cursor:pointer;color:var(--text-secondary);">
<input type="radio" :value="1" v-model="form.is_planned" /> 计划停机
</label>
</div>
</div>
<div class="form-field" style="grid-column:1/-1;">
<div class="kv-label">故障描述</div>
<textarea v-model="form.fault_desc" class="kv-input" rows="2" style="resize:vertical;"></textarea>
</div>
<div class="form-field" style="grid-column:1/-1;">
<div class="kv-label">处理措施</div>
<textarea v-model="form.action_taken" class="kv-input" rows="2" style="resize:vertical;"></textarea>
</div>
<div class="form-field">
<div class="kv-label">报告人</div>
<input v-model="form.reporter" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">处理人</div>
<input v-model="form.handler" class="kv-input" />
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-outline" @click="dialogVisible=false">取消</button>
<button class="btn btn-primary" :disabled="saving" @click="save">{{ saving ? '保存中...' : '保存' }}</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { getDowntimeCategories, getDowntimeRecords, createDowntime, updateDowntime } from '@/api'
export default {
name: 'Downtime',
data() {
return {
loading: false, saving: false,
tableData: [], total: 0, categories: [],
query: { page: 1, page_size: 20, category_code: '', is_planned: '', start_date: '', end_date: '' },
dialogVisible: false, editRow: null, form: { is_planned: 0 },
}
},
computed: {
totalDuration() {
return this.tableData.reduce((sum, r) => sum + (r.duration || 0), 0).toFixed(0)
}
},
created() { this.fetchCategories(); this.fetchData() },
methods: {
async fetchCategories() { const res = await getDowntimeCategories(); this.categories = res.data },
async fetchData() {
this.loading = true
const params = { ...this.query }
if (params.start_date) params.start_date += 'T00:00:00'
if (params.end_date) params.end_date += 'T23:59:59'
if (params.is_planned === '') delete params.is_planned
try { const res = await getDowntimeRecords(params); this.tableData = res.data.items; this.total = res.data.total } finally { this.loading = false }
},
fmtTime(t) { return t ? t.replace('T',' ').slice(0,16) : '—' },
onCatChange(e) { const c = this.categories.find(x => x.code === e.target.value); if (c) this.form.category_name = c.name },
openDialog(row = null) { this.editRow = row; this.form = row ? { ...row } : { is_planned: 0 }; this.dialogVisible = true },
async save() {
if (!this.form.start_time) { this.$message.error('开始时间不能为空'); return }
this.saving = true
const d = { ...this.form }
if (d.start_time && !d.start_time.includes('T')) d.start_time += ':00'
if (d.end_time && !d.end_time.includes('T')) d.end_time += ':00'
try {
if (this.editRow) await updateDowntime(this.editRow.id, d)
else await createDowntime(d)
this.$message.success('保存成功')
this.dialogVisible = false; this.fetchData()
} finally { this.saving = false }
}
}
}
</script>
<style lang="scss" scoped>
@import '@/assets/styles/variables';
.action-link { color: $sms-highlight; cursor: pointer; font-size: 12px; margin-right: 12px; &:hover { text-decoration: underline; } }
.form-field { display: flex; flex-direction: column; gap: 5px; }
.modal-mask { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: flex; align-items: center; justify-content: center; z-index: 9999; }
.modal-box { background: $bg-card; border: 1px solid $border; border-radius: 6px; max-width: 95vw; max-height: 90vh; display: flex; flex-direction: column; }
.modal-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: $bg-panel; border-bottom: 1px solid $border; font-size: 13px; font-weight: 600; color: $sms-highlight; .modal-close { cursor: pointer; color: $text-muted; &:hover { color: $text-primary; } } }
.modal-body { padding: 16px; overflow-y: auto; }
.modal-footer { padding: 10px 16px; background: $bg-panel; border-top: 1px solid $border; display: flex; justify-content: flex-end; gap: 10px; }
</style>