yanghao 1 týždeň pred
rodič
commit
18bd30d10f

+ 7 - 6
src/api/pms/iotprojecttaskplan/index.ts

@@ -26,9 +26,10 @@ export interface IotProjectTaskPlanPageResult {
   total: number
 }
 
-export interface IotProjectTaskPlanExtProperty {
-  identifier: string
-  [key: string]: unknown
+export interface IotProjectTaskPlanTeamProjectBiz {
+  business: string
+  businessName: string
+  unit: string
 }
 
 export interface IotProjectTaskPlanCreateData {
@@ -92,9 +93,9 @@ export const IotProjectTaskPlanApi = {
       url: `/pms/iot-project-task-plan/get?id=${id}`
     })
   },
-  getPlanExtProperties: async (deptId: number) => {
-    return await request.get<IotProjectTaskPlanExtProperty[]>({
-      url: '/pms/iot-project-task-plan/planExtProperties',
+  getTeamProjectBiz: async (deptId: number) => {
+    return await request.get<IotProjectTaskPlanTeamProjectBiz[]>({
+      url: '/pms/iot-project-task-plan/teamProjectBiz',
       params: { deptId }
     })
   },

+ 16 - 0
src/api/pms/iotsapstock/index.ts

@@ -14,6 +14,7 @@ export interface IotSapStockVO {
   materialGroupId: number // 物料组id
   quantity: number // 数量
   unitPrice: number // 单价
+  currency: string // 币种
   unit: string // 单位
   safetyStock: number // 安全库存
   storageAreaId: number // 库区id
@@ -29,8 +30,23 @@ export interface IotSapStockVO {
   remark: string // 备注
 }
 
+// SAP 库存汇总 VO
+export interface SapStockSummaryVO {
+  currency: string // 币种
+  totalAmount: number // 总金额
+  totalQuantity: number // 总数量
+}
+
 // PMS SAP 库存(通用库存/项目部库存) API
 export const IotSapStockApi = {
+  // 查询 SAP 库存汇总
+  getSapStockSummary: async (params: PageParam) => {
+    return await request.get<SapStockSummaryVO[]>({
+      url: `/pms/iot-sap-stock/sapStockSummary`,
+      params
+    })
+  },
+
   // 查询PMS SAP 库存(通用库存/项目部库存)分页
   getIotSapStockPage: async (params: PageParam) => {
     return await request.get({ url: `/pms/iot-sap-stock/page`, params })

+ 339 - 144
src/views/pms/iotsapstock/index.vue

@@ -1,8 +1,8 @@
 <script setup lang="ts">
 import { useTableComponents } from '@/components/ZmTable/useTableComponents'
-import { IotSapStockApi, IotSapStockVO } from '@/api/pms/iotsapstock'
+import { IotSapStockApi, IotSapStockVO, SapStockSummaryVO } from '@/api/pms/iotsapstock'
 import * as SapOrgApi from '@/api/system/saporg'
-import { dateFormatter } from '@/utils/formatTime'
+import { dateFormatter, rangeShortcuts } from '@/utils/formatTime'
 import download from '@/utils/download'
 import CountTo from '@/components/count-to1.vue'
 
@@ -83,10 +83,10 @@ const queryParams = reactive<QueryParams>({ ...initQuery })
 const queryFormRef = ref()
 const loading = ref(false)
 const exportLoading = ref(false)
+const summaryLoading = ref(false)
 const list = ref<StockRow[]>([])
 const total = ref(0)
-const totalQuantity = ref(0)
-const totalAmount = ref(0)
+const stockSummary = ref<SapStockSummaryVO[]>([])
 const factoryList = ref<SapOrgApi.SapOrgVO[]>([])
 const storageLocationList = ref<SapOrgApi.SapOrgVO[]>([])
 
@@ -96,24 +96,31 @@ const resultOptions = computed(() => [
   { label: '否', value: 'N' }
 ])
 
-const stockStatCards = computed(() => [
-  {
-    label: t('stock.totalQuantity'),
-    value: totalQuantity.value,
-    prefix: '',
-    unit: '',
-    decimals: 0,
-    icon: 'i-tabler:packages text-sky'
-  },
-  {
-    label: t('stock.totalAmount'),
-    value: totalAmount.value,
-    prefix: '¥',
-    unit: '',
-    decimals: 2,
-    icon: 'i-material-symbols:payments-outline-rounded text-emerald'
+const totalQuantity = computed(() =>
+  stockSummary.value.reduce((totalValue, item) => totalValue + Number(item.totalQuantity || 0), 0)
+)
+
+const getCurrencySymbol = (currency: string) => {
+  const currencySymbols: Record<string, string> = {
+    CNY: '¥',
+    USD: '$'
   }
-])
+  const currencyCode = currency.trim().toUpperCase()
+  return currencySymbols[currencyCode] || currencyCode
+}
+
+const getCurrencyName = (currency: string) => {
+  const currencyNames: Record<string, string> = {
+    CNY: '人民币',
+    USD: '美元'
+  }
+  return currencyNames[currency.trim().toUpperCase()] || '其他币种'
+}
+
+const getCurrencyClass = (currency: string) => {
+  const currencyCode = currency.trim().toLowerCase()
+  return ['cny', 'usd'].includes(currencyCode) ? `currency-card--${currencyCode}` : ''
+}
 
 const getList = async () => {
   loading.value = true
@@ -121,15 +128,20 @@ const getList = async () => {
     const data = await IotSapStockApi.getIotSapStockPage(queryParams)
     list.value = data.list
     total.value = data.total
-
-    const firstRow = data.list?.[0]
-    totalQuantity.value = Number(firstRow?.totalQuantity) || 0
-    totalAmount.value = Number(firstRow?.totalAmount) || 0
   } finally {
     loading.value = false
   }
 }
 
+const getSapStockSummary = async () => {
+  summaryLoading.value = true
+  try {
+    stockSummary.value = await IotSapStockApi.getSapStockSummary(queryParams)
+  } finally {
+    summaryLoading.value = false
+  }
+}
+
 const loadSapOrgOptions = async () => {
   const [factories, storageLocations] = await Promise.all([
     SapOrgApi.SapOrgApi.getSimpleSapOrgList(1),
@@ -142,6 +154,7 @@ const loadSapOrgOptions = async () => {
 const handleQuery = () => {
   queryParams.pageNo = 1
   getList()
+  getSapStockSummary()
 }
 
 const resetQuery = () => {
@@ -152,7 +165,8 @@ const resetQuery = () => {
 
 const handleSizeChange = (val: number) => {
   queryParams.pageSize = val
-  handleQuery()
+  queryParams.pageNo = 1
+  getList()
 }
 
 const handleCurrentChange = (val: number) => {
@@ -191,20 +205,19 @@ const handleExport = async () => {
 onMounted(() => {
   loadSapOrgOptions()
   getList()
+  getSapStockSummary()
 })
 </script>
 
 <template>
   <div
-    class="iot-sap-stock-page grid grid-rows-[auto_auto_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
-  >
+    class="iot-sap-stock-page grid grid-rows-[auto_auto_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
     <el-form
       ref="queryFormRef"
       :model="queryParams"
       size="default"
       label-width="82px"
-      class="iot-sap-stock-query bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-6 py-3 min-w-0"
-    >
+      class="iot-sap-stock-query bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-6 py-3 min-w-0">
       <div class="query-row">
         <el-form-item :label="t('workOrderMaterial.factory')" prop="factoryId">
           <el-select
@@ -213,14 +226,12 @@ onMounted(() => {
             filterable
             :placeholder="t('faultForm.choose')"
             class="query-control"
-            @change="selectedFactoryChange"
-          >
+            @change="selectedFactoryChange">
             <el-option
               v-for="item in factoryList"
               :key="item.id"
               :label="item.factoryName"
-              :value="item.id!"
-            />
+              :value="item.id!" />
           </el-select>
         </el-form-item>
         <el-form-item :label="t('workOrderMaterial.storageLocation')" prop="storageLocationId">
@@ -229,14 +240,12 @@ onMounted(() => {
             clearable
             filterable
             :placeholder="t('faultForm.choose')"
-            class="query-control"
-          >
+            class="query-control">
             <el-option
               v-for="item in storageLocationList"
               :key="item.id"
               :label="item.storageLocationName"
-              :value="item.id!"
-            />
+              :value="item.id!" />
           </el-select>
         </el-form-item>
         <el-form-item :label="t('chooseMaintain.materialCode')" prop="materialCode">
@@ -245,8 +254,7 @@ onMounted(() => {
             :placeholder="t('chooseMaintain.materialCode')"
             clearable
             class="query-control"
-            @keyup.enter="handleQuery"
-          />
+            @keyup.enter="handleQuery" />
         </el-form-item>
         <el-form-item :label="t('chooseMaintain.materialName')" prop="materialName">
           <el-input
@@ -254,33 +262,30 @@ onMounted(() => {
             :placeholder="t('chooseMaintain.materialName')"
             clearable
             class="query-control"
-            @keyup.enter="handleQuery"
-          />
+            @keyup.enter="handleQuery" />
         </el-form-item>
         <el-form-item :label="t('chooseMaintain.createTime')" prop="createTime">
           <el-date-picker
             v-model="queryParams.createTime"
             value-format="YYYY-MM-DD HH:mm:ss"
             type="daterange"
+            :shortcuts="rangeShortcuts"
             :start-placeholder="t('info.start')"
             :end-placeholder="t('info.end')"
             :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
-            class="query-control query-control--date"
-          />
+            class="query-control query-control--date" />
         </el-form-item>
         <el-form-item :label="t('route.IsItConfigured')" prop="configFlag" label-width="116px">
           <el-select
             v-model="queryParams.configFlag"
             :placeholder="t('faultForm.choose')"
             clearable
-            class="query-control query-control--small"
-          >
+            class="query-control query-control--small">
             <el-option
               v-for="dict in resultOptions"
               :key="dict.value"
               :label="dict.label"
-              :value="dict.value"
-            />
+              :value="dict.value" />
           </el-select>
         </el-form-item>
       </div>
@@ -300,26 +305,55 @@ onMounted(() => {
       </div>
     </el-form>
 
-    <div class="stock-stat-grid">
-      <div v-for="item in stockStatCards" :key="item.label" class="stock-stat-card group">
-        <div class="relative z-10 flex h-full flex-col justify-center">
-          <span class="stat-label">{{ item.label }}</span>
-          <div class="flex items-baseline gap-1">
-            <span v-if="item.prefix" class="text-[12px] text-gray-400">{{ item.prefix }}</span>
-            <count-to
-              class="stat-value"
-              :start-val="0"
-              :end-val="item.value"
-              :decimals="item.decimals"
-            />
-            <span v-if="item.unit" class="stat-unit">{{ item.unit }}</span>
+    <section class="stock-overview" v-loading="summaryLoading">
+      <div class="quantity-overview">
+        <div class="quantity-icon">
+          <Icon icon="ep:box" class="quantity-icon__glyph" />
+        </div>
+        <div class="quantity-content">
+          <div class="quantity-label">{{ t('stock.totalQuantity') }}</div>
+          <count-to class="quantity-value" :start-val="0" :end-val="totalQuantity" :decimals="2" />
+        </div>
+      </div>
+
+      <div class="currency-overview">
+        <div class="currency-overview__header">
+          <div class="currency-overview__title">
+            <Icon icon="ep:wallet" />
+            <span>库存总金额</span>
           </div>
+          <div class="currency-overview__subtitle">按币种独立核算</div>
         </div>
-        <div class="stat-icon">
-          <div :class="item.icon" class="text-5xl"></div>
+
+        <div v-if="stockSummary.length" class="currency-grid">
+          <div
+            v-for="item in stockSummary"
+            :key="item.currency"
+            class="currency-card"
+            :class="getCurrencyClass(item.currency)">
+            <div class="currency-token">{{ getCurrencySymbol(item.currency) }}</div>
+            <div class="currency-card__content">
+              <div class="currency-card__heading">
+                <span class="currency-name">{{ getCurrencyName(item.currency) }}</span>
+                <span class="currency-code">{{ item.currency }}</span>
+              </div>
+              <div class="currency-card__amount">
+                <span class="currency-symbol">{{ getCurrencySymbol(item.currency) }}</span>
+                <count-to
+                  class="currency-value"
+                  :start-val="0"
+                  :end-val="Number(item.totalAmount) || 0"
+                  :decimals="2" />
+              </div>
+            </div>
+          </div>
+        </div>
+        <div v-else-if="!summaryLoading" class="summary-empty">
+          <Icon icon="ep:coin" />
+          <span>暂无金额汇总数据</span>
         </div>
       </div>
-    </div>
+    </section>
 
     <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col p-4 min-w-0 min-h-0">
       <div class="flex-1 relative min-h-0">
@@ -331,50 +365,25 @@ onMounted(() => {
               :width="width"
               :height="height"
               :max-height="height"
-              show-border
-            >
+              show-border>
               <ZmTableColumn
                 type="index"
                 :label="t('monitor.serial')"
                 :width="70"
                 fixed="left"
-                hide-in-column-settings
-              />
-              <ZmTableColumn
-                prop="factory"
-                :label="t('workOrderMaterial.factory')"
-                min-width="120"
-              />
+                hide-in-column-settings />
+              <ZmTableColumn prop="factory" :label="t('workOrderMaterial.factory')" />
               <ZmTableColumn
                 prop="projectDepartment"
-                :label="t('workOrderMaterial.storageLocation')"
-                min-width="140"
-              />
-              <ZmTableColumn
-                prop="materialCode"
-                :label="t('chooseMaintain.materialCode')"
-                min-width="150"
-              />
-              <ZmTableColumn
-                prop="materialName"
-                :label="t('chooseMaintain.materialName')"
-                min-width="220"
-                align="left"
-              />
-              <ZmTableColumn prop="quantity" :label="t('route.quantity')" min-width="100" />
-              <ZmTableColumn
-                prop="unitPrice"
-                :label="t('workOrderMaterial.unitPrice')"
-                min-width="100"
-              />
-              <ZmTableColumn prop="unit" :label="t('workOrderMaterial.unit')" min-width="90" />
-              <ZmTableColumn prop="safetyStock" :label="t('route.safetyStock')" min-width="120" />
-              <ZmTableColumn
-                prop="createTime"
-                :label="t('chooseMaintain.createTime')"
-                min-width="180"
-                action
-              >
+                :label="t('workOrderMaterial.storageLocation')" />
+              <ZmTableColumn prop="materialCode" :label="t('chooseMaintain.materialCode')" />
+              <ZmTableColumn prop="materialName" :label="t('chooseMaintain.materialName')" />
+              <ZmTableColumn prop="quantity" :label="t('route.quantity')" />
+              <ZmTableColumn prop="unitPrice" :label="t('workOrderMaterial.unitPrice')" />
+              <ZmTableColumn prop="currency" label="币种" />
+              <ZmTableColumn prop="unit" :label="t('workOrderMaterial.unit')" />
+              <ZmTableColumn prop="safetyStock" :label="t('route.safetyStock')" />
+              <ZmTableColumn prop="createTime" :label="t('chooseMaintain.createTime')" action>
                 <template #default="{ row }">
                   {{ dateFormatter(row, null, row.createTime) }}
                 </template>
@@ -395,8 +404,7 @@ onMounted(() => {
           :total="total"
           layout="total, sizes, prev, pager, next, jumper"
           @size-change="handleSizeChange"
-          @current-change="handleCurrentChange"
-        />
+          @current-change="handleCurrentChange" />
       </div>
     </div>
   </div>
@@ -429,67 +437,233 @@ onMounted(() => {
   gap: 12px 18px;
 }
 
-.stat-label {
-  margin-bottom: 2px;
+.stock-overview {
+  position: relative;
+  display: grid;
+  grid-template-columns: minmax(280px, 0.72fr) minmax(0, 1.48fr);
+  min-height: 88px;
+  overflow: hidden;
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 12px;
+  box-shadow: 0 3px 12px rgb(31 45 61 / 7%);
+}
+
+.stock-overview::before {
+  position: absolute;
+  top: 0;
+  right: 0;
+  left: 0;
+  z-index: 2;
+  height: 2px;
+  background: linear-gradient(90deg, var(--el-color-primary), var(--el-color-success));
+  content: '';
+}
+
+.quantity-overview {
+  position: relative;
+  z-index: 1;
+  display: flex;
+  align-items: center;
+  min-width: 0;
+  padding: 12px 22px;
+  background: linear-gradient(90deg, var(--el-color-primary-light-9), transparent 70%);
+  border-right: 1px solid var(--el-border-color-lighter);
+}
+
+.quantity-icon {
+  display: flex;
+  flex: 0 0 auto;
+  align-items: center;
+  justify-content: center;
+  width: 40px;
+  height: 40px;
+  margin-right: 13px;
+  color: #fff;
+  background: linear-gradient(145deg, var(--el-color-primary-light-3), var(--el-color-primary));
+  border-radius: 11px;
+  box-shadow: 0 5px 12px var(--el-color-primary-light-7);
+}
+
+.quantity-icon__glyph {
+  font-size: 21px;
+}
+
+.quantity-content {
+  min-width: 0;
+}
+
+.quantity-label {
   font-size: 11px;
-  font-weight: 500;
-  letter-spacing: 0;
-  color: var(--el-text-color-regular);
+  color: var(--el-text-color-secondary);
 }
 
-.stat-value {
+.quantity-value {
+  display: block;
+  margin-top: 5px;
   font-family: var(--el-font-family);
-  font-size: 18px;
-  font-weight: 700;
+  font-size: 23px;
+  font-weight: 750;
   line-height: 1;
+  letter-spacing: -0.5px;
   color: var(--el-text-color-primary);
 }
 
-.stat-unit {
+.currency-overview {
+  position: relative;
+  z-index: 1;
+  display: flex;
+  align-items: center;
+  min-width: 0;
+  padding: 10px 18px;
+}
+
+.currency-overview__header {
+  flex: 0 0 108px;
+}
+
+.currency-overview__title {
+  display: flex;
+  align-items: center;
+  font-size: 12px;
+  font-weight: 650;
+  color: var(--el-text-color-primary);
+}
+
+.currency-overview__title .el-icon,
+.currency-overview__title > :first-child {
+  margin-right: 6px;
+  color: var(--el-color-success);
+}
+
+.currency-overview__subtitle {
+  margin-top: 5px;
   font-size: 10px;
-  font-weight: 400;
-  color: var(--el-text-color-secondary);
+  color: var(--el-text-color-placeholder);
 }
 
-.stock-stat-grid {
-  display: grid;
-  grid-template-columns: repeat(2, minmax(0, 1fr));
-  gap: 12px;
+.currency-grid {
+  flex: 1 1 auto;
+  display: flex;
+  gap: 8px;
+  min-width: 0;
+  margin-left: 16px;
+  overflow: auto hidden;
 }
 
-.stock-stat-card {
-  position: relative;
-  min-height: 78px;
-  padding: 10px 14px;
-  overflow: hidden;
-  background-color: var(--el-bg-color);
+.currency-card {
+  --currency-color: var(--el-color-warning);
+  --currency-soft: var(--el-color-warning-light-9);
+
+  display: flex;
+  flex: 1 0 210px;
+  align-items: center;
+  min-width: 0;
+  padding: 7px 10px;
+  background: linear-gradient(120deg, var(--currency-soft), var(--el-bg-color) 72%);
   border: 1px solid var(--el-border-color-extra-light);
   border-radius: 8px;
-  box-shadow: var(--el-box-shadow-lighter);
   transition:
-    border-color 0.3s,
-    box-shadow 0.3s;
+    border-color 0.25s ease,
+    box-shadow 0.25s ease,
+    transform 0.25s ease;
 }
 
-.stock-stat-card:hover {
-  border-color: var(--el-color-primary-light-7);
-  box-shadow: var(--el-box-shadow-light);
+.currency-card:hover {
+  border-color: var(--currency-color);
+  box-shadow: 0 4px 10px rgb(31 45 61 / 7%);
 }
 
-.stat-icon {
-  position: absolute;
-  right: -8px;
-  bottom: -12px;
-  opacity: 0.4;
-  transform: rotate(-10deg);
-  transition:
-    transform 0.5s,
-    opacity 0.5s;
+.currency-card--cny {
+  --currency-color: var(--el-color-danger);
+  --currency-soft: var(--el-color-danger-light-9);
+}
+
+.currency-card--usd {
+  --currency-color: var(--el-color-primary);
+  --currency-soft: var(--el-color-primary-light-9);
+}
+
+.currency-token {
+  display: flex;
+  flex: 0 0 auto;
+  align-items: center;
+  justify-content: center;
+  width: 32px;
+  height: 32px;
+  margin-right: 9px;
+  font-size: 15px;
+  font-weight: 750;
+  color: var(--currency-color);
+  background: var(--el-bg-color);
+  border: 1px solid var(--currency-color);
+  border-radius: 9px;
+}
+
+.currency-card__content {
+  min-width: 0;
+}
+
+.currency-card__heading {
+  display: flex;
+  align-items: center;
+}
+
+.currency-name {
+  font-size: 11px;
+  color: var(--el-text-color-secondary);
+}
+
+.currency-code {
+  padding: 2px 5px;
+  margin-left: 6px;
+  font-size: 9px;
+  font-weight: 700;
+  line-height: 1;
+  color: var(--currency-color);
+  background: var(--el-bg-color);
+  border-radius: 3px;
+}
+
+.currency-card__amount {
+  display: flex;
+  align-items: baseline;
+  min-width: 0;
+  margin-top: 4px;
+}
+
+.currency-symbol {
+  margin-right: 4px;
+  font-size: 13px;
+  font-weight: 700;
+  color: var(--currency-color);
+}
+
+.currency-value {
+  font-family: var(--el-font-family);
+  font-size: 18px;
+  font-weight: 750;
+  line-height: 1;
+  letter-spacing: -0.2px;
+  color: var(--el-text-color-primary);
+}
+
+.summary-empty {
+  flex: 1 1 auto;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  min-height: 44px;
+  margin-left: 16px;
+  font-size: 12px;
+  color: var(--el-text-color-placeholder);
+  background: var(--el-fill-color-extra-light);
+  border: 1px dashed var(--el-border-color);
+  border-radius: 8px;
 }
 
-.stock-stat-card:hover .stat-icon {
-  opacity: 0.55;
-  transform: scale(1.08) rotate(0);
+.summary-empty > :first-child {
+  margin-right: 6px;
 }
 
 .query-actions {
@@ -564,6 +738,17 @@ onMounted(() => {
   }
 }
 
+@media (width <= 900px) {
+  .stock-overview {
+    grid-template-columns: minmax(0, 1fr);
+  }
+
+  .quantity-overview {
+    border-right: 0;
+    border-bottom: 1px solid var(--el-border-color-lighter);
+  }
+}
+
 @media (width <= 768px) {
   .iot-sap-stock-query {
     padding: 12px;
@@ -582,8 +767,18 @@ onMounted(() => {
     width: 100%;
   }
 
-  .stock-stat-grid {
-    grid-template-columns: minmax(0, 1fr);
+  .quantity-overview,
+  .currency-overview {
+    padding: 14px;
+  }
+
+  .currency-overview {
+    display: block;
+  }
+
+  .currency-grid {
+    margin-top: 10px;
+    margin-left: 0;
   }
 
   .query-actions :deep(.el-form-item__content) {

+ 28 - 29
src/views/pms/plan-work/fill.vue

@@ -2,9 +2,9 @@
 import {
   IotProjectTaskPlanApi,
   type IotProjectTaskPlanCreateData,
-  type IotProjectTaskPlanExtProperty,
   type IotProjectTaskPlanPageParams,
   type IotProjectTaskPlanPageVO,
+  type IotProjectTaskPlanTeamProjectBiz,
   type IotProjectTaskPlanUpdateData
 } from '@/api/pms/iotprojecttaskplan'
 import * as DeptApi from '@/api/system/dept'
@@ -71,12 +71,15 @@ const BUSINESS_PLAN_CONFIG: Record<string, BusinessPlanConfig> = {
   井下: { field: 'planWellTrips', unit: '井' }
 }
 
-const businessOptions = getStrDictOptions(DICT_TYPE.RQ_IOT_WORKLOAD_PLAN_BIZ)
+const businessDictOptions = getStrDictOptions(DICT_TYPE.RQ_IOT_WORKLOAD_PLAN_BIZ)
 const deptTreeProps = { ...defaultProps, disabled: 'disabled' }
+const teamProjectBizOptions = ref<IotProjectTaskPlanTeamProjectBiz[]>([])
 
 const getBusinessPlanConfig = (business: string) => {
   const businessValue = String(business || '').trim()
-  const businessLabel = businessOptions.find((option) => option.value === businessValue)?.label
+  const businessLabel =
+    teamProjectBizOptions.value.find((option) => option.business === businessValue)?.businessName ||
+    businessDictOptions.find((option) => option.value === businessValue)?.label
   return BUSINESS_PLAN_CONFIG[businessValue] || BUSINESS_PLAN_CONFIG[businessLabel || '']
 }
 
@@ -92,10 +95,9 @@ const createDialogVisible = ref(false)
 const createLoading = ref(false)
 const formMode = ref<FormMode>('create')
 const deptOptionsLoading = ref(false)
-const extPropertiesLoading = ref(false)
+const businessOptionsLoading = ref(false)
 const deptOptions = ref<DeptOption[]>([])
 const flatDeptOptions = ref<DeptOption[]>([])
-const planExtProperties = ref<IotProjectTaskPlanExtProperty[]>([])
 const createFormRef = ref<FormInstance>()
 
 const isReadonly = computed(() => formMode.value === 'view')
@@ -108,15 +110,6 @@ const dialogDescription = computed(() =>
   isReadonly.value ? '查看部门项目业务计划工作量详情' : '维护部门各月份的项目业务计划工作量'
 )
 
-const selectableBusinessOptions = computed(() => {
-  const identifiers = new Set(planExtProperties.value.map((property) => property.identifier))
-
-  return businessOptions.filter((option) => {
-    const config = getBusinessPlanConfig(option.value)
-    return config ? identifiers.has(config.field) : false
-  })
-})
-
 const createInitialForm = (): CreatePlanForm => ({
   id: undefined,
   deptId: undefined,
@@ -166,16 +159,16 @@ const findCompanyId = (selectedDeptId: number) => {
   return undefined
 }
 
-const loadPlanExtProperties = async (selectedDeptId?: number) => {
-  planExtProperties.value = []
+const loadTeamProjectBizOptions = async (selectedDeptId?: number) => {
+  teamProjectBizOptions.value = []
   if (!selectedDeptId) return
 
-  extPropertiesLoading.value = true
+  businessOptionsLoading.value = true
   try {
-    const data = await IotProjectTaskPlanApi.getPlanExtProperties(selectedDeptId)
-    planExtProperties.value = Array.isArray(data) ? data : []
+    const data = await IotProjectTaskPlanApi.getTeamProjectBiz(selectedDeptId)
+    teamProjectBizOptions.value = Array.isArray(data) ? data : []
   } finally {
-    extPropertiesLoading.value = false
+    businessOptionsLoading.value = false
   }
 }
 
@@ -184,7 +177,7 @@ const handleDeptChange = async (selectedDeptId?: number) => {
   createFormData.value.business = ''
   createFormData.value.workloadUnit = ''
   createFormData.value.planWorkload = undefined
-  await loadPlanExtProperties(selectedDeptId)
+  await loadTeamProjectBizOptions(selectedDeptId)
 }
 
 const normalizeStartTime = (value: unknown) => {
@@ -206,7 +199,11 @@ const handleOpenForm = async (mode: FormMode, id?: number) => {
     if (id) {
       const data = await IotProjectTaskPlanApi.get(id)
       const business = String(data.business || '')
+      await loadTeamProjectBizOptions(data.deptId)
       const config = getBusinessPlanConfig(business)
+      const businessOption = teamProjectBizOptions.value.find(
+        (option) => option.business === business
+      )
       const workloadValue = config ? data[config.field] : undefined
 
       createFormData.value = {
@@ -215,11 +212,10 @@ const handleOpenForm = async (mode: FormMode, id?: number) => {
         companyId: data.companyId || findCompanyId(data.deptId),
         startTime: normalizeStartTime(data.startTime),
         business,
-        workloadUnit: data.workloadUnit || config?.unit || '',
+        workloadUnit: data.workloadUnit || businessOption?.unit || config?.unit || '',
         planWorkload:
           workloadValue === null || workloadValue === undefined ? undefined : Number(workloadValue)
       }
-      await loadPlanExtProperties(data.deptId)
     }
 
     await nextTick()
@@ -232,8 +228,11 @@ const handleOpenForm = async (mode: FormMode, id?: number) => {
 const handleOpenCreate = () => handleOpenForm('create')
 
 const handleBusinessChange = () => {
+  const businessOption = teamProjectBizOptions.value.find(
+    (option) => option.business === createFormData.value.business
+  )
   const config = getBusinessPlanConfig(createFormData.value.business)
-  createFormData.value.workloadUnit = config?.unit || ''
+  createFormData.value.workloadUnit = businessOption?.unit || config?.unit || ''
   createFormData.value.planWorkload = undefined
 }
 
@@ -495,16 +494,16 @@ watch(
                 <el-select
                   v-model="createFormData.business"
                   :placeholder="createFormData.deptId ? '请选择项目业务' : '请先选择部门'"
-                  :loading="extPropertiesLoading"
+                  :loading="businessOptionsLoading"
                   :disabled="isReadonly || !createFormData.deptId"
                   clearable
                   class="w-full"
                   @change="handleBusinessChange">
                   <el-option
-                    v-for="option in selectableBusinessOptions"
-                    :key="option.value"
-                    :label="option.label"
-                    :value="option.value" />
+                    v-for="option in teamProjectBizOptions"
+                    :key="option.business"
+                    :label="option.businessName"
+                    :value="option.business" />
                 </el-select>
               </el-form-item>
             </el-col>