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.

1366 lines
50 KiB

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