feat: 添加工单附件和库存管理功能模块
- 新增生产工单模块,支持工单列表查看、流转卡生成和附件管理
- 新增库存管理模块,支持合格/不合格库存分页查询和状态展示
- 添加仓储物流菜单入口,调整销售服务菜单顺序
- 实现工单附件上传、预览和删除功能
- 实现库存冻结/解冻、增减操作接口
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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, |
| | | }); |
| | | }; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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, |
| | | }); |
| | | }; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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", |
| | | }); |
| | | } |
| | |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/productionManagement/workOrder/index", |
| | | "style": { |
| | | "navigationBarTitleText": "ç产工å", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | // { |
| | | // "path": "pages/productionManagement/productionCosting/index", |
| | | // "style": { |
| | |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/inventoryManagement/stockManagement/index", |
| | | "style": { |
| | | "navigationBarTitleText": "åºå管ç", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/safeProduction/safeQualifications/index", |
| | | "style": { |
| | | "navigationBarTitleText": "è§ç¨ä¸èµè´¨", |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| | |
| | | </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"> |
| | |
| | | }, |
| | | ]); |
| | | |
| | | // ä»å¨ç©æµåè½æ°æ® |
| | | const warehouseLogisticsItems = reactive([ |
| | | { |
| | | icon: "/static/images/icon/xiaoshoutaizhang.svg", |
| | | label: "åºå管ç", |
| | | }, |
| | | ]); |
| | | |
| | | const humanResourcesItems = reactive([ |
| | | { |
| | | icon: "/static/images/icon/dakaqiandao.svg", |
| | |
| | | { |
| | | icon: "/static/images/icon/shengchanbaogong.svg", |
| | | label: "ç产æ¥å·¥", |
| | | }, |
| | | { |
| | | icon: "/static/images/icon/shengchanbaogong.svg", |
| | | label: "ç产工å", |
| | | }, |
| | | // { |
| | | // icon: "/static/images/icon/shengchanhesuan@2x.svg", |
| | |
| | | url: "/pages/productionManagement/processScheduling/index", |
| | | }); |
| | | break; |
| | | case "ç产工å": |
| | | uni.navigateTo({ |
| | | url: "/pages/productionManagement/workOrder/index", |
| | | }); |
| | | break; |
| | | case "ç产æ¥å·¥": |
| | | getcode(); |
| | | break; |
| | |
| | | case "åºåæ£éª": |
| | | uni.navigateTo({ |
| | | url: "/pages/qualityManagement/finalInspection/index", |
| | | }); |
| | | break; |
| | | case "åºå管ç": |
| | | uni.navigateTo({ |
| | | url: "/pages/inventoryManagement/stockManagement/index", |
| | | }); |
| | | break; |
| | | case "åé¦ç»è®°": |
| | |
| | | filterArray(collaborationItems, menuMapping.collaboration.specialMapping); |
| | | filterArray(safetyItems); |
| | | filterArray(humanResourcesItems); |
| | | filterArray(warehouseLogisticsItems); |
| | | filterArray(qualityItems); |
| | | filterArray(productionItems); |
| | | filterArray(equipmentItems); |
| | |
| | | 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); |
| | | |