feat:添加产品物料模块,扫一扫、详情

master
黄伟杰 1 month ago
parent 41fd0348bf
commit d5710f1e87

@ -344,6 +344,20 @@
"navigationStyle": "custom"
}
},
{
"path": "product/index",
"style": {
"navigationBarTitleText": "产品物料查询",
"navigationStyle": "custom"
}
},
{
"path": "product/detail",
"style": {
"navigationBarTitleText": "产品物料详情",
"navigationStyle": "custom"
}
},
{
"path": "inspection/index",
"style": {

@ -136,7 +136,7 @@ const navList = reactive([
{ name: '设备', icon: '⚙️', bgColor: '#2d5a87', path: '/pages_function/equipment' },
{ name: '关键件', icon: '🔩', bgColor: '#3d7ab5', path: '/pages_function/keypart' },
{ name: '备件', icon: '📦', bgColor: '#4a90c2', path: '/pages_function/spare' },
{ name: '出入库', icon: '📊', bgColor: '#5aa0d2', path: '/pages_function/warehouse' }
{ name: '产品物料', icon: '🧾', bgColor: '#5aa0d2', path: '/pages_function/product' }
]);
const statsData = reactive([
@ -163,7 +163,8 @@ function handleNavClick(item) {
'模具': '/pages_function/pages/mold/index',
'设备': '/pages_function/pages/equipment/index',
'备件': '/pages_function/pages/spare/index',
'关键件': '/pages_function/pages/keypart/index'
'关键件': '/pages_function/pages/keypart/index',
'产品物料': '/pages_function/pages/product/index'
};
const url = navMap[item.name];

@ -0,0 +1,264 @@
<template>
<view class="page-container">
<view class="header-section">
<view class="back-btn" @click="goBack">
<text class="back-icon"></text>
<text class="back-text">返回</text>
</view>
<view class="header-content">
<text class="header-title">产品物料详情</text>
</view>
</view>
<view class="content-section">
<view class="info-card">
<view class="card-title">基本信息</view>
<view class="info-list">
<view class="info-row">
<text class="info-label">物料编码</text>
<text class="info-value">{{ getField('barCode') }}</text>
</view>
<view class="info-row">
<text class="info-label">物料名称</text>
<text class="info-value">{{ getField('name') }}</text>
</view>
<view class="info-row">
<text class="info-label">物料分类</text>
<text class="info-value">{{ categoryLabel }}</text>
</view>
<view class="info-row">
<text class="info-label">单位</text>
<text class="info-value">{{ unitLabel }}</text>
</view>
<view class="info-row">
<text class="info-label">规格</text>
<text class="info-value">{{ getField('standard') }}</text>
</view>
<view class="info-row">
<text class="info-label">状态</text>
<text class="info-value">{{ statusLabel }}</text>
</view>
<view class="info-row">
<text class="info-label">预警库存</text>
<text class="info-value">{{ getField('safetyNumber') }}</text>
</view>
<view class="info-row">
<text class="info-label">备注</text>
<text class="info-value">{{ getField('remark') }}</text>
</view>
<view class="info-row">
<text class="info-label">创建时间</text>
<text class="info-value">{{ createTimeLabel }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import request from '@/utils/request'
const productId = ref(undefined)
const loading = ref(false)
const detailData = ref(null)
const unitList = ref([])
function goBack() {
uni.navigateBack()
}
function detailValue(v) {
if (v === 0) return '0'
if (v === false) return '否'
if (v === true) return '是'
if (v === null || v === undefined) return '-'
const s = String(v).trim()
return s ? s : '-'
}
function getField(field) {
const d = detailData.value
return detailValue(d ? d[field] : undefined)
}
function pad2(v) {
return String(v).padStart(2, '0')
}
function formatDateTime(v) {
if (v === null || v === undefined || v === '') return '-'
const d = new Date(typeof v === 'number' ? v : String(v))
if (Number.isNaN(d.getTime())) return '-'
const yyyy = d.getFullYear()
const mm = pad2(d.getMonth() + 1)
const dd = pad2(d.getDate())
const hh = pad2(d.getHours())
const mi = pad2(d.getMinutes())
const ss = pad2(d.getSeconds())
return `${yyyy}-${mm}-${dd} ${hh}:${mi}:${ss}`
}
const createTimeLabel = computed(() => {
const d = detailData.value
return formatDateTime(d ? d.createTime : undefined)
})
const categoryLabel = computed(() => {
const d = detailData.value
if (!d) return '-'
const v = d.subCategoryName || d.categoryName
return detailValue(v)
})
const unitLabel = computed(() => {
const d = detailData.value
const explicit = d ? d.unitName : undefined
if (explicit) return detailValue(explicit)
const unitId = d ? d.unitId : undefined
const list = unitList.value
const matched = Array.isArray(list) ? list.find(u => u && u.id === unitId) : undefined
return detailValue(matched ? matched.name : undefined)
})
const statusLabel = computed(() => {
const d = detailData.value
const status = d ? d.status : undefined
if (status === 0 || status === '0') return '启用'
if (status === 1 || status === '1') return '禁用'
return detailValue(status)
})
async function fetchAll() {
if (!productId.value) {
uni.showToast({ title: '缺少产品物料ID', icon: 'none' })
return
}
loading.value = true
try {
const [detailRes, unitRes] = await Promise.all([
request({
url: '/admin-api/erp/product/get',
method: 'get',
params: { id: productId.value },
showLoading: false
}),
request({
url: '/admin-api/erp/product-unit/simple-list',
method: 'get',
showLoading: false
})
])
detailData.value = detailRes ? detailRes.data : null
unitList.value = unitRes ? unitRes.data : []
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
onLoad((query) => {
const rawId = query && (query.id !== undefined ? query.id : query.code)
const decoded = rawId ? decodeURIComponent(String(rawId)) : ''
productId.value = decoded ? decoded : undefined
fetchAll()
})
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: #f0f2f5;
}
.header-section {
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%);
padding: 40rpx 30rpx 80rpx;
position: relative;
}
.back-btn {
display: flex;
align-items: center;
margin-bottom: 20rpx;
&:active {
opacity: 0.7;
}
.back-icon {
font-size: 48rpx;
color: #ffffff;
margin-right: 8rpx;
}
.back-text {
font-size: 28rpx;
color: #ffffff;
}
}
.header-content {
.header-title {
display: block;
font-size: 44rpx;
font-weight: bold;
color: #ffffff;
margin-bottom: 16rpx;
}
}
.content-section {
padding: 0 30rpx 30rpx;
margin-top: 40rpx;
}
.info-card {
background: #ffffff;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.card-title {
font-size: 32rpx;
font-weight: 600;
color: #1a3a5c;
margin-bottom: 24rpx;
padding-bottom: 20rpx;
border-bottom: 2rpx solid #f0f2f5;
}
.info-list {
display: flex;
flex-direction: column;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f5f7fa;
&:last-child {
border-bottom: none;
}
}
.info-label {
font-size: 28rpx;
color: #999999;
}
.info-value {
font-size: 28rpx;
color: #333333;
text-align: right;
max-width: 65%;
}
</style>

@ -0,0 +1,404 @@
<template>
<view class="page-container">
<view class="header-section">
<view class="back-btn" @click="goBack">
<text class="back-icon"></text>
<text class="back-text">返回</text>
</view>
<view class="header-content">
<text class="header-title">产品物料查询</text>
<text class="header-desc">请选择查询方式</text>
</view>
</view>
<view class="content-section">
<view class="scan-section">
<view class="scan-area" @click="startScan">
<view class="scan-icon">
<text class="icon-text">📷</text>
</view>
<text class="scan-title">自动扫描</text>
<text class="scan-desc">点击启动扫描产品物料二维码</text>
</view>
<view v-if="isScanning" class="scanning-overlay">
<view class="scanning-animation">
<view class="scan-line"></view>
<view class="scan-corners">
<view class="corner corner-tl"></view>
<view class="corner corner-tr"></view>
<view class="corner corner-bl"></view>
<view class="corner corner-br"></view>
</view>
</view>
<text class="scanning-text">正在打开扫码...</text>
</view>
</view>
<view class="divider">
<view class="divider-line"></view>
<text class="divider-text"></text>
<view class="divider-line"></view>
</view>
<view class="input-section">
<view class="input-label">手动输入产品物料ID</view>
<view class="input-wrapper">
<input
v-model="productId"
class="code-input"
type="text"
placeholder="请输入产品物料ID"
placeholder-class="input-placeholder"
/>
</view>
<view class="confirm-btn" :class="{ active: productId.length > 0 }" @click="confirmInput">
<text class="btn-text">确定</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
const productId = ref('')
const isScanning = ref(false)
function goBack() {
uni.navigateBack()
}
function startScan() {
if (isScanning.value) return
isScanning.value = true
const finish = () => {
isScanning.value = false
}
uni.scanCode({
onlyFromCamera: true,
scanType: ['qrCode', 'barCode'],
success: (res) => {
let parsed
try {
parsed = res && res.result ? JSON.parse(res.result) : undefined
} catch (e) {
parsed = undefined
}
const id = parsed && parsed.id !== undefined ? parsed.id : (res ? res.result : undefined)
if (!id) {
uni.showToast({ title: '未获取到扫码结果', icon: 'none' })
return
}
navigateToDetail(id)
},
fail: (err) => {
const msg = String(err?.errMsg || '')
if (msg.includes('cancel')) {
uni.showToast({ title: '已取消扫码', icon: 'none' })
return
}
if (msg.toLowerCase().includes('not support') || msg.toLowerCase().includes('not supported')) {
uni.showToast({ title: '当前平台不支持扫码', icon: 'none' })
return
}
uni.showToast({ title: '扫码失败', icon: 'none' })
},
complete: finish
})
}
function confirmInput() {
if (!productId.value.trim()) {
uni.showToast({ title: '请输入产品物料ID', icon: 'none' })
return
}
navigateToDetail(productId.value.trim())
}
function navigateToDetail(id) {
uni.navigateTo({
url: `/pages_function/pages/product/detail?id=${encodeURIComponent(id)}`
})
}
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: #f0f2f5;
}
.header-section {
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%);
padding: 40rpx 30rpx 80rpx;
position: relative;
}
.back-btn {
display: flex;
align-items: center;
margin-bottom: 20rpx;
&:active {
opacity: 0.7;
}
.back-icon {
font-size: 48rpx;
color: #ffffff;
margin-right: 8rpx;
}
.back-text {
font-size: 28rpx;
color: #ffffff;
}
}
.header-content {
.header-title {
display: block;
font-size: 44rpx;
font-weight: bold;
color: #ffffff;
margin-bottom: 16rpx;
}
.header-desc {
display: block;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.7);
}
}
.content-section {
padding: 40rpx 30rpx;
}
.scan-section {
position: relative;
background: #ffffff;
border-radius: 24rpx;
padding: 60rpx 40rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.scan-area {
display: flex;
flex-direction: column;
align-items: center;
&:active {
opacity: 0.8;
}
}
.scan-icon {
width: 160rpx;
height: 160rpx;
background: linear-gradient(135deg, #1a3a5c 0%, #3d7ab5 100%);
border-radius: 32rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 30rpx;
.icon-text {
font-size: 72rpx;
}
}
.scan-title {
font-size: 34rpx;
font-weight: 600;
color: #1a3a5c;
margin-bottom: 12rpx;
}
.scan-desc {
font-size: 26rpx;
color: #999999;
}
.scanning-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(26, 58, 92, 0.95);
border-radius: 24rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
}
.scanning-animation {
width: 300rpx;
height: 300rpx;
position: relative;
margin-bottom: 40rpx;
}
.scan-line {
position: absolute;
top: 0;
left: 20rpx;
right: 20rpx;
height: 4rpx;
background: linear-gradient(90deg, transparent, #ff8c00, transparent);
animation: scanMove 1.5s ease-in-out infinite;
}
@keyframes scanMove {
0% {
top: 20rpx;
}
50% {
top: 260rpx;
}
100% {
top: 20rpx;
}
}
.scan-corners {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.corner {
position: absolute;
width: 40rpx;
height: 40rpx;
border-color: #ff8c00;
border-style: solid;
border-width: 0;
}
.corner-tl {
top: 0;
left: 0;
border-top-width: 6rpx;
border-left-width: 6rpx;
border-top-left-radius: 12rpx;
}
.corner-tr {
top: 0;
right: 0;
border-top-width: 6rpx;
border-right-width: 6rpx;
border-top-right-radius: 12rpx;
}
.corner-bl {
bottom: 0;
left: 0;
border-bottom-width: 6rpx;
border-left-width: 6rpx;
border-bottom-left-radius: 12rpx;
}
.corner-br {
bottom: 0;
right: 0;
border-bottom-width: 6rpx;
border-right-width: 6rpx;
border-bottom-right-radius: 12rpx;
}
.scanning-text {
font-size: 32rpx;
color: #ffffff;
}
.divider {
display: flex;
align-items: center;
margin-bottom: 30rpx;
.divider-line {
flex: 1;
height: 2rpx;
background: #e8eaed;
}
.divider-text {
padding: 0 30rpx;
font-size: 28rpx;
color: #999999;
}
}
.input-section {
background: #ffffff;
border-radius: 24rpx;
padding: 40rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.input-label {
font-size: 30rpx;
font-weight: 600;
color: #1a3a5c;
margin-bottom: 24rpx;
}
.input-wrapper {
margin-bottom: 30rpx;
}
.code-input {
width: 100%;
height: 96rpx;
background: #f5f7fa;
border-radius: 16rpx;
padding: 0 30rpx;
font-size: 30rpx;
color: #333333;
border: 2rpx solid transparent;
&:focus {
border-color: #1a3a5c;
background: #ffffff;
}
}
.input-placeholder {
color: #c0c4cc;
}
.confirm-btn {
height: 96rpx;
background: #c0c4cc;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
&.active {
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%);
&:active {
opacity: 0.8;
transform: scale(0.98);
}
}
.btn-text {
font-size: 32rpx;
font-weight: 600;
color: #ffffff;
}
}
</style>
Loading…
Cancel
Save