Explorar el Código

完成物料采购功能

wuhb hace 4 días
padre
commit
5ac0f87b11

+ 17 - 0
api/purchase/index.uts

@@ -0,0 +1,17 @@
+/**
+ * 采购单相关接口
+ */
+import { request } from '../../utils/request'
+
+/**
+ * 创建合并采购单
+ * @param data 采购单数据
+ * @returns 
+ */
+export const createMergePurchase = (data: UTSJSONObject | null): Promise<any> => {
+	return request({
+		url: '/mes/wm/mergePurchase/create',
+		method: 'POST',
+		data: data
+	})
+}

+ 7 - 0
pages.json

@@ -188,6 +188,13 @@
 				"navigationBarTitleText": "网页",
 				"navigationStyle": "custom"
 			}
+		},
+		{
+			"path": "pages/purchase/createByApply",
+			"style": {
+				"navigationBarTitleText": "创建采购单",
+				"navigationStyle": "custom"
+			}
 		}
 	],
 	"globalStyle": {

+ 47 - 1
pages/apply/applyInfo.uvue

@@ -1,7 +1,7 @@
 <template>
 	<uni-navbar-lite :showRight=false title="申请详情"></uni-navbar-lite>
     <view class="page-container">
-        <scroll-view class="page-content" :class="{ 'has-bottom-buttons': applyStatus != null && applyStatus.trim() == 'PREPARE' }" :scroll-y="true">
+        <scroll-view class="page-content" :class="{ 'has-bottom-buttons': applyStatus != null && (applyStatus.trim() == 'PREPARE' || applyStatus.trim() == 'CONFIRMED') }" :scroll-y="true">
             <!-- 申请单信息 -->
             <view class="section">
                 <view class="section-header">
@@ -83,6 +83,11 @@
             <button class="delete-btn" @click="handleDelete">删除</button>
             <button class="confirm-btn" @click="handleConfirm">确认申请</button>
         </view>
+        
+        <!-- 采购按钮 -->
+        <view v-if="showPurchaseButton" class="bottom-buttons">
+            <button class="purchase-btn" @click="handlePurchase">采购</button>
+        </view>
     </view>
 </template>
 
@@ -98,6 +103,14 @@
     const nickName = ref<string>("")
     const lineList = ref<UTSJSONObject[]>([])
 
+    const showPurchaseButton = computed((): boolean => {
+        const status = applyStatus.value
+        if (status != null && status.trim() === 'CONFIRMED') {
+            return true
+        }
+        return false
+    })
+
     const applyStatusText = computed((): string => {
         const status = applyStatus.value
         switch (status) {
@@ -243,6 +256,28 @@
         })
     }
 
+    const handlePurchase = (): void => {
+        const hasPurchased = purchaseStatus.value != null && purchaseStatus.value.trim() === '1'
+        
+        if (hasPurchased) {
+            uni.showModal({
+                title: '提示',
+                content: '该单据已申购,继续采购将重复申购,确定继续吗?',
+                success: (res) => {
+                    if (res.confirm) {
+                        uni.navigateTo({
+                            url: `/pages/purchase/createByApply?applyIds=${applyId.value}`
+                        })
+                    }
+                }
+            })
+        } else {
+            uni.navigateTo({
+                url: `/pages/purchase/createByApply?applyIds=${applyId.value}`
+            })
+        }
+    }
+
     onLoad((options: any) => {
 		const params = options as UTSJSONObject
         if (params != null && params['id'] != null) {
@@ -487,4 +522,15 @@
         border-radius: 20rpx;
         border: none;
     }
+
+    .purchase-btn {
+        width: 100%;
+        height: 80rpx;
+        background-color: #52c41a;
+        color: #ffffff;
+        font-size: 28rpx;
+        font-weight: 600;
+        border-radius: 20rpx;
+        border: none;
+    }
 </style>

+ 153 - 5
pages/apply/index.uvue

@@ -1,6 +1,6 @@
 <template>
 	<uni-navbar-lite @rightClick="handleRight" :show-right="showRight" title="物料申请"></uni-navbar-lite>
-    <view class="list-page">
+    <view class="list-page" :class="{ 'has-bottom-bar': showPurchaseBar }">
         <!-- 搜索栏和状态标签 -->
         <view class="search-block">
             <view class="search-bar">
@@ -63,13 +63,17 @@
             :refreshing="refreshing" 
             :hasMore="hasMore" 
             @refresh="handleRefresh" 
-            @loadMore="loadMore" 
-            @itemClick="handleItemClick"
+            @loadMore="loadMore"
         >
             <template #default="{ item, index }">
-                <view class="list-item">
+                <view class="list-item" @tap="handleItemClick(item, index)">
                     <view class="item-container">
                         <view class="item-header">
+                            <view v-if="showPurchaseBar && canPurchase(item)" class="checkbox-wrapper" @tap.stop="toggleSelect(item)">
+                                <view class="checkbox" :class="{ 'checked': isSelected(item) }">
+                                    <text v-if="isSelected(item)" class="check-icon">✓</text>
+                                </view>
+                            </view>
                             <text class="item-title">{{ getApplyCode(item) }}</text>
                             <text class="item-status" :class="'status-' + getStatus(item)">{{ getStatusText(item) }}</text>
                         </view>
@@ -110,6 +114,14 @@
             </template>
         </common-list>
     </view>
+    
+    <!-- 底部固定悬浮采购栏 -->
+    <view v-if="showPurchaseBar" class="bottom-bar">
+        <text class="select-count">已选中 {{ selectedIds.size }} 项</text>
+        <view class="purchase-btn" @tap="handlePurchase">
+            <text class="purchase-btn-text">批量采购</text>
+        </view>
+    </view>
 </template>
 
 <script setup lang="uts">
@@ -117,6 +129,7 @@
 	import { onLoad, onShow } from '@dcloudio/uni-app';
     import { getPurchaseApplyList, getPendingReceiveApplyCount } from '../../api/apply/index'
     import { getUserInfo } from '../../utils/storage'
+    import { checkPermission } from '../../utils/permission'
 
     let currentUserId: string = ''
 
@@ -132,6 +145,8 @@
     const refreshing = ref<boolean>(false)
 	const showRight = ref<boolean>(false)
     const pendingReceiveCount = ref<number>(0)
+    const selectedIds = ref<Set<string>>(new Set())
+    const showPurchaseBar = ref<boolean>(false)
 
     // 加载待领取数量
     const loadPendingReceiveCount = (): void => {
@@ -359,6 +374,78 @@
 		})
 	}
 	
+    // 判断是否可以采购(已确认状态)
+    const canPurchase = (item: any | null): boolean => {
+        if (item == null) return false
+        const jsonItem = item as UTSJSONObject
+        const status = jsonItem['status']
+        return status != null && status.toString() === 'CONFIRMED'
+    }
+    
+    // 判断是否选中
+    const isSelected = (item: any | null): boolean => {
+        if (item == null) return false
+        const jsonItem = item as UTSJSONObject
+        const applyId = jsonItem['applyId']
+        return applyId != null && selectedIds.value.has(applyId.toString())
+    }
+    
+    // 切换选中状态
+    const toggleSelect = (item: any | null): void => {
+        if (item == null) return
+        const jsonItem = item as UTSJSONObject
+        const applyId = jsonItem['applyId']
+        if (applyId == null) return
+        const idStr = applyId.toString()
+        if (selectedIds.value.has(idStr)) {
+            selectedIds.value.delete(idStr)
+        } else {
+            selectedIds.value.add(idStr)
+        }
+    }
+    
+    // 采购按钮点击
+    const handlePurchase = (): void => {
+        if (selectedIds.value.size === 0) {
+            uni.showToast({
+                title: '请先选择单据',
+                icon: 'none',
+                duration: 2000
+            })
+            return
+        }
+        
+        const hasPurchased = dataList.value.some((item: any) => {
+            const jsonItem = item as UTSJSONObject
+            const applyId = jsonItem['applyId']
+            if (applyId != null && selectedIds.value.has(applyId.toString())) {
+                const applyStatus = jsonItem['applyStatus']
+                return applyStatus != null && applyStatus.toString() === '1'
+            }
+            return false
+        })
+        
+        if (hasPurchased) {
+            uni.showModal({
+                title: '提示',
+                content: '选中的单据中存在已申购的单据,继续采购将重复申购,确定继续吗?',
+                success: (res) => {
+                    if (res.confirm) {
+                        const ids = Array.from(selectedIds.value).join(',')
+                        uni.navigateTo({
+                            url: `/pages/purchase/createByApply?applyIds=${ids}`
+                        })
+                    }
+                }
+            })
+        } else {
+            const ids = Array.from(selectedIds.value).join(',')
+            uni.navigateTo({
+                url: `/pages/purchase/createByApply?applyIds=${ids}`
+            })
+        }
+    }
+    
     // 点击列表项
     const handleItemClick = (item: any | null, index: number): void => {
         if (item == null) return
@@ -386,7 +473,6 @@
 			// 初始化
 			loadData(true)
 		}
-	// 获取页面参数,判断是否从index.uvue跳转过来
 		const pages = getCurrentPages()
 		const currentPage = pages[pages.length - 1]
 		const options = currentPage.options
@@ -395,6 +481,7 @@
 		}else{
 			showRight.value = true
 		}
+		showPurchaseBar.value = checkPermission('mes:wm:mergePurchase:add')
 	})
 </script>
 
@@ -403,6 +490,10 @@
         flex: 1;
         background-color: #e8f0f9;
     }
+    
+    .list-page.has-bottom-bar {
+        padding-bottom: 140rpx;
+    }
 
     .search-block {
         background-color: #ffffff;
@@ -551,6 +642,37 @@
         }
     }
 
+    .bottom-bar {
+        position: fixed;
+        bottom: 0;
+        left: 0;
+        right: 0;
+        background-color: #ffffff;
+        padding: 24rpx 40rpx;
+        border-top: 1rpx solid #e5e5e5;
+        flex-direction: row;
+        justify-content: space-between;
+        align-items: center;
+        box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.08);
+    }
+    
+    .select-count {
+        font-size: 28rpx;
+        color: #666666;
+    }
+    
+    .purchase-btn {
+        background-color: #1677ff;
+        padding: 20rpx 60rpx;
+        border-radius: 16rpx;
+    }
+    
+    .purchase-btn-text {
+        color: #ffffff;
+        font-size: 30rpx;
+        font-weight: bold;
+    }
+    
     .list-item {
         margin: 10rpx 20rpx;
         background-color: #ffffff;
@@ -565,6 +687,32 @@
         flex-direction: row;
         align-items: center;
         margin-bottom: 20rpx;
+        
+        .checkbox-wrapper {
+            margin-right: 16rpx;
+        }
+        
+        .checkbox {
+            width: 40rpx;
+            height: 40rpx;
+            border: 2rpx solid #d9d9d9;
+            border-radius: 6rpx;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            background-color: #ffffff;
+            
+            &.checked {
+                background-color: #1677ff;
+                border-color: #1677ff;
+            }
+        }
+        
+        .check-icon {
+            color: #ffffff;
+            font-size: 24rpx;
+            font-weight: bold;
+        }
 
         .item-title {
             flex: 1;

+ 583 - 0
pages/purchase/createByApply.uvue

@@ -0,0 +1,583 @@
+<template>
+	<uni-navbar-lite :showRight=false title="创建采购单"></uni-navbar-lite>
+    <view class="page-container">
+        <scroll-view class="page-content" scroll-y="true">
+            <view v-if="applyList.length === 0" class="empty-tip">
+                <text class="empty-tip-text">暂无申请单数据</text>
+            </view>
+            
+            <view v-else>
+                <view class="purchase-info-section">
+                    <view class="purchase-info-row">
+                        <text class="purchase-info-label">采购单名称</text>
+                        <view class="purchase-info-value-wrap">
+                            <input 
+                                class="purchase-info-input" 
+                                v-model="purchaseName"
+                                placeholder="请输入采购单名称"
+                            />
+                        </view>
+                    </view>
+                    <view class="purchase-remark-section">
+                        <text class="purchase-remark-label">采购单备注</text>
+                        <textarea 
+                            class="purchase-remark-input" 
+                            placeholder="请输入采购单备注"
+                            v-model="purchaseRemark"
+                            :maxlength="500"
+                        ></textarea>
+                    </view>
+                </view>
+
+                <view v-for="(apply, applyIndex) in applyList" :key="applyIndex" class="section">
+                    <view class="section-header">
+                        <view class="section-indicator"></view>
+                        <text class="section-title">申请单 {{ apply.applyCode }}</text>
+                    </view>
+                    <view class="info-card">
+                        <view class="info-row">
+                            <text class="info-label">申请人</text>
+                            <text class="info-value">{{ apply.nickName }}</text>
+                        </view>
+                        <view class="info-row">
+                            <text class="info-label">申请时间</text>
+                            <text class="info-value">{{ apply.createTime }}</text>
+                        </view>
+                        <view class="info-row">
+                            <text class="info-label">用途</text>
+                            <text class="info-value">{{ apply.remark }}</text>
+                        </view>
+                    </view>
+
+                    <view class="material-section">
+                        <view class="material-section-header">
+                            <text class="material-section-title">物料明细</text>
+                        </view>
+                        <view class="material-list">
+                            <view 
+                                v-for="(item, itemIndex) in apply.lineList" 
+                                :key="itemIndex" 
+                                class="material-item"
+                            >
+                                <view class="material-info">
+                                    <text class="material-name">{{ getItemName(item) }}</text>
+                                    <text class="material-spec" v-if="getSpecification(item)">规格:{{ getSpecification(item) }}</text>
+                                </view>
+                                <view class="material-detail">
+                                    <view class="detail-row">
+                                        <text class="detail-label">申请数量</text>
+                                        <text class="detail-value">{{ getQuantity(item) }} {{ getMeasureName(item) }}</text>
+                                    </view>
+                                    <view class="detail-row purchase-row">
+                                        <text class="detail-label">采购数量</text>
+                                        <view class="purchase-input-wrap">
+                                            <input 
+                                                class="purchase-input" 
+                                                type="number" 
+                                                v-model="item.purchaseQty"
+                                                @input="handlePurchaseQtyChange"
+                                            />
+                                            <text class="purchase-unit">{{ getMeasureName(item) }}</text>
+                                        </view>
+                                    </view>
+                                </view>
+                            </view>
+                        </view>
+                    </view>
+                </view>
+            </view>
+
+            <view class="bottom-space"></view>
+        </scroll-view>
+
+        <view class="bottom-buttons">
+            <button class="submit-btn" @click="handleSubmit">生成采购单</button>
+        </view>
+    </view>
+</template>
+
+<script setup lang="uts">
+    import { ref } from 'vue'
+    import { getPurchaseApplyById } from '../../api/apply/index'
+    import { createMergePurchase } from '../../api/purchase/index'
+    import { getUserInfo } from '../../utils/storage'
+
+    const applyIds = ref<string>("")
+    const idList = ref<string[]>([])
+    const applyList = ref<UTSJSONObject[]>([])
+    const loading = ref<boolean>(false)
+    const purchaseName = ref<string>("")
+    const purchaseRemark = ref<string>("")
+    const currentUserId = ref<string>("")
+
+    const getItemName = (item: UTSJSONObject): string => {
+        if (item == null) return ''
+        const val = item['itemName']
+        return val != null ? val.toString() : ''
+    }
+
+    const getSpecification = (item: UTSJSONObject): string => {
+        if (item == null) return ''
+        const val = item['specification']
+        return val != null ? val.toString() : ''
+    }
+
+    const getQuantity = (item: UTSJSONObject): string => {
+        if (item == null) return '0'
+        const val = item['quantityApply']
+        return val != null ? val.toString() : '0'
+    }
+
+    const getMeasureName = (item: UTSJSONObject): string => {
+        if (item == null) return ''
+        const val = item['measureName']
+        return val != null ? val.toString() : ''
+    }
+
+    const handlePurchaseQtyChange = (): void => {
+    }
+
+    const loadApplyDetails = (): void => {
+        if (idList.value.length === 0) return
+        loading.value = true
+        
+        const promises = idList.value.map((id) => {
+            return getPurchaseApplyById(id)
+        })
+
+        Promise.all(promises).then((responses: any[]) => {
+            const resultList: UTSJSONObject[] = []
+            responses.forEach((response: any) => {
+                const res = response as UTSJSONObject
+                const data = res["data"] as UTSJSONObject
+                const apply = new UTSJSONObject()
+                apply['applyId'] = data['applyId'] != null ? parseInt(data['applyId'].toString()) : 0
+                apply['applyCode'] = data['applyCode'] != null ? data['applyCode'].toString() : ''
+                apply['nickName'] = data['nickName'] != null ? data['nickName'].toString() : ''
+                apply['createTime'] = data['createTime'] != null ? data['createTime'].toString() : ''
+                apply['remark'] = data['remark'] != null ? data['remark'].toString() : ''
+                apply['applyUser'] = data['applyUser'] != null ? data['applyUser'].toString() : ''
+                apply['applyUserId'] = data['applyUserId'] != null ? parseInt(data['applyUserId'].toString()) : 0
+                apply['applyDeptName'] = data['applyDeptName'] != null ? data['applyDeptName'].toString() : ''
+                
+                const lines = data['wmPurchaseApplyLineList']
+                const lineList: UTSJSONObject[] = []
+                if (lines != null) {
+                    (lines as UTSJSONObject[]).forEach((line: UTSJSONObject) => {
+                        const lineItem = new UTSJSONObject()
+                        lineItem['lineId'] = line['id'] != null ? line['id'].toString() : ''
+                        lineItem['itemId'] = line['itemId'] != null ? line['itemId'].toString() : ''
+                        lineItem['itemCode'] = line['itemCode'] != null ? line['itemCode'].toString() : ''
+                        lineItem['itemName'] = line['itemName'] != null ? line['itemName'].toString() : ''
+                        lineItem['specification'] = line['specification'] != null ? line['specification'].toString() : ''
+                        lineItem['unitOfMeasure'] = line['unitOfMeasure'] != null ? line['unitOfMeasure'].toString() : ''
+                        lineItem['measureName'] = line['measureName'] != null ? line['measureName'].toString() : ''
+                        lineItem['quantityApply'] = line['quantityApply'] != null ? line['quantityApply'] : 0
+                        lineItem['purchaseQty'] = line['quantityApply'] != null ? line['quantityApply'].toString() : '0'
+                        lineList.push(lineItem)
+                    })
+                }
+                apply['lineList'] = lineList
+                resultList.push(apply)
+            })
+            applyList.value = resultList
+            loading.value = false
+            generatePurchaseName()
+        }).catch((e) => {
+            console.error('加载申请单详情失败:', e)
+            loading.value = false
+            uni.showToast({ title: '加载失败', icon: 'none' })
+        })
+    }
+
+    const generatePurchaseName = (): void => {
+        if (applyList.value.length === 0) {
+            purchaseName.value = ''
+            return
+        }
+
+        const now = new Date()
+        const year = now.getFullYear()
+        const month = String(now.getMonth() + 1).padStart(2, '0')
+        const day = String(now.getDate()).padStart(2, '0')
+        const hours = String(now.getHours()).padStart(2, '0')
+        const minutes = String(now.getMinutes()).padStart(2, '0')
+        const seconds = String(now.getSeconds()).padStart(2, '0')
+        
+        const dateStr = `${year}${month}${day}${hours}${minutes}${seconds}`
+
+        const applicants = new Set<string>()
+        applyList.value.forEach((apply) => {
+            const name = apply['nickName']
+            if (name != null && name.toString().length > 0) {
+                applicants.add(name.toString())
+            }
+        })
+
+        const applicantStr = Array.from(applicants).join(',')
+
+        purchaseName.value = `${dateStr}-${applicantStr}`
+    }
+
+    const handleSubmit = (): void => {
+        const purchaseData = new UTSJSONObject()
+        const mergedItems: UTSJSONObject[] = []
+        
+        let totalQuantity = 0
+        
+        applyList.value.forEach((apply) => {
+            const applyId = apply['applyId']
+            const applyRemark = apply['remark'] != null ? apply['remark'].toString() : ''
+            const applyUser = apply['applyUser'] != null ? apply['applyUser'].toString() : ''
+            const applyUserId = apply['applyUserId']
+            const applyDeptName = apply['applyDeptName'] != null ? apply['applyDeptName'].toString() : ''
+            const lineList = apply['lineList'] as UTSJSONObject[]
+            lineList.forEach((item) => {
+                const qty = parseInt(item['purchaseQty'])
+                if (qty > 0 && qty <= parseInt(getQuantity(item))) {
+                    const lineItem = new UTSJSONObject()
+                    lineItem['applyId'] = applyId
+                    lineItem['applyLineId'] = parseInt(item['lineId'])
+                    lineItem['itemId'] = parseInt(item['itemId'])
+                    lineItem['itemCode'] = item['itemCode'] != null ? item['itemCode'].toString() : ''
+                    lineItem['itemName'] = item['itemName'] != null ? item['itemName'].toString() : ''
+                    lineItem['specification'] = item['specification'] != null ? item['specification'].toString() : ''
+                    lineItem['unitOfMeasure'] = item['unitOfMeasure'] != null ? item['unitOfMeasure'].toString() : ''
+                    lineItem['measureName'] = item['measureName'] != null ? item['measureName'].toString() : ''
+                    lineItem['quantityApply'] = parseInt(getQuantity(item))
+                    lineItem['quantityPurchase'] = qty
+                    lineItem['applyRemark'] = applyRemark
+                    lineItem['applyUser'] = applyUser
+                    lineItem['applyUserId'] = applyUserId
+                    lineItem['applyDeptName'] = applyDeptName
+                    mergedItems.push(lineItem)
+                    totalQuantity += qty
+                }
+            })
+        })
+
+        if (mergedItems.length === 0) {
+            uni.showToast({ title: '请填写有效的采购数量', icon: 'none' })
+            return
+        }
+
+        const mergePurchase = new UTSJSONObject()
+        mergePurchase['mergeName'] = purchaseName.value
+        mergePurchase['remark'] = purchaseRemark.value
+        mergePurchase['totalQuantity'] = totalQuantity
+
+        purchaseData['mergePurchase'] = mergePurchase
+        purchaseData['applyUserId'] = currentUserId.value.length > 0 ? parseInt(currentUserId.value) : 0
+        purchaseData['applyIds'] = idList.value.map(id => parseInt(id))
+        purchaseData['mergedItems'] = mergedItems
+        purchaseData['platform'] = 'mobile'
+
+        uni.showModal({
+            title: '提示',
+            content: '确定生成采购单吗?',
+            success: (res) => {
+                if (res.confirm) {
+                    uni.showLoading({ title: '生成中...' })
+                    createMergePurchase(purchaseData).then((response: any) => {
+                        uni.hideLoading()
+                        const result = response as UTSJSONObject
+                        if (result['code'] === 200) {
+                            uni.showToast({ title: '生成成功', icon: 'success' })
+                            setTimeout(() => {
+                                uni.redirectTo({
+                                    url: '/pages/apply/index?refresh=1'
+                                })
+                            }, 1500)
+                        } else {
+                            const msg = result['msg'] != null ? result['msg'].toString() : '生成失败'
+                            uni.showToast({ title: msg, icon: 'none' })
+                        }
+                    }).catch((e) => {
+                        uni.hideLoading()
+                        const error = e as UTSError
+                        const errMsg = error?.message
+                        uni.showToast({ title: errMsg != null ? errMsg.toString() : '生成失败', icon: 'none' })
+                    })
+                }
+            }
+        })
+    }
+
+    onLoad((options: any) => {
+        const userInfo = getUserInfo()
+        if (userInfo != null) {
+            const userId = userInfo['userId']
+            currentUserId.value = userId != null ? userId.toString() : ''
+        }
+        
+        const params = options as UTSJSONObject
+        if (params != null && params['applyIds'] != null) {
+            applyIds.value = params['applyIds'].toString()
+            idList.value = applyIds.value.split(',').filter((id: string) => id.trim().length > 0)
+            loadApplyDetails()
+        }
+    })
+</script>
+
+<style lang="scss">
+    .page-container {
+        flex: 1;
+        background-color: #e8f0f9;
+    }
+
+    .page-content {
+        flex: 1;
+        padding: 20rpx;
+        padding-bottom: 140rpx;
+    }
+
+    .purchase-info-section {
+        margin-bottom: 20rpx;
+        background: #ffffff;
+        border-radius: 16rpx;
+        padding: 20rpx;
+    }
+
+    .purchase-info-row {
+        flex-direction: row;
+        justify-content: space-between;
+        align-items: center;
+        margin-bottom: 20rpx;
+    }
+
+    .purchase-info-label {
+        font-size: 28rpx;
+        color: #666666;
+    }
+
+    .purchase-info-value-wrap {
+        flex: 1;
+        margin-left: 20rpx;
+        background-color: #f8f9fa;
+        border-radius: 8rpx;
+        padding: 12rpx 16rpx;
+    }
+
+    .purchase-info-value {
+        font-size: 28rpx;
+        color: #333333;
+        font-weight: bold;
+    }
+
+    .purchase-info-input {
+        font-size: 28rpx;
+        color: #333333;
+        width: 100%;
+        background-color: transparent;
+    }
+
+    .purchase-remark-section {
+        margin-top: 16rpx;
+    }
+
+    .purchase-remark-label {
+        font-size: 28rpx;
+        color: #666666;
+        margin-bottom: 12rpx;
+        display: block;
+    }
+
+    .purchase-remark-input {
+        width: 100%;
+        min-height: 160rpx;
+        background-color: #f8f9fa;
+        border-radius: 8rpx;
+        padding: 16rpx;
+        font-size: 28rpx;
+        color: #333333;
+        box-sizing: border-box;
+    }
+
+    .section {
+        margin-bottom: 20rpx;
+        background: #ffffff;
+        border-radius: 16rpx;
+        padding: 20rpx;
+    }
+
+    .section-header {
+        flex-direction: row;
+        align-items: center;
+        margin-bottom: 20rpx;
+    }
+
+    .section-indicator {
+        width: 6rpx;
+        height: 32rpx;
+        background-color: #007aff;
+        border-radius: 3rpx;
+        margin-right: 12rpx;
+    }
+
+    .section-title {
+        font-size: 32rpx;
+        color: #333333;
+        font-weight: bold;
+    }
+
+    .info-card {
+        background-color: #f8f9fa;
+        border-radius: 8rpx;
+        padding: 20rpx;
+        margin-bottom: 20rpx;
+    }
+
+    .info-row {
+        flex-direction: row;
+        justify-content: space-between;
+        margin-bottom: 16rpx;
+
+        &:last-child {
+            margin-bottom: 0;
+        }
+    }
+
+    .info-label {
+        font-size: 28rpx;
+        color: #666666;
+    }
+
+    .info-value {
+        font-size: 28rpx;
+        color: #333333;
+    }
+
+    .material-section {
+        background-color: #f8f9fa;
+        border-radius: 8rpx;
+        padding: 20rpx;
+    }
+
+    .material-section-header {
+        margin-bottom: 16rpx;
+    }
+
+    .material-section-title {
+        font-size: 28rpx;
+        color: #333333;
+        font-weight: bold;
+    }
+
+    .material-list {
+        background-color: #ffffff;
+        border-radius: 8rpx;
+        padding: 16rpx;
+    }
+
+    .material-item {
+        padding: 16rpx 0;
+        border-bottom: 1rpx solid #f0f0f0;
+
+        &:last-child {
+            border-bottom: none;
+        }
+    }
+
+    .material-info {
+        margin-bottom: 12rpx;
+    }
+
+    .material-name {
+        font-size: 28rpx;
+        color: #333333;
+        font-weight: bold;
+        display: block;
+    }
+
+    .material-spec {
+        font-size: 24rpx;
+        color: #999999;
+        margin-top: 4rpx;
+        display: block;
+    }
+
+    .material-detail {
+        flex-direction: row;
+        flex-wrap: wrap;
+    }
+
+    .detail-row {
+        flex-direction: row;
+        width: 50%;
+        align-items: center;
+        margin-bottom: 8rpx;
+    }
+
+    .detail-label {
+        font-size: 26rpx;
+        color: #666666;
+        margin-right: 8rpx;
+    }
+
+    .detail-value {
+        font-size: 26rpx;
+        color: #ff0000;
+    }
+
+    .purchase-row {
+        justify-content: flex-end;
+    }
+
+    .purchase-input-wrap {
+        flex-direction: row;
+        align-items: center;
+        background-color: #f5f5f5;
+        border-radius: 8rpx;
+        padding: 8rpx 12rpx;
+    }
+
+    .purchase-input {
+        width: 120rpx;
+        height: 48rpx;
+        font-size: 26rpx;
+        color: #ff0000;
+        text-align: right;
+        background: transparent;
+    }
+
+    .purchase-unit {
+        font-size: 24rpx;
+        color: #666666;
+        margin-left: 8rpx;
+    }
+
+    .empty-tip {
+        align-items: center;
+        padding: 100rpx 20rpx;
+    }
+
+    .empty-tip-text {
+        color: #999999;
+        font-size: 28rpx;
+    }
+
+    .bottom-space {
+        height: 40rpx;
+    }
+
+    .bottom-buttons {
+        position: fixed;
+        bottom: 0;
+        left: 0;
+        right: 0;
+        padding: 20rpx 30rpx;
+        background-color: #ffffff;
+        border-top: 1rpx solid #e5e5e5;
+    }
+
+    .submit-btn {
+        width: 100%;
+        height: 88rpx;
+        background-color: #007aff;
+        color: #ffffff;
+        font-size: 32rpx;
+        font-weight: 600;
+        border-radius: 22rpx;
+        border: none;
+    }
+</style>

+ 13 - 0
pages/purchase/index.uvue

@@ -0,0 +1,13 @@
+<template>
+	<view>
+		
+	</view>
+</template>
+
+<script setup>
+	
+</script>
+
+<style>
+
+</style>

+ 89 - 0
utils/permission.uts

@@ -0,0 +1,89 @@
+const USER_INFO_KEY = "user_info"
+
+const ALL_PERMISSION = "*:*:*"
+
+export const checkPermission = (permission: string): boolean => {
+    try {
+        const info = uni.getStorageSync(USER_INFO_KEY)
+        if (info == null) return false
+        
+        const infoStr = info as string
+        if (infoStr.length === 0) return false
+        
+        const userObj = JSON.parse(infoStr) as UTSJSONObject
+        const permissions = userObj['permissions']
+        
+        if (permissions == null || !(permissions instanceof Array)) return false
+        
+        const permissionArray = permissions as UTSArray<string>
+        
+        if (permissionArray.includes(ALL_PERMISSION)) {
+            return true
+        }
+        
+        return permissionArray.includes(permission)
+    } catch (e) {
+        console.error('权限检查异常:', e)
+        return false
+    }
+}
+
+export const checkPermissions = (permissions: string[]): boolean => {
+    if (permissions == null || permissions.length === 0) return false
+    
+    try {
+        const info = uni.getStorageSync(USER_INFO_KEY)
+        if (info == null) return false
+        
+        const infoStr = info as string
+        if (infoStr.length === 0) return false
+        
+        const userObj = JSON.parse(infoStr) as UTSJSONObject
+        const userPermissions = userObj['permissions']
+        
+        if (userPermissions == null || !(userPermissions instanceof Array)) return false
+        
+        const permissionArray = userPermissions as UTSArray<string>
+        
+        if (permissionArray.includes(ALL_PERMISSION)) {
+            return true
+        }
+        
+        return permissions.some((perm: string) => permissionArray.includes(perm))
+    } catch (e) {
+        console.error('权限检查异常:', e)
+        return false
+    }
+}
+
+export const hasAnyPermission = (permissions: string[]): boolean => {
+    return checkPermissions(permissions)
+}
+
+export const hasAllPermissions = (permissions: string[]): boolean => {
+    if (permissions == null || permissions.length === 0) return false
+    
+    try {
+        const info = uni.getStorageSync(USER_INFO_KEY)
+        if (info == null) return false
+        
+        const infoStr = info as string
+        if (infoStr.length === 0) return false
+        
+        const userObj = JSON.parse(infoStr) as UTSJSONObject
+        const userPermissions = userObj['permissions']
+        
+        if (userPermissions == null || !(userPermissions instanceof Array)) return false
+        
+        const permissionArray = userPermissions as UTSArray<string>
+        
+        if (permissionArray.includes(ALL_PERMISSION)) {
+            return true
+        }
+        
+        return permissions.every((perm: string) => permissionArray.includes(perm))
+    } catch (e) {
+        console.error('权限检查异常:', e)
+        return false
+    }
+}

+ 2 - 2
utils/request.uts

@@ -13,10 +13,10 @@ export type RequestConfig = {
 };
 
 // 基础 URL
-// const BASE_URL = "http://192.168.2.26:8300";
+const BASE_URL = "http://192.168.2.20:8300";
 // const BASE_URL = "http://192.168.189.43:8300";
 // const BASE_URL = "http://222.243.138.146:8150/prod-api" //测试服务器;
-const BASE_URL = "http://222.243.138.146:880/prod-api" //正式服务器;
+// const BASE_URL = "http://222.243.138.146:880/prod-api" //正式服务器;
 
 /**
  * 获取基础 URL