You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1500 lines
54 KiB

2 months ago
1 month ago
2 months ago
4 weeks ago
2 months ago
1 month ago
1 month ago
2 months ago
2 months ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
2 months ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
2 months ago
2 months ago
2 months ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
2 months ago
2 months ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
4 weeks ago
2 months ago
4 weeks ago
2 months ago
2 months ago
  1. <template>
  2. <div class="mod-config">
  3. <el-form :inline="true" label-position="top" class="query-form">
  4. <el-form-item label="物料号">
  5. <el-input v-model="searchData.modelNo" clearable placeholder="请输入物料号" style="width: 160px"></el-input>
  6. </el-form-item>
  7. <el-form-item label="物料名称">
  8. <el-input v-model="searchData.materialName" clearable placeholder="请输入物料名称" style="width: 160px"></el-input>
  9. </el-form-item>
  10. <el-form-item label="规格">
  11. <el-input v-model="searchData.materialSpec" clearable placeholder="请输入规格" style="width: 160px"></el-input>
  12. </el-form-item>
  13. <el-form-item label="状态">
  14. <el-select v-model="searchData.status" clearable placeholder="全部" style="width: 120px">
  15. <el-option label="已排产" value="已排产"></el-option>
  16. <el-option label="进行中" value="进行中"></el-option>
  17. <el-option label="已完成" value="已完成"></el-option>
  18. </el-select>
  19. </el-form-item>
  20. <el-form-item label="计划完工日期">
  21. <el-date-picker v-model="searchData.planStartDate" type="date" value-format="yyyy-MM-dd" placeholder="开始" style="width: 130px"></el-date-picker>
  22. </el-form-item>
  23. <el-form-item label="至">
  24. <el-date-picker v-model="searchData.planEndDate" type="date" value-format="yyyy-MM-dd" placeholder="结束" style="width: 130px"></el-date-picker>
  25. </el-form-item>
  26. <el-form-item label=" " style="margin-top: -11px">
  27. <el-button @click="searchTable('Y')" plain class="search-btn">查询</el-button>
  28. <el-button @click="resetQuery()" plain class="reset-btn">重置</el-button>
  29. <el-button @click="openEditDialog()" plain class="add-btn">新增任务单</el-button>
  30. </el-form-item>
  31. <el-form-item label=" " class="query-finish-item">
  32. <el-button
  33. @click="finishTask()"
  34. plain
  35. class="search-btn"
  36. :disabled="!selectedOrder.orderNo || selectedOrder.status === '已完成'">
  37. 完工
  38. </el-button>
  39. </el-form-item>
  40. </el-form>
  41. <el-table
  42. ref="orderTable"
  43. class="data-table"
  44. :data="dataList"
  45. :height="tableHeight"
  46. border
  47. highlight-current-row
  48. v-loading="dataListLoading"
  49. style="width: 100%"
  50. @current-change="onCurrentRowChange">
  51. <el-table-column label="操作" width="200" align="center">
  52. <template slot-scope="scope">
  53. <a type="text" @click="openEditDialog(scope.row)">修改</a>
  54. <a type="text" @click="openAssignDialog(scope.row)" v-if="scope.row.status !== '已完成' && scope.row.currentNode!=='全部完成'">分配人员</a>
  55. <a type="text" style="color:#F56C6C" @click="deleteTask(scope.row)" v-if="scope.row.status !== '已完成'">删除</a>
  56. </template>
  57. </el-table-column>
  58. <el-table-column type="index" label="#" width="50" align="center"></el-table-column>
  59. <!-- <el-table-column prop="orderNo" label="任务单号" width="180" align="center"></el-table-column>-->
  60. <el-table-column prop="modelNo" label="物料号" width="160" align="center"></el-table-column>
  61. <el-table-column prop="materialName" label="物料名称" width="160" align="center" show-overflow-tooltip></el-table-column>
  62. <el-table-column prop="materialSpec" label="规格" width="160" align="center" show-overflow-tooltip></el-table-column>
  63. <el-table-column prop="taskQty" label="计划数量" width="100" align="center"></el-table-column>
  64. <el-table-column prop="reportQty" label="累计报工数量" width="120" align="center"></el-table-column>
  65. <el-table-column prop="currentNode" label="当前节点" width="120" align="center"></el-table-column>
  66. <el-table-column prop="assigneeSummary" label="节点负责人" min-width="160" show-overflow-tooltip></el-table-column>
  67. <el-table-column label="节点进度" width="100" align="center">
  68. <template slot-scope="scope">
  69. <el-tag size="small" type="info">{{ scope.row.nodeDoneCount }}/{{ scope.row.nodeTotalCount }}</el-tag>
  70. </template>
  71. </el-table-column>
  72. <el-table-column prop="status" label="状态" width="90" align="center">
  73. <template slot-scope="scope">
  74. <el-tag :type="getStatusType(scope.row.status)" size="small">{{ scope.row.status }}</el-tag>
  75. </template>
  76. </el-table-column>
  77. <el-table-column prop="planFinishDate" label="计划完工日期" width="120" align="center"></el-table-column>
  78. <el-table-column prop="finishDate" label="完工日期" width="120" align="center">
  79. <template slot-scope="scope">{{ scope.row.finishDate || '-' }}</template>
  80. </el-table-column>
  81. </el-table>
  82. <el-pagination
  83. @size-change="sizeChangeHandle"
  84. @current-change="currentChangeHandle"
  85. :current-page="pageIndex"
  86. :page-sizes="[10, 20, 50, 100]"
  87. :page-size="pageSize"
  88. :total="totalPage"
  89. layout="total, sizes, prev, pager, next, jumper"
  90. style="margin-top: 20px; text-align: right">
  91. </el-pagination>
  92. <div class="detail-tabs-wrap">
  93. <el-tabs v-model="detailTabName" type="border-card">
  94. <el-tab-pane label="节点状态和日志" name="statusLogs">
  95. <div v-if="selectedOrder.orderNo" class="two-column-layout">
  96. <div class="stages-column">
  97. <div class="column-header">
  98. <i class="el-icon-s-order"></i>
  99. <span>节点流程</span>
  100. <span class="progress-badge">{{ selectedOrderProgressPercent }}%</span>
  101. </div>
  102. <div class="stages-list">
  103. <div
  104. v-for="(stage, index) in selectedOrderNodeList"
  105. :key="stage.nodeCode || index"
  106. class="stage-item"
  107. :class="'stage-' + getStageClass(stage.status)">
  108. <div class="stage-icon">
  109. <i :class="getStageIcon(stage.status)"></i>
  110. </div>
  111. <div class="stage-content">
  112. <div class="stage-name">{{ stage.nodeName }}</div>
  113. <div class="stage-meta">
  114. <el-tag :type="getStageTagType(stage.status)" size="mini" effect="plain">{{ stage.status || '未开始' }}</el-tag>
  115. <span class="stage-owner">负责人{{ stage.assigneeUserName || '-' }}</span>
  116. </div>
  117. </div>
  118. </div>
  119. </div>
  120. </div>
  121. <div class="logs-column">
  122. <div class="column-header">
  123. <i class="el-icon-tickets"></i>
  124. <span>操作日志</span>
  125. <span class="logs-count">{{ selectedOrderLogList.length }}</span>
  126. </div>
  127. <div class="logs-table-wrapper">
  128. <el-table :data="selectedOrderLogList" border size="small" class="detail-table" v-loading="detailLogLoading" height="295px">
  129. <el-table-column prop="logTime" label="时间" width="160" align="center"></el-table-column>
  130. <el-table-column prop="action" label="操作" width="95" align="center"></el-table-column>
  131. <el-table-column prop="nodeName" label="节点" width="130" align="center">
  132. <template slot-scope="scope">{{ scope.row.nodeName || scope.row.nodeCode || '-' }}</template>
  133. </el-table-column>
  134. <el-table-column prop="operatorName" label="操作人" width="100" align="center"></el-table-column>
  135. <el-table-column prop="comment" label="备注" min-width="220" show-overflow-tooltip>
  136. <template slot-scope="scope">{{ scope.row.comment || '-' }}</template>
  137. </el-table-column>
  138. <el-table-column label="影像" width="90" align="center">
  139. <template slot-scope="scope">
  140. <a
  141. v-if="isMediaNodeLog(scope.row)"
  142. type="text"
  143. size="mini"
  144. @click="openMediaFileDialog(scope.row)">
  145. 查看文件
  146. </a>
  147. <span v-else>-</span>
  148. </template>
  149. </el-table-column>
  150. </el-table>
  151. </div>
  152. </div>
  153. </div>
  154. <el-empty v-else description="请先选择一条任务记录"></el-empty>
  155. </el-tab-pane>
  156. </el-tabs>
  157. </div>
  158. <el-dialog :title="saveHeaderData.orderNo ? '修改机加工生产任务单' : '新增机加工生产任务单'" :visible.sync="setUp.reviewFlag" width="550px" :close-on-click-modal="false" v-drag>
  159. <el-form
  160. ref="editForm"
  161. :model="saveHeaderData"
  162. label-position="top"
  163. class="edit-form">
  164. <el-row :gutter="20">
  165. <el-col :span="12"><el-form-item label="物料号" required><el-input v-model="saveHeaderData.modelNo" :disabled="isDateOnlyEditMode()"></el-input></el-form-item></el-col>
  166. <el-col :span="12"><el-form-item label="物料名称" required><el-input v-model="saveHeaderData.materialName" :disabled="isDateOnlyEditMode()"></el-input></el-form-item></el-col>
  167. </el-row>
  168. <el-row :gutter="20">
  169. <el-col :span="12"><el-form-item label="规格" required><el-input v-model="saveHeaderData.materialSpec" :disabled="isDateOnlyEditMode()"></el-input></el-form-item></el-col>
  170. <el-col :span="12"><el-form-item label="计划数量" required><el-input v-model="saveHeaderData.taskQty" :min="1" :max="999999" style="width: 100%" :disabled="isDateOnlyEditMode()"></el-input></el-form-item></el-col>
  171. </el-row>
  172. <el-row :gutter="20">
  173. <el-col :span="12"><el-form-item label="计划完工日期" required><el-date-picker v-model="saveHeaderData.planFinishDate" type="date" value-format="yyyy-MM-dd" style="width: 100%"></el-date-picker></el-form-item></el-col>
  174. <el-col :span="12"><el-form-item label="状态"><el-select disabled v-model="saveHeaderData.status" style="width: 100%"><el-option label="已排产" value="已排产"></el-option><el-option label="进行中" value="进行中"></el-option><el-option label="已完成" value="已完成"></el-option></el-select></el-form-item></el-col>
  175. </el-row>
  176. <!-- <el-row :gutter="20">
  177. <el-col :span="12">
  178. <el-form-item>
  179. <template slot="label">
  180. 人员分配策略
  181. <el-tooltip effect="dark" placement="top">
  182. <div slot="content">
  183. 默认分配创建订单后系统会按节点角色自动分配该角色下全部人员<br>
  184. 手动分配创建订单后不自动分配需要在分配人员里手工选择负责人
  185. </div>
  186. <i class="el-icon-question" style="margin-left:6px;color:#909399;cursor:pointer;"></i>
  187. </el-tooltip>
  188. </template>
  189. <el-radio-group v-model="saveHeaderData.autoAssignAllUsers">
  190. <el-radio :label="true">默认分配</el-radio>
  191. <el-radio :label="false">手动分配</el-radio>
  192. </el-radio-group>
  193. </el-form-item>
  194. </el-col>
  195. <el-col :span="12">
  196. <el-form-item label="节点报工模式">
  197. <el-radio-group v-model="saveHeaderData.nodeReportMode">
  198. <el-radio label="PARALLEL">并行</el-radio>
  199. <el-radio label="SEQUENTIAL">串行</el-radio>
  200. </el-radio-group>
  201. </el-form-item>
  202. </el-col>
  203. </el-row>-->
  204. </el-form>
  205. <el-footer style="height: 40px; margin-top: 50px; text-align: center">
  206. <el-button plain class="reset-btn" @click="setUp.reviewFlag = false">取消</el-button>
  207. <el-button plain class="add-btn" :loading="setUp.saveButton" @click="saveTask">保存</el-button>
  208. </el-footer>
  209. </el-dialog>
  210. <el-dialog title="机加工节点报工" :visible.sync="setUp.reportFlag" width="460px" :close-on-click-modal="false" @close="handleReportDialogClose" v-drag>
  211. <el-form :model="reportData" label-width="110px" label-position="top">
  212. <el-form-item label="任务单号"><el-input v-model="reportData.taskNo" disabled></el-input></el-form-item>
  213. <el-form-item label="报工节点">
  214. <el-select v-model="reportData.nodeCode" placeholder="请选择节点" style="width: 100%" @change="handleReportNodeChange">
  215. <el-option v-for="item in reportNodeOptions" :key="item.nodeCode" :label="item.nodeName" :value="item.nodeCode"></el-option>
  216. </el-select>
  217. </el-form-item>
  218. <el-form-item v-if="isMachiningProductionNode(reportData.nodeCode)" label="本次报工数量">
  219. <el-input v-model="reportData.reportQty" :min="1" :max="999999" style="width: 100%"></el-input>
  220. </el-form-item>
  221. <el-form-item v-if="isMachiningInspectionNode(reportData.nodeCode)" label="检验影像上传">
  222. <div class="inspection-upload-wrap">
  223. <el-button plain type="primary" size="mini" @click="triggerInspectionImagePicker">选择图片</el-button>
  224. <span class="inspection-upload-tip">支持图片格式可多选上传</span>
  225. <input
  226. ref="inspectionImageInput"
  227. class="inspection-file-input"
  228. type="file"
  229. accept="image/*"
  230. multiple
  231. @change="handleInspectionImageSelect">
  232. <div v-if="inspectionImageList.length > 0" class="inspection-file-list">
  233. <div v-for="(item, index) in inspectionImageList" :key="item.uid" class="inspection-file-item">
  234. <span class="inspection-file-name">{{ item.name }}</span>
  235. <el-button type="text" style="color:#F56C6C" @click="removeInspectionImage(index)">删除</el-button>
  236. </div>
  237. </div>
  238. <div v-else class="inspection-upload-empty">请至少上传1张检验图片</div>
  239. </div>
  240. </el-form-item>
  241. <el-form-item label="报工备注"><el-input v-model="reportData.remark" type="textarea" :rows="2"></el-input></el-form-item>
  242. </el-form>
  243. <el-footer style="height: 40px; margin-top: 50px; text-align: center">
  244. <el-button plain class="reset-btn" @click="setUp.reportFlag = false">取消</el-button>
  245. <el-button plain class="search-btn" :loading="setUp.reportButton" @click="submitNodeReport">提交报工</el-button>
  246. </el-footer>
  247. </el-dialog>
  248. <el-dialog title="节点负责人分配" :visible.sync="setUp.assignFlag" width="550px" :close-on-click-modal="false" v-drag>
  249. <el-table :data="assignNodeList" border class="data-table">
  250. <el-table-column prop="nodeName" label="节点" width="150"></el-table-column>
  251. <el-table-column label="负责人" width="370">
  252. <template slot-scope="scope">
  253. <el-select v-model="scope.row.assigneeUserIdList" filterable clearable multiple placeholder="请选择负责人" style="width: 100%">
  254. <el-option v-for="user in scope.row.userOptions" :key="user.userId" :label="user.displayName || user.username" :value="user.userId"></el-option>
  255. </el-select>
  256. </template>
  257. </el-table-column>
  258. </el-table>
  259. <el-footer style="height: 40px; margin-top: 20px; text-align: center">
  260. <el-button plain class="reset-btn" @click="setUp.assignFlag = false">取消</el-button>
  261. <el-button plain class="add-btn" :loading="setUp.assignButton" @click="saveNodeAssigneeAction">保存分配</el-button>
  262. </el-footer>
  263. </el-dialog>
  264. <el-dialog
  265. title="报工影像文件"
  266. :visible.sync="mediaDialogVisible"
  267. width="680px"
  268. :close-on-click-modal="false"
  269. @close="handleMediaDialogClose"
  270. v-drag>
  271. <div v-loading="mediaDialogLoading" class="media-dialog-body">
  272. <div class="media-dialog-meta">
  273. <span>节点{{ mediaDialogLog.nodeName || mediaDialogLog.nodeCode || '-' }}</span>
  274. <span>报工时间{{ mediaDialogLog.logTime || '-' }}</span>
  275. </div>
  276. <el-table v-if="mediaFileList.length > 0" class="file-table" :data="mediaFileList" border size="small" height="360px">
  277. <el-table-column type="index" label="#" width="55" align="center"></el-table-column>
  278. <el-table-column label="缩略图" width="150" align="center">
  279. <template slot-scope="scope">
  280. <div class="media-thumb-cell">
  281. <el-button v-if="resolveMediaKind(scope.row) === 'other'" type="text" size="mini" disabled>不支持</el-button>
  282. <span v-else-if="scope.row.previewLoading" class="media-thumb-loading">加载中...</span>
  283. <img
  284. v-else-if="resolveMediaKind(scope.row) === 'image' && scope.row.previewUrl"
  285. :src="scope.row.previewUrl"
  286. class="media-thumb media-thumb-image"
  287. alt="media-thumb"
  288. @click="previewMediaFile(scope.row)">
  289. <video
  290. v-else-if="resolveMediaKind(scope.row) === 'video'"
  291. :src="getVideoStreamUrl(scope.row)"
  292. class="media-thumb media-thumb-video"
  293. muted
  294. playsinline
  295. preload="metadata"
  296. @click="previewMediaFile(scope.row)">
  297. </video>
  298. <el-button v-else type="text" size="mini" @click="previewMediaFile(scope.row)">加载预览</el-button>
  299. </div>
  300. </template>
  301. </el-table-column>
  302. <el-table-column label="类型" width="90" align="center">
  303. <template slot-scope="scope">
  304. <el-tag size="mini" :type="resolveMediaKind(scope.row) === 'video' ? 'warning' : (resolveMediaKind(scope.row) === 'image' ? 'success' : 'info')">
  305. {{ getMediaKindLabel(scope.row) }}
  306. </el-tag>
  307. </template>
  308. </el-table-column>
  309. <el-table-column label="步骤" width="120" align="center">
  310. <template slot-scope="scope">{{ getMediaStepLabel(scope.row) }}</template>
  311. </el-table-column>
  312. <el-table-column label="上传时间" min-width="170" align="center">
  313. <template slot-scope="scope">{{ scope.row.createDate || '-' }}</template>
  314. </el-table-column>
  315. <el-table-column label="操作" width="60" align="center">
  316. <template slot-scope="scope">
  317. <el-button
  318. type="text"
  319. style="color:#F56C6C"
  320. :loading="scope.row.deleting"
  321. @click="deleteMediaFile(scope.row)">
  322. 删除
  323. </el-button>
  324. </template>
  325. </el-table-column>
  326. </el-table>
  327. <el-empty v-else description="当前节点暂无影像文件"></el-empty>
  328. </div>
  329. <div slot="footer" class="dialog-footer">
  330. <el-button type="primary" @click="mediaDialogVisible = false">关闭</el-button>
  331. </div>
  332. </el-dialog>
  333. <div v-if="mediaPreviewVisible" class="media-preview-overlay" @click="closeMediaPreview">
  334. <i class="el-icon-close media-preview-close" @click.stop="closeMediaPreview"></i>
  335. <img
  336. v-if="mediaPreviewType === 'image'"
  337. :src="mediaPreviewUrl"
  338. :alt="mediaPreviewName || 'media-preview'"
  339. class="media-preview-image"
  340. @click.stop="closeMediaPreview">
  341. <video
  342. v-else-if="mediaPreviewType === 'video'"
  343. ref="mediaPreviewVideo"
  344. :key="mediaPreviewUrl"
  345. :src="mediaPreviewUrl"
  346. class="media-preview-video"
  347. controls
  348. autoplay
  349. playsinline
  350. preload="auto"
  351. @error="handleMediaPreviewError"
  352. @click.stop>
  353. </video>
  354. </div>
  355. </div>
  356. </template>
  357. <script>
  358. import { deleteMachiningTask, finishMachiningTask, getMachiningTaskList, getNodeAssigneeList, getNodeAssigneeUsers, getReportLogList, reportMachiningTaskNode, reportWorkNodeWithMedia, saveMachiningTask, saveNodeAssignee } from '@/api/longchuang/productionPlan'
  359. import { getOssVideoStreamUrl, previewOssFileById2, queryOssFilePlus, removeOss } from '@/api/oss/oss'
  360. export default {
  361. name: 'ProductionPlanMachiningTask',
  362. data() {
  363. return {
  364. searchData: { modelNo: '', materialName: '', materialSpec: '', status: '', planStartDate: '', planEndDate: '', page: 1, limit: 20 },
  365. saveHeaderData: {},
  366. reportData: { orderNo: '', taskNo: '', nodeCode: 'machiningProduction', reportQty: '', remark: '' },
  367. reportNodeOptions: [],
  368. inspectionImageList: [],
  369. setUp: { reviewFlag: false, reportFlag: false, assignFlag: false, saveButton: false, reportButton: false, assignButton: false },
  370. dataList: [],
  371. currentAssignOrder: { orderNo: '', orderType: 'MACHINING' },
  372. assignNodeList: [],
  373. selectedOrder: {},
  374. detailTabName: 'statusLogs',
  375. selectedOrderLogList: [],
  376. detailLogLoading: false,
  377. mediaDialogVisible: false,
  378. mediaDialogLoading: false,
  379. mediaDialogLog: {},
  380. mediaFileList: [],
  381. mediaPreviewVisible: false,
  382. mediaPreviewUrl: '',
  383. mediaPreviewType: '',
  384. mediaPreviewName: '',
  385. pageIndex: 1,
  386. pageSize: 20,
  387. totalPage: 0,
  388. dataListLoading: false,
  389. tableHeight: (window.innerHeight - 320) / 2
  390. }
  391. },
  392. activated() {
  393. this.searchTable()
  394. },
  395. beforeDestroy() {
  396. this.releaseMediaFileUrls()
  397. },
  398. methods: {
  399. isDateOnlyEditMode() {
  400. const status = String(this.saveHeaderData.status || '').trim()
  401. return !!this.saveHeaderData.orderNo && !!status && status !== '已排产'
  402. },
  403. searchTable(flag) {
  404. if (flag === 'Y') this.pageIndex = 1
  405. this.searchData.page = this.pageIndex
  406. this.searchData.limit = this.pageSize
  407. this.dataListLoading = true
  408. getMachiningTaskList(this.searchData).then(({ data }) => {
  409. this.dataListLoading = false
  410. if (data && data.code === 0) {
  411. this.dataList = (data.page.list || []).map(this.normalizeRow)
  412. this.totalPage = data.page.totalCount || 0
  413. this.syncSelectedOrder()
  414. } else this.loadMockData()
  415. }).catch(() => {
  416. this.dataListLoading = false
  417. this.loadMockData()
  418. })
  419. },
  420. normalizeRow(row) {
  421. const list = row.nodeList || []
  422. const done = list.filter(item => item.status === '已完成').length
  423. const currentNode = (list.find(item => item.status !== '已完成') || {}).nodeName || '全部完成'
  424. const assigneeSummary = list.filter(item => item.assigneeUserName).map(item => `${item.nodeName}:${item.assigneeUserName}`).join(';')
  425. return { ...row, autoAssignAllUsers: !!row.autoAssignAllUsers, nodeReportMode: row.nodeReportMode || 'SEQUENTIAL', nodeList: list, nodeDoneCount: done, nodeTotalCount: list.length, currentNode: row.currentNode || currentNode, reportQty: row.reportQty || 0, assigneeSummary: assigneeSummary || '-' }
  426. },
  427. getMachiningDefaultNodeList() {
  428. return [
  429. { nodeCode: 'machiningProduction', nodeName: '机加工生产', status: '未开始' },
  430. { nodeCode: 'machiningInspection', nodeName: '机加工检验', status: '未开始' }
  431. ]
  432. },
  433. isMachiningProductionNode(nodeCode) {
  434. return nodeCode === 'machiningProduction'
  435. },
  436. isMachiningInspectionNode(nodeCode) {
  437. return nodeCode === 'machiningInspection'
  438. },
  439. loadMockData() {
  440. this.dataList = [
  441. {
  442. orderNo: 'MOCK-MACH-001',
  443. projectNo: 'MAT-120001',
  444. modelNo: 'MAT-120001',
  445. materialName: '导轨压板',
  446. materialSpec: 'DG-80-12',
  447. taskQty: 120,
  448. reportQty: 30,
  449. autoAssignAllUsers: true,
  450. nodeReportMode: 'SEQUENTIAL',
  451. planFinishDate: '2026-05-25',
  452. finishDate: '',
  453. status: '进行中',
  454. nodeList: [
  455. { nodeCode: 'machiningProduction', nodeName: '机加工生产', status: '已完成' },
  456. { nodeCode: 'machiningInspection', nodeName: '机加工检验', status: '进行中' }
  457. ]
  458. },
  459. {
  460. orderNo: 'MOCK-MACH-002',
  461. projectNo: 'MAT-120188',
  462. modelNo: 'MAT-120188',
  463. materialName: '轿底连接件',
  464. materialSpec: 'JD-LJ-06',
  465. taskQty: 240,
  466. reportQty: 0,
  467. autoAssignAllUsers: true,
  468. nodeReportMode: 'SEQUENTIAL',
  469. planFinishDate: '2026-05-27',
  470. finishDate: '',
  471. status: '已排产',
  472. nodeList: this.getMachiningDefaultNodeList()
  473. }
  474. ].map(this.normalizeRow)
  475. this.totalPage = this.dataList.length
  476. this.syncSelectedOrder()
  477. },
  478. syncSelectedOrder() {
  479. if (!this.dataList.length) {
  480. this.selectedOrder = {}
  481. this.selectedOrderLogList = []
  482. return
  483. }
  484. const selectedOrderNo = this.selectedOrder && this.selectedOrder.orderNo
  485. const matched = selectedOrderNo ? this.dataList.find(item => item.orderNo === selectedOrderNo) : null
  486. const current = matched || this.dataList[0]
  487. this.selectedOrder = current
  488. this.$nextTick(() => {
  489. if (this.$refs.orderTable && current) {
  490. this.$refs.orderTable.setCurrentRow(current)
  491. }
  492. })
  493. this.loadSelectedOrderLogList(current)
  494. },
  495. onCurrentRowChange(row) {
  496. if (!row || !row.orderNo) {
  497. this.selectedOrder = {}
  498. this.selectedOrderLogList = []
  499. return
  500. }
  501. this.selectedOrder = row
  502. this.loadSelectedOrderLogList(row)
  503. },
  504. loadSelectedOrderLogList(row) {
  505. if (!row || !row.orderNo) {
  506. this.selectedOrderLogList = []
  507. return
  508. }
  509. this.detailLogLoading = true
  510. getReportLogList({ orderNo: row.orderNo, orderType: 'MACHINING' }).then(({ data }) => {
  511. this.detailLogLoading = false
  512. this.selectedOrderLogList = data && data.code === 0 ? (data.rows || []) : []
  513. }).catch(() => {
  514. this.detailLogLoading = false
  515. this.selectedOrderLogList = []
  516. })
  517. },
  518. isMediaNodeLog(row) {
  519. if (!row || !row.logNo) {
  520. return false
  521. }
  522. if (row.action && row.action !== '报工完成') {
  523. return false
  524. }
  525. const codeSet = ['machiningInspection']
  526. const nameSet = ['机加工检验', '检验']
  527. const nodeCode = String(row.nodeCode || '').trim()
  528. const nodeName = String(row.nodeName || '').replace(/\s+/g, '')
  529. return codeSet.includes(nodeCode) || nameSet.some(item => item.replace(/\s+/g, '') === nodeName)
  530. },
  531. async openMediaFileDialog(row) {
  532. if (!row || !row.logNo || !row.nodeCode) {
  533. this.$message.warning('当前日志未关联影像文件')
  534. return
  535. }
  536. this.releaseMediaFileUrls()
  537. this.closeMediaPreview()
  538. this.mediaFileList = []
  539. this.mediaDialogLog = { ...row }
  540. this.mediaDialogVisible = true
  541. this.mediaDialogLoading = true
  542. try {
  543. const { data } = await queryOssFilePlus({
  544. orderRef1: this.selectedOrder.orderNo || row.orderNo,
  545. orderRef2: row.nodeCode
  546. })
  547. this.mediaFileList = (data && data.code === 0 ? (data.rows || []) : []).map(item => ({
  548. ...item,
  549. previewUrl: '',
  550. previewLoading: false,
  551. deleting: false
  552. }))
  553. this.mediaFileList.forEach(item => {
  554. const kind = this.resolveMediaKind(item)
  555. if (kind === 'image') {
  556. this.loadMediaPreviewUrl(item, kind, false)
  557. }
  558. })
  559. } catch (e) {
  560. this.mediaFileList = []
  561. this.$message.error('影像文件加载失败')
  562. } finally {
  563. this.mediaDialogLoading = false
  564. }
  565. },
  566. handleMediaDialogClose() {
  567. this.mediaDialogLoading = false
  568. this.mediaDialogLog = {}
  569. this.releaseMediaFileUrls()
  570. this.mediaFileList = []
  571. this.closeMediaPreview()
  572. },
  573. releaseMediaFileUrls() {
  574. this.mediaFileList.forEach(item => {
  575. if (item && item.previewUrl) {
  576. URL.revokeObjectURL(item.previewUrl)
  577. }
  578. })
  579. },
  580. getMediaExt(fileRow) {
  581. if (!fileRow) {
  582. return ''
  583. }
  584. const ext = fileRow.fileType || fileRow.fileSuffix || this.getExtFromFileName(fileRow.fileName || fileRow.newFileName || '')
  585. return String(ext).replace(/^\./, '').toLowerCase()
  586. },
  587. getExtFromFileName(fileName) {
  588. const name = String(fileName || '')
  589. const dotIndex = name.lastIndexOf('.')
  590. if (dotIndex < 0 || dotIndex >= name.length - 1) {
  591. return ''
  592. }
  593. return name.slice(dotIndex + 1)
  594. },
  595. resolveMediaKind(fileRow) {
  596. const ext = this.getMediaExt(fileRow)
  597. const imageExtList = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']
  598. const videoExtList = ['mp4', 'webm', 'mov', 'avi', 'm4v', '3gp']
  599. if (imageExtList.includes(ext)) {
  600. return 'image'
  601. }
  602. if (videoExtList.includes(ext)) {
  603. return 'video'
  604. }
  605. return 'other'
  606. },
  607. getMediaKindLabel(fileRow) {
  608. const kind = this.resolveMediaKind(fileRow)
  609. if (kind === 'image') {
  610. return '照片'
  611. }
  612. if (kind === 'video') {
  613. return '视频'
  614. }
  615. return '其他'
  616. },
  617. getMediaStepLabel(fileRow) {
  618. const step = String((fileRow && fileRow.orderRef6) || '').replace(/\s+/g, '')
  619. const stepList = ['1平台箱', '2门箱', '3背景墙箱', '4框架箱', '5钣金箱', '6铝型材箱']
  620. return stepList.includes(step) ? step : '-'
  621. },
  622. buildPreviewMimeType(fileRow, kind) {
  623. const ext = this.getMediaExt(fileRow)
  624. if (kind === 'image') {
  625. const imageMimeMap = {
  626. jpg: 'image/jpeg',
  627. jpeg: 'image/jpeg',
  628. png: 'image/png',
  629. gif: 'image/gif',
  630. bmp: 'image/bmp',
  631. webp: 'image/webp'
  632. }
  633. return imageMimeMap[ext] || 'image/jpeg'
  634. }
  635. if (kind === 'video') {
  636. const videoMimeMap = {
  637. mp4: 'video/mp4',
  638. m4v: 'video/mp4',
  639. webm: 'video/webm',
  640. mov: 'video/quicktime',
  641. avi: 'video/x-msvideo',
  642. '3gp': 'video/3gpp'
  643. }
  644. return videoMimeMap[ext] || 'video/mp4'
  645. }
  646. return 'application/octet-stream'
  647. },
  648. async loadMediaPreviewUrl(fileRow, fixedKind = '', showError = true) {
  649. if (!fileRow || !fileRow.id) {
  650. return false
  651. }
  652. const kind = fixedKind || this.resolveMediaKind(fileRow)
  653. if (kind === 'other') {
  654. return false
  655. }
  656. if (fileRow.previewUrl) {
  657. return true
  658. }
  659. if (fileRow.previewLoading) {
  660. return false
  661. }
  662. this.$set(fileRow, 'previewLoading', true)
  663. const mimeType = this.buildPreviewMimeType(fileRow, kind)
  664. try {
  665. const { data } = await previewOssFileById2({ id: fileRow.id, fileType: mimeType })
  666. const sourceBlob = data instanceof Blob ? data : new Blob([data])
  667. const blobType = mimeType || sourceBlob.type || 'application/octet-stream'
  668. const blob = new Blob([sourceBlob], { type: blobType })
  669. this.$set(fileRow, 'previewUrl', URL.createObjectURL(blob))
  670. return true
  671. } catch (e) {
  672. if (showError) {
  673. this.$message.error('文件预览加载失败')
  674. }
  675. return false
  676. } finally {
  677. this.$set(fileRow, 'previewLoading', false)
  678. }
  679. },
  680. getVideoStreamUrl(fileRow) {
  681. if (!fileRow || !fileRow.id) {
  682. return ''
  683. }
  684. return getOssVideoStreamUrl(fileRow.id)
  685. },
  686. async previewMediaFile(fileRow) {
  687. const kind = this.resolveMediaKind(fileRow)
  688. if (kind === 'other') {
  689. this.$message.warning('当前文件暂不支持预览')
  690. return
  691. }
  692. if (!fileRow || !fileRow.id) {
  693. this.$message.warning('文件信息不完整,无法预览')
  694. return
  695. }
  696. if (kind === 'video') {
  697. const videoUrl = this.getVideoStreamUrl(fileRow)
  698. if (!videoUrl) {
  699. this.$message.warning('视频地址无效,无法预览')
  700. return
  701. }
  702. this.mediaPreviewType = 'video'
  703. this.mediaPreviewName = fileRow.fileName || fileRow.newFileName || ''
  704. this.mediaPreviewUrl = videoUrl
  705. this.mediaPreviewVisible = true
  706. this.$nextTick(() => {
  707. const previewVideo = this.$refs.mediaPreviewVideo
  708. if (previewVideo && typeof previewVideo.play === 'function') {
  709. const playPromise = previewVideo.play()
  710. if (playPromise && typeof playPromise.catch === 'function') {
  711. playPromise.catch(() => {})
  712. }
  713. }
  714. })
  715. return
  716. }
  717. const loaded = await this.loadMediaPreviewUrl(fileRow, kind, true)
  718. if (!loaded) {
  719. return
  720. }
  721. this.mediaPreviewType = kind
  722. this.mediaPreviewName = fileRow.fileName || fileRow.newFileName || ''
  723. this.mediaPreviewUrl = fileRow.previewUrl
  724. this.mediaPreviewVisible = true
  725. },
  726. closeMediaPreview() {
  727. const previewVideo = this.$refs.mediaPreviewVideo
  728. if (previewVideo && typeof previewVideo.pause === 'function') {
  729. previewVideo.pause()
  730. }
  731. this.mediaPreviewVisible = false
  732. this.mediaPreviewType = ''
  733. this.mediaPreviewName = ''
  734. this.mediaPreviewUrl = ''
  735. },
  736. handleMediaPreviewError() {
  737. this.$message.warning('视频播放失败,请稍后重试')
  738. },
  739. async deleteMediaFile(fileRow) {
  740. const fileId = Number(fileRow && fileRow.id)
  741. if (!Number.isFinite(fileId) || fileId <= 0) {
  742. this.$message.warning('文件ID无效,无法删除')
  743. return
  744. }
  745. try {
  746. await this.$confirm('确定删除该影像文件吗?删除后不可恢复。', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
  747. } catch (e) {
  748. return
  749. }
  750. this.$set(fileRow, 'deleting', true)
  751. try {
  752. const { data } = await removeOss([fileId])
  753. if (!data || data.code !== 0) {
  754. this.$message.error((data && data.msg) || '删除失败')
  755. return
  756. }
  757. const currentPreviewUrl = this.mediaPreviewUrl || ''
  758. const isCurrentImage = !!(fileRow.previewUrl && currentPreviewUrl && fileRow.previewUrl === currentPreviewUrl)
  759. const isCurrentVideo = !!(currentPreviewUrl && currentPreviewUrl.indexOf(`/video/${fileId}`) > -1)
  760. if (isCurrentImage || isCurrentVideo) {
  761. this.closeMediaPreview()
  762. }
  763. if (fileRow.previewUrl) {
  764. URL.revokeObjectURL(fileRow.previewUrl)
  765. }
  766. this.mediaFileList = this.mediaFileList.filter(item => Number(item.id) !== fileId)
  767. this.$message.success('删除成功')
  768. } catch (e) {
  769. this.$message.error('删除失败')
  770. } finally {
  771. this.$set(fileRow, 'deleting', false)
  772. }
  773. },
  774. resetQuery() {
  775. this.searchData = { modelNo: '', materialName: '', materialSpec: '', status: '', planStartDate: '', planEndDate: '', page: 1, limit: 20 }
  776. this.searchTable('Y')
  777. },
  778. openEditDialog(row) {
  779. this.saveHeaderData = row
  780. ? { ...row, autoAssignAllUsers: !!row.autoAssignAllUsers, nodeReportMode: row.nodeReportMode || 'SEQUENTIAL' }
  781. : { orderNo: '', modelNo: '', materialName: '', materialSpec: '', taskQty: 1, reportQty: 0, planFinishDate: '', status: '已排产', autoAssignAllUsers: true, nodeReportMode: 'SEQUENTIAL', nodeList: [] }
  782. this.setUp.reviewFlag = true
  783. },
  784. saveTask() {
  785. if (!this.saveHeaderData.modelNo) return this.$message.warning('请先填写物料号')
  786. if (!this.saveHeaderData.materialName) return this.$message.warning('请先填写物料名称')
  787. if (!this.saveHeaderData.materialSpec) return this.$message.warning('请先填写规格')
  788. if (!this.saveHeaderData.taskQty || Number(this.saveHeaderData.taskQty) <= 0) return this.$message.warning('请输入有效的计划数量')
  789. if (!this.saveHeaderData.planFinishDate) return this.$message.warning('请选择计划完工日期')
  790. const payload = {
  791. ...this.saveHeaderData,
  792. projectNo: this.saveHeaderData.modelNo,
  793. nodeList: this.saveHeaderData.nodeList && this.saveHeaderData.nodeList.length
  794. ? this.saveHeaderData.nodeList
  795. : this.getMachiningDefaultNodeList()
  796. }
  797. this.setUp.saveButton = true
  798. saveMachiningTask(payload).then(({ data }) => {
  799. this.setUp.saveButton = false
  800. if (data && data.code === 0) {
  801. this.$message.success(data.msg || '保存成功')
  802. this.setUp.reviewFlag = false
  803. this.searchTable()
  804. } else this.$message.error(data.msg || '保存失败')
  805. }).catch(() => {
  806. this.setUp.saveButton = false
  807. const orderNo = this.saveHeaderData.orderNo || String(Date.now())
  808. const index = this.dataList.findIndex(item => item.orderNo === orderNo)
  809. const saveData = this.normalizeRow({
  810. ...payload,
  811. orderNo: orderNo
  812. })
  813. if (index > -1) this.$set(this.dataList, index, saveData)
  814. else this.dataList.unshift(saveData)
  815. this.totalPage = this.dataList.length
  816. this.setUp.reviewFlag = false
  817. this.$message.success('后端未完成,已在前端演示保存')
  818. })
  819. },
  820. openReportDialog(row) {
  821. this.reportNodeOptions = (row.nodeList || []).filter(item => item.status !== '已完成')
  822. const defaultNode = this.reportNodeOptions[0] || {}
  823. const nodeCode = defaultNode.nodeCode || 'machiningProduction'
  824. this.reportData = {
  825. orderNo: row.orderNo,
  826. taskNo: row.orderNo,
  827. nodeCode: nodeCode,
  828. reportQty: this.isMachiningProductionNode(nodeCode) ? '' : 1,
  829. remark: ''
  830. }
  831. this.clearInspectionImageList()
  832. this.setUp.reportFlag = true
  833. },
  834. handleReportNodeChange(nodeCode) {
  835. if (this.isMachiningProductionNode(nodeCode)) {
  836. this.reportData.reportQty = ''
  837. } else {
  838. this.reportData.reportQty = 1
  839. }
  840. if (!this.isMachiningInspectionNode(nodeCode)) {
  841. this.clearInspectionImageList()
  842. }
  843. },
  844. handleReportDialogClose() {
  845. this.clearInspectionImageList()
  846. },
  847. triggerInspectionImagePicker() {
  848. if (this.$refs.inspectionImageInput) {
  849. this.$refs.inspectionImageInput.click()
  850. }
  851. },
  852. handleInspectionImageSelect(event) {
  853. const fileList = Array.from((event && event.target && event.target.files) || [])
  854. if (!fileList.length) {
  855. return
  856. }
  857. let invalidCount = 0
  858. fileList.forEach(file => {
  859. const fileType = String((file && file.type) || '').toLowerCase()
  860. const fileName = String((file && file.name) || '').toLowerCase()
  861. const isImage = fileType.indexOf('image/') === 0 || /\.(jpg|jpeg|png|gif|bmp|webp)$/.test(fileName)
  862. if (!file || !isImage) {
  863. invalidCount++
  864. return
  865. }
  866. this.inspectionImageList.push({
  867. uid: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
  868. name: file.name || 'inspection-image',
  869. raw: file
  870. })
  871. })
  872. if (invalidCount > 0) {
  873. this.$message.warning('仅支持上传图片文件')
  874. }
  875. if (event && event.target) {
  876. event.target.value = ''
  877. }
  878. },
  879. removeInspectionImage(index) {
  880. this.inspectionImageList.splice(index, 1)
  881. },
  882. clearInspectionImageList() {
  883. this.inspectionImageList = []
  884. if (this.$refs.inspectionImageInput) {
  885. this.$refs.inspectionImageInput.value = ''
  886. }
  887. },
  888. openAssignDialog(row) {
  889. if (!row.orderNo) return this.$message.warning('请先保存任务单后再分配人员')
  890. this.currentAssignOrder = { orderNo: row.orderNo, orderType: 'MACHINING' }
  891. getNodeAssigneeList({ orderNo: row.orderNo, orderType: 'MACHINING' }).then(({ data }) => {
  892. const assignRows = data && data.code === 0 ? (data.rows || []) : []
  893. this.assignNodeList = assignRows.map(item => ({ ...item, assigneeUserIdList: item.assigneeUserIdList || [], userOptions: [] }))
  894. const requests = this.assignNodeList.map(item =>
  895. getNodeAssigneeUsers({ orderType: 'MACHINING', nodeCode: item.nodeCode }).then(({ data: userData }) => {
  896. item.userOptions = userData && userData.code === 0 ? (userData.rows || []) : []
  897. }).catch(() => { item.userOptions = [] })
  898. )
  899. Promise.all(requests).finally(() => { this.setUp.assignFlag = true })
  900. }).catch(() => {
  901. this.$message.error('加载节点分配信息失败')
  902. })
  903. },
  904. saveNodeAssigneeAction() {
  905. this.setUp.assignButton = true
  906. const assigneeList = this.assignNodeList.map(item => ({
  907. nodeCode: item.nodeCode,
  908. nodeName: item.nodeName,
  909. assigneeUserIdList: item.assigneeUserIdList || []
  910. }))
  911. saveNodeAssignee({
  912. orderNo: this.currentAssignOrder.orderNo,
  913. orderType: this.currentAssignOrder.orderType,
  914. assigneeList: assigneeList
  915. }).then(({ data }) => {
  916. this.setUp.assignButton = false
  917. if (data && data.code === 0) {
  918. this.$message.success(data.msg || '分配成功')
  919. this.setUp.assignFlag = false
  920. this.searchTable()
  921. } else this.$message.error(data.msg || '分配失败')
  922. }).catch(() => {
  923. this.setUp.assignButton = false
  924. this.$message.error('分配失败')
  925. })
  926. },
  927. submitNodeReport() {
  928. if (!this.reportData.nodeCode) this.reportData.nodeCode = 'machiningProduction'
  929. if (this.isMachiningProductionNode(this.reportData.nodeCode)) {
  930. const qty = Number(this.reportData.reportQty)
  931. if (!Number.isFinite(qty) || qty <= 0) return this.$message.warning('请输入有效报工数量')
  932. this.submitMachiningProductionReport(qty)
  933. return
  934. }
  935. if (this.isMachiningInspectionNode(this.reportData.nodeCode)) {
  936. this.submitMachiningInspectionReport()
  937. return
  938. }
  939. this.$message.warning('当前节点暂不支持报工')
  940. },
  941. submitMachiningProductionReport(reportQty) {
  942. const payload = {
  943. ...this.reportData,
  944. reportQty: reportQty
  945. }
  946. this.setUp.reportButton = true
  947. reportMachiningTaskNode(payload).then(({ data }) => {
  948. this.setUp.reportButton = false
  949. if (data && data.code === 0) {
  950. this.$message.success(data.msg || '报工成功')
  951. this.setUp.reportFlag = false
  952. this.searchTable()
  953. } else this.$message.error(data.msg || '报工失败')
  954. }).catch(() => {
  955. this.setUp.reportButton = false
  956. this.simulateNodeReport(this.reportData.orderNo, this.reportData.nodeCode, reportQty)
  957. this.setUp.reportFlag = false
  958. this.$message.success('后端未完成,已在前端演示节点报工')
  959. })
  960. },
  961. submitMachiningInspectionReport() {
  962. const uploadFileList = this.inspectionImageList.map(item => item.raw).filter(item => !!item)
  963. if (!uploadFileList.length) {
  964. return this.$message.warning('请先上传检验图片')
  965. }
  966. const formData = new FormData()
  967. formData.append('orderNo', this.reportData.orderNo)
  968. formData.append('orderType', 'MACHINING')
  969. formData.append('nodeCode', this.reportData.nodeCode)
  970. formData.append('reportQty', '1')
  971. formData.append('remark', this.reportData.remark || '')
  972. formData.append('reportNode', 'true')
  973. uploadFileList.forEach(file => {
  974. formData.append('file', file, file.name || 'inspection-image')
  975. })
  976. this.setUp.reportButton = true
  977. reportWorkNodeWithMedia(formData).then(({ data }) => {
  978. this.setUp.reportButton = false
  979. if (data && data.code === 0) {
  980. this.$message.success(data.msg || '检验报工成功')
  981. this.setUp.reportFlag = false
  982. this.clearInspectionImageList()
  983. this.searchTable()
  984. } else {
  985. this.$message.error(data.msg || '检验报工失败')
  986. }
  987. }).catch(() => {
  988. this.setUp.reportButton = false
  989. this.simulateNodeReport(this.reportData.orderNo, this.reportData.nodeCode, 0)
  990. this.setUp.reportFlag = false
  991. this.clearInspectionImageList()
  992. this.$message.success('后端未完成,已在前端演示检验报工')
  993. })
  994. },
  995. simulateNodeReport(orderNo, nodeCode, qty) {
  996. const row = this.dataList.find(item => item.orderNo === orderNo)
  997. if (!row) return
  998. const node = row.nodeList.find(item => item.nodeCode === nodeCode)
  999. if (!node) return
  1000. node.status = '已完成'
  1001. if (this.isMachiningProductionNode(nodeCode)) {
  1002. row.reportQty = Number(row.reportQty || 0) + Number(qty || 0)
  1003. }
  1004. row.status = '进行中'
  1005. const nextNode = row.nodeList.find(item => item.status !== '已完成')
  1006. row.currentNode = nextNode ? nextNode.nodeName : '全部完成'
  1007. row.nodeDoneCount = row.nodeList.filter(item => item.status === '已完成').length
  1008. row.finishDate = ''
  1009. },
  1010. finishTask(row) {
  1011. const targetRow = row || this.selectedOrder
  1012. if (!targetRow || !targetRow.orderNo) {
  1013. this.$message.warning('请先选择一条任务记录')
  1014. return
  1015. }
  1016. if (targetRow.status === '已完成') {
  1017. this.$message.warning('该任务单已完工')
  1018. return
  1019. }
  1020. this.$confirm('确定将该任务单标记为完工吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => {
  1021. finishMachiningTask({ orderNo: targetRow.orderNo }).then(({ data }) => {
  1022. if (data && data.code === 0) {
  1023. this.$message.success(data.msg || '完工成功')
  1024. this.searchTable()
  1025. } else this.$message.error(data.msg || '完工失败')
  1026. }).catch(() => {
  1027. targetRow.status = '已完成'
  1028. targetRow.finishDate = this.dayjs().format('YYYY-MM-DD')
  1029. targetRow.nodeList = targetRow.nodeList.map(item => ({ ...item, status: '已完成' }))
  1030. targetRow.currentNode = '全部完成'
  1031. targetRow.nodeDoneCount = targetRow.nodeTotalCount
  1032. this.$message.success('后端未完成,已在前端演示完工')
  1033. })
  1034. }).catch(() => {})
  1035. },
  1036. deleteTask(row) {
  1037. this.$confirm('确定删除该任务单吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => {
  1038. deleteMachiningTask({ orderNo: row.orderNo }).then(({ data }) => {
  1039. if (data && data.code === 0) {
  1040. this.$message.success(data.msg || '删除成功')
  1041. this.searchTable()
  1042. } else this.$message.error(data.msg || '删除失败')
  1043. }).catch(() => {
  1044. this.dataList = this.dataList.filter(item => item.orderNo !== row.orderNo)
  1045. this.totalPage = this.dataList.length
  1046. this.$message.success('后端未完成,已在前端演示删除')
  1047. })
  1048. }).catch(() => {})
  1049. },
  1050. getStatusType(status) {
  1051. const map = { 已排产: 'info', 进行中: 'warning', 已完成: 'success' }
  1052. return map[status] || 'info'
  1053. },
  1054. getStageClass(status) {
  1055. if (status === '已完成') return 'done'
  1056. if (status === '进行中') return 'processing'
  1057. return 'pending'
  1058. },
  1059. getStageIcon(status) {
  1060. if (status === '已完成') return 'el-icon-check'
  1061. if (status === '进行中') return 'el-icon-loading'
  1062. return 'el-icon-time'
  1063. },
  1064. getStageTagType(status) {
  1065. if (status === '已完成') return 'success'
  1066. if (status === '进行中') return 'warning'
  1067. return 'info'
  1068. },
  1069. sizeChangeHandle(val) {
  1070. this.pageSize = val
  1071. this.pageIndex = 1
  1072. this.searchTable()
  1073. },
  1074. currentChangeHandle(val) {
  1075. this.pageIndex = val
  1076. this.searchTable()
  1077. }
  1078. },
  1079. computed: {
  1080. selectedOrderNodeList() {
  1081. return (this.selectedOrder && this.selectedOrder.nodeList) ? this.selectedOrder.nodeList : []
  1082. },
  1083. selectedOrderProgressPercent() {
  1084. const total = this.selectedOrderNodeList.length
  1085. if (!total) {
  1086. return 0
  1087. }
  1088. const done = this.selectedOrderNodeList.filter(item => item.status === '已完成').length
  1089. return Math.round((done * 100) / total)
  1090. }
  1091. }
  1092. }
  1093. </script>
  1094. <style scoped>
  1095. .data-table {
  1096. background-color: #fff;
  1097. border-radius: 4px;
  1098. }
  1099. .data-table >>> .cell {
  1100. line-height: 20px;
  1101. height: 20px;
  1102. }
  1103. .data-table >>> .el-table__header-wrapper th,
  1104. .data-table >>> .el-table__fixed-header-wrapper th {
  1105. background-color: #f5f7fa !important;
  1106. color: #333;
  1107. font-weight: 600;
  1108. border-color: #ebeef5;
  1109. padding: 8px 0;
  1110. }
  1111. .data-table >>> .el-table__header-wrapper .cell,
  1112. .data-table >>> .el-table__fixed-header-wrapper .cell,
  1113. .data-table >>> .el-table__body-wrapper .cell,
  1114. .data-table >>> .el-table__fixed-body-wrapper .cell {
  1115. padding: 0 10px;
  1116. overflow: hidden;
  1117. text-overflow: ellipsis;
  1118. white-space: nowrap;
  1119. font-size: 13px !important;
  1120. }
  1121. .data-table >>> .el-table__body tr:hover > td {
  1122. background-color: #f5f7fa !important;
  1123. }
  1124. .data-table >>> .el-table__body tr.current-row > td {
  1125. background-color: #ecf5ff !important;
  1126. }
  1127. .query-form {
  1128. background-color: #fff;
  1129. padding: 15px 15px 5px 15px;
  1130. border-radius: 4px;
  1131. margin-bottom: 12px;
  1132. }
  1133. .query-finish-item {
  1134. margin-top: -11px;
  1135. float: right;
  1136. margin-right: 0 !important;
  1137. }
  1138. .query-form >>> .el-form-item__label {
  1139. color: #333;
  1140. font-size: 13px;
  1141. padding-bottom: 5px;
  1142. }
  1143. .query-form >>> .el-input__inner {
  1144. height: 32px;
  1145. line-height: 32px;
  1146. border-radius: 4px;
  1147. font-size: 13px;
  1148. }
  1149. .query-form >>> .el-button {
  1150. height: 32px;
  1151. padding: 0 15px;
  1152. font-size: 13px;
  1153. border-radius: 4px;
  1154. }
  1155. .search-btn {
  1156. background-color: #ecf5ff;
  1157. border-color: #b3d8ff;
  1158. color: #409eff;
  1159. }
  1160. .search-btn:hover {
  1161. background-color: #409eff;
  1162. border-color: #409eff;
  1163. color: #fff;
  1164. }
  1165. .reset-btn {
  1166. background-color: #f5f7fa;
  1167. border-color: #d3d4d6;
  1168. color: #606266;
  1169. }
  1170. .reset-btn:hover {
  1171. background-color: #909399;
  1172. border-color: #909399;
  1173. color: #fff;
  1174. }
  1175. .add-btn {
  1176. background-color: #f0f9eb;
  1177. border-color: #c2e7b0;
  1178. color: #67c23a;
  1179. }
  1180. .add-btn:hover {
  1181. background-color: #67c23a;
  1182. border-color: #67c23a;
  1183. color: #fff;
  1184. }
  1185. .dialog-footer {
  1186. text-align: right;
  1187. }
  1188. .edit-form {
  1189. margin-left: 5px;
  1190. margin-top: -5px;
  1191. }
  1192. .detail-tabs-wrap {
  1193. margin-top: 12px;
  1194. background-color: #fff;
  1195. border-radius: 4px;
  1196. }
  1197. .detail-table {
  1198. width: 100%;
  1199. }
  1200. .detail-table >>> .el-table__header-wrapper th,
  1201. .detail-table >>> .el-table__fixed-header-wrapper th {
  1202. background-color: #f5f7fa !important;
  1203. color: #333;
  1204. font-weight: 600;
  1205. border-color: #ebeef5;
  1206. }
  1207. .two-column-layout {
  1208. display: flex;
  1209. gap: 12px;
  1210. }
  1211. .stages-column {
  1212. width: 38%;
  1213. min-width: 320px;
  1214. border: 1px solid #ebeef5;
  1215. border-radius: 4px;
  1216. background: #fff;
  1217. }
  1218. .logs-column {
  1219. flex: 1;
  1220. border: 1px solid #ebeef5;
  1221. border-radius: 4px;
  1222. background: #fff;
  1223. }
  1224. .column-header {
  1225. height: 24px;
  1226. display: flex;
  1227. align-items: center;
  1228. gap: 6px;
  1229. padding: 0 12px;
  1230. border-bottom: 1px solid #ebeef5;
  1231. font-weight: 600;
  1232. color: #303133;
  1233. }
  1234. .progress-badge,
  1235. .logs-count {
  1236. margin-left: auto;
  1237. color: #409eff;
  1238. font-size: 12px;
  1239. font-weight: 500;
  1240. }
  1241. .stages-list {
  1242. max-height: 295px;
  1243. overflow-y: auto;
  1244. padding: 10px;
  1245. }
  1246. .stage-item {
  1247. display: flex;
  1248. align-items: flex-start;
  1249. gap: 10px;
  1250. padding: 3px 8px;
  1251. border-radius: 4px;
  1252. }
  1253. .stage-item + .stage-item {
  1254. margin-top: 2px;
  1255. }
  1256. .stage-item.stage-done {
  1257. background: #f0f9eb;
  1258. }
  1259. .stage-item.stage-processing {
  1260. background: #fdf6ec;
  1261. }
  1262. .stage-item.stage-pending {
  1263. background: #f5f7fa;
  1264. }
  1265. .stage-icon {
  1266. width: 22px;
  1267. height: 22px;
  1268. border-radius: 50%;
  1269. display: flex;
  1270. align-items: center;
  1271. justify-content: center;
  1272. font-size: 13px;
  1273. background: #fff;
  1274. border: 1px solid #dcdfe6;
  1275. color: #606266;
  1276. }
  1277. .stage-content {
  1278. flex: 1;
  1279. }
  1280. .stage-name {
  1281. font-size: 13px;
  1282. color: #303133;
  1283. line-height: 20px;
  1284. }
  1285. .stage-meta {
  1286. margin-top: 6px;
  1287. display: flex;
  1288. align-items: center;
  1289. gap: 8px;
  1290. }
  1291. .stage-owner {
  1292. color: #606266;
  1293. font-size: 12px;
  1294. }
  1295. .logs-table-wrapper {
  1296. padding: 8px;
  1297. }
  1298. .inspection-upload-wrap {
  1299. border: 1px dashed #dcdfe6;
  1300. border-radius: 4px;
  1301. padding: 10px;
  1302. }
  1303. .inspection-upload-tip {
  1304. margin-left: 8px;
  1305. color: #909399;
  1306. font-size: 12px;
  1307. }
  1308. .inspection-file-input {
  1309. display: none;
  1310. }
  1311. .inspection-file-list {
  1312. margin-top: 10px;
  1313. max-height: 150px;
  1314. overflow-y: auto;
  1315. }
  1316. .inspection-file-item {
  1317. display: flex;
  1318. align-items: center;
  1319. justify-content: space-between;
  1320. border-bottom: 1px solid #f0f2f5;
  1321. padding: 4px 0;
  1322. }
  1323. .inspection-file-name {
  1324. flex: 1;
  1325. padding-right: 10px;
  1326. font-size: 12px;
  1327. color: #606266;
  1328. overflow: hidden;
  1329. text-overflow: ellipsis;
  1330. white-space: nowrap;
  1331. }
  1332. .inspection-upload-empty {
  1333. margin-top: 8px;
  1334. font-size: 12px;
  1335. color: #909399;
  1336. }
  1337. .media-dialog-body {
  1338. min-height: 360px;
  1339. }
  1340. .media-dialog-meta {
  1341. display: flex;
  1342. justify-content: space-between;
  1343. margin-bottom: 10px;
  1344. font-size: 13px;
  1345. color: #606266;
  1346. }
  1347. .media-thumb-cell {
  1348. min-height: 70px;
  1349. display: flex;
  1350. align-items: center;
  1351. justify-content: center;
  1352. }
  1353. .media-thumb-loading {
  1354. color: #909399;
  1355. font-size: 12px;
  1356. }
  1357. .media-thumb {
  1358. width: 110px;
  1359. height: 68px;
  1360. border-radius: 4px;
  1361. object-fit: cover;
  1362. cursor: pointer;
  1363. border: 1px solid #ebeef5;
  1364. background: #f5f7fa;
  1365. }
  1366. .media-thumb-video {
  1367. background: #000;
  1368. }
  1369. .media-preview-overlay {
  1370. position: fixed;
  1371. inset: 0;
  1372. z-index: 2100;
  1373. background: rgba(0, 0, 0, 0.82);
  1374. display: flex;
  1375. justify-content: center;
  1376. align-items: center;
  1377. padding: 30px;
  1378. }
  1379. .media-preview-close {
  1380. position: absolute;
  1381. top: 18px;
  1382. right: 22px;
  1383. color: #fff;
  1384. font-size: 30px;
  1385. cursor: pointer;
  1386. }
  1387. .media-preview-image,
  1388. .media-preview-video {
  1389. max-width: 92vw;
  1390. max-height: 88vh;
  1391. object-fit: contain;
  1392. border-radius: 6px;
  1393. background: #000;
  1394. }
  1395. .file-table {
  1396. background-color: #fff;
  1397. border-radius: 4px;
  1398. }
  1399. .file-table >>> .cell {
  1400. line-height: 55px;
  1401. height: 55px;
  1402. }
  1403. .file-table >>> .el-table__header-wrapper th,
  1404. .file-table >>> .el-table__fixed-header-wrapper th {
  1405. background-color: #f5f7fa !important;
  1406. color: #333;
  1407. font-weight: 600;
  1408. border-color: #ebeef5;
  1409. padding: 8px 0;
  1410. }
  1411. .file-table >>> .el-table__header-wrapper .cell,
  1412. .file-table >>> .el-table__fixed-header-wrapper .cell{
  1413. padding: 0 10px;
  1414. overflow: hidden;
  1415. text-overflow: ellipsis;
  1416. white-space: nowrap;
  1417. font-size: 13px !important;
  1418. line-height: 20px;
  1419. height: 20px;
  1420. }
  1421. .file-table >>> .el-table__body tr:hover > td {
  1422. background-color: #f5f7fa !important;
  1423. }
  1424. .file-table >>> .el-table__body tr.current-row > td {
  1425. background-color: #ecf5ff !important;
  1426. }
  1427. .el-icon-check {
  1428. color: #67c23a;
  1429. font-weight: 1000;
  1430. }
  1431. </style>