| | |
| | | stockProductSales, |
| | | stockReturnIssue, |
| | | stockReturnSales, |
| | | submitProductSales, |
| | | submitSalesNotice, |
| | | type WlsDocBase, |
| | | } from "../../../api/wls"; |
| | | import { generateSalesNoticeFromSaleOrder, getSaleOrderPage, type SaleOrder } from "../../../api/sale"; |
| | | import { useAuth } from "../../../stores/auth"; |
| | | import { dateText, dayText, dictLabel, documentStatusClass } from "./shared"; |
| | | |
| | |
| | | uni.navigateTo({ url: `/pages/wls/product-sales/form?id=${id}` }); |
| | | }); |
| | | } |
| | | /** 列表行内提交:发货通知 草稿(0)→待出库(3);销售出库 草稿(0)→待拣货(2) */ |
| | | function submitDoc(row: WlsDocBase) { |
| | | const token = state.accessToken; |
| | | if (!token || !row.id) return; |
| | | const label = props.kind === "sales-notice" ? "发货通知单" : "销售出库单"; |
| | | confirmAction("确认提交", `确认提交${label} ${row.code || ""} 吗?提交后才能继续后续流程`, async () => { |
| | | if (props.kind === "sales-notice") return submitSalesNotice(token, row.id!); |
| | | return submitProductSales(token, row.id!); |
| | | }); |
| | | } |
| | | // 发货通知「从销售订单生成」 |
| | | const orderPickerOpen = ref(false); |
| | | const orderRows = ref<SaleOrder[]>([]); |
| | | const orderKeyword = ref(""); |
| | | const orderPageNo = ref(1); |
| | | const orderTotal = ref(0); |
| | | const orderLoading = ref(false); |
| | | const generatingId = ref<number>(); |
| | | const orderHasMore = computed(() => orderRows.value.length < orderTotal.value); |
| | | async function loadOrders(reset = false) { |
| | | if (!state.accessToken || orderLoading.value) return; |
| | | if (reset) { orderPageNo.value = 1; orderRows.value = []; orderTotal.value = 0; } |
| | | orderLoading.value = true; |
| | | try { |
| | | const data = await getSaleOrderPage(state.accessToken, { |
| | | pageNo: orderPageNo.value, |
| | | pageSize: 15, |
| | | no: orderKeyword.value.trim() || undefined, |
| | | status: 20, |
| | | }); |
| | | const list = (data.list || []).filter(item => item.canCreateSalesNotice !== false); |
| | | orderRows.value = reset ? list : [...orderRows.value, ...list]; |
| | | orderTotal.value = Number(data.total || 0); |
| | | orderPageNo.value += 1; |
| | | } catch (error) { uni.showToast({ title: errorText(error), icon: "none" }); } |
| | | finally { orderLoading.value = false; } |
| | | } |
| | | function openOrderPicker() { |
| | | orderPickerOpen.value = true; |
| | | loadOrders(true); |
| | | } |
| | | function closeOrderPicker() { |
| | | if (generatingId.value) return; |
| | | orderPickerOpen.value = false; |
| | | } |
| | | async function pickOrder(order: SaleOrder) { |
| | | if (!state.accessToken || !order.id || generatingId.value) return; |
| | | const ok = await new Promise<boolean>(resolve => uni.showModal({ |
| | | title: "确认生成", |
| | | content: `确认根据销售订单 ${order.no || order.id} 生成发货通知单?`, |
| | | success: r => resolve(r.confirm), |
| | | })); |
| | | if (!ok) return; |
| | | generatingId.value = order.id; |
| | | try { |
| | | await generateSalesNoticeFromSaleOrder(state.accessToken, order.id); |
| | | uni.showToast({ title: "生成成功", icon: "success" }); |
| | | orderPickerOpen.value = false; |
| | | load(true); |
| | | } catch (error) { |
| | | uni.showToast({ title: errorText(error), icon: "none" }); |
| | | } finally { |
| | | generatingId.value = undefined; |
| | | } |
| | | } |
| | | function qcTip() { uni.showToast({ title: "请前往质量管理执行质检", icon: "none" }); } |
| | | function stockText() { |
| | | if (props.kind === "product-receipt" || props.kind === "return-issue" || props.kind === "return-sales") return "执行上架"; |
| | |
| | | <view class="page"> |
| | | <template v-if="isLoggedIn"> |
| | | <view v-if="canQuery" class="search"><input v-model="keyword" :placeholder="`搜索${config.code}`" confirm-type="search" @confirm="load(true)" /><text v-if="keyword" @click="keyword='';load(true)">清除</text></view> |
| | | <view v-if="canQuery && kind === 'sales-notice' && canCreate" class="top-actions"><button @click="openOrderPicker">从销售订单生成</button></view> |
| | | <scroll-view v-if="canQuery" class="content" scroll-y lower-threshold="80" @scrolltolower="hasMore && load()"> |
| | | <view v-if="loadError" class="state error" @click="load(true)">{{ loadError }},点击重试</view> |
| | | <view v-else-if="rows.length"> |
| | |
| | | <view class="actions" @click.stop> |
| | | <button v-if="canQuery" @click="go(row,'detail')">详情</button> |
| | | <button v-if="canUpdate && isDraft(row)" @click="go(row)">编辑</button> |
| | | <button v-if="canUpdate && isDraft(row) && (kind === 'sales-notice' || kind === 'product-sales')" @click="submitDoc(row)">提交</button> |
| | | <button v-if="canDelete && isDraft(row)" class="danger" @click="remove(row)">删除</button> |
| | | <button v-if="(kind === 'return-issue' || kind === 'return-sales') && isConfirmed(row)" @click="qcTip">执行质检</button> |
| | | <button v-if="kind === 'sales-notice' && canGenerateSales && isApproved(row)" @click="generateSales(row)">生成出库单</button> |
| | |
| | | <view v-else class="state">暂无{{ config.title }}查看权限</view> |
| | | </template> |
| | | <view v-else class="state">登录后查看{{ config.title }}</view> |
| | | <button v-if="canCreate" class="fab" @click="go()"><view /></button> |
| | | <!-- 发货通知只能从销售订单生成,不提供手工新建入口 --> |
| | | <button v-if="canCreate && kind !== 'sales-notice'" class="fab" @click="go()"><view /></button> |
| | | <!-- 从销售订单生成发货通知 --> |
| | | <view v-if="orderPickerOpen" class="picker-mask" @click="closeOrderPicker"> |
| | | <view class="picker-panel" @click.stop> |
| | | <view class="picker-head"> |
| | | <text class="picker-title">选择销售订单生成发货通知</text> |
| | | <text class="picker-close" @click="closeOrderPicker">×</text> |
| | | </view> |
| | | <view class="picker-search"> |
| | | <input v-model="orderKeyword" placeholder="搜索订单号" confirm-type="search" @confirm="loadOrders(true)" /> |
| | | <text @click="loadOrders(true)">搜索</text> |
| | | </view> |
| | | <scroll-view class="picker-list" scroll-y lower-threshold="80" @scrolltolower="orderHasMore && loadOrders()"> |
| | | <view v-if="orderRows.length"> |
| | | <view v-for="order in orderRows" :key="order.id" class="order-card" @click="pickOrder(order)"> |
| | | <view class="order-head"> |
| | | <text class="order-no">{{ order.no || '--' }}</text> |
| | | <text class="order-pick">{{ generatingId === order.id ? '生成中...' : '选择' }}</text> |
| | | </view> |
| | | <text class="order-customer">{{ order.customerName || '--' }}</text> |
| | | <view class="order-grid"> |
| | | <text>产品:{{ order.productNames || '—' }}</text> |
| | | <text>总数量:{{ order.totalCount ?? '—' }}</text> |
| | | <text>订单时间:{{ order.orderTime?.replace('T', ' ').slice(0, 16) || '—' }}</text> |
| | | </view> |
| | | </view> |
| | | <text class="more">{{ orderLoading ? '加载中...' : orderHasMore ? '上拉加载更多' : '没有更多了' }}</text> |
| | | </view> |
| | | <view v-else class="state">{{ orderLoading ? '加载中...' : '暂无可生成发货通知的已审核订单' }}</view> |
| | | </scroll-view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <style lang="scss" scoped> |
| | | .page{height:100vh;display:flex;flex-direction:column;padding:0 30rpx;box-sizing:border-box;overflow:hidden;background:#f3f6fb}.search{display:flex;align-items:center;height:80rpx;margin:20rpx 0 16rpx;padding:0 20rpx;background:#fff;border-radius:16rpx;box-shadow:0 8rpx 20rpx rgba(43,69,107,.06)}.search input{flex:1;font-size:27rpx}.search text{color:var(--color-primary, #1769d8);font-size:24rpx}.content{height:0;flex:1;padding-bottom: 132rpx;box-sizing:border-box}.row{padding:24rpx 22rpx 20rpx;margin-bottom:18rpx;background:#fff;border-radius:16rpx;box-shadow:0 10rpx 24rpx rgba(43,69,107,.08)}.head{display:flex;align-items:flex-start;justify-content:space-between;gap:16rpx}.head>view:first-child{display:flex;min-width:0;flex:1;flex-direction:column;gap:8rpx}.head>view:first-child text:last-child{color:#78879b;font-size:23rpx}.name{overflow:hidden;color:#20334c;font-size:30rpx;font-weight:600;text-overflow:ellipsis;white-space:nowrap}.status-wrap{display:flex;align-items:flex-start;justify-content:center;flex:none;min-height:44rpx}.status{display:inline-flex;align-items:center;justify-content:center;height:44rpx;padding:0 18rpx;font-size:23rpx;line-height:44rpx;border-radius:999rpx;white-space:nowrap}.tag-draft{color:#6f7e92;background:#f3f6fa;border:1rpx solid #dce4ee}.tag-progress{color:#b57800;background:#fff7dd;border:1rpx solid #f3dc98}.tag-approved{color:var(--color-primary, #1769d8);background:var(--color-primary-soft, #edf5ff);border:1rpx solid var(--color-primary-border, #bfdbff)}.tag-finished{color:#17865a;background:#eafaf2;border:1rpx solid #b8efd5}.tag-cancelled{color:#c65050;background:#fff7f7;border:1rpx solid #e3a0a0}.tag-info{color:#66778c;background:#f6f8fb;border:1rpx solid #dce4ee}.grid{display:grid;gap:10rpx;padding:16rpx 0;margin-top:16rpx;color:#66778c;font-size:23rpx;border-top:1rpx solid #edf1f6;border-bottom:1rpx solid #edf1f6}.grid text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:12rpx;padding-top:16rpx}.actions button{min-height:50rpx;padding:0 16rpx;margin:0;color:var(--color-primary, #1769d8);font-size:22rpx;line-height:48rpx;background:var(--color-primary-soft, #f5f8ff);border:1rpx solid var(--color-primary-border, #8ab3ee);border-radius:8rpx}.actions .danger{color:#c65050;background:#fff7f7;border-color:#e3a0a0}.actions button::after,.fab::after{border:0}.more,.state{display:block;padding:35rpx;color:#8795a8;font-size:26rpx;text-align:center}.error{color:#c3654d}.fab{position:fixed;right:30rpx;bottom:calc(32rpx + env(safe-area-inset-bottom));display:flex;align-items:center;justify-content:center;width: 104rpx; height: 104rpx; min-height: 104rpx;padding:0;margin:0;background:var(--color-primary, #1769d8);border-radius:50%;box-shadow:0 12rpx 24rpx rgba(23,105,216,.3)}.fab view{width:38rpx;height:38rpx;background:linear-gradient(#fff,#fff) center/38rpx 8rpx no-repeat,linear-gradient(#fff,#fff) center/8rpx 38rpx no-repeat}.fab view::before,.fab view::after{display:none;content:none} |
| | | .top-actions{display:flex;justify-content:flex-end;flex:none;margin-bottom:14rpx} |
| | | .top-actions button{min-height:56rpx;padding:0 24rpx;margin:0;color:#fff;font-size:24rpx;line-height:54rpx;background:var(--color-primary, #1769d8);border-radius:8rpx} |
| | | .top-actions button::after{border:0} |
| | | .picker-mask{position:fixed;top:0;right:0;bottom:0;left:0;z-index:999;background:rgba(0,0,0,.45)} |
| | | .picker-panel{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;flex-direction:column;background:#f3f6fb} |
| | | .picker-head{display:flex;align-items:center;justify-content:space-between;flex:none;padding:28rpx 30rpx;background:#fff} |
| | | .picker-title{font-size:30rpx;font-weight:600;color:#20334c} |
| | | .picker-close{color:#8a97a8;font-size:48rpx;line-height:1} |
| | | .picker-search{display:flex;align-items:center;flex:none;height:80rpx;margin:20rpx 30rpx 12rpx;padding:0 20rpx;background:#fff;border-radius:16rpx} |
| | | .picker-search input{flex:1;font-size:27rpx} |
| | | .picker-search text{color:var(--color-primary, #1769d8);font-size:24rpx} |
| | | .picker-list{flex:1;height:0;padding:0 30rpx 30rpx;box-sizing:border-box} |
| | | .order-card{padding:22rpx;margin-bottom:16rpx;background:#fff;border-radius:16rpx;box-shadow:0 8rpx 20rpx rgba(43,69,107,.06)} |
| | | .order-head{display:flex;align-items:center;justify-content:space-between;gap:16rpx} |
| | | .order-no{overflow:hidden;font-size:28rpx;font-weight:600;color:#20334c;text-overflow:ellipsis;white-space:nowrap} |
| | | .order-pick{flex:none;padding:6rpx 20rpx;color:#fff;font-size:22rpx;background:var(--color-primary, #1769d8);border-radius:999rpx} |
| | | .order-customer{display:block;margin-top:10rpx;color:#20334c;font-size:25rpx} |
| | | .order-grid{display:grid;grid-template-columns:1fr;gap:8rpx;padding-top:12rpx;margin-top:12rpx;color:#78879b;font-size:22rpx;border-top:1rpx solid #edf1f6} |
| | | </style> |