设备总包项目管理剩余页面

This commit is contained in:
jhd
2026-06-17 09:29:22 +08:00
parent 690729e266
commit 2f92ef57de
171 changed files with 7592 additions and 113 deletions

View File

@@ -0,0 +1,8 @@
import request from '@/utils/request'
export function listAcceptanceChecklist(query) { return request({ url: '/rm/acceptanceChecklist/list', method: 'get', params: query }) }
export function getAcceptanceChecklist(checkId) { return request({ url: `/rm/acceptanceChecklist/${checkId}`, method: 'get' }) }
export function addAcceptanceChecklist(data) { return request({ url: '/rm/acceptanceChecklist', method: 'post', data }) }
export function updateAcceptanceChecklist(data) { return request({ url: '/rm/acceptanceChecklist', method: 'put', data }) }
export function delAcceptanceChecklist(checkIds) { return request({ url: `/rm/acceptanceChecklist/${checkIds}`, method: 'delete' }) }
export function listAcceptanceChecklistAll(query) { return request({ url: '/rm/acceptanceChecklist/all', method: 'get', params: query }) }

View File

@@ -0,0 +1,8 @@
import request from '@/utils/request'
export function listAcceptanceItem(query) { return request({ url: '/rm/acceptanceItem/list', method: 'get', params: query }) }
export function getAcceptanceItem(acceptItemId) { return request({ url: `/rm/acceptanceItem/${acceptItemId}`, method: 'get' }) }
export function addAcceptanceItem(data) { return request({ url: '/rm/acceptanceItem', method: 'post', data }) }
export function updateAcceptanceItem(data) { return request({ url: '/rm/acceptanceItem', method: 'put', data }) }
export function delAcceptanceItem(acceptItemIds) { return request({ url: `/rm/acceptanceItem/${acceptItemIds}`, method: 'delete' }) }
export function listAcceptanceItemAll(query) { return request({ url: '/rm/acceptanceItem/all', method: 'get', params: query }) }

View File

@@ -0,0 +1,8 @@
import request from '@/utils/request'
export function listCommissioningChecklist(query) { return request({ url: '/rm/commissioningChecklist/list', method: 'get', params: query }) }
export function getCommissioningChecklist(checkId) { return request({ url: `/rm/commissioningChecklist/${checkId}`, method: 'get' }) }
export function addCommissioningChecklist(data) { return request({ url: '/rm/commissioningChecklist', method: 'post', data }) }
export function updateCommissioningChecklist(data) { return request({ url: '/rm/commissioningChecklist', method: 'put', data }) }
export function delCommissioningChecklist(checkIds) { return request({ url: `/rm/commissioningChecklist/${checkIds}`, method: 'delete' }) }
export function listCommissioningChecklistAll(query) { return request({ url: '/rm/commissioningChecklist/all', method: 'get', params: query }) }

View File

@@ -0,0 +1,8 @@
import request from '@/utils/request'
export function listCommissioningClause(query) { return request({ url: '/rm/commissioningClause/list', method: 'get', params: query }) }
export function getCommissioningClause(clauseId) { return request({ url: `/rm/commissioningClause/${clauseId}`, method: 'get' }) }
export function addCommissioningClause(data) { return request({ url: '/rm/commissioningClause', method: 'post', data }) }
export function updateCommissioningClause(data) { return request({ url: '/rm/commissioningClause', method: 'put', data }) }
export function delCommissioningClause(clauseIds) { return request({ url: `/rm/commissioningClause/${clauseIds}`, method: 'delete' }) }
export function listCommissioningClauseAll(query) { return request({ url: '/rm/commissioningClause/all', method: 'get', params: query }) }

View File

@@ -0,0 +1,3 @@
import request from '@/utils/request'
export function getStageStatus(projectId) { return request({ url: `/rm/dashboard/stageStatus/${projectId}`, method: 'get' }) }

View File

@@ -0,0 +1,8 @@
import request from '@/utils/request'
export function listDocLib(query) { return request({ url: '/rm/docLib/list', method: 'get', params: query }) }
export function getDocLib(docId) { return request({ url: `/rm/docLib/${docId}`, method: 'get' }) }
export function addDocLib(data) { return request({ url: '/rm/docLib', method: 'post', data }) }
export function updateDocLib(data) { return request({ url: '/rm/docLib', method: 'put', data }) }
export function delDocLib(docIds) { return request({ url: `/rm/docLib/${docIds}`, method: 'delete' }) }
export function listDocLibAll(query) { return request({ url: '/rm/docLib/all', method: 'get', params: query }) }

View File

@@ -0,0 +1,8 @@
import request from '@/utils/request'
export function listDrawingCompare(query) { return request({ url: '/rm/drawingCompare/list', method: 'get', params: query }) }
export function getDrawingCompare(compareId) { return request({ url: `/rm/drawingCompare/${compareId}`, method: 'get' }) }
export function addDrawingCompare(data) { return request({ url: '/rm/drawingCompare', method: 'post', data }) }
export function updateDrawingCompare(data) { return request({ url: '/rm/drawingCompare', method: 'put', data }) }
export function delDrawingCompare(compareIds) { return request({ url: `/rm/drawingCompare/${compareIds}`, method: 'delete' }) }
export function listDrawingCompareAll(query) { return request({ url: '/rm/drawingCompare/all', method: 'get', params: query }) }

View File

@@ -37,3 +37,11 @@ export function delDrawingReview(reviewIds) {
method: 'delete'
})
}
export function listDrawingReviewAll(query) {
return request({
url: '/rm/drawingReview/all',
method: 'get',
params: query
})
}

View File

@@ -0,0 +1,8 @@
import request from '@/utils/request'
export function listInstallFeedback(query) { return request({ url: '/rm/installFeedback/list', method: 'get', params: query }) }
export function getInstallFeedback(feedbackId) { return request({ url: `/rm/installFeedback/${feedbackId}`, method: 'get' }) }
export function addInstallFeedback(data) { return request({ url: '/rm/installFeedback', method: 'post', data }) }
export function updateInstallFeedback(data) { return request({ url: '/rm/installFeedback', method: 'put', data }) }
export function delInstallFeedback(feedbackIds) { return request({ url: `/rm/installFeedback/${feedbackIds}`, method: 'delete' }) }
export function listInstallFeedbackAll(query) { return request({ url: '/rm/installFeedback/all', method: 'get', params: query }) }

View File

@@ -0,0 +1,8 @@
import request from '@/utils/request'
export function listInstallPersonnel(query) { return request({ url: '/rm/installPersonnel/list', method: 'get', params: query }) }
export function getInstallPersonnel(personnelId) { return request({ url: `/rm/installPersonnel/${personnelId}`, method: 'get' }) }
export function addInstallPersonnel(data) { return request({ url: '/rm/installPersonnel', method: 'post', data }) }
export function updateInstallPersonnel(data) { return request({ url: '/rm/installPersonnel', method: 'put', data }) }
export function delInstallPersonnel(personnelIds) { return request({ url: `/rm/installPersonnel/${personnelIds}`, method: 'delete' }) }
export function listInstallPersonnelAll(query) { return request({ url: '/rm/installPersonnel/all', method: 'get', params: query }) }

View File

@@ -0,0 +1,8 @@
import request from '@/utils/request'
export function listInstallPrecision(query) { return request({ url: '/rm/installPrecision/list', method: 'get', params: query }) }
export function getInstallPrecision(precisionId) { return request({ url: `/rm/installPrecision/${precisionId}`, method: 'get' }) }
export function addInstallPrecision(data) { return request({ url: '/rm/installPrecision', method: 'post', data }) }
export function updateInstallPrecision(data) { return request({ url: '/rm/installPrecision', method: 'put', data }) }
export function delInstallPrecision(precisionIds) { return request({ url: `/rm/installPrecision/${precisionIds}`, method: 'delete' }) }
export function listInstallPrecisionAll(query) { return request({ url: '/rm/installPrecision/all', method: 'get', params: query }) }

View File

@@ -0,0 +1,8 @@
import request from '@/utils/request'
export function listInstallProgress(query) { return request({ url: '/rm/installProgress/list', method: 'get', params: query }) }
export function getInstallProgress(progressId) { return request({ url: `/rm/installProgress/${progressId}`, method: 'get' }) }
export function addInstallProgress(data) { return request({ url: '/rm/installProgress', method: 'post', data }) }
export function updateInstallProgress(data) { return request({ url: '/rm/installProgress', method: 'put', data }) }
export function delInstallProgress(progressIds) { return request({ url: `/rm/installProgress/${progressIds}`, method: 'delete' }) }
export function listInstallProgressAll(query) { return request({ url: '/rm/installProgress/all', method: 'get', params: query }) }

View File

@@ -0,0 +1,8 @@
import request from '@/utils/request'
export function listInstallTool(query) { return request({ url: '/rm/installTool/list', method: 'get', params: query }) }
export function getInstallTool(toolId) { return request({ url: `/rm/installTool/${toolId}`, method: 'get' }) }
export function addInstallTool(data) { return request({ url: '/rm/installTool', method: 'post', data }) }
export function updateInstallTool(data) { return request({ url: '/rm/installTool', method: 'put', data }) }
export function delInstallTool(toolIds) { return request({ url: `/rm/installTool/${toolIds}`, method: 'delete' }) }
export function listInstallToolAll(query) { return request({ url: '/rm/installTool/all', method: 'get', params: query }) }

View File

@@ -37,3 +37,11 @@ export function delLayoutFile(layoutFileIds) {
method: 'delete'
})
}
export function listLayoutFileAll(query) {
return request({
url: '/rm/layout/all',
method: 'get',
params: query
})
}

View File

@@ -0,0 +1,8 @@
import request from '@/utils/request'
export function listManual(query) { return request({ url: '/rm/manual/list', method: 'get', params: query }) }
export function getManual(manualId) { return request({ url: `/rm/manual/${manualId}`, method: 'get' }) }
export function addManual(data) { return request({ url: '/rm/manual', method: 'post', data }) }
export function updateManual(data) { return request({ url: '/rm/manual', method: 'put', data }) }
export function delManual(manualIds) { return request({ url: `/rm/manual/${manualIds}`, method: 'delete' }) }
export function listManualAll(query) { return request({ url: '/rm/manual/all', method: 'get', params: query }) }

View File

@@ -0,0 +1,47 @@
import request from '@/utils/request'
export function listProjectMember(query) {
return request({
url: '/rm/projectMember/list',
method: 'get',
params: query
})
}
export function getProjectMember(memberId) {
return request({
url: `/rm/projectMember/${memberId}`,
method: 'get'
})
}
export function addProjectMember(data) {
return request({
url: '/rm/projectMember',
method: 'post',
data
})
}
export function updateProjectMember(data) {
return request({
url: '/rm/projectMember',
method: 'put',
data
})
}
export function delProjectMember(memberIds) {
return request({
url: `/rm/projectMember/${memberIds}`,
method: 'delete'
})
}
export function allProjectMember(query) {
return request({
url: '/rm/projectMember/all',
method: 'get',
params: query
})
}

View File

@@ -0,0 +1,8 @@
import request from '@/utils/request'
export function listShippingChecklist(query) { return request({ url: '/rm/shippingChecklist/list', method: 'get', params: query }) }
export function getShippingChecklist(checklistId) { return request({ url: `/rm/shippingChecklist/${checklistId}`, method: 'get' }) }
export function addShippingChecklist(data) { return request({ url: '/rm/shippingChecklist', method: 'post', data }) }
export function updateShippingChecklist(data) { return request({ url: '/rm/shippingChecklist', method: 'put', data }) }
export function delShippingChecklist(checklistIds) { return request({ url: `/rm/shippingChecklist/${checklistIds}`, method: 'delete' }) }
export function listShippingChecklistAll(query) { return request({ url: '/rm/shippingChecklist/all', method: 'get', params: query }) }

View File

@@ -0,0 +1,8 @@
import request from '@/utils/request'
export function listShippingItem(query) { return request({ url: '/rm/shippingItem/list', method: 'get', params: query }) }
export function getShippingItem(itemId) { return request({ url: `/rm/shippingItem/${itemId}`, method: 'get' }) }
export function addShippingItem(data) { return request({ url: '/rm/shippingItem', method: 'post', data }) }
export function updateShippingItem(data) { return request({ url: '/rm/shippingItem', method: 'put', data }) }
export function delShippingItem(itemIds) { return request({ url: `/rm/shippingItem/${itemIds}`, method: 'delete' }) }
export function listShippingItemAll(query) { return request({ url: '/rm/shippingItem/all', method: 'get', params: query }) }

View File

@@ -0,0 +1,13 @@
import request from '@/utils/request'
export function listSiteMod(query) { return request({ url: '/rm/siteMod/list', method: 'get', params: query }) }
export function getSiteMod(modId) { return request({ url: `/rm/siteMod/${modId}`, method: 'get' }) }
export function addSiteMod(data) { return request({ url: '/rm/siteMod', method: 'post', data }) }
export function updateSiteMod(data) { return request({ url: '/rm/siteMod', method: 'put', data }) }
export function delSiteMod(modIds) { return request({ url: `/rm/siteMod/${modIds}`, method: 'delete' }) }
export function listSiteModAll(query) { return request({ url: '/rm/siteMod/all', method: 'get', params: query }) }
export function listSiteModMedia(query) { return request({ url: '/rm/siteModMedia/list', method: 'get', params: query }) }
export function addSiteModMedia(data) { return request({ url: '/rm/siteModMedia', method: 'post', data }) }
export function delSiteModMedia(mediaIds) { return request({ url: `/rm/siteModMedia/${mediaIds}`, method: 'delete' }) }
export function listSiteModMediaAll(query) { return request({ url: '/rm/siteModMedia/all', method: 'get', params: query }) }

View File

@@ -37,3 +37,11 @@ export function delTechPlan(planItemIds) {
method: 'delete'
})
}
export function listTechPlanAll(query) {
return request({
url: '/rm/techPlan/all',
method: 'get',
params: query
})
}

View File

@@ -240,6 +240,17 @@ body {
box-shadow: 0 1px 2px rgba(0, 21, 41, 0.04);
}
.current-project-bar {
background: #e8f4fd;
padding: 6px 14px;
font-size: 12px;
color: #2176ae;
border-bottom: 1px solid #d0e8f8;
margin: -1px -1px 0 -1px;
border-radius: 8px 8px 0 0;
font-weight: 500;
}
// 圆角按钮/输入框
.el-input__inner,
.el-textarea__inner {

View File

@@ -36,7 +36,7 @@ $base-sub-menu-background:#000c17;
$base-sub-menu-hover:#001528;
*/
$base-sidebar-width: 150px;
$base-sidebar-width: 180px;
// the :export directive is the magic sauce for webpack
// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass

View File

@@ -0,0 +1,207 @@
<template>
<div class="rm-container">
<div class="current-project-bar">当前项目{{ currentProjectName }}</div>
<div class="rm-panel">
<div class="rm-panel-header">
<span> 安装后验收</span>
<el-button size="small" type="primary" @click="handleAddItem">+ 添加验收项</el-button>
</div>
<div class="rm-panel-body">
<!-- Checklist section -->
<div style="margin-bottom:16px;">
<div style="font-weight:600;margin-bottom:8px;font-size:13px;">
📋 验收检查清单
<el-tag v-if="checklistAllDone" size="mini" type="success" style="margin-left:6px;"> 全部合格</el-tag>
<el-tag v-else size="mini" type="warning" style="margin-left:6px;"> 尚有未完成验收项</el-tag>
<el-button size="mini" type="text" icon="el-icon-plus" style="margin-left:8px;" @click="handleAddCheck">添加检查项</el-button>
</div>
<div v-if="checklist.length === 0" style="color:#aaa;font-size:12px;padding:8px 0;">暂无检查项</div>
<div v-for="(it, i) in checklist" :key="i" class="checklist-item" :class="it.isChecked === '1' ? 'checked' : ''">
<el-checkbox v-model="it._checked" @change="toggleCheck(it)"></el-checkbox>
<span class="checklist-text">{{ it.itemText }}</span>
<el-button type="text" size="mini" style="margin-left:auto;color:#999;" icon="el-icon-delete" @click="handleDeleteCheck(it)"></el-button>
</div>
</div>
<el-divider></el-divider>
<!-- Items table -->
<div style="font-weight:600;margin-bottom:8px;font-size:13px;">📊 详细验收数据</div>
<el-table :data="items" 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="requirement" label="要求值" width="120" />
<el-table-column prop="actualValue" label="实测值" width="120" />
<el-table-column label="结果" width="80" align="center">
<template slot-scope="s">
<el-tag :type="resultTag(s.row.result)" size="mini">{{ resultLabel(s.row.result) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="80">
<template slot-scope="s">
<el-button type="text" size="mini" @click="handleDeleteItem(s.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- Add/Edit Item Dialog -->
<el-dialog :title="itemDialogTitle" :visible.sync="itemDialogVisible" width="480px" append-to-body @closed="onItemClosed">
<el-form ref="itemForm" :model="itemForm" :rules="itemRules" label-width="0" size="small">
<div class="form-group"><label>验收项目</label><el-input v-model="itemForm.itemName" /></div>
<div class="form-row">
<div class="form-group"><label>要求值</label><el-input v-model="itemForm.requirement" /></div>
<div class="form-group"><label>实测值</label><el-input v-model="itemForm.actualValue" /></div>
</div>
<div class="form-group"><label>验收结果</label>
<el-select v-model="itemForm.result" style="width:100%;">
<el-option label="待检" value="pending" />
<el-option label="合格" value="pass" />
<el-option label="不合格" value="fail" />
</el-select>
</div>
<div class="form-group"><label>备注</label><el-input v-model="itemForm.notes" type="textarea" :rows="2" /></div>
</el-form>
<div slot="footer">
<el-button size="small" @click="itemDialogVisible = false">取消</el-button>
<el-button size="small" type="primary" @click="saveItem">保存</el-button>
</div>
</el-dialog>
<!-- Add Checklist Item Dialog -->
<el-dialog title="添加检查项" :visible.sync="checkDialogVisible" width="400px" append-to-body>
<el-form ref="checkForm" :model="checkForm" label-width="0" size="small">
<div class="form-group"><label>检查项内容</label><el-input v-model="checkForm.itemText" /></div>
</el-form>
<div slot="footer">
<el-button size="small" @click="checkDialogVisible = false">取消</el-button>
<el-button size="small" type="primary" @click="saveCheck">保存</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listAcceptanceChecklistAll, addAcceptanceChecklist, updateAcceptanceChecklist, delAcceptanceChecklist } from '@/api/rm/acceptanceChecklist'
import { listAcceptanceItemAll, addAcceptanceItem, updateAcceptanceItem, delAcceptanceItem } from '@/api/rm/acceptanceItem'
import { listProject } from '@/api/rm/project'
export default {
name: 'RmAcceptance',
data() {
return {
currentProjectName: this.$route.query.projectName || sessionStorage.getItem('rm_current_project_name') || '',
loading: false,
checklist: [],
items: [],
currentProjectId: null,
itemDialogVisible: false,
itemDialogTitle: '',
itemForm: { itemName: '', requirement: '', actualValue: '', result: 'pending', notes: '' },
itemRules: { itemName: [{ required: true, message: '请填写验收项目', trigger: 'blur' }] },
checkDialogVisible: false,
checkForm: { itemText: '' }
}
},
computed: {
checklistAllDone() {
return this.checklist.length > 0 && this.checklist.every(it => it.isChecked === '1')
}
},
created() { this.loadCurrentProject() },
methods: {
resultTag(result) {
return { pass: 'success', fail: 'danger', pending: 'info' }[result] || ''
},
resultLabel(result) {
return { pass: '合格', fail: '不合格', pending: '待检' }[result] || result
},
loadCurrentProject() {
const pid = this.$route.query.projectId || sessionStorage.getItem('rm_current_project_id')
if (pid) {
this.currentProjectId = pid
this.loadData()
} else {
listProject({ pageNum: 1, pageSize: 1 }).then(res => {
const rows = res.rows || []
if (rows.length > 0) { this.currentProjectId = rows[0].projectId; this.loadData() }
})
}
},
loadData() {
if (!this.currentProjectId) return
this.loading = true
Promise.all([
listAcceptanceChecklistAll({ projectId: this.currentProjectId }),
listAcceptanceItemAll({ projectId: this.currentProjectId })
]).then(([clRes, itRes]) => {
this.checklist = (clRes.data || []).map(it => ({ ...it, _checked: it.isChecked === '1' }))
this.items = itRes.data || []
}).finally(() => { this.loading = false })
},
// Checklist
handleAddCheck() {
this.checkForm = { itemText: '' }
this.checkDialogVisible = true
},
saveCheck() {
if (!this.checkForm.itemText) { this.$message.warning('请填写检查项内容'); return }
addAcceptanceChecklist({ projectId: this.currentProjectId, itemText: this.checkForm.itemText, isChecked: '0', sortOrder: this.checklist.length + 1 }).then(() => {
this.$message.success('检查项已添加')
this.checkDialogVisible = false
this.loadData()
})
},
toggleCheck(it) {
updateAcceptanceChecklist({ checkId: it.checkId, projectId: this.currentProjectId, itemText: it.itemText, isChecked: it._checked ? '1' : '0' }).then(() => {
it.isChecked = it._checked ? '1' : '0'
})
},
handleDeleteCheck(it) {
this.$confirm('确认删除该检查项?', '提示', { type: 'warning' }).then(() => {
delAcceptanceChecklist(it.checkId).then(() => { this.loadData() })
}).catch(() => {})
},
// Items
handleAddItem() {
this.itemDialogTitle = '添加验收项'
this.itemForm = { itemName: '', requirement: '', actualValue: '', result: 'pending', notes: '' }
this.itemDialogVisible = true
},
saveItem() {
this.$refs.itemForm.validate(valid => {
if (!valid) return
const data = { ...this.itemForm, projectId: this.currentProjectId }
const action = data.acceptItemId ? updateAcceptanceItem(data) : addAcceptanceItem(data)
action.then(() => {
this.$message.success('验收项已保存')
this.itemDialogVisible = false
this.loadData()
})
})
},
handleDeleteItem(row) {
this.$confirm('确认删除该验收项?', '提示', { type: 'warning' }).then(() => {
delAcceptanceItem(row.acceptItemId).then(() => { this.loadData() })
}).catch(() => {})
},
onItemClosed() { this.$refs.itemForm?.clearValidate() }
}
}
</script>
<style scoped>
.rm-container { padding: 8px; }
.rm-panel { background: #fff; border: 1px solid #d0d7de; border-radius: 8px; overflow: hidden; }
.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: 12px; }
.checklist-item { display: flex; align-items: center; gap: 8px; padding: 5px 0; border-bottom: 1px solid #f0f0f0; font-size: 12px; }
.checklist-item:last-child { border-bottom: none; }
.checklist-item.checked .checklist-text { text-decoration: line-through; color: #aaa; }
.form-group { margin-bottom: 6px; }
.form-group label { display: block; font-size: 12px; color: #555; margin-bottom: 3px; font-weight: 500; }
.form-row { display: flex; gap: 10px; }
.form-row .form-group { flex: 1; }
</style>

View File

@@ -1,5 +1,6 @@
<template>
<div class="app-container">
<div class="current-project-bar">当前项目{{ currentProjectName }}</div>
<el-card shadow="never" class="module-panel">
<div slot="header" class="module-header">
<span>💰 项目预算管理</span>
@@ -129,6 +130,7 @@ export default {
name: 'RmBudget',
data() {
return {
currentProjectName: this.$route.query.projectName || sessionStorage.getItem('rm_current_project_name') || '',
loading: false,
budgetList: [],
total: 0,
@@ -173,14 +175,21 @@ export default {
},
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()
}
})
const pid = this.$route.query.projectId || sessionStorage.getItem('rm_current_project_id')
if (pid) {
this.currentProjectId = pid
this.query.projectId = pid
this.loadList()
} else {
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

View File

@@ -0,0 +1,219 @@
<template>
<div class="rm-container">
<div class="current-project-bar">当前项目{{ currentProjectName }}</div>
<div class="rm-panel">
<div class="rm-panel-header">
<span>🔥 热负荷试车</span>
<el-button size="small" type="primary" @click="handleAddClause">+ 添加技术协议条款</el-button>
</div>
<div class="rm-panel-body">
<!-- Notice -->
<div style="margin-bottom:16px;padding:10px;background:#fff3cd;border-radius:6px;border:1px solid #ffe0b2;font-size:12px;">
📋 热负荷试车按照<b>技术协议签订条款</b>逐项确定请添加技术协议中的关键条款并逐项确认
</div>
<!-- Checklist section -->
<div style="margin-bottom:16px;">
<div style="font-weight:600;margin-bottom:8px;font-size:13px;">
📋 热负荷试车检查清单
<el-tag v-if="checklistAllDone" size="mini" type="success" style="margin-left:6px;"> 全部完成</el-tag>
<el-tag v-else size="mini" type="warning" style="margin-left:6px;"> 尚有未完成项</el-tag>
<el-button size="mini" type="text" style="margin-left:8px;" @click="handleAddCheck">添加检查项</el-button>
</div>
<div v-if="checklist.length === 0" style="color:#aaa;font-size:12px;padding:8px 0;">暂无检查项</div>
<div v-for="(it, i) in checklist" :key="i" class="checklist-item" :class="it.isChecked === '1' ? 'checked' : ''">
<el-checkbox v-model="it._checked" @change="toggleCheck(it)"></el-checkbox>
<span class="checklist-text">{{ it.itemText }}</span>
<el-button type="text" size="mini" style="margin-left:auto;color:#999;" icon="el-icon-delete" @click="handleDeleteCheck(it)"></el-button>
</div>
</div>
<el-divider></el-divider>
<!-- Clauses section -->
<div style="font-weight:600;margin-bottom:8px;font-size:13px;">📊 技术协议条款确认</div>
<el-table :data="clauses" v-loading="loading" stripe border highlight-current-row size="small">
<el-table-column type="index" label="#" width="45" />
<el-table-column prop="clause" label="协议条款" min-width="160" />
<el-table-column prop="standard" label="标准要求" width="130" />
<el-table-column prop="result" label="试车结果" min-width="140" show-overflow-tooltip />
<el-table-column label="确认状态" width="80" align="center">
<template slot-scope="s">
<el-tag :type="s.row.passFlag === '1' ? 'success' : 'warning'" size="mini">{{ s.row.passFlag === '1' ? '合格' : '待确认' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="80">
<template slot-scope="s">
<el-button type="text" size="mini" @click="handleEditClause(s.row)">编辑</el-button>
<el-button type="text" size="mini" @click="handleDeleteClause(s.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div v-if="clauses.length === 0" style="text-align:center;color:#aaa;font-size:12px;padding:12px 0;border:1px solid #f0f0f0;border-top:none;">
暂无技术协议条款请点击"添加技术协议条款"
</div>
</div>
</div>
<!-- Clause Dialog -->
<el-dialog :title="clauseDialogTitle" :visible.sync="clauseDialogVisible" width="500px" append-to-body @closed="onClauseClosed">
<el-form ref="clauseForm" :model="clauseForm" :rules="clauseRules" label-width="0" size="small">
<div class="form-group"><label>技术协议条款</label><el-input v-model="clauseForm.clause" placeholder="如轧制力控制精度±2%" /></div>
<div class="form-group"><label>标准要求</label><el-input v-model="clauseForm.standard" placeholder="如GB/T 标准" /></div>
<div class="form-group"><label>试车结果</label><el-input v-model="clauseForm.result" type="textarea" :rows="2" placeholder="试车实际结果" /></div>
<div style="margin-top:8px;">
<el-checkbox v-model="clauseForm._pass">合格/通过</el-checkbox>
</div>
</el-form>
<div slot="footer">
<el-button size="small" @click="clauseDialogVisible = false">取消</el-button>
<el-button size="small" type="primary" @click="saveClause">保存</el-button>
</div>
</el-dialog>
<!-- Check Dialog -->
<el-dialog title="添加检查项" :visible.sync="checkDialogVisible" width="400px" append-to-body>
<el-form ref="checkForm" :model="checkForm" label-width="0" size="small">
<div class="form-group"><label>检查项内容</label><el-input v-model="checkForm.itemText" /></div>
</el-form>
<div slot="footer">
<el-button size="small" @click="checkDialogVisible = false">取消</el-button>
<el-button size="small" type="primary" @click="saveCheck">保存</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listCommissioningChecklistAll, addCommissioningChecklist, updateCommissioningChecklist, delCommissioningChecklist } from '@/api/rm/commissioningChecklist'
import { listCommissioningClauseAll, addCommissioningClause, updateCommissioningClause, delCommissioningClause } from '@/api/rm/commissioningClause'
import { listProject } from '@/api/rm/project'
export default {
name: 'RmCommissioning',
data() {
return {
currentProjectName: this.$route.query.projectName || sessionStorage.getItem('rm_current_project_name') || '',
loading: false,
checklist: [],
clauses: [],
currentProjectId: null,
checkDialogVisible: false,
checkForm: { itemText: '' },
clauseDialogVisible: false,
clauseDialogTitle: '',
clauseForm: { clause: '', standard: '', result: '', _pass: false },
clauseRules: { clause: [{ required: true, message: '请填写技术协议条款', trigger: 'blur' }] }
}
},
computed: {
checklistAllDone() {
return this.checklist.length > 0 && this.checklist.every(it => it.isChecked === '1')
}
},
created() { this.loadCurrentProject() },
methods: {
loadCurrentProject() {
const pid = this.$route.query.projectId || sessionStorage.getItem('rm_current_project_id')
if (pid) {
this.currentProjectId = pid
this.loadData()
} else {
listProject({ pageNum: 1, pageSize: 1 }).then(res => {
const rows = res.rows || []
if (rows.length > 0) { this.currentProjectId = rows[0].projectId; this.loadData() }
})
}
},
loadData() {
if (!this.currentProjectId) return
this.loading = true
Promise.all([
listCommissioningChecklistAll({ projectId: this.currentProjectId }),
listCommissioningClauseAll({ projectId: this.currentProjectId })
]).then(([clRes, ccRes]) => {
this.checklist = (clRes.data || []).map(it => ({ ...it, _checked: it.isChecked === '1' }))
this.clauses = ccRes.data || []
}).finally(() => { this.loading = false })
},
// Checklist
handleAddCheck() {
this.checkForm = { itemText: '' }
this.checkDialogVisible = true
},
saveCheck() {
if (!this.checkForm.itemText) { this.$message.warning('请填写检查项内容'); return }
addCommissioningChecklist({ projectId: this.currentProjectId, itemText: this.checkForm.itemText, isChecked: '0', sortOrder: this.checklist.length + 1 }).then(() => {
this.checkDialogVisible = false
this.loadData()
})
},
toggleCheck(it) {
updateCommissioningChecklist({ checkId: it.checkId, projectId: this.currentProjectId, itemText: it.itemText, isChecked: it._checked ? '1' : '0' }).then(() => {
it.isChecked = it._checked ? '1' : '0'
})
},
handleDeleteCheck(it) {
this.$confirm('确认删除该检查项?', '提示', { type: 'warning' }).then(() => {
delCommissioningChecklist(it.checkId).then(() => { this.loadData() })
}).catch(() => {})
},
// Clauses
handleAddClause() {
this.clauseDialogTitle = '添加技术协议条款'
this.clauseForm = { clause: '', standard: '', result: '', _pass: false }
this.clauseDialogVisible = true
},
handleEditClause(row) {
this.clauseDialogTitle = '编辑条款'
this.clauseForm = {
clauseId: row.clauseId,
clause: row.clause,
standard: row.standard,
result: row.result,
_pass: row.passFlag === '1'
}
this.clauseDialogVisible = true
},
saveClause() {
this.$refs.clauseForm.validate(valid => {
if (!valid) return
const data = {
clauseId: this.clauseForm.clauseId,
projectId: this.currentProjectId,
clause: this.clauseForm.clause,
standard: this.clauseForm.standard,
result: this.clauseForm.result,
passFlag: this.clauseForm._pass ? '1' : '0'
}
const action = data.clauseId ? updateCommissioningClause(data) : addCommissioningClause(data)
action.then(() => {
this.clauseDialogVisible = false
this.loadData()
})
})
},
handleDeleteClause(row) {
this.$confirm('确认删除该条款?', '提示', { type: 'warning' }).then(() => {
delCommissioningClause(row.clauseId).then(() => { this.loadData() })
}).catch(() => {})
},
onClauseClosed() { this.$refs.clauseForm?.clearValidate() }
}
}
</script>
<style scoped>
.rm-container { padding: 8px; }
.rm-panel { background: #fff; border: 1px solid #d0d7de; border-radius: 8px; overflow: hidden; }
.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: 12px; }
.checklist-item { display: flex; align-items: center; gap: 8px; padding: 5px 0; border-bottom: 1px solid #f0f0f0; font-size: 12px; }
.checklist-item:last-child { border-bottom: none; }
.checklist-item.checked .checklist-text { text-decoration: line-through; color: #aaa; }
.form-group { margin-bottom: 6px; }
.form-group label { display: block; font-size: 12px; color: #555; margin-bottom: 3px; font-weight: 500; }
.form-row { display: flex; gap: 10px; }
.form-row .form-group { flex: 1; }
</style>

View File

@@ -0,0 +1,189 @@
<template>
<div class="rm-container">
<div class="current-project-bar">当前项目{{ currentProjectName }}</div>
<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">
<div class="rm-search-bar" style="margin-bottom:8px">
<el-input v-model="queryParams.docName" placeholder="文件名称" size="small" clearable style="width:160px;margin-right:8px" @keyup.enter="loadList" />
<el-select v-model="queryParams.category" placeholder="分类" size="small" clearable style="width:120px;margin-right:8px">
<el-option label="图纸" value="图纸" />
<el-option label="技术协议" value="技术协议" />
<el-option label="计算书" value="计算书" />
<el-option label="说明书" value="说明书" />
<el-option label="其他" value="其他" />
</el-select>
<el-button size="small" type="primary" icon="el-icon-search" @click="loadList">查询</el-button>
</div>
<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="docName" label="文件名称" min-width="160" />
<el-table-column prop="category" label="分类" width="80" align="center" />
<el-table-column prop="version" label="版本" width="70" align="center" />
<el-table-column prop="uploader" label="上传人" width="80" />
<el-table-column prop="uploadDate" label="上传日期" width="100" align="center" />
<el-table-column label="操作" width="160" fixed="right">
<template slot-scope="s">
<el-button type="text" size="mini" icon="el-icon-view" @click="handleView(s.row)">查看</el-button>
<el-button type="text" size="mini" icon="el-icon-edit" @click="handleEdit(s.row)">编辑</el-button>
<el-button type="text" size="mini" icon="el-icon-delete" @click="handleDelete(s.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</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="120px" size="small">
<el-row :gutter="16">
<el-col :span="16"><el-form-item label="文件名称" prop="docName"><el-input v-model="form.docName" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="分类" prop="category">
<el-select v-model="form.category" 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="6"><el-form-item label="版本" prop="version"><el-input v-model="form.version" placeholder="V1.0" /></el-form-item></el-col>
<el-col :span="9"><el-form-item label="上传人" prop="uploader"><el-input v-model="form.uploader" /></el-form-item></el-col>
<el-col :span="9"><el-form-item label="上传日期" prop="uploadDate"><el-date-picker v-model="form.uploadDate" type="date" style="width:100%" value-format="yyyy-MM-dd" /></el-form-item></el-col>
</el-row>
<el-form-item label="描述" prop="description">
<el-input v-model="form.description" type="textarea" :rows="2" placeholder="文件描述说明" />
</el-form-item>
<el-form-item label="文件" prop="fileUrl">
<div style="display:flex;align-items:center">
<el-input v-model="form.fileUrl" placeholder="请上传文件" style="flex:1;margin-right:8px" />
<el-upload
:action="uploadUrl"
:headers="uploadHeaders"
:on-success="handleUploadSuccess"
:before-upload="beforeUpload"
:show-file-list="false">
<el-button size="small" type="primary">上传</el-button>
</el-upload>
</div>
</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>
<!-- View Dialog -->
<el-dialog title="资料详情" :visible.sync="viewVisible" width="520px" append-to-body>
<div class="detail-grid" v-if="viewItem">
<div class="dg-label">文件名称</div><div class="dg-value">{{ viewItem.docName }}</div>
<div class="dg-label">分类</div><div class="dg-value">{{ viewItem.category || '-' }}</div>
<div class="dg-label">版本</div><div class="dg-value">{{ viewItem.version || '-' }}</div>
<div class="dg-label">上传人</div><div class="dg-value">{{ viewItem.uploader || '-' }}</div>
<div class="dg-label">上传日期</div><div class="dg-value">{{ viewItem.uploadDate || '-' }}</div>
<div class="dg-label">描述</div><div class="dg-value">{{ viewItem.description || '无' }}</div>
<div class="dg-label">文件</div>
<div class="dg-value">
<a v-if="viewItem.fileUrl" :href="viewItem.fileUrl" target="_blank" style="color:#409eff">{{ viewItem.fileName || '下载文件' }}</a>
<span v-else>-</span>
</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button size="small" @click="viewVisible = false">关闭</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listDocLib, getDocLib, addDocLib, updateDocLib, delDocLib } from '@/api/rm/docLib'
import { listProject } from '@/api/rm/project'
import { getToken } from '@/utils/auth'
export default {
name: 'RmDocLib',
data() {
return {
currentProjectName: this.$route.query.projectName || sessionStorage.getItem('rm_current_project_name') || '',
loading: false, list: [], currentProjectId: null,
queryParams: { docName: '', category: '' },
dialogVisible: false, dialogTitle: '', submitting: false, form: {},
rules: { docName: [{ required: true, message: '请填写文件名称', trigger: 'blur' }] },
viewVisible: false, viewItem: null,
uploadUrl: process.env.VUE_APP_BASE_API + '/common/upload',
uploadHeaders: { Authorization: 'Bearer ' + getToken() }
}
},
created() { this.loadCurrentProject() },
methods: {
loadCurrentProject() {
const pid = this.$route.query.projectId || sessionStorage.getItem('rm_current_project_id')
if (pid) {
this.currentProjectId = pid
this.loadList()
} else {
listProject({ pageNum: 1, pageSize: 1 }).then(res => {
const rows = res.rows || []
if (rows.length > 0) { this.currentProjectId = rows[0].projectId; this.loadList() }
})
}
},
loadList() {
this.loading = true
listDocLib({ ...this.queryParams, projectId: this.currentProjectId }).then(r => {
this.list = r.rows || []
}).finally(() => { this.loading = false })
},
handleAdd() {
if (!this.currentProjectId) { this.$message.warning('请先在项目总览中创建项目'); return }
this.dialogTitle = '新增资料'
this.form = { projectId: this.currentProjectId, docName: '', category: '', version: 'V1.0', uploader: '', uploadDate: '', description: '', fileUrl: '' }
this.dialogVisible = true; this.$nextTick(() => { this.$refs.formRef?.clearValidate() })
},
handleEdit(row) {
this.dialogTitle = '编辑资料'
getDocLib(row.docId).then(r => { this.form = r.data; this.dialogVisible = true; this.$nextTick(() => { this.$refs.formRef?.clearValidate() }) })
},
handleView(row) {
this.viewItem = row; this.viewVisible = true
},
handleDelete(row) {
this.$confirm(`确认删除 "${row.docName}"`, '提示', { type: 'warning' }).then(() => { delDocLib(row.docId).then(() => { this.$message.success('删除成功'); this.loadList() }) })
},
handleSubmit() {
this.$refs.formRef.validate(v => {
if (!v) return; this.submitting = true
const api = this.form.docId ? updateDocLib : addDocLib
api(this.form).then(() => { this.$message.success('保存成功'); this.dialogVisible = false; this.loadList() }).finally(() => { this.submitting = false })
})
},
handleUploadSuccess(res) {
if (res.code === 200) { this.form.fileUrl = res.url; this.$message.success('上传成功') }
else { this.$message.error(res.msg || '上传失败') }
},
beforeUpload(file) {
const ext = file.name.split('.').pop().toLowerCase()
const allowed = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'dwg', 'dxf', 'zip', 'rar', 'jpg', 'png']
if (!allowed.includes(ext)) { this.$message.error('不支持的文件格式'); return false }
return true
}
}
}
</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; }
.detail-grid { display: grid; grid-template-columns: 100px 1fr; font-size: 13px; }
.dg-label { background: #fafbfc; font-weight: 600; padding: 8px 12px; border-bottom: 1px solid #eee; }
.dg-value { padding: 8px 12px; border-bottom: 1px solid #eee; }
</style>

View File

@@ -0,0 +1,160 @@
<template>
<div class="rm-container">
<div class="current-project-bar">当前项目{{ currentProjectName }}</div>
<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">
<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="oldVersion" label="优化前版本" width="100" align="center" />
<el-table-column prop="newVersion" label="优化后版本" width="100" align="center" />
<el-table-column prop="optimizer" label="优化人" width="80" />
<el-table-column prop="compareDate" label="优化日期" width="100" align="center" />
<el-table-column prop="status" label="状态" width="90" align="center">
<template slot-scope="s">
<el-tag :type="statusTag(s.row.status)" size="mini">{{ statusLabel(s.row.status) }}</el-tag>
</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-view" @click="handleView(s.row)">查看对比</el-button>
<el-button type="text" size="mini" icon="el-icon-edit" @click="handleEdit(s.row)">编辑</el-button>
<el-button type="text" size="mini" icon="el-icon-delete" @click="handleDelete(s.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</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="120px" size="small">
<el-row :gutter="16">
<el-col :span="16"><el-form-item label="图纸名称" prop="drawingName"><el-input v-model="form.drawingName" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="优化人" prop="optimizer"><el-input v-model="form.optimizer" /></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="8"><el-form-item label="优化前版本" prop="oldVersion"><el-input v-model="form.oldVersion" placeholder="V1.0" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="优化后版本" prop="newVersion"><el-input v-model="form.newVersion" placeholder="V2.0" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="优化日期" prop="compareDate"><el-date-picker v-model="form.compareDate" type="date" style="width:100%" value-format="yyyy-MM-dd" /></el-form-item></el-col>
</el-row>
<el-form-item label="优化前问题描述" prop="beforeDesc">
<el-input v-model="form.beforeDesc" type="textarea" :rows="2" placeholder="优化前存在的问题" />
</el-form-item>
<el-form-item label="优化后改进内容" prop="afterDesc">
<el-input v-model="form.afterDesc" type="textarea" :rows="2" placeholder="优化后改进的内容" />
</el-form-item>
<el-form-item label="优化效果评价" prop="diffNotes">
<el-input v-model="form.diffNotes" type="textarea" :rows="2" placeholder="如刚度提升15%振动降低20%" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" style="width:100%">
<el-option label="待确认" value="pending" />
<el-option label="已确认" value="approved" />
<el-option label="已驳回" value="rejected" />
</el-select>
</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>
<!-- View Compare Dialog -->
<el-dialog title="图纸优化对比" :visible.sync="viewVisible" width="700px" append-to-body>
<div class="compare-grid" v-if="viewItem">
<div class="cg-header">对比项</div>
<div class="cg-header">优化前{{ viewItem.oldVersion }}</div>
<div class="cg-header">优化后{{ viewItem.newVersion }}</div>
<div class="cg-label">问题描述</div>
<div class="cg-cell">{{ viewItem.beforeDesc || '无' }}</div>
<div class="cg-cell">{{ viewItem.afterDesc || '无' }}</div>
<div class="cg-label">改进效果</div>
<div class="cg-cell cg-ok">{{ viewItem.diffNotes || '无' }}</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button size="small" @click="viewVisible = false">关闭</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listDrawingCompareAll, getDrawingCompare, addDrawingCompare, updateDrawingCompare, delDrawingCompare } from '@/api/rm/drawingCompare'
import { listProject } from '@/api/rm/project'
export default {
name: 'RmDrawingCompare',
data() {
return {
currentProjectName: this.$route.query.projectName || sessionStorage.getItem('rm_current_project_name') || '',
loading: false, list: [], currentProjectId: null,
dialogVisible: false, dialogTitle: '', submitting: false, form: {},
rules: { drawingName: [{ required: true, message: '请填写图纸名称', trigger: 'blur' }] },
viewVisible: false, viewItem: null
}
},
created() { this.loadCurrentProject() },
methods: {
loadCurrentProject() {
const pid = this.$route.query.projectId || sessionStorage.getItem('rm_current_project_id')
if (pid) {
this.currentProjectId = pid
this.loadList()
} else {
listProject({ pageNum: 1, pageSize: 1 }).then(res => {
const rows = res.rows || []
if (rows.length > 0) { this.currentProjectId = rows[0].projectId; this.loadList() }
})
}
},
loadList() {
this.loading = true
listDrawingCompareAll({ projectId: this.currentProjectId }).then(r => { this.list = r.data || [] }).finally(() => { this.loading = false })
},
handleAdd() {
if (!this.currentProjectId) { this.$message.warning('请先在项目总览中创建项目'); return }
this.dialogTitle = '新增图纸优化对比'
this.form = { projectId: this.currentProjectId, drawingName: '', oldVersion: 'V1.0', newVersion: 'V2.0', optimizer: '', compareDate: '', beforeDesc: '', afterDesc: '', diffNotes: '', status: 'pending' }
this.dialogVisible = true; this.$nextTick(() => { this.$refs.formRef?.clearValidate() })
},
handleEdit(row) {
this.dialogTitle = '编辑图纸对比'
getDrawingCompare(row.compareId).then(r => { this.form = r.data; this.dialogVisible = true; this.$nextTick(() => { this.$refs.formRef?.clearValidate() }) })
},
handleView(row) {
this.viewItem = row; this.viewVisible = true
},
handleDelete(row) {
this.$confirm(`确认删除 "${row.drawingName}" 的对比记录?`, '提示', { type: 'warning' }).then(() => { delDrawingCompare(row.compareId).then(() => { this.$message.success('删除成功'); this.loadList() }) })
},
handleSubmit() {
this.$refs.formRef.validate(v => {
if (!v) return; this.submitting = true
const api = this.form.compareId ? updateDrawingCompare : addDrawingCompare
api(this.form).then(() => { this.$message.success('保存成功'); this.dialogVisible = false; 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; }
.dialog-footer { text-align: right; }
.compare-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; border: 1px solid #d0d7de; border-radius: 4px; overflow: hidden; font-size: 13px; }
.cg-header { background: #f0f2f5; font-weight: 600; padding: 10px; border-bottom: 1px solid #d0d7de; }
.cg-label { background: #fafbfc; font-weight: 600; padding: 10px; border-bottom: 1px solid #eee; }
.cg-cell { padding: 10px; border-bottom: 1px solid #eee; }
.cg-ok { color: #67c23a; font-weight: 600; }
</style>

View File

@@ -1,5 +1,6 @@
<template>
<div class="rm-container">
<div class="current-project-bar">当前项目{{ currentProjectName }}</div>
<div class="rm-panel">
<div class="rm-panel-header">
<span>图纸详细设计</span>
@@ -140,6 +141,7 @@ export default {
name: 'RmDrawingDesign',
data() {
return {
currentProjectName: this.$route.query.projectName || sessionStorage.getItem('rm_current_project_name') || '',
loading: false,
list: [],
total: 0,
@@ -155,14 +157,21 @@ export default {
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()
}
})
const pid = this.$route.query.projectId || sessionStorage.getItem('rm_current_project_id')
if (pid) {
this.currentProjectId = pid
this.query.projectId = pid
this.loadList()
} else {
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

View File

@@ -1,5 +1,6 @@
<template>
<div class="rm-container">
<div class="current-project-bar">当前项目{{ currentProjectName }}</div>
<div class="rm-panel">
<div class="rm-panel-header">
<span>图纸审查</span>
@@ -98,6 +99,7 @@ export default {
name: 'RmDrawingReview',
data() {
return {
currentProjectName: this.$route.query.projectName || sessionStorage.getItem('rm_current_project_name') || '',
loading: false,
list: [],
total: 0,
@@ -113,14 +115,21 @@ export default {
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()
}
})
const pid = this.$route.query.projectId || sessionStorage.getItem('rm_current_project_id')
if (pid) {
this.currentProjectId = pid
this.query.projectId = pid
this.loadList()
} else {
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

View File

@@ -0,0 +1,152 @@
<template>
<div class="rm-container">
<div class="current-project-bar">当前项目{{ currentProjectName }}</div>
<div class="rm-panel">
<div class="rm-panel-header">
<span>💬 安装时出现的问题反馈</span>
<el-button size="small" type="primary" @click="handleAdd">+ 反馈问题</el-button>
</div>
<div class="rm-panel-body">
<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="title" label="问题标题" min-width="160" />
<el-table-column prop="location" label="发生位置" width="120" />
<el-table-column prop="proposer" label="反馈人" width="80" />
<el-table-column prop="feedbackDate" label="反馈日期" width="100" align="center" />
<el-table-column label="处理状态" width="90" align="center">
<template slot-scope="s">
<el-tag :type="statusTag(s.row.status)" size="mini">{{ statusLabel(s.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120">
<template slot-scope="s">
<el-button type="text" size="mini" @click="handleEdit(s.row)">处理</el-button>
<el-button type="text" size="mini" @click="handleDelete(s.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="560px" append-to-body @closed="onClosed">
<el-form ref="form" :model="form" :rules="rules" label-width="0" size="small">
<div class="form-group"><label>问题标题</label><el-input v-model="form.title" /></div>
<div class="form-row">
<div class="form-group"><label>发生位置</label><el-input v-model="form.location" placeholder="如:主轧机底座安装" /></div>
<div class="form-group"><label>反馈人</label><el-input v-model="form.proposer" /></div>
</div>
<div class="form-group"><label>问题描述</label><el-input v-model="form.issueDesc" type="textarea" :rows="2" /></div>
<div class="form-group"><label>解决方案</label><el-input v-model="form.solution" type="textarea" :rows="2" /></div>
<div class="form-group"><label>防止再发措施</label><el-input v-model="form.preventAction" type="textarea" :rows="2" placeholder="防止同类问题再次发生的措施" /></div>
<div class="form-group"><label>处理状态</label>
<el-select v-model="form.status" style="width:100%;">
<el-option label="待处理" value="pending" />
<el-option label="处理中" value="processing" />
<el-option label="已解决" value="resolved" />
</el-select>
</div>
</el-form>
<div slot="footer">
<el-button size="small" @click="dialogVisible = false">取消</el-button>
<el-button size="small" type="primary" @click="save">保存</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listInstallFeedbackAll, addInstallFeedback, updateInstallFeedback, delInstallFeedback } from '@/api/rm/installFeedback'
import { listProject } from '@/api/rm/project'
export default {
name: 'RmInstallFeedback',
data() {
return {
currentProjectName: this.$route.query.projectName || sessionStorage.getItem('rm_current_project_name') || '',
loading: false,
list: [],
currentProjectId: null,
dialogVisible: false,
dialogTitle: '',
form: { title: '', location: '', proposer: '', issueDesc: '', solution: '', preventAction: '', status: 'pending' },
rules: { title: [{ required: true, message: '请填写问题标题', trigger: 'blur' }] }
}
},
created() { this.loadCurrentProject() },
methods: {
statusTag(status) {
return { pending: 'warning', processing: 'primary', resolved: 'success' }[status] || ''
},
statusLabel(status) {
return { pending: '待处理', processing: '处理中', resolved: '已解决' }[status] || status
},
loadCurrentProject() {
const pid = this.$route.query.projectId || sessionStorage.getItem('rm_current_project_id')
if (pid) {
this.currentProjectId = pid
this.loadData()
} else {
listProject({ pageNum: 1, pageSize: 1 }).then(res => {
const rows = res.rows || []
if (rows.length > 0) { this.currentProjectId = rows[0].projectId; this.loadData() }
})
}
},
loadData() {
if (!this.currentProjectId) return
this.loading = true
listInstallFeedbackAll({ projectId: this.currentProjectId }).then(res => {
this.list = res.data || []
}).finally(() => { this.loading = false })
},
handleAdd() {
this.dialogTitle = '反馈问题'
this.form = { title: '', location: '', proposer: '', issueDesc: '', solution: '', preventAction: '', status: 'pending' }
this.dialogVisible = true
},
handleEdit(row) {
this.dialogTitle = '处理问题反馈'
this.form = {
feedbackId: row.feedbackId,
title: row.title,
location: row.location,
proposer: row.proposer,
issueDesc: row.issueDesc,
solution: row.solution,
preventAction: row.preventAction,
status: row.status
}
this.dialogVisible = true
},
save() {
this.$refs.form.validate(valid => {
if (!valid) return
const data = { ...this.form, projectId: this.currentProjectId }
const action = data.feedbackId ? updateInstallFeedback(data) : addInstallFeedback(data)
action.then(() => {
this.$message.success('已保存')
this.dialogVisible = false
this.loadData()
})
})
},
handleDelete(row) {
this.$confirm('确认删除?', '提示', { type: 'warning' }).then(() => {
delInstallFeedback(row.feedbackId).then(() => { this.loadData() })
}).catch(() => {})
},
onClosed() { this.$refs.form?.clearValidate() }
}
}
</script>
<style scoped>
.rm-container { padding: 8px; }
.rm-panel { background: #fff; border: 1px solid #d0d7de; border-radius: 8px; overflow: hidden; }
.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: 12px; }
.form-group { margin-bottom: 6px; }
.form-group label { display: block; font-size: 12px; color: #555; margin-bottom: 3px; font-weight: 500; }
.form-row { display: flex; gap: 10px; }
.form-row .form-group { flex: 1; }
</style>

View File

@@ -0,0 +1,70 @@
<template>
<div class="rm-container">
<div class="current-project-bar">当前项目{{ currentProjectName }}</div>
<div class="rm-panel">
<div class="rm-panel-header">
<span>🛠 安装前准备</span>
</div>
<div class="rm-panel-body">
<el-tabs v-model="activeTab" @tab-click="onTabClick">
<el-tab-pane label="🔧 工具准备" name="tools">
<install-tools ref="toolsTab" :project-id="currentProjectId" />
</el-tab-pane>
<el-tab-pane label="👷 安装人员" name="personnel">
<install-personnel ref="personnelTab" :project-id="currentProjectId" />
</el-tab-pane>
<el-tab-pane label="📏 安装精度" name="precision">
<install-precision ref="precisionTab" :project-id="currentProjectId" />
</el-tab-pane>
<el-tab-pane label="📊 安装进度" name="progress">
<install-progress ref="progressTab" :project-id="currentProjectId" />
</el-tab-pane>
</el-tabs>
</div>
</div>
</div>
</template>
<script>
import { listProject } from '@/api/rm/project'
import InstallTools from './tools.vue'
import InstallPersonnel from './personnel.vue'
import InstallPrecision from './precision.vue'
import InstallProgress from './progress.vue'
export default {
name: 'RmInstallPrep',
components: { InstallTools, InstallPersonnel, InstallPrecision, InstallProgress },
data() {
return {
currentProjectName: this.$route.query.projectName || sessionStorage.getItem('rm_current_project_name') || '',
activeTab: 'tools',
currentProjectId: null
}
},
created() { this.loadCurrentProject() },
methods: {
loadCurrentProject() {
const pid = this.$route.query.projectId || sessionStorage.getItem('rm_current_project_id')
if (pid) {
this.currentProjectId = pid
} else {
listProject({ pageNum: 1, pageSize: 1 }).then(res => {
const rows = res.rows || []
if (rows.length > 0) { this.currentProjectId = rows[0].projectId }
})
}
},
onTabClick() {
// Child components load data reactively via projectId watcher
}
}
}
</script>
<style scoped>
.rm-container { padding: 8px; }
.rm-panel { background: #fff; border: 1px solid #d0d7de; border-radius: 8px; overflow: hidden; }
.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: 12px; }
</style>

View File

@@ -0,0 +1,141 @@
<template>
<div>
<!-- Stats -->
<div class="dashboard-grid">
<div class="stat-card"><div class="label">人员总数</div><div class="value">{{ personnel.length }}</div><div class="sub"></div></div>
<div class="stat-card green"><div class="label">人工费总预算</div><div class="value">¥{{ totalWages }}</div><div class="sub">含全部人员</div></div>
<div class="stat-card orange"><div class="label">总出勤天数</div><div class="value">{{ totalDays }}</div><div class="sub">·</div></div>
<div class="stat-card"><div class="label">日均工资</div><div class="value">¥{{ avgWage }}</div><div class="sub">人均</div></div>
</div>
<div style="margin-bottom:12px;">
<el-button size="mini" type="primary" @click="handleAdd">+ 添加人员</el-button>
</div>
<!-- Table -->
<el-table :data="personnel" v-loading="loading" stripe border size="small" style="width:100%;">
<el-table-column type="index" label="#" width="40" />
<el-table-column prop="name" label="姓名" width="80" />
<el-table-column prop="position" label="岗位" width="120" show-overflow-tooltip />
<el-table-column prop="positionEn" label="Position" width="120" show-overflow-tooltip />
<el-table-column prop="planIn" label="计划入场" width="90" />
<el-table-column prop="planOut" label="计划退场" width="90" />
<el-table-column prop="days" label="天数" width="55" />
<el-table-column prop="dailyRate" label="日工资" width="80" align="right" />
<el-table-column prop="totalWages" label="总工资" width="90" align="right" />
<el-table-column prop="duty" label="主要职责" width="150" show-overflow-tooltip />
<el-table-column prop="qualification" label="资质要求" width="120" show-overflow-tooltip />
<el-table-column prop="phone" label="电话" width="100" />
<el-table-column label="操作" width="80" fixed="right">
<template slot-scope="s">
<el-button type="text" size="mini" @click="handleEdit(s.row)">编辑</el-button>
<el-button type="text" size="mini" @click="handleDelete(s.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div v-if="personnel.length===0" style="text-align:center;color:#aaa;padding:20px;">暂无人员数据</div>
<!-- Dialog -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="600px" append-to-body @closed="onClosed">
<el-form ref="form" :model="form" :rules="rules" label-width="0" size="small">
<div class="form-row">
<div class="form-group" style="flex:1;"><label>姓名</label><el-input v-model="form.name" /></div>
<div class="form-group" style="flex:1;"><label>岗位中文</label><el-input v-model="form.position" /></div>
<div class="form-group" style="flex:1;"><label>Position (EN)</label><el-input v-model="form.positionEn" /></div>
</div>
<div class="form-row">
<div class="form-group"><label>计划入场</label><el-input v-model="form.planIn" placeholder="如D+0(开工)" /></div>
<div class="form-group"><label>计划退场</label><el-input v-model="form.planOut" placeholder="如D+82" /></div>
<div class="form-group"><label>在岗天数</label><el-input v-model="form.days" /></div>
</div>
<div class="form-row">
<div class="form-group"><label>日工资()</label><el-input v-model="form.dailyRate" @input="autoCalcWages" /></div>
<div class="form-group"><label>总工资()</label><el-input v-model="form.totalWages" /></div>
</div>
<div class="form-group"><label>主要职责</label><el-input v-model="form.duty" /></div>
<div class="form-row">
<div class="form-group"><label>资质要求</label><el-input v-model="form.qualification" /></div>
<div class="form-group"><label>联系电话</label><el-input v-model="form.phone" /></div>
</div>
<div class="form-group"><label>备注</label><el-input v-model="form.remark" /></div>
</el-form>
<div slot="footer">
<el-button size="small" @click="dialogVisible=false">取消</el-button>
<el-button size="small" type="primary" @click="save">保存</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listInstallPersonnelAll, addInstallPersonnel, updateInstallPersonnel, delInstallPersonnel } from '@/api/rm/installPersonnel'
export default {
name: 'InstallPersonnel',
props: { projectId: { type: [Number, String], default: null } },
watch: { projectId: { immediate: true, handler(v) { if (v) this.loadData() } } },
data() {
return {
loading: false,
personnel: [],
dialogVisible: false,
dialogTitle: '',
form: this.cleanForm(),
rules: { name: [{ required: true, message: '请填写姓名', trigger: 'blur' }] }
}
},
computed: {
totalWages() { return this.personnel.reduce((s,p) => s+(parseFloat(p.totalWages)||0), 0).toLocaleString() },
totalDays() { return this.personnel.reduce((s,p) => s+(parseInt(p.days)||0), 0) },
avgWage() {
const n = this.personnel.length
if (!n) return '0'
const total = this.personnel.reduce((s,p) => s+(parseFloat(p.totalWages)||0), 0)
return Math.round(total/n).toLocaleString()
}
},
methods: {
cleanForm() { return { name:'', position:'', positionEn:'', planIn:'', planOut:'', days:'', dailyRate:'', totalWages:'', duty:'', qualification:'', phone:'', remark:'' } },
loadData() {
if (!this.projectId) return
this.loading = true
listInstallPersonnelAll({ projectId: this.projectId }).then(res => {
this.personnel = res.data || []
}).finally(() => { this.loading = false })
},
handleAdd() { this.dialogTitle='添加人员'; this.form=this.cleanForm(); this.dialogVisible=true },
handleEdit(row) { this.dialogTitle='编辑人员'; this.form={...row}; this.dialogVisible=true },
handleDelete(row) {
this.$confirm('确认删除?','提示',{type:'warning'}).then(() => {
delInstallPersonnel(row.personnelId).then(() => { this.loadData() })
}).catch(() => {})
},
autoCalcWages() {
const days = parseInt(this.form.days)||0
const rate = parseFloat(this.form.dailyRate)||0
if (days && rate) this.form.totalWages = (days * rate).toString()
},
save() {
this.$refs.form.validate(valid => {
if (!valid) return
const data = { ...this.form, projectId: this.projectId }
const act = data.personnelId ? updateInstallPersonnel(data) : addInstallPersonnel(data)
act.then(() => { this.dialogVisible=false; this.loadData() })
})
},
onClosed() { this.$refs.form?.clearValidate() }
}
}
</script>
<style scoped>
.dashboard-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 8px; margin-bottom: 12px; }
.stat-card { background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 6px; padding: 10px; text-align: center; }
.stat-card .label { font-size: 11px; color: #6c757d; }
.stat-card .value { font-size: 20px; font-weight: 700; color: #1a5a9e; }
.stat-card.green .value { color: #28a745; }
.stat-card.orange .value { color: #e67e22; }
.form-group { margin-bottom: 6px; }
.form-group label { display: block; font-size: 12px; color: #555; margin-bottom: 3px; font-weight: 500; }
.form-row { display: flex; gap: 10px; }
.form-row .form-group { flex: 1; }
</style>

View File

@@ -0,0 +1,194 @@
<template>
<div>
<!-- Stats -->
<div class="dashboard-grid">
<div class="stat-card"><div class="label">精度项总数</div><div class="value">{{ precision.length }}</div><div class="sub">{{ groupKeys.length }} 个子系统</div></div>
<div class="stat-card green"><div class="label">合格率</div><div class="value">{{ passRate }}%</div><div class="sub">{{ passCount }}/{{ precision.length }} 项合格</div></div>
<div class="stat-card orange"><div class="label">关键项()</div><div class="value">{{ criticalCount }}</div><div class="sub">必须全部合格</div></div>
<div class="stat-card"><div class="label">待检项</div><div class="value">{{ precision.length - passCount }}</div><div class="sub"></div></div>
</div>
<div style="margin-bottom:12px;">
<el-button size="mini" type="primary" @click="handleAdd">+ 添加精度项</el-button>
</div>
<!-- Grouped sections -->
<div v-if="precision.length === 0" style="text-align:center;color:#aaa;padding:40px;">暂无精度数据</div>
<div v-for="(grp, sys) in grouped" :key="sys" class="cat-section">
<div class="cat-header">
<span>{{ sysIcon(sys) }} {{ sys }}</span>
<span class="cat-count">{{ grp.length }} · 合格 {{ grp.filter(p=>p.isQualified==='1').length }}/{{ grp.length }}</span>
</div>
<el-table :data="grp" stripe border size="small" style="width:100%;">
<el-table-column type="index" label="#" width="40" />
<el-table-column prop="itemName" label="精度项目" min-width="140" show-overflow-tooltip />
<el-table-column prop="nameEn" label="English" width="130" show-overflow-tooltip />
<el-table-column prop="targetValue" label="目标值" width="90" />
<el-table-column prop="unit" label="单位" width="50" />
<el-table-column label="重要性" width="70" align="center">
<template slot-scope="s">
<el-tag v-if="s.row.importance==='★★★'" size="mini" type="danger">关键</el-tag>
<el-tag v-else-if="s.row.importance==='★★'" size="mini" type="warning">重要</el-tag>
</template>
</el-table-column>
<el-table-column prop="tool" label="检测工具" width="110" show-overflow-tooltip />
<el-table-column prop="methodDesc" label="检测方法" width="110" show-overflow-tooltip />
<el-table-column prop="standardRef" label="依据标准" width="90" />
<el-table-column prop="actualValue" label="实测值" width="80" />
<el-table-column label="状态" width="60" align="center">
<template slot-scope="s">
<el-tag :type="s.row.isQualified==='1'?'success':'info'" size="mini">{{ s.row.isQualified==='1'?'合格':'待检' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="照片" width="60" align="center">
<template slot-scope="s">
<el-tag v-if="s.row.photos" size="mini" type="success" style="cursor:pointer;" @click="viewPhotos(s.row)">📷 {{ (s.row.photos||'').split(';').filter(Boolean).length }}</el-tag>
<span v-else style="color:#bbb;">📷 0</span>
</template>
</el-table-column>
<el-table-column label="操作" width="80" fixed="right">
<template slot-scope="s">
<el-button type="text" size="mini" @click="handleEdit(s.row)">编辑</el-button>
<el-button type="text" size="mini" @click="handleDelete(s.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- Dialog -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="600px" append-to-body @closed="onClosed">
<el-form ref="form" :model="form" :rules="rules" label-width="0" size="small">
<div class="form-row">
<div class="form-group" style="flex:0 0 140px;"><label>子系统</label><el-select v-model="form.systemName" style="width:100%;"><el-option v-for="s in systems" :key="s" :label="s" :value="s" /></el-select></div>
<div class="form-group" style="flex:1;"><label>精度项目中文</label><el-input v-model="form.itemName" /></div>
</div>
<div class="form-group"><label>English Name</label><el-input v-model="form.nameEn" /></div>
<div class="form-row">
<div class="form-group"><label>目标值</label><el-input v-model="form.targetValue" /></div>
<div class="form-group" style="flex:0 0 70px;"><label>单位</label><el-input v-model="form.unit" /></div>
<div class="form-group" style="flex:0 0 120px;"><label>重要性</label><el-select v-model="form.importance" style="width:100%;"><el-option label="★★★ 关键" value="★★★" /><el-option label="★★ 重要" value="★★" /><el-option label="★ 一般" value="★" /></el-select></div>
</div>
<div class="form-row">
<div class="form-group"><label>检测工具</label><el-input v-model="form.tool" /></div>
<div class="form-group"><label>检测方法</label><el-input v-model="form.methodDesc" /></div>
</div>
<div class="form-row">
<div class="form-group"><label>依据标准</label><el-input v-model="form.standardRef" /></div>
<div class="form-group"><label>实测值</label><el-input v-model="form.actualValue" /></div>
</div>
<div style="margin-top:8px;">
<el-checkbox v-model="form._qualified">合格</el-checkbox>
</div>
<div class="form-group" style="margin-top:8px;">
<label>📷 检测照片文件名 ; 分隔多个</label>
<el-input v-model="form.photos" />
</div>
</el-form>
<div slot="footer">
<el-button size="small" @click="dialogVisible=false">取消</el-button>
<el-button size="small" type="primary" @click="save">保存</el-button>
</div>
</el-dialog>
<!-- Photos dialog -->
<el-dialog title="检测照片" :visible.sync="photoVisible" width="400px" append-to-body>
<div v-if="photoList.length===0" style="text-align:center;color:#aaa;">暂无照片</div>
<div v-for="(f,i) in photoList" :key="i" class="photo-item">📷 {{ f }}</div>
</el-dialog>
</div>
</template>
<script>
import { listInstallPrecisionAll, addInstallPrecision, updateInstallPrecision, delInstallPrecision } from '@/api/rm/installPrecision'
export default {
name: 'InstallPrecision',
props: { projectId: { type: [Number, String], default: null } },
watch: { projectId: { immediate: true, handler(v) { if (v) this.loadData() } } },
data() {
return {
loading: false,
precision: [],
dialogVisible: false,
dialogTitle: '',
form: this.cleanForm(),
rules: { itemName: [{ required: true, message: '请填写精度项目', trigger: 'blur' }] },
systems: ['轧辊系统','AGC系统','主机框架','液压系统','电气系统','辅助设备','冷却润滑','安全装置'],
photoVisible: false,
photoList: []
}
},
computed: {
grouped() {
const map = {}
this.precision.forEach(p => { const s = p.systemName||'其他'; if (!map[s]) map[s]=[]; map[s].push(p) })
return map
},
groupKeys() { return Object.keys(this.grouped) },
passCount() { return this.precision.filter(p => p.isQualified==='1').length },
criticalCount() { return this.precision.filter(p => p.importance==='★★★').length },
passRate() {
const n = this.precision.length
return n ? Math.round(this.passCount/n*100) : 0
}
},
methods: {
sysIcon(s) { return { '轧辊系统':'🔄','AGC系统':'📐','主机框架':'🏛️','液压系统':'💧','电气系统':'⚡','辅助设备':'🔩','冷却润滑':'❄️','安全装置':'🛡️' }[s]||'📌' },
cleanForm() { return { systemName:'轧辊系统', itemName:'', nameEn:'', targetValue:'', unit:'mm', importance:'★★★', tool:'', methodDesc:'', standardRef:'', actualValue:'', _qualified:false, photos:'' } },
loadData() {
if (!this.projectId) return
this.loading = true
listInstallPrecisionAll({ projectId: this.projectId }).then(res => {
this.precision = res.data || []
}).finally(() => { this.loading = false })
},
handleAdd() { this.dialogTitle='添加精度项'; this.form=this.cleanForm(); this.dialogVisible=true },
handleEdit(row) {
this.dialogTitle='编辑精度项'
this.form = { ...row, _qualified: row.isQualified==='1' }
this.dialogVisible = true
},
handleDelete(row) {
this.$confirm('确认删除?','提示',{type:'warning'}).then(() => {
delInstallPrecision(row.precisionId).then(() => { this.loadData() })
}).catch(() => {})
},
viewPhotos(row) {
const list = (row.photos||'').split(';').filter(Boolean)
if (list.length === 0) { this.$message.info('暂无检测照片'); return }
this.photoList = list
this.photoVisible = true
},
save() {
this.$refs.form.validate(valid => {
if (!valid) return
const data = {
...this.form,
projectId: this.projectId,
isQualified: this.form._qualified ? '1' : '0'
}
delete data._qualified
const act = data.precisionId ? updateInstallPrecision(data) : addInstallPrecision(data)
act.then(() => { this.dialogVisible=false; this.loadData() })
})
},
onClosed() { this.$refs.form?.clearValidate() }
}
}
</script>
<style scoped>
.dashboard-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 8px; margin-bottom: 12px; }
.stat-card { background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 6px; padding: 10px; text-align: center; }
.stat-card .label { font-size: 11px; color: #6c757d; }
.stat-card .value { font-size: 20px; font-weight: 700; color: #1a5a9e; }
.stat-card.green .value { color: #28a745; }
.stat-card.orange .value { color: #e67e22; }
.cat-section { margin-bottom: 12px; border: 1px solid #e9ecef; border-radius: 6px; overflow: hidden; }
.cat-header { padding: 6px 10px; background: #f8f9fa; font-size: 13px; font-weight: 600; display: flex; justify-content: space-between; border-bottom: 1px solid #e9ecef; }
.cat-count { font-size: 11px; color: #6c757d; font-weight: 400; }
.form-group { margin-bottom: 6px; }
.form-group label { display: block; font-size: 12px; color: #555; margin-bottom: 3px; font-weight: 500; }
.form-row { display: flex; gap: 10px; }
.form-row .form-group { flex: 1; }
.photo-item { padding: 4px 0; font-size: 12px; }
</style>

View File

@@ -0,0 +1,203 @@
<template>
<div>
<!-- Stats -->
<div class="dashboard-grid">
<div class="stat-card"><div class="label">安装总进度</div><div class="value" style="color:#1890ff;">{{ overallPct }}%</div><div class="sub">{{ doneCount }}/{{ progress.length }} 项完成</div></div>
<div class="stat-card green"><div class="label">已完成</div><div class="value">{{ doneCount }}</div><div class="sub"></div></div>
<div class="stat-card orange"><div class="label">进行中</div><div class="value">{{ progCount }}</div><div class="sub"></div></div>
<div class="stat-card"><div class="label">未开始</div><div class="value">{{ pendingCount }}</div><div class="sub"></div></div>
</div>
<div style="margin-bottom:12px;">
<el-button size="mini" type="primary" @click="handleAdd">+ 添加进度项</el-button>
</div>
<!-- Bar chart -->
<div style="font-weight:600;font-size:13px;margin-bottom:8px;">📊 安装进度条形图</div>
<div v-if="progress.length === 0" style="text-align:center;color:#aaa;padding:20px;">暂无安装进度数据</div>
<div v-for="(p,i) in progress" :key="i" style="margin-bottom:8px;">
<div style="display:flex;justify-content:space-between;font-size:11px;margin-bottom:3px;">
<span style="font-weight:600;">
{{ p.itemName }}
<el-tag v-if="isOverdue(p)" size="mini" type="danger" style="margin-left:4px;">逾期</el-tag>
</span>
<span style="color:#6c757d;">{{ p.planStart||'?' }}~{{ p.planEnd||'?' }} · {{ statusLabel(p) }} · {{ barPercent(p) }}%</span>
</div>
<div style="height:16px;background:#eee;border-radius:4px;overflow:hidden;">
<div :style="'height:100%;width:'+barPercent(p)+'%;border-radius:4px;background:'+barColor(p)+';transition:width 0.5s;'"></div>
</div>
<div v-if="p.actualStart||p.actualEnd" style="font-size:9px;color:#6c757d;margin-top:1px;">
实际: {{ p.actualStart||'-' }}~{{ p.actualEnd||'-' }}
</div>
<div v-if="isOverdue(p) && p.delayReason" style="font-size:10px;color:#e74c3c;margin-top:1px;">
延误原因{{ p.delayReason }}
</div>
</div>
<el-divider></el-divider>
<!-- Detail table -->
<div style="font-weight:600;font-size:13px;margin-bottom:8px;">📋 安装进度明细</div>
<el-table :data="progress" v-loading="loading" stripe border size="small" style="width:100%;">
<el-table-column type="index" label="#" width="40" />
<el-table-column prop="itemName" label="安装项目" min-width="130" show-overflow-tooltip />
<el-table-column prop="planStart" label="计划开始" width="90" />
<el-table-column prop="planEnd" label="计划结束" width="90" />
<el-table-column prop="actualStart" label="实际开始" width="90" />
<el-table-column prop="actualEnd" label="实际结束" width="90" />
<el-table-column label="状态" width="80" align="center">
<template slot-scope="s">
<el-tag :type="s.row.status==='done'?'success':s.row.status==='progress'?'primary':'info'" size="mini">
{{ s.row.status==='done'?'已完成':s.row.status==='progress'?'进行中':'未开始' }}
</el-tag>
<el-tag v-if="isOverdue(s.row)" size="mini" type="danger" style="margin-left:2px;">逾期</el-tag>
</template>
</el-table-column>
<el-table-column prop="delayReason" label="延误原因" width="120" show-overflow-tooltip />
<el-table-column label="📷图片" width="55" align="center">
<template slot-scope="s">
<el-tag v-if="s.row.images" size="mini" type="success">📷 {{ (s.row.images||'').split(';').filter(Boolean).length }}</el-tag>
<span v-else style="color:#bbb;">📷 0</span>
</template>
</el-table-column>
<el-table-column label="🎬视频" width="55" align="center">
<template slot-scope="s">
<el-tag v-if="s.row.videos" size="mini" type="primary">🎬 {{ (s.row.videos||'').split(';').filter(Boolean).length }}</el-tag>
<span v-else style="color:#bbb;">🎬 0</span>
</template>
</el-table-column>
<el-table-column label="操作" width="80" fixed="right">
<template slot-scope="s">
<el-button type="text" size="mini" @click="handleEdit(s.row)">编辑</el-button>
<el-button type="text" size="mini" @click="handleDelete(s.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- Dialog -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="500px" append-to-body @closed="onClosed">
<el-form ref="form" :model="form" :rules="rules" label-width="0" size="small">
<div class="form-group"><label>安装项目名称</label><el-input v-model="form.itemName" /></div>
<div class="form-row">
<div class="form-group"><label>计划开始</label><el-date-picker v-model="form.planStart" type="date" value-format="yyyy-MM-dd" style="width:100%;" /></div>
<div class="form-group"><label>计划结束</label><el-date-picker v-model="form.planEnd" type="date" value-format="yyyy-MM-dd" style="width:100%;" /></div>
</div>
<div class="form-row">
<div class="form-group"><label>实际开始</label><el-date-picker v-model="form.actualStart" type="date" value-format="yyyy-MM-dd" style="width:100%;" /></div>
<div class="form-group"><label>实际结束</label><el-date-picker v-model="form.actualEnd" type="date" value-format="yyyy-MM-dd" style="width:100%;" /></div>
</div>
<div class="form-row">
<div class="form-group"><label>状态</label><el-select v-model="form.status" style="width:100%;"><el-option label="未开始" value="pending" /><el-option label="进行中" value="progress" /><el-option label="已完成" value="done" /></el-select></div>
<div class="form-group"><label>延误原因</label><el-input v-model="form.delayReason" /></div>
</div>
<div class="form-row">
<div class="form-group"><label>📷 图片文件名;分隔</label><el-input v-model="form.images" /></div>
<div class="form-group"><label>🎬 视频文件名;分隔</label><el-input v-model="form.videos" /></div>
</div>
<div class="form-group"><label>备注</label><el-input v-model="form.remark" /></div>
</el-form>
<div slot="footer">
<el-button size="small" @click="dialogVisible=false">取消</el-button>
<el-button size="small" type="primary" @click="save">保存</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listInstallProgressAll, addInstallProgress, updateInstallProgress, delInstallProgress } from '@/api/rm/installProgress'
export default {
name: 'InstallProgress',
props: { projectId: { type: [Number, String], default: null } },
watch: { projectId: { immediate: true, handler(v) { if (v) this.loadData() } } },
data() {
return {
loading: false,
progress: [],
dialogVisible: false,
dialogTitle: '',
form: this.cleanForm(),
rules: { itemName: [{ required: true, message: '请填写安装项目名称', trigger: 'blur' }] }
}
},
computed: {
doneCount() { return this.progress.filter(p => p.status==='done').length },
progCount() { return this.progress.filter(p => p.status==='progress').length },
pendingCount() { return this.progress.filter(p => p.status==='pending'||!p.status).length },
overallPct() {
const n = this.progress.length
return n ? Math.round(this.doneCount/n*100) : 0
}
},
methods: {
cleanForm() { return { itemName:'', planStart:'', planEnd:'', actualStart:'', actualEnd:'', status:'pending', delayReason:'', images:'', videos:'', remark:'' } },
loadData() {
if (!this.projectId) return
this.loading = true
listInstallProgressAll({ projectId: this.projectId }).then(res => {
this.progress = res.data || []
}).finally(() => { this.loading = false })
},
isOverdue(p) {
if (p.status==='done' || !p.planEnd) return false
return new Date(p.planEnd+'T00:00:00') < new Date()
},
statusLabel(p) {
if (p.status==='done') return '已完成'
if (p.status==='progress') return '进行中'
return '未开始'
},
barPercent(p) {
if (p.status==='done') return 100
if (p.status==='progress') {
if (p.planStart && p.planEnd) {
const s = new Date(p.planStart+'T00:00:00')
const e = new Date(p.planEnd+'T00:00:00')
const now = new Date()
if (now < s) return 0
const total = e - s
if (total <= 0) return 50
return Math.min(100, Math.round((now - s)/total*100))
}
return 30
}
return 0
},
barColor(p) {
if (p.status==='done') return '#28a745'
if (this.isOverdue(p)) return '#e74c3c'
if (p.status==='progress') return '#1890ff'
return '#ddd'
},
handleAdd() { this.dialogTitle='添加进度项'; this.form=this.cleanForm(); this.dialogVisible=true },
handleEdit(row) { this.dialogTitle='编辑进度项'; this.form={...row}; this.dialogVisible=true },
handleDelete(row) {
this.$confirm('确认删除?','提示',{type:'warning'}).then(() => {
delInstallProgress(row.progressId).then(() => { this.loadData() })
}).catch(() => {})
},
save() {
this.$refs.form.validate(valid => {
if (!valid) return
const data = { ...this.form, projectId: this.projectId }
const act = data.progressId ? updateInstallProgress(data) : addInstallProgress(data)
act.then(() => { this.dialogVisible=false; this.loadData() })
})
},
onClosed() { this.$refs.form?.clearValidate() }
}
}
</script>
<style scoped>
.dashboard-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 8px; margin-bottom: 12px; }
.stat-card { background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 6px; padding: 10px; text-align: center; }
.stat-card .label { font-size: 11px; color: #6c757d; }
.stat-card .value { font-size: 20px; font-weight: 700; color: #1a5a9e; }
.stat-card.green .value { color: #28a745; }
.stat-card.orange .value { color: #e67e22; }
.form-group { margin-bottom: 6px; }
.form-group label { display: block; font-size: 12px; color: #555; margin-bottom: 3px; font-weight: 500; }
.form-row { display: flex; gap: 10px; }
.form-row .form-group { flex: 1; }
</style>

View File

@@ -0,0 +1,162 @@
<template>
<div>
<!-- Stats -->
<div class="dashboard-grid">
<div class="stat-card"><div class="label">工具总项数</div><div class="value">{{ tools.length }}</div><div class="sub">{{ groupKeys.length }} 个分类</div></div>
<div class="stat-card green"><div class="label">工具总金额</div><div class="value">¥{{ totalAmount }}</div><div class="sub">预估采购总价</div></div>
<div class="stat-card orange"><div class="label">关键工具()</div><div class="value">{{ criticalCount }}</div><div class="sub">必须到位</div></div>
<div class="stat-card"><div class="label">已到位</div><div class="value">{{ arrivedCount }}/{{ tools.length }}</div><div class="sub"></div></div>
</div>
<div style="margin-bottom:12px;">
<el-button size="mini" type="primary" @click="handleAdd">+ 添加工具</el-button>
</div>
<!-- Grouped sections -->
<div v-if="tools.length === 0" style="text-align:center;color:#aaa;padding:40px;">暂无工具数据</div>
<div v-for="(grp, cat) in grouped" :key="cat" class="cat-section">
<div class="cat-header">
<span>{{ catIcon(cat) }} {{ cat }}</span>
<span class="cat-count">{{ grp.length }} · ¥{{ groupTotal(grp).toLocaleString() }}</span>
</div>
<el-table :data="grp" stripe border size="small" style="width:100%;">
<el-table-column type="index" label="#" width="40" />
<el-table-column prop="name" label="工具名称" min-width="140" show-overflow-tooltip />
<el-table-column prop="spec" label="规格型号" width="130" show-overflow-tooltip />
<el-table-column prop="qty" label="数量" width="50" />
<el-table-column prop="unit" label="单位" width="50" />
<el-table-column prop="unitPrice" label="单价" width="80" align="right" />
<el-table-column prop="totalPrice" label="总价" width="90" align="right" />
<el-table-column label="重要" width="65" align="center">
<template slot-scope="s">
<el-tag v-if="s.row.priority==='★★'" size="mini" type="danger">关键</el-tag>
<el-tag v-else-if="s.row.priority==='★'" size="mini" type="warning">重要</el-tag>
</template>
</el-table-column>
<el-table-column prop="purpose" label="用途" width="130" show-overflow-tooltip />
<el-table-column prop="responsible" label="责任人" width="70" />
<el-table-column label="状态" width="70" align="center">
<template slot-scope="s">
<el-tag :type="s.row.status==='已到位'?'success':s.row.status==='已确认'?'':'info'" size="mini">{{ s.row.status||'待确认' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="80" fixed="right">
<template slot-scope="s">
<el-button type="text" size="mini" @click="handleEdit(s.row)">编辑</el-button>
<el-button type="text" size="mini" @click="handleDelete(s.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- Dialog -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="600px" append-to-body @closed="onClosed">
<el-form ref="form" :model="form" :rules="rules" label-width="0" size="small">
<div class="form-row">
<div class="form-group" style="flex:1;"><label>工具名称</label><el-input v-model="form.name" /></div>
<div class="form-group" style="flex:0 0 140px;"><label>分类</label><el-select v-model="form.category" style="width:100%;"><el-option v-for="c in categories" :key="c" :label="c" :value="c" /></el-select></div>
</div>
<div class="form-group"><label>英文名称</label><el-input v-model="form.nameEn" /></div>
<div class="form-row">
<div class="form-group" style="flex:1;"><label>规格型号</label><el-input v-model="form.spec" /></div>
<div class="form-group" style="flex:0 0 70px;"><label>数量</label><el-input v-model="form.qty" /></div>
<div class="form-group" style="flex:0 0 70px;"><label>单位</label><el-input v-model="form.unit" /></div>
</div>
<div class="form-row">
<div class="form-group"><label>单价()</label><el-input v-model="form.unitPrice" @input="autoCalcTotal" /></div>
<div class="form-group"><label>总价()</label><el-input v-model="form.totalPrice" /></div>
<div class="form-group"><label>重要程度</label><el-select v-model="form.priority" style="width:100%;" clearable><el-option label="普通" value="" /><el-option label="★ 重要" value="★" /><el-option label="★★ 关键" value="★★" /></el-select></div>
</div>
<div class="form-row">
<div class="form-group"><label>到位日期</label><el-date-picker v-model="form.arrivalDate" type="date" value-format="yyyy-MM-dd" style="width:100%;" /></div>
<div class="form-group"><label>责任人</label><el-input v-model="form.responsible" /></div>
</div>
<div class="form-group"><label>主要用途</label><el-input v-model="form.purpose" /></div>
<div class="form-row">
<div class="form-group"><label>状态</label><el-select v-model="form.status" style="width:100%;"><el-option label="待确认" value="待确认" /><el-option label="已确认" value="已确认" /><el-option label="已到位" value="已到位" /><el-option label="已取消" value="已取消" /></el-select></div>
<div class="form-group"><label>备注</label><el-input v-model="form.remark" /></div>
</div>
</el-form>
<div slot="footer">
<el-button size="small" @click="dialogVisible=false">取消</el-button>
<el-button size="small" type="primary" @click="save">保存</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listInstallToolAll, addInstallTool, updateInstallTool, delInstallTool } from '@/api/rm/installTool'
export default {
name: 'InstallTools',
props: { projectId: { type: [Number, String], default: null } },
watch: { projectId: { immediate: true, handler(v) { if (v) this.loadData() } } },
data() {
return {
tools: [],
dialogVisible: false,
dialogTitle: '',
form: this.cleanForm(),
rules: { name: [{ required: true, message: '请填写工具名称', trigger: 'blur' }] },
categories: ['起重吊装','测量仪器','机械安装','液压专用','电气安装','其他']
}
},
computed: {
grouped() {
const map = {}
this.tools.forEach(t => { const c = t.category||'其他'; if (!map[c]) map[c]=[]; map[c].push(t) })
return map
},
groupKeys() { return Object.keys(this.grouped) },
totalAmount() { return this.tools.reduce((s,t) => s+(parseFloat(t.totalPrice)||0), 0).toLocaleString() },
criticalCount() { return this.tools.filter(t => t.priority==='★★').length },
arrivedCount() { return this.tools.filter(t => t.status==='已到位').length }
},
methods: {
catIcon(cat) { return { '起重吊装':'🏗️','测量仪器':'📐','机械安装':'🔩','液压专用':'💧','电气安装':'⚡' }[cat]||'📌' },
groupTotal(arr) { return arr.reduce((s,t) => s+(parseFloat(t.totalPrice)||0), 0) },
cleanForm() { return { name:'', nameEn:'', spec:'', qty:'', unit:'台', unitPrice:'', totalPrice:'', priority:'', arrivalDate:'', purpose:'', responsible:'', status:'待确认', category:'起重吊装', remark:'' } },
loadData() {
if (!this.projectId) return
listInstallToolAll({ projectId: this.projectId }).then(res => { this.tools = res.data || [] })
},
handleAdd() { this.dialogTitle='添加工具'; this.form=this.cleanForm(); this.dialogVisible=true },
handleEdit(row) { this.dialogTitle='编辑工具'; this.form={...row, arrivalDate: row.arrivalDate||''}; this.dialogVisible=true },
handleDelete(row) {
this.$confirm('确认删除?','提示',{type:'warning'}).then(() => {
delInstallTool(row.toolId).then(() => { this.loadData() })
}).catch(() => {})
},
autoCalcTotal() {
const qty = parseFloat(this.form.qty)||0
const price = parseFloat(this.form.unitPrice)||0
if (qty && price) this.form.totalPrice = (qty * price).toString()
},
save() {
this.$refs.form.validate(valid => {
if (!valid) return
const data = { ...this.form, projectId: this.projectId }
const act = data.toolId ? updateInstallTool(data) : addInstallTool(data)
act.then(() => { this.dialogVisible=false; this.loadData() })
})
},
onClosed() { this.$refs.form?.clearValidate() }
}
}
</script>
<style scoped>
.dashboard-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 8px; margin-bottom: 12px; }
.stat-card { background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 6px; padding: 10px; text-align: center; }
.stat-card .label { font-size: 11px; color: #6c757d; }
.stat-card .value { font-size: 20px; font-weight: 700; color: #1a5a9e; }
.stat-card.green .value { color: #28a745; }
.stat-card.orange .value { color: #e67e22; }
.cat-section { margin-bottom: 12px; border: 1px solid #e9ecef; border-radius: 6px; overflow: hidden; }
.cat-header { padding: 6px 10px; background: #f8f9fa; font-size: 13px; font-weight: 600; display: flex; justify-content: space-between; border-bottom: 1px solid #e9ecef; }
.cat-count { font-size: 11px; color: #6c757d; font-weight: 400; }
.form-group { margin-bottom: 6px; }
.form-group label { display: block; font-size: 12px; color: #555; margin-bottom: 3px; font-weight: 500; }
.form-row { display: flex; gap: 10px; }
.form-row .form-group { flex: 1; }
</style>

View File

@@ -1,5 +1,6 @@
<template>
<div class="rm-container">
<div class="current-project-bar">当前项目{{ currentProjectName }}</div>
<div class="rm-panel">
<div class="rm-panel-header">
<span>布局图确定</span>
@@ -115,6 +116,7 @@ export default {
name: 'RmLayoutFile',
data() {
return {
currentProjectName: this.$route.query.projectName || sessionStorage.getItem('rm_current_project_name') || '',
loading: false,
list: [],
total: 0,
@@ -130,14 +132,21 @@ export default {
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()
}
})
const pid = this.$route.query.projectId || sessionStorage.getItem('rm_current_project_id')
if (pid) {
this.currentProjectId = pid
this.query.projectId = pid
this.loadList()
} else {
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

View File

@@ -0,0 +1,148 @@
<template>
<div class="rm-container">
<div class="current-project-bar">当前项目{{ currentProjectName }}</div>
<div class="rm-panel">
<div class="rm-panel-header">
<span>📖 设备说明书和图纸</span>
<el-button size="small" type="primary" @click="handleAdd">+ 上传说明书/图纸</el-button>
</div>
<div class="rm-panel-body">
<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="manualName" label="文件名称" min-width="160" />
<el-table-column prop="docType" label="类型" width="100" align="center">
<template slot-scope="s">
<el-tag size="mini" :type="tagType(s.row.docType)">{{ s.row.docType }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="version" label="版本" width="70" align="center" />
<el-table-column prop="uploadDate" label="上传日期" width="100" align="center" />
<el-table-column label="操作" width="120">
<template slot-scope="s">
<el-button type="text" size="mini" @click="handleEdit(s.row)">编辑</el-button>
<el-button type="text" size="mini" @click="handleDelete(s.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="500px" append-to-body @closed="onClosed">
<el-form ref="form" :model="form" :rules="rules" label-width="0" size="small">
<div class="form-group"><label>文件名称</label><el-input v-model="form.manualName" placeholder="如1380mm轧机操作说明书" /></div>
<div class="form-row">
<div class="form-group"><label>类型</label>
<el-select v-model="form.docType" style="width:100%;">
<el-option label="说明书" value="说明书" />
<el-option label="图纸" value="图纸" />
<el-option label="维护手册" value="维护手册" />
<el-option label="备件清单" value="备件清单" />
</el-select>
</div>
<div class="form-group"><label>版本</label><el-input v-model="form.version" placeholder="V1.0" /></div>
</div>
<div class="form-group"><label>上传日期</label><el-input v-model="form.uploadDate" placeholder="yyyy-MM-dd" /></div>
<div class="form-group"><label>文件链接/路径</label><el-input v-model="form.fileUrl" placeholder="文件URL或路径" /></div>
<div class="form-group"><label>描述</label><el-input v-model="form.description" type="textarea" :rows="2" /></div>
</el-form>
<div slot="footer">
<el-button size="small" @click="dialogVisible = false">取消</el-button>
<el-button size="small" type="primary" @click="save">保存</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listManualAll, addManual, updateManual, delManual } from '@/api/rm/manual'
import { listProject } from '@/api/rm/project'
export default {
name: 'RmManual',
data() {
const today = new Date().toISOString().slice(0, 10)
return {
currentProjectName: this.$route.query.projectName || sessionStorage.getItem('rm_current_project_name') || '',
loading: false,
list: [],
currentProjectId: null,
dialogVisible: false,
dialogTitle: '',
form: { manualName: '', docType: '说明书', version: 'V1.0', uploadDate: today, fileUrl: '', description: '' },
rules: { manualName: [{ required: true, message: '请填写文件名称', trigger: 'blur' }] }
}
},
created() { this.loadCurrentProject() },
methods: {
tagType(type) {
return { '说明书': 'primary', '图纸': 'success', '维护手册': 'warning', '备件清单': 'info' }[type] || ''
},
loadCurrentProject() {
const pid = this.$route.query.projectId || sessionStorage.getItem('rm_current_project_id')
if (pid) {
this.currentProjectId = pid
this.loadData()
} else {
listProject({ pageNum: 1, pageSize: 1 }).then(res => {
const rows = res.rows || []
if (rows.length > 0) { this.currentProjectId = rows[0].projectId; this.loadData() }
})
}
},
loadData() {
if (!this.currentProjectId) return
this.loading = true
listManualAll({ projectId: this.currentProjectId }).then(res => {
this.list = res.data || []
}).finally(() => { this.loading = false })
},
handleAdd() {
this.dialogTitle = '上传说明书/图纸'
this.form = { manualName: '', docType: '说明书', version: 'V1.0', uploadDate: new Date().toISOString().slice(0, 10), fileUrl: '', description: '' }
this.dialogVisible = true
},
handleEdit(row) {
this.dialogTitle = '编辑'
this.form = {
manualId: row.manualId,
manualName: row.manualName,
docType: row.docType,
version: row.version,
uploadDate: row.uploadDate,
fileUrl: row.fileUrl,
description: row.description
}
this.dialogVisible = true
},
save() {
this.$refs.form.validate(valid => {
if (!valid) return
const data = { ...this.form, projectId: this.currentProjectId }
const action = data.manualId ? updateManual(data) : addManual(data)
action.then(() => {
this.$message.success('已保存')
this.dialogVisible = false
this.loadData()
})
})
},
handleDelete(row) {
this.$confirm('确认删除?', '提示', { type: 'warning' }).then(() => {
delManual(row.manualId).then(() => { this.loadData() })
}).catch(() => {})
},
onClosed() { this.$refs.form?.clearValidate() }
}
}
</script>
<style scoped>
.rm-container { padding: 8px; }
.rm-panel { background: #fff; border: 1px solid #d0d7de; border-radius: 8px; overflow: hidden; }
.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: 12px; }
.form-group { margin-bottom: 6px; }
.form-group label { display: block; font-size: 12px; color: #555; margin-bottom: 3px; font-weight: 500; }
.form-row { display: flex; gap: 10px; }
.form-row .form-group { flex: 1; }
</style>

View File

@@ -1,5 +1,6 @@
<template>
<div class="rm-container">
<div class="current-project-bar">当前项目{{ currentProjectName }}</div>
<div class="rm-panel">
<div class="rm-panel-header">
<span>设备制造进度</span>
@@ -118,6 +119,7 @@ export default {
name: 'RmManufacturing',
data() {
return {
currentProjectName: this.$route.query.projectName || sessionStorage.getItem('rm_current_project_name') || '',
loading: false,
deviceList: [],
stageMap: {},
@@ -132,13 +134,19 @@ export default {
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()
}
})
const pid = this.$route.query.projectId || sessionStorage.getItem('rm_current_project_id')
if (pid) {
this.currentProjectId = pid
this.loadDevices()
} else {
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

View File

@@ -1,5 +1,6 @@
<template>
<div class="rm-container">
<div class="current-project-bar">当前项目{{ currentProjectName }}</div>
<div class="rm-panel">
<div class="rm-panel-header">
<span>采购管理</span>
@@ -238,6 +239,7 @@ export default {
name: 'RmProcurement',
data() {
return {
currentProjectName: this.$route.query.projectName || sessionStorage.getItem('rm_current_project_name') || '',
activeTab: 'quotes',
currentProjectId: null,
// Quotes
@@ -286,13 +288,19 @@ export default {
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()
}
})
const pid = this.$route.query.projectId || sessionStorage.getItem('rm_current_project_id')
if (pid) {
this.currentProjectId = pid
this.loadAll()
} else {
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()

View File

@@ -1,5 +1,26 @@
<template>
<div class="dashboard-page">
<!-- Project Selector Bar -->
<div class="project-selector-bar">
<span class="selector-label">当前项目</span>
<el-select
v-model="currentProjectId"
placeholder="请选择项目"
size="small"
style="width: 320px;"
filterable
@change="onProjectChange"
>
<el-option
v-for="p in projectList"
:key="p.projectId"
:label="p.projectName + ' (' + p.projectNo + ')'"
:value="p.projectId"
/>
</el-select>
<el-button type="primary" size="small" icon="el-icon-plus" style="margin-left: 8px;" @click="createProject">新建项目</el-button>
</div>
<!-- Info Cards -->
<div class="dashboard-grid">
<div class="stat-card">
@@ -7,7 +28,7 @@
<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;" />
<el-button type="text" size="mini" icon="el-icon-edit" @click="editProject" style="color:#2176ae;" />
</div>
</div>
<div class="stat-card green">
@@ -70,7 +91,7 @@
</div>
<!-- Project Info Edit Modal -->
<el-dialog title="项目信息设置" :visible.sync="dialogVisible" width="600px" append-to-body>
<el-dialog :title="isCreating ? '新建项目' : '项目信息设置'" :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">
@@ -91,8 +112,25 @@
</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 label="项目经理" prop="managerId">
<el-select
v-model="form.managerId"
placeholder="请搜索并选择项目经理"
filterable
remote
clearable
reserve-keyword
:remote-method="searchUsers"
:loading="userSearchLoading"
style="width:100%"
>
<el-option
v-for="u in userOptions"
:key="u.userId"
:label="u.nickName + ' (' + u.userName + ')'"
:value="u.userId"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
@@ -121,7 +159,9 @@
</template>
<script>
import { getProject, updateProject, listProject, addProject } from '@/api/rm/project'
import { getProject, updateProject, listProject, addProject, allProject } from '@/api/rm/project'
import { getStageStatus } from '@/api/rm/dashboard'
import { selectUser } from '@/api/system/user'
const STAGES = [
{ key: 'budget', label: '项目预算', icon: '💰' },
@@ -159,7 +199,11 @@ export default {
clientName: [{ required: true, message: '请输入客户名称', trigger: 'blur' }]
},
// Default project will be fetched from backend
currentProjectId: null
currentProjectId: null,
projectList: [],
userOptions: [],
userSearchLoading: false,
isCreating: false
}
},
computed: {
@@ -184,47 +228,95 @@ export default {
}
},
created() {
this.loadProject()
this.loadProjectList()
},
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)
loadProjectList() {
allProject({}).then(res => {
this.projectList = res.data || []
if (this.projectList.length > 0) {
// Try sessionStorage first, fall back to first project
const savedId = sessionStorage.getItem('rm_current_project_id')
if (savedId && this.projectList.find(p => p.projectId === savedId)) {
this.currentProjectId = savedId
} else {
this.currentProjectId = this.projectList[0].projectId
}
sessionStorage.setItem('rm_current_project_id', this.currentProjectId)
this.loadProjectInfo()
this.loadStageStatus()
} else {
// No project exists yet, show empty form
this.projectInfo = {}
return null
}
}).then(res => {
if (res && res.data) {
this.projectInfo = res.data
this.currentProjectId = null
}
})
},
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()
})
})
loadProjectInfo() {
if (!this.currentProjectId) return
getProject(this.currentProjectId).then(res => {
if (res.data) {
this.projectInfo = res.data
sessionStorage.setItem('rm_current_project_name', res.data.projectName || '')
}
})
},
onProjectChange(projectId) {
if (projectId) {
sessionStorage.setItem('rm_current_project_id', projectId)
sessionStorage.removeItem('rm_current_project_name')
this.loadProjectInfo()
this.loadStageStatus()
}
},
loadStageStatus() {
if (!this.currentProjectId) return
getStageStatus(this.currentProjectId).then(res => {
this.stageStatus = res.data || {}
})
},
searchUsers(query) {
if (query === '') {
this.userOptions = []
return
}
this.form = { ...this.projectInfo }
this.userSearchLoading = true
selectUser({ nickName: query, pageNum: 1, pageSize: 20 }).then(res => {
this.userOptions = res.rows || []
}).finally(() => {
this.userSearchLoading = false
})
},
createProject() {
this.isCreating = true
this.form = {
projectName: '',
projectNo: '',
clientName: '',
startDate: '',
endDate: '',
managerId: null,
remark: ''
}
this.userOptions = []
this.dialogVisible = true
this.$nextTick(() => { this.$refs.formRef?.clearValidate() })
},
editProject() {
this.isCreating = false
this.form = {
...this.projectInfo,
managerId: this.projectInfo.managerId || null
}
// Pre-populate user select with current manager
if (this.projectInfo.managerId && this.projectInfo.managerName) {
this.userOptions = [{
userId: this.projectInfo.managerId,
nickName: this.projectInfo.managerName,
userName: ''
}]
} else {
this.userOptions = []
}
this.dialogVisible = true
this.$nextTick(() => { this.$refs.formRef?.clearValidate() })
},
@@ -232,14 +324,24 @@ export default {
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
})
if (this.isCreating) {
addProject(this.form).then(() => {
this.$message.success('项目已创建')
this.dialogVisible = false
this.loadProjectList()
}).finally(() => {
this.submitting = false
})
} else {
this.form.projectId = this.currentProjectId
updateProject(this.form).then(() => {
this.$message.success('项目信息已更新')
this.dialogVisible = false
this.loadProjectList()
}).finally(() => {
this.submitting = false
})
}
})
},
stageClass(key) {
@@ -258,8 +360,28 @@ export default {
return map[this.stageStatus[key]] || '未开始'
},
switchStage(key) {
this.$message.info(`"${STAGES.find(s => s.key === key).label}" 功能开发中`)
}
const routeMap = {
budget: '/zongbao/budget',
tech_plan: '/zongbao/tech-group/techPlan',
layout: '/zongbao/tech-group/layout',
tech_review: '/zongbao/tech-group/techReview',
drawing_design: '/zongbao/tech-group/drawingDesign',
drawing_review: '/zongbao/tech-group/drawingReview',
procurement: '/zongbao/procurement-group/procurement',
manufacturing: '/zongbao/procurement-group/manufacturing',
drawing_compare: '/zongbao/drawing-group/drawingCompare',
doc_lib: '/zongbao/drawing-group/docLib',
site_mod: '/zongbao/drawing-group/siteMod',
shipping: '/zongbao/shipping-group/shipping',
manuals: '/zongbao/shipping-group/manuals',
install_prep: '/zongbao/shipping-group/installPrep',
install_feedback: '/zongbao/shipping-group/installFeedback',
acceptance: '/zongbao/shipping-group/acceptance',
hot_commissioning: '/zongbao/shipping-group/hotCommissioning'
}
const path = routeMap[key]
if (path) this.$router.push({ path, query: { projectId: this.currentProjectId, projectName: this.projectInfo.projectName || '' } })
},
}
}
</script>
@@ -371,4 +493,21 @@ export default {
.dialog-footer {
text-align: right;
}
.project-selector-bar {
display: flex;
align-items: center;
background: #fff;
border-radius: 5px;
padding: 8px 14px;
margin-bottom: 10px;
border: 1px solid #d0d7de;
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
}
.project-selector-bar .selector-label {
font-size: 13px;
font-weight: 600;
color: #1a1a2e;
margin-right: 8px;
white-space: nowrap;
}
</style>

View File

@@ -0,0 +1,277 @@
<template>
<div class="rm-container">
<div class="current-project-bar">当前项目{{ currentProjectName }}</div>
<div class="rm-panel">
<div class="rm-panel-header">
<span>📦 发货前设备清单</span>
<el-button size="small" type="primary" @click="handleAddItem">+ 添加设备项</el-button>
</div>
<div class="rm-panel-body">
<!-- Checklist section -->
<div style="margin-bottom:16px;">
<div style="font-weight:600;margin-bottom:8px;font-size:13px;">
📋 发货前检查清单
<el-tag v-if="checklistAllDone" size="mini" type="success" style="margin-left:6px;"> 全部完成</el-tag>
<el-tag v-else size="mini" type="warning" style="margin-left:6px;"> 尚有未完成项</el-tag>
<el-button size="mini" type="text" icon="el-icon-plus" style="margin-left:8px;" @click="handleAddCheck">添加检查项</el-button>
</div>
<div v-if="checklist.length === 0" style="color:#aaa;font-size:12px;padding:8px 0;">暂无检查项</div>
<div v-for="(it, i) in checklist" :key="i" class="checklist-item" :class="it.isChecked === '1' ? 'checked' : ''">
<el-checkbox v-model="it._checked" @change="toggleCheck(it, i)"></el-checkbox>
<span class="checklist-text">{{ it.itemText }}</span>
<el-button type="text" size="mini" style="margin-left:auto;color:#999;" icon="el-icon-delete" @click="handleDeleteCheck(it, i)"></el-button>
</div>
</div>
<el-divider></el-divider>
<!-- Equipment items table -->
<div style="font-weight:600;margin-bottom:8px;font-size:13px;">📦 设备清单明细</div>
<el-table :data="items" v-loading="loading" stripe border highlight-current-row size="small" style="width:100%;">
<el-table-column type="index" label="#" width="45" />
<el-table-column prop="deviceName" label="设备名称" min-width="130" />
<el-table-column prop="spec" label="规格" width="120" />
<el-table-column prop="qty" label="数量" width="60" align="center" />
<el-table-column label="打包状态" width="80" align="center">
<template slot-scope="s">
<el-tag :type="s.row.packed === '1' ? 'success' : 'info'" size="mini">{{ s.row.packed === '1' ? '已打包' : '未打包' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="照片" width="70" align="center">
<template slot-scope="s">
<span v-if="photoCount(s.row) > 0" class="tag-media tag-media-img" @click="handleViewPhotos(s.row)">📷 {{ photoCount(s.row) }}</span>
<span v-else class="tag-media tag-media-none">📷 0</span>
</template>
</el-table-column>
<el-table-column prop="note" label="备注" min-width="120" show-overflow-tooltip />
<el-table-column label="操作" width="160" fixed="right">
<template slot-scope="s">
<el-button type="text" size="mini" icon="el-icon-edit" @click="handleEditItem(s.row)">编辑</el-button>
<el-button type="text" size="mini" icon="el-icon-delete" @click="handleDeleteItem(s.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- Add/Edit Item Dialog -->
<el-dialog :title="itemDialogTitle" :visible.sync="itemDialogVisible" width="480px" append-to-body @closed="onItemDialogClosed">
<el-form ref="itemFormRef" :model="itemForm" :rules="itemRules" label-width="0" size="small">
<div class="form-group"><label>设备名称 *</label><el-input v-model="itemForm.deviceName" /></div>
<div class="form-row">
<div class="form-group"><label>规格型号</label><el-input v-model="itemForm.spec" /></div>
<div class="form-group"><label>数量</label><el-input v-model="itemForm.qty" type="number" /></div>
</div>
<div style="margin-top:8px;">
<el-checkbox v-model="itemForm._packed">已打包</el-checkbox>
</div>
<div class="form-group" style="margin-top:10px;">
<label>📷 设备照片文件名 ; 分隔多个文件</label>
<el-input v-model="itemForm._photosStr" placeholder="主机整体.jpg;铭牌特写.jpg;打包外观.jpg" />
<div v-if="itemForm._photosStr" style="font-size:10px;color:#999;margin-top:3px;">
已上传 {{ itemForm._photosArr.length }} 张照片
</div>
</div>
<div class="form-group" style="margin-top:8px;"><label>备注</label><el-input v-model="itemForm.note" type="textarea" :rows="2" /></div>
</el-form>
<div slot="footer">
<el-button size="small" @click="itemDialogVisible = false">取消</el-button>
<el-button size="small" type="primary" @click="saveItem">保存</el-button>
</div>
</el-dialog>
<!-- Add Checklist Item Dialog -->
<el-dialog title="添加检查项" :visible.sync="checkDialogVisible" width="400px" append-to-body>
<el-form ref="checkFormRef" :model="checkForm" label-width="0" size="small">
<div class="form-group"><label>检查项内容 *</label><el-input v-model="checkForm.itemText" placeholder="如:设备外观无损伤" /></div>
</el-form>
<div slot="footer">
<el-button size="small" @click="checkDialogVisible = false">取消</el-button>
<el-button size="small" type="primary" @click="saveCheck">保存</el-button>
</div>
</el-dialog>
<!-- View Photos Dialog -->
<el-dialog :title="photoDialogTitle" :visible.sync="photoDialogVisible" width="400px" append-to-body>
<div style="font-size:12px;color:#999;margin-bottom:10px;"> {{ viewingPhotos.length }} 张照片</div>
<div v-if="viewingPhotos.length === 0" style="color:#aaa;text-align:center;padding:20px;">暂无照片</div>
<div v-for="(p, i) in viewingPhotos" :key="i" class="evidence-item ok" style="margin-bottom:4px;">
<span class="ev-icon">📷</span>{{ p }}
</div>
<div slot="footer">
<el-button size="small" @click="photoDialogVisible = false">关闭</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listShippingChecklistAll, addShippingChecklist, updateShippingChecklist, delShippingChecklist } from '@/api/rm/shippingChecklist'
import { listShippingItemAll, addShippingItem, updateShippingItem, delShippingItem } from '@/api/rm/shippingItem'
import { listProject } from '@/api/rm/project'
export default {
name: 'Shipping',
data() {
return {
currentProjectName: this.$route.query.projectName || sessionStorage.getItem('rm_current_project_name') || '',
loading: false,
checklist: [],
items: [],
currentProjectId: null,
// item dialog
itemDialogVisible: false,
itemDialogTitle: '添加设备项',
itemForm: { deviceName: '', spec: '', qty: 0, _packed: false, _photosStr: '', note: '' },
itemRules: { deviceName: [{ required: true, message: '请填写设备名称', trigger: 'blur' }] },
// check dialog
checkDialogVisible: false,
checkForm: { itemText: '' },
// photo dialog
photoDialogVisible: false,
photoDialogTitle: '',
viewingPhotos: []
}
},
computed: {
checklistAllDone() {
return this.checklist.length > 0 && this.checklist.every(it => it.isChecked === '1')
}
},
created() { this.loadCurrentProject() },
methods: {
loadCurrentProject() {
const pid = this.$route.query.projectId || sessionStorage.getItem('rm_current_project_id')
if (pid) {
this.currentProjectId = pid
this.loadData()
} else {
listProject({ pageNum: 1, pageSize: 1 }).then(res => {
const rows = res.rows || []
if (rows.length > 0) { this.currentProjectId = rows[0].projectId; this.loadData() }
})
}
},
loadData() {
if (!this.currentProjectId) return
this.loading = true
Promise.all([
listShippingChecklistAll({ projectId: this.currentProjectId }),
listShippingItemAll({ projectId: this.currentProjectId })
]).then(([clRes, itRes]) => {
this.checklist = (clRes.data || []).map(it => ({ ...it, _checked: it.isChecked === '1' }))
this.items = itRes.data || []
}).finally(() => { this.loading = false })
},
photoCount(row) {
if (!row.photos) return 0
try { return JSON.parse(row.photos).length } catch { return row.photos.split(';').filter(Boolean).length }
},
// ---- Checklist ----
handleAddCheck() {
this.checkForm = { itemText: '' }
this.checkDialogVisible = true
},
saveCheck() {
if (!this.checkForm.itemText) { this.$message.warning('请填写检查项内容'); return }
addShippingChecklist({ projectId: this.currentProjectId, itemText: this.checkForm.itemText, isChecked: '0', sortOrder: this.checklist.length + 1 }).then(() => {
this.$message.success('检查项已添加')
this.checkDialogVisible = false
this.loadData()
})
},
toggleCheck(it) {
const val = it._checked ? '1' : '0'
updateShippingChecklist({ checklistId: it.checklistId, projectId: this.currentProjectId, itemText: it.itemText, isChecked: val }).then(() => {
it.isChecked = val
})
},
handleDeleteCheck(it) {
this.$confirm('确认删除该检查项?', '提示', { type: 'warning' }).then(() => {
delShippingChecklist(it.checklistId).then(() => { this.loadData() })
}).catch(() => {})
},
// ---- Items ----
handleAddItem() {
this.itemDialogTitle = '添加设备项'
this.itemForm = { deviceName: '', spec: '', qty: 0, _packed: false, _photosStr: '', note: '' }
this.itemDialogVisible = true
},
handleEditItem(row) {
this.itemDialogTitle = '编辑设备项'
let photosStr = ''
if (row.photos) {
try { const arr = JSON.parse(row.photos); photosStr = arr.join(';') } catch { photosStr = row.photos }
}
this.itemForm = {
itemId: row.itemId,
deviceName: row.deviceName,
spec: row.spec,
qty: row.qty,
_packed: row.packed === '1',
_photosStr: photosStr,
note: row.note
}
this.itemDialogVisible = true
},
saveItem() {
this.$refs.itemFormRef.validate(valid => {
if (!valid) return
const photosArr = this.itemForm._photosStr ? this.itemForm._photosStr.split(';').map(f => f.trim()).filter(Boolean) : []
const data = {
itemId: this.itemForm.itemId,
projectId: this.currentProjectId,
deviceName: this.itemForm.deviceName,
spec: this.itemForm.spec,
qty: this.itemForm.qty,
packed: this.itemForm._packed ? '1' : '0',
photos: photosArr.length > 0 ? JSON.stringify(photosArr) : null,
note: this.itemForm.note
}
const action = data.itemId ? updateShippingItem(data) : addShippingItem(data)
action.then(() => {
this.$message.success(data.itemId ? '设备项已更新' : '设备项已添加')
this.itemDialogVisible = false
this.loadData()
})
})
},
handleDeleteItem(row) {
this.$confirm(`确认删除设备「${row.deviceName}」?`, '提示', { type: 'warning' }).then(() => {
delShippingItem(row.itemId).then(() => { this.loadData() })
}).catch(() => {})
},
onItemDialogClosed() { this.$refs.itemFormRef?.clearValidate() },
handleViewPhotos(row) {
let arr = []
if (row.photos) {
try { arr = JSON.parse(row.photos) } catch { arr = row.photos.split(';').filter(Boolean) }
}
if (arr.length === 0) { this.$message.info('该设备暂无照片'); return }
this.viewingPhotos = arr
this.photoDialogTitle = `📷 ${row.deviceName} - 设备照片`
this.photoDialogVisible = true
}
}
}
</script>
<style scoped>
.checklist-item {
display: flex;
align-items: center;
gap: 8px;
padding: 5px 0;
border-bottom: 1px solid #f0f0f0;
font-size: 12px;
}
.checklist-item:last-child { border-bottom: none; }
.checklist-item.checked .checklist-text { text-decoration: line-through; color: #aaa; }
.tag-media { font-size: 12px; cursor: pointer; padding: 2px 8px; border-radius: 4px; }
.tag-media-img { background: #e8f5e9; color: #2e7d32; }
.tag-media-none { background: #f5f5f5; color: #999; cursor: default; }
.tag-media:hover { opacity: 0.8; }
.evidence-item { display: flex; align-items: center; gap: 6px; padding: 6px 10px; background: #f9f9f9; border-radius: 4px; font-size: 12px; }
.ev-icon { font-size: 14px; }
</style>

View File

@@ -0,0 +1,417 @@
<template>
<div class="rm-container">
<div class="current-project-bar">当前项目{{ currentProjectName }}</div>
<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">
<div class="rm-warning">
<b>重要</b>现场修改后的设备必须上传修改后的图纸至资料库防止下次再次出现同样问题所有现场修改记录将自动同步至图纸资料库
</div>
<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="deviceName" label="设备名称" min-width="130" />
<el-table-column prop="location" label="修改位置" width="120" />
<el-table-column prop="modReason" label="问题描述" min-width="160" show-overflow-tooltip />
<el-table-column prop="modPerson" label="修改人" width="80" />
<el-table-column prop="modDate" label="修改日期" width="100" align="center" />
<el-table-column prop="status" label="状态" width="80" align="center">
<template slot-scope="s">
<el-tag :type="s.row.status === 'done' ? 'success' : 'warning'" size="mini">{{ s.row.status === 'done' ? '已整改' : '待整改' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="图片" width="64" align="center">
<template slot-scope="s">
<span class="tag-media" :class="s.row._imageCount ? 'tag-media-img' : 'tag-media-none'" @click="s.row._imageCount && handleMedia(s.row)">
📷 {{ s.row._imageCount || 0 }}
</span>
</template>
</el-table-column>
<el-table-column label="视频" width="64" align="center">
<template slot-scope="s">
<span class="tag-media" :class="s.row._videoCount ? 'tag-media-vid' : 'tag-media-none'" @click="s.row._videoCount && handleMedia(s.row)">
🎬 {{ s.row._videoCount || 0 }}
</span>
</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-view" @click="handleView(s.row)">查看</el-button>
<el-button type="text" size="mini" icon="el-icon-edit" @click="handleEdit(s.row)">编辑</el-button>
<el-button type="text" size="mini" icon="el-icon-delete" @click="handleDelete(s.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- Add/Edit Dialog matching HTML prototype -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="620px" append-to-body @closed="onDialogClosed">
<el-form ref="formRef" :model="form" :rules="rules" label-width="0" size="small">
<div class="form-row">
<div class="form-group"><label>设备名称</label><el-input v-model="form.deviceName" /></div>
<div class="form-group"><label>修改位置/部位</label><el-input v-model="form.location" placeholder="如:主传动轴轴承座" /></div>
</div>
<div class="form-group"><label>现场问题描述</label><el-input v-model="form.modReason" type="textarea" :rows="2" /></div>
<div class="form-group"><label>修改方案/解决措施</label><el-input v-model="form.solution" type="textarea" :rows="2" /></div>
<div class="form-row">
<div class="form-group sm-media-section">
<label>📷 现场照片 <span style="font-weight:400;color:#888;">(可多选)</span></label>
<div class="sm-upload-zone" @click="$refs.imgInput.click()">
<div class="uz-icon">📤</div>
<div class="uz-text">点击选择现场照片</div>
<div class="uz-hint">支持 JPG/PNG/WebP单张 5MB</div>
</div>
<input ref="imgInput" type="file" accept="image/*" multiple style="display:none" @change="onImageSelect">
<div v-if="existingImages.length || pendingImages.length" class="sm-media-grid">
<div class="sm-media-thumb" v-for="(img, i) in existingImages" :key="'e'+i">
<el-image :src="img.fileUrl" fit="cover" class="sm-thumb-img" :preview-src-list="existingPreviewList" />
<div class="sm-thumb-name">{{ img.fileName }}</div>
<button class="sm-thumb-remove" @click="removeExistingMedia(i, 'image')" title="移除">×</button>
</div>
<div class="sm-media-thumb" v-for="(img, i) in pendingImages" :key="'p'+i">
<img :src="img.url" class="sm-thumb-img" />
<div class="sm-thumb-name">{{ img.name }}</div>
<button class="sm-thumb-remove" @click="removePendingMedia(i, 'image')" title="移除">×</button>
</div>
</div>
</div>
<div class="form-group sm-media-section">
<label>🎬 现场视频 <span style="font-weight:400;color:#888;">(可选)</span></label>
<div class="sm-upload-zone" @click="$refs.vidInput.click()">
<div class="uz-icon">🎥</div>
<div class="uz-text">点击选择现场视频</div>
<div class="uz-hint">支持 MP4/WebM单段 100MB</div>
</div>
<input ref="vidInput" type="file" accept="video/*" style="display:none" @change="onVideoSelect">
<div v-if="existingVideos.length || pendingVideos.length" class="sm-video-list">
<div class="sm-video-item" v-for="(v, i) in existingVideos" :key="'e'+i">
<span class="vi-icon">🎬</span>
<div class="vi-info"><div class="vi-name">{{ v.fileName }}</div></div>
<span class="vi-remove" @click="removeExistingMedia(i, 'video')" title="移除">×</span>
</div>
<div class="sm-video-item" v-for="(v, i) in pendingVideos" :key="'p'+i">
<span class="vi-icon">🎬</span>
<div class="vi-info"><div class="vi-name">{{ v.name }}</div></div>
<span class="vi-remove" @click="removePendingMedia(i, 'video')" title="移除">×</span>
</div>
</div>
</div>
</div>
<div class="form-row">
<div class="form-group"><label>修改人</label><el-input v-model="form.modPerson" /></div>
<div class="form-group"><label>修改日期</label><el-date-picker v-model="form.modDate" type="date" style="width:100%" value-format="yyyy-MM-dd" /></div>
<div class="form-group"><label>状态</label>
<el-select v-model="form.status" style="width:100%">
<el-option label="待整改" value="pending" />
<el-option label="已整改" value="done" />
</el-select>
</div>
</div>
<div class="form-group"><label>防止再发措施经验反馈</label><el-input v-model="form.preventAction" type="textarea" :rows="2" placeholder="记录防止同样问题再次发生的措施" /></div>
<div style="margin-top:8px;">
<el-checkbox v-model="form.drawingUpdated" true-label="1" false-label="0">已上传修改后的图纸至资料库</el-checkbox>
</div>
</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>
<!-- View Dialog -->
<el-dialog title="修改详情" :visible.sync="viewVisible" width="560px" append-to-body>
<div class="detail-grid" v-if="viewItem">
<div class="dg-label">设备名称</div><div class="dg-value">{{ viewItem.deviceName }}</div>
<div class="dg-label">修改位置</div><div class="dg-value">{{ viewItem.location || '-' }}</div>
<div class="dg-label">问题描述</div><div class="dg-value">{{ viewItem.modReason || '无' }}</div>
<div class="dg-label">修改方案</div><div class="dg-value">{{ viewItem.solution || '无' }}</div>
<div class="dg-label">修改人</div><div class="dg-value">{{ viewItem.modPerson || '-' }}</div>
<div class="dg-label">修改日期</div><div class="dg-value">{{ viewItem.modDate || '-' }}</div>
<div class="dg-label">状态</div>
<div class="dg-value">
<el-tag :type="viewItem.status === 'done' ? 'success' : 'warning'" size="mini">{{ viewItem.status === 'done' ? '已整改' : '待整改' }}</el-tag>
</div>
<div class="dg-label">防止再发措施</div><div class="dg-value">{{ viewItem.preventAction || '无' }}</div>
<div class="dg-label">图纸更新</div><div class="dg-value">{{ viewItem.drawingUpdated === '1' ? '已上传' : '未上传' }}</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button size="small" @click="viewVisible = false">关闭</el-button>
</div>
</el-dialog>
<!-- Media View Dialog (read-only popup for image/video badge click) -->
<el-dialog title="多媒体查看" :visible.sync="mediaVisible" width="580px" append-to-body @closed="mediaList = []">
<div v-if="mediaImages.length" style="margin-bottom:16px">
<strong>图片 ({{ mediaImages.length }})</strong>
<div class="sm-media-grid" style="margin-top:8px">
<div class="sm-media-thumb" v-for="m in mediaImages" :key="m.mediaId">
<el-image :src="m.fileUrl" fit="cover" class="sm-thumb-img" :preview-src-list="mediaPreviewList" />
</div>
</div>
</div>
<div v-if="mediaVideos.length">
<strong>视频 ({{ mediaVideos.length }})</strong>
<div class="sm-video-list" style="margin-top:8px">
<div class="sm-video-item" v-for="m in mediaVideos" :key="m.mediaId">
<span class="vi-icon">🎬</span>
<div class="vi-info"><a :href="m.fileUrl" target="_blank" class="vi-name" style="color:#409eff">{{ m.fileName }}</a></div>
</div>
</div>
</div>
<div v-if="!mediaImages.length && !mediaVideos.length" style="text-align:center;color:#999;padding:20px">暂无多媒体文件</div>
<div slot="footer" class="dialog-footer">
<el-button size="small" @click="mediaVisible = false">关闭</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import axios from 'axios'
import { listSiteModAll, getSiteMod, addSiteMod, updateSiteMod, delSiteMod } from '@/api/rm/siteMod'
import { listSiteModMediaAll, addSiteModMedia, delSiteModMedia } from '@/api/rm/siteMod'
import { listProject } from '@/api/rm/project'
import { getToken } from '@/utils/auth'
export default {
name: 'RmSiteMod',
data() {
return {
currentProjectName: this.$route.query.projectName || sessionStorage.getItem('rm_current_project_name') || '',
loading: false, list: [], currentProjectId: null,
dialogVisible: false, dialogTitle: '', submitting: false,
form: {
deviceName: '', location: '', modReason: '', solution: '',
modPerson: '', modDate: '', status: 'pending',
preventAction: '', drawingUpdated: '0'
},
rules: { deviceName: [{ required: true, message: '请填写设备名称', trigger: 'blur' }] },
viewVisible: false, viewItem: null,
mediaVisible: false, mediaLoading: false, mediaList: [],
// Media management for dialog
existingMedia: [], // from DB
pendingImages: [], // { file, name, url }
pendingVideos: [], // { file, name, url }
deletedMediaIds: [] // media IDs to remove on save
}
},
computed: {
existingImages() { return this.existingMedia.filter(m => m.mediaType === 'image') },
existingVideos() { return this.existingMedia.filter(m => m.mediaType === 'video') },
existingPreviewList() { return this.existingImages.map(m => m.fileUrl) },
mediaImages() { return this.mediaList.filter(m => m.mediaType === 'image') },
mediaVideos() { return this.mediaList.filter(m => m.mediaType === 'video') },
mediaPreviewList() { return this.mediaImages.map(m => m.fileUrl) }
},
created() { this.loadCurrentProject() },
methods: {
loadCurrentProject() {
const pid = this.$route.query.projectId || sessionStorage.getItem('rm_current_project_id')
if (pid) {
this.currentProjectId = pid
this.loadList()
} else {
listProject({ pageNum: 1, pageSize: 1 }).then(res => {
const rows = res.rows || []
if (rows.length > 0) { this.currentProjectId = rows[0].projectId; this.loadList() }
})
}
},
loadList() {
this.loading = true
Promise.all([
listSiteModAll({ projectId: this.currentProjectId }),
listSiteModMediaAll({})
]).then(([modRes, mediaRes]) => {
const mods = modRes.data || []
const counts = {}
;(mediaRes.data || []).forEach(m => {
if (!counts[m.modId]) counts[m.modId] = { images: 0, videos: 0 }
if (m.mediaType === 'image') counts[m.modId].images++
else if (m.mediaType === 'video') counts[m.modId].videos++
})
mods.forEach(item => {
const c = counts[item.modId] || { images: 0, videos: 0 }
item._imageCount = c.images
item._videoCount = c.videos
})
this.list = mods
}).finally(() => { this.loading = false })
},
handleAdd() {
if (!this.currentProjectId) { this.$message.warning('请先在项目总览中创建项目'); return }
this.dialogTitle = '记录现场修改'
this.form = { projectId: this.currentProjectId, deviceName: '', location: '', modReason: '', solution: '', modPerson: '', modDate: '', status: 'pending', preventAction: '', drawingUpdated: '0' }
this.existingMedia = []
this.pendingImages = []
this.pendingVideos = []
this.deletedMediaIds = []
this.dialogVisible = true; this.$nextTick(() => { this.$refs.formRef?.clearValidate() })
},
handleEdit(row) {
this.dialogTitle = '编辑现场修改'
this.pendingImages = []
this.pendingVideos = []
this.deletedMediaIds = []
getSiteMod(row.modId).then(r => {
this.form = r.data
this.dialogVisible = true
this.loadExistingMedia(row.modId)
this.$nextTick(() => { this.$refs.formRef?.clearValidate() })
})
},
loadExistingMedia(modId) {
listSiteModMediaAll({ modId }).then(r => {
this.existingMedia = r.data || []
})
},
handleView(row) {
this.viewItem = row; this.viewVisible = true
},
handleDelete(row) {
this.$confirm(`确认删除 "${row.deviceName}" 的修改记录?`, '提示', { type: 'warning' }).then(() => { delSiteMod(row.modId).then(() => { this.$message.success('删除成功'); this.loadList() }) })
},
// File selection
onImageSelect(e) {
const files = Array.from(e.target.files || [])
files.forEach(file => {
if (file.size > 5 * 1024 * 1024) { this.$message.warning(`图片 "${file.name}" 超过5MB限制`); return }
this.pendingImages.push({ file, name: file.name, url: URL.createObjectURL(file) })
})
e.target.value = ''
},
onVideoSelect(e) {
const files = Array.from(e.target.files || [])
files.forEach(file => {
if (file.size > 100 * 1024 * 1024) { this.$message.warning(`视频 "${file.name}" 超过100MB限制`); return }
this.pendingVideos.push({ file, name: file.name, url: URL.createObjectURL(file) })
})
e.target.value = ''
},
removePendingMedia(index, type) {
if (type === 'image') {
URL.revokeObjectURL(this.pendingImages[index].url)
this.pendingImages.splice(index, 1)
} else {
URL.revokeObjectURL(this.pendingVideos[index].url)
this.pendingVideos.splice(index, 1)
}
},
removeExistingMedia(index, type) {
const items = type === 'image' ? this.existingImages : this.existingVideos
const item = items[index]
if (item.mediaId) this.deletedMediaIds.push(item.mediaId)
const idx = this.existingMedia.indexOf(item)
if (idx >= 0) this.existingMedia.splice(idx, 1)
},
onDialogClosed() {
// Cleanup pending object URLs
this.pendingImages.forEach(img => URL.revokeObjectURL(img.url))
this.pendingVideos.forEach(vid => URL.revokeObjectURL(vid.url))
},
// Submit
async uploadPending(modId) {
const uploadUrl = process.env.VUE_APP_BASE_API + '/common/upload'
const headers = { Authorization: 'Bearer ' + getToken() }
const allPending = [
...this.pendingImages.map(p => ({ ...p, mediaType: 'image' })),
...this.pendingVideos.map(p => ({ ...p, mediaType: 'video' }))
]
for (const item of allPending) {
const formData = new FormData()
formData.append('file', item.file)
try {
const res = await axios.post(uploadUrl, formData, { headers })
if (res.data?.code === 200) {
await addSiteModMedia({ modId, mediaType: item.mediaType, fileName: item.name, fileUrl: res.data.url })
}
} catch (e) { console.error('Upload failed:', e) }
}
},
async handleSubmit() {
this.$refs.formRef.validate(async v => {
if (!v) return
this.submitting = true
try {
const api = this.form.modId ? updateSiteMod : addSiteMod
const res = await api(this.form)
const modId = this.form.modId || res.data || this.form.projectId
// Upload pending media
if (this.pendingImages.length || this.pendingVideos.length) {
await this.uploadPending(this.form.modId || modId)
}
// Delete removed media
for (const id of this.deletedMediaIds) {
await delSiteModMedia(id).catch(() => {})
}
this.$message.success('保存成功')
this.dialogVisible = false
this.loadList()
} catch (e) { console.error(e) }
finally { this.submitting = false }
})
},
handleMedia(row) {
this.mediaVisible = true
this.mediaLoading = true
listSiteModMediaAll({ modId: row.modId }).then(r => {
this.mediaList = r.data || []
}).finally(() => { this.mediaLoading = 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; }
.rm-warning { font-size: 12px; color: #856404; margin-bottom: 12px; padding: 10px; background: #fff3cd; border-radius: 6px; border: 1px solid #ffe0b2; }
.dialog-footer { text-align: right; }
.detail-grid { display: grid; grid-template-columns: 100px 1fr; font-size: 13px; }
.dg-label { background: #fafbfc; font-weight: 600; padding: 8px 12px; border-bottom: 1px solid #eee; }
.dg-value { padding: 8px 12px; border-bottom: 1px solid #eee; }
/* Form layout matching HTML */
.form-row { display: flex; gap: 12px; margin-bottom: 12px; }
.form-row .form-group { flex: 1; }
.form-group { margin-bottom: 12px; }
.form-group label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 4px; color: #333; }
/* Media upload zone */
.sm-media-section { flex: 1; }
.sm-upload-zone { border: 2px dashed #d0d7de; border-radius: 8px; padding: 16px; text-align: center; cursor: pointer; transition: border-color .2s; margin-bottom: 8px; }
.sm-upload-zone:hover { border-color: #409eff; }
.uz-icon { font-size: 24px; line-height: 1; }
.uz-text { font-size: 13px; color: #666; margin-top: 4px; }
.uz-hint { font-size: 11px; color: #999; margin-top: 2px; }
/* Media grid */
.sm-media-grid { display: flex; flex-wrap: wrap; gap: 8px; }
.sm-media-thumb { width: 90px; border: 1px solid #e8e8e8; border-radius: 4px; overflow: hidden; position: relative; }
.sm-thumb-img { width: 90px; height: 68px; display: block; object-fit: cover; }
.sm-thumb-name { font-size: 10px; color: #666; padding: 2px 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sm-thumb-remove { position: absolute; top: 2px; right: 2px; width: 18px; height: 18px; border-radius: 50%; border: none; background: rgba(0,0,0,.5); color: #fff; font-size: 12px; line-height: 18px; text-align: center; cursor: pointer; padding: 0; }
.sm-thumb-remove:hover { background: rgba(245,108,108,.8); }
/* Video list */
.sm-video-list { display: flex; flex-direction: column; gap: 4px; }
.sm-video-item { display: flex; align-items: center; padding: 6px 8px; border: 1px solid #eee; border-radius: 4px; }
.vi-icon { margin-right: 6px; }
.vi-info { flex: 1; min-width: 0; }
.vi-name { font-size: 12px; color: #333; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vi-remove { cursor: pointer; color: #999; font-size: 16px; line-height: 1; padding: 0 4px; }
.vi-remove:hover { color: #f56c6c; }
/* Media count tags in table */
.tag-media { font-size: 12px; cursor: pointer; padding: 2px 6px; border-radius: 3px; display: inline-block; }
.tag-media-img { background: #e6f7e6; color: #52c41a; }
.tag-media-vid { background: #e6f0ff; color: #409eff; }
.tag-media-none { background: #f5f5f5; color: #bbb; cursor: default; }
</style>

View File

@@ -1,5 +1,6 @@
<template>
<div class="rm-container">
<div class="current-project-bar">当前项目{{ currentProjectName }}</div>
<div class="rm-panel">
<div class="rm-panel-header">
<span>技术方案确定</span>
@@ -109,6 +110,7 @@ export default {
name: 'RmTechPlan',
data() {
return {
currentProjectName: this.$route.query.projectName || sessionStorage.getItem('rm_current_project_name') || '',
loading: false,
list: [],
total: 0,
@@ -126,14 +128,21 @@ export default {
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()
}
})
const pid = this.$route.query.projectId || sessionStorage.getItem('rm_current_project_id')
if (pid) {
this.currentProjectId = pid
this.query.projectId = pid
this.loadList()
} else {
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

View File

@@ -1,5 +1,6 @@
<template>
<div class="rm-container">
<div class="current-project-bar">当前项目{{ currentProjectName }}</div>
<div class="rm-panel">
<div class="rm-panel-header">
<span>技术审查</span>
@@ -186,6 +187,7 @@ export default {
name: 'RmTechReview',
data() {
return {
currentProjectName: this.$route.query.projectName || sessionStorage.getItem('rm_current_project_name') || '',
activeTab: 'mechanical',
loading: false,
reviewList: [],
@@ -218,14 +220,21 @@ export default {
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()
}
})
const pid = this.$route.query.projectId || sessionStorage.getItem('rm_current_project_id')
if (pid) {
this.currentProjectId = pid
this.loadReviews()
this.loadColors()
} else {
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