Selaa lähdekoodia

Merge branch 'refactor/meeting'

zhangcl 2 päivää sitten
vanhempi
commit
01760640a7

+ 7 - 0
src/api/pms/meeting/index.ts

@@ -38,6 +38,13 @@ export const OperationMeetingApi = {
     return await request.get({ url: `/pms/iot-operation-meeting/cachedProjects` })
   },
 
+  getCurrentCompanyExtProperties: async () => {
+    return await request.get({
+      url: `/pms/iot-operation-meeting/currentCompanyExtProperties`,
+      hideErrorNotification: true
+    })
+  },
+
   getPreviousWorkPlan: async (params: {
     projectName: string
     id?: string | number

+ 4 - 1
src/config/axios/service.ts

@@ -28,6 +28,9 @@ let isRefreshToken = false
 // 请求白名单,无须token的接口
 const whiteList: string[] = ['/login', '/refresh-token']
 
+const shouldHideErrorNotification = (config?: InternalAxiosRequestConfig | AxiosError['config']) =>
+  Boolean((config as Record<string, unknown> | undefined)?.hideErrorNotification)
+
 // 创建axios实例
 const service: AxiosInstance = axios.create({
   baseURL: base_url, // api 的 base_url
@@ -185,7 +188,7 @@ service.interceptors.response.use(
         // hard coding:忽略这个提示,直接登出
         console.log(msg)
         return handleAuthorized()
-      } else {
+      } else if (!shouldHideErrorNotification(config)) {
         ElNotification.error({ title: msg })
       }
       return Promise.reject('error')

+ 355 - 142
src/views/pms/operation-meeting/components/meeting-detail-drawer.vue

@@ -1,14 +1,15 @@
 <script lang="ts" setup>
 import type { FormInstance, FormRules } from 'element-plus'
-import type { DetailItem } from '../types'
+import type { DetailItem, ExtPropertyItem } from '../types'
 import { OperationMeetingApi } from '@/api/pms/meeting'
-import { useDebounceFn, useWindowSize } from '@vueuse/core'
+import { useWindowSize } from '@vueuse/core'
 
 interface Props {
   visible: boolean
   detail?: DetailItem
   type: 'create' | 'edit' | 'view'
   formType: 'create' | 'edit'
+  extProperties?: ExtPropertyItem[]
 }
 
 interface ProjectNameSuggestion {
@@ -27,12 +28,11 @@ const emits = defineEmits<{
 const { width } = useWindowSize()
 const detailFormRef = ref<FormInstance>()
 const detailForm = ref<DetailItem>(createDetailItem())
-const previousWorkPlanQueryKey = ref('')
-const equipmentUtilizationRateRequired = ref(true)
 
 const detailDrawerSize = computed(() => (width.value <= 768 ? '100%' : '50%'))
+const isReadonly = computed(() => props.type === 'view')
 const detailDrawerTitle = computed(() => {
-  if (props.type === 'view') {
+  if (isReadonly.value) {
     return '查看会议明细'
   }
 
@@ -44,23 +44,42 @@ const projectNameSuggestions = computed<ProjectNameSuggestion[]>(() =>
   projectNameOptions.value.map((item) => ({ value: item }))
 )
 
+const hasExtProperties = computed(() =>
+  Boolean(props.extProperties?.length || detailForm.value.extProperty.length)
+)
+const currentPeriodExtProperties = computed(() =>
+  hasExtProperties.value
+    ? detailForm.value.extProperty.filter((item) => item.defaultValue !== 'next')
+    : []
+)
+const nextPlanExtProperties = computed(() =>
+  hasExtProperties.value
+    ? detailForm.value.extProperty.filter((item) => item.defaultValue === 'next')
+    : []
+)
+
 function createDetailItem(): DetailItem {
   return {
     raw: {},
     projectName: '',
     currentRevenue: undefined,
     cumulativeRevenue: undefined,
+    beforeRevenue: undefined,
     currentOnAccount: undefined,
     cumulativeOnAccount: undefined,
+    beforeOnAccount: undefined,
     currentPayment: undefined,
     cumulativePayment: undefined,
+    beforePayment: undefined,
     plannedWorkload: '',
     actualCompletion: '',
     equipmentUtilizationRate: undefined,
     keyWorkCompletion: '',
     problemsAnalysis: '',
+    qhse: '',
     nextPlannedWorkload: '',
-    priorityTasks: ''
+    priorityTasks: '',
+    extProperty: cloneExtProperties(props.extProperties)
   }
 }
 
@@ -69,19 +88,108 @@ const cloneDetailItem = (data?: Partial<DetailItem>): DetailItem => ({
   projectName: data?.projectName || '',
   currentRevenue: data?.currentRevenue,
   cumulativeRevenue: data?.cumulativeRevenue,
+  beforeRevenue: data?.beforeRevenue,
   currentOnAccount: data?.currentOnAccount,
   cumulativeOnAccount: data?.cumulativeOnAccount,
+  beforeOnAccount: data?.beforeOnAccount,
   currentPayment: data?.currentPayment,
   cumulativePayment: data?.cumulativePayment,
+  beforePayment: data?.beforePayment,
   plannedWorkload: data?.plannedWorkload || '',
   actualCompletion: data?.actualCompletion || '',
   equipmentUtilizationRate: data?.equipmentUtilizationRate,
   keyWorkCompletion: data?.keyWorkCompletion || '',
   problemsAnalysis: data?.problemsAnalysis || '',
+  qhse: data?.qhse || '',
   nextPlannedWorkload: data?.nextPlannedWorkload || '',
-  priorityTasks: data?.priorityTasks || ''
+  priorityTasks: data?.priorityTasks || '',
+  extProperty: mergeExtProperties(props.extProperties || [], data?.extProperty)
 })
 
+function normalizeTextValue(value: unknown) {
+  return String(value ?? '').replace(/\\r\\n|\\n/g, '\n')
+}
+
+function parseNumberValue(value: unknown) {
+  if (value === undefined || value === null || value === '') return undefined
+  const parsed = Number(String(value).replace('%', ''))
+
+  return Number.isNaN(parsed) ? undefined : parsed
+}
+
+function normalizeExtProperty(data?: Partial<ExtPropertyItem>): ExtPropertyItem {
+  const dataType = normalizeTextValue(data?.dataType)
+
+  return {
+    name: normalizeTextValue(data?.name),
+    identifier: normalizeTextValue(data?.identifier),
+    dataType,
+    required: data?.required ?? 0,
+    unit: normalizeTextValue(data?.unit),
+    accessMode: normalizeTextValue(data?.accessMode),
+    defaultValue: normalizeTextValue(data?.defaultValue),
+    maxValue: data?.maxValue,
+    minValue: data?.minValue,
+    sort: parseNumberValue(data?.sort),
+    actualValue: dataType === 'double' ? parseNumberValue(data?.actualValue) : data?.actualValue,
+    dropdownList: data?.dropdownList
+  }
+}
+
+function parseExtPropertyList(data?: ExtPropertyItem[] | unknown): unknown[] {
+  if (Array.isArray(data)) return data
+
+  if (typeof data === 'string') {
+    const text = data.trim()
+    if (!text) return []
+
+    try {
+      return parseExtPropertyList(JSON.parse(text))
+    } catch {
+      return []
+    }
+  }
+
+  if (data && typeof data === 'object') {
+    const values = Object.values(data as Record<string, unknown>)
+
+    return values.every((item) => item && typeof item === 'object') ? values : []
+  }
+
+  return []
+}
+
+function cloneExtProperties(data?: ExtPropertyItem[] | unknown): ExtPropertyItem[] {
+  return parseExtPropertyList(data)
+    .map((item) => normalizeExtProperty(item as Partial<ExtPropertyItem>))
+    .filter((item) => item.identifier)
+}
+
+function mergeExtProperties(
+  defaults: ExtPropertyItem[],
+  current?: ExtPropertyItem[] | unknown
+): ExtPropertyItem[] {
+  const currentItems = cloneExtProperties(current)
+  if (!defaults.length) return currentItems
+
+  const currentMap = new Map(currentItems.map((item) => [item.identifier, item]))
+  const defaultIdentifiers = new Set(defaults.map((item) => item.identifier))
+  const mergedDefaults = defaults.map((item) => {
+    const currentItem = currentMap.get(item.identifier)
+
+    return {
+      ...normalizeExtProperty(item),
+      actualValue:
+        currentItem && 'actualValue' in currentItem ? currentItem.actualValue : item.actualValue
+    }
+  })
+  const extraItems = currentItems.filter((item) => !defaultIdentifiers.has(item.identifier))
+
+  return [...mergedDefaults, ...extraItems].sort(
+    (a, b) => Number(a.sort || 0) - Number(b.sort || 0)
+  )
+}
+
 const requiredTextRule = (message: string) => [
   {
     required: true,
@@ -110,32 +218,6 @@ const nonNegativeNumberRule = (requiredMessage: string, fieldLabel: string) => [
   }
 ]
 
-const numberRangeRule = (requiredMessage: string, fieldLabel: string, min: number, max: number) => [
-  ...requiredNumberRule(requiredMessage),
-  {
-    type: 'number' as const,
-    min,
-    max,
-    message: `${fieldLabel}需在${min}到${max}之间`,
-    trigger: ['blur', 'change']
-  }
-]
-
-const optionalNumberRangeRule = (fieldLabel: string, min: number, max: number) => [
-  {
-    type: 'number' as const,
-    min,
-    max,
-    message: `${fieldLabel}需在${min}到${max}之间`,
-    trigger: ['blur', 'change']
-  }
-]
-
-const getEquipmentUtilizationRateRules = () =>
-  equipmentUtilizationRateRequired.value
-    ? numberRangeRule('请输入设备利用率', '设备利用率', 0, 100)
-    : optionalNumberRangeRule('设备利用率', 0, 100)
-
 const detailRules = reactive<FormRules>({
   projectName: requiredTextRule('请选择或者输入项目名称'),
   currentRevenue: nonNegativeNumberRule('请输入收入-本期', '收入-本期'),
@@ -144,107 +226,220 @@ const detailRules = reactive<FormRules>({
   cumulativeOnAccount: nonNegativeNumberRule('请输入挂帐-累计', '挂帐-累计'),
   currentPayment: nonNegativeNumberRule('请输入回款-本期', '回款-本期'),
   cumulativePayment: nonNegativeNumberRule('请输入回款-累计', '回款-累计'),
-  plannedWorkload: requiredTextRule('请输入计划工作量'),
-  actualCompletion: requiredTextRule('请输入实际完成'),
-  equipmentUtilizationRate: getEquipmentUtilizationRateRules(),
   keyWorkCompletion: requiredTextRule('请输入重点工作及完成情况'),
   problemsAnalysis: requiredTextRule('请输入存在问题及分析'),
+  qhse: requiredTextRule('请输入设备安全管理工作'),
   nextPlannedWorkload: requiredTextRule('请输入下期计划工作量'),
   priorityTasks: requiredTextRule('请输入重点工作事项')
 })
 
-const updateEquipmentUtilizationRateRule = (required: boolean) => {
-  equipmentUtilizationRateRequired.value = required
-  detailRules.equipmentUtilizationRate = getEquipmentUtilizationRateRules()
-  nextTick(() => detailFormRef.value?.clearValidate('equipmentUtilizationRate'))
+const isRequiredExtProperty = (item: ExtPropertyItem) =>
+  item.required === true || item.required === 1 || item.required === '1'
+
+const isDoubleExtProperty = (item: ExtPropertyItem) => item.dataType === 'double'
+
+const normalizeExtPropertyKeyword = (value: unknown) =>
+  String(value ?? '')
+    .toLowerCase()
+    .replace(/[\s%()()]/g, '')
+
+const getExtPropertySearchText = (item: ExtPropertyItem) =>
+  `${normalizeExtPropertyKeyword(item.identifier)}${normalizeExtPropertyKeyword(item.name)}`
+
+const isEquipmentUtilizationExtProperty = (item: ExtPropertyItem) => {
+  const text = getExtPropertySearchText(item)
+
+  return (
+    text.includes('设备利用率') ||
+    text.includes('equipmentutilization') ||
+    text.includes('utilizationrate')
+  )
 }
 
-const loadEquipmentUtilizationRateRule = async () => {
-  updateEquipmentUtilizationRateRule(true)
+const isConstructionEquipmentCountExtProperty = (item: ExtPropertyItem) => {
+  const text = getExtPropertySearchText(item)
 
-  try {
-    const data = await OperationMeetingApi.getMandatoryOrNot()
-    updateEquipmentUtilizationRateRule(data?.mandatory !== false)
-  } catch {
-    updateEquipmentUtilizationRateRule(true)
-  }
+  return (
+    text.includes('施工设备数量') ||
+    text.includes('施工设备数') ||
+    text.includes('constructionequipmentcount') ||
+    text.includes('constructiondevicecount')
+  )
 }
 
-const queryProjectNameSearch = (
-  queryString: string,
-  cb: (results: ProjectNameSuggestion[]) => void
-) => {
-  const keyword = queryString.trim().toLowerCase()
+const isOperatingEquipmentCountExtProperty = (item: ExtPropertyItem) => {
+  const text = getExtPropertySearchText(item)
 
-  if (!keyword) {
-    cb(projectNameSuggestions.value)
-    return
-  }
+  return (
+    text.includes('投运设备数量') ||
+    text.includes('投运设备数') ||
+    text.includes('operatingequipmentcount') ||
+    text.includes('operationequipmentcount') ||
+    text.includes('runningequipmentcount')
+  )
+}
 
-  cb(projectNameSuggestions.value.filter((item) => item.value.toLowerCase().includes(keyword)))
+const equipmentUtilizationExtProperty = computed(() =>
+  currentPeriodExtProperties.value.find(isEquipmentUtilizationExtProperty)
+)
+
+const constructionEquipmentCountExtProperty = computed(() =>
+  currentPeriodExtProperties.value.find(isConstructionEquipmentCountExtProperty)
+)
+
+const operatingEquipmentCountExtProperty = computed(() =>
+  currentPeriodExtProperties.value.find(isOperatingEquipmentCountExtProperty)
+)
+
+const updateEquipmentUtilizationExtProperty = () => {
+  const utilizationItem = equipmentUtilizationExtProperty.value
+  const constructionItem = constructionEquipmentCountExtProperty.value
+  const operatingItem = operatingEquipmentCountExtProperty.value
+  if (!utilizationItem || !constructionItem || !operatingItem) return
+
+  const constructionCount = parseNumberValue(constructionItem.actualValue)
+  const operatingCount = parseNumberValue(operatingItem.actualValue)
+
+  utilizationItem.actualValue =
+    constructionCount === undefined || operatingCount === undefined || operatingCount === 0
+      ? undefined
+      : Number(((constructionCount / operatingCount) * 100).toFixed(2))
+
+  nextTick(() => detailFormRef.value?.clearValidate(getExtPropertyProp(utilizationItem)))
 }
 
-const getDetailRawQueryValue = (key: 'id' | 'meetingId') => {
-  const value = detailForm.value.raw?.[key]
+const hasExtPropertyActualValue = (item: ExtPropertyItem) => {
+  const value = item.actualValue
 
-  if (value === undefined || value === null) {
-    return ''
-  }
+  return typeof value === 'string' ? value.trim() !== '' : value !== undefined && value !== null
+}
+
+const getExtPropertyScope = (item: ExtPropertyItem) =>
+  item.defaultValue === 'next' ? 'next' : 'current'
+
+const getExtPropertyAlternativeGroupKey = (item: ExtPropertyItem) => {
+  const value = String(item.maxValue ?? '').trim()
+
+  if (!value || parseNumberValue(value) !== undefined) return ''
+
+  return value
+}
+
+const getExtPropertyAlternativeGroups = (scope: string) => {
+  const groups = new Map<string, ExtPropertyItem[]>()
+
+  detailForm.value.extProperty.forEach((property) => {
+    if (!isRequiredExtProperty(property) || getExtPropertyScope(property) !== scope) return
+
+    const groupKey = getExtPropertyAlternativeGroupKey(property)
+    if (!groupKey) return
 
-  return typeof value === 'string' || typeof value === 'number' ? value : String(value)
+    groups.set(groupKey, [...(groups.get(groupKey) || []), property])
+  })
+
+  return groups
 }
 
-const updatePlannedWorkloadFromPreviousPlan = (data?: Record<string, unknown>) => {
-  detailForm.value.plannedWorkload = String(data?.nextPlannedWorkload || '')
+const hasCompleteExtPropertyAlternativeGroup = (scope: string) =>
+  Array.from(getExtPropertyAlternativeGroups(scope).values()).some((items) =>
+    items.every(hasExtPropertyActualValue)
+  )
+
+const isAlternativeExtProperty = (item: ExtPropertyItem) => {
+  if (!getExtPropertyAlternativeGroupKey(item)) return false
+
+  return getExtPropertyAlternativeGroups(getExtPropertyScope(item)).size > 1
 }
 
-const loadPreviousWorkPlan = async () => {
-  const projectName = detailForm.value.projectName.trim()
+const getExtPropertyAlternativeProps = (item: ExtPropertyItem) =>
+  Array.from(getExtPropertyAlternativeGroups(getExtPropertyScope(item)).values())
+    .flat()
+    .map((property) => getExtPropertyProp(property))
 
-  if (!projectName) {
-    previousWorkPlanQueryKey.value = ''
+const handleExtPropertyValueChange = (item: ExtPropertyItem) => {
+  updateEquipmentUtilizationExtProperty()
+
+  if (
+    !isAlternativeExtProperty(item) ||
+    !hasCompleteExtPropertyAlternativeGroup(getExtPropertyScope(item))
+  ) {
     return
   }
 
-  const params = {
-    projectName,
-    id: getDetailRawQueryValue('id'),
-    meetingId: getDetailRawQueryValue('meetingId')
-  }
-  const queryKey = JSON.stringify(params)
+  nextTick(() => detailFormRef.value?.clearValidate(getExtPropertyAlternativeProps(item)))
+}
 
-  if (previousWorkPlanQueryKey.value === queryKey) return
+const getExtPropertyLabel = (item: ExtPropertyItem) =>
+  item.unit ? `${item.name}(${item.unit})` : item.name
 
-  previousWorkPlanQueryKey.value = queryKey
+const getExtPropertyProp = (item: ExtPropertyItem) => {
+  const index = detailForm.value.extProperty.findIndex(
+    (property) => property.identifier === item.identifier
+  )
 
-  try {
-    const data = await OperationMeetingApi.getPreviousWorkPlan(params)
-    if (detailForm.value.projectName.trim() !== projectName) return
-    updatePlannedWorkloadFromPreviousPlan(data)
-  } catch {
-    previousWorkPlanQueryKey.value = ''
+  return `extProperty.${Math.max(index, 0)}.actualValue`
+}
+
+const getExtPropertyRules = (item: ExtPropertyItem) => {
+  if (!isRequiredExtProperty(item)) return []
+
+  if (isAlternativeExtProperty(item)) {
+    return [
+      {
+        validator: (_rule, _value, callback) => {
+          if (hasCompleteExtPropertyAlternativeGroup(getExtPropertyScope(item))) {
+            callback()
+            return
+          }
+
+          callback(new Error('请至少完整填写一种工作量'))
+        },
+        trigger: ['blur', 'change']
+      }
+    ]
   }
+
+  return isDoubleExtProperty(item)
+    ? requiredNumberRule(`请输入${item.name}`)
+    : requiredTextRule(`请输入${item.name}`)
 }
 
-const handleProjectNameComplete = useDebounceFn(loadPreviousWorkPlan, 300)
+const queryProjectNameSearch = (
+  queryString: string,
+  cb: (results: ProjectNameSuggestion[]) => void
+) => {
+  const keyword = queryString.trim().toLowerCase()
 
-const handleProjectNameSelect = (item: ProjectNameSuggestion) => {
-  detailForm.value.projectName = item.value
-  void handleProjectNameComplete()
+  if (!keyword) {
+    cb(projectNameSuggestions.value)
+    return
+  }
+
+  cb(projectNameSuggestions.value.filter((item) => item.value.toLowerCase().includes(keyword)))
 }
 
+watch(
+  () =>
+    [
+      constructionEquipmentCountExtProperty.value?.actualValue,
+      operatingEquipmentCountExtProperty.value?.actualValue,
+      equipmentUtilizationExtProperty.value?.identifier
+    ] as const,
+  () => updateEquipmentUtilizationExtProperty(),
+  { immediate: true }
+)
+
 const handleVisibleChange = (visible: boolean) => {
   emits('update:visible', visible)
 
   if (!visible) {
     detailForm.value = createDetailItem()
-    previousWorkPlanQueryKey.value = ''
     nextTick(() => detailFormRef.value?.clearValidate())
   }
 }
 
 const saveDetailItem = async () => {
-  if (props.type === 'view' || !detailFormRef.value) return
+  if (isReadonly.value || !detailFormRef.value) return
 
   const valid = await detailFormRef.value.validate().catch(() => false)
 
@@ -268,7 +463,6 @@ watch(
     if (!visible) return
 
     detailForm.value = cloneDetailItem(props.detail)
-    previousWorkPlanQueryKey.value = ''
     nextTick(() => detailFormRef.value?.clearValidate())
   },
   { immediate: true }
@@ -276,7 +470,6 @@ watch(
 
 onMounted(() => {
   loadProjectNameOptions()
-  loadEquipmentUtilizationRateRule()
 })
 </script>
 
@@ -291,8 +484,7 @@ onMounted(() => {
     :show-close="false"
     header-class="mb-0! p-4!"
     body-class="bg-gray-100"
-    footer-class="p-4!"
-  >
+    footer-class="p-4!">
     <template #header>
       <div class="flex items-center">
         <span class="font-bold text-xl">{{ detailDrawerTitle }}</span>
@@ -305,10 +497,9 @@ onMounted(() => {
       size="default"
       :model="detailForm"
       :rules="detailRules"
-      :disabled="type === 'view'"
+      :disabled="isReadonly"
       scroll-to-error
-      require-asterisk-position="right"
-    >
+      require-asterisk-position="right">
       <section class="detail-section">
         <div class="detail-section__grid detail-section__grid--single">
           <el-form-item label="项目名称" prop="projectName">
@@ -318,10 +509,7 @@ onMounted(() => {
               placeholder="请选择或输入项目名称"
               clearable
               :fetch-suggestions="queryProjectNameSearch"
-              :trigger-on-focus="true"
-              @change="handleProjectNameComplete"
-              @select="handleProjectNameSelect"
-            />
+              :trigger-on-focus="true" />
           </el-form-item>
         </div>
       </section>
@@ -334,78 +522,71 @@ onMounted(() => {
               v-model="detailForm.currentRevenue"
               class="w-full!"
               :controls="false"
-              :precision="2"
-            />
+              :precision="2" />
           </el-form-item>
           <el-form-item label="收入-累计" prop="cumulativeRevenue">
             <el-input-number
               v-model="detailForm.cumulativeRevenue"
               class="w-full!"
               :controls="false"
-              :precision="2"
-            />
+              :precision="2" />
           </el-form-item>
           <el-form-item label="挂帐-本期" prop="currentOnAccount">
             <el-input-number
               v-model="detailForm.currentOnAccount"
               class="w-full!"
               :controls="false"
-              :precision="2"
-            />
+              :precision="2" />
           </el-form-item>
           <el-form-item label="挂帐-累计" prop="cumulativeOnAccount">
             <el-input-number
               v-model="detailForm.cumulativeOnAccount"
               class="w-full!"
               :controls="false"
-              :precision="2"
-            />
+              :precision="2" />
           </el-form-item>
           <el-form-item label="回款-本期" prop="currentPayment">
             <el-input-number
               v-model="detailForm.currentPayment"
               class="w-full!"
               :controls="false"
-              :precision="2"
-            />
+              :precision="2" />
           </el-form-item>
           <el-form-item label="回款-累计" prop="cumulativePayment">
             <el-input-number
               v-model="detailForm.cumulativePayment"
               class="w-full!"
               :controls="false"
-              :precision="2"
-            />
+              :precision="2" />
           </el-form-item>
         </div>
       </section>
 
-      <section class="detail-section">
+      <section v-if="currentPeriodExtProperties.length" class="detail-section">
         <h4 class="detail-section__title">本期生产运行情况</h4>
         <div class="detail-section__grid">
-          <el-form-item label="计划工作量" prop="plannedWorkload">
-            <el-input
-              v-model="detailForm.plannedWorkload"
-              type="textarea"
-              :rows="3"
-              placeholder="请输入计划工作量"
-            />
-          </el-form-item>
-          <el-form-item label="实际完成" prop="actualCompletion">
-            <el-input
-              v-model="detailForm.actualCompletion"
-              type="textarea"
-              :rows="3"
-              placeholder="请输入实际完成"
-            />
-          </el-form-item>
-          <el-form-item label="设备利用率(%)" prop="equipmentUtilizationRate">
+          <el-form-item
+            v-for="item in currentPeriodExtProperties"
+            :key="item.identifier"
+            :label="getExtPropertyLabel(item)"
+            :prop="getExtPropertyProp(item)"
+            :rules="getExtPropertyRules(item)">
             <el-input-number
-              v-model="detailForm.equipmentUtilizationRate"
+              v-if="isDoubleExtProperty(item)"
+              v-model="item.actualValue"
               class="w-full!"
               :controls="false"
+              :disabled="isReadonly || isEquipmentUtilizationExtProperty(item)"
               :precision="2"
-            />
+              @change="handleExtPropertyValueChange(item)" />
+            <el-input
+              v-else
+              v-model="item.actualValue"
+              type="textarea"
+              :rows="3"
+              :disabled="isReadonly"
+              :placeholder="`请输入${item.name}`"
+              @input="handleExtPropertyValueChange(item)" />
           </el-form-item>
         </div>
       </section>
@@ -418,16 +599,27 @@ onMounted(() => {
               v-model="detailForm.keyWorkCompletion"
               type="textarea"
               :rows="4"
-              placeholder="请输入重点工作及完成情况"
-            />
+              placeholder="请输入重点工作及完成情况" />
           </el-form-item>
           <el-form-item label="存在问题及分析" prop="problemsAnalysis">
             <el-input
               v-model="detailForm.problemsAnalysis"
               type="textarea"
               :rows="4"
-              placeholder="请输入存在问题及分析"
-            />
+              placeholder="请输入存在问题及分析" />
+          </el-form-item>
+        </div>
+      </section>
+
+      <section class="detail-section">
+        <h4 class="detail-section__title">设备安全管理工作</h4>
+        <div class="detail-section__grid detail-section__grid--single">
+          <el-form-item prop="qhse">
+            <el-input
+              v-model="detailForm.qhse"
+              type="textarea"
+              :rows="4"
+              placeholder="请输入设备安全管理工作" />
           </el-form-item>
         </div>
       </section>
@@ -440,16 +632,37 @@ onMounted(() => {
               v-model="detailForm.nextPlannedWorkload"
               type="textarea"
               :rows="4"
-              placeholder="请输入下期计划工作量"
-            />
+              placeholder="请输入下期计划工作量" />
           </el-form-item>
           <el-form-item label="重点工作事项" prop="priorityTasks">
             <el-input
               v-model="detailForm.priorityTasks"
               type="textarea"
               :rows="4"
-              placeholder="请输入重点工作事项"
-            />
+              placeholder="请输入重点工作事项" />
+          </el-form-item>
+          <el-form-item
+            v-for="item in nextPlanExtProperties"
+            :key="item.identifier"
+            :label="getExtPropertyLabel(item)"
+            :prop="getExtPropertyProp(item)"
+            :rules="getExtPropertyRules(item)">
+            <el-input-number
+              v-if="isDoubleExtProperty(item)"
+              v-model="item.actualValue"
+              class="w-full!"
+              :controls="false"
+              :disabled="isReadonly"
+              :precision="2"
+              @change="handleExtPropertyValueChange(item)" />
+            <el-input
+              v-else
+              v-model="item.actualValue"
+              type="textarea"
+              :rows="4"
+              :disabled="isReadonly"
+              :placeholder="`请输入${item.name}`"
+              @input="handleExtPropertyValueChange(item)" />
           </el-form-item>
         </div>
       </section>
@@ -457,7 +670,7 @@ onMounted(() => {
 
     <template #footer>
       <el-button size="default" @click="handleVisibleChange(false)">取消</el-button>
-      <el-button size="default" v-if="type !== 'view'" type="primary" @click="saveDetailItem">
+      <el-button size="default" v-if="!isReadonly" type="primary" @click="saveDetailItem">
         保存
       </el-button>
     </template>

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 710 - 101
src/views/pms/operation-meeting/components/operation-meeting-content.vue


+ 1 - 0
src/views/pms/operation-meeting/index.vue

@@ -254,6 +254,7 @@ function formatMeetingDate(row: Pick<OperationMeetingListItem, 'meetingDate'>) {
   </div>
   <meeting-form
     v-model:visible="visible"
+    mode="fill"
     :id="currentId"
     :type="type"
     :dept-options="deptOptions"

+ 269 - 54
src/views/pms/operation-meeting/meeting-form.vue

@@ -1,6 +1,14 @@
 <script lang="ts" setup>
 import type { FormInstance, FormRules } from 'element-plus'
-import type { DeptOption, DetailItem, OperationMeeting } from './types'
+import type {
+  DeptOption,
+  DetailItem,
+  ExtPropertyItem,
+  OperationMeeting,
+  OperationMeetingDrawerMode,
+  OperationMeetingForm,
+  OperationMeetingFormItem
+} from './types'
 import MeetingDetailDrawer from './components/meeting-detail-drawer.vue'
 import OperationMeetingContent from './components/operation-meeting-content.vue'
 import { OperationMeetingApi } from '@/api/pms/meeting'
@@ -10,36 +18,45 @@ interface Props {
   visible: boolean
   id?: number
   type: 'create' | 'edit' | 'view'
+  mode?: OperationMeetingDrawerMode
+  meetingSeries?: string
+  year?: string | number
   deptOptions?: DeptOption[]
 }
 
-interface OperationMeetingForm
-  extends Omit<Partial<OperationMeeting>, 'meetingDate' | 'meetingSeries'> {
-  meetingDate?: number | string | Date
-  meetingSeries?: number
-}
-
 const props = withDefaults(defineProps<Props>(), {
   type: 'create',
+  mode: 'fill',
+  meetingSeries: '',
+  year: '',
   deptOptions: () => []
 })
 
 const emits = defineEmits(['update:visible', 'success'])
 const message = useMessage()
 
-const operationMeeting = ref<OperationMeetingForm>({})
 const operationMeetingRef = ref<FormInstance>()
 const loading = ref(false)
 const { width } = useWindowSize()
 
-const detailItems = ref<DetailItem[]>([])
+const extProperties = ref<ExtPropertyItem[]>([])
+const extPropertiesLoaded = ref(false)
+const form = ref<OperationMeetingFormItem[]>([createMeetingFormItem()])
 const detailDrawerVisible = ref(false)
 const detailEditingIndex = ref(-1)
 const detailFormType = ref<'create' | 'edit'>('create')
 const detailForm = ref<DetailItem>(createDetailItem())
+const filterForm = reactive({
+  companyFilterValue: ''
+})
 
 const isMobile = computed(() => width.value <= 768)
 const drawerSize = computed(() => (isMobile.value ? '100%' : '100%'))
+const isSummaryMode = computed(() => props.mode === 'summary')
+const activeFormItem = computed(() => form.value[0] || createMeetingFormItem())
+const operationMeeting = computed(() => activeFormItem.value.meeting)
+const detailItems = computed(() => activeFormItem.value.details)
+const detailDrawerType = computed(() => (isSummaryMode.value ? 'view' : props.type))
 
 function createDetailItem(): DetailItem {
   return {
@@ -47,17 +64,41 @@ function createDetailItem(): DetailItem {
     projectName: '',
     currentRevenue: undefined,
     cumulativeRevenue: undefined,
+    beforeRevenue: undefined,
     currentOnAccount: undefined,
     cumulativeOnAccount: undefined,
+    beforeOnAccount: undefined,
     currentPayment: undefined,
     cumulativePayment: undefined,
+    beforePayment: undefined,
     plannedWorkload: '',
     actualCompletion: '',
     equipmentUtilizationRate: undefined,
     keyWorkCompletion: '',
     problemsAnalysis: '',
+    qhse: '',
     nextPlannedWorkload: '',
-    priorityTasks: ''
+    priorityTasks: '',
+    extProperty: cloneExtProperties(extProperties.value)
+  }
+}
+
+function createMeetingFormItem(
+  meeting: OperationMeetingForm = {},
+  details: DetailItem[] = [],
+  extProperty: ExtPropertyItem[] = [],
+  index = 0
+): OperationMeetingFormItem {
+  const id = meeting.id === undefined || meeting.id === null ? 'new' : String(meeting.id)
+
+  return {
+    key: `${id}-${index}`,
+    meeting,
+    details,
+    extProperty,
+    beforeRevenue: undefined,
+    beforeOnAccount: undefined,
+    beforePayment: undefined
   }
 }
 
@@ -66,17 +107,22 @@ const cloneDetailItem = (data?: Partial<DetailItem>): DetailItem => ({
   projectName: data?.projectName || '',
   currentRevenue: data?.currentRevenue,
   cumulativeRevenue: data?.cumulativeRevenue,
+  beforeRevenue: data?.beforeRevenue,
   currentOnAccount: data?.currentOnAccount,
   cumulativeOnAccount: data?.cumulativeOnAccount,
+  beforeOnAccount: data?.beforeOnAccount,
   currentPayment: data?.currentPayment,
   cumulativePayment: data?.cumulativePayment,
+  beforePayment: data?.beforePayment,
   plannedWorkload: data?.plannedWorkload || '',
   actualCompletion: data?.actualCompletion || '',
   equipmentUtilizationRate: data?.equipmentUtilizationRate,
   keyWorkCompletion: data?.keyWorkCompletion || '',
   problemsAnalysis: data?.problemsAnalysis || '',
+  qhse: data?.qhse || '',
   nextPlannedWorkload: data?.nextPlannedWorkload || '',
-  priorityTasks: data?.priorityTasks || ''
+  priorityTasks: data?.priorityTasks || '',
+  extProperty: cloneExtProperties(data?.extProperty)
 })
 
 const parseNumberValue = (value: unknown) => {
@@ -93,24 +139,143 @@ const parseMeetingSeries = (value: unknown) => {
   return matched ? Number(matched[0]) : undefined
 }
 
+const normalizeTextValue = (value: unknown) => String(value ?? '').replace(/\\r\\n|\\n/g, '\n')
+
+function normalizeExtProperty(data?: Partial<ExtPropertyItem>): ExtPropertyItem {
+  const dataType = normalizeTextValue(data?.dataType)
+
+  return {
+    name: normalizeTextValue(data?.name),
+    identifier: normalizeTextValue(data?.identifier),
+    dataType,
+    required: data?.required ?? 0,
+    unit: normalizeTextValue(data?.unit),
+    accessMode: normalizeTextValue(data?.accessMode),
+    defaultValue: normalizeTextValue(data?.defaultValue),
+    maxValue: data?.maxValue,
+    minValue: data?.minValue,
+    sort: parseNumberValue(data?.sort),
+    actualValue: dataType === 'double' ? parseNumberValue(data?.actualValue) : data?.actualValue,
+    dropdownList: data?.dropdownList
+  }
+}
+
+function parseExtPropertyList(data?: ExtPropertyItem[] | unknown): unknown[] {
+  if (Array.isArray(data)) return data
+
+  if (typeof data === 'string') {
+    const text = data.trim()
+    if (!text) return []
+
+    try {
+      return parseExtPropertyList(JSON.parse(text))
+    } catch {
+      return []
+    }
+  }
+
+  if (data && typeof data === 'object') {
+    const values = Object.values(data as Record<string, unknown>)
+
+    return values.every((item) => item && typeof item === 'object') ? values : []
+  }
+
+  return []
+}
+
+function cloneExtProperties(data?: ExtPropertyItem[] | unknown): ExtPropertyItem[] {
+  return parseExtPropertyList(data)
+    .map((item) => normalizeExtProperty(item as Partial<ExtPropertyItem>))
+    .filter((item) => item.identifier)
+}
+
+function mergeExtProperties(
+  defaults: ExtPropertyItem[],
+  current?: ExtPropertyItem[] | unknown
+): ExtPropertyItem[] {
+  const currentItems = cloneExtProperties(current)
+  if (!defaults.length) return currentItems
+
+  const currentMap = new Map(currentItems.map((item) => [item.identifier, item]))
+  const defaultIdentifiers = new Set(defaults.map((item) => item.identifier))
+  const mergedDefaults = defaults.map((item) => {
+    const currentItem = currentMap.get(item.identifier)
+
+    return {
+      ...normalizeExtProperty(item),
+      actualValue:
+        currentItem && 'actualValue' in currentItem ? currentItem.actualValue : item.actualValue
+    }
+  })
+  const extraItems = currentItems.filter((item) => !defaultIdentifiers.has(item.identifier))
+
+  return [...mergedDefaults, ...extraItems].sort(
+    (a, b) => Number(a.sort || 0) - Number(b.sort || 0)
+  )
+}
+
 const normalizeDetailItem = (data?: Record<string, unknown>): DetailItem => ({
   raw: data || {},
-  projectName: String(data?.projectName || ''),
+  projectName: normalizeTextValue(data?.projectName),
   currentRevenue: parseNumberValue(data?.currentRevenue),
   cumulativeRevenue: parseNumberValue(data?.cumulativeRevenue),
+  beforeRevenue: parseNumberValue(data?.beforeRevenue),
   currentOnAccount: parseNumberValue(data?.currentOnAccount),
   cumulativeOnAccount: parseNumberValue(data?.cumulativeOnAccount),
+  beforeOnAccount: parseNumberValue(data?.beforeOnAccount),
   currentPayment: parseNumberValue(data?.currentPayment),
   cumulativePayment: parseNumberValue(data?.cumulativePayment),
-  plannedWorkload: String(data?.plannedWorkload || ''),
-  actualCompletion: String(data?.actualCompletion || ''),
+  beforePayment: parseNumberValue(data?.beforePayment),
+  plannedWorkload: normalizeTextValue(data?.plannedWorkload),
+  actualCompletion: normalizeTextValue(data?.actualCompletion),
   equipmentUtilizationRate: parseNumberValue(data?.equipmentUtilizationRate),
-  keyWorkCompletion: String(data?.keyWorkCompletion || ''),
-  problemsAnalysis: String(data?.problemsAnalysis || ''),
-  nextPlannedWorkload: String(data?.nextPlannedWorkload || ''),
-  priorityTasks: String(data?.priorityTasks || '')
+  keyWorkCompletion: normalizeTextValue(data?.keyWorkCompletion),
+  problemsAnalysis: normalizeTextValue(data?.problemsAnalysis),
+  qhse: normalizeTextValue(data?.qhse),
+  nextPlannedWorkload: normalizeTextValue(data?.nextPlannedWorkload),
+  priorityTasks: normalizeTextValue(data?.priorityTasks),
+  extProperty: mergeExtProperties(extProperties.value, data?.extProperty)
 })
 
+const normalizeMeetingFormItem = (
+  data: Record<string, unknown> = {},
+  index: number
+): OperationMeetingFormItem => {
+  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: normalizeTextValue(data.companyName),
+      meetingDate: data.meetingDate as number | string | Date | undefined,
+      support: normalizeTextValue(data.support),
+      cumulative: data.cumulative as boolean | undefined,
+      meetingSeries: parseMeetingSeries(data.meetingSeries)
+    },
+    details: details.map((item) => normalizeDetailItem(item as Record<string, unknown>)),
+    extProperty: cloneExtProperties(data.extProperty),
+    beforeRevenue: parseNumberValue(data.beforeRevenue),
+    beforeOnAccount: parseNumberValue(data.beforeOnAccount),
+    beforePayment: parseNumberValue(data.beforePayment)
+  }
+}
+
+const loadCurrentCompanyExtProperties = async () => {
+  if (extPropertiesLoaded.value) return
+
+  try {
+    const data = await OperationMeetingApi.getCurrentCompanyExtProperties()
+    extProperties.value = cloneExtProperties(data)
+  } catch {
+    extProperties.value = []
+  } finally {
+    extPropertiesLoaded.value = true
+  }
+}
+
 const requiredTextRule = (message: string) => [
   {
     required: true,
@@ -140,6 +305,8 @@ const operationMeetingRules = reactive<FormRules>({
 })
 
 const handleAddDetailItem = () => {
+  if (isSummaryMode.value) return
+
   detailFormType.value = 'create'
   detailEditingIndex.value = -1
   detailForm.value = createDetailItem()
@@ -148,13 +315,15 @@ const handleAddDetailItem = () => {
 
 const handleEditDetailItem = (row: DetailItem, index: number) => {
   detailFormType.value = 'edit'
-  detailEditingIndex.value = index
+  detailEditingIndex.value = isSummaryMode.value ? -1 : index
   detailForm.value = cloneDetailItem(row)
   detailDrawerVisible.value = true
 }
 
 const handleDeleteDetailItem = (index: number) => {
-  detailItems.value.splice(index, 1)
+  if (isSummaryMode.value) return
+
+  activeFormItem.value.details.splice(index, 1)
 }
 
 const handleDetailDrawerChange = (visible: boolean) => {
@@ -168,48 +337,79 @@ const handleDetailDrawerChange = (visible: boolean) => {
 }
 
 const updateOperationMeeting = (nextOperationMeeting: OperationMeetingForm) => {
-  operationMeeting.value = nextOperationMeeting
+  activeFormItem.value.meeting = nextOperationMeeting
 }
 
 const saveDetailItem = (item: DetailItem) => {
+  if (isSummaryMode.value) return
+
   const nextItem = cloneDetailItem(item)
+  const currentDetails = activeFormItem.value.details
 
   if (detailEditingIndex.value > -1) {
-    detailItems.value.splice(detailEditingIndex.value, 1, nextItem)
+    currentDetails.splice(detailEditingIndex.value, 1, nextItem)
   } else {
-    detailItems.value.push(nextItem)
+    currentDetails.push(nextItem)
   }
 
   handleDetailDrawerChange(false)
 }
 
 const resetForm = () => {
-  operationMeeting.value = {}
-  detailItems.value = []
+  form.value = isSummaryMode.value ? [] : [createMeetingFormItem()]
+  filterForm.companyFilterValue = ''
   loading.value = false
   handleDetailDrawerChange(false)
   nextTick(() => operationMeetingRef.value?.clearValidate())
 }
 
 const loadOperationMeetingDetail = async (id: number) => {
+  await loadCurrentCompanyExtProperties()
+
   loading.value = true
   try {
     const data = await OperationMeetingApi.getOperationMeeting(id)
 
     if (!props.visible) return
 
-    operationMeeting.value = {
-      id: data?.id,
-      deptId: data?.deptId,
-      companyName: data?.companyName || '',
-      meetingDate: data?.meetingDate,
-      support: data?.support || '',
-      meetingSeries: parseMeetingSeries(data?.meetingSeries)
+    form.value = [normalizeMeetingFormItem((data || {}) as Record<string, unknown>, 0)]
+    nextTick(() => operationMeetingRef.value?.clearValidate())
+  } finally {
+    loading.value = false
+  }
+}
+
+const loadSummaryDetails = async () => {
+  await loadCurrentCompanyExtProperties()
+
+  if (!props.meetingSeries || !props.year) {
+    form.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.mode !== 'summary' ||
+      props.meetingSeries !== meetingSeries ||
+      props.year !== year
+    ) {
+      return
     }
 
-    const details = Array.isArray(data?.details) ? data.details : []
-    detailItems.value = details.map((item) => normalizeDetailItem(item as Record<string, unknown>))
-    nextTick(() => operationMeetingRef.value?.clearValidate())
+    const meetings = Array.isArray(res) ? res : []
+    form.value = meetings.map((item, index) =>
+      normalizeMeetingFormItem(item as Record<string, unknown>, index)
+    )
+    filterForm.companyFilterValue = ''
   } finally {
     loading.value = false
   }
@@ -224,10 +424,18 @@ const handleVisibleChange = (visible: boolean) => {
 }
 
 watch(
-  () => [props.visible, props.id, props.type] as const,
-  ([visible, id, type]) => {
+  () => [props.visible, props.id, props.type, props.mode, props.meetingSeries, props.year] as const,
+  async ([visible, id, type, mode]) => {
     if (!visible) return
 
+    await loadCurrentCompanyExtProperties()
+    if (!props.visible) return
+
+    if (mode === 'summary') {
+      loadSummaryDetails()
+      return
+    }
+
     if (type === 'create') {
       resetForm()
       return
@@ -235,7 +443,10 @@ watch(
 
     if (id) {
       loadOperationMeetingDetail(id)
+      return
     }
+
+    resetForm()
   }
 )
 
@@ -273,7 +484,7 @@ const buildDetailPayload = (item: DetailItem): Omit<DetailItem, 'raw'> => {
 }
 
 const submitForm = async () => {
-  if (props.type === 'view' || !operationMeetingRef.value) return
+  if (isSummaryMode.value || props.type === 'view' || !operationMeetingRef.value) return
 
   const valid = await operationMeetingRef.value.validate().catch(() => false)
 
@@ -307,41 +518,45 @@ const submitForm = async () => {
     body-class="bg-gray-100"
     footer-class="p-4!"
     :size="drawerSize"
-    :with-header="false"
-  >
+    :with-header="false">
     <el-form
       ref="operationMeetingRef"
       label-width="auto"
       label-position="top"
       size="default"
-      :model="operationMeeting"
-      :rules="operationMeetingRules"
+      :model="isSummaryMode ? filterForm : operationMeeting"
+      :rules="isSummaryMode ? undefined : operationMeetingRules"
       v-loading="loading"
       scroll-to-error
-      require-asterisk-position="right"
-    >
+      require-asterisk-position="right">
       <OperationMeetingContent
+        v-if="!isSummaryMode || form.length"
         :meeting="operationMeeting"
         :details="detailItems"
-        :type="type"
+        :type="detailDrawerType"
+        :mode="mode"
+        :meetings="form"
+        :ext-properties="extProperties"
         :dept-options="deptOptions"
         :loading="loading"
+        v-model:company-filter-value="filterForm.companyFilterValue"
         @update:meeting="updateOperationMeeting"
         @add-detail="handleAddDetailItem"
         @edit-detail="handleEditDetailItem"
-        @delete-detail="handleDeleteDetailItem"
-      />
+        @delete-detail="handleDeleteDetailItem" />
+      <el-empty v-else description="暂无汇总会议详情" :image-size="100" />
     </el-form>
 
     <template #footer>
-      <el-button size="default" @click="handleVisibleChange(false)">取消</el-button>
+      <el-button size="default" @click="handleVisibleChange(false)">
+        {{ isSummaryMode ? '关闭' : '取消' }}
+      </el-button>
       <el-button
-        v-if="type !== 'view'"
+        v-if="!isSummaryMode && type !== 'view'"
         size="default"
         type="primary"
         :loading="loading"
-        @click="submitForm"
-      >
+        @click="submitForm">
         保存
       </el-button>
     </template>
@@ -349,10 +564,10 @@ const submitForm = async () => {
     <MeetingDetailDrawer
       :visible="detailDrawerVisible"
       :detail="detailForm"
-      :type="type"
+      :type="detailDrawerType"
       :form-type="detailFormType"
+      :ext-properties="extProperties"
       @update:visible="handleDetailDrawerChange"
-      @save="saveDetailItem"
-    />
+      @save="saveDetailItem" />
   </el-drawer>
 </template>

+ 0 - 1116
src/views/pms/operation-meeting/summary-form.vue

@@ -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>

+ 11 - 16
src/views/pms/operation-meeting/summary.vue

@@ -2,7 +2,7 @@
 import { OperationMeetingApi } from '@/api/pms/meeting'
 import type { DeptOption, OperationMeetingListItem } from './types'
 import { useTableComponents } from '@/components/ZmTable/useTableComponents'
-import SummaryForm from './summary-form.vue'
+import MeetingForm from './meeting-form.vue'
 import dayjs from 'dayjs'
 
 const loading = ref(false)
@@ -69,17 +69,14 @@ onMounted(() => {
 
 <template>
   <div
-    class="operation-meeting-page min-w-0 overflow-x-hidden grid grid-rows-[auto_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
-  >
+    class="operation-meeting-page min-w-0 overflow-x-hidden grid grid-rows-[auto_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
     <el-form
       size="default"
       label-position="left"
-      class="operation-meeting-query min-w-0 overflow-hidden rounded-lg bg-white p-4 shadow dark:bg-[#1d1e1f]"
-    >
+      class="operation-meeting-query min-w-0 overflow-hidden rounded-lg bg-white p-4 shadow dark:bg-[#1d1e1f]">
       <div class="min-w-0 flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-end">
         <div
-          class="operation-meeting-query__actions min-w-0 flex flex-col gap-3 sm:flex-row lg:shrink-0"
-        >
+          class="operation-meeting-query__actions min-w-0 flex flex-col gap-3 sm:flex-row lg:shrink-0">
           <el-button type="primary" class="!ml-0 w-full sm:w-auto" @click="handleQuery">
             搜索
           </el-button>
@@ -89,8 +86,7 @@ onMounted(() => {
     </el-form>
 
     <div
-      class="operation-meeting-data-panel bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col p-4 min-h-0"
-    >
+      class="operation-meeting-data-panel bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col p-4 min-h-0">
       <div class="flex-1 min-h-0 relative">
         <el-auto-resizer class="operation-meeting-table-view absolute">
           <template #default="{ width, height }">
@@ -100,8 +96,7 @@ onMounted(() => {
               :width="width"
               :max-height="height"
               :height="height"
-              show-border
-            >
+              show-border>
               <ZmTableColumn label="会议期次" prop="meetingSeries" width="180" />
               <ZmTableColumn label="公司名称" prop="companyName" :min-width="220" />
               <ZmTableColumn
@@ -109,8 +104,7 @@ onMounted(() => {
                 prop="meetingDate"
                 cover-formatter
                 :real-value="formatMeetingDate"
-                :min-width="160"
-              />
+                :min-width="160" />
               <ZmTableColumn label="操作" width="100" fixed="right">
                 <template #default="{ row }">
                   <el-button size="default" link type="success" @click="handleView(row)">
@@ -156,13 +150,14 @@ onMounted(() => {
     </div>
   </div>
 
-  <summary-form
+  <meeting-form
     v-model:visible="visible"
+    mode="summary"
+    type="view"
     :meeting-series="currentMeetingSeries"
     :year="currentYear"
     :dept-options="deptOptions"
-    @update:visible="visible = $event"
-  />
+    @update:visible="visible = $event" />
 </template>
 
 <style scoped lang="scss">

+ 48 - 0
src/views/pms/operation-meeting/types.ts

@@ -13,22 +13,69 @@ export interface OperationMeeting {
   cumulative: boolean
 }
 
+export interface OperationMeetingForm
+  extends Omit<Partial<OperationMeeting>, 'meetingDate' | 'meetingSeries'> {
+  meetingDate?: number | string | Date
+  meetingSeries?: number
+}
+
+export interface OperationMeetingFormItem {
+  key: string
+  meeting: OperationMeetingForm
+  details: DetailItem[]
+  extProperty: ExtPropertyItem[]
+  beforeRevenue: number | undefined // 上期收入
+  beforeOnAccount: number | undefined // 上期挂帐
+  beforePayment: number | undefined // 上期回款
+}
+
+export type ExtPropertyActualValue = string | number | null | undefined
+
+export interface ExtPropertyItem {
+  name: string
+  identifier: string
+  dataType: string
+  required: number | string | boolean
+  unit?: string
+  accessMode?: string
+  defaultValue?: string
+  maxValue?: string | number
+  minValue?: string | number
+  sort?: number
+  actualValue?: ExtPropertyActualValue
+  dropdownList?: unknown
+}
+
 export interface DetailItem {
   raw: Record<string, unknown>
   projectName: string
   currentRevenue: number | undefined // 本期收入
   cumulativeRevenue: number | undefined // 累计收入
+  beforeRevenue: number | undefined // 上期收入
   currentOnAccount: number | undefined // 本期挂帐
   cumulativeOnAccount: number | undefined // 累计挂帐
+  beforeOnAccount: number | undefined // 上期挂帐
   currentPayment: number | undefined // 本期回款
   cumulativePayment: number | undefined // 累计回款
+  beforePayment: number | undefined // 上期回款
   plannedWorkload: string // 计划工作量
   actualCompletion: string // 实际完成
   equipmentUtilizationRate: number | undefined // 设备利用率
   keyWorkCompletion: string // 重点工作及完成情况
   problemsAnalysis: string // 存在问题及分析
+  qhse: string // 设备安全管理工作
   nextPlannedWorkload: string // 下期计划工作量
   priorityTasks: string // 重点工作事项
+  extProperty: ExtPropertyItem[] // 扩展属性
+}
+
+export interface SummaryDetailItem extends DetailItem {
+  summaryDetailKey: string
+  meetingKey: string
+  companyName: string
+  companyFilterValue: string
+  cumulative?: boolean
+  deptId?: number
 }
 
 export interface OperationMeetingListItem {
@@ -41,3 +88,4 @@ export interface OperationMeetingListItem {
 }
 
 export type OperationMeetingOpenType = 'create' | 'edit' | 'readonly'
+export type OperationMeetingDrawerMode = 'fill' | 'summary'

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä