255 lines
12 KiB
Vue
255 lines
12 KiB
Vue
|
|
<template>
|
|||
|
|
<div class="cost-page">
|
|||
|
|
<!-- 成本项切换 -->
|
|||
|
|
<div class="card">
|
|||
|
|
<div class="card-body" style="padding:8px 12px;">
|
|||
|
|
<div class="item-tabs">
|
|||
|
|
<span
|
|||
|
|
v-for="it in items"
|
|||
|
|
:key="it.item"
|
|||
|
|
:class="['item-tab', { active: it.item === curItem }]"
|
|||
|
|
@click="selectItem(it.item)"
|
|||
|
|
>{{ it.item_name }}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 图表 + 记录表 -->
|
|||
|
|
<div class="cost-main">
|
|||
|
|
<div class="card chart-card">
|
|||
|
|
<div class="card-header">{{ curMeta.item_name }}耗量统计</div>
|
|||
|
|
<div class="card-body">
|
|||
|
|
<v-chart v-if="records.length" class="cost-chart" :option="chartOption" autoresize />
|
|||
|
|
<div v-else class="empty-tip">暂无数据,请调整时间范围或点击「新增」录入</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="card table-card">
|
|||
|
|
<div class="card-header">记录明细 <span class="ch-badge">{{ records.length }} 条</span></div>
|
|||
|
|
<div class="table-scroll" style="max-height:420px;">
|
|||
|
|
<table class="data-table compact">
|
|||
|
|
<thead>
|
|||
|
|
<tr>
|
|||
|
|
<th>A班量</th><th>B班量</th><th>吨耗量</th><th>记录时间</th><th>操作</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody>
|
|||
|
|
<tr v-for="r in recordsDesc" :key="r.id">
|
|||
|
|
<td class="td-num td-ok">{{ fmt(r.shift_a) }}</td>
|
|||
|
|
<td class="td-num td-warn">{{ fmt(r.shift_b) }}</td>
|
|||
|
|
<td class="td-num">{{ fmt(r.unit_cost) }}</td>
|
|||
|
|
<td class="td-muted">{{ fmtTime(r.record_date) }}</td>
|
|||
|
|
<td>
|
|||
|
|
<span class="action-link" @click="openDialog(r)">编辑</span>
|
|||
|
|
<span class="action-link del" @click="remove(r)">删除</span>
|
|||
|
|
</td>
|
|||
|
|
</tr>
|
|||
|
|
<tr v-if="!records.length">
|
|||
|
|
<td colspan="5" class="td-muted" style="text-align:center;padding:14px;">暂无数据</td>
|
|||
|
|
</tr>
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</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>
|
|||
|
|
<input v-model="query.start_date" type="date" class="kv-input" style="width:150px;" />
|
|||
|
|
</div>
|
|||
|
|
<div class="flex-row">
|
|||
|
|
<span class="kv-label">结束时间</span>
|
|||
|
|
<input v-model="query.end_date" type="date" class="kv-input" style="width:150px;" />
|
|||
|
|
</div>
|
|||
|
|
<div class="flex-row" style="margin-left:auto;gap:8px;">
|
|||
|
|
<button class="btn btn-primary" @click="fetchData">查找</button>
|
|||
|
|
<button class="btn btn-outline" @click="openDialog()">+ 新增</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 新增/编辑 -->
|
|||
|
|
<div v-if="dialogVisible" class="modal-mask" @click.self="dialogVisible=false">
|
|||
|
|
<div class="modal-box" style="width:420px;">
|
|||
|
|
<div class="modal-header">
|
|||
|
|
{{ editRow ? '编辑' : '新增' }}{{ curMeta.item_name }}记录
|
|||
|
|
<span class="modal-close" @click="dialogVisible=false">✕</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="modal-body">
|
|||
|
|
<div class="form-field">
|
|||
|
|
<div class="kv-label">记录时间 *</div>
|
|||
|
|
<input v-model="form.record_date" type="datetime-local" class="kv-input" />
|
|||
|
|
</div>
|
|||
|
|
<div class="grid-2" style="gap:12px;margin-top:10px;">
|
|||
|
|
<div class="form-field">
|
|||
|
|
<div class="kv-label">A班量 ({{ curMeta.unit }})</div>
|
|||
|
|
<input v-model.number="form.shift_a" type="number" step="0.01" class="kv-input" />
|
|||
|
|
</div>
|
|||
|
|
<div class="form-field">
|
|||
|
|
<div class="kv-label">B班量 ({{ curMeta.unit }})</div>
|
|||
|
|
<input v-model.number="form.shift_b" type="number" step="0.01" class="kv-input" />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="form-field" style="margin-top:10px;">
|
|||
|
|
<div class="kv-label">吨耗量 ({{ curMeta.unit_cost_label }})</div>
|
|||
|
|
<input v-model.number="form.unit_cost" type="number" step="0.001" class="kv-input" />
|
|||
|
|
</div>
|
|||
|
|
<div class="form-field" style="margin-top:10px;">
|
|||
|
|
<div class="kv-label">备注</div>
|
|||
|
|
<input v-model="form.remark" class="kv-input" />
|
|||
|
|
</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 { use } from 'echarts/core'
|
|||
|
|
import { CanvasRenderer } from 'echarts/renderers'
|
|||
|
|
import { BarChart, LineChart } from 'echarts/charts'
|
|||
|
|
import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/components'
|
|||
|
|
import VChart from 'vue-echarts'
|
|||
|
|
import { getCostItems, getCostRecords, createCostRecord, updateCostRecord, deleteCostRecord } from '@/api'
|
|||
|
|
|
|||
|
|
use([CanvasRenderer, BarChart, LineChart, GridComponent, TooltipComponent, LegendComponent])
|
|||
|
|
|
|||
|
|
export default {
|
|||
|
|
name: 'CostManagement',
|
|||
|
|
components: { VChart },
|
|||
|
|
data() {
|
|||
|
|
return {
|
|||
|
|
items: [], curItem: '',
|
|||
|
|
records: [],
|
|||
|
|
query: { start_date: '', end_date: '' },
|
|||
|
|
dialogVisible: false, editRow: null, saving: false,
|
|||
|
|
form: this.emptyForm(),
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
computed: {
|
|||
|
|
curMeta() { return this.items.find(x => x.item === this.curItem) || { item_name: '', unit: '', unit_cost_label: '' } },
|
|||
|
|
recordsDesc() { return [...this.records].slice().reverse() },
|
|||
|
|
chartOption() {
|
|||
|
|
const dates = this.records.map(r => this.fmtDay(r.record_date))
|
|||
|
|
const meta = this.curMeta
|
|||
|
|
return {
|
|||
|
|
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
|||
|
|
legend: { data: ['A班耗量', 'B班耗量', '吨耗量'], top: 4, textStyle: { color: '#606266' } },
|
|||
|
|
grid: { left: 50, right: 56, top: 40, bottom: 40 },
|
|||
|
|
xAxis: [{ type: 'category', data: dates, axisLabel: { color: '#909399' }, axisLine: { lineStyle: { color: '#dcdfe6' } } }],
|
|||
|
|
yAxis: [
|
|||
|
|
{ type: 'value', name: `耗量 ${meta.unit}`, nameTextStyle: { color: '#909399' }, axisLabel: { color: '#909399' }, splitLine: { lineStyle: { color: '#ebeef5' } } },
|
|||
|
|
{ type: 'value', name: `吨耗 ${meta.unit_cost_label}`, nameTextStyle: { color: '#909399' }, axisLabel: { color: '#909399' }, splitLine: { show: false } },
|
|||
|
|
],
|
|||
|
|
series: [
|
|||
|
|
{ name: 'A班耗量', type: 'bar', data: this.records.map(r => r.shift_a), itemStyle: { color: '#67C23A' }, barMaxWidth: 22 },
|
|||
|
|
{ name: 'B班耗量', type: 'bar', data: this.records.map(r => r.shift_b), itemStyle: { color: '#E6A23C' }, barMaxWidth: 22 },
|
|||
|
|
{ name: '吨耗量', type: 'line', yAxisIndex: 1, data: this.records.map(r => r.unit_cost), itemStyle: { color: '#C03639' }, lineStyle: { color: '#C03639', width: 2 }, symbol: 'circle', symbolSize: 7 },
|
|||
|
|
],
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
async created() {
|
|||
|
|
await this.fetchItems()
|
|||
|
|
const today = new Date()
|
|||
|
|
const past = new Date(today.getTime() - 7 * 86400000)
|
|||
|
|
this.query.start_date = this.toDay(past)
|
|||
|
|
this.query.end_date = this.toDay(today)
|
|||
|
|
this.fetchData()
|
|||
|
|
},
|
|||
|
|
methods: {
|
|||
|
|
emptyForm() { return { record_date: '', shift_a: null, shift_b: null, unit_cost: null, remark: '' } },
|
|||
|
|
async fetchItems() {
|
|||
|
|
try {
|
|||
|
|
const res = await getCostItems()
|
|||
|
|
this.items = res.data || []
|
|||
|
|
if (this.items.length && !this.curItem) this.curItem = this.items[0].item
|
|||
|
|
} catch (e) { /* ignore */ }
|
|||
|
|
},
|
|||
|
|
selectItem(item) { this.curItem = item; this.fetchData() },
|
|||
|
|
async fetchData() {
|
|||
|
|
const params = { item: this.curItem }
|
|||
|
|
if (this.query.start_date) params.start_date = this.query.start_date + 'T00:00:00'
|
|||
|
|
if (this.query.end_date) params.end_date = this.query.end_date + 'T23:59:59'
|
|||
|
|
try {
|
|||
|
|
const res = await getCostRecords(params)
|
|||
|
|
this.records = res.data || []
|
|||
|
|
} catch (e) { /* ignore */ }
|
|||
|
|
},
|
|||
|
|
fmt(v, n = 2) { return v != null && v !== '' ? Number(v).toFixed(n) : '—' },
|
|||
|
|
fmtTime(t) { return t ? t.slice(0, 16).replace('T', ' ') : '—' },
|
|||
|
|
fmtDay(t) { return t ? t.slice(5, 10) : '' },
|
|||
|
|
toDay(d) { const p = n => String(n).padStart(2, '0'); return `${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())}` },
|
|||
|
|
nowDT() { const d = new Date(); const p = n => String(n).padStart(2, '0'); return `${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())}T${p(d.getHours())}:${p(d.getMinutes())}` },
|
|||
|
|
openDialog(row = null) {
|
|||
|
|
this.editRow = row
|
|||
|
|
if (row) {
|
|||
|
|
this.form = { record_date: (row.record_date || '').slice(0, 16), shift_a: row.shift_a, shift_b: row.shift_b, unit_cost: row.unit_cost, remark: row.remark || '' }
|
|||
|
|
} else {
|
|||
|
|
this.form = this.emptyForm(); this.form.record_date = this.nowDT()
|
|||
|
|
}
|
|||
|
|
this.dialogVisible = true
|
|||
|
|
},
|
|||
|
|
async save() {
|
|||
|
|
if (!this.form.record_date) { this.$message.error('记录时间不能为空'); return }
|
|||
|
|
this.saving = true
|
|||
|
|
try {
|
|||
|
|
const d = { ...this.form }
|
|||
|
|
if (d.record_date && d.record_date.length === 16) d.record_date += ':00'
|
|||
|
|
if (this.editRow) await updateCostRecord(this.editRow.id, d)
|
|||
|
|
else await createCostRecord({ ...d, item: this.curItem })
|
|||
|
|
this.$message.success('保存成功')
|
|||
|
|
this.dialogVisible = false; this.fetchData()
|
|||
|
|
} catch (e) {
|
|||
|
|
this.$message.error(e?.response?.data?.detail || '保存失败')
|
|||
|
|
} finally { this.saving = false }
|
|||
|
|
},
|
|||
|
|
async remove(row) {
|
|||
|
|
if (!confirm('确认删除该记录?')) return
|
|||
|
|
try { await deleteCostRecord(row.id); this.$message.success('已删除'); this.fetchData() }
|
|||
|
|
catch (e) { this.$message.error('删除失败') }
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
@import '@/assets/styles/variables';
|
|||
|
|
|
|||
|
|
.cost-page { display: flex; flex-direction: column; gap: 12px; }
|
|||
|
|
|
|||
|
|
.item-tabs { display: flex; flex-wrap: wrap; gap: 6px; }
|
|||
|
|
.item-tab {
|
|||
|
|
padding: 5px 14px; font-size: 12px; border-radius: 4px; cursor: pointer;
|
|||
|
|
color: $text-secondary; border: 1px solid $border; background: $bg-card;
|
|||
|
|
&:hover { color: $sms-teal; border-color: $sms-teal; }
|
|||
|
|
&.active { color: #fff; background: $sms-teal; border-color: $sms-teal; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.cost-main { display: grid; grid-template-columns: 3fr 2fr; gap: 12px; align-items: stretch; }
|
|||
|
|
.chart-card, .table-card { display: flex; flex-direction: column; }
|
|||
|
|
.cost-chart { height: 420px; width: 100%; }
|
|||
|
|
.empty-tip { text-align: center; padding: 60px 20px; color: $text-muted; font-size: 13px; }
|
|||
|
|
|
|||
|
|
.form-field { display: flex; flex-direction: column; gap: 5px; }
|
|||
|
|
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; }
|
|||
|
|
.action-link { color: $sms-highlight; cursor: pointer; font-size: 12px; margin-right: 10px; &:hover { text-decoration: underline; } &.del { color: $accent-red; } }
|
|||
|
|
|
|||
|
|
.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; }
|
|||
|
|
|
|||
|
|
@media (max-width: 1100px) { .cost-main { grid-template-columns: 1fr; } }
|
|||
|
|
</style>
|