feat(耗材物流): 新增耗材物流模块相关页面与接口
- 添加耗材入库管理、出库台账、库存管理和库存报表页面
- 实现耗材合格/不合格库存的增删改查功能
- 新增通用分页组件和产品选择对话框组件
- 在首页添加耗材物料模块入口
- 集成耗材物流相关API接口
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | export const getConsumablesInListPage = (params) => { |
| | | return request({ |
| | | url: "/consumablesInventory/pageConsumablesInventory", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | }; |
| | | |
| | | export const createConsumablesIn = (params) => { |
| | | return request({ |
| | | url: "/consumablesInventory/addConsumablesInventory", |
| | | method: "post", |
| | | data: params, |
| | | }); |
| | | }; |
| | | |
| | | export const subtractConsumablesIn = (params) => { |
| | | return request({ |
| | | url: "/consumablesInventory/subtractConsumablesInventory", |
| | | method: "post", |
| | | data: params, |
| | | }); |
| | | }; |
| | | |
| | | export const getConsumablesInReportList = (params) => { |
| | | return request({ |
| | | url: "/consumablesInventory/ConsumablesInventoryPage", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | }; |
| | | |
| | | export const getConsumablesInInAndOutReportList = (params) => { |
| | | return request({ |
| | | url: "/consumablesInventory/ConsumablesInAndOutRecord", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | }; |
| | | |
| | | export const frozenConsumablesIn = (params) => { |
| | | return request({ |
| | | url: "/consumablesInventory/frozenConsumables", |
| | | method: "post", |
| | | data: params, |
| | | }); |
| | | }; |
| | | |
| | | export const thawConsumablesIn = (params) => { |
| | | return request({ |
| | | url: "/consumablesInventory/thawConsumables", |
| | | method: "post", |
| | | data: params, |
| | | }); |
| | | }; |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | export const getConsumablesOutRecordPage = (params) => { |
| | | return request({ |
| | | url: "/consumablesOutRecord/listPage", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | }; |
| | | |
| | | export const delConsumablesOutRecord = (ids) => { |
| | | return request({ |
| | | url: "/consumablesOutRecord", |
| | | method: "delete", |
| | | data: ids, |
| | | }); |
| | | }; |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | export const getConsumablesUninventoryListPage = (params) => { |
| | | return request({ |
| | | url: "/consumablesUnInventory/pageConsumablesUnInventory", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | }; |
| | | |
| | | export const createConsumablesUnInventory = (params) => { |
| | | return request({ |
| | | url: "/consumablesUninventory/addstockUninventory", |
| | | method: "post", |
| | | data: params, |
| | | }); |
| | | }; |
| | | |
| | | export const subtractConsumablesUnInventory = (params) => { |
| | | return request({ |
| | | url: "/consumablesUninventory/subtractstockUninventory", |
| | | method: "post", |
| | | data: params, |
| | | }); |
| | | }; |
| | | |
| | | export const frozenConsumablesUninventory = (params) => { |
| | | return request({ |
| | | url: "/consumablesUninventory/frozenStock", |
| | | method: "post", |
| | | data: params, |
| | | }); |
| | | }; |
| | | |
| | | export const thawConsumablesUninventory = (params) => { |
| | | return request({ |
| | | url: "/consumablesUninventory/thawStock", |
| | | method: "post", |
| | | data: params, |
| | | }); |
| | | }; |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="pagination-container" v-show="total > 0"> |
| | | <el-pagination |
| | | v-model:current-page="currentPage" |
| | | v-model:page-size="pageSize" |
| | | :total="total" |
| | | :layout="layout" |
| | | :page-sizes="pageSizes" |
| | | background |
| | | @size-change="handleSizeChange" |
| | | @current-change="handleCurrentChange" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed } from "vue"; |
| | | |
| | | const props = defineProps({ |
| | | total: { type: Number, default: 0 }, |
| | | page: { type: Number, default: 1 }, |
| | | limit: { type: Number, default: 10 }, |
| | | pageSizes: { |
| | | type: Array, |
| | | default: () => [10, 20, 50, 100], |
| | | }, |
| | | layout: { |
| | | type: String, |
| | | default: "total, sizes, prev, pager, next, jumper", |
| | | }, |
| | | }); |
| | | |
| | | const emit = defineEmits(["pagination"]); |
| | | |
| | | const currentPage = computed({ |
| | | get: () => props.page, |
| | | set: val => { |
| | | emit("pagination", { page: val, limit: props.limit }); |
| | | }, |
| | | }); |
| | | |
| | | const pageSize = computed({ |
| | | get: () => props.limit, |
| | | set: val => { |
| | | emit("pagination", { page: 1, limit: val }); |
| | | }, |
| | | }); |
| | | |
| | | const handleSizeChange = val => { |
| | | emit("pagination", { page: 1, limit: val }); |
| | | }; |
| | | |
| | | const handleCurrentChange = val => { |
| | | emit("pagination", { page: val, limit: props.limit }); |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .pagination-container { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | padding-top: 12px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <el-dialog |
| | | v-model="visibleProxy" |
| | | title="éæ©äº§å" |
| | | width="900px" |
| | | @close="handleClose" |
| | | > |
| | | <div class="search-row"> |
| | | <el-input |
| | | v-model="keyword" |
| | | placeholder="请è¾å
¥äº§ååç§°/è§æ ¼æç´¢" |
| | | clearable |
| | | style="width: 320px" |
| | | @keyup.enter="handleQuery" |
| | | /> |
| | | <el-button type="primary" @click="handleQuery">æ¥è¯¢</el-button> |
| | | <el-button @click="handleReset">éç½®</el-button> |
| | | </div> |
| | | |
| | | <el-table |
| | | v-loading="loading" |
| | | :data="tableData" |
| | | border |
| | | height="420" |
| | | style="width: 100%" |
| | | @selection-change="handleSelectionChange" |
| | | @row-click="handleRowClick" |
| | | > |
| | | <el-table-column |
| | | v-if="!single" |
| | | type="selection" |
| | | width="55" |
| | | align="center" |
| | | /> |
| | | <el-table-column label="产ååç§°" prop="productName" min-width="180" /> |
| | | <el-table-column label="è§æ ¼åå·" prop="model" min-width="180" /> |
| | | <el-table-column label="åä½" prop="unit" width="100" /> |
| | | <el-table-column label="产åç±»å" prop="parentName" width="120" /> |
| | | </el-table> |
| | | |
| | | <Pagination |
| | | :total="total" |
| | | :page="page.current" |
| | | :limit="page.size" |
| | | @pagination="handlePagination" |
| | | /> |
| | | |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" :disabled="selectedRows.length === 0" @click="handleConfirm"> |
| | | ç¡®å® |
| | | </el-button> |
| | | <el-button @click="visibleProxy = false">åæ¶</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, onMounted, reactive, ref, watch } from "vue"; |
| | | import Pagination from "@/components/PIMTable/Pagination.vue"; |
| | | import { modelListPage } from "@/api/basicData/product.js"; |
| | | |
| | | const props = defineProps({ |
| | | modelValue: { type: Boolean, default: false }, |
| | | single: { type: Boolean, default: false }, |
| | | }); |
| | | |
| | | const emit = defineEmits(["update:modelValue", "confirm"]); |
| | | |
| | | const visibleProxy = computed({ |
| | | get: () => props.modelValue, |
| | | set: val => emit("update:modelValue", val), |
| | | }); |
| | | |
| | | const keyword = ref(""); |
| | | const loading = ref(false); |
| | | const tableData = ref([]); |
| | | const selectedRows = ref([]); |
| | | const total = ref(0); |
| | | |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 10, |
| | | }); |
| | | |
| | | const normalizeRow = row => { |
| | | if (!row) return row; |
| | | return { |
| | | ...row, |
| | | productId: row.productId ?? row.product_id ?? row.product?.id, |
| | | productName: row.productName ?? row.product_name ?? row.name, |
| | | model: row.model ?? row.productModelName ?? row.modelName ?? row.specificationModel, |
| | | id: row.id ?? row.productModelId ?? row.modelId, |
| | | unit: row.unit ?? row.productUnit, |
| | | productType: row.productType ?? row.type, |
| | | }; |
| | | }; |
| | | |
| | | const fetchList = () => { |
| | | loading.value = true; |
| | | const params = { |
| | | current: page.current, |
| | | size: page.size, |
| | | }; |
| | | if (keyword.value) { |
| | | params.keyword = keyword.value; |
| | | params.productName = keyword.value; |
| | | params.model = keyword.value; |
| | | } |
| | | modelListPage(params) |
| | | .then(res => { |
| | | const records = res?.data?.records || []; |
| | | tableData.value = records.map(normalizeRow); |
| | | total.value = res?.data?.total || 0; |
| | | }) |
| | | .finally(() => { |
| | | loading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | const handleQuery = () => { |
| | | page.current = 1; |
| | | fetchList(); |
| | | }; |
| | | |
| | | const handleReset = () => { |
| | | keyword.value = ""; |
| | | page.current = 1; |
| | | fetchList(); |
| | | }; |
| | | |
| | | const handlePagination = ({ page: p, limit }) => { |
| | | page.current = p; |
| | | page.size = limit; |
| | | fetchList(); |
| | | }; |
| | | |
| | | const handleSelectionChange = selection => { |
| | | selectedRows.value = Array.isArray(selection) ? selection : []; |
| | | }; |
| | | |
| | | const handleRowClick = row => { |
| | | if (!props.single) return; |
| | | selectedRows.value = row ? [row] : []; |
| | | }; |
| | | |
| | | const handleConfirm = () => { |
| | | if (selectedRows.value.length === 0) return; |
| | | emit("confirm", selectedRows.value); |
| | | visibleProxy.value = false; |
| | | }; |
| | | |
| | | const handleClose = () => { |
| | | selectedRows.value = []; |
| | | }; |
| | | |
| | | watch( |
| | | () => props.modelValue, |
| | | val => { |
| | | if (!val) return; |
| | | page.current = 1; |
| | | selectedRows.value = []; |
| | | fetchList(); |
| | | } |
| | | ); |
| | | |
| | | onMounted(() => { |
| | | if (props.modelValue) fetchList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .search-row { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | padding-bottom: 12px; |
| | | } |
| | | </style> |
| | |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/consumablesLogistics/receiptManagement/index", |
| | | "style": { |
| | | "navigationBarTitleText": "èæå
¥åºç®¡ç", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/consumablesLogistics/receiptManagement/view", |
| | | "style": { |
| | | "navigationBarTitleText": "å
¥åºè¯¦æ
", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/consumablesLogistics/dispatchLog/index", |
| | | "style": { |
| | | "navigationBarTitleText": "èæåºåºå°è´¦", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/consumablesLogistics/dispatchLog/view", |
| | | "style": { |
| | | "navigationBarTitleText": "åºåºè¯¦æ
", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/consumablesLogistics/stockManagement/index", |
| | | "style": { |
| | | "navigationBarTitleText": "èæåºå管ç", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/consumablesLogistics/stockManagement/add", |
| | | "style": { |
| | | "navigationBarTitleText": "æ°å¢åºå", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/consumablesLogistics/stockManagement/subtract", |
| | | "style": { |
| | | "navigationBarTitleText": "åºåº", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/consumablesLogistics/stockManagement/view", |
| | | "style": { |
| | | "navigationBarTitleText": "åºå详æ
", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/consumablesLogistics/stockReport/index", |
| | | "style": { |
| | | "navigationBarTitleText": "èæåºåæ¥è¡¨", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/safeProduction/safeQualifications/index", |
| | | "style": { |
| | | "navigationBarTitleText": "è§ç¨ä¸èµè´¨", |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <div class="search_form"> |
| | | <div> |
| | | <span class="search_title ml10">åºåºæ¥æï¼</span> |
| | | <el-date-picker |
| | | v-model="searchForm.timeStr" |
| | | type="date" |
| | | placeholder="è¯·éæ©æ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | clearable |
| | | @change="handleQuery" |
| | | /> |
| | | <span class="search_title ml10">产ååç§°ï¼</span> |
| | | <el-input |
| | | v-model="searchForm.productName" |
| | | style="width: 240px" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | @keyup.enter="handleQuery" |
| | | /> |
| | | <span class="search_title ml10">æ¥æºï¼</span> |
| | | <el-select |
| | | v-model="searchForm.recordType" |
| | | style="width: 240px" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | > |
| | | <el-option |
| | | v-for="item in stockRecordTypeOptions" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | <el-button type="primary" @click="handleQuery" style="margin-left: 10px">æç´¢</el-button> |
| | | </div> |
| | | <div> |
| | | <el-button type="danger" plain @click="handleDelete">å é¤</el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="table_list"> |
| | | <el-table |
| | | :data="tableData" |
| | | border |
| | | v-loading="tableLoading" |
| | | @selection-change="handleSelectionChange" |
| | | style="width: 100%" |
| | | height="calc(100vh - 18.5em)" |
| | | > |
| | | <el-table-column align="center" type="selection" width="55" /> |
| | | <el-table-column align="center" label="åºå·" type="index" width="60" /> |
| | | <el-table-column label="åºåºæ¶é´" prop="createTime" width="160" show-overflow-tooltip /> |
| | | <el-table-column label="产ååç§°" prop="productName" min-width="160" show-overflow-tooltip /> |
| | | <el-table-column label="è§æ ¼åå·" prop="model" min-width="160" show-overflow-tooltip /> |
| | | <el-table-column label="åä½" prop="unit" width="100" show-overflow-tooltip /> |
| | | <el-table-column label="åºåºæ°é" prop="stockOutNum" width="110" show-overflow-tooltip /> |
| | | <el-table-column label="åºåºäºº" prop="createBy" width="120" show-overflow-tooltip /> |
| | | <el-table-column label="æ¥æº" prop="recordType" width="140" show-overflow-tooltip> |
| | | <template #default="scope"> |
| | | {{ getRecordType(scope.row.recordType) }} |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <Pagination |
| | | v-show="total > 0" |
| | | :total="total" |
| | | :page="page.current" |
| | | :limit="page.size" |
| | | @pagination="paginationChange" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { onMounted, reactive, ref, toRefs, watch } from "vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import Pagination from "@/components/PIMTable/Pagination.vue"; |
| | | import { |
| | | delConsumablesOutRecord, |
| | | getConsumablesOutRecordPage, |
| | | } from "@/api/consumablesLogistics/consumablesOutRecord.js"; |
| | | import { |
| | | findAllQualifiedStockOutRecordTypeOptions, |
| | | findAllUnQualifiedStockOutRecordTypeOptions, |
| | | } from "@/api/basicData/enum.js"; |
| | | |
| | | const props = defineProps({ |
| | | type: { |
| | | type: String, |
| | | required: true, |
| | | default: "0", |
| | | }, |
| | | }); |
| | | |
| | | const tableData = ref([]); |
| | | const selectedRows = ref([]); |
| | | const tableLoading = ref(false); |
| | | const stockRecordTypeOptions = ref([]); |
| | | |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 100, |
| | | }); |
| | | const total = ref(0); |
| | | |
| | | const data = reactive({ |
| | | searchForm: { |
| | | timeStr: "", |
| | | productName: "", |
| | | recordType: "", |
| | | }, |
| | | }); |
| | | const { searchForm } = toRefs(data); |
| | | |
| | | const getRecordType = (recordType) => { |
| | | return stockRecordTypeOptions.value.find((item) => item.value === recordType)?.label || ""; |
| | | }; |
| | | |
| | | const fetchStockRecordTypeOptions = () => { |
| | | const api = |
| | | props.type === "1" |
| | | ? findAllUnQualifiedStockOutRecordTypeOptions |
| | | : findAllQualifiedStockOutRecordTypeOptions; |
| | | api() |
| | | .then((res) => { |
| | | stockRecordTypeOptions.value = res.data || []; |
| | | }) |
| | | .catch(() => { |
| | | stockRecordTypeOptions.value = []; |
| | | }); |
| | | }; |
| | | |
| | | const handleQuery = () => { |
| | | page.current = 1; |
| | | getList(); |
| | | }; |
| | | |
| | | const paginationChange = obj => { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | getList(); |
| | | }; |
| | | |
| | | const getList = () => { |
| | | tableLoading.value = true; |
| | | getConsumablesOutRecordPage({ ...searchForm.value, ...page, type: props.type }) |
| | | .then(res => { |
| | | tableData.value = res?.data?.records || []; |
| | | total.value = res?.data?.total || 0; |
| | | }) |
| | | .finally(() => { |
| | | tableLoading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | const handleSelectionChange = selection => { |
| | | selectedRows.value = Array.isArray(selection) ? selection : []; |
| | | }; |
| | | |
| | | const handleDelete = () => { |
| | | const ids = selectedRows.value.map(i => i.id).filter(Boolean); |
| | | if (ids.length === 0) { |
| | | ElMessage.warning("è¯·éæ©æ°æ®"); |
| | | return; |
| | | } |
| | | ElMessageBox.confirm("éä¸çå
容å°è¢«å é¤ï¼æ¯å¦ç¡®è®¤å é¤ï¼", "å é¤", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }) |
| | | .then(() => delConsumablesOutRecord(ids)) |
| | | .then(() => { |
| | | ElMessage.success("å 餿å"); |
| | | getList(); |
| | | }) |
| | | .catch(() => {}); |
| | | }; |
| | | |
| | | watch( |
| | | () => props.type, |
| | | () => { |
| | | searchForm.value.recordType = ""; |
| | | fetchStockRecordTypeOptions(); |
| | | handleQuery(); |
| | | } |
| | | ); |
| | | |
| | | onMounted(() => { |
| | | fetchStockRecordTypeOptions(); |
| | | getList(); |
| | | }); |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="dispatch-page"> |
| | | <PageHeader title="åºåºå°è´¦" @back="goBack" /> |
| | | <view class="tabs-wrap"> |
| | | <view |
| | | v-for="tab in tabs" |
| | | :key="tab.name" |
| | | class="tab-item" |
| | | :class="{ active: activeTab === tab.name }" |
| | | @click="activeTab = tab.name" |
| | | > |
| | | <text>{{ tab.label }}</text> |
| | | </view> |
| | | </view> |
| | | <view class="search-section"> |
| | | <view class="search-row"> |
| | | <view class="search-input-wrap"> |
| | | <up-input v-model="searchForm.productName" placeholder="产å大类" clearable /> |
| | | </view> |
| | | <view class="btn-search" @click="handleQuery"> |
| | | <view class="btn-search-inner"> |
| | | <up-icon name="search" size="22" color="#fff"></up-icon> |
| | | <text>æç´¢</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view class="list-section"> |
| | | <view v-if="tableData.length > 0"> |
| | | <view v-for="(item, index) in tableData" :key="item.id || index" class="card-item"> |
| | | <view class="card-click" @click="goDetail(item)"> |
| | | <view class="card-header"> |
| | | <view class="header-main"> |
| | | <text class="product-name">{{ item.productName }}</text> |
| | | <text class="outbound-date">{{ item.createTime }}</text> |
| | | </view> |
| | | </view> |
| | | <up-divider /> |
| | | <view class="card-body"> |
| | | <view class="row"><text class="l">è§æ ¼åå·</text><text class="r">{{ item.model }}</text></view> |
| | | <view class="row"><text class="l">åä½</text><text class="r">{{ item.unit }}</text></view> |
| | | <view class="row"><text class="l">åºåºæ°é</text><text class="r highlight">{{ item.stockOutNum }}</text></view> |
| | | <view class="row"><text class="l">åºåºäºº</text><text class="r">{{ item.createBy }}</text></view> |
| | | <view class="row" v-if="item.recordType !== undefined"><text class="l">æ¥æº</text><text class="r">{{ getRecordType(item.recordType) || item.recordType }}</text></view> |
| | | <view class="row"><text class="l">æ¯é(å¨)</text><text class="r">{{ item.grossWeight ?? '-' }}</text></view> |
| | | <view class="row"><text class="l">ç®é(å¨)</text><text class="r">{{ item.tareWeight ?? '-' }}</text></view> |
| | | <view class="row"><text class="l">åé(å¨)</text><text class="r">{{ item.netWeight ?? '-' }}</text></view> |
| | | <view class="row"><text class="l">è¿ç£
æ¥æ</text><text class="r">{{ item.weighingDate || '-' }}</text></view> |
| | | <view class="row"><text class="l">è¿ç£
å</text><text class="r">{{ item.weighingOperator || '-' }}</text></view> |
| | | </view> |
| | | </view> |
| | | <view class="card-actions"> |
| | | <view class="btn-delete" @click.stop="handleDeleteSingle(item)">å é¤</view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else class="no-data">ææ æ°æ®</view> |
| | | </view> |
| | | <view class="load-more-wrap" v-if="tableData.length > 0"> |
| | | <u-loadmore :status="loadStatus" @loadmore="loadMore" /> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { reactive, ref, toRefs, watch } from "vue"; |
| | | import { onReachBottom, onShow } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import { getConsumablesOutRecordPage, delConsumablesOutRecord } from "@/api/consumablesLogistics/consumablesOutRecord.js"; |
| | | import { findAllQualifiedStockOutRecordTypeOptions, findAllUnQualifiedStockOutRecordTypeOptions } from "@/api/basicData/enum.js"; |
| | | |
| | | const activeTab = ref("qualified"); |
| | | const stockRecordTypeOptions = ref([]); |
| | | const tabs = [ |
| | | { label: "åæ ¼åºåº", name: "qualified", type: "0" }, |
| | | { label: "ä¸åæ ¼åºåº", name: "unqualified", type: "1" }, |
| | | ]; |
| | | const tableData = ref([]); |
| | | const total = ref(0); |
| | | const loadStatus = ref("loadmore"); |
| | | const page = reactive({ current: 1, size: 20 }); |
| | | const data = reactive({ searchForm: { productName: "" } }); |
| | | const { searchForm } = toRefs(data); |
| | | |
| | | const currentType = () => tabs.find((t) => t.name === activeTab.value)?.type || "0"; |
| | | |
| | | function getRecordType(recordType) { |
| | | if (recordType == null || recordType === "") return ""; |
| | | return stockRecordTypeOptions.value.find((item) => item.value === recordType)?.label || ""; |
| | | } |
| | | |
| | | function fetchRecordTypeOptions() { |
| | | const api = |
| | | currentType() === "1" |
| | | ? findAllUnQualifiedStockOutRecordTypeOptions |
| | | : findAllQualifiedStockOutRecordTypeOptions; |
| | | api() |
| | | .then((res) => { |
| | | const list = res.data != null ? res.data : res; |
| | | stockRecordTypeOptions.value = Array.isArray(list) ? list : []; |
| | | }) |
| | | .catch(() => { |
| | | stockRecordTypeOptions.value = []; |
| | | }); |
| | | } |
| | | |
| | | const getList = () => { |
| | | const isFirstPage = page.current === 1; |
| | | if (isFirstPage) { |
| | | uni.showLoading({ title: "å è½½ä¸...", mask: true }); |
| | | } |
| | | getConsumablesOutRecordPage({ |
| | | ...page, |
| | | type: currentType(), |
| | | productName: searchForm.value.productName, |
| | | }) |
| | | .then((res) => { |
| | | uni.hideLoading(); |
| | | const records = res.data?.records || []; |
| | | const totalCount = res.data?.total || 0; |
| | | if (isFirstPage) { |
| | | tableData.value = records; |
| | | fetchRecordTypeOptions(); |
| | | } else { |
| | | tableData.value = [...tableData.value, ...records]; |
| | | } |
| | | total.value = totalCount; |
| | | loadStatus.value = tableData.value.length >= totalCount || totalCount === 0 ? "nomore" : "loadmore"; |
| | | }) |
| | | .catch(() => { |
| | | uni.hideLoading(); |
| | | loadStatus.value = "error"; |
| | | if (isFirstPage) { |
| | | uni.showToast({ title: "å 载失败", icon: "none" }); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | const loadMore = () => { |
| | | if (loadStatus.value === "nomore" || loadStatus.value === "loading") return; |
| | | loadStatus.value = "loading"; |
| | | page.current++; |
| | | getList(); |
| | | }; |
| | | |
| | | watch(activeTab, () => { |
| | | page.current = 1; |
| | | loadStatus.value = "loadmore"; |
| | | stockRecordTypeOptions.value = []; |
| | | getList(); |
| | | }); |
| | | |
| | | const handleQuery = () => { |
| | | page.current = 1; |
| | | loadStatus.value = "loadmore"; |
| | | getList(); |
| | | }; |
| | | |
| | | const goDetail = (item) => { |
| | | if (!item?.id) return; |
| | | try { |
| | | uni.setStorageSync( |
| | | "dispatchDetailItem", |
| | | JSON.stringify({ |
| | | item, |
| | | type: currentType(), |
| | | }) |
| | | ); |
| | | } catch (e) {} |
| | | uni.navigateTo({ |
| | | url: "/pages/consumablesLogistics/dispatchLog/view?id=" + item.id, |
| | | }); |
| | | }; |
| | | |
| | | const handleDeleteSingle = (item) => { |
| | | if (!item?.id) return; |
| | | uni.showModal({ |
| | | title: "å é¤", |
| | | content: "确认å é¤è¯¥æ¡åºåºè®°å½ï¼", |
| | | success: (res) => { |
| | | if (!res.confirm) return; |
| | | delConsumablesOutRecord([item.id]) |
| | | .then(() => { |
| | | uni.showToast({ title: "å 餿å", icon: "success" }); |
| | | getList(); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "å é¤å¤±è´¥", icon: "none" }); |
| | | }); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | onShow(() => getList()); |
| | | onReachBottom(() => loadMore()); |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .dispatch-page { min-height: 100vh; background: #f5f5f5; padding-bottom: 40rpx; } |
| | | .tabs-wrap { display: flex; background: #fff; padding: 24rpx; gap: 24rpx; } |
| | | .tab-item { flex: 1; text-align: center; padding: 20rpx; border-radius: 12rpx; background: #f0f0f0; font-size: 28rpx; color: #666; } |
| | | .tab-item.active { background: #2979ff; color: #fff; } |
| | | .search-section { background: #fff; margin: 24rpx; padding: 24rpx; border-radius: 16rpx; } |
| | | .search-row { display: flex; align-items: center; } |
| | | .search-input-wrap { flex: 1; margin-right: 20rpx; min-width: 0; } |
| | | .btn-search { display: flex; align-items: center; justify-content: center; width: 160rpx; min-height: 72rpx; flex-shrink: 0; padding: 20rpx 24rpx; background: #2979ff; color: #fff; border-radius: 12rpx; font-size: 28rpx; box-sizing: border-box; text-align: center; } |
| | | .btn-search-inner { display: flex; flex-direction: row; align-items: center; justify-content: center; gap: 8rpx; } |
| | | .list-section { margin: 24rpx; } |
| | | .card-item { background: #fff; border-radius: 16rpx; padding: 20rpx 24rpx; margin-bottom: 20rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); } |
| | | .card-header { padding: 4rpx 0 12rpx; } |
| | | .header-main { display: flex; justify-content: space-between; gap: 16rpx; } |
| | | .product-name { font-size: 30rpx; font-weight: 500; color: #333; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |
| | | .outbound-date { font-size: 24rpx; color: #999; } |
| | | .card-body .row { display: flex; justify-content: space-between; padding: 6rpx 0; font-size: 26rpx; } |
| | | .card-body .l { color: #666; } |
| | | .card-body .r { color: #333; } |
| | | .card-body .r.highlight { color: #2979ff; font-weight: 500; } |
| | | .card-actions { display: flex; justify-content: flex-end; margin-top: 12rpx; } |
| | | .btn-delete { color: #f56c6c; font-size: 28rpx; } |
| | | .no-data { text-align: center; padding: 60rpx 0; color: #999; font-size: 28rpx; } |
| | | .load-more-wrap { padding: 24rpx 24rpx 8rpx; } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="detail-page"> |
| | | <PageHeader title="åºåºè¯¦æ
" @back="goBack" /> |
| | | <view v-if="loading" class="loading-wrap"> |
| | | <text class="loading-text">å è½½ä¸...</text> |
| | | </view> |
| | | <view v-else-if="detail" class="detail-wrap"> |
| | | <view class="section-card"> |
| | | <view class="section-head"> |
| | | <view class="section-dot"></view> |
| | | <text class="section-title">åºç¡ä¿¡æ¯</text> |
| | | </view> |
| | | <view class="section-body"> |
| | | <view class="detail-row"> |
| | | <text class="label">åºå·</text> |
| | | <text class="value">{{ detail.index ?? '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="label">åºåºæ¹æ¬¡</text> |
| | | <text class="value value-strong">{{ detail.outboundBatches || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="label">åºåºæ¶é´</text> |
| | | <text class="value">{{ detail.createTime || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="label">产å大类</text> |
| | | <text class="value value-strong">{{ detail.productName || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="label">è§æ ¼åå·</text> |
| | | <text class="value">{{ detail.model || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="label">åä½</text> |
| | | <text class="value">{{ detail.unit || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row detail-row-highlight"> |
| | | <text class="label">åºåºæ°é</text> |
| | | <text class="value value-num">{{ detail.stockOutNum ?? '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="label">åºåºäºº</text> |
| | | <text class="value">{{ detail.createBy || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="label">æ¥æº</text> |
| | | <text class="value">{{ getRecordType(detail.recordType) || '-' }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view class="section-card"> |
| | | <view class="section-head"> |
| | | <view class="section-dot"></view> |
| | | <text class="section-title">åºåºä¿¡æ¯</text> |
| | | </view> |
| | | <view class="section-body"> |
| | | <view class="detail-row"> |
| | | <text class="label">车çå·</text> |
| | | <text class="value">{{ detail.licensePlateNo || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="label">æ¯é(å¨)</text> |
| | | <text class="value">{{ detail.grossWeight ?? '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="label">ç®é(å¨)</text> |
| | | <text class="value">{{ detail.tareWeight ?? '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="label">åé(å¨)</text> |
| | | <text class="value">{{ detail.netWeight ?? '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="label">è¿ç£
æ¥æ</text> |
| | | <text class="value">{{ detail.weighingDate || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="label">è¿ç£
å</text> |
| | | <text class="value">{{ detail.weighingOperator || '-' }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else class="empty"> |
| | | <text class="empty-text">ææ è¯¦æ
æ°æ®</text> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import { findAllQualifiedStockOutRecordTypeOptions, findAllUnQualifiedStockOutRecordTypeOptions } from "@/api/basicData/enum.js"; |
| | | |
| | | const detail = ref(null); |
| | | const loading = ref(true); |
| | | const stockRecordTypeOptions = ref([]); |
| | | |
| | | function normalizeDetail(raw) { |
| | | if (!raw) return null; |
| | | const d = typeof raw === "object" ? raw : {}; |
| | | return { |
| | | index: d.index ?? 1, |
| | | outboundBatches: d.outboundBatches, |
| | | createTime: d.createTime, |
| | | productName: d.productName, |
| | | model: d.model, |
| | | unit: d.unit, |
| | | stockOutNum: d.stockOutNum, |
| | | createBy: d.createBy, |
| | | recordType: d.recordType, |
| | | licensePlateNo: d.licensePlateNo, |
| | | grossWeight: d.grossWeight, |
| | | tareWeight: d.tareWeight, |
| | | netWeight: d.netWeight, |
| | | weighingDate: d.weighingDate, |
| | | weighingOperator: d.weighingOperator, |
| | | }; |
| | | } |
| | | |
| | | function getRecordType(recordType) { |
| | | if (recordType == null || recordType === "") return ""; |
| | | return stockRecordTypeOptions.value.find((item) => item.value === recordType)?.label || ""; |
| | | } |
| | | |
| | | function fetchRecordTypeOptions(type) { |
| | | const api = |
| | | type === "1" ? findAllUnQualifiedStockOutRecordTypeOptions : findAllQualifiedStockOutRecordTypeOptions; |
| | | api() |
| | | .then((res) => { |
| | | const data = res.data != null ? res.data : res; |
| | | stockRecordTypeOptions.value = Array.isArray(data) ? data : []; |
| | | }) |
| | | .catch(() => { |
| | | stockRecordTypeOptions.value = []; |
| | | }); |
| | | } |
| | | |
| | | onLoad(() => { |
| | | const cached = uni.getStorageSync("dispatchDetailItem"); |
| | | if (cached) { |
| | | try { |
| | | const payload = typeof cached === "string" ? JSON.parse(cached) : cached; |
| | | const item = payload && payload.item != null ? payload.item : payload; |
| | | const type = payload && payload.type != null ? payload.type : "0"; |
| | | detail.value = normalizeDetail({ ...item, index: 1 }); |
| | | fetchRecordTypeOptions(type); |
| | | uni.removeStorageSync("dispatchDetailItem"); |
| | | } catch (e) { |
| | | uni.removeStorageSync("dispatchDetailItem"); |
| | | } |
| | | } |
| | | loading.value = false; |
| | | }); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .detail-page { min-height: 100vh; background: linear-gradient(180deg, #e8eef7 0%, #f2f5fa 100%); padding-bottom: 48rpx; } |
| | | .loading-wrap { padding: 120rpx 48rpx; text-align: center; } |
| | | .loading-text { color: #8c9aa8; font-size: 28rpx; } |
| | | .empty { padding: 120rpx 48rpx; text-align: center; } |
| | | .empty-text { color: #8c9aa8; font-size: 28rpx; } |
| | | .detail-wrap { padding: 24rpx 24rpx 32rpx; } |
| | | .section-card { background: #fff; border-radius: 24rpx; overflow: hidden; margin-bottom: 28rpx; box-shadow: 0 8rpx 32rpx rgba(41, 121, 255, 0.06); border: 1rpx solid rgba(41, 121, 255, 0.06); } |
| | | .section-head { display: flex; align-items: center; padding: 28rpx 32rpx; background: linear-gradient(135deg, #f8fbff 0%, #f0f6ff 100%); border-bottom: 1rpx solid #eef3fa; } |
| | | .section-dot { width: 8rpx; height: 8rpx; border-radius: 50%; background: #2979ff; margin-right: 16rpx; } |
| | | .section-title { font-size: 30rpx; font-weight: 600; color: #1e3a5f; letter-spacing: 0.5rpx; } |
| | | .section-body { padding: 8rpx 32rpx 24rpx; } |
| | | .detail-row { display: flex; align-items: center; min-height: 96rpx; padding: 0 16rpx; border-radius: 12rpx; font-size: 28rpx; margin-bottom: 4rpx; } |
| | | .detail-row .label { width: 200rpx; flex-shrink: 0; color: #6b7c93; font-size: 26rpx; } |
| | | .detail-row .value { flex: 1; color: #2c3e50; text-align: right; word-break: break-all; font-size: 28rpx; } |
| | | .detail-row .value-strong { color: #1e3a5f; font-weight: 500; } |
| | | .detail-row .value-num { color: #2979ff; font-weight: 600; font-size: 32rpx; } |
| | | .detail-row-highlight { background: linear-gradient(90deg, rgba(41, 121, 255, 0.06) 0%, transparent 100%); margin: 12rpx -16rpx 4rpx; padding: 20rpx 16rpx; } |
| | | </style> |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <div class="search_form"> |
| | | <div> |
| | | <span class="search_title ml10">å
¥åºæ¥æï¼</span> |
| | | <el-date-picker |
| | | v-model="searchForm.timeStr" |
| | | type="date" |
| | | placeholder="è¯·éæ©æ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | clearable |
| | | @change="handleQuery" |
| | | /> |
| | | <span class="search_title ml10">产ååç§°ï¼</span> |
| | | <el-input |
| | | v-model="searchForm.productName" |
| | | style="width: 240px" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | @keyup.enter="handleQuery" |
| | | /> |
| | | <span class="search_title ml10">æ¥æºï¼</span> |
| | | <el-select |
| | | v-model="searchForm.recordType" |
| | | style="width: 240px" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | > |
| | | <el-option |
| | | v-for="item in stockRecordTypeOptions" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | <el-button type="primary" @click="handleQuery" style="margin-left: 10px">æç´¢</el-button> |
| | | </div> |
| | | <div> |
| | | <el-button type="danger" plain @click="handleDelete">å é¤</el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="table_list"> |
| | | <el-table |
| | | :data="tableData" |
| | | border |
| | | v-loading="tableLoading" |
| | | @selection-change="handleSelectionChange" |
| | | style="width: 100%" |
| | | height="calc(100vh - 18.5em)" |
| | | > |
| | | <el-table-column align="center" type="selection" width="55" /> |
| | | <el-table-column align="center" label="åºå·" type="index" width="60" /> |
| | | <el-table-column label="å
¥åºæ¶é´" prop="createTime" width="160" show-overflow-tooltip /> |
| | | <el-table-column label="产ååç§°" prop="productName" min-width="160" show-overflow-tooltip /> |
| | | <el-table-column label="è§æ ¼åå·" prop="model" min-width="160" show-overflow-tooltip /> |
| | | <el-table-column label="åä½" prop="unit" width="100" show-overflow-tooltip /> |
| | | <el-table-column label="å
¥åºæ°é" prop="stockInNum" width="110" show-overflow-tooltip /> |
| | | <el-table-column label="å
¥åºäºº" prop="createBy" width="120" show-overflow-tooltip /> |
| | | <el-table-column label="æ¥æº" prop="recordType" width="140" show-overflow-tooltip> |
| | | <template #default="scope"> |
| | | {{ getRecordType(scope.row.recordType) }} |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <Pagination |
| | | v-show="total > 0" |
| | | :total="total" |
| | | :page="page.current" |
| | | :limit="page.size" |
| | | @pagination="paginationChange" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { onMounted, reactive, ref, toRefs, watch } from "vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import Pagination from "@/components/PIMTable/Pagination.vue"; |
| | | import { |
| | | batchDeleteConsumablesInRecords, |
| | | getConsumablesInRecordListPage, |
| | | } from "@/api/consumablesLogistics/consumablesInRecord.js"; |
| | | import { |
| | | findAllQualifiedStockInRecordTypeOptions, |
| | | findAllUnQualifiedStockInRecordTypeOptions, |
| | | } from "@/api/basicData/enum.js"; |
| | | |
| | | const props = defineProps({ |
| | | type: { |
| | | type: String, |
| | | required: true, |
| | | default: "0", |
| | | }, |
| | | }); |
| | | |
| | | const tableData = ref([]); |
| | | const selectedRows = ref([]); |
| | | const tableLoading = ref(false); |
| | | const stockRecordTypeOptions = ref([]); |
| | | |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 100, |
| | | }); |
| | | const total = ref(0); |
| | | |
| | | const data = reactive({ |
| | | searchForm: { |
| | | timeStr: "", |
| | | productName: "", |
| | | recordType: "", |
| | | }, |
| | | }); |
| | | const { searchForm } = toRefs(data); |
| | | |
| | | const getRecordType = (recordType) => { |
| | | return stockRecordTypeOptions.value.find((item) => item.value === recordType)?.label || ""; |
| | | }; |
| | | |
| | | const fetchStockRecordTypeOptions = () => { |
| | | const api = |
| | | props.type === "1" |
| | | ? findAllUnQualifiedStockInRecordTypeOptions |
| | | : findAllQualifiedStockInRecordTypeOptions; |
| | | api() |
| | | .then((res) => { |
| | | stockRecordTypeOptions.value = res.data || []; |
| | | }) |
| | | .catch(() => { |
| | | stockRecordTypeOptions.value = []; |
| | | }); |
| | | }; |
| | | |
| | | const handleQuery = () => { |
| | | page.current = 1; |
| | | getList(); |
| | | }; |
| | | |
| | | const paginationChange = obj => { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | getList(); |
| | | }; |
| | | |
| | | const getList = () => { |
| | | tableLoading.value = true; |
| | | getConsumablesInRecordListPage({ ...searchForm.value, ...page, type: props.type }) |
| | | .then(res => { |
| | | tableData.value = res?.data?.records || []; |
| | | total.value = res?.data?.total || 0; |
| | | }) |
| | | .finally(() => { |
| | | tableLoading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | const handleSelectionChange = selection => { |
| | | selectedRows.value = Array.isArray(selection) ? selection : []; |
| | | }; |
| | | |
| | | const handleDelete = () => { |
| | | const ids = selectedRows.value.map(i => i.id).filter(Boolean); |
| | | if (ids.length === 0) { |
| | | ElMessage.warning("è¯·éæ©æ°æ®"); |
| | | return; |
| | | } |
| | | ElMessageBox.confirm("éä¸çå
容å°è¢«å é¤ï¼æ¯å¦ç¡®è®¤å é¤ï¼", "å é¤", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }) |
| | | .then(() => batchDeleteConsumablesInRecords(ids)) |
| | | .then(() => { |
| | | ElMessage.success("å 餿å"); |
| | | getList(); |
| | | }) |
| | | .catch(() => {}); |
| | | }; |
| | | |
| | | watch( |
| | | () => props.type, |
| | | () => { |
| | | searchForm.value.recordType = ""; |
| | | fetchStockRecordTypeOptions(); |
| | | handleQuery(); |
| | | } |
| | | ); |
| | | |
| | | onMounted(() => { |
| | | fetchStockRecordTypeOptions(); |
| | | getList(); |
| | | }); |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="receipt-page"> |
| | | <PageHeader title="å
¥åºç®¡ç" @back="goBack" /> |
| | | <view class="tabs-wrap"> |
| | | <view |
| | | v-for="tab in tabs" |
| | | :key="tab.name" |
| | | class="tab-item" |
| | | :class="{ active: activeTab === tab.name }" |
| | | @click="activeTab = tab.name" |
| | | > |
| | | <text>{{ tab.label }}</text> |
| | | </view> |
| | | </view> |
| | | <view class="search-section"> |
| | | <view class="search-row"> |
| | | <view class="search-input-wrap"> |
| | | <up-input v-model="searchForm.productName" placeholder="产å大类" clearable /> |
| | | </view> |
| | | <view class="btn-search" @click="handleQuery"> |
| | | <view class="btn-search-inner"> |
| | | <up-icon name="search" size="22" color="#fff"></up-icon> |
| | | <text>æç´¢</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view class="list-section"> |
| | | <view v-if="tableData.length > 0"> |
| | | <view v-for="(item, index) in tableData" :key="item.id || index" class="card-item"> |
| | | <view class="card-click" @click="goDetail(item)"> |
| | | <view class="card-header"> |
| | | <view class="header-main"> |
| | | <text class="product-name">{{ item.productName }}</text> |
| | | <text class="inbound-date">{{ item.createTime || item.inboundDate }}</text> |
| | | </view> |
| | | </view> |
| | | <up-divider /> |
| | | <view class="card-body"> |
| | | <view class="row"><text class="l">è§æ ¼åå·</text><text class="r">{{ item.model }}</text></view> |
| | | <view class="row"><text class="l">åä½</text><text class="r">{{ item.unit }}</text></view> |
| | | <view class="row"><text class="l">å
¥åºæ°é</text><text class="r highlight">{{ item.stockInNum }}</text></view> |
| | | <view class="row"><text class="l">å
¥åºäºº</text><text class="r">{{ item.createBy }}</text></view> |
| | | <view class="row" v-if="item.recordType !== undefined"><text class="l">æ¥æº</text><text class="r">{{ getRecordType(item.recordType) || item.recordType }}</text></view> |
| | | </view> |
| | | </view> |
| | | <view class="card-actions"> |
| | | <view class="btn-delete" @click.stop="handleDeleteSingle(item)">å é¤</view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else class="no-data">ææ æ°æ®</view> |
| | | </view> |
| | | <view class="load-more-wrap" v-if="tableData.length > 0"> |
| | | <u-loadmore :status="loadStatus" @loadmore="loadMore" /> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { reactive, ref, toRefs, watch } from "vue"; |
| | | import { onReachBottom, onShow } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import request from "@/utils/request"; |
| | | import { findAllQualifiedStockInRecordTypeOptions, findAllUnQualifiedStockInRecordTypeOptions } from "@/api/basicData/enum.js"; |
| | | |
| | | const activeTab = ref("qualified"); |
| | | const stockRecordTypeOptions = ref([]); |
| | | const tabs = [ |
| | | { label: "åæ ¼å
¥åº", name: "qualified", type: "0" }, |
| | | { label: "ä¸åæ ¼å
¥åº", name: "unqualified", type: "1" }, |
| | | ]; |
| | | const tableData = ref([]); |
| | | const total = ref(0); |
| | | const loadStatus = ref("loadmore"); |
| | | const page = reactive({ current: 1, size: 4 }); |
| | | const data = reactive({ searchForm: { productName: "" } }); |
| | | const { searchForm } = toRefs(data); |
| | | |
| | | const currentType = () => tabs.find((t) => t.name === activeTab.value)?.type || "0"; |
| | | |
| | | function getRecordType(recordType) { |
| | | if (recordType == null || recordType === "") return ""; |
| | | return stockRecordTypeOptions.value.find((item) => item.value === recordType)?.label || ""; |
| | | } |
| | | |
| | | function fetchRecordTypeOptions() { |
| | | const api = |
| | | currentType() === "1" |
| | | ? findAllUnQualifiedStockInRecordTypeOptions |
| | | : findAllQualifiedStockInRecordTypeOptions; |
| | | api() |
| | | .then((res) => { |
| | | const data = res.data != null ? res.data : res; |
| | | stockRecordTypeOptions.value = Array.isArray(data) ? data : []; |
| | | }) |
| | | .catch(() => { |
| | | stockRecordTypeOptions.value = []; |
| | | }); |
| | | } |
| | | |
| | | const getList = () => { |
| | | const isFirstPage = page.current === 1; |
| | | if (isFirstPage) { |
| | | uni.showLoading({ title: "å è½½ä¸...", mask: true }); |
| | | } |
| | | request({ |
| | | url: "/consumablesInRecord/listPage", |
| | | method: "get", |
| | | params: { |
| | | ...page, |
| | | type: currentType(), |
| | | productName: searchForm.value.productName, |
| | | }, |
| | | }) |
| | | .then((res) => { |
| | | uni.hideLoading(); |
| | | const records = res.data?.records || []; |
| | | const totalCount = res.data?.total || 0; |
| | | if (isFirstPage) { |
| | | tableData.value = records; |
| | | fetchRecordTypeOptions(); |
| | | } else { |
| | | tableData.value = [...tableData.value, ...records]; |
| | | } |
| | | total.value = totalCount; |
| | | loadStatus.value = tableData.value.length >= totalCount || totalCount === 0 ? "nomore" : "loadmore"; |
| | | }) |
| | | .catch(() => { |
| | | uni.hideLoading(); |
| | | loadStatus.value = "error"; |
| | | if (isFirstPage) { |
| | | uni.showToast({ title: "å 载失败", icon: "none" }); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | const loadMore = () => { |
| | | if (loadStatus.value === "nomore" || loadStatus.value === "loading") return; |
| | | loadStatus.value = "loading"; |
| | | page.current++; |
| | | getList(); |
| | | }; |
| | | |
| | | watch(activeTab, () => { |
| | | page.current = 1; |
| | | loadStatus.value = "loadmore"; |
| | | stockRecordTypeOptions.value = []; |
| | | getList(); |
| | | }); |
| | | |
| | | const handleQuery = () => { |
| | | page.current = 1; |
| | | loadStatus.value = "loadmore"; |
| | | getList(); |
| | | }; |
| | | |
| | | const goDetail = (item) => { |
| | | if (!item?.id) return; |
| | | try { |
| | | uni.setStorageSync( |
| | | "receiptDetailItem", |
| | | JSON.stringify({ |
| | | item, |
| | | type: currentType(), |
| | | }) |
| | | ); |
| | | } catch (e) {} |
| | | uni.navigateTo({ |
| | | url: "/pages/consumablesLogistics/receiptManagement/view?id=" + item.id, |
| | | }); |
| | | }; |
| | | |
| | | const handleDeleteSingle = (item) => { |
| | | if (!item?.id) return; |
| | | uni.showModal({ |
| | | title: "å é¤", |
| | | content: "确认å é¤è¯¥æ¡å
¥åºè®°å½ï¼", |
| | | success: (res) => { |
| | | if (!res.confirm) return; |
| | | request({ |
| | | url: "/consumablesInRecord", |
| | | method: "delete", |
| | | data: [item.id], |
| | | }) |
| | | .then(() => { |
| | | uni.showToast({ title: "å 餿å", icon: "success" }); |
| | | getList(); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "å é¤å¤±è´¥", icon: "none" }); |
| | | }); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | onShow(() => { |
| | | getList(); |
| | | }); |
| | | onReachBottom(() => { |
| | | loadMore(); |
| | | }); |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .receipt-page { min-height: 100vh; background: #f5f5f5; padding-bottom: 40rpx; } |
| | | .tabs-wrap { display: flex; background: #fff; padding: 24rpx; gap: 24rpx; } |
| | | .tab-item { flex: 1; text-align: center; padding: 20rpx; border-radius: 12rpx; background: #f0f0f0; font-size: 28rpx; color: #666; } |
| | | .tab-item.active { background: #2979ff; color: #fff; } |
| | | .search-section { background: #fff; margin: 24rpx; padding: 24rpx; border-radius: 16rpx; } |
| | | .search-row { display: flex; align-items: center; } |
| | | .search-input-wrap { flex: 1; margin-right: 20rpx; min-width: 0; } |
| | | .btn-search { display: flex; align-items: center; justify-content: center; width: 160rpx; min-height: 72rpx; flex-shrink: 0; padding: 20rpx 24rpx; background: #2979ff; color: #fff; border-radius: 12rpx; font-size: 28rpx; box-sizing: border-box; text-align: center; } |
| | | .btn-search-inner { display: flex; flex-direction: row; align-items: center; justify-content: center; gap: 8rpx; } |
| | | .list-section { margin: 24rpx; } |
| | | .card-item { background: #fff; border-radius: 16rpx; padding: 20rpx 24rpx; margin-bottom: 20rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); } |
| | | .card-header { padding: 4rpx 0 12rpx; } |
| | | .header-main { display: flex; justify-content: space-between; gap: 16rpx; } |
| | | .product-name { font-size: 30rpx; font-weight: 500; color: #333; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |
| | | .inbound-date { font-size: 24rpx; color: #999; } |
| | | .card-body .row { display: flex; justify-content: space-between; padding: 6rpx 0; font-size: 26rpx; } |
| | | .card-body .l { color: #666; } |
| | | .card-body .r { color: #333; } |
| | | .card-body .r.highlight { color: #2979ff; font-weight: 500; } |
| | | .card-actions { display: flex; justify-content: flex-end; margin-top: 12rpx; } |
| | | .btn-delete { color: #f56c6c; font-size: 28rpx; } |
| | | .no-data { text-align: center; padding: 60rpx 0; color: #999; font-size: 28rpx; } |
| | | .load-more-wrap { padding: 24rpx 24rpx 8rpx; } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="detail-page"> |
| | | <PageHeader title="å
¥åºè¯¦æ
" @back="goBack" /> |
| | | <view v-if="loading" class="loading-wrap"> |
| | | <text class="loading-text">å è½½ä¸...</text> |
| | | </view> |
| | | <view v-else-if="detail" class="detail-wrap"> |
| | | <view class="section-card"> |
| | | <view class="section-head"> |
| | | <view class="section-dot"></view> |
| | | <text class="section-title">åºç¡ä¿¡æ¯</text> |
| | | </view> |
| | | <view class="section-body"> |
| | | <view class="detail-row"> |
| | | <text class="label">åºå·</text> |
| | | <text class="value">{{ detail.index ?? '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="label">å
¥åºæ¹æ¬¡</text> |
| | | <text class="value value-strong">{{ detail.inboundBatches || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="label">å
¥åºæ¶é´</text> |
| | | <text class="value">{{ detail.createTime || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="label">产å大类</text> |
| | | <text class="value value-strong">{{ detail.productName || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="label">è§æ ¼åå·</text> |
| | | <text class="value">{{ detail.model || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="label">åä½</text> |
| | | <text class="value">{{ detail.unit || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row detail-row-highlight"> |
| | | <text class="label">å
¥åºæ°é</text> |
| | | <text class="value value-num">{{ detail.stockInNum ?? '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="label">å
¥åºäºº</text> |
| | | <text class="value">{{ detail.createBy || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="label">æ¥æº</text> |
| | | <text class="value">{{ getRecordType(detail.recordType) || '-' }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view class="section-card"> |
| | | <view class="section-head"> |
| | | <view class="section-dot"></view> |
| | | <text class="section-title">å
¥åºä¿¡æ¯</text> |
| | | </view> |
| | | <view class="section-body"> |
| | | <view class="detail-row"> |
| | | <text class="label">车çå·</text> |
| | | <text class="value">{{ detail.licensePlateNo || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="label">æ¯é(å¨)</text> |
| | | <text class="value">{{ detail.grossWeight ?? '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="label">ç®é(å¨)</text> |
| | | <text class="value">{{ detail.tareWeight ?? '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="label">åé(å¨)</text> |
| | | <text class="value">{{ detail.netWeight ?? '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="label">è¿ç£
æ¥æ</text> |
| | | <text class="value">{{ detail.weighingDate || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="label">è¿ç£
å</text> |
| | | <text class="value">{{ detail.weighingOperator || '-' }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else class="empty"> |
| | | <text class="empty-text">ææ è¯¦æ
æ°æ®</text> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import { findAllQualifiedStockInRecordTypeOptions, findAllUnQualifiedStockInRecordTypeOptions } from "@/api/basicData/enum.js"; |
| | | |
| | | const detail = ref(null); |
| | | const loading = ref(true); |
| | | const stockRecordTypeOptions = ref([]); |
| | | |
| | | function normalizeDetail(raw) { |
| | | if (!raw) return null; |
| | | const d = typeof raw === "object" ? raw : {}; |
| | | return { |
| | | index: d.index ?? 1, |
| | | inboundBatches: d.inboundBatches, |
| | | createTime: d.createTime, |
| | | productName: d.productName, |
| | | model: d.model, |
| | | unit: d.unit, |
| | | stockInNum: d.stockInNum, |
| | | createBy: d.createBy, |
| | | recordType: d.recordType, |
| | | licensePlateNo: d.licensePlateNo, |
| | | grossWeight: d.grossWeight, |
| | | tareWeight: d.tareWeight, |
| | | netWeight: d.netWeight, |
| | | weighingDate: d.weighingDate, |
| | | weighingOperator: d.weighingOperator, |
| | | }; |
| | | } |
| | | |
| | | function getRecordType(recordType) { |
| | | if (recordType == null || recordType === "") return ""; |
| | | return stockRecordTypeOptions.value.find((item) => item.value === recordType)?.label || ""; |
| | | } |
| | | |
| | | function fetchRecordTypeOptions(type) { |
| | | const api = |
| | | type === "1" ? findAllUnQualifiedStockInRecordTypeOptions : findAllQualifiedStockInRecordTypeOptions; |
| | | api() |
| | | .then((res) => { |
| | | const data = res.data != null ? res.data : res; |
| | | stockRecordTypeOptions.value = Array.isArray(data) ? data : []; |
| | | }) |
| | | .catch(() => { |
| | | stockRecordTypeOptions.value = []; |
| | | }); |
| | | } |
| | | |
| | | onLoad(() => { |
| | | const cached = uni.getStorageSync("receiptDetailItem"); |
| | | if (cached) { |
| | | try { |
| | | const payload = typeof cached === "string" ? JSON.parse(cached) : cached; |
| | | const item = payload && payload.item != null ? payload.item : payload; |
| | | const type = payload && payload.type != null ? payload.type : "0"; |
| | | detail.value = normalizeDetail({ ...item, index: 1 }); |
| | | fetchRecordTypeOptions(type); |
| | | uni.removeStorageSync("receiptDetailItem"); |
| | | } catch (e) { |
| | | uni.removeStorageSync("receiptDetailItem"); |
| | | } |
| | | } |
| | | loading.value = false; |
| | | }); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .detail-page { min-height: 100vh; background: linear-gradient(180deg, #e8eef7 0%, #f2f5fa 100%); padding-bottom: 48rpx; } |
| | | .loading-wrap { padding: 120rpx 48rpx; text-align: center; } |
| | | .loading-text { color: #8c9aa8; font-size: 28rpx; } |
| | | .empty { padding: 120rpx 48rpx; text-align: center; } |
| | | .empty-text { color: #8c9aa8; font-size: 28rpx; } |
| | | .detail-wrap { padding: 24rpx 24rpx 32rpx; } |
| | | .section-card { background: #fff; border-radius: 24rpx; overflow: hidden; margin-bottom: 28rpx; box-shadow: 0 8rpx 32rpx rgba(41, 121, 255, 0.06); border: 1rpx solid rgba(41, 121, 255, 0.06); } |
| | | .section-head { display: flex; align-items: center; padding: 28rpx 32rpx; background: linear-gradient(135deg, #f8fbff 0%, #f0f6ff 100%); border-bottom: 1rpx solid #eef3fa; } |
| | | .section-dot { width: 8rpx; height: 8rpx; border-radius: 50%; background: #2979ff; margin-right: 16rpx; } |
| | | .section-title { font-size: 30rpx; font-weight: 600; color: #1e3a5f; letter-spacing: 0.5rpx; } |
| | | .section-body { padding: 8rpx 32rpx 24rpx; } |
| | | .detail-row { display: flex; align-items: center; min-height: 96rpx; padding: 0 16rpx; border-radius: 12rpx; font-size: 28rpx; margin-bottom: 4rpx; } |
| | | .detail-row .label { width: 200rpx; flex-shrink: 0; color: #6b7c93; font-size: 26rpx; } |
| | | .detail-row .value { flex: 1; color: #2c3e50; text-align: right; word-break: break-all; font-size: 28rpx; } |
| | | .detail-row .value-strong { color: #1e3a5f; font-weight: 500; } |
| | | .detail-row .value-num { color: #2979ff; font-weight: 600; font-size: 32rpx; } |
| | | .detail-row-highlight { background: linear-gradient(90deg, rgba(41, 121, 255, 0.06) 0%, transparent 100%); margin: 12rpx -16rpx 4rpx; padding: 20rpx 16rpx; } |
| | | </style> |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <div class="search_form"> |
| | | <div> |
| | | <span class="search_title ml10">产ååç§°ï¼</span> |
| | | <el-input |
| | | v-model="searchForm.productName" |
| | | style="width: 240px" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | @keyup.enter="handleQuery" |
| | | /> |
| | | <el-button type="primary" @click="handleQuery" style="margin-left: 10px">æç´¢</el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="table_list"> |
| | | <el-table |
| | | :data="tableData" |
| | | border |
| | | v-loading="tableLoading" |
| | | style="width: 100%" |
| | | height="calc(100vh - 18.5em)" |
| | | > |
| | | <el-table-column align="center" label="åºå·" type="index" width="60" /> |
| | | <el-table-column label="产ååç§°" prop="productName" min-width="180" show-overflow-tooltip /> |
| | | <el-table-column label="è§æ ¼åå·" prop="model" min-width="160" show-overflow-tooltip /> |
| | | <el-table-column label="åä½" prop="unit" width="100" show-overflow-tooltip /> |
| | | <el-table-column label="åºåæ°é" prop="qualitity" width="110" show-overflow-tooltip /> |
| | | <el-table-column label="å»ç»æ°é" prop="lockedQuantity" width="110" show-overflow-tooltip /> |
| | | <el-table-column label="æè¿æ´æ°æ¶é´" prop="updateTime" width="180" show-overflow-tooltip /> |
| | | <el-table-column label="夿³¨" prop="remark" min-width="140" show-overflow-tooltip /> |
| | | </el-table> |
| | | |
| | | <Pagination |
| | | v-show="total > 0" |
| | | :total="total" |
| | | :page="page.current" |
| | | :limit="page.size" |
| | | @pagination="paginationChange" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { reactive, ref, toRefs } from "vue"; |
| | | import Pagination from "@/components/PIMTable/Pagination.vue"; |
| | | import { getConsumablesInListPage } from "@/api/consumablesLogistics/consumablesIn.js"; |
| | | |
| | | const tableData = ref([]); |
| | | const tableLoading = ref(false); |
| | | |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 100, |
| | | }); |
| | | const total = ref(0); |
| | | |
| | | const data = reactive({ |
| | | searchForm: { |
| | | productName: "", |
| | | }, |
| | | }); |
| | | const { searchForm } = toRefs(data); |
| | | |
| | | const handleQuery = () => { |
| | | page.current = 1; |
| | | getList(); |
| | | }; |
| | | |
| | | const paginationChange = obj => { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | getList(); |
| | | }; |
| | | |
| | | const getList = () => { |
| | | tableLoading.value = true; |
| | | getConsumablesInListPage({ ...searchForm.value, ...page }) |
| | | .then(res => { |
| | | tableData.value = res?.data?.records || []; |
| | | total.value = res?.data?.total || 0; |
| | | }) |
| | | .finally(() => { |
| | | tableLoading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | getList(); |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <div class="search_form"> |
| | | <div> |
| | | <span class="search_title ml10">产ååç§°ï¼</span> |
| | | <el-input |
| | | v-model="searchForm.productName" |
| | | style="width: 240px" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | @keyup.enter="handleQuery" |
| | | /> |
| | | <el-button type="primary" @click="handleQuery" style="margin-left: 10px">æç´¢</el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="table_list"> |
| | | <el-table |
| | | :data="tableData" |
| | | border |
| | | v-loading="tableLoading" |
| | | style="width: 100%" |
| | | height="calc(100vh - 18.5em)" |
| | | > |
| | | <el-table-column align="center" label="åºå·" type="index" width="60" /> |
| | | <el-table-column label="产ååç§°" prop="productName" min-width="180" show-overflow-tooltip /> |
| | | <el-table-column label="è§æ ¼åå·" prop="model" min-width="160" show-overflow-tooltip /> |
| | | <el-table-column label="åä½" prop="unit" width="100" show-overflow-tooltip /> |
| | | <el-table-column label="åºåæ°é" prop="qualitity" width="110" show-overflow-tooltip /> |
| | | <el-table-column label="å»ç»æ°é" prop="lockedQuantity" width="110" show-overflow-tooltip /> |
| | | <el-table-column label="æè¿æ´æ°æ¶é´" prop="updateTime" width="180" show-overflow-tooltip /> |
| | | <el-table-column label="夿³¨" prop="remark" min-width="140" show-overflow-tooltip /> |
| | | </el-table> |
| | | |
| | | <Pagination |
| | | v-show="total > 0" |
| | | :total="total" |
| | | :page="page.current" |
| | | :limit="page.size" |
| | | @pagination="paginationChange" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { reactive, ref, toRefs } from "vue"; |
| | | import Pagination from "@/components/PIMTable/Pagination.vue"; |
| | | import { getConsumablesUninventoryListPage } from "@/api/consumablesLogistics/consumablesUninventory.js"; |
| | | |
| | | const tableData = ref([]); |
| | | const tableLoading = ref(false); |
| | | |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 100, |
| | | }); |
| | | const total = ref(0); |
| | | |
| | | const data = reactive({ |
| | | searchForm: { |
| | | productName: "", |
| | | }, |
| | | }); |
| | | const { searchForm } = toRefs(data); |
| | | |
| | | const handleQuery = () => { |
| | | page.current = 1; |
| | | getList(); |
| | | }; |
| | | |
| | | const paginationChange = obj => { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | getList(); |
| | | }; |
| | | |
| | | const getList = () => { |
| | | tableLoading.value = true; |
| | | getConsumablesUninventoryListPage({ ...searchForm.value, ...page }) |
| | | .then(res => { |
| | | tableData.value = res?.data?.records || []; |
| | | total.value = res?.data?.total || 0; |
| | | }) |
| | | .finally(() => { |
| | | tableLoading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | getList(); |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="add-stock-page"> |
| | | <PageHeader title="æ°å¢åºå" @back="goBack" /> |
| | | |
| | | <scroll-view scroll-y class="content-scroll"> |
| | | <view class="form-section"> |
| | | <view class="form-row"> |
| | | <text class="form-label required">产ååç§°</text> |
| | | <view class="selector-trigger" @click="openProductSelector"> |
| | | <text class="selector-text" :class="{ placeholder: !form.productName }"> |
| | | {{ form.productName || "è¯·éæ©äº§å" }} |
| | | </text> |
| | | <up-icon name="arrow-right" size="16" color="#999"></up-icon> |
| | | </view> |
| | | </view> |
| | | <view class="form-row"> |
| | | <text class="form-label">è§æ ¼</text> |
| | | <up-input v-model="form.productModelName" disabled placeholder="è¯·éæ©äº§ååèªå¨å¸¦åº" /> |
| | | </view> |
| | | <view class="form-row"> |
| | | <text class="form-label">åä½</text> |
| | | <up-input v-model="form.unit" disabled placeholder="è¯·éæ©äº§ååèªå¨å¸¦åº" /> |
| | | </view> |
| | | </view> |
| | | |
| | | <view v-if="isQualified" class="form-section"> |
| | | <view class="section-title">è¿ç£
ä¿¡æ¯</view> |
| | | <view class="form-row"> |
| | | <text class="form-label">车çå·</text> |
| | | <up-input v-model="form.licensePlateNo" placeholder="请è¾å
¥è½¦çå·" /> |
| | | </view> |
| | | <view class="form-row"> |
| | | <text class="form-label">æ¯é(å¨)</text> |
| | | <up-input v-model="form.grossWeight" type="number" placeholder="请è¾å
¥æ¯é" /> |
| | | </view> |
| | | <view class="form-row"> |
| | | <text class="form-label">ç®é(å¨)</text> |
| | | <up-input v-model="form.tareWeight" type="number" placeholder="请è¾å
¥ç®é" /> |
| | | </view> |
| | | <view class="form-row"> |
| | | <text class="form-label">åé(å¨)</text> |
| | | <up-input v-model="form.netWeight" type="number" disabled placeholder="èªå¨è®¡ç®" /> |
| | | </view> |
| | | <view class="form-row"> |
| | | <text class="form-label">è¿ç£
æ¥æ</text> |
| | | <view class="selector-trigger" @click="openWeighingDatePicker"> |
| | | <text class="selector-text" :class="{ placeholder: !form.weighingDate }"> |
| | | {{ form.weighingDate || "è¯·éæ©è¿ç£
æ¥æ" }} |
| | | </text> |
| | | <up-icon name="calendar" size="16" color="#999"></up-icon> |
| | | </view> |
| | | </view> |
| | | <view class="form-row"> |
| | | <text class="form-label">è¿ç£
å</text> |
| | | <up-input v-model="form.weighingOperator" placeholder="请è¾å
¥è¿ç£
å" /> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="form-section"> |
| | | <view class="form-row"> |
| | | <text class="form-label">夿³¨</text> |
| | | <up-input v-model="form.remark" type="textarea" placeholder="éå¡«" /> |
| | | </view> |
| | | </view> |
| | | </scroll-view> |
| | | |
| | | <view class="bottom-bar"> |
| | | <view class="btn-submit" @click="handleSubmit">æäº¤</view> |
| | | </view> |
| | | |
| | | <up-popup :show="showProductPopup" mode="bottom" @close="showProductPopup = false"> |
| | | <view class="product-popup"> |
| | | <view class="popup-header"> |
| | | <text class="popup-title">éæ©äº§å</text> |
| | | </view> |
| | | <view class="popup-search"> |
| | | <up-input v-model="productQuery.productName" placeholder="产å大类" clearable /> |
| | | <up-input v-model="productQuery.model" placeholder="åå·åç§°" clearable /> |
| | | <view class="popup-search-btn" @click="loadProductList">æç´¢</view> |
| | | </view> |
| | | <scroll-view scroll-y class="product-list"> |
| | | <view |
| | | v-for="item in productList" |
| | | :key="item.id" |
| | | class="product-item" |
| | | @click="selectProduct(item)" |
| | | > |
| | | <view class="product-name-row"> |
| | | <text class="product-name">{{ item.productName }}</text> |
| | | <text class="product-unit">{{ item.unit }}</text> |
| | | </view> |
| | | <view class="product-model">åå·ï¼{{ item.model }}</view> |
| | | </view> |
| | | <view v-if="!productLoading && productList.length === 0" class="no-data">ææ æ°æ®</view> |
| | | </scroll-view> |
| | | </view> |
| | | </up-popup> |
| | | |
| | | <up-popup :show="showWeighingDatePicker" mode="bottom" @close="showWeighingDatePicker = false"> |
| | | <up-datetime-picker |
| | | :show="true" |
| | | v-model="weighingDateValue" |
| | | mode="datetime" |
| | | @confirm="onWeighingDateConfirm" |
| | | @cancel="showWeighingDatePicker = false" |
| | | /> |
| | | </up-popup> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, reactive, ref, watch } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import dayjs from "dayjs"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import { createConsumablesIn } from "@/api/consumablesLogistics/consumablesIn.js"; |
| | | import { createConsumablesUnInventory } from "@/api/consumablesLogistics/consumablesUninventory.js"; |
| | | import { productModelList } from "@/api/basicData/productModel.js"; |
| | | |
| | | const form = reactive({ |
| | | productId: undefined, |
| | | productModelId: undefined, |
| | | productName: "", |
| | | productModelName: "", |
| | | unit: "", |
| | | productType: undefined, |
| | | licensePlateNo: "", |
| | | grossWeight: "", |
| | | tareWeight: "", |
| | | netWeight: "", |
| | | weighingDate: "", |
| | | weighingOperator: "", |
| | | remark: "", |
| | | }); |
| | | |
| | | const type = ref("0"); |
| | | const isQualified = computed(() => type.value === "0"); |
| | | |
| | | const showProductPopup = ref(false); |
| | | const productQuery = reactive({ |
| | | productName: "", |
| | | model: "", |
| | | }); |
| | | const productList = ref([]); |
| | | const productLoading = ref(false); |
| | | |
| | | const showWeighingDatePicker = ref(false); |
| | | const weighingDateValue = ref(Date.now()); |
| | | |
| | | onLoad((options) => { |
| | | if (options && options.type != null) { |
| | | type.value = options.type; |
| | | } |
| | | }); |
| | | |
| | | const openProductSelector = () => { |
| | | showProductPopup.value = true; |
| | | if (productList.value.length === 0) { |
| | | loadProductList(); |
| | | } |
| | | }; |
| | | |
| | | const loadProductList = () => { |
| | | productLoading.value = true; |
| | | productModelList({ |
| | | productName: productQuery.productName || "", |
| | | model: productQuery.model || "", |
| | | current: 1, |
| | | size: 20, |
| | | }) |
| | | .then((res) => { |
| | | const data = res?.records || res?.data?.records || []; |
| | | productList.value = Array.isArray(data) ? data : []; |
| | | }) |
| | | .finally(() => { |
| | | productLoading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | const selectProduct = (item) => { |
| | | form.productId = item.productId || item.id; |
| | | form.productModelId = item.id; |
| | | form.productName = item.productName; |
| | | form.productModelName = item.model; |
| | | form.unit = item.unit; |
| | | form.productType = item.productType; |
| | | showProductPopup.value = false; |
| | | }; |
| | | |
| | | const computeNetWeight = () => { |
| | | const gross = Number(form.grossWeight); |
| | | const tare = Number(form.tareWeight); |
| | | if (!isNaN(gross) && !isNaN(tare)) { |
| | | const net = Number((gross - tare).toFixed(2)); |
| | | form.netWeight = net > 0 ? net : 0; |
| | | } else { |
| | | form.netWeight = ""; |
| | | } |
| | | }; |
| | | |
| | | watch( |
| | | () => [form.grossWeight, form.tareWeight], |
| | | () => { |
| | | computeNetWeight(); |
| | | } |
| | | ); |
| | | |
| | | const openWeighingDatePicker = () => { |
| | | weighingDateValue.value = form.weighingDate |
| | | ? dayjs(form.weighingDate, "YYYY-MM-DD HH:mm:ss").valueOf() |
| | | : Date.now(); |
| | | showWeighingDatePicker.value = true; |
| | | }; |
| | | |
| | | const onWeighingDateConfirm = (e) => { |
| | | const ts = e?.value ?? weighingDateValue.value; |
| | | form.weighingDate = dayjs(ts).format("YYYY-MM-DD HH:mm:ss"); |
| | | showWeighingDatePicker.value = false; |
| | | }; |
| | | |
| | | const handleSubmit = () => { |
| | | if (!form.productName || !form.productModelId) { |
| | | uni.showToast({ title: "è¯·éæ©äº§å", icon: "none" }); |
| | | return; |
| | | } |
| | | const payload = { |
| | | productId: form.productId, |
| | | productModelId: form.productModelId, |
| | | productName: form.productName, |
| | | productModelName: form.productModelName, |
| | | unit: form.unit, |
| | | productType: form.productType, |
| | | licensePlateNo: form.licensePlateNo, |
| | | grossWeight: form.grossWeight, |
| | | tareWeight: form.tareWeight, |
| | | netWeight: form.netWeight, |
| | | weighingDate: form.weighingDate, |
| | | weighingOperator: form.weighingOperator, |
| | | remark: form.remark, |
| | | }; |
| | | const api = isQualified.value ? createConsumablesIn : createConsumablesUnInventory; |
| | | api(payload) |
| | | .then(() => { |
| | | uni.showToast({ title: "æ°å¢æå", icon: "success" }); |
| | | setTimeout(() => { |
| | | uni.navigateBack(); |
| | | }, 400); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "æ°å¢å¤±è´¥", icon: "none" }); |
| | | }); |
| | | }; |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .add-stock-page { min-height: 100vh; background: #f5f5f5; padding-bottom: 100rpx; } |
| | | .content-scroll { height: calc(100vh - 100rpx); } |
| | | .form-section { background: #fff; margin: 24rpx; padding: 24rpx; border-radius: 16rpx; } |
| | | .section-title { font-size: 28rpx; font-weight: 500; color: #333; margin-bottom: 12rpx; } |
| | | .form-row { margin-bottom: 24rpx; } |
| | | .form-label { display: block; font-size: 26rpx; color: #666; margin-bottom: 12rpx; } |
| | | .form-label.required:before { content: "*"; color: #f56c6c; margin-right: 6rpx; } |
| | | .selector-trigger { display: flex; align-items: center; justify-content: space-between; padding: 20rpx 24rpx; background: #f5f5f5; border-radius: 12rpx; } |
| | | .selector-text { font-size: 28rpx; color: #333; } |
| | | .selector-text.placeholder { color: #999; } |
| | | .bottom-bar { position: fixed; left: 0; right: 0; bottom: 0; padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom)); background: #fff; box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04); } |
| | | .btn-submit { height: 88rpx; border-radius: 999rpx; background: #2979ff; color: #fff; font-size: 30rpx; display: flex; align-items: center; justify-content: center; } |
| | | .product-popup { background: #fff; border-top-left-radius: 20rpx; border-top-right-radius: 20rpx; padding: 24rpx; } |
| | | .popup-header { text-align: center; margin-bottom: 20rpx; } |
| | | .popup-title { font-size: 30rpx; font-weight: 500; color: #333; } |
| | | .popup-search { display: grid; grid-template-columns: 1fr 1fr 140rpx; gap: 16rpx; align-items: center; margin-bottom: 16rpx; } |
| | | .popup-search-btn { height: 72rpx; border-radius: 12rpx; background: #2979ff; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 28rpx; } |
| | | .product-list { max-height: 60vh; } |
| | | .product-item { padding: 20rpx 12rpx; border-bottom: 1rpx solid #eee; } |
| | | .product-name-row { display: flex; justify-content: space-between; align-items: center; } |
| | | .product-name { font-size: 28rpx; color: #333; font-weight: 500; } |
| | | .product-unit { font-size: 24rpx; color: #999; } |
| | | .product-model { font-size: 24rpx; color: #666; margin-top: 8rpx; } |
| | | .no-data { text-align: center; padding: 40rpx 0; color: #999; } |
| | | </style> |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="stock-mgmt-page"> |
| | | <PageHeader title="åºå管ç" @back="goBack" /> |
| | | |
| | | <view class="tabs-wrap"> |
| | | <view |
| | | v-for="tab in tabs" |
| | | :key="tab.name" |
| | | class="tab-item" |
| | | :class="{ active: activeTab === tab.name }" |
| | | @click="activeTab = tab.name" |
| | | > |
| | | <text>{{ tab.label }}</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="search-section"> |
| | | <view class="search-row"> |
| | | <view class="search-input-wrap"> |
| | | <up-input |
| | | v-model="searchForm.productName" |
| | | placeholder="产å大类" |
| | | clearable |
| | | /> |
| | | </view> |
| | | <view class="btn-search" @click="handleQuery"> |
| | | <view class="btn-search-inner"> |
| | | <up-icon name="search" size="22" color="#fff"></up-icon> |
| | | <text>æç´¢</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="list-section"> |
| | | <view v-if="tableData.length > 0"> |
| | | <view |
| | | v-for="(item, index) in tableData" |
| | | :key="item.id || index" |
| | | class="card-item" |
| | | > |
| | | <view class="card-click" @click="goDetail(item)"> |
| | | <view class="card-header"> |
| | | <view class="header-main"> |
| | | <text class="product-name">{{ item.productName }}</text> |
| | | <text class="sub-title">{{ item.model || item.updateTime }}</text> |
| | | </view> |
| | | </view> |
| | | <up-divider /> |
| | | <view class="card-body"> |
| | | <view class="row"><text class="l">è§æ ¼åå·</text><text class="r">{{ item.model }}</text></view> |
| | | <view class="row"><text class="l">åä½</text><text class="r">{{ item.unit }}</text></view> |
| | | <view class="row"><text class="l">æ»åºåæ°</text><text class="r highlight">{{ item.qualitity }}</text></view> |
| | | <view class="row"><text class="l">å»ç»æ°é</text><text class="r">{{ item.lockedQuantity || 0 }}</text></view> |
| | | <view class="row"><text class="l">å¯ç¨åºå</text><text class="r">{{ item.unLockedQuantity ?? (item.qualitity - (item.lockedQuantity || 0)) }}</text></view> |
| | | <view class="row"><text class="l">æè¿æ´æ°æ¶é´</text><text class="r">{{ item.updateTime }}</text></view> |
| | | </view> |
| | | </view> |
| | | <view class="card-actions"> |
| | | <view |
| | | class="btn-link btn-link-primary" |
| | | :class="{ disabled: !(item.unLockedQuantity > 0) }" |
| | | @click="openSubtract(item)" |
| | | >åºåº</view> |
| | | <view |
| | | class="btn-link btn-link-warn" |
| | | v-if="item.unLockedQuantity > 0" |
| | | @click="openFrozen(item)" |
| | | >å»ç»</view> |
| | | <view |
| | | class="btn-link btn-link-plain" |
| | | v-if="(item.lockedQuantity || 0) > 0" |
| | | @click="openThaw(item)" |
| | | >è§£å»</view> |
| | | </view> |
| | | </view> |
| | | <view class="load-more-wrap"> |
| | | <u-loadmore :status="loadStatus" @loadmore="loadMore" /> |
| | | </view> |
| | | </view> |
| | | <view v-else class="no-data">ææ æ°æ®</view> |
| | | </view> |
| | | |
| | | <up-popup :show="showQuantityPopup" mode="center" round="16" @close="closeQuantityPopup"> |
| | | <view class="popup-inner"> |
| | | <view class="popup-title">{{ quantityTitle }}</view> |
| | | <view class="form-row"> |
| | | <text class="form-label">æ°é</text> |
| | | <up-input v-model="quantityForm.num" type="number" :placeholder="'æå¤§' + maxQuantity" /> |
| | | </view> |
| | | <view class="popup-footer"> |
| | | <view class="btn-cancel" @click="closeQuantityPopup">åæ¶</view> |
| | | <view class="btn-ok" @click="submitQuantity">ç¡®å®</view> |
| | | </view> |
| | | </view> |
| | | </up-popup> |
| | | |
| | | <view class="fab-button" @click="goAdd"> |
| | | <up-icon name="plus" size="24" color="#ffffff"></up-icon> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, reactive, ref, toRefs, watch } from "vue"; |
| | | import { onReachBottom, onShow } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import { frozenConsumablesIn, getConsumablesInListPage, thawConsumablesIn } from "@/api/consumablesLogistics/consumablesIn.js"; |
| | | import { frozenConsumablesUninventory, getConsumablesUninventoryListPage, thawConsumablesUninventory } from "@/api/consumablesLogistics/consumablesUninventory.js"; |
| | | |
| | | const activeTab = ref("qualified"); |
| | | const tabs = [ |
| | | { label: "åæ ¼åºå", name: "qualified" }, |
| | | { label: "ä¸åæ ¼åºå", name: "unqualified" }, |
| | | ]; |
| | | const tableData = ref([]); |
| | | const total = ref(0); |
| | | const loadStatus = ref("loadmore"); |
| | | const showQuantityPopup = ref(false); |
| | | const quantityOp = ref(""); |
| | | const currentRecord = ref(null); |
| | | const page = reactive({ current: 1, size: 20 }); |
| | | const data = reactive({ |
| | | searchForm: { productName: "" }, |
| | | quantityForm: { num: "" }, |
| | | }); |
| | | const { searchForm, quantityForm } = toRefs(data); |
| | | |
| | | const isQualified = () => activeTab.value === "qualified"; |
| | | const getList = () => { |
| | | const isFirstPage = page.current === 1; |
| | | if (isFirstPage) { |
| | | uni.showLoading({ title: "å è½½ä¸...", mask: true }); |
| | | } |
| | | const params = { ...page, productName: searchForm.value.productName }; |
| | | const api = isQualified() ? getConsumablesInListPage : getConsumablesUninventoryListPage; |
| | | api(params) |
| | | .then((res) => { |
| | | uni.hideLoading(); |
| | | const records = res.data?.records || []; |
| | | const totalCount = res.data?.total || 0; |
| | | if (isFirstPage) { |
| | | tableData.value = records; |
| | | } else { |
| | | tableData.value = [...tableData.value, ...records]; |
| | | } |
| | | total.value = totalCount; |
| | | loadStatus.value = tableData.value.length >= totalCount || totalCount === 0 ? "nomore" : "loadmore"; |
| | | }) |
| | | .catch(() => { |
| | | uni.hideLoading(); |
| | | loadStatus.value = "error"; |
| | | if (isFirstPage) { |
| | | uni.showToast({ title: "å 载失败", icon: "none" }); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | const loadMore = () => { |
| | | if (loadStatus.value === "nomore" || loadStatus.value === "loading") return; |
| | | loadStatus.value = "loading"; |
| | | page.current++; |
| | | getList(); |
| | | }; |
| | | |
| | | watch(activeTab, () => { |
| | | page.current = 1; |
| | | loadStatus.value = "loadmore"; |
| | | getList(); |
| | | }); |
| | | |
| | | const handleQuery = () => { |
| | | page.current = 1; |
| | | loadStatus.value = "loadmore"; |
| | | getList(); |
| | | }; |
| | | |
| | | const goAdd = () => { |
| | | const type = isQualified() ? "0" : "1"; |
| | | uni.navigateTo({ |
| | | url: `/pages/consumablesLogistics/stockManagement/add?type=${type}`, |
| | | }); |
| | | }; |
| | | |
| | | const quantityTitle = computed(() => { |
| | | if (quantityOp.value === "frozen") return "å»ç»åºå"; |
| | | if (quantityOp.value === "thaw") return "è§£å»åºå"; |
| | | return ""; |
| | | }); |
| | | |
| | | const maxQuantity = computed(() => { |
| | | if (!currentRecord.value) return 0; |
| | | if (quantityOp.value === "frozen") return currentRecord.value.unLockedQuantity || 0; |
| | | if (quantityOp.value === "thaw") return currentRecord.value.lockedQuantity || 0; |
| | | return 0; |
| | | }); |
| | | |
| | | const openSubtract = (row) => { |
| | | if (!(row.unLockedQuantity > 0)) return; |
| | | try { |
| | | uni.setStorageSync( |
| | | "stockSubtractRecord", |
| | | JSON.stringify({ |
| | | item: row, |
| | | type: isQualified() ? "0" : "1", |
| | | }) |
| | | ); |
| | | } catch (e) {} |
| | | const typeParam = isQualified() ? "0" : "1"; |
| | | uni.navigateTo({ |
| | | url: `/pages/consumablesLogistics/stockManagement/subtract?type=${typeParam}&id=${row.id}`, |
| | | }); |
| | | }; |
| | | |
| | | const openFrozen = (row) => { |
| | | quantityOp.value = "frozen"; |
| | | currentRecord.value = row; |
| | | quantityForm.value.num = ""; |
| | | showQuantityPopup.value = true; |
| | | }; |
| | | |
| | | const openThaw = (row) => { |
| | | quantityOp.value = "thaw"; |
| | | currentRecord.value = row; |
| | | quantityForm.value.num = ""; |
| | | showQuantityPopup.value = true; |
| | | }; |
| | | |
| | | const closeQuantityPopup = () => { |
| | | showQuantityPopup.value = false; |
| | | currentRecord.value = null; |
| | | quantityOp.value = ""; |
| | | }; |
| | | |
| | | const submitQuantity = () => { |
| | | const num = Number(quantityForm.value.num); |
| | | if (!num || num <= 0 || num > maxQuantity.value) { |
| | | uni.showToast({ title: `请è¾å
¥ 1~${maxQuantity.value} ä¹é´çæ°é`, icon: "none" }); |
| | | return; |
| | | } |
| | | const id = currentRecord.value?.id; |
| | | if (!id) return; |
| | | const base = { id, lockedQuantity: num }; |
| | | let promise; |
| | | if (quantityOp.value === "frozen") { |
| | | promise = isQualified() ? frozenConsumablesIn(base) : frozenConsumablesUninventory(base); |
| | | } else { |
| | | promise = isQualified() ? thawConsumablesIn(base) : thawConsumablesUninventory(base); |
| | | } |
| | | promise |
| | | .then(() => { |
| | | uni.showToast({ title: "æä½æå", icon: "success" }); |
| | | closeQuantityPopup(); |
| | | getList(); |
| | | }) |
| | | .catch(() => uni.showToast({ title: "æä½å¤±è´¥", icon: "none" })); |
| | | }; |
| | | |
| | | const goDetail = (item) => { |
| | | if (!item) return; |
| | | try { |
| | | uni.setStorageSync( |
| | | "stockDetailItem", |
| | | JSON.stringify({ |
| | | item, |
| | | type: isQualified() ? "0" : "1", |
| | | }) |
| | | ); |
| | | } catch (e) {} |
| | | if (!item.id) { |
| | | uni.navigateTo({ url: "/pages/consumablesLogistics/stockManagement/view" }); |
| | | } else { |
| | | uni.navigateTo({ url: "/pages/consumablesLogistics/stockManagement/view?id=" + item.id }); |
| | | } |
| | | }; |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | onShow(() => getList()); |
| | | onReachBottom(() => { |
| | | loadMore(); |
| | | }); |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .stock-mgmt-page { min-height: 100vh; background: #f5f5f5; padding-bottom: 120rpx; } |
| | | .tabs-wrap { display: flex; background: #fff; padding: 24rpx; gap: 24rpx; } |
| | | .tab-item { flex: 1; text-align: center; padding: 20rpx; border-radius: 12rpx; background: #f0f0f0; font-size: 28rpx; color: #666; } |
| | | .tab-item.active { background: #2979ff; color: #fff; } |
| | | .search-section { background: #fff; margin: 24rpx; padding: 24rpx; border-radius: 16rpx; } |
| | | .search-row { display: flex; align-items: center; } |
| | | .search-input-wrap { flex: 1; margin-right: 20rpx; min-width: 0; } |
| | | .btn-search { display: flex; align-items: center; justify-content: center; width: 180rpx; min-height: 72rpx; flex-shrink: 0; padding: 20rpx 24rpx; background: #2979ff; color: #fff; border-radius: 12rpx; font-size: 28rpx; box-sizing: border-box; text-align: center; } |
| | | .btn-search-inner { display: flex; flex-direction: row; align-items: center; justify-content: center; gap: 8rpx; } |
| | | .list-section { padding: 0 24rpx; } |
| | | .card-item { background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 24rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); } |
| | | .card-header { padding: 8rpx 0; } |
| | | .header-main { display: flex; flex-direction: column; gap: 8rpx; } |
| | | .product-name { font-size: 30rpx; font-weight: 500; color: #333; } |
| | | .sub-title { font-size: 24rpx; color: #999; } |
| | | .card-body .row { display: flex; justify-content: space-between; padding: 12rpx 0; font-size: 26rpx; } |
| | | .card-body .l { color: #666; } |
| | | .card-body .r { color: #333; } |
| | | .card-body .r.highlight { color: #2979ff; font-weight: 500; } |
| | | .card-actions { display: flex; gap: 16rpx; margin-top: 16rpx; padding-top: 16rpx; border-top: 1rpx solid #eee; justify-content: flex-end; } |
| | | .btn-link { min-width: 120rpx; padding: 10rpx 20rpx; border-radius: 24rpx; font-size: 26rpx; text-align: center; border-width: 1rpx; border-style: solid; } |
| | | .btn-link-primary { color: #ffffff; background: #2979ff; border-color: #2979ff; } |
| | | .btn-link-warn { color: #ff9f1a; background: rgba(255, 159, 26, 0.08); border-color: rgba(255, 159, 26, 0.6); } |
| | | .btn-link-plain { color: #666666; background: #f5f5f5; border-color: #e0e0e0; } |
| | | .btn-link.disabled { color: #cccccc; background: #f5f5f5; border-color: #e0e0e0; } |
| | | .no-data { text-align: center; padding: 80rpx 0; color: #999; font-size: 28rpx; } |
| | | .fab-button { position: fixed; bottom: calc(30px + env(safe-area-inset-bottom)); right: 30px; width: 56px; height: 56px; background: #2979ff; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 16px rgba(41, 121, 255, 0.3); z-index: 1000; } |
| | | .popup-inner { padding: 40rpx; min-width: 560rpx; background: #fff; } |
| | | .popup-title { font-size: 32rpx; font-weight: 500; margin-bottom: 32rpx; } |
| | | .form-row { margin-bottom: 24rpx; } |
| | | .form-row .form-label { display: block; font-size: 26rpx; color: #666; margin-bottom: 12rpx; } |
| | | .popup-footer { display: flex; gap: 24rpx; margin-top: 40rpx; } |
| | | .btn-cancel { flex: 1; text-align: center; padding: 24rpx; background: #f0f0f0; border-radius: 12rpx; } |
| | | .btn-ok { flex: 1; text-align: center; padding: 24rpx; background: #2979ff; color: #fff; border-radius: 12rpx; } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="subtract-stock-page"> |
| | | <PageHeader title="åºåº" @back="goBack" /> |
| | | |
| | | <scroll-view scroll-y class="content-scroll"> |
| | | <view class="form-section"> |
| | | <view class="section-title">åºåä¿¡æ¯</view> |
| | | <view class="info-row"> |
| | | <text class="label">产å大类</text> |
| | | <text class="value">{{ stockRecord.productName }}</text> |
| | | </view> |
| | | <view class="info-row"> |
| | | <text class="label">è§æ ¼åå·</text> |
| | | <text class="value">{{ stockRecord.model }}</text> |
| | | </view> |
| | | <view class="info-row"> |
| | | <text class="label">å¯ç¨åºå</text> |
| | | <text class="value highlight">{{ stockRecord.unLockedQuantity }}</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="form-section"> |
| | | <view class="section-title">åºåºä¿¡æ¯</view> |
| | | <view class="form-row"> |
| | | <text class="form-label required">åºåºæ°é</text> |
| | | <up-input v-model="form.stockOutNum" type="number" :placeholder="'æå¤§' + stockRecord.unLockedQuantity" /> |
| | | </view> |
| | | <view class="form-row" v-if="isQualified"> |
| | | <text class="form-label">车çå·</text> |
| | | <up-input v-model="form.licensePlateNo" placeholder="请è¾å
¥è½¦çå·" /> |
| | | </view> |
| | | <view class="form-row" v-if="isQualified"> |
| | | <text class="form-label">æ¯é(å¨)</text> |
| | | <up-input v-model="form.grossWeight" type="number" placeholder="请è¾å
¥æ¯é" /> |
| | | </view> |
| | | <view class="form-row" v-if="isQualified"> |
| | | <text class="form-label">ç®é(å¨)</text> |
| | | <up-input v-model="form.tareWeight" type="number" placeholder="请è¾å
¥ç®é" /> |
| | | </view> |
| | | <view class="form-row" v-if="isQualified"> |
| | | <text class="form-label">åé(å¨)</text> |
| | | <up-input v-model="form.netWeight" type="number" disabled placeholder="èªå¨è®¡ç®" /> |
| | | </view> |
| | | <view class="form-row" v-if="isQualified"> |
| | | <text class="form-label">è¿ç£
æ¥æ</text> |
| | | <view class="selector-trigger" @click="openWeighingDatePicker"> |
| | | <text class="selector-text" :class="{ placeholder: !form.weighingDate }"> |
| | | {{ form.weighingDate || "è¯·éæ©è¿ç£
æ¥æ" }} |
| | | </text> |
| | | <up-icon name="calendar" size="16" color="#999"></up-icon> |
| | | </view> |
| | | </view> |
| | | <view class="form-row" v-if="isQualified"> |
| | | <text class="form-label">è¿ç£
å</text> |
| | | <up-input v-model="form.weighingOperator" placeholder="请è¾å
¥è¿ç£
å" /> |
| | | </view> |
| | | <view class="form-row"> |
| | | <text class="form-label">夿³¨</text> |
| | | <up-input v-model="form.remark" type="textarea" placeholder="éå¡«" /> |
| | | </view> |
| | | </view> |
| | | </scroll-view> |
| | | |
| | | <view class="bottom-bar"> |
| | | <view class="btn-submit" @click="handleSubmit">æäº¤</view> |
| | | </view> |
| | | |
| | | <up-popup :show="showWeighingDatePicker" mode="bottom" @close="showWeighingDatePicker = false"> |
| | | <up-datetime-picker |
| | | :show="true" |
| | | v-model="weighingDateValue" |
| | | mode="datetime" |
| | | @confirm="onWeighingDateConfirm" |
| | | @cancel="showWeighingDatePicker = false" |
| | | /> |
| | | </up-popup> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, reactive, ref, watch } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import dayjs from "dayjs"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import { subtractConsumablesIn } from "@/api/consumablesLogistics/consumablesIn.js"; |
| | | import { subtractConsumablesUnInventory } from "@/api/consumablesLogistics/consumablesUninventory.js"; |
| | | |
| | | const type = ref("0"); |
| | | const isQualified = computed(() => type.value === "0"); |
| | | |
| | | const stockRecord = reactive({ |
| | | id: "", |
| | | productName: "", |
| | | model: "", |
| | | unLockedQuantity: 0, |
| | | }); |
| | | |
| | | const form = reactive({ |
| | | stockOutNum: "", |
| | | licensePlateNo: "", |
| | | grossWeight: "", |
| | | tareWeight: "", |
| | | netWeight: "", |
| | | weighingDate: "", |
| | | weighingOperator: "", |
| | | remark: "", |
| | | }); |
| | | |
| | | const showWeighingDatePicker = ref(false); |
| | | const weighingDateValue = ref(Date.now()); |
| | | |
| | | onLoad((options) => { |
| | | if (options && options.type != null) { |
| | | type.value = options.type; |
| | | } |
| | | const cached = uni.getStorageSync("stockSubtractRecord"); |
| | | if (cached) { |
| | | try { |
| | | const payload = typeof cached === "string" ? JSON.parse(cached) : cached; |
| | | const item = payload && payload.item != null ? payload.item : payload; |
| | | stockRecord.id = item.id; |
| | | stockRecord.productName = item.productName; |
| | | stockRecord.model = item.model; |
| | | stockRecord.unLockedQuantity = item.unLockedQuantity || 0; |
| | | uni.removeStorageSync("stockSubtractRecord"); |
| | | } catch (e) { |
| | | uni.removeStorageSync("stockSubtractRecord"); |
| | | } |
| | | } |
| | | }); |
| | | |
| | | const computeNetWeight = () => { |
| | | const gross = Number(form.grossWeight); |
| | | const tare = Number(form.tareWeight); |
| | | if (!isNaN(gross) && !isNaN(tare)) { |
| | | const net = Number((gross - tare).toFixed(2)); |
| | | form.netWeight = net > 0 ? net : 0; |
| | | } else { |
| | | form.netWeight = ""; |
| | | } |
| | | }; |
| | | |
| | | watch( |
| | | () => [form.grossWeight, form.tareWeight], |
| | | () => computeNetWeight() |
| | | ); |
| | | |
| | | const openWeighingDatePicker = () => { |
| | | weighingDateValue.value = form.weighingDate |
| | | ? dayjs(form.weighingDate, "YYYY-MM-DD HH:mm:ss").valueOf() |
| | | : Date.now(); |
| | | showWeighingDatePicker.value = true; |
| | | }; |
| | | |
| | | const onWeighingDateConfirm = (e) => { |
| | | const ts = e?.value ?? weighingDateValue.value; |
| | | form.weighingDate = dayjs(ts).format("YYYY-MM-DD HH:mm:ss"); |
| | | showWeighingDatePicker.value = false; |
| | | }; |
| | | |
| | | const handleSubmit = () => { |
| | | const outNum = Number(form.stockOutNum); |
| | | if (!outNum || outNum <= 0 || outNum > Number(stockRecord.unLockedQuantity)) { |
| | | uni.showToast({ title: `请è¾å
¥ 1~${stockRecord.unLockedQuantity} ä¹é´çæ°é`, icon: "none" }); |
| | | return; |
| | | } |
| | | const api = isQualified.value ? subtractConsumablesIn : subtractConsumablesUnInventory; |
| | | api({ |
| | | id: stockRecord.id, |
| | | stockOutNum: outNum, |
| | | licensePlateNo: form.licensePlateNo, |
| | | grossWeight: form.grossWeight, |
| | | tareWeight: form.tareWeight, |
| | | netWeight: form.netWeight, |
| | | weighingDate: form.weighingDate, |
| | | weighingOperator: form.weighingOperator, |
| | | remark: form.remark, |
| | | }) |
| | | .then(() => { |
| | | uni.showToast({ title: "åºåºæå", icon: "success" }); |
| | | setTimeout(() => uni.navigateBack(), 400); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "åºåºå¤±è´¥", icon: "none" }); |
| | | }); |
| | | }; |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .subtract-stock-page { min-height: 100vh; background: #f5f5f5; padding-bottom: 100rpx; } |
| | | .content-scroll { height: calc(100vh - 100rpx); } |
| | | .form-section { background: #fff; margin: 24rpx; padding: 24rpx; border-radius: 16rpx; } |
| | | .section-title { font-size: 28rpx; font-weight: 500; color: #333; margin-bottom: 12rpx; } |
| | | .info-row { display: flex; justify-content: space-between; padding: 12rpx 0; font-size: 26rpx; } |
| | | .info-row .label { color: #666; } |
| | | .info-row .value { color: #333; } |
| | | .info-row .value.highlight { color: #2979ff; font-weight: 500; } |
| | | .form-row { margin-bottom: 24rpx; } |
| | | .form-label { display: block; font-size: 26rpx; color: #666; margin-bottom: 12rpx; } |
| | | .form-label.required:before { content: "*"; color: #f56c6c; margin-right: 6rpx; } |
| | | .selector-trigger { display: flex; align-items: center; justify-content: space-between; padding: 20rpx 24rpx; background: #f5f5f5; border-radius: 12rpx; } |
| | | .selector-text { font-size: 28rpx; color: #333; } |
| | | .selector-text.placeholder { color: #999; } |
| | | .bottom-bar { position: fixed; left: 0; right: 0; bottom: 0; padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom)); background: #fff; box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04); } |
| | | .btn-submit { height: 88rpx; border-radius: 999rpx; background: #2979ff; color: #fff; font-size: 30rpx; display: flex; align-items: center; justify-content: center; } |
| | | </style> |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="detail-page"> |
| | | <PageHeader title="åºå详æ
" @back="goBack" /> |
| | | <view v-if="loading" class="loading-wrap"> |
| | | <text class="loading-text">å è½½ä¸...</text> |
| | | </view> |
| | | <view v-else-if="detail" class="detail-wrap"> |
| | | <view class="section-card"> |
| | | <view class="section-head"> |
| | | <view class="section-dot"></view> |
| | | <text class="section-title">åºç¡ä¿¡æ¯</text> |
| | | </view> |
| | | <view class="section-body"> |
| | | <view class="detail-row"> |
| | | <text class="label">åºå·</text> |
| | | <text class="value">{{ detail.index ?? '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="label">产å大类</text> |
| | | <text class="value value-strong">{{ detail.productName || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="label">è§æ ¼åå·</text> |
| | | <text class="value">{{ detail.model || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="label">åä½</text> |
| | | <text class="value">{{ detail.unit || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row detail-row-highlight"> |
| | | <text class="label">æ»åºå</text> |
| | | <text class="value value-num">{{ detail.qualitity ?? '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="label">å»ç»æ°é</text> |
| | | <text class="value">{{ detail.lockedQuantity ?? 0 }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="label">å¯ç¨åºå</text> |
| | | <text class="value">{{ detail.unLockedQuantity ?? '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="label">åºåç±»å</text> |
| | | <text class="value">{{ detail.typeLabel }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="label">æè¿æ´æ°æ¶é´</text> |
| | | <text class="value">{{ detail.updateTime || '-' }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else class="empty"> |
| | | <text class="empty-text">ææ è¯¦æ
æ°æ®</text> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | |
| | | const detail = ref(null); |
| | | const loading = ref(true); |
| | | |
| | | function normalizeDetail(raw, type) { |
| | | if (!raw) return null; |
| | | const d = typeof raw === "object" ? raw : {}; |
| | | return { |
| | | index: d.index ?? 1, |
| | | productName: d.productName, |
| | | model: d.model, |
| | | unit: d.unit, |
| | | qualitity: d.qualitity, |
| | | lockedQuantity: d.lockedQuantity, |
| | | unLockedQuantity: d.unLockedQuantity ?? (d.qualitity - (d.lockedQuantity || 0)), |
| | | updateTime: d.updateTime, |
| | | typeLabel: type === "1" ? "ä¸åæ ¼åºå" : "åæ ¼åºå", |
| | | }; |
| | | } |
| | | |
| | | onLoad(() => { |
| | | const cached = uni.getStorageSync("stockDetailItem"); |
| | | if (cached) { |
| | | try { |
| | | const payload = typeof cached === "string" ? JSON.parse(cached) : cached; |
| | | const item = payload && payload.item != null ? payload.item : payload; |
| | | const type = payload && payload.type != null ? payload.type : "0"; |
| | | detail.value = normalizeDetail({ ...item, index: 1 }, type); |
| | | uni.removeStorageSync("stockDetailItem"); |
| | | } catch (e) { |
| | | uni.removeStorageSync("stockDetailItem"); |
| | | } |
| | | } |
| | | loading.value = false; |
| | | }); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .detail-page { min-height: 100vh; background: linear-gradient(180deg, #e8eef7 0%, #f2f5fa 100%); padding-bottom: 48rpx; } |
| | | .loading-wrap { padding: 120rpx 48rpx; text-align: center; } |
| | | .loading-text { color: #8c9aa8; font-size: 28rpx; } |
| | | .empty { padding: 120rpx 48rpx; text-align: center; } |
| | | .empty-text { color: #8c9aa8; font-size: 28rpx; } |
| | | .detail-wrap { padding: 24rpx 24rpx 32rpx; } |
| | | .section-card { background: #fff; border-radius: 24rpx; overflow: hidden; margin-bottom: 28rpx; box-shadow: 0 8rpx 32rpx rgba(41, 121, 255, 0.06); border: 1rpx solid rgba(41, 121, 255, 0.06); } |
| | | .section-head { display: flex; align-items: center; padding: 28rpx 32rpx; background: linear-gradient(135deg, #f8fbff 0%, #f0f6ff 100%); border-bottom: 1rpx solid #eef3fa; } |
| | | .section-dot { width: 8rpx; height: 8rpx; border-radius: 50%; background: #2979ff; margin-right: 16rpx; } |
| | | .section-title { font-size: 30rpx; font-weight: 600; color: #1e3a5f; letter-spacing: 0.5rpx; } |
| | | .section-body { padding: 8rpx 32rpx 24rpx; } |
| | | .detail-row { display: flex; align-items: center; min-height: 96rpx; padding: 0 16rpx; border-radius: 12rpx; font-size: 28rpx; margin-bottom: 4rpx; } |
| | | .detail-row .label { width: 200rpx; flex-shrink: 0; color: #6b7c93; font-size: 26rpx; } |
| | | .detail-row .value { flex: 1; color: #2c3e50; text-align: right; word-break: break-all; font-size: 28rpx; } |
| | | .detail-row .value-strong { color: #1e3a5f; font-weight: 500; } |
| | | .detail-row .value-num { color: #2979ff; font-weight: 600; font-size: 32rpx; } |
| | | .detail-row-highlight { background: linear-gradient(90deg, rgba(41, 121, 255, 0.06) 0%, transparent 100%); margin: 12rpx -16rpx 4rpx; padding: 20rpx 16rpx; } |
| | | </style> |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="report-page"> |
| | | <PageHeader title="åºåæ¥è¡¨" @back="goBack" /> |
| | | <view class="tabs-wrap"> |
| | | <view |
| | | v-for="t in reportTypes" |
| | | :key="t.value" |
| | | class="tab-item" |
| | | :class="{ active: searchForm.reportType === t.value }" |
| | | @click="searchForm.reportType = t.value" |
| | | > |
| | | <text>{{ t.label }}</text> |
| | | </view> |
| | | </view> |
| | | <view class="list-section"> |
| | | <view class="section-header"> |
| | | <text class="table-title">{{ tableTitle }}</text> |
| | | </view> |
| | | <view v-if="tableData.length > 0"> |
| | | <view v-for="(item, index) in tableData" :key="index" class="card-item"> |
| | | <view class="card-header"> |
| | | <view class="header-main"> |
| | | <text class="product-name">{{ item.productName }}</text> |
| | | <text class="sub-title">{{ item.model }}</text> |
| | | </view> |
| | | </view> |
| | | <up-divider /> |
| | | <view class="card-body"> |
| | | <view class="row"><text class="l">åä½</text><text class="r">{{ item.unit }}</text></view> |
| | | <view class="row" v-if="searchForm.reportType !== 'inout'"><text class="l">å
¥åºæ¶é´</text><text class="r">{{ item.createTime }}</text></view> |
| | | <view class="row" v-if="searchForm.reportType !== 'inout'"><text class="l">å
¥åºæ¹æ¬¡</text><text class="r">{{ item.inboundBatches }}</text></view> |
| | | <view class="row"><text class="l">å
¥åºæ°é</text><text class="r">{{ item.totalStockIn ?? item.stockInNum }}</text></view> |
| | | <view class="row" v-if="searchForm.reportType === 'inout'"><text class="l">åºåºæ°é</text><text class="r">{{ item.totalStockOut }}</text></view> |
| | | <view class="row"><text class="l">ç°å¨åºå</text><text class="r highlight">{{ item.currentStock }}</text></view> |
| | | <view class="row" v-if="item.createBy"><text class="l">å
¥åºäºº</text><text class="r">{{ item.createBy }}</text></view> |
| | | </view> |
| | | </view> |
| | | <view class="load-more-wrap"> |
| | | <u-loadmore :status="loadStatus" @loadmore="loadMore" /> |
| | | </view> |
| | | </view> |
| | | <view v-else class="no-data">ææ æ°æ®</view> |
| | | </view> |
| | | <up-popup :show="showDatePicker" mode="bottom" @close="showDatePicker = false"> |
| | | <up-datetime-picker |
| | | v-model="dateValue" |
| | | :mode="datePickerMode" |
| | | @confirm="onDateConfirm" |
| | | @cancel="showDatePicker = false" |
| | | /> |
| | | </up-popup> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, toRefs, computed, watch } from "vue"; |
| | | import dayjs from "dayjs"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import { formatDateToYMD } from "@/utils/ruoyi"; |
| | | import { onShow, onReachBottom } from "@dcloudio/uni-app"; |
| | | import { getConsumablesInReportList, getConsumablesInInAndOutReportList } from "@/api/consumablesLogistics/consumablesIn.js"; |
| | | |
| | | const reportTypes = [ |
| | | { label: "æ¥æ¥", value: "daily" }, |
| | | { label: "ææ¥", value: "monthly" }, |
| | | { label: "è¿åºåæ¥è¡¨", value: "inout" }, |
| | | ]; |
| | | const tableData = ref([]); |
| | | const showDatePicker = ref(false); |
| | | const dateValue = ref(Date.now()); |
| | | const datePickerTarget = ref(""); |
| | | const loadStatus = ref("loadmore"); |
| | | const page = reactive({ current: 1, size: 20 }); |
| | | const data = reactive({ |
| | | searchForm: { |
| | | reportType: "daily", |
| | | singleDate: "", |
| | | startMonth: "", |
| | | endMonth: "", |
| | | startDate: "", |
| | | endDate: "", |
| | | }, |
| | | }); |
| | | const { searchForm } = toRefs(data); |
| | | |
| | | const datePickerMode = computed(() => { |
| | | if (datePickerTarget.value === "startMonth" || datePickerTarget.value === "endMonth") return "month"; |
| | | return "date"; |
| | | }); |
| | | |
| | | const tableTitle = computed(() => { |
| | | const m = { daily: "æ¥æ¥è¯¦ç»æ°æ®", monthly: "ææ¥è¯¦ç»æ°æ®", inout: "è¿åºåæ¥è¡¨è¯¦ç»æ°æ®" }; |
| | | return m[searchForm.value.reportType] || "æ¥è¡¨æ°æ®"; |
| | | }); |
| | | |
| | | const getQueryParams = () => { |
| | | const p = { |
| | | reportType: searchForm.value.reportType, |
| | | current: page.current, |
| | | size: page.size, |
| | | }; |
| | | if (searchForm.value.reportType === "daily") { |
| | | p.reportDate = searchForm.value.singleDate; |
| | | } else if (searchForm.value.reportType === "monthly") { |
| | | p.startMonth = searchForm.value.startMonth; |
| | | p.endMonth = searchForm.value.endMonth; |
| | | } else if (searchForm.value.reportType === "monthly") { |
| | | p.startMonth = searchForm.value.startMonth; |
| | | p.endMonth = searchForm.value.endMonth; |
| | | } else { |
| | | p.startDate = searchForm.value.startDate; |
| | | p.endDate = searchForm.value.endDate; |
| | | } |
| | | return p; |
| | | }; |
| | | |
| | | const getList = () => { |
| | | const isFirstPage = page.current === 1; |
| | | if (isFirstPage) { |
| | | uni.showLoading({ title: "æ¥è¯¢ä¸...", mask: true }); |
| | | } |
| | | const params = getQueryParams(); |
| | | const isInout = searchForm.value.reportType === "inout"; |
| | | const api = isInout ? getConsumablesInInAndOutReportList : getConsumablesInReportList; |
| | | api(params) |
| | | .then((res) => { |
| | | uni.hideLoading(); |
| | | const records = res.data?.records || []; |
| | | const total = res.data?.total || records.length; |
| | | if (isFirstPage) { |
| | | tableData.value = records; |
| | | } else { |
| | | tableData.value = [...tableData.value, ...records]; |
| | | } |
| | | if (tableData.value.length >= total || total === 0) { |
| | | loadStatus.value = "nomore"; |
| | | } else { |
| | | loadStatus.value = "loadmore"; |
| | | } |
| | | }) |
| | | .catch(() => { |
| | | uni.hideLoading(); |
| | | loadStatus.value = "error"; |
| | | if (isFirstPage) { |
| | | uni.showToast({ title: "æ¥è¯¢å¤±è´¥", icon: "none" }); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | const handleQuery = () => { |
| | | page.current = 1; |
| | | loadStatus.value = "loadmore"; |
| | | getList(); |
| | | }; |
| | | |
| | | const loadMore = () => { |
| | | if (loadStatus.value === "nomore" || loadStatus.value === "loading") return; |
| | | loadStatus.value = "loading"; |
| | | page.current++; |
| | | getList(); |
| | | }; |
| | | |
| | | const openDatePicker = (target) => { |
| | | datePickerTarget.value = target; |
| | | let val = ""; |
| | | if (target === "single") val = searchForm.value.singleDate; |
| | | else if (target === "startMonth") val = searchForm.value.startMonth; |
| | | else if (target === "endMonth") val = searchForm.value.endMonth; |
| | | dateValue.value = val ? new Date(val).getTime() : Date.now(); |
| | | showDatePicker.value = true; |
| | | }; |
| | | |
| | | const onDateConfirm = (e) => { |
| | | const isMonth = datePickerTarget.value === "startMonth" || datePickerTarget.value === "endMonth"; |
| | | const str = isMonth ? dayjs(e.value).format("YYYY-MM") : formatDateToYMD(e.value); |
| | | if (datePickerTarget.value === "single") searchForm.value.singleDate = str; |
| | | else if (datePickerTarget.value === "startMonth") searchForm.value.startMonth = str; |
| | | else if (datePickerTarget.value === "endMonth") searchForm.value.endMonth = str; |
| | | showDatePicker.value = false; |
| | | handleQuery(); |
| | | }; |
| | | |
| | | const initDefaultDates = () => { |
| | | const today = dayjs(); |
| | | if (!searchForm.value.singleDate) { |
| | | searchForm.value.singleDate = today.format("YYYY-MM-DD"); |
| | | } |
| | | if (!searchForm.value.startMonth || !searchForm.value.endMonth) { |
| | | const startOfMonth = today.startOf("month").format("YYYY-MM-DD"); |
| | | const endOfMonth = today.endOf("month").format("YYYY-MM-DD"); |
| | | searchForm.value.startMonth = startOfMonth; |
| | | searchForm.value.endMonth = endOfMonth; |
| | | } |
| | | if (!searchForm.value.startDate || !searchForm.value.endDate) { |
| | | searchForm.value.endDate = today.format("YYYY-MM-DD"); |
| | | searchForm.value.startDate = today.subtract(6, "day").format("YYYY-MM-DD"); |
| | | } |
| | | }; |
| | | |
| | | watch( |
| | | () => searchForm.value.reportType, |
| | | () => { |
| | | handleQuery(); |
| | | } |
| | | ); |
| | | |
| | | onShow(() => { |
| | | initDefaultDates(); |
| | | handleQuery(); |
| | | }); |
| | | |
| | | onReachBottom(() => loadMore()); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .report-page { min-height: 100vh; background: #f5f5f5; padding-bottom: 40rpx; } |
| | | .tabs-wrap { display: flex; background: #fff; padding: 24rpx; gap: 24rpx; } |
| | | .tab-item { flex: 1; text-align: center; padding: 20rpx; border-radius: 12rpx; background: #f0f0f0; font-size: 28rpx; color: #666; } |
| | | .tab-item.active { background: #2979ff; color: #fff; } |
| | | .search-section { background: #fff; margin: 24rpx; padding: 24rpx; border-radius: 16rpx; } |
| | | .search-row { display: flex; align-items: center; margin-bottom: 0; flex-wrap: wrap; } |
| | | .search-row .label { width: 140rpx; font-size: 26rpx; color: #666; } |
| | | .search-row .label.end { margin-left: 24rpx; } |
| | | .date-picker { flex: 1; min-width: 200rpx; padding: 20rpx; background: #f5f5f5; border-radius: 12rpx; font-size: 28rpx; } |
| | | .btn-row { display: flex; gap: 24rpx; margin-top: 24rpx; } |
| | | .btn-query { flex: 1; text-align: center; padding: 24rpx; background: #2979ff; color: #fff; border-radius: 12rpx; } |
| | | .btn-reset { flex: 1; text-align: center; padding: 24rpx; background: #e0e0e0; border-radius: 12rpx; } |
| | | .list-section { margin: 24rpx; } |
| | | .section-header { margin-bottom: 16rpx; padding: 16rpx 20rpx; } |
| | | .table-title { font-size: 30rpx; font-weight: 500; color: #333; } |
| | | .card-item { background: #fff; border-radius: 16rpx; padding: 20rpx 24rpx; margin-bottom: 20rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); } |
| | | .card-header { padding: 4rpx 0 12rpx; } |
| | | .header-main { display: flex; flex-direction: column; gap: 6rpx; } |
| | | .product-name { font-size: 30rpx; font-weight: 500; color: #333; } |
| | | .sub-title { font-size: 24rpx; color: #999; } |
| | | .card-body .row { display: flex; justify-content: space-between; padding: 6rpx 0; font-size: 26rpx; } |
| | | .card-body .l { color: #666; } .card-body .r { color: #333; } .card-body .r.highlight { color: #2979ff; font-weight: 500; } |
| | | .no-data { text-align: center; padding: 60rpx 0; color: #999; font-size: 28rpx; } |
| | | .load-more-wrap { padding: 24rpx 0 8rpx; } |
| | | </style> |
| | |
| | | </up-grid> |
| | | </view> |
| | | </view> |
| | | <!-- èæç©ææ¨¡å --> |
| | | <view class="common-module material-module" |
| | | v-if="hasMaterialItems"> |
| | | <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 materialItems" |
| | | :key="index" |
| | | @click="handleCommonItemClick(item)"> |
| | | <view class="icon-container" |
| | | :style="{ background: item.bgColor }"> |
| | | <up-icon :name="item.icon" |
| | | :size="58" |
| | | color="#ffffff"></up-icon> |
| | | </view> |
| | | <text class="item-label">{{item.label}}</text> |
| | | </up-grid-item> |
| | | </up-grid> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | |
| | | { |
| | | icon: "/static/images/icon/caigoutaizhang@2x.png", |
| | | label: "åºåæ¥è¡¨", |
| | | }, |
| | | ]); |
| | | // èæç©æåè½æ°æ® |
| | | const materialItems = reactive([ |
| | | { |
| | | icon: "/static/images/icon/caigoutaizhang@2x.png", |
| | | label: "å
¥åºç®¡ç", |
| | | module: "material", |
| | | }, |
| | | { |
| | | icon: "/static/images/icon/caigoutaizhang@2x.png", |
| | | label: "åºåºå°è´¦", |
| | | module: "material", |
| | | }, |
| | | { |
| | | icon: "/static/images/icon/caigoutaizhang@2x.png", |
| | | label: "åºå管ç", |
| | | module: "material", |
| | | }, |
| | | { |
| | | icon: "/static/images/icon/caigoutaizhang@2x.png", |
| | | label: "åºåæ¥è¡¨", |
| | | module: "material", |
| | | }, |
| | | ]); |
| | | |
| | |
| | | break; |
| | | case "å
¥åºç®¡ç": |
| | | uni.navigateTo({ |
| | | url: "/pages/inventoryManagement/receiptManagement/index", |
| | | url: |
| | | item.module === "material" |
| | | ? "/pages/consumablesLogistics/receiptManagement/index" |
| | | : "/pages/inventoryManagement/receiptManagement/index", |
| | | }); |
| | | break; |
| | | case "åºåºå°è´¦": |
| | | uni.navigateTo({ |
| | | url: "/pages/inventoryManagement/dispatchLog/index", |
| | | url: |
| | | item.module === "material" |
| | | ? "/pages/consumablesLogistics/dispatchLog/index" |
| | | : "/pages/inventoryManagement/dispatchLog/index", |
| | | }); |
| | | break; |
| | | case "åºå管ç": |
| | | uni.navigateTo({ |
| | | url: "/pages/inventoryManagement/stockManagement/index", |
| | | url: |
| | | item.module === "material" |
| | | ? "/pages/consumablesLogistics/stockManagement/index" |
| | | : "/pages/inventoryManagement/stockManagement/index", |
| | | }); |
| | | break; |
| | | case "åºåæ¥è¡¨": |
| | | uni.navigateTo({ |
| | | url: "/pages/inventoryManagement/stockReport/index", |
| | | url: |
| | | item.module === "material" |
| | | ? "/pages/consumablesLogistics/stockReport/index" |
| | | : "/pages/inventoryManagement/stockReport/index", |
| | | }); |
| | | break; |
| | | default: |
| | |
| | | |
| | | // è¿æ»¤ä»å¨ç©æµèå |
| | | const originalWarehouseLogistics = [ |
| | | { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "å
¥åºç®¡ç" }, |
| | | { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "åºåºå°è´¦" }, |
| | | { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "åºå管ç" }, |
| | | { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "åºåæ¥è¡¨" }, |
| | | { |
| | | icon: "/static/images/icon/caigoutaizhang@2x.png", |
| | | label: "å
¥åºç®¡ç", |
| | | module: "warehouse", |
| | | }, |
| | | { |
| | | icon: "/static/images/icon/caigoutaizhang@2x.png", |
| | | label: "åºåºå°è´¦", |
| | | module: "warehouse", |
| | | }, |
| | | { |
| | | icon: "/static/images/icon/caigoutaizhang@2x.png", |
| | | label: "åºå管ç", |
| | | module: "warehouse", |
| | | }, |
| | | { |
| | | icon: "/static/images/icon/caigoutaizhang@2x.png", |
| | | label: "åºåæ¥è¡¨", |
| | | module: "warehouse", |
| | | }, |
| | | ]; |
| | | const filteredWarehouseLogistics = originalWarehouseLogistics.filter( |
| | | item => allowedMenuTitles.has(item.label) |
| | |
| | | warehouseLogisticsItems.length, |
| | | ...filteredWarehouseLogistics |
| | | ); |
| | | |
| | | // è¿æ»¤èæç©æèå |
| | | const materialCandidates = label => { |
| | | const list = [label, `èæ${label}`]; |
| | | if (label.endsWith("管ç")) { |
| | | list.push(`èæ${label.replace("管ç", "")}`); |
| | | } |
| | | return list; |
| | | }; |
| | | const originalMaterial = [ |
| | | { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "å
¥åºç®¡ç", module: "material" }, |
| | | { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "åºåºå°è´¦", module: "material" }, |
| | | { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "åºå管ç", module: "material" }, |
| | | { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "åºåæ¥è¡¨", module: "material" }, |
| | | ]; |
| | | const filteredMaterial = originalMaterial.filter(item => { |
| | | return materialCandidates(item.label).some(t => allowedMenuTitles.has(t)); |
| | | }); |
| | | materialItems.splice(0, materialItems.length, ...filteredMaterial); |
| | | }; |
| | | |
| | | // æ£æ¥æ¨¡åæ¯å¦æèå项éè¦æ¾ç¤º |
| | |
| | | const hasWarehouseLogisticsItems = computed( |
| | | () => warehouseLogisticsItems.length > 0 |
| | | ); |
| | | const hasMaterialItems = computed(() => materialItems.length > 0); |
| | | |
| | | onMounted(() => { |
| | | // æ¯æ¬¡è¿å
¥é¦é¡µé½å¼ºå¶å·æ°ç¨æ·ä¿¡æ¯åè·¯ç±æéï¼ä¸åæ¬å°ç¼å夿 |