فهرست منبع

Merge branch 'feature-hetong' into 'dev'

应急管理物资申领功能

See merge request product-group-fe/sfy-safety-group/sfy-safety!290
毕欣怡 4 ماه پیش
والد
کامیت
20defc8269

+ 127 - 0
src/api/emergency-supplier/index.ts

@@ -10,6 +10,11 @@ import type {
   InventoryTaskListRes,
   InventoryCheckListQuery,
   InventoryCheckListResponse,
+  SupplyRequestListQuery,
+  SupplyRequestListItem,
+  SupplyRequestDetailItem,
+  ReceiveSupplyRequestDetailForm,
+  AddSupplyQuery,
 } from '@/types/emergency-supplier';
 
 /**
@@ -118,6 +123,7 @@ export const saveInventoryTask = (taskName: string, endTime: string) => {
     data: { taskName, endTime },
   });
 };
+
 /**
  * 物资报废
  */
@@ -144,3 +150,124 @@ export const exportDiscardRecord = (startTime: string, endTime: string) => {
     },
   );
 };
+
+/**
+ * 分页查询应急物资申领计划列表
+ */
+export const getSupplyRequestList = (data: QueryPageRequest<SupplyRequestListQuery>) => {
+  return http.request<QueryPageResponse<SupplyRequestListItem>>({
+    url: '/emergencySupplies/queryEmergencySuppliesRequestPlanPage',
+    method: 'post',
+    data,
+  });
+};
+/**
+ * 通过id查询应急物资申领计划信息
+ */
+export const getSupplyRequestInfoById = (planId: number) => {
+  return http.request<SupplyRequestListItem>({
+    url: `/emergencySupplies/queryEmergencySuppliesRequestPlan?planId=${planId}`,
+    method: 'get',
+  });
+};
+/**
+ * 删除应急物资申领计划
+ */
+export const deleteSupplyRequest = (planId: number) => {
+  return http.request({
+    url: `/emergencySupplies/deleteEmergencySuppliesRequestPlan?planId=${planId}`,
+    method: 'delete',
+  });
+};
+/**
+ * 创建物资申领计划
+ */
+export const createSupplyRequest = (data: SupplyRequestListItem) => {
+  return http.request({
+    url: '/emergencySupplies/saveEmergencySuppliesRequestPlan',
+    method: 'post',
+    data,
+  });
+};
+/**
+ * 编辑物资申领计划
+ */
+export const updateSupplyRequest = (data: SupplyRequestListItem) => {
+  return http.request({
+    url: '/emergencySupplies/updateEmergencySuppliesRequestPlan',
+    method: 'put',
+    data,
+  });
+};
+/**
+ * 查询应急物资申领计划详情
+ */
+export const getSupplyRequestDetail = (planId: number) => {
+  return http.request<SupplyRequestDetailItem[]>({
+    url: `/emergencySupplies/queryEmergencySuppliesRequestDetail?planId=${planId}`,
+    method: 'get',
+  });
+};
+/**
+ * 创建需求物资
+ */
+export const createSupplyRequestDetail = (data: AddSupplyQuery) => {
+  return http.request({
+    url: '/emergencySupplies/saveRequestSupply',
+    method: 'post',
+    data,
+  });
+};
+/**
+ * 编辑需求物资
+ */
+export const updateSupplyRequestDetail = (data: AddSupplyQuery) => {
+  return http.request({
+    url: '/emergencySupplies/updateRequestSupply',
+    method: 'put',
+    data,
+  });
+};
+/**
+ * 删除需求物资
+ */
+export const deleteSupplyRequestDetail = (infoId: number) => {
+  return http.request({
+    url: `/emergencySupplies/deleteRequestSupply?infoId=${infoId}`,
+    method: 'delete',
+  });
+};
+/**
+ * 领用物资
+ */
+export const receiveSupplyRequestDetail = (data: ReceiveSupplyRequestDetailForm) => {
+  return http.request({
+    url: '/emergencySupplies/receiveEmergencySupplies',
+    method: 'post',
+    data,
+  });
+};
+/**
+ * 下载申领记录
+ */
+export const exportSupplyRequestRecord = (planId: number) => {
+  return http.request(
+    {
+      url: `/emergencySupplies/downloadRequestRecord?planId=${planId}`,
+      method: 'get',
+      responseType: 'blob',
+    },
+    {
+      isTransformResponse: false,
+    },
+  );
+};
+/**
+ * 获取物资名称列表
+ */
+export const getSupplyNameList = (planId: number) => {
+  return http.request<string[]>({
+    url: `/emergencySupplies/queryEmergencySuppliesNameList?planId=${planId}`,
+    method: 'get',
+  });
+};

+ 40 - 0
src/router/routers/emergency.ts

@@ -80,6 +80,46 @@ const emergencyManagementRoute = {
         title: '应急物资',
       },
       children: [
+        {
+          id: 200401,
+          parentId: 2004,
+          name: 'supply-request',
+          path: 'supply-request',
+          component: '/emergency/emergency-supplies/PageSupplyRequest',
+          redirect: '',
+          meta: {
+            activeMenu: null,
+            alwaysShow: false,
+            frameSrc: '',
+            hidden: false,
+            icon: '',
+            isFrame: 0,
+            isRoot: false,
+            noCache: false,
+            query: '',
+            title: '物资申领',
+          },
+        },
+        {
+          id: 200402,
+          parentId: 2004,
+          name: 'supply-request-detail',
+          path: 'supply-request-detail/:id',
+          component: '/emergency/emergency-supplies/PageSupplyRequestDetail',
+          redirect: '',
+          meta: {
+            activeMenu: '/emergency-management/emergency-supplies/supply-request',
+            alwaysShow: false,
+            frameSrc: '',
+            hidden: false,
+            icon: '',
+            isFrame: 0,
+            isRoot: false,
+            noCache: false,
+            query: '',
+            title: '物资申领详情',
+          },
+        },
         {
           id: 2005,
           parentId: 2004,

+ 97 - 0
src/types/emergency-supplier/index.ts

@@ -100,3 +100,100 @@ export interface InventoryTaskListRes {
   taskName: string;
   endTime: string; //任务截止时间
 }
+
+/**
+ * 物资申领
+ */
+export interface SupplyRequestListQuery {
+  planName?: string;
+  status?: number;
+}
+
+export interface SupplyRequestListItem {
+  /*自增主键 */
+  id?: number;
+  /*申领计划名称 */
+  planName?: string;
+  /*采购日期 */
+  purchaseDate?: string;
+  /*总价(元) */
+  totalPrice?: number;
+  /*状态:1-申请中 2-采购中 3-领用中 4-已领用 */
+  status?: number;
+  /*创建时间 */
+  createdAt?: string;
+  /*更新时间 */
+  updatedAt?: string;
+  /*0-未删除,大于0(时间戳)-已删除 */
+  isDeleted?: number;
+}
+
+export interface SupplyRequestDetailInfo {
+  /*自增主键 */
+  id: number;
+  /*申领计划id */
+  planId: number;
+  /*物资名称 */
+  supplyName: string;
+  /*规格 */
+  specs: string;
+  /*单价(元) */
+  unitPrice: number;
+  /*小计 */
+  subtotal: string;
+  /*创建时间 */
+  createdAt: string;
+  /*更新时间 */
+  updatedAt: string;
+  /*0-未删除,大于0(时间戳)-已删除 */
+  isDeleted: number;
+}
+
+export interface SupplyRequestDetailList {
+  /*自增主键 */
+  id: number;
+  /*申领信息ID */
+  requestId: number;
+  /*需求部门ID */
+  deptId: number;
+  /*需求部门名称 */
+  deptName: string;
+  /*数量 */
+  quantity: number;
+  /*尺寸明细 */
+  sizeDetail: string;
+  /*状态:1-申请中 2-采购中 3-领用中 4-已领用 */
+  status: number;
+  /*创建时间 */
+  createdAt: string;
+  /*更新时间 */
+  updatedAt: string;
+  /*0-未删除,大于0(时间戳)-已删除 */
+  isDeleted: number;
+}
+
+export interface SupplyRequestDetailItem {
+  info: SupplyRequestDetailInfo;
+  detailList: SupplyRequestDetailList[];
+}
+
+export interface ReceiveSupplyRequestDetailForm {
+  supplyId: number; // 应急物资ID
+  quantity: number; // 领用数量
+  detailId: number; // 需求物资ID
+  planId: number; // 申领计划ID
+}
+
+// 添加需求物资单个部门表单
+export type AddSupplyRequestSubForm = Pick<SupplyRequestDetailList, 'id'> &
+  Partial<Omit<SupplyRequestDetailList, 'id'>>;
+// 添加需求物资表单
+export interface AddSupplyRequestForm {
+  info: Partial<SupplyRequestDetailInfo>;
+  detailList: AddSupplyRequestSubForm[];
+}
+// 添加需求物资接口查询参数
+export interface AddSupplyQuery {
+  info: Partial<SupplyRequestDetailInfo>;
+  detailList: Partial<AddSupplyRequestSubForm>[];
+}

+ 225 - 0
src/views/emergency/emergency-supplies/PageSupplyRequest.vue

@@ -0,0 +1,225 @@
+<template>
+  <div class="safety-platform-container">
+    <div class="safety-platform-container__header">
+      <div class="breadcrumb-title">物资申领</div>
+    </div>
+    <div class="safety-platform-container__main">
+      <div class="search-table-container">
+        <header class="disaster-precaution__header">
+          <el-button
+            type="primary"
+            class="search-table-container--button"
+            :icon="Plus"
+            @click="handleCreateRequest"
+            v-if="supplyRequestManagePermission"
+          >
+            创建申领计划
+          </el-button>
+          <BasicSearch
+            :searchConfig="SUPPLY_REQUEST_SEARCH_CONFIG"
+            :searchData="searchData"
+            @update:search-data="handleSearch"
+          >
+            <template #planName>
+              <el-input
+                v-model="searchData.planName"
+                placeholder="请输入物资申领计划"
+                clearable
+                @keyup.enter="handleSearch"
+              />
+            </template>
+          </BasicSearch>
+        </header>
+        <BasicTable
+          :tableData="tableData"
+          :tableConfig="tableConfig"
+          @update:page-size="handleSizeChange"
+          @update:page-number="handleCurrentChange"
+        >
+          <template #status="scope">
+            <span>{{ getStatusText(scope.row.status) }}</span>
+          </template>
+          <template #action="scope">
+            <div class="action-container--div">
+              <ActionButton text="详情" @click="handleViewDetail(scope.row.id)" />
+              <template v-if="scope.row.status === SUPPLY_REQUEST_STATUS.APPLYING">
+                <ActionButton
+                  text="发起采购"
+                  @click="handleStartPurchase(scope.row.id, scope.row.purchaseDate)"
+                  v-if="supplyRequestManagePermission && scope.row.totalPrice > 0"
+                />
+                <ActionButton
+                  text="编辑"
+                  @click="handleEdit(scope.row.id, scope.row.planName, scope.row.purchaseDate)"
+                  v-if="supplyRequestManagePermission"
+                />
+                <ActionButton
+                  text="删除"
+                  :popconfirm="{
+                    title: '确定删除?',
+                  }"
+                  @confirm="handleDelete(scope.row.id)"
+                  v-if="supplyRequestManagePermission"
+                />
+              </template>
+              <template
+                v-else-if="
+                  scope.row.status === SUPPLY_REQUEST_STATUS.PURCHASING ||
+                  scope.row.status === SUPPLY_REQUEST_STATUS.RECEIVING
+                "
+              >
+                <ActionButton
+                  text="编辑"
+                  @click="handleEdit(scope.row.id, scope.row.planName, scope.row.purchaseDate)"
+                  v-if="supplyRequestManagePermission"
+                />
+              </template>
+            </div>
+          </template>
+        </BasicTable>
+      </div>
+    </div>
+  </div>
+  <SupplyRequestForm ref="supplyRequestFormRef" @success="handleFormSuccess" />
+  <StartPurchaseForm ref="startPurchaseFormRef" @success="handleFormSuccess" />
+</template>
+
+<script setup lang="ts">
+  import { ref, reactive, onMounted } from 'vue';
+  import { Plus } from '@element-plus/icons-vue';
+  import { ElMessage } from 'element-plus';
+  import { useRouter } from 'vue-router';
+  import BasicSearch from '@/components/BasicSearch.vue';
+  import BasicTable from '@/components/BasicTable.vue';
+  import ActionButton from '@/components/ActionButton.vue';
+  import SupplyRequestForm from './src/components/SupplyRequestForm.vue';
+  import StartPurchaseForm from './src/components/StartPurchaseForm.vue';
+  import {
+    SUPPLY_REQUEST_SEARCH_CONFIG,
+    SUPPLY_REQUEST_TABLE_COLUMNS,
+    SUPPLY_REQUEST_TABLE_OPTIONS,
+    SUPPLY_REQUEST_TABLE_MAX_HEIGHT_DEFAULT,
+  } from './src/config';
+  import useTableConfig from '@/hooks/useTableConfigHook';
+  import { useUserInfoHook } from '@/hooks/useUserInfoHook';
+  import { EMERGENCY_PERMISSIONS } from '@/views/emergency/src/constant';
+  import { SUPPLY_REQUEST_STATUS, SUPPLY_REQUEST_STATUS_MAP } from './src/constant';
+  import type { QueryPageRequest } from '@/types/basic-query';
+  import type { SupplyRequestListQuery, SupplyRequestListItem } from '@/types/emergency-supplier';
+  import { getSupplyRequestList, deleteSupplyRequest } from '@/api/emergency-supplier';
+
+  const router = useRouter();
+  const { permissions } = useUserInfoHook();
+
+  const supplyRequestManagePermission = ref<Boolean>(false);
+
+  const { tableConfig, pagination } = useTableConfig(SUPPLY_REQUEST_TABLE_COLUMNS, SUPPLY_REQUEST_TABLE_OPTIONS);
+  let supplyRequestListQuery: QueryPageRequest<SupplyRequestListQuery> = {
+    pageNumber: pagination.pageNumber,
+    pageSize: pagination.pageSize,
+    queryParam: {},
+  };
+
+  const tableData = ref<SupplyRequestListItem[]>([]);
+  const searchData = reactive({
+    planName: '',
+    status: SUPPLY_REQUEST_STATUS.ALL,
+  });
+
+  const supplyRequestFormRef = ref<InstanceType<typeof SupplyRequestForm>>();
+  const startPurchaseFormRef = ref<InstanceType<typeof StartPurchaseForm>>();
+
+  // 获取状态文本
+  const getStatusText = (status: number) => {
+    return SUPPLY_REQUEST_STATUS_MAP[status] || '';
+  };
+
+  // 搜索处理
+  const handleSearch = () => {
+    supplyRequestListQuery.queryParam = {};
+    if (searchData.planName) {
+      supplyRequestListQuery.queryParam.planName = searchData.planName;
+    }
+    if (searchData.status !== null && searchData.status !== SUPPLY_REQUEST_STATUS.ALL) {
+      supplyRequestListQuery.queryParam.status = searchData.status;
+    }
+    getTableData();
+  };
+
+  // 获取表格数据
+  const getTableData = async () => {
+    tableConfig.loading = true;
+    const res = await getSupplyRequestList(supplyRequestListQuery);
+    tableData.value = res.records;
+    pagination.total = res.totalRow;
+    tableConfig.loading = false;
+  };
+
+  // 分页处理
+  const handleSizeChange = (value: number) => {
+    pagination.pageSize = value;
+    supplyRequestListQuery.pageSize = value;
+    getTableData();
+  };
+
+  const handleCurrentChange = (value: number) => {
+    pagination.pageNumber = value;
+    supplyRequestListQuery.pageNumber = value;
+    getTableData();
+  };
+
+  // 创建申领计划
+  const handleCreateRequest = () => {
+    supplyRequestFormRef.value?.openAddDialog();
+  };
+  // 查看详情
+  const handleViewDetail = (id: number) => {
+    router.push({
+      name: 'supply-request-detail',
+      params: {
+        id,
+      },
+    });
+  };
+
+  // 发起采购
+  const handleStartPurchase = (id: number, purchaseDate: string) => {
+    startPurchaseFormRef.value?.openDialog(id, purchaseDate);
+  };
+
+  // 编辑
+  const handleEdit = (id: number, planName: string, purchaseDate: string) => {
+    supplyRequestFormRef.value?.openEditDialog({
+      id: id,
+      planName: planName,
+      purchaseDate: purchaseDate,
+    });
+  };
+
+  // 表单提交成功回调
+  const handleFormSuccess = () => {
+    getTableData();
+  };
+
+  // 删除
+  const handleDelete = async (id: number) => {
+    await deleteSupplyRequest(id);
+    await getTableData();
+    ElMessage.success('删除成功');
+  };
+
+  onMounted(() => {
+    getTableData();
+    tableConfig.maxHeight = SUPPLY_REQUEST_TABLE_MAX_HEIGHT_DEFAULT;
+    supplyRequestManagePermission.value = Boolean(
+      permissions.find((item: { code: string }) => item.code === EMERGENCY_PERMISSIONS.SUPPLY_REQUEST_MANAGE),
+    );
+  });
+</script>
+
+<style scoped lang="scss">
+  @use '@/styles/page-details-layout.scss' as *;
+  @use '@/styles/page-main-layout.scss' as *;
+  @use '@/styles/basic-table-action.scss' as *;
+  @use './src/styles/page-common.scss' as *;
+</style>

+ 313 - 0
src/views/emergency/emergency-supplies/PageSupplyRequestDetail.vue

@@ -0,0 +1,313 @@
+<template>
+  <div class="safety-platform-container">
+    <div class="safety-platform-container__header">
+      <div class="bread">
+        <BreadcrumbBack />
+        <div class="breadcrumb-title">{{ supplyRequestInfo?.planName || '物资采购申领详情' }}</div>
+      </div>
+    </div>
+    <div class="safety-platform-container__main">
+      <div class="detail-container">
+        <header class="detail-header">
+          <el-button
+            type="primary"
+            :icon="Plus"
+            @click="handleAddMaterial"
+            v-if="supplyRequestInfo?.status === SUPPLY_REQUEST_STATUS.APPLYING && supplyRequestManagePermission"
+            >添加需求物资</el-button
+          >
+          <el-button type="primary" :icon="Download" @click="handleDownloadRecord">下载采购记录</el-button>
+        </header>
+        <div class="table-container">
+          <el-table :data="tableData" :span-method="handleSpanMethod" border v-loading="loading" style="width: 100%">
+            <el-table-column type="index" label="序号" width="80" align="center" />
+            <el-table-column prop="materialName" label="物资名称" min-width="140" align="center" />
+            <el-table-column prop="specification" label="规格" min-width="200" align="center" />
+            <el-table-column prop="unitPrice" label="单价 (元)" width="120" align="center" />
+            <el-table-column prop="subtotal" label="小计" min-width="200" align="center" />
+            <el-table-column prop="department" label="需求部门" min-width="140" align="center" />
+            <el-table-column prop="quantity" label="数量" width="100" align="center" />
+            <el-table-column prop="sizeDetails" label="尺寸明细" min-width="200" align="center" />
+            <el-table-column prop="status" label="状态" width="120" align="center">
+              <template #default="scope">
+                <span>{{ getStatusText(scope.row.status) }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column prop="claim" label="领用" width="100" align="center" v-if="supplyRequestManagePermission">
+              <template #default="scope">
+                <el-link
+                  v-if="scope.row.status === SUPPLY_REQUEST_STATUS.PURCHASING && !scope.row.isClaimed"
+                  type="primary"
+                  @click="handleClaim(scope.row)"
+                >
+                  领用
+                </el-link>
+                <span v-else>-</span>
+              </template>
+            </el-table-column>
+            <el-table-column
+              prop="action"
+              label="物资操作"
+              width="150"
+              align="center"
+              fixed="right"
+              v-if="supplyRequestManagePermission"
+            >
+              <template #default="scope">
+                <div class="action-container">
+                  <el-link
+                    v-if="scope.row.status === SUPPLY_REQUEST_STATUS.APPLYING"
+                    type="primary"
+                    @click="handleEdit(scope.row)"
+                  >
+                    编辑
+                  </el-link>
+                  <el-link
+                    v-if="scope.row.status === SUPPLY_REQUEST_STATUS.APPLYING"
+                    type="primary"
+                    style="margin-left: 8px"
+                    @click="handleDelete(scope.row)"
+                  >
+                    删除
+                  </el-link>
+                  <span v-if="scope.row.status !== SUPPLY_REQUEST_STATUS.APPLYING">-</span>
+                </div>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+      </div>
+    </div>
+    <!-- 领用弹窗 -->
+    <ReceiveSupplyDialog ref="receiveSupplyDialogRef" @success="handleClaimSuccess" />
+    <!-- 添加物资弹窗 -->
+    <AddSuppliesDrawer ref="addSupplyDrawerRef" @success="handleAddMaterialSuccess" />
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, onMounted } from 'vue';
+  import { useRoute } from 'vue-router';
+  import { Plus, Download } from '@element-plus/icons-vue';
+  import { ElMessage, ElMessageBox } from 'element-plus';
+  import BreadcrumbBack from '@/components/BreadcrumbBack.vue';
+  import { SUPPLY_REQUEST_STATUS, SUPPLY_REQUEST_DETAIL_STATUS_MAP } from './src/constant';
+  import {
+    getSupplyRequestInfoById,
+    getSupplyRequestDetail,
+    deleteSupplyRequestDetail,
+    exportSupplyRequestRecord,
+  } from '@/api/emergency-supplier';
+  import type { SupplyRequestDetailItem } from '@/types/emergency-supplier';
+  import { SupplyRequestListItem } from '@/types/emergency-supplier';
+  import { downloadFile } from '@/views/disaster/utils/download';
+  import ReceiveSupplyDialog from './src/components/ReceiveSupplyDialog.vue';
+  import { useUserInfoHook } from '@/hooks/useUserInfoHook';
+  import { EMERGENCY_PERMISSIONS } from '@/views/emergency/src/constant';
+  import AddSuppliesDrawer from './src/components/AddSuppliesDrawer.vue';
+
+  const { permissions } = useUserInfoHook();
+  const supplyRequestManagePermission = ref<Boolean>(false);
+
+  // 表格行数据类型
+  interface TableRowData {
+    id: number; // detailList 的 id
+    requestId: number; // detailList 的 requestId
+    infoId: number; // info 的 id,用于合并单元格
+    materialName: string; // info 的 supplyName
+    specification: string; // info 的 specs
+    unitPrice: number; // info 的 unitPrice
+    subtotal: string; // info 的 subtotal
+    department: string; // detailList 的 deptName
+    quantity: number; // detailList 的 quantity
+    sizeDetails: string; // detailList 的 sizeDetail
+    status: number; // detailList 的 status
+    isClaimed?: boolean; // 是否已领用(根据状态判断)
+  }
+
+  const route = useRoute();
+  const id = Number(route.params.id);
+
+  const supplyRequestInfo = ref<SupplyRequestListItem>();
+
+  const loading = ref(false);
+  const tableData = ref<TableRowData[]>([]);
+  const addSupplyDrawerRef = ref<InstanceType<typeof AddSuppliesDrawer>>();
+  const supplyRequestDetailData = ref<SupplyRequestDetailItem[]>([]);
+
+  // 领用弹窗相关
+  const receiveSupplyDialogRef = ref<InstanceType<typeof ReceiveSupplyDialog>>();
+
+  // 获取状态文本
+  const getStatusText = (status: number) => {
+    return SUPPLY_REQUEST_DETAIL_STATUS_MAP[status] || '';
+  };
+
+  // 合并单元格方法
+  const handleSpanMethod = ({ row, rowIndex, columnIndex }: any) => {
+    // 需要合并的列索引:物资名称(1)、规格(2)、单价(3)、小计(4)、物资操作(10)
+    const mergeColumns = [1, 2, 3, 4, 10];
+    if (!mergeColumns.includes(columnIndex)) {
+      return { rowspan: 1, colspan: 1 };
+    }
+
+    // 按照 infoId 来分组,找到同一个 info 下的所有行
+    const infoId = row.infoId;
+    const sameInfoRows = tableData.value.filter((item) => item.infoId === infoId);
+    const firstRowIndex = tableData.value.findIndex((item) => item.infoId === infoId);
+
+    // 如果是第一行,返回合并的行数
+    if (rowIndex === firstRowIndex) {
+      return {
+        rowspan: sameInfoRows.length,
+        colspan: 1,
+      };
+    }
+
+    // 其他行隐藏
+    return {
+      rowspan: 0,
+      colspan: 0,
+    };
+  };
+
+  // 获取详情数据
+  const getDetailData = async () => {
+    loading.value = true;
+    try {
+      const res = await getSupplyRequestDetail(id);
+      supplyRequestDetailData.value = res;
+
+      // 将 SupplyRequestDetailItem[] 转换为表格需要的扁平化数据结构
+      const flatData: TableRowData[] = [];
+
+      res.forEach((item) => {
+        // 将每个 detailList 项与 info 合并成一行
+        item.detailList.forEach((detail) => {
+          flatData.push({
+            id: detail.id,
+            requestId: detail.requestId,
+            infoId: item.info.id, // 用于合并单元格
+            materialName: item.info.supplyName,
+            specification: item.info.specs,
+            unitPrice: item.info.unitPrice,
+            subtotal: item.info.subtotal,
+            department: detail.deptName,
+            quantity: detail.quantity,
+            sizeDetails: detail.sizeDetail,
+            status: detail.status,
+            isClaimed: detail.status === SUPPLY_REQUEST_STATUS.RECEIVED,
+          });
+        });
+      });
+
+      tableData.value = flatData;
+    } catch (error) {
+      console.error('获取详情失败:', error);
+      ElMessage.error('获取详情失败');
+    } finally {
+      loading.value = false;
+    }
+  };
+
+  // 添加需求物资
+  const handleAddMaterial = () => {
+    addSupplyDrawerRef.value?.openDrawer();
+  };
+
+  // 添加物资成功回调
+  const handleAddMaterialSuccess = async () => {
+    await getDetailData();
+  };
+
+  // 下载采购记录
+  const handleDownloadRecord = async () => {
+    try {
+      const res = await exportSupplyRequestRecord(id);
+      if (res.size === 0) return;
+      const blob = new Blob([res], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
+      const url = window.URL.createObjectURL(blob);
+      downloadFile(url, '采购申领记录.xlsx');
+    } catch (error) {
+      ElMessage.error('下载失败');
+    }
+  };
+
+  // 领用
+  const handleClaim = (row: TableRowData) => {
+    receiveSupplyDialogRef.value?.openDialog(row.materialName, row.quantity, row.id, id);
+  };
+
+  // 领用成功回调
+  const handleClaimSuccess = async () => {
+    await getDetailData();
+  };
+
+  // 编辑
+  const handleEdit = (row: TableRowData) => {
+    // 根据 infoId 找到对应的 SupplyRequestDetailItem
+    const editData = supplyRequestDetailData.value.find((item) => item.info.id === row.infoId);
+    if (!editData) {
+      ElMessage.error('未找到要编辑的数据');
+      return;
+    }
+    addSupplyDrawerRef.value?.openDrawer(editData);
+  };
+
+  // 删除
+  const handleDelete = async (row: TableRowData) => {
+    try {
+      await ElMessageBox.confirm('确定要删除该物资吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      });
+      await deleteSupplyRequestDetail(row.infoId);
+      ElMessage.success('删除成功');
+      await getDetailData();
+    } catch (error) {
+      ElMessage.error('删除失败');
+    }
+  };
+
+  onMounted(async () => {
+    getDetailData();
+    supplyRequestInfo.value = await getSupplyRequestInfoById(id);
+    supplyRequestManagePermission.value = Boolean(
+      permissions.find((item: { code: string }) => item.code === EMERGENCY_PERMISSIONS.SUPPLY_REQUEST_MANAGE),
+    );
+  });
+</script>
+
+<style scoped lang="scss">
+  @use '@/styles/page-details-layout.scss' as *;
+  @use './src/styles/page-common.scss' as *;
+
+  .bread {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+  }
+
+  .detail-container {
+    background: #fff;
+    padding: 16px;
+    border-radius: 4px;
+  }
+
+  .detail-header {
+    display: flex;
+    gap: 12px;
+    margin-bottom: 16px;
+  }
+
+  .table-container {
+    width: 100%;
+  }
+
+  .action-container {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+</style>

+ 308 - 0
src/views/emergency/emergency-supplies/src/components/AddSuppliesDrawer.vue

@@ -0,0 +1,308 @@
+<template>
+  <el-drawer
+    v-model="showDrawer"
+    direction="rtl"
+    :title="isEditMode ? '编辑需求物资' : '添加需求物资'"
+    size="35%"
+    :close-on-click-modal="false"
+    destroy-on-close
+  >
+    <el-form ref="formRef" :model="formData" label-width="auto" class="add-supply-form">
+      <el-form-item
+        label="物资名称:"
+        prop="info.supplyName"
+        :rules="[{ required: true, message: '请输入物资名称', trigger: 'blur' }]"
+      >
+        <el-select v-model="formData.info.supplyName" placeholder="请输入物资名称">
+          <el-option v-for="item in SuppliesNameList" :key="item" :value="item" />
+        </el-select>
+      </el-form-item>
+      <el-form-item
+        label="规格:"
+        prop="info.specs"
+        :rules="[{ required: true, message: '请输入规格', trigger: 'blur' }]"
+      >
+        <el-input v-model="formData.info.specs" placeholder="请输入规格" />
+      </el-form-item>
+      <el-form-item
+        label="单价(元):"
+        prop="info.unitPrice"
+        :rules="[
+          { required: true, message: '请输入单价', trigger: 'blur' },
+          { validator: validateFormNumber, trigger: 'blur' },
+        ]"
+      >
+        <el-input v-model.number="formData.info.unitPrice" placeholder="请输入单价" type="number" min="0" />
+      </el-form-item>
+      <el-form-item label="小计:" prop="info.subtotal">
+        <el-input v-model="formData.info.subtotal" placeholder="请输入小计" type="textarea" :rows="3" />
+      </el-form-item>
+      <el-form-item label="需求详情:" v-if="deptTree">
+        <AddSuppliesSubForm
+          ref="subFormRef"
+          v-for="item in formData.detailList"
+          :key="item.id"
+          :init-data="item"
+          :dept-tree="deptTree"
+          :dept-selected="deptSelected"
+          @update-sub-form="onUpdateSubForm"
+          @close-sub-form="onCloseSubForm(item.id)"
+        />
+      </el-form-item>
+      <el-form-item>
+        <div class="add-supply-form-btns">
+          <el-button @click="addDetail">新增</el-button>
+        </div>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="showDrawer = false">取消</el-button>
+      <el-button type="primary" @click="handleSubmit"> 提交 </el-button>
+    </template>
+  </el-drawer>
+</template>
+
+<script setup lang="ts">
+  import { ElDrawer, ElForm, ElInput, ElSelect, ElMessage } from 'element-plus';
+  import { ref, reactive, onMounted, useTemplateRef, computed } from 'vue';
+  import type {
+    AddSupplyRequestForm,
+    AddSupplyRequestSubForm,
+    SupplyRequestDetailItem,
+  } from '@/types/emergency-supplier';
+  import { createSupplyRequestDetail, getSupplyNameList, updateSupplyRequestDetail } from '@/api/emergency-supplier';
+  import AddSuppliesSubForm from './AddSuppliesSubForm.vue';
+
+  import type { DeptTree } from '@/types/dept/type';
+  import { getAllDepartments } from '@/api/auth/dept';
+  import { useRoute } from 'vue-router';
+
+  const route = useRoute();
+  const planId = Number(route.params.id);
+
+  const formRef = ref<InstanceType<typeof ElForm>>();
+  const subFormRefs = useTemplateRef('subFormRef');
+
+  const showDrawer = ref(false);
+  const isEditMode = ref(false);
+  const editInfoId = ref<number | null>(null);
+
+  const lastDetailId = ref(0);
+
+  // 获取物资名称
+  const SuppliesNameList = ref<string[]>();
+  const getSuppliesNameList = async () => {
+    const res = await getSupplyNameList(planId);
+    SuppliesNameList.value = res;
+  };
+
+  // 获取部门树
+  const deptTree = ref<DeptTree[]>();
+  const getDeptTreeData = async () => {
+    const res = await getAllDepartments();
+    deptTree.value = res[0].children;
+  };
+
+  // 表单数据
+  const formData = reactive<AddSupplyRequestForm>({
+    info: {},
+    detailList: [
+      {
+        id: lastDetailId.value,
+      },
+    ],
+  });
+  // 表单中已选择的部门id
+  const deptSelected = computed(() => {
+    return formData.detailList.map((item) => item.deptId).filter((item) => item !== undefined);
+  });
+
+  // 清理表单数据
+  const clearFormData = () => {
+    lastDetailId.value = 0;
+    formData.info = {};
+    formData.detailList = [
+      {
+        id: lastDetailId.value,
+      },
+    ];
+    isEditMode.value = false;
+    editInfoId.value = null;
+  };
+
+  // 初始化编辑数据
+  const initEditData = (editData: SupplyRequestDetailItem) => {
+    isEditMode.value = true;
+    editInfoId.value = editData.info.id;
+
+    // 设置 info 数据
+    formData.info = {
+      id: editData.info.id,
+      supplyName: editData.info.supplyName,
+      specs: editData.info.specs,
+      unitPrice: editData.info.unitPrice,
+      subtotal: editData.info.subtotal,
+    };
+
+    // 设置 detailList 数据(编辑时不需要保留 id,使用负数作为临时 id)
+    formData.detailList = editData.detailList.map((detail, index) => {
+      return {
+        id: -(index + 1), // 使用负数作为临时 id
+        deptId: detail.deptId,
+        deptName: detail.deptName,
+        quantity: detail.quantity,
+        sizeDetail: detail.sizeDetail,
+      };
+    });
+
+    // 更新 lastDetailId,确保新增项的 id 不会冲突
+    if (formData.detailList.length > 0) {
+      lastDetailId.value = formData.detailList.length;
+    }
+  };
+
+  // 打开需求列表&数据初始化
+  const openDrawer = async (editData?: SupplyRequestDetailItem) => {
+    // 先打开 drawer,再加载数据
+    showDrawer.value = true;
+
+    // 如果是编辑模式,初始化编辑数据
+    if (editData) {
+      initEditData(editData);
+    } else {
+      // 创建时清理为初始化
+      clearFormData();
+    }
+
+    // 重新加载数据,确保数据是最新的
+    try {
+      await Promise.all([getSuppliesNameList(), getDeptTreeData()]);
+    } catch (error) {
+      console.error('加载数据失败:', error);
+      // 即使数据加载失败,drawer 也应该保持打开状态
+    }
+  };
+
+  // 表单需求详情加一项
+  const addDetail = () => {
+    lastDetailId.value += 1;
+    // 使用负数作为新项的 id,避免与已存在的记录 id 冲突
+    formData.detailList.push({
+      id: -lastDetailId.value,
+    });
+  };
+
+  // 删除一项需求详情
+  const onCloseSubForm = (id: number) => {
+    if (formData.detailList.length === 1) {
+      ElMessage.warning('至少一条需求详情');
+      return;
+    }
+    formData.detailList = formData.detailList.filter((item) => item.id !== id);
+  };
+
+  // 更新一项需求详情
+  const onUpdateSubForm = (data: AddSupplyRequestSubForm) => {
+    const detail = formData.detailList.find((item) => item.id === data.id);
+    if (!detail) return;
+    Object.assign(detail, data);
+  };
+
+  // 表单校验
+  const formValidate = async () => {
+    if (!formRef.value) return;
+    if (!subFormRefs.value) return;
+    const subFormsValidateResultList = await Promise.all(subFormRefs.value.map((item) => item?.subFormValidate()));
+    const isSubFormsValidateResult = subFormsValidateResultList.every((item) => item);
+    if (!isSubFormsValidateResult) return;
+    return await formRef.value.validate();
+  };
+
+  const getFormData = () => {
+    return {
+      info: { ...formData.info, planId },
+      detailList: formData.detailList.map((item) => {
+        // 编辑时不需要保留 detailList 的 id,只保留 info 的 id
+        return {
+          deptId: item.deptId,
+          deptName: item.deptName,
+          quantity: item.quantity,
+          sizeDetail: item.sizeDetail,
+        };
+      }),
+    };
+  };
+
+  interface Emits {
+    (e: 'success'): void;
+  }
+
+  const emit = defineEmits<Emits>();
+
+  // 提交表单
+  const handleSubmit = async () => {
+    const res = await formValidate();
+    if (!res) return;
+    const data = getFormData();
+    try {
+      if (isEditMode.value && editInfoId.value) {
+        // 编辑模式
+        await updateSupplyRequestDetail(data);
+        ElMessage.success('编辑成功');
+      } else {
+        // 创建模式
+        await createSupplyRequestDetail(data);
+        ElMessage.success('提交成功');
+      }
+      showDrawer.value = false;
+      emit('success');
+    } catch (error) {
+      ElMessage.error(isEditMode.value ? '编辑失败' : '提交失败');
+    }
+  };
+
+  const validateFormNumber = (rule: any, value: number, callback: any) => {
+    if (value < 0) {
+      callback(new Error('输入不能小于0'));
+    } else {
+      callback();
+    }
+  };
+
+  onMounted(() => {
+    getSuppliesNameList();
+    getDeptTreeData();
+  });
+
+  defineExpose({
+    openDrawer,
+  });
+</script>
+
+<style scoped>
+  .el-form {
+    display: flex;
+    flex-direction: column;
+    width: auto;
+    height: 100%;
+    gap: 24px;
+  }
+  .el-form-item {
+    margin-bottom: 0;
+  }
+  .add-supply-form-btns {
+    padding-left: 88px;
+  }
+
+  :deep(.el-form-item__label) {
+    padding: 0;
+  }
+
+  :deep(.el-form-item__content) {
+    gap: 16px;
+  }
+
+  :deep(.el-textarea__inner) {
+    padding-bottom: 20px;
+  }
+</style>

+ 149 - 0
src/views/emergency/emergency-supplies/src/components/AddSuppliesSubForm.vue

@@ -0,0 +1,149 @@
+<template>
+  <div class="add-supply-sub-form">
+    <div class="sub-form-title">
+      <img class="delete-supply" src="@/assets/icons/delete.png" alt="" @click="emits('closeSubForm')" />
+    </div>
+    <el-form ref="subFormRef" :model="subFormData" label-width="auto" class="add-supply-sub-form">
+      <el-form-item
+        label="需求部门:"
+        prop="deptId"
+        :rules="[{ required: true, message: '请选择需求部门', trigger: 'change' }]"
+      >
+        <el-cascader
+          ref="cascaderRef"
+          popper-class="cascader-popper--custom"
+          v-model="subFormData.deptId"
+          :options="deptTree"
+          :props="cascaderProp"
+          :show-all-levels="false"
+          placeholder="请选择需求部门"
+          filterable
+          @change="handleChangeDept"
+        />
+      </el-form-item>
+      <el-form-item
+        label="数量:"
+        prop="quantity"
+        :rules="[
+          { required: true, message: '请输入数量', trigger: 'blur' },
+          { validator: validateFormNumber, trigger: 'blur' },
+        ]"
+      >
+        <el-input
+          v-model.number="subFormData.quantity"
+          placeholder="请输入数量"
+          type="number"
+          min="1"
+          @change="emits('updateSubForm', subFormData)"
+        />
+      </el-form-item>
+      <el-form-item label="尺寸明细:" prop="sizeDetail">
+        <el-input
+          v-model="subFormData.sizeDetail"
+          placeholder="请输入尺寸明细"
+          @change="emits('updateSubForm', subFormData)"
+        />
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ElForm, ElMessage } from 'element-plus';
+  import { ref } from 'vue';
+  import type { AddSupplyRequestSubForm } from '@/types/emergency-supplier';
+  import type { DeptTree } from '@/types/dept/type';
+
+  const props = defineProps<{
+    initData: AddSupplyRequestSubForm;
+    deptTree: DeptTree[];
+    deptSelected: number[];
+  }>();
+
+  const emits = defineEmits<{
+    (e: 'closeSubForm'): void;
+    (e: 'updateSubForm', data: AddSupplyRequestSubForm): void;
+  }>();
+
+  const cascaderRef = ref();
+  const cascaderProp = {
+    checkStrictly: true,
+    expandTrigger: 'hover',
+    value: 'id',
+    label: 'deptName',
+    emitPath: false,
+  };
+  const handleChangeDept = () => {
+    const deptInfo = cascaderRef.value?.getCheckedNodes();
+    if (!deptInfo || !deptInfo[0]) return;
+
+    const deptId = deptInfo[0].value;
+    subFormRef.value?.clearValidate();
+    if (props.deptSelected.some((item) => item === deptId)) {
+      ElMessage.warning('需求部门不能重复');
+      cascaderRef.value.cascaderPanelRef.clearCheckedNodes();
+    } else {
+      emits('updateSubForm', subFormData.value);
+      subFormData.value.deptName = deptInfo[0].label;
+    }
+  };
+
+  const subFormRef = ref<InstanceType<typeof ElForm>>();
+  const subFormData = ref<AddSupplyRequestSubForm>(props.initData);
+
+  const subFormValidate = async () => {
+    return new Promise((resolve) => {
+      subFormRef.value?.validate((valid) => {
+        resolve(valid);
+      });
+    });
+  };
+
+  const validateFormNumber = (rule: any, value: number, callback: any) => {
+    if (!Number.isInteger(value)) return callback(new Error('请输入正整数'));
+    if (value <= 0) {
+      callback(new Error('请输入正整数'));
+    } else {
+      callback();
+    }
+  };
+
+  defineExpose({
+    subFormValidate,
+  });
+</script>
+
+<style scoped>
+  .add-supply-sub-form {
+    width: 100%;
+    padding: 10px;
+    border-radius: 8px;
+    background: #f0f2f5;
+  }
+  .sub-form-title {
+    text-align: end;
+    padding: 0 10px;
+  }
+  .delete-supply {
+    cursor: pointer;
+    width: 16px;
+  }
+
+  .el-form {
+    display: flex;
+    flex-direction: column;
+    width: auto;
+    height: 100%;
+    gap: 16px;
+  }
+  .el-form-item {
+    margin-bottom: 0;
+  }
+
+  :deep(.el-form-item__label) {
+    padding: 0;
+  }
+  :deep(.el-cascader) {
+    width: 100%;
+  }
+</style>

+ 1 - 1
src/views/emergency/emergency-supplies/src/components/DiscardSupplies.vue

@@ -58,7 +58,7 @@
         if (value == null) return callback(new Error('请输入报废数量'));
         if (!Number.isInteger(value)) return callback(new Error('请输入正整数'));
         if (value > supply.value?.currentQuantity) {
-          callback(new Error('超过数量上限'));
+          callback(new Error('报废数量不能超过物资当前数量'));
         } else if (value <= 0) {
           callback(new Error('数量不能小于1'));
         } else {

+ 143 - 0
src/views/emergency/emergency-supplies/src/components/ReceiveSupplyDialog.vue

@@ -0,0 +1,143 @@
+<template>
+  <BasicDialog ref="basicDialogRef" title="物资领用" width="500px" @refresh="refreshFormData">
+    <template #form>
+      <div class="claim-dialog-content">
+        <div class="claim-dialog-label">请选择领用存放信息</div>
+        <el-select
+          v-model="selectedSupplyId"
+          placeholder="请选择需求部门"
+          filterable
+          style="width: 100%"
+          :loading="supplyListLoading"
+        >
+          <el-option
+            v-for="item in supplyList"
+            :key="item.id"
+            :label="`${item.supplyName} | ${getLocation(item.location)} | ${item.keeperName}`"
+            :value="item.id"
+          />
+        </el-select>
+      </div>
+    </template>
+    <template #footer>
+      <el-button @click="basicDialogRef?.closeDialog">取消</el-button>
+      <el-button type="primary" @click="handleSubmit" :loading="submitLoading">提交</el-button>
+    </template>
+  </BasicDialog>
+</template>
+
+<script setup lang="ts">
+  import { onMounted, ref } from 'vue';
+  import { ElMessage } from 'element-plus';
+  import BasicDialog from '@/components/BasicDialog.vue';
+  import { getEmergencySuppliesInfoList } from '@/api/command-center';
+  import type { QueryEmergencySuppliesInfoListRes } from '@/api/command-center';
+  import { receiveSupplyRequestDetail } from '@/api/emergency-supplier';
+  import { useEmergencySuppliesHook } from '@/views/emergency/emergency-supplies/src/hook';
+
+  const { getLocationDict, getLocation } = useEmergencySuppliesHook();
+
+  interface Emits {
+    (e: 'success'): void;
+  }
+
+  const emit = defineEmits<Emits>();
+
+  const basicDialogRef = ref<InstanceType<typeof BasicDialog>>();
+  const selectedSupplyId = ref<number | null>(null);
+  const supplyList = ref<QueryEmergencySuppliesInfoListRes[]>([]);
+  const supplyListLoading = ref(false);
+  const submitLoading = ref(false);
+
+  // 当前领用的数据
+  const currentSupplyName = ref('');
+  const currentQuantity = ref(0);
+  const currentDetailId = ref(0);
+  const currentPlanId = ref(0);
+
+  // 打开对话框
+  const openDialog = (supplyName: string, quantity: number, detailId: number, planId: number) => {
+    currentSupplyName.value = supplyName;
+    currentQuantity.value = quantity;
+    currentDetailId.value = detailId;
+    currentPlanId.value = planId;
+    if (supplyName) {
+      loadSupplyList(supplyName);
+    }
+    basicDialogRef.value?.openDialog();
+  };
+
+  // 刷新表单数据
+  const refreshFormData = () => {
+    selectedSupplyId.value = null;
+    supplyList.value = [];
+  };
+
+  // 加载应急物资列表
+  const loadSupplyList = async (supplyName: string) => {
+    if (!supplyName) {
+      return;
+    }
+    supplyListLoading.value = true;
+    try {
+      const res = await getEmergencySuppliesInfoList({
+        supplyName,
+      });
+      supplyList.value = res || [];
+      if (supplyList.value.length === 0) {
+        ElMessage.warning('未找到匹配的应急物资信息');
+      }
+    } catch (error) {
+      console.error('获取应急物资列表失败:', error);
+      ElMessage.error('获取应急物资列表失败');
+    } finally {
+      supplyListLoading.value = false;
+    }
+  };
+
+  // 提交领用
+  const handleSubmit = async () => {
+    if (!selectedSupplyId.value) {
+      ElMessage.warning('请选择领用存放信息');
+      return;
+    }
+
+    submitLoading.value = true;
+    try {
+      await receiveSupplyRequestDetail({
+        supplyId: selectedSupplyId.value,
+        quantity: currentQuantity.value,
+        detailId: currentDetailId.value,
+        planId: currentPlanId.value,
+      });
+      ElMessage.success('领用操作成功');
+      basicDialogRef.value?.closeDialog();
+      emit('success');
+    } catch (error) {
+      console.error('领用失败:', error);
+      ElMessage.error('领用操作失败');
+    } finally {
+      submitLoading.value = false;
+    }
+  };
+
+  defineExpose({
+    openDialog,
+  });
+
+  onMounted(() => {
+    getLocationDict('all');
+  });
+</script>
+
+<style scoped lang="scss">
+  .claim-dialog-content {
+    padding: 20px 0;
+  }
+
+  .claim-dialog-label {
+    margin-bottom: 12px;
+    font-size: 14px;
+    color: #606266;
+  }
+</style>

+ 85 - 0
src/views/emergency/emergency-supplies/src/components/StartPurchaseForm.vue

@@ -0,0 +1,85 @@
+<template>
+  <BasicDialog ref="basicDialogRef" title="发起采购" @refresh="refreshFormData">
+    <template #form>
+      <BasicForm ref="basicFormRef" :formData="ruleFormData" :formRules="formRules" :formConfig="ruleFormConfig" />
+    </template>
+    <template #footer>
+      <el-button type="primary" @click="handleSubmit">提交</el-button>
+      <el-button @click="basicDialogRef?.closeDialog">取消</el-button>
+    </template>
+  </BasicDialog>
+</template>
+
+<script setup lang="ts">
+  import { ref } from 'vue';
+  import { ElMessage } from 'element-plus';
+  import BasicDialog from '@/components/BasicDialog.vue';
+  import BasicForm from '@/components/BasicForm.vue';
+  import { useFormConfigHook } from '@/hooks/useFormConfigHook';
+  import { START_PURCHASE_FORM_CONFIG, START_PURCHASE_FORM_DATA, START_PURCHASE_FORM_RULES } from '../config';
+  import { updateSupplyRequest } from '@/api/emergency-supplier';
+  import { SUPPLY_REQUEST_STATUS } from '../constant';
+
+  const emits = defineEmits<{
+    (e: 'success'): void;
+  }>();
+
+  const basicDialogRef = ref<InstanceType<typeof BasicDialog>>();
+  const basicFormRef = ref<InstanceType<typeof BasicForm>>();
+  const requestId = ref<number | undefined>(undefined);
+
+  const { ruleFormConfig, ruleFormData, formRules } = useFormConfigHook(
+    START_PURCHASE_FORM_CONFIG,
+    START_PURCHASE_FORM_DATA,
+    START_PURCHASE_FORM_RULES,
+  );
+
+  // 打开对话框
+  const openDialog = (id: number, existingPurchaseDate?: string) => {
+    requestId.value = id;
+    // 先设置采购日期,再打开对话框
+    // 如果有已有采购日期,则反显
+    ruleFormData.purchaseDate = existingPurchaseDate || '';
+    basicDialogRef.value?.openDialog();
+  };
+
+  // 提交表单
+  const handleSubmit = async () => {
+    const validate = await basicFormRef.value?.validateForm();
+    if (!validate) return;
+
+    if (!requestId.value) {
+      ElMessage.error('缺少必要参数');
+      return;
+    }
+
+    try {
+      await updateSupplyRequest({
+        id: requestId.value,
+        purchaseDate: ruleFormData.purchaseDate,
+        status: SUPPLY_REQUEST_STATUS.PURCHASING,
+      });
+      ElMessage.success('发起采购成功');
+      basicDialogRef.value?.closeDialog();
+      // 清理数据
+      requestId.value = undefined;
+      ruleFormData.purchaseDate = '';
+      // 触发父组件刷新列表
+      emits('success');
+    } catch (error) {
+      console.error('发起采购失败:', error);
+      ElMessage.error('发起采购失败');
+    }
+  };
+
+  // 刷新表单数据(在对话框打开时调用,用于清除验证状态)
+  const refreshFormData = () => {
+    basicFormRef.value?.clearValidate();
+  };
+
+  defineExpose({
+    openDialog,
+  });
+</script>
+
+<style scoped lang="scss"></style>

+ 106 - 0
src/views/emergency/emergency-supplies/src/components/SupplyRequestForm.vue

@@ -0,0 +1,106 @@
+<template>
+  <BasicDialog ref="basicDialogRef" :title="dialogTitle" @close="refreshFormData">
+    <template #form>
+      <BasicForm ref="basicFormRef" :formData="ruleFormData" :formRules="formRules" :formConfig="ruleFormConfig" />
+    </template>
+    <template #footer>
+      <el-button type="primary" @click="handleSubmit">提交</el-button>
+      <el-button @click="basicDialogRef?.closeDialog">取消</el-button>
+    </template>
+  </BasicDialog>
+</template>
+
+<script setup lang="ts">
+  import { ref, computed } from 'vue';
+  import { ElMessage } from 'element-plus';
+  import BasicDialog from '@/components/BasicDialog.vue';
+  import BasicForm from '@/components/BasicForm.vue';
+  import { useFormConfigHook } from '@/hooks/useFormConfigHook';
+  import { SUPPLY_REQUEST_FORM_CONFIG, SUPPLY_REQUEST_FORM_DATA, SUPPLY_REQUEST_FORM_RULES } from '../config';
+  import { createSupplyRequest, updateSupplyRequest } from '@/api/emergency-supplier';
+
+  const emits = defineEmits<{
+    (e: 'success'): void;
+  }>();
+
+  const basicDialogRef = ref<InstanceType<typeof BasicDialog>>();
+  const basicFormRef = ref<InstanceType<typeof BasicForm>>();
+  const isEditMode = ref(false);
+  const editId = ref<number | undefined>(undefined);
+
+  const { ruleFormConfig, ruleFormData, formRules } = useFormConfigHook(
+    SUPPLY_REQUEST_FORM_CONFIG,
+    SUPPLY_REQUEST_FORM_DATA,
+    SUPPLY_REQUEST_FORM_RULES,
+  );
+
+  // 对话框标题
+  const dialogTitle = computed(() => {
+    return isEditMode.value ? '编辑申领计划' : '创建申领计划';
+  });
+
+  // 打开对话框(创建模式)
+  const openAddDialog = () => {
+    isEditMode.value = false;
+    editId.value = undefined;
+    basicDialogRef.value?.openDialog();
+  };
+
+  // 打开对话框(编辑模式)
+  const openEditDialog = async (info: { id: number; planName?: string; purchaseDate?: string }) => {
+    isEditMode.value = true;
+    editId.value = info.id;
+    ruleFormData.planName = info.planName || '';
+    ruleFormData.purchaseDate = info.purchaseDate || '';
+    basicDialogRef.value?.openDialog();
+  };
+
+  // 提交表单
+  const handleSubmit = async () => {
+    const validate = await basicFormRef.value?.validateForm();
+    if (!validate) return;
+
+    try {
+      const submitData = ruleFormData.purchaseDate
+        ? {
+            planName: ruleFormData.planName,
+            purchaseDate: ruleFormData.purchaseDate,
+          }
+        : {
+            planName: ruleFormData.planName,
+          };
+
+      if (isEditMode.value && editId.value) {
+        await updateSupplyRequest({
+          id: editId.value,
+          ...submitData,
+        });
+        ElMessage.success('编辑成功');
+      } else {
+        await createSupplyRequest(submitData);
+        ElMessage.success('创建成功');
+      }
+
+      basicDialogRef.value?.closeDialog();
+      // 触发父组件刷新列表
+      emits('success');
+    } catch (error) {
+      console.error('提交失败:', error);
+      ElMessage.error(isEditMode.value ? '编辑失败' : '创建失败');
+    }
+  };
+
+  // 刷新表单数据
+  const refreshFormData = () => {
+    basicFormRef.value?.clearValidate();
+    ruleFormData.planName = '';
+    ruleFormData.purchaseDate = '';
+  };
+
+  defineExpose({
+    openAddDialog,
+    openEditDialog,
+  });
+</script>
+
+<style scoped lang="scss"></style>

+ 59 - 0
src/views/emergency/emergency-supplies/src/config/form.ts

@@ -327,3 +327,62 @@ export const SUPPLIES_DISCARD_FROM_DATA = {
 export const SUPPLIES_DISCARD_FROM_RULES = {
   quantity: [{ required: true, message: '请输入报废数量', trigger: 'blur' }],
 };
+
+// 物资申领计划表单配置
+export const SUPPLY_REQUEST_FORM_CONFIG: FormConfig[] = [
+  {
+    label: '物资申领计划:',
+    prop: 'planName',
+    component: 'ElInput',
+    componentProps: {
+      placeholder: '请输入物资申领计划',
+    },
+  },
+  {
+    label: '采购日期:',
+    prop: 'purchaseDate',
+    component: 'ElDatePicker',
+    componentProps: {
+      placeholder: '请选择采购日期',
+      type: 'date',
+      format: 'YYYY-MM-DD',
+      valueFormat: 'YYYY-MM-DD',
+    },
+  },
+];
+
+// 物资申领计划表单数据
+export const SUPPLY_REQUEST_FORM_DATA = {
+  planName: '',
+  purchaseDate: '',
+};
+
+// 物资申领计划表单规则
+export const SUPPLY_REQUEST_FORM_RULES = {
+  planName: [{ required: true, message: '请输入物资申领计划', trigger: 'blur' }],
+};
+
+// 发起采购表单配置
+export const START_PURCHASE_FORM_CONFIG: FormConfig[] = [
+  {
+    label: '采购日期:',
+    prop: 'purchaseDate',
+    component: 'ElDatePicker',
+    componentProps: {
+      placeholder: '请选择采购日期',
+      type: 'date',
+      format: 'YYYY-MM-DD',
+      valueFormat: 'YYYY-MM-DD',
+    },
+  },
+];
+
+// 发起采购表单数据
+export const START_PURCHASE_FORM_DATA = {
+  purchaseDate: '',
+};
+
+// 发起采购表单规则
+export const START_PURCHASE_FORM_RULES = {
+  purchaseDate: [{ required: true, message: '请选择采购日期', trigger: 'change' }],
+};

+ 22 - 1
src/views/emergency/emergency-supplies/src/config/index.ts

@@ -1,4 +1,4 @@
-import { SUPPLY_LIST_SEARCH_CONFIG, INVENTORY_LIST_SEARCH_CONFIG } from './search';
+import { SUPPLY_LIST_SEARCH_CONFIG, INVENTORY_LIST_SEARCH_CONFIG, SUPPLY_REQUEST_SEARCH_CONFIG } from './search';
 import {
   SUPPLY_LIST_TABLE_COLUMNS,
   SUPPLY_LIST_TABLE_OPTIONS,
@@ -10,6 +10,10 @@ import {
   INVENTORY_CHECK_TABLE_OPTIONS,
   INVENTORY_CHECK_TABLE_MAX_HEIGHT_DEFAULT,
   INVENTORY_CHECK_TABLE_MAX_HEIGHT_PERMISSION,
+  SUPPLY_REQUEST_TABLE_COLUMNS,
+  SUPPLY_REQUEST_TABLE_OPTIONS,
+  SUPPLY_REQUEST_TABLE_MAX_HEIGHT_DEFAULT,
+  SUPPLY_REQUEST_TABLE_MAX_HEIGHT_PERMISSION,
 } from './table';
 import {
   INVENTORY_TASK_FROM_CONFIG,
@@ -29,11 +33,18 @@ import {
   SUPPLIES_DISCARD_FROM_CONFIG,
   SUPPLIES_DISCARD_FROM_DATA,
   SUPPLIES_DISCARD_FROM_RULES,
+  SUPPLY_REQUEST_FORM_CONFIG,
+  SUPPLY_REQUEST_FORM_DATA,
+  SUPPLY_REQUEST_FORM_RULES,
+  START_PURCHASE_FORM_CONFIG,
+  START_PURCHASE_FORM_DATA,
+  START_PURCHASE_FORM_RULES,
 } from './form';
 
 export {
   SUPPLY_LIST_SEARCH_CONFIG,
   INVENTORY_LIST_SEARCH_CONFIG,
+  SUPPLY_REQUEST_SEARCH_CONFIG,
   SUPPLY_LIST_TABLE_COLUMNS,
   SUPPLY_LIST_TABLE_OPTIONS,
   SUPPLY_LIST_TABLE_MAX_HEIGHT_DEFAULT,
@@ -61,4 +72,14 @@ export {
   SUPPLIES_DISCARD_FROM_CONFIG,
   SUPPLIES_DISCARD_FROM_DATA,
   SUPPLIES_DISCARD_FROM_RULES,
+  SUPPLY_REQUEST_TABLE_COLUMNS,
+  SUPPLY_REQUEST_TABLE_OPTIONS,
+  SUPPLY_REQUEST_TABLE_MAX_HEIGHT_DEFAULT,
+  SUPPLY_REQUEST_TABLE_MAX_HEIGHT_PERMISSION,
+  SUPPLY_REQUEST_FORM_CONFIG,
+  SUPPLY_REQUEST_FORM_DATA,
+  SUPPLY_REQUEST_FORM_RULES,
+  START_PURCHASE_FORM_CONFIG,
+  START_PURCHASE_FORM_DATA,
+  START_PURCHASE_FORM_RULES,
 };

+ 19 - 1
src/views/emergency/emergency-supplies/src/config/search.ts

@@ -1,5 +1,5 @@
 import type { SearchConfig } from '@/types/basic-search';
-import { EMERGENCY_SUPPLY_STATUS_OPTIONS, INVENTORY_RESULT_OPTIONS } from '../constant';
+import { EMERGENCY_SUPPLY_STATUS_OPTIONS, INVENTORY_RESULT_OPTIONS, SUPPLY_REQUEST_STATUS_OPTIONS } from '../constant';
 
 const BASIC_SEARCH_CONFIG = {
   EMERGENCY_TYPE: {
@@ -79,3 +79,21 @@ export const INVENTORY_LIST_SEARCH_CONFIG: SearchConfig[] = [
     },
   },
 ];
+
+// 物资申领搜索配置
+export const SUPPLY_REQUEST_SEARCH_CONFIG: SearchConfig[] = [
+  {
+    label: '物资申领计划:',
+    prop: 'planName',
+    slot: 'planName',
+  },
+  {
+    label: '状态:',
+    prop: 'status',
+    component: 'ElSelect',
+    selectOptions: SUPPLY_REQUEST_STATUS_OPTIONS,
+    componentProps: {
+      placeholder: '请选择状态',
+    },
+  },
+];

+ 37 - 2
src/views/emergency/emergency-supplies/src/config/table.ts

@@ -228,7 +228,7 @@ export const INVENTORY_CHECK_TABLE_COLUMNS: TableColumnProps[] = [
     slot: 'imageList',
     minWidth: '120px',
     align: 'center',
-    fixed: 'right'
+    fixed: 'right',
   },
 ];
 
@@ -236,5 +236,40 @@ export const INVENTORY_CHECK_TABLE_MAX_HEIGHT_DEFAULT = 'calc(70vh - 100px)';
 export const INVENTORY_CHECK_TABLE_MAX_HEIGHT_PERMISSION = 'calc(70vh - 150px)';
 
 export const INVENTORY_CHECK_TABLE_OPTIONS = {
-  ...TABLE_OPTIONS
+  ...TABLE_OPTIONS,
+};
+
+// 物资申领表格样式配置
+export const SUPPLY_REQUEST_TABLE_OPTIONS = {
+  ...TABLE_OPTIONS,
 };
+
+export const SUPPLY_REQUEST_TABLE_MAX_HEIGHT_DEFAULT = 'calc(70vh - 100px)';
+export const SUPPLY_REQUEST_TABLE_MAX_HEIGHT_PERMISSION = 'calc(70vh - 150px)';
+
+// 物资申领表格列配置
+export const SUPPLY_REQUEST_TABLE_COLUMNS: TableColumnProps[] = [
+  BASIC_TABLE_COLUMNS.INDEX,
+  {
+    label: '物资申领计划',
+    prop: 'planName',
+    minWidth: '200px',
+  },
+  {
+    label: '采购日期',
+    prop: 'purchaseDate',
+    width: '180px',
+  },
+  {
+    label: '总价(元)',
+    prop: 'totalPrice',
+    minWidth: '140px',
+  },
+  {
+    label: '状态',
+    prop: 'status',
+    slot: 'status',
+    minWidth: '120px',
+  },
+  BASIC_TABLE_COLUMNS.ACTION,
+];

+ 44 - 0
src/views/emergency/emergency-supplies/src/constant/index.ts

@@ -32,12 +32,14 @@ enum CHANGE_TYPE {
   CHECK = 1,
   CHANGE,
   DISCARD,
+  REQUEST,
 }
 
 export const CHANGE_TYPE_MAP = {
   [CHANGE_TYPE.CHECK]: '物资盘点',
   [CHANGE_TYPE.CHANGE]: '数量变更',
   [CHANGE_TYPE.DISCARD]: '报废',
+  [CHANGE_TYPE.REQUEST]: '物资申领',
 };
 
 export enum INVENTORY_RESULT {
@@ -60,3 +62,45 @@ export const INVENTORY_RESULT_OPTIONS = [
   { label: INVENTORY_RESULT_MAP[INVENTORY_RESULT.DAMAGED], value: INVENTORY_RESULT.DAMAGED },
   { label: INVENTORY_RESULT_MAP[INVENTORY_RESULT.OVERDUE], value: INVENTORY_RESULT.OVERDUE },
 ];
+
+/**
+ * 物资申领计划状态
+ */
+export enum SUPPLY_REQUEST_STATUS {
+  ALL = 0,
+  APPLYING = 1, // 申请中
+  PURCHASING = 2, // 采购中
+  RECEIVING = 3, // 领用中
+  RECEIVED = 4, // 已领用
+}
+
+export const SUPPLY_REQUEST_STATUS_MAP = {
+  [SUPPLY_REQUEST_STATUS.ALL]: '全部',
+  [SUPPLY_REQUEST_STATUS.APPLYING]: '申请中',
+  [SUPPLY_REQUEST_STATUS.PURCHASING]: '采购中',
+  [SUPPLY_REQUEST_STATUS.RECEIVING]: '领用中',
+  [SUPPLY_REQUEST_STATUS.RECEIVED]: '已领用',
+};
+
+export const SUPPLY_REQUEST_STATUS_OPTIONS = [
+  { label: SUPPLY_REQUEST_STATUS_MAP[SUPPLY_REQUEST_STATUS.ALL], value: SUPPLY_REQUEST_STATUS.ALL },
+  { label: SUPPLY_REQUEST_STATUS_MAP[SUPPLY_REQUEST_STATUS.APPLYING], value: SUPPLY_REQUEST_STATUS.APPLYING },
+  { label: SUPPLY_REQUEST_STATUS_MAP[SUPPLY_REQUEST_STATUS.PURCHASING], value: SUPPLY_REQUEST_STATUS.PURCHASING },
+  { label: SUPPLY_REQUEST_STATUS_MAP[SUPPLY_REQUEST_STATUS.RECEIVING], value: SUPPLY_REQUEST_STATUS.RECEIVING },
+  { label: SUPPLY_REQUEST_STATUS_MAP[SUPPLY_REQUEST_STATUS.RECEIVED], value: SUPPLY_REQUEST_STATUS.RECEIVED },
+];
+
+/**
+ * 物资申领计划物资状态
+ */
+export enum SUPPLY_REQUEST_DETAIL_STATUS {
+  APPLYING = 1, // 申请中
+  PURCHASING = 2, // 采购中
+  RECEIVED = 3, // 已领用
+}
+
+export const SUPPLY_REQUEST_DETAIL_STATUS_MAP = {
+  [SUPPLY_REQUEST_DETAIL_STATUS.APPLYING]: '申请中',
+  [SUPPLY_REQUEST_DETAIL_STATUS.PURCHASING]: '采购中',
+  [SUPPLY_REQUEST_DETAIL_STATUS.RECEIVED]: '已领用',
+};

+ 3 - 0
src/views/emergency/src/constant.ts

@@ -9,4 +9,7 @@ export const EMERGENCY_PERMISSIONS = {
   // 应急处置-应急指挥中心管理权限:打开、编辑
   EMERGENCY_COMMAND_CENTER_MANAGE: 'emergency_business_module:emergency_command_center_manage',
   PLANE_MANAGEMENT: 'emergency_business_module:plan_management',
+
+  // 应急物资申领权限
+  SUPPLY_REQUEST_MANAGE: 'emergency_business_module:supply_request_manage',
 };