ZN
昨天 dbb8a7488164f41302c88c55c9add26d32cc69f3
feat: 添加工单附件和库存管理功能模块

- 新增生产工单模块,支持工单列表查看、流转卡生成和附件管理
- 新增库存管理模块,支持合格/不合格库存分页查询和状态展示
- 添加仓储物流菜单入口,调整销售服务菜单顺序
- 实现工单附件上传、预览和删除功能
- 实现库存冻结/解冻、增减操作接口
已添加9个文件
已修改2个文件
1014 ■■■■■ 文件已修改
src/api/inventoryManagement/stockInventory.js 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockUninventory.js 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productWorkOrderFile.js 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/workOrder.js 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inventoryManagement/stockManagement/Qualified.vue 151 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inventoryManagement/stockManagement/Unqualified.vue 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inventoryManagement/stockManagement/index.vue 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/workOrder/components/filesDia.vue 175 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/workOrder/index.vue 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/works.vue 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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,
    });
};
// è§£å†»åº“存记录
export const thawStockInventory = (params) => {
    return request({
        url: "/stockInventory/thawStock",
        method: "post",
        data: params,
    });
};
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,
    });
};
// è§£å†»åº“存记录
export const thawStockUninventory = (params) => {
    return request({
        url: "/stockUninventory/thawStock",
        method: "post",
        data: params,
    });
};
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,
  });
}
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",
  });
}
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": "规程与资质",
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>
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>
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>
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: '正在打开...' });
    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>
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>
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);