设备总包项目管理部分

This commit is contained in:
jhd
2026-06-12 13:54:43 +08:00
parent 79e536aeca
commit 1e140cf1da
115 changed files with 6614 additions and 0 deletions

View File

@@ -0,0 +1,325 @@
<template>
<div class="app-container">
<el-card shadow="never" class="module-panel">
<div slot="header" class="module-header">
<span>💰 项目预算管理</span>
<div class="header-actions">
<el-button size="small" icon="el-icon-upload2" @click="handleArchive">💾 存档当前预算</el-button>
<el-button size="small" type="primary" icon="el-icon-plus" @click="handleAdd">+ 新增预算项</el-button>
</div>
</div>
<!-- Summary Cards -->
<div class="summary-row">
<div class="stat-card">
<div class="label">预算总额</div>
<div class="value">¥{{ totalBudget }}</div>
</div>
<div class="stat-card orange">
<div class="label">实际支出</div>
<div class="value">¥{{ totalSpent }}</div>
</div>
<div class="stat-card" :class="budgetBalanceClass">
<div class="label">预算余量</div>
<div class="value">¥{{ totalBalance }}</div>
</div>
</div>
<!-- Table -->
<el-table :data="budgetList" v-loading="loading" stripe border highlight-current-row>
<el-table-column type="index" label="#" width="50" />
<el-table-column prop="category" label="类别" width="120" />
<el-table-column prop="item" label="项目名称" min-width="180" />
<el-table-column prop="budgetAmount" label="预算金额(¥)" width="140" align="right">
<template slot-scope="scope">{{ formatMoney(scope.row.budgetAmount) }}</template>
</el-table-column>
<el-table-column prop="spentAmount" label="实际金额(¥)" width="140" align="right">
<template slot-scope="scope">{{ formatMoney(scope.row.spentAmount) }}</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="110" align="center">
<template slot-scope="scope">
<el-tag :type="statusTag(scope.row.status)" size="small">
{{ statusLabel(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template slot-scope="scope">
<el-button type="text" size="mini" icon="el-icon-edit" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="text" size="mini" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper">
<el-pagination :current-page="query.pageNum" :page-sizes="[10, 20, 50, 100]" :page-size="query.pageSize"
:total="total" layout="total, sizes, prev, pager, next, jumper" @size-change="handleSizeChange"
@current-change="handlePageChange" />
</div>
</el-card>
<!-- Add/Edit Dialog -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="550px" append-to-body>
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px" size="small">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="类别" prop="category">
<el-select v-model="form.category" placeholder="请选择类别" style="width:100%">
<el-option label="机械设备" value="机械设备" />
<el-option label="电气设备" value="电气设备" />
<el-option label="液压设备" value="液压设备" />
<el-option label="流体设备" value="流体设备" />
<el-option label="能源介质" value="能源介质" />
<el-option label="安装费用" value="安装费用" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目名称" prop="item">
<el-input v-model="form.item" placeholder="如:主轧机牌坊" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="预算金额(¥)" prop="budgetAmount">
<el-input v-model="form.budgetAmount" type="number" placeholder="预算金额" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="实际金额(¥)" prop="spentAmount">
<el-input v-model="form.spentAmount" type="number" placeholder="实际金额" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" placeholder="请选择状态" style="width:100%">
<el-option label="草稿" value="draft" />
<el-option label="审核中" value="review" />
<el-option label="已批准" value="approved" />
<el-option label="已驳回" value="rejected" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="备注信息" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="handleSubmit">保存</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listBudget, getBudget, addBudget, updateBudget, delBudget } from '@/api/rm/budget'
import { listProject } from '@/api/rm/project'
export default {
name: 'RmBudget',
data() {
return {
loading: false,
budgetList: [],
total: 0,
currentProjectId: null,
query: {
pageNum: 1,
pageSize: 10,
projectId: undefined
},
dialogVisible: false,
dialogTitle: '',
submitting: false,
form: {},
rules: {
category: [{ required: true, message: '请选择类别', trigger: 'change' }],
item: [{ required: true, message: '请填写项目名称', trigger: 'blur' }],
budgetAmount: [{ required: true, message: '请填写预算金额', trigger: 'blur' }],
projectId: [{ required: true, message: '缺少项目ID', trigger: 'blur' }]
},
}
},
computed: {
totalBudget() {
return this.formatMoney(this.budgetList.reduce((s, b) => s + (Number(b.budgetAmount) || 0), 0))
},
totalSpent() {
return this.formatMoney(this.budgetList.reduce((s, b) => s + (Number(b.spentAmount) || 0), 0))
},
totalBalance() {
const budget = this.budgetList.reduce((s, b) => s + (Number(b.budgetAmount) || 0), 0)
const spent = this.budgetList.reduce((s, b) => s + (Number(b.spentAmount) || 0), 0)
return this.formatMoney(budget - spent)
},
budgetBalanceClass() {
const balance = this.budgetList.reduce((s, b) => s + (Number(b.budgetAmount) || 0), 0)
- this.budgetList.reduce((s, b) => s + (Number(b.spentAmount) || 0), 0)
return balance < 0 ? 'red' : 'green'
}
},
created() {
this.loadCurrentProject()
},
methods: {
loadCurrentProject() {
listProject({ pageNum: 1, pageSize: 1 }).then(res => {
const rows = res.rows || []
if (rows.length > 0) {
this.currentProjectId = rows[0].projectId
this.query.projectId = rows[0].projectId
this.loadList()
}
})
},
loadList() {
this.loading = true
listBudget(this.query).then(res => {
this.budgetList = res.rows || []
this.total = res.total || 0
}).finally(() => {
this.loading = false
})
},
handleSizeChange(val) {
this.query.pageSize = val
this.loadList()
},
handlePageChange(val) {
this.query.pageNum = val
this.loadList()
},
handleAdd() {
if (!this.currentProjectId) {
this.$message.warning('请先在项目总览中创建项目')
return
}
this.dialogTitle = '新增预算项'
this.form = {
projectId: this.currentProjectId,
category: '',
item: '',
budgetAmount: '',
spentAmount: '',
status: 'draft',
remark: ''
}
this.dialogVisible = true
this.$nextTick(() => { this.$refs.formRef?.clearValidate() })
},
handleEdit(row) {
this.dialogTitle = '编辑预算项'
getBudget(row.budgetId).then(res => {
this.form = res.data
this.dialogVisible = true
this.$nextTick(() => { this.$refs.formRef?.clearValidate() })
})
},
handleDelete(row) {
this.$confirm(`确认删除预算项 "${row.item}" 吗?`, '提示', { type: 'warning' }).then(() => {
delBudget(row.budgetId).then(() => {
this.$message.success('删除成功')
this.loadList()
})
})
},
handleSubmit() {
this.$refs.formRef.validate(valid => {
if (!valid) return
this.submitting = true
const api = this.form.budgetId ? updateBudget : addBudget
api(this.form).then(() => {
this.$message.success(this.form.budgetId ? '更新成功' : '新增成功')
this.dialogVisible = false
if (!this.form.budgetId) {
this.query.pageNum = 1
}
this.loadList()
}).finally(() => {
this.submitting = false
})
})
},
handleArchive() {
this.$message.info('存档功能待实现')
},
formatMoney(val) {
if (val === null || val === undefined || val === '') return '0.00'
return Number(val).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
},
statusTag(status) {
const map = { draft: 'info', review: 'warning', approved: 'success', rejected: 'danger' }
return map[status] || 'info'
},
statusLabel(status) {
const map = { draft: '草稿', review: '审核中', approved: '已批准', rejected: '已驳回' }
return map[status] || status
}
}
}
</script>
<style scoped>
.module-panel {
border: none;
border-radius: 4px;
}
.module-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 16px;
font-weight: 600;
color: #303133;
}
.header-actions {
display: flex;
gap: 8px;
}
.summary-row {
display: flex;
gap: 10px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.stat-card {
flex: 1;
min-width: 140px;
background: #fff;
border-radius: 5px;
padding: 10px 14px;
border: 1px solid #d0d7de;
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
}
.stat-card .label {
font-size: 11px;
color: #666;
margin-bottom: 4px;
}
.stat-card .value {
font-size: 20px;
font-weight: 700;
color: #2176ae;
}
.stat-card.green .value { color: #27ae60; }
.stat-card.orange .value { color: #f39c12; }
.stat-card.red .value { color: #e74c3c; }
.pagination-wrapper {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
.dialog-footer {
text-align: right;
}
</style>

View File

@@ -0,0 +1,225 @@
<template>
<div class="rm-container">
<div class="rm-panel">
<div class="rm-panel-header">
<span>图纸详细设计</span>
<el-button size="small" type="primary" icon="el-icon-plus" @click="handleAdd">新增图纸</el-button>
</div>
<div class="rm-panel-body">
<!-- Search -->
<div class="search-row">
<el-input v-model="query.drawingName" size="small" placeholder="图纸名称" clearable style="width:160px" @keyup.enter="handleSearch" />
<el-select v-model="query.drawingType" size="small" placeholder="图纸类型" clearable style="width:130px">
<el-option label="总装图" value="总装图" />
<el-option label="部件图" value="部件图" />
<el-option label="零件图" value="零件图" />
<el-option label="液压原理图" value="液压原理图" />
<el-option label="电气原理图" value="电气原理图" />
</el-select>
<el-select v-model="query.status" size="small" placeholder="状态" clearable style="width:110px">
<el-option label="进行中" value="in_progress" />
<el-option label="已完成" value="completed" />
<el-option label="已审查" value="reviewed" />
</el-select>
<el-button size="small" type="primary" icon="el-icon-search" @click="handleSearch">查询</el-button>
<el-button size="small" icon="el-icon-refresh" @click="handleReset">重置</el-button>
</div>
<!-- Table -->
<el-table :data="list" v-loading="loading" stripe border highlight-current-row size="small">
<el-table-column type="index" label="#" width="45" />
<el-table-column prop="drawingName" label="图纸名称" min-width="160" />
<el-table-column prop="drawingNo" label="图号" width="110" />
<el-table-column prop="drawingType" label="图纸类型" width="100" />
<el-table-column prop="version" label="版本" width="60" align="center" />
<el-table-column prop="drawer" label="设计人" width="80" />
<el-table-column prop="startDate" label="设计日期" width="100" align="center" />
<el-table-column prop="endDate" label="完成日期" width="100" align="center" />
<el-table-column prop="status" label="状态" width="90" align="center">
<template slot-scope="scope">
<el-tag :type="statusTag(scope.row.status)" size="mini">{{ statusLabel(scope.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="110" fixed="right">
<template slot-scope="scope">
<el-button type="text" size="mini" icon="el-icon-edit" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="text" size="mini" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-row">
<el-pagination :current-page="query.pageNum" :page-sizes="[10, 20, 50, 100]" :page-size="query.pageSize"
:total="total" layout="total, sizes, prev, pager, next, jumper" size="small"
@size-change="handleSizeChange" @current-change="handlePageChange" />
</div>
</div>
</div>
<!-- Add/Edit Dialog -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="560px" append-to-body>
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px" size="small">
<el-row :gutter="16">
<el-col :span="16">
<el-form-item label="图纸名称" prop="drawingName">
<el-input v-model="form.drawingName" placeholder="图纸名称" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="版本" prop="version">
<el-input v-model="form.version" placeholder="V1.0" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="图号" prop="drawingNo">
<el-input v-model="form.drawingNo" placeholder="图号" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="图纸类型" prop="drawingType">
<el-select v-model="form.drawingType" placeholder="请选择" style="width:100%">
<el-option label="总装图" value="总装图" />
<el-option label="部件图" value="部件图" />
<el-option label="零件图" value="零件图" />
<el-option label="液压原理图" value="液压原理图" />
<el-option label="电气原理图" value="电气原理图" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="设计人" prop="drawer">
<el-input v-model="form.drawer" placeholder="设计人姓名" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" placeholder="请选择" style="width:100%">
<el-option label="进行中" value="in_progress" />
<el-option label="已完成" value="completed" />
<el-option label="已审查" value="reviewed" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="设计日期" prop="startDate">
<el-date-picker v-model="form.startDate" type="date" placeholder="选择日期" style="width:100%" value-format="yyyy-MM-dd" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="完成日期" prop="endDate">
<el-date-picker v-model="form.endDate" type="date" placeholder="选择日期" style="width:100%" value-format="yyyy-MM-dd" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="文件路径" prop="fileUrl">
<el-input v-model="form.fileUrl" placeholder="文件存储路径或 URL" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="备注信息" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button size="small" @click="dialogVisible = false">取消</el-button>
<el-button size="small" type="primary" :loading="submitting" @click="handleSubmit">保存</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listDrawingDesign, getDrawingDesign, addDrawingDesign, updateDrawingDesign, delDrawingDesign } from '@/api/rm/drawingDesign'
import { listProject } from '@/api/rm/project'
export default {
name: 'RmDrawingDesign',
data() {
return {
loading: false,
list: [],
total: 0,
currentProjectId: null,
query: { pageNum: 1, pageSize: 10, drawingName: undefined, drawingType: undefined, status: undefined, projectId: undefined },
dialogVisible: false,
dialogTitle: '',
submitting: false,
form: {},
rules: { drawingName: [{ required: true, message: '请填写图纸名称', trigger: 'blur' }] }
}
},
created() { this.loadCurrentProject() },
methods: {
loadCurrentProject() {
listProject({ pageNum: 1, pageSize: 1 }).then(res => {
const rows = res.rows || []
if (rows.length > 0) {
this.currentProjectId = rows[0].projectId
this.query.projectId = rows[0].projectId
this.loadList()
}
})
},
loadList() {
this.loading = true
listDrawingDesign(this.query).then(res => {
this.list = res.rows || []
this.total = res.total || 0
}).finally(() => { this.loading = false })
},
handleSearch() { this.query.pageNum = 1; this.loadList() },
handleReset() { this.query = { pageNum: 1, pageSize: 10 }; this.loadList() },
handleSizeChange(val) { this.query.pageSize = val; this.loadList() },
handlePageChange(val) { this.query.pageNum = val; this.loadList() },
handleAdd() {
this.dialogTitle = '新增图纸'
if (!this.currentProjectId) { this.$message.warning('请先在项目总览中创建项目'); return }
this.form = { projectId: this.currentProjectId, drawingName: '', drawingNo: '', version: 'V1.0', drawingType: '', drawer: '', startDate: '', endDate: '', fileUrl: '', status: 'in_progress', remark: '' }
this.dialogVisible = true
this.$nextTick(() => { this.$refs.formRef?.clearValidate() })
},
handleEdit(row) {
this.dialogTitle = '编辑图纸'
getDrawingDesign(row.drawingId).then(res => {
this.form = res.data
this.dialogVisible = true
this.$nextTick(() => { this.$refs.formRef?.clearValidate() })
})
},
handleDelete(row) {
this.$confirm(`确认删除 "${row.drawingName}"`, '提示', { type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消' }).then(() => {
delDrawingDesign(row.drawingId).then(() => { this.$message.success('删除成功'); this.loadList() })
})
},
handleSubmit() {
this.$refs.formRef.validate(valid => {
if (!valid) return
this.submitting = true
const api = this.form.drawingId ? updateDrawingDesign : addDrawingDesign
api(this.form).then(() => {
this.$message.success(this.form.drawingId ? '更新成功' : '新增成功')
this.dialogVisible = false
if (!this.form.drawingId) this.query.pageNum = 1
this.loadList()
}).finally(() => { this.submitting = false })
})
},
statusTag(s) { return { in_progress: 'info', completed: 'success', reviewed: 'primary' }[s] || 'info' },
statusLabel(s) { return { in_progress: '进行中', completed: '已完成', reviewed: '已审查' }[s] || s }
}
}
</script>
<style scoped>
.rm-container { padding: 8px; }
.rm-panel { background: #fff; border-radius: 4px; border: 1px solid #d0d7de; }
.rm-panel-header { padding: 8px 12px; font-size: 14px; font-weight: 600; border-bottom: 1px solid #d0d7de; display: flex; align-items: center; justify-content: space-between; }
.rm-panel-body { padding: 8px 12px; }
.search-row { display: flex; gap: 6px; align-items: center; margin-bottom: 8px; flex-wrap: wrap; }
.pagination-row { margin-top: 10px; display: flex; justify-content: flex-end; }
.dialog-footer { text-align: right; }
</style>

View File

@@ -0,0 +1,183 @@
<template>
<div class="rm-container">
<div class="rm-panel">
<div class="rm-panel-header">
<span>图纸审查</span>
<el-button size="small" type="primary" icon="el-icon-plus" @click="handleAdd">新增审查记录</el-button>
</div>
<div class="rm-panel-body">
<!-- Search -->
<div class="search-row">
<el-input v-model="query.drawingName" size="small" placeholder="图纸名称" clearable style="width:160px" @keyup.enter="handleSearch" />
<el-select v-model="query.status" size="small" placeholder="状态" clearable style="width:110px">
<el-option label="待审查" value="pending" />
<el-option label="通过" value="approved" />
<el-option label="驳回" value="rejected" />
</el-select>
<el-button size="small" type="primary" icon="el-icon-search" @click="handleSearch">查询</el-button>
<el-button size="small" icon="el-icon-refresh" @click="handleReset">重置</el-button>
</div>
<!-- Table -->
<el-table :data="list" v-loading="loading" stripe border highlight-current-row size="small">
<el-table-column type="index" label="#" width="45" />
<el-table-column prop="drawingName" label="图纸名称" min-width="160" />
<el-table-column prop="reviewer" label="审查人" width="90" />
<el-table-column prop="reviewDate" label="审查日期" width="100" align="center" />
<el-table-column prop="status" label="结果" width="90" align="center">
<template slot-scope="scope">
<el-tag :type="statusTag(scope.row.status)" size="mini">{{ statusLabel(scope.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="reviewOpinion" label="审查意见" min-width="160" show-overflow-tooltip />
<el-table-column label="操作" width="120" fixed="right">
<template slot-scope="scope">
<el-button type="text" size="mini" icon="el-icon-edit" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="text" size="mini" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-row">
<el-pagination :current-page="query.pageNum" :page-sizes="[10, 20, 50, 100]" :page-size="query.pageSize"
:total="total" layout="total, sizes, prev, pager, next, jumper" size="small"
@size-change="handleSizeChange" @current-change="handlePageChange" />
</div>
</div>
</div>
<!-- Add/Edit Dialog -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="500px" append-to-body>
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px" size="small">
<el-form-item label="图纸名称" prop="drawingName">
<el-input v-model="form.drawingName" placeholder="关联图纸名称" />
</el-form-item>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="审查人" prop="reviewer">
<el-input v-model="form.reviewer" placeholder="审查人姓名" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="审查日期" prop="reviewDate">
<el-date-picker v-model="form.reviewDate" type="date" placeholder="选择日期" style="width:100%" value-format="yyyy-MM-dd" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="结果" prop="status">
<el-select v-model="form.status" placeholder="请选择" style="width:100%">
<el-option label="待审查" value="pending" />
<el-option label="通过" value="approved" />
<el-option label="驳回" value="rejected" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="审查意见" prop="reviewOpinion">
<el-input v-model="form.reviewOpinion" type="textarea" :rows="3" placeholder="审查意见" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="备注信息" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button size="small" @click="dialogVisible = false">取消</el-button>
<el-button size="small" type="primary" :loading="submitting" @click="handleSubmit">保存</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listDrawingReview, getDrawingReview, addDrawingReview, updateDrawingReview, delDrawingReview } from '@/api/rm/drawingReview'
import { listProject } from '@/api/rm/project'
export default {
name: 'RmDrawingReview',
data() {
return {
loading: false,
list: [],
total: 0,
currentProjectId: null,
query: { pageNum: 1, pageSize: 10, drawingName: undefined, status: undefined, projectId: undefined },
dialogVisible: false,
dialogTitle: '',
submitting: false,
form: {},
rules: { drawingName: [{ required: true, message: '请填写图纸名称', trigger: 'blur' }] }
}
},
created() { this.loadCurrentProject() },
methods: {
loadCurrentProject() {
listProject({ pageNum: 1, pageSize: 1 }).then(res => {
const rows = res.rows || []
if (rows.length > 0) {
this.currentProjectId = rows[0].projectId
this.query.projectId = rows[0].projectId
this.loadList()
}
})
},
loadList() {
this.loading = true
listDrawingReview(this.query).then(res => {
this.list = res.rows || []
this.total = res.total || 0
}).finally(() => { this.loading = false })
},
handleSearch() { this.query.pageNum = 1; this.loadList() },
handleReset() { this.query = { pageNum: 1, pageSize: 10 }; this.loadList() },
handleSizeChange(val) { this.query.pageSize = val; this.loadList() },
handlePageChange(val) { this.query.pageNum = val; this.loadList() },
handleAdd() {
this.dialogTitle = '新增审查记录'
if (!this.currentProjectId) { this.$message.warning('请先在项目总览中创建项目'); return }
this.form = { projectId: this.currentProjectId, drawingName: '', reviewer: '', reviewDate: '', status: 'pending', reviewOpinion: '', remark: '' }
this.dialogVisible = true
this.$nextTick(() => { this.$refs.formRef?.clearValidate() })
},
handleEdit(row) {
this.dialogTitle = '编辑审查记录'
getDrawingReview(row.reviewId).then(res => {
this.form = res.data
this.dialogVisible = true
this.$nextTick(() => { this.$refs.formRef?.clearValidate() })
})
},
handleDelete(row) {
this.$confirm(`确认删除 "${row.drawingName}" 的审查记录?`, '提示', { type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消' }).then(() => {
delDrawingReview(row.reviewId).then(() => { this.$message.success('删除成功'); this.loadList() })
})
},
handleSubmit() {
this.$refs.formRef.validate(valid => {
if (!valid) return
this.submitting = true
const api = this.form.reviewId ? updateDrawingReview : addDrawingReview
api(this.form).then(() => {
this.$message.success(this.form.reviewId ? '更新成功' : '新增成功')
this.dialogVisible = false
if (!this.form.reviewId) this.query.pageNum = 1
this.loadList()
}).finally(() => { this.submitting = false })
})
},
statusTag(s) { return { pending: 'info', approved: 'success', rejected: 'danger' }[s] || 'info' },
statusLabel(s) { return { pending: '待审查', approved: '通过', rejected: '驳回' }[s] || s }
}
}
</script>
<style scoped>
.rm-container { padding: 8px; }
.rm-panel { background: #fff; border-radius: 4px; border: 1px solid #d0d7de; }
.rm-panel-header { padding: 8px 12px; font-size: 14px; font-weight: 600; border-bottom: 1px solid #d0d7de; display: flex; align-items: center; justify-content: space-between; }
.rm-panel-body { padding: 8px 12px; }
.search-row { display: flex; gap: 6px; align-items: center; margin-bottom: 8px; flex-wrap: wrap; }
.pagination-row { margin-top: 10px; display: flex; justify-content: flex-end; }
.dialog-footer { text-align: right; }
</style>

View File

@@ -0,0 +1,204 @@
<template>
<div class="rm-container">
<div class="rm-panel">
<div class="rm-panel-header">
<span>布局图确定</span>
<el-button size="small" type="primary" icon="el-icon-upload2" @click="handleAdd">上传布局图</el-button>
</div>
<div class="rm-panel-body">
<!-- Upload area (matching prototype) -->
<div class="upload-area" @click="handleAdd">
<div class="upload-icon">📤</div>
<div class="upload-text">点击上传布局图文件DWG / PDF / DXF</div>
</div>
<!-- Search -->
<div class="search-row">
<el-input v-model="query.fileName" size="small" placeholder="文件名" clearable style="width:160px" @keyup.enter="handleSearch" />
<el-select v-model="query.fileType" size="small" placeholder="文件类型" clearable style="width:110px">
<el-option label="PDF" value="PDF" />
<el-option label="DWG" value="DWG" />
<el-option label="DXF" value="DXF" />
</el-select>
<el-select v-model="query.status" size="small" placeholder="状态" clearable style="width:100px">
<el-option label="待审核" value="pending" />
<el-option label="已批准" value="approved" />
</el-select>
<el-button size="small" type="primary" icon="el-icon-search" @click="handleSearch">查询</el-button>
<el-button size="small" icon="el-icon-refresh" @click="handleReset">重置</el-button>
</div>
<!-- Table -->
<el-table :data="list" v-loading="loading" stripe border highlight-current-row size="small">
<el-table-column type="index" label="#" width="45" />
<el-table-column prop="fileName" label="文件名" min-width="180" />
<el-table-column prop="fileType" label="类型" width="70" align="center" />
<el-table-column prop="uploadDate" label="上传日期" width="100" align="center" />
<el-table-column prop="status" label="状态" width="90" align="center">
<template slot-scope="scope">
<el-tag :type="statusTag(scope.row.status)" size="mini">{{ statusLabel(scope.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template slot-scope="scope">
<el-button type="text" size="mini" icon="el-icon-edit" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="text" size="mini" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-row">
<el-pagination :current-page="query.pageNum" :page-sizes="[10, 20, 50, 100]" :page-size="query.pageSize"
:total="total" layout="total, sizes, prev, pager, next, jumper" size="small"
@size-change="handleSizeChange" @current-change="handlePageChange" />
</div>
</div>
</div>
<!-- Add/Edit Dialog -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="500px" append-to-body>
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px" size="small">
<el-form-item label="文件名" prop="fileName">
<el-input v-model="form.fileName" placeholder="如1380mm轧机平面布置图.dwg" />
</el-form-item>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="文件类型" prop="fileType">
<el-select v-model="form.fileType" placeholder="请选择" style="width:100%">
<el-option label="PDF" value="PDF" />
<el-option label="DWG" value="DWG" />
<el-option label="DXF" value="DXF" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="上传日期" prop="uploadDate">
<el-date-picker v-model="form.uploadDate" type="date" placeholder="选择日期" style="width:100%" value-format="yyyy-MM-dd" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="版本" prop="version">
<el-input v-model="form.version" placeholder="如 V1.0" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" placeholder="请选择" style="width:100%">
<el-option label="待审核" value="pending" />
<el-option label="已批准" value="approved" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="文件路径" prop="fileUrl">
<el-input v-model="form.fileUrl" placeholder="文件存储路径或 URL" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="备注信息" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button size="small" @click="dialogVisible = false">取消</el-button>
<el-button size="small" type="primary" :loading="submitting" @click="handleSubmit">保存</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listLayoutFile, getLayoutFile, addLayoutFile, updateLayoutFile, delLayoutFile } from '@/api/rm/layoutFile'
import { listProject } from '@/api/rm/project'
export default {
name: 'RmLayoutFile',
data() {
return {
loading: false,
list: [],
total: 0,
currentProjectId: null,
query: { pageNum: 1, pageSize: 10, fileName: undefined, fileType: undefined, status: undefined, projectId: undefined },
dialogVisible: false,
dialogTitle: '',
submitting: false,
form: {},
rules: { fileName: [{ required: true, message: '请填写文件名', trigger: 'blur' }] }
}
},
created() { this.loadCurrentProject() },
methods: {
loadCurrentProject() {
listProject({ pageNum: 1, pageSize: 1 }).then(res => {
const rows = res.rows || []
if (rows.length > 0) {
this.currentProjectId = rows[0].projectId
this.query.projectId = rows[0].projectId
this.loadList()
}
})
},
loadList() {
this.loading = true
listLayoutFile(this.query).then(res => {
this.list = res.rows || []
this.total = res.total || 0
}).finally(() => { this.loading = false })
},
handleSearch() { this.query.pageNum = 1; this.loadList() },
handleReset() { this.query = { pageNum: 1, pageSize: 10 }; this.loadList() },
handleSizeChange(val) { this.query.pageSize = val; this.loadList() },
handlePageChange(val) { this.query.pageNum = val; this.loadList() },
handleAdd() {
this.dialogTitle = '新增布局图'
if (!this.currentProjectId) { this.$message.warning('请先在项目总览中创建项目'); return }
this.form = { projectId: this.currentProjectId, fileName: '', fileType: '', uploadDate: '', version: '', status: 'pending', fileUrl: '', remark: '' }
this.dialogVisible = true
this.$nextTick(() => { this.$refs.formRef?.clearValidate() })
},
handleEdit(row) {
this.dialogTitle = '编辑布局图'
getLayoutFile(row.layoutFileId).then(res => {
this.form = res.data
this.dialogVisible = true
this.$nextTick(() => { this.$refs.formRef?.clearValidate() })
})
},
handleDelete(row) {
this.$confirm(`确认删除 "${row.fileName}"`, '提示', { type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消' }).then(() => {
delLayoutFile(row.layoutFileId).then(() => { this.$message.success('删除成功'); this.loadList() })
})
},
handleSubmit() {
this.$refs.formRef.validate(valid => {
if (!valid) return
this.submitting = true
const api = this.form.layoutFileId ? updateLayoutFile : addLayoutFile
api(this.form).then(() => {
this.$message.success(this.form.layoutFileId ? '更新成功' : '新增成功')
this.dialogVisible = false
if (!this.form.layoutFileId) this.query.pageNum = 1
this.loadList()
}).finally(() => { this.submitting = false })
})
},
statusTag(s) { return { pending: 'info', approved: 'success' }[s] || 'info' },
statusLabel(s) { return { pending: '待审核', approved: '已批准' }[s] || s }
}
}
</script>
<style scoped>
.rm-container { padding: 8px; }
.rm-panel { background: #fff; border-radius: 4px; border: 1px solid #d0d7de; }
.rm-panel-header { padding: 8px 12px; font-size: 14px; font-weight: 600; border-bottom: 1px solid #d0d7de; display: flex; align-items: center; justify-content: space-between; }
.rm-panel-body { padding: 8px 12px; }
.upload-area { border: 2px dashed #d0d7de; border-radius: 4px; padding: 20px; text-align: center; cursor: pointer; margin-bottom: 10px; transition: border-color 0.2s; }
.upload-area:hover { border-color: #2176ae; }
.upload-icon { font-size: 28px; margin-bottom: 6px; }
.upload-text { font-size: 12px; color: #666; }
.search-row { display: flex; gap: 6px; align-items: center; margin-bottom: 8px; flex-wrap: wrap; }
.pagination-row { margin-top: 10px; display: flex; justify-content: flex-end; }
.dialog-footer { text-align: right; }
</style>

View File

@@ -0,0 +1,237 @@
<template>
<div class="rm-container">
<div class="rm-panel">
<div class="rm-panel-header">
<span>设备制造进度</span>
<el-button size="small" type="primary" icon="el-icon-plus" @click="handleAddDevice">新增设备</el-button>
</div>
<div class="rm-panel-body">
<el-table :data="deviceList" v-loading="loading" stripe border highlight-current-row size="small" row-key="deviceId">
<el-table-column type="index" label="#" width="45" />
<el-table-column prop="deviceName" label="设备名称" min-width="140" />
<el-table-column prop="supplierName" label="供应商" min-width="120" />
<el-table-column prop="deliveryDate" label="交货日期" width="100" align="center" />
<el-table-column label="制造进度" min-width="160">
<template slot-scope="s">
<div style="display:flex;align-items:center;gap:6px;">
<el-progress :percentage="devicePct(s.row)" :stroke-width="10" style="flex:1;" />
<span style="font-size:12px;font-weight:600;white-space:nowrap;">{{ devicePct(s.row) }}%</span>
</div>
<div style="font-size:10px;color:#888;">{{ doneStageCount(s.row) }}/{{ stageCount(s.row) }} 阶段</div>
</template>
</el-table-column>
<el-table-column label="操作" width="160" fixed="right">
<template slot-scope="s">
<el-button type="text" size="mini" icon="el-icon-s-operation" @click="toggleDetail(s.row)">{{ s.row._showDetail ? '收起' : '阶段' }}</el-button>
<el-button type="text" size="mini" icon="el-icon-edit" @click="handleEditDevice(s.row)">编辑</el-button>
<el-button type="text" size="mini" icon="el-icon-delete" @click="handleDeleteDevice(s.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- Stage Detail Panels -->
<div v-for="d in deviceList" :key="d.deviceId">
<div v-if="d._showDetail" class="stage-detail-panel">
<div v-for="(stg, si) in deviceStages(d)" :key="si" class="stage-card">
<div class="stage-header" :class="stageHeaderClass(stg)" @click="stg._open = !stg._open">
<span>{{ si+1 }}. {{ stg.stageName }} <el-tag :type="stageTag(stg.status)" size="mini" style="margin-left:6px;">{{ stageLabel(stg.status) }}</el-tag></span>
<span style="font-size:10px;color:#888;">{{ stg.planEndDate ? '计划: '+stg.planEndDate : '' }} {{ stg.submittedDate ? '· 提交: '+stg.submittedDate : '' }}</span>
</div>
<div v-show="stg._open" class="stage-body">
<div class="stage-field"><label>计划开始</label>{{ stg.planStartDate || '-' }}</div>
<div class="stage-field"><label>计划结束</label>{{ stg.planEndDate || '-' }}</div>
<div class="stage-field"><label>实际开始</label>{{ stg.actualStart || '-' }}</div>
<div class="stage-field"><label>实际结束</label>{{ stg.actualEnd || '-' }}</div>
<div class="stage-field"><label>提交日期</label>{{ stg.submittedDate || '-' }}</div>
<div class="stage-field"><label>制造照片</label>{{ stg.manufacturingPhotos || '-' }}</div>
<div class="stage-field"><label>制造视频</label>{{ stg.manufacturingVideo || '-' }}</div>
<div class="stage-field"><label>材质报告</label>{{ stg.materialReport || '-' }}</div>
<div class="stage-field"><label>精度报告</label>{{ stg.precisionReport || '-' }}</div>
<div style="margin-top:6px;">
<el-button size="mini" type="primary" @click="handleEditStage(stg)">编辑阶段</el-button>
</div>
</div>
</div>
<div v-if="!deviceStages(d).length" style="text-align:center;padding:16px;color:#aaa;font-size:13px;">暂无阶段数据</div>
</div>
</div>
</div>
</div>
<!-- Device Dialog -->
<el-dialog :title="deviceTitle" :visible.sync="deviceVisible" width="520px" append-to-body>
<el-form ref="deviceFormRef" :model="deviceForm" :rules="deviceRules" label-width="100px" size="small">
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="设备名称" prop="deviceName"><el-input v-model="deviceForm.deviceName" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="规格型号" prop="spec"><el-input v-model="deviceForm.spec" /></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="供应商" prop="supplierName"><el-input v-model="deviceForm.supplierName" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="合同编号" prop="contractNo"><el-input v-model="deviceForm.contractNo" /></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="8"><el-form-item label="下单日期"><el-date-picker v-model="deviceForm.orderDate" type="date" style="width:100%" value-format="yyyy-MM-dd" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="交货日期" prop="deliveryDate"><el-date-picker v-model="deviceForm.deliveryDate" type="date" style="width:100%" value-format="yyyy-MM-dd" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="罚款(元/天)"><el-input-number v-model="deviceForm.penaltyRate" :min="0" :precision="2" style="width:100%" /></el-form-item></el-col>
</el-row>
<el-form-item label="备注"><el-input v-model="deviceForm.remark" type="textarea" :rows="2" /></el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button size="small" @click="deviceVisible = false">取消</el-button>
<el-button size="small" type="primary" :loading="deviceSubmitting" @click="handleDeviceSubmit">保存</el-button>
</div>
</el-dialog>
<!-- Stage Dialog -->
<el-dialog title="编辑阶段" :visible.sync="stageVisible" width="560px" append-to-body>
<el-form ref="stageFormRef" :model="stageForm" label-width="110px" size="small">
<div style="font-weight:600;margin-bottom:8px;font-size:14px;">{{ stageForm.stageName }}</div>
<el-row :gutter="16">
<el-col :span="8"><el-form-item label="计划开始"><el-date-picker v-model="stageForm.planStartDate" type="date" style="width:100%" value-format="yyyy-MM-dd" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="计划结束"><el-date-picker v-model="stageForm.planEndDate" type="date" style="width:100%" value-format="yyyy-MM-dd" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="状态"><el-select v-model="stageForm.status" style="width:100%"><el-option label="待开始" value="0" /><el-option label="进行中" value="1" /><el-option label="已完成" value="2" /><el-option label="逾期" value="3" /></el-select></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="8"><el-form-item label="实际开始"><el-date-picker v-model="stageForm.actualStart" type="date" style="width:100%" value-format="yyyy-MM-dd" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="实际结束"><el-date-picker v-model="stageForm.actualEnd" type="date" style="width:100%" value-format="yyyy-MM-dd" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="提交日期"><el-date-picker v-model="stageForm.submittedDate" type="date" style="width:100%" value-format="yyyy-MM-dd" /></el-form-item></el-col>
</el-row>
<el-form-item label="制造照片"><el-input v-model="stageForm.manufacturingPhotos" type="textarea" :rows="1" placeholder="JSON或URL" /></el-form-item>
<el-form-item label="制造视频"><el-input v-model="stageForm.manufacturingVideo" placeholder="视频URL" /></el-form-item>
<el-form-item label="材质报告"><el-input v-model="stageForm.materialReport" type="textarea" :rows="1" placeholder="JSON" /></el-form-item>
<el-form-item label="精度报告"><el-input v-model="stageForm.precisionReport" type="textarea" :rows="1" placeholder="JSON" /></el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button size="small" @click="stageVisible = false">取消</el-button>
<el-button size="small" type="primary" :loading="stageSubmitting" @click="handleStageSubmit">保存</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listMfgDeviceAll, getMfgDevice, addMfgDevice, updateMfgDevice, delMfgDevice } from '@/api/rm/mfgDevice'
import { listMfgStageAll, updateMfgStage } from '@/api/rm/mfgDevice'
import { listProject } from '@/api/rm/project'
export default {
name: 'RmManufacturing',
data() {
return {
loading: false,
deviceList: [],
stageMap: {},
currentProjectId: null,
deviceVisible: false, deviceTitle: '', deviceSubmitting: false,
deviceForm: {}, deviceRules: { deviceName: [{ required: true, message: '请填写设备名称', trigger: 'blur' }] },
stageVisible: false, stageSubmitting: false,
stageForm: {},
detailMap: {}
}
},
created() { this.loadCurrentProject() },
methods: {
loadCurrentProject() {
listProject({ pageNum: 1, pageSize: 1 }).then(res => {
const rows = res.rows || []
if (rows.length > 0) {
this.currentProjectId = rows[0].projectId
this.loadDevices()
}
})
},
loadDevices() {
if (!this.currentProjectId) return
this.loading = true
listMfgDeviceAll({ projectId: this.currentProjectId }).then(r => {
this.deviceList = (r.data || []).map(d => ({ ...d, _showDetail: false }))
// Load stages for all devices
this.deviceList.forEach(d => this.loadStages(d))
}).finally(() => { this.loading = false })
},
loadStages(device) {
listMfgStageAll({ deviceId: device.deviceId }).then(r => {
this.$set(this.stageMap, device.deviceId, (r.data || []).map(s => ({ ...s, _open: false })))
})
},
deviceStages(device) {
return this.stageMap[device.deviceId] || []
},
stageCount(device) {
return this.deviceStages(device).length
},
doneStageCount(device) {
return this.deviceStages(device).filter(s => s.status === '2').length
},
devicePct(device) {
const st = this.deviceStages(device)
if (!st.length) return 0
return Math.round(st.filter(s => s.status === '2').length / st.length * 100)
},
toggleDetail(device) {
device._showDetail = !device._showDetail
if (device._showDetail && !this.stageMap[device.deviceId]) {
this.loadStages(device)
}
},
handleAddDevice() {
if (!this.currentProjectId) { this.$message.warning('请先在项目总览中创建项目'); return }
this.deviceTitle = '新增设备'
this.deviceForm = { projectId: this.currentProjectId, deviceName: '', spec: '', supplierName: '', contractNo: '', orderDate: '', deliveryDate: '', penaltyRate: 0, remark: '' }
this.deviceVisible = true
this.$nextTick(() => { this.$refs.deviceFormRef?.clearValidate() })
},
handleEditDevice(row) {
this.deviceTitle = '编辑设备'
getMfgDevice(row.deviceId).then(r => { this.deviceForm = r.data; this.deviceVisible = true; this.$nextTick(() => { this.$refs.deviceFormRef?.clearValidate() }) })
},
handleDeleteDevice(row) {
this.$confirm(`确认删除 "${row.deviceName}"`, '提示', { type: 'warning' }).then(() => { delMfgDevice(row.deviceId).then(() => { this.$message.success('删除成功'); this.loadDevices() }) })
},
handleDeviceSubmit() {
this.$refs.deviceFormRef.validate(v => {
if (!v) return; this.deviceSubmitting = true
const api = this.deviceForm.deviceId ? updateMfgDevice : addMfgDevice
api(this.deviceForm).then(() => { this.$message.success('保存成功'); this.deviceVisible = false; this.loadDevices() }).finally(() => { this.deviceSubmitting = false })
})
},
handleEditStage(stg) {
this.stageForm = { ...stg }
this.stageVisible = true
},
handleStageSubmit() {
this.stageSubmitting = true
updateMfgStage(this.stageForm).then(() => {
this.$message.success('阶段已更新')
this.stageVisible = false
const device = this.deviceList.find(d => d.deviceId === this.stageForm.deviceId)
if (device) this.loadStages(device)
}).finally(() => { this.stageSubmitting = false })
},
stageTag(s) { return { '0': 'info', '1': 'primary', '2': 'success', '3': 'danger' }[s] || 'info' },
stageLabel(s) { return { '0': '待开始', '1': '进行中', '2': '已完成', '3': '逾期' }[s] || s },
stageHeaderClass(stg) {
if (stg.status === '3') return 'overdue'
if (stg.status === '2') return 'done'
if (stg.status === '1') return 'active'
return ''
}
}
}
</script>
<style scoped>
.rm-container { padding: 8px; }
.rm-panel { background: #fff; border-radius: 4px; border: 1px solid #d0d7de; }
.rm-panel-header { padding: 8px 12px; font-size: 14px; font-weight: 600; border-bottom: 1px solid #d0d7de; display: flex; align-items: center; justify-content: space-between; }
.rm-panel-body { padding: 8px 12px; }
.dialog-footer { text-align: right; }
.stage-detail-panel { background: #f8f9fc; border: 1px solid #e8ecf0; border-radius: 4px; padding: 8px; margin: 4px 0 12px; }
.stage-card { margin-bottom: 6px; border: 1px solid #e8ecf0; border-radius: 4px; background: #fff; overflow: hidden; }
.stage-header { padding: 8px 10px; font-size: 13px; font-weight: 600; cursor: pointer; display: flex; justify-content: space-between; align-items: center; }
.stage-header.done { background: #f0f9eb; }
.stage-header.active { background: #ecf5ff; }
.stage-header.overdue { background: #fef0f0; }
.stage-body { padding: 10px; display: grid; grid-template-columns: 1fr 1fr; gap: 4px 16px; font-size: 12px; }
.stage-field label { color: #888; }
</style>

View File

@@ -0,0 +1,426 @@
<template>
<div class="rm-container">
<div class="rm-panel">
<div class="rm-panel-header">
<span>采购管理</span>
</div>
<div class="rm-panel-body">
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane label="报价比较" name="quotes" />
<el-tab-pane label="合同管理" name="contracts" />
<el-tab-pane label="合同模版" name="template" />
<el-tab-pane label="采购进度" name="progress" />
</el-tabs>
<!-- ========== 报价比较 Tab ========== -->
<div v-if="activeTab === 'quotes'">
<div style="margin-bottom:8px;display:flex;gap:6px;flex-wrap:wrap;align-items:center;">
<el-button size="small" type="primary" icon="el-icon-plus" @click="handleAddQuote">新增报价</el-button>
</div>
<el-table :data="quoteList" v-loading="quoteLoading" stripe border highlight-current-row size="small">
<el-table-column type="index" label="#" width="45" />
<el-table-column prop="itemName" label="设备项" min-width="140" />
<el-table-column prop="supplierName" label="供应商" min-width="140" />
<el-table-column prop="unitPrice" label="单价(¥)" width="100" align="right" />
<el-table-column prop="totalPrice" label="总价(¥)" width="110" align="right" />
<el-table-column prop="deliveryDays" label="交货期" width="80" align="center">
<template slot-scope="s">{{ s.row.deliveryDays }}</template>
</el-table-column>
<el-table-column prop="warrantyMonths" label="质保期" width="80" align="center">
<template slot-scope="s">{{ s.row.warrantyMonths }}</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="80" align="center">
<template slot-scope="s">
<el-tag :type="s.row.status === '1' ? 'success' : 'info'" size="mini">{{ s.row.status === '1' ? '已选' : '待选' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template slot-scope="s">
<el-button type="text" size="mini" icon="el-icon-edit" @click="handleEditQuote(s.row)">编辑</el-button>
<el-button type="text" size="mini" icon="el-icon-delete" @click="handleDeleteQuote(s.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- ========== 合同管理 Tab ========== -->
<div v-if="activeTab === 'contracts'">
<div style="margin-bottom:8px;">
<el-button size="small" type="primary" icon="el-icon-plus" @click="handleAddContract">起草合同</el-button>
</div>
<el-table :data="contractList" v-loading="contractLoading" stripe border highlight-current-row size="small">
<el-table-column type="index" label="#" width="45" />
<el-table-column prop="contractNo" label="合同编号" width="140" />
<el-table-column prop="contractName" label="合同名称" min-width="150" />
<el-table-column prop="supplierName" label="供应商" min-width="120" />
<el-table-column prop="totalAmount" label="金额(¥)" width="110" align="right" />
<el-table-column prop="signDate" label="签订日期" width="100" align="center" />
<el-table-column prop="status" label="状态" width="90" align="center">
<template slot-scope="s">
<el-tag :type="contractTag(s.row.status)" size="mini">{{ contractLabel(s.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template slot-scope="s">
<el-button type="text" size="mini" icon="el-icon-edit" @click="handleEditContract(s.row)">编辑</el-button>
<el-button type="text" size="mini" icon="el-icon-delete" @click="handleDeleteContract(s.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- ========== 合同模版 Tab ========== -->
<div v-if="activeTab === 'template'">
<div class="template-content">
<h3 style="text-align:center;margin-bottom:12px;">设备采购合同模版</h3>
<p><b>合同编号</b>__________</p>
<p><b>甲方采购方</b>昆山德睿福成套设备有限公司</p>
<p><b>乙方供货方</b>__________</p>
<p style="margin-top:10px;"><b>设备清单</b></p>
<p>设备名称__________ 规格型号__________ 数量__________ 单价__________</p>
<p><b>技术标准</b></p>
<p>1. 设备设计制造应符合国家相关标准及甲方提供的技术协议要求</p>
<p>2. 安装精度材质要求详见技术附件</p>
<p><b>交货期</b></p>
<p>乙方应于 _____ __ __ 日前将设备交付至甲方指定地点每延迟1天乙方应按合同总金额的 <b>0.5%</b> 向甲方支付违约金</p>
<p><b>质量检验</b></p>
<p>1. 乙方应在每个制造阶段向甲方提交进度报告及相关检验数据</p>
<p>2. 未按合同要求上传检验报告的每项处以合同金额 <b>1%</b> 的罚款</p>
<p>3. 设备到场后甲方按技术协议进行验收不合格设备乙方应无偿整改</p>
<p><b>质保期</b></p>
<p>设备质保期为投产之日起 <b>12个月</b> 或货到之日起 <b>18个月</b>以先到者为准</p>
<p><b>付款方式</b></p>
<p>1. 合同签订后支付 <b>30%</b> 预付款</p>
<p>2. 设备制造完成预验收合格后支付 <b>30%</b></p>
<p>3. 设备到场安装调试验收合格后支付 <b>35%</b></p>
<p>4. 质保期满后支付剩余 <b>5%</b> 质保金</p>
<p><b>争议解决</b></p>
<p>双方发生争议应协商解决协商不成的提交甲方所在地人民法院诉讼解决</p>
<p style="margin-top:16px;"><b>甲方盖章</b>__________ <b>乙方盖章</b>__________</p>
<p><b>签订日期</b>_____ __ __ </p>
</div>
</div>
<!-- ========== 采购进度 Tab ========== -->
<div v-if="activeTab === 'progress'">
<div class="progress-summary">
<div class="summary-card"><div class="s-label">采购总进度</div><div class="s-val">{{ overallPct }}%</div></div>
<div class="summary-card"><div class="s-label">询价/报价中</div><div class="s-val">{{ inquiringCount }}</div></div>
<div class="summary-card"><div class="s-label">制造/运输中</div><div class="s-val">{{ shippingCount }}</div></div>
<div class="summary-card"><div class="s-label">已到货</div><div class="s-val">{{ arrivedCount }}</div></div>
</div>
<div style="margin-bottom:8px;">
<el-button size="small" type="primary" icon="el-icon-plus" @click="handleAddProgress">新增采购进度</el-button>
</div>
<div v-if="progressList.length === 0" style="text-align:center;padding:40px 0;color:#aaa;">暂无采购进度数据</div>
<div v-for="(p, idx) in progressList" :key="idx" class="progress-item">
<div class="progress-header">
<span><b>{{ p.itemName }}</b> <span style="font-weight:400;font-size:12px;color:#888;">{{ p.supplierName }} {{ p.contractNo ? '· '+p.contractNo : '' }}</span></span>
<div>
<span style="font-size:12px;font-weight:600;margin-right:10px;">{{ progressPct(p) }}%</span>
<el-button type="text" size="mini" icon="el-icon-edit" @click="handleEditProgress(p)">编辑</el-button>
<el-button type="text" size="mini" icon="el-icon-delete" @click="handleDeleteProgress(p)">删除</el-button>
</div>
</div>
<div class="progress-steps">
<template v-for="(stg, si) in parsedStages(p.stages)">
<div v-if="si > 0" :key="'line-'+si" class="step-line" :class="{ done: isStageDone(parsedStages(p.stages), si-1) }" />
<div :key="si" class="step" :class="{ done: stg.done, active: isActiveStage(p, si) }">
<div class="step-circle">{{ stg.done ? '✓' : si+1 }}</div>
<div class="step-label">{{ stg.name }}</div>
<div v-if="stg.date" class="step-date">{{ stg.date.slice(5) }}</div>
</div>
</template>
</div>
<div v-if="p.remark" style="font-size:11px;color:#888;margin-top:4px;">备注{{ p.remark }}</div>
</div>
</div>
</div>
</div>
<!-- ========== Quote Dialog ========== -->
<el-dialog :title="quoteTitle" :visible.sync="quoteVisible" width="500px" append-to-body>
<el-form ref="quoteFormRef" :model="quoteForm" :rules="quoteRules" label-width="100px" size="small">
<el-form-item label="设备项" prop="itemName">
<el-input v-model="quoteForm.itemName" placeholder="如:工作辊轴承座" />
</el-form-item>
<el-form-item label="供应商" prop="supplierName">
<el-input v-model="quoteForm.supplierName" placeholder="供应商全称" />
</el-form-item>
<el-row :gutter="16">
<el-col :span="8"><el-form-item label="数量" prop="qty"><el-input-number v-model="quoteForm.qty" :min="1" style="width:100%" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="单位" prop="unit"><el-input v-model="quoteForm.unit" placeholder="台/套" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="规格" prop="spec"><el-input v-model="quoteForm.spec" placeholder="规格" /></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="8"><el-form-item label="单价(¥)" prop="unitPrice"><el-input-number v-model="quoteForm.unitPrice" :min="0" :precision="2" style="width:100%" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="交货期(天)" prop="deliveryDays"><el-input-number v-model="quoteForm.deliveryDays" :min="0" style="width:100%" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="质保期(月)" prop="warrantyMonths"><el-input-number v-model="quoteForm.warrantyMonths" :min="0" style="width:100%" /></el-form-item></el-col>
</el-row>
<el-form-item label="备注" prop="remark">
<el-input v-model="quoteForm.remark" type="textarea" :rows="2" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button size="small" @click="quoteVisible = false">取消</el-button>
<el-button size="small" type="primary" :loading="quoteSubmitting" @click="handleQuoteSubmit">保存</el-button>
</div>
</el-dialog>
<!-- ========== Contract Dialog ========== -->
<el-dialog :title="contractTitle" :visible.sync="contractVisible" width="560px" append-to-body>
<el-form ref="contractFormRef" :model="contractForm" :rules="contractRules" label-width="100px" size="small">
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="合同编号" prop="contractNo"><el-input v-model="contractForm.contractNo" placeholder="如DRF-CT-2026-001" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="合同名称" prop="contractName"><el-input v-model="contractForm.contractName" /></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="供应商" prop="supplierName"><el-input v-model="contractForm.supplierName" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="金额(¥)" prop="totalAmount"><el-input-number v-model="contractForm.totalAmount" :min="0" :precision="2" style="width:100%" /></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="签订日期" prop="signDate"><el-date-picker v-model="contractForm.signDate" type="date" style="width:100%" value-format="yyyy-MM-dd" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="状态" prop="status"><el-select v-model="contractForm.status" style="width:100%"><el-option label="草稿" value="draft" /><el-option label="审核中" value="review" /><el-option label="已签订" value="signed" /></el-select></el-form-item></el-col>
</el-row>
<el-form-item label="合同条款" prop="clauses">
<el-input v-model="contractForm.clauses" type="textarea" :rows="2" placeholder="主要合同条款" />
</el-form-item>
<el-form-item label="违约罚款" prop="penaltyClause">
<el-input v-model="contractForm.penaltyClause" type="textarea" :rows="2" placeholder="延期/质量违约罚款条款" />
</el-form-item>
<el-form-item label="文件路径" prop="fileUrl">
<el-input v-model="contractForm.fileUrl" placeholder="合同文件 URL" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button size="small" @click="contractVisible = false">取消</el-button>
<el-button size="small" type="primary" :loading="contractSubmitting" @click="handleContractSubmit">保存</el-button>
</div>
</el-dialog>
<!-- ========== Progress Dialog ========== -->
<el-dialog :title="progressTitle" :visible.sync="progressVisible" width="520px" append-to-body>
<el-form ref="progressFormRef" :model="progressForm" :rules="progressRules" label-width="100px" size="small">
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="采购项" prop="itemName"><el-input v-model="progressForm.itemName" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="供应商" prop="supplierName"><el-input v-model="progressForm.supplierName" /></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="合同编号" prop="contractNo"><el-input v-model="progressForm.contractNo" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="金额(¥)" prop="amount"><el-input-number v-model="progressForm.amount" :min="0" :precision="2" style="width:100%" /></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="8"><el-form-item label="下单日期"><el-date-picker v-model="progressForm.orderDate" type="date" style="width:100%" value-format="yyyy-MM-dd" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="预计到货"><el-date-picker v-model="progressForm.expectDate" type="date" style="width:100%" value-format="yyyy-MM-dd" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="实际到货"><el-date-picker v-model="progressForm.actualDate" type="date" style="width:100%" value-format="yyyy-MM-dd" /></el-form-item></el-col>
</el-row>
<el-form-item label="备注" prop="remark">
<el-input v-model="progressForm.remark" type="textarea" :rows="2" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button size="small" @click="progressVisible = false">取消</el-button>
<el-button size="small" type="primary" :loading="progressSubmitting" @click="handleProgressSubmit">保存</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listQuoteAll, getQuote, addQuote, updateQuote, delQuote } from '@/api/rm/quote'
import { listContractAll, getContract, addContract, updateContract, delContract } from '@/api/rm/contract'
import { listProcProgressAll, getProcProgress, addProcProgress, updateProcProgress, delProcProgress } from '@/api/rm/procProgress'
import { listProject } from '@/api/rm/project'
const PROC_STAGES = ['询价', '报价', '比价', '合同签订', '下单', '制造中', '已发货', '已到货']
export default {
name: 'RmProcurement',
data() {
return {
activeTab: 'quotes',
currentProjectId: null,
// Quotes
quoteLoading: false, quoteList: [], quoteVisible: false, quoteTitle: '', quoteSubmitting: false,
quoteForm: {}, quoteRules: { itemName: [{ required: true, message: '请填写设备项名称', trigger: 'blur' }], supplierName: [{ required: true, message: '请填写供应商', trigger: 'blur' }] },
// Contracts
contractLoading: false, contractList: [], contractVisible: false, contractTitle: '', contractSubmitting: false,
contractForm: {}, contractRules: { contractNo: [{ required: true, message: '请填写合同编号', trigger: 'blur' }] },
// Progress
progressList: [], progressVisible: false, progressTitle: '', progressSubmitting: false,
progressForm: {}, progressRules: { itemName: [{ required: true, message: '请填写采购项名称', trigger: 'blur' }] }
}
},
computed: {
overallPct() {
if (!this.progressList.length) return 0
const total = this.progressList.reduce((s, p) => {
const st = this.parsedStages(p.stages)
return s + (st.length || 1)
}, 0)
const done = this.progressList.reduce((s, p) => {
const st = this.parsedStages(p.stages)
return s + st.filter(x => x.done).length
}, 0)
return total ? Math.round(done / total * 100) : 0
},
inquiringCount() {
return this.progressList.filter(p => {
const st = this.parsedStages(p.stages)
return !st.length || !st[0].done
}).length
},
shippingCount() {
return this.progressList.filter(p => {
const st = this.parsedStages(p.stages)
return st.length > 4 && st[4].done && (!st[7] || !st[7].done)
}).length
},
arrivedCount() {
return this.progressList.filter(p => {
const st = this.parsedStages(p.stages)
return st.length > 7 && st[7].done
}).length
}
},
created() { this.loadCurrentProject() },
methods: {
loadCurrentProject() {
listProject({ pageNum: 1, pageSize: 1 }).then(res => {
const rows = res.rows || []
if (rows.length > 0) {
this.currentProjectId = rows[0].projectId
this.loadAll()
}
})
},
loadAll() {
this.loadQuotes(); this.loadContracts(); this.loadProgress()
},
handleTabClick() { this.loadAll() },
parsedStages(stagesStr) {
if (!stagesStr) return []
try { return JSON.parse(stagesStr) } catch { return [] }
},
progressPct(p) {
const st = this.parsedStages(p.stages)
if (!st.length) return 0
return Math.round(st.filter(x => x.done).length / st.length * 100)
},
isStageDone(stages, idx) {
return stages[idx] && stages[idx].done
},
isActiveStage(p, si) {
const st = this.parsedStages(p.stages)
return !st[si].done && (si === 0 || st[si - 1].done)
},
// === Quotes ===
loadQuotes() {
if (!this.currentProjectId) return
this.quoteLoading = true
listQuoteAll({ projectId: this.currentProjectId }).then(r => { this.quoteList = r.data || [] }).finally(() => { this.quoteLoading = false })
},
handleAddQuote() {
if (!this.currentProjectId) { this.$message.warning('请先在项目总览中创建项目'); return }
this.quoteTitle = '新增报价'; this.quoteForm = { projectId: this.currentProjectId, itemName: '', supplierName: '', spec: '', qty: 1, unit: '台', unitPrice: 0, totalPrice: 0, deliveryDays: 30, warrantyMonths: 12, remark: '' }
this.quoteVisible = true; this.$nextTick(() => { this.$refs.quoteFormRef?.clearValidate() })
},
handleEditQuote(row) {
this.quoteTitle = '编辑报价'
getQuote(row.quoteId).then(r => { this.quoteForm = r.data; this.quoteVisible = true; this.$nextTick(() => { this.$refs.quoteFormRef?.clearValidate() }) })
},
handleDeleteQuote(row) {
this.$confirm('确认删除此报价?', '提示', { type: 'warning' }).then(() => { delQuote(row.quoteId).then(() => { this.$message.success('删除成功'); this.loadQuotes() }) })
},
handleQuoteSubmit() {
this.$refs.quoteFormRef.validate(v => {
if (!v) return; this.quoteSubmitting = true
const fd = { ...this.quoteForm, totalPrice: this.quoteForm.unitPrice * this.quoteForm.qty }
const api = fd.quoteId ? updateQuote : addQuote
api(fd).then(() => { this.$message.success('保存成功'); this.quoteVisible = false; this.loadQuotes() }).finally(() => { this.quoteSubmitting = false })
})
},
// === Contracts ===
loadContracts() {
if (!this.currentProjectId) return
this.contractLoading = true
listContractAll({ projectId: this.currentProjectId }).then(r => { this.contractList = r.data || [] }).finally(() => { this.contractLoading = false })
},
handleAddContract() {
if (!this.currentProjectId) { this.$message.warning('请先在项目总览中创建项目'); return }
this.contractTitle = '起草合同'; this.contractForm = { projectId: this.currentProjectId, contractNo: '', contractName: '', supplierName: '', totalAmount: 0, signDate: '', fileUrl: '', status: 'draft', clauses: '', penaltyClause: '', remark: '' }
this.contractVisible = true; this.$nextTick(() => { this.$refs.contractFormRef?.clearValidate() })
},
handleEditContract(row) {
this.contractTitle = '编辑合同'
getContract(row.contractId).then(r => { this.contractForm = r.data; this.contractVisible = true; this.$nextTick(() => { this.$refs.contractFormRef?.clearValidate() }) })
},
handleDeleteContract(row) {
this.$confirm('确认删除此合同?', '提示', { type: 'warning' }).then(() => { delContract(row.contractId).then(() => { this.$message.success('删除成功'); this.loadContracts() }) })
},
handleContractSubmit() {
this.$refs.contractFormRef.validate(v => {
if (!v) return; this.contractSubmitting = true
const api = this.contractForm.contractId ? updateContract : addContract
api(this.contractForm).then(() => { this.$message.success('保存成功'); this.contractVisible = false; this.loadContracts() }).finally(() => { this.contractSubmitting = false })
})
},
contractTag(s) { return { draft: 'info', review: 'warning', signed: 'success' }[s] || 'info' },
contractLabel(s) { return { draft: '草稿', review: '审核中', signed: '已签订' }[s] || s },
// === Progress ===
loadProgress() {
if (!this.currentProjectId) return
listProcProgressAll({ projectId: this.currentProjectId }).then(r => {
this.progressList = (r.data || []).map(p => {
if (!p.stages) p.stages = JSON.stringify(PROC_STAGES.map((name, i) => ({ name, done: i === 0 && p.currentStage === '0' ? true : i < parseInt(p.currentStage || '0') ? true : false, date: '' })))
return p
})
})
},
handleAddProgress() {
if (!this.currentProjectId) { this.$message.warning('请先在项目总览中创建项目'); return }
this.progressTitle = '新增采购进度'
this.progressForm = { projectId: this.currentProjectId, itemName: '', supplierName: '', contractNo: '', amount: 0, orderDate: '', expectDate: '', actualDate: '', currentStage: '0', stages: JSON.stringify(PROC_STAGES.map(n => ({ name: n, done: false, date: '' }))), remark: '' }
this.progressVisible = true; this.$nextTick(() => { this.$refs.progressFormRef?.clearValidate() })
},
handleEditProgress(row) {
this.progressTitle = '编辑采购进度'
getProcProgress(row.progressId).then(r => { this.progressForm = r.data; this.progressVisible = true; this.$nextTick(() => { this.$refs.progressFormRef?.clearValidate() }) })
},
handleDeleteProgress(row) {
this.$confirm('确认删除此采购进度?', '提示', { type: 'warning' }).then(() => { delProcProgress(row.progressId).then(() => { this.$message.success('删除成功'); this.loadProgress() }) })
},
handleProgressSubmit() {
this.$refs.progressFormRef.validate(v => {
if (!v) return; this.progressSubmitting = true
const api = this.progressForm.progressId ? updateProcProgress : addProcProgress
api(this.progressForm).then(() => { this.$message.success('保存成功'); this.progressVisible = false; this.loadProgress() }).finally(() => { this.progressSubmitting = false })
})
}
}
}
</script>
<style scoped>
.rm-container { padding: 8px; }
.rm-panel { background: #fff; border-radius: 4px; border: 1px solid #d0d7de; }
.rm-panel-header { padding: 8px 12px; font-size: 14px; font-weight: 600; border-bottom: 1px solid #d0d7de; display: flex; align-items: center; justify-content: space-between; }
.rm-panel-body { padding: 8px 12px; }
.dialog-footer { text-align: right; }
.progress-summary { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 12px; }
.summary-card { text-align: center; padding: 12px; background: #f8f9fc; border-radius: 6px; border: 1px solid #e8ecf0; }
.s-label { font-size: 11px; color: #888; }
.s-val { font-size: 22px; font-weight: 700; color: #2176ae; margin-top: 2px; }
.progress-item { background: #f8f9fc; border: 1px solid #e8ecf0; border-radius: 6px; padding: 10px; margin-bottom: 8px; }
.progress-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; font-size: 13px; }
.progress-steps { display: flex; align-items: center; flex-wrap: wrap; gap: 2px; }
.step { display: flex; flex-direction: column; align-items: center; gap: 2px; min-width: 50px; }
.step-circle { width: 24px; height: 24px; border-radius: 50%; background: #e8ecf0; color: #888; font-size: 11px; font-weight: 700; display: flex; align-items: center; justify-content: center; }
.step.done .step-circle { background: #67c23a; color: #fff; }
.step.active .step-circle { background: #2176ae; color: #fff; }
.step-label { font-size: 10px; color: #666; white-space: nowrap; }
.step-date { font-size: 9px; color: #999; }
.step-line { width: 20px; height: 2px; background: #e8ecf0; margin-bottom: 18px; }
.step-line.done { background: #67c23a; }
.template-content { font-size: 13px; line-height: 2; padding: 16px; background: #f8f9fc; border: 1px solid #e8ecf0; border-radius: 6px; }
</style>

View File

@@ -0,0 +1,374 @@
<template>
<div class="dashboard-page">
<!-- Info Cards -->
<div class="dashboard-grid">
<div class="stat-card">
<div class="label">项目名称</div>
<div class="value" style="font-size:14px;color:#1a1a2e;">{{ projectInfo.projectName || '未设置' }}</div>
<div class="sub">
编号: {{ projectInfo.projectNo || '-' }}
<el-button type="text" size="mini" icon="el-icon-edit" @click="openProjectInfoModal" style="color:#2176ae;" />
</div>
</div>
<div class="stat-card green">
<div class="label">已完成阶段</div>
<div class="value">{{ doneCount }}</div>
<div class="sub">/ {{ totalStages }} 个阶段</div>
</div>
<div class="stat-card orange">
<div class="label">进行中</div>
<div class="value">{{ progressCount }}</div>
<div class="sub">个阶段</div>
</div>
<div class="stat-card">
<div class="label">项目整体进度</div>
<div class="value" style="color:#2176ae;">{{ overallProgress }}%</div>
<div class="sub">总包项目全生命周期</div>
</div>
</div>
<div class="dashboard-grid" style="grid-template-columns:repeat(3,1fr);margin-top:10px;">
<div class="stat-card">
<div class="label">关联合同</div>
<div class="value" style="font-size:13px;">{{ contractCount }}</div>
<div class="sub">{{ contractNosText }}</div>
</div>
<div class="stat-card">
<div class="label">项目日期</div>
<div class="value" style="font-size:13px;color:#1a1a2e;">{{ projectInfo.startDate || '未设置' }} ~ {{ projectInfo.endDate || '未设置' }}</div>
<div class="sub">项目经理: {{ projectInfo.manager || '未设置' }}</div>
</div>
<div class="stat-card">
<div class="label">客户</div>
<div class="value" style="font-size:13px;color:#1a1a2e;">{{ projectInfo.clientName || '未设置' }}</div>
<div class="sub">点击编辑项目信息</div>
</div>
</div>
<!-- Progress Bar -->
<div class="progress-bar-wrap">
<div class="title">📊 项目全生命周期进度</div>
<div class="progress-steps">
<template v-for="(stage, index) in stages">
<div :key="stage.key" :class="['progress-step', stageClass(stage.key)]">
<div class="circle">{{ stageIcon(stage.key) }}</div>
<div class="step-label">{{ stage.label }}</div>
</div>
<div :key="'line-'+index" :class="['progress-line', stageLineClass(stage.key)]" v-if="index < stages.length - 1" />
</template>
</div>
</div>
<!-- Stage Overview -->
<div style="font-size:12px;font-weight:600;margin-bottom:8px;">📋 各阶段概览点击进入</div>
<div class="dashboard-grid">
<div v-for="stage in stages" :key="stage.key" class="stat-card" style="cursor:pointer;" @click="switchStage(stage.key)">
<div class="label">{{ stage.icon }} {{ stage.label }}</div>
<div class="value" style="font-size:14px;">{{ stageStatusText(stage.key) }}</div>
<div class="sub">点击进入管理</div>
</div>
</div>
<!-- Project Info Edit Modal -->
<el-dialog title="项目信息设置" :visible.sync="dialogVisible" width="600px" append-to-body>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px" size="small">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="项目名称" prop="projectName">
<el-input v-model="form.projectName" placeholder="请输入项目名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目编号" prop="projectNo">
<el-input v-model="form.projectNo" placeholder="如DRF-2026-A01" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="客户名称" prop="clientName">
<el-input v-model="form.clientName" placeholder="请输入客户名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目经理" prop="manager">
<el-input v-model="form.manager" placeholder="请输入项目经理" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="开始日期" prop="startDate">
<el-date-picker v-model="form.startDate" type="date" placeholder="选择开始日期" value-format="yyyy-MM-dd" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束日期" prop="endDate">
<el-date-picker v-model="form.endDate" type="date" placeholder="选择结束日期" value-format="yyyy-MM-dd" style="width:100%" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="handleSubmit">保存项目信息</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { getProject, updateProject, listProject, addProject } from '@/api/rm/project'
const STAGES = [
{ key: 'budget', label: '项目预算', icon: '💰' },
{ key: 'tech_plan', label: '技术方案', icon: '📋' },
{ key: 'layout', label: '布局图', icon: '🗺️' },
{ key: 'tech_review', label: '技术审查', icon: '🔍' },
{ key: 'drawing_design', label: '图纸设计', icon: '📐' },
{ key: 'drawing_review', label: '图纸审查', icon: '✏️' },
{ key: 'procurement', label: '采购管理', icon: '🛒' },
{ key: 'manufacturing', label: '制造进度', icon: '🏭' },
{ key: 'drawing_compare', label: '图纸比较', icon: '🔄' },
{ key: 'doc_lib', label: '资料库', icon: '📁' },
{ key: 'site_mod', label: '现场修改', icon: '🔧' },
{ key: 'shipping', label: '发货清单', icon: '📦' },
{ key: 'manuals', label: '说明书', icon: '📖' },
{ key: 'install_prep', label: '安装准备', icon: '🛠️' },
{ key: 'install_feedback', label: '问题反馈', icon: '💬' },
{ key: 'acceptance', label: '安装验收', icon: '✅' },
{ key: 'hot_commissioning', label: '热负荷试车', icon: '🔥' }
]
export default {
name: 'RmProjectDashboard',
data() {
return {
stages: STAGES,
projectInfo: {},
stageStatus: {},
dialogVisible: false,
submitting: false,
form: {},
rules: {
projectName: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
projectNo: [{ required: true, message: '请输入项目编号', trigger: 'blur' }],
clientName: [{ required: true, message: '请输入客户名称', trigger: 'blur' }]
},
// Default project will be fetched from backend
currentProjectId: null
}
},
computed: {
totalStages() {
return this.stages.length
},
doneCount() {
return Object.values(this.stageStatus).filter(v => v === 'done').length
},
progressCount() {
return Object.values(this.stageStatus).filter(v => v === 'progress').length
},
overallProgress() {
if (this.totalStages === 0) return 0
return Math.round((this.doneCount / this.totalStages) * 100)
},
contractCount() {
return '0份'
},
contractNosText() {
return '暂无关联合同'
}
},
created() {
this.loadProject()
},
methods: {
loadProject() {
// Try to get the first project as default
listProject({ pageNum: 1, pageSize: 1 }).then(res => {
const rows = res.rows || []
if (rows.length > 0) {
this.currentProjectId = rows[0].projectId
return getProject(rows[0].projectId)
} else {
// No project exists yet, show empty form
this.projectInfo = {}
return null
}
}).then(res => {
if (res && res.data) {
this.projectInfo = res.data
}
})
},
openProjectInfoModal() {
// If no project exists, check if we need to create one first
if (!this.currentProjectId) {
this.$confirm('暂无项目数据,是否先创建一个默认项目?', '提示', { type: 'info' }).then(() => {
addProject({
projectName: '1380mm六辊可逆轧机设备总包项目',
projectNo: 'DRF-2026-001',
clientName: '昆山德睿福成套设备有限公司',
startDate: '2026-06-01',
endDate: '2027-02-28',
manager: '工程师',
remark: ''
}).then(res => {
this.$message.success('默认项目已创建')
this.loadProject()
})
})
return
}
this.form = { ...this.projectInfo }
this.dialogVisible = true
this.$nextTick(() => { this.$refs.formRef?.clearValidate() })
},
handleSubmit() {
this.$refs.formRef.validate(valid => {
if (!valid) return
this.submitting = true
this.form.projectId = this.currentProjectId
updateProject(this.form).then(() => {
this.$message.success('项目信息已更新')
this.dialogVisible = false
Object.assign(this.projectInfo, this.form)
}).finally(() => {
this.submitting = false
})
})
},
stageClass(key) {
const st = this.stageStatus[key] || 'pending'
return st === 'done' ? 'done' : st === 'progress' ? 'active' : ''
},
stageLineClass(key) {
return (this.stageStatus[key] || 'pending') === 'done' ? 'done' : ''
},
stageIcon(key) {
const st = this.stageStatus[key] || 'pending'
return st === 'done' ? '✓' : (STAGES.findIndex(s => s.key === key) + 1)
},
stageStatusText(key) {
const map = { done: '已完成', progress: '进行中', pending: '未开始' }
return map[this.stageStatus[key]] || '未开始'
},
switchStage(key) {
this.$message.info(`"${STAGES.find(s => s.key === key).label}" 功能开发中`)
}
}
}
</script>
<style scoped>
.dashboard-page {
padding: 12px 16px;
background: #f0f2f5;
min-height: calc(100vh - 84px);
}
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 10px;
margin-bottom: 12px;
}
.stat-card {
background: #fff;
border-radius: 5px;
padding: 10px 14px;
border: 1px solid #d0d7de;
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
transition: box-shadow 0.15s;
}
.stat-card:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.stat-card .label {
font-size: 11px;
color: #666;
margin-bottom: 4px;
}
.stat-card .value {
font-size: 20px;
font-weight: 700;
color: #2176ae;
}
.stat-card .sub {
font-size: 10px;
color: #666;
margin-top: 2px;
}
.stat-card.green .value { color: #27ae60; }
.stat-card.orange .value { color: #f39c12; }
.stat-card.red .value { color: #e74c3c; }
/* Progress Bar */
.progress-bar-wrap {
background: #fff;
border-radius: 5px;
padding: 10px 14px;
margin-bottom: 12px;
border: 1px solid #d0d7de;
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
}
.progress-bar-wrap .title {
font-size: 12px;
font-weight: 600;
margin-bottom: 8px;
}
.progress-steps {
display: flex;
gap: 0;
align-items: center;
overflow-x: auto;
padding-bottom: 4px;
}
.progress-step {
display: flex;
flex-direction: column;
align-items: center;
min-width: 55px;
position: relative;
}
.progress-step .circle {
width: 24px;
height: 24px;
border-radius: 50%;
background: #ddd;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: 600;
flex-shrink: 0;
z-index: 1;
}
.progress-step.done .circle { background: #27ae60; }
.progress-step.active .circle { background: #2176ae; }
.progress-step .step-label {
font-size: 9px;
color: #666;
margin-top: 2px;
text-align: center;
max-width: 55px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.progress-line {
height: 2px;
background: #ddd;
flex: 1;
min-width: 10px;
}
.progress-line.done { background: #27ae60; }
.dialog-footer {
text-align: right;
}
</style>

View File

@@ -0,0 +1,198 @@
<template>
<div class="rm-container">
<div class="rm-panel">
<div class="rm-panel-header">
<span>技术方案确定</span>
<el-button size="small" type="primary" icon="el-icon-plus" @click="handleAdd">新增方案项</el-button>
</div>
<div class="rm-panel-body">
<!-- Overview row (matching prototype) -->
<div class="overview-row">
<div class="overview-item">
<label>方案整体状态</label>
<el-select v-model="overviewStatus" size="small" style="width:140px" @change="loadList">
<el-option label="未开始" value="pending" />
<el-option label="进行中" value="progress" />
<el-option label="已完成" value="done" />
</el-select>
</div>
<div class="overview-item" style="flex:1;">
<label>方案概述</label>
<el-input v-model="overviewDesc" size="small" placeholder="技术方案总体描述" />
</div>
</div>
<!-- Search -->
<div class="search-row">
<el-input v-model="query.itemName" size="small" placeholder="方案名称" clearable style="width:160px" @keyup.enter="handleSearch" />
<el-select v-model="query.status" size="small" placeholder="状态" clearable style="width:110px">
<el-option label="未开始" value="pending" />
<el-option label="进行中" value="progress" />
<el-option label="已完成" value="done" />
</el-select>
<el-button size="small" type="primary" icon="el-icon-search" @click="handleSearch">查询</el-button>
<el-button size="small" icon="el-icon-refresh" @click="handleReset">重置</el-button>
</div>
<!-- Table -->
<el-table :data="list" v-loading="loading" stripe border highlight-current-row size="small">
<el-table-column type="index" label="#" width="45" />
<el-table-column prop="itemName" label="方案名称" min-width="160" />
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
<el-table-column prop="owner" label="负责人" width="90" />
<el-table-column prop="status" label="状态" width="90" align="center">
<template slot-scope="scope">
<el-tag :type="statusTag(scope.row.status)" size="mini">{{ statusLabel(scope.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template slot-scope="scope">
<el-button type="text" size="mini" icon="el-icon-edit" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="text" size="mini" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-row">
<el-pagination :current-page="query.pageNum" :page-sizes="[10, 20, 50, 100]" :page-size="query.pageSize"
:total="total" layout="total, sizes, prev, pager, next, jumper" size="small"
@size-change="handleSizeChange" @current-change="handlePageChange" />
</div>
</div>
</div>
<!-- Add/Edit Dialog -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="500px" append-to-body>
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px" size="small">
<el-form-item label="方案名称" prop="itemName">
<el-input v-model="form.itemName" placeholder="如:主轧机技术方案" />
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input v-model="form.description" type="textarea" :rows="3" placeholder="方案详细描述" />
</el-form-item>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="负责人" prop="owner">
<el-input v-model="form.owner" placeholder="负责人姓名" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" placeholder="请选择" style="width:100%">
<el-option label="未开始" value="pending" />
<el-option label="进行中" value="progress" />
<el-option label="已完成" value="done" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="附件" prop="attachmentUrl">
<el-input v-model="form.attachmentUrl" placeholder="附件路径或 URL" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="备注信息" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button size="small" @click="dialogVisible = false">取消</el-button>
<el-button size="small" type="primary" :loading="submitting" @click="handleSubmit">保存</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listTechPlan, getTechPlan, addTechPlan, updateTechPlan, delTechPlan } from '@/api/rm/techPlan'
import { listProject } from '@/api/rm/project'
export default {
name: 'RmTechPlan',
data() {
return {
loading: false,
list: [],
total: 0,
currentProjectId: null,
overviewStatus: 'pending',
overviewDesc: '',
query: { pageNum: 1, pageSize: 10, itemName: undefined, status: undefined, projectId: undefined },
dialogVisible: false,
dialogTitle: '',
submitting: false,
form: {},
rules: { itemName: [{ required: true, message: '请填写方案名称', trigger: 'blur' }] }
}
},
created() { this.loadCurrentProject() },
methods: {
loadCurrentProject() {
listProject({ pageNum: 1, pageSize: 1 }).then(res => {
const rows = res.rows || []
if (rows.length > 0) {
this.currentProjectId = rows[0].projectId
this.query.projectId = rows[0].projectId
this.loadList()
}
})
},
loadList() {
this.loading = true
listTechPlan(this.query).then(res => {
this.list = res.rows || []
this.total = res.total || 0
}).finally(() => { this.loading = false })
},
handleSearch() { this.query.pageNum = 1; this.loadList() },
handleReset() { this.query = { pageNum: 1, pageSize: 10 }; this.loadList() },
handleSizeChange(val) { this.query.pageSize = val; this.loadList() },
handlePageChange(val) { this.query.pageNum = val; this.loadList() },
handleAdd() {
this.dialogTitle = '新增方案项'
if (!this.currentProjectId) { this.$message.warning('请先在项目总览中创建项目'); return }
this.form = { projectId: this.currentProjectId, itemName: '', status: 'pending', description: '', owner: '', attachmentUrl: '', remark: '' }
this.dialogVisible = true
this.$nextTick(() => { this.$refs.formRef?.clearValidate() })
},
handleEdit(row) {
this.dialogTitle = '编辑方案项'
getTechPlan(row.planItemId).then(res => {
this.form = res.data
this.dialogVisible = true
this.$nextTick(() => { this.$refs.formRef?.clearValidate() })
})
},
handleDelete(row) {
this.$confirm(`确认删除 "${row.itemName}"`, '提示', { type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消' }).then(() => {
delTechPlan(row.planItemId).then(() => { this.$message.success('删除成功'); this.loadList() })
})
},
handleSubmit() {
this.$refs.formRef.validate(valid => {
if (!valid) return
this.submitting = true
const api = this.form.planItemId ? updateTechPlan : addTechPlan
api(this.form).then(() => {
this.$message.success(this.form.planItemId ? '更新成功' : '新增成功')
this.dialogVisible = false
if (!this.form.planItemId) this.query.pageNum = 1
this.loadList()
}).finally(() => { this.submitting = false })
})
},
statusTag(s) { return { pending: 'info', progress: 'warning', done: 'success' }[s] || 'info' },
statusLabel(s) { return { pending: '未开始', progress: '进行中', done: '已完成' }[s] || s }
}
}
</script>
<style scoped>
.rm-container { padding: 8px; }
.rm-panel { background: #fff; border-radius: 4px; border: 1px solid #d0d7de; }
.rm-panel-header { padding: 8px 12px; font-size: 14px; font-weight: 600; border-bottom: 1px solid #d0d7de; display: flex; align-items: center; justify-content: space-between; }
.rm-panel-body { padding: 8px 12px; }
.overview-row { display: flex; gap: 12px; align-items: flex-end; margin-bottom: 10px; }
.overview-item label { display: block; font-size: 12px; color: #666; margin-bottom: 3px; }
.search-row { display: flex; gap: 6px; align-items: center; margin-bottom: 8px; flex-wrap: wrap; }
.pagination-row { margin-top: 10px; display: flex; justify-content: flex-end; }
.dialog-footer { text-align: right; }
</style>

View File

@@ -0,0 +1,336 @@
<template>
<div class="rm-container">
<div class="rm-panel">
<div class="rm-panel-header">
<span>技术审查</span>
</div>
<div class="rm-panel-body">
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane label="机械审查" name="mechanical" />
<el-tab-pane label="电气审查" name="electrical" />
<el-tab-pane label="液压审查" name="hydraulic" />
<el-tab-pane label="流体审查" name="fluid" />
<el-tab-pane label="能源介质审查" name="energy" />
<el-tab-pane label="色卡确定" name="color" />
</el-tabs>
<!-- Review Items Tab -->
<div v-if="activeTab !== 'color'">
<div style="margin-bottom:8px;">
<el-button size="small" type="primary" icon="el-icon-plus" @click="handleAdd">新增{{ tabLabel(activeTab) }}</el-button>
</div>
<el-table :data="reviewList" v-loading="loading" stripe border highlight-current-row size="small">
<el-table-column type="index" label="#" width="45" />
<el-table-column prop="itemName" label="审查项目" min-width="140" />
<el-table-column prop="reviewer" label="审查人" width="80" />
<el-table-column prop="reviewDate" label="审查日期" width="100" align="center" />
<el-table-column prop="conclusion" label="状态" width="90" align="center">
<template slot-scope="scope">
<el-tag :type="conclusionTag(scope.row.conclusion)" size="mini">{{ conclusionLabel(scope.row.conclusion) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="reviewOpinion" label="审查意见" min-width="140" show-overflow-tooltip />
<el-table-column prop="thinking" label="思维导入" min-width="120" show-overflow-tooltip />
<el-table-column label="操作" width="120" fixed="right">
<template slot-scope="scope">
<el-button type="text" size="mini" icon="el-icon-edit" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="text" size="mini" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- Color Card Tab -->
<div v-else>
<div style="margin-bottom:8px;">
<el-button size="small" type="primary" icon="el-icon-plus" @click="handleAddColor">新增色卡</el-button>
<span style="font-size:12px;color:#888;margin-left:8px;"> {{ colorCards.length }} 条色卡</span>
</div>
<div v-if="colorCards.length === 0" style="text-align:center;padding:40px 0;color:#aaa;font-size:13px;">暂无色卡点击上方按钮添加</div>
<div v-for="(group, gIdx) in colorGroups" :key="gIdx" class="color-group">
<div class="color-group-title">{{ group.category || '未分类' }}</div>
<div class="color-card-grid">
<div v-for="(card, cIdx) in group.items" :key="cIdx" class="color-card-item">
<div class="color-swatch" :style="{ background: card.hexValue }" />
<div class="color-info">
<div class="color-name">{{ card.colorName }}</div>
<div class="color-hex">{{ card.hexValue }}</div>
<div class="color-standard" v-if="card.standard">{{ card.standardLabel }} {{ card.standard }}</div>
<div class="color-usage" v-if="card.usageDesc">{{ card.usageDesc }}</div>
</div>
<div class="color-actions">
<el-button type="text" size="mini" icon="el-icon-edit" @click="handleEditColor(card)" />
<el-button type="text" size="mini" icon="el-icon-delete" @click="handleDeleteColor(card)" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Add/Edit Review Item Dialog -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="520px" append-to-body>
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px" size="small">
<el-form-item label="审查项目" prop="itemName">
<el-input v-model="form.itemName" placeholder="审查项目名称" />
</el-form-item>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="审查人" prop="reviewer">
<el-input v-model="form.reviewer" placeholder="审查人姓名" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="审查日期" prop="reviewDate">
<el-date-picker v-model="form.reviewDate" type="date" placeholder="选择日期" style="width:100%" value-format="yyyy-MM-dd" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="结论" prop="conclusion">
<el-select v-model="form.conclusion" placeholder="请选择" style="width:100%">
<el-option label="待审查" value="pending" />
<el-option label="通过" value="approved" />
<el-option label="需整改" value="rectify" />
</el-select>
</el-form-item>
<el-form-item label="审查意见" prop="reviewOpinion">
<el-input v-model="form.reviewOpinion" type="textarea" :rows="2" placeholder="审查意见" />
</el-form-item>
<el-form-item label="思维导入" prop="thinking">
<el-input v-model="form.thinking" type="textarea" :rows="2" placeholder="思考备注" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="1" placeholder="备注信息" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button size="small" @click="dialogVisible = false">取消</el-button>
<el-button size="small" type="primary" :loading="submitting" @click="handleSubmit">保存</el-button>
</div>
</el-dialog>
<!-- Color Card Dialog -->
<el-dialog :title="colorDialogTitle" :visible.sync="colorDialogVisible" width="520px" append-to-body>
<el-form ref="colorFormRef" :model="colorForm" :rules="colorRules" label-width="100px" size="small">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="部位/名称" prop="colorName">
<el-input v-model="colorForm.colorName" placeholder="如:轧机牌坊" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="HEX颜色值" prop="hexValue">
<el-input v-model="colorForm.hexValue" placeholder="#C8CBCE">
<template slot="prepend">
<span class="color-preview" :style="{ background: colorForm.hexValue, display:'inline-block', width:16, height:16, borderRadius:2, verticalAlign:'middle' }" />
</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="标准色号" prop="standard">
<el-input v-model="colorForm.standard" placeholder="如 RAL 5005" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="标准类型" prop="standardLabel">
<el-select v-model="colorForm.standardLabel" placeholder="请选择" style="width:100%">
<el-option label="RAL" value="RAL" />
<el-option label="Pantone" value="Pantone" />
<el-option label="国标" value="GB" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="分类" prop="category">
<el-select v-model="colorForm.category" placeholder="请选择" style="width:100%">
<el-option label="机械-被动" value="机械-被动" />
<el-option label="机械-动力" value="机械-动力" />
<el-option label="流体管路" value="流体管路" />
<el-option label="液压缸" value="液压缸" />
<el-option label="电机" value="电机" />
<el-option label="减速机" value="减速机" />
</el-select>
</el-form-item>
<el-form-item label="用途说明" prop="usageDesc">
<el-input v-model="colorForm.usageDesc" placeholder="用途说明" />
</el-form-item>
<el-form-item label="详细描述" prop="description">
<el-input v-model="colorForm.description" type="textarea" :rows="2" placeholder="详细描述" />
</el-form-item>
<el-form-item label="排序号" prop="sortOrder">
<el-input-number v-model="colorForm.sortOrder" :min="0" size="small" style="width:100%" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button size="small" @click="colorDialogVisible = false">取消</el-button>
<el-button size="small" type="primary" :loading="colorSubmitting" @click="handleColorSubmit">保存</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listTechReviewAll, getTechReview, addTechReview, updateTechReview, delTechReview } from '@/api/rm/techReview'
import { listColorCardAll, addColorCard, updateColorCard, delColorCard } from '@/api/rm/colorCard'
import { listProject } from '@/api/rm/project'
const TAB_TYPES = { mechanical: 'mechanical', electrical: 'electrical', hydraulic: 'hydraulic', fluid: 'fluid', energy: 'energy' }
const TAB_LABELS = { mechanical: '机械审查', electrical: '电气审查', hydraulic: '液压审查', fluid: '流体审查', energy: '能源介质审查' }
export default {
name: 'RmTechReview',
data() {
return {
activeTab: 'mechanical',
loading: false,
reviewList: [],
currentProjectId: null,
dialogVisible: false,
dialogTitle: '',
submitting: false,
form: {},
rules: { itemName: [{ required: true, message: '请填写审查项目名称', trigger: 'blur' }] },
colorCards: [],
colorDialogVisible: false,
colorDialogTitle: '',
colorSubmitting: false,
colorForm: {},
colorRules: { colorName: [{ required: true, message: '请填写色名', trigger: 'blur' }] }
}
},
computed: {
colorGroups() {
const groups = {}
this.colorCards.forEach(c => {
const key = c.category || '未分类'
if (!groups[key]) groups[key] = { category: c.category, items: [] }
groups[key].items.push(c)
})
return Object.values(groups)
}
},
created() { this.loadCurrentProject() },
methods: {
tabLabel(key) { return TAB_LABELS[key] || key },
loadCurrentProject() {
listProject({ pageNum: 1, pageSize: 1 }).then(res => {
const rows = res.rows || []
if (rows.length > 0) {
this.currentProjectId = rows[0].projectId
this.loadReviews()
this.loadColors()
}
})
},
loadReviews() {
if (!this.currentProjectId) return
this.loading = true
const type = TAB_TYPES[this.activeTab]
listTechReviewAll({ projectId: this.currentProjectId, reviewType: type }).then(res => {
this.reviewList = res.data || []
}).finally(() => { this.loading = false })
},
loadColors() {
if (!this.currentProjectId) return
listColorCardAll({ projectId: this.currentProjectId }).then(res => {
this.colorCards = res.data || []
})
},
handleTabClick() {
if (this.activeTab === 'color') {
this.loadColors()
} else {
this.loadReviews()
}
},
handleAdd() {
this.dialogTitle = '新增' + this.tabLabel(this.activeTab) + '项'
if (!this.currentProjectId) { this.$message.warning('请先在项目总览中创建项目'); return }
this.form = { projectId: this.currentProjectId, reviewType: TAB_TYPES[this.activeTab], itemName: '', reviewer: '', reviewDate: '', conclusion: 'pending', reviewOpinion: '', thinking: '', remark: '' }
this.dialogVisible = true
this.$nextTick(() => { this.$refs.formRef?.clearValidate() })
},
handleEdit(row) {
this.dialogTitle = '编辑审查项'
getTechReview(row.reviewItemId).then(res => {
this.form = res.data
this.dialogVisible = true
this.$nextTick(() => { this.$refs.formRef?.clearValidate() })
})
},
handleDelete(row) {
this.$confirm(`确认删除 "${row.itemName}"`, '提示', { type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消' }).then(() => {
delTechReview(row.reviewItemId).then(() => { this.$message.success('删除成功'); this.loadReviews() })
})
},
handleSubmit() {
this.$refs.formRef.validate(valid => {
if (!valid) return
this.submitting = true
const api = this.form.reviewItemId ? updateTechReview : addTechReview
api(this.form).then(() => {
this.$message.success(this.form.reviewItemId ? '更新成功' : '新增成功')
this.dialogVisible = false
this.loadReviews()
}).finally(() => { this.submitting = false })
})
},
handleAddColor() {
this.colorDialogTitle = '新增色卡'
this.colorForm = { projectId: this.currentProjectId, colorName: '', hexValue: '#C8CBCE', standard: '', standardLabel: 'RAL', category: '机械-被动', usageDesc: '', description: '', sortOrder: 0 }
this.colorDialogVisible = true
this.$nextTick(() => { this.$refs.colorFormRef?.clearValidate() })
},
handleEditColor(row) {
this.colorDialogTitle = '编辑色卡'
this.colorForm = { ...row }
this.colorDialogVisible = true
this.$nextTick(() => { this.$refs.colorFormRef?.clearValidate() })
},
handleDeleteColor(row) {
this.$confirm(`确认删除色卡 "${row.colorName}"`, '提示', { type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消' }).then(() => {
delColorCard(row.colorCardId).then(() => { this.$message.success('删除成功'); this.loadColors() })
})
},
handleColorSubmit() {
this.$refs.colorFormRef.validate(valid => {
if (!valid) return
this.colorSubmitting = true
const api = this.colorForm.colorCardId ? updateColorCard : addColorCard
api(this.colorForm).then(() => {
this.$message.success(this.colorForm.colorCardId ? '更新成功' : '新增成功')
this.colorDialogVisible = false
this.loadColors()
}).finally(() => { this.colorSubmitting = false })
})
},
conclusionTag(s) { return { pending: 'info', approved: 'success', rectify: 'warning' }[s] || 'info' },
conclusionLabel(s) { return { pending: '待审查', approved: '通过', rectify: '需整改' }[s] || s }
}
}
</script>
<style scoped>
.rm-container { padding: 8px; }
.rm-panel { background: #fff; border-radius: 4px; border: 1px solid #d0d7de; }
.rm-panel-header { padding: 8px 12px; font-size: 14px; font-weight: 600; border-bottom: 1px solid #d0d7de; display: flex; align-items: center; justify-content: space-between; }
.rm-panel-body { padding: 8px 12px; }
.dialog-footer { text-align: right; }
.color-group { margin-bottom: 16px; }
.color-group-title { font-size: 13px; font-weight: 600; padding: 6px 0; border-bottom: 1px solid #eee; margin-bottom: 8px; }
.color-card-grid { display: flex; flex-wrap: wrap; gap: 10px; }
.color-card-item { display: flex; align-items: center; gap: 10px; width: calc(33.33% - 10px); padding: 8px; border: 1px solid #eee; border-radius: 4px; background: #fafbfc; position: relative; min-width: 240px; }
.color-swatch { width: 40px; height: 40px; border-radius: 4px; border: 1px solid #ddd; flex-shrink: 0; }
.color-info { flex: 1; min-width: 0; }
.color-name { font-size: 13px; font-weight: 600; }
.color-hex { font-size: 11px; color: #888; font-family: monospace; }
.color-standard { font-size: 11px; color: #888; }
.color-usage { font-size: 11px; color: #aaa; }
.color-actions { position: absolute; top: 4px; right: 4px; display: flex; gap: 2px; }
.color-preview { border: 1px solid #ddd; }
</style>