- 从物料跟踪页面移除订单号列和表单字段 - 从导航菜单移除PDI管理,添加设备巡检 - 新增InspectionLocation和InspectionRecord后端模型和API - 新增设备巡检前端页面(左侧点位列表,右侧设备和历史记录)
209 lines
10 KiB
Vue
209 lines
10 KiB
Vue
<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>
|