lhl01 1 hafta önce
ebeveyn
işleme
24c8ccf5c7
2 değiştirilmiş dosya ile 500 ekleme ve 4 silme
  1. 46 0
      api/user.js
  2. 454 4
      pages/user/jxs.vue

+ 46 - 0
api/user.js

@@ -8,6 +8,52 @@ export function getJxsInfo() {
 	});
 }
 
+/**
+ * 可发货商品列表(含规格、库存)
+ * GET /api/dealer/shipment/products
+ * @description 返回自己有剩余库存的商品及规格,用于发货时选择商品、规格、数量;无请求参数
+ */
+export function getShipmentProducts() {
+	return request({
+		url: '/api/dealer/shipment/products',
+		method: 'get',
+	});
+}
+
+/**
+ * 经销商互发(发货)
+ * POST /api/dealer/shipment/submit
+ * @param {Object} data
+ * @param {number} data.to_uid - 收货方 UID(必须是自己的下级或同级经销商)
+ * @param {number} data.product_id - 商品 ID
+ * @param {string} data.attr_unique - 商品规格唯一值(取商品列表返回的 unique)
+ * @param {number} data.quantity - 发货数量(按规格数量计,必须大于 0)
+ * @param {string} [data.mark] - 备注
+ */
+export function submitShipment(data) {
+	return request({
+		url: '/api/dealer/shipment/submit',
+		method: 'post',
+		data
+	});
+}
+
+/**
+ * 经销商发货记录
+ * GET /api/dealer/shipment/records
+ * @param {Object} data
+ * @param {number} [data.type=0] - 1=我发出的,2=我收到的,0=全部
+ * @param {number} [data.page=1] - 页码
+ * @param {number} [data.limit=10] - 每页条数
+ */
+export function getShipmentRecords(data) {
+	return request({
+		url: '/api/dealer/shipment/records',
+		method: 'get',
+		data
+	});
+}
+
 // 订单统计信息
 export function orderData(data) {
 	return request({

+ 454 - 4
pages/user/jxs.vue

@@ -3,6 +3,12 @@
 		<view class="header-info">
 			进货总金额:{{jxsInfo.total_purchase}}
 		</view>
+		<view class="ship-entry" @click="openShip">
+			发货供应商
+		</view>
+		<view class="ship-entry" @click="openShipRecord">
+			发货记录
+		</view>
 		<view class="stock-list">
 			<view class="stock-card" v-for="item in jxsInfo.stock_list" @click="goItem(item)">
 				<view class="product-image-wrapper">
@@ -29,12 +35,94 @@
 				<view class="code-close" @click="closeCode">关闭</view>
 			</view>
 		</uni-popup>
+		<uni-popup type="center" ref="ship">
+			<view class="ship-popup">
+				<view class="ship-title">发货供应商</view>
+				<view class="ship-form">
+					<view class="ship-item">
+						<view class="ship-label">供应商 UID</view>
+						<input class="ship-input" type="number" v-model="shipForm.to_uid" placeholder="请输入收货方 UID" />
+					</view>
+					<view class="ship-item">
+						<view class="ship-label">商品</view>
+						<picker mode="selector" :range="productNames" :value="productIndex" @change="onProductChange">
+							<view class="ship-picker" :class="{ placeholder: productIndex < 0 }">
+								{{ productIndex >= 0 ? productNames[productIndex] : '请选择商品' }}
+							</view>
+						</picker>
+					</view>
+					<view class="ship-item">
+						<view class="ship-label">规格</view>
+						<picker mode="selector" :range="skuNames" :value="skuIndex" :disabled="productIndex < 0" @change="onSkuChange">
+							<view class="ship-picker" :class="{ placeholder: skuIndex < 0 }">
+								{{ skuIndex >= 0 ? skuNames[skuIndex] : '请选择规格' }}
+							</view>
+						</picker>
+						<view class="ship-tip" v-if="selectedProduct">剩余库存(基础单位):{{ selectedProduct.remaining_quantity }}</view>
+					</view>
+					<view class="ship-item">
+						<view class="ship-label">发货数量</view>
+						<input class="ship-input" type="number" v-model="shipForm.quantity" placeholder="请输入发货数量" />
+					</view>
+					<view class="ship-item">
+						<view class="ship-label">备注(选填)</view>
+						<input class="ship-input" type="text" v-model="shipForm.mark" placeholder="请输入备注" />
+					</view>
+				</view>
+				<view class="ship-actions">
+					<view class="ship-cancel" @click="closeShip">取消</view>
+					<view class="ship-submit" @click="submitShip">确认发货</view>
+				</view>
+			</view>
+		</uni-popup>
+		<uni-popup type="bottom" ref="shipRecord">
+			<view class="record-popup">
+				<view class="record-header">
+					<view class="ship-title">发货记录</view>
+					<view class="record-close" @click="closeShipRecord">关闭</view>
+				</view>
+				<view class="record-tabs">
+					<view
+						class="record-tab"
+						:class="{ active: recordType === item.value }"
+						v-for="item in recordTabs"
+						:key="item.value"
+						@click="changeRecordType(item.value)"
+					>{{ item.label }}</view>
+				</view>
+				<scroll-view
+					class="record-list"
+					scroll-y
+					@scrolltolower="loadMoreRecords"
+				>
+					<view class="record-empty" v-if="!recordList.length && !recordLoading">暂无发货记录</view>
+					<view class="record-card" v-for="item in recordList" :key="item.id">
+						<view class="record-card-top">
+							<view class="record-name">{{ item.product_name }}</view>
+							<view class="record-direction" :class="item.direction === '发出' ? 'out' : 'in'">{{ item.direction }}</view>
+						</view>
+						<view class="record-row">发货方:{{ item.from_nickname || '平台' }}</view>
+						<view class="record-row">收货方:{{ item.to_nickname }}</view>
+						<view class="record-row">数量:{{ item.quantity }}(基础单位 {{ item.base_quantity }})</view>
+						<view class="record-row" v-if="item.diff_amount && Number(item.diff_amount) > 0">差价佣金:¥{{ item.diff_amount }}</view>
+						<view class="record-row" v-if="item.mark">备注:{{ item.mark }}</view>
+						<view class="record-time">{{ item.add_time }}</view>
+					</view>
+					<view class="record-status" v-if="recordList.length">
+						{{ recordFinished ? '没有更多了' : (recordLoading ? '加载中...' : '上拉加载更多') }}
+					</view>
+				</scroll-view>
+			</view>
+		</uni-popup>
 	</view>
 </template>
 
 <script>
 	import {
-		getJxsInfo
+		getJxsInfo,
+		getShipmentProducts,
+		submitShipment,
+		getShipmentRecords
 	} from '@/api/user.js'
 	import {
 		mapState
@@ -49,12 +137,48 @@
 					dealer_name: '',
 					stock_list: [],
 					total_purchase: ''
-				}
+				},
+				productList: [],
+				productIndex: -1,
+				skuIndex: -1,
+				shipForm: {
+					to_uid: '',
+					quantity: '',
+					mark: ''
+				},
+				submitting: false,
+				recordTabs: [
+					{ label: '全部', value: 0 },
+					{ label: '我发出的', value: 1 },
+					{ label: '我收到的', value: 2 }
+				],
+				recordType: 0,
+				recordList: [],
+				recordPage: 1,
+				recordLimit: 10,
+				recordCount: 0,
+				recordLoading: false,
+				recordFinished: false
 			}
 		},
 		computed: {
 			...mapState(['baseURL']),
-			...mapState('user', ['userInfo'])
+			...mapState('user', ['userInfo']),
+			productNames() {
+				return this.productList.map(item => item.product_name)
+			},
+			selectedProduct() {
+				return this.productIndex >= 0 ? this.productList[this.productIndex] : null
+			},
+			skuList() {
+				return this.selectedProduct ? (this.selectedProduct.skus || []) : []
+			},
+			skuNames() {
+				return this.skuList.map(item => `${item.suk}(×${item.unit_quantity})`)
+			},
+			selectedSku() {
+				return this.skuIndex >= 0 ? this.skuList[this.skuIndex] : null
+			}
 		},
 		methods: {
 			goItem(item) {
@@ -74,6 +198,124 @@
 			},
 			closeCode() {
 				this.$refs.code.close()
+			},
+			resetShipForm() {
+				this.productIndex = -1
+				this.skuIndex = -1
+				this.shipForm = {
+					to_uid: '',
+					quantity: '',
+					mark: ''
+				}
+			},
+			openShip() {
+				this.resetShipForm()
+				getShipmentProducts().then(res => {
+					this.productList = res.data || []
+					if (!this.productList.length) {
+						uni.showToast({
+							title: '暂无可发货商品',
+							icon: 'none'
+						})
+						return
+					}
+					this.$refs.ship.open()
+				})
+			},
+			closeShip() {
+				this.$refs.ship.close()
+			},
+			onProductChange(e) {
+				this.productIndex = Number(e.detail.value)
+				this.skuIndex = -1
+			},
+			onSkuChange(e) {
+				this.skuIndex = Number(e.detail.value)
+			},
+			submitShip() {
+				const toUid = String(this.shipForm.to_uid || '').trim()
+				const quantity = Number(this.shipForm.quantity)
+				if (!toUid) {
+					uni.showToast({ title: '请输入供应商 UID', icon: 'none' })
+					return
+				}
+				if (!this.selectedProduct) {
+					uni.showToast({ title: '请选择商品', icon: 'none' })
+					return
+				}
+				if (!this.selectedSku) {
+					uni.showToast({ title: '请选择规格', icon: 'none' })
+					return
+				}
+				if (!quantity || quantity <= 0) {
+					uni.showToast({ title: '发货数量必须大于0', icon: 'none' })
+					return
+				}
+				if (this.submitting) return
+				this.submitting = true
+				const data = {
+					to_uid: Number(toUid),
+					product_id: this.selectedProduct.product_id,
+					attr_unique: this.selectedSku.unique,
+					quantity: quantity
+				}
+				const mark = String(this.shipForm.mark || '').trim()
+				if (mark) data.mark = mark
+				submitShipment(data).then(res => {
+					uni.showToast({
+						title: res.msg || '发货成功',
+						icon: 'success'
+					})
+					this.closeShip()
+					this.getJxsInfo()
+				}).finally(() => {
+					this.submitting = false
+				})
+			},
+			openShipRecord() {
+				this.recordType = 0
+				this.resetRecordList()
+				this.$refs.shipRecord.open()
+				this.getShipRecords()
+			},
+			closeShipRecord() {
+				this.$refs.shipRecord.close()
+			},
+			resetRecordList() {
+				this.recordPage = 1
+				this.recordList = []
+				this.recordCount = 0
+				this.recordFinished = false
+			},
+			changeRecordType(type) {
+				if (this.recordType === type) return
+				this.recordType = type
+				this.resetRecordList()
+				this.getShipRecords()
+			},
+			loadMoreRecords() {
+				if (this.recordLoading || this.recordFinished) return
+				this.recordPage += 1
+				this.getShipRecords()
+			},
+			getShipRecords() {
+				if (this.recordLoading) return
+				this.recordLoading = true
+				getShipmentRecords({
+					type: this.recordType,
+					page: this.recordPage,
+					limit: this.recordLimit
+				}).then(res => {
+					const data = res.data || {}
+					const list = data.list || []
+					this.recordCount = data.count || 0
+					this.recordList = this.recordPage === 1 ? list : this.recordList.concat(list)
+					this.recordFinished = this.recordList.length >= this.recordCount || list.length < this.recordLimit
+				}).catch(() => {
+					if (this.recordPage > 1) this.recordPage -= 1
+				}).finally(() => {
+					this.recordLoading = false
+				})
 			}
 		},
 		onLoad() {
@@ -98,6 +340,17 @@
 		margin-bottom: 20rpx;
 	}
 
+	.ship-entry {
+		background-color: #fff;
+		padding: 28rpx 30rpx;
+		border-radius: 16rpx;
+		font-size: 30rpx;
+		color: #FF6B6B;
+		font-weight: 500;
+		margin-bottom: 20rpx;
+		text-align: center;
+	}
+
 	.stock-list {
 		display: flex;
 		flex-wrap: wrap;
@@ -194,4 +447,201 @@
 		border: 1rpx solid #e5e5e5;
 		border-radius: 40rpx;
 	}
-</style>
+
+	.ship-popup {
+		background-color: #ffffff;
+		border-radius: 24rpx;
+		padding: 40rpx 36rpx;
+		width: 620rpx;
+		box-sizing: border-box;
+		box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.15);
+	}
+
+	.ship-title {
+		font-size: 34rpx;
+		color: #333;
+		font-weight: 600;
+		text-align: center;
+		margin-bottom: 30rpx;
+	}
+
+	.ship-form {
+		display: flex;
+		flex-direction: column;
+		gap: 24rpx;
+	}
+
+	.ship-item {
+		display: flex;
+		flex-direction: column;
+		gap: 12rpx;
+	}
+
+	.ship-label {
+		font-size: 26rpx;
+		color: #666;
+	}
+
+	.ship-input,
+	.ship-picker {
+		height: 76rpx;
+		line-height: 76rpx;
+		padding: 0 24rpx;
+		background-color: #f7f7f7;
+		border-radius: 12rpx;
+		font-size: 28rpx;
+		color: #333;
+		box-sizing: border-box;
+	}
+
+	.ship-picker.placeholder {
+		color: #999;
+	}
+
+	.ship-tip {
+		font-size: 22rpx;
+		color: #999;
+	}
+
+	.ship-actions {
+		display: flex;
+		gap: 20rpx;
+		margin-top: 40rpx;
+	}
+
+	.ship-cancel,
+	.ship-submit {
+		flex: 1;
+		height: 80rpx;
+		line-height: 80rpx;
+		text-align: center;
+		border-radius: 40rpx;
+		font-size: 28rpx;
+	}
+
+	.ship-cancel {
+		color: #999;
+		border: 1rpx solid #e5e5e5;
+		box-sizing: border-box;
+	}
+
+	.ship-submit {
+		color: #fff;
+		background-color: #FF6B6B;
+		font-weight: 500;
+	}
+
+	.record-popup {
+		background-color: #fff;
+		border-radius: 24rpx 24rpx 0 0;
+		padding: 30rpx 24rpx calc(30rpx + env(safe-area-inset-bottom));
+		height: 75vh;
+		box-sizing: border-box;
+		display: flex;
+		flex-direction: column;
+	}
+
+	.record-header {
+		position: relative;
+		margin-bottom: 10rpx;
+	}
+
+	.record-header .ship-title {
+		margin-bottom: 0;
+	}
+
+	.record-close {
+		position: absolute;
+		right: 0;
+		top: 0;
+		font-size: 26rpx;
+		color: #999;
+		padding: 8rpx 0 8rpx 20rpx;
+	}
+
+	.record-tabs {
+		display: flex;
+		gap: 16rpx;
+		margin: 24rpx 0;
+	}
+
+	.record-tab {
+		flex: 1;
+		height: 64rpx;
+		line-height: 64rpx;
+		text-align: center;
+		font-size: 26rpx;
+		color: #666;
+		background-color: #f7f7f7;
+		border-radius: 32rpx;
+	}
+
+	.record-tab.active {
+		color: #fff;
+		background-color: #FF6B6B;
+	}
+
+	.record-list {
+		flex: 1;
+		height: 0;
+	}
+
+	.record-empty,
+	.record-status {
+		text-align: center;
+		font-size: 24rpx;
+		color: #999;
+		padding: 40rpx 0;
+	}
+
+	.record-card {
+		background-color: #f7f7f7;
+		border-radius: 16rpx;
+		padding: 24rpx;
+		margin-bottom: 16rpx;
+	}
+
+	.record-card-top {
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+		gap: 16rpx;
+		margin-bottom: 12rpx;
+	}
+
+	.record-name {
+		flex: 1;
+		font-size: 28rpx;
+		color: #333;
+		font-weight: 500;
+	}
+
+	.record-direction {
+		font-size: 22rpx;
+		padding: 4rpx 14rpx;
+		border-radius: 20rpx;
+		flex-shrink: 0;
+	}
+
+	.record-direction.out {
+		color: #FF6B6B;
+		background-color: rgba(255, 107, 107, 0.12);
+	}
+
+	.record-direction.in {
+		color: #2ecc71;
+		background-color: rgba(46, 204, 113, 0.12);
+	}
+
+	.record-row {
+		font-size: 24rpx;
+		color: #666;
+		line-height: 1.7;
+	}
+
+	.record-time {
+		font-size: 22rpx;
+		color: #999;
+		margin-top: 10rpx;
+	}
+</style>