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.

1242 lines
45 KiB

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