fix(miniprogram): wire db pages to capsule safe-area header

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
wuzilongandCursor committed 2026-09-12 18:53:40 +08:00
1 parent b3fc308446
commit 8505115f05
9 files changed
+1666 -863

No files matched your search

+41
View File
@@ -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}`)
}
+135 -117
View File
@@ -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>
+217 -110
View File
@@ -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>
+228 -123
View File
@@ -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>
+198 -123
View File
@@ -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>
+253 -95
View File
@@ -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>
+33
View File
@@ -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}`
}
+323 -145
View File
@@ -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>
+238 -150
View File
@@ -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>