| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | // BOM å表å页æ¥è¯¢ |
| | | export function listPage(query) { |
| | | return request({ |
| | | url: "/technologyBom/listPage", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // æ°å¢ BOM |
| | | export function add(data) { |
| | | return request({ |
| | | url: "/technologyBom/add", |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // ä¿®æ¹ BOM |
| | | export function update(data) { |
| | | return request({ |
| | | url: "/technologyBom/update", |
| | | method: "put", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // å é¤ BOM |
| | | export function batchDelete(ids) { |
| | | return request({ |
| | | url: "/technologyBom/batchDelete", |
| | | method: "delete", |
| | | data: ids, |
| | | }); |
| | | } |
| | | |
| | | // å¤å¶ BOM |
| | | export function copy(data) { |
| | | return request({ |
| | | url: "/technologyBom/copy", |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // è·å产åå表 (ç¨äºæ°å¢BOMæ¶éæ©äº§å) |
| | | export function getProductList(query) { |
| | | return request({ |
| | | url: "/product/ledger/listPage", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // --- BOM ç»æç¸å
³ --- |
| | | |
| | | // æ ¹æ® BOM ID è·åç»æå表 |
| | | export function queryStructureList(bomId) { |
| | | return request({ |
| | | url: "/technologyBomStructure/listByBomId/" + bomId, |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | // ä¿å BOM ç»æ |
| | | export function addStructure(data) { |
| | | return request({ |
| | | url: "/technologyBomStructure/batchSave", |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // å é¤ BOM ç»æé¡¹ |
| | | export function deleteStructure(id) { |
| | | return request({ |
| | | url: "/technologyBomStructure/batchDelete/" + id, |
| | | method: "delete", |
| | | }); |
| | | } |
| | |
| | | }); |
| | | } |
| | | |
| | | export function list() { |
| | | return request({ |
| | | url: "/technologyOperation/list", |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | export function add(data) { |
| | | return request({ |
| | | url: "/technologyOperation/add", |
| | |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/productionDesign/bom/index", |
| | | "style": { |
| | | "navigationBarTitleText": "BOM管ç", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/productionDesign/bom/structure", |
| | | "style": { |
| | | "navigationBarTitleText": "BOMç»æ", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/productionDesign/bom/edit", |
| | | "style": { |
| | | "navigationBarTitleText": "BOM详æ
", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/cooperativeOffice/collaborativeApproval/index1", |
| | | "style": { |
| | | "navigationBarTitleText": "å
¬åºç®¡ç", |
| | |
| | | "navigationBarTitleText": "RuoYi", |
| | | "navigationBarBackgroundColor": "#FFFFFF" |
| | | } |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="structure-item-wrapper" |
| | | :class="{ 'is-root': level === 0, 'is-last': isLast }"> |
| | | <!-- æ å½¢è¿æ¥çº¿ (éæ ¹èç¹æ¾ç¤º) --> |
| | | <template v-if="level > 0"> |
| | | <view class="line-v"></view> |
| | | <view class="line-h"></view> |
| | | </template> |
| | | <view class="structure-item-card" |
| | | :class="{ 'has-children': hasChildren }"> |
| | | <view class="card-main"> |
| | | <view class="item-header" |
| | | @click="toggleExpand"> |
| | | <view class="header-left"> |
| | | <view v-if="hasChildren" |
| | | class="expand-icon" |
| | | :class="{ 'is-expanded': isExpanded }"> |
| | | <up-icon name="arrow-right" |
| | | size="14" |
| | | color="#999"></up-icon> |
| | | </view> |
| | | <view v-else |
| | | class="dot-icon"></view> |
| | | <text class="item-title">{{ item.productName || 'æªéæ©äº§å' }}</text> |
| | | </view> |
| | | <up-tag v-if="hasChildren" |
| | | text="ç»å" |
| | | type="primary" |
| | | size="mini" |
| | | plain |
| | | shape="circle" /> |
| | | </view> |
| | | <view class="item-body"> |
| | | <view class="info-grid"> |
| | | <view class="info-item"> |
| | | <text class="label">è§æ ¼åå·ï¼</text> |
| | | <text class="value">{{ item.model || '-' }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="label">æ¶èå·¥åºï¼</text> |
| | | <text class="value">{{ getProcessName(item.processId) }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="label">å使°éï¼</text> |
| | | <text class="value highlight">{{ item.unitQuantity || 0 }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="label">éæ±æ»éï¼</text> |
| | | <text class="value highlight">{{ item.demandedQuantity || 0 }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="label">åä½ï¼</text> |
| | | <text class="value">{{ item.unit || '-' }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="label">çæ°ï¼</text> |
| | | <text class="value">{{ item.diskQuantity || 0 }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <!-- éå½å±ç¤ºåèç¹ --> |
| | | <view v-if="hasChildren && isExpanded" |
| | | class="children-container"> |
| | | <BomStructureItem v-for="(child, index) in item.children" |
| | | :key="index" |
| | | :item="child" |
| | | :level="level + 1" |
| | | :isLast="index === item.children.length - 1" |
| | | :processOptions="processOptions" /> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, computed, defineProps } from "vue"; |
| | | |
| | | const props = defineProps({ |
| | | item: { |
| | | type: Object, |
| | | required: true, |
| | | }, |
| | | level: { |
| | | type: Number, |
| | | default: 0, |
| | | }, |
| | | isLast: { |
| | | type: Boolean, |
| | | default: false, |
| | | }, |
| | | processOptions: { |
| | | type: Array, |
| | | default: () => [], |
| | | }, |
| | | }); |
| | | |
| | | const isExpanded = ref(true); |
| | | const hasChildren = computed( |
| | | () => props.item.children && props.item.children.length > 0 |
| | | ); |
| | | |
| | | const toggleExpand = () => { |
| | | if (hasChildren.value) { |
| | | isExpanded.value = !isExpanded.value; |
| | | } |
| | | }; |
| | | |
| | | const getProcessName = id => { |
| | | const process = props.processOptions.find(p => p.id === id); |
| | | return process ? process.name : "-"; |
| | | }; |
| | | </script> |
| | | |
| | | <script> |
| | | export default { |
| | | name: "BomStructureItem", |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .structure-item-wrapper { |
| | | position: relative; |
| | | padding-left: 44rpx; |
| | | |
| | | &.is-root { |
| | | padding-left: 0; |
| | | } |
| | | } |
| | | |
| | | // åç´è¿æ¥çº¿æ®µ |
| | | .line-v { |
| | | position: absolute; |
| | | left: 18rpx; // å±
ä¸äº 44rpx ç缩è¿å
|
| | | top: -20rpx; // åä¸å»¶ä¼¸è¦çä¸ä¸ä¸ªèç¹ç margin-bottom |
| | | bottom: 0; |
| | | width: 2rpx; |
| | | background-color: #ddd; |
| | | z-index: 1; |
| | | } |
| | | |
| | | // æåä¸ä¸ªèç¹çåç´çº¿åªå»¶ä¼¸å°æ°´å¹³çº¿ä½ç½® |
| | | .is-last > .line-v { |
| | | bottom: auto; |
| | | height: 60rpx; // 20rpx (top offset) + 40rpx (to horizontal line) |
| | | } |
| | | |
| | | // æ°´å¹³è¿æ¥çº¿ |
| | | .line-h { |
| | | position: absolute; |
| | | left: 18rpx; |
| | | top: 40rpx; // 对é½å°å¡çå
é¨å¾æ ä¸å¿ (padding 24 + icon 32/2) |
| | | width: 26rpx; |
| | | height: 2rpx; |
| | | background-color: #ddd; |
| | | z-index: 1; |
| | | } |
| | | |
| | | .structure-item-card { |
| | | position: relative; |
| | | background: #fff; |
| | | border-radius: 16rpx; |
| | | margin-bottom: 20rpx; |
| | | padding: 24rpx; |
| | | box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05); |
| | | border: 1rpx solid #f0f0f0; |
| | | transition: all 0.3s; |
| | | z-index: 2; |
| | | |
| | | &:active { |
| | | background-color: #f9f9f9; |
| | | } |
| | | |
| | | &.has-children { |
| | | border-left: 6rpx solid #3c9cff; |
| | | } |
| | | } |
| | | |
| | | .card-main { |
| | | .item-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 20rpx; |
| | | |
| | | .header-left { |
| | | display: flex; |
| | | align-items: center; |
| | | flex: 1; |
| | | |
| | | .expand-icon { |
| | | margin-right: 12rpx; |
| | | transition: transform 0.3s; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | width: 32rpx; |
| | | height: 32rpx; |
| | | |
| | | &.is-expanded { |
| | | transform: rotate(90deg); |
| | | } |
| | | } |
| | | |
| | | .dot-icon { |
| | | width: 12rpx; |
| | | height: 12rpx; |
| | | border-radius: 50%; |
| | | background-color: #ccc; |
| | | margin-right: 20rpx; |
| | | margin-left: 10rpx; |
| | | } |
| | | |
| | | .item-title { |
| | | font-size: 30rpx; |
| | | font-weight: bold; |
| | | color: #333; |
| | | line-height: 1.4; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .item-body { |
| | | .info-grid { |
| | | display: grid; |
| | | grid-template-columns: 1fr 1fr; |
| | | gap: 12rpx 20rpx; |
| | | |
| | | .info-item { |
| | | display: flex; |
| | | font-size: 24rpx; |
| | | line-height: 1.5; |
| | | |
| | | .label { |
| | | color: #999; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .value { |
| | | color: #666; |
| | | word-break: break-all; |
| | | |
| | | &.highlight { |
| | | color: #3c9cff; |
| | | font-weight: 500; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .children-container { |
| | | position: relative; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="bom-list"> |
| | | <PageHeader title="BOM管ç" |
| | | @back="goBack" /> |
| | | <view class="search-section"> |
| | | <view class="search-bar"> |
| | | <view class="search-input"> |
| | | <up-input class="search-text" |
| | | v-model="queryParams.productName" |
| | | placeholder="请è¾å
¥äº§ååç§°" |
| | | clearable |
| | | @change="handleSearch" /> |
| | | </view> |
| | | <view class="filter-button" |
| | | @click="handleSearch"> |
| | | <up-icon name="search" |
| | | size="24" |
| | | color="#999999"></up-icon> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-if="list.length > 0" |
| | | class="ledger-list"> |
| | | <view v-for="item in list" |
| | | :key="item.id" |
| | | class="ledger-item"> |
| | | <view class="item-header"> |
| | | <view class="item-left"> |
| | | <view class="document-icon"> |
| | | <up-icon name="list-dot" |
| | | size="16" |
| | | color="#ffffff"></up-icon> |
| | | </view> |
| | | <text class="item-id">{{ item.bomNo || "-" }}</text> |
| | | </view> |
| | | <up-tag :text="'V' + (item.version || '1.0')" |
| | | type="primary" |
| | | size="mini" /> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | <view class="item-details"> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">产ååç§°</text> |
| | | <text class="detail-value">{{ item.productName || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">è§æ ¼åå·</text> |
| | | <text class="detail-value">{{ item.productModelName || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">夿³¨</text> |
| | | <text class="detail-value">{{ item.remark || "-" }}</text> |
| | | </view> |
| | | </view> |
| | | <view class="action-buttons"> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | type="primary" |
| | | @click="goStructure(item)">æ¥ç详æ
</up-button> |
| | | </view> |
| | | </view> |
| | | <up-loadmore :status="pageStatus" /> |
| | | </view> |
| | | <view v-else |
| | | class="no-data"> |
| | | <up-empty text="ææ BOMæ°æ®" |
| | | mode="list"></up-empty> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { reactive, ref } from "vue"; |
| | | import { onReachBottom, onShow } from "@dcloudio/uni-app"; |
| | | import { listPage } from "@/api/productionManagement/bom"; |
| | | |
| | | const queryParams = reactive({ |
| | | productName: "", |
| | | }); |
| | | const list = ref([]); |
| | | const pageStatus = ref("loadmore"); |
| | | |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 3, |
| | | total: 0, |
| | | }); |
| | | |
| | | const goBack = () => { |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | const handleSearch = () => { |
| | | page.current = 1; |
| | | pageStatus.value = "loadmore"; |
| | | list.value = []; |
| | | getList(); |
| | | }; |
| | | |
| | | const getList = () => { |
| | | if (pageStatus.value === "loading" || pageStatus.value === "nomore") return; |
| | | |
| | | pageStatus.value = "loading"; |
| | | listPage({ |
| | | current: page.current, |
| | | size: page.size, |
| | | productName: queryParams.productName, |
| | | }) |
| | | .then(res => { |
| | | const records = res?.data?.records || res?.records || []; |
| | | const total = res?.data?.total || res?.total || 0; |
| | | |
| | | if (page.current === 1) { |
| | | list.value = records; |
| | | } else { |
| | | list.value = [...list.value, ...records]; |
| | | } |
| | | |
| | | page.total = total; |
| | | if (list.value.length >= total) { |
| | | pageStatus.value = "nomore"; |
| | | } else { |
| | | pageStatus.value = "loadmore"; |
| | | page.current++; |
| | | } |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "æ¥è¯¢å¤±è´¥", icon: "error" }); |
| | | pageStatus.value = "loadmore"; |
| | | }); |
| | | }; |
| | | |
| | | const goStructure = item => { |
| | | uni.navigateTo({ |
| | | url: `/pages/productionDesign/bom/structure?id=${ |
| | | item.id |
| | | }&bomNo=${encodeURIComponent(item.bomNo)}&productName=${encodeURIComponent( |
| | | item.productName || "" |
| | | )}&productModelName=${encodeURIComponent( |
| | | item.productModelName || "" |
| | | )}&remark=${encodeURIComponent( |
| | | item.remark || "" |
| | | )}&version=${encodeURIComponent(item.version || 1)}`, |
| | | }); |
| | | }; |
| | | |
| | | onReachBottom(() => { |
| | | getList(); |
| | | }); |
| | | |
| | | onShow(() => { |
| | | handleSearch(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/styles/procurement-common.scss"; |
| | | |
| | | .no-data { |
| | | padding-top: 100rpx; |
| | | text-align: center; |
| | | color: #999; |
| | | font-size: 28rpx; |
| | | } |
| | | |
| | | .action-buttons { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | gap: 15rpx; |
| | | padding: 0 30rpx 30rpx; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .action-btn { |
| | | width: calc(50% - 15rpx); |
| | | margin: 0 !important; |
| | | margin-bottom: 15rpx !important; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="structure-page"> |
| | | <PageHeader :title="'BOMç»æ - ' + bomNo" |
| | | @back="goBack" /> |
| | | <view class="info-card"> |
| | | <view class="info-row"> |
| | | <text class="info-label">产ååç§°ï¼</text> |
| | | <text class="info-value">{{ productName }}-{{ productModelName }}</text> |
| | | </view> |
| | | </view> |
| | | <view class="structure-list" |
| | | v-if="dataList.length > 0"> |
| | | <BomStructureItem v-for="(item, index) in dataList" |
| | | :key="index" |
| | | :item="item" |
| | | :level="0" |
| | | :isLast="index === dataList.length - 1" |
| | | :processOptions="processOptions" /> |
| | | </view> |
| | | <view v-else |
| | | class="no-data"> |
| | | <up-empty text="ææ ç»ææ°æ®" |
| | | mode="list"></up-empty> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, computed } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import { queryStructureList } from "@/api/productionManagement/bom"; |
| | | import { list as getProcessList } from "@/api/productionManagement/processManagement"; |
| | | import BomStructureItem from "./BomStructureItem.vue"; |
| | | |
| | | const bomId = ref(null); |
| | | const bomNo = ref(""); |
| | | const productName = ref(""); |
| | | const dataList = ref([]); |
| | | const processOptions = ref([]); |
| | | |
| | | const goBack = () => { |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | const fetchData = () => { |
| | | queryStructureList(bomId.value).then(res => { |
| | | dataList.value = res.data || []; |
| | | }); |
| | | }; |
| | | |
| | | const fetchProcess = () => { |
| | | getProcessList().then(res => { |
| | | processOptions.value = res.data || []; |
| | | }); |
| | | }; |
| | | |
| | | const productModelName = ref(""); |
| | | |
| | | onLoad(options => { |
| | | bomId.value = options.id; |
| | | bomNo.value = decodeURIComponent(options.bomNo); |
| | | productName.value = decodeURIComponent(options.productName); |
| | | productModelName.value = decodeURIComponent(options.productModelName); |
| | | fetchData(); |
| | | fetchProcess(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .structure-page { |
| | | background-color: #f5f5f5; |
| | | min-height: 100vh; |
| | | padding-bottom: 120rpx; |
| | | } |
| | | |
| | | .info-card { |
| | | background: #fff; |
| | | padding: 30rpx; |
| | | margin-bottom: 20rpx; |
| | | .info-row { |
| | | display: flex; |
| | | font-size: 28rpx; |
| | | .info-label { |
| | | color: #666; |
| | | } |
| | | .info-value { |
| | | color: #333; |
| | | font-weight: bold; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .structure-list { |
| | | padding: 20rpx; |
| | | } |
| | | |
| | | .no-data { |
| | | padding-top: 100rpx; |
| | | } |
| | | </style> |
| | |
| | | url: "/pages/productionDesign/processManagement/index", |
| | | }); |
| | | break; |
| | | case "BOM": |
| | | uni.navigateTo({ |
| | | url: "/pages/productionDesign/bom/index", |
| | | }); |
| | | break; |
| | | default: |
| | | uni.showToast({ |
| | | title: `ç¹å»äº${item.label}`, |