fix(miniprogram): wire db pages to capsule safe-area header
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
b3fc308446
commit
8505115f05
9 files changed
+1666
-863
No files matched your search
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 地磅业务 API(对齐 mobile-h5)
|
||||
*/
|
||||
import db from '@/utils/dbRequest'
|
||||
|
||||
export const dashboardApi = {
|
||||
getInboundByCargo: () => db.get('/dashboard/inbound/by-cargo'),
|
||||
getInboundByPartnerCargo: () => db.get('/dashboard/inbound/by-partner-cargo'),
|
||||
getOutboundByCargo: () => db.get('/dashboard/outbound/by-cargo'),
|
||||
getOutboundByPartnerCargo: () => db.get('/dashboard/outbound/by-partner-cargo')
|
||||
}
|
||||
|
||||
export const planApi = {
|
||||
list: (params) => db.get('/plans', params),
|
||||
get: (id) => db.get(`/plans/${id}`),
|
||||
getRelated: (id) => db.get(`/plans/${id}/related`),
|
||||
create: (payload) => db.post('/plans', payload),
|
||||
update: (id, payload) => db.patch(`/plans/${id}`, payload),
|
||||
delete: (id) => db.del(`/plans/${id}`)
|
||||
}
|
||||
|
||||
export const inventoryApi = {
|
||||
list: (params) => db.get('/inventory', params),
|
||||
get: (id) => db.get(`/inventory/${id}`),
|
||||
getRelated: (id) => db.get(`/inventory/${id}/related`),
|
||||
create: (payload) => db.post('/inventory', payload),
|
||||
update: (id, payload) => db.patch(`/inventory/${id}`, payload),
|
||||
delete: (id) => db.del(`/inventory/${id}`),
|
||||
createLinkForPlan: (purchasePlanId, remarks) =>
|
||||
db.post('/inventory/create-link-for-plan', { purchasePlanId, remarks })
|
||||
}
|
||||
|
||||
export const weighingApi = {
|
||||
list: (params) => db.get('/weighing', params),
|
||||
get: (id) => db.get(`/weighing/${id}`),
|
||||
getRelated: (id) => db.get(`/weighing/${id}/related`),
|
||||
create: (payload) => db.post('/weighing', payload),
|
||||
update: (id, payload) => db.patch(`/weighing/${id}`, payload),
|
||||
void: (id) => db.post(`/weighing/${id}/void`),
|
||||
delete: (id) => db.del(`/weighing/${id}`)
|
||||
}
|
||||
@@ -1,134 +1,152 @@
|
||||
<template>
|
||||
<view class="db-page">
|
||||
<view class="db-header" :style="safeAreaStyle">
|
||||
<view class="db-header__nav">
|
||||
<view class="db-header__left">
|
||||
<text class="db-header__back" @click="goWorkbench">←</text>
|
||||
<text class="db-header__title">仪表盘</text>
|
||||
</view>
|
||||
<text class="db-header__user">{{ username }}</text>
|
||||
<view />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="db-menu-grid">
|
||||
<view class="db-menu-item" @click="go(PAGE_PATHS.dbPlansList)">
|
||||
<view class="db-menu-icon">📋</view>
|
||||
<view class="db-menu-label">计划管理</view>
|
||||
</view>
|
||||
<view class="db-menu-item" @click="go(PAGE_PATHS.dbInventoryList)">
|
||||
<view class="db-menu-icon">📦</view>
|
||||
<view class="db-menu-label">出入库管理</view>
|
||||
</view>
|
||||
<view class="db-menu-item" @click="go(PAGE_PATHS.dbWeighingList)">
|
||||
<view class="db-menu-icon">⚖️</view>
|
||||
<view class="db-menu-label">地磅单管理</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="db-stat-grid">
|
||||
<view class="db-stat-card">
|
||||
<view class="db-stat-title">今日入库</view>
|
||||
<view class="db-stat-value">
|
||||
{{ inboundCount }}<text class="db-stat-suffix">车</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="db-stat-card">
|
||||
<view class="db-stat-title">今日出库</view>
|
||||
<view class="db-stat-value">
|
||||
{{ outboundCount }}<text class="db-stat-suffix">车</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-for="block in chartBlocks" :key="block.title" class="db-chart-block">
|
||||
<view class="db-chart-title">{{ block.title }}</view>
|
||||
<view v-if="!block.rows.length" class="db-empty" style="padding: 16px">暂无数据</view>
|
||||
<view v-for="row in block.rows" :key="row.label" class="db-bar-row">
|
||||
<view class="db-bar-label">{{ row.label }}</view>
|
||||
<view class="db-bar-track">
|
||||
<view class="db-bar-planned" :style="{ width: barPct(row.planned, block.max) }" />
|
||||
<view class="db-bar-actual" :style="{ width: barPct(row.actual, block.max) }" />
|
||||
</view>
|
||||
<view class="db-bar-meta">计划 {{ fmt(row.planned) }} · 实绩 {{ fmt(row.actual) }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import {
|
||||
getDashboardStats,
|
||||
listWeighing,
|
||||
getInboundByCargo,
|
||||
getOutboundByCargo
|
||||
} from '@/api/db'
|
||||
import { weighingPhase, todayYmd, dash, formatWeight } from '@/utils/dbLabels'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { PAGE_PATHS } from '@/config/api'
|
||||
import { dashboardApi } from '@/pages/db/api/db'
|
||||
import { getSafeAreaStyle } from '@/utils/safeArea'
|
||||
|
||||
const stats = ref({
|
||||
todayInbound: 0,
|
||||
todayOutbound: 0,
|
||||
todayWeighing: 0,
|
||||
needSecond: 0
|
||||
})
|
||||
const topCargo = ref([])
|
||||
const loading = ref(false)
|
||||
const safeAreaStyle = getSafeAreaStyle()
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack({
|
||||
fail: () => uni.switchTab({ url: PAGE_PATHS.workbench })
|
||||
const userStore = useUserStore()
|
||||
const inboundByCargo = ref([])
|
||||
const outboundByCargo = ref([])
|
||||
const inboundByPartner = ref([])
|
||||
const outboundByPartner = ref([])
|
||||
|
||||
const username = computed(() => userStore.userInfo?.username || userStore.userInfo?.realName || '用户')
|
||||
|
||||
const inboundCount = computed(() => {
|
||||
const set = new Set()
|
||||
inboundByCargo.value.forEach((i) => {
|
||||
if (Number(i.planned) > 0) set.add(i.label)
|
||||
})
|
||||
return set.size
|
||||
})
|
||||
|
||||
const outboundCount = computed(() => {
|
||||
const set = new Set()
|
||||
outboundByCargo.value.forEach((i) => {
|
||||
if (Number(i.planned) > 0) set.add(i.label)
|
||||
})
|
||||
return set.size
|
||||
})
|
||||
|
||||
const chartBlocks = computed(() => [
|
||||
pack('今日入库量(按货物,t)', inboundByCargo.value),
|
||||
pack('今日出库量(按货物,t)', outboundByCargo.value),
|
||||
pack('今日入库量(按往来单位+货物,t)', inboundByPartner.value),
|
||||
pack('今日出库量(按往来单位+货物,t)', outboundByPartner.value)
|
||||
])
|
||||
|
||||
function pack(title, rows) {
|
||||
const list = Array.isArray(rows) ? rows : []
|
||||
const max = Math.max(1, ...list.map((r) => Math.max(Number(r.planned) || 0, Number(r.actual) || 0)))
|
||||
return { title, rows: list, max }
|
||||
}
|
||||
|
||||
const goWeighing = () => uni.navigateTo({ url: PAGE_PATHS.dbWeighingList })
|
||||
const goPlans = () => uni.navigateTo({ url: PAGE_PATHS.dbPlansList })
|
||||
const goInventory = () => uni.navigateTo({ url: PAGE_PATHS.dbInventoryList })
|
||||
function barPct(v, max) {
|
||||
const n = Number(v) || 0
|
||||
return `${Math.min(100, (n / max) * 100)}%`
|
||||
}
|
||||
|
||||
const load = async () => {
|
||||
loading.value = true
|
||||
function fmt(v) {
|
||||
const n = Number(v)
|
||||
return Number.isFinite(n) ? n.toFixed(2) : '0'
|
||||
}
|
||||
|
||||
function goWorkbench() {
|
||||
uni.reLaunch({ url: PAGE_PATHS.workbench })
|
||||
}
|
||||
|
||||
function go(url) {
|
||||
uni.navigateTo({ url })
|
||||
}
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const [s, weighings, inCargo, outCargo] = await Promise.all([
|
||||
getDashboardStats(),
|
||||
listWeighing({ dateFrom: todayYmd(), dateTo: todayYmd() }),
|
||||
getInboundByCargo(),
|
||||
getOutboundByCargo()
|
||||
const [a, b, c, d] = await Promise.all([
|
||||
dashboardApi.getInboundByCargo(),
|
||||
dashboardApi.getOutboundByCargo(),
|
||||
dashboardApi.getInboundByPartnerCargo(),
|
||||
dashboardApi.getOutboundByPartnerCargo()
|
||||
])
|
||||
const todayList = weighings || []
|
||||
const needSecond = todayList.filter((w) => {
|
||||
const p = weighingPhase(w)
|
||||
return p.text === '一次过磅'
|
||||
}).length
|
||||
stats.value = {
|
||||
todayInbound: s.todayInbound ?? s.today_inbound ?? 0,
|
||||
todayOutbound: s.todayOutbound ?? s.today_outbound ?? 0,
|
||||
todayWeighing: todayList.filter((w) => Number(w.isVoided) !== 1).length,
|
||||
needSecond
|
||||
}
|
||||
const merged = []
|
||||
.concat(inCargo || [])
|
||||
.concat(outCargo || [])
|
||||
.slice(0, 6)
|
||||
topCargo.value = merged
|
||||
inboundByCargo.value = a || []
|
||||
outboundByCargo.value = b || []
|
||||
inboundByPartner.value = c || []
|
||||
outboundByPartner.value = d || []
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
onShow(() => load())
|
||||
onMounted(load)
|
||||
onShow(() => {
|
||||
userStore.checkLoginStatus()
|
||||
if (!userStore.isLoggedIn) {
|
||||
uni.reLaunch({ url: PAGE_PATHS.login })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="mp-page mp-page--sub">
|
||||
<view class="mp-header mp-header--db">
|
||||
<text class="mp-subpage-back" @click="goBack">← 工作台</text>
|
||||
<text class="mp-header__title" style="margin-top: 16rpx">地磅仪表盘</text>
|
||||
<text class="mp-header__meta">今日出入库与过磅概况</text>
|
||||
</view>
|
||||
|
||||
<view class="mp-stats mp-stats--4">
|
||||
<view class="mp-stat">
|
||||
<text class="mp-stat__num">{{ stats.todayWeighing }}</text>
|
||||
<text class="mp-stat__label">今日过磅</text>
|
||||
</view>
|
||||
<view class="mp-stat">
|
||||
<text class="mp-stat__num">{{ stats.needSecond }}</text>
|
||||
<text class="mp-stat__label">待二次过磅</text>
|
||||
</view>
|
||||
<view class="mp-stat">
|
||||
<text class="mp-stat__num">{{ Number(stats.todayInbound).toFixed(0) }}</text>
|
||||
<text class="mp-stat__label">今日入库</text>
|
||||
</view>
|
||||
<view class="mp-stat">
|
||||
<text class="mp-stat__num">{{ Number(stats.todayOutbound).toFixed(0) }}</text>
|
||||
<text class="mp-stat__label">今日出库</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mp-section">
|
||||
<view class="mp-list">
|
||||
<view class="mp-card" @click="goWeighing">
|
||||
<view class="mp-card__head">
|
||||
<text class="mp-card__title">地磅单管理</text>
|
||||
<text class="mp-tag mp-tag--warning">现场</text>
|
||||
</view>
|
||||
<text class="mp-card__sub">查看过磅记录,办理二次过磅</text>
|
||||
</view>
|
||||
<view class="mp-card" @click="goPlans">
|
||||
<view class="mp-card__head">
|
||||
<text class="mp-card__title">地磅计划</text>
|
||||
<text class="mp-tag mp-tag--info">计划</text>
|
||||
</view>
|
||||
<text class="mp-card__sub">查看入/出库计划执行进度</text>
|
||||
</view>
|
||||
<view class="mp-card" @click="goInventory">
|
||||
<view class="mp-card__head">
|
||||
<text class="mp-card__title">出入库单</text>
|
||||
<text class="mp-tag mp-tag--success">单据</text>
|
||||
</view>
|
||||
<text class="mp-card__sub">查看出入库及关联过磅</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="topCargo.length" class="mp-block">
|
||||
<text class="mp-block__title">货品计划 vs 实绩(节选)</text>
|
||||
<view class="mp-block__panel">
|
||||
<view v-for="(c, i) in topCargo" :key="i" class="mp-kv">
|
||||
<text class="mp-kv__label">{{ dash(c.label) }}</text>
|
||||
<text class="mp-kv__value">
|
||||
计划 {{ formatWeight(c.planned, '') }} / 实绩 {{ formatWeight(c.actual, '') }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loading" class="mp-list-loading">加载中…</view>
|
||||
</view>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
@import '@/styles/db-theme.scss';
|
||||
</style>
|
||||
@@ -1,121 +1,228 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||
import { getInventory, getInventoryRelated } from '@/api/db'
|
||||
import { inventoryStatusLabel, dash, formatWeight } from '@/utils/dbLabels'
|
||||
import { PAGE_PATHS } from '@/config/api'
|
||||
|
||||
const id = ref('')
|
||||
const detail = ref(null)
|
||||
const related = ref({ plan: null, weighing: null })
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack({
|
||||
fail: () => uni.navigateTo({ url: PAGE_PATHS.dbInventoryList })
|
||||
})
|
||||
}
|
||||
|
||||
const load = async () => {
|
||||
if (!id.value) return
|
||||
try {
|
||||
detail.value = await getInventory(id.value)
|
||||
related.value = await getInventoryRelated(id.value)
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '加载失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const openWeighing = () => {
|
||||
if (!related.value?.weighing?.id) {
|
||||
uni.showToast({ title: '暂无关联地磅单', icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.navigateTo({
|
||||
url: `${PAGE_PATHS.dbWeighingDetail}?id=${related.value.weighing.id}`
|
||||
})
|
||||
}
|
||||
|
||||
const openPlan = () => {
|
||||
if (!related.value?.plan?.id) {
|
||||
uni.showToast({ title: '暂无关联计划', icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.navigateTo({
|
||||
url: `${PAGE_PATHS.dbPlansDetail}?id=${related.value.plan.id}`
|
||||
})
|
||||
}
|
||||
|
||||
onLoad((q) => {
|
||||
id.value = q?.id || ''
|
||||
})
|
||||
onShow(() => load())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="mp-page mp-page--sub">
|
||||
<view class="mp-header mp-header--db">
|
||||
<text class="mp-subpage-back" @click="goBack">← 返回</text>
|
||||
<text class="mp-header__title" style="margin-top: 16rpx">出入库详情</text>
|
||||
</view>
|
||||
|
||||
<view v-if="detail" class="mp-detail-hero">
|
||||
<view class="mp-detail-hero__top">
|
||||
<view>
|
||||
<text class="mp-detail-hero__title">{{ dash(detail.batchNo || detail.id) }}</text>
|
||||
<text class="mp-detail-hero__sub">{{ dash(detail.product) }} · {{ dash(detail.businessType) }}</text>
|
||||
<view class="db-page">
|
||||
<view class="db-header" :style="safeAreaStyle">
|
||||
<view class="db-header__nav">
|
||||
<view class="db-header__left">
|
||||
<text class="db-header__back" @click="goBack">←</text>
|
||||
<text class="db-header__title">{{ title }}</text>
|
||||
</view>
|
||||
<text class="mp-tag" :class="'mp-tag--' + inventoryStatusLabel(detail.status).tag">
|
||||
{{ inventoryStatusLabel(detail.status).text }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="mp-detail-hero__meta">
|
||||
<text class="mp-detail-hero__chip">{{ dash(detail.vehicleNo) }}</text>
|
||||
<text class="mp-detail-hero__chip">{{ formatWeight(detail.quantity) }}</text>
|
||||
<view />
|
||||
<view />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mp-block">
|
||||
<text class="mp-block__title">基本信息</text>
|
||||
<view class="mp-block__panel">
|
||||
<view class="mp-kv"><text class="mp-kv__label">日期</text><text class="mp-kv__value">{{ dash(detail?.date) }}</text></view>
|
||||
<view class="mp-kv"><text class="mp-kv__label">往来单位</text><text class="mp-kv__value">{{ dash(detail?.partnerCompany) }}</text></view>
|
||||
<view class="mp-kv"><text class="mp-kv__label">规格</text><text class="mp-kv__value">{{ dash(detail?.specification) }}</text></view>
|
||||
<view class="mp-kv"><text class="mp-kv__label">计划号</text><text class="mp-kv__value">{{ dash(detail?.planNo) }}</text></view>
|
||||
<view class="mp-kv"><text class="mp-kv__label">备注</text><text class="mp-kv__value">{{ dash(detail?.remarks) }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mp-block">
|
||||
<text class="mp-block__title">过磅数据</text>
|
||||
<view class="mp-block__panel">
|
||||
<view class="mp-kv"><text class="mp-kv__label">毛重</text><text class="mp-kv__value">{{ formatWeight(detail?.grossWeight) }}</text></view>
|
||||
<view class="mp-kv"><text class="mp-kv__label">皮重</text><text class="mp-kv__value">{{ formatWeight(detail?.tareWeight) }}</text></view>
|
||||
<view class="mp-kv"><text class="mp-kv__label">净重</text><text class="mp-kv__value">{{ formatWeight(detail?.netWeight) }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="detail?.siteFeedbackRemark || detail?.siteFeedbackImages?.length" class="mp-block">
|
||||
<text class="mp-block__title">现场反馈</text>
|
||||
<view class="mp-block__panel" style="padding: 28rpx">
|
||||
<text style="font-size: 26rpx">{{ dash(detail.siteFeedbackRemark) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mp-section">
|
||||
<view class="mp-list">
|
||||
<view class="mp-card" @click="openWeighing">
|
||||
<view class="mp-card__head">
|
||||
<text class="mp-card__title">关联地磅单</text>
|
||||
<text class="mp-tag mp-tag--info">{{ related.weighing ? '查看' : '无' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mp-card" @click="openPlan">
|
||||
<view class="mp-card__head">
|
||||
<text class="mp-card__title">关联计划</text>
|
||||
<text class="mp-tag mp-tag--info">{{ related.plan ? '查看' : '无' }}</text>
|
||||
<view class="db-form-section">
|
||||
<view class="db-form-item">
|
||||
<view class="db-form-label">日期 <text class="req">*</text></view>
|
||||
<view v-if="isView" class="db-form-value">{{ form.date || '--' }}</view>
|
||||
<picker v-else mode="date" :value="form.date" @change="(e) => (form.date = e.detail.value)">
|
||||
<view class="db-form-value">
|
||||
<text :class="{ ph: !form.date }">{{ form.date || '选择日期' }}</text>
|
||||
<text>›</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="db-form-item">
|
||||
<view class="db-form-label">往来单位 <text class="req">*</text></view>
|
||||
<view v-if="isView" class="db-form-value">{{ form.partnerCompany || '--' }}</view>
|
||||
<view v-else class="db-form-value" @click="showCustomer = true">
|
||||
<text :class="{ ph: !form.partnerCompany }">{{ form.partnerCompany || '请选择往来单位' }}</text>
|
||||
<text>›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="db-form-item">
|
||||
<view class="db-form-label">业务类型 <text class="req">*</text></view>
|
||||
<db-radio-group v-model="form.businessType" :disabled="isView" />
|
||||
</view>
|
||||
|
||||
<view class="db-form-item">
|
||||
<view class="db-form-label">产品 <text class="req">*</text></view>
|
||||
<view v-if="isView" class="db-form-value">{{ productDisplay || '--' }}</view>
|
||||
<view v-else class="db-form-value" @click="showProduct = true">
|
||||
<text :class="{ ph: !productDisplay }">{{ productDisplay || '请选择产品' }}</text>
|
||||
<text>›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="db-form-item">
|
||||
<view class="db-form-label">数量(吨) <text class="req">*</text></view>
|
||||
<view v-if="isView" class="db-form-value">{{ form.quantity ?? '--' }}</view>
|
||||
<db-field
|
||||
v-else
|
||||
v-model="form.quantity"
|
||||
type="digit"
|
||||
placeholder="请输入数量(默认32)"
|
||||
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="db-form-item">
|
||||
<view class="db-form-label">车牌号 <text class="req">*</text></view>
|
||||
<view v-if="isView" class="db-form-value">{{ form.vehicleNo || '--' }}</view>
|
||||
<view v-else class="db-form-value" @click="showVehicle = true">
|
||||
<text :class="{ ph: !form.vehicleNo }">{{ form.vehicleNo || '请选择车牌号' }}</text>
|
||||
<text>›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="db-form-item">
|
||||
<view class="db-form-label">计划号</view>
|
||||
<view v-if="isView || planNoReadonly" class="db-form-value">{{ form.planNo || '--' }}</view>
|
||||
<db-field
|
||||
v-else
|
||||
v-model="form.planNo"
|
||||
placeholder="请输入计划号"
|
||||
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="db-form-item">
|
||||
<view class="db-form-label">备注</view>
|
||||
<view v-if="isView" class="db-form-value">{{ form.remarks || '--' }}</view>
|
||||
<db-field
|
||||
v-else
|
||||
v-model="form.remarks"
|
||||
type="textarea"
|
||||
autosize
|
||||
placeholder="请输入备注"
|
||||
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="!isView" class="db-form-actions">
|
||||
<db-btn block type="primary" :loading="saving" @click="save">保存</db-btn>
|
||||
<db-btn block plain style="margin-top:12px" @click="goBack">取消</db-btn>
|
||||
</view>
|
||||
|
||||
<db-customer-picker :show="showCustomer" @update:show="(v) => (showCustomer = v)" @select="onCustomer" />
|
||||
<db-product-picker :show="showProduct" @update:show="(v) => (showProduct = v)" @select="onProduct" />
|
||||
<db-vehicle-picker :show="showVehicle" @update:show="(v) => (showVehicle = v)" @select="onVehicle" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { PAGE_PATHS } from '@/config/api'
|
||||
import { getSafeAreaStyle } from '@/utils/safeArea'
|
||||
import { inventoryApi } from '@/pages/db/api/db'
|
||||
import { formatYmd } from '@/pages/db/utils/dbDate'
|
||||
import DbField from '@/components/db-ui/db-field.vue'
|
||||
import DbBtn from '@/components/db-ui/db-btn.vue'
|
||||
import DbRadioGroup from '@/components/db-ui/db-radio-group.vue'
|
||||
import DbCustomerPicker from '@/components/db-customer-picker/db-customer-picker.vue'
|
||||
import DbProductPicker from '@/components/db-product-picker/db-product-picker.vue'
|
||||
import DbVehiclePicker from '@/components/db-vehicle-picker/db-vehicle-picker.vue'
|
||||
|
||||
const safeAreaStyle = getSafeAreaStyle()
|
||||
const mode = ref('create')
|
||||
const id = ref('')
|
||||
const saving = ref(false)
|
||||
const showCustomer = ref(false)
|
||||
const showProduct = ref(false)
|
||||
const showVehicle = ref(false)
|
||||
const productDisplay = ref('')
|
||||
const planNoReadonly = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
date: formatYmd(),
|
||||
partnerCompany: '',
|
||||
businessType: '入库',
|
||||
productId: '',
|
||||
quantity: 32,
|
||||
vehicleNo: '',
|
||||
planNo: '',
|
||||
remarks: '',
|
||||
purchasePlanId: undefined
|
||||
})
|
||||
|
||||
const isView = computed(() => mode.value === 'view')
|
||||
const isEdit = computed(() => mode.value === 'edit')
|
||||
const title = computed(() => (isEdit.value ? '编辑出入库' : isView.value ? '查看出入库' : '新增出入库'))
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack({ fail: () => uni.redirectTo({ url: PAGE_PATHS.dbInventoryList }) })
|
||||
}
|
||||
function onCustomer(item) {
|
||||
form.partnerCompany = item.customerName
|
||||
}
|
||||
function onProduct(item) {
|
||||
form.productId = item.id
|
||||
productDisplay.value = item.productName
|
||||
}
|
||||
function onVehicle(plate) {
|
||||
form.vehicleNo = plate
|
||||
}
|
||||
|
||||
async function loadDetail() {
|
||||
const data = await inventoryApi.get(id.value)
|
||||
Object.assign(form, {
|
||||
date: data.date || '',
|
||||
partnerCompany: data.partnerCompany || '',
|
||||
businessType: data.businessType || '入库',
|
||||
productId: data.productId || '',
|
||||
quantity: data.quantity ?? 0,
|
||||
vehicleNo: data.vehicleNo || '',
|
||||
planNo: data.planNo || '',
|
||||
remarks: data.remarks || '',
|
||||
purchasePlanId: data.purchasePlanId
|
||||
})
|
||||
productDisplay.value = data.product || ''
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!form.date || !form.partnerCompany || !form.productId || !form.vehicleNo) {
|
||||
uni.showToast({ title: '请完善必填项', icon: 'none' })
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
const payload = {
|
||||
date: form.date,
|
||||
partnerCompany: form.partnerCompany,
|
||||
businessType: form.businessType,
|
||||
productId: form.productId,
|
||||
quantity: Number(form.quantity),
|
||||
vehicleNo: form.vehicleNo,
|
||||
planNo: form.planNo || undefined,
|
||||
remarks: form.remarks || undefined,
|
||||
purchasePlanId: form.purchasePlanId
|
||||
}
|
||||
if (id.value) await inventoryApi.update(id.value, payload)
|
||||
else await inventoryApi.create(payload)
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
setTimeout(goBack, 500)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onLoad(async (q) => {
|
||||
mode.value = q.mode || (q.id ? 'edit' : 'create')
|
||||
id.value = q.id || ''
|
||||
if (q.planNo) {
|
||||
form.planNo = decodeURIComponent(q.planNo)
|
||||
planNoReadonly.value = true
|
||||
form.date = ''
|
||||
}
|
||||
if (q.product) productDisplay.value = decodeURIComponent(q.product)
|
||||
if (q.productId) form.productId = decodeURIComponent(q.productId)
|
||||
if (q.businessType) form.businessType = decodeURIComponent(q.businessType)
|
||||
if (q.customerName) form.partnerCompany = decodeURIComponent(q.customerName)
|
||||
if (q.purchasePlanId) form.purchasePlanId = decodeURIComponent(q.purchasePlanId)
|
||||
if (id.value) {
|
||||
try {
|
||||
await loadDetail()
|
||||
} catch (e) {
|
||||
// handled
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '@/styles/db-theme.scss';
|
||||
</style>
|
||||
@@ -1,141 +1,246 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { listInventory } from '@/api/db'
|
||||
import {
|
||||
inventoryStatusLabel,
|
||||
dash,
|
||||
formatWeight,
|
||||
filterByKeyword,
|
||||
todayYmd
|
||||
} from '@/utils/dbLabels'
|
||||
import { PAGE_PATHS } from '@/config/api'
|
||||
<template>
|
||||
<view class="db-page">
|
||||
<view class="db-header" :style="safeAreaStyle">
|
||||
<view class="db-header__nav">
|
||||
<view class="db-header__left">
|
||||
<text class="db-header__back" @click="goBack">←</text>
|
||||
<text class="db-header__title">出入库管理</text>
|
||||
</view>
|
||||
<view />
|
||||
<view />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
const tabs = [
|
||||
{ key: 'all', label: '全部' },
|
||||
{ key: '入库', label: '入库' },
|
||||
{ key: '出库', label: '出库' },
|
||||
{ key: 'pending', label: '待处理' }
|
||||
<view class="db-search-wrap">
|
||||
<db-search
|
||||
v-model="keyword"
|
||||
placeholder="搜索往来单位 / 车牌 / 产品"
|
||||
@change="onKeyword"
|
||||
@search="reload"
|
||||
@clear="onClear"
|
||||
/>
|
||||
</view>
|
||||
<view class="db-filter-bar">
|
||||
<view
|
||||
v-for="item in dateFilters"
|
||||
:key="item.value"
|
||||
class="db-filter-chip"
|
||||
:class="{ 'db-filter-chip--active': dateFilter === item.value }"
|
||||
@click="setFilter(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="db-list">
|
||||
<view v-if="!loading && !list.length" class="db-empty">暂无数据</view>
|
||||
<view v-for="item in list" :key="item.id" class="db-card" @click="viewItem(item)">
|
||||
<view class="db-card__head">
|
||||
<text class="db-card__title">{{ item.vehicleNo || '未填车牌' }}</text>
|
||||
<db-tag :type="item.businessType === '入库' ? 'success' : 'warning'">
|
||||
{{ item.businessType }}
|
||||
</db-tag>
|
||||
</view>
|
||||
<view class="db-card__row">
|
||||
<text class="db-card__label">日期</text>
|
||||
<text>{{ item.date || '--' }}</text>
|
||||
</view>
|
||||
<view class="db-card__row">
|
||||
<text class="db-card__label">往来单位</text>
|
||||
<text>{{ item.partnerCompany || '--' }}</text>
|
||||
</view>
|
||||
<view class="db-card__row">
|
||||
<text class="db-card__label">产品</text>
|
||||
<text>{{ item.product || '--' }}</text>
|
||||
</view>
|
||||
<view class="db-card__row">
|
||||
<text class="db-card__label">数量</text>
|
||||
<text>{{ item.quantity ?? '--' }} 吨</text>
|
||||
</view>
|
||||
<view class="db-card__row">
|
||||
<text class="db-card__label">状态</text>
|
||||
<text>{{ item.status || '待处理' }}</text>
|
||||
</view>
|
||||
<view class="db-card__foot" @click.stop>
|
||||
<db-btn size="small" type="primary" plain @click="viewRelated(item)">关联</db-btn>
|
||||
<db-btn size="small" type="primary" plain @click="editItem(item)">编辑</db-btn>
|
||||
<db-btn
|
||||
v-if="!item.hasWeighingSlip"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="genWeighing(item)"
|
||||
>
|
||||
生成地磅单
|
||||
</db-btn>
|
||||
<db-btn size="small" type="danger" plain @click="removeItem(item)">删除</db-btn>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="loading" class="db-empty">加载中...</view>
|
||||
</view>
|
||||
|
||||
<db-popup :show="relatedOpen" height="75vh" @close="relatedOpen = false" @update:show="(v) => (relatedOpen = v)">
|
||||
<view style="padding: 16px">
|
||||
<view class="db-header__title" style="margin-bottom: 12px">关联数据</view>
|
||||
<view v-if="relatedLoading" class="db-empty">加载中...</view>
|
||||
<template v-else>
|
||||
<view v-if="related.plan" class="db-card">
|
||||
<db-tag type="primary">计划</db-tag>
|
||||
<view class="db-card__row" style="margin-top: 8px">
|
||||
<text class="db-card__label">计划号</text>
|
||||
<text>{{ related.plan.planNo || '--' }}</text>
|
||||
</view>
|
||||
<view class="db-card__row">
|
||||
<text class="db-card__label">产品</text>
|
||||
<text>{{ related.plan.product || '--' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="related.weighing" class="db-card">
|
||||
<db-tag type="warning">地磅单</db-tag>
|
||||
<view class="db-card__row" style="margin-top: 8px">
|
||||
<text class="db-card__label">车牌</text>
|
||||
<text>{{ related.weighing.vehicleNo || '--' }}</text>
|
||||
</view>
|
||||
<view class="db-card__row">
|
||||
<text class="db-card__label">净重</text>
|
||||
<text>{{ related.weighing.netWeight ?? '--' }} kg</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="!related.plan && !related.weighing" class="db-empty">暂无关联数据</view>
|
||||
</template>
|
||||
</view>
|
||||
</db-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { PAGE_PATHS } from '@/config/api'
|
||||
import { getSafeAreaStyle } from '@/utils/safeArea'
|
||||
import { inventoryApi } from '@/pages/db/api/db'
|
||||
import { formatYmd, addDays, weekRange } from '@/pages/db/utils/dbDate'
|
||||
import DbSearch from '@/components/db-ui/db-search.vue'
|
||||
import DbTag from '@/components/db-ui/db-tag.vue'
|
||||
import DbBtn from '@/components/db-ui/db-btn.vue'
|
||||
import DbPopup from '@/components/db-ui/db-popup.vue'
|
||||
|
||||
const safeAreaStyle = getSafeAreaStyle()
|
||||
const keyword = ref('')
|
||||
const dateFilter = ref('today')
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const relatedOpen = ref(false)
|
||||
const relatedLoading = ref(false)
|
||||
const related = ref({ plan: null, weighing: null })
|
||||
let debounce = null
|
||||
|
||||
const dateFilters = [
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '今日', value: 'today' },
|
||||
{ label: '明日', value: 'tomorrow' },
|
||||
{ label: '昨日', value: 'yesterday' },
|
||||
{ label: '本周', value: 'week' }
|
||||
]
|
||||
|
||||
const activeTab = ref('all')
|
||||
const keyword = ref('')
|
||||
const rawList = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
const list = computed(() => {
|
||||
let rows = rawList.value
|
||||
if (activeTab.value === '入库' || activeTab.value === '出库') {
|
||||
rows = rows.filter((r) => r.businessType === activeTab.value)
|
||||
} else if (activeTab.value === 'pending') {
|
||||
rows = rows.filter((r) => String(r.status || '').includes('待'))
|
||||
function buildParams() {
|
||||
const params = { q: keyword.value || undefined }
|
||||
const today = new Date()
|
||||
if (dateFilter.value === 'today') {
|
||||
const d = formatYmd(today)
|
||||
params.dateFrom = d
|
||||
params.dateTo = d
|
||||
} else if (dateFilter.value === 'tomorrow') {
|
||||
const t = formatYmd(addDays(today, 1))
|
||||
params.dateFrom = t
|
||||
params.dateTo = t
|
||||
} else if (dateFilter.value === 'yesterday') {
|
||||
const y = formatYmd(addDays(today, -1))
|
||||
params.dateFrom = y
|
||||
params.dateTo = y
|
||||
} else if (dateFilter.value === 'week') {
|
||||
const w = weekRange(today)
|
||||
params.dateFrom = w.from
|
||||
params.dateTo = w.to
|
||||
}
|
||||
return filterByKeyword(rows, keyword.value, [
|
||||
'batchNo',
|
||||
'planNo',
|
||||
'vehicleNo',
|
||||
'product',
|
||||
'partnerCompany',
|
||||
'status'
|
||||
]).slice(0, 80)
|
||||
})
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack({
|
||||
fail: () => uni.switchTab({ url: PAGE_PATHS.workbench })
|
||||
})
|
||||
return params
|
||||
}
|
||||
|
||||
const goDetail = (row) =>
|
||||
uni.navigateTo({ url: `${PAGE_PATHS.dbInventoryDetail}?id=${row.id}` })
|
||||
|
||||
const load = async () => {
|
||||
async function reload() {
|
||||
loading.value = true
|
||||
try {
|
||||
// 默认拉今日,减轻全量压力;切「全部」再无日期
|
||||
if (activeTab.value === 'all' && !keyword.value) {
|
||||
const today = todayYmd()
|
||||
rawList.value = await listInventory({ dateFrom: today, dateTo: today })
|
||||
if (rawList.value.length < 5) {
|
||||
rawList.value = await listInventory()
|
||||
}
|
||||
} else {
|
||||
rawList.value = await listInventory()
|
||||
}
|
||||
list.value = (await inventoryApi.list(buildParams())) || []
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '加载失败', icon: 'none' })
|
||||
list.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const switchTab = (key) => {
|
||||
activeTab.value = key
|
||||
load()
|
||||
function onKeyword(v) {
|
||||
keyword.value = typeof v === 'string' ? v : keyword.value
|
||||
if (debounce) clearTimeout(debounce)
|
||||
debounce = setTimeout(reload, 300)
|
||||
}
|
||||
function onClear() {
|
||||
keyword.value = ''
|
||||
reload()
|
||||
}
|
||||
function setFilter(v) {
|
||||
if (dateFilter.value === v) return
|
||||
dateFilter.value = v
|
||||
reload()
|
||||
}
|
||||
function goBack() {
|
||||
uni.navigateBack({ fail: () => uni.reLaunch({ url: PAGE_PATHS.workbench }) })
|
||||
}
|
||||
function editItem(item) {
|
||||
uni.navigateTo({ url: `${PAGE_PATHS.dbInventoryDetail}?id=${item.id}&mode=edit` })
|
||||
}
|
||||
function viewItem(item) {
|
||||
uni.navigateTo({ url: `${PAGE_PATHS.dbInventoryDetail}?id=${item.id}&mode=view` })
|
||||
}
|
||||
async function viewRelated(item) {
|
||||
relatedOpen.value = true
|
||||
relatedLoading.value = true
|
||||
try {
|
||||
related.value = (await inventoryApi.getRelated(item.id)) || { plan: null, weighing: null }
|
||||
} catch (e) {
|
||||
relatedOpen.value = false
|
||||
} finally {
|
||||
relatedLoading.value = false
|
||||
}
|
||||
}
|
||||
function genWeighing(item) {
|
||||
const q = [
|
||||
`vehicleNo=${encodeURIComponent(item.vehicleNo || '')}`,
|
||||
`productId=${encodeURIComponent(item.productId || '')}`,
|
||||
`cargoName=${encodeURIComponent(item.product || '')}`,
|
||||
`businessType=${encodeURIComponent(item.businessType || '')}`,
|
||||
`supplier=${encodeURIComponent(item.partnerCompany || '')}`,
|
||||
`consignee=${encodeURIComponent(item.partnerCompany || '')}`,
|
||||
`planNo=${encodeURIComponent(item.planNo || '')}`,
|
||||
`inboundOutboundId=${encodeURIComponent(item.id)}`,
|
||||
`purchasePlanId=${encodeURIComponent(item.purchasePlanId || '')}`,
|
||||
'mode=create'
|
||||
].join('&')
|
||||
uni.navigateTo({ url: `${PAGE_PATHS.dbWeighingDetail}?${q}` })
|
||||
}
|
||||
function removeItem(item) {
|
||||
uni.showModal({
|
||||
title: '确认删除',
|
||||
content: '确定删除该出入库单吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return
|
||||
await inventoryApi.delete(item.id)
|
||||
uni.showToast({ title: '删除成功', icon: 'success' })
|
||||
reload()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onShow(() => load())
|
||||
onShow(reload)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="mp-page mp-page--sub">
|
||||
<view class="mp-header mp-header--db">
|
||||
<text class="mp-subpage-back" @click="goBack">← 工作台</text>
|
||||
<text class="mp-header__title" style="margin-top: 16rpx">地磅出入库</text>
|
||||
<view class="mp-header__search">
|
||||
<view class="mp-header__search-dot" />
|
||||
<input
|
||||
v-model="keyword"
|
||||
class="mp-header__search-input"
|
||||
placeholder="单号 / 车牌 / 产品 / 往来单位"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mp-section" style="padding-bottom: 0">
|
||||
<scroll-view scroll-x class="mp-tabs">
|
||||
<view
|
||||
v-for="t in tabs"
|
||||
:key="t.key"
|
||||
class="mp-tab"
|
||||
:class="{ 'mp-tab--active': activeTab === t.key }"
|
||||
@click="switchTab(t.key)"
|
||||
>
|
||||
{{ t.label }}
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<view class="mp-section">
|
||||
<mp-empty v-if="!list.length && !loading" title="暂无出入库单" />
|
||||
<view v-else class="mp-list">
|
||||
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
|
||||
<view class="mp-card__head">
|
||||
<view>
|
||||
<text class="mp-card__title">{{ dash(row.batchNo || row.id) }}</text>
|
||||
<text class="mp-card__sub">
|
||||
{{ dash(row.vehicleNo) }} · {{ dash(row.product) }} · {{ dash(row.businessType) }}
|
||||
</text>
|
||||
</view>
|
||||
<text class="mp-tag" :class="'mp-tag--' + inventoryStatusLabel(row.status).tag">
|
||||
{{ inventoryStatusLabel(row.status).text }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="mp-card__row">
|
||||
<text>数量</text>
|
||||
<text class="mp-card__row-val">{{ formatWeight(row.quantity) }}</text>
|
||||
</view>
|
||||
<view class="mp-card__row">
|
||||
<text>往来单位</text>
|
||||
<text class="mp-card__row-val">{{ dash(row.partnerCompany) }}</text>
|
||||
</view>
|
||||
<view class="mp-card__row">
|
||||
<text>日期</text>
|
||||
<text class="mp-card__row-val">{{ dash(row.date) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="loading" class="mp-list-loading">加载中…</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
@import '@/styles/db-theme.scss';
|
||||
</style>
|
||||
@@ -1,139 +1,214 @@
|
||||
<template>
|
||||
<view class="db-page">
|
||||
<view class="db-header" :style="safeAreaStyle">
|
||||
<view class="db-header__nav">
|
||||
<view class="db-header__left">
|
||||
<text class="db-header__back" @click="goBack">←</text>
|
||||
<text class="db-header__title">{{ title }}</text>
|
||||
</view>
|
||||
<view />
|
||||
<view />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="db-form-section">
|
||||
<view class="db-form-item">
|
||||
<view class="db-form-label">计划号</view>
|
||||
<view class="db-form-value">{{ form.planNo || '系统自动生成' }}</view>
|
||||
</view>
|
||||
|
||||
<view class="db-form-item">
|
||||
<view class="db-form-label">业务类型 <text class="req">*</text></view>
|
||||
<db-radio-group v-model="form.businessType" :disabled="isView" />
|
||||
</view>
|
||||
|
||||
<view class="db-form-item">
|
||||
<view class="db-form-label">往来单位</view>
|
||||
<view v-if="isView" class="db-form-value">{{ form.customerName || '--' }}</view>
|
||||
<view v-else class="db-form-value" @click="showCustomer = true">
|
||||
<text :class="{ ph: !form.customerName }">{{ form.customerName || '请选择往来单位' }}</text>
|
||||
<text>›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="db-form-item">
|
||||
<view class="db-form-label">产品 <text class="req">*</text></view>
|
||||
<view v-if="isView" class="db-form-value">{{ productDisplay || '--' }}</view>
|
||||
<view v-else class="db-form-value" @click="showProduct = true">
|
||||
<text :class="{ ph: !productDisplay }">{{ productDisplay || '请选择产品' }}</text>
|
||||
<text>›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="db-form-item">
|
||||
<view class="db-form-label">计划开始时间</view>
|
||||
<view v-if="isView" class="db-form-value">{{ form.startDate || '--' }}</view>
|
||||
<picker v-else mode="date" :value="form.startDate" @change="(e) => (form.startDate = e.detail.value)">
|
||||
<view class="db-form-value">
|
||||
<text :class="{ ph: !form.startDate }">{{ form.startDate || '选择日期' }}</text>
|
||||
<text>›</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="db-form-item">
|
||||
<view class="db-form-label">计划结束时间</view>
|
||||
<view v-if="isView" class="db-form-value">{{ form.endDate || '--' }}</view>
|
||||
<picker v-else mode="date" :value="form.endDate" @change="(e) => (form.endDate = e.detail.value)">
|
||||
<view class="db-form-value">
|
||||
<text :class="{ ph: !form.endDate }">{{ form.endDate || '选择日期' }}</text>
|
||||
<text>›</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="db-form-item">
|
||||
<view class="db-form-label">计划量(吨) <text class="req">*</text></view>
|
||||
<view v-if="isView" class="db-form-value">{{ form.plannedQuantity ?? '--' }}</view>
|
||||
<db-field
|
||||
v-else
|
||||
v-model="form.plannedQuantity"
|
||||
type="digit"
|
||||
placeholder="请输入计划量"
|
||||
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="db-form-item">
|
||||
<view class="db-form-label">备注</view>
|
||||
<view v-if="isView" class="db-form-value">{{ form.remarks || '--' }}</view>
|
||||
<db-field
|
||||
v-else
|
||||
v-model="form.remarks"
|
||||
type="textarea"
|
||||
autosize
|
||||
placeholder="填写备注信息"
|
||||
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="!isView" class="db-form-actions">
|
||||
<db-btn block type="primary" :loading="saving" @click="save">保存</db-btn>
|
||||
<db-btn block plain style="margin-top:12px" @click="goBack">取消</db-btn>
|
||||
</view>
|
||||
|
||||
<db-customer-picker :show="showCustomer" @update:show="(v) => (showCustomer = v)" @select="onCustomer" />
|
||||
<db-product-picker :show="showProduct" @update:show="(v) => (showProduct = v)" @select="onProduct" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||
import { getPlan, getPlanRelated, createInventoryLinkForPlan } from '@/api/db'
|
||||
import { planStatusLabel, dash, formatWeight } from '@/utils/dbLabels'
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { PAGE_PATHS } from '@/config/api'
|
||||
import { getSafeAreaStyle } from '@/utils/safeArea'
|
||||
import { planApi } from '@/pages/db/api/db'
|
||||
import { formatYmd } from '@/pages/db/utils/dbDate'
|
||||
import DbField from '@/components/db-ui/db-field.vue'
|
||||
import DbBtn from '@/components/db-ui/db-btn.vue'
|
||||
import DbRadioGroup from '@/components/db-ui/db-radio-group.vue'
|
||||
import DbCustomerPicker from '@/components/db-customer-picker/db-customer-picker.vue'
|
||||
import DbProductPicker from '@/components/db-product-picker/db-product-picker.vue'
|
||||
|
||||
const safeAreaStyle = getSafeAreaStyle()
|
||||
const mode = ref('create')
|
||||
const id = ref('')
|
||||
const detail = ref(null)
|
||||
const related = ref({ inventories: [], weighings: [] })
|
||||
const acting = ref(false)
|
||||
const saving = ref(false)
|
||||
const showCustomer = ref(false)
|
||||
const showProduct = ref(false)
|
||||
const productDisplay = ref('')
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack({
|
||||
fail: () => uni.navigateTo({ url: PAGE_PATHS.dbPlansList })
|
||||
})
|
||||
const form = reactive({
|
||||
planNo: '',
|
||||
planDate: formatYmd(),
|
||||
startDate: formatYmd(),
|
||||
endDate: '',
|
||||
businessType: '入库',
|
||||
productId: '',
|
||||
plannedQuantity: 0,
|
||||
customerName: '',
|
||||
remarks: ''
|
||||
})
|
||||
|
||||
const isView = computed(() => mode.value === 'view')
|
||||
const isEdit = computed(() => mode.value === 'edit')
|
||||
const title = computed(() => (isEdit.value ? '编辑计划' : isView.value ? '查看计划' : '新增计划'))
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack({ fail: () => uni.redirectTo({ url: PAGE_PATHS.dbPlansList }) })
|
||||
}
|
||||
|
||||
const load = async () => {
|
||||
function onCustomer(item) {
|
||||
form.customerName = item.customerName
|
||||
}
|
||||
|
||||
function onProduct(item) {
|
||||
form.productId = item.id
|
||||
productDisplay.value = item.productName
|
||||
}
|
||||
|
||||
async function loadDetail() {
|
||||
if (!id.value) return
|
||||
const data = await planApi.get(id.value)
|
||||
Object.assign(form, {
|
||||
planNo: data.planNo || '',
|
||||
planDate: data.planDate || formatYmd(),
|
||||
startDate: data.startDate || '',
|
||||
endDate: data.endDate || '',
|
||||
businessType: data.businessType || '入库',
|
||||
productId: data.productId || '',
|
||||
plannedQuantity: data.plannedQuantity ?? 0,
|
||||
customerName: data.customerName || '',
|
||||
remarks: data.remarks || ''
|
||||
})
|
||||
productDisplay.value = data.product || ''
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!form.productId) {
|
||||
uni.showToast({ title: '请选择产品', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!form.plannedQuantity) {
|
||||
uni.showToast({ title: '请输入计划量', icon: 'none' })
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
detail.value = await getPlan(id.value)
|
||||
related.value = await getPlanRelated(id.value)
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '加载失败', icon: 'none' })
|
||||
const payload = {
|
||||
planNo: form.planNo || undefined,
|
||||
planDate: form.planDate || form.startDate || formatYmd(),
|
||||
startDate: form.startDate || undefined,
|
||||
endDate: form.endDate || undefined,
|
||||
businessType: form.businessType,
|
||||
productId: form.productId,
|
||||
plannedQuantity: Number(form.plannedQuantity),
|
||||
customerName: form.customerName || undefined,
|
||||
remarks: form.remarks || undefined
|
||||
}
|
||||
if (id.value) await planApi.update(id.value, payload)
|
||||
else await planApi.create(payload)
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
setTimeout(goBack, 500)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const createLink = () => {
|
||||
uni.showModal({
|
||||
title: '生成出入库单',
|
||||
content: '为该计划创建可填写的出入库关联单?',
|
||||
success: async (r) => {
|
||||
if (!r.confirm) return
|
||||
acting.value = true
|
||||
try {
|
||||
const inv = await createInventoryLinkForPlan(id.value)
|
||||
uni.showToast({ title: '已生成', icon: 'success' })
|
||||
if (inv?.id) {
|
||||
uni.navigateTo({ url: `${PAGE_PATHS.dbInventoryDetail}?id=${inv.id}` })
|
||||
} else {
|
||||
load()
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '操作失败', icon: 'none' })
|
||||
} finally {
|
||||
acting.value = false
|
||||
}
|
||||
onLoad(async (q) => {
|
||||
mode.value = q.mode || (q.id ? 'edit' : 'create')
|
||||
id.value = q.id || ''
|
||||
if (id.value) {
|
||||
try {
|
||||
await loadDetail()
|
||||
} catch (e) {
|
||||
// handled
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const openWeighing = (w) =>
|
||||
uni.navigateTo({ url: `${PAGE_PATHS.dbWeighingDetail}?id=${w.id}` })
|
||||
const openInventory = (inv) =>
|
||||
uni.navigateTo({ url: `${PAGE_PATHS.dbInventoryDetail}?id=${inv.id}` })
|
||||
|
||||
onLoad((q) => {
|
||||
id.value = q?.id || ''
|
||||
}
|
||||
})
|
||||
onShow(() => load())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="mp-page mp-page--sub mp-page--with-bottom">
|
||||
<view class="mp-header mp-header--db">
|
||||
<text class="mp-subpage-back" @click="goBack">← 返回</text>
|
||||
<text class="mp-header__title" style="margin-top: 16rpx">计划详情</text>
|
||||
</view>
|
||||
|
||||
<view v-if="detail" class="mp-detail-hero">
|
||||
<view class="mp-detail-hero__top">
|
||||
<view>
|
||||
<text class="mp-detail-hero__title">{{ dash(detail.planNo) }}</text>
|
||||
<text class="mp-detail-hero__sub">{{ dash(detail.product) }}</text>
|
||||
</view>
|
||||
<text class="mp-tag" :class="'mp-tag--' + planStatusLabel(detail.executionStatus).tag">
|
||||
{{ planStatusLabel(detail.executionStatus).text }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="mp-detail-hero__meta">
|
||||
<text class="mp-detail-hero__chip">{{ dash(detail.businessType) }}</text>
|
||||
<text class="mp-detail-hero__chip">
|
||||
{{ formatWeight(detail.executedQuantity) }} / {{ formatWeight(detail.plannedQuantity) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mp-block">
|
||||
<text class="mp-block__title">基本信息</text>
|
||||
<view class="mp-block__panel">
|
||||
<view class="mp-kv"><text class="mp-kv__label">计划日期</text><text class="mp-kv__value">{{ dash(detail?.planDate) }}</text></view>
|
||||
<view class="mp-kv"><text class="mp-kv__label">起止</text><text class="mp-kv__value">{{ dash(detail?.startDate) }} ~ {{ dash(detail?.endDate) }}</text></view>
|
||||
<view class="mp-kv"><text class="mp-kv__label">规格</text><text class="mp-kv__value">{{ dash(detail?.specification) }}</text></view>
|
||||
<view class="mp-kv"><text class="mp-kv__label">车牌</text><text class="mp-kv__value">{{ dash(detail?.vehicleNo) }}</text></view>
|
||||
<view class="mp-kv"><text class="mp-kv__label">客户</text><text class="mp-kv__value">{{ dash(detail?.customerName) }}</text></view>
|
||||
<view class="mp-kv"><text class="mp-kv__label">备注</text><text class="mp-kv__value">{{ dash(detail?.remarks) }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mp-block">
|
||||
<text class="mp-block__title">关联出入库({{ related.inventories?.length || 0 }})</text>
|
||||
<mp-empty v-if="!related.inventories?.length" compact title="暂无出入库单" />
|
||||
<view v-else class="mp-list">
|
||||
<view
|
||||
v-for="inv in related.inventories"
|
||||
:key="inv.id"
|
||||
class="mp-card"
|
||||
@click="openInventory(inv)"
|
||||
>
|
||||
<text class="mp-card__title">{{ dash(inv.batchNo || inv.id) }}</text>
|
||||
<text class="mp-card__sub">{{ dash(inv.vehicleNo) }} · {{ dash(inv.status) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mp-block">
|
||||
<text class="mp-block__title">关联地磅单({{ related.weighings?.length || 0 }})</text>
|
||||
<mp-empty v-if="!related.weighings?.length" compact title="暂无地磅单" />
|
||||
<view v-else class="mp-list">
|
||||
<view
|
||||
v-for="w in related.weighings"
|
||||
:key="w.id"
|
||||
class="mp-card"
|
||||
@click="openWeighing(w)"
|
||||
>
|
||||
<text class="mp-card__title">{{ dash(w.vehicleNo) }}</text>
|
||||
<text class="mp-card__sub">净重 {{ formatWeight(w.netWeight) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mp-bottom-bar">
|
||||
<view class="mp-btn mp-btn--primary" @click="createLink">
|
||||
{{ acting ? '…' : '生成出入库关联单' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
@import '@/styles/db-theme.scss';
|
||||
</style>
|
||||
@@ -1,119 +1,277 @@
|
||||
<template>
|
||||
<view class="db-page">
|
||||
<view class="db-header" :style="safeAreaStyle">
|
||||
<view class="db-header__nav">
|
||||
<view class="db-header__left">
|
||||
<text class="db-header__back" @click="goBack">←</text>
|
||||
<text class="db-header__title">计划管理</text>
|
||||
</view>
|
||||
<view />
|
||||
<view />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="db-search-wrap">
|
||||
<db-search
|
||||
v-model="keyword"
|
||||
placeholder="搜索往来单位 / 计划号"
|
||||
@change="onKeyword"
|
||||
@search="reload"
|
||||
@clear="onClear"
|
||||
/>
|
||||
</view>
|
||||
<view class="db-filter-bar">
|
||||
<view
|
||||
v-for="item in dateFilters"
|
||||
:key="item.value"
|
||||
class="db-filter-chip"
|
||||
:class="{ 'db-filter-chip--active': dateFilter === item.value }"
|
||||
@click="setFilter(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="db-list">
|
||||
<view v-if="!loading && !list.length" class="db-empty">暂无数据</view>
|
||||
<view v-for="item in list" :key="item.id" class="db-card" @click="viewItem(item)">
|
||||
<view class="db-card__head">
|
||||
<text class="db-card__title">{{ item.planNo || '未填计划号' }}</text>
|
||||
<db-tag :type="item.businessType === '入库' ? 'success' : 'warning'">
|
||||
{{ item.businessType }}
|
||||
</db-tag>
|
||||
</view>
|
||||
<view class="db-card__row">
|
||||
<text class="db-card__label">往来单位</text>
|
||||
<text>{{ item.customerName || '未填' }}</text>
|
||||
</view>
|
||||
<view class="db-card__row">
|
||||
<text class="db-card__label">产品</text>
|
||||
<text>{{ item.product || '未填' }}</text>
|
||||
</view>
|
||||
<view class="db-card__row">
|
||||
<text class="db-card__label">计划量</text>
|
||||
<text>{{ item.plannedQuantity ?? '--' }} 吨</text>
|
||||
</view>
|
||||
<view class="db-card__row">
|
||||
<text class="db-card__label">状态</text>
|
||||
<db-tag :type="statusType(execStatus(item))">
|
||||
{{ execStatus(item) }}
|
||||
</db-tag>
|
||||
</view>
|
||||
<view class="db-card__foot" @click.stop>
|
||||
<db-btn size="small" type="primary" plain @click="viewRelated(item)">查看</db-btn>
|
||||
<db-btn size="small" type="primary" plain @click="editItem(item)">编辑</db-btn>
|
||||
<db-btn size="small" type="primary" @click="genInventory(item)">生成出入库单</db-btn>
|
||||
<db-btn size="small" type="danger" plain @click="removeItem(item)">删除</db-btn>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="loading" class="db-empty">加载中...</view>
|
||||
</view>
|
||||
|
||||
<db-popup :show="relatedOpen" height="75vh" @close="relatedOpen = false" @update:show="(v) => (relatedOpen = v)">
|
||||
<view style="padding: 16px; max-height: 75vh; overflow: auto">
|
||||
<view class="db-header__title" style="margin-bottom: 12px">关联数据</view>
|
||||
<view v-if="relatedLoading" class="db-empty">加载中...</view>
|
||||
<view v-else-if="!mergedRelated.length" class="db-empty">暂无关联数据</view>
|
||||
<view v-for="(row, idx) in mergedRelated" :key="idx" class="db-card">
|
||||
<db-tag :type="row.type === 'inventory' ? 'primary' : 'warning'">
|
||||
{{ row.type === 'inventory' ? '出入库单' : '地磅单' }}
|
||||
</db-tag>
|
||||
<view class="db-card__row" style="margin-top: 8px">
|
||||
<text class="db-card__label">业务</text>
|
||||
<text>{{ row.businessType || '--' }}</text>
|
||||
</view>
|
||||
<view class="db-card__row">
|
||||
<text class="db-card__label">产品</text>
|
||||
<text>{{ row.product || row.cargoName || '--' }}</text>
|
||||
</view>
|
||||
<view class="db-card__row">
|
||||
<text class="db-card__label">车牌</text>
|
||||
<text>{{ row.vehicleNo || '--' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</db-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { listPlans } from '@/api/db'
|
||||
import { planStatusLabel, dash, formatWeight, filterByKeyword } from '@/utils/dbLabels'
|
||||
import { PAGE_PATHS } from '@/config/api'
|
||||
import { getSafeAreaStyle } from '@/utils/safeArea'
|
||||
import { planApi } from '@/pages/db/api/db'
|
||||
import { formatYmd, addDays, weekRange } from '@/pages/db/utils/dbDate'
|
||||
import DbSearch from '@/components/db-ui/db-search.vue'
|
||||
import DbTag from '@/components/db-ui/db-tag.vue'
|
||||
import DbBtn from '@/components/db-ui/db-btn.vue'
|
||||
import DbPopup from '@/components/db-ui/db-popup.vue'
|
||||
|
||||
const tabs = [
|
||||
{ key: 'all', label: '全部' },
|
||||
{ key: '入库', label: '入库' },
|
||||
{ key: '出库', label: '出库' }
|
||||
const safeAreaStyle = getSafeAreaStyle()
|
||||
const keyword = ref('')
|
||||
const dateFilter = ref('all')
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const relatedOpen = ref(false)
|
||||
const relatedLoading = ref(false)
|
||||
const relatedData = ref({ inventories: [], weighings: [] })
|
||||
let debounce = null
|
||||
|
||||
const dateFilters = [
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '今日', value: 'today' },
|
||||
{ label: '明日', value: 'tomorrow' },
|
||||
{ label: '昨日', value: 'yesterday' },
|
||||
{ label: '本周', value: 'week' }
|
||||
]
|
||||
|
||||
const activeTab = ref('all')
|
||||
const keyword = ref('')
|
||||
const rawList = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
const list = computed(() => {
|
||||
let rows = rawList.value
|
||||
if (activeTab.value !== 'all') {
|
||||
rows = rows.filter((r) => r.businessType === activeTab.value)
|
||||
}
|
||||
return filterByKeyword(rows, keyword.value, [
|
||||
'planNo',
|
||||
'product',
|
||||
'customerName',
|
||||
'vehicleNo',
|
||||
'executionStatus'
|
||||
]).slice(0, 80)
|
||||
const mergedRelated = computed(() => {
|
||||
const items = []
|
||||
;(relatedData.value.inventories || []).forEach((inv) => {
|
||||
items.push({ ...inv, type: 'inventory', sortTime: inv.createdAt || inv.date || '' })
|
||||
})
|
||||
;(relatedData.value.weighings || []).forEach((w) => {
|
||||
items.push({ ...w, type: 'weighing', sortTime: w.grossWeightTime || w.createdAt || '' })
|
||||
})
|
||||
return items.sort((a, b) => {
|
||||
const ta = a.sortTime ? new Date(a.sortTime).getTime() : 0
|
||||
const tb = b.sortTime ? new Date(b.sortTime).getTime() : 0
|
||||
if (!ta && !tb) return 0
|
||||
if (!ta) return 1
|
||||
if (!tb) return -1
|
||||
return tb - ta
|
||||
})
|
||||
})
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack({
|
||||
fail: () => uni.switchTab({ url: PAGE_PATHS.workbench })
|
||||
})
|
||||
function execStatus(row) {
|
||||
if (!row.executionStatus) return row.businessType === '入库' ? '待入库' : '待出库'
|
||||
if (row.businessType === '入库') {
|
||||
if (['待出库', '出库中', '已出库'].includes(row.executionStatus)) {
|
||||
return (row.executedQuantity || 0) > 0 ? '入库中' : '待入库'
|
||||
}
|
||||
} else if (['待入库', '入库中', '已入库'].includes(row.executionStatus)) {
|
||||
return (row.executedQuantity || 0) > 0 ? '出库中' : '待出库'
|
||||
}
|
||||
return row.executionStatus
|
||||
}
|
||||
|
||||
const goDetail = (row) =>
|
||||
uni.navigateTo({ url: `${PAGE_PATHS.dbPlansDetail}?id=${row.id}` })
|
||||
function statusType(status) {
|
||||
if (status === '已入库' || status === '已出库') return 'success'
|
||||
if (status === '入库中' || status === '出库中') return 'primary'
|
||||
if (status === '待入库' || status === '待出库') return 'warning'
|
||||
return 'default'
|
||||
}
|
||||
|
||||
const load = async () => {
|
||||
function buildParams() {
|
||||
const params = { q: keyword.value || undefined }
|
||||
const today = new Date()
|
||||
if (dateFilter.value === 'today') {
|
||||
const d = formatYmd(today)
|
||||
params.createdFrom = d
|
||||
params.createdTo = d
|
||||
} else if (dateFilter.value === 'tomorrow') {
|
||||
const t = formatYmd(addDays(today, 1))
|
||||
params.createdFrom = t
|
||||
params.createdTo = t
|
||||
} else if (dateFilter.value === 'yesterday') {
|
||||
const y = formatYmd(addDays(today, -1))
|
||||
params.createdFrom = y
|
||||
params.createdTo = y
|
||||
} else if (dateFilter.value === 'week') {
|
||||
const w = weekRange(today)
|
||||
params.createdFrom = w.from
|
||||
params.createdTo = w.to
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
async function reload() {
|
||||
loading.value = true
|
||||
try {
|
||||
rawList.value = await listPlans()
|
||||
list.value = (await planApi.list(buildParams())) || []
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '加载失败', icon: 'none' })
|
||||
list.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const switchTab = (key) => {
|
||||
activeTab.value = key
|
||||
function onKeyword(v) {
|
||||
keyword.value = typeof v === 'string' ? v : keyword.value
|
||||
if (debounce) clearTimeout(debounce)
|
||||
debounce = setTimeout(reload, 300)
|
||||
}
|
||||
|
||||
onShow(() => load())
|
||||
function onClear() {
|
||||
keyword.value = ''
|
||||
reload()
|
||||
}
|
||||
|
||||
function setFilter(v) {
|
||||
if (dateFilter.value === v) return
|
||||
dateFilter.value = v
|
||||
reload()
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack({ fail: () => uni.reLaunch({ url: PAGE_PATHS.workbench }) })
|
||||
}
|
||||
|
||||
function editItem(item) {
|
||||
uni.navigateTo({ url: `${PAGE_PATHS.dbPlansDetail}?id=${item.id}&mode=edit` })
|
||||
}
|
||||
|
||||
function viewItem(item) {
|
||||
uni.navigateTo({ url: `${PAGE_PATHS.dbPlansDetail}?id=${item.id}&mode=view` })
|
||||
}
|
||||
|
||||
async function viewRelated(item) {
|
||||
relatedOpen.value = true
|
||||
relatedLoading.value = true
|
||||
try {
|
||||
relatedData.value = (await planApi.getRelated(item.id)) || { inventories: [], weighings: [] }
|
||||
} catch (e) {
|
||||
relatedOpen.value = false
|
||||
} finally {
|
||||
relatedLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function genInventory(item) {
|
||||
const q = [
|
||||
`planNo=${encodeURIComponent(item.planNo || '')}`,
|
||||
`product=${encodeURIComponent(item.product || '')}`,
|
||||
`productId=${encodeURIComponent(item.productId || '')}`,
|
||||
`businessType=${encodeURIComponent(item.businessType || '')}`,
|
||||
`customerName=${encodeURIComponent(item.customerName || '')}`,
|
||||
`purchasePlanId=${encodeURIComponent(item.id)}`,
|
||||
'from=plan',
|
||||
'mode=create'
|
||||
].join('&')
|
||||
uni.navigateTo({ url: `${PAGE_PATHS.dbInventoryDetail}?${q}` })
|
||||
}
|
||||
|
||||
function removeItem(item) {
|
||||
uni.showModal({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这个计划吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return
|
||||
try {
|
||||
await planApi.delete(item.id)
|
||||
uni.showToast({ title: '删除成功', icon: 'success' })
|
||||
reload()
|
||||
} catch (e) {
|
||||
// toast by request
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onShow(reload)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="mp-page mp-page--sub">
|
||||
<view class="mp-header mp-header--db">
|
||||
<text class="mp-subpage-back" @click="goBack">← 工作台</text>
|
||||
<text class="mp-header__title" style="margin-top: 16rpx">地磅计划</text>
|
||||
<view class="mp-header__search">
|
||||
<view class="mp-header__search-dot" />
|
||||
<input
|
||||
v-model="keyword"
|
||||
class="mp-header__search-input"
|
||||
placeholder="计划号 / 产品 / 客户 / 车牌"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mp-section" style="padding-bottom: 0">
|
||||
<scroll-view scroll-x class="mp-tabs">
|
||||
<view
|
||||
v-for="t in tabs"
|
||||
:key="t.key"
|
||||
class="mp-tab"
|
||||
:class="{ 'mp-tab--active': activeTab === t.key }"
|
||||
@click="switchTab(t.key)"
|
||||
>
|
||||
{{ t.label }}
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<view class="mp-section">
|
||||
<mp-empty v-if="!list.length && !loading" title="暂无计划" />
|
||||
<view v-else class="mp-list">
|
||||
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
|
||||
<view class="mp-card__head">
|
||||
<view>
|
||||
<text class="mp-card__title">{{ dash(row.planNo) }}</text>
|
||||
<text class="mp-card__sub">{{ dash(row.product) }} · {{ dash(row.businessType) }}</text>
|
||||
</view>
|
||||
<text class="mp-tag" :class="'mp-tag--' + planStatusLabel(row.executionStatus).tag">
|
||||
{{ planStatusLabel(row.executionStatus).text }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="mp-card__row">
|
||||
<text>计划量</text>
|
||||
<text class="mp-card__row-val">{{ formatWeight(row.plannedQuantity) }}</text>
|
||||
</view>
|
||||
<view class="mp-card__row">
|
||||
<text>已执行</text>
|
||||
<text class="mp-card__row-val">{{ formatWeight(row.executedQuantity) }}</text>
|
||||
</view>
|
||||
<view class="mp-card__row">
|
||||
<text>客户</text>
|
||||
<text class="mp-card__row-val">{{ dash(row.customerName) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="loading" class="mp-list-loading">加载中…</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
@import '@/styles/db-theme.scss';
|
||||
</style>
|
||||
@@ -0,0 +1,33 @@
|
||||
/** 地磅页日期工具(避免引入 dayjs) */
|
||||
export function formatYmd(d = new Date()) {
|
||||
const x = d instanceof Date ? d : new Date(d)
|
||||
if (Number.isNaN(x.getTime())) return ''
|
||||
const y = x.getFullYear()
|
||||
const m = String(x.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(x.getDate()).padStart(2, '0')
|
||||
return `${y}-${m}-${day}`
|
||||
}
|
||||
|
||||
export function addDays(base, n) {
|
||||
const d = new Date(base)
|
||||
d.setDate(d.getDate() + n)
|
||||
return d
|
||||
}
|
||||
|
||||
/** 本周周一~周日 YYYY-MM-DD */
|
||||
export function weekRange(base = new Date()) {
|
||||
const today = new Date(base)
|
||||
const day = today.getDay() || 7
|
||||
const monday = addDays(today, 1 - day)
|
||||
const sunday = addDays(monday, 6)
|
||||
return { from: formatYmd(monday), to: formatYmd(sunday) }
|
||||
}
|
||||
|
||||
export function nowYmdHms() {
|
||||
const d = new Date()
|
||||
const ymd = formatYmd(d)
|
||||
const hh = String(d.getHours()).padStart(2, '0')
|
||||
const mm = String(d.getMinutes()).padStart(2, '0')
|
||||
const ss = String(d.getSeconds()).padStart(2, '0')
|
||||
return `${ymd} ${hh}:${mm}:${ss}`
|
||||
}
|
||||
@@ -1,157 +1,335 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||
import { getWeighing, updateWeighing, voidWeighing } from '@/api/db'
|
||||
import { weighingPhase, dash, formatWeight, nowYmdHms, todayYmd } from '@/utils/dbLabels'
|
||||
import { PAGE_PATHS } from '@/config/api'
|
||||
|
||||
const id = ref('')
|
||||
const detail = ref(null)
|
||||
const showSecond = ref(false)
|
||||
const tareWeight = ref('')
|
||||
const acting = ref(false)
|
||||
|
||||
const phase = computed(() => weighingPhase(detail.value))
|
||||
const canSecond = computed(() => phase.value.text === '一次过磅')
|
||||
const canVoid = computed(() => {
|
||||
if (!detail.value || Number(detail.value.isVoided) === 1) return false
|
||||
const t = String(detail.value.grossWeightTime || detail.value.createdAt || '')
|
||||
return t.startsWith(todayYmd())
|
||||
})
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack({
|
||||
fail: () => uni.navigateTo({ url: PAGE_PATHS.dbWeighingList })
|
||||
})
|
||||
}
|
||||
|
||||
const load = async () => {
|
||||
if (!id.value) return
|
||||
try {
|
||||
detail.value = await getWeighing(id.value)
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '加载失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const openSecond = () => {
|
||||
tareWeight.value = ''
|
||||
showSecond.value = true
|
||||
}
|
||||
|
||||
const submitSecond = async () => {
|
||||
const tare = Number(tareWeight.value)
|
||||
if (!tare || tare <= 0) {
|
||||
uni.showToast({ title: '请填写皮重', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const gross = Number(detail.value?.grossWeight) || 0
|
||||
const deduction = Number(detail.value?.deduction) || 0
|
||||
const net = Math.max(gross - tare - deduction, 0)
|
||||
acting.value = true
|
||||
try {
|
||||
await updateWeighing(id.value, {
|
||||
tareWeight: tare,
|
||||
tareWeightTime: nowYmdHms(),
|
||||
netWeight: net
|
||||
})
|
||||
uni.showToast({ title: '二次过磅已保存', icon: 'success' })
|
||||
showSecond.value = false
|
||||
load()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '保存失败', icon: 'none' })
|
||||
} finally {
|
||||
acting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const onVoid = () => {
|
||||
uni.showModal({
|
||||
title: '作废地磅单',
|
||||
content: '仅支持作废当天单据,确认作废?',
|
||||
success: async (r) => {
|
||||
if (!r.confirm) return
|
||||
acting.value = true
|
||||
try {
|
||||
await voidWeighing(id.value)
|
||||
uni.showToast({ title: '已作废', icon: 'success' })
|
||||
load()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '作废失败', icon: 'none' })
|
||||
} finally {
|
||||
acting.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onLoad((q) => {
|
||||
id.value = q?.id || ''
|
||||
})
|
||||
onShow(() => load())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="mp-page mp-page--sub mp-page--with-bottom">
|
||||
<view class="mp-header mp-header--db">
|
||||
<text class="mp-subpage-back" @click="goBack">← 返回</text>
|
||||
<text class="mp-header__title" style="margin-top: 16rpx">地磅单详情</text>
|
||||
</view>
|
||||
|
||||
<view v-if="detail" class="mp-detail-hero">
|
||||
<view class="mp-detail-hero__top">
|
||||
<view>
|
||||
<text class="mp-detail-hero__title">{{ dash(detail.vehicleNo) }}</text>
|
||||
<text class="mp-detail-hero__sub">{{ dash(detail.cargoName) }} · {{ dash(detail.businessType) }}</text>
|
||||
<view class="db-page">
|
||||
<view class="db-header" :style="safeAreaStyle">
|
||||
<view class="db-header__nav">
|
||||
<view class="db-header__left">
|
||||
<text class="db-header__back" @click="goBack">←</text>
|
||||
<text class="db-header__title">{{ title }}</text>
|
||||
</view>
|
||||
<text class="mp-tag" :class="'mp-tag--' + phase.tag">{{ phase.text }}</text>
|
||||
</view>
|
||||
<view class="mp-detail-hero__meta">
|
||||
<text class="mp-detail-hero__chip">净重 {{ formatWeight(detail.netWeight) }}</text>
|
||||
<text class="mp-detail-hero__chip">{{ dash(detail.batchNo) }}</text>
|
||||
<view />
|
||||
<view />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mp-block">
|
||||
<text class="mp-block__title">过磅数据</text>
|
||||
<view class="mp-block__panel">
|
||||
<view class="mp-kv"><text class="mp-kv__label">毛重</text><text class="mp-kv__value">{{ formatWeight(detail?.grossWeight) }}</text></view>
|
||||
<view class="mp-kv"><text class="mp-kv__label">毛重时间</text><text class="mp-kv__value">{{ dash(detail?.grossWeightTime) }}</text></view>
|
||||
<view class="mp-kv"><text class="mp-kv__label">皮重</text><text class="mp-kv__value">{{ formatWeight(detail?.tareWeight) }}</text></view>
|
||||
<view class="mp-kv"><text class="mp-kv__label">皮重时间</text><text class="mp-kv__value">{{ dash(detail?.tareWeightTime) }}</text></view>
|
||||
<view class="mp-kv"><text class="mp-kv__label">扣重</text><text class="mp-kv__value">{{ formatWeight(detail?.deduction) }}</text></view>
|
||||
<view class="mp-kv"><text class="mp-kv__label">净重</text><text class="mp-kv__value">{{ formatWeight(detail?.netWeight) }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mp-block">
|
||||
<text class="mp-block__title">业务信息</text>
|
||||
<view class="mp-block__panel">
|
||||
<view class="mp-kv"><text class="mp-kv__label">计划号</text><text class="mp-kv__value">{{ dash(detail?.planNo) }}</text></view>
|
||||
<view class="mp-kv"><text class="mp-kv__label">规格</text><text class="mp-kv__value">{{ dash(detail?.specification) }}</text></view>
|
||||
<view class="mp-kv"><text class="mp-kv__label">供货单位</text><text class="mp-kv__value">{{ dash(detail?.supplier) }}</text></view>
|
||||
<view class="mp-kv"><text class="mp-kv__label">收货单位</text><text class="mp-kv__value">{{ dash(detail?.consignee) }}</text></view>
|
||||
<view class="mp-kv"><text class="mp-kv__label">过磅点</text><text class="mp-kv__value">{{ dash(detail?.weighingPoint) }}</text></view>
|
||||
<view class="mp-kv"><text class="mp-kv__label">备注</text><text class="mp-kv__value">{{ dash(detail?.remarks) }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="showSecond" class="mp-form-panel">
|
||||
<view class="mp-form-row">
|
||||
<text class="mp-form-label mp-form-label--req">皮重(kg)</text>
|
||||
<input v-model="tareWeight" class="mp-form-input" type="digit" placeholder="二次过磅皮重" />
|
||||
</view>
|
||||
<view class="mp-form-row" style="display: flex; gap: 16rpx">
|
||||
<view class="mp-btn mp-btn--ghost" style="flex: 1" @click="showSecond = false">取消</view>
|
||||
<view class="mp-btn mp-btn--primary" style="flex: 1" @click="submitSecond">
|
||||
{{ acting ? '…' : '确认保存' }}
|
||||
<view class="db-form-section">
|
||||
<view class="db-form-item">
|
||||
<view class="db-form-label">车牌号 <text class="req">*</text></view>
|
||||
<view v-if="isView" class="db-form-value">{{ form.vehicleNo || '--' }}</view>
|
||||
<view v-else class="db-form-value" @click="showVehicle = true">
|
||||
<text :class="{ ph: !form.vehicleNo }">{{ form.vehicleNo || '请选择车牌号' }}</text>
|
||||
<text>›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="db-form-item">
|
||||
<view class="db-form-label">货物名称 <text class="req">*</text></view>
|
||||
<view v-if="isView" class="db-form-value">{{ cargoDisplay || '--' }}</view>
|
||||
<view v-else class="db-form-value" @click="showCargo = true">
|
||||
<text :class="{ ph: !cargoDisplay }">{{ cargoDisplay || '请选择货物名称' }}</text>
|
||||
<text>›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="db-form-item">
|
||||
<view class="db-form-label">业务类型 <text class="req">*</text></view>
|
||||
<db-radio-group v-model="form.businessType" :disabled="isView" />
|
||||
</view>
|
||||
|
||||
<view class="db-form-item">
|
||||
<view class="db-form-label">供货单位 <text class="req">*</text></view>
|
||||
<view v-if="isView" class="db-form-value">{{ form.supplier || '--' }}</view>
|
||||
<view v-else class="db-form-value" @click="pickSupplier = true">
|
||||
<text :class="{ ph: !form.supplier }">{{ form.supplier || '请选择供货单位' }}</text>
|
||||
<text>›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="db-form-item">
|
||||
<view class="db-form-label">收货单位 <text class="req">*</text></view>
|
||||
<view v-if="isView" class="db-form-value">{{ form.consignee || '--' }}</view>
|
||||
<view v-else class="db-form-value" @click="pickConsignee = true">
|
||||
<text :class="{ ph: !form.consignee }">{{ form.consignee || '请选择收货单位' }}</text>
|
||||
<text>›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="db-form-item">
|
||||
<view class="db-form-label">毛重(kg)</view>
|
||||
<view v-if="isView" class="db-form-value">{{ form.grossWeight ?? '--' }}</view>
|
||||
<db-field
|
||||
v-else
|
||||
v-model="form.grossWeight"
|
||||
type="digit"
|
||||
placeholder="请输入毛重"
|
||||
@change="onGross"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="db-form-item">
|
||||
<view class="db-form-label">毛重时间</view>
|
||||
<view v-if="isView" class="db-form-value">{{ form.grossWeightTime || '--' }}</view>
|
||||
<view v-else class="db-form-value" @click="fillGrossTime">
|
||||
<text :class="{ ph: !form.grossWeightTime }">{{ form.grossWeightTime || '点此填当前时间' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="db-form-item">
|
||||
<view class="db-form-label">皮重(kg)</view>
|
||||
<view v-if="isView" class="db-form-value">{{ form.tareWeight ?? '--' }}</view>
|
||||
<db-field
|
||||
v-else
|
||||
v-model="form.tareWeight"
|
||||
type="digit"
|
||||
placeholder="请输入皮重"
|
||||
@change="onTare"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="db-form-item">
|
||||
<view class="db-form-label">皮重时间</view>
|
||||
<view v-if="isView" class="db-form-value">{{ form.tareWeightTime || '--' }}</view>
|
||||
<view v-else class="db-form-value" @click="fillTareTime">
|
||||
<text :class="{ ph: !form.tareWeightTime }">{{ form.tareWeightTime || '点此填当前时间' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="db-form-item">
|
||||
<view class="db-form-label">扣杂(kg)</view>
|
||||
<view v-if="isView" class="db-form-value">{{ form.deduction || 0 }}</view>
|
||||
<db-field
|
||||
v-else
|
||||
v-model="form.deduction"
|
||||
type="digit"
|
||||
placeholder="请输入扣杂"
|
||||
@change="onDeduction"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="db-form-item">
|
||||
<view class="db-form-label">净重(kg)</view>
|
||||
<view class="db-form-value">
|
||||
<text :style="{ color: netInvalid ? '#ee0a24' : '#111827' }">
|
||||
{{ canShowNet ? netWeight : '--' }}
|
||||
</text>
|
||||
</view>
|
||||
<view v-if="netInvalid" style="color: #ee0a24; font-size: 12px; margin-top: 4px">净重必须大于0</view>
|
||||
</view>
|
||||
|
||||
<view class="db-form-item">
|
||||
<view class="db-form-label">计划号</view>
|
||||
<view v-if="isView" class="db-form-value">{{ form.planNo || '--' }}</view>
|
||||
<db-field
|
||||
v-else
|
||||
v-model="form.planNo"
|
||||
placeholder="请输入计划号"
|
||||
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="db-form-item">
|
||||
<view class="db-form-label">备注</view>
|
||||
<view v-if="isView" class="db-form-value">{{ form.remarks || '--' }}</view>
|
||||
<db-field
|
||||
v-else
|
||||
v-model="form.remarks"
|
||||
type="textarea"
|
||||
autosize
|
||||
placeholder="请输入备注"
|
||||
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mp-bottom-bar">
|
||||
<view v-if="canSecond" class="mp-btn mp-btn--primary" @click="openSecond">二次过磅</view>
|
||||
<view v-if="canVoid" class="mp-btn mp-btn--danger" @click="onVoid">作废</view>
|
||||
<view v-if="!isView" class="db-form-actions">
|
||||
<db-btn block type="primary" :loading="saving" :disabled="netInvalid" @click="save">保存</db-btn>
|
||||
<db-btn block plain style="margin-top:12px" @click="goBack">取消</db-btn>
|
||||
</view>
|
||||
|
||||
<db-product-picker :show="showCargo" @update:show="(v) => (showCargo = v)" @select="onProduct" />
|
||||
<db-customer-picker :show="pickSupplier" @update:show="(v) => (pickSupplier = v)" @select="onSupplier" />
|
||||
<db-customer-picker :show="pickConsignee" @update:show="(v) => (pickConsignee = v)" @select="onConsignee" />
|
||||
<db-vehicle-picker :show="showVehicle" @update:show="(v) => (showVehicle = v)" @select="onVehicle" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { PAGE_PATHS } from '@/config/api'
|
||||
import { getSafeAreaStyle } from '@/utils/safeArea'
|
||||
import { weighingApi } from '@/pages/db/api/db'
|
||||
import { nowYmdHms } from '@/pages/db/utils/dbDate'
|
||||
import DbField from '@/components/db-ui/db-field.vue'
|
||||
import DbBtn from '@/components/db-ui/db-btn.vue'
|
||||
import DbRadioGroup from '@/components/db-ui/db-radio-group.vue'
|
||||
import DbCustomerPicker from '@/components/db-customer-picker/db-customer-picker.vue'
|
||||
import DbProductPicker from '@/components/db-product-picker/db-product-picker.vue'
|
||||
import DbVehiclePicker from '@/components/db-vehicle-picker/db-vehicle-picker.vue'
|
||||
|
||||
const safeAreaStyle = getSafeAreaStyle()
|
||||
const mode = ref('create')
|
||||
const id = ref('')
|
||||
const saving = ref(false)
|
||||
const showVehicle = ref(false)
|
||||
const showCargo = ref(false)
|
||||
const pickSupplier = ref(false)
|
||||
const pickConsignee = ref(false)
|
||||
const cargoDisplay = ref('')
|
||||
|
||||
const form = reactive({
|
||||
vehicleNo: '',
|
||||
planNo: '',
|
||||
productId: '',
|
||||
businessType: '入库',
|
||||
supplier: '',
|
||||
consignee: '',
|
||||
grossWeight: 0,
|
||||
grossWeightTime: '',
|
||||
tareWeight: 0,
|
||||
tareWeightTime: '',
|
||||
deduction: 0,
|
||||
netWeight: 0,
|
||||
purchasePlanId: undefined,
|
||||
inboundOutboundId: undefined,
|
||||
remarks: ''
|
||||
})
|
||||
|
||||
const isView = computed(() => mode.value === 'view')
|
||||
const isEdit = computed(() => mode.value === 'edit')
|
||||
const title = computed(() => (isEdit.value ? '编辑地磅单' : isView.value ? '查看地磅单' : '新增地磅单'))
|
||||
|
||||
const netWeight = computed(() => {
|
||||
const g = Number(form.grossWeight) || 0
|
||||
const t = Number(form.tareWeight) || 0
|
||||
const d = Number(form.deduction) || 0
|
||||
return Math.max(0, g - t - d)
|
||||
})
|
||||
const canShowNet = computed(() => Number(form.grossWeight) > 0 && Number(form.tareWeight) > 0)
|
||||
const netInvalid = computed(() => canShowNet.value && netWeight.value <= 0)
|
||||
|
||||
function calcNet() {
|
||||
form.netWeight = netWeight.value
|
||||
}
|
||||
function onGross(v) {
|
||||
form.grossWeight = Number(v) || 0
|
||||
if (form.grossWeight && !form.grossWeightTime) form.grossWeightTime = nowYmdHms()
|
||||
calcNet()
|
||||
}
|
||||
function onTare(v) {
|
||||
form.tareWeight = Number(v) || 0
|
||||
if (form.tareWeight && !form.tareWeightTime) form.tareWeightTime = nowYmdHms()
|
||||
calcNet()
|
||||
}
|
||||
function onDeduction(v) {
|
||||
form.deduction = Number(v) || 0
|
||||
calcNet()
|
||||
}
|
||||
function fillGrossTime() {
|
||||
form.grossWeightTime = nowYmdHms()
|
||||
}
|
||||
function fillTareTime() {
|
||||
form.tareWeightTime = nowYmdHms()
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack({ fail: () => uni.redirectTo({ url: PAGE_PATHS.dbWeighingList }) })
|
||||
}
|
||||
function onVehicle(plate) {
|
||||
form.vehicleNo = plate
|
||||
}
|
||||
function onProduct(item) {
|
||||
form.productId = item.id
|
||||
cargoDisplay.value = item.productName
|
||||
}
|
||||
function onSupplier(item) {
|
||||
form.supplier = item.customerName
|
||||
}
|
||||
function onConsignee(item) {
|
||||
form.consignee = item.customerName
|
||||
}
|
||||
|
||||
async function loadDetail() {
|
||||
const data = await weighingApi.get(id.value)
|
||||
Object.assign(form, {
|
||||
vehicleNo: data.vehicleNo || '',
|
||||
planNo: data.planNo || '',
|
||||
productId: data.productId || '',
|
||||
businessType: data.businessType || '入库',
|
||||
supplier: data.supplier || '',
|
||||
consignee: data.consignee || '',
|
||||
grossWeight: data.grossWeight ?? 0,
|
||||
grossWeightTime: data.grossWeightTime || '',
|
||||
tareWeight: data.tareWeight ?? 0,
|
||||
tareWeightTime: data.tareWeightTime || '',
|
||||
deduction: data.deduction ?? 0,
|
||||
netWeight: data.netWeight ?? 0,
|
||||
purchasePlanId: data.purchasePlanId,
|
||||
inboundOutboundId: data.inboundOutboundId,
|
||||
remarks: data.remarks || ''
|
||||
})
|
||||
cargoDisplay.value = data.cargoName || ''
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!form.vehicleNo || !form.productId || !form.supplier || !form.consignee) {
|
||||
uni.showToast({ title: '请完善必填项', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (netInvalid.value) {
|
||||
uni.showToast({ title: '净重必须大于0', icon: 'none' })
|
||||
return
|
||||
}
|
||||
calcNet()
|
||||
saving.value = true
|
||||
try {
|
||||
const payload = {
|
||||
vehicleNo: form.vehicleNo,
|
||||
planNo: form.planNo || undefined,
|
||||
productId: form.productId,
|
||||
businessType: form.businessType,
|
||||
supplier: form.supplier,
|
||||
consignee: form.consignee,
|
||||
grossWeight: Number(form.grossWeight) || 0,
|
||||
grossWeightTime: form.grossWeightTime || nowYmdHms(),
|
||||
tareWeight: Number(form.tareWeight) || 0,
|
||||
tareWeightTime: form.tareWeightTime || (Number(form.tareWeight) ? nowYmdHms() : ''),
|
||||
deduction: Number(form.deduction) || 0,
|
||||
netWeight: form.netWeight,
|
||||
purchasePlanId: form.purchasePlanId,
|
||||
inboundOutboundId: form.inboundOutboundId,
|
||||
remarks: form.remarks || undefined
|
||||
}
|
||||
if (id.value) await weighingApi.update(id.value, payload)
|
||||
else await weighingApi.create(payload)
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
setTimeout(goBack, 500)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onLoad(async (q) => {
|
||||
mode.value = q.mode || (q.id ? 'edit' : 'create')
|
||||
id.value = q.id || ''
|
||||
if (q.vehicleNo) form.vehicleNo = decodeURIComponent(q.vehicleNo)
|
||||
if (q.productId) form.productId = decodeURIComponent(q.productId)
|
||||
if (q.cargoName) cargoDisplay.value = decodeURIComponent(q.cargoName)
|
||||
if (q.businessType) form.businessType = decodeURIComponent(q.businessType)
|
||||
if (q.supplier) form.supplier = decodeURIComponent(q.supplier)
|
||||
if (q.consignee) form.consignee = decodeURIComponent(q.consignee)
|
||||
if (q.planNo) form.planNo = decodeURIComponent(q.planNo)
|
||||
if (q.inboundOutboundId) form.inboundOutboundId = decodeURIComponent(q.inboundOutboundId)
|
||||
if (q.purchasePlanId) form.purchasePlanId = decodeURIComponent(q.purchasePlanId)
|
||||
if (id.value) {
|
||||
try {
|
||||
await loadDetail()
|
||||
} catch (e) {
|
||||
// handled
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '@/styles/db-theme.scss';
|
||||
</style>
|
||||
@@ -1,168 +1,256 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { listWeighing } from '@/api/db'
|
||||
import {
|
||||
weighingPhase,
|
||||
dash,
|
||||
formatWeight,
|
||||
filterByKeyword,
|
||||
todayYmd
|
||||
} from '@/utils/dbLabels'
|
||||
import { PAGE_PATHS } from '@/config/api'
|
||||
<template>
|
||||
<view class="db-page">
|
||||
<view class="db-header" :style="safeAreaStyle">
|
||||
<view class="db-header__nav">
|
||||
<view class="db-header__left">
|
||||
<text class="db-header__back" @click="goBack">←</text>
|
||||
<text class="db-header__title">地磅单管理</text>
|
||||
</view>
|
||||
<view />
|
||||
<view />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
const tabs = [
|
||||
{ key: 'today', label: '今日' },
|
||||
{ key: 'second', label: '待二次' },
|
||||
{ key: 'all', label: '全部' }
|
||||
<view class="db-search-wrap">
|
||||
<db-search
|
||||
v-model="keyword"
|
||||
placeholder="搜索车牌号"
|
||||
@change="onKeyword"
|
||||
@search="reload"
|
||||
@clear="onClear"
|
||||
/>
|
||||
</view>
|
||||
<view class="db-filter-bar">
|
||||
<view
|
||||
v-for="item in dateFilters"
|
||||
:key="item.value"
|
||||
class="db-filter-chip"
|
||||
:class="{ 'db-filter-chip--active': dateFilter === item.value }"
|
||||
@click="setFilter(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="db-list">
|
||||
<view v-if="!loading && !list.length" class="db-empty">暂无数据</view>
|
||||
<view v-for="item in list" :key="item.id" class="db-card" @click="viewItem(item)">
|
||||
<view class="db-card__head">
|
||||
<text class="db-card__title">{{ item.vehicleNo || '未填车牌' }}</text>
|
||||
<db-tag v-if="item.isVoided" type="danger">已作废</db-tag>
|
||||
<db-tag v-else :type="item.businessType === '入库' ? 'success' : 'warning'">
|
||||
{{ item.businessType }}
|
||||
</db-tag>
|
||||
</view>
|
||||
<view class="db-card__row">
|
||||
<text class="db-card__label">货物</text>
|
||||
<text>{{ item.cargoName || '--' }}</text>
|
||||
</view>
|
||||
<view class="db-card__row">
|
||||
<text class="db-card__label">供货单位</text>
|
||||
<text>{{ item.supplier || '--' }}</text>
|
||||
</view>
|
||||
<view class="db-card__row">
|
||||
<text class="db-card__label">毛重/皮重</text>
|
||||
<text>{{ item.grossWeight ?? '--' }} / {{ item.tareWeight ?? '--' }} kg</text>
|
||||
</view>
|
||||
<view class="db-card__row">
|
||||
<text class="db-card__label">净重</text>
|
||||
<text>{{ item.netWeight ?? '--' }} kg</text>
|
||||
</view>
|
||||
<view class="db-card__foot" @click.stop>
|
||||
<db-btn size="small" type="primary" plain @click="viewRelated(item)">关联</db-btn>
|
||||
<db-btn
|
||||
v-if="!item.isVoided"
|
||||
size="small"
|
||||
type="primary"
|
||||
|
||||
@click="editItem(item)"
|
||||
>
|
||||
{{ needsSecond(item) ? '二次过磅' : '编辑' }}
|
||||
</db-btn>
|
||||
<db-btn
|
||||
v-if="!item.isVoided"
|
||||
size="small"
|
||||
type="danger"
|
||||
|
||||
@click="voidItem(item)"
|
||||
>
|
||||
作废
|
||||
</db-btn>
|
||||
<db-btn size="small" type="danger" plain @click="removeItem(item)">删除</db-btn>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="loading" class="db-empty">加载中...</view>
|
||||
</view>
|
||||
|
||||
<db-popup :show="relatedOpen" height="75vh" @close="relatedOpen = false" @update:show="(v) => (relatedOpen = v)">
|
||||
<view style="padding: 16px">
|
||||
<view class="db-header__title" style="margin-bottom: 12px">关联数据</view>
|
||||
<view v-if="relatedLoading" class="db-empty">加载中...</view>
|
||||
<template v-else>
|
||||
<view v-if="related.plan" class="db-card">
|
||||
<db-tag type="primary">计划</db-tag>
|
||||
<view class="db-card__row" style="margin-top: 8px">
|
||||
<text class="db-card__label">计划号</text>
|
||||
<text>{{ related.plan.planNo || '--' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="related.inventory" class="db-card">
|
||||
<db-tag type="success">出入库</db-tag>
|
||||
<view class="db-card__row" style="margin-top: 8px">
|
||||
<text class="db-card__label">车牌</text>
|
||||
<text>{{ related.inventory.vehicleNo || '--' }}</text>
|
||||
</view>
|
||||
<view class="db-card__row">
|
||||
<text class="db-card__label">数量</text>
|
||||
<text>{{ related.inventory.quantity ?? '--' }} 吨</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="!related.plan && !related.inventory" class="db-empty">暂无关联数据</view>
|
||||
</template>
|
||||
</view>
|
||||
</db-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { PAGE_PATHS } from '@/config/api'
|
||||
import { getSafeAreaStyle } from '@/utils/safeArea'
|
||||
import { weighingApi } from '@/pages/db/api/db'
|
||||
import { formatYmd, addDays, weekRange } from '@/pages/db/utils/dbDate'
|
||||
import DbSearch from '@/components/db-ui/db-search.vue'
|
||||
import DbTag from '@/components/db-ui/db-tag.vue'
|
||||
import DbBtn from '@/components/db-ui/db-btn.vue'
|
||||
import DbPopup from '@/components/db-ui/db-popup.vue'
|
||||
|
||||
const safeAreaStyle = getSafeAreaStyle()
|
||||
const keyword = ref('')
|
||||
const dateFilter = ref('all')
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const relatedOpen = ref(false)
|
||||
const relatedLoading = ref(false)
|
||||
const related = ref({ plan: null, inventory: null })
|
||||
let debounce = null
|
||||
|
||||
const dateFilters = [
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '今日', value: 'today' },
|
||||
{ label: '明日', value: 'tomorrow' },
|
||||
{ label: '昨日', value: 'yesterday' },
|
||||
{ label: '本周', value: 'week' }
|
||||
]
|
||||
|
||||
const activeTab = ref('today')
|
||||
const keyword = ref('')
|
||||
const rawList = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
const stats = computed(() => {
|
||||
const today = todayYmd()
|
||||
const todayRows = rawList.value.filter((w) => {
|
||||
const t = String(w.grossWeightTime || w.createdAt || '')
|
||||
return t.startsWith(today) && Number(w.isVoided) !== 1
|
||||
})
|
||||
const needSecond = todayRows.filter((w) => weighingPhase(w).text === '一次过磅').length
|
||||
return {
|
||||
today: todayRows.length,
|
||||
needSecond
|
||||
}
|
||||
})
|
||||
|
||||
const list = computed(() => {
|
||||
let rows = rawList.value
|
||||
const today = todayYmd()
|
||||
if (activeTab.value === 'today') {
|
||||
rows = rows.filter((w) => String(w.grossWeightTime || w.createdAt || '').startsWith(today))
|
||||
} else if (activeTab.value === 'second') {
|
||||
rows = rows.filter((w) => weighingPhase(w).text === '一次过磅')
|
||||
}
|
||||
return filterByKeyword(rows, keyword.value, [
|
||||
'vehicleNo',
|
||||
'cargoName',
|
||||
'planNo',
|
||||
'batchNo',
|
||||
'supplier',
|
||||
'consignee'
|
||||
]).slice(0, 80)
|
||||
})
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack({
|
||||
fail: () => uni.switchTab({ url: PAGE_PATHS.workbench })
|
||||
})
|
||||
function needsSecond(item) {
|
||||
return Number(item.grossWeight) > 0 && (!item.tareWeight || Number(item.tareWeight) === 0)
|
||||
}
|
||||
|
||||
const goDetail = (row) =>
|
||||
uni.navigateTo({ url: `${PAGE_PATHS.dbWeighingDetail}?id=${row.id}` })
|
||||
function buildParams() {
|
||||
const params = {}
|
||||
if (keyword.value) params.vehicleNo = keyword.value
|
||||
const today = new Date()
|
||||
if (dateFilter.value === 'today') {
|
||||
const d = formatYmd(today)
|
||||
params.dateFrom = d
|
||||
params.dateTo = d
|
||||
} else if (dateFilter.value === 'tomorrow') {
|
||||
const t = formatYmd(addDays(today, 1))
|
||||
params.dateFrom = t
|
||||
params.dateTo = t
|
||||
} else if (dateFilter.value === 'yesterday') {
|
||||
const y = formatYmd(addDays(today, -1))
|
||||
params.dateFrom = y
|
||||
params.dateTo = y
|
||||
} else if (dateFilter.value === 'week') {
|
||||
const w = weekRange(today)
|
||||
params.dateFrom = w.from
|
||||
params.dateTo = w.to
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
const load = async () => {
|
||||
async function reload() {
|
||||
loading.value = true
|
||||
try {
|
||||
// 今日优先带日期参数;全部再拉全量(数据量大时可能慢)
|
||||
if (activeTab.value === 'all') {
|
||||
rawList.value = await listWeighing()
|
||||
} else {
|
||||
const today = todayYmd()
|
||||
rawList.value = await listWeighing({ dateFrom: today, dateTo: today })
|
||||
// 待二次若今日无,再尝试不带日期限制的一次过磅(限制体验:仅今日+已加载)
|
||||
if (activeTab.value === 'second' && !rawList.value.some((w) => weighingPhase(w).text === '一次过磅')) {
|
||||
const all = await listWeighing()
|
||||
rawList.value = all
|
||||
}
|
||||
}
|
||||
const records = (await weighingApi.list(buildParams())) || []
|
||||
const getDate = (r) =>
|
||||
r.inventoryDate ||
|
||||
(r.grossWeightTime ? String(r.grossWeightTime).split(' ')[0] : '') ||
|
||||
(r.createdAt ? String(r.createdAt).split(' ')[0] : '') ||
|
||||
''
|
||||
list.value = records.slice().sort((a, b) => getDate(b).localeCompare(getDate(a)))
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '加载失败', icon: 'none' })
|
||||
list.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const switchTab = (key) => {
|
||||
activeTab.value = key
|
||||
load()
|
||||
function onKeyword(v) {
|
||||
keyword.value = typeof v === 'string' ? v : keyword.value
|
||||
if (debounce) clearTimeout(debounce)
|
||||
debounce = setTimeout(reload, 300)
|
||||
}
|
||||
function onClear() {
|
||||
keyword.value = ''
|
||||
reload()
|
||||
}
|
||||
function setFilter(v) {
|
||||
if (dateFilter.value === v) return
|
||||
dateFilter.value = v
|
||||
reload()
|
||||
}
|
||||
function goBack() {
|
||||
uni.navigateBack({ fail: () => uni.reLaunch({ url: PAGE_PATHS.workbench }) })
|
||||
}
|
||||
function editItem(item) {
|
||||
uni.navigateTo({ url: `${PAGE_PATHS.dbWeighingDetail}?id=${item.id}&mode=edit` })
|
||||
}
|
||||
function viewItem(item) {
|
||||
uni.navigateTo({ url: `${PAGE_PATHS.dbWeighingDetail}?id=${item.id}&mode=view` })
|
||||
}
|
||||
async function viewRelated(item) {
|
||||
relatedOpen.value = true
|
||||
relatedLoading.value = true
|
||||
try {
|
||||
related.value = (await weighingApi.getRelated(item.id)) || { plan: null, inventory: null }
|
||||
} catch (e) {
|
||||
relatedOpen.value = false
|
||||
} finally {
|
||||
relatedLoading.value = false
|
||||
}
|
||||
}
|
||||
function voidItem(item) {
|
||||
uni.showModal({
|
||||
title: '确认作废',
|
||||
content: '确定作废该地磅单吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return
|
||||
await weighingApi.void(item.id)
|
||||
uni.showToast({ title: '已作废', icon: 'success' })
|
||||
reload()
|
||||
}
|
||||
})
|
||||
}
|
||||
function removeItem(item) {
|
||||
uni.showModal({
|
||||
title: '确认删除',
|
||||
content: '确定删除该地磅单吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return
|
||||
await weighingApi.delete(item.id)
|
||||
uni.showToast({ title: '删除成功', icon: 'success' })
|
||||
reload()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onShow(() => load())
|
||||
onShow(reload)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="mp-page mp-page--sub">
|
||||
<view class="mp-header mp-header--db">
|
||||
<text class="mp-subpage-back" @click="goBack">← 工作台</text>
|
||||
<text class="mp-header__title" style="margin-top: 16rpx">地磅单管理</text>
|
||||
<view class="mp-header__search">
|
||||
<view class="mp-header__search-dot" />
|
||||
<input
|
||||
v-model="keyword"
|
||||
class="mp-header__search-input"
|
||||
placeholder="车牌 / 货物 / 计划号"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mp-stats">
|
||||
<view class="mp-stat">
|
||||
<text class="mp-stat__num">{{ stats.today }}</text>
|
||||
<text class="mp-stat__label">今日过磅</text>
|
||||
</view>
|
||||
<view class="mp-stat">
|
||||
<text class="mp-stat__num">{{ stats.needSecond }}</text>
|
||||
<text class="mp-stat__label">待二次过磅</text>
|
||||
</view>
|
||||
<view class="mp-stat">
|
||||
<text class="mp-stat__num">{{ list.length }}</text>
|
||||
<text class="mp-stat__label">当前列表</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mp-section" style="padding-bottom: 0; padding-top: 0">
|
||||
<scroll-view scroll-x class="mp-tabs">
|
||||
<view
|
||||
v-for="t in tabs"
|
||||
:key="t.key"
|
||||
class="mp-tab"
|
||||
:class="{ 'mp-tab--active': activeTab === t.key }"
|
||||
@click="switchTab(t.key)"
|
||||
>
|
||||
{{ t.label }}
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<view class="mp-section">
|
||||
<mp-empty v-if="!list.length && !loading" title="暂无地磅单" />
|
||||
<view v-else class="mp-list">
|
||||
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
|
||||
<view class="mp-card__head">
|
||||
<view>
|
||||
<text class="mp-card__title">{{ dash(row.batchNo || row.id) }}</text>
|
||||
<text class="mp-card__sub">{{ dash(row.vehicleNo) }} · {{ dash(row.cargoName) }}</text>
|
||||
</view>
|
||||
<text class="mp-tag" :class="'mp-tag--' + weighingPhase(row).tag">
|
||||
{{ weighingPhase(row).text }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="mp-card__row">
|
||||
<text>净重</text>
|
||||
<text class="mp-card__row-val">{{ formatWeight(row.netWeight) }}</text>
|
||||
</view>
|
||||
<view class="mp-card__row">
|
||||
<text>过磅时间</text>
|
||||
<text class="mp-card__row-val">{{ dash(row.grossWeightTime) }}</text>
|
||||
</view>
|
||||
<view v-if="weighingPhase(row).text === '一次过磅'" class="mp-card__foot">
|
||||
<view class="mp-btn mp-btn--primary mp-btn--sm">二次过磅</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="loading" class="mp-list-loading">加载中…</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
@import '@/styles/db-theme.scss';
|
||||
</style>
|
||||
Reference in new issue
Block a user