Files
yunzerwebsiteallinone/uniapp/pages/erp/basic/product-detail.vue
T
2026-10-19 01:13:17 +08:00

392 lines
8.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page">
<view v-if="loading && !product" class="state-box">
<text class="state-text">加载中...</text>
</view>
<block v-else-if="product">
<view class="hero">
<view class="hero-top">
<text class="hero-name">{{ product.product_name }}</text>
<u-tag v-if="!isOnSale" text="停用/草稿" type="info" size="mini" />
</view>
<text class="hero-code">{{ product.product_code || '无编码' }}</text>
<view class="hero-grid">
<view class="hero-cell">
<text class="cell-label">总库存</text>
<text class="cell-value">{{ qtyText(product.total_stock) }} {{ product.base_unit || '' }}</text>
</view>
<view class="hero-cell">
<text class="cell-label">库存状态</text>
<text class="cell-value">{{ safetyStateLabel(product.safety_state) }}</text>
</view>
<view class="hero-cell">
<text class="cell-label">售价</text>
<text class="cell-value">¥{{ moneyText(product.sale_price) }}</text>
</view>
</view>
</view>
<view class="card">
<view class="block-head">
<text class="block-title">商品资料</text>
</view>
<view class="line">
<text class="line-label">分类</text>
<text class="line-value">{{ product.category_name || '未分类' }}</text>
</view>
<view class="line">
<text class="line-label">品牌 / 规格</text>
<text class="line-value">{{ join([product.brand, product.spec]) }}</text>
</view>
<view class="line">
<text class="line-label">型号</text>
<text class="line-value">{{ dash(product.model) }}</text>
</view>
<view class="line">
<text class="line-label">单位</text>
<text class="line-value">
{{ dash(product.base_unit) }}
<text v-if="product.aux_unit"> / 辅 {{ product.aux_unit }} × {{ qtyText(product.aux_ratio || 1) }}</text>
</text>
</view>
<view class="line">
<text class="line-label">条码</text>
<text class="line-value">{{ dash(product.barcode) }}</text>
</view>
<view v-if="product.remark" class="remark">
<text>{{ product.remark }}</text>
</view>
</view>
<view class="card">
<view class="block-head">
<text class="block-title">价格与管控</text>
<text class="block-tip">手机端只读</text>
</view>
<view class="grid3">
<view v-for="p in priceCells" :key="p.label" class="g-cell">
<text class="cell-label">{{ p.label }}</text>
<text class="cell-value">{{ p.value }}</text>
</view>
</view>
</view>
<view class="card">
<view class="block-head">
<text class="block-title">分仓库存({{ stocks.length }} 行)</text>
<text class="block-tip">货值 ¥{{ moneyText(stockAmount) }}</text>
</view>
<view v-if="!stocks.length" class="empty-line">
<text>暂无在库库存</text>
</view>
<view v-for="(s, i) in stocks" :key="i" class="line">
<view class="line-label grow">
<text>{{ s.warehouse_name || `仓库#${s.warehouse_id}` }}</text>
<text v-if="s.batch_no" class="sub">批次 {{ s.batch_no }}</text>
</view>
<text class="line-value">{{ qtyText(s.quantity) }} · ¥{{ moneyText(s.cost_price) }}</text>
</view>
</view>
<view class="footer-space"></view>
<view class="actions">
<view class="act" @tap="go('/pages/erp/stock/ledger')">
<text>出入库流水</text>
</view>
<view class="act" @tap="go('/pages/erp/stock/batch')">
<text>批次台账</text>
</view>
<view class="act primary" @tap="go('/pages/erp/document/index')">
<text>相关单据</text>
</view>
</view>
</block>
</view>
</template>
<script setup>
/**
* 商品详情(只读):资料 / 价格 / 库存管控 / 分仓结存。
* 建档与改价一律回 Web 端,这里把「这个货现在有多少、成本多少、卖多少」讲清楚,
* 现场接单或盘库时一眼能判断能不能发。
*/
import { computed, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import {
getProduct,
moneyText,
qtyText,
safetyStateLabel
} from '@/api/erp.js'
const product = ref(null)
const loading = ref(false)
const productId = ref(0)
const stocks = computed(() => product.value?.stocks || [])
const stockAmount = computed(() => stocks.value.reduce((sum, s) => sum + Number(s.amount || 0), 0))
const isOnSale = computed(() => Number(product.value?.status) === 1 && Number(product.value?.is_draft) !== 1)
const priceCells = computed(() => {
const p = product.value || {}
return [
{ label: '采购价', value: `¥${moneyText(p.purchase_price)}` },
{ label: '销售价', value: `¥${moneyText(p.sale_price)}` },
{ label: '批发价', value: `¥${moneyText(p.wholesale_price)}` },
{ label: '会员价', value: `¥${moneyText(p.member_price)}` },
{ label: '最低售价', value: `¥${moneyText(p.min_price)}` },
{ label: '税率', value: `${Number(p.tax_rate || 0)}%` },
{ label: '安全库存', value: qtyText(p.safety_qty) },
{ label: '最高库存', value: qtyText(p.max_qty) },
{ label: '保质期', value: Number(p.shelf_life_days) > 0 ? `${p.shelf_life_days} 天` : '不管理' },
{ label: '批次管理', value: Number(p.enable_batch) === 1 ? '启用' : '关闭' }
]
})
onLoad((query = {}) => {
productId.value = Number(query.id) || 0
load()
})
async function load() {
if (!productId.value) {
uni.showToast({ title: '缺少商品 ID', icon: 'none' })
return
}
loading.value = true
try {
product.value = await getProduct(productId.value)
if (product.value?.product_name) uni.setNavigationBarTitle({ title: product.value.product_name })
} catch (e) {
uni.showToast({ title: '商品加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
function go(base) {
uni.navigateTo({ url: `${base}?product_id=${productId.value}` })
}
function dash(v) {
const s = String(v ?? '').trim()
return s || '—'
}
function join(arr) {
const s = (arr || []).filter(Boolean).join(' / ')
return s || '—'
}
</script>
<style lang="scss" scoped>
@import '@/src/styles/page-common.scss';
.page {
min-height: 100vh;
background: $color-bg-page;
padding-top: 20rpx;
}
.hero {
margin: 0 $page-padding-x 24rpx;
padding: 32rpx;
border-radius: $radius-lg;
background: $hero-gradient;
color: #ffffff;
box-shadow: $shadow-float;
}
.hero-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16rpx;
}
.hero-name {
flex: 1;
font-size: 36rpx;
font-weight: 600;
line-height: 1.4;
}
.hero-code {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
opacity: 0.85;
}
.hero-grid {
display: flex;
margin-top: 28rpx;
padding-top: 22rpx;
border-top: 1rpx solid rgba(255, 255, 255, 0.22);
}
.hero-cell {
flex: 1;
.cell-label {
display: block;
font-size: 22rpx;
opacity: 0.8;
}
.cell-value {
display: block;
margin-top: 6rpx;
font-size: 28rpx;
font-weight: 500;
}
}
.card {
@include card;
margin: 0 $page-padding-x 24rpx;
padding: 24rpx 28rpx;
}
.block-head {
@include block-head;
}
.block-title {
@include section-title;
}
.block-tip {
font-size: 22rpx;
color: $color-text-muted;
}
.line {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20rpx;
padding: 14rpx 0;
border-bottom: 1rpx solid $color-divider;
font-size: 26rpx;
&:last-child {
border-bottom: none;
}
}
.line-label {
color: $color-text-secondary;
flex-shrink: 0;
&.grow {
flex: 1;
min-width: 0;
}
}
.sub {
display: block;
margin-top: 4rpx;
font-size: 22rpx;
color: $color-text-muted;
}
.line-value {
flex: 1;
text-align: right;
color: $color-text;
word-break: break-all;
}
.grid3 {
display: flex;
flex-wrap: wrap;
}
.g-cell {
width: 33.33%;
padding: 12rpx 0;
.cell-label {
display: block;
font-size: 22rpx;
color: $color-text-muted;
}
.cell-value {
display: block;
margin-top: 4rpx;
font-size: 26rpx;
color: $color-text;
}
}
.remark {
margin-top: 12rpx;
padding: 16rpx 20rpx;
background: $color-bg-page;
border-radius: $radius-sm;
font-size: 24rpx;
line-height: 1.6;
color: $color-text-secondary;
}
.empty-line {
padding: 40rpx 0;
text-align: center;
font-size: 25rpx;
color: $color-text-muted;
}
.footer-space {
height: calc(150rpx + env(safe-area-inset-bottom));
}
.actions {
position: fixed;
left: 0;
right: 0;
bottom: 0;
display: flex;
gap: 16rpx;
padding: 18rpx $page-padding-x calc(18rpx + env(safe-area-inset-bottom));
background: $color-card;
box-shadow: 0 -4rpx 16rpx rgba(15, 23, 42, 0.06);
@include dialog-responsive;
}
.act {
flex: 1;
height: 76rpx;
line-height: 76rpx;
text-align: center;
border-radius: $radius-full;
background: $color-bg-page;
font-size: 26rpx;
color: $color-text-secondary;
&.primary {
background: #2563eb;
color: #ffffff;
}
&:active {
opacity: 0.85;
}
}
.state-box {
display: flex;
justify-content: center;
padding: 140rpx 0;
}
.state-text {
font-size: 28rpx;
color: $color-text-muted;
}
</style>