src/api/productionManagement/productionOrder.js
@@ -17,6 +17,14 @@ data: query, }); } // èªå¨æ´¾å·¥ export function productionDispatchList(query) { return request({ url: "/salesLedger/scheduling/productionDispatchList", method: "post", data: query, }); } // è·åçæºæ£å¨å·¥ä½éæ°æ® export function schedulingList(query) { return request({ @@ -42,4 +50,30 @@ method: "post", data: data, }); } // æ¥è¯¢æèç export function getLossRate() { return request({ url: "/salesLedger/scheduling/loss", method: "get", }); } // æ°å¢æèç export function addLossRate(data) { return request({ url: "/salesLedger/scheduling/addLoss", method: "post", data: data, }); } // ä¿®æ¹æèç export function updateLossRate(data) { return request({ url: "/salesLedger/scheduling/updateLoss", method: "post", data: data, }); } src/config.js
@@ -2,9 +2,8 @@ const config = { // baseUrl: 'https://vue.ruoyi.vip/prod-api', // baseUrl: 'http://localhost/prod-api', // baseUrl: 'http://114.132.189.42:9066', // å®å¤æ¶¦æ³° // baseUrl: 'http://114.132.189.42:9068', // æ°çæµ·å·å¼å¿ baseUrl: 'http://192.168.1.147:8080', // æ¬å°æµè¯ baseUrl: 'http://114.132.189.42:9068', // æ°çæµ·å·å¼å¿ // baseUrl: 'http://192.168.1.185:9988', // æ¬å°æµè¯ //cloudåå°ç½å ³å°å // baseUrl: 'http://192.168.10.3:8080', // åºç¨ä¿¡æ¯ src/pages.json
@@ -413,6 +413,27 @@ "navigationBarTitleText": "ç产派工", "navigationStyle": "custom" } }, { "path": "pages/productionManagement/operationScheduling/index", "style": { "navigationBarTitleText": "å·¥åºæäº§", "navigationStyle": "custom" } }, { "path": "pages/productionManagement/productionReporting/index", "style": { "navigationBarTitleText": "ç产æ¥å·¥", "navigationStyle": "custom" } }, { "path": "pages/productionManagement/productionCosting/index", "style": { "navigationBarTitleText": "çäº§æ ¸ç®", "navigationStyle": "custom" } } ], "subPackages": [ src/pages/index.vue
@@ -437,17 +437,17 @@ break; case 'å·¥åºæäº§': uni.navigateTo({ url: '/pages/productionManagement/processScheduling/index' url: '/pages/productionManagement/operationScheduling/index' }); break; case 'ç产æ¥å·¥': uni.navigateTo({ url: '/pages/productionManagement/productionReport/index' url: '/pages/productionManagement/productionReporting/index' }); break; case 'çäº§æ ¸ç®': uni.navigateTo({ url: '/pages/productionManagement/productionAccounting/index' url: '/pages/productionManagement/productionCosting/index' }); break; case '设å¤å°è´¦': src/pages/login.vue
@@ -138,8 +138,6 @@ showToast("请è¾å ¥æ¨çè´¦å·") } else if (loginForm.value.password === "") { showToast("请è¾å ¥æ¨çå¯ç ") } else if (loginForm.value.factoryId === "") { showToast("è¯·éæ©å ¬å¸") } else { showToast("ç»å½ä¸ï¼è¯·èå¿çå¾ ...") pwdLogin() src/pages/productionManagement/operationScheduling/components/formDia.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,282 @@ <template> <view> <up-popup v-model:show="dialogFormVisible" mode="bottom" round="12" @close="closeDia" :customStyle="{ height: '85vh' }"> <view class="dia-container"> <view class="dia-header"> <text class="title">å·¥åºæäº§</text> <up-button size="mini" @click="addRow" type="primary">æ°å¢</up-button> <text class="pending">å¾ æäº§æ°éï¼{{ pendingNum }}</text> </view> <scroll-view class="rows" scroll-y> <view v-for="(row, index) in tableData" :key="index" class="row-card"> <view class="row-header"> <text class="row-index">#{{ index + 1 }}</text> <up-button size="mini" type="error" plain @click="removeRow(index)">å é¤</up-button> </view> <up-form> <up-form-item label="å·¥åº" label-width="80"> <up-input v-model="row.process" placeholder="请è¾å ¥å·¥åº" /> </up-form-item> <up-form-item label="åä½" label-width="80"> <up-input v-model="row.unit" placeholder="请è¾å ¥åä½" /> </up-form-item> <up-form-item label="å£å³/åå/è§æ ¼" label-width="110"> <up-input v-model="row.type" placeholder="请è¾å ¥" /> </up-form-item> <up-form-item label="æäº§æ°é" label-width="80"> <up-input v-model.number="row.schedulingNum" type="number" placeholder="请è¾å ¥" /> </up-form-item> <up-form-item label="å·¥æ¶å®é¢" label-width="80"> <up-input v-model.number="row.workHours" type="number" placeholder="请è¾å ¥" /> </up-form-item> <up-form-item label="æäº§æ¥æ" label-width="80" @click="openDatePicker(index)"> <up-input v-model="row.schedulingDate" placeholder="éæ©æ¥æ" readonly @click="openDatePicker(index)" /> <template #right> <up-icon name="calendar" @click="openDatePicker(index)"></up-icon> </template> </up-form-item> <up-form-item label="æäº§äºº" label-width="80" @click="openUserPicker(index)"> <up-input v-model="row.schedulingUserName" placeholder="éæ©äººå" readonly @click="openUserPicker(index)" /> <template #right> <up-icon name="arrow-right" @click="openUserPicker(index)"></up-icon> </template> </up-form-item> <up-form-item label="夿³¨" label-width="80"> <up-input v-model="row.remark" placeholder="请è¾å ¥å¤æ³¨" /> </up-form-item> </up-form> </view> </scroll-view> <view class="summary"> <text>æäº§æ°éå计ï¼{{ totalSchedulingNum }}</text> </view> <view class="dia-footer"> <up-button type="primary" @click="submitForm">确认</up-button> <up-button @click="closeDia">åæ¶</up-button> </view> </view> </up-popup> <!-- æ¥æéæ©å¨ï¼up ç³»åï¼ --> <up-popup :show="datePicker.show" mode="bottom" @close="datePicker.show = false"> <up-datetime-picker :show="true" v-model="datePicker.valueData" mode="date" @confirm="onDateConfirm" @cancel="datePicker.show = false" /> </up-popup> <!-- 人åéæ©å¨ï¼up ç³»å action-sheetï¼ --> <up-action-sheet :show="userPicker.show" :actions="userActionList" title="éæ©äººå" @select="onUserSelect" @close="userPicker.show = false" /> </view> </template> <script setup> import { ref, getCurrentInstance, computed } from 'vue' import { userListNoPageByTenantId } from '@/api/system/user.js' import { processScheduling } from '@/api/productionManagement/operationScheduling.js' const { proxy } = getCurrentInstance() const emit = defineEmits(['close']) const dialogFormVisible = ref(false) const operationType = ref('') const tableData = ref([]) const unitFromRow = ref('') const idFromRow = ref('') const specificationModelFromRow = ref('') const pendingNum = ref(0) const userList = ref([]) const receive = ref('') // pickers const datePicker = ref({ show: false, valueData: Date.now(), rowIndex: -1 }) const userPicker = ref({ show: false, rowIndex: -1 }) // ActionSheet æ°æ® const userActionList = computed(() => { return userList.value.map(u => ({ name: u.nickName, value: u.userId })) }) const totalSchedulingNum = computed(() => { return tableData.value.reduce((sum, r) => sum + (Number(r.schedulingNum || 0)), 0) }) const userLabel = (uid) => { const u = userList.value.find(u => u.userId === uid) return u ? u.nickName : '' } // æå¼å¼¹æ¡ const openDialog = (type, row) => { operationType.value = type dialogFormVisible.value = true userListNoPageByTenantId().then((res) => { userList.value = res.data }) pendingNum.value = row?.pendingNum ?? 0 unitFromRow.value = row?.unit ?? '' idFromRow.value = row?.id ?? '' specificationModelFromRow.value = row?.specificationModel ?? '' tableData.value = [createRow()] } const createRow = () => ({ id: idFromRow.value, process: '', schedulingDate: '', schedulingNum: null, schedulingUserId: '', schedulingUserName: '', workHours: null, unit: unitFromRow.value, remark: '', type: specificationModelFromRow.value, }) const openDatePicker = (idx) => { datePicker.value.rowIndex = idx datePicker.value.valueData = Date.now() datePicker.value.show = true } const onDateConfirm = (e) => { const val = e.value const d = new Date(val) const y = d.getFullYear() const m = String(d.getMonth() + 1).padStart(2, '0') const day = String(d.getDate()).padStart(2, '0') const str = `${y}-${m}-${day}` if (datePicker.value.rowIndex > -1) { tableData.value[datePicker.value.rowIndex].schedulingDate = str } datePicker.value.show = false } const openUserPicker = (idx) => { userPicker.value.rowIndex = idx userPicker.value.show = true } const onUserSelect = (item) => { if (item && userPicker.value.rowIndex > -1) { const row = tableData.value[userPicker.value.rowIndex] row.schedulingUserId = item.value row.schedulingUserName = item.name } userPicker.value.show = false } const submitForm = () => { // 1. æ£æ¥æ¯ä¸è¡æ¯å¦å¡«å宿´ for (let i = 0; i < tableData.value.length; i++) { const row = tableData.value[i] if (!row.process || !row.schedulingDate || row.schedulingNum === '' || row.schedulingNum === null || !row.schedulingUserId || row.workHours === '' || row.workHours === null || !row.unit) { uni.showToast({ title: `第${i + 1}è¡æ°æ®æªå¡«å宿´`, icon: 'none' }) return } } // 2. å计æäº§æ°é const total = tableData.value.reduce((sum, row) => sum + Number(row.schedulingNum || 0), 0) if (total > Number(pendingNum.value)) { uni.showToast({ title: 'æäº§æ°éå计ä¸è½è¶ è¿å¾ æäº§æ°é', icon: 'none' }) return } // 3. æ¼è£ æ°æ® const submitData = tableData.value.map(row => { const { loss, ...rest } = row return { ...rest, receive: receive.value } }) processScheduling(submitData).then(() => { uni.showToast({ title: 'æäº¤æå', icon: 'success' }) closeDia() }) } // å ³éå¼¹æ¡ const closeDia = () => { dialogFormVisible.value = false receive.value = '' tableData.value = [] unitFromRow.value = '' idFromRow.value = '' specificationModelFromRow.value = '' pendingNum.value = 0 emit('close') } defineExpose({ openDialog }) const addRow = () => { tableData.value.push(createRow()) } const removeRow = (index) => { tableData.value.splice(index, 1) } </script> <style scoped lang="scss"> .dia-container { padding: 12px; height: 100%; display: flex; flex-direction: column; } .dia-header { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; } .dia-header .title { font-weight: 600; font-size: 16px; color: #333; } .dia-header .pending { margin-left: auto; color: #666; font-size: 12px; } .rows { display: flex; flex-direction: column; gap: 10px; overflow-y: auto; -webkit-overflow-scrolling: touch; flex: 1; min-height: 0; } .row-card { background: #fff; border-radius: 10px; padding: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); } .row-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; } .row-index { color: #999; font-size: 12px; } .summary { padding: 10px 0; color: #333; font-size: 14px; text-align: right; } .dia-footer { display: flex; gap: 10px; justify-content: flex-end; padding-top: 8px; } </style> src/pages/productionManagement/operationScheduling/index.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,282 @@ <template> <view class="op-scheduling"> <PageHeader title="å·¥åºæäº§" /> <view class="search_form"> <u-form> <view class="form-row"> <u-form-item label="客æ·åç§°" label-width="80"> <up-input v-model="searchForm.customerName" placeholder="请è¾å ¥" clearable @change="handleQuery" /> </u-form-item> <u-form-item label="项ç®åç§°" label-width="80"> <up-input v-model="searchForm.projectName" placeholder="请è¾å ¥" clearable @change="handleQuery" /> </u-form-item> </view> <view class="form-row"> <u-form-item label="ç¶æ" label-width="80"> <up-input v-model="statusDisplay" placeholder="è¯·éæ©ç¶æ" readonly @click="showStatusPicker = true" /> </u-form-item> </view> <view class="form-actions"> <u-button type="primary" @click="handleQuery" size="small">æç´¢</u-button> </view> </u-form> </view> <!-- 顶鍿ä½å·²ç§»é¤ --> <view class="list_container"> <u-loading-icon v-if="tableLoading" text="å è½½ä¸..."></u-loading-icon> <view v-else> <view v-if="!tableData || tableData.length === 0" class="empty">ææ æ°æ®</view> <view v-else class="card_list"> <view v-for="item in tableData" :key="item.id" class="card_item"> <view class="card_header"> <u-tag :type="statusType(item.status)" size="mini">{{ statusText(item.status) }}</u-tag> <text class="card_title">{{ item.projectName }}</text> </view> <view class="card_body"> <view class="row"><text class="label">æ´¾å·¥æ¥æ</text><text class="value">{{ item.schedulingDate }}</text></view> <view class="row"><text class="label">派工人</text><text class="value">{{ item.schedulingUserName }}</text></view> <view class="row"><text class="label">ååå·</text><text class="value">{{ item.salesContractNo }}</text></view> <view class="row"><text class="label">客æ·ååå·</text><text class="value">{{ item.customerContractNo }}</text></view> <view class="row"><text class="label">客æ·åç§°</text><text class="value">{{ item.customerName }}</text></view> <view class="row"><text class="label">产å大类</text><text class="value">{{ item.productCategory }}</text></view> <view class="row"><text class="label">è§æ ¼åå·</text><text class="value">{{ item.specificationModel }}</text></view> <view class="row"><text class="label">ç»å®æºå¨</text><text class="value">{{ item.speculativeTradingName }}</text></view> <view class="row inline"> <view class="col"><text class="label">åä½</text><text class="value">{{ item.unit }}</text></view> <view class="col"><text class="label">æäº§æ»æ°</text><text class="value">{{ item.schedulingNum }}</text></view> <view class="col"><text class="label">å·²æäº§æ°é</text><text class="value">{{ item.successNum }}</text></view> <view class="col"><text class="label">å¾ æäº§æ°é</text><text class="value">{{ item.pendingNum }}</text></view> </view> </view> <view class="card_actions"> <u-button type="primary" size="small" @click="openForm('add', item)" :disabled="item.pendingNum == 0" >å·¥åºæäº§</u-button> <u-button type="error" plain size="small" class="ml8" @click="handleCancel(item)" :disabled="item.status == 3" >åæ¶æäº§</u-button> </view> </view> </view> </view> </view> <form-dia ref="formDia" @close="handleQuery"></form-dia> <!-- ç¶æéæ©å¨ï¼up ç³»åï¼ --> <up-action-sheet :show="showStatusPicker" :actions="statusActions" title="éæ©ç¶æ" @select="onStatusSelect" @close="showStatusPicker = false" /> </view> </template> <script setup> import { onMounted, ref, reactive, toRefs, nextTick, getCurrentInstance, computed } from 'vue' import PageHeader from '@/components/PageHeader.vue' import FormDia from './components/formDia.vue' import dayjs from 'dayjs' import { listPageProcess, productionDispatchDelete } from '@/api/productionManagement/operationScheduling.js' const data = reactive({ searchForm: { staffName: "", status: 1, entryDate: null, // å½å ¥æ¥æ entryDateStart: undefined, entryDateEnd: undefined, }, }); const { searchForm } = toRefs(data); const tableData = ref([]) const tableLoading = ref(false) const page = reactive({ current: -1, size: -1, }); const formDia = ref() const { proxy } = getCurrentInstance() // ç¶æéæ©å¨ const showStatusPicker = ref(false) const statusOptions = ref([ { label: 'å¾ æäº§', value: 1 }, { label: 'æäº§ä¸', value: 2 }, { label: 'å·²æäº§', value: 3 } ]) const statusActions = computed(() => statusOptions.value.map(o => ({ name: o.label, value: o.value }))) const statusDisplay = ref('') // æ¥æèå´çéå·²ç§»é¤ // picker handlers const onStatusSelect = (item) => { if (item) { searchForm.value.status = item.value statusDisplay.value = item.name handleQuery() } showStatusPicker.value = false } // æ¥æèå´åè°å·²ç§»é¤ // status display helpers const statusText = (s) => { if (s == 3) return 'å·²æäº§' if (s == 1) return 'å¾ æäº§' return 'æäº§ä¸' } const statusType = (s) => { if (s == 3) return 'success' if (s == 1) return 'primary' return 'warning' } // æ¥è¯¢å表 /** æç´¢æé®æä½ */ const handleQuery = () => { page.current = -1; page.size = -1; getList(); }; // changeDaterange å·²ç§»é¤ const getList = () => { tableLoading.value = true; const params = { ...searchForm.value, ...page }; params.entryDate = undefined listPageProcess(params).then(res => { tableLoading.value = false; tableData.value = res.data.records.map(item => ({ ...item, pendingNum: (Number(item.schedulingNum) || 0) - (Number(item.successNum) || 0) })); }).catch(err => { tableLoading.value = false; }) }; // åæ¶å¤éç¸å ³é»è¾ï¼éç¨å¡çå åæ¡æä½ // æå¼å¼¹æ¡ const openForm = (type, row) => { if (!row) { uni.showToast({ title: 'æªæ¾å°æ°æ®', icon: 'none' }) return; } if ((Number(row.pendingNum) || 0) === 0) { uni.showToast({ title: 'æ éåæäº§', icon: 'none' }) return; } nextTick(() => { formDia.value?.openDialog(type, row) }) }; // 忡忶æäº§ const handleCancel = (row) => { if (!row) return if (row.status == 3) { uni.showToast({ title: 'å·²æäº§æ°æ®ä¸è½åæ¶æäº§', icon: 'none' }) return } uni.showModal({ title: 'å é¤æç¤º', content: 'æ¯å¦ç¡®è®¤åæ¶æäº§ï¼', success: (res) => { if (res.confirm) { tableLoading.value = true productionDispatchDelete([row.id]) .then(() => { uni.showToast({ title: 'åæ¶æäº§æå', icon: 'success' }) getList() }) .finally(() => { tableLoading.value = false }) } } }) } onMounted(() => { getList(); // åå§åæ¾ç¤ºå段 const cur = statusOptions.value.find(o => o.value === searchForm.value.status) statusDisplay.value = cur ? cur.label : '' }); </script> <style scoped lang="scss"> .op-scheduling { padding-bottom: 12px; } .search_form { margin: 12px; background: #fff; border-radius: 8px; padding: 10px; } .form-row { display: flex; gap: 12px; } .form-actions { display: flex; justify-content: flex-end; } .table_actions { display: flex; gap: 8px; padding: 0 12px 10px 12px; justify-content: flex-end; } .list_container { padding: 0 12px; } .empty { text-align: center; color: #888; padding: 20px 0; } .card_list { display: flex; flex-direction: column; gap: 10px; } .card_item { background: #fff; border-radius: 10px; padding: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); } .card_header { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; } .card_title { font-weight: 500; color: #333; margin-left: auto; } .card_body .row { display: flex; justify-content: space-between; padding: 4px 0; } .card_body .row.inline { display: flex; gap: 10px; flex-wrap: wrap; } .card_body .row.inline .col { min-width: 45%; display: flex; justify-content: space-between; } .label { color: #666; font-size: 12px; } .value { color: #333; font-size: 12px; } .card_actions { display: flex; justify-content: flex-end; gap: 8px; padding-top: 8px; } .ml8 { margin-left: 8px; } </style> src/pages/productionManagement/productionCosting/index.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,107 @@ <template> <view class="prod-costing"> <PageHeader title="çäº§æ ¸ç®" /> <view class="search_form"> <u-form> <view class="form-row"> <u-form-item label="ç产人" label-width="80"> <up-input v-model="searchForm.schedulingUserName" placeholder="请è¾å ¥" clearable @change="handleQuery" /> </u-form-item> </view> <view class="form-actions"> <u-button type="primary" size="small" @click="handleQuery">æç´¢</u-button> </view> </u-form> </view> <view class="list_container"> <u-loading-icon v-if="tableLoading" text="å è½½ä¸..." /> <view v-else> <view v-if="!tableData || tableData.length === 0" class="empty">ææ æ°æ®</view> <view v-else class="card_list"> <view v-for="item in tableData" :key="item.id" class="card_item"> <view class="card_header"> <text class="card_title">{{ item.projectName }}</text> </view> <view class="card_body"> <view class="row"><text class="label">çäº§æ¥æ</text><text class="value">{{ item.schedulingDate }}</text></view> <view class="row"><text class="label">ç产人</text><text class="value">{{ item.schedulingUserName }}</text></view> <view class="row"><text class="label">ååå·</text><text class="value">{{ item.salesContractNo }}</text></view> <view class="row"><text class="label">客æ·ååå·</text><text class="value">{{ item.customerContractNo }}</text></view> <view class="row"><text class="label">客æ·åç§°</text><text class="value">{{ item.customerName }}</text></view> <view class="row"><text class="label">产å大类</text><text class="value">{{ item.productCategory }}</text></view> <view class="row"><text class="label">è§æ ¼åå·</text><text class="value">{{ item.specificationModel }}</text></view> <view class="row inline"> <view class="col"><text class="label">åä½</text><text class="value">{{ item.unit }}</text></view> <view class="col"><text class="label">å·¥åº</text><text class="value">{{ item.process }}</text></view> <view class="col"><text class="label">ç产æ°é</text><text class="value">{{ item.finishedNum }}</text></view> <view class="col"><text class="label">å·¥æ¶å®é¢</text><text class="value">{{ item.workHours }}</text></view> <view class="col"><text class="label">å·¥èµ</text><text class="value">{{ item.wages }}</text></view> </view> </view> </view> </view> </view> </view> </view> </template> <script setup> import { onMounted, ref, reactive, toRefs } from "vue"; import PageHeader from '@/components/PageHeader.vue' import { productionAccountingListPage } from "@/api/productionManagement/productionCosting.js"; // state const tableData = ref([]); const tableLoading = ref(false); const page = reactive({ current: -1, size: -1 }); const data = reactive({ searchForm: { schedulingUserName: "", }, }); const { searchForm } = toRefs(data); // æ æ¥æçé // æ¥è¯¢ï¼ä¸å页ï¼åºå®ä¼ -1ï¼ const handleQuery = () => { page.current = -1; page.size = -1; getList(); }; const getList = () => { tableLoading.value = true; const params = { ...searchForm.value, ...page }; productionAccountingListPage(params).then((res) => { tableLoading.value = false; tableData.value = res.data.records || []; }).catch(() => { tableLoading.value = false; }) }; onMounted(() => { getList(); }); </script> <style scoped lang="scss"> .prod-costing { padding-bottom: 12px; } .search_form { margin: 12px; background: #fff; border-radius: 8px; padding: 10px; } .form-row { display: flex; gap: 12px; } .form-actions { display: flex; justify-content: flex-end; } .ml8 { margin-left: 8px; } .list_container { padding: 0 12px; } .empty { text-align: center; color: #888; padding: 20px 0; } .card_list { display: flex; flex-direction: column; gap: 10px; } .card_item { background: #fff; border-radius: 10px; padding: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); } .card_header { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; } .card_title { font-weight: 500; color: #333; } .card_body .row { display: flex; justify-content: space-between; padding: 4px 0; } .card_body .row.inline { display: flex; gap: 10px; flex-wrap: wrap; } .card_body .row.inline .col { min-width: 45%; display: flex; justify-content: space-between; } .label { color: #666; font-size: 12px; } .value { color: #333; font-size: 12px; } .pagination { display: flex; align-items: center; justify-content: center; gap: 10px; padding: 12px 0; } .page_text { color: #666; font-size: 12px; } </style> src/pages/productionManagement/productionDispatching/components/autoDispatchDia.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,381 @@ <template> <view> <up-popup v-model:show="dialogFormVisible" mode="center" border-radius="20" :closeable="true" @close="closeDia" > <view class="popup-content"> <view class="popup-header"> <text class="popup-title">èªå¨æ´¾å·¥</text> </view> <view class="popup-body"> <view class="section-title">派工å表</view> <view class="card-list"> <view v-for="(item, index) in dispatchList" :key="index" class="dispatch-card" :class="{ 'even-card': index % 2 === 1 }" > <view class="card-header"> <text class="card-index">{{ index + 1 }}</text> <text class="card-project">{{ item.projectName }}</text> </view> <view class="card-content"> <view class="info-row"> <text class="info-label">ååå·ï¼</text> <text class="info-value">{{ item.salesContractNo }}</text> </view> <view class="info-row"> <text class="info-label">客æ·ï¼</text> <text class="info-value">{{ item.customerName }}</text> </view> <view class="info-row"> <text class="info-label">产åç±»å«ï¼</text> <text class="info-value">{{ item.productCategory }}</text> </view> <view class="info-row"> <text class="info-label">è§æ ¼åå·ï¼</text> <text class="info-value">{{ item.specificationModel }}</text> </view> <view class="info-row"> <text class="info-label">ç»å®æºå¨ï¼</text> <text class="info-value">{{ item.speculativeTradingName }}</text> </view> <view class="quantity-row"> <view class="quantity-item"> <text class="quantity-label">æ»æ°éï¼</text> <text class="quantity-value">{{ item.quantity }}</text> </view> <view class="quantity-item"> <text class="quantity-label">å·²æäº§ï¼</text> <text class="quantity-value">{{ item.schedulingNum }}</text> </view> <view class="quantity-item"> <text class="quantity-label">å¾ æäº§ï¼</text> <text class="quantity-value">{{ item.pendingQuantity }}</text> </view> </view> <view class="scheduling-row"> <text class="scheduling-label">æ¬æ¬¡æäº§ï¼</text> <up-number-box v-model="item.schedulingNum" :min="0" :max="item.pendingQuantity" :step="1" :precision="0" size="mini" @change="(value) => changeCurrentNum(value, item)" class="scheduling-input" /> </view> </view> </view> </view> <view v-if="dispatchList.length === 0" class="empty-state"> <text class="empty-text">ææ æ´¾å·¥æ°æ®</text> </view> </view> <view class="popup-footer"> <up-button type="primary" @click="submitForm" class="confirm-btn">确认派工</up-button> <up-button @click="closeDia" class="cancel-btn">åæ¶</up-button> </view> </view> </up-popup> </view> </template> <script setup> import { ref, reactive, toRefs } from "vue"; import { productionDispatchList } from "@/api/productionManagement/productionOrder.js"; const emit = defineEmits(['close']) const dialogFormVisible = ref(false); const operationType = ref('') const data = reactive({ form: {}, dispatchList: [], // 派工åè¡¨æ°æ® }); const { form, dispatchList } = toRefs(data); // è¡¨æ ¼è¡æ ·å¼ const tableRowClassName = ({ rowIndex }) => { if (rowIndex % 2 === 1) { return 'even-row' } return '' } // ä¿®æ¹æ¬æ¬¡æäº§æ°é const changeCurrentNum = (value, row) => { if (value > row.pendingQuantity) { row.schedulingNum = row.pendingQuantity uni.$u.toast('æäº§æ°éä¸å¯å¤§äºå¾ æäº§æ°é') } } // æå¼å¼¹æ¡ const openDialog = (rows) => { dialogFormVisible.value = true; console.log('æ¥æ¶å°ä¼ å ¥çæ°æ®:', rows); console.log('ä¼ å ¥æ°æ®æ°é:', rows.length); // å¤çä¼ å ¥çæ°æ® dispatchList.value = rows.map((row, index) => ({ ...row, schedulingNum: 0, // åå§åæ¬æ¬¡æäº§æ°é为0 pendingQuantity: (Number(row.quantity) || 0) - (Number(row.schedulingNum) || 0) // 计ç®å¾ æäº§æ°é })) console.log('å¤çåçæ´¾å·¥å表:', dispatchList.value); console.log('派工å表æ°é:', dispatchList.value.length); } // æäº¤è¡¨å const submitForm = () => { // æ£æ¥æ¯å¦ææäº§æ°æ® const hasSchedulingData = dispatchList.value.some(item => item.schedulingNum > 0) if (!hasSchedulingData) { uni.$u.toast('请è³å°ä¸ºä¸æ¡è®°å½è®¾ç½®æäº§æ°é') return } // æé æäº¤æ°æ® - ç´æ¥ä¼ éæ°ç»ï¼ä¸è¿æ»¤ const submitData = dispatchList.value console.log('æäº¤èªå¨æ´¾å·¥æ°æ®:', submitData) // è°ç¨APIï¼è¿ééè¦æ ¹æ®å®é æ¥å£è°æ´ï¼ productionDispatchList(submitData).then(res => { uni.$u.toast(res.msg || '派工æå'); closeDia(); }).catch(err => { uni.$u.toast('派工失败'); console.error('派工失败:', err); }) } // å ³éå¼¹æ¡ const closeDia = () => { dialogFormVisible.value = false; dispatchList.value = [] emit('close') }; defineExpose({ openDialog, }); </script> <style lang="scss" scoped> .popup-content { width: 90vw; max-width: 1200px; background: #fff; border-radius: 20rpx; overflow: hidden; } .popup-header { padding: 30rpx; border-bottom: 1rpx solid #f0f0f0; text-align: center; } .popup-title { font-size: 36rpx; font-weight: bold; color: #333; } .popup-body { padding: 30rpx; height: 60vh; overflow-y: auto; overflow-x: hidden; -webkit-overflow-scrolling: touch; max-height: 60vh; } .section-title { font-size: 32rpx; font-weight: bold; color: #333; margin-bottom: 20rpx; flex-shrink: 0; } .card-list { display: flex; flex-direction: column; gap: 20rpx; min-height: 0; overflow-y: auto; max-height: 60vh; } .dispatch-card { background: #f8f9fa; border-radius: 12rpx; padding: 24rpx; border: 1rpx solid #e9ecef; transition: all 0.3s ease; flex-shrink: 0; } .dispatch-card:hover { box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1); transform: translateY(-2rpx); } .even-card { background: #ffffff; } .card-header { display: flex; align-items: center; margin-bottom: 20rpx; padding-bottom: 16rpx; border-bottom: 1rpx solid #e9ecef; } .card-index { background: #1890ff; color: white; width: 40rpx; height: 40rpx; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 24rpx; font-weight: bold; margin-right: 16rpx; } .card-project { font-size: 28rpx; font-weight: bold; color: #333; flex: 1; } .card-content { display: flex; flex-direction: column; gap: 12rpx; } .info-row { display: flex; align-items: center; } .info-label { font-size: 26rpx; color: #666; width: 140rpx; flex-shrink: 0; } .info-value { font-size: 26rpx; color: #333; flex: 1; } .quantity-row { display: flex; gap: 20rpx; margin: 8rpx 0; } .quantity-item { display: flex; align-items: center; background: #f8f9fa; padding: 8rpx 16rpx; border-radius: 6rpx; border: 1rpx solid #e9ecef; } .quantity-label { font-size: 24rpx; color: #666; margin-right: 8rpx; } .quantity-value { font-size: 24rpx; color: #1890ff; font-weight: bold; } .scheduling-row { display: flex; align-items: center; margin-top: 12rpx; padding-top: 12rpx; border-top: 1rpx dashed #e9ecef; } .scheduling-label { font-size: 26rpx; color: #333; font-weight: bold; margin-right: 16rpx; width: 140rpx; flex-shrink: 0; } .scheduling-input { flex: 1; } .empty-state { text-align: center; padding: 60rpx 30rpx; color: #999; } .empty-text { font-size: 28rpx; } .popup-footer { padding: 30rpx; border-top: 1rpx solid #f0f0f0; display: flex; justify-content: center; gap: 20rpx; } .confirm-btn { width: 200rpx; } .cancel-btn { width: 200rpx; } </style> src/pages/productionManagement/productionDispatching/index.vue
@@ -2,7 +2,7 @@ <view class="production-dispatching"> <!-- 使ç¨éç¨é¡µé¢å¤´é¨ç»ä»¶ --> <PageHeader title="ç产派工" @back="goBack" /> <!-- çæºç¶æå±ç¤º --> <view class="machines-section"> <view class="section-title">çæºç¶æ</view> @@ -33,6 +33,28 @@ </view> </view> </view> <!-- æèç设置 --> <view class="loss-rate-section"> <view class="section-title">æèç设置</view> <view class="loss-rate-content"> <view class="loss-rate-item"> <up-button class="loss-rate-btn" type="primary" plain size="small" @click="showLossRateSheet = true" >{{ lossRate ? `æèç: ${lossRate}%` : 'è¯·éæ©æèç' }}</up-button> <up-action-sheet :show="showLossRateSheet" :actions="lossRateOptions" @select="onLossRateSelect" title="éæ©æèç" @close="showLossRateSheet = false" /> </view> </view> </view> <view class="save-section"> <up-button type="primary" @click="saveMachineTotals" size="normal" class="save-btn">ä¿åçæºè®¾ç½®</up-button> </view> @@ -71,46 +93,41 @@ </view> <!-- æ¹éæä½åºå --> <view v-if="showBatchActions" class="batch-actions-section"> <view class="batch-actions-section" v-if="showBatchActions"> <view class="batch-info"> <text class="batch-count">已鿩 {{ selectedItems.length }} 个项ç®</text> <text class="batch-text">已鿩 {{ selectedItems.length }} 个项ç®</text> </view> <view class="batch-buttons"> <up-button type="primary" size="small" @click="handleAutoDispatch" class="batch-btn"> <up-icon name="play-circle" size="16" color="#ffffff"></up-icon> èªå¨æ´¾å </up-button> <up-button type="default" size="small" @click="clearSelection" class="batch-btn"> <up-icon name="close-circle" size="16" color="#6c757d"></up-icon> åæ¶éæ© </up-button> <up-button type="primary" size="small" @click="handleAutoDispatch" class="batch-btn">èªå¨æ´¾å</up-button> <up-button type="default" size="small" @click="clearSelection" class="batch-btn">åæ¶éæ©</up-button> </view> </view> <!-- å ¨éæä½åºå --> <view v-if="tableData.length > 0" class="select-all-section"> <view class="select-all-checkbox" @click="toggleAllSelection"> <up-icon :name="isAllSelected ? 'checkbox-mark' : 'circle'" :color="isAllSelected ? '#409eff' : '#c0c4cc'" size="18" ></up-icon> <text class="select-all-text">{{ isAllSelected ? 'åæ¶å ¨é' : 'å ¨é' }}</text> <view class="select-all-section" v-if="tableData.length > 0"> <view class="select-all-content"> <up-checkbox v-model="isAllSelected" @change="toggleAllSelection" label="å ¨é" class="select-all-checkbox" :disabled="tableData.length === 0 || tableData.filter(item => item.pendingQuantity > 0 && item.speculativeTradingName).length === 0" /> </view> <text class="select-all-hint">ï¼ä» éæ©å¾ ææ°é大äº0ç项ç®ï¼</text> </view> <!-- ç产派工å表 --> <view class="ledger-list" v-if="tableData.length > 0"> <view v-for="(item, index) in tableData" :key="item.id || index" class="list-item"> <view class="ledger-item"> <!-- éæ©å¤éæ¡ --> <view class="item-checkbox" @click="toggleItemSelection(item)"> <up-icon :name="selectedItems.includes(item.id) ? 'checkbox-mark' : 'circle'" :color="selectedItems.includes(item.id) ? '#409eff' : '#c0c4cc'" size="18" ></up-icon> <view class="item-checkbox"> <up-checkbox :model-value="selectedItems.some(selected => selected.id === item.id)" @change="(checked) => toggleItemSelection(item, checked)" :disabled="item.pendingQuantity <= 0 || !item.speculativeTradingName" shape="circle" /> </view> <view class="item-content"> @@ -146,6 +163,10 @@ <text class="detail-value">{{ item.specificationModel }}</text> </view> <view class="detail-row"> <text class="detail-label">ç»å®æºå¨</text> <text class="detail-value">{{ item.speculativeTradingName }}</text> </view> <view class="detail-row"> <text class="detail-label">åä½</text> <text class="detail-value">{{ item.unit }}</text> </view> @@ -165,14 +186,14 @@ <!-- æä½æé®åºå --> <view class="action-buttons"> <up-button type="primary" size="small" @click="handleDispatch(item)" class="action-btn" :disabled="item.pendingQuantity <= 0" > {{ item.pendingQuantity <= 0 ? 'æ éæ´¾å·¥' : 'ç产派工' }} </up-button> type="primary" size="small" @click="handleDispatch(item)" class="action-btn" :disabled="item.pendingQuantity <= 0 || !item.speculativeTradingName" > {{ item.pendingQuantity <= 0 ? 'æ éæ´¾å·¥' : !item.speculativeTradingName ? 'æªç»å®æºå¨' : 'ç产派工' }} </up-button> </view> </view> </view> @@ -188,16 +209,20 @@ <!-- æ´¾å·¥å¼¹çª --> <DispatchModal ref="dispatchModalRef" @confirm="handleDispatchConfirm" /> <!-- èªå¨æ´¾åå¼¹çª --> <AutoDispatchDia ref="autoDispatchDia" /> </view> </template> <script setup> import { ref, reactive, toRefs, getCurrentInstance } from "vue"; import { ref, reactive, toRefs, getCurrentInstance, nextTick } from "vue"; import { onShow } from '@dcloudio/uni-app'; import dayjs from "dayjs"; import {schedulingListPage, schedulingList, addSpeculatTrading, updateSpeculatTrading} from "@/api/productionManagement/productionOrder.js"; import {schedulingListPage, schedulingList, addSpeculatTrading, updateSpeculatTrading, getLossRate, addLossRate, updateLossRate} from "@/api/productionManagement/productionOrder.js"; import PageHeader from "@/components/PageHeader.vue"; import DispatchModal from "./components/DispatchModal.vue"; import AutoDispatchDia from "./components/autoDispatchDia.vue"; const { proxy } = getCurrentInstance(); @@ -207,10 +232,10 @@ // åè¡¨æ°æ® const tableData = ref([]); // æ¹ééæ©ç¸å ³æ°æ® const selectedItems = ref([]); // éä¸ç项ç®IDæ°ç» const isAllSelected = ref(false); // æ¯å¦å ¨é const showBatchActions = ref(false); // æ¯å¦æ¾ç¤ºæ¹éæä½åºå // éæ©ç¸å ³æ°æ® const selectedItems = ref([]); const isAllSelected = ref(false); const showBatchActions = ref(false); // æç´¢è¡¨åæ°æ® const data = reactive({ @@ -261,8 +286,23 @@ // æ¯å¦ææ¥è¯¢æ°æ®ï¼ç¨äºå¤ææ¯æ°å¢è¿æ¯ä¿®æ¹ï¼ const hasQueryData = ref(false); // æèçç¸å ³æ°æ® const lossRate = ref(""); // å½åéæ©çæèç const showLossRateSheet = ref(false); // æ§å¶æèç鿩颿¿æ¾ç¤º const lossRateOptions = ref([ { name: "6%", value: "6" }, { name: "7%", value: "7" }, { name: "8%", value: "8" }, { name: "9%", value: "9" }, { name: "10%", value: "10" } ]); const lossRateData = ref(null); // æèçæ¥è¯¢è¿åçæ°æ® // 派工弹çªå¼ç¨ const dispatchModalRef = ref(); // èªå¨æ´¾åå¼¹çªå¼ç¨ const autoDispatchDia = ref(); // éç¨æç¤ºå½æ° const showLoadingToast = (message) => { @@ -328,6 +368,28 @@ }); }; // æèçéæ©äºä»¶ const onLossRateSelect = (action) => { lossRate.value = action.value; showLossRateSheet.value = false; console.log('éæ©äºæèç:', action.name, 'å¼:', action.value); }; // è·åæèçæ°æ® const getLossRateData = () => { getLossRate().then((res) => { if (res.data) { lossRateData.value = res.data; // 设置å½åéæ©çæèç if (res.data.rate !== null && res.data.rate !== undefined) { lossRate.value = res.data.rate.toString(); } } }).catch(err => { console.error('è·åæèç失败:', err); }); }; // è·ååè¡¨æ°æ® const getList = () => { loading.value = true; @@ -340,14 +402,17 @@ closeToast(); tableData.value = (res.data.records || []).map(item => ({ ...item, pendingQuantity: (Number(item.quantity) || 0) - (Number(item.schedulingNum) || 0) })); ...item, pendingQuantity: (Number(item.quantity) || 0) - (Number(item.schedulingNum) || 0) })).filter(item => item.pendingQuantity > 0); page.total = res.data.total || 0; // è·åçæºæ°æ® getMachineProductionData(); // è·åæèçæ°æ® getLossRateData(); }).catch(() => { loading.value = false; @@ -364,6 +429,14 @@ if (item.pendingQuantity <= 0) { uni.showToast({ title: 'è¯¥é¡¹ç®æ éåæ´¾å·¥', icon: 'none' }); return; } if (!item.speculativeTradingName) { uni.showToast({ title: 'è¯¥é¡¹ç®æªç»å®æºå¨ï¼æ æ³æ´¾å·¥', icon: 'none' }); return; @@ -392,6 +465,51 @@ workLoad: machineTotal[`m${machineId}`] || 0, currentWorkLoad: machineInProduction[`m${machineId}`] || 0 }; }; // ä¿åæèç设置 const saveLossRate = () => { if (!lossRate.value) { console.log('æªéæ©æèçï¼è·³è¿ä¿å'); return Promise.resolve(); } const lossRateDataToSave = { rate: parseFloat(lossRate.value) || 0 }; // å¦æææ¥è¯¢å°çæèçæ°æ®ï¼è¯´ææ¯ä¿®æ¹æä½ï¼éè¦ä¼ éid if (lossRateData.value && lossRateData.value.id) { lossRateDataToSave.id = lossRateData.value.id; } console.log('ä¿åæèçæ°æ®:', lossRateDataToSave); // æ ¹æ®æ¯å¦ææèçæ°æ®å³å®è°ç¨æ°å¢æ¥å£è¿æ¯ä¿®æ¹æ¥å£ const saveLossApi = lossRateData.value && lossRateData.value.id ? updateLossRate : addLossRate; const successMessage = lossRateData.value && lossRateData.value.id ? 'æèçä¿®æ¹æå' : 'æèçæ°å¢æå'; return saveLossApi(lossRateDataToSave).then(res => { console.log('æèçä¿åæå:', res); uni.showToast({ title: successMessage, icon: 'success' }); // æ´æ°æèçæ°æ® if (res.data) { lossRateData.value = res.data; } return res; }).catch(err => { console.error('æèçä¿å失败:', err); uni.showToast({ title: 'æèçä¿å失败', icon: 'none' }); throw err; }); }; // ä¿åçæºæ»é设置 @@ -425,9 +543,15 @@ console.log(`è°ç¨æ¥å£: ${hasQueryData.value ? 'ä¿®æ¹' : 'æ°å¢'}`); // è°ç¨å端APIä¿å saveApi(saveData).then(res => { proxy.$message.success(successMessage); // å ä¿åæèçï¼åä¿åçæºè®¾ç½® saveLossRate().then(() => { // è°ç¨å端APIä¿åçæºè®¾ç½® return saveApi(saveData); }).then(res => { uni.showToast({ title: successMessage, icon: 'success' }); console.log('ä¿åæå:', res); // ä¿åæååï¼è®¾ç½®hasQueryData为trueï¼ä¸æ¬¡ä¿åå°è°ç¨ä¿®æ¹æ¥å£ @@ -435,61 +559,69 @@ hasQueryData.value = true; } }).catch(err => { proxy.$message.error('ä¿å失败'); uni.showToast({ title: 'ä¿å失败', icon: 'none' }); console.error('ä¿å失败:', err); }); }; // æ¹ééæ©ç¸å ³å½æ° // 忢å个项ç®çéæ©ç¶æ const toggleItemSelection = (item) => { const itemId = item.id; const index = selectedItems.value.indexOf(itemId); // 忢å个项ç®éæ©ç¶æ const toggleItemSelection = (item, checked) => { // ä» å è®¸éæ©å·²ç»å®æºå¨ä¸å¾ æ´¾æ°é>0çé¡¹ç® if (!item.speculativeTradingName || item.pendingQuantity <= 0) return; if (index > -1) { // 妿已éä¸ï¼ååæ¶éæ© selectedItems.value.splice(index, 1); console.log('åæ¢éæ©ç¶æ:', item.id, checked); // ä½¿ç¨æ´ä¸¥æ ¼çæ¯è¾é»è¾ï¼ç¡®ä¿IDå¯ä¸æ§ const index = selectedItems.value.findIndex(selected => { // 深度æ¯è¾å¯¹è±¡ï¼ç¡®ä¿æ¯åä¸ä¸ªé¡¹ç® return JSON.stringify(selected) === JSON.stringify(item); }); if (checked) { // 妿éä¸ä¸ä¸å¨éä¸å表ä¸ï¼åæ·»å if (index === -1) { selectedItems.value.push({...item}); // å建æ°å¯¹è±¡ï¼é¿å å¼ç¨é®é¢ console.log('æ·»å 项ç®åé䏿°é:', selectedItems.value.length); } } else { // 妿æªéä¸ï¼åæ·»å éæ© selectedItems.value.push(itemId); // 妿忶éä¸ä¸å¨éä¸å表ä¸ï¼åç§»é¤ if (index > -1) { selectedItems.value.splice(index, 1); console.log('ç§»é¤é¡¹ç®åé䏿°é:', selectedItems.value.length); } } // æ´æ°å ¨éç¶æ console.log('å½åéä¸é¡¹ç®å表:', selectedItems.value.map(s => s.id)); updateAllSelectedStatus(); // æ´æ°æ¹éæä½åºåæ¾ç¤ºç¶æ updateBatchActionsVisibility(); }; // åæ¢å ¨éç¶æ const toggleAllSelection = () => { if (isAllSelected.value) { // åæ¶å ¨é selectedItems.value = []; } else { // å ¨é selectedItems.value = tableData.value .filter(item => item.pendingQuantity > 0) // åªéæ©å¾ ææ°é大äº0çé¡¹ç® .map(item => item.id); selectedItems.value = tableData.value.filter(item => item.pendingQuantity > 0 && item.speculativeTradingName).map(item => ({ ...item })); } isAllSelected.value = !isAllSelected.value; updateBatchActionsVisibility(); }; // æ´æ°å ¨éç¶æ const updateAllSelectedStatus = () => { const selectableItems = tableData.value.filter(item => item.pendingQuantity > 0); if (selectableItems.length === 0) { const selectableItems = tableData.value.filter(item => item.pendingQuantity > 0 && item.speculativeTradingName); if (selectableItems.length > 0 && selectedItems.value.length === selectableItems.length && selectableItems.every(item => selectedItems.value.some(selected => selected.id === item.id))) { isAllSelected.value = true; } else { isAllSelected.value = false; return; } isAllSelected.value = selectedItems.value.length === selectableItems.length && selectableItems.every(item => selectedItems.value.includes(item.id)); }; // æ´æ°æ¹éæä½åºåæ¾ç¤ºç¶æ // æ´æ°æ¹éæä½æ¾ç¤ºç¶æ const updateBatchActionsVisibility = () => { showBatchActions.value = selectedItems.value.length > 0; }; @@ -503,80 +635,36 @@ // è·åéä¸çé¡¹ç® const getSelectedItems = () => { return tableData.value.filter(item => selectedItems.value.includes(item.id)); return selectedItems.value; }; // èªå¨æ´¾ååè½ // å¤çèªå¨æ´¾å const handleAutoDispatch = () => { const selectedItemsList = getSelectedItems(); if (selectedItemsList.length === 0) { if (selectedItems.value.length === 0) { uni.showToast({ title: '请å éæ©è¦æ´¾å·¥ç项ç®', title: 'è¯·éæ©è¦æ´¾å·¥ç项ç®', icon: 'none' }); return; } // æ£æ¥æ¯å¦æé¡¹ç®å¾ ææ°éä¸è¶³ const invalidItems = selectedItemsList.filter(item => item.pendingQuantity <= 0); if (invalidItems.length > 0) { // æ£æ¥æ¯å¦ææéä¸é¡¹ç®é½æç»å®æºå¨ const unboundItems = selectedItems.value.filter(item => !item.speculativeTradingName); if (unboundItems.length > 0) { uni.showToast({ title: `æ${invalidItems.length}ä¸ªé¡¹ç®æ éæ´¾å·¥ï¼å·²èªå¨è¿æ»¤`, icon: 'none' }); } // è¿æ»¤æå¾ ææ°éä¸è¶³çé¡¹ç® const validItems = selectedItemsList.filter(item => item.pendingQuantity > 0); if (validItems.length === 0) { uni.showToast({ title: '没æå¯æ´¾å·¥ç项ç®', title: 'æé项ç®ä¸ææªç»å®æºå¨ç项ç®ï¼æ æ³èªå¨æ´¾å', icon: 'none' }); return; } uni.showModal({ title: '确认èªå¨æ´¾å', content: `ç¡®å®è¦å¯¹éä¸ç${validItems.length}个项ç®è¿è¡èªå¨æ´¾ååï¼`, success: (res) => { if (res.confirm) { executeAutoDispatch(validItems); } } }); }; // æ§è¡èªå¨æ´¾å const executeAutoDispatch = (items) => { showLoadingToast('èªå¨æ´¾åä¸...'); // 模æèªå¨æ´¾åè¿ç¨ setTimeout(() => { closeToast(); // è¿éåºè¯¥è°ç¨å®é çèªå¨æ´¾åAPI // ææ¶ä½¿ç¨æ¨¡ææå uni.showToast({ title: `æå为${items.length}个项ç®å®æèªå¨æ´¾å`, icon: 'success' }); // æ¸ ç©ºéæ© clearSelection(); // å·æ°å表 getList(); console.log('èªå¨æ´¾å项ç®:', items); }, 1500); // ç¡®ä¿ä¼ éçæ¯å®æ´çéä¸é¡¹ç®æ°ç» autoDispatchDia.value?.openDialog([...selectedItems.value]); }; // 页颿¾ç¤ºæ¶å è½½æ°æ® onShow(() => { getList(); // æ¸ ç©ºéæ©ç¶æ clearSelection(); }); </script> @@ -588,16 +676,63 @@ padding: 20rpx; } // æèç设置åºå .loss-rate-section { background: #ffffff; border: 1rpx solid #e4e7ed; border-radius: 12rpx; padding: 32rpx; margin-top: 24rpx; margin-bottom: 32rpx; box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08); } .loss-rate-section .section-title { font-size: 32rpx; font-weight: 600; color: #303133; margin-bottom: 20rpx; } .loss-rate-section .loss-rate-content { display: flex; flex-direction: column; gap: 24rpx; } .loss-rate-section .loss-rate-content .loss-rate-item { display: flex; align-items: center; gap: 24rpx; } .loss-rate-section .loss-rate-content .loss-rate-label { font-size: 30rpx; font-weight: 500; color: #303133; min-width: 140rpx; white-space: nowrap; } .loss-rate-section .loss-rate-content .loss-rate-btn { min-width: 260rpx; font-size: 28rpx; height: 64rpx; line-height: 64rpx; border-radius: 8rpx; font-weight: 500; } // çæºç¶æåºå .machines-section { margin-bottom: 30rpx; .section-title { font-size: 32rpx; font-weight: 600; color: #303133; margin-bottom: 20rpx; } } .machines-section .section-title { font-size: 32rpx; font-weight: 600; color: #303133; margin-bottom: 20rpx; } .machines-grid { @@ -771,10 +906,10 @@ align-items: center; padding: 12rpx 0; border-bottom: 1rpx solid #f5f5f5; &:last-child { border-bottom: none; } } .detail-row:last-child { border-bottom: none; } .detail-label { @@ -786,16 +921,16 @@ font-size: 26rpx; color: #303133; font-weight: 500; &.highlight { color: #ff6b35; font-weight: 600; } &.danger { color: #ee0a24; font-weight: 600; } } .detail-value.highlight { color: #ff6b35; font-weight: 600; } .detail-value.danger { color: #ee0a24; font-weight: 600; } .action-buttons { @@ -808,6 +943,69 @@ min-width: 180rpx; } // æ¹éæä½åºåæ ·å¼ .batch-actions-section { background: #e8f4ff; border: 1rpx solid #409eff; border-radius: 12rpx; padding: 20rpx 24rpx; margin-bottom: 24rpx; display: flex; justify-content: space-between; align-items: center; } .batch-actions-section .batch-text { font-size: 28rpx; font-weight: 600; color: #409eff; } .batch-actions-section .batch-buttons { display: flex; gap: 16rpx; } .batch-actions-section .batch-btn { min-width: 140rpx; } // å ¨éæä½åºåæ ·å¼ .select-all-section { background: #ffffff; border-radius: 12rpx; padding: 20rpx 24rpx; margin-bottom: 16rpx; border: 1rpx solid #e4e7ed; } .select-all-section .select-all-content { display: flex; align-items: center; } .select-all-section .select-all-checkbox { font-size: 28rpx; font-weight: 500; } // åè¡¨é¡¹éæ©æ¡æ ·å¼ .ledger-item { display: flex; align-items: flex-start; padding: 0; } .item-checkbox { padding: 24rpx 16rpx 0 24rpx; display: flex; align-items: center; } .item-content { flex: 1; } // ç©ºç¶æ .no-data { padding: 100rpx 0; @@ -818,88 +1016,6 @@ font-size: 28rpx; color: #909399; margin-top: 20rpx; } // æ¹éæä½åºåæ ·å¼ .batch-actions-section { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 16rpx; padding: 24rpx; margin-bottom: 24rpx; box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.3); color: #ffffff; } .batch-info { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20rpx; } .batch-count { font-size: 28rpx; font-weight: 600; } .batch-buttons { display: flex; gap: 20rpx; justify-content: flex-end; } .batch-btn { min-width: 180rpx; } // å ¨éæä½åºåæ ·å¼ .select-all-section { display: flex; justify-content: space-between; align-items: center; background: #f8f9fa; border-radius: 12rpx; padding: 20rpx 24rpx; margin-bottom: 20rpx; border: 1rpx solid #e9ecef; } .select-all-checkbox { display: flex; align-items: center; gap: 12rpx; cursor: pointer; } .select-all-text { font-size: 26rpx; color: #606266; font-weight: 500; } .select-all-hint { font-size: 22rpx; color: #909399; } // åè¡¨é¡¹éæ©æ ·å¼ .ledger-item { display: flex; align-items: flex-start; padding: 0; } .item-checkbox { padding: 24rpx 16rpx 0 24rpx; cursor: pointer; display: flex; align-items: center; min-height: 48rpx; } .item-content { flex: 1; padding: 0; } // ç¹å»ç¼è¾åºåæ ·å¼ src/pages/productionManagement/productionReporting/components/formDia.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,160 @@ <template> <view> <up-popup v-model:show="dialogFormVisible" mode="bottom" round="12" :customStyle="{ height: '70vh' }" @close="closeDia"> <view class="dia-container"> <view class="dia-header"> <text class="title">ç产æ¥å·¥</text> </view> <scroll-view class="rows" scroll-y> <up-form :model="form" label-width="120" ref="formRef"> <up-form-item label="æäº§æ°é"> <up-input v-model="form.schedulingNum" placeholder="请è¾å ¥" disabled /> </up-form-item> <up-form-item label="æ¬æ¬¡ç产æ°é" prop="finishedNum"> <up-input v-model="form.finishedNum" type="number" placeholder="请è¾å ¥" @change="changeNum" /> </up-form-item> <up-form-item label="å¾ ç产æ°é"> <up-input v-model="form.pendingNum" placeholder="请è¾å ¥" disabled /> </up-form-item> <up-form-item label="ç产人" @click="openUserPicker"> <up-input v-model="form.schedulingUserName" placeholder="éæ©äººå" readonly @click="openUserPicker" /> <template #right> <up-icon name="arrow-right" @click="openUserPicker"></up-icon> </template> </up-form-item> <up-form-item label="çäº§æ¥æ" @click="openDatePicker"> <up-input v-model="form.schedulingDate" placeholder="è¯·éæ©æ¥æ" readonly @click="openDatePicker" /> <template #right> <up-icon name="calendar" @click="openDatePicker"></up-icon> </template> </up-form-item> </up-form> </scroll-view> <view class="dia-footer"> <up-button type="primary" @click="submitForm">确认</up-button> <up-button @click="closeDia">åæ¶</up-button> </view> </view> </up-popup> <!-- æ¥æéæ©å¨ --> <up-popup :show="datePicker.show" mode="bottom" @close="datePicker.show = false"> <up-datetime-picker :show="true" v-model="datePicker.value" mode="date" @confirm="onDateConfirm" @cancel="datePicker.show = false" /> </up-popup> <!-- 人åéæ©å¨ --> <up-action-sheet :show="userPicker.show" :actions="userActionList" title="éæ©äººå" @select="onUserSelect" @close="userPicker.show = false" /> </view> </template> <script setup> import { ref, reactive, toRefs, getCurrentInstance, computed } from "vue"; import { userListNoPageByTenantId } from "@/api/system/user.js"; import { productionReport, productionReportUpdate } from "@/api/productionManagement/productionReporting.js"; const { proxy } = getCurrentInstance() const emit = defineEmits(['close']) const userList = ref([]) const dialogFormVisible = ref(false); const operationType = ref('') const data = reactive({ form: { successNum: "", schedulingNum: "", finishedNum: "", schedulingUserId: "", schedulingUserName: "", schedulingDate: "", }, rules: { schedulingNum: [{ required: true, message: "请è¾å ¥", trigger: "blur" },], }, }); const { form, rules } = toRefs(data); // pickers const datePicker = ref({ show: false, value: Date.now() }) const userPicker = ref({ show: false }) const userActionList = computed(() => userList.value.map(u => ({ name: u.nickName, value: u.userId }))) // æå¼å¼¹æ¡ const openDialog = (type, row) => { operationType.value = type; dialogFormVisible.value = true; userListNoPageByTenantId().then((res) => { userList.value = res.data; }); form.value = { ...row, schedulingUserName: row.schedulingUserName || '' } // åå§åï¼æ¬æ¬¡ç产æ°é置空ï¼å¾ ç产 = æäº§æ°é const sched = Number(form.value.schedulingNum) || 0 form.value.finishedNum = '' form.value.pendingNum = sched } const changeNum = (value) => { const sched = Number(form.value.schedulingNum) || 0 let num = Number(value) if (isNaN(num) || num < 0) num = 0 if (num > sched) { num = sched uni.showToast({ title: 'æ¬æ¬¡ç产æ°éä¸å¯å¤§äºæäº§æ°é', icon: 'none' }) } form.value.finishedNum = String(num) form.value.pendingNum = sched - num } const openDatePicker = () => { datePicker.value.value = Date.now() datePicker.value.show = true } const onDateConfirm = (e) => { const d = new Date(e.value) const y = d.getFullYear(); const m = String(d.getMonth()+1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0') form.value.schedulingDate = `${y}-${m}-${day}` datePicker.value.show = false } const openUserPicker = () => { userPicker.value.show = true } const onUserSelect = (item) => { if (item) { form.value.schedulingUserId = item.value form.value.schedulingUserName = item.name } userPicker.value.show = false } // æäº¤äº§å表å const submitForm = () => { // ç®åæ ¡éªï¼ä» ç¡®ä¿å¿ å¡«åæ®µ if (!form.value.finishedNum || !form.value.schedulingUserId || !form.value.schedulingDate) { uni.showToast({ title: '请å®åå¿ å¡«ä¿¡æ¯', icon: 'none' }) return } form.value.staffState = 1 const req = operationType.value === 'add' ? productionReport : productionReportUpdate req(form.value).then(() => { uni.showToast({ title: 'æäº¤æå', icon: 'success' }) closeDia() }) } // å ³éå¼¹æ¡ const closeDia = () => { dialogFormVisible.value = false; emit('close') }; defineExpose({ openDialog, }); </script> <style scoped lang="scss"> .dia-container { padding: 12px; height: 100%; display: flex; flex-direction: column; } .dia-header { display: flex; align-items: center; justify-content: center; margin-bottom: 10px; } .title { font-weight: 600; font-size: 16px; color: #333; } .rows { flex: 1; min-height: 0; overflow-y: auto; } .dia-footer { display: flex; gap: 10px; justify-content: flex-end; padding-top: 8px; } </style> src/pages/productionManagement/productionReporting/index.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,202 @@ <template> <view class="prod-reporting"> <PageHeader title="ç产æ¥å·¥" /> <view class="search_form"> <u-form> <view class="form-row"> <u-form-item label="客æ·åç§°" label-width="80"> <up-input v-model="searchForm.customerName" placeholder="请è¾å ¥" clearable @change="handleQuery" /> </u-form-item> <u-form-item label="项ç®åç§°" label-width="80"> <up-input v-model="searchForm.projectName" placeholder="请è¾å ¥" clearable @change="handleQuery" /> </u-form-item> </view> <view class="form-row"> <u-form-item label="ç¶æ" label-width="80"> <up-input v-model="statusDisplay" placeholder="è¯·éæ©ç¶æ" readonly @click="showStatusPicker = true" /> </u-form-item> </view> <view class="form-actions"> <u-button type="primary" size="small" @click="handleQuery">æç´¢</u-button> </view> </u-form> </view> <view class="list_container"> <u-loading-icon v-if="tableLoading" text="å è½½ä¸..." /> <view v-else> <view v-if="!tableData || tableData.length === 0" class="empty">ææ æ°æ®</view> <view v-else class="card_list"> <view v-for="item in tableData" :key="item.id" class="card_item"> <view class="card_header"> <u-tag :type="statusType(item.status)" size="mini">{{ statusText(item.status) }}</u-tag> <text class="card_title">{{ item.projectName }}</text> </view> <view class="card_body"> <view class="row"><text class="label">æäº§æ¥æ</text><text class="value">{{ item.schedulingDate }}</text></view> <view class="row"><text class="label">æäº§äºº</text><text class="value">{{ item.schedulingUserName }}</text></view> <view class="row"><text class="label">ååå·</text><text class="value">{{ item.salesContractNo }}</text></view> <view class="row"><text class="label">客æ·ååå·</text><text class="value">{{ item.customerContractNo }}</text></view> <view class="row"><text class="label">客æ·åç§°</text><text class="value">{{ item.customerName }}</text></view> <view class="row"><text class="label">产å大类</text><text class="value">{{ item.productCategory }}</text></view> <view class="row"><text class="label">è§æ ¼åå·</text><text class="value">{{ item.specificationModel }}</text></view> <view class="row inline"> <view class="col"><text class="label">åä½</text><text class="value">{{ item.unit }}</text></view> <view class="col"><text class="label">æäº§æ°é</text><text class="value">{{ item.schedulingNum }}</text></view> <view class="col"><text class="label">ç产æ°é</text><text class="value">{{ item.finishedNum }}</text></view> <view class="col"><text class="label">å¾ ç产æ°é</text><text class="value">{{ item.pendingFinishNum }}</text></view> </view> </view> <view class="card_actions"> <u-button type="primary" size="small" @click="openForm('add', item)" :disabled="item.pendingFinishNum == 0">ç产æ¥å·¥</u-button> </view> </view> </view> </view> </view> <form-dia ref="formDia" @close="handleQuery"></form-dia> <!-- ç¶æéæ©å¨ --> <up-action-sheet :show="showStatusPicker" :actions="statusActions" title="éæ©ç¶æ" @select="onStatusSelect" @close="showStatusPicker = false" /> </view> </template> <script setup> import { onMounted, ref, reactive, toRefs, nextTick, computed } from "vue"; import PageHeader from '@/components/PageHeader.vue' import FormDia from './components/formDia.vue' import { workListPage } from "@/api/productionManagement/productionReporting.js"; const data = reactive({ searchForm: { customerName: "", projectName: "", status: undefined, }, }); const { searchForm } = toRefs(data); const showStatusPicker = ref(false) const statusOptions = ref([ { label: 'å¾ ç产', value: 1 }, { label: 'ç产ä¸', value: 2 }, { label: 'å·²æ¥å·¥', value: 3 }, ]) const statusActions = computed(() => statusOptions.value.map(o => ({ name: o.label, value: o.value }))) const statusDisplay = ref('') const tableData = ref([]); const tableLoading = ref(false); const page = reactive({ current: -1, size: -1, }); const formDia = ref() // æ¥è¯¢å表 /** æç´¢æé®æä½ */ const handleQuery = () => { page.current = -1; page.size = -1; getList(); }; const getList = () => { tableLoading.value = true; const params = { ...searchForm.value, ...page }; workListPage(params).then(res => { tableLoading.value = false; tableData.value = res.data.records.map(item => ({ ...item, pendingFinishNum: (Number(item.schedulingNum) || 0) - (Number(item.finishedNum) || 0) })); }).catch(err => { tableLoading.value = false; }) }; // ç¶æéæ© const onStatusSelect = (item) => { searchForm.value.status = item?.value statusDisplay.value = item?.name || '' showStatusPicker.value = false handleQuery() } // æå¼å¼¹æ¡ const openForm = (type, row) => { if (!row) return if ((Number(row.pendingFinishNum) || 0) === 0) { uni.showToast({ title: 'æ é忥工', icon: 'none' }) return } nextTick(() => { formDia.value?.openDialog(type, row) }) }; // ç¶æææ¬/ç±»å const statusText = (s) => { if (s == 3) return 'å·²æ¥å·¥' if (s == 1) return 'å¾ ç产' return 'ç产ä¸' } const statusType = (s) => { if (s == 3) return 'success' if (s == 1) return 'primary' return 'warning' } // æ å页 // æç»äººå/æ¥æéæ© const openChildUserPicker = (index) => { if (!expandData.value[index]?.editType) return childUserPicker.value.index = index childUserPicker.value.show = true } const onChildUserSelect = (item) => { if (item && childUserPicker.value.index > -1) { const row = expandData.value[childUserPicker.value.index] row.schedulingUserId = item.value row.schedulingUserName = item.name } childUserPicker.value.show = false } const openChildDatePicker = (index) => { if (!expandData.value[index]?.editType) return childDatePicker.value.index = index childDatePicker.value.value = Date.now() childDatePicker.value.show = true } const onChildDateConfirm = (e) => { const d = new Date(e.value) const y = d.getFullYear(); const m = String(d.getMonth()+1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0') const str = `${y}-${m}-${day}` if (childDatePicker.value.index > -1) { expandData.value[childDatePicker.value.index].schedulingDate = str } childDatePicker.value.show = false } onMounted(() => { getList(); }); </script> <style scoped lang="scss"> .prod-reporting { padding-bottom: 12px; } .search_form { margin: 12px; background: #fff; border-radius: 8px; padding: 10px; } .form-row { display: flex; gap: 12px; } .form-actions { display: flex; justify-content: flex-end; } .list_container { padding: 0 12px; } .empty { text-align: center; color: #888; padding: 20px 0; } .card_list { display: flex; flex-direction: column; gap: 10px; } .card_item { background: #fff; border-radius: 10px; padding: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); } .card_header { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; } .card_title { font-weight: 500; color: #333; margin-left: auto; } .card_body .row { display: flex; justify-content: space-between; padding: 4px 0; } .card_body .row.inline { display: flex; gap: 10px; flex-wrap: wrap; } .card_body .row.inline .col { min-width: 45%; display: flex; justify-content: space-between; } .label { color: #666; font-size: 12px; } .value { color: #333; font-size: 12px; } .card_actions { display: flex; justify-content: flex-end; gap: 8px; padding-top: 8px; } .ml8 { margin-left: 8px; } </style> src/pages/sales/salesAccount/detail.vue
@@ -223,6 +223,18 @@ ></up-icon> </template> </up-form-item> <!-- ç»å®æºå¨ --> <up-form-item label="ç»å®æºå¨" prop="speculativeTradingName" required > <up-input disabled v-model="product.speculativeTradingName" placeholder="请è¾å ¥" /> </up-form-item> <!-- åä½ --> <up-form-item @@ -437,7 +449,8 @@ return modelOptions.value.map(model => ({ name: model.text, value: model.value, unit: model.unit unit: model.unit, speculativeTradingName: model.speculativeTradingName })) }) @@ -521,6 +534,7 @@ specificationModel: '', productModelId: '', unit: '', speculativeTradingName: '', taxRate: '', taxInclusiveUnitPrice: '', quantity: '', @@ -603,14 +617,17 @@ text: user.model, value: user.id, unit: user.unit, speculativeTradingName: user.speculativeTradingName, })); }); }; // è§æ ¼åå·éæ©äºä»¶ const onSpecificationSelect = (item) => { console.log('selected item---', item); productData.value[currentProductIndex.value].specificationModel = item.name productData.value[currentProductIndex.value].productModelId = item.value productData.value[currentProductIndex.value].unit = item.unit productData.value[currentProductIndex.value].speculativeTradingName = item.speculativeTradingName } // ç¨çéæ©äºä»¶ const onTaxRateSelect = (item) => { src/pages/sales/salesAccount/index.vue
@@ -22,7 +22,7 @@ </view> <!-- éå®å°è´¦ç叿µ --> <view class="ledger-list" v-if="total > 0"> <view class="ledger-list" v-if="ledgerList.length > 0"> <view v-for="(item, index) in ledgerList" :key="index"> <view class="ledger-item" @click="handleInfo('edit', item)"> <view class="item-header"> @@ -115,7 +115,6 @@ // éå®å°è´¦æ°æ® const ledgerList = ref([]); const total = ref(0); // è¿åä¸ä¸é¡µ const goBack = () => { @@ -129,8 +128,8 @@ size: -1 } ledgerListPage({...page, salesContractNo: salesContractNo.value}).then((res) => { console.log('éå®å°è´¦----', res); ledgerList.value = res.records; total.value = res.total; closeToast() }).catch(() => { closeToast() src/pages/sales/salesAccount/view.vue
@@ -69,6 +69,10 @@ <text class="info-value">{{ product.specificationModel }}</text> </view> <view class="info-item"> <text class="info-label">ç»å®æºå¨</text> <text class="info-value">{{ product.speculativeTradingName }}</text> </view> <view class="info-item"> <text class="info-label">åä½</text> <text class="info-value">{{ product.unit }}</text> </view> src/utils/request.ts
@@ -22,12 +22,12 @@ config.url = url } // è®°å½è¯·æ±åæ° console.log('请æ±åéåæ°:', { url: (config.baseUrl || baseUrl) + config.url, method: config.method || 'GET', headers: config.header, data: config.data }) // console.log('请æ±åéåæ°:', { // url: (config.baseUrl || baseUrl) + config.url, // method: config.method || 'GET', // headers: config.header, // data: config.data // }) return new Promise((resolve, reject) => { uni.request({ method: config.method || 'GET', @@ -49,11 +49,11 @@ // @ts-ignore const msg: string = errorCode[code] || data.msg || errorCode['default'] // è®°å½æ¥æ¶å°çåæ° console.log('æ¥æ¶å°çåæ°:', { url: (config.baseUrl || baseUrl) + config.url, code: code, data: data }) // console.log('æ¥æ¶å°çåæ°:', { // url: (config.baseUrl || baseUrl) + config.url, // code: code, // data: data // }) if (code === 401) { showConfirm('ç»å½ç¶æå·²è¿æï¼æ¨å¯ä»¥ç»§ç»çå¨è¯¥é¡µé¢ï¼æè éæ°ç»å½?').then(res => { if (res.confirm) {