|
|
@@ -1,1116 +0,0 @@
|
|
|
-<script lang="ts" setup>
|
|
|
-import type { CSSProperties } from 'vue'
|
|
|
-import type { DeptOption, DetailItem, OperationMeeting } from './types'
|
|
|
-import MeetingDetailDrawer from './components/meeting-detail-drawer.vue'
|
|
|
-import { OperationMeetingApi } from '@/api/pms/meeting'
|
|
|
-import { useTableComponents } from '@/components/ZmTable/useTableComponents'
|
|
|
-import { useWindowSize } from '@vueuse/core'
|
|
|
-
|
|
|
-interface Props {
|
|
|
- visible: boolean
|
|
|
- meetingSeries?: string
|
|
|
- year?: string | number
|
|
|
- deptOptions?: DeptOption[]
|
|
|
-}
|
|
|
-
|
|
|
-interface OperationMeetingForm
|
|
|
- extends Omit<Partial<OperationMeeting>, 'meetingDate' | 'meetingSeries'> {
|
|
|
- meetingDate?: number | string | Date
|
|
|
- meetingSeries?: number
|
|
|
-}
|
|
|
-
|
|
|
-interface SummaryMeetingItem {
|
|
|
- key: string
|
|
|
- meeting: OperationMeetingForm
|
|
|
- details: DetailItem[]
|
|
|
-}
|
|
|
-
|
|
|
-interface SummaryDetailItem extends DetailItem {
|
|
|
- summaryDetailKey: string
|
|
|
- meetingKey: string
|
|
|
- companyName: string
|
|
|
- companyFilterValue: string
|
|
|
- deptId?: number
|
|
|
-}
|
|
|
-
|
|
|
-interface CompanyFilterOption {
|
|
|
- label: string
|
|
|
- value: string
|
|
|
-}
|
|
|
-
|
|
|
-interface DetailCardField {
|
|
|
- label: string
|
|
|
- prop: keyof DetailItem
|
|
|
- unit?: string
|
|
|
- numeric?: boolean
|
|
|
-}
|
|
|
-
|
|
|
-interface MeetingTableCellStyleProps {
|
|
|
- column: {
|
|
|
- property?: string
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-const props = withDefaults(defineProps<Props>(), {
|
|
|
- meetingSeries: '',
|
|
|
- year: '',
|
|
|
- deptOptions: () => []
|
|
|
-})
|
|
|
-
|
|
|
-const emits = defineEmits<{
|
|
|
- 'update:visible': [visible: boolean]
|
|
|
-}>()
|
|
|
-
|
|
|
-const { ZmTable, ZmTableColumn } = useTableComponents<SummaryDetailItem>()
|
|
|
-const { width } = useWindowSize()
|
|
|
-const drawerSize = computed(() => (width.value <= 768 ? '100%' : '100%'))
|
|
|
-
|
|
|
-const loading = ref(false)
|
|
|
-const summaryMeetings = ref<SummaryMeetingItem[]>([])
|
|
|
-const detailDrawerVisible = ref(false)
|
|
|
-const detailForm = ref<DetailItem>(createDetailItem())
|
|
|
-const filterForm = reactive({
|
|
|
- companyFilterValue: ''
|
|
|
-})
|
|
|
-
|
|
|
-const detailSummaryFields = [
|
|
|
- 'currentRevenue',
|
|
|
- 'cumulativeRevenue',
|
|
|
- 'currentOnAccount',
|
|
|
- 'cumulativeOnAccount',
|
|
|
- 'currentPayment',
|
|
|
- 'cumulativePayment'
|
|
|
-] as const
|
|
|
-
|
|
|
-type DetailSummaryField = (typeof detailSummaryFields)[number]
|
|
|
-
|
|
|
-const detailSummaryLabelMap: Record<DetailSummaryField, string> = {
|
|
|
- currentRevenue: '收入-本期',
|
|
|
- cumulativeRevenue: '收入-累计',
|
|
|
- currentOnAccount: '挂帐-本期',
|
|
|
- cumulativeOnAccount: '挂帐-累计',
|
|
|
- currentPayment: '回款-本期',
|
|
|
- cumulativePayment: '回款-累计'
|
|
|
-}
|
|
|
-
|
|
|
-const detailSummaryToneMap: Record<DetailSummaryField, 'revenue' | 'account' | 'payment'> = {
|
|
|
- currentRevenue: 'revenue',
|
|
|
- cumulativeRevenue: 'revenue',
|
|
|
- currentOnAccount: 'account',
|
|
|
- cumulativeOnAccount: 'account',
|
|
|
- currentPayment: 'payment',
|
|
|
- cumulativePayment: 'payment'
|
|
|
-}
|
|
|
-
|
|
|
-const detailSummaryIconMap: Record<DetailSummaryField, string> = {
|
|
|
- currentRevenue: 'i-lucide:badge-japanese-yen',
|
|
|
- cumulativeRevenue: 'i-lucide:badge-japanese-yen',
|
|
|
- currentOnAccount: 'i-lucide:badge-alert',
|
|
|
- cumulativeOnAccount: 'i-lucide:badge-alert',
|
|
|
- currentPayment: 'i-lucide:badge-check',
|
|
|
- cumulativePayment: 'i-lucide:badge-check'
|
|
|
-}
|
|
|
-
|
|
|
-const detailCardGroups: { title: string; fields: DetailCardField[] }[] = [
|
|
|
- {
|
|
|
- title: '经营情况',
|
|
|
- fields: [
|
|
|
- { label: '收入-本期', prop: 'currentRevenue', unit: '万元', numeric: true },
|
|
|
- { label: '收入-累计', prop: 'cumulativeRevenue', unit: '万元', numeric: true },
|
|
|
- { label: '挂帐-本期', prop: 'currentOnAccount', unit: '万元', numeric: true },
|
|
|
- { label: '挂帐-累计', prop: 'cumulativeOnAccount', unit: '万元', numeric: true },
|
|
|
- { label: '回款-本期', prop: 'currentPayment', unit: '万元', numeric: true },
|
|
|
- { label: '回款-累计', prop: 'cumulativePayment', unit: '万元', numeric: true }
|
|
|
- ]
|
|
|
- },
|
|
|
- {
|
|
|
- title: '本期生产运行情况',
|
|
|
- fields: [
|
|
|
- { label: '计划工作量', prop: 'plannedWorkload' },
|
|
|
- { label: '实际完成', prop: 'actualCompletion' },
|
|
|
- { label: '设备利用率', prop: 'equipmentUtilizationRate', unit: '%', numeric: true }
|
|
|
- ]
|
|
|
- },
|
|
|
- {
|
|
|
- title: '生产管理情况及重点工作',
|
|
|
- fields: [
|
|
|
- { label: '重点工作及完成情况', prop: 'keyWorkCompletion' },
|
|
|
- { label: '存在问题及分析', prop: 'problemsAnalysis' }
|
|
|
- ]
|
|
|
- },
|
|
|
- {
|
|
|
- title: '下期工作计划',
|
|
|
- fields: [
|
|
|
- { label: '计划工作量', prop: 'nextPlannedWorkload' },
|
|
|
- { label: '重点工作事项', prop: 'priorityTasks' }
|
|
|
- ]
|
|
|
- }
|
|
|
-]
|
|
|
-
|
|
|
-const meetingTableBlueColumns = new Set<keyof DetailItem>([
|
|
|
- 'projectName',
|
|
|
- 'actualCompletion',
|
|
|
- 'keyWorkCompletion',
|
|
|
- 'problemsAnalysis'
|
|
|
-])
|
|
|
-
|
|
|
-function createDetailItem(): DetailItem {
|
|
|
- return {
|
|
|
- raw: {},
|
|
|
- projectName: '',
|
|
|
- currentRevenue: undefined,
|
|
|
- cumulativeRevenue: undefined,
|
|
|
- currentOnAccount: undefined,
|
|
|
- cumulativeOnAccount: undefined,
|
|
|
- currentPayment: undefined,
|
|
|
- cumulativePayment: undefined,
|
|
|
- plannedWorkload: '',
|
|
|
- actualCompletion: '',
|
|
|
- equipmentUtilizationRate: undefined,
|
|
|
- keyWorkCompletion: '',
|
|
|
- problemsAnalysis: '',
|
|
|
- nextPlannedWorkload: '',
|
|
|
- priorityTasks: ''
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-const cloneDetailItem = (data?: Partial<DetailItem>): DetailItem => ({
|
|
|
- raw: data?.raw || {},
|
|
|
- projectName: data?.projectName || '',
|
|
|
- currentRevenue: data?.currentRevenue,
|
|
|
- cumulativeRevenue: data?.cumulativeRevenue,
|
|
|
- currentOnAccount: data?.currentOnAccount,
|
|
|
- cumulativeOnAccount: data?.cumulativeOnAccount,
|
|
|
- currentPayment: data?.currentPayment,
|
|
|
- cumulativePayment: data?.cumulativePayment,
|
|
|
- plannedWorkload: data?.plannedWorkload || '',
|
|
|
- actualCompletion: data?.actualCompletion || '',
|
|
|
- equipmentUtilizationRate: data?.equipmentUtilizationRate,
|
|
|
- keyWorkCompletion: data?.keyWorkCompletion || '',
|
|
|
- problemsAnalysis: data?.problemsAnalysis || '',
|
|
|
- nextPlannedWorkload: data?.nextPlannedWorkload || '',
|
|
|
- priorityTasks: data?.priorityTasks || ''
|
|
|
-})
|
|
|
-
|
|
|
-const parseNumberValue = (value: unknown) => {
|
|
|
- if (value === undefined || value === null || value === '') return undefined
|
|
|
- const parsed = Number(String(value).replace('%', ''))
|
|
|
-
|
|
|
- return Number.isNaN(parsed) ? undefined : parsed
|
|
|
-}
|
|
|
-
|
|
|
-const parseMeetingSeries = (value: unknown) => {
|
|
|
- if (value === undefined || value === null || value === '') return undefined
|
|
|
- const matched = String(value).match(/\d+/)
|
|
|
-
|
|
|
- return matched ? Number(matched[0]) : undefined
|
|
|
-}
|
|
|
-
|
|
|
-const normalizeTextValue = (value: unknown) => String(value ?? '').replace(/\\r\\n|\\n/g, '\n')
|
|
|
-
|
|
|
-const normalizeDetailItem = (data?: Record<string, unknown>): DetailItem => ({
|
|
|
- raw: data || {},
|
|
|
- projectName: normalizeTextValue(data?.projectName),
|
|
|
- currentRevenue: parseNumberValue(data?.currentRevenue),
|
|
|
- cumulativeRevenue: parseNumberValue(data?.cumulativeRevenue),
|
|
|
- currentOnAccount: parseNumberValue(data?.currentOnAccount),
|
|
|
- cumulativeOnAccount: parseNumberValue(data?.cumulativeOnAccount),
|
|
|
- currentPayment: parseNumberValue(data?.currentPayment),
|
|
|
- cumulativePayment: parseNumberValue(data?.cumulativePayment),
|
|
|
- plannedWorkload: normalizeTextValue(data?.plannedWorkload),
|
|
|
- actualCompletion: normalizeTextValue(data?.actualCompletion),
|
|
|
- equipmentUtilizationRate: parseNumberValue(data?.equipmentUtilizationRate),
|
|
|
- keyWorkCompletion: normalizeTextValue(data?.keyWorkCompletion),
|
|
|
- problemsAnalysis: normalizeTextValue(data?.problemsAnalysis),
|
|
|
- nextPlannedWorkload: normalizeTextValue(data?.nextPlannedWorkload),
|
|
|
- priorityTasks: normalizeTextValue(data?.priorityTasks)
|
|
|
-})
|
|
|
-
|
|
|
-const normalizeSummaryMeeting = (
|
|
|
- data: Record<string, unknown>,
|
|
|
- index: number
|
|
|
-): SummaryMeetingItem => {
|
|
|
- const details = Array.isArray(data.details) ? data.details : []
|
|
|
- const id = data.id === undefined || data.id === null ? index : String(data.id)
|
|
|
-
|
|
|
- return {
|
|
|
- key: `${id}-${index}`,
|
|
|
- meeting: {
|
|
|
- id: parseNumberValue(data.id),
|
|
|
- deptId: parseNumberValue(data.deptId),
|
|
|
- companyName: String(data.companyName || ''),
|
|
|
- meetingDate: data.meetingDate as number | string | Date | undefined,
|
|
|
- support: String(data.support || ''),
|
|
|
- cumulative: data.cumulative as boolean | undefined,
|
|
|
- meetingSeries: parseMeetingSeries(data.meetingSeries)
|
|
|
- },
|
|
|
- details: details.map((item) => normalizeDetailItem(item as Record<string, unknown>))
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-const getCompanyDisplayName = (meeting: OperationMeetingForm) => {
|
|
|
- if (meeting.companyName) return meeting.companyName
|
|
|
-
|
|
|
- if (meeting.deptId) {
|
|
|
- return props.deptOptions.find((item) => item.value === meeting.deptId)?.label || ''
|
|
|
- }
|
|
|
-
|
|
|
- return ''
|
|
|
-}
|
|
|
-
|
|
|
-const getCompanyFilterValue = (meeting: OperationMeetingForm, index: number) => {
|
|
|
- if (meeting.deptId !== undefined) return `dept:${meeting.deptId}`
|
|
|
-
|
|
|
- const companyName = getCompanyDisplayName(meeting).trim()
|
|
|
-
|
|
|
- return companyName ? `company:${companyName}` : `unknown:${index}`
|
|
|
-}
|
|
|
-
|
|
|
-const summaryMeetingMeta = computed<OperationMeetingForm>(
|
|
|
- () => summaryMeetings.value[0]?.meeting || {}
|
|
|
-)
|
|
|
-
|
|
|
-const companyOptions = computed<CompanyFilterOption[]>(() => {
|
|
|
- const optionMap = new Map<string, CompanyFilterOption>()
|
|
|
-
|
|
|
- summaryMeetings.value.forEach((item, index) => {
|
|
|
- const value = getCompanyFilterValue(item.meeting, index)
|
|
|
-
|
|
|
- if (optionMap.has(value)) return
|
|
|
-
|
|
|
- optionMap.set(value, {
|
|
|
- value,
|
|
|
- label: getCompanyDisplayName(item.meeting) || '未知公司'
|
|
|
- })
|
|
|
- })
|
|
|
-
|
|
|
- return Array.from(optionMap.values())
|
|
|
-})
|
|
|
-
|
|
|
-const summaryDetailRows = computed<SummaryDetailItem[]>(() =>
|
|
|
- summaryMeetings.value.flatMap((item, meetingIndex) => {
|
|
|
- const companyFilterValue = getCompanyFilterValue(item.meeting, meetingIndex)
|
|
|
- const companyName = getCompanyDisplayName(item.meeting) || '未知公司'
|
|
|
-
|
|
|
- return item.details.map((detail, detailIndex) => ({
|
|
|
- ...detail,
|
|
|
- summaryDetailKey: `${item.key}-${detailIndex}`,
|
|
|
- meetingKey: item.key,
|
|
|
- companyName,
|
|
|
- cumulative: item.meeting.cumulative,
|
|
|
- companyFilterValue,
|
|
|
- deptId: item.meeting.deptId
|
|
|
- }))
|
|
|
- })
|
|
|
-)
|
|
|
-
|
|
|
-const filteredDetailRows = computed(() => {
|
|
|
- if (!filterForm.companyFilterValue) return summaryDetailRows.value
|
|
|
-
|
|
|
- return summaryDetailRows.value.filter(
|
|
|
- (item) => item.companyFilterValue === filterForm.companyFilterValue
|
|
|
- )
|
|
|
-})
|
|
|
-
|
|
|
-const currentSummaryScopeName = computed(
|
|
|
- () =>
|
|
|
- companyOptions.value.find((item) => item.value === filterForm.companyFilterValue)?.label ||
|
|
|
- '全部公司'
|
|
|
-)
|
|
|
-
|
|
|
-const currentSummarySupport = computed(() => {
|
|
|
- if (!filterForm.companyFilterValue) return ''
|
|
|
-
|
|
|
- return (
|
|
|
- summaryMeetings.value.find(
|
|
|
- (item, index) => getCompanyFilterValue(item.meeting, index) === filterForm.companyFilterValue
|
|
|
- )?.meeting.support || ''
|
|
|
- )
|
|
|
-})
|
|
|
-
|
|
|
-const formatSummaryNumber = (value: number) =>
|
|
|
- value.toLocaleString('zh-CN', {
|
|
|
- maximumFractionDigits: 2,
|
|
|
- minimumFractionDigits: Number.isInteger(value) ? 0 : 2
|
|
|
- })
|
|
|
-
|
|
|
-const getDetailSummaryTotal = (field: DetailSummaryField) =>
|
|
|
- filteredDetailRows.value.reduce((sum, item) => {
|
|
|
- if (item.companyFilterValue === filterForm.companyFilterValue || (item as any).cumulative) {
|
|
|
- return sum + Number(item[field] || 0)
|
|
|
- }
|
|
|
- return sum
|
|
|
- }, 0)
|
|
|
-
|
|
|
-const detailSummaryCards = computed(() =>
|
|
|
- detailSummaryFields.map((field) => ({
|
|
|
- label: detailSummaryLabelMap[field],
|
|
|
- value: formatSummaryNumber(getDetailSummaryTotal(field)),
|
|
|
- tone: detailSummaryToneMap[field],
|
|
|
- icon: detailSummaryIconMap[field]
|
|
|
- }))
|
|
|
-)
|
|
|
-
|
|
|
-const formatDetailCardValue = (item: SummaryDetailItem, field: DetailCardField) => {
|
|
|
- const value = item[field.prop]
|
|
|
-
|
|
|
- if (value === undefined || value === null || value === '') {
|
|
|
- return '-'
|
|
|
- }
|
|
|
-
|
|
|
- if (!field.numeric) {
|
|
|
- return String(value)
|
|
|
- }
|
|
|
-
|
|
|
- const numericValue = Number(value)
|
|
|
-
|
|
|
- if (Number.isNaN(numericValue)) {
|
|
|
- return '-'
|
|
|
- }
|
|
|
-
|
|
|
- return `${formatSummaryNumber(numericValue)}${field.unit || ''}`
|
|
|
-}
|
|
|
-
|
|
|
-const formatEquipmentUtilizationRate = (row: SummaryDetailItem) => {
|
|
|
- if (row.equipmentUtilizationRate === undefined || row.equipmentUtilizationRate === null) {
|
|
|
- return '-'
|
|
|
- }
|
|
|
-
|
|
|
- return `${formatSummaryNumber(Number(row.equipmentUtilizationRate))}%`
|
|
|
-}
|
|
|
-
|
|
|
-const getMeetingTableCellStyle: any = ({
|
|
|
- column
|
|
|
-}: MeetingTableCellStyleProps): CSSProperties | undefined => {
|
|
|
- const property = column.property as keyof DetailItem | undefined
|
|
|
-
|
|
|
- if (property && meetingTableBlueColumns.has(property)) {
|
|
|
- return { color: '#1b71f6' }
|
|
|
- }
|
|
|
-
|
|
|
- return undefined
|
|
|
-}
|
|
|
-
|
|
|
-const getMeetingTableRowStyle = (): CSSProperties => ({
|
|
|
- height: 'auto',
|
|
|
- minHeight: '40px'
|
|
|
-})
|
|
|
-
|
|
|
-const getMeetingTableCellClassName = ({ column }: MeetingTableCellStyleProps) => {
|
|
|
- const property = column.property as keyof DetailItem | undefined
|
|
|
-
|
|
|
- return property ? 'meeting-table__cell' : ''
|
|
|
-}
|
|
|
-
|
|
|
-const resetForm = () => {
|
|
|
- summaryMeetings.value = []
|
|
|
- filterForm.companyFilterValue = ''
|
|
|
- loading.value = false
|
|
|
- detailDrawerVisible.value = false
|
|
|
- detailForm.value = createDetailItem()
|
|
|
-}
|
|
|
-
|
|
|
-const handleVisibleChange = (visible: boolean) => {
|
|
|
- emits('update:visible', visible)
|
|
|
-
|
|
|
- if (!visible) {
|
|
|
- resetForm()
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-const loadSummaryDetails = async () => {
|
|
|
- if (!props.meetingSeries || !props.year) {
|
|
|
- summaryMeetings.value = []
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- const meetingSeries = props.meetingSeries
|
|
|
- const year = props.year
|
|
|
- loading.value = true
|
|
|
- try {
|
|
|
- const res = await OperationMeetingApi.getSummarizedProjectDetails({
|
|
|
- meetingSeries,
|
|
|
- year
|
|
|
- })
|
|
|
-
|
|
|
- if (!props.visible || props.meetingSeries !== meetingSeries || props.year !== year) return
|
|
|
-
|
|
|
- const meetings = Array.isArray(res) ? res : []
|
|
|
- summaryMeetings.value = meetings.map((item, index) =>
|
|
|
- normalizeSummaryMeeting(item as Record<string, unknown>, index)
|
|
|
- )
|
|
|
- filterForm.companyFilterValue = ''
|
|
|
- } finally {
|
|
|
- loading.value = false
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-const handleViewDetail = (row: SummaryDetailItem) => {
|
|
|
- detailForm.value = cloneDetailItem(row)
|
|
|
- detailDrawerVisible.value = true
|
|
|
-}
|
|
|
-
|
|
|
-const handleDetailDrawerChange = (visible: boolean) => {
|
|
|
- detailDrawerVisible.value = visible
|
|
|
-
|
|
|
- if (!visible) {
|
|
|
- detailForm.value = createDetailItem()
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-watch(
|
|
|
- () => [props.visible, props.meetingSeries, props.year] as const,
|
|
|
- ([visible]) => {
|
|
|
- if (!visible) return
|
|
|
- loadSummaryDetails()
|
|
|
- }
|
|
|
-)
|
|
|
-</script>
|
|
|
-
|
|
|
-<template>
|
|
|
- <el-drawer
|
|
|
- :model-value="visible"
|
|
|
- @update:model-value="handleVisibleChange"
|
|
|
- body-class="bg-gray-100"
|
|
|
- footer-class="p-4!"
|
|
|
- :size="drawerSize"
|
|
|
- :with-header="false"
|
|
|
- >
|
|
|
- <div v-loading="loading" class="summary-form">
|
|
|
- <template v-if="summaryMeetings.length">
|
|
|
- <el-form
|
|
|
- label-width="auto"
|
|
|
- label-position="top"
|
|
|
- size="default"
|
|
|
- :model="filterForm"
|
|
|
- class="summary-form__section"
|
|
|
- >
|
|
|
- <div class="meeting-section__top">
|
|
|
- <h2 class="meeting-section__title">生产运营双周例会汇报</h2>
|
|
|
-
|
|
|
- <div class="meeting-section__grid">
|
|
|
- <el-form-item label="会议期次" label-position="left" class="mb-0! min-w-0">
|
|
|
- <el-input-number
|
|
|
- :model-value="summaryMeetingMeta.meetingSeries"
|
|
|
- class="w-full!"
|
|
|
- placeholder="暂无会议期次"
|
|
|
- disabled
|
|
|
- :controls="false"
|
|
|
- />
|
|
|
- </el-form-item>
|
|
|
-
|
|
|
- <el-form-item
|
|
|
- label="专业公司"
|
|
|
- label-position="left"
|
|
|
- prop="companyFilterValue"
|
|
|
- class="mb-0! min-w-0"
|
|
|
- >
|
|
|
- <el-select
|
|
|
- v-model="filterForm.companyFilterValue"
|
|
|
- class="w-full!"
|
|
|
- placeholder="全部公司"
|
|
|
- clearable
|
|
|
- filterable
|
|
|
- >
|
|
|
- <el-option
|
|
|
- v-for="item in companyOptions"
|
|
|
- :key="item.value"
|
|
|
- :label="item.label"
|
|
|
- :value="item.value"
|
|
|
- />
|
|
|
- </el-select>
|
|
|
- </el-form-item>
|
|
|
-
|
|
|
- <el-form-item label="会议日期" label-position="left" class="mb-0! min-w-0">
|
|
|
- <el-date-picker
|
|
|
- :model-value="summaryMeetingMeta.meetingDate"
|
|
|
- type="date"
|
|
|
- placeholder="暂无会议日期"
|
|
|
- disabled
|
|
|
- class="w-full!"
|
|
|
- />
|
|
|
- </el-form-item>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <section class="meeting-summary-strip">
|
|
|
- <div class="meeting-summary-strip__title">
|
|
|
- <span>{{ currentSummaryScopeName }}</span>
|
|
|
- <small>经营数据汇总</small>
|
|
|
- </div>
|
|
|
- <div class="meeting-summary-strip__grid">
|
|
|
- <div
|
|
|
- v-for="item in detailSummaryCards"
|
|
|
- :key="item.label"
|
|
|
- :class="[
|
|
|
- 'meeting-summary-strip__item',
|
|
|
- `meeting-summary-strip__item--${item.tone}`
|
|
|
- ]"
|
|
|
- >
|
|
|
- <div :class="item.icon + ' size-5 icon'"></div>
|
|
|
- <span>{{ item.label }}</span>
|
|
|
- <strong>{{ item.value }}<em>万元</em></strong>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </section>
|
|
|
-
|
|
|
- <section class="meeting-detail-panel">
|
|
|
- <div class="meeting-detail-panel__header">
|
|
|
- <h3 class="text-lg font-bold m-0">会议明细</h3>
|
|
|
- <el-tag size="small" effect="plain">共 {{ filteredDetailRows.length }} 项</el-tag>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="meeting-detail-table-view">
|
|
|
- <ZmTable
|
|
|
- class="meeting-table"
|
|
|
- :data="filteredDetailRows"
|
|
|
- :loading="loading"
|
|
|
- :max-height="650"
|
|
|
- align="left"
|
|
|
- :show-overflow-tooltip="false"
|
|
|
- :cell-style="getMeetingTableCellStyle"
|
|
|
- :row-style="getMeetingTableRowStyle"
|
|
|
- :cell-class-name="getMeetingTableCellClassName"
|
|
|
- >
|
|
|
- <zm-table-column
|
|
|
- min-width="6%"
|
|
|
- align="center"
|
|
|
- label="专业公司"
|
|
|
- prop="companyName"
|
|
|
- fixed="left"
|
|
|
- />
|
|
|
- <zm-table-column
|
|
|
- min-width="6%"
|
|
|
- align="center"
|
|
|
- label="项目名称"
|
|
|
- prop="projectName"
|
|
|
- />
|
|
|
- <zm-table-column min-width="24%" label="本期生产运行情况">
|
|
|
- <zm-table-column min-width="9%" label="计划工作量" prop="plannedWorkload" />
|
|
|
- <zm-table-column min-width="9%" label="实际完成" prop="actualCompletion" />
|
|
|
- <zm-table-column
|
|
|
- min-width="6%"
|
|
|
- label="设备利用率"
|
|
|
- prop="equipmentUtilizationRate"
|
|
|
- :formatter="formatEquipmentUtilizationRate"
|
|
|
- />
|
|
|
- </zm-table-column>
|
|
|
- <zm-table-column min-width="28%" label="生产管理情况及重点工作">
|
|
|
- <zm-table-column
|
|
|
- min-width="18%"
|
|
|
- label="重点工作及完成情况"
|
|
|
- prop="keyWorkCompletion"
|
|
|
- />
|
|
|
- <zm-table-column min-width="10%" label="存在问题及分析" prop="problemsAnalysis" />
|
|
|
- </zm-table-column>
|
|
|
- <zm-table-column min-width="27%" label="下期工作计划">
|
|
|
- <zm-table-column min-width="9%" label="计划工作量" prop="nextPlannedWorkload" />
|
|
|
- <zm-table-column min-width="18%" label="重点工作事项" prop="priorityTasks" />
|
|
|
- </zm-table-column>
|
|
|
- <zm-table-column label="操作" min-width="5%" align="center" fixed="right">
|
|
|
- <template #default="{ row }">
|
|
|
- <div class="meeting-table__actions">
|
|
|
- <el-button link size="default" type="primary" @click="handleViewDetail(row)">
|
|
|
- 查看
|
|
|
- </el-button>
|
|
|
- </div>
|
|
|
- </template>
|
|
|
- </zm-table-column>
|
|
|
- </ZmTable>
|
|
|
- </div>
|
|
|
-
|
|
|
- <section v-if="currentSummarySupport" class="meeting-support-panel">
|
|
|
- <el-form-item
|
|
|
- label="其他重点事项及需要集团协调事项"
|
|
|
- class="meeting-support-panel__item"
|
|
|
- >
|
|
|
- <el-input :model-value="currentSummarySupport" type="textarea" :rows="4" readonly />
|
|
|
- </el-form-item>
|
|
|
- </section>
|
|
|
-
|
|
|
- <div v-loading="loading" class="meeting-detail-card-view">
|
|
|
- <template v-if="filteredDetailRows.length">
|
|
|
- <article
|
|
|
- v-for="(item, index) in filteredDetailRows"
|
|
|
- :key="item.summaryDetailKey"
|
|
|
- class="meeting-detail-card"
|
|
|
- >
|
|
|
- <div class="meeting-detail-card__header">
|
|
|
- <div class="meeting-detail-card__title">
|
|
|
- <span>项目名称</span>
|
|
|
- <strong>{{ item.projectName || '-' }}</strong>
|
|
|
- </div>
|
|
|
- <el-tag size="small" effect="plain">第 {{ index + 1 }} 项</el-tag>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="meeting-detail-card__company">
|
|
|
- <span>专业公司</span>
|
|
|
- <strong>{{ item.companyName || '-' }}</strong>
|
|
|
- </div>
|
|
|
-
|
|
|
- <section
|
|
|
- v-for="group in detailCardGroups"
|
|
|
- :key="group.title"
|
|
|
- class="meeting-detail-card__group"
|
|
|
- >
|
|
|
- <h4>{{ group.title }}</h4>
|
|
|
- <div class="meeting-detail-card__fields">
|
|
|
- <div
|
|
|
- v-for="field in group.fields"
|
|
|
- :key="field.prop"
|
|
|
- class="meeting-detail-card__field"
|
|
|
- >
|
|
|
- <span>{{ field.label }}</span>
|
|
|
- <strong>{{ formatDetailCardValue(item, field) }}</strong>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </section>
|
|
|
-
|
|
|
- <div class="meeting-detail-card__actions">
|
|
|
- <el-button link size="small" type="primary" @click="handleViewDetail(item)">
|
|
|
- 查看
|
|
|
- </el-button>
|
|
|
- </div>
|
|
|
- </article>
|
|
|
- </template>
|
|
|
- <el-empty v-else description="暂无会议明细" :image-size="80" />
|
|
|
- </div>
|
|
|
- </section>
|
|
|
- </el-form>
|
|
|
- </template>
|
|
|
-
|
|
|
- <el-empty v-else description="暂无汇总会议详情" :image-size="100" />
|
|
|
- </div>
|
|
|
-
|
|
|
- <template #footer>
|
|
|
- <el-button size="default" @click="handleVisibleChange(false)">关闭</el-button>
|
|
|
- </template>
|
|
|
-
|
|
|
- <MeetingDetailDrawer
|
|
|
- :visible="detailDrawerVisible"
|
|
|
- :detail="detailForm"
|
|
|
- type="view"
|
|
|
- form-type="edit"
|
|
|
- @update:visible="handleDetailDrawerChange"
|
|
|
- />
|
|
|
- </el-drawer>
|
|
|
-</template>
|
|
|
-
|
|
|
-<style scoped lang="scss">
|
|
|
-.summary-form {
|
|
|
- min-height: 240px;
|
|
|
-}
|
|
|
-
|
|
|
-.summary-form__section {
|
|
|
- padding: 8px;
|
|
|
- background: #fff;
|
|
|
- border: 1px solid rgb(229 231 235 / 90%);
|
|
|
- border-radius: 12px;
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-section__grid {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
|
- gap: 18px 20px;
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-section__top {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: max-content minmax(0, 1fr);
|
|
|
- gap: 240px;
|
|
|
- align-items: start;
|
|
|
- margin-bottom: 18px;
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-section__title {
|
|
|
- padding-top: 4px;
|
|
|
- margin: 0;
|
|
|
- font-size: 18px;
|
|
|
- font-weight: 800;
|
|
|
- line-height: 32px;
|
|
|
- color: #1f2937;
|
|
|
- white-space: nowrap;
|
|
|
- transform: translateY(-4px);
|
|
|
-}
|
|
|
-
|
|
|
-:deep(.el-form-item__label) {
|
|
|
- font-weight: 800;
|
|
|
- color: #1f2937;
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-summary-strip {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: 112px minmax(0, 1fr);
|
|
|
- gap: 8px;
|
|
|
- align-items: stretch;
|
|
|
- padding: 6px 8px;
|
|
|
- background: linear-gradient(180deg, #f8fbff 0%, #fff 100%);
|
|
|
- border: 1px solid #dbeafe;
|
|
|
- border-radius: 8px;
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-summary-strip__title {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- justify-content: center;
|
|
|
- min-width: 0;
|
|
|
- padding-right: 8px;
|
|
|
- border-right: 1px solid #dbeafe;
|
|
|
-
|
|
|
- span {
|
|
|
- overflow: hidden;
|
|
|
- font-size: 14px;
|
|
|
- font-weight: 800;
|
|
|
- line-height: 20px;
|
|
|
- color: #1f2937;
|
|
|
- text-overflow: ellipsis;
|
|
|
- white-space: nowrap;
|
|
|
- }
|
|
|
-
|
|
|
- small {
|
|
|
- margin-top: 0;
|
|
|
- font-size: 12px;
|
|
|
- font-weight: 800;
|
|
|
- line-height: 16px;
|
|
|
- color: #64748b;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-summary-strip__grid {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: repeat(6, minmax(0, 1fr));
|
|
|
- gap: 4px;
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-summary-strip__item {
|
|
|
- display: flex;
|
|
|
- min-width: 0;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
- gap: 4px;
|
|
|
- padding: 4px 6px;
|
|
|
- background: rgb(255 255 255 / 72%);
|
|
|
- border: 1px solid rgb(219 234 254 / 70%);
|
|
|
- border-radius: 7px;
|
|
|
-
|
|
|
- span {
|
|
|
- display: block;
|
|
|
- flex: 1;
|
|
|
- min-width: 0;
|
|
|
- overflow: hidden;
|
|
|
- font-size: 12px;
|
|
|
- font-weight: 800;
|
|
|
- line-height: 16px;
|
|
|
- color: #1f2937;
|
|
|
- text-overflow: ellipsis;
|
|
|
- white-space: nowrap;
|
|
|
- }
|
|
|
-
|
|
|
- strong {
|
|
|
- display: flex;
|
|
|
- min-width: 0;
|
|
|
- flex-shrink: 0;
|
|
|
- align-items: baseline;
|
|
|
- gap: 3px;
|
|
|
- margin-top: 0;
|
|
|
- overflow: hidden;
|
|
|
- font-size: 15px;
|
|
|
- font-weight: 800;
|
|
|
- line-height: 18px;
|
|
|
- color: #1b71f6;
|
|
|
- text-overflow: ellipsis;
|
|
|
- white-space: nowrap;
|
|
|
- }
|
|
|
-
|
|
|
- em {
|
|
|
- font-size: 12px;
|
|
|
- font-style: normal;
|
|
|
- font-weight: 800;
|
|
|
- color: #64748b;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-summary-strip__item--revenue strong,
|
|
|
-.meeting-summary-strip__item--revenue .icon {
|
|
|
- color: #1b71f6;
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-summary-strip__item--revenue {
|
|
|
- background: linear-gradient(180deg, #eff6ff 0%, #fff 100%);
|
|
|
- border-color: #bfdbfe;
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-summary-strip__item--account strong,
|
|
|
-.meeting-summary-strip__item--account .icon {
|
|
|
- color: #f59e0b;
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-summary-strip__item--account {
|
|
|
- background: linear-gradient(180deg, #fffbeb 0%, #fff 100%);
|
|
|
- border-color: #fde68a;
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-summary-strip__item--payment strong,
|
|
|
-.meeting-summary-strip__item--payment .icon {
|
|
|
- color: #10b981;
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-summary-strip__item--payment {
|
|
|
- background: linear-gradient(180deg, #ecfdf5 0%, #fff 100%);
|
|
|
- border-color: #bbf7d0;
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-detail-panel {
|
|
|
- padding-top: 12px;
|
|
|
- margin-top: 12px;
|
|
|
- border-top: 1px solid var(--el-border-color-lighter);
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-support-panel {
|
|
|
- padding-top: 12px;
|
|
|
- margin-top: 12px;
|
|
|
- border-top: 1px solid var(--el-border-color-lighter);
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-support-panel__item {
|
|
|
- margin-bottom: 0;
|
|
|
-
|
|
|
- :deep(.el-textarea__inner) {
|
|
|
- font-size: 16px;
|
|
|
- font-weight: 700;
|
|
|
- color: #24364d;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-detail-panel__header {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
- gap: 12px;
|
|
|
- margin-bottom: 12px;
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-table {
|
|
|
- --zm-table-font-size: 18px;
|
|
|
- --zm-table-header-font-size: 18px;
|
|
|
- --zm-table-header-font-weight: 800;
|
|
|
- --zm-table-row-font-weight: 800;
|
|
|
- --zm-table-header-text-color: #333;
|
|
|
- --zm-table-border-color: #cbd5e1;
|
|
|
- --zm-table-header-border-color: #c2ccda;
|
|
|
- --zm-table-row-border-color: #d4dce8;
|
|
|
-
|
|
|
- :deep(.header-wrapper) {
|
|
|
- height: 20px;
|
|
|
- justify-content: center !important;
|
|
|
- text-align: center;
|
|
|
-
|
|
|
- .truncate {
|
|
|
- width: 100%;
|
|
|
- height: 20px;
|
|
|
- text-align: center;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- :deep(.el-table__body .el-table__cell) {
|
|
|
- height: auto;
|
|
|
- padding-top: 7px;
|
|
|
- padding-bottom: 7px;
|
|
|
- vertical-align: middle;
|
|
|
-
|
|
|
- .cell {
|
|
|
- line-height: 1.5;
|
|
|
- word-break: break-word;
|
|
|
- white-space: pre-wrap;
|
|
|
- overflow-wrap: anywhere;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- :deep(.el-table__body .el-table__row) {
|
|
|
- height: auto;
|
|
|
- }
|
|
|
-
|
|
|
- :deep(.el-table__body .el-table__row > .el-table__cell) {
|
|
|
- min-height: 40px;
|
|
|
- }
|
|
|
-
|
|
|
- :deep(.meeting-table__cell) {
|
|
|
- .cell {
|
|
|
- word-break: break-word;
|
|
|
- white-space: pre-wrap;
|
|
|
- overflow-wrap: anywhere;
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-table__actions {
|
|
|
- display: flex;
|
|
|
- width: 100%;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- gap: 8px;
|
|
|
-
|
|
|
- :deep(.el-button) {
|
|
|
- margin-left: 0;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-detail-table-view {
|
|
|
- display: block;
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-detail-card-view {
|
|
|
- display: none;
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-detail-card {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 14px;
|
|
|
- padding: 12px;
|
|
|
- padding-bottom: 4px;
|
|
|
- background: var(--el-bg-color);
|
|
|
- border: 1px solid var(--el-border-color-lighter);
|
|
|
- border-radius: 8px;
|
|
|
- box-shadow: 0 2px 8px rgb(15 23 42 / 6%);
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-detail-card__header,
|
|
|
-.meeting-detail-card__actions {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
- gap: 12px;
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-detail-card__title {
|
|
|
- display: flex;
|
|
|
- min-width: 0;
|
|
|
- flex-direction: column;
|
|
|
- gap: 4px;
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-detail-card__title span,
|
|
|
-.meeting-detail-card__company span,
|
|
|
-.meeting-detail-card__field span {
|
|
|
- font-size: 12px;
|
|
|
- line-height: 1.4;
|
|
|
- color: var(--el-text-color-secondary);
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-detail-card__title strong {
|
|
|
- overflow: hidden;
|
|
|
- font-size: 16px;
|
|
|
- line-height: 1.35;
|
|
|
- color: var(--el-text-color-primary);
|
|
|
- text-overflow: ellipsis;
|
|
|
- white-space: nowrap;
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-detail-card__company {
|
|
|
- display: flex;
|
|
|
- min-width: 0;
|
|
|
- flex-direction: column;
|
|
|
- gap: 4px;
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-detail-card__company strong {
|
|
|
- min-width: 0;
|
|
|
- font-size: 13px;
|
|
|
- font-weight: 700;
|
|
|
- line-height: 1.5;
|
|
|
- color: var(--el-text-color-primary);
|
|
|
- overflow-wrap: anywhere;
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-detail-card__group {
|
|
|
- padding-top: 12px;
|
|
|
- border-top: 1px solid var(--el-border-color-lighter);
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-detail-card__group h4 {
|
|
|
- margin: 0 0 10px;
|
|
|
- font-size: 14px;
|
|
|
- font-weight: 700;
|
|
|
- color: var(--el-text-color-primary);
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-detail-card__fields {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
- gap: 10px 12px;
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-detail-card__field {
|
|
|
- display: flex;
|
|
|
- min-width: 0;
|
|
|
- flex-direction: column;
|
|
|
- gap: 4px;
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-detail-card__field strong {
|
|
|
- min-width: 0;
|
|
|
- font-size: 13px;
|
|
|
- font-weight: 600;
|
|
|
- line-height: 1.5;
|
|
|
- color: var(--el-text-color-primary);
|
|
|
- overflow-wrap: anywhere;
|
|
|
- white-space: pre-wrap;
|
|
|
-}
|
|
|
-
|
|
|
-.meeting-detail-card__actions {
|
|
|
- justify-content: flex-end;
|
|
|
- padding-top: 2px;
|
|
|
- border-top: 1px solid var(--el-border-color-lighter);
|
|
|
-}
|
|
|
-
|
|
|
-@media (width <= 960px) {
|
|
|
- .meeting-section__top {
|
|
|
- grid-template-columns: 1fr;
|
|
|
- gap: 12px;
|
|
|
- }
|
|
|
-
|
|
|
- .meeting-section__title {
|
|
|
- padding-top: 0;
|
|
|
- line-height: 24px;
|
|
|
- transform: translateY(0);
|
|
|
- }
|
|
|
-
|
|
|
- .meeting-section__grid {
|
|
|
- grid-template-columns: 1fr;
|
|
|
- }
|
|
|
-
|
|
|
- .meeting-summary-strip {
|
|
|
- grid-template-columns: 1fr;
|
|
|
- }
|
|
|
-
|
|
|
- .meeting-summary-strip__title {
|
|
|
- padding-right: 0;
|
|
|
- padding-bottom: 12px;
|
|
|
- border-right: 0;
|
|
|
- border-bottom: 1px solid #dbeafe;
|
|
|
- }
|
|
|
-
|
|
|
- .meeting-summary-strip__grid {
|
|
|
- grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-@media (width <= 768px) {
|
|
|
- .meeting-detail-table-view {
|
|
|
- display: none;
|
|
|
- }
|
|
|
-
|
|
|
- .meeting-detail-card-view {
|
|
|
- display: flex;
|
|
|
- min-height: 160px;
|
|
|
- flex-direction: column;
|
|
|
- gap: 12px;
|
|
|
- }
|
|
|
-
|
|
|
- .meeting-summary-strip__grid,
|
|
|
- .meeting-detail-card__fields {
|
|
|
- grid-template-columns: 1fr;
|
|
|
- }
|
|
|
-}
|
|
|
-</style>
|