ProcessProduction.vue 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909
  1. <template>
  2. <div class="process-production-page" :class="{ 'in-dialog': inDialog, 'embedded-in-tab': embeddedInTab }">
  3. <div class="process-production-inner">
  4. <el-form v-if="!embeddedInTab" class="process-production-search" inline @submit.prevent="handleQueryClick">
  5. <div ref="searchWrapRef" class="workorder-search-wrap">
  6. <el-input
  7. v-model="searchKeyword"
  8. clearable
  9. placeholder="请输入订单编号/生产款号/款式"
  10. style="width: 360px"
  11. @keyup.enter="handleQueryClick"
  12. @input="onSearchKeywordInput"
  13. @clear="closeWorkOrderDropdown"
  14. />
  15. <div
  16. v-show="workOrderDropdownVisible && workOrderOptions.length"
  17. class="workorder-dropdown-panel"
  18. >
  19. <div class="workorder-suggest-header">
  20. <span>订单编号</span>
  21. <span>生产款号</span>
  22. <span>款式</span>
  23. </div>
  24. <div class="workorder-dropdown-list">
  25. <div
  26. v-for="item in workOrderOptions"
  27. :key="item.订单编号"
  28. class="workorder-suggest-row"
  29. @click="onPickWorkOrder(item)"
  30. >
  31. <span class="workorder-suggest-no" :title="item.订单编号">{{ item.订单编号 }}</span>
  32. <span class="workorder-suggest-style" :title="item.生产款号">{{ item.生产款号 }}</span>
  33. <span class="workorder-suggest-name" :title="item.款式">{{ item.款式 }}</span>
  34. </div>
  35. </div>
  36. </div>
  37. </div>
  38. <el-button type="primary" icon="search" :loading="workOrderSuggestLoading" @click="handleQueryClick">
  39. 查询
  40. </el-button>
  41. </el-form>
  42. <el-alert
  43. v-if="pageHint"
  44. class="process-production-hint"
  45. :title="pageHint"
  46. type="warning"
  47. show-icon
  48. :closable="false"
  49. />
  50. <!-- 订单摘要:新增字段只在此处数组里加一行,key 与 WorkOrderList 返回字段一致 -->
  51. <el-descriptions
  52. v-if="!embeddedInTab"
  53. class="order-summary-desc"
  54. :column="5"
  55. border
  56. >
  57. <el-descriptions-item
  58. v-for="field in [
  59. { label: '客户编号', key: '客户编号' },
  60. { label: '订单编号', key: '订单编号' },
  61. { label: '生产款号', key: '生产款号' },
  62. { label: '款式', key: '款式' },
  63. { label: '订单数量', key: '订单数量' },
  64. ]"
  65. :key="field.key"
  66. :label="field.label"
  67. >
  68. {{ orderSummary[field.key] }}
  69. </el-descriptions-item>
  70. </el-descriptions>
  71. <div class="process-production-table-wrap">
  72. <el-table
  73. v-loading="loading"
  74. :data="displayTableData"
  75. :height="tableHeight"
  76. border
  77. stripe
  78. row-key="rowKey"
  79. style="width: 100%"
  80. :row-style="{ height: '32px' }"
  81. :header-row-style="{ height: '36px' }"
  82. :span-method="tableSpanMethod"
  83. :summary-method="tableSummaryMethod"
  84. show-summary
  85. show-overflow-tooltip
  86. >
  87. <el-table-column label="小组" prop="小组" width="100" align="center">
  88. <template #header>
  89. <div class="process-production-col-header">
  90. <span>小组</span>
  91. <el-popover trigger="click" placement="bottom-start" :width="180" :teleported="true" :z-index="10050" append-to-body popper-class="gy-detail-filter-popper">
  92. <template #reference>
  93. <span class="gy-detail-filter-btn" :class="{ 'is-active': isGroupFilterActive }" @click.stop>
  94. <svg viewBox="0 0 1024 1024" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M880.1 154H143.9c-24.5 0-39.9 26.7-27.6 48L349 597.4V838c0 17.7 14.2 32 31.8 32h262.4c17.6 0 31.8-14.3 31.8-32V597.4L907.7 202c12.2-21.3-3.1-48-27.6-48z"/></svg>
  95. </span>
  96. </template>
  97. <div class="gy-detail-filter-panel">
  98. <div class="gy-detail-filter-options">
  99. <el-checkbox-group v-model="groupFilter">
  100. <el-checkbox v-for="item in groupFilterOptions" :key="item.value" :label="item.value">{{ item.text }}</el-checkbox>
  101. </el-checkbox-group>
  102. </div>
  103. <div class="gy-detail-filter-actions">
  104. <span class="gy-detail-filter-link" @click.stop="clearGroupFilter">取消全选</span>
  105. <span class="gy-detail-filter-link" @click.stop="selectAllGroupFilter">全选</span>
  106. </div>
  107. </div>
  108. </el-popover>
  109. </div>
  110. </template>
  111. </el-table-column>
  112. <el-table-column label="工序编号" prop="工序号" width="100" align="center" />
  113. <el-table-column label="工序名称" prop="工序名称" min-width="280" align="left" show-overflow-tooltip />
  114. <el-table-column label="员工姓名" prop="员工姓名" width="120" align="left">
  115. <template #header>
  116. <div class="process-production-col-header">
  117. <span>员工姓名</span>
  118. <el-popover trigger="click" placement="bottom-start" :width="180" :teleported="true" :z-index="10050" append-to-body popper-class="gy-detail-filter-popper">
  119. <template #reference>
  120. <span class="gy-detail-filter-btn" :class="{ 'is-active': isStaffNameFilterActive }" @click.stop>
  121. <svg viewBox="0 0 1024 1024" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M880.1 154H143.9c-24.5 0-39.9 26.7-27.6 48L349 597.4V838c0 17.7 14.2 32 31.8 32h262.4c17.6 0 31.8-14.3 31.8-32V597.4L907.7 202c12.2-21.3-3.1-48-27.6-48z"/></svg>
  122. </span>
  123. </template>
  124. <div class="gy-detail-filter-panel">
  125. <div class="gy-detail-filter-options">
  126. <el-checkbox-group v-model="staffNameFilter">
  127. <el-checkbox v-for="item in staffNameFilterOptions" :key="item.value" :label="item.value">{{ item.text }}</el-checkbox>
  128. </el-checkbox-group>
  129. </div>
  130. <div class="gy-detail-filter-actions">
  131. <span class="gy-detail-filter-link" @click.stop="clearStaffNameFilter">取消全选</span>
  132. <span class="gy-detail-filter-link" @click.stop="selectAllStaffNameFilter">全选</span>
  133. </div>
  134. </div>
  135. </el-popover>
  136. </div>
  137. </template>
  138. </el-table-column>
  139. <el-table-column label="完工数量" prop="数量" width="90" align="center" />
  140. <el-table-column v-if="!hideAmountColumns" label="工分" prop="工分" width="100" align="center" />
  141. <el-table-column v-if="!hideAmountColumns" label="工时" prop="工时" width="100" align="center" />
  142. <el-table-column v-if="!hideAmountColumns" label="工资" prop="工资" width="100" align="center" />
  143. <el-table-column label="开工日期" prop="开工日期" width="110" align="center">
  144. <template #header>
  145. <div class="process-production-col-header">
  146. <span>开工日期</span>
  147. <el-popover trigger="click" placement="bottom-start" :width="180" :teleported="true" :z-index="10050" append-to-body popper-class="gy-detail-filter-popper">
  148. <template #reference>
  149. <span class="gy-detail-filter-btn" :class="{ 'is-active': isStartDateFilterActive }" @click.stop>
  150. <svg viewBox="0 0 1024 1024" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M880.1 154H143.9c-24.5 0-39.9 26.7-27.6 48L349 597.4V838c0 17.7 14.2 32 31.8 32h262.4c17.6 0 31.8-14.3 31.8-32V597.4L907.7 202c12.2-21.3-3.1-48-27.6-48z"/></svg>
  151. </span>
  152. </template>
  153. <div class="gy-detail-filter-panel">
  154. <div class="gy-detail-filter-options">
  155. <el-checkbox-group v-model="startDateFilter">
  156. <el-checkbox v-for="item in startDateFilterOptions" :key="item.value" :label="item.value">{{ item.text }}</el-checkbox>
  157. </el-checkbox-group>
  158. </div>
  159. <div class="gy-detail-filter-actions">
  160. <span class="gy-detail-filter-link" @click.stop="clearStartDateFilter">取消全选</span>
  161. <span class="gy-detail-filter-link" @click.stop="selectAllStartDateFilter">全选</span>
  162. </div>
  163. </div>
  164. </el-popover>
  165. </div>
  166. </template>
  167. </el-table-column>
  168. <el-table-column label="完工日期" prop="完工日期" width="110" align="center">
  169. <template #header>
  170. <div class="process-production-col-header">
  171. <span>完工日期</span>
  172. <el-popover trigger="click" placement="bottom-start" :width="180" :teleported="true" :z-index="10050" append-to-body popper-class="gy-detail-filter-popper">
  173. <template #reference>
  174. <span class="gy-detail-filter-btn" :class="{ 'is-active': isFinishDateFilterActive }" @click.stop>
  175. <svg viewBox="0 0 1024 1024" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M880.1 154H143.9c-24.5 0-39.9 26.7-27.6 48L349 597.4V838c0 17.7 14.2 32 31.8 32h262.4c17.6 0 31.8-14.3 31.8-32V597.4L907.7 202c12.2-21.3-3.1-48-27.6-48z"/></svg>
  176. </span>
  177. </template>
  178. <div class="gy-detail-filter-panel">
  179. <div class="gy-detail-filter-options">
  180. <el-checkbox-group v-model="finishDateFilter">
  181. <el-checkbox v-for="item in finishDateFilterOptions" :key="item.value" :label="item.value">{{ item.text }}</el-checkbox>
  182. </el-checkbox-group>
  183. </div>
  184. <div class="gy-detail-filter-actions">
  185. <span class="gy-detail-filter-link" @click.stop="clearFinishDateFilter">取消全选</span>
  186. <span class="gy-detail-filter-link" @click.stop="selectAllFinishDateFilter">全选</span>
  187. </div>
  188. </div>
  189. </el-popover>
  190. </div>
  191. </template>
  192. </el-table-column>
  193. <template #empty>
  194. <el-empty
  195. v-if="!loading"
  196. :description="emptyDescription"
  197. :image-size="100"
  198. />
  199. </template>
  200. </el-table>
  201. </div>
  202. </div>
  203. </div>
  204. </template>
  205. <script setup>
  206. import { computed, reactive, ref, watch, onMounted, onUnmounted } from 'vue'
  207. import { ElMessage } from 'element-plus'
  208. import { checkProcessProduction, WorkOrderList } from '@/api/mes/job'
  209. defineOptions({ name: 'ProcessProduction' })
  210. const props = defineProps({
  211. /** 弹窗打开时传入的订单编号,会自动查询 */
  212. initialWorkorder: { type: String, default: '' },
  213. /** 嵌入订单资料弹窗时使用 */
  214. inDialog: { type: Boolean, default: false },
  215. /** 为 true 时不显示工分、工时、工资列(如样衣批核页) */
  216. hideAmountColumns: { type: Boolean, default: false },
  217. /** 嵌入工分报工页 Tab:隐藏搜索栏与订单摘要,固定表格高度 */
  218. embeddedInTab: { type: Boolean, default: false },
  219. })
  220. /** 主页面表格高度:改此值即可 */
  221. const MAIN_TABLE_HEIGHT = 'calc(100vh - 300px)'
  222. /** 弹窗内表格最大高度(距视口顶部的预留像素,含标题/搜索/订单信息栏) */
  223. const DIALOG_TABLE_OFFSET = 220
  224. const DIALOG_TABLE_EMPTY_HEIGHT = 320
  225. const TABLE_SUMMARY_ROW_HEIGHT = 40
  226. const sumTableColumn = (data, prop) => {
  227. return data.reduce((sum, row) => {
  228. const n = Number(row[prop])
  229. return sum + (Number.isFinite(n) ? n : 0)
  230. }, 0)
  231. }
  232. const formatSummaryValue = (total, prop) => {
  233. if (!Number.isFinite(total)) return ''
  234. if (prop === '数量') {
  235. return Number.isInteger(total) ? String(total) : total.toFixed(2)
  236. }
  237. return total.toFixed(2)
  238. }
  239. const tableSummaryMethod = ({ columns, data }) => {
  240. const summaryProps = props.hideAmountColumns
  241. ? ['数量']
  242. : ['数量', '工分', '工时', '工资']
  243. return columns.map((column, index) => {
  244. if (index === 0) return '合计'
  245. const prop = column.property
  246. if (!summaryProps.includes(prop)) return ''
  247. return formatSummaryValue(sumTableColumn(data, prop), prop)
  248. })
  249. }
  250. /** 嵌入 Tab 时与工分报工记录表同高 */
  251. const EMBEDDED_TAB_TABLE_HEIGHT = '52vh'
  252. /** 弹窗按行数自适应高度,避免数据少时底部大块空白;主页面固定高度 */
  253. const tableHeight = computed(() => {
  254. if (props.embeddedInTab) return EMBEDDED_TAB_TABLE_HEIGHT
  255. if (!props.inDialog) return MAIN_TABLE_HEIGHT
  256. const rows = displayTableData.value.length
  257. const maxH = Math.max(200, window.innerHeight - DIALOG_TABLE_OFFSET)
  258. if (!rows) return Math.min(DIALOG_TABLE_EMPTY_HEIGHT, maxH)
  259. const needH = rows * 32 + 36 + 12 + TABLE_SUMMARY_ROW_HEIGHT
  260. return Math.min(Math.max(needH, 200), maxH)
  261. })
  262. const loading = ref(false)
  263. const searched = ref(false)
  264. /** 查询结果页内提示(如:未找到报工数据),不用 ElMessage 弹窗 */
  265. const pageHint = ref('')
  266. const emptyDescription = computed(() => {
  267. if (!searched.value) {
  268. return props.embeddedInTab ? '请先查询订单' : '请输入订单编号后查询'
  269. }
  270. return pageHint.value || '暂无数据'
  271. })
  272. const searchKeyword = ref('')
  273. const searchWrapRef = ref(null)
  274. const workOrderDropdownVisible = ref(false)
  275. const orderSummary = reactive({})
  276. const isScalarSummaryValue = (val) =>
  277. val == null || (typeof val !== 'object' && typeof val !== 'function')
  278. const processListRaw = ref([])
  279. const normalizeTableDate = (val) => {
  280. const s = String(val ?? '').trim()
  281. if (!s) return ''
  282. return s.replace(/\s+\d{2}:\d{2}(:\d{2})?$/, '').split('T')[0].split(' ')[0]
  283. }
  284. const pickGroupFromStaff = (staff) =>
  285. String(
  286. staff?.小组 ?? staff?.group ?? staff?.['设备编组'] ?? staff?.['设备编号'] ?? staff?.team_name ?? ''
  287. ).trim()
  288. const flattenProcessList = (processList = []) => {
  289. const rows = []
  290. for (const proc of processList) {
  291. const staffs = Array.isArray(proc.staffs) ? proc.staffs : []
  292. if (!staffs.length) {
  293. rows.push({
  294. rowKey: `${proc.工序号}-empty`,
  295. 小组: '',
  296. 工序号: proc.工序号 ?? '',
  297. 工序名称: proc.工序名称 ?? '',
  298. 员工姓名: '',
  299. 数量: '',
  300. 开工日期: '',
  301. 完工日期: '',
  302. 工分: '',
  303. 工时: '',
  304. 工资: '',
  305. _processRowSpan: 1,
  306. })
  307. continue
  308. }
  309. staffs.forEach((staff, index) => {
  310. rows.push({
  311. rowKey: `${proc.工序号}-${index}-${staff.员工姓名 || ''}`,
  312. 小组: pickGroupFromStaff(staff),
  313. 工序号: proc.工序号 ?? '',
  314. 工序名称: proc.工序名称 ?? '',
  315. 员工姓名: staff.员工姓名 ?? '',
  316. 数量: staff.数量 ?? '',
  317. 开工日期: staff.开工日期 ?? '',
  318. 完工日期: staff.完工日期 ?? '',
  319. 工分: staff.工分 ?? '',
  320. 工时: staff.工时 ?? '',
  321. 工资: staff.工资 ?? '',
  322. _processRowSpan: index === 0 ? staffs.length : 0,
  323. })
  324. })
  325. }
  326. return rows
  327. }
  328. const flatTableRows = computed(() => flattenProcessList(processListRaw.value || []))
  329. const syncCheckboxFilterAfterDataChange = (options, selectedRef) => {
  330. const allValues = options.map((item) => item.value)
  331. const selected = selectedRef.value
  332. if (!selected.length) {
  333. selectedRef.value = [...allValues]
  334. return
  335. }
  336. const wasAllSelected = allValues.length > 0 && allValues.every((v) => selected.includes(v))
  337. if (wasAllSelected) {
  338. selectedRef.value = [...allValues]
  339. return
  340. }
  341. selectedRef.value = selected.filter((v) => allValues.includes(v))
  342. }
  343. const applyCheckboxColumnFilter = (rows, options, selected, getValue) => {
  344. if (!selected.length) return []
  345. if (!options.length || selected.length >= options.length) return rows
  346. const set = new Set(selected)
  347. return rows.filter((row) => {
  348. const val = getValue(row)
  349. if (!val) return true
  350. return set.has(val)
  351. })
  352. }
  353. function createColumnFilter(getValue, { sortDesc = false } = {}) {
  354. const selected = ref([])
  355. const options = computed(() => {
  356. const values = [...new Set(flatTableRows.value.map(getValue).filter(Boolean))]
  357. if (sortDesc) values.sort((a, b) => b.localeCompare(a))
  358. else values.sort((a, b) => a.localeCompare(b, 'zh-CN'))
  359. return values.map((v) => ({ text: v, value: v }))
  360. })
  361. const isActive = computed(() => {
  362. const allValues = options.value.map((item) => item.value)
  363. if (!allValues.length) return false
  364. return selected.value.length < allValues.length
  365. })
  366. return {
  367. selected,
  368. options,
  369. isActive,
  370. selectAll: () => {
  371. selected.value = options.value.map((item) => item.value)
  372. },
  373. clear: () => {
  374. selected.value = []
  375. },
  376. sync: () => syncCheckboxFilterAfterDataChange(options.value, selected),
  377. apply: (rows) => applyCheckboxColumnFilter(rows, options.value, selected.value, getValue),
  378. }
  379. }
  380. const groupFilterCtrl = createColumnFilter((row) => String(row.小组 || '').trim())
  381. const staffNameFilterCtrl = createColumnFilter((row) => String(row.员工姓名 || '').trim())
  382. const startDateFilterCtrl = createColumnFilter((row) => normalizeTableDate(row.开工日期), { sortDesc: true })
  383. const finishDateFilterCtrl = createColumnFilter((row) => normalizeTableDate(row.完工日期), { sortDesc: true })
  384. const {
  385. selected: groupFilter,
  386. options: groupFilterOptions,
  387. isActive: isGroupFilterActive,
  388. selectAll: selectAllGroupFilter,
  389. clear: clearGroupFilter,
  390. sync: syncGroupFilterAfterDataChange,
  391. apply: applyGroupFilter,
  392. } = groupFilterCtrl
  393. const {
  394. selected: staffNameFilter,
  395. options: staffNameFilterOptions,
  396. isActive: isStaffNameFilterActive,
  397. selectAll: selectAllStaffNameFilter,
  398. clear: clearStaffNameFilter,
  399. sync: syncStaffNameFilterAfterDataChange,
  400. apply: applyStaffNameFilter,
  401. } = staffNameFilterCtrl
  402. const {
  403. selected: startDateFilter,
  404. options: startDateFilterOptions,
  405. isActive: isStartDateFilterActive,
  406. selectAll: selectAllStartDateFilter,
  407. clear: clearStartDateFilter,
  408. sync: syncStartDateFilterAfterDataChange,
  409. apply: applyStartDateFilter,
  410. } = startDateFilterCtrl
  411. const {
  412. selected: finishDateFilter,
  413. options: finishDateFilterOptions,
  414. isActive: isFinishDateFilterActive,
  415. selectAll: selectAllFinishDateFilter,
  416. clear: clearFinishDateFilter,
  417. sync: syncFinishDateFilterAfterDataChange,
  418. apply: applyFinishDateFilter,
  419. } = finishDateFilterCtrl
  420. const recomputeProcessRowSpan = (rows) => {
  421. const result = []
  422. let i = 0
  423. while (i < rows.length) {
  424. const processNo = rows[i].工序号
  425. const processName = rows[i].工序名称
  426. let j = i
  427. while (j < rows.length && rows[j].工序号 === processNo && rows[j].工序名称 === processName) {
  428. j++
  429. }
  430. const group = rows.slice(i, j)
  431. group.forEach((row, idx) => {
  432. result.push({ ...row, _processRowSpan: idx === 0 ? group.length : 0 })
  433. })
  434. i = j
  435. }
  436. return result
  437. }
  438. const displayTableData = computed(() => {
  439. let filtered = flatTableRows.value
  440. filtered = applyGroupFilter(filtered)
  441. filtered = applyStaffNameFilter(filtered)
  442. filtered = applyStartDateFilter(filtered)
  443. filtered = applyFinishDateFilter(filtered)
  444. return recomputeProcessRowSpan(filtered)
  445. })
  446. watch(flatTableRows, () => {
  447. syncGroupFilterAfterDataChange()
  448. syncStaffNameFilterAfterDataChange()
  449. syncStartDateFilterAfterDataChange()
  450. syncFinishDateFilterAfterDataChange()
  451. })
  452. const tableSpanMethod = ({ row, column }) => {
  453. if (column.property !== '工序号' && column.property !== '工序名称') {
  454. return { rowspan: 1, colspan: 1 }
  455. }
  456. const rowspan = row._processRowSpan || 0
  457. if (rowspan > 0) {
  458. return { rowspan, colspan: 1 }
  459. }
  460. return { rowspan: 0, colspan: 0 }
  461. }
  462. const resetOrderSummary = (workorder = '') => {
  463. Object.keys(orderSummary).forEach((key) => {
  464. orderSummary[key] = ''
  465. })
  466. orderSummary.订单编号 = workorder
  467. }
  468. const applyOrderSummaryFromWorkOrder = (data, workorder) => {
  469. Object.keys(orderSummary).forEach((key) => {
  470. orderSummary[key] = ''
  471. })
  472. for (const [key, val] of Object.entries(data || {})) {
  473. if (isScalarSummaryValue(val)) {
  474. orderSummary[key] = val ?? ''
  475. }
  476. }
  477. orderSummary.订单编号 = orderSummary.订单编号 || workorder
  478. }
  479. const loadOrderSummaryFromWorkOrderList = async (workorder) => {
  480. resetOrderSummary(workorder)
  481. try {
  482. const res = await WorkOrderList({ search: workorder, page: 1, limit: 1 })
  483. const list = res?.data?.data
  484. if (res?.code === 0 && Array.isArray(list) && list.length) {
  485. applyOrderSummaryFromWorkOrder(list[0], workorder)
  486. return true
  487. }
  488. } catch (error) {
  489. console.error(error)
  490. }
  491. return false
  492. }
  493. let workOrderSuggestSeq = 0
  494. const workOrderOptions = ref([])
  495. const workOrderSuggestLoading = ref(false)
  496. let searchInputTimer = null
  497. const closeWorkOrderDropdown = () => {
  498. workOrderDropdownVisible.value = false
  499. workOrderOptions.value = []
  500. }
  501. const onSearchKeywordInput = () => {
  502. clearTimeout(searchInputTimer)
  503. searchInputTimer = setTimeout(async () => {
  504. const q = String(searchKeyword.value || '').trim()
  505. if (!q) {
  506. closeWorkOrderDropdown()
  507. return
  508. }
  509. await remoteSearchWorkOrder(q)
  510. workOrderDropdownVisible.value = workOrderOptions.value.length > 0
  511. }, 300)
  512. }
  513. const onDocClick = (event) => {
  514. if (!searchWrapRef.value?.contains(event.target)) {
  515. workOrderDropdownVisible.value = false
  516. }
  517. }
  518. onMounted(() => {
  519. document.addEventListener('click', onDocClick)
  520. })
  521. onUnmounted(() => {
  522. document.removeEventListener('click', onDocClick)
  523. clearTimeout(searchInputTimer)
  524. })
  525. const remoteSearchWorkOrder = async (query) => {
  526. const q = String(query || '').trim()
  527. if (!q) {
  528. workOrderOptions.value = []
  529. return
  530. }
  531. const seq = ++workOrderSuggestSeq
  532. workOrderSuggestLoading.value = true
  533. try {
  534. const res = await WorkOrderList({ search: q, page: 1, limit: 20 })
  535. if (seq !== workOrderSuggestSeq) return
  536. workOrderOptions.value = res?.code === 0 && Array.isArray(res.data?.data) ? res.data.data : []
  537. } catch (error) {
  538. console.error(error)
  539. workOrderOptions.value = []
  540. } finally {
  541. if (seq === workOrderSuggestSeq) {
  542. workOrderSuggestLoading.value = false
  543. }
  544. }
  545. }
  546. const onPickWorkOrder = async (item) => {
  547. const no = String(item?.订单编号 || '').trim()
  548. if (!no) return
  549. applyOrderSummaryFromWorkOrder(item, no)
  550. workOrderDropdownVisible.value = false
  551. await executeProductionSearch(no)
  552. searchKeyword.value = ''
  553. }
  554. /** 点击查询 / 回车:仅 1 条则直接查,多条则展开下拉;查询前不清空输入框 */
  555. const handleQueryClick = async () => {
  556. const q = String(searchKeyword.value || '').trim()
  557. if (!q) {
  558. ElMessage.warning('请输入订单编号/生产款号/款式')
  559. return
  560. }
  561. await remoteSearchWorkOrder(q)
  562. if (!workOrderOptions.value.length) {
  563. ElMessage.warning('未找到匹配订单')
  564. workOrderDropdownVisible.value = false
  565. return
  566. }
  567. if (workOrderOptions.value.length === 1) {
  568. await onPickWorkOrder(workOrderOptions.value[0])
  569. return
  570. }
  571. workOrderDropdownVisible.value = true
  572. }
  573. const loadByWorkorder = async (workorder) => {
  574. const w = String(workorder || '').trim()
  575. if (!w) return
  576. searchKeyword.value = w
  577. await executeProductionSearch(w)
  578. }
  579. const executeProductionSearch = async (orderNo) => {
  580. const workorder = String(orderNo || '').trim()
  581. if (!workorder) {
  582. ElMessage.warning('请输入订单编号')
  583. return
  584. }
  585. loading.value = true
  586. searched.value = true
  587. pageHint.value = ''
  588. try {
  589. const [, res] = await Promise.all([
  590. loadOrderSummaryFromWorkOrderList(workorder),
  591. checkProcessProduction({ workorder }),
  592. ])
  593. if (res?.code !== 0) {
  594. processListRaw.value = []
  595. pageHint.value = res?.msg || '未找到报工数据'
  596. return
  597. }
  598. const data = res.data || {}
  599. processListRaw.value = data.工序列表 || []
  600. if (!processListRaw.value.length) {
  601. pageHint.value = '未找到报工数据'
  602. }
  603. } catch (error) {
  604. console.error(error)
  605. pageHint.value = ''
  606. ElMessage.error('查询失败,请稍后重试')
  607. processListRaw.value = []
  608. resetOrderSummary(workorder)
  609. } finally {
  610. loading.value = false
  611. }
  612. }
  613. watch(
  614. () => props.initialWorkorder,
  615. (val) => {
  616. if (val) loadByWorkorder(val)
  617. },
  618. { immediate: true }
  619. )
  620. defineExpose({ loadByWorkorder })
  621. </script>
  622. <style scoped>
  623. .process-production-page {
  624. height: 100%;
  625. background: transparent;
  626. }
  627. .process-production-page.in-dialog {
  628. min-height: 0;
  629. }
  630. /* 主页面:右侧与左侧留白一致(左侧由布局已有间距,此处补右侧) */
  631. .process-production-page:not(.in-dialog) {
  632. padding-right: 16px;
  633. box-sizing: border-box;
  634. }
  635. .process-production-inner {
  636. padding: 0 16px 12px;
  637. box-sizing: border-box;
  638. max-width: 100%;
  639. }
  640. .process-production-page.embedded-in-tab .process-production-inner {
  641. padding: 0;
  642. }
  643. .process-production-search {
  644. margin-bottom: 8px;
  645. }
  646. .process-production-hint {
  647. margin-bottom: 10px;
  648. }
  649. .process-production-search :deep(.el-form-item) {
  650. margin-bottom: 0;
  651. margin-right: 12px;
  652. }
  653. .workorder-search-wrap {
  654. position: relative;
  655. display: inline-block;
  656. vertical-align: middle;
  657. }
  658. .workorder-dropdown-panel {
  659. position: absolute;
  660. top: calc(100% + 4px);
  661. left: 0;
  662. z-index: 3000;
  663. width: 680px;
  664. max-width: min(920px, 96vw);
  665. background: #fff;
  666. border: 1px solid #e4e7ed;
  667. border-radius: 4px;
  668. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
  669. box-sizing: border-box;
  670. overflow: hidden;
  671. }
  672. .workorder-dropdown-panel .workorder-suggest-header {
  673. padding: 8px 12px 6px;
  674. border-bottom: 1px solid #ebeef5;
  675. background: #fafafa;
  676. }
  677. .workorder-dropdown-list {
  678. max-height: 280px;
  679. overflow-y: auto;
  680. }
  681. .workorder-suggest-row {
  682. display: grid;
  683. grid-template-columns: 118px minmax(220px, 1fr) minmax(140px, 180px);
  684. gap: 10px;
  685. align-items: center;
  686. padding: 8px 12px;
  687. line-height: 1.4;
  688. font-size: 13px;
  689. cursor: pointer;
  690. box-sizing: border-box;
  691. }
  692. .workorder-suggest-row:hover {
  693. background: #f5f7fa;
  694. }
  695. .workorder-suggest-header,
  696. .workorder-suggest-row {
  697. display: grid;
  698. grid-template-columns: 118px minmax(220px, 1fr) minmax(140px, 180px);
  699. gap: 10px;
  700. align-items: center;
  701. line-height: 1.4;
  702. font-size: 13px;
  703. box-sizing: border-box;
  704. }
  705. .workorder-suggest-header {
  706. font-weight: 600;
  707. color: #909399;
  708. font-size: 12px;
  709. }
  710. .workorder-suggest-no {
  711. color: #303133;
  712. font-weight: 500;
  713. overflow: hidden;
  714. text-overflow: ellipsis;
  715. white-space: nowrap;
  716. }
  717. .workorder-suggest-style {
  718. color: #606266;
  719. overflow: hidden;
  720. text-overflow: ellipsis;
  721. white-space: nowrap;
  722. }
  723. .workorder-suggest-name {
  724. color: #303133;
  725. overflow: hidden;
  726. text-overflow: ellipsis;
  727. white-space: nowrap;
  728. }
  729. .order-summary-desc {
  730. width: 100%;
  731. margin-bottom: 10px;
  732. }
  733. .order-summary-desc :deep(.el-descriptions__label) {
  734. width: 100px;
  735. font-weight: normal;
  736. color: #606266;
  737. background: #f5f7fa;
  738. }
  739. .order-summary-desc :deep(.el-descriptions__content) {
  740. color: #303133;
  741. }
  742. /* 不用 gva-table-box,避免全局白底卡片(main.scss: bg-white p-6 rounded) */
  743. .process-production-table-wrap {
  744. width: 100%;
  745. padding: 0;
  746. background: transparent;
  747. border-radius: 0;
  748. }
  749. .process-production-table-wrap :deep(.el-table__footer-wrapper td) {
  750. font-weight: 600;
  751. color: #303133;
  752. background: #fafafa;
  753. }
  754. .process-production-col-header {
  755. display: inline-flex;
  756. align-items: center;
  757. justify-content: center;
  758. gap: 4px;
  759. }
  760. .gy-detail-filter-btn {
  761. display: inline-flex;
  762. align-items: center;
  763. justify-content: center;
  764. width: 22px;
  765. height: 22px;
  766. cursor: pointer;
  767. color: #909399;
  768. border-radius: 2px;
  769. }
  770. .gy-detail-filter-btn:hover,
  771. .gy-detail-filter-btn.is-active {
  772. color: var(--el-color-primary);
  773. background: #ecf5ff;
  774. }
  775. .gy-detail-filter-options {
  776. max-height: 220px;
  777. overflow-x: hidden;
  778. overflow-y: auto;
  779. margin-bottom: 4px;
  780. }
  781. .gy-detail-filter-panel :deep(.el-checkbox) {
  782. display: flex;
  783. margin-right: 0;
  784. margin-bottom: 6px;
  785. }
  786. .gy-detail-filter-actions {
  787. display: flex;
  788. justify-content: space-between;
  789. align-items: center;
  790. margin-top: 4px;
  791. padding-top: 8px;
  792. border-top: 1px solid #ebeef5;
  793. }
  794. .gy-detail-filter-link {
  795. color: var(--el-color-primary);
  796. font-size: 12px;
  797. line-height: 1;
  798. cursor: pointer;
  799. user-select: none;
  800. padding: 4px 2px;
  801. }
  802. .gy-detail-filter-link:hover {
  803. opacity: 0.85;
  804. }
  805. </style>
  806. <style>
  807. .gy-detail-filter-popper {
  808. z-index: 10050 !important;
  809. pointer-events: auto;
  810. padding: 8px 10px !important;
  811. }
  812. .gy-detail-filter-popper .gy-detail-filter-panel {
  813. pointer-events: auto;
  814. }
  815. .gy-detail-filter-popper .gy-detail-filter-panel .el-checkbox {
  816. display: flex;
  817. margin-right: 0;
  818. margin-bottom: 6px;
  819. }
  820. </style>