From dbb8a7488164f41302c88c55c9add26d32cc69f3 Mon Sep 17 00:00:00 2001
From: ZN <zhang_12370@163.com>
Date: 星期二, 31 三月 2026 13:20:37 +0800
Subject: [PATCH] feat: 添加工单附件和库存管理功能模块

---
 src/pages.json                                                   |   14 
 src/api/productionManagement/workOrder.js                        |   35 ++
 src/api/inventoryManagement/stockUninventory.js                  |   45 ++
 src/pages/inventoryManagement/stockManagement/Qualified.vue      |  151 ++++++++
 src/pages/works.vue                                              |   46 ++
 src/pages/inventoryManagement/stockManagement/index.vue          |   57 +++
 src/pages/inventoryManagement/stockManagement/Unqualified.vue    |  134 +++++++
 src/pages/productionManagement/workOrder/index.vue               |  268 +++++++++++++++
 src/pages/productionManagement/workOrder/components/filesDia.vue |  175 ++++++++++
 src/api/inventoryManagement/stockInventory.js                    |   61 +++
 src/api/productionManagement/productWorkOrderFile.js             |   28 +
 11 files changed, 1,014 insertions(+), 0 deletions(-)

diff --git a/src/api/inventoryManagement/stockInventory.js b/src/api/inventoryManagement/stockInventory.js
new file mode 100644
index 0000000..dfa5f46
--- /dev/null
+++ b/src/api/inventoryManagement/stockInventory.js
@@ -0,0 +1,61 @@
+import request from "@/utils/request";
+// 鍒嗛〉鏌ヨ搴撳瓨璁板綍鍒楄〃
+export const getStockInventoryListPage = (params) => {
+    return request({
+        url: "/stockInventory/pagestockInventory",
+        method: "get",
+        params,
+    });
+};
+
+// 鍒涘缓搴撳瓨璁板綍
+export const createStockInventory = (params) => {
+    return request({
+        url: "/stockInventory/addstockInventory",
+        method: "post",
+        data: params,
+    });
+};
+
+// 鍑忓皯搴撳瓨璁板綍
+export const subtractStockInventory = (params) => {
+    return request({
+        url: "/stockInventory/subtractStockInventory",
+        method: "post",
+        data: params,
+    });
+};
+
+export const getStockInventoryReportList = (params) => {
+    return request({
+        url: "/stockInventory/stockInventoryPage",
+        method: "get",
+        params,
+    });
+};
+
+export const getStockInventoryInAndOutReportList = (params) => {
+    return request({
+        url: "/stockInventory/stockInAndOutRecord",
+        method: "get",
+        params,
+    });
+};
+
+// 鍐荤粨搴撳瓨璁板綍
+export const frozenStockInventory = (params) => {
+    return request({
+        url: "/stockInventory/frozenStock",
+        method: "post",
+        data: params,
+    });
+};
+
+// 瑙e喕搴撳瓨璁板綍
+export const thawStockInventory = (params) => {
+    return request({
+        url: "/stockInventory/thawStock",
+        method: "post",
+        data: params,
+    });
+};
diff --git a/src/api/inventoryManagement/stockUninventory.js b/src/api/inventoryManagement/stockUninventory.js
new file mode 100644
index 0000000..fee8908
--- /dev/null
+++ b/src/api/inventoryManagement/stockUninventory.js
@@ -0,0 +1,45 @@
+import request from "@/utils/request";
+// 鍒嗛〉鏌ヨ搴撳瓨璁板綍鍒楄〃
+export const getStockUninventoryListPage = (params) => {
+    return request({
+        url: "/stockUninventory/pagestockUninventory",
+        method: "get",
+        params,
+    });
+};
+
+// 鍒涘缓搴撳瓨璁板綍
+export const createStockUnInventory = (params) => {
+    return request({
+        url: "/stockUninventory/addstockUninventory",
+        method: "post",
+        data: params,
+    });
+};
+
+// 鍑忓皯搴撳瓨璁板綍
+export const subtractStockUnInventory = (params) => {
+    return request({
+        url: "/stockUninventory/subtractstockUninventory",
+        method: "post",
+        data: params,
+    });
+};
+
+// 鍐荤粨搴撳瓨璁板綍
+export const frozenStockUninventory = (params) => {
+    return request({
+        url: "/stockUninventory/frozenStock",
+        method: "post",
+        data: params,
+    });
+};
+
+// 瑙e喕搴撳瓨璁板綍
+export const thawStockUninventory = (params) => {
+    return request({
+        url: "/stockUninventory/thawStock",
+        method: "post",
+        data: params,
+    });
+};
diff --git a/src/api/productionManagement/productWorkOrderFile.js b/src/api/productionManagement/productWorkOrderFile.js
new file mode 100644
index 0000000..597cab6
--- /dev/null
+++ b/src/api/productionManagement/productWorkOrderFile.js
@@ -0,0 +1,28 @@
+import request from "@/utils/request";
+
+// 鏌ヨ宸ュ崟闄勪欢鍒楄〃
+export function productWorkOrderFileListPage(query) {
+  return request({
+    url: "/productWorkOrderFile/listPage",
+    method: "get",
+    params: query,
+  });
+}
+
+// 鏂板宸ュ崟闄勪欢
+export function productWorkOrderFileAdd(data) {
+  return request({
+    url: "/productWorkOrderFile/add",
+    method: "post",
+    data,
+  });
+}
+
+// 鍒犻櫎宸ュ崟闄勪欢
+export function productWorkOrderFileDel(data) {
+  return request({
+    url: "/productWorkOrderFile/del",
+    method: "delete",
+    data,
+  });
+}
diff --git a/src/api/productionManagement/workOrder.js b/src/api/productionManagement/workOrder.js
new file mode 100644
index 0000000..7e8bd86
--- /dev/null
+++ b/src/api/productionManagement/workOrder.js
@@ -0,0 +1,35 @@
+import request from "@/utils/request";
+
+export function productWorkOrderPage(query) {
+  return request({
+    url: "/productWorkOrder/page",
+    method: "get",
+    params: query,
+  });
+}
+
+export function updateProductWorkOrder(data) {
+  return request({
+    url: "/productWorkOrder/updateProductWorkOrder",
+    method: "post",
+    data: data,
+  });
+}
+
+export function addProductMain(data) {
+  return request({
+    url: "/productionProductMain/addProductMain",
+    method: "post",
+    data: data,
+  });
+}
+
+// 涓嬭浇宸ュ崟娴佽浆鍗★紙杩斿洖鏂囦欢娴侊級
+export function downProductWorkOrder(id) {
+  return request({
+    url: "/productWorkOrder/down",
+    method: "post",
+    data: { id },
+    responseType: "blob",
+  });
+}
diff --git a/src/pages.json b/src/pages.json
index 419c3df..060bdad 100644
--- a/src/pages.json
+++ b/src/pages.json
@@ -704,6 +704,13 @@
         "navigationStyle": "custom"
       }
     },
+    {
+      "path": "pages/productionManagement/workOrder/index",
+      "style": {
+        "navigationBarTitleText": "鐢熶骇宸ュ崟",
+        "navigationStyle": "custom"
+      }
+    },
     // {
     //   "path": "pages/productionManagement/productionCosting/index",
     //   "style": {
@@ -733,6 +740,13 @@
       }
     },
     {
+      "path": "pages/inventoryManagement/stockManagement/index",
+      "style": {
+        "navigationBarTitleText": "搴撳瓨绠$悊",
+        "navigationStyle": "custom"
+      }
+    },
+    {
       "path": "pages/safeProduction/safeQualifications/index",
       "style": {
         "navigationBarTitleText": "瑙勭▼涓庤祫璐�",
diff --git a/src/pages/inventoryManagement/stockManagement/Qualified.vue b/src/pages/inventoryManagement/stockManagement/Qualified.vue
new file mode 100644
index 0000000..12b9060
--- /dev/null
+++ b/src/pages/inventoryManagement/stockManagement/Qualified.vue
@@ -0,0 +1,151 @@
+<template>
+  <view class="qualified-record-container">
+    <view class="search-section">
+      <view class="search-bar">
+        <view class="search-input">
+          <up-input
+            class="search-text"
+            placeholder="璇疯緭鍏ヤ骇鍝佸ぇ绫�"
+            v-model="searchForm.productName"
+            @confirm="handleQuery"
+            clearable
+          />
+        </view>
+        <view class="filter-button" @click="handleQuery">
+          <up-icon name="search" size="24" color="#999"></up-icon>
+        </view>
+      </view>
+    </view>
+
+    <scroll-view scroll-y class="ledger-list" v-if="tableData.length > 0" @scrolltolower="loadMore">
+      <view v-for="item in tableData" :key="item.id" class="ledger-item" :class="{ 'low-stock': isLowStock(item) }">
+        <view class="item-header">
+          <view class="item-left">
+            <view class="document-icon">
+              <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
+            </view>
+            <text class="item-id">{{ item.productName }}</text>
+          </view>
+          <view class="item-right">
+            <text class="item-tag tag-type">鍚堟牸搴撳瓨</text>
+          </view>
+        </view>
+        
+        <up-divider></up-divider>
+
+        <view class="item-details">
+          <view class="detail-row">
+            <text class="detail-label">瑙勬牸鍨嬪彿</text>
+            <text class="detail-value">{{ item.model }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="detail-label">搴撳瓨鏁伴噺</text>
+            <text class="detail-value">{{ item.qualitity }} {{ item.unit }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="detail-label">閿佸畾/鍐荤粨</text>
+            <text class="detail-value">{{ item.lockedQuantity }} {{ item.unit }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="detail-label">鍙敤搴撳瓨</text>
+            <text class="detail-value" style="color: #2979ff; font-weight: bold;">{{ item.unLockedQuantity }} {{ item.unit }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="detail-label">搴撳瓨棰勮</text>
+            <text class="detail-value">{{ item.warnNum }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="detail-label">鏇存柊鏃堕棿</text>
+            <text class="detail-value">{{ item.updateTime }}</text>
+          </view>
+        </view>
+      </view>
+      <up-loadmore :status="loadStatus" />
+    </scroll-view>
+    <view v-else-if="!loading" class="no-data">
+      <up-empty mode="data" text="鏆傛棤搴撳瓨鏁版嵁"></up-empty>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted } from 'vue';
+import { getStockInventoryListPage } from "@/api/inventoryManagement/stockInventory.js";
+
+const tableData = ref([]);
+const loading = ref(false);
+const loadStatus = ref('loadmore');
+const page = reactive({ current: 1, size: 10 });
+const total = ref(0);
+const searchForm = reactive({ productName: '' });
+
+const handleQuery = () => {
+  page.current = 1;
+  tableData.value = [];
+  getList();
+};
+
+const getList = () => {
+  if (loading.value) return;
+  loading.value = true;
+  loadStatus.value = 'loading';
+  getStockInventoryListPage({ ...searchForm, ...page, type: 'qualified' }).then(res => {
+    loading.value = false;
+    const records = res.data.records || [];
+    tableData.value = page.current === 1 ? records : [...tableData.value, ...records];
+    total.value = res.data.total;
+    loadStatus.value = tableData.value.length >= total.value ? 'nomore' : 'loadmore';
+  }).catch(() => {
+    loading.value = false;
+    loadStatus.value = 'loadmore';
+  });
+};
+
+const loadMore = () => {
+  if (loadStatus.value === 'nomore' || loading.value) return;
+  page.current++;
+  getList();
+};
+
+const isLowStock = (row) => {
+  const stock = Number(row?.unLockedQuantity ?? 0);
+  const warn = Number(row?.warnNum ?? 0);
+  return Number.isFinite(stock) && Number.isFinite(warn) && stock < warn;
+};
+
+onMounted(() => {
+  getList();
+});
+</script>
+
+<style scoped lang="scss">
+@import '@/styles/sales-common.scss';
+
+.qualified-record-container {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
+.tag-type {
+  background-color: #e3f2fd;
+  color: #2196f3;
+  padding: 2px 8px;
+  border-radius: 4px;
+  font-size: 12px;
+}
+
+.ledger-list {
+  flex: 1;
+  overflow-y: auto;
+}
+
+.low-stock {
+  background-color: #fde2e2;
+  color: #c45656;
+}
+
+.no-data {
+  padding-top: 100px;
+}
+</style>
diff --git a/src/pages/inventoryManagement/stockManagement/Unqualified.vue b/src/pages/inventoryManagement/stockManagement/Unqualified.vue
new file mode 100644
index 0000000..48dafc4
--- /dev/null
+++ b/src/pages/inventoryManagement/stockManagement/Unqualified.vue
@@ -0,0 +1,134 @@
+<template>
+  <view class="unqualified-record-container">
+    <view class="search-section">
+      <view class="search-bar">
+        <view class="search-input">
+          <up-input
+            class="search-text"
+            placeholder="璇疯緭鍏ヤ骇鍝佸ぇ绫�"
+            v-model="searchForm.productName"
+            @confirm="handleQuery"
+            clearable
+          />
+        </view>
+        <view class="filter-button" @click="handleQuery">
+          <up-icon name="search" size="24" color="#999"></up-icon>
+        </view>
+      </view>
+    </view>
+
+    <scroll-view scroll-y class="ledger-list" v-if="tableData.length > 0" @scrolltolower="loadMore">
+      <view v-for="item in tableData" :key="item.id" class="ledger-item">
+        <view class="item-header">
+          <view class="item-left">
+            <view class="document-icon">
+              <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
+            </view>
+            <text class="item-id">{{ item.productName }}</text>
+          </view>
+          <view class="item-right">
+            <text class="item-tag tag-type" style="background-color: #fde2e2; color: #f56c6c;">涓嶅悎鏍煎簱瀛�</text>
+          </view>
+        </view>
+        
+        <up-divider></up-divider>
+
+        <view class="item-details">
+          <view class="detail-row">
+            <text class="detail-label">瑙勬牸鍨嬪彿</text>
+            <text class="detail-value">{{ item.model }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="detail-label">搴撳瓨鏁伴噺</text>
+            <text class="detail-value">{{ item.qualitity }} {{ item.unit }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="detail-label">閿佸畾/鍐荤粨</text>
+            <text class="detail-value">{{ item.lockedQuantity }} {{ item.unit }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="detail-label">鍙敤搴撳瓨</text>
+            <text class="detail-value" style="color: #f56c6c; font-weight: bold;">{{ item.unLockedQuantity }} {{ item.unit }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="detail-label">鏇存柊鏃堕棿</text>
+            <text class="detail-value">{{ item.updateTime }}</text>
+          </view>
+        </view>
+      </view>
+      <up-loadmore :status="loadStatus" />
+    </scroll-view>
+    <view v-else-if="!loading" class="no-data">
+      <up-empty mode="data" text="鏆傛棤涓嶅悎鏍煎簱瀛樻暟鎹�"></up-empty>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted } from 'vue';
+import { getStockUninventoryListPage } from "@/api/inventoryManagement/stockUninventory.js";
+
+const tableData = ref([]);
+const loading = ref(false);
+const loadStatus = ref('loadmore');
+const page = reactive({ current: 1, size: 10 });
+const total = ref(0);
+const searchForm = reactive({ productName: '' });
+
+const handleQuery = () => {
+  page.current = 1;
+  tableData.value = [];
+  getList();
+};
+
+const getList = () => {
+  if (loading.value) return;
+  loading.value = true;
+  loadStatus.value = 'loading';
+  getStockUninventoryListPage({ ...searchForm, ...page, type: 'unqualified' }).then(res => {
+    loading.value = false;
+    const records = res.data.records || [];
+    tableData.value = page.current === 1 ? records : [...tableData.value, ...records];
+    total.value = res.data.total;
+    loadStatus.value = tableData.value.length >= total.value ? 'nomore' : 'loadmore';
+  }).catch(() => {
+    loading.value = false;
+    loadStatus.value = 'loadmore';
+  });
+};
+
+const loadMore = () => {
+  if (loadStatus.value === 'nomore' || loading.value) return;
+  page.current++;
+  getList();
+};
+
+onMounted(() => {
+  getList();
+});
+</script>
+
+<style scoped lang="scss">
+@import '@/styles/sales-common.scss';
+
+.unqualified-record-container {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
+.tag-type {
+  padding: 2px 8px;
+  border-radius: 4px;
+  font-size: 12px;
+}
+
+.ledger-list {
+  flex: 1;
+  overflow-y: auto;
+}
+
+.no-data {
+  padding-top: 100px;
+}
+</style>
diff --git a/src/pages/inventoryManagement/stockManagement/index.vue b/src/pages/inventoryManagement/stockManagement/index.vue
new file mode 100644
index 0000000..98ebf44
--- /dev/null
+++ b/src/pages/inventoryManagement/stockManagement/index.vue
@@ -0,0 +1,57 @@
+<template>
+  <view class="app-container">
+    <PageHeader title="搴撳瓨绠$悊" @back="goBack" />
+    <up-tabs :list="tabs" @click="handleTabClick" :current="activeTab"/>
+    <swiper class="swiper-box" :current="activeTab" @change="handleSwiperChange">
+      <swiper-item class="swiper-item">
+        <qualified-record />
+      </swiper-item>
+      <swiper-item class="swiper-item">
+        <unqualified-record />
+      </swiper-item>
+    </swiper>
+  </view>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import PageHeader from "@/components/PageHeader.vue";
+import QualifiedRecord from "./Qualified.vue";
+import UnqualifiedRecord from "./Unqualified.vue";
+
+const activeTab = ref(0);
+const tabs = ref([
+  { name: '鍚堟牸搴撳瓨' },
+  { name: '涓嶅悎鏍煎簱瀛�' }
+]);
+
+const handleTabClick = (item) => {
+  activeTab.value = item.index;
+};
+
+const handleSwiperChange = (e) => {
+  activeTab.value = e.detail.current;
+};
+
+const goBack = () => {
+  uni.navigateBack();
+};
+</script>
+
+<style scoped lang="scss">
+.app-container {
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+  background-color: #f8f9fa;
+}
+.swiper-box {
+  flex: 1;
+}
+.swiper-item {
+  height: 100%;
+}
+:deep(.up-tabs) {
+  background-color: #fff;
+}
+</style>
diff --git a/src/pages/productionManagement/workOrder/components/filesDia.vue b/src/pages/productionManagement/workOrder/components/filesDia.vue
new file mode 100644
index 0000000..50ed8f3
--- /dev/null
+++ b/src/pages/productionManagement/workOrder/components/filesDia.vue
@@ -0,0 +1,175 @@
+<template>
+  <up-popup :show="show" mode="bottom" @close="close" round="20">
+    <view class="files-container">
+      <view class="header">
+        <text class="title">宸ュ崟闄勪欢</text>
+        <up-icon name="close" size="20" @click="close"></up-icon>
+      </view>
+      
+      <scroll-view scroll-y class="file-list">
+        <view v-if="tableData.length > 0">
+          <view v-for="(item, index) in tableData" :key="item.id || index" class="file-item">
+            <view class="file-info">
+              <up-icon name="file-text" size="24" color="#2979ff"></up-icon>
+              <text class="file-name">{{ item.name }}</text>
+            </view>
+            <view class="file-actions">
+              <up-button 
+                text="棰勮" 
+                size="mini" 
+                type="primary" 
+                plain 
+                @click="handlePreview(item)"
+              ></up-button>
+            </view>
+          </view>
+        </view>
+        <view v-else class="no-data">
+          <text>鏆傛棤闄勪欢</text>
+        </view>
+      </scroll-view>
+      
+      <view class="footer">
+        <up-button text="鍏抽棴" @click="close"></up-button>
+      </view>
+    </view>
+  </up-popup>
+</template>
+
+<script setup>
+import { ref, reactive } from "vue";
+import { productWorkOrderFileListPage } from "@/api/productionManagement/productWorkOrderFile.js";
+
+const show = ref(false);
+const currentWorkOrderId = ref("");
+const tableData = ref([]);
+const loading = ref(false);
+
+const openDialog = (row) => {
+  show.value = true;
+  currentWorkOrderId.value = row.id;
+  getList();
+};
+
+const close = () => {
+  show.value = false;
+};
+
+const getList = () => {
+  loading.value = true;
+  productWorkOrderFileListPage({
+    workOrderId: currentWorkOrderId.value,
+    current: 1,
+    size: 100,
+  })
+    .then((res) => {
+      tableData.value = res.data.records || [];
+    })
+    .finally(() => {
+      loading.value = false;
+    });
+};
+
+const handlePreview = (item) => {
+  const url = item.url;
+  if (!url) return;
+  
+  // 鍒ゆ柇鏄惁涓哄浘鐗�
+  const isImage = /\.(jpg|jpeg|png|gif|webp)$/i.test(url);
+  if (isImage) {
+    uni.previewImage({
+      urls: [url],
+      current: url
+    });
+  } else {
+    // 闈炲浘鐗囨枃浠跺皾璇曟墦寮�鏂囨。
+    uni.showLoading({ title: '姝e湪鎵撳紑...' });
+    uni.downloadFile({
+      url: url,
+      success: (res) => {
+        if (res.statusCode === 200) {
+          uni.openDocument({
+            filePath: res.tempFilePath,
+            success: () => {
+              uni.hideLoading();
+            },
+            fail: () => {
+              uni.hideLoading();
+              uni.showToast({ title: '鏆備笉鏀寔棰勮璇ョ被鍨嬫枃浠�', icon: 'none' });
+            }
+          });
+        }
+      },
+      fail: () => {
+        uni.hideLoading();
+        uni.showToast({ title: '涓嬭浇澶辫触', icon: 'none' });
+      }
+    });
+  }
+};
+
+defineExpose({
+  openDialog,
+});
+</script>
+
+<style scoped lang="scss">
+.files-container {
+  background-color: #fff;
+  padding: 20px;
+  height: 60vh;
+  display: flex;
+  flex-direction: column;
+}
+
+.header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+  
+  .title {
+    font-size: 18px;
+    font-weight: bold;
+  }
+}
+
+.file-list {
+  flex: 1;
+  overflow: hidden;
+}
+
+.file-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 15px 0;
+  border-bottom: 1px solid #f5f5f5;
+  
+  .file-info {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    flex: 1;
+    margin-right: 10px;
+    
+    .file-name {
+      font-size: 14px;
+      color: #333;
+      word-break: break-all;
+    }
+  }
+}
+
+.no-data {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 200px;
+  color: #999;
+}
+
+.footer {
+  margin-top: 20px;
+}
+</style>
diff --git a/src/pages/productionManagement/workOrder/index.vue b/src/pages/productionManagement/workOrder/index.vue
new file mode 100644
index 0000000..5360a11
--- /dev/null
+++ b/src/pages/productionManagement/workOrder/index.vue
@@ -0,0 +1,268 @@
+<template>
+  <view class="work-order">
+    <!-- 閫氱敤椤甸潰澶撮儴 -->
+    <PageHeader title="鐢熶骇宸ュ崟" @back="goBack" />
+    
+    <!-- 鎼滅储鍖哄煙 -->
+    <view class="search-section">
+      <view class="search-bar">
+        <view class="search-input">
+          <up-input
+            class="search-text"
+            placeholder="璇疯緭鍏ュ伐鍗曠紪鍙锋悳绱�"
+            v-model="searchForm.workOrderNo"
+            @confirm="handleQuery"
+            clearable
+          />
+        </view>
+        
+        <view class="filter-button" @click="handleQuery">
+          <up-icon name="search" size="24" color="#999"></up-icon>
+        </view>
+      </view>
+    </view>
+    
+    <!-- 宸ュ崟鍒楄〃 -->
+    <scroll-view scroll-y class="ledger-list" v-if="tableData.length > 0" @scrolltolower="loadMore">
+      <view v-for="(item, index) in tableData" :key="item.id || index" class="ledger-item">
+        <view class="item-header">
+          <view class="item-left">
+            <view class="document-icon">
+              <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
+            </view>
+            <text class="item-id">{{ item.workOrderNo }}</text>
+          </view>
+          <view class="item-right">
+            <text class="item-tag tag-type">{{ item.workOrderType }}</text>
+          </view>
+        </view>
+        
+        <up-divider></up-divider>
+        
+        <view class="item-details">
+          <view class="detail-row">
+            <text class="detail-label">浜у搧鍚嶇О</text>
+            <text class="detail-value">{{ item.productName }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="detail-label">瑙勬牸鍨嬪彿</text>
+            <text class="detail-value">{{ item.model }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="detail-label">宸ュ簭鍚嶇О</text>
+            <text class="detail-value">{{ item.processName }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="detail-label">闇�姹�/瀹屾垚鏁伴噺</text>
+            <text class="detail-value">{{ item.planQuantity }} / {{ item.completeQuantity }} {{ item.unit }}</text>
+          </view>
+          
+          <view class="progress-section">
+            <text class="detail-label">瀹屾垚杩涘害</text>
+            <view class="progress-bar">
+              <up-line-progress 
+                :percentage="toProgressPercentage(item.completionStatus)" 
+                activeColor="#2979ff"
+                :showText="true"
+              ></up-line-progress>
+            </view>
+          </view>
+          
+          <view class="detail-row">
+            <text class="detail-label">璁″垝寮�濮�</text>
+            <text class="detail-value">{{ item.planStartTime }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="detail-label">璁″垝缁撴潫</text>
+            <text class="detail-value">{{ item.planEndTime }}</text>
+          </view>
+        </view>
+      </view>
+      <up-loadmore :status="loadStatus" />
+    </scroll-view>
+    
+    <view v-else-if="!loading" class="no-data">
+      <up-empty mode="data" text="鏆傛棤宸ュ崟鏁版嵁"></up-empty>
+    </view>
+
+    <!-- 娴佽浆鍗″脊绐� -->
+    <up-popup :show="transferCardVisible" mode="center" @close="transferCardVisible = false" round="10">
+      <view class="qr-popup">
+        <text class="qr-title">宸ュ崟娴佽浆鍗′簩缁寸爜</text>
+        <view class="qr-box">
+          <geek-qrcode
+            v-if="transferCardRowData"
+            :val="String(transferCardRowData.id)"
+            :size="200"
+          />
+        </view>
+        <text class="qr-info" v-if="transferCardRowData">{{ transferCardRowData.workOrderNo }}</text>
+        <up-button text="鍏抽棴" @click="transferCardVisible = false" style="margin-top: 20px;"></up-button>
+      </view>
+    </up-popup>
+
+    <!-- 闄勪欢缁勪欢 -->
+    <FilesDia ref="workOrderFilesRef" />
+  </view>
+</template>
+
+<script setup>
+import { ref, reactive, toRefs, getCurrentInstance } from "vue";
+import { onShow } from '@dcloudio/uni-app';
+import { productWorkOrderPage } from "@/api/productionManagement/workOrder.js";
+import PageHeader from "@/components/PageHeader.vue";
+import FilesDia from "./components/filesDia.vue";
+
+const { proxy } = getCurrentInstance();
+
+const loading = ref(false);
+const tableData = ref([]);
+const loadStatus = ref('loadmore');
+const transferCardVisible = ref(false);
+const transferCardRowData = ref(null);
+const workOrderFilesRef = ref(null);
+
+const page = reactive({
+  current: 1,
+  size: 10,
+  total: 0,
+});
+
+const data = reactive({
+  searchForm: {
+    workOrderNo: "",
+  },
+});
+const { searchForm } = toRefs(data);
+
+const goBack = () => {
+  uni.navigateBack();
+};
+
+const handleQuery = () => {
+  page.current = 1;
+  tableData.value = [];
+  getList();
+};
+
+const getList = () => {
+  if (loading.value) return;
+  loading.value = true;
+  
+  const params = { ...searchForm.value, ...page };
+  
+  productWorkOrderPage(params).then((res) => {
+    loading.value = false;
+    const records = res.data.records || [];
+    tableData.value = page.current === 1 ? records : [...tableData.value, ...records];
+    page.total = res.data.total;
+    
+    if (tableData.value.length >= page.total) {
+      loadStatus.value = 'nomore';
+    } else {
+      loadStatus.value = 'loadmore';
+    }
+  }).catch(() => {
+    loading.value = false;
+    uni.showToast({ title: '鍔犺浇澶辫触', icon: 'error' });
+  });
+};
+
+const loadMore = () => {
+  if (loadStatus.value === 'nomore' || loading.value) return;
+  page.current++;
+  getList();
+};
+
+const toProgressPercentage = (val) => {
+  const n = Number(val);
+  if (!Number.isFinite(n)) return 0;
+  if (n <= 0) return 0;
+  if (n >= 100) return 100;
+  return Math.round(n);
+};
+
+const showTransferCard = (row) => {
+  transferCardRowData.value = row;
+  transferCardVisible.value = true;
+};
+
+const openWorkOrderFiles = (row) => {
+  workOrderFilesRef.value?.openDialog(row);
+};
+
+onShow(() => {
+  handleQuery();
+});
+</script>
+
+<style scoped lang="scss">
+@import '@/styles/sales-common.scss';
+
+.work-order {
+  min-height: 100vh;
+  background: #f8f9fa;
+}
+
+.tag-type {
+  background-color: #e3f2fd;
+  color: #2196f3;
+  padding: 2px 8px;
+  border-radius: 4px;
+  font-size: 12px;
+}
+
+.progress-section {
+  margin: 15px 0;
+  .detail-label {
+    display: block;
+    margin-bottom: 8px;
+    font-size: 13px;
+    color: #666;
+  }
+}
+
+.item-actions {
+  display: flex;
+  justify-content: flex-end;
+  gap: 10px;
+  padding: 12px 0;
+  border-top: 1px solid #f5f5f5;
+  
+  :deep(.up-button) {
+    margin: 0;
+    width: auto;
+  }
+}
+
+.qr-popup {
+  padding: 30px;
+  background-color: #fff;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  
+  .qr-title {
+    font-size: 18px;
+    font-weight: bold;
+    margin-bottom: 20px;
+  }
+  
+  .qr-box {
+    padding: 20px;
+    background-color: #fff;
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+    border-radius: 8px;
+    margin-bottom: 15px;
+  }
+  
+  .qr-info {
+    font-size: 14px;
+    color: #666;
+  }
+}
+
+.no-data {
+  padding-top: 100px;
+}
+</style>
diff --git a/src/pages/works.vue b/src/pages/works.vue
index 76ffe24..89c8d26 100644
--- a/src/pages/works.vue
+++ b/src/pages/works.vue
@@ -66,6 +66,28 @@
         </up-grid>
       </view>
     </view>
+    <!-- 浠撳偍鐗╂祦妯″潡 -->
+    <view class="common-module warehouse-logistics-module"
+          v-if="hasWarehouseLogisticsItems">
+      <view class="module-header">
+        <view class="module-title-container">
+          <text class="module-title">浠撳偍鐗╂祦</text>
+        </view>
+      </view>
+      <view class="module-content">
+        <up-grid :border="false"
+                 col="4">
+          <up-grid-item v-for="(item, index) in warehouseLogisticsItems"
+                        :key="index"
+                        @click="handleCommonItemClick(item)">
+            <view class="icon-container">
+              <image :src="item.icon" class="item-icon"></image>
+            </view>
+            <text class="item-label">{{item.label}}</text>
+          </up-grid-item>
+        </up-grid>
+      </view>
+    </view>
     <!-- 浜哄姏璧勬簮妯″潡 -->
     <view class="common-module collaboration-module"
           v-if="hasHumanResourcesItems">
@@ -371,6 +393,14 @@
     },
   ]);
 
+  // 浠撳偍鐗╂祦鍔熻兘鏁版嵁
+  const warehouseLogisticsItems = reactive([
+    {
+      icon: "/static/images/icon/xiaoshoutaizhang.svg",
+      label: "搴撳瓨绠$悊",
+    },
+  ]);
+
   const humanResourcesItems = reactive([
     {
       icon: "/static/images/icon/dakaqiandao.svg",
@@ -482,6 +512,10 @@
     {
       icon: "/static/images/icon/shengchanbaogong.svg",
       label: "鐢熶骇鎶ュ伐",
+    },
+    {
+      icon: "/static/images/icon/shengchanbaogong.svg",
+      label: "鐢熶骇宸ュ崟",
     },
     // {
     //   icon: "/static/images/icon/shengchanhesuan@2x.svg",
@@ -722,6 +756,11 @@
           url: "/pages/productionManagement/processScheduling/index",
         });
         break;
+      case "鐢熶骇宸ュ崟":
+        uni.navigateTo({
+          url: "/pages/productionManagement/workOrder/index",
+        });
+        break;
       case "鐢熶骇鎶ュ伐":
         getcode();
         break;
@@ -843,6 +882,11 @@
       case "鍑哄巶妫�楠�":
         uni.navigateTo({
           url: "/pages/qualityManagement/finalInspection/index",
+        });
+        break;
+      case "搴撳瓨绠$悊":
+        uni.navigateTo({
+          url: "/pages/inventoryManagement/stockManagement/index",
         });
         break;
       case "鍙嶉鐧昏":
@@ -1066,6 +1110,7 @@
     filterArray(collaborationItems, menuMapping.collaboration.specialMapping);
     filterArray(safetyItems);
     filterArray(humanResourcesItems);
+    filterArray(warehouseLogisticsItems);
     filterArray(qualityItems);
     filterArray(productionItems);
     filterArray(equipmentItems);
@@ -1081,6 +1126,7 @@
   const hasSafetyItems = computed(() => safetyItems.length > 0);
   const hasQualityItems = computed(() => qualityItems.length > 0);
   const hasHumanResourcesItems = computed(() => humanResourcesItems.length > 0);
+  const hasWarehouseLogisticsItems = computed(() => warehouseLogisticsItems.length > 0);
   const hasProductionItems = computed(() => productionItems.length > 0);
   const hasEquipmentItems = computed(() => equipmentItems.length > 0);
 

--
Gitblit v1.9.3