summary-form.vue 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. <script lang="ts" setup>
  2. import type { DeptOption, DetailItem, OperationMeeting } from './types'
  3. import MeetingDetailDrawer from './components/meeting-detail-drawer.vue'
  4. import OperationMeetingContent from './components/operation-meeting-content.vue'
  5. import { OperationMeetingApi } from '@/api/pms/meeting'
  6. import { useWindowSize } from '@vueuse/core'
  7. interface Props {
  8. visible: boolean
  9. meetingSeries?: string
  10. year?: string | number
  11. deptOptions?: DeptOption[]
  12. }
  13. interface OperationMeetingForm
  14. extends Omit<Partial<OperationMeeting>, 'meetingDate' | 'meetingSeries'> {
  15. meetingDate?: number | string | Date
  16. meetingSeries?: number
  17. }
  18. interface SummaryMeetingItem {
  19. key: string
  20. meeting: OperationMeetingForm
  21. details: DetailItem[]
  22. }
  23. const props = withDefaults(defineProps<Props>(), {
  24. meetingSeries: '',
  25. year: '',
  26. deptOptions: () => []
  27. })
  28. const emits = defineEmits<{
  29. 'update:visible': [visible: boolean]
  30. }>()
  31. const { width } = useWindowSize()
  32. const drawerSize = computed(() => (width.value <= 768 ? '100%' : '100%'))
  33. const loading = ref(false)
  34. const summaryMeetings = ref<SummaryMeetingItem[]>([])
  35. const detailDrawerVisible = ref(false)
  36. const detailForm = ref<DetailItem>(createDetailItem())
  37. function createDetailItem(): DetailItem {
  38. return {
  39. raw: {},
  40. projectName: '',
  41. currentRevenue: undefined,
  42. cumulativeRevenue: undefined,
  43. currentOnAccount: undefined,
  44. cumulativeOnAccount: undefined,
  45. currentPayment: undefined,
  46. cumulativePayment: undefined,
  47. plannedWorkload: '',
  48. actualCompletion: '',
  49. equipmentUtilizationRate: undefined,
  50. keyWorkCompletion: '',
  51. problemsAnalysis: '',
  52. nextPlannedWorkload: '',
  53. priorityTasks: ''
  54. }
  55. }
  56. const cloneDetailItem = (data?: Partial<DetailItem>): DetailItem => ({
  57. raw: data?.raw || {},
  58. projectName: data?.projectName || '',
  59. currentRevenue: data?.currentRevenue,
  60. cumulativeRevenue: data?.cumulativeRevenue,
  61. currentOnAccount: data?.currentOnAccount,
  62. cumulativeOnAccount: data?.cumulativeOnAccount,
  63. currentPayment: data?.currentPayment,
  64. cumulativePayment: data?.cumulativePayment,
  65. plannedWorkload: data?.plannedWorkload || '',
  66. actualCompletion: data?.actualCompletion || '',
  67. equipmentUtilizationRate: data?.equipmentUtilizationRate,
  68. keyWorkCompletion: data?.keyWorkCompletion || '',
  69. problemsAnalysis: data?.problemsAnalysis || '',
  70. nextPlannedWorkload: data?.nextPlannedWorkload || '',
  71. priorityTasks: data?.priorityTasks || ''
  72. })
  73. const parseNumberValue = (value: unknown) => {
  74. if (value === undefined || value === null || value === '') return undefined
  75. const parsed = Number(String(value).replace('%', ''))
  76. return Number.isNaN(parsed) ? undefined : parsed
  77. }
  78. const parseMeetingSeries = (value: unknown) => {
  79. if (value === undefined || value === null || value === '') return undefined
  80. const matched = String(value).match(/\d+/)
  81. return matched ? Number(matched[0]) : undefined
  82. }
  83. const normalizeDetailItem = (data?: Record<string, unknown>): DetailItem => ({
  84. raw: data || {},
  85. projectName: String(data?.projectName || ''),
  86. currentRevenue: parseNumberValue(data?.currentRevenue),
  87. cumulativeRevenue: parseNumberValue(data?.cumulativeRevenue),
  88. currentOnAccount: parseNumberValue(data?.currentOnAccount),
  89. cumulativeOnAccount: parseNumberValue(data?.cumulativeOnAccount),
  90. currentPayment: parseNumberValue(data?.currentPayment),
  91. cumulativePayment: parseNumberValue(data?.cumulativePayment),
  92. plannedWorkload: String(data?.plannedWorkload || ''),
  93. actualCompletion: String(data?.actualCompletion || ''),
  94. equipmentUtilizationRate: parseNumberValue(data?.equipmentUtilizationRate),
  95. keyWorkCompletion: String(data?.keyWorkCompletion || ''),
  96. problemsAnalysis: String(data?.problemsAnalysis || ''),
  97. nextPlannedWorkload: String(data?.nextPlannedWorkload || ''),
  98. priorityTasks: String(data?.priorityTasks || '')
  99. })
  100. const normalizeSummaryMeeting = (
  101. data: Record<string, unknown>,
  102. index: number
  103. ): SummaryMeetingItem => {
  104. const details = Array.isArray(data.details) ? data.details : []
  105. const id = data.id === undefined || data.id === null ? index : String(data.id)
  106. return {
  107. key: `${id}-${index}`,
  108. meeting: {
  109. id: parseNumberValue(data.id),
  110. deptId: parseNumberValue(data.deptId),
  111. companyName: String(data.companyName || ''),
  112. meetingDate: data.meetingDate as number | string | Date | undefined,
  113. support: String(data.support || ''),
  114. meetingSeries: parseMeetingSeries(data.meetingSeries)
  115. },
  116. details: details.map((item) => normalizeDetailItem(item as Record<string, unknown>))
  117. }
  118. }
  119. const resetForm = () => {
  120. summaryMeetings.value = []
  121. loading.value = false
  122. detailDrawerVisible.value = false
  123. detailForm.value = createDetailItem()
  124. }
  125. const handleVisibleChange = (visible: boolean) => {
  126. emits('update:visible', visible)
  127. if (!visible) {
  128. resetForm()
  129. }
  130. }
  131. const loadSummaryDetails = async () => {
  132. if (!props.meetingSeries || !props.year) {
  133. summaryMeetings.value = []
  134. return
  135. }
  136. const meetingSeries = props.meetingSeries
  137. const year = props.year
  138. loading.value = true
  139. try {
  140. const res = await OperationMeetingApi.getSummarizedProjectDetails({
  141. meetingSeries,
  142. year
  143. })
  144. if (!props.visible || props.meetingSeries !== meetingSeries || props.year !== year) return
  145. const meetings = Array.isArray(res) ? res : []
  146. summaryMeetings.value = meetings.map((item, index) =>
  147. normalizeSummaryMeeting(item as Record<string, unknown>, index)
  148. )
  149. } finally {
  150. loading.value = false
  151. }
  152. }
  153. const handleViewDetail = (row: DetailItem) => {
  154. detailForm.value = cloneDetailItem(row)
  155. detailDrawerVisible.value = true
  156. }
  157. const handleDetailDrawerChange = (visible: boolean) => {
  158. detailDrawerVisible.value = visible
  159. if (!visible) {
  160. detailForm.value = createDetailItem()
  161. }
  162. }
  163. watch(
  164. () => [props.visible, props.meetingSeries, props.year] as const,
  165. ([visible]) => {
  166. if (!visible) return
  167. loadSummaryDetails()
  168. }
  169. )
  170. </script>
  171. <template>
  172. <el-drawer
  173. :model-value="visible"
  174. @update:model-value="handleVisibleChange"
  175. body-class="bg-gray-100"
  176. footer-class="p-4!"
  177. :size="drawerSize"
  178. :with-header="false"
  179. >
  180. <div v-loading="loading" class="summary-form">
  181. <template v-if="summaryMeetings.length">
  182. <el-form
  183. v-for="(item, index) in summaryMeetings"
  184. :key="item.key"
  185. label-width="auto"
  186. label-position="top"
  187. size="default"
  188. :model="item.meeting"
  189. class="summary-form__meeting"
  190. >
  191. <OperationMeetingContent
  192. :meeting="item.meeting"
  193. :details="item.details"
  194. type="view"
  195. :dept-options="deptOptions"
  196. :loading="loading"
  197. :show-meeting-meta="index === 0"
  198. @edit-detail="handleViewDetail"
  199. />
  200. </el-form>
  201. </template>
  202. <el-empty v-else description="暂无汇总会议详情" :image-size="100" />
  203. </div>
  204. <template #footer>
  205. <el-button size="default" @click="handleVisibleChange(false)">关闭</el-button>
  206. </template>
  207. <MeetingDetailDrawer
  208. :visible="detailDrawerVisible"
  209. :detail="detailForm"
  210. type="view"
  211. form-type="edit"
  212. @update:visible="handleDetailDrawerChange"
  213. />
  214. </el-drawer>
  215. </template>
  216. <style scoped lang="scss">
  217. .summary-form {
  218. min-height: 240px;
  219. }
  220. .summary-form__meeting {
  221. margin-bottom: 16px;
  222. }
  223. .summary-form__meeting:last-child {
  224. margin-bottom: 0;
  225. }
  226. </style>