|
@@ -1,14 +1,15 @@
|
|
|
<script lang="ts" setup>
|
|
<script lang="ts" setup>
|
|
|
import type { FormInstance, FormRules } from 'element-plus'
|
|
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 { OperationMeetingApi } from '@/api/pms/meeting'
|
|
|
-import { useDebounceFn, useWindowSize } from '@vueuse/core'
|
|
|
|
|
|
|
+import { useWindowSize } from '@vueuse/core'
|
|
|
|
|
|
|
|
interface Props {
|
|
interface Props {
|
|
|
visible: boolean
|
|
visible: boolean
|
|
|
detail?: DetailItem
|
|
detail?: DetailItem
|
|
|
type: 'create' | 'edit' | 'view'
|
|
type: 'create' | 'edit' | 'view'
|
|
|
formType: 'create' | 'edit'
|
|
formType: 'create' | 'edit'
|
|
|
|
|
+ extProperties?: ExtPropertyItem[]
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
interface ProjectNameSuggestion {
|
|
interface ProjectNameSuggestion {
|
|
@@ -27,12 +28,11 @@ const emits = defineEmits<{
|
|
|
const { width } = useWindowSize()
|
|
const { width } = useWindowSize()
|
|
|
const detailFormRef = ref<FormInstance>()
|
|
const detailFormRef = ref<FormInstance>()
|
|
|
const detailForm = ref<DetailItem>(createDetailItem())
|
|
const detailForm = ref<DetailItem>(createDetailItem())
|
|
|
-const previousWorkPlanQueryKey = ref('')
|
|
|
|
|
-const equipmentUtilizationRateRequired = ref(true)
|
|
|
|
|
|
|
|
|
|
const detailDrawerSize = computed(() => (width.value <= 768 ? '100%' : '50%'))
|
|
const detailDrawerSize = computed(() => (width.value <= 768 ? '100%' : '50%'))
|
|
|
|
|
+const isReadonly = computed(() => props.type === 'view')
|
|
|
const detailDrawerTitle = computed(() => {
|
|
const detailDrawerTitle = computed(() => {
|
|
|
- if (props.type === 'view') {
|
|
|
|
|
|
|
+ if (isReadonly.value) {
|
|
|
return '查看会议明细'
|
|
return '查看会议明细'
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -44,23 +44,42 @@ const projectNameSuggestions = computed<ProjectNameSuggestion[]>(() =>
|
|
|
projectNameOptions.value.map((item) => ({ value: item }))
|
|
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 {
|
|
function createDetailItem(): DetailItem {
|
|
|
return {
|
|
return {
|
|
|
raw: {},
|
|
raw: {},
|
|
|
projectName: '',
|
|
projectName: '',
|
|
|
currentRevenue: undefined,
|
|
currentRevenue: undefined,
|
|
|
cumulativeRevenue: undefined,
|
|
cumulativeRevenue: undefined,
|
|
|
|
|
+ beforeRevenue: undefined,
|
|
|
currentOnAccount: undefined,
|
|
currentOnAccount: undefined,
|
|
|
cumulativeOnAccount: undefined,
|
|
cumulativeOnAccount: undefined,
|
|
|
|
|
+ beforeOnAccount: undefined,
|
|
|
currentPayment: undefined,
|
|
currentPayment: undefined,
|
|
|
cumulativePayment: undefined,
|
|
cumulativePayment: undefined,
|
|
|
|
|
+ beforePayment: undefined,
|
|
|
plannedWorkload: '',
|
|
plannedWorkload: '',
|
|
|
actualCompletion: '',
|
|
actualCompletion: '',
|
|
|
equipmentUtilizationRate: undefined,
|
|
equipmentUtilizationRate: undefined,
|
|
|
keyWorkCompletion: '',
|
|
keyWorkCompletion: '',
|
|
|
problemsAnalysis: '',
|
|
problemsAnalysis: '',
|
|
|
|
|
+ qhse: '',
|
|
|
nextPlannedWorkload: '',
|
|
nextPlannedWorkload: '',
|
|
|
- priorityTasks: ''
|
|
|
|
|
|
|
+ priorityTasks: '',
|
|
|
|
|
+ extProperty: cloneExtProperties(props.extProperties)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -69,19 +88,108 @@ const cloneDetailItem = (data?: Partial<DetailItem>): DetailItem => ({
|
|
|
projectName: data?.projectName || '',
|
|
projectName: data?.projectName || '',
|
|
|
currentRevenue: data?.currentRevenue,
|
|
currentRevenue: data?.currentRevenue,
|
|
|
cumulativeRevenue: data?.cumulativeRevenue,
|
|
cumulativeRevenue: data?.cumulativeRevenue,
|
|
|
|
|
+ beforeRevenue: data?.beforeRevenue,
|
|
|
currentOnAccount: data?.currentOnAccount,
|
|
currentOnAccount: data?.currentOnAccount,
|
|
|
cumulativeOnAccount: data?.cumulativeOnAccount,
|
|
cumulativeOnAccount: data?.cumulativeOnAccount,
|
|
|
|
|
+ beforeOnAccount: data?.beforeOnAccount,
|
|
|
currentPayment: data?.currentPayment,
|
|
currentPayment: data?.currentPayment,
|
|
|
cumulativePayment: data?.cumulativePayment,
|
|
cumulativePayment: data?.cumulativePayment,
|
|
|
|
|
+ beforePayment: data?.beforePayment,
|
|
|
plannedWorkload: data?.plannedWorkload || '',
|
|
plannedWorkload: data?.plannedWorkload || '',
|
|
|
actualCompletion: data?.actualCompletion || '',
|
|
actualCompletion: data?.actualCompletion || '',
|
|
|
equipmentUtilizationRate: data?.equipmentUtilizationRate,
|
|
equipmentUtilizationRate: data?.equipmentUtilizationRate,
|
|
|
keyWorkCompletion: data?.keyWorkCompletion || '',
|
|
keyWorkCompletion: data?.keyWorkCompletion || '',
|
|
|
problemsAnalysis: data?.problemsAnalysis || '',
|
|
problemsAnalysis: data?.problemsAnalysis || '',
|
|
|
|
|
+ qhse: data?.qhse || '',
|
|
|
nextPlannedWorkload: data?.nextPlannedWorkload || '',
|
|
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) => [
|
|
const requiredTextRule = (message: string) => [
|
|
|
{
|
|
{
|
|
|
required: true,
|
|
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>({
|
|
const detailRules = reactive<FormRules>({
|
|
|
projectName: requiredTextRule('请选择或者输入项目名称'),
|
|
projectName: requiredTextRule('请选择或者输入项目名称'),
|
|
|
currentRevenue: nonNegativeNumberRule('请输入收入-本期', '收入-本期'),
|
|
currentRevenue: nonNegativeNumberRule('请输入收入-本期', '收入-本期'),
|
|
@@ -144,107 +226,220 @@ const detailRules = reactive<FormRules>({
|
|
|
cumulativeOnAccount: nonNegativeNumberRule('请输入挂帐-累计', '挂帐-累计'),
|
|
cumulativeOnAccount: nonNegativeNumberRule('请输入挂帐-累计', '挂帐-累计'),
|
|
|
currentPayment: nonNegativeNumberRule('请输入回款-本期', '回款-本期'),
|
|
currentPayment: nonNegativeNumberRule('请输入回款-本期', '回款-本期'),
|
|
|
cumulativePayment: nonNegativeNumberRule('请输入回款-累计', '回款-累计'),
|
|
cumulativePayment: nonNegativeNumberRule('请输入回款-累计', '回款-累计'),
|
|
|
- plannedWorkload: requiredTextRule('请输入计划工作量'),
|
|
|
|
|
- actualCompletion: requiredTextRule('请输入实际完成'),
|
|
|
|
|
- equipmentUtilizationRate: getEquipmentUtilizationRateRules(),
|
|
|
|
|
keyWorkCompletion: requiredTextRule('请输入重点工作及完成情况'),
|
|
keyWorkCompletion: requiredTextRule('请输入重点工作及完成情况'),
|
|
|
problemsAnalysis: requiredTextRule('请输入存在问题及分析'),
|
|
problemsAnalysis: requiredTextRule('请输入存在问题及分析'),
|
|
|
|
|
+ qhse: requiredTextRule('请输入设备、安全管理工作'),
|
|
|
nextPlannedWorkload: requiredTextRule('请输入下期计划工作量'),
|
|
nextPlannedWorkload: requiredTextRule('请输入下期计划工作量'),
|
|
|
priorityTasks: 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
|
|
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) => {
|
|
const handleVisibleChange = (visible: boolean) => {
|
|
|
emits('update:visible', visible)
|
|
emits('update:visible', visible)
|
|
|
|
|
|
|
|
if (!visible) {
|
|
if (!visible) {
|
|
|
detailForm.value = createDetailItem()
|
|
detailForm.value = createDetailItem()
|
|
|
- previousWorkPlanQueryKey.value = ''
|
|
|
|
|
nextTick(() => detailFormRef.value?.clearValidate())
|
|
nextTick(() => detailFormRef.value?.clearValidate())
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const saveDetailItem = async () => {
|
|
const saveDetailItem = async () => {
|
|
|
- if (props.type === 'view' || !detailFormRef.value) return
|
|
|
|
|
|
|
+ if (isReadonly.value || !detailFormRef.value) return
|
|
|
|
|
|
|
|
const valid = await detailFormRef.value.validate().catch(() => false)
|
|
const valid = await detailFormRef.value.validate().catch(() => false)
|
|
|
|
|
|
|
@@ -268,7 +463,6 @@ watch(
|
|
|
if (!visible) return
|
|
if (!visible) return
|
|
|
|
|
|
|
|
detailForm.value = cloneDetailItem(props.detail)
|
|
detailForm.value = cloneDetailItem(props.detail)
|
|
|
- previousWorkPlanQueryKey.value = ''
|
|
|
|
|
nextTick(() => detailFormRef.value?.clearValidate())
|
|
nextTick(() => detailFormRef.value?.clearValidate())
|
|
|
},
|
|
},
|
|
|
{ immediate: true }
|
|
{ immediate: true }
|
|
@@ -276,7 +470,6 @@ watch(
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
onMounted(() => {
|
|
|
loadProjectNameOptions()
|
|
loadProjectNameOptions()
|
|
|
- loadEquipmentUtilizationRateRule()
|
|
|
|
|
})
|
|
})
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
@@ -291,8 +484,7 @@ onMounted(() => {
|
|
|
:show-close="false"
|
|
:show-close="false"
|
|
|
header-class="mb-0! p-4!"
|
|
header-class="mb-0! p-4!"
|
|
|
body-class="bg-gray-100"
|
|
body-class="bg-gray-100"
|
|
|
- footer-class="p-4!"
|
|
|
|
|
- >
|
|
|
|
|
|
|
+ footer-class="p-4!">
|
|
|
<template #header>
|
|
<template #header>
|
|
|
<div class="flex items-center">
|
|
<div class="flex items-center">
|
|
|
<span class="font-bold text-xl">{{ detailDrawerTitle }}</span>
|
|
<span class="font-bold text-xl">{{ detailDrawerTitle }}</span>
|
|
@@ -305,10 +497,9 @@ onMounted(() => {
|
|
|
size="default"
|
|
size="default"
|
|
|
:model="detailForm"
|
|
:model="detailForm"
|
|
|
:rules="detailRules"
|
|
:rules="detailRules"
|
|
|
- :disabled="type === 'view'"
|
|
|
|
|
|
|
+ :disabled="isReadonly"
|
|
|
scroll-to-error
|
|
scroll-to-error
|
|
|
- require-asterisk-position="right"
|
|
|
|
|
- >
|
|
|
|
|
|
|
+ require-asterisk-position="right">
|
|
|
<section class="detail-section">
|
|
<section class="detail-section">
|
|
|
<div class="detail-section__grid detail-section__grid--single">
|
|
<div class="detail-section__grid detail-section__grid--single">
|
|
|
<el-form-item label="项目名称" prop="projectName">
|
|
<el-form-item label="项目名称" prop="projectName">
|
|
@@ -318,10 +509,7 @@ onMounted(() => {
|
|
|
placeholder="请选择或输入项目名称"
|
|
placeholder="请选择或输入项目名称"
|
|
|
clearable
|
|
clearable
|
|
|
:fetch-suggestions="queryProjectNameSearch"
|
|
:fetch-suggestions="queryProjectNameSearch"
|
|
|
- :trigger-on-focus="true"
|
|
|
|
|
- @change="handleProjectNameComplete"
|
|
|
|
|
- @select="handleProjectNameSelect"
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ :trigger-on-focus="true" />
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
</div>
|
|
</div>
|
|
|
</section>
|
|
</section>
|
|
@@ -334,78 +522,71 @@ onMounted(() => {
|
|
|
v-model="detailForm.currentRevenue"
|
|
v-model="detailForm.currentRevenue"
|
|
|
class="w-full!"
|
|
class="w-full!"
|
|
|
:controls="false"
|
|
:controls="false"
|
|
|
- :precision="2"
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ :precision="2" />
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
<el-form-item label="收入-累计" prop="cumulativeRevenue">
|
|
<el-form-item label="收入-累计" prop="cumulativeRevenue">
|
|
|
<el-input-number
|
|
<el-input-number
|
|
|
v-model="detailForm.cumulativeRevenue"
|
|
v-model="detailForm.cumulativeRevenue"
|
|
|
class="w-full!"
|
|
class="w-full!"
|
|
|
:controls="false"
|
|
:controls="false"
|
|
|
- :precision="2"
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ :precision="2" />
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
<el-form-item label="挂帐-本期" prop="currentOnAccount">
|
|
<el-form-item label="挂帐-本期" prop="currentOnAccount">
|
|
|
<el-input-number
|
|
<el-input-number
|
|
|
v-model="detailForm.currentOnAccount"
|
|
v-model="detailForm.currentOnAccount"
|
|
|
class="w-full!"
|
|
class="w-full!"
|
|
|
:controls="false"
|
|
:controls="false"
|
|
|
- :precision="2"
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ :precision="2" />
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
<el-form-item label="挂帐-累计" prop="cumulativeOnAccount">
|
|
<el-form-item label="挂帐-累计" prop="cumulativeOnAccount">
|
|
|
<el-input-number
|
|
<el-input-number
|
|
|
v-model="detailForm.cumulativeOnAccount"
|
|
v-model="detailForm.cumulativeOnAccount"
|
|
|
class="w-full!"
|
|
class="w-full!"
|
|
|
:controls="false"
|
|
:controls="false"
|
|
|
- :precision="2"
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ :precision="2" />
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
<el-form-item label="回款-本期" prop="currentPayment">
|
|
<el-form-item label="回款-本期" prop="currentPayment">
|
|
|
<el-input-number
|
|
<el-input-number
|
|
|
v-model="detailForm.currentPayment"
|
|
v-model="detailForm.currentPayment"
|
|
|
class="w-full!"
|
|
class="w-full!"
|
|
|
:controls="false"
|
|
:controls="false"
|
|
|
- :precision="2"
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ :precision="2" />
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
<el-form-item label="回款-累计" prop="cumulativePayment">
|
|
<el-form-item label="回款-累计" prop="cumulativePayment">
|
|
|
<el-input-number
|
|
<el-input-number
|
|
|
v-model="detailForm.cumulativePayment"
|
|
v-model="detailForm.cumulativePayment"
|
|
|
class="w-full!"
|
|
class="w-full!"
|
|
|
:controls="false"
|
|
:controls="false"
|
|
|
- :precision="2"
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ :precision="2" />
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
</div>
|
|
</div>
|
|
|
</section>
|
|
</section>
|
|
|
|
|
|
|
|
- <section class="detail-section">
|
|
|
|
|
|
|
+ <section v-if="currentPeriodExtProperties.length" class="detail-section">
|
|
|
<h4 class="detail-section__title">本期生产运行情况</h4>
|
|
<h4 class="detail-section__title">本期生产运行情况</h4>
|
|
|
<div class="detail-section__grid">
|
|
<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
|
|
<el-input-number
|
|
|
- v-model="detailForm.equipmentUtilizationRate"
|
|
|
|
|
|
|
+ v-if="isDoubleExtProperty(item)"
|
|
|
|
|
+ v-model="item.actualValue"
|
|
|
class="w-full!"
|
|
class="w-full!"
|
|
|
:controls="false"
|
|
:controls="false"
|
|
|
|
|
+ :disabled="isReadonly || isEquipmentUtilizationExtProperty(item)"
|
|
|
:precision="2"
|
|
: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>
|
|
</el-form-item>
|
|
|
</div>
|
|
</div>
|
|
|
</section>
|
|
</section>
|
|
@@ -418,16 +599,27 @@ onMounted(() => {
|
|
|
v-model="detailForm.keyWorkCompletion"
|
|
v-model="detailForm.keyWorkCompletion"
|
|
|
type="textarea"
|
|
type="textarea"
|
|
|
:rows="4"
|
|
:rows="4"
|
|
|
- placeholder="请输入重点工作及完成情况"
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ placeholder="请输入重点工作及完成情况" />
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
<el-form-item label="存在问题及分析" prop="problemsAnalysis">
|
|
<el-form-item label="存在问题及分析" prop="problemsAnalysis">
|
|
|
<el-input
|
|
<el-input
|
|
|
v-model="detailForm.problemsAnalysis"
|
|
v-model="detailForm.problemsAnalysis"
|
|
|
type="textarea"
|
|
type="textarea"
|
|
|
:rows="4"
|
|
: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>
|
|
</el-form-item>
|
|
|
</div>
|
|
</div>
|
|
|
</section>
|
|
</section>
|
|
@@ -440,16 +632,37 @@ onMounted(() => {
|
|
|
v-model="detailForm.nextPlannedWorkload"
|
|
v-model="detailForm.nextPlannedWorkload"
|
|
|
type="textarea"
|
|
type="textarea"
|
|
|
:rows="4"
|
|
:rows="4"
|
|
|
- placeholder="请输入下期计划工作量"
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ placeholder="请输入下期计划工作量" />
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
<el-form-item label="重点工作事项" prop="priorityTasks">
|
|
<el-form-item label="重点工作事项" prop="priorityTasks">
|
|
|
<el-input
|
|
<el-input
|
|
|
v-model="detailForm.priorityTasks"
|
|
v-model="detailForm.priorityTasks"
|
|
|
type="textarea"
|
|
type="textarea"
|
|
|
:rows="4"
|
|
: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>
|
|
</el-form-item>
|
|
|
</div>
|
|
</div>
|
|
|
</section>
|
|
</section>
|
|
@@ -457,7 +670,7 @@ onMounted(() => {
|
|
|
|
|
|
|
|
<template #footer>
|
|
<template #footer>
|
|
|
<el-button size="default" @click="handleVisibleChange(false)">取消</el-button>
|
|
<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>
|
|
</el-button>
|
|
|
</template>
|
|
</template>
|